# Data Model Redesign

## Overview

This document defines the complete Prisma schema for the MOPC architecture redesign. It covers new models, modified models, eliminated models, and the migration path from the current schema.

### Naming Convention Changes

| Current | Redesigned | Rationale |
|---------|-----------|-----------|
| `Pipeline` | `Competition` | Domain-specific — admins think "Competition 2026" |
| `Track` | *(eliminated)* | Main flow is linear; awards are standalone |
| `Stage` | `Round` | Domain-specific — "Round 3: Jury 1 Evaluation" |
| `StageType` | `RoundType` | Follows rename |
| `StageStatus` | `RoundStatus` | Follows rename |
| `ProjectStageState` | `ProjectRoundState` | Follows rename, drops trackId |
| `StageTransition` | *(eliminated)* | Replaced by linear sortOrder + advancement rules |

---

## 1. Eliminated Models & Enums

### Models Removed

```
Track                  -- Main flow is linear; awards are standalone SpecialAward entities
StageTransition        -- Replaced by linear round ordering + AdvancementRule
CohortProject          -- Merged into round-level project ordering
TrackKind (enum)       -- No tracks
RoutingMode (enum)     -- No tracks
DecisionMode (enum)    -- Moved to SpecialAward.decisionMode as a string field
```

### Why Track Is Eliminated

The `Track` model served two purposes:
1. **Main competition flow** — But this is always linear (Intake -> Filter -> Eval -> ... -> Finals)
2. **Award branches** — But awards don't need their own stage pipeline; they need eligibility + voting

Without Track:
- `Round` belongs directly to `Competition` (no intermediate layer)
- `SpecialAward` is self-contained (has its own jury, voting, and result)
- `ProjectRoundState` drops `trackId` (project is in a round, period)
- Admin UI shows a flat list of rounds instead of nested Track > Stage

---

## 2. Core Competition Structure

### Competition (replaces Pipeline)

```prisma
model Competition {
  id           String @id @default(cuid())
  programId    String
  name         String           // "MOPC 2026 Competition"
  slug         String @unique   // "mopc-2026"
  status       CompetitionStatus @default(DRAFT)

  // Competition-wide settings (typed, not generic JSON)
  categoryMode       String @default("SHARED")  // "SHARED" (both categories same flow) | "SPLIT" (separate finalist counts)
  startupFinalistCount   Int @default(3)
  conceptFinalistCount   Int @default(3)

  // Notification preferences
  notifyOnRoundAdvance   Boolean @default(true)
  notifyOnDeadlineApproach Boolean @default(true)
  deadlineReminderDays   Int[] @default([7, 3, 1])  // Days before deadline to send reminders

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  // Relations
  program            Program              @relation(fields: [programId], references: [id], onDelete: Cascade)
  rounds             Round[]
  juryGroups         JuryGroup[]
  submissionWindows  SubmissionWindow[]
  specialAwards      SpecialAward[]
  winnerProposals    WinnerProposal[]

  @@index([programId])
  @@index([status])
}

enum CompetitionStatus {
  DRAFT
  ACTIVE
  CLOSED
  ARCHIVED
}
```

### Round (replaces Stage)

```prisma
model Round {
  id              String      @id @default(cuid())
  competitionId   String
  name            String           // "Jury 1 - Semi-finalist Selection"
  slug            String           // "jury-1-semifinalist"
  roundType       RoundType
  status          RoundStatus @default(ROUND_DRAFT)
  sortOrder       Int         @default(0)

  // Time windows
  windowOpenAt    DateTime?
  windowCloseAt   DateTime?

  // Round-type-specific configuration (validated by Zod per RoundType)
  configJson      Json?       @db.JsonB

  // Links to other entities
  juryGroupId         String?      // Which jury evaluates this round (EVALUATION, LIVE_FINAL)
  submissionWindowId  String?      // Which submission window this round collects docs for (INTAKE, SUBMISSION)

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  // Relations
  competition          Competition           @relation(fields: [competitionId], references: [id], onDelete: Cascade)
  juryGroup            JuryGroup?            @relation(fields: [juryGroupId], references: [id], onDelete: SetNull)
  submissionWindow     SubmissionWindow?     @relation(fields: [submissionWindowId], references: [id], onDelete: SetNull)

  projectRoundStates   ProjectRoundState[]
  assignments          Assignment[]
  evaluationForms      EvaluationForm[]
  filteringRules       FilteringRule[]
  filteringResults     FilteringResult[]
  filteringJobs        FilteringJob[]
  evaluationSummaries  EvaluationSummary[]
  evaluationDiscussions EvaluationDiscussion[]
  gracePeriods         GracePeriod[]
  liveCursor           LiveProgressCursor?
  liveVotingSession    LiveVotingSession?
  cohorts              Cohort[]
  advancementRules     AdvancementRule[]

  // Visible submission windows (which doc rounds jury can see)
  visibleSubmissionWindows RoundSubmissionVisibility[]

  @@unique([competitionId, slug])
  @@unique([competitionId, sortOrder])
  @@index([competitionId])
  @@index([roundType])
  @@index([status])
}

enum RoundType {
  INTAKE          // Application window — collect initial submissions
  FILTERING       // AI screening — automated eligibility check
  EVALUATION      // Jury evaluation — scoring, feedback, advancement decision
  SUBMISSION      // New submission window — additional docs from advancing teams
  MENTORING       // Mentor-team collaboration period
  LIVE_FINAL      // Live ceremony — real-time voting, audience participation
  CONFIRMATION    // Winner agreement — jury signatures + admin confirmation
}

enum RoundStatus {
  ROUND_DRAFT     // Being configured, not visible to participants
  ROUND_ACTIVE    // Open/in progress
  ROUND_CLOSED    // Window closed, results pending or finalized
  ROUND_ARCHIVED  // Historical, read-only
}
```

