1802 lines
55 KiB
Markdown
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
|