docs(13): create phase plan
Some checks failed
CI / ci (push) Failing after 19s

This commit is contained in:
2026-03-17 16:53:47 +01:00
parent 79d84f1333
commit a826381981
3 changed files with 588 additions and 2 deletions

View File

@@ -96,7 +96,10 @@ Plans:
2. When the selected setup contains an item in the same category as the thread, the delta reflects replacing that item (negative delta is possible) rather than pure addition
3. When no category match exists in the selected setup, the delta shows a pure addition amount clearly labeled as "add"
4. A candidate with no weight recorded shows a "-- (no weight data)" indicator instead of a zero delta
**Plans**: TBD
**Plans:** 2 plans
Plans:
- [ ] 13-01-PLAN.md — TDD pure impact delta computation, uiStore state, ThreadWithCandidates type fix, useImpactDeltas hook
- [ ] 13-02-PLAN.md — SetupImpactSelector + ImpactDeltaBadge components, wire into thread detail and all candidate views
## Progress
@@ -114,4 +117,4 @@ Plans:
| 10. Schema Foundation + Pros/Cons Fields | v1.3 | 1/1 | Complete | 2026-03-16 |
| 11. Candidate Ranking | 2/2 | Complete | 2026-03-16 | - |
| 12. Comparison View | 1/1 | Complete | 2026-03-17 | - |
| 13. Setup Impact Preview | v1.3 | 0/TBD | Not started | - |
| 13. Setup Impact Preview | v1.3 | 0/2 | Not started | - |

View File