### ProjectRoundState (replaces ProjectStageState)

```prisma
model ProjectRoundState {
  id           String                 @id @default(cuid())
  projectId    String
  roundId      String
  state        ProjectRoundStateValue @default(PENDING)
  enteredAt    DateTime               @default(now())
  exitedAt     DateTime?
  metadataJson Json?                  @db.JsonB   // Round-type-specific state data

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  // Relations
  project  Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
  round    Round   @relation(fields: [roundId], references: [id], onDelete: Cascade)

  @@unique([projectId, roundId])
  @@index([projectId])
  @@index([roundId])
  @@index([state])
}

enum ProjectRoundStateValue {
  PENDING       // Entered round, awaiting action
  IN_PROGRESS   // Active (submission in progress, evaluation ongoing)
  PASSED        // Cleared this round, eligible to advance
  REJECTED      // Did not pass this round
  COMPLETED     // Round fully complete for this project
  WITHDRAWN     // Project withdrew
}
```

### AdvancementRule (replaces StageTransition + guardJson)

```prisma
model AdvancementRule {
  id              String @id @default(cuid())
  roundId         String       // The round this rule applies to (source round)
  targetRoundId   String?      // Where projects advance to (null = next round by sortOrder)

  ruleType        AdvancementRuleType
  configJson      Json   @db.JsonB   // Rule-type-specific config

  isDefault       Boolean @default(true)  // Default advancement path
  sortOrder       Int     @default(0)     // Priority when multiple rules exist

  createdAt DateTime @default(now())

  round       Round  @relation(fields: [roundId], references: [id], onDelete: Cascade)

  @@index([roundId])
}

enum AdvancementRuleType {
  AUTO_ADVANCE      // All PASSED projects advance to next round automatically
  SCORE_THRESHOLD   // Projects above score threshold advance
  TOP_N             // Top N projects per category advance
  ADMIN_SELECTION   // Admin manually selects who advances
  AI_RECOMMENDED    // AI suggests advancement, admin confirms
}
```

**AdvancementRule configJson shapes:**

```typescript
// AUTO_ADVANCE
{ trigger: "on_round_close" | "immediate" }

// SCORE_THRESHOLD
{ minScore: 7.0, metric: "average_global" | "weighted_criteria" }

// TOP_N
{
  perCategory: true,
  counts: { STARTUP: 10, BUSINESS_CONCEPT: 10 },
  tieBreaker: "admin_decides" | "highest_individual" | "revote"
}

// ADMIN_SELECTION
{ requireAIRecommendation: true, showRankings: true }

// AI_RECOMMENDED
{ topN: 10, confidenceThreshold: 0.7, requireAdminApproval: true }
```

---

## 3. Jury System

### JuryGroup

```prisma
model JuryGroup {
  id              String @id @default(cuid())
  competitionId   String
  name            String      // "Jury 1", "Jury 2", "Jury 3", "Innovation Award Jury"
  slug            String      // "jury-1", "jury-2"
  description     String?     @db.Text
  sortOrder       Int         @default(0)

  // Default assignment configuration for this jury
  defaultMaxAssignments    Int  @default(20)
  defaultCapMode           CapMode @default(SOFT)
  softCapBuffer            Int  @default(2)    // Extra assignments above cap for load balancing

  // Default category quotas (per juror)
  categoryQuotasEnabled    Boolean @default(false)
  defaultCategoryQuotas    Json?   @db.JsonB   // { "STARTUP": { "min": 2, "max": 15 }, "BUSINESS_CONCEPT": { "min": 2, "max": 15 } }

  // Onboarding: can jurors adjust their own cap/ratio during onboarding?
  allowJurorCapAdjustment     Boolean @default(false)
  allowJurorRatioAdjustment   Boolean @default(false)

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  // Relations
  competition Competition       @relation(fields: [competitionId], references: [id], onDelete: Cascade)
  members     JuryGroupMember[]
  rounds      Round[]           // Rounds this jury is assigned to
  assignments Assignment[]      // Assignments made through this jury group

  @@unique([competitionId, slug])
  @@index([competitionId])
}

enum CapMode {
  HARD    // Absolute maximum — AI/algorithm cannot exceed
  SOFT    // Target maximum — can exceed by softCapBuffer for load balancing
  NONE    // No cap — unlimited assignments
}
```

