Files
GearBox/.planning/phases/10-schema-foundation-pros-cons-fields/10-01-PLAN.md

13 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
10-schema-foundation-pros-cons-fields 01 execute 1
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
true
RANK-03
truths artifacts key_links
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)
path provides contains
src/db/schema.ts pros and cons nullable TEXT columns on threadCandidates pros: text
path provides contains
tests/helpers/db.ts Mirrored pros/cons columns in test DB CREATE TABLE pros TEXT
path provides contains
src/server/services/thread.service.ts pros/cons in createCandidate, updateCandidate, getThreadWithCandidates pros:
path provides contains
src/shared/schemas.ts pros and cons optional string fields in createCandidateSchema pros: z.string
path provides contains
src/client/components/CandidateForm.tsx Pros and Cons textarea inputs in candidate form candidate-pros
path provides contains
src/client/components/CandidateCard.tsx Visual indicator badge when pros or cons are present pros || cons
path provides contains
tests/services/thread.service.test.ts Tests for pros/cons in create, update, and get operations pros
from to via pattern
src/db/schema.ts tests/helpers/db.ts Manual column mirroring in CREATE TABLE pros TEXT
from to via pattern
src/shared/schemas.ts src/server/services/thread.service.ts Zod-inferred CreateCandidate type used in service CreateCandidate
from to via pattern
src/server/services/thread.service.ts src/client/hooks/useCandidates.ts API JSON response includes pros/cons fields pros.*string.*null
from to via pattern
src/client/hooks/useCandidates.ts src/client/components/CandidateForm.tsx CandidateResponse type drives form pre-fill candidate.pros
from to via pattern
src/client/routes/threads/$threadId.tsx src/client/components/CandidateCard.tsx Props threaded from candidate data to card pros=.*candidate.pros
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.

<execution_context> @/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md @/home/jlmak/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/10-schema-foundation-pros-cons-fields/10-RESEARCH.md

From src/db/schema.ts (threadCandidates table -- add pros/cons after status):

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):

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:

// 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:

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:

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:

interface FormData {
  name: string; weightGrams: string; priceDollars: string;
  categoryId: number; notes: string; productUrl: string;
  imageFilename: string | null;
  // ADD: pros: string;
  // ADD: cons: string;
}
Task 1: Add pros/cons columns through backend + tests src/db/schema.ts, tests/helpers/db.ts, src/server/services/thread.service.ts, src/shared/schemas.ts, tests/services/thread.service.test.ts - 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) 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.
cd /home/jlmak/Projects/jlmak/GearBox && bun test tests/services/thread.service.test.ts - 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 Task 2: Wire pros/cons through client hooks, form, and card indicator src/client/hooks/useCandidates.ts, src/client/components/CandidateForm.tsx, src/client/components/CandidateCard.tsx, src/client/routes/threads/$threadId.tsx 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).
cd /home/jlmak/Projects/jlmak/GearBox && bun test && bun run lint - 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 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

<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>
After completion, create `.planning/phases/10-schema-foundation-pros-cons-fields/10-01-SUMMARY.md`