Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@ Requirements for this milestone. Each maps to roadmap phases.
|
|||||||
### Candidate Ranking
|
### Candidate Ranking
|
||||||
|
|
||||||
- [x] **RANK-01**: User can drag candidates to reorder priority ranking within a thread
|
- [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-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-04**: Candidate rank order persists across sessions
|
||||||
- [x] **RANK-05**: Drag handles and ranking are disabled on resolved threads
|
- [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-03 | Phase 12 | Pending |
|
||||||
| COMP-04 | Phase 12 | Pending |
|
| COMP-04 | Phase 12 | Pending |
|
||||||
| RANK-01 | Phase 11 | Complete |
|
| RANK-01 | Phase 11 | Complete |
|
||||||
| RANK-02 | Phase 11 | Pending |
|
| RANK-02 | Phase 11 | Complete |
|
||||||
| RANK-03 | Phase 10 | Complete |
|
| RANK-03 | Phase 10 | Complete |
|
||||||
| RANK-04 | Phase 11 | Complete |
|
| RANK-04 | Phase 11 | Complete |
|
||||||
| RANK-05 | Phase 11 | Complete |
|
| RANK-05 | Phase 11 | Complete |
|
||||||
|
|||||||
@@ -110,6 +110,6 @@ Plans:
|
|||||||
| 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 | 1/1 | 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 | - |
|
| 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 | - |
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ milestone: v1.3
|
|||||||
milestone_name: Research & Decision Tools
|
milestone_name: Research & Decision Tools
|
||||||
status: planning
|
status: planning
|
||||||
stopped_at: Completed 11-candidate-ranking/11-02-PLAN.md
|
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
|
last_activity: 2026-03-16 — Roadmap created for v1.3 milestone
|
||||||
progress:
|
progress:
|
||||||
total_phases: 4
|
total_phases: 4
|
||||||
|
|||||||
175
.planning/phases/11-candidate-ranking/11-VERIFICATION.md
Normal file
175
.planning/phases/11-candidate-ranking/11-VERIFICATION.md
Normal file
@@ -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 <Reorder.Group> (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 `<div>` via `onPointerUp`, not to the active-thread `<Reorder.Group>`. 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 `<Reorder.Group>` for the active-thread path. The resolved-thread `<div>` (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 `<Reorder.Group>` 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 && <RankBadge rank={rank} />}`; `$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 `<div>` (not `Reorder.Group`) at `$threadId.tsx:217` |
|
||||||
|
| 10 | Rank badges remain visible on resolved threads | VERIFIED | Resolved thread renders `<CandidateListItem isActive={false}>` which always renders `<RankBadge rank={rank} />` 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<typeof reorderCandidatesSchema>` |
|
||||||
|
| `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 `<Reorder.Group>` |
|
||||||
|
| `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 `<Reorder.Group>` 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 `<div>` 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 `<Reorder.Group>` and absent from the resolved-thread `<div>`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 `<div>` (isActive=false path) only; the active `<Reorder.Group>` had no handler to trigger the mutation.
|
||||||
|
|
||||||
|
**Fix:** `src/client/routes/threads/$threadId.tsx` line 198 — `onPointerUp={handleDragEnd}` is now on `<Reorder.Group axis="y" values={displayItems} onReorder={setTempItems} onPointerUp={handleDragEnd}>`. The resolved-thread `<div>` 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_
|
||||||
Reference in New Issue
Block a user