diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 4f05671..7dc1b45 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -17,7 +17,7 @@ Requirements for this milestone. Each maps to roadmap phases. ### Candidate Ranking - [x] **RANK-01**: User can drag candidates to reorder priority ranking within a thread -- [ ] **RANK-02**: Top 3 ranked candidates display rank badges (gold, silver, bronze) +- [x] **RANK-02**: Top 3 ranked candidates display rank badges (gold, silver, bronze) - [x] **RANK-03**: User can add pros and cons text per candidate displayed as bullet lists - [x] **RANK-04**: Candidate rank order persists across sessions - [x] **RANK-05**: Drag handles and ranking are disabled on resolved threads @@ -73,7 +73,7 @@ Which phases cover which requirements. Updated during roadmap creation. | COMP-03 | Phase 12 | Pending | | COMP-04 | Phase 12 | Pending | | RANK-01 | Phase 11 | Complete | -| RANK-02 | Phase 11 | Pending | +| RANK-02 | Phase 11 | Complete | | RANK-03 | Phase 10 | Complete | | RANK-04 | Phase 11 | Complete | | RANK-05 | Phase 11 | Complete | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 2209885..90f3611 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -110,6 +110,6 @@ Plans: | 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 | | 10. Schema Foundation + Pros/Cons Fields | v1.3 | 1/1 | Complete | 2026-03-16 | -| 11. Candidate Ranking | 2/2 | Complete | 2026-03-16 | - | +| 11. Candidate Ranking | 2/2 | Complete | 2026-03-16 | - | | 12. Comparison View | v1.3 | 0/TBD | Not started | - | | 13. Setup Impact Preview | v1.3 | 0/TBD | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index 13851ac..8c2db52 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,7 +4,7 @@ milestone: v1.3 milestone_name: Research & Decision Tools status: planning stopped_at: Completed 11-candidate-ranking/11-02-PLAN.md -last_updated: "2026-03-16T21:30:15.460Z" +last_updated: "2026-03-16T21:39:11.967Z" last_activity: 2026-03-16 — Roadmap created for v1.3 milestone progress: total_phases: 4 diff --git a/.planning/phases/11-candidate-ranking/11-VERIFICATION.md b/.planning/phases/11-candidate-ranking/11-VERIFICATION.md new file mode 100644 index 0000000..cf570e2 --- /dev/null +++ b/.planning/phases/11-candidate-ranking/11-VERIFICATION.md @@ -0,0 +1,175 @@ +--- +phase: 11-candidate-ranking +verified: 2026-03-16T23:30:00Z +status: human_needed +score: 11/11 must-haves verified +re_verification: + previous_status: gaps_found + previous_score: 9/11 + gaps_closed: + - "User can drag a candidate card to a new position in list view and it persists after page refresh — onPointerUp={handleDragEnd} is now correctly on the active (line 198)" + gaps_remaining: [] + regressions: [] +human_verification: + - test: "Drag a candidate on an active thread and refresh" + expected: "Dragged order is preserved after page reload (new order loaded from server)" + why_human: "Smooth drag animation, gap preview, pointer-event timing, and actual persistence need visual inspection and interaction" + - test: "Drag handles visibility on resolved vs active threads" + expected: "Active threads show GripVertical drag handles; resolved threads show no drag handles but rank badges remain" + why_human: "CSS visibility and conditional rendering need visual verification" + - test: "Top 3 rank badges appearance" + expected: "Gold (#D4AF37), silver (#C0C0C0), bronze (#CD7F32) medal icons appear on positions 1, 2, 3 in both list and grid views" + why_human: "Color rendering and icon display need visual confirmation" +--- + +# Phase 11: Candidate Ranking Verification Report + +**Phase Goal:** Users can drag candidates into a priority order that persists and is visually communicated +**Verified:** 2026-03-16T23:30:00Z +**Status:** human_needed +**Re-verification:** Yes — after gap closure + +## Re-verification Summary + +Previous status was `gaps_found` (score 9/11). The one critical blocker was: + +> `handleDragEnd` (which calls `reorderMutation.mutate`) was wired to the resolved-thread `
` via `onPointerUp`, not to the active-thread ``. Dragging updated `tempItems` visually but never fired the mutation. + +**Fix verified:** `src/client/routes/threads/$threadId.tsx` line 198 now has `onPointerUp={handleDragEnd}` on the `` for the active-thread path. The resolved-thread `
` (lines 217-233) has no `onPointerUp` handler. The fix is correct and complete. + +All 11 truths now pass automated checks. 135/135 tests pass. No regressions detected. + +--- + +## Goal Achievement + +### Observable Truths + +| # | Truth | Status | Evidence | +|---|-------|--------|----------| +| 1 | Candidates returned from getThreadWithCandidates are ordered by sort_order ascending | VERIFIED | `thread.service.ts:90` uses `.orderBy(asc(threadCandidates.sortOrder))` | +| 2 | Calling reorderCandidates with a new ID sequence updates sort_order values | VERIFIED | `reorderCandidates` loops `orderedIds`, sets `sortOrder: (i+1)*1000` per candidate in a transaction (`thread.service.ts:240`) | +| 3 | PATCH /api/threads/:id/candidates/reorder returns 200 and persists new order | VERIFIED | Route at `threads.ts:129-140`; Zod-validated; returns `{ success: true }` or 400 | +| 4 | reorderCandidates returns error when thread status is not active | VERIFIED | `thread.service.ts:234` checks `thread.status !== "active"`, returns `{ success: false, error: "Thread not active" }` | +| 5 | New candidates appended to end of rank (max sort_order + 1000) | VERIFIED | `createCandidate` queries MAX, sets `sortOrder: (maxRow?.maxOrder ?? 0) + 1000` (`thread.service.ts:150,171`) | +| 6 | User can drag a candidate card to a new position in list view and it persists after page refresh | VERIFIED (code) | `handleDragEnd` is now wired via `onPointerUp={handleDragEnd}` on `` at `$threadId.tsx:198`. Mutation fires on pointer-up after drag. Persistence needs human confirmation. | +| 7 | Top 3 candidates display gold, silver, and bronze medal badges | VERIFIED (code) | `RankBadge` in `CandidateListItem.tsx:37-47` renders medal icon with `RANK_COLORS` for rank 1-3, returns null for rank > 3. Visual confirmation needed. | +| 8 | Rank badges appear in both list view and grid view | VERIFIED | `CandidateCard.tsx:165` renders `{rank != null && }`; `$threadId.tsx:258` passes `rank={index + 1}` to all grid cards | +| 9 | Drag handles are hidden and drag is disabled on resolved threads | VERIFIED | `CandidateListItem.tsx:73` renders drag handle only if `isActive`; resolved threads render plain `
` (not `Reorder.Group`) at `$threadId.tsx:217` | +| 10 | Rank badges remain visible on resolved threads | VERIFIED | Resolved thread renders `` which always renders `` at line 85 | +| 11 | User can toggle between list and grid view with list as default | VERIFIED | `uiStore.ts:112` initializes `candidateViewMode: "list"`; toggle buttons in `$threadId.tsx:146-172` call `setCandidateViewMode` | + +**Score:** 11/11 truths verified (all pass automated checks; 3 require human visual confirmation) + +--- + +## Required Artifacts + +### Plan 11-01 Artifacts + +| Artifact | Expected | Status | Details | +|----------|----------|--------|---------| +| `src/db/schema.ts` | sortOrder REAL column on threadCandidates | VERIFIED | Line 64: `sortOrder: real("sort_order").notNull().default(0)` | +| `src/shared/schemas.ts` | reorderCandidatesSchema Zod validator | VERIFIED | Line 66: `export const reorderCandidatesSchema = z.object({ orderedIds: ... })` | +| `src/shared/types.ts` | ReorderCandidates type | VERIFIED | Line 37: `export type ReorderCandidates = z.infer` | +| `src/server/services/thread.service.ts` | reorderCandidates function exported | VERIFIED | Lines 220+: full implementation exported; sortOrder used at lines 90, 150, 171, 240 | +| `src/server/routes/threads.ts` | PATCH /:id/candidates/reorder endpoint | VERIFIED | Lines 129-140: registered with Zod validation | +| `tests/helpers/db.ts` | sort_order column in CREATE TABLE | VERIFIED | Line 60: `sort_order REAL NOT NULL DEFAULT 0` | + +### Plan 11-02 Artifacts + +| Artifact | Expected | Status | Details | +|----------|----------|--------|---------| +| `src/client/components/CandidateListItem.tsx` | Horizontal list card with drag handle and rank badge (min 60 lines) | VERIFIED | 211 lines; `Reorder.Item` with `useDragControls`, drag handle (lines 73-82), `RankBadge` (line 85) | +| `src/client/routes/threads/$threadId.tsx` | Reorder.Group wrapping + tempItems pattern + handleDragEnd on Reorder.Group | VERIFIED | `Reorder.Group` at line 194 with `onPointerUp={handleDragEnd}` at line 198; `tempItems` pattern at lines 28-37, 76 | +| `src/client/hooks/useCandidates.ts` | useReorderCandidates mutation hook | VERIFIED | Lines 66-78: calls `apiPatch` to `candidates/reorder`, invalidates query on settled | +| `src/client/stores/uiStore.ts` | candidateViewMode state | VERIFIED | Lines 53-54 (interface), 112-113 (implementation): default "list" | +| `src/client/components/CandidateCard.tsx` | RankBadge on grid cards | VERIFIED | Imports `RankBadge` from `CandidateListItem` (line 6); renders at line 165 when `rank != null` | + +--- + +## Key Link Verification + +### Plan 11-01 Key Links + +| From | To | Via | Status | Details | +|------|----|-----|--------|---------| +| `threads.ts` | `thread.service.ts` | `reorderCandidates(db, threadId, orderedIds)` | WIRED | Line 136 imports and calls `reorderCandidates` | +| `threads.ts` | `schemas.ts` | `zValidator with reorderCandidatesSchema` | WIRED | Line 8 imports `reorderCandidatesSchema`; line 131 uses `zValidator("json", reorderCandidatesSchema)` | +| `thread.service.ts` | `schema.ts` | `threadCandidates.sortOrder in ORDER BY and UPDATE` | WIRED | Line 90 uses `asc(threadCandidates.sortOrder)`; line 240 sets `sortOrder: (i+1)*1000` | + +### Plan 11-02 Key Links + +| From | To | Via | Status | Details | +|------|----|-----|--------|---------| +| `threads/$threadId.tsx` | `useCandidates.ts` | `useReorderCandidates(threadId)` | WIRED | Line 7 imports, line 26 calls `useReorderCandidates(threadId)` | +| `useCandidates.ts` | `/api/threads/:id/candidates/reorder` | `apiPatch` | WIRED | Lines 70-73: `apiPatch<{ success: boolean }>(\`/api/threads/${threadId}/candidates/reorder\`, data)` | +| `threads/$threadId.tsx` | `framer-motion` | `Reorder.Group + Reorder.Item` | WIRED | Line 2 imports `{ Reorder }`; line 194 uses `` | +| `CandidateListItem.tsx` | `framer-motion` | `Reorder.Item + useDragControls` | WIRED | Line 1 imports `{ Reorder, useDragControls }`; line 55 calls `useDragControls()` | +| `uiStore.ts` | `threads/$threadId.tsx` | `candidateViewMode state` | WIRED | Lines 23-24 consume `candidateViewMode`/`setCandidateViewMode`; lines 148-170 use them in toggle buttons | +| `Reorder.Group` | `reorderMutation` | `handleDragEnd via onPointerUp` | WIRED | `onPointerUp={handleDragEnd}` is on the active-thread `` at line 198. `handleDragEnd` at lines 78-84 calls `reorderMutation.mutate`. Fix confirmed. | + +--- + +## Requirements Coverage + +| Requirement | Source Plan | Description | Status | Evidence | +|-------------|-------------|-------------|--------|----------| +| RANK-01 | 11-01, 11-02 | User can drag candidates to reorder priority ranking | SATISFIED | Drag via framer-motion `Reorder.Group`; `handleDragEnd` now wired at `onPointerUp` on active `Reorder.Group` (line 198); mutation fires `PATCH /api/threads/:id/candidates/reorder` | +| RANK-02 | 11-02 | Top 3 ranked candidates display rank badges (gold, silver, bronze) | SATISFIED | `RankBadge` renders medal icon with `RANK_COLORS`; used in both `CandidateListItem` (line 85) and `CandidateCard` (line 165) | +| RANK-04 | 11-01, 11-02 | Candidate rank order persists across sessions | SATISFIED | `sort_order` column in DB; `reorderCandidates` service updates it in a transaction; React Query invalidates on `onSettled` so next load fetches fresh sorted order | +| RANK-05 | 11-01, 11-02 | Drag handles and ranking disabled on resolved threads | SATISFIED | `CandidateListItem.tsx:73` renders drag handle only if `isActive`; resolved threads use plain `
` without `Reorder.Group`; service returns 400 if thread not active | + +Note: RANK-03 (pros/cons fields) was handled in Phase 10 and is not part of Phase 11. + +--- + +## Anti-Patterns Found + +No blockers or warnings detected in the fixed code. + +| File | Line | Pattern | Severity | Impact | +|------|------|---------|----------|--------| +| — | — | — | — | No anti-patterns found | + +Previously identified blockers have been resolved: `onPointerUp={handleDragEnd}` is now correctly placed on the active `` and absent from the resolved-thread `
`. + +--- + +## Human Verification Required + +### 1. Drag persistence after refresh + +**Test:** Open an active thread with 3+ candidates, drag a candidate to a different position (e.g. drag position 3 to position 1), then refresh the page. +**Expected:** The new order is preserved after refresh. The `PATCH /api/threads/:id/candidates/reorder` call fires on pointer-up, and the invalidated React Query refetch loads the persisted sort order. +**Why human:** Real-time drag animation quality, gap animation between items, pointer-event timing, and the full round-trip to the server cannot be confirmed by static code analysis. + +### 2. Gold/silver/bronze badge colors + +**Test:** Open an active thread with 3+ candidates and view in list mode. +**Expected:** Position 1 shows a gold medal icon (`#D4AF37`), position 2 shows silver (`#C0C0C0`), position 3 shows bronze (`#CD7F32`). Positions 4 and above show no badge. Toggle to grid view and verify the same badges appear on the first 3 cards. +**Why human:** Hex color rendering accuracy and icon (medal) correctness need visual confirmation. + +### 3. Drag handle visibility on resolved threads + +**Test:** Navigate to a resolved thread in list view. +**Expected:** No GripVertical drag handle icons are visible. Gold/silver/bronze rank badges are still present on the top 3 candidates in their sorted order. Candidates cannot be dragged. +**Why human:** Conditional rendering of drag handles and static-only resolved state need visual verification. + +--- + +## Gap Closure Confirmation + +The single gap from the previous verification has been closed: + +**Gap:** `onPointerUp={handleDragEnd}` was on the resolved-thread `
` (isActive=false path) only; the active `` had no handler to trigger the mutation. + +**Fix:** `src/client/routes/threads/$threadId.tsx` line 198 — `onPointerUp={handleDragEnd}` is now on ``. The resolved-thread `
` at lines 217-233 has no `onPointerUp`. The wiring is correct. + +**Regression check:** 135/135 tests pass. All previously-verified artifacts and key links remain intact. + +--- + +_Verified: 2026-03-16T23:30:00Z_ +_Verifier: Claude (gsd-verifier)_ +_Re-verification: Yes — gap closure after previous gaps_found verdict_