### JuryGroupMember

```prisma
model JuryGroupMember {
  id              String @id @default(cuid())
  juryGroupId     String
  userId          String
  isLead          Boolean @default(false)
  joinedAt        DateTime @default(now())

  // Per-juror overrides (null = use group defaults)
  maxAssignmentsOverride   Int?
  capModeOverride          CapMode?
  categoryQuotasOverride   Json?   @db.JsonB  // Same shape as JuryGroup.defaultCategoryQuotas

  // Juror preferences (set during onboarding)
  preferredStartupRatio    Float?  // 0.0 to 1.0 — desired % of startups (e.g., 0.6 = 60% startups)
  availabilityNotes        String? @db.Text

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  // Relations
  juryGroup JuryGroup @relation(fields: [juryGroupId], references: [id], onDelete: Cascade)
  user      User      @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([juryGroupId, userId])
  @@index([juryGroupId])
  @@index([userId])
}
```

---

## 4. Multi-Round Submission System

### SubmissionWindow

```prisma
model SubmissionWindow {
  id              String @id @default(cuid())
  competitionId   String
  name            String       // "Round 1 Application Docs", "Semi-finalist Additional Docs"
  slug            String       // "round-1-docs"
  roundNumber     Int          // 1, 2, 3... (sequential)
  sortOrder       Int @default(0)

  // Window timing
  windowOpenAt    DateTime?
  windowCloseAt   DateTime?

  // Deadline behavior
  deadlinePolicy  DeadlinePolicy @default(FLAG)
  graceHours      Int?           // Hours after windowCloseAt where late submissions accepted

  // Locking behavior
  lockOnClose     Boolean @default(true)  // Applicants can't edit after window closes

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  // Relations
  competition      Competition              @relation(fields: [competitionId], references: [id], onDelete: Cascade)
  fileRequirements SubmissionFileRequirement[]
  projectFiles     ProjectFile[]
  rounds           Round[]                  // Rounds that collect submissions for this window
  visibility       RoundSubmissionVisibility[]  // Which evaluation rounds can see these docs

  @@unique([competitionId, slug])
  @@unique([competitionId, roundNumber])
  @@index([competitionId])
}

enum DeadlinePolicy {
  HARD    // Submissions rejected after close
  FLAG    // Submissions accepted but marked late
  GRACE   // Grace period after close, then hard cutoff
}
```

### SubmissionFileRequirement

```prisma
model SubmissionFileRequirement {
  id                  String @id @default(cuid())
  submissionWindowId  String
  name                String           // "Executive Summary", "Business Plan", "Video Pitch"
  description         String?          @db.Text
  acceptedMimeTypes   String[]         // ["application/pdf", "video/*"]
  maxSizeMB           Int?             // Size limit
  isRequired          Boolean @default(true)
  sortOrder           Int     @default(0)

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  // Relations
  submissionWindow SubmissionWindow @relation(fields: [submissionWindowId], references: [id], onDelete: Cascade)
  files            ProjectFile[]    // Files uploaded against this requirement

  @@index([submissionWindowId])
}
```

### RoundSubmissionVisibility

Controls which submission windows a jury evaluation round can see.

```prisma
model RoundSubmissionVisibility {
  id                  String @id @default(cuid())
  roundId             String
  submissionWindowId  String

  canView             Boolean @default(true)   // Jury can see these docs
  displayLabel        String?                  // "Round 1 Docs", "Round 2 Docs" (shown to jury)

  // Relations
  round            Round            @relation(fields: [roundId], references: [id], onDelete: Cascade)
  submissionWindow SubmissionWindow @relation(fields: [submissionWindowId], references: [id], onDelete: Cascade)

  @@unique([roundId, submissionWindowId])
  @@index([roundId])
}
```

**Example usage:**

Jury 1 (Round 3) sees only Round 1 docs:
```
RoundSubmissionVisibility { roundId: round-3, submissionWindowId: sw-1, canView: true, displayLabel: "Application Docs" }
```

