1140 lines
38 KiB
Markdown
1140 lines
38 KiB
Markdown
# Data Model Redesign
|
|
|
|
## Overview
|
|
|
|
This document defines the complete Prisma schema for the MOPC architecture redesign. It covers new models, modified models, eliminated models, and the migration path from the current schema.
|
|
|
|
### Naming Convention Changes
|
|
|
|
| Current | Redesigned | Rationale |
|
|
|---------|-----------|-----------|
|
|
| `Pipeline` | `Competition` | Domain-specific — admins think "Competition 2026" |
|
|
| `Track` | *(eliminated)* | Main flow is linear; awards are standalone |
|
|
| `Stage` | `Round` | Domain-specific — "Round 3: Jury 1 Evaluation" |
|
|
| `StageType` | `RoundType` | Follows rename |
|
|
| `StageStatus` | `RoundStatus` | Follows rename |
|
|
| `ProjectStageState` | `ProjectRoundState` | Follows rename, drops trackId |
|
|
| `StageTransition` | *(eliminated)* | Replaced by linear sortOrder + advancement rules |
|
|
|
|
---
|
|
|
|
## 1. Eliminated Models & Enums
|
|
|
|
### Models Removed
|
|
|
|
```
|
|
Track -- Main flow is linear; awards are standalone SpecialAward entities
|
|
StageTransition -- Replaced by linear round ordering + AdvancementRule
|
|
CohortProject -- Merged into round-level project ordering
|
|
TrackKind (enum) -- No tracks
|
|
RoutingMode (enum) -- No tracks
|
|
DecisionMode (enum) -- Moved to SpecialAward.decisionMode as a string field
|
|
```
|
|
|
|
### Why Track Is Eliminated
|
|
|
|
The `Track` model served two purposes:
|
|
1. **Main competition flow** — But this is always linear (Intake -> Filter -> Eval -> ... -> Finals)
|
|
2. **Award branches** — But awards don't need their own stage pipeline; they need eligibility + voting
|
|
|
|
Without Track:
|
|
- `Round` belongs directly to `Competition` (no intermediate layer)
|
|
- `SpecialAward` is self-contained (has its own jury, voting, and result)
|
|
- `ProjectRoundState` drops `trackId` (project is in a round, period)
|
|
- Admin UI shows a flat list of rounds instead of nested Track > Stage
|
|
|
|
---
|
|
|
|
## 2. Core Competition Structure
|
|
|
|
### Competition (replaces Pipeline)
|
|
|
|
```prisma
|
|
model Competition {
|
|
id String @id @default(cuid())
|
|
programId String
|
|
name String // "MOPC 2026 Competition"
|
|
slug String @unique // "mopc-2026"
|
|
status CompetitionStatus @default(DRAFT)
|
|
|
|
// Competition-wide settings (typed, not generic JSON)
|
|
categoryMode String @default("SHARED") // "SHARED" (both categories same flow) | "SPLIT" (separate finalist counts)
|
|
startupFinalistCount Int @default(3)
|
|
conceptFinalistCount Int @default(3)
|
|
|
|
// Notification preferences
|
|
notifyOnRoundAdvance Boolean @default(true)
|
|
notifyOnDeadlineApproach Boolean @default(true)
|
|
deadlineReminderDays Int[] @default([7, 3, 1]) // Days before deadline to send reminders
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
|
|
rounds Round[]
|
|
juryGroups JuryGroup[]
|
|
submissionWindows SubmissionWindow[]
|
|
specialAwards SpecialAward[]
|
|
winnerProposals WinnerProposal[]
|
|
|
|
@@index([programId])
|
|
@@index([status])
|
|
}
|
|
|
|
enum CompetitionStatus {
|
|
DRAFT
|
|
ACTIVE
|
|
CLOSED
|
|
ARCHIVED
|
|
}
|
|
```
|
|
|
|
### Round (replaces Stage)
|
|
|
|
```prisma
|
|
model Round {
|
|
id String @id @default(cuid())
|
|
competitionId String
|
|
name String // "Jury 1 - Semi-finalist Selection"
|
|
slug String // "jury-1-semifinalist"
|
|
roundType RoundType
|
|
status RoundStatus @default(ROUND_DRAFT)
|
|
sortOrder Int @default(0)
|
|
|
|
// Time windows
|
|
windowOpenAt DateTime?
|
|
windowCloseAt DateTime?
|
|
|
|
// Round-type-specific configuration (validated by Zod per RoundType)
|
|
configJson Json? @db.JsonB
|
|
|
|
// Links to other entities
|
|
juryGroupId String? // Which jury evaluates this round (EVALUATION, LIVE_FINAL)
|
|
submissionWindowId String? // Which submission window this round collects docs for (INTAKE, SUBMISSION)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
|
|
juryGroup JuryGroup? @relation(fields: [juryGroupId], references: [id], onDelete: SetNull)
|
|
submissionWindow SubmissionWindow? @relation(fields: [submissionWindowId], references: [id], onDelete: SetNull)
|
|
|
|
projectRoundStates ProjectRoundState[]
|
|
assignments Assignment[]
|
|
evaluationForms EvaluationForm[]
|
|
filteringRules FilteringRule[]
|
|
filteringResults FilteringResult[]
|
|
filteringJobs FilteringJob[]
|
|
evaluationSummaries EvaluationSummary[]
|
|
evaluationDiscussions EvaluationDiscussion[]
|
|
gracePeriods GracePeriod[]
|
|
liveCursor LiveProgressCursor?
|
|
liveVotingSession LiveVotingSession?
|
|
cohorts Cohort[]
|
|
advancementRules AdvancementRule[]
|
|
|
|
// Visible submission windows (which doc rounds jury can see)
|
|
visibleSubmissionWindows RoundSubmissionVisibility[]
|
|
|
|
@@unique([competitionId, slug])
|
|
@@unique([competitionId, sortOrder])
|
|
@@index([competitionId])
|
|
@@index([roundType])
|
|
@@index([status])
|
|
}
|
|
|
|
enum RoundType {
|
|
INTAKE // Application window — collect initial submissions
|
|
FILTERING // AI screening — automated eligibility check
|
|
EVALUATION // Jury evaluation — scoring, feedback, advancement decision
|
|
SUBMISSION // New submission window — additional docs from advancing teams
|
|
MENTORING // Mentor-team collaboration period
|
|
LIVE_FINAL // Live ceremony — real-time voting, audience participation
|
|
CONFIRMATION // Winner agreement — jury signatures + admin confirmation
|
|
}
|
|
|
|
enum RoundStatus {
|
|
ROUND_DRAFT // Being configured, not visible to participants
|
|
ROUND_ACTIVE // Open/in progress
|
|
ROUND_CLOSED // Window closed, results pending or finalized
|
|
ROUND_ARCHIVED // Historical, read-only
|
|
}
|
|
```
|
|
|
|
### ProjectRoundState (replaces ProjectStageState)
|
|
|
|
```prisma
|
|
model ProjectRoundState {
|
|
id String @id @default(cuid())
|
|
projectId String
|
|
roundId String
|
|
state ProjectRoundStateValue @default(PENDING)
|
|
enteredAt DateTime @default(now())
|
|
exitedAt DateTime?
|
|
metadataJson Json? @db.JsonB // Round-type-specific state data
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([projectId, roundId])
|
|
@@index([projectId])
|
|
@@index([roundId])
|
|
@@index([state])
|
|
}
|
|
|
|
enum ProjectRoundStateValue {
|
|
PENDING // Entered round, awaiting action
|
|
IN_PROGRESS // Active (submission in progress, evaluation ongoing)
|
|
PASSED // Cleared this round, eligible to advance
|
|
REJECTED // Did not pass this round
|
|
COMPLETED // Round fully complete for this project
|
|
WITHDRAWN // Project withdrew
|
|
}
|
|
```
|
|
|
|
### AdvancementRule (replaces StageTransition + guardJson)
|
|
|
|
```prisma
|
|
model AdvancementRule {
|
|
id String @id @default(cuid())
|
|
roundId String // The round this rule applies to (source round)
|
|
targetRoundId String? // Where projects advance to (null = next round by sortOrder)
|
|
|
|
ruleType AdvancementRuleType
|
|
configJson Json @db.JsonB // Rule-type-specific config
|
|
|
|
isDefault Boolean @default(true) // Default advancement path
|
|
sortOrder Int @default(0) // Priority when multiple rules exist
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([roundId])
|
|
}
|
|
|
|
enum AdvancementRuleType {
|
|
AUTO_ADVANCE // All PASSED projects advance to next round automatically
|
|
SCORE_THRESHOLD // Projects above score threshold advance
|
|
TOP_N // Top N projects per category advance
|
|
ADMIN_SELECTION // Admin manually selects who advances
|
|
AI_RECOMMENDED // AI suggests advancement, admin confirms
|
|
}
|
|
```
|
|
|
|
**AdvancementRule configJson shapes:**
|
|
|
|
```typescript
|
|
// AUTO_ADVANCE
|
|
{ trigger: "on_round_close" | "immediate" }
|
|
|
|
// SCORE_THRESHOLD
|
|
{ minScore: 7.0, metric: "average_global" | "weighted_criteria" }
|
|
|
|
// TOP_N
|
|
{
|
|
perCategory: true,
|
|
counts: { STARTUP: 10, BUSINESS_CONCEPT: 10 },
|
|
tieBreaker: "admin_decides" | "highest_individual" | "revote"
|
|
}
|
|
|
|
// ADMIN_SELECTION
|
|
{ requireAIRecommendation: true, showRankings: true }
|
|
|
|
// AI_RECOMMENDED
|
|
{ topN: 10, confidenceThreshold: 0.7, requireAdminApproval: true }
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Jury System
|
|
|
|
### JuryGroup
|
|
|
|
```prisma
|
|
model JuryGroup {
|
|
id String @id @default(cuid())
|
|
competitionId String
|
|
name String // "Jury 1", "Jury 2", "Jury 3", "Innovation Award Jury"
|
|
slug String // "jury-1", "jury-2"
|
|
description String? @db.Text
|
|
sortOrder Int @default(0)
|
|
|
|
// Default assignment configuration for this jury
|
|
defaultMaxAssignments Int @default(20)
|
|
defaultCapMode CapMode @default(SOFT)
|
|
softCapBuffer Int @default(2) // Extra assignments above cap for load balancing
|
|
|
|
// Default category quotas (per juror)
|
|
categoryQuotasEnabled Boolean @default(false)
|
|
defaultCategoryQuotas Json? @db.JsonB // { "STARTUP": { "min": 2, "max": 15 }, "BUSINESS_CONCEPT": { "min": 2, "max": 15 } }
|
|
|
|
// Onboarding: can jurors adjust their own cap/ratio during onboarding?
|
|
allowJurorCapAdjustment Boolean @default(false)
|
|
allowJurorRatioAdjustment Boolean @default(false)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
|
|
members JuryGroupMember[]
|
|
rounds Round[] // Rounds this jury is assigned to
|
|
assignments Assignment[] // Assignments made through this jury group
|
|
|
|
@@unique([competitionId, slug])
|
|
@@index([competitionId])
|
|
}
|
|
|
|
enum CapMode {
|
|
HARD // Absolute maximum — AI/algorithm cannot exceed
|
|
SOFT // Target maximum — can exceed by softCapBuffer for load balancing
|
|
NONE // No cap — unlimited assignments
|
|
}
|
|
```
|
|
|
|
### JuryGroupMember
|
|
|
|
```prisma
|
|
model JuryGroupMember {
|
|
id String @id @default(cuid())
|
|
juryGroupId String
|
|
userId String
|
|
isLead Boolean @default(false)
|
|
joinedAt DateTime @default(now())
|
|
|
|
// Per-juror overrides (null = use group defaults)
|
|
maxAssignmentsOverride Int?
|
|
capModeOverride CapMode?
|
|
categoryQuotasOverride Json? @db.JsonB // Same shape as JuryGroup.defaultCategoryQuotas
|
|
|
|
// Juror preferences (set during onboarding)
|
|
preferredStartupRatio Float? // 0.0 to 1.0 — desired % of startups (e.g., 0.6 = 60% startups)
|
|
availabilityNotes String? @db.Text
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
juryGroup JuryGroup @relation(fields: [juryGroupId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([juryGroupId, userId])
|
|
@@index([juryGroupId])
|
|
@@index([userId])
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Multi-Round Submission System
|
|
|
|
### SubmissionWindow
|
|
|
|
```prisma
|
|
model SubmissionWindow {
|
|
id String @id @default(cuid())
|
|
competitionId String
|
|
name String // "Round 1 Application Docs", "Semi-finalist Additional Docs"
|
|
slug String // "round-1-docs"
|
|
roundNumber Int // 1, 2, 3... (sequential)
|
|
sortOrder Int @default(0)
|
|
|
|
// Window timing
|
|
windowOpenAt DateTime?
|
|
windowCloseAt DateTime?
|
|
|
|
// Deadline behavior
|
|
deadlinePolicy DeadlinePolicy @default(FLAG)
|
|
graceHours Int? // Hours after windowCloseAt where late submissions accepted
|
|
|
|
// Locking behavior
|
|
lockOnClose Boolean @default(true) // Applicants can't edit after window closes
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
|
|
fileRequirements SubmissionFileRequirement[]
|
|
projectFiles ProjectFile[]
|
|
rounds Round[] // Rounds that collect submissions for this window
|
|
visibility RoundSubmissionVisibility[] // Which evaluation rounds can see these docs
|
|
|
|
@@unique([competitionId, slug])
|
|
@@unique([competitionId, roundNumber])
|
|
@@index([competitionId])
|
|
}
|
|
|
|
enum DeadlinePolicy {
|
|
HARD // Submissions rejected after close
|
|
FLAG // Submissions accepted but marked late
|
|
GRACE // Grace period after close, then hard cutoff
|
|
}
|
|
```
|
|
|
|
### SubmissionFileRequirement
|
|
|
|
```prisma
|
|
model SubmissionFileRequirement {
|
|
id String @id @default(cuid())
|
|
submissionWindowId String
|
|
name String // "Executive Summary", "Business Plan", "Video Pitch"
|
|
description String? @db.Text
|
|
acceptedMimeTypes String[] // ["application/pdf", "video/*"]
|
|
maxSizeMB Int? // Size limit
|
|
isRequired Boolean @default(true)
|
|
sortOrder Int @default(0)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
submissionWindow SubmissionWindow @relation(fields: [submissionWindowId], references: [id], onDelete: Cascade)
|
|
files ProjectFile[] // Files uploaded against this requirement
|
|
|
|
@@index([submissionWindowId])
|
|
}
|
|
```
|
|
|
|
### RoundSubmissionVisibility
|
|
|
|
Controls which submission windows a jury evaluation round can see.
|
|
|
|
```prisma
|
|
model RoundSubmissionVisibility {
|
|
id String @id @default(cuid())
|
|
roundId String
|
|
submissionWindowId String
|
|
|
|
canView Boolean @default(true) // Jury can see these docs
|
|
displayLabel String? // "Round 1 Docs", "Round 2 Docs" (shown to jury)
|
|
|
|
// Relations
|
|
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
|
|
submissionWindow SubmissionWindow @relation(fields: [submissionWindowId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([roundId, submissionWindowId])
|
|
@@index([roundId])
|
|
}
|
|
```
|
|
|
|
**Example usage:**
|
|
|
|
Jury 1 (Round 3) sees only Round 1 docs:
|
|
```
|
|
RoundSubmissionVisibility { roundId: round-3, submissionWindowId: sw-1, canView: true, displayLabel: "Application Docs" }
|
|
```
|
|
|
|
Jury 2 (Round 5) sees Round 1 AND Round 2 docs:
|
|
```
|
|
RoundSubmissionVisibility { roundId: round-5, submissionWindowId: sw-1, canView: true, displayLabel: "Round 1 Docs" }
|
|
RoundSubmissionVisibility { roundId: round-5, submissionWindowId: sw-2, canView: true, displayLabel: "Round 2 Docs" }
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Mentoring Workspace
|
|
|
|
### MentorFile
|
|
|
|
```prisma
|
|
model MentorFile {
|
|
id String @id @default(cuid())
|
|
mentorAssignmentId String
|
|
uploadedByUserId String
|
|
|
|
fileName String
|
|
mimeType String
|
|
size Int
|
|
bucket String
|
|
objectKey String
|
|
description String? @db.Text
|
|
|
|
// Promotion to official submission
|
|
isPromoted Boolean @default(false)
|
|
promotedToFileId String? @unique // Links to the ProjectFile created on promotion
|
|
promotedAt DateTime?
|
|
promotedByUserId String?
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
// Relations
|
|
mentorAssignment MentorAssignment @relation(fields: [mentorAssignmentId], references: [id], onDelete: Cascade)
|
|
uploadedBy User @relation("MentorFileUploader", fields: [uploadedByUserId], references: [id])
|
|
promotedBy User? @relation("MentorFilePromoter", fields: [promotedByUserId], references: [id])
|
|
promotedFile ProjectFile? @relation("PromotedFromMentorFile", fields: [promotedToFileId], references: [id], onDelete: SetNull)
|
|
comments MentorFileComment[]
|
|
|
|
@@index([mentorAssignmentId])
|
|
@@index([uploadedByUserId])
|
|
}
|
|
```
|
|
|
|
### MentorFileComment
|
|
|
|
```prisma
|
|
model MentorFileComment {
|
|
id String @id @default(cuid())
|
|
mentorFileId String
|
|
authorId String
|
|
content String @db.Text
|
|
|
|
// Threading support
|
|
parentCommentId String? // null = top-level comment, non-null = reply
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
mentorFile MentorFile @relation(fields: [mentorFileId], references: [id], onDelete: Cascade)
|
|
author User @relation("MentorFileCommentAuthor", fields: [authorId], references: [id])
|
|
parentComment MentorFileComment? @relation("CommentThread", fields: [parentCommentId], references: [id], onDelete: Cascade)
|
|
replies MentorFileComment[] @relation("CommentThread")
|
|
|
|
@@index([mentorFileId])
|
|
@@index([authorId])
|
|
@@index([parentCommentId])
|
|
}
|
|
```
|
|
|
|
### MentorAssignment Modifications
|
|
|
|
```prisma
|
|
model MentorAssignment {
|
|
// ... existing fields preserved ...
|
|
id String @id @default(cuid())
|
|
projectId String @unique
|
|
mentorId String
|
|
method AssignmentMethod @default(MANUAL)
|
|
assignedAt DateTime @default(now())
|
|
assignedBy String?
|
|
aiConfidenceScore Float?
|
|
expertiseMatchScore Float?
|
|
aiReasoning String? @db.Text
|
|
completionStatus String @default("in_progress")
|
|
lastViewedAt DateTime?
|
|
|
|
// NEW: Workspace activation
|
|
workspaceEnabled Boolean @default(false) // Activated when MENTORING round opens
|
|
workspaceOpenAt DateTime? // When mentoring files/chat becomes available
|
|
workspaceCloseAt DateTime? // When workspace access ends
|
|
|
|
// Relations (existing + new)
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
mentor User @relation("MentorAssignment", fields: [mentorId], references: [id])
|
|
notes MentorNote[]
|
|
milestoneCompletions MentorMilestoneCompletion[]
|
|
messages MentorMessage[]
|
|
files MentorFile[] // NEW: Workspace files
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Winner Confirmation System
|
|
|
|
### WinnerProposal
|
|
|
|
```prisma
|
|
model WinnerProposal {
|
|
id String @id @default(cuid())
|
|
competitionId String
|
|
category CompetitionCategory // STARTUP or BUSINESS_CONCEPT
|
|
|
|
status WinnerProposalStatus @default(PENDING)
|
|
|
|
// Proposed rankings (ordered list of project IDs)
|
|
rankedProjectIds String[] // ["proj-1st", "proj-2nd", "proj-3rd"]
|
|
|
|
// Selection basis (evidence)
|
|
sourceRoundId String // Which round's scores/votes informed this
|
|
selectionBasis Json @db.JsonB // { method, scores, aiRecommendation, reasoning }
|
|
|
|
// Proposer
|
|
proposedById String
|
|
proposedAt DateTime @default(now())
|
|
|
|
// Finalization
|
|
frozenAt DateTime?
|
|
frozenById String?
|
|
|
|
// Admin override (if used)
|
|
overrideUsed Boolean @default(false)
|
|
overrideMode String? // "FORCE_MAJORITY" | "ADMIN_DECISION"
|
|
overrideReason String? @db.Text
|
|
overrideById String?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
|
|
sourceRound Round @relation("WinnerProposalSource", fields: [sourceRoundId], references: [id])
|
|
proposedBy User @relation("WinnerProposer", fields: [proposedById], references: [id])
|
|
frozenBy User? @relation("WinnerFreezer", fields: [frozenById], references: [id])
|
|
overrideBy User? @relation("WinnerOverrider", fields: [overrideById], references: [id])
|
|
approvals WinnerApproval[]
|
|
|
|
@@index([competitionId])
|
|
@@index([status])
|
|
}
|
|
|
|
enum WinnerProposalStatus {
|
|
PENDING // Waiting for jury approvals
|
|
APPROVED // All required approvals received
|
|
REJECTED // At least one rejection
|
|
OVERRIDDEN // Admin used override
|
|
FROZEN // Locked — official results
|
|
}
|
|
```
|
|
|
|
### WinnerApproval
|
|
|
|
```prisma
|
|
model WinnerApproval {
|
|
id String @id @default(cuid())
|
|
winnerProposalId String
|
|
userId String
|
|
role WinnerApprovalRole
|
|
|
|
// Response
|
|
approved Boolean? // null = not yet responded
|
|
comments String? @db.Text
|
|
respondedAt DateTime?
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
// Relations
|
|
proposal WinnerProposal @relation(fields: [winnerProposalId], references: [id], onDelete: Cascade)
|
|
user User @relation("WinnerApprovalUser", fields: [userId], references: [id])
|
|
|
|
@@unique([winnerProposalId, userId])
|
|
@@index([winnerProposalId])
|
|
@@index([userId])
|
|
}
|
|
|
|
enum WinnerApprovalRole {
|
|
JURY_MEMBER // Must individually confirm
|
|
ADMIN // Final sign-off (or override)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Modified Existing Models
|
|
|
|
### ProjectFile — Add Submission Window Link
|
|
|
|
```prisma
|
|
model ProjectFile {
|
|
id String @id @default(cuid())
|
|
projectId String
|
|
|
|
// CHANGED: Link to SubmissionWindow instead of legacy roundId
|
|
submissionWindowId String?
|
|
requirementId String? // Links to SubmissionFileRequirement
|
|
|
|
fileType FileType
|
|
fileName String
|
|
mimeType String
|
|
size Int
|
|
bucket String
|
|
objectKey String
|
|
|
|
isLate Boolean @default(false)
|
|
version Int @default(1)
|
|
replacedById String? @unique
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
submissionWindow SubmissionWindow? @relation(fields: [submissionWindowId], references: [id], onDelete: SetNull)
|
|
requirement SubmissionFileRequirement? @relation(fields: [requirementId], references: [id], onDelete: SetNull)
|
|
replacedBy ProjectFile? @relation("FileVersion", fields: [replacedById], references: [id])
|
|
previousVersion ProjectFile? @relation("FileVersion")
|
|
promotedFrom MentorFile? @relation("PromotedFromMentorFile") // NEW
|
|
|
|
@@index([projectId])
|
|
@@index([submissionWindowId])
|
|
@@index([requirementId])
|
|
}
|
|
```
|
|
|
|
### Assignment — Add JuryGroup Link
|
|
|
|
```prisma
|
|
model Assignment {
|
|
// ... all existing fields preserved ...
|
|
id String @id @default(cuid())
|
|
userId String
|
|
projectId String
|
|
roundId String // RENAMED from stageId
|
|
method AssignmentMethod @default(MANUAL)
|
|
isRequired Boolean @default(true)
|
|
isCompleted Boolean @default(false)
|
|
aiConfidenceScore Float?
|
|
expertiseMatchScore Float?
|
|
aiReasoning String? @db.Text
|
|
|
|
// NEW: Link to jury group
|
|
juryGroupId String?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
|
|
juryGroup JuryGroup? @relation(fields: [juryGroupId], references: [id], onDelete: SetNull)
|
|
evaluation Evaluation?
|
|
coi ConflictOfInterest?
|
|
|
|
@@unique([userId, projectId, roundId])
|
|
@@index([userId])
|
|
@@index([projectId])
|
|
@@index([roundId])
|
|
@@index([juryGroupId])
|
|
}
|
|
```
|
|
|
|
### SpecialAward — Enhanced Standalone
|
|
|
|
```prisma
|
|
model SpecialAward {
|
|
id String @id @default(cuid())
|
|
competitionId String // CHANGED: Links to Competition, not Track
|
|
name String
|
|
description String? @db.Text
|
|
criteriaText String? @db.Text // For AI eligibility
|
|
|
|
// Award mode
|
|
eligibilityMode AwardEligibilityMode @default(STAY_IN_MAIN)
|
|
|
|
// Scoring/voting
|
|
scoringMode AwardScoringMode @default(PICK_WINNER)
|
|
maxRankedPicks Int? // For RANKED mode
|
|
|
|
// Decision
|
|
decisionMode String @default("JURY_VOTE") // "JURY_VOTE" | "AWARD_MASTER_DECISION" | "ADMIN_DECISION"
|
|
|
|
// Status
|
|
status AwardStatus @default(DRAFT)
|
|
|
|
// Voting window
|
|
votingStartAt DateTime?
|
|
votingEndAt DateTime?
|
|
|
|
// Runs alongside which evaluation round
|
|
evaluationRoundId String? // NEW: Which round this award runs during
|
|
|
|
// Jury (can be its own group or share a competition jury group)
|
|
juryGroupId String? // NEW: Dedicated or shared jury group
|
|
|
|
// Winner
|
|
winnerProjectId String?
|
|
winnerOverridden Boolean @default(false)
|
|
|
|
// AI
|
|
useAiEligibility Boolean @default(false)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
|
|
evaluationRound Round? @relation("AwardEvaluationRound", fields: [evaluationRoundId], references: [id], onDelete: SetNull)
|
|
juryGroup JuryGroup? @relation("AwardJuryGroup", fields: [juryGroupId], references: [id], onDelete: SetNull)
|
|
winnerProject Project? @relation("AwardWinner", fields: [winnerProjectId], references: [id], onDelete: SetNull)
|
|
eligibilities AwardEligibility[]
|
|
jurors AwardJuror[]
|
|
votes AwardVote[]
|
|
|
|
@@index([competitionId])
|
|
@@index([status])
|
|
@@index([evaluationRoundId])
|
|
}
|
|
|
|
enum AwardEligibilityMode {
|
|
SEPARATE_POOL // Projects pulled out of main flow into award-only track
|
|
STAY_IN_MAIN // Projects remain in main competition, flagged as award-eligible
|
|
}
|
|
|
|
enum AwardStatus {
|
|
DRAFT
|
|
NOMINATIONS_OPEN
|
|
VOTING_OPEN
|
|
CLOSED
|
|
ARCHIVED
|
|
}
|
|
|
|
enum AwardScoringMode {
|
|
PICK_WINNER
|
|
RANKED
|
|
SCORED
|
|
}
|
|
```
|
|
|
|
### Evaluation — Rename stageId to roundId
|
|
|
|
```prisma
|
|
model Evaluation {
|
|
// All fields preserved, stageId references updated to roundId via Assignment.roundId
|
|
id String @id @default(cuid())
|
|
assignmentId String @unique
|
|
status EvaluationStatus @default(NOT_STARTED)
|
|
criterionScoresJson Json? @db.JsonB
|
|
globalScore Int?
|
|
binaryDecision Boolean?
|
|
feedbackText String? @db.Text
|
|
submittedAt DateTime?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
assignment Assignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
|
|
}
|
|
```
|
|
|
|
### Project — Remove roundId, add competition link
|
|
|
|
```prisma
|
|
model Project {
|
|
// ... existing fields ...
|
|
id String @id @default(cuid())
|
|
programId String
|
|
competitionId String? // NEW: Direct link to competition
|
|
|
|
// Remove legacy roundId field
|
|
// roundId String? -- REMOVED
|
|
|
|
status ProjectStatus @default(SUBMITTED)
|
|
title String
|
|
teamName String?
|
|
description String? @db.Text
|
|
competitionCategory CompetitionCategory?
|
|
// ... all other existing fields preserved ...
|
|
|
|
wantsMentorship Boolean @default(false) // Preserved — drives mentoring eligibility
|
|
|
|
// Relations (updated)
|
|
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
|
|
competition Competition? @relation(fields: [competitionId], references: [id], onDelete: SetNull)
|
|
files ProjectFile[]
|
|
teamMembers TeamMember[]
|
|
projectRoundStates ProjectRoundState[] // RENAMED from projectStageStates
|
|
assignments Assignment[]
|
|
mentorAssignment MentorAssignment?
|
|
// ... other existing relations preserved ...
|
|
|
|
@@index([programId])
|
|
@@index([competitionId])
|
|
@@index([status])
|
|
@@index([competitionCategory])
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Round-Type Config Shapes (Zod-validated)
|
|
|
|
Each round type has a specific config shape stored in `Round.configJson`. These replace the old generic `configJson` approach with documented, validated structures.
|
|
|
|
### INTAKE Config
|
|
|
|
```typescript
|
|
type IntakeConfig = {
|
|
// Submission behavior
|
|
allowDrafts: boolean // Allow saving drafts before submitting
|
|
draftExpiryDays: number // Auto-delete drafts after N days (default: 30)
|
|
|
|
// What categories are accepted
|
|
acceptedCategories: ("STARTUP" | "BUSINESS_CONCEPT")[]
|
|
|
|
// Public form settings
|
|
publicFormEnabled: boolean // Allow anonymous form access via slug
|
|
customFields: CustomFieldDef[] // Additional form fields beyond standard ones
|
|
}
|
|
```
|
|
|
|
### FILTERING Config
|
|
|
|
```typescript
|
|
type FilteringConfig = {
|
|
// Rule engine
|
|
rules: FilterRuleDef[] // Field-based and document-check rules
|
|
|
|
// AI screening
|
|
aiScreeningEnabled: boolean
|
|
aiCriteriaText: string // Plain-language criteria for AI
|
|
aiConfidenceThresholds: {
|
|
high: number // Above this = auto-pass (default: 0.85)
|
|
medium: number // Above this = flag for review (default: 0.6)
|
|
low: number // Below this = auto-reject (default: 0.4)
|
|
}
|
|
|
|
// Manual queue
|
|
manualReviewEnabled: boolean // Flagged projects need admin review
|
|
|
|
// Batch processing
|
|
batchSize: number // Projects per AI batch (default: 20)
|
|
}
|
|
```
|
|
|
|
### EVALUATION Config
|
|
|
|
```typescript
|
|
type EvaluationConfig = {
|
|
// Assignment settings (work with JuryGroup)
|
|
requiredReviewsPerProject: number // How many jurors review each project (default: 3)
|
|
|
|
// Assignment caps are now on JuryGroup and JuryGroupMember
|
|
// (no longer duplicated here)
|
|
|
|
// Scoring
|
|
scoringMode: "criteria" | "global" | "binary" // How jurors score
|
|
requireFeedback: boolean // Must submit text feedback
|
|
|
|
// COI
|
|
coiRequired: boolean // Must declare COI before evaluating (default: true)
|
|
|
|
// Peer review
|
|
peerReviewEnabled: boolean // Allow jurors to see anonymized peer evaluations
|
|
anonymizationLevel: "fully_anonymous" | "show_initials" | "named"
|
|
|
|
// AI features
|
|
aiSummaryEnabled: boolean // Generate AI evaluation summaries
|
|
|
|
// Advancement (what happens after evaluation ends)
|
|
advancementMode: "auto_top_n" | "admin_selection" | "ai_recommended"
|
|
advancementConfig: {
|
|
perCategory: boolean
|
|
startupCount: number // How many startups advance
|
|
conceptCount: number // How many concepts advance
|
|
tieBreaker: "admin_decides" | "highest_individual" | "revote"
|
|
}
|
|
}
|
|
```
|
|
|
|
### SUBMISSION Config
|
|
|
|
```typescript
|
|
type SubmissionConfig = {
|
|
// Who can submit (based on status from previous round)
|
|
eligibleStatuses: ProjectRoundStateValue[] // Usually ["PASSED"]
|
|
|
|
// Notification
|
|
notifyEligibleTeams: boolean // Email teams when window opens
|
|
|
|
// Previous rounds become read-only for applicants
|
|
lockPreviousWindows: boolean // Default: true
|
|
}
|
|
```
|
|
|
|
### MENTORING Config
|
|
|
|
```typescript
|
|
type MentoringConfig = {
|
|
// Who gets mentoring
|
|
eligibility: "all_advancing" | "requested_only" // All finalists or only those who requested
|
|
|
|
// Workspace features
|
|
chatEnabled: boolean
|
|
fileUploadEnabled: boolean
|
|
fileCommentsEnabled: boolean
|
|
filePromotionEnabled: boolean // Can promote files to official submissions
|
|
|
|
// Target submission window for promotions
|
|
promotionTargetWindowId: string? // Which SubmissionWindow promoted files go to
|
|
|
|
// Auto-assignment
|
|
autoAssignMentors: boolean // Use AI/algorithm to assign mentors
|
|
}
|
|
```
|
|
|
|
### LIVE_FINAL Config
|
|
|
|
```typescript
|
|
type LiveFinalConfig = {
|
|
// Jury voting
|
|
juryVotingEnabled: boolean
|
|
votingMode: "simple" | "criteria" // Simple 1-10 or criteria-based
|
|
|
|
// Audience voting
|
|
audienceVotingEnabled: boolean
|
|
audienceVoteWeight: number // 0.0 to 1.0 (weight vs jury vote)
|
|
audienceVotingMode: "per_project" | "per_category" | "favorites"
|
|
audienceMaxFavorites: number? // For "favorites" mode
|
|
audienceRequireIdentification: boolean
|
|
|
|
// Deliberation
|
|
deliberationEnabled: boolean
|
|
deliberationDurationMinutes: number // Length of deliberation period
|
|
showAudienceVotesToJury: boolean // Jury sees audience results during deliberation
|
|
|
|
// Presentation
|
|
presentationOrderMode: "manual" | "random" | "score_based"
|
|
|
|
// Results reveal
|
|
revealPolicy: "immediate" | "delayed" | "ceremony"
|
|
}
|
|
```
|
|
|
|
### CONFIRMATION Config
|
|
|
|
```typescript
|
|
type ConfirmationConfig = {
|
|
// Approval requirements
|
|
requireAllJuryApproval: boolean // All jury members must individually confirm (default: true)
|
|
juryGroupId: string? // Which jury group must approve (usually Jury 3 / finals jury)
|
|
|
|
// Admin override
|
|
adminOverrideEnabled: boolean // Admin can force result (default: true)
|
|
overrideModes: ("FORCE_MAJORITY" | "ADMIN_DECISION")[] // Available override options
|
|
|
|
// Freeze behavior
|
|
autoFreezeOnApproval: boolean // Lock results immediately when all approve (default: true)
|
|
|
|
// Per-category confirmation
|
|
perCategory: boolean // Separate confirmation per STARTUP vs BUSINESS_CONCEPT
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Models Preserved As-Is (rename stageId -> roundId only)
|
|
|
|
These models are structurally unchanged. The only modification is renaming foreign keys from `stageId` to `roundId` and removing any `trackId` references:
|
|
|
|
| Model | Change |
|
|
|-------|--------|
|
|
| `EvaluationForm` | `stageId` -> `roundId` |
|
|
| `Evaluation` | No direct roundId (via Assignment) |
|
|
| `ConflictOfInterest` | No change (via Assignment) |
|
|
| `GracePeriod` | `stageId` -> `roundId` |
|
|
| `EvaluationSummary` | `stageId` -> `roundId` |
|
|
| `EvaluationDiscussion` | `stageId` -> `roundId` |
|
|
| `DiscussionComment` | No change |
|
|
| `FilteringRule` | `stageId` -> `roundId` |
|
|
| `FilteringResult` | `stageId` -> `roundId` |
|
|
| `FilteringJob` | `stageId` -> `roundId` |
|
|
| `LiveVotingSession` | `stageId` -> `roundId` |
|
|
| `LiveVote` | No change (via Session) |
|
|
| `AudienceVoter` | No change (via Session) |
|
|
| `LiveProgressCursor` | `stageId` -> `roundId` |
|
|
| `Cohort` | `stageId` -> `roundId` |
|
|
| `CohortProject` | No change |
|
|
| `AwardEligibility` | No change |
|
|
| `AwardJuror` | No change |
|
|
| `AwardVote` | No change |
|
|
| `MentorMessage` | No change |
|
|
| `MentorNote` | No change |
|
|
| `MentorMilestone` | No change |
|
|
| `MentorMilestoneCompletion` | No change |
|
|
| `InAppNotification` | No change |
|
|
| `AuditLog` | No change |
|
|
| `DecisionAuditLog` | No change |
|
|
| `User` | Add `juryGroupMemberships` relation |
|
|
| `Program` | Add `competitions` relation (was `pipelines`) |
|
|
|
|
---
|
|
|
|
## 10. User Model Additions
|
|
|
|
```prisma
|
|
model User {
|
|
// ... all existing fields preserved ...
|
|
|
|
// NEW relations
|
|
juryGroupMemberships JuryGroupMember[]
|
|
mentorFileUploads MentorFile[] @relation("MentorFileUploader")
|
|
mentorFilePromotions MentorFile[] @relation("MentorFilePromoter")
|
|
mentorFileComments MentorFileComment[] @relation("MentorFileCommentAuthor")
|
|
winnerProposals WinnerProposal[] @relation("WinnerProposer")
|
|
winnerFreezes WinnerProposal[] @relation("WinnerFreezer")
|
|
winnerOverrides WinnerProposal[] @relation("WinnerOverrider")
|
|
winnerApprovals WinnerApproval[] @relation("WinnerApprovalUser")
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 11. Entity Relationship Summary
|
|
|
|
```
|
|
Program (1) ──── (N) Competition
|
|
Competition (1) ──── (N) Round
|
|
Competition (1) ──── (N) JuryGroup
|
|
Competition (1) ──── (N) SubmissionWindow
|
|
Competition (1) ──── (N) SpecialAward
|
|
Competition (1) ──── (N) WinnerProposal
|
|
|
|
Round (N) ──── (1) JuryGroup (optional — for EVALUATION and LIVE_FINAL rounds)
|
|
Round (N) ──── (1) SubmissionWindow (optional — for INTAKE and SUBMISSION rounds)
|
|
Round (1) ──── (N) RoundSubmissionVisibility ──── (N) SubmissionWindow
|
|
|
|
JuryGroup (1) ──── (N) JuryGroupMember ──── (1) User
|
|
JuryGroup (1) ──── (N) Assignment
|
|
|
|
SubmissionWindow (1) ──── (N) SubmissionFileRequirement
|
|
SubmissionWindow (1) ──── (N) ProjectFile
|
|
|
|
Project (1) ──── (N) ProjectRoundState ──── (1) Round
|
|
Project (1) ──── (N) Assignment ──── (1) Evaluation
|
|
Project (1) ──── (N) ProjectFile
|
|
Project (1) ──── (0..1) MentorAssignment ──── (N) MentorFile ──── (N) MentorFileComment
|
|
|
|
SpecialAward (N) ──── (1) JuryGroup (optional)
|
|
SpecialAward (N) ──── (1) Round (evaluationRound — runs alongside)
|
|
|
|
WinnerProposal (1) ──── (N) WinnerApproval
|
|
```
|
|
|
|
---
|
|
|
|
## 12. Migration Strategy (High-Level)
|
|
|
|
### Phase 1: Add new tables (non-breaking)
|
|
1. Create `Competition` table
|
|
2. Create `Round` table
|
|
3. Create `JuryGroup`, `JuryGroupMember` tables
|
|
4. Create `SubmissionWindow`, `SubmissionFileRequirement`, `RoundSubmissionVisibility` tables
|
|
5. Create `MentorFile`, `MentorFileComment` tables
|
|
6. Create `WinnerProposal`, `WinnerApproval` tables
|
|
7. Create `AdvancementRule` table
|
|
8. Create `ProjectRoundState` table
|
|
9. Add new columns to `Assignment` (juryGroupId), `ProjectFile` (submissionWindowId), `MentorAssignment` (workspace fields), `SpecialAward` (competitionId, eligibilityMode, juryGroupId, evaluationRoundId), `Project` (competitionId)
|
|
|
|
### Phase 2: Data migration
|
|
1. For each Pipeline: Create a Competition record
|
|
2. For each Stage in the MAIN Track: Create a Round record (maintaining sortOrder)
|
|
3. For each ProjectStageState: Create a ProjectRoundState record (dropping trackId)
|
|
4. For each AWARD Track: Migrate to SpecialAward (link to Competition + evaluation round)
|
|
5. For existing FileRequirements: Create SubmissionWindow + SubmissionFileRequirement
|
|
6. For existing Assignments with stageId: Update roundId reference
|
|
7. Create default JuryGroups from existing assignments (group by stageId)
|
|
|
|
### Phase 3: Code migration
|
|
1. Update all services (stageId -> roundId, remove trackId references)
|
|
2. Update all routers (rename, new endpoints)
|
|
3. Update all UI (new pages, enhanced existing pages)
|
|
|
|
### Phase 4: Drop old tables (after verification)
|
|
1. Drop `Track` table
|
|
2. Drop `StageTransition` table
|
|
3. Drop `ProjectStageState` table (after verifying ProjectRoundState)
|
|
4. Drop `Stage` table (after verifying Round)
|
|
5. Drop `Pipeline` table (after verifying Competition)
|
|
6. Clean up old enums
|
|
|
|
Detailed migration SQL will be in `21-migration-strategy.md`.
|