docs(10): create phase plan
This commit is contained in:
@@ -56,7 +56,9 @@
|
|||||||
2. User can save pros and cons text; the text persists across page refreshes
|
2. User can save pros and cons text; the text persists across page refreshes
|
||||||
3. CandidateCard shows a visual indicator when a candidate has pros or cons entered
|
3. CandidateCard shows a visual indicator when a candidate has pros or cons entered
|
||||||
4. All existing tests pass after the schema migration (no column drift in test helper)
|
4. All existing tests pass after the schema migration (no column drift in test helper)
|
||||||
**Plans**: TBD
|
**Plans:** 1 plan
|
||||||
|
Plans:
|
||||||
|
- [ ] 10-01-PLAN.md — Add pros/cons fields through full stack (schema, service, Zod, form, card indicator)
|
||||||
|
|
||||||
### Phase 11: Candidate Ranking
|
### Phase 11: Candidate Ranking
|
||||||
**Goal**: Users can drag candidates into a priority order that persists and is visually communicated
|
**Goal**: Users can drag candidates into a priority order that persists and is visually communicated
|
||||||
@@ -104,7 +106,7 @@
|
|||||||
| 7. Weight Unit Selection | v1.2 | 2/2 | Complete | 2026-03-16 |
|
| 7. Weight Unit Selection | v1.2 | 2/2 | Complete | 2026-03-16 |
|
||||||
| 8. Search, Filter, and Candidate Status | v1.2 | 2/2 | Complete | 2026-03-16 |
|
| 8. Search, Filter, and Candidate Status | v1.2 | 2/2 | Complete | 2026-03-16 |
|
||||||
| 9. Weight Classification and Visualization | v1.2 | 2/2 | Complete | 2026-03-16 |
|
| 9. Weight Classification and Visualization | v1.2 | 2/2 | Complete | 2026-03-16 |
|
||||||
| 10. Schema Foundation + Pros/Cons Fields | v1.3 | 0/TBD | Not started | - |
|
| 10. Schema Foundation + Pros/Cons Fields | v1.3 | 0/1 | Not started | - |
|
||||||
| 11. Candidate Ranking | v1.3 | 0/TBD | Not started | - |
|
| 11. Candidate Ranking | v1.3 | 0/TBD | Not started | - |
|
||||||
| 12. Comparison View | v1.3 | 0/TBD | Not started | - |
|
| 12. Comparison View | v1.3 | 0/TBD | Not started | - |
|
||||||
| 13. Setup Impact Preview | v1.3 | 0/TBD | Not started | - |
|
| 13. Setup Impact Preview | v1.3 | 0/TBD | Not started | - |
|
||||||
|
|||||||
@@ -0,0 +1,302 @@
|
|||||||
|
---
|
||||||
|
phase: 10-schema-foundation-pros-cons-fields
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- src/db/schema.ts
|
||||||
|
- tests/helpers/db.ts
|
||||||
|
- src/server/services/thread.service.ts
|
||||||
|
- src/shared/schemas.ts
|
||||||
|
- src/client/hooks/useCandidates.ts
|
||||||
|
- src/client/components/CandidateForm.tsx
|
||||||
|
- src/client/components/CandidateCard.tsx
|
||||||
|
- src/client/routes/threads/$threadId.tsx
|
||||||
|
- tests/services/thread.service.test.ts
|
||||||
|
autonomous: true
|
||||||
|
requirements: [RANK-03]
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "User can open a candidate edit form and see pros and cons text fields"
|
||||||
|
- "User can save pros and cons text; the text persists across page refreshes"
|
||||||
|
- "CandidateCard shows a visual indicator when a candidate has pros or cons entered"
|
||||||
|
- "All existing tests pass after the schema migration (no column drift in test helper)"
|
||||||
|
artifacts:
|
||||||
|
- path: "src/db/schema.ts"
|
||||||
|
provides: "pros and cons nullable TEXT columns on threadCandidates"
|
||||||
|
contains: "pros: text"
|
||||||
|
- path: "tests/helpers/db.ts"
|
||||||
|
provides: "Mirrored pros/cons columns in test DB CREATE TABLE"
|
||||||
|
contains: "pros TEXT"
|
||||||
|
- path: "src/server/services/thread.service.ts"
|
||||||
|
provides: "pros/cons in createCandidate, updateCandidate, getThreadWithCandidates"
|
||||||
|
contains: "pros:"
|
||||||
|
- path: "src/shared/schemas.ts"
|
||||||
|
provides: "pros and cons optional string fields in createCandidateSchema"
|
||||||
|
contains: "pros: z.string"
|
||||||
|
- path: "src/client/components/CandidateForm.tsx"
|
||||||
|
provides: "Pros and Cons textarea inputs in candidate form"
|
||||||
|
contains: "candidate-pros"
|
||||||
|
- path: "src/client/components/CandidateCard.tsx"
|
||||||
|
provides: "Visual indicator badge when pros or cons are present"
|
||||||
|
contains: "pros || cons"
|
||||||
|
- path: "tests/services/thread.service.test.ts"
|
||||||
|
provides: "Tests for pros/cons in create, update, and get operations"
|
||||||
|
contains: "pros"
|
||||||
|
key_links:
|
||||||
|
- from: "src/db/schema.ts"
|
||||||
|
to: "tests/helpers/db.ts"
|
||||||
|
via: "Manual column mirroring in CREATE TABLE"
|
||||||
|
pattern: "pros TEXT"
|
||||||
|
- from: "src/shared/schemas.ts"
|
||||||
|
to: "src/server/services/thread.service.ts"
|
||||||
|
via: "Zod-inferred CreateCandidate type used in service"
|
||||||
|
pattern: "CreateCandidate"
|
||||||
|
- from: "src/server/services/thread.service.ts"
|
||||||
|
to: "src/client/hooks/useCandidates.ts"
|
||||||
|
via: "API JSON response includes pros/cons fields"
|
||||||
|
pattern: "pros.*string.*null"
|
||||||
|
- from: "src/client/hooks/useCandidates.ts"
|
||||||
|
to: "src/client/components/CandidateForm.tsx"
|
||||||
|
via: "CandidateResponse type drives form pre-fill"
|
||||||
|
pattern: "candidate\\.pros"
|
||||||
|
- from: "src/client/routes/threads/$threadId.tsx"
|
||||||
|
to: "src/client/components/CandidateCard.tsx"
|
||||||
|
via: "Props threaded from candidate data to card"
|
||||||
|
pattern: "pros=.*candidate\\.pros"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Add pros and cons annotation fields to thread candidates, from database through UI.
|
||||||
|
|
||||||
|
Purpose: RANK-03 requires users to add pros/cons text per candidate for decision-making. This plan follows the established field-addition ladder: schema -> migration -> test helper -> service -> Zod -> types -> hook -> form -> card indicator.
|
||||||
|
|
||||||
|
Output: Two new nullable TEXT columns (pros, cons) on thread_candidates, fully wired through all layers, with service-level tests and a visual indicator on CandidateCard.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<execution_context>
|
||||||
|
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
|
||||||
|
@/home/jlmak/.claude/get-shit-done/templates/summary.md
|
||||||
|
</execution_context>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
@.planning/PROJECT.md
|
||||||
|
@.planning/ROADMAP.md
|
||||||
|
@.planning/STATE.md
|
||||||
|
@.planning/phases/10-schema-foundation-pros-cons-fields/10-RESEARCH.md
|
||||||
|
|
||||||
|
<interfaces>
|
||||||
|
<!-- Current codebase contracts the executor needs. -->
|
||||||
|
|
||||||
|
From src/db/schema.ts (threadCandidates table -- add pros/cons after status):
|
||||||
|
```typescript
|
||||||
|
export const threadCandidates = sqliteTable("thread_candidates", {
|
||||||
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
threadId: integer("thread_id").notNull().references(() => threads.id, { onDelete: "cascade" }),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
weightGrams: real("weight_grams"),
|
||||||
|
priceCents: integer("price_cents"),
|
||||||
|
categoryId: integer("category_id").notNull().references(() => categories.id),
|
||||||
|
notes: text("notes"),
|
||||||
|
productUrl: text("product_url"),
|
||||||
|
imageFilename: text("image_filename"),
|
||||||
|
status: text("status").notNull().default("researching"),
|
||||||
|
// ADD: pros: text("pros"),
|
||||||
|
// ADD: cons: text("cons"),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => new Date()),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => new Date()),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
From src/shared/schemas.ts (createCandidateSchema -- add optional pros/cons):
|
||||||
|
```typescript
|
||||||
|
export const createCandidateSchema = z.object({
|
||||||
|
name: z.string().min(1, "Name is required"),
|
||||||
|
weightGrams: z.number().nonnegative().optional(),
|
||||||
|
priceCents: z.number().int().nonnegative().optional(),
|
||||||
|
categoryId: z.number().int().positive(),
|
||||||
|
notes: z.string().optional(),
|
||||||
|
productUrl: z.string().url().optional().or(z.literal("")),
|
||||||
|
imageFilename: z.string().optional(),
|
||||||
|
status: candidateStatusSchema.optional(),
|
||||||
|
});
|
||||||
|
// updateCandidateSchema = createCandidateSchema.partial() -- inherits automatically
|
||||||
|
```
|
||||||
|
|
||||||
|
From src/server/services/thread.service.ts:
|
||||||
|
```typescript
|
||||||
|
// createCandidate: values() object needs pros/cons
|
||||||
|
// updateCandidate: inline Partial<{...}> type needs pros/cons
|
||||||
|
// getThreadWithCandidates: explicit .select({}) projection needs pros/cons
|
||||||
|
```
|
||||||
|
|
||||||
|
From src/client/hooks/useCandidates.ts:
|
||||||
|
```typescript
|
||||||
|
interface CandidateResponse {
|
||||||
|
id: number; threadId: number; name: string;
|
||||||
|
weightGrams: number | null; priceCents: number | null;
|
||||||
|
categoryId: number; notes: string | null;
|
||||||
|
productUrl: string | null; imageFilename: string | null;
|
||||||
|
status: "researching" | "ordered" | "arrived";
|
||||||
|
createdAt: string; updatedAt: string;
|
||||||
|
// ADD: pros: string | null;
|
||||||
|
// ADD: cons: string | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From src/client/components/CandidateCard.tsx:
|
||||||
|
```typescript
|
||||||
|
interface CandidateCardProps {
|
||||||
|
id: number; name: string; weightGrams: number | null;
|
||||||
|
priceCents: number | null; categoryName: string;
|
||||||
|
categoryIcon: string; imageFilename: string | null;
|
||||||
|
productUrl?: string | null; threadId: number;
|
||||||
|
isActive: boolean;
|
||||||
|
status: "researching" | "ordered" | "arrived";
|
||||||
|
onStatusChange: (status: "researching" | "ordered" | "arrived") => void;
|
||||||
|
// ADD: pros?: string | null;
|
||||||
|
// ADD: cons?: string | null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
From src/client/components/CandidateForm.tsx:
|
||||||
|
```typescript
|
||||||
|
interface FormData {
|
||||||
|
name: string; weightGrams: string; priceDollars: string;
|
||||||
|
categoryId: number; notes: string; productUrl: string;
|
||||||
|
imageFilename: string | null;
|
||||||
|
// ADD: pros: string;
|
||||||
|
// ADD: cons: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</interfaces>
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto" tdd="true">
|
||||||
|
<name>Task 1: Add pros/cons columns through backend + tests</name>
|
||||||
|
<files>src/db/schema.ts, tests/helpers/db.ts, src/server/services/thread.service.ts, src/shared/schemas.ts, tests/services/thread.service.test.ts</files>
|
||||||
|
<behavior>
|
||||||
|
- createCandidate with pros/cons returns them in the response
|
||||||
|
- createCandidate without pros/cons returns null for both fields
|
||||||
|
- updateCandidate can set pros and cons on an existing candidate
|
||||||
|
- updateCandidate can clear pros/cons by setting to empty string (becomes null via service)
|
||||||
|
- getThreadWithCandidates includes pros and cons on each candidate object
|
||||||
|
- All existing thread service tests still pass (no column drift)
|
||||||
|
</behavior>
|
||||||
|
<action>
|
||||||
|
1. **Schema** (`src/db/schema.ts`): Add two nullable TEXT columns to `threadCandidates` after `status`:
|
||||||
|
```typescript
|
||||||
|
pros: text("pros"),
|
||||||
|
cons: text("cons"),
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Migration**: Run `bun run db:generate` to produce the ALTER TABLE migration, then `bun run db:push` to apply.
|
||||||
|
|
||||||
|
3. **Test helper** (`tests/helpers/db.ts`): Add `pros TEXT,` and `cons TEXT,` to the `CREATE TABLE thread_candidates` statement, between the `status` line and the `created_at` line. This is CRITICAL -- without it, in-memory test DBs will silently lack the columns.
|
||||||
|
|
||||||
|
4. **Service** (`src/server/services/thread.service.ts`):
|
||||||
|
- `createCandidate`: Add `pros: data.pros ?? null,` and `cons: data.cons ?? null,` to the `.values({})` object.
|
||||||
|
- `updateCandidate`: Add `pros: string;` and `cons: string;` to the inline `Partial<{...}>` type parameter.
|
||||||
|
- `getThreadWithCandidates`: Add `pros: threadCandidates.pros,` and `cons: threadCandidates.cons,` to the explicit `.select({})` projection, before the `categoryName` line.
|
||||||
|
|
||||||
|
5. **Zod schemas** (`src/shared/schemas.ts`): Add to `createCandidateSchema`:
|
||||||
|
```typescript
|
||||||
|
pros: z.string().optional(),
|
||||||
|
cons: z.string().optional(),
|
||||||
|
```
|
||||||
|
`updateCandidateSchema` inherits via `.partial()` -- no changes needed there.
|
||||||
|
|
||||||
|
6. **Tests** (`tests/services/thread.service.test.ts`): Add three test cases:
|
||||||
|
- In `describe("createCandidate")`: "stores and returns pros and cons" -- create a candidate with `pros: "Lightweight\nGood reviews"` and `cons: "Expensive"`, assert both fields are returned correctly.
|
||||||
|
- In `describe("updateCandidate")`: "can set and clear pros and cons" -- create a candidate, update with pros/cons values, assert they are set, then update with empty strings, assert they are cleared (returned as empty string or null from DB).
|
||||||
|
- In `describe("getThreadWithCandidates")`: "includes pros and cons on each candidate" -- create a candidate with pros/cons, fetch via getThreadWithCandidates, assert `candidate.pros` and `candidate.cons` match.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /home/jlmak/Projects/jlmak/GearBox && bun test tests/services/thread.service.test.ts</automated>
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
- pros and cons columns exist in schema.ts and test helper
|
||||||
|
- Drizzle migration generated and applied
|
||||||
|
- createCandidate, updateCandidate, getThreadWithCandidates all handle pros/cons
|
||||||
|
- Zod schemas accept optional pros/cons strings
|
||||||
|
- All existing + new thread service tests pass
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Wire pros/cons through client hooks, form, and card indicator</name>
|
||||||
|
<files>src/client/hooks/useCandidates.ts, src/client/components/CandidateForm.tsx, src/client/components/CandidateCard.tsx, src/client/routes/threads/$threadId.tsx</files>
|
||||||
|
<action>
|
||||||
|
1. **Hook** (`src/client/hooks/useCandidates.ts`): Add to `CandidateResponse` interface:
|
||||||
|
```typescript
|
||||||
|
pros: string | null;
|
||||||
|
cons: string | null;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **CandidateForm** (`src/client/components/CandidateForm.tsx`):
|
||||||
|
- Add `pros: string;` and `cons: string;` to `FormData` interface.
|
||||||
|
- Add `pros: "",` and `cons: "",` to `INITIAL_FORM`.
|
||||||
|
- In the `useEffect` pre-fill block, add: `pros: candidate.pros ?? "",` and `cons: candidate.cons ?? "",`.
|
||||||
|
- In `handleSubmit` payload, add: `pros: form.pros.trim() || undefined,` and `cons: form.cons.trim() || undefined,`.
|
||||||
|
- Add two textarea elements in the form, AFTER the Notes textarea and BEFORE the Product Link input. Each should follow the exact same pattern as the Notes textarea:
|
||||||
|
- **Pros**: label "Pros", id `candidate-pros`, placeholder "One pro per line...", rows={3}
|
||||||
|
- **Cons**: label "Cons", id `candidate-cons`, placeholder "One con per line...", rows={3}
|
||||||
|
- Use identical Tailwind classes as the existing Notes textarea.
|
||||||
|
|
||||||
|
3. **CandidateCard** (`src/client/components/CandidateCard.tsx`):
|
||||||
|
- Add `pros?: string | null;` and `cons?: string | null;` to `CandidateCardProps` interface.
|
||||||
|
- Destructure `pros` and `cons` in the component function parameters.
|
||||||
|
- Add a visual indicator badge in the `flex flex-wrap gap-1.5` div, after the StatusBadge. When `(pros || cons)` is truthy, render:
|
||||||
|
```tsx
|
||||||
|
{(pros || cons) && (
|
||||||
|
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-purple-50 text-purple-700">
|
||||||
|
+/- Notes
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Thread detail route** (`src/client/routes/threads/$threadId.tsx`): Pass `pros` and `cons` props to the `<CandidateCard>` component in the candidates map:
|
||||||
|
```tsx
|
||||||
|
pros={candidate.pros}
|
||||||
|
cons={candidate.cons}
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Run `bun run lint` to verify Biome compliance (tabs, double quotes, organized imports).
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
<automated>cd /home/jlmak/Projects/jlmak/GearBox && bun test && bun run lint</automated>
|
||||||
|
</verify>
|
||||||
|
<done>
|
||||||
|
- CandidateResponse includes pros/cons fields
|
||||||
|
- CandidateForm shows Pros and Cons textareas, pre-fills in edit mode, sends in payload
|
||||||
|
- CandidateCard shows purple "+/- Notes" badge when pros or cons text exists
|
||||||
|
- Thread detail page threads pros/cons props to CandidateCard
|
||||||
|
- Full test suite passes, lint passes
|
||||||
|
</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
1. `bun test` -- full suite green (existing + new tests)
|
||||||
|
2. `bun run lint` -- no Biome violations
|
||||||
|
3. Manual: create a thread, add a candidate with pros and cons text, verify:
|
||||||
|
- Pros/cons fields appear in the edit form
|
||||||
|
- Saved text persists after page refresh
|
||||||
|
- CandidateCard shows the "+/- Notes" indicator badge
|
||||||
|
- A candidate without pros/cons does NOT show the badge
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- RANK-03 fully implemented: pros/cons fields on candidates, editable via form, persisted, with visual indicator
|
||||||
|
- Zero test regressions
|
||||||
|
- No column drift between schema.ts and test helper
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/10-schema-foundation-pros-cons-fields/10-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
Reference in New Issue
Block a user