38 KiB
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:
- Main competition flow — But this is always linear (Intake -> Filter -> Eval -> ... -> Finals)
- Award branches — But awards don't need their own stage pipeline; they need eligibility + voting
Without Track:
Roundbelongs directly toCompetition(no intermediate layer)SpecialAwardis self-contained (has its own jury, voting, and result)ProjectRoundStatedropstrackId(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)
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)
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)
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)
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:
// 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
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
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
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
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.
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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)
- Create
Competitiontable - Create
Roundtable - Create
JuryGroup,JuryGroupMembertables - Create
SubmissionWindow,SubmissionFileRequirement,RoundSubmissionVisibilitytables - Create
MentorFile,MentorFileCommenttables - Create
WinnerProposal,WinnerApprovaltables - Create
AdvancementRuletable - Create
ProjectRoundStatetable - Add new columns to
Assignment(juryGroupId),ProjectFile(submissionWindowId),MentorAssignment(workspace fields),SpecialAward(competitionId, eligibilityMode, juryGroupId, evaluationRoundId),Project(competitionId)
Phase 2: Data migration
- For each Pipeline: Create a Competition record
- For each Stage in the MAIN Track: Create a Round record (maintaining sortOrder)
- For each ProjectStageState: Create a ProjectRoundState record (dropping trackId)
- For each AWARD Track: Migrate to SpecialAward (link to Competition + evaluation round)
- For existing FileRequirements: Create SubmissionWindow + SubmissionFileRequirement
- For existing Assignments with stageId: Update roundId reference
- Create default JuryGroups from existing assignments (group by stageId)
Phase 3: Code migration
- Update all services (stageId -> roundId, remove trackId references)
- Update all routers (rename, new endpoints)
- Update all UI (new pages, enhanced existing pages)
Phase 4: Drop old tables (after verification)
- Drop
Tracktable - Drop
StageTransitiontable - Drop
ProjectStageStatetable (after verifying ProjectRoundState) - Drop
Stagetable (after verifying Round) - Drop
Pipelinetable (after verifying Competition) - Clean up old enums
Detailed migration SQL will be in 21-migration-strategy.md.