MOPC-App/docs/claude-architecture-redesign/03-data-model.md

1140 lines
38 KiB
Markdown
Raw Normal View History

# 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`.