--- phase: 11-candidate-ranking plan: "01" subsystem: database, api tags: [drizzle, sqlite, hono, zod, sort-order, reorder, candidates] # Dependency graph requires: [] provides: - sortOrder REAL column on threadCandidates with default 0 - reorderCandidates service function (transaction, active-only guard) - PATCH /api/threads/:id/candidates/reorder endpoint with Zod validation - getThreadWithCandidates returns candidates ordered by sort_order ASC - createCandidate appends at max sort_order + 1000 (first=1000, second=2000) - reorderCandidatesSchema Zod validator in shared/schemas.ts - ReorderCandidates type in shared/types.ts affects: [11-02, frontend-drag-reorder, candidate-lists] # Tech tracking tech-stack: added: [] patterns: - "Append-at-end sort_order: query MAX(sort_order), insert at +1000 gap" - "Reorder transaction pattern: verify active thread, loop UPDATE sort_order = (index+1)*1000" - "Active-only guard in reorder: return { success: false, error } when thread status != active" key-files: created: - drizzle/0005_clear_micromax.sql - drizzle/meta/0005_snapshot.json modified: - src/db/schema.ts - tests/helpers/db.ts - src/shared/schemas.ts - src/shared/types.ts - src/server/services/thread.service.ts - src/server/routes/threads.ts - tests/services/thread.service.test.ts - tests/routes/threads.test.ts key-decisions: - "sortOrder uses REAL type (not INTEGER) to allow fractional values for future midpoint insertions without bulk rewrites" - "First candidate gets sort_order=1000, subsequent at +1000 gaps, giving room for future insertions" - "reorderCandidates uses (index+1)*1000 to space out assignments and reset gaps after each reorder" - "Applied migration directly via sqlite3 CLI + data backfill instead of db:push (avoided data-loss warning on existing rows)" patterns-established: - "Reorder endpoint pattern: PATCH /:id/candidates/reorder, Zod validates orderedIds array, service returns {success, error}" - "Service active-only guard: check thread.status !== 'active', return {success: false, error: 'Thread not active'}" requirements-completed: [RANK-01, RANK-04, RANK-05] # Metrics duration: 4min completed: 2026-03-16 --- # Phase 11 Plan 01: Candidate Ranking Backend Summary **sortOrder REAL column, reorderCandidates transaction service, and PATCH /api/threads/:id/candidates/reorder endpoint with active-thread guard** ## Performance - **Duration:** ~4 min - **Started:** 2026-03-16T21:19:26Z - **Completed:** 2026-03-16T21:22:46Z - **Tasks:** 2 of 2 - **Files modified:** 8 ## Accomplishments - Added sortOrder REAL column to threadCandidates with 1000-gap append strategy - Implemented reorderCandidates service with transaction and active-thread guard - Added PATCH /api/threads/:id/candidates/reorder endpoint with Zod validation - getThreadWithCandidates now orders candidates by sort_order ASC - 10 new tests (5 service + 5 route) added; all 135 tests pass with zero regressions ## Task Commits Each task was committed atomically: 1. **Task 1: Schema, migration, service layer, and tests for sort_order + reorder** - `f01d71d` (feat) 2. **Task 2: PATCH reorder route + route tests** - `d6acfcb` (feat) _Note: TDD tasks each committed after GREEN phase._ ## Files Created/Modified - `src/db/schema.ts` - Added sortOrder REAL column to threadCandidates - `tests/helpers/db.ts` - Added sort_order REAL NOT NULL DEFAULT 0 to CREATE TABLE - `src/shared/schemas.ts` - Added reorderCandidatesSchema - `src/shared/types.ts` - Added ReorderCandidates type, imported reorderCandidatesSchema - `src/server/services/thread.service.ts` - Added reorderCandidates, updated createCandidate + getThreadWithCandidates - `src/server/routes/threads.ts` - Added PATCH /:id/candidates/reorder route - `tests/services/thread.service.test.ts` - Added 5 new tests for sort_order behavior - `tests/routes/threads.test.ts` - Added 5 new route tests for reorder endpoint - `drizzle/0005_clear_micromax.sql` - Generated migration SQL for sort_order column - `drizzle/meta/0005_snapshot.json` - Drizzle schema snapshot ## Decisions Made - Used REAL type for sort_order (not INTEGER) to allow fractional values for future midpoint insertions - 1000-gap strategy: first candidate = 1000, each subsequent += 1000; reorder resets to (index+1)*1000 - Applied migration directly via sqlite3 CLI to avoid Drizzle's data-loss warning on existing rows (db had 2 rows; column has DEFAULT 0 so no actual data loss) - Backfilled existing candidates with ROW_NUMBER * 1000 per thread to give proper initial ordering ## Deviations from Plan None - plan executed exactly as written. ## Issues Encountered - `bun run db:push` showed data-loss warning for adding NOT NULL column to existing rows. Applied the migration directly via sqlite3 CLI instead (`ALTER TABLE thread_candidates ADD COLUMN sort_order REAL NOT NULL DEFAULT 0`). The column has DEFAULT 0 so no actual data loss; existing rows got 0 then were backfilled to proper 1000-gap values. ## Next Phase Readiness - Backend reorder API fully operational; frontend drag-to-reorder (11-02) can now consume PATCH /api/threads/:id/candidates/reorder - sort_order values returned in getThreadWithCandidates response, available to frontend for drag state initialization --- *Phase: 11-candidate-ranking* *Completed: 2026-03-16*