--- phase: 01-ai-ranking-backend plan: 04 subsystem: ai-ranking tags: [auto-trigger, notifications, ranking, evaluation, tRPC] requirements: [RANK-09, RANK-10] dependency_graph: requires: [01-01, 01-02, 01-03] provides: [auto-trigger-on-evaluation-complete, retroactive-scan, admin-notifications] affects: [evaluation-submit-mutation, ranking-router, in-app-notifications] tech_stack: added: [] patterns: [fire-and-forget async, cooldown-guard, module-level-helper-function] key_files: created: [] modified: - src/server/services/in-app-notification.ts - src/server/routers/evaluation.ts - src/server/routers/ranking.ts decisions: - "triggerAutoRankIfComplete defined as module-level (non-exported) function in evaluation.ts — avoids circular imports and keeps the auto-trigger logic colocated with the mutation it serves" - "EvaluationConfig null fallback typed as {} as EvaluationConfig rather than just {} — required for TypeScript strict mode to recognize rankingCriteria and autoRankOnComplete fields" - "ParsedRankingRule[] cast via unknown as Prisma.InputJsonValue — Prisma InputJsonValue does not overlap with typed arrays, double-cast is the correct pattern throughout the codebase" - "retroactiveScan uses RETROACTIVE triggerType to distinguish from MANUAL/AUTO/QUICK — prevents duplicate re-runs on subsequent scans" - "triggerAutoRank procedure in ranking.ts uses MANUAL triggerType (not AUTO) because it is admin-initiated, not system-initiated" metrics: duration: ~8min completed_date: "2026-02-27" tasks_completed: 2 files_modified: 3 --- # Phase 1 Plan 04: Auto-Trigger + Retroactive Scan Summary **One-liner:** Fire-and-forget auto-ranking on evaluation completion with 5-minute cooldown guard, plus retroactive scan procedure for rounds already complete at deploy time. ## What Was Built ### Task 1: AI_RANKING_COMPLETE + AI_RANKING_FAILED notification types Added two new entries to `src/server/services/in-app-notification.ts`: - `NotificationTypes.AI_RANKING_COMPLETE` — type string + `BarChart3` icon + `normal` priority - `NotificationTypes.AI_RANKING_FAILED` — type string + `AlertTriangle` icon + `high` priority Pattern follows existing FILTERING_COMPLETE / FILTERING_FAILED entries exactly. ### Task 2: Auto-trigger hook + new ranking procedures **evaluation.ts — `triggerAutoRankIfComplete` function:** ```typescript async function triggerAutoRankIfComplete( roundId: string, prisma: PrismaClient, userId: string, ): Promise ``` Logic flow: 1. Count required assignments — skip if not all complete 2. Read `round.configJson` → check `autoRankOnComplete` + `rankingCriteria` — skip silently if not configured 3. Cooldown guard — skip if AUTO snapshot exists within last 5 minutes 4. Call `aiQuickRank()` — executes in 10-30s asynchronously 5. Create `RankingSnapshot` with `triggerType: 'AUTO'`, `triggeredById: null` 6. Notify admins via `AI_RANKING_COMPLETE` notification 7. Catch-all: any error sends `AI_RANKING_FAILED` notification, never rethrows **Fire-and-forget call in submit mutation (line 378):** ```typescript void triggerAutoRankIfComplete(evaluation.assignment.roundId, ctx.prisma, ctx.user.id) ``` Placed after `$transaction([evaluation.update, assignment.update])`, before `logAudit`. The submission returns immediately — ranking runs asynchronously. **ranking.ts — 7 total procedures:** | Procedure | Type | Trigger | Description | |-----------|------|---------|-------------| | `parseRankingCriteria` | mutation | admin | Preview-only parse (RANK-01, RANK-03) | | `executeRanking` | mutation | admin | Confirmed ranking with pre-parsed rules (RANK-05, RANK-06, RANK-08) | | `quickRank` | mutation | admin | Parse + execute in one step (RANK-04) | | `listSnapshots` | query | admin | List snapshots for round, most recent first | | `getSnapshot` | query | admin | Retrieve single snapshot by ID | | `triggerAutoRank` | mutation | admin | Manual trigger from round config (RANK-09) | | `retroactiveScan` | mutation | admin | Scan all active/closed rounds (RANK-10) | **retroactiveScan logic:** - Finds all `ROUND_ACTIVE` and `ROUND_CLOSED` rounds - Checks each for `autoRankOnComplete + rankingCriteria` config - Checks if all required assignments are complete - Skips rounds that already have a `RETROACTIVE` snapshot - Executes ranking sequentially (not parallel) to avoid OpenAI rate limits - Returns `{ results[], total, triggered }` summary ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 1 - Bug] TypeScript strict mode: `?? {}` loses EvaluationConfig type** - **Found during:** Task 2 typecheck - **Issue:** `(configJson as EvaluationConfig | null) ?? {}` — the `{}` fallback widens the type to `{}` which doesn't have `rankingCriteria` / `autoRankOnComplete` fields, causing TS2339 errors - **Fix:** Changed to `?? ({} as EvaluationConfig)` in all three locations (evaluation.ts + two in ranking.ts) - **Files modified:** `src/server/routers/evaluation.ts`, `src/server/routers/ranking.ts` - **Commit:** c310631 **2. [Rule 1 - Bug] TypeScript: ParsedRankingRule[] requires double-cast to InputJsonValue** - **Found during:** Task 2 typecheck - **Issue:** `result.parsedRules as Prisma.InputJsonValue` produces TS2352 — neither type overlaps - **Fix:** Changed to `result.parsedRules as unknown as Prisma.InputJsonValue` (matching the pattern already used for `rankedProjects` arrays) - **Files modified:** `src/server/routers/ranking.ts` (triggerAutoRank + retroactiveScan) - **Commit:** c310631 ## Build Status - `npm run typecheck` — PASSED (0 errors) - `npm run build` — PASSED (full production build) ## Key Links Implemented | From | To | Via | |------|----|-----| | `evaluation.ts submit` | `triggerAutoRankIfComplete` | `void` fire-and-forget after `isCompleted: true` | | `triggerAutoRankIfComplete` | `ai-ranking.ts quickRank` | `aiQuickRank(criteriaText, roundId, prisma, userId)` | | `triggerAutoRankIfComplete` | `in-app-notification.ts` | `notifyAdmins({ type: AI_RANKING_COMPLETE })` | | `ranking.ts triggerAutoRank` | `ai-ranking.ts quickRank` | admin-initiated, creates MANUAL snapshot | | `ranking.ts retroactiveScan` | `ai-ranking.ts quickRank` | sequential per-round, creates RETROACTIVE snapshot | ## Self-Check: PASSED - `src/server/routers/evaluation.ts` — modified, contains `void triggerAutoRankIfComplete(...)` - `src/server/routers/ranking.ts` — modified, contains `triggerAutoRank` and `retroactiveScan` - `src/server/services/in-app-notification.ts` — modified, contains `AI_RANKING_COMPLETE` and `AI_RANKING_FAILED` - Commits: 4683bb8 (Task 1), c310631 (Task 2) - Build: PASSED