MOPC-App/docs/unified-architecture-redesign/02-data-model.md

1802 lines
55 KiB
Markdown

# 02. Data Model
## Overview
This document defines the complete Prisma schema and Zod configuration contracts for the MOPC platform redesign. The data model uses **Competition** and **Round** terminology (replacing Pipeline/Stage/Track), implements a 5-layer policy precedence system, and introduces new models for deliberation, mentoring workspace, result locking, and assignment governance.
**Key Design Principles:**
- Competition-centric flow (no intermediate Track layer for main competition)
- Round-type-specific typed configurations (Zod-validated)
- First-class Jury Group entities with per-member policy overrides
- Multi-round submission windows with explicit file requirements
- Deliberation system for final winner confirmation (replaces WinnerProposal)
- Result locking with audit trail
- Mentoring workspace with file promotion to official submissions
---
## 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) DeliberationSession
Round (N) ──── (1) JuryGroup (optional — for EVALUATION, LIVE_FINAL, DELIBERATION)
Round (N) ──── (1) SubmissionWindow (optional — for INTAKE, SUBMISSION)
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
──── (N) MentorMessage
SpecialAward (N) ──── (1) JuryGroup (optional)
SpecialAward (N) ──── (1) Round (evaluationRound — runs alongside)
DeliberationSession (1) ──── (N) DeliberationVote
DeliberationSession (1) ──── (N) DeliberationResult
DeliberationSession (1) ──── (N) DeliberationParticipant
ResultLock (1) ──── (N) ResultUnlockEvent
```
---
## Core Models
### 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[]
deliberationSessions DeliberationSession[]
resultLocks ResultLock[]
@@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
// Optional analytics tag — semantic label for grouping/reporting (e.g., "jury1_selection", "semifinal_docs")
purposeKey String? // Free-form, not an enum — allows flexible analytics without schema changes
// Links to other entities
juryGroupId String? // Which jury evaluates this round (EVALUATION, LIVE_FINAL, DELIBERATION)
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?
advancementRules AdvancementRule[]
resultLocks ResultLock[]
// 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
DELIBERATION // Winner confirmation — jury consensus + admin approval
}
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)
```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 }
```
### Program
```prisma
model Program {
id String @id @default(cuid())
name String // e.g., "Monaco Ocean Protection Challenge"
slug String? @unique // URL-friendly identifier
year Int // e.g., 2026
status ProgramStatus @default(DRAFT)
description String?
settingsJson Json? @db.JsonB
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
projects Project[]
learningResources LearningResource[]
partners Partner[]
specialAwards SpecialAward[]
taggingJobs TaggingJob[]
wizardTemplates WizardTemplate[]
mentorMilestones MentorMilestone[]
competitions Competition[]
@@unique([name, year])
@@index([status])
}
enum ProgramStatus {
DRAFT
ACTIVE
ARCHIVED
}
```
---
## Jury & Assignment Models
### 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
awards SpecialAward[] // Awards using this jury
@@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
role JuryGroupMemberRole @default(MEMBER) // CHAIR (lead), MEMBER, OBSERVER (view-only)
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)
assignmentIntents AssignmentIntent[]
deliberationVotes DeliberationVote[]
deliberationParticipations DeliberationParticipant[]
@@unique([juryGroupId, userId])
@@index([juryGroupId])
@@index([userId])
}
```
### Assignment (modified)
```prisma
model Assignment {
id String @id @default(cuid())
userId String
projectId String
roundId String
// Assignment info
method AssignmentMethod @default(MANUAL)
isRequired Boolean @default(true)
isCompleted Boolean @default(false)
// AI assignment metadata
aiConfidenceScore Float?
expertiseMatchScore Float?
aiReasoning String? @db.Text
// Jury group link
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?
conflictOfInterest ConflictOfInterest?
exceptions AssignmentException[]
@@unique([userId, projectId, roundId])
@@index([userId])
@@index([projectId])
@@index([roundId])
@@index([juryGroupId])
@@index([isCompleted])
}
enum AssignmentMethod {
MANUAL
BULK
AI_SUGGESTED
AI_AUTO
ALGORITHM
}
```
### AssignmentIntent
```prisma
model AssignmentIntent {
id String @id @default(cuid())
juryGroupMemberId String
roundId String
projectId String
source AssignmentIntentSource // How this intent was created
status AssignmentIntentStatus @default(PENDING) // Lifecycle: PENDING → HONORED/OVERRIDDEN/EXPIRED
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
juryGroupMember JuryGroupMember @relation(fields: [juryGroupMemberId], references: [id], onDelete: Cascade)
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@unique([juryGroupMemberId, roundId, projectId])
@@index([roundId])
@@index([projectId])
@@index([status])
}
enum AssignmentIntentSource {
INVITE // Created during member invite (pre-assignment)
ADMIN // Admin-created intent
SYSTEM // System-generated (e.g., from algorithm)
}
enum AssignmentIntentStatus {
PENDING // Created, awaiting assignment algorithm execution
HONORED // Assignment algorithm materialized this intent into an Assignment record
OVERRIDDEN // Admin changed the assignment, superseding this intent
EXPIRED // Round completed without this intent being honored
CANCELLED // Explicitly cancelled by admin or system
}
```
### AssignmentException
```prisma
model AssignmentException {
id String @id @default(cuid())
assignmentId String
reason String @db.Text // Why exception was granted
overCapBy Int // How many assignments over cap
approvedById String
createdAt DateTime @default(now())
// Relations
assignment Assignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
approvedBy User @relation("AssignmentExceptionApprover", fields: [approvedById], references: [id])
@@index([assignmentId])
@@index([approvedById])
}
```
---
## Submission & Document Models
### 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
isLocked Boolean @default(false) // Manual lock toggle — admin can lock/unlock independently of window close
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
```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])
}
```
### ProjectFile (modified)
```prisma
model ProjectFile {
id String @id @default(cuid())
projectId String
// 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")
@@index([projectId])
@@index([submissionWindowId])
@@index([requirementId])
}
enum FileType {
EXEC_SUMMARY
PRESENTATION
VIDEO
OTHER
BUSINESS_PLAN
VIDEO_PITCH
SUPPORTING_DOC
}
```
### MentorFile (NEW)
```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[]
promotionEvents SubmissionPromotionEvent[]
@@index([mentorAssignmentId])
@@index([uploadedByUserId])
}
```
### MentorFileComment (NEW)
```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])
}
```
### MentorMessage (NEW)
```prisma
model MentorMessage {
id String @id @default(cuid())
workspaceId String // Links to MentorAssignment
senderId String
senderRole MentorMessageRole // MENTOR | APPLICANT | ADMIN
content String @db.Text
createdAt DateTime @default(now())
// Relations
workspace MentorAssignment @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
sender User @relation("MentorMessageSender", fields: [senderId], references: [id])
@@index([workspaceId])
@@index([senderId])
@@index([createdAt])
}
enum MentorMessageRole {
MENTOR
APPLICANT
ADMIN
}
```
### SubmissionPromotionEvent (NEW)
```prisma
model SubmissionPromotionEvent {
id String @id @default(cuid())
projectId String
roundId String
slotKey String // Which requirement slot this promoted file fills
sourceType SubmissionPromotionSource
sourceFileId String? // MentorFile ID if promoted from mentoring workspace
promotedById String
createdAt DateTime @default(now())
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
sourceFile MentorFile? @relation(fields: [sourceFileId], references: [id], onDelete: SetNull)
promotedBy User @relation("SubmissionPromoter", fields: [promotedById], references: [id])
@@index([projectId])
@@index([roundId])
@@index([sourceFileId])
}
enum SubmissionPromotionSource {
MENTOR_FILE // Promoted from mentoring workspace
ADMIN_REPLACEMENT // Admin directly replaced official submission
}
```
### MentorAssignment (modified)
```prisma
model MentorAssignment {
id String @id @default(cuid())
projectId String @unique // One mentor per project
mentorId String // User with MENTOR role or expertise
// Assignment tracking
method AssignmentMethod @default(MANUAL)
assignedAt DateTime @default(now())
assignedBy String?
// AI assignment metadata
aiConfidenceScore Float?
expertiseMatchScore Float?
aiReasoning String? @db.Text
// Tracking
completionStatus String @default("in_progress") // 'in_progress' | 'completed' | 'paused'
lastViewedAt DateTime?
// 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
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[]
@@index([mentorId])
@@index([method])
}
```
---
## Evaluation & Scoring Models
### Evaluation
```prisma
model Evaluation {
id String @id @default(cuid())
assignmentId String @unique
formId String
status EvaluationStatus @default(NOT_STARTED)
// Scores
criterionScoresJson Json? @db.JsonB
globalScore Int?
binaryDecision Boolean?
feedbackText String? @db.Text
version Int @default(1)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
submittedAt DateTime?
// Relations
assignment Assignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
form EvaluationForm @relation(fields: [formId], references: [id], onDelete: Cascade)
@@index([status])
@@index([submittedAt])
@@index([formId])
}
enum EvaluationStatus {
NOT_STARTED
DRAFT
SUBMITTED
LOCKED
}
```
### EvaluationForm
```prisma
model EvaluationForm {
id String @id @default(cuid())
roundId String
version Int @default(1)
// Form configuration
criteriaJson Json @db.JsonB // Array of { id, label, description, scale, weight, required }
scalesJson Json? @db.JsonB // { "1-5": { min, max, labels }, "1-10": { min, max, labels } }
isActive Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
evaluations Evaluation[]
@@unique([roundId, version])
@@index([roundId, isActive])
}
```
### ConflictOfInterest
```prisma
model ConflictOfInterest {
id String @id @default(cuid())
assignmentId String @unique
userId String
projectId String
hasConflict Boolean @default(false)
conflictType String? // "financial", "personal", "organizational", "other"
description String? @db.Text
declaredAt DateTime @default(now())
// Admin review
reviewedById String?
reviewedAt DateTime?
reviewAction String? // "cleared", "reassigned", "noted"
// Relations
assignment Assignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id])
reviewedBy User? @relation("COIReviewedBy", fields: [reviewedById], references: [id])
@@index([userId])
@@index([hasConflict])
}
```
### EvaluationSummary
```prisma
model EvaluationSummary {
id String @id @default(cuid())
projectId String
roundId String
summaryJson Json @db.JsonB
generatedAt DateTime @default(now())
generatedById String
model String
tokensUsed Int
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
generatedBy User @relation("EvaluationSummaryGeneratedBy", fields: [generatedById], references: [id])
@@unique([projectId, roundId])
@@index([roundId])
}
```
---
## Deliberation Models (NEW - replaces WinnerProposal/WinnerApproval)
### DeliberationSession
```prisma
model DeliberationSession {
id String @id @default(cuid())
competitionId String
roundId String
category ProjectCategory
mode DeliberationMode // SINGLE_WINNER_VOTE | FULL_RANKING
showCollectiveRankings Boolean @default(false)
showPriorJuryData Boolean @default(false)
status DeliberationStatus // OPEN | VOTING | TALLYING | RUNOFF | LOCKED
tieBreakMethod TieBreakMethod // RUNOFF | ADMIN_DECIDES | SCORE_FALLBACK
adminOverrideResult Json? @db.JsonB
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
votes DeliberationVote[]
results DeliberationResult[]
participants DeliberationParticipant[]
@@index([competitionId])
@@index([roundId])
@@index([status])
}
enum DeliberationMode {
SINGLE_WINNER_VOTE // Each jury member votes for one winner
FULL_RANKING // Each jury member ranks all projects
}
enum DeliberationStatus {
OPEN // Session created, waiting for votes
VOTING // Active voting window
TALLYING // Counting votes
RUNOFF // Tie detected, runoff voting in progress
LOCKED // Results finalized, no more changes
}
enum TieBreakMethod {
RUNOFF // Create new voting round for tied projects
ADMIN_DECIDES // Admin manually breaks tie
SCORE_FALLBACK // Use previous round scores to break tie
}
enum ProjectCategory {
STARTUP // Existing companies
BUSINESS_CONCEPT // Students/graduates
}
```
### DeliberationVote
```prisma
model DeliberationVote {
id String @id @default(cuid())
sessionId String
juryMemberId String
projectId String
rank Int? // ordinal rank in FULL_RANKING mode
isWinnerPick Boolean @default(false) // true in SINGLE_WINNER_VOTE mode
runoffRound Int @default(0) // 0 = initial, 1+ = runoff rounds
createdAt DateTime @default(now())
// Relations
session DeliberationSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
juryMember JuryGroupMember @relation(fields: [juryMemberId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@unique([sessionId, juryMemberId, projectId, runoffRound])
@@index([sessionId])
@@index([juryMemberId])
@@index([projectId])
}
```
### DeliberationResult
```prisma
model DeliberationResult {
id String @id @default(cuid())
sessionId String
projectId String
finalRank Int
voteCount Int @default(0)
isAdminOverridden Boolean @default(false)
overrideReason String? @db.Text
// Relations
session DeliberationSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@unique([sessionId, projectId])
@@index([sessionId])
@@index([projectId])
}
```
### DeliberationParticipant
```prisma
model DeliberationParticipant {
id String @id @default(cuid())
sessionId String
userId String
status DeliberationParticipantStatus // REQUIRED | ABSENT_EXCUSED | REPLACED | REPLACEMENT_ACTIVE
replacedById String?
// Relations
session DeliberationSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
user JuryGroupMember @relation(fields: [userId], references: [id], onDelete: Cascade)
replacedBy User? @relation("DeliberationReplacement", fields: [replacedById], references: [id])
@@unique([sessionId, userId])
@@index([sessionId])
@@index([userId])
}
enum DeliberationParticipantStatus {
REQUIRED // Expected to participate
ABSENT_EXCUSED // Absent with permission
REPLACED // Original member replaced
REPLACEMENT_ACTIVE // Acting as replacement
}
```
---
## Result & Audit Models
### ResultLock (NEW)
```prisma
model ResultLock {
id String @id @default(cuid())
competitionId String
roundId String
category ProjectCategory
lockedById String
resultSnapshot Json @db.JsonB // Frozen result data
lockedAt DateTime @default(now())
// Relations
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
lockedBy User @relation("ResultLockCreator", fields: [lockedById], references: [id])
unlockEvents ResultUnlockEvent[]
@@index([competitionId])
@@index([roundId])
@@index([category])
}
```
### ResultUnlockEvent (NEW)
```prisma
model ResultUnlockEvent {
id String @id @default(cuid())
resultLockId String
unlockedById String
reason String @db.Text
unlockedAt DateTime @default(now())
// Relations
resultLock ResultLock @relation(fields: [resultLockId], references: [id], onDelete: Cascade)
unlockedBy User @relation("ResultUnlocker", fields: [unlockedById], references: [id])
@@index([resultLockId])
@@index([unlockedById])
}
```
### DecisionAuditLog
```prisma
model DecisionAuditLog {
id String @id @default(cuid())
eventType String // round.transitioned, routing.executed, filtering.completed, etc.
entityType String
entityId String
actorId String?
detailsJson Json? @db.JsonB
snapshotJson Json? @db.JsonB // State at time of decision
createdAt DateTime @default(now())
@@index([eventType])
@@index([entityType, entityId])
@@index([actorId])
@@index([createdAt])
}
```
**Audit `detailsJson` convention**: All admin override events MUST include `beforeState` and `afterState` fields for diff tracking:
```typescript
// Example: admin overrides eligibility
{
action: "override_eligibility",
beforeState: { eligible: false, aiConfidence: 0.35 },
afterState: { eligible: true },
reason: "Project meets criteria despite low AI confidence — manual review confirms eligibility"
}
// Example: admin overrides assignment
{
action: "override_assignment",
beforeState: { assignedToUserId: "user-a", method: "ALGORITHM" },
afterState: { assignedToUserId: "user-b", method: "MANUAL" },
reason: "Reassigned due to scheduling conflict"
}
```
This convention enables:
- Automated diff reports for compliance
- Undo/revert capability for admin overrides
- Before/after comparison in audit UI
---
## Special Award Models
### SpecialAward (modified)
```prisma
model SpecialAward {
id String @id @default(cuid())
competitionId String // Links to Competition
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?
// Jury (can be its own group or share a competition jury group)
juryGroupId String?
// 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(fields: [evaluationRoundId], references: [id], onDelete: SetNull)
juryGroup JuryGroup? @relation(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
}
```
### AwardEligibility
```prisma
model AwardEligibility {
id String @id @default(cuid())
awardId String
projectId String
method EligibilityMethod @default(AUTO)
eligible Boolean @default(false)
aiReasoningJson Json? @db.JsonB
// Admin override
overriddenBy String?
overriddenAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
award SpecialAward @relation(fields: [awardId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
overriddenByUser User? @relation("AwardEligibilityOverriddenBy", fields: [overriddenBy], references: [id], onDelete: SetNull)
@@unique([awardId, projectId])
@@index([awardId])
@@index([projectId])
@@index([eligible])
}
enum EligibilityMethod {
AUTO
MANUAL
}
```
### AwardJuror
```prisma
model AwardJuror {
id String @id @default(cuid())
awardId String
userId String
createdAt DateTime @default(now())
// Relations
award SpecialAward @relation(fields: [awardId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([awardId, userId])
@@index([awardId])
@@index([userId])
}
```
### AwardVote
```prisma
model AwardVote {
id String @id @default(cuid())
awardId String
userId String
projectId String
rank Int? // For RANKED mode
votedAt DateTime @default(now())
// Relations
award SpecialAward @relation(fields: [awardId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@unique([awardId, userId, projectId])
@@index([awardId])
@@index([userId])
@@index([projectId])
}
```
---
## Policy & Override Models
### 5-Layer Policy Precedence
Policy resolution follows this precedence order (highest to lowest):
| Layer | Description | Example Use Case |
|-------|-------------|------------------|
| **1. Admin Override** | Explicit override action with mandatory reason | Admin grants exception to assign 25 projects to a jury member with cap of 20 |
| **2. Per-Member Override** | Individual jury member policy settings | Jury member sets personal cap to 15 instead of group default 20 |
| **3. Jury Group Default** | JuryGroup-level configuration | Jury 1 has defaultMaxAssignments=20, softCapBuffer=2 |
| **4. Program Default** | Program-wide settings in Program.settingsJson | All competitions in MOPC 2026 default to 3 finalists per category |
| **5. System Default** | Platform-wide hardcoded defaults | Default CapMode is SOFT if not specified |
**Resolution Logic:**
- Each policy query returns: `{ value, source: "admin_override" | "member" | "jury_group" | "program" | "system", explanation: string }`
- UI shows policy source and allows drilling into why a value was chosen
- All policy evaluations are logged in DecisionAuditLog
### Policy Models
Policy configuration is embedded in existing models:
**System Defaults** → Hardcoded in application config
**Program Defaults**`Program.settingsJson`
**Jury Group Defaults**`JuryGroup.defaultMaxAssignments`, `JuryGroup.defaultCapMode`, etc.
**Per-Member Overrides**`JuryGroupMember.maxAssignmentsOverride`, `JuryGroupMember.capModeOverride`, etc.
**Admin Overrides**`AssignmentException`, `FilteringResult.overriddenBy`, `AwardEligibility.overriddenBy`
---
## Enum Definitions
```prisma
enum CompetitionStatus {
DRAFT
ACTIVE
CLOSED
ARCHIVED
}
enum RoundType {
INTAKE
FILTERING
EVALUATION
SUBMISSION
MENTORING
LIVE_FINAL
DELIBERATION
}
enum RoundStatus {
ROUND_DRAFT
ROUND_ACTIVE
ROUND_CLOSED
ROUND_ARCHIVED
}
enum ProjectRoundStateValue {
PENDING
IN_PROGRESS
PASSED
REJECTED
COMPLETED
WITHDRAWN
}
enum AdvancementRuleType {
AUTO_ADVANCE
SCORE_THRESHOLD
TOP_N
ADMIN_SELECTION
AI_RECOMMENDED
}
enum CapMode {
HARD
SOFT
NONE
}
enum AssignmentMethod {
MANUAL
BULK
AI_SUGGESTED
AI_AUTO
ALGORITHM
}
enum AssignmentIntentSource {
INVITE
ADMIN
SYSTEM
}
enum AssignmentIntentStatus {
PENDING
HONORED
OVERRIDDEN
EXPIRED
CANCELLED
}
enum JuryGroupMemberRole {
CHAIR // Jury lead — can manage session, see aggregate data
MEMBER // Regular juror — votes, scores, provides feedback
OBSERVER // View-only — can see evaluations/deliberations but cannot vote
}
enum DeadlinePolicy {
HARD
FLAG
GRACE
}
enum FileType {
EXEC_SUMMARY
PRESENTATION
VIDEO
OTHER
BUSINESS_PLAN
VIDEO_PITCH
SUPPORTING_DOC
}
enum MentorMessageRole {
MENTOR
APPLICANT
ADMIN
}
enum SubmissionPromotionSource {
MENTOR_FILE
ADMIN_REPLACEMENT
}
enum EvaluationStatus {
NOT_STARTED
DRAFT
SUBMITTED
LOCKED
}
enum DeliberationMode {
SINGLE_WINNER_VOTE
FULL_RANKING
}
enum DeliberationStatus {
OPEN
VOTING
TALLYING
RUNOFF
LOCKED
}
enum TieBreakMethod {
RUNOFF
ADMIN_DECIDES
SCORE_FALLBACK
}
enum DeliberationParticipantStatus {
REQUIRED
ABSENT_EXCUSED
REPLACED
REPLACEMENT_ACTIVE
}
enum ProjectCategory {
STARTUP
BUSINESS_CONCEPT
}
enum AwardEligibilityMode {
SEPARATE_POOL
STAY_IN_MAIN
}
enum AwardStatus {
DRAFT
NOMINATIONS_OPEN
VOTING_OPEN
CLOSED
ARCHIVED
}
enum AwardScoringMode {
PICK_WINNER
RANKED
SCORED
}
enum EligibilityMethod {
AUTO
MANUAL
}
```
---
## Typed Config Schemas
Each RoundType has a specific Zod-validated config schema stored in `Round.configJson`.
### 1. IntakeConfig
```typescript
import { z } from 'zod';
export const IntakeConfigSchema = z.object({
// Submission behavior
allowDrafts: z.boolean().default(true),
draftExpiryDays: z.number().int().positive().default(30),
// Accepted categories
acceptedCategories: z.array(z.enum(['STARTUP', 'BUSINESS_CONCEPT'])).default(['STARTUP', 'BUSINESS_CONCEPT']),
// File constraints
maxFileSize: z.number().int().positive().default(50), // MB per file
allowedFileTypes: z.array(z.string()).default(['application/pdf']),
maxFilesPerSlot: z.number().int().positive().default(1),
// Late submission notification
lateSubmissionNotification: z.boolean().default(true), // Notify admin when late submissions arrive
// File constraints (applied to all slots in this intake window)
maxFileSizeMB: z.number().int().positive().default(50),
maxFilesPerSlot: z.number().int().positive().default(1),
allowedMimeTypes: z.array(z.string()).default(['application/pdf']),
// Late submission notification
lateSubmissionNotification: z.boolean().default(true), // Notify admin when late submissions arrive (FLAG policy)
// Public form settings
publicFormEnabled: z.boolean().default(false),
customFields: z.array(z.object({
id: z.string(),
label: z.string(),
type: z.enum(['text', 'textarea', 'select', 'checkbox', 'date']),
required: z.boolean().default(false),
options: z.array(z.string()).optional(),
})).default([]),
});
export type IntakeConfig = z.infer<typeof IntakeConfigSchema>;
```
### 2. FilteringConfig
```typescript
export const FilteringConfigSchema = z.object({
// Rule engine
rules: z.array(z.object({
id: z.string(),
name: z.string(),
ruleType: z.enum(['FIELD_BASED', 'DOCUMENT_CHECK']),
conditions: z.any(), // Rule-specific conditions
action: z.enum(['PASS', 'REJECT', 'FLAG']),
})).default([]),
// AI screening
aiScreeningEnabled: z.boolean().default(true),
aiCriteriaText: z.string().optional(),
aiConfidenceThresholds: z.object({
high: z.number().min(0).max(1).default(0.85), // Auto-pass
medium: z.number().min(0).max(1).default(0.6), // Flag for review
low: z.number().min(0).max(1).default(0.4), // Auto-reject
}).default({
high: 0.85,
medium: 0.6,
low: 0.4,
}),
// Manual queue
manualReviewEnabled: z.boolean().default(true),
// Auto-advancement
autoAdvanceEligible: z.boolean().default(false), // Passing projects auto-advance without admin review
// Duplicate detection
duplicateDetectionEnabled: z.boolean().default(true), // Flag potential duplicate submissions
// Auto-advance eligible projects (skip admin review for clear passes)
autoAdvanceEligible: z.boolean().default(false),
// Duplicate detection
duplicateDetectionEnabled: z.boolean().default(true),
// Batch processing
batchSize: z.number().int().positive().default(20),
});
export type FilteringConfig = z.infer<typeof FilteringConfigSchema>;
```
### 3. EvaluationConfig
```typescript
export const EvaluationConfigSchema = z.object({
// Assignment settings
requiredReviewsPerProject: z.number().int().positive().default(3),
// Scoring
scoringMode: z.enum(['criteria', 'global', 'binary']).default('criteria'),
requireFeedback: z.boolean().default(true),
feedbackMinLength: z.number().int().nonnegative().default(0), // 0 = no minimum
requireAllCriteriaScored: z.boolean().default(true), // Block submission if any criterion is blank
// COI
coiRequired: z.boolean().default(true),
// Feedback
feedbackMinLength: z.number().int().nonnegative().default(0), // 0 = no minimum
requireAllCriteriaScored: z.boolean().default(true),
// Peer review
peerReviewEnabled: z.boolean().default(false),
anonymizationLevel: z.enum(['fully_anonymous', 'show_initials', 'named']).default('fully_anonymous'),
// AI features
aiSummaryEnabled: z.boolean().default(false),
generateAiShortlist: z.boolean().default(true), // AI-ranked shortlist per category at round end
generateAiShortlist: z.boolean().default(false), // Generate AI-ranked shortlist at round end
// Advancement
advancementMode: z.enum(['auto_top_n', 'admin_selection', 'ai_recommended']).default('admin_selection'),
advancementConfig: z.object({
perCategory: z.boolean().default(true),
startupCount: z.number().int().nonnegative().default(10),
conceptCount: z.number().int().nonnegative().default(10),
tieBreaker: z.enum(['admin_decides', 'highest_individual', 'revote']).default('admin_decides'),
}).optional(),
});
export type EvaluationConfig = z.infer<typeof EvaluationConfigSchema>;
```
### 4. SubmissionConfig
```typescript
export const SubmissionConfigSchema = z.object({
// Who can submit
eligibleStatuses: z.array(z.enum(['PENDING', 'IN_PROGRESS', 'PASSED', 'REJECTED', 'COMPLETED', 'WITHDRAWN'])).default(['PASSED']),
// Notification
notifyEligibleTeams: z.boolean().default(true),
// Lock previous rounds
lockPreviousWindows: z.boolean().default(true),
});
export type SubmissionConfig = z.infer<typeof SubmissionConfigSchema>;
```
### 5. MentoringConfig
```typescript
export const MentoringConfigSchema = z.object({
// Eligibility
eligibility: z.enum(['all_advancing', 'requested_only']).default('requested_only'),
// Workspace features
chatEnabled: z.boolean().default(true),
fileUploadEnabled: z.boolean().default(true),
fileCommentsEnabled: z.boolean().default(true),
filePromotionEnabled: z.boolean().default(true),
// Promotion target
promotionTargetWindowId: z.string().optional(),
// Auto-assignment
autoAssignMentors: z.boolean().default(false),
});
export type MentoringConfig = z.infer<typeof MentoringConfigSchema>;
```
### 6. LiveFinalConfig
```typescript
export const LiveFinalConfigSchema = z.object({
// Jury voting
juryVotingEnabled: z.boolean().default(true),
votingMode: z.enum(['simple', 'criteria']).default('simple'),
// Audience voting
audienceVotingEnabled: z.boolean().default(false),
audienceVoteWeight: z.number().min(0).max(1).default(0),
audienceVotingMode: z.enum(['per_project', 'per_category', 'favorites']).default('per_project'),
audienceMaxFavorites: z.number().int().positive().default(3),
audienceRequireIdentification: z.boolean().default(false),
// Deliberation
deliberationEnabled: z.boolean().default(false),
deliberationDurationMinutes: z.number().int().positive().default(30),
showAudienceVotesToJury: z.boolean().default(false),
// Presentation
presentationOrderMode: z.enum(['manual', 'random', 'score_based']).default('manual'),
presentationDurationMinutes: z.number().int().positive().default(15), // Per-project presentation time
qaDurationMinutes: z.number().int().nonnegative().default(5), // Per-project Q&A time (0 = no Q&A)
// Results reveal
revealPolicy: z.enum(['immediate', 'delayed', 'ceremony']).default('ceremony'),
});
export type LiveFinalConfig = z.infer<typeof LiveFinalConfigSchema>;
```
### 7. DeliberationConfig
```typescript
export const DeliberationConfigSchema = z.object({
// Jury group
juryGroupId: z.string(),
// Mode
mode: z.enum(['SINGLE_WINNER_VOTE', 'FULL_RANKING']).default('SINGLE_WINNER_VOTE'),
// Display
showCollectiveRankings: z.boolean().default(false),
showPriorJuryData: z.boolean().default(false), // Show Jury 1/2 scores/feedback to deliberation jury
// Tie handling
tieBreakMethod: z.enum(['RUNOFF', 'ADMIN_DECIDES', 'SCORE_FALLBACK']).default('ADMIN_DECIDES'),
// Timing
votingDuration: z.number().int().positive().default(60), // minutes
// Winner count
topN: z.number().int().positive().default(3), // How many winners per category (podium size)
// Admin control
allowAdminOverride: z.boolean().default(true),
});
export type DeliberationConfig = z.infer<typeof DeliberationConfigSchema>;
```
---
## Migration Notes
### New Models Added
- **Competition** — Replaces Pipeline
- **Round** — Replaces Stage
- **JuryGroup**, **JuryGroupMember** — New jury management system
- **SubmissionWindow**, **SubmissionFileRequirement**, **RoundSubmissionVisibility** — Multi-round document submission
- **MentorFile**, **MentorFileComment**, **MentorMessage** — Mentoring workspace
- **DeliberationSession**, **DeliberationVote**, **DeliberationResult**, **DeliberationParticipant** — Winner confirmation
- **ResultLock**, **ResultUnlockEvent** — Result immutability
- **AssignmentIntent**, **AssignmentException** — Assignment governance
- **SubmissionPromotionEvent** — Mentoring file promotion tracking
- **AdvancementRule** — Replaces StageTransition
### Renamed Models
- `Pipeline``Competition`
- `Stage``Round`
- `StageType``RoundType`
- `StageStatus``RoundStatus`
- `ProjectStageState``ProjectRoundState`
- `ProjectStageStateValue``ProjectRoundStateValue`
### Removed Models
- **Track** — Eliminated (main flow is linear; awards are SpecialAward entities)
- **StageTransition** — Replaced by AdvancementRule
- **WinnerProposal**, **WinnerApproval** — Replaced by Deliberation system
### New Enums Added
- `JuryGroupMemberRole` (CHAIR, MEMBER, OBSERVER) — replaces boolean `isLead`
- `AssignmentIntentStatus` (PENDING, HONORED, OVERRIDDEN, EXPIRED, CANCELLED) — replaces String status
### Modified Models
- **Assignment** — Added `juryGroupId` link
- **Round** — Added `purposeKey` (optional analytics tag)
- **JuryGroupMember** — Changed `isLead: Boolean` to `role: JuryGroupMemberRole`
- **AssignmentIntent** — Changed `status: String` to `status: AssignmentIntentStatus` (proper enum with lifecycle)
- **SubmissionWindow** — Added `isLocked: Boolean` (manual lock override independent of window close)
- **ProjectFile** — Changed `roundId` to `submissionWindowId`, added `requirementId`, added `replacedById` self-reference
- **SpecialAward** — Added `competitionId`, `evaluationRoundId`, `juryGroupId`
- **MentorAssignment** — Added workspace fields (`workspaceEnabled`, `workspaceOpenAt`, etc.)
- **Project** — Added `competitionId` (direct link to competition)
- **DecisionAuditLog** — Documented `beforeState`/`afterState` convention in `detailsJson`
### Field Renames
All `stageId` foreign keys → `roundId`
All `trackId` references → Removed
---
## Cross-References
- See [03-competition-flow.md](./03-competition-flow.md) for end-to-end competition orchestration
- See [04-jury-system.md](./04-jury-system.md) for jury management and assignment logic
- See [05-submission-system.md](./05-submission-system.md) for multi-round document handling
- See [06-mentoring-workspace.md](./06-mentoring-workspace.md) for mentoring file collaboration
- See [07-deliberation-flow.md](./07-deliberation-flow.md) for winner confirmation process
- See [08-result-locking.md](./08-result-locking.md) for result immutability and unlock audit