Jury 2 (Round 5) sees Round 1 AND Round 2 docs:
```
RoundSubmissionVisibility { roundId: round-5, submissionWindowId: sw-1, canView: true, displayLabel: "Round 1 Docs" }
RoundSubmissionVisibility { roundId: round-5, submissionWindowId: sw-2, canView: true, displayLabel: "Round 2 Docs" }
```

---

## 5. Mentoring Workspace

### MentorFile

```prisma
model MentorFile {
  id                   String @id @default(cuid())
  mentorAssignmentId   String
  uploadedByUserId     String

  fileName             String
  mimeType             String
  size                 Int
  bucket               String
  objectKey            String
  description          String?  @db.Text

  // Promotion to official submission
  isPromoted           Boolean @default(false)
  promotedToFileId     String?  @unique          // Links to the ProjectFile created on promotion
  promotedAt           DateTime?
  promotedByUserId     String?

  createdAt DateTime @default(now())

  // Relations
  mentorAssignment MentorAssignment @relation(fields: [mentorAssignmentId], references: [id], onDelete: Cascade)
  uploadedBy       User             @relation("MentorFileUploader", fields: [uploadedByUserId], references: [id])
  promotedBy       User?            @relation("MentorFilePromoter", fields: [promotedByUserId], references: [id])
  promotedFile     ProjectFile?     @relation("PromotedFromMentorFile", fields: [promotedToFileId], references: [id], onDelete: SetNull)
  comments         MentorFileComment[]

  @@index([mentorAssignmentId])
  @@index([uploadedByUserId])
}
```

### MentorFileComment

```prisma
model MentorFileComment {
  id           String @id @default(cuid())
  mentorFileId String
  authorId     String
  content      String @db.Text

  // Threading support
  parentCommentId String?     // null = top-level comment, non-null = reply

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  // Relations
  mentorFile    MentorFile         @relation(fields: [mentorFileId], references: [id], onDelete: Cascade)
  author        User               @relation("MentorFileCommentAuthor", fields: [authorId], references: [id])
  parentComment MentorFileComment? @relation("CommentThread", fields: [parentCommentId], references: [id], onDelete: Cascade)
  replies       MentorFileComment[] @relation("CommentThread")

  @@index([mentorFileId])
  @@index([authorId])
  @@index([parentCommentId])
}
```

### MentorAssignment Modifications

```prisma
model MentorAssignment {
  // ... existing fields preserved ...
  id                 String @id @default(cuid())
  projectId          String @unique
  mentorId           String
  method             AssignmentMethod @default(MANUAL)
  assignedAt         DateTime @default(now())
  assignedBy         String?
  aiConfidenceScore  Float?
  expertiseMatchScore Float?
  aiReasoning        String? @db.Text
  completionStatus   String  @default("in_progress")
  lastViewedAt       DateTime?

  // NEW: Workspace activation
  workspaceEnabled   Boolean @default(false)   // Activated when MENTORING round opens
  workspaceOpenAt    DateTime?                 // When mentoring files/chat becomes available
  workspaceCloseAt   DateTime?                 // When workspace access ends

  // Relations (existing + new)
  project   Project     @relation(fields: [projectId], references: [id], onDelete: Cascade)
  mentor    User        @relation("MentorAssignment", fields: [mentorId], references: [id])
  notes     MentorNote[]
  milestoneCompletions MentorMilestoneCompletion[]
  messages  MentorMessage[]
  files     MentorFile[]    // NEW: Workspace files
}
```

---

## 6. Winner Confirmation System

### WinnerProposal

```prisma
model WinnerProposal {
  id              String @id @default(cuid())
  competitionId   String
  category        CompetitionCategory   // STARTUP or BUSINESS_CONCEPT

  status          WinnerProposalStatus @default(PENDING)

  // Proposed rankings (ordered list of project IDs)
  rankedProjectIds String[]             // ["proj-1st", "proj-2nd", "proj-3rd"]

  // Selection basis (evidence)
  sourceRoundId   String               // Which round's scores/votes informed this
  selectionBasis  Json    @db.JsonB    // { method, scores, aiRecommendation, reasoning }

  // Proposer
  proposedById    String
  proposedAt      DateTime @default(now())

  // Finalization
  frozenAt        DateTime?
  frozenById      String?

  // Admin override (if used)
  overrideUsed    Boolean @default(false)
  overrideMode    String?              // "FORCE_MAJORITY" | "ADMIN_DECISION"
  overrideReason  String? @db.Text
  overrideById    String?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  // Relations
  competition  Competition      @relation(fields: [competitionId], references: [id], onDelete: Cascade)
  sourceRound  Round            @relation("WinnerProposalSource", fields: [sourceRoundId], references: [id])
  proposedBy   User             @relation("WinnerProposer", fields: [proposedById], references: [id])
  frozenBy     User?            @relation("WinnerFreezer", fields: [frozenById], references: [id])
  overrideBy   User?            @relation("WinnerOverrider", fields: [overrideById], references: [id])
  approvals    WinnerApproval[]

  @@index([competitionId])
  @@index([status])
}

enum WinnerProposalStatus {
  PENDING           // Waiting for jury approvals
  APPROVED          // All required approvals received
  REJECTED          // At least one rejection
  OVERRIDDEN        // Admin used override
  FROZEN            // Locked — official results
}
```

