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