From 91bc1005591ccc202a8eca763cf96d39469e724c Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 27 Feb 2026 00:51:07 +0100 Subject: [PATCH] feat(01-01): add RankingSnapshot model + enums to schema.prisma - Add RankingTriggerType enum (MANUAL, AUTO, RETROACTIVE, QUICK) - Add RankingMode enum (PREVIEW, CONFIRMED, QUICK) - Add RankingSnapshotStatus enum (PENDING, RUNNING, COMPLETED, FAILED) - Add RankingSnapshot model with roundId/triggeredById FKs, criteria/results JSON fields, AI metadata - Add Round.rankingSnapshots back-relation (RoundRankingSnapshots) - Add User.rankingSnapshots back-relation (TriggeredRankingSnapshots) - Create migration 20260227000000_add_ranking_snapshot - Regenerate Prisma client (prisma.rankingSnapshot accessible) --- .../migration.sql | 45 ++++++++++++ prisma/schema.prisma | 72 +++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 prisma/migrations/20260227000000_add_ranking_snapshot/migration.sql diff --git a/prisma/migrations/20260227000000_add_ranking_snapshot/migration.sql b/prisma/migrations/20260227000000_add_ranking_snapshot/migration.sql new file mode 100644 index 0000000..8386883 --- /dev/null +++ b/prisma/migrations/20260227000000_add_ranking_snapshot/migration.sql @@ -0,0 +1,45 @@ +-- CreateEnum +CREATE TYPE "RankingTriggerType" AS ENUM ('MANUAL', 'AUTO', 'RETROACTIVE', 'QUICK'); + +-- CreateEnum +CREATE TYPE "RankingMode" AS ENUM ('PREVIEW', 'CONFIRMED', 'QUICK'); + +-- CreateEnum +CREATE TYPE "RankingSnapshotStatus" AS ENUM ('PENDING', 'RUNNING', 'COMPLETED', 'FAILED'); + +-- CreateTable +CREATE TABLE "RankingSnapshot" ( + "id" TEXT NOT NULL, + "roundId" TEXT NOT NULL, + "triggeredById" TEXT, + "triggerType" "RankingTriggerType" NOT NULL DEFAULT 'MANUAL', + "criteriaText" TEXT NOT NULL, + "parsedRulesJson" JSONB NOT NULL, + "startupRankingJson" JSONB, + "conceptRankingJson" JSONB, + "evaluationDataJson" JSONB, + "mode" "RankingMode" NOT NULL DEFAULT 'PREVIEW', + "status" "RankingSnapshotStatus" NOT NULL DEFAULT 'COMPLETED', + "reordersJson" JSONB, + "model" TEXT, + "tokensUsed" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "RankingSnapshot_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "RankingSnapshot_roundId_idx" ON "RankingSnapshot"("roundId"); + +-- CreateIndex +CREATE INDEX "RankingSnapshot_triggeredById_idx" ON "RankingSnapshot"("triggeredById"); + +-- CreateIndex +CREATE INDEX "RankingSnapshot_createdAt_idx" ON "RankingSnapshot"("createdAt"); + +-- AddForeignKey +ALTER TABLE "RankingSnapshot" ADD CONSTRAINT "RankingSnapshot_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RankingSnapshot" ADD CONSTRAINT "RankingSnapshot_triggeredById_fkey" FOREIGN KEY ("triggeredById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e43b79d..c584efe 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -425,6 +425,9 @@ model User { submissionPromotions SubmissionPromotionEvent[] @relation("SubmissionPromoter") deliberationReplacements DeliberationParticipant[] @relation("DeliberationReplacement") + // AI Ranking + rankingSnapshots RankingSnapshot[] @relation("TriggeredRankingSnapshots") + @@index([role]) @@index([status]) } @@ -1405,6 +1408,74 @@ enum AssignmentJobStatus { FAILED } +// ============================================================================= +// AI RANKING MODELS +// ============================================================================= + +enum RankingTriggerType { + MANUAL // Admin clicked "Run ranking" + AUTO // Auto-triggered by assignment completion + RETROACTIVE // Retroactive scan on deployment + QUICK // Quick-rank mode (no preview) +} + +enum RankingMode { + PREVIEW // Parsed rules shown to admin (not yet applied) + CONFIRMED // Admin confirmed rules, ranking applied + QUICK // Quick-rank: parse + apply without preview +} + +enum RankingSnapshotStatus { + PENDING + RUNNING + COMPLETED + FAILED +} + +// Captures a point-in-time AI ranking run for a round +model RankingSnapshot { + id String @id @default(cuid()) + + roundId String + + // Trigger metadata + triggeredById String? // null = auto-triggered + triggerType RankingTriggerType @default(MANUAL) + + // Criteria used + criteriaText String @db.Text + parsedRulesJson Json @db.JsonB + + // Results per category (either can be null/empty if no projects in that category) + startupRankingJson Json? @db.JsonB + conceptRankingJson Json? @db.JsonB + + // Evaluation data freeze (raw scores at time of ranking) + evaluationDataJson Json? @db.JsonB + + // Mode and status + mode RankingMode @default(PREVIEW) + status RankingSnapshotStatus @default(COMPLETED) + + // Post-drag-and-drop reorders (Phase 2 will populate this) + reordersJson Json? @db.JsonB + + // AI metadata + model String? + tokensUsed Int @default(0) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Relations + round Round @relation("RoundRankingSnapshots", fields: [roundId], references: [id], onDelete: Cascade) + triggeredBy User? @relation("TriggeredRankingSnapshots", fields: [triggeredById], references: [id], onDelete: SetNull) + + @@index([roundId]) + @@index([triggeredById]) + @@index([createdAt]) +} + // Tracks progress of long-running AI tagging jobs model TaggingJob { id String @id @default(cuid()) @@ -2146,6 +2217,7 @@ model Round { filteringResults FilteringResult[] filteringJobs FilteringJob[] assignmentJobs AssignmentJob[] + rankingSnapshots RankingSnapshot[] @relation("RoundRankingSnapshots") reminderLogs ReminderLog[] evaluationSummaries EvaluationSummary[] evaluationDiscussions EvaluationDiscussion[]