### WinnerApproval

```prisma
model WinnerApproval {
  id                 String @id @default(cuid())
  winnerProposalId   String
  userId             String
  role               WinnerApprovalRole

  // Response
  approved           Boolean?         // null = not yet responded
  comments           String?  @db.Text
  respondedAt        DateTime?

  createdAt DateTime @default(now())

  // Relations
  proposal WinnerProposal @relation(fields: [winnerProposalId], references: [id], onDelete: Cascade)
  user     User           @relation("WinnerApprovalUser", fields: [userId], references: [id])

  @@unique([winnerProposalId, userId])
  @@index([winnerProposalId])
  @@index([userId])
}

enum WinnerApprovalRole {
  JURY_MEMBER       // Must individually confirm
  ADMIN             // Final sign-off (or override)
}
```

---

## 7. Modified Existing Models

### ProjectFile — Add Submission Window Link

```prisma
model ProjectFile {
  id              String @id @default(cuid())
  projectId       String

  // CHANGED: Link to SubmissionWindow instead of legacy roundId
  submissionWindowId  String?
  requirementId       String?          // Links to SubmissionFileRequirement

  fileType        FileType
  fileName        String
  mimeType        String
  size            Int
  bucket          String
  objectKey       String

  isLate          Boolean  @default(false)
  version         Int      @default(1)
  replacedById    String?  @unique

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  // Relations
  project           Project                    @relation(fields: [projectId], references: [id], onDelete: Cascade)
  submissionWindow   SubmissionWindow?          @relation(fields: [submissionWindowId], references: [id], onDelete: SetNull)
  requirement       SubmissionFileRequirement?  @relation(fields: [requirementId], references: [id], onDelete: SetNull)
  replacedBy        ProjectFile?               @relation("FileVersion", fields: [replacedById], references: [id])
  previousVersion   ProjectFile?               @relation("FileVersion")
  promotedFrom      MentorFile?                @relation("PromotedFromMentorFile")  // NEW

  @@index([projectId])
  @@index([submissionWindowId])
  @@index([requirementId])
}
```

### Assignment — Add JuryGroup Link

```prisma
model Assignment {
  // ... all existing fields preserved ...
  id            String @id @default(cuid())
  userId        String
  projectId     String
  roundId       String           // RENAMED from stageId
  method        AssignmentMethod @default(MANUAL)
  isRequired    Boolean          @default(true)
  isCompleted   Boolean          @default(false)
  aiConfidenceScore    Float?
  expertiseMatchScore  Float?
  aiReasoning          String? @db.Text

  // NEW: Link to jury group
  juryGroupId   String?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  // Relations
  user        User        @relation(fields: [userId], references: [id], onDelete: Cascade)
  project     Project     @relation(fields: [projectId], references: [id], onDelete: Cascade)
  round       Round       @relation(fields: [roundId], references: [id], onDelete: Cascade)
  juryGroup   JuryGroup?  @relation(fields: [juryGroupId], references: [id], onDelete: SetNull)
  evaluation  Evaluation?
  coi         ConflictOfInterest?

  @@unique([userId, projectId, roundId])
  @@index([userId])
  @@index([projectId])
  @@index([roundId])
  @@index([juryGroupId])
}
```

### SpecialAward — Enhanced Standalone