@@ -0,0 +1,253 @@
---
phase: 13-setup-impact-preview
plan: 01
type: tdd
wave: 1
depends_on: []
files_modified:
- src/client/lib/impactDeltas.ts
- src/client/hooks/useImpactDeltas.ts
- src/client/hooks/useThreads.ts
- src/client/stores/uiStore.ts
- tests/lib/impactDeltas.test.ts
autonomous: true
requirements: [IMPC-01, IMPC-02, IMPC-03, IMPC-04]
must_haves:
truths:
- "computeImpactDeltas returns replace-mode deltas when a setup item matches the thread category"
- "computeImpactDeltas returns add-mode deltas (candidate value only) when no category match exists"
- "computeImpactDeltas returns null weightDelta/priceDelta when candidate has null weight/price"
- "computeImpactDeltas returns mode 'none' with empty deltas when setupItems is undefined"
- "selectedSetupId state persists in uiStore and can be set/cleared"
- "ThreadWithCandidates interface includes categoryId field"
artifacts:
- path: "src/client/lib/impactDeltas.ts"
provides: "Pure computeImpactDeltas function with CandidateDelta, ImpactDeltas types"
exports: ["computeImpactDeltas", "CandidateInput", "CandidateDelta", "DeltaMode", "ImpactDeltas"]
- path: "src/client/hooks/useImpactDeltas.ts"
provides: "React hook wrapping computeImpactDeltas in useMemo"
exports: ["useImpactDeltas"]
- path: "tests/lib/impactDeltas.test.ts"
provides: "Unit tests for all four IMPC requirements"
contains: "computeImpactDeltas"
key_links:
- from: "src/client/hooks/useImpactDeltas.ts"
to: "src/client/lib/impactDeltas.ts"
via: "import computeImpactDeltas"
pattern: "import.*computeImpactDeltas.*from.*lib/impactDeltas"
- from: "src/client/hooks/useImpactDeltas.ts"
to: "src/client/hooks/useSetups.ts"
via: "SetupItemWithCategory type import"
pattern: "import.*SetupItemWithCategory.*from.*useSetups"
---
<objective>
Create the pure impact delta computation logic with full TDD coverage, add selectedSetupId to uiStore, fix the ThreadWithCandidates type to include categoryId, and wrap it all in a useImpactDeltas hook.
Purpose: Establish the data layer and contracts that Plan 02 will consume for rendering delta indicators. TDD ensures the replace/add mode logic and null-weight handling are correct before any UI work.
Output: Tested pure function, React hook, updated types, uiStore state.
</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/13-setup-impact-preview/13-RESEARCH.md
<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
From src/client/hooks/useSetups.ts:
```typescript
interface SetupItemWithCategory {
id: number;
name: string;
weightGrams: number | null;
priceCents: number | null;
categoryId: number;
notes: string | null;
productUrl: string | null;
imageFilename: string | null;
createdAt: string;
updatedAt: string;
categoryName: string;
categoryIcon: string;
classification: string;
}
export function useSetup(setupId: number | null) {
return useQuery({
queryKey: ["setups", setupId],
queryFn: () => apiGet<SetupWithItems>(`/api/setups/${setupId}`),
enabled: setupId != null, // CRITICAL: prevents null fetch
});
}
```
From src/client/hooks/useThreads.ts (BEFORE fix):
```typescript
interface ThreadWithCandidates {
id: number;
name: string;
status: "active" | "resolved";
resolvedCandidateId: number | null;
createdAt: string;
updatedAt: string;
candidates: CandidateWithCategory[];
// NOTE: categoryId is MISSING — server returns it but type omits it
}
```
From src/client/stores/uiStore.ts (existing pattern):
```typescript
candidateViewMode: "list" | "grid" | "compare";
setCandidateViewMode: (mode: "list" | "grid" | "compare") => void;
// Add selectedSetupId + setter using same pattern
```
From src/client/lib/formatters.ts:
```typescript
export type WeightUnit = "g" | "oz" | "lb" | "kg";
export function formatWeight(grams: number | null | undefined, unit: WeightUnit = "g"): string;
export type Currency = "USD" | "EUR" | "GBP" | "JPY" | "CAD" | "AUD";
export function formatPrice(cents: number | null | undefined, currency: Currency = "USD"): string;
```
</interfaces>
</context>
<feature>
<name>Impact Delta Computation</name>
<files>src/client/lib/impactDeltas.ts, tests/lib/impactDeltas.test.ts, src/client/hooks/useImpactDeltas.ts, src/client/hooks/useThreads.ts, src/client/stores/uiStore.ts</files>
<behavior>
IMPC-01 (setup selected, deltas computed):
- Given candidates with weight/price and a setup with items, returns per-candidate delta objects with weightDelta and priceDelta numbers
- Given no setup selected (setupItems = undefined), returns { mode: "none", deltas: {} }
IMPC-02 (replace mode auto-detection):
- Given setup items where one has categoryId === threadCategoryId, mode is "replace"
- In replace mode, weightDelta = candidate.weightGrams - replacedItem.weightGrams
- In replace mode, priceDelta = candidate.priceCents - replacedItem.priceCents
- replacedItemName is populated with the matched item's name
IMPC-03 (add mode):
- Given setup items where NONE have categoryId === threadCategoryId, mode is "add"
- In add mode, weightDelta = candidate.weightGrams (pure addition)
- In add mode, priceDelta = candidate.priceCents (pure addition)
- replacedItemName is null
IMPC-04 (null weight handling):
- Given candidate.weightGrams is null, weightDelta is null (not 0, not NaN)
- Given candidate.priceCents is null, priceDelta is null
- In replace mode with replacedItem.weightGrams null but candidate has weight, weightDelta = candidate.weightGrams (treat as add for that field)
Edge cases:
- Empty candidates array -> returns { mode based on setup, deltas: {} }
- Multiple setup items in same category as thread -> first match used for replacement
</behavior>
<implementation>
1. Create src/client/lib/impactDeltas.ts with:
- CandidateInput interface: { id: number; weightGrams: number | null; priceCents: number | null }
- DeltaMode type: "replace" | "add" | "none"
- CandidateDelta interface: { candidateId, mode, weightDelta, priceDelta, replacedItemName }
- ImpactDeltas interface: { mode: DeltaMode; deltas: Record<number, CandidateDelta> }
- SetupItemInput interface: { categoryId: number; weightGrams: number | null; priceCents: number | null; name: string } (minimal subset of SetupItemWithCategory)
- computeImpactDeltas(candidates, setupItems, threadCategoryId) pure function
2. Create src/client/hooks/useImpactDeltas.ts wrapping in useMemo
3. Add categoryId to ThreadWithCandidates in useThreads.ts
4. Add selectedSetupId + setSelectedSetupId to uiStore.ts
</implementation>
</feature>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: TDD pure computeImpactDeltas function</name>
<files>src/client/lib/impactDeltas.ts, tests/lib/impactDeltas.test.ts</files>
<behavior>
- Test: no setup selected (undefined) returns mode "none", empty deltas
- Test: replace mode — setup item matches threadCategoryId, deltas are candidate minus replaced item
- Test: add mode — no setup item matches, deltas equal candidate values
- Test: null candidate weight returns null weightDelta, not zero
- Test: null candidate price returns null priceDelta
- Test: replace mode with null replacedItem weight but valid candidate weight returns candidate weight as delta (add-like for that field)
- Test: negative delta in replace mode (candidate lighter than replaced item)
- Test: zero delta in replace mode (identical weight)
- Test: replacedItemName populated in replace mode, null in add mode
</behavior>
<action>
RED: Create tests/lib/impactDeltas.test.ts importing computeImpactDeltas from @/client/lib/impactDeltas. Write all test cases above using Bun test (describe/test/expect). Run tests — they MUST fail (module not found).
GREEN: Create src/client/lib/impactDeltas.ts with:
- Export types: CandidateInput, SetupItemInput, DeltaMode, CandidateDelta, ImpactDeltas
- Export function computeImpactDeltas(candidates: CandidateInput[], setupItems: SetupItemInput[] | undefined, threadCategoryId: number): ImpactDeltas
- Logic: if !setupItems return { mode: "none", deltas: {} }
- Find replacedItem = setupItems.find(item => item.categoryId === threadCategoryId) ?? null
- mode = replacedItem ? "replace" : "add"
- For each candidate: null-guard weight/price BEFORE arithmetic. In replace mode with non-null replaced value, delta = candidate - replaced. In replace mode with null replaced value, delta = candidate value (like add). In add mode, delta = candidate value.
- Run tests — all MUST pass.
REFACTOR: None needed for pure function.
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/GearBox && bun test tests/lib/impactDeltas.test.ts</automated>
</verify>
<done>All 9+ test cases pass. computeImpactDeltas correctly handles replace mode, add mode, null weights, null prices, and edge cases. Types are exported.</done>
</task>
<task type="auto">
<name>Task 2: Add uiStore state, fix ThreadWithCandidates type, create useImpactDeltas hook</name>
<files>src/client/stores/uiStore.ts, src/client/hooks/useThreads.ts, src/client/hooks/useImpactDeltas.ts</files>
<action>
1. In src/client/stores/uiStore.ts, add to UIState interface:
- selectedSetupId: number | null;
- setSelectedSetupId: (id: number | null) => void;
Add to create() initializer:
- selectedSetupId: null,
- setSelectedSetupId: (id) => set({ selectedSetupId: id }),
Place after the "Candidate view mode" section as "// Setup impact preview" section.
2. In src/client/hooks/useThreads.ts, add `categoryId: number;` to the ThreadWithCandidates interface, after the `resolvedCandidateId` field. The server already returns this field — the type was simply missing it.
3. Create src/client/hooks/useImpactDeltas.ts:
- Import useMemo from react
- Import computeImpactDeltas and types from "../lib/impactDeltas"
- Import type { SetupItemWithCategory } from "./useSetups"
- Export function useImpactDeltas(candidates: CandidateInput[], setupItems: SetupItemWithCategory[] | undefined, threadCategoryId: number): ImpactDeltas
- Body: return useMemo(() => computeImpactDeltas(candidates, setupItems, threadCategoryId), [candidates, setupItems, threadCategoryId])
- Re-export CandidateDelta, DeltaMode, ImpactDeltas types for convenience
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/GearBox && bun test tests/lib/impactDeltas.test.ts && bun test tests/lib/formatters.test.ts</automated>
</verify>
<done>uiStore has selectedSetupId + setter. ThreadWithCandidates includes categoryId. useImpactDeltas hook created and exports types. All existing tests still pass.</done>
</task>
</tasks>
<verification>
- `bun test tests/lib/` passes all tests (impactDeltas + formatters)
- `bun test` full suite passes (no regressions)
- Types export correctly: CandidateInput, CandidateDelta, DeltaMode, ImpactDeltas, SetupItemInput from impactDeltas.ts
- useImpactDeltas hook wraps pure function in useMemo
- uiStore.selectedSetupId defaults to null
- ThreadWithCandidates.categoryId is declared as number
</verification>
<success_criteria>
- All IMPC requirement behaviors are tested and passing via pure function unit tests
- Data layer contracts (types, hook, uiStore state) are ready for Plan 02 UI consumption
- Zero regressions in existing test suite
</success_criteria>
<output>
After completion, create `.planning/phases/13-setup-impact-preview/13-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,330 @@
---
phase: 13-setup-impact-preview
plan: 02
type: execute
wave: 2
depends_on: ["13-01"]
files_modified:
- src/client/components/SetupImpactSelector.tsx
- src/client/components/ImpactDeltaBadge.tsx
- src/client/routes/threads/$threadId.tsx
- src/client/components/CandidateListItem.tsx
- src/client/components/CandidateCard.tsx
- src/client/components/ComparisonTable.tsx
autonomous: true
requirements: [IMPC-01, IMPC-02, IMPC-03, IMPC-04]
must_haves:
truths:
- "User can select a setup from a dropdown in the thread header"
- "Each candidate displays weight and cost delta badges when a setup is selected"
- "Replace mode shows signed delta with replaced item name context"
- "Add mode shows positive delta labeled as '(add)'"
- "Candidate with no weight shows '-- (no weight data)' instead of a zero"
- "Candidate with no price shows '-- (no price data)' instead of a zero"
- "Deselecting setup ('None') clears all delta indicators"
- "Deltas appear in list view, grid view, and comparison table"
artifacts:
- path: "src/client/components/SetupImpactSelector.tsx"
provides: "Setup dropdown for thread header"
exports: ["SetupImpactSelector"]
- path: "src/client/components/ImpactDeltaBadge.tsx"
provides: "Inline delta indicator component"
exports: ["ImpactDeltaBadge"]
- path: "src/client/routes/threads/$threadId.tsx"
provides: "Thread detail page wired with impact preview"
- path: "src/client/components/CandidateListItem.tsx"
provides: "List item with delta badges"
- path: "src/client/components/CandidateCard.tsx"
provides: "Card with delta badges"
- path: "src/client/components/ComparisonTable.tsx"
provides: "Comparison table with impact delta rows"
key_links:
- from: "src/client/routes/threads/$threadId.tsx"
to: "src/client/hooks/useImpactDeltas.ts"
via: "useImpactDeltas hook call at page level"
pattern: "useImpactDeltas"
- from: "src/client/routes/threads/$threadId.tsx"
to: "src/client/hooks/useSetups.ts"
via: "useSetup(selectedSetupId) for setup item data"
pattern: "useSetup\\(selectedSetupId"
- from: "src/client/routes/threads/$threadId.tsx"
to: "src/client/stores/uiStore.ts"
via: "selectedSetupId state read"
pattern: "useUIStore.*selectedSetupId"
- from: "src/client/components/SetupImpactSelector.tsx"
to: "src/client/stores/uiStore.ts"
via: "setSelectedSetupId state write"
pattern: "setSelectedSetupId"
- from: "src/client/components/ImpactDeltaBadge.tsx"
to: "src/client/lib/impactDeltas.ts"
via: "CandidateDelta type import"
pattern: "import.*CandidateDelta"
- from: "src/client/components/ComparisonTable.tsx"
to: "src/client/lib/impactDeltas.ts"
via: "ImpactDeltas type for deltas prop"
pattern: "import.*ImpactDeltas"
---
<objective>
Build the UI components (setup dropdown + delta badges) and wire them into the thread detail page across all three view modes (list, grid, compare).
Purpose: This is the user-facing delivery of the impact preview feature. Plan 01 built the logic; this plan renders it.
Output: SetupImpactSelector component, ImpactDeltaBadge component, updated CandidateListItem/CandidateCard/ComparisonTable with delta rendering, wired thread detail page.
</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/13-setup-impact-preview/13-RESEARCH.md
@.planning/phases/13-setup-impact-preview/13-01-SUMMARY.md
<interfaces>
<!-- Types created by Plan 01 that this plan consumes -->
From src/client/lib/impactDeltas.ts (created in Plan 01):
```typescript
export interface CandidateInput {
id: number;
weightGrams: number | null;
priceCents: number | null;
}
export type DeltaMode = "replace" | "add" | "none";
export interface CandidateDelta {
candidateId: number;
mode: DeltaMode;
weightDelta: number | null;
priceDelta: number | null;
replacedItemName: string | null;
}
export interface ImpactDeltas {
mode: DeltaMode;
deltas: Record<number, CandidateDelta>;
}
```
From src/client/hooks/useImpactDeltas.ts (created in Plan 01):
```typescript
export function useImpactDeltas(
candidates: CandidateInput[],
setupItems: SetupItemWithCategory[] | undefined,
threadCategoryId: number,
): ImpactDeltas;
```
From src/client/hooks/useSetups.ts:
```typescript
export function useSetups(): UseQueryResult<SetupListItem[]>;
export function useSetup(setupId: number | null): UseQueryResult<SetupWithItems>;
```
From src/client/stores/uiStore.ts (updated in Plan 01):
```typescript
selectedSetupId: number | null;
setSelectedSetupId: (id: number | null) => void;
```
From src/client/hooks/useThreads.ts (updated in Plan 01):
```typescript
interface ThreadWithCandidates {
id: number;
name: string;
status: "active" | "resolved";
resolvedCandidateId: number | null;
categoryId: number; // <-- Added in Plan 01
createdAt: string;
updatedAt: string;
candidates: CandidateWithCategory[];
}
```
From src/client/lib/formatters.ts:
```typescript
export function formatWeight(grams: number | null | undefined, unit: WeightUnit = "g"): string;
export function formatPrice(cents: number | null | undefined, currency: Currency = "USD"): string;
```
Existing component props that need delta additions:
CandidateListItem props:
```typescript
interface CandidateListItemProps {
candidate: CandidateWithCategory;
rank: number;
isActive: boolean;
onStatusChange: (status: "researching" | "ordered" | "arrived") => void;
// Will add: delta?: CandidateDelta;
}
```
CandidateCard props:
```typescript
interface CandidateCardProps {
id: number;
name: string;
weightGrams: number | null;
priceCents: number | null;
// ... other props
// Will add: delta?: CandidateDelta;
}
```
ComparisonTable props:
```typescript
interface ComparisonTableProps {
candidates: CandidateWithCategory[];
resolvedCandidateId: number | null;
// Will add: deltas?: Record<number, CandidateDelta>;
}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Create SetupImpactSelector and ImpactDeltaBadge components</name>
<files>src/client/components/SetupImpactSelector.tsx, src/client/components/ImpactDeltaBadge.tsx</files>
<action>
1. Create src/client/components/SetupImpactSelector.tsx:
- Import useSetups from hooks/useSetups
- Import useUIStore from stores/uiStore
- Export function SetupImpactSelector()
- Read selectedSetupId and setSelectedSetupId from uiStore
- Fetch setups via useSetups()
- If no setups or loading, return null
- Render: a flex row with label "Impact on setup:" (text-xs text-gray-500) and a native `<select>` element
- Select value = selectedSetupId ?? "", onChange parses to number or null
- Options: "None" (value="") + each setup by name
- Styling: text-sm border border-gray-200 rounded-lg px-2 py-1 text-gray-700 bg-white focus:outline-none focus:ring-1 focus:ring-gray-300
2. Create src/client/components/ImpactDeltaBadge.tsx:
- Import type CandidateDelta from lib/impactDeltas
- Import formatWeight, formatPrice, WeightUnit, Currency from lib/formatters
- Import useWeightUnit from hooks/useWeightUnit
- Import useCurrency from hooks/useCurrency
- Export function ImpactDeltaBadge({ delta, type }: { delta: CandidateDelta | undefined; type: "weight" | "price" })
- If !delta or delta.mode === "none", return null
- Pick value: type === "weight" ? delta.weightDelta : delta.priceDelta
- If value === null (no data): render `<span className="text-xs text-gray-300">` with "-- (no weight data)" or "-- (no price data)" depending on type
- If value is a number:
- formatted = type === "weight" ? formatWeight(Math.abs(value), unit) : formatPrice(Math.abs(value), currency)
- sign: value > 0 -> "+" , value < 0 -> "-" (use minus sign), value === 0 -> +/-
- colorClass: value < 0 -> "text-green-600" (lighter/cheaper is good), value > 0 -> "text-red-500", value === 0 -> "text-gray-400"
- modeLabel: delta.mode === "add" ? " (add)" : ""
- vsLabel: delta.mode === "replace" && delta.replacedItemName ? ` vs ${delta.replacedItemName}` : "" (only show this as a title attribute on the span, not inline text -- too long)
- Render: `<span className="text-xs font-medium {colorClass}" title={vsLabel || undefined}>{sign}{formatted}{modeLabel}</span>`
- The component reads unit/currency internally via hooks so callers don't need to pass them.
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/GearBox && bun run lint 2>&1 | head -20</automated>
</verify>
<done>SetupImpactSelector renders a setup dropdown reading from uiStore. ImpactDeltaBadge renders signed, colored delta indicators with null-data handling. Both components lint-clean.</done>
</task>
<task type="auto">
<name>Task 2: Wire impact preview into thread detail page and all candidate views</name>
<files>src/client/routes/threads/$threadId.tsx, src/client/components/CandidateListItem.tsx, src/client/components/CandidateCard.tsx, src/client/components/ComparisonTable.tsx</files>
<action>
1. In src/client/routes/threads/$threadId.tsx:
- Add imports: useSetup from hooks/useSetups, useImpactDeltas from hooks/useImpactDeltas, SetupImpactSelector from components/SetupImpactSelector, type CandidateDelta from lib/impactDeltas
- Read selectedSetupId from useUIStore: `const selectedSetupId = useUIStore((s) => s.selectedSetupId);`
- Fetch setup data: `const { data: setupData } = useSetup(selectedSetupId ?? null);`
- Compute deltas: `const impactDeltas = useImpactDeltas(thread.candidates, setupData?.items, thread.categoryId);` (place after thread is loaded, inside the render body after the isLoading/isError guards)
- Place `<SetupImpactSelector />` in the header section, after the thread name/status row and before the toolbar. Wrap it in a div for spacing if needed.
- Pass delta to CandidateListItem: add prop `delta={impactDeltas.deltas[candidate.id]}` to each CandidateListItem (both Reorder.Group and static div renderings)
- Pass delta to CandidateCard: add prop `delta={impactDeltas.deltas[candidate.id]}` (the CandidateCard receives individual props, so pass it as `delta={impactDeltas.deltas[candidate.id]}`)
- Pass deltas to ComparisonTable: add prop `deltas={impactDeltas.deltas}` alongside existing candidates and resolvedCandidateId
2. In src/client/components/CandidateListItem.tsx:
- Import type CandidateDelta from lib/impactDeltas
- Import ImpactDeltaBadge from ./ImpactDeltaBadge
- Add `delta?: CandidateDelta;` to CandidateListItemProps
- Add `delta` to destructured props
- Render two ImpactDeltaBadge components inside the badges flex-wrap div (after the existing weight and price badges):
- `{delta && <ImpactDeltaBadge delta={delta} type="weight" />}`
- `{delta && <ImpactDeltaBadge delta={delta} type="price" />}`
- Place these AFTER the existing weight/price badges so they appear as secondary indicators
3. In src/client/components/CandidateCard.tsx:
- Import type CandidateDelta from lib/impactDeltas
- Import ImpactDeltaBadge from ./ImpactDeltaBadge
- Add `delta?: CandidateDelta;` to CandidateCardProps
- Add `delta` to destructured props
- Render two ImpactDeltaBadge components inside the badges flex-wrap div (after weight/price badges):
- `{delta && <ImpactDeltaBadge delta={delta} type="weight" />}`
- `{delta && <ImpactDeltaBadge delta={delta} type="price" />}`
4. In src/client/components/ComparisonTable.tsx:
- Import type CandidateDelta from lib/impactDeltas
- Import ImpactDeltaBadge from ./ImpactDeltaBadge
- Add `deltas?: Record<number, CandidateDelta>;` to ComparisonTableProps
- Add `deltas` to destructured props
- Add two new rows to ATTRIBUTE_ROWS array, placed right after the "weight" row and "price" row respectively:
a. After "weight" row, add:
```
{
key: "impact-weight",
label: "Impact (wt)",
render: (c) => deltas?.[c.id] ? <ImpactDeltaBadge delta={deltas[c.id]} type="weight" /> : <span className="text-gray-300">--</span>,
}
```
b. After "price" row, add:
```
{
key: "impact-price",
label: "Impact ($)",
render: (c) => deltas?.[c.id] ? <ImpactDeltaBadge delta={deltas[c.id]} type="price" /> : <span className="text-gray-300">--</span>,
}
```
- These are separate rows (per research recommendation) to avoid conflating candidate-relative deltas with setup impact deltas.
- The impact rows show "--" when no setup is selected (deltas undefined or no entry for candidate).
</action>
<verify>
<automated>cd /home/jlmak/Projects/jlmak/GearBox && bun test && bun run lint 2>&1 | head -20</automated>
</verify>
<done>
- SetupImpactSelector dropdown visible in thread header
- Selecting a setup shows weight/cost delta badges on each candidate in list, grid, and compare views
- Replace mode: signed delta with green (lighter/cheaper) or red (heavier/pricier) coloring
- Add mode: positive delta with "(add)" label
- Null weight/price: shows "-- (no weight data)" / "-- (no price data)" indicator
- Deselecting setup clears all delta indicators
- ComparisonTable has dedicated "Impact (wt)" and "Impact ($)" rows
- All tests pass, lint clean
</done>
</task>
</tasks>
<verification>
- `bun test` full suite passes
- `bun run lint` clean
- SetupImpactSelector renders in thread header with all setups as options
- Selecting a setup triggers useSetup fetch and delta computation
- CandidateListItem, CandidateCard, ComparisonTable all render delta badges
- Replace mode detected when setup has item in same category as thread
- Add mode used otherwise
- Null weight/price shows clear indicator
- Deselecting shows no deltas (clean state)
</verification>
<success_criteria>
- All four IMPC requirements visible in the UI
- Delta rendering works across list, grid, and compare views
- No regressions in existing functionality
- Clean lint output
</success_criteria>
<output>
After completion, create `.planning/phases/13-setup-impact-preview/13-02-SUMMARY.md`
</output>