```prisma
model SpecialAward {
  id              String @id @default(cuid())
  competitionId   String                    // CHANGED: Links to Competition, not Track
  name            String
  description     String? @db.Text
  criteriaText    String? @db.Text          // For AI eligibility

  // Award mode
  eligibilityMode  AwardEligibilityMode @default(STAY_IN_MAIN)

  // Scoring/voting
  scoringMode     AwardScoringMode @default(PICK_WINNER)
  maxRankedPicks  Int?                      // For RANKED mode

  // Decision
  decisionMode    String @default("JURY_VOTE")  // "JURY_VOTE" | "AWARD_MASTER_DECISION" | "ADMIN_DECISION"

  // Status
  status          AwardStatus @default(DRAFT)

  // Voting window
  votingStartAt   DateTime?
  votingEndAt     DateTime?

  // Runs alongside which evaluation round
  evaluationRoundId String?                 // NEW: Which round this award runs during

  // Jury (can be its own group or share a competition jury group)
  juryGroupId     String?                   // NEW: Dedicated or shared jury group

  // Winner
  winnerProjectId    String?
  winnerOverridden   Boolean @default(false)

  // AI
  useAiEligibility   Boolean @default(false)

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  // Relations
  competition      Competition        @relation(fields: [competitionId], references: [id], onDelete: Cascade)
  evaluationRound  Round?             @relation("AwardEvaluationRound", fields: [evaluationRoundId], references: [id], onDelete: SetNull)
  juryGroup        JuryGroup?         @relation("AwardJuryGroup", fields: [juryGroupId], references: [id], onDelete: SetNull)
  winnerProject    Project?           @relation("AwardWinner", fields: [winnerProjectId], references: [id], onDelete: SetNull)
  eligibilities    AwardEligibility[]
  jurors           AwardJuror[]
  votes            AwardVote[]

  @@index([competitionId])
  @@index([status])
  @@index([evaluationRoundId])
}

enum AwardEligibilityMode {
  SEPARATE_POOL    // Projects pulled out of main flow into award-only track
  STAY_IN_MAIN     // Projects remain in main competition, flagged as award-eligible
}

enum AwardStatus {
  DRAFT
  NOMINATIONS_OPEN
  VOTING_OPEN
  CLOSED
  ARCHIVED
}

enum AwardScoringMode {
  PICK_WINNER
  RANKED
  SCORED
}
```

### Evaluation — Rename stageId to roundId

```prisma
model Evaluation {
  // All fields preserved, stageId references updated to roundId via Assignment.roundId
  id              String @id @default(cuid())
  assignmentId    String @unique
  status          EvaluationStatus @default(NOT_STARTED)
  criterionScoresJson Json? @db.JsonB
  globalScore     Int?
  binaryDecision  Boolean?
  feedbackText    String? @db.Text
  submittedAt     DateTime?

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  assignment Assignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
}
```

### Project — Remove roundId, add competition link

```prisma
model Project {
  // ... existing fields ...
  id                String @id @default(cuid())
  programId         String
  competitionId     String?                // NEW: Direct link to competition

  // Remove legacy roundId field
  // roundId        String?  -- REMOVED

  status            ProjectStatus @default(SUBMITTED)
  title             String
  teamName          String?
  description       String? @db.Text
  competitionCategory CompetitionCategory?
  // ... all other existing fields preserved ...

  wantsMentorship   Boolean @default(false)  // Preserved — drives mentoring eligibility

  // Relations (updated)
  program           Program           @relation(fields: [programId], references: [id], onDelete: Cascade)
  competition       Competition?      @relation(fields: [competitionId], references: [id], onDelete: SetNull)
  files             ProjectFile[]
  teamMembers       TeamMember[]
  projectRoundStates ProjectRoundState[]   // RENAMED from projectStageStates
  assignments       Assignment[]
  mentorAssignment  MentorAssignment?
  // ... other existing relations preserved ...

  @@index([programId])
  @@index([competitionId])
  @@index([status])
  @@index([competitionCategory])
}
```

---

## 8. Round-Type Config Shapes (Zod-validated)

Each round type has a specific config shape stored in `Round.configJson`. These replace the old generic `configJson` approach with documented, validated structures.

### INTAKE Config

```typescript
type IntakeConfig = {
  // Submission behavior
  allowDrafts: boolean           // Allow saving drafts before submitting
  draftExpiryDays: number        // Auto-delete drafts after N days (default: 30)

  // What categories are accepted
  acceptedCategories: ("STARTUP" | "BUSINESS_CONCEPT")[]

  // Public form settings
  publicFormEnabled: boolean     // Allow anonymous form access via slug
  customFields: CustomFieldDef[] // Additional form fields beyond standard ones
}
```

### FILTERING Config

```typescript
type FilteringConfig = {
  // Rule engine
  rules: FilterRuleDef[]         // Field-based and document-check rules

  // AI screening
  aiScreeningEnabled: boolean
  aiCriteriaText: string         // Plain-language criteria for AI
  aiConfidenceThresholds: {
    high: number                 // Above this = auto-pass (default: 0.85)
    medium: number               // Above this = flag for review (default: 0.6)
    low: number                  // Below this = auto-reject (default: 0.4)
  }

  // Manual queue
  manualReviewEnabled: boolean   // Flagged projects need admin review

  // Batch processing
  batchSize: number              // Projects per AI batch (default: 20)
}
```

### EVALUATION Config

```typescript
type EvaluationConfig = {
  // Assignment settings (work with JuryGroup)
  requiredReviewsPerProject: number     // How many jurors review each project (default: 3)

  // Assignment caps are now on JuryGroup and JuryGroupMember
  // (no longer duplicated here)

  // Scoring
  scoringMode: "criteria" | "global" | "binary"  // How jurors score
  requireFeedback: boolean                        // Must submit text feedback

  // COI
  coiRequired: boolean          // Must declare COI before evaluating (default: true)

  // Peer review
  peerReviewEnabled: boolean    // Allow jurors to see anonymized peer evaluations
  anonymizationLevel: "fully_anonymous" | "show_initials" | "named"

  // AI features
  aiSummaryEnabled: boolean     // Generate AI evaluation summaries

  // Advancement (what happens after evaluation ends)
  advancementMode: "auto_top_n" | "admin_selection" | "ai_recommended"
  advancementConfig: {
    perCategory: boolean
    startupCount: number         // How many startups advance
    conceptCount: number         // How many concepts advance
    tieBreaker: "admin_decides" | "highest_individual" | "revote"
  }
}
```

### SUBMISSION Config

```typescript
type SubmissionConfig = {
  // Who can submit (based on status from previous round)
  eligibleStatuses: ProjectRoundStateValue[]  // Usually ["PASSED"]

  // Notification
  notifyEligibleTeams: boolean   // Email teams when window opens

  // Previous rounds become read-only for applicants
  lockPreviousWindows: boolean   // Default: true
}
```

### MENTORING Config

```typescript
type MentoringConfig = {
  // Who gets mentoring
  eligibility: "all_advancing" | "requested_only"  // All finalists or only those who requested

  // Workspace features
  chatEnabled: boolean
  fileUploadEnabled: boolean
  fileCommentsEnabled: boolean
  filePromotionEnabled: boolean     // Can promote files to official submissions

  // Target submission window for promotions
  promotionTargetWindowId: string?  // Which SubmissionWindow promoted files go to

  // Auto-assignment
  autoAssignMentors: boolean        // Use AI/algorithm to assign mentors
}
```

### LIVE_FINAL Config

```typescript
type LiveFinalConfig = {
  // Jury voting
  juryVotingEnabled: boolean
  votingMode: "simple" | "criteria"     // Simple 1-10 or criteria-based

  // Audience voting
  audienceVotingEnabled: boolean
  audienceVoteWeight: number            // 0.0 to 1.0 (weight vs jury vote)
  audienceVotingMode: "per_project" | "per_category" | "favorites"
  audienceMaxFavorites: number?         // For "favorites" mode
  audienceRequireIdentification: boolean

  // Deliberation
  deliberationEnabled: boolean
  deliberationDurationMinutes: number   // Length of deliberation period
  showAudienceVotesToJury: boolean      // Jury sees audience results during deliberation

  // Presentation
  presentationOrderMode: "manual" | "random" | "score_based"

  // Results reveal
  revealPolicy: "immediate" | "delayed" | "ceremony"
}
```

### CONFIRMATION Config

```typescript
type ConfirmationConfig = {
  // Approval requirements
  requireAllJuryApproval: boolean    // All jury members must individually confirm (default: true)
  juryGroupId: string?               // Which jury group must approve (usually Jury 3 / finals jury)

  // Admin override
  adminOverrideEnabled: boolean      // Admin can force result (default: true)
  overrideModes: ("FORCE_MAJORITY" | "ADMIN_DECISION")[]  // Available override options

  // Freeze behavior
  autoFreezeOnApproval: boolean      // Lock results immediately when all approve (default: true)

  // Per-category confirmation
  perCategory: boolean               // Separate confirmation per STARTUP vs BUSINESS_CONCEPT
}
```

---

## 9. Models Preserved As-Is (rename stageId -> roundId only)

These models are structurally unchanged. The only modification is renaming foreign keys from `stageId` to `roundId` and removing any `trackId` references:

| Model | Change |
|-------|--------|
| `EvaluationForm` | `stageId` -> `roundId` |
| `Evaluation` | No direct roundId (via Assignment) |
| `ConflictOfInterest` | No change (via Assignment) |
| `GracePeriod` | `stageId` -> `roundId` |
| `EvaluationSummary` | `stageId` -> `roundId` |
| `EvaluationDiscussion` | `stageId` -> `roundId` |
| `DiscussionComment` | No change |
| `FilteringRule` | `stageId` -> `roundId` |
| `FilteringResult` | `stageId` -> `roundId` |
| `FilteringJob` | `stageId` -> `roundId` |
| `LiveVotingSession` | `stageId` -> `roundId` |
| `LiveVote` | No change (via Session) |
| `AudienceVoter` | No change (via Session) |
| `LiveProgressCursor` | `stageId` -> `roundId` |
| `Cohort` | `stageId` -> `roundId` |
| `CohortProject` | No change |
| `AwardEligibility` | No change |
| `AwardJuror` | No change |
| `AwardVote` | No change |
| `MentorMessage` | No change |
| `MentorNote` | No change |
| `MentorMilestone` | No change |
| `MentorMilestoneCompletion` | No change |
| `InAppNotification` | No change |
| `AuditLog` | No change |
| `DecisionAuditLog` | No change |
| `User` | Add `juryGroupMemberships` relation |
| `Program` | Add `competitions` relation (was `pipelines`) |

---

## 10. User Model Additions

```prisma
model User {
  // ... all existing fields preserved ...

  // NEW relations
  juryGroupMemberships  JuryGroupMember[]
  mentorFileUploads     MentorFile[]          @relation("MentorFileUploader")
  mentorFilePromotions  MentorFile[]          @relation("MentorFilePromoter")
  mentorFileComments    MentorFileComment[]   @relation("MentorFileCommentAuthor")
  winnerProposals       WinnerProposal[]      @relation("WinnerProposer")
  winnerFreezes         WinnerProposal[]      @relation("WinnerFreezer")
  winnerOverrides       WinnerProposal[]      @relation("WinnerOverrider")
  winnerApprovals       WinnerApproval[]      @relation("WinnerApprovalUser")
}
```

---

## 11. Entity Relationship Summary

```
Program (1) ──── (N) Competition
Competition (1) ──── (N) Round
Competition (1) ──── (N) JuryGroup
Competition (1) ──── (N) SubmissionWindow
Competition (1) ──── (N) SpecialAward
Competition (1) ──── (N) WinnerProposal

Round (N) ──── (1) JuryGroup (optional — for EVALUATION and LIVE_FINAL rounds)
Round (N) ──── (1) SubmissionWindow (optional — for INTAKE and SUBMISSION rounds)
Round (1) ──── (N) RoundSubmissionVisibility ──── (N) SubmissionWindow

JuryGroup (1) ──── (N) JuryGroupMember ──── (1) User
JuryGroup (1) ──── (N) Assignment

SubmissionWindow (1) ──── (N) SubmissionFileRequirement
SubmissionWindow (1) ──── (N) ProjectFile

Project (1) ──── (N) ProjectRoundState ──── (1) Round
Project (1) ──── (N) Assignment ──── (1) Evaluation
Project (1) ──── (N) ProjectFile
Project (1) ──── (0..1) MentorAssignment ──── (N) MentorFile ──── (N) MentorFileComment

SpecialAward (N) ──── (1) JuryGroup (optional)
SpecialAward (N) ──── (1) Round (evaluationRound — runs alongside)

WinnerProposal (1) ──── (N) WinnerApproval
```

---

## 12. Migration Strategy (High-Level)

### Phase 1: Add new tables (non-breaking)
1. Create `Competition` table
2. Create `Round` table
3. Create `JuryGroup`, `JuryGroupMember` tables
4. Create `SubmissionWindow`, `SubmissionFileRequirement`, `RoundSubmissionVisibility` tables
5. Create `MentorFile`, `MentorFileComment` tables
6. Create `WinnerProposal`, `WinnerApproval` tables
7. Create `AdvancementRule` table
8. Create `ProjectRoundState` table
9. Add new columns to `Assignment` (juryGroupId), `ProjectFile` (submissionWindowId), `MentorAssignment` (workspace fields), `SpecialAward` (competitionId, eligibilityMode, juryGroupId, evaluationRoundId), `Project` (competitionId)

### Phase 2: Data migration
1. For each Pipeline: Create a Competition record
2. For each Stage in the MAIN Track: Create a Round record (maintaining sortOrder)
3. For each ProjectStageState: Create a ProjectRoundState record (dropping trackId)
4. For each AWARD Track: Migrate to SpecialAward (link to Competition + evaluation round)
5. For existing FileRequirements: Create SubmissionWindow + SubmissionFileRequirement
6. For existing Assignments with stageId: Update roundId reference
7. Create default JuryGroups from existing assignments (group by stageId)

### Phase 3: Code migration
1. Update all services (stageId -> roundId, remove trackId references)
2. Update all routers (rename, new endpoints)
3. Update all UI (new pages, enhanced existing pages)

### Phase 4: Drop old tables (after verification)
1. Drop `Track` table
2. Drop `StageTransition` table
3. Drop `ProjectStageState` table (after verifying ProjectRoundState)
4. Drop `Stage` table (after verifying Round)
5. Drop `Pipeline` table (after verifying Competition)
6. Clean up old enums

Detailed migration SQL will be in `21-migration-strategy.md`.
