MOPC-App/docs/unified-architecture-redesign/03-competition-flow.md

2950 lines
99 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 03. Competition Flow — The Eight-Round Monaco System
**Document Version**: 1.0
**Last Updated**: 2026-02-15
**Status**: Architecture Specification
---
## Table of Contents
1. [Overview](#1-overview)
2. [Competition Flow Diagram](#2-competition-flow-diagram)
3. [R1: Intake (Application Window)](#3-r1-intake-application-window)
4. [R2: Filtering (Eligibility Screening)](#4-r2-filtering-eligibility-screening)
5. [R3: Evaluation (Jury 1 — Semi-Finalist Selection)](#5-r3-evaluation-jury-1--semi-finalist-selection)
6. [R4: Submission (Semi-Finalist Documents)](#6-r4-submission-semi-finalist-documents)
7. [R5: Evaluation (Jury 2 — Finalist Selection)](#7-r5-evaluation-jury-2--finalist-selection)
8. [R6: Mentoring (Finalist Collaboration)](#8-r6-mentoring-finalist-collaboration)
9. [R7: Live Finals (Jury 3 — Live Ceremony)](#9-r7-live-finals-jury-3--live-ceremony)
10. [R8: Deliberation (Final Winner Confirmation)](#10-r8-deliberation-final-winner-confirmation)
11. [Cross-Cutting Behaviors](#11-cross-cutting-behaviors)
12. [Monaco 2026 Reference Configuration](#12-monaco-2026-reference-configuration)
---
## 1. Overview
### 1.1 Purpose of This Document
This document specifies the complete operational flow of a Monaco Ocean Protection Challenge competition from first application through final winner confirmation. It defines eight distinct round types, their purpose, configuration, behaviors, and advancement criteria.
**Key Principles:**
- **Sequential Advancement**: Projects progress through rounds based on explicit status changes
- **Independent Juries**: Three distinct jury groups evaluate at different stages
- **Multi-Window Documents**: Teams submit documents across multiple submission windows that lock over time
- **Full Audit Trail**: Every advancement decision, override, and state change is logged
- **Admin Control**: Admins can intervene at any stage with mandatory audit justification
### 1.2 Round vs Stage vs Track Terminology
**Competition/Round Architecture (Redesigned):**
```
Competition (MOPC 2026)
├─ Round 1: Application Window (type: INTAKE)
├─ Round 2: AI Screening (type: FILTERING)
├─ Round 3: Jury 1 Evaluation (type: EVALUATION)
├─ Round 4: Semi-Finalist Submission (type: SUBMISSION)
├─ Round 5: Jury 2 Evaluation (type: EVALUATION)
├─ Round 6: Mentoring (type: MENTORING)
├─ Round 7: Live Finals (type: LIVE_FINAL)
└─ Round 8: Deliberation (type: CONFIRMATION)
```
**Previous System (Pipeline/Track/Stage):**
- Pipeline → Competition
- Track → Eliminated (rounds are sequential, not parallel)
- Stage → Round
- StageType → RoundType
**Throughout this document:**
- **Competition** = the entire contest (e.g., "MOPC 2026")
- **Round** = one phase of the competition (e.g., "Jury 1 Evaluation")
- **RoundType** = the kind of round (INTAKE, FILTERING, EVALUATION, etc.)
- **Stage** = legacy term, avoid using
### 1.3 Project Status Evolution
```
Project.status progression:
DRAFT → SUBMITTED → PENDING → UNDER_REVIEW → SEMI_FINALIST → FINALIST → WINNER
↓ ↓ ↓ ↓ ↓ ↓
(Round 1) (Round 2) (Round 3) (Round 5) (Round 7) (Round 8)
Alternative paths:
SUBMITTED → FILTERED_OUT (Round 2 rejection)
PENDING → REJECTED (Round 3 rejection)
SEMI_FINALIST → REJECTED (Round 5 rejection)
FINALIST → NOT_SELECTED (Round 7/8 not chosen as winner)
```
### 1.4 ProjectRoundState vs Project.status
**ProjectRoundState** (per-round):
- Tracks a project's state within a specific round
- Values: `PENDING`, `IN_PROGRESS`, `PASSED`, `FAILED`, `WITHDRAWN`
- Example: A project can be `PASSED` in Round 3 but `PENDING` in Round 5
**Project.status** (global):
- Tracks the project's overall competition status
- Updated when crossing major milestones (semi-finalist, finalist, winner)
- Example: When `ProjectRoundState.status = PASSED` in Round 3, `Project.status → SEMI_FINALIST`
---
## 2. Competition Flow Diagram
```
┌────────────────────────────────────────────────────────────────────────────┐
│ MOPC 2026 COMPETITION FLOW │
└────────────────────────────────────────────────────────────────────────────┘
R1: INTAKE (Application Window)
├─ Opens: 2026-02-01
├─ Closes: 2026-05-31
├─ Participants: 150 applicants (all categories)
├─ Creates: SubmissionWindow 1 ("Application Documents")
├─ File requirements: Executive Summary, Business Plan, Team CV
├─ Deadline policy: FLAG (late submissions marked but accepted)
└─ Output: 150 projects with status SUBMITTED
├─ 90 STARTUP
└─ 60 BUSINESS_CONCEPT
R2: FILTERING (Eligibility Screening)
├─ Trigger: Admin-initiated after R1 closes
├─ No user deadline (automated/instant)
├─ Rules: Field-based + document-check + AI screening
├─ AI screening: GPT-4 with rubric (batch processing)
├─ Duplicate detection: Email-based (flags for review)
└─ Output: Projects split into three categories
├─ 120 PASSED (eligible, advance to R3)
├─ 15 FLAGGED (manual admin review required)
└─ 15 FILTERED_OUT (auto-rejected, visible to admins with reasons)
R3: EVALUATION (Jury 1 — Semi-Finalist Selection)
├─ Opens: 2026-06-05
├─ Closes: 2026-06-25
├─ Jury: Jury 1 (8 members)
├─ Participants: 120 eligible projects
├─ Assignment: 3 evaluations per project (hard/soft cap system)
├─ Scoring: Criteria-based (Innovation, Feasibility, Impact, Team)
├─ Visible documents: Window 1 ONLY
├─ AI recommendation: Generates ranked shortlist per category
└─ Output: Projects advancing become SEMI_FINALIST
├─ Admin selects top 20 STARTUP (from ranked list)
├─ Admin selects top 20 BUSINESS_CONCEPT
└─ 80 projects status → REJECTED
R4: SUBMISSION (Semi-Finalist Documents)
├─ Opens: 2026-06-28 (Window 1 auto-locks at this moment)
├─ Closes: 2026-07-20
├─ Creates: SubmissionWindow 2 ("Semi-Finalist Materials")
├─ Participants: 40 semi-finalists (20 STARTUP, 20 BUSINESS_CONCEPT)
├─ File requirements: Updated Pitch Deck, Video Pitch, Financial Projections
├─ Deadline policy: HARD (no late submissions)
├─ Previous window state: Window 1 becomes read-only for applicants
└─ Output: 40 projects with complete Round 2 submission bundles
├─ Window 1 files: locked, visible to jury
└─ Window 2 files: locked after deadline
R5: EVALUATION (Jury 2 — Finalist Selection + Special Awards)
├─ Opens: 2026-07-24
├─ Closes: 2026-08-10
├─ Jury: Jury 2 (12 members, may overlap with Jury 1)
├─ Participants: 40 semi-finalists
├─ Assignment: 3-5 evaluations per project
├─ Scoring: Criteria-based (Business Model, Team, Presentation, Viability)
├─ Visible documents: Window 1 AND Window 2 (tabbed interface)
├─ Special awards: Run alongside (see Section 7.7)
├─ AI recommendation: Ranked shortlist + suggested top N finalists
└─ Output: Projects advancing become FINALIST
├─ Admin selects top 10 STARTUP
├─ Admin selects top 10 BUSINESS_CONCEPT
├─ Special award winners selected separately
└─ 20 projects status → REJECTED
R6: MENTORING (Finalist Collaboration)
├─ Opens: 2026-08-15
├─ Closes: 2026-08-31
├─ Participants: 20 finalists who requested mentoring
├─ Assignment: 1 mentor per team (max 3 teams per mentor)
├─ Workspace: Private chat, file upload, threaded comments
├─ File promotion: Mentoring files can be promoted to official submissions
├─ NO judging or scoring (collaboration layer only)
└─ Output: Better-prepared finalist submissions
└─ Some mentoring files promoted → Window 2 (replaces previous versions)
R7: LIVE_FINAL (Jury 3 — Live Ceremony)
├─ Event date: 2026-09-15
├─ Jury: Jury 3 (8 members, may overlap with Jury 1/2)
├─ Participants: 20 finalists (10 STARTUP, 10 BUSINESS_CONCEPT)
├─ Presentation: 8-minute pitch + 5-minute Q&A per project
├─ Category windows: STARTUP window first, then BUSINESS_CONCEPT window
├─ Voting: Numeric scoring (1-10) with optional criteria
├─ Audience voting: Optional, weighted (typically 20% audience, 80% jury)
├─ Visible documents: Window 1 + Window 2 (all prior submissions)
├─ Deliberation: 30-minute period per category after voting
└─ Output: Jury 3 scores + audience totals
├─ Recommended winners per category (highest weighted score)
└─ Tied projects flagged for deliberation tie-breaker
R8: DELIBERATION (Final Winner Confirmation)
├─ Trigger: After R7 voting completes
├─ Mode: SINGLE_WINNER_VOTE (each juror picks one winner per category)
├─ Deliberation session: Per category (STARTUP and BUSINESS_CONCEPT independent)
├─ Collective ranking shown: Yes (transparency option)
├─ Tie-breaking: Runoff vote (if tied) → Admin break (if still tied)
├─ Admin override: Enabled (must provide mandatory reason + audit)
└─ Output: Final winners confirmed with ResultLock snapshot
├─ 1 STARTUP winner (status → WINNER)
├─ 1 BUSINESS_CONCEPT winner (status → WINNER)
├─ Special award winners (determined by award-specific juries)
└─ ResultUnlockEvent: Super-admin only, mandatory reason
┌────────────────────────────────────────────────────────────────────────────┐
│ END OF COMPETITION │
│ Winners announced publicly, prizes awarded, case studies │
└────────────────────────────────────────────────────────────────────────────┘
```
**Timeline Summary:**
- **R1 (Intake)**: 4 months (Feb 1 - May 31)
- **R2 (Filtering)**: 1 week (Jun 1 - Jun 5, automated)
- **R3 (Jury 1)**: 3 weeks (Jun 5 - Jun 25)
- **R4 (Submission)**: 3 weeks (Jun 28 - Jul 20)
- **R5 (Jury 2)**: 3 weeks (Jul 24 - Aug 10)
- **R6 (Mentoring)**: 2 weeks (Aug 15 - Aug 31)
- **R7 (Live Finals)**: 1 day (Sep 15)
- **R8 (Deliberation)**: Immediate (Sep 15-16)
**Total duration**: ~7.5 months from application open to winner announcement
---
## 3. R1: Intake (Application Window)
### 3.1 Purpose
The Intake round is the first phase of the competition. It opens a submission window where teams apply by uploading required documents and completing an application form.
**Key Objectives:**
- Collect applications from teams worldwide
- Capture project metadata (title, description, category, team info)
- Receive required documentation (executive summary, business plan, optional video)
- Support draft-save-and-continue workflow
- Enforce submission deadlines with configurable late policies
### 3.2 Prerequisites
**Before R1 can open:**
- Competition created with `Competition.status = ACTIVE`
- Round 1 configured with `RoundType = INTAKE`
- SubmissionWindow 1 created with file requirements
- Round 1 linked to SubmissionWindow 1 via `Round.submissionWindowId`
### 3.3 Configuration Shape (IntakeConfig)
Reference: `src/types/round-configs.ts`
```typescript
type IntakeConfig = {
// Application form (currently hardcoded, future: dynamic form builder)
applicationFormId: string // Links to ApplicationForm template
// Submission window reference
submissionWindowId: string // Which SubmissionWindow to use
// Deadline behavior
deadlinePolicy: 'HARD' | 'FLAG' | 'GRACE'
gracePeriodMinutes?: number // For GRACE policy (e.g., 180 = 3 hours)
// Draft system
allowDraftSubmissions: boolean // Save-and-continue enabled
draftExpiryDays: number // Auto-delete abandoned drafts after N days
// Team profile
requireTeamProfile: boolean // Require team member info
maxTeamSize: number // Max team members (including lead)
minTeamSize: number // Min team members (default: 1)
// Notifications
autoConfirmReceipt: boolean // Email confirmation on submission
reminderEmailSchedule: number[] // Days before deadline: [7, 3, 1]
// Public access
publicFormEnabled: boolean // Allow external application link
publicFormSlug?: string // Custom slug for public URL
// Category quotas (STARTUP vs BUSINESS_CONCEPT)
categoryQuotasEnabled: boolean
categoryQuotas?: {
STARTUP: number // Max startups accepted
BUSINESS_CONCEPT: number // Max concepts accepted
}
quotaOverflowPolicy?: 'reject' | 'waitlist'
// Custom fields (future: dynamic form builder)
customFields?: CustomFieldDef[]
}
```
### 3.4 Key Behaviors
**Multi-Step Application Form:**
1. **Step 1**: Project information (title, description, category, ocean issue)
2. **Step 2**: Team members (name, email, role, title)
3. **Step 3**: Document upload (executive summary, business plan, video pitch)
4. **Step 4**: Review & submit (confirmation checkboxes, final submit button)
**Auto-Save:**
- Client debounces and auto-saves form data every 30 seconds
- Draft status tracked via `Project.isDraft = true`
- Draft expiry: `draftExpiresAt = now + draftExpiryDays`
**Deadline Enforcement:**
```typescript
if (deadlinePolicy === 'HARD') {
// No submissions after windowCloseAt
if (now > windowCloseAt) return { canSubmit: false }
}
if (deadlinePolicy === 'FLAG') {
// Accept late submissions, mark as late
if (now > windowCloseAt) {
// Create ProjectFile with isLate = true
}
}
if (deadlinePolicy === 'GRACE') {
// Accept within grace period, then hard reject
const graceDeadline = windowCloseAt + gracePeriodMinutes
if (now > graceDeadline) return { canSubmit: false }
if (now > windowCloseAt) {
// Create ProjectFile with isLate = true
}
}
```
**Category Quotas:**
```typescript
async function checkCategoryQuota(category: 'STARTUP' | 'BUSINESS_CONCEPT') {
const submittedCount = await prisma.project.count({
where: {
competitionId,
competitionCategory: category,
isDraft: false
}
})
if (submittedCount >= categoryQuotas[category]) {
if (quotaOverflowPolicy === 'waitlist') {
// Create project with status WAITLISTED
} else {
// Reject submission
throw new Error(`Quota full for ${category}`)
}
}
}
```
### 3.5 Admin Controls
**Intake Dashboard:**
- View all submissions (real-time count)
- Filter by category, status (draft/submitted/late)
- Search by project title or team name
- Export submissions to CSV
- Extend deadline for individual applicants (GracePeriod record)
**Admin Override:**
- Can upload documents on behalf of applicants
- Can replace/delete files (with provenance tracking)
- Can extend submission window globally (update `windowCloseAt`)
- Can manually admit projects beyond quota
### 3.6 Output & Advancement Criteria
**Round 1 Closes:**
- All projects with `isDraft = false` become eligible for Round 2
- Projects with `isDraft = true` are excluded (abandoned drafts)
**ProjectRoundState updates:**
```typescript
// For each submitted project:
await prisma.projectRoundState.create({
data: {
projectId: project.id,
roundId: round1.id,
status: 'PASSED', // All submitted projects pass Round 1
enteredAt: project.submittedAt
}
})
// Update global status:
await prisma.project.update({
where: { id: project.id },
data: { status: 'SUBMITTED' }
})
```
**Advancement to R2:**
- Automatic when R1 window closes (or admin manually triggers R2)
- All projects with `ProjectRoundState.status = PASSED` in R1 become `PENDING` in R2
---
## 4. R2: Filtering (Eligibility Screening)
### 4.1 Purpose
The Filtering round performs automated screening of applications to identify eligible projects, detect duplicates, and flag edge cases for admin review.
**Key Objectives:**
- Automated eligibility checks (field-based rules, document checks)
- AI-powered screening (GPT rubric evaluation with confidence banding)
- Duplicate detection (email-based cross-application similarity)
- Manual review queue for flagged projects
- Admin override system with full audit trail
### 4.2 Prerequisites
**Before R2 can run:**
- Round 1 (INTAKE) has closed
- All submitted projects have `ProjectRoundState.status = PASSED` in R1
- FilteringRule records configured for Round 2
- Round 2 configured with `RoundType = FILTERING`
### 4.3 Configuration Shape (FilteringConfig)
Reference: `src/types/round-configs.ts`
```typescript
type FilteringConfig = {
// Rule engine
rules: FilterRuleDef[] // Configured rules (can be empty)
// AI screening
aiScreeningEnabled: boolean
aiRubricPrompt: string // Custom rubric for AI
aiConfidenceThresholds: {
high: number // Above this = auto-pass (default: 0.85)
medium: number // Above this = flag (default: 0.6)
low: number // Below this = auto-reject (default: 0.4)
}
aiBatchSize: number // Projects per AI batch (default: 20, max: 50)
aiParallelBatches: number // Concurrent batches (default: 1, max: 10)
// Duplicate detection
duplicateDetectionEnabled: boolean
duplicateThreshold: number // Email similarity threshold (0-1, default: 1.0)
duplicateAction: 'FLAG' | 'AUTO_REJECT' // Default: FLAG
// Advancement behavior
autoAdvancePassingProjects: boolean // Auto-advance PASSED projects to R3
manualReviewRequired: boolean // All results require admin approval
// Eligibility criteria
eligibilityCriteria: EligibilityCriteria[]
// Category-specific rules
categorySpecificRules: {
STARTUP?: CategoryRuleSet
BUSINESS_CONCEPT?: CategoryRuleSet
}
}
```
### 4.4 Key Behaviors
**Rule Evaluation Order:**
```
1. Built-in Duplicate Detection (if enabled)
2. FIELD_CHECK rules (sorted by priority ascending)
3. DOCUMENT_CHECK rules (sorted by priority ascending)
4. AI_SCORE rules (if aiScreeningEnabled) — batch processed
5. Determine final outcome: PASSED | FILTERED_OUT | FLAGGED
```
**Rule Combination Logic:**
```typescript
let finalOutcome: 'PASSED' | 'FILTERED_OUT' | 'FLAGGED' = 'PASSED'
for (const rule of rules.sort((a, b) => a.priority - b.priority)) {
const result = evaluateRule(rule, project)
if (!result.passed) {
if (rule.action === 'REJECT') {
finalOutcome = 'FILTERED_OUT'
break // Short-circuit
} else if (rule.action === 'FLAG') {
finalOutcome = 'FLAGGED'
// Continue to next rule
}
}
}
// Override: Duplicates always flagged (never auto-rejected)
if (isDuplicate && finalOutcome === 'FILTERED_OUT') {
finalOutcome = 'FLAGGED'
}
```
**AI Screening Pipeline:**
```
1. Load projects with ProjectRoundState PENDING/IN_PROGRESS
2. Anonymize project data (strip PII via anonymization.ts)
3. Batch projects (configurable size: 1-50, default 20)
4. Parallel processing (configurable: 1-10 concurrent batches)
5. OpenAI API call (GPT-4o with rubric)
6. Parse JSON response: { project_id, meets_criteria, confidence, reasoning, quality_score, spam_risk }
7. Map anonymous IDs → real project IDs
8. Band by confidence threshold:
- confidence ≥ 0.85 + meets_criteria → PASSED
- confidence 0.6-0.84 → FLAGGED
- confidence ≤ 0.39 + !meets_criteria → FILTERED_OUT
9. Store results in FilteringResult table
10. Log token usage (AIUsageLog)
```
**Duplicate Detection:**
```typescript
const emailToProjects = new Map<string, Array<{ id: string; title: string }>>()
for (const project of projects) {
const email = (project.submittedByEmail ?? '').toLowerCase().trim()
if (!email) continue
if (!emailToProjects.has(email)) emailToProjects.set(email, [])
emailToProjects.get(email)!.push({ id: project.id, title: project.title })
}
// Flag all projects in groups of size > 1
emailToProjects.forEach((group) => {
if (group.length <= 1) return
for (const p of group) {
duplicateProjectIds.add(p.id)
// Store metadata: { isDuplicate: true, siblingProjectIds: [...], duplicateNote: "..." }
}
})
```
### 4.5 Admin Controls
**Filtering Dashboard:**
- Results summary (passed/rejected/flagged counts)
- AI usage stats (tokens, cost, processing time)
- Manual review queue (flagged projects only)
- Per-project detail view:
- Rule results (which rules passed/failed)
- AI screening JSON (confidence, reasoning, quality score)
- Duplicate metadata (sibling project IDs)
- Batch actions:
- Approve all flagged
- Reject all flagged
- Override individual decisions
**Manual Override:**
```typescript
async function resolveManualDecision(
filteringResultId: string,
outcome: 'PASSED' | 'FILTERED_OUT',
reason: string,
actorId: string
) {
await prisma.filteringResult.update({
where: { id: filteringResultId },
data: {
finalOutcome: outcome,
overriddenBy: actorId,
overriddenAt: new Date(),
overrideReason: reason
}
})
// Update ProjectRoundState
await prisma.projectRoundState.update({
where: { roundId_projectId: { roundId: round2.id, projectId } },
data: { status: outcome === 'PASSED' ? 'PASSED' : 'FAILED' }
})
// Audit log
await createAuditLog({
action: 'FILTERING_MANUAL_DECISION',
userId: actorId,
entityType: 'FilteringResult',
entityId: filteringResultId,
metadata: { outcome, reason }
})
}
```
### 4.6 Output & Advancement Criteria
**Round 2 Completes:**
- All projects have `FilteringResult.outcome` (or `FilteringResult.finalOutcome` if overridden)
- Projects split into three buckets:
- **PASSED**: Advance to Round 3 (Jury 1)
- **FILTERED_OUT**: Excluded from competition (status → REJECTED)
- **FLAGGED**: Awaiting admin review (manual queue)
**ProjectRoundState updates:**
```typescript
// For each project:
if (finalOutcome === 'PASSED') {
await prisma.projectRoundState.update({
where: { roundId_projectId: { roundId: round2.id, projectId } },
data: { status: 'PASSED' }
})
}
if (finalOutcome === 'FILTERED_OUT') {
await prisma.projectRoundState.update({
where: { roundId_projectId: { roundId: round2.id, projectId } },
data: { status: 'FAILED' }
})
await prisma.project.update({
where: { id: projectId },
data: { status: 'REJECTED' }
})
}
if (finalOutcome === 'FLAGGED') {
// Remains in PENDING state until admin resolves
}
```
**Advancement to R3:**
- Manual trigger (admin clicks "Advance Passing Projects")
- All projects with `ProjectRoundState.status = PASSED` in R2 become `PENDING` in R3
---
## 5. R3: Evaluation (Jury 1 — Semi-Finalist Selection)
### 5.1 Purpose
The first evaluation round where Jury 1 reviews eligible projects and selects semi-finalists.
**Key Objectives:**
- Expert jury evaluation of all passing projects from R2
- Criteria-based scoring (Innovation, Feasibility, Impact, Team)
- Independent evaluations (3 per project)
- AI-generated ranked shortlist per category
- Admin-confirmed semi-finalist selection
### 5.2 Prerequisites
**Before R3 can open:**
- Round 2 (FILTERING) completed
- All eligible projects have `ProjectRoundState.status = PASSED` in R2
- Jury 1 (JuryGroup) created with members assigned
- Round 3 configured with `RoundType = EVALUATION`
- Round 3 linked to Jury 1 via `Round.juryGroupId`
- Assignment algorithm run (or admin manually assigns)
- RoundSubmissionVisibility configured (R3 sees Window 1 only)
### 5.3 Configuration Shape (EvaluationConfig)
Reference: `src/types/round-configs.ts`
```typescript
type EvaluationConfig = {
// Assignment settings
requiredReviewsPerProject: number // How many jurors review each project (default: 3)
// Scoring mode
scoringMode: 'criteria' | 'global' | 'binary'
requireFeedback: boolean // Must provide text feedback (default: true)
// COI (Conflict of Interest)
coiRequired: boolean // Must declare COI before evaluating (default: true)
// Peer review
peerReviewEnabled: boolean // Jurors see anonymized peer evaluations after submission
anonymizationLevel: 'fully_anonymous' | 'show_initials' | 'named'
// AI features
aiSummaryEnabled: boolean // Generate AI-powered evaluation summaries
aiAssignmentEnabled: boolean // Allow AI-suggested jury-project matching
// Advancement
advancementMode: 'auto_top_n' | 'admin_selection' | 'ai_recommended'
advancementConfig: {
perCategory: boolean // Separate counts per STARTUP / BUSINESS_CONCEPT
startupCount: number // How many startups advance (default: 10)
conceptCount: number // How many concepts advance
tieBreaker: 'admin_decides' | 'highest_individual' | 'revote'
}
}
```
### 5.4 Key Behaviors
**Assignment System (Hard/Soft Cap Logic):**
```typescript
function canAssignMore(
jurorId: string,
projectCategory: 'STARTUP' | 'BUSINESS_CONCEPT',
currentLoad: LoadTracker,
limits: EffectiveLimits
): { allowed: boolean; penalty: number; reason?: string } {
const total = currentLoad.total(jurorId)
const catLoad = currentLoad.byCategory(jurorId, projectCategory)
// 1. HARD cap check
if (limits.capMode === 'HARD' && total >= limits.maxAssignments) {
return { allowed: false, penalty: 0, reason: 'Hard cap reached' }
}
// 2. SOFT cap check (can exceed by buffer)
let overflowPenalty = 0
if (limits.capMode === 'SOFT') {
if (total >= limits.maxAssignments + limits.softCapBuffer) {
return { allowed: false, penalty: 0, reason: 'Soft cap + buffer exceeded' }
}
if (total >= limits.maxAssignments) {
// In buffer zone — apply increasing penalty
overflowPenalty = (total - limits.maxAssignments + 1) * 15
}
}
// 3. Category quota check
if (limits.categoryQuotasEnabled && limits.categoryQuotas) {
const quota = limits.categoryQuotas[projectCategory]
if (quota) {
if (catLoad >= quota.max) {
return { allowed: false, penalty: 0, reason: `Category ${projectCategory} max reached` }
}
if (catLoad < quota.min) {
overflowPenalty -= 15 // Bonus for under-min
}
}
}
// 4. Ratio preference alignment
if (limits.preferredStartupRatio != null && total > 0) {
const currentStartupRatio = currentLoad.byCategory(jurorId, 'STARTUP') / total
const isStartup = projectCategory === 'STARTUP'
const wantMore = isStartup
? currentStartupRatio < limits.preferredStartupRatio
: currentStartupRatio > limits.preferredStartupRatio
if (wantMore) overflowPenalty -= 10 // Bonus
else overflowPenalty += 10 // Penalty
}
return { allowed: true, penalty: overflowPenalty }
}
```
**COI Declaration (Blocking):**
```typescript
// Before evaluating any project, juror MUST declare COI
async function checkCOI(jurorId: string, projectId: string): Promise<boolean> {
const coi = await prisma.conflictOfInterest.findFirst({
where: { userId: jurorId, projectId }
})
if (!coi) {
// Show blocking dialog: "Do you have a conflict of interest?"
return false // Block evaluation
}
if (coi.hasConflict) {
// Assignment flagged, admin notified, juror may be reassigned
return false // Block evaluation
}
return true // Can proceed
}
```
**Evaluation Submission:**
```typescript
async function submitEvaluation(
assignmentId: string,
data: { scores, feedback, recommendation },
userId: string
) {
// Validate window is open (or juror has grace period)
const assignment = await prisma.assignment.findUnique({
where: { id: assignmentId },
include: { round: true }
})
if (now > assignment.round.windowCloseAt) {
// Check grace period
const grace = await prisma.gracePeriod.findFirst({
where: {
roundId: assignment.roundId,
userId,
extendedUntil: { gt: now }
}
})
if (!grace) throw new Error('Evaluation window closed')
}
// Create evaluation record
await prisma.evaluation.create({
data: {
assignmentId,
userId,
scores: data.scores,
feedback: data.feedback,
recommendation: data.recommendation,
status: 'SUBMITTED',
submittedAt: new Date()
}
})
}
```
**AI Ranked Shortlist:**
```typescript
// After all evaluations submitted
async function generateAIShortlist(roundId: string) {
// Fetch all projects with evaluations
const projects = await prisma.project.findMany({
where: {
projectRoundStates: {
some: { roundId, status: 'IN_PROGRESS' }
}
},
include: {
evaluations: {
where: { assignmentId: { in: assignmentIds } }
}
}
})
// Anonymize + send to OpenAI
const anonymized = anonymizeProjects(projects)
const prompt = `
Analyze these projects based on jury scores and feedback.
Rank them per category (STARTUP, BUSINESS_CONCEPT).
For each project: { rank, reasoning, strengths, weaknesses, recommendation }
`
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'system', content: prompt }, { role: 'user', content: JSON.stringify(anonymized) }]
})
// Store AI recommendation
return parseAIRankedShortlist(response)
}
```
### 5.5 Admin Controls
**Evaluation Dashboard:**
- Completion stats (evaluations submitted / total required)
- Per-juror progress (assigned projects, completed evaluations)
- Per-project status (evaluations received / required)
- Results visualization:
- Ranked list per category (average score, consensus, review count)
- AI recommended shortlist (if aiSummaryEnabled)
- Score distribution charts
- Override tools:
- Manually select advancing projects (drag to reorder)
- Accept AI recommendation
- Set custom cutoff line
- Force-advance or force-reject individual projects
**Advancement Decision:**
```typescript
async function confirmAdvancement(
roundId: string,
selectedProjectIds: string[],
actorId: string
) {
// Update ProjectRoundState
for (const projectId of selectedProjectIds) {
await prisma.projectRoundState.update({
where: { roundId_projectId: { roundId, projectId } },
data: { status: 'PASSED' }
})
// Update global status
await prisma.project.update({
where: { id: projectId },
data: { status: 'SEMI_FINALIST' }
})
}
// Reject non-selected projects
const allProjects = await prisma.projectRoundState.findMany({
where: { roundId, status: 'IN_PROGRESS' }
})
const rejectedIds = allProjects
.map(prs => prs.projectId)
.filter(id => !selectedProjectIds.includes(id))
for (const projectId of rejectedIds) {
await prisma.projectRoundState.update({
where: { roundId_projectId: { roundId, projectId } },
data: { status: 'FAILED' }
})
await prisma.project.update({
where: { id: projectId },
data: { status: 'REJECTED' }
})
}
// Send notifications
await notifyAdvancingTeams(selectedProjectIds)
await notifyRejectedTeams(rejectedIds)
// Audit log
await createAuditLog({
action: 'ADVANCEMENT_CONFIRMED',
userId: actorId,
entityType: 'Round',
entityId: roundId,
metadata: { selectedCount: selectedProjectIds.length, rejectedCount: rejectedIds.length }
})
}
```
### 5.6 Output & Advancement Criteria
**Round 3 Completes:**
- Admin has selected semi-finalists (typically top N per category)
- Selected projects: `ProjectRoundState.status = PASSED`, `Project.status = SEMI_FINALIST`
- Rejected projects: `ProjectRoundState.status = FAILED`, `Project.status = REJECTED`
**Advancement to R4:**
- All projects with `ProjectRoundState.status = PASSED` in R3 become eligible for R4 (SUBMISSION)
- Round 4 opens automatically or via admin trigger
---
## 6. R4: Submission (Semi-Finalist Documents)
### 6.1 Purpose
The second submission window where semi-finalists submit additional documents required for Jury 2 evaluation.
**Key Objectives:**
- Collect new documents from advancing teams (video pitch, updated materials)
- Lock previous submission window (Window 1) to read-only for applicants
- Maintain separate file requirements per window
- Prevent editing of Round 1 materials while Round 2 is open
- Full admin control over all windows regardless of lock state
### 6.2 Prerequisites
**Before R4 can open:**
- Round 3 (EVALUATION) completed
- Semi-finalists selected with `ProjectRoundState.status = PASSED` in R3
- SubmissionWindow 2 created with file requirements
- Round 4 configured with `RoundType = SUBMISSION`
- Round 4 linked to SubmissionWindow 2 via `Round.submissionWindowId`
### 6.3 Configuration Shape (SubmissionConfig)
Reference: `src/types/round-configs.ts`
```typescript
type SubmissionConfig = {
// Eligibility
eligibleStatuses: ProjectRoundStateValue[] // Which statuses from previous round can submit
// Default: ['PASSED']
// Notifications
notifyEligibleTeams: boolean // Email teams when window opens (default: true)
// Locking
lockPreviousWindows: boolean // Lock all previous windows when this opens (default: true)
// Optional: Custom email template
notificationTemplate?: {
subject: string
bodyHtml: string
variables: Record<string, string>
}
// Optional: Window configuration
windowConfig?: {
name: string
description: string
openDate: string // ISO 8601
closeDate: string // ISO 8601
latePolicy: 'HARD' | 'FLAG' | 'GRACE'
gracePeriodHours?: number
}
// Optional: File requirements
fileRequirements?: Array<{
label: string
description?: string
isRequired: boolean
allowedFileTypes: string[]
maxSizeMB: number
displayOrder: number
}>
}
```
### 6.4 Key Behaviors
**Window Opening (Automatic Lock):**
```typescript
async function openSubmissionWindow(windowId: string, ctx: Context) {
const window = await ctx.prisma.submissionWindow.findUnique({
where: { id: windowId },
include: { round: { select: { configJson: true, competitionId: true } } }
})
const config = window.round.configJson as SubmissionConfig
// Step 1: Set this window's openDate to now
await ctx.prisma.submissionWindow.update({
where: { id: windowId },
data: { openDate: new Date() }
})
// Step 2: Lock previous windows if config says so
if (config.lockPreviousWindows) {
const allWindows = await ctx.prisma.submissionWindow.findMany({
where: {
competitionId: window.competitionId,
openDate: { lt: new Date() } // Opened before now
}
})
for (const prevWindow of allWindows) {
if (!prevWindow.isLocked) {
await ctx.prisma.submissionWindow.update({
where: { id: prevWindow.id },
data: {
isLocked: true,
lockDate: new Date()
}
})
}
}
}
// Step 3: Notify eligible teams
if (config.notifyEligibleTeams) {
await notifyEligibleTeams(windowId, ctx)
}
}
```
**Upload Permission Check:**
```typescript
async function checkUploadPermission(
projectId: string,
windowId: string,
userId: string,
ctx: Context
): Promise<{ allowed: boolean; reason?: string }> {
const user = await ctx.prisma.user.findUnique({ where: { id: userId } })
// Admins bypass all checks
if (user?.role === 'SUPER_ADMIN' || user?.role === 'PROGRAM_ADMIN') {
return { allowed: true }
}
// Check: User owns this project?
const project = await ctx.prisma.project.findFirst({
where: { id: projectId, applicantId: userId }
})
if (!project) {
return { allowed: false, reason: 'Unauthorized' }
}
// Check: Window locked?
const window = await ctx.prisma.submissionWindow.findUnique({ where: { id: windowId } })
if (window.isLocked) {
return { allowed: false, reason: 'This submission window is now closed.' }
}
// Check: Window not yet open?
const now = new Date()
if (now < window.openDate) {
return { allowed: false, reason: 'Submission window has not opened yet.' }
}
// Check: Deadline enforcement (HARD/FLAG/GRACE)
if (now > window.closeDate) {
if (window.latePolicy === 'HARD') {
return { allowed: false, reason: 'Deadline has passed. No late submissions allowed.' }
}
if (window.latePolicy === 'FLAG') {
return { allowed: true } // Will be marked late
}
if (window.latePolicy === 'GRACE') {
if (window.lockDate && now > window.lockDate) {
return { allowed: false, reason: 'Grace period has ended.' }
}
return { allowed: true } // Within grace period
}
}
return { allowed: true }
}
```
**Applicant View (Locked vs Open Windows):**
**Before R4 opens (R3 just finished):**
```
┌─ Round 1: Application Documents (OPEN) ─────────────┐
│ ✅ Pitch Deck │
│ [View] [Download] [Replace] ← Can still edit │
│ │
│ ✅ Budget │
│ [View] [Download] [Replace] │
└──────────────────────────────────────────────────────┘
```
**After R4 opens (Window 1 auto-locks):**
```
┌─ 🔒 Round 1: Application Documents (LOCKED) ────────┐
│ ✅ Pitch Deck │
│ [View] [Download] ← No Replace button │
│ │
│ ✅ Budget │
│ [View] [Download] │
│ │
These documents are locked. Contact admin if │
│ you need to make changes. │
└──────────────────────────────────────────────────────┘
┌─ 📤 Round 2: Semi-Finalist Materials (OPEN) ────────┐
│ ❌ Updated Pitch Deck (Required) │
│ [Choose File] or Drag & Drop Here │
│ │
│ ❌ Video Pitch (Required) │
│ [Choose File] or Drag & Drop Here │
│ │
│ ❌ Financial Projections (Required) │
│ [Choose File] or Drag & Drop Here │
└──────────────────────────────────────────────────────┘
```
### 6.5 Admin Controls
**Submission Dashboard:**
- View all eligible teams (semi-finalists)
- Completion status (files submitted / required)
- Late submission indicators
- Upload files on behalf of teams
- Replace/delete files (with provenance)
- Unlock windows (emergency override)
**File Replacement (Admin Only):**
```typescript
async function adminReplaceFile(
projectFileId: string,
newFile: { fileName, mimeType, size, bucket, objectKey },
actorId: string,
reason: string
) {
// Mark old file as superseded
await prisma.projectFile.update({
where: { id: projectFileId },
data: {
supersededBy: newFileId,
supersededAt: new Date()
}
})
// Create new file record
const newProjectFile = await prisma.projectFile.create({
data: {
projectId,
submissionWindowId,
requirementId,
fileName: newFile.fileName,
mimeType: newFile.mimeType,
sizeBytes: newFile.size,
storagePath: newFile.objectKey,
uploadedBy: actorId,
uploadedAt: new Date(),
version: oldFile.version + 1
}
})
// Audit log
await createAuditLog({
action: 'FILE_REPLACED_BY_ADMIN',
userId: actorId,
entityType: 'ProjectFile',
entityId: newProjectFile.id,
metadata: { oldFileId: projectFileId, reason }
})
}
```
### 6.6 Output & Advancement Criteria
**Round 4 Closes:**
- All semi-finalists have submitted required documents (or flagged as incomplete)
- Window 2 locked automatically (or admin manually locks)
**ProjectRoundState updates:**
```typescript
// For each semi-finalist who completed submission:
await prisma.projectRoundState.update({
where: { roundId_projectId: { roundId: round4.id, projectId } },
data: { status: 'PASSED' }
})
// For incomplete submissions:
await prisma.projectRoundState.update({
where: { roundId_projectId: { roundId: round4.id, projectId } },
data: { status: 'FAILED' } // Or admin can override
})
```
**Advancement to R5:**
- All projects with `ProjectRoundState.status = PASSED` in R4 become `PENDING` in R5
---
## 7. R5: Evaluation (Jury 2 — Finalist Selection)
### 7.1 Purpose
The second evaluation round where Jury 2 reviews semi-finalists and selects finalists. Special awards may run alongside.
**Key Objectives:**
- Expert jury evaluation of semi-finalists
- Access to both Round 1 and Round 2 documents (multi-window visibility)
- Criteria-based scoring (Business Model, Team, Presentation, Viability)
- Special award eligibility and selection (parallel process)
- AI-generated ranked shortlist + suggested top N finalists
- Admin-confirmed finalist selection
### 7.2 Prerequisites
**Before R5 can open:**
- Round 4 (SUBMISSION) completed
- All semi-finalists have `ProjectRoundState.status = PASSED` in R4
- Jury 2 (JuryGroup) created with members assigned
- Round 5 configured with `RoundType = EVALUATION`
- Round 5 linked to Jury 2 via `Round.juryGroupId`
- RoundSubmissionVisibility configured (R5 sees Window 1 AND Window 2)
### 7.3 Configuration Shape
Same as R3 (EvaluationConfig), but with:
- `advancementConfig.startupCount`: Lower than R3 (e.g., 10 instead of 20)
- `advancementConfig.conceptCount`: Lower than R3 (e.g., 10 instead of 20)
### 7.4 Key Behaviors
**Multi-Window Document Visibility:**
```typescript
// Fetch all visible files for a project in R5
const visibleWindows = await prisma.roundSubmissionVisibility.findMany({
where: { evaluationRoundId: round5.id },
include: {
submissionWindow: {
include: { fileRequirements: { orderBy: { displayOrder: 'asc' } } }
}
},
orderBy: { displayOrder: 'asc' }
})
// Returns two windows:
// [
// { windowId: 'window_1', displayLabel: 'Round 1 Application', displayOrder: 1 },
// { windowId: 'window_2', displayLabel: 'Semi-Final Submissions', displayOrder: 2 }
// ]
// UI shows tabbed interface:
// Tab 1: "Round 1 Application" (Pitch Deck, Budget, Team CV from Window 1)
// Tab 2: "Semi-Final Submissions" (Updated Pitch, Video, Financials from Window 2)
```
**Jury View (Multi-Window):**
```
┌─ PROJECT DOCUMENTS ──────────────────────────────────┐
│ [ Round 1 Application ] [ Semi-Final Materials ] │
│ ▲ Active tab │
└──────────────────────────────────────────────────────┘
┌─ Round 1 Application ────────────────────────────────┐
│ 📄 Pitch Deck (PDF, 2.4 MB) │
│ Uploaded: Jan 25, 2026 │
│ [View] [Download] │
│ │
│ 📄 Budget (XLSX, 850 KB) │
│ Uploaded: Feb 1, 2026 │
│ [View] [Download] │
│ │
│ 📄 Team CV (PDF, 1.2 MB) │
│ Uploaded: Jan 25, 2026 │
│ [View] [Download] │
└──────────────────────────────────────────────────────┘
(Click "Semi-Final Materials" tab)
┌─ Semi-Final Materials ───────────────────────────────┐
│ 📄 Updated Pitch Deck (PDF, 3.1 MB) │
│ Uploaded: Mar 10, 2026 │
│ [View] [Download] │
│ │
│ 🎥 Video Pitch (MP4, 85.2 MB) │
│ Uploaded: Mar 1, 2026 │
│ [View] [Download] │
│ │
│ 📄 Financial Projections (XLSX, 1.5 MB) │
│ Uploaded: Mar 16, 2026 ⚠️ LATE SUBMISSION │
│ [View] [Download] │
│ ⚠️ This file was submitted after the deadline │
└──────────────────────────────────────────────────────┘
```
### 7.5 Special Awards Integration
**How Special Awards Work Alongside R5:**
During the Jury 2 evaluation round, special awards can run in parallel:
**Award Configuration:**
```typescript
type SpecialAward = {
id: string
name: string // "Innovation Award", "Impact Award"
evaluationRoundId: string // Links to R5 (runs alongside Jury 2)
eligibilityMode: 'STAY_IN_MAIN' | 'SEPARATE_POOL'
juryGroupId: string // Award-specific jury (or subset of Jury 2)
votingMode: 'PICK_WINNER' | 'RANKED' | 'SCORED'
}
```
**Award Eligibility Flow:**
```
1. Before R5 opens: Admin runs award eligibility (AI or manual)
- Projects flagged as eligible for specific awards
- Mode A (SEPARATE_POOL): Projects may be pulled from main (admin confirms)
- Mode B (STAY_IN_MAIN): Projects remain in main, flagged "eligible for award"
2. During R5 evaluation window:
- Main Jury 2: Evaluates all semi-finalists
- Award juries: Evaluate award-eligible projects (parallel)
- Award jury members see their award assignments alongside regular evaluations
3. After R5 closes:
- Main results: Jury 2 selects finalists
- Award results: Award juries select award winners
- Both processes independent (separate deliberations, separate confirmations)
```
**Award Jury Dashboard:**
```
┌─ MY EVALUATIONS ─────────────────────────────────────┐
│ Main Competition (Jury 2) │
│ ├─ Project A (STARTUP) — ✅ Completed │
│ ├─ Project B (STARTUP) — ⏳ In Progress │
│ └─ Project C (CONCEPT) — ⬜ Pending │
│ │
│ Innovation Award │
│ ├─ Project X (Award Eligible) — ✅ Completed │
│ └─ Project Y (Award Eligible) — ⏳ In Progress │
└──────────────────────────────────────────────────────┘
```
### 7.6 Admin Controls
Same as R3, plus:
- **Special Award Management**:
- Run award eligibility (AI or manual filtering)
- Assign award juries
- View award voting progress
- Confirm award winners (separate from main finalists)
### 7.7 Output & Advancement Criteria
**Round 5 Completes:**
- Admin has selected finalists (typically top 10 per category)
- Selected projects: `ProjectRoundState.status = PASSED`, `Project.status = FINALIST`
- Rejected projects: `ProjectRoundState.status = FAILED`, `Project.status = REJECTED`
- Special award winners: Separate tracking (SpecialAwardWinner records)
**Advancement to R6:**
- All projects with `ProjectRoundState.status = PASSED` in R5 become eligible for R6 (MENTORING)
- Only projects with `wantsMentorship = true` get mentor assignments
---
## 8. R6: Mentoring (Finalist Collaboration)
### 8.1 Purpose
The Mentoring round is NOT a judging stage — it is a collaboration layer that provides finalist teams with a private workspace to refine submissions with guidance from an assigned mentor.
**Key Objectives:**
- One-on-one mentor-team collaboration
- Private workspace with chat, file upload, threaded comments
- File promotion from workspace to official submission
- Better-prepared finalists for Live Finals (R7)
### 8.2 Prerequisites
**Before R6 can open:**
- Round 5 (EVALUATION) completed
- Finalists selected with `ProjectRoundState.status = PASSED` in R5
- Mentors recruited and added to platform with role `MENTOR`
- Round 6 configured with `RoundType = MENTORING`
### 8.3 Configuration Shape (MentoringConfig)
```typescript
type MentoringConfig = {
// Who gets mentoring
eligibility: 'all_advancing' | 'requested_only'
// Workspace features
chatEnabled: boolean // Bidirectional messaging (default: true)
fileUploadEnabled: boolean // Mentor + team can upload files (default: true)
fileCommentsEnabled: boolean // Threaded comments on files (default: true)
filePromotionEnabled: boolean // Promote workspace file to official submission (default: true)
// Promotion target
promotionTargetWindowId: string | null // Which SubmissionWindow promoted files go to
// Auto-assignment
autoAssignMentors: boolean // Use AI/algorithm to assign (default: false)
maxProjectsPerMentor: number // Mentor workload cap (default: 3)
// Notifications
notifyTeamsOnOpen: boolean // Email teams when mentoring opens (default: true)
notifyMentorsOnAssign: boolean // Email mentors when assigned (default: true)
reminderBeforeClose: number[] // Days before close to remind (default: [7, 3, 1])
}
```
### 8.4 Key Behaviors
**Workspace Activation:**
```typescript
// When mentor assigned and R6 opens:
await prisma.mentorAssignment.update({
where: { id: assignmentId },
data: {
workspaceEnabled: true,
workspaceOpenAt: round.windowOpenAt,
workspaceCloseAt: round.windowCloseAt
}
})
```
**File Promotion Flow:**
```
1. Team member (or admin) clicks "Promote →" on a workspace file
2. Dialog appears:
- Select target submission window (Window 2)
- Select requirement slot (e.g., "Business Plan")
- Confirm replacement (if slot already has a file)
3. On confirmation:
a. Create new ProjectFile record:
- projectId: team's project ID
- submissionWindowId: selected window
- requirementId: selected requirement slot
- fileName, mimeType, size: copied from MentorFile
- bucket, objectKey: SAME as MentorFile (no file duplication)
- version: incremented from previous file in slot
b. Mark previous file as superseded:
- Old ProjectFile: supersededBy = new file ID, supersededAt = now
c. Update MentorFile flags:
- isPromoted: true
- promotedToFileId: new ProjectFile ID
- promotedAt: now
- promotedByUserId: actor ID
d. Audit log entry:
- action: "MENTOR_FILE_PROMOTED"
- details: { mentorFileId, projectFileId, submissionWindowId, requirementId, replacedFileId }
```
**Privacy Model:**
```
Visibility Matrix:
┌──────────────────┬────────┬──────────┬───────┬──────┐
│ Content │ Mentor │ Team │ Admin │ Jury │
├──────────────────┼────────┼──────────┼───────┼──────┤
│ Chat messages │ ✅ │ ✅ │ ✅ │ ❌ │
│ Workspace files │ ✅ │ ✅ │ ✅ │ ❌ │
│ File comments │ ✅ │ ✅ │ ✅ │ ❌ │
│ Mentor notes │ ✅ │ ❌ │ ✅* │ ❌ │
│ Promoted files │ ✅ │ ✅ │ ✅ │ ✅** │
└──────────────────┴────────┴──────────┴───────┴──────┘
* Only if MentorNote.isVisibleToAdmin = true
** Promoted files become official submissions visible to Jury 3
```
### 8.5 Admin Controls
**Mentoring Dashboard:**
- View all mentor assignments
- Track activity (messages sent, files uploaded, milestones completed)
- Reassign mentors (if mentor goes inactive)
- View any workspace (read-only or full edit access)
- Promote files on behalf of teams
- Extend mentoring window (per team or globally)
### 8.6 Output & Advancement Criteria
**Round 6 Closes:**
- Mentoring workspaces become read-only
- Promoted files remain as official submissions in Window 2
- All finalists remain with `ProjectRoundState.status = PASSED` in R6
**Advancement to R7:**
- All projects with `ProjectRoundState.status = PASSED` in R6 become eligible for R7 (LIVE_FINAL)
---
## 9. R7: Live Finals (Jury 3 — Live Ceremony)
### 9.1 Purpose
The Live Finals round orchestrates the live ceremony where Jury 3 evaluates finalist presentations in real-time, with optional audience participation.
**Key Objectives:**
- Real-time stage manager controls (presentation cursor, timing, pause/resume)
- Jury voting with multiple modes (numeric, ranking, binary)
- Optional audience voting with weighted scores
- Per-category presentation windows (STARTUP, then BUSINESS_CONCEPT)
- Deliberation period for jury discussion
- Live results display or ceremony reveal
### 9.2 Prerequisites
**Before R7 can open:**
- Round 6 (MENTORING) completed (or skipped)
- All finalists have `ProjectRoundState.status = PASSED` in R5/R6
- Jury 3 (JuryGroup) created with members assigned
- Round 7 configured with `RoundType = LIVE_FINAL`
- LiveVotingSession created for R7
- Presentation order configured (per category)
### 9.3 Configuration Shape (LiveFinalConfig)
```typescript
type LiveFinalConfig = {
// Jury
juryGroupId: string // Which jury evaluates (Jury 3)
// Voting mode
votingMode: 'NUMERIC' | 'RANKING' | 'BINARY'
// Numeric mode settings
numericScale?: {
min: number // Default: 1
max: number // Default: 10
allowDecimals: boolean // Default: false
}
// Criteria-based voting (optional enhancement)
criteriaEnabled?: boolean
criteriaJson?: LiveVotingCriterion[]
importFromEvalForm?: string // Import criteria from prior evaluation form
// Ranking mode settings
rankingSettings?: {
maxRankedProjects: number // Top N projects each juror ranks
pointsSystem: 'DESCENDING' | 'BORDA' // 3-2-1 or Borda count
}
// Audience voting
audienceVotingEnabled: boolean
audienceVotingWeight: number // 0-100, percentage weight
juryVotingWeight: number // complement (must sum to 100)
audienceVotingMode: 'PER_PROJECT' | 'FAVORITES' | 'CATEGORY_FAVORITES'
audienceMaxFavorites?: number // For FAVORITES mode
audienceRequireIdentification: boolean
audienceAntiSpamMeasures: {
ipRateLimit: boolean
deviceFingerprint: boolean
emailVerification: boolean
}
// Timing
presentationDurationMinutes: number // Per project presentation time
qaDurationMinutes: number // Per project Q&A time
// Deliberation
deliberationEnabled: boolean
deliberationDurationMinutes: number
deliberationAllowsVoteRevision: boolean // Can jury change votes during deliberation?
// Category windows
categoryWindowsEnabled: boolean // Separate windows per category
categoryWindows: CategoryWindow[]
// Results display
showLiveResults: boolean // Real-time leaderboard
showLiveScores: boolean // Show actual scores vs just rankings
anonymizeJuryVotes: boolean // Hide individual jury votes from audience
requireAllJuryVotes: boolean // Voting can't end until all jury members vote
// Overrides
adminCanOverrideVotes: boolean
adminCanAdjustWeights: boolean // Mid-ceremony weight adjustment
// Presentation order
presentationOrderMode: 'MANUAL' | 'RANDOM' | 'SCORE_BASED' | 'CATEGORY_SPLIT'
}
type CategoryWindow = {
category: 'STARTUP' | 'BUSINESS_CONCEPT'
projectOrder: string[] // Ordered project IDs
startTime?: string // Scheduled start (ISO 8601)
endTime?: string // Scheduled end
deliberationMinutes?: number // Override global deliberation
}
```
### 9.4 Key Behaviors
**Ceremony State Machine:**
```
NOT_STARTED → (start session) → IN_PROGRESS → (deliberation starts) → DELIBERATION → (voting ends) → COMPLETED
NOT_STARTED:
- Session created but not started
- Projects ordered (manual or automatic)
- Jury and audience links generated
IN_PROGRESS:
- Presentations ongoing
- Per-project state: WAITING → PRESENTING → Q_AND_A → VOTING → VOTED → SCORED
- Admin can pause, skip, reorder on the fly
DELIBERATION:
- Timer running for deliberation period
- Jury can discuss (optional chat/discussion interface)
- Votes may be revised (if deliberationAllowsVoteRevision=true)
COMPLETED:
- All voting finished
- Results calculated
- Ceremony locked (or unlocked for result reveal)
```
**Stage Manager Controls:**
- Start/pause/resume ceremony
- Jump to specific project
- Skip project (emergency)
- Reorder queue (drag-and-drop)
- Open/close voting window
- Extend timer (+1 min, +5 min)
- Start deliberation period
- Force end session
- Override individual votes (if enabled)
**Jury Voting Interface:**
```
┌─ VOTING PANEL (Numeric Mode: 1-10) ─────────────────┐
│ │
│ How would you rate this project overall? │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ 1 2 3 4 5 6 7 8 9 10│ │
│ │ ○ ○ ○ ○ ○ ○ ○ ● ○ ○│ │
│ └────────────────────────────────────────────────┘ │
│ │
│ Your score: 8 │
│ │
│ [Submit Vote] │
│ │
│ ⚠️ Votes cannot be changed after submission │
│ unless admin resets. │
└──────────────────────────────────────────────────────┘
```
**Weighted Score Calculation:**
```typescript
function calculateWeightedScore(
juryScores: number[],
audienceScore: number,
config: LiveFinalConfig
): number {
const juryAvg = juryScores.reduce((sum, s) => sum + s, 0) / juryScores.length
const juryWeight = config.juryVotingWeight / 100
const audienceWeight = config.audienceVotingWeight / 100
return (juryAvg * juryWeight) + (audienceScore * audienceWeight)
}
```
### 9.5 Admin Controls
**Stage Manager Dashboard:**
- Ceremony status (NOT_STARTED, IN_PROGRESS, DELIBERATION, COMPLETED)
- Current project indicator
- Presentation/Q&A/Voting timers
- Jury vote count (submitted / total)
- Audience vote count
- Live leaderboard (if enabled)
- Category window controls
- Emergency controls (pause, skip, reset)
### 9.6 Output & Advancement Criteria
**Round 7 Completes:**
- All finalists have Jury 3 scores + audience scores (if enabled)
- Weighted scores calculated per project
- Recommended winners per category (highest weighted score)
- Tied projects flagged for R8 deliberation
**ProjectRoundState updates:**
```typescript
// All finalists remain PASSED in R7:
await prisma.projectRoundState.updateMany({
where: { roundId: round7.id },
data: { status: 'PASSED' }
})
// Scores stored in LiveVote table
// Winners not yet final — wait for R8 deliberation
```
**Advancement to R8:**
- All projects with `ProjectRoundState.status = PASSED` in R7 become eligible for R8 (DELIBERATION)
---
## 10. R8: Deliberation (Final Winner Confirmation)
### 10.1 Purpose
The Deliberation round replaces the "all jury agree + admin approval" confirmation model. It provides a structured deliberation session where Jury 3 formally selects winners per category.
**Key Objectives:**
- Formal winner selection per category (STARTUP and BUSINESS_CONCEPT independent)
- Deliberation session with voting modes (single winner vote, full ranking)
- Tie-breaking mechanisms (runoff vote, admin break)
- Admin override with mandatory audit justification
- ResultLock snapshot for final results
### 10.2 Prerequisites
**Before R8 can open:**
- Round 7 (LIVE_FINAL) completed
- All finalists have Jury 3 scores
- Deliberation session created per category
### 10.3 Configuration Shape (DeliberationConfig)
```typescript
type DeliberationConfig = {
// Mode
mode: 'SINGLE_WINNER_VOTE' | 'FULL_RANKING'
// Visibility
showCollectiveRankings: boolean // Show aggregate rankings to jury during deliberation
// Tie-breaking
tieBreakMethod: 'RUNOFF_VOTE' | 'ADMIN_BREAK' | 'ADMIN_OVERRIDE'
// Admin controls
adminCanOverride: boolean // Admin can override entire result
adminOverrideRequiresReason: boolean // Mandatory reason for override (default: true)
// Result locking
autoLockOnFinalize: boolean // Create ResultLock snapshot immediately (default: true)
unlockRequiresSuperAdmin: boolean // Only super-admin can unlock (default: true)
}
```
### 10.4 Key Behaviors
**Deliberation Session (Per Category):**
```typescript
// Create one DeliberationSession per category
await prisma.deliberationSession.create({
data: {
roundId: round8.id,
category: 'STARTUP',
mode: 'SINGLE_WINNER_VOTE',
status: 'PENDING',
projects: {
connect: startupFinalists.map(p => ({ id: p.id }))
}
}
})
await prisma.deliberationSession.create({
data: {
roundId: round8.id,
category: 'BUSINESS_CONCEPT',
mode: 'SINGLE_WINNER_VOTE',
status: 'PENDING',
projects: {
connect: conceptFinalists.map(p => ({ id: p.id }))
}
}
})
```
**Voting Modes:**
**SINGLE_WINNER_VOTE:**
```typescript
// Each juror picks one winner
// Project with most votes = proposed winner
// Others ranked by vote count
async function tallyWinnerVotes(sessionId: string) {
const votes = await prisma.deliberationVote.findMany({
where: { sessionId },
select: { projectId: true }
})
const voteCounts = new Map<string, number>()
for (const vote of votes) {
voteCounts.set(vote.projectId, (voteCounts.get(vote.projectId) || 0) + 1)
}
const sorted = Array.from(voteCounts.entries())
.sort((a, b) => b[1] - a[1])
const winner = sorted[0]
const isTied = sorted.length > 1 && sorted[1][1] === winner[1]
return { winner: winner[0], voteCount: winner[1], isTied, tiedProjects: isTied ? sorted.filter(s => s[1] === winner[1]).map(s => s[0]) : [] }
}
```
**FULL_RANKING:**
```typescript
// Each juror submits ordinal ranks (1st, 2nd, 3rd...)
// Aggregated via Borda count
async function tallyRankingVotes(sessionId: string) {
const votes = await prisma.deliberationVote.findMany({
where: { sessionId },
select: { projectId: true, rank: true }
})
const bordaScores = new Map<string, number>()
const totalProjects = new Set(votes.map(v => v.projectId)).size
for (const vote of votes) {
const points = totalProjects - vote.rank + 1 // 1st = N points, 2nd = N-1, etc.
bordaScores.set(vote.projectId, (bordaScores.get(vote.projectId) || 0) + points)
}
const sorted = Array.from(bordaScores.entries())
.sort((a, b) => b[1] - a[1])
const winner = sorted[0]
const isTied = sorted.length > 1 && sorted[1][1] === winner[1]
return { winner: winner[0], bordaScore: winner[1], isTied, tiedProjects: isTied ? sorted.filter(s => s[1] === winner[1]).map(s => s[0]) : [] }
}
```
**Tie-Breaking:**
**Runoff Vote:**
```typescript
// If tied, create new voting round with only tied projects
async function createRunoffVote(sessionId: string, tiedProjectIds: string[]) {
await prisma.deliberationSession.update({
where: { id: sessionId },
data: {
status: 'RUNOFF',
runoffProjects: tiedProjectIds
}
})
// Notify jury: "Runoff vote required for tied projects"
// Jury votes again, only on tied projects
}
```
**Admin Break:**
```typescript
// Admin manually selects winner from tied projects
async function adminBreakTie(sessionId: string, winnerId: string, actorId: string, reason: string) {
await prisma.deliberationSession.update({
where: { id: sessionId },
data: {
finalWinnerId: winnerId,
tieBreakMethod: 'ADMIN_BREAK',
tieBreakReason: reason,
tieBreakByUserId: actorId,
tieBreakAt: new Date(),
status: 'COMPLETED'
}
})
// Audit log
await createAuditLog({
action: 'TIE_BREAK_ADMIN',
userId: actorId,
entityType: 'DeliberationSession',
entityId: sessionId,
metadata: { winnerId, reason }
})
}
```
**Admin Override:**
```typescript
// Admin overrides entire result (not just tie-break)
async function adminOverrideResult(sessionId: string, winnerId: string, actorId: string, reason: string) {
// Mandatory reason requirement
if (!reason || reason.trim().length < 10) {
throw new Error('Admin override requires a detailed reason (min 10 characters)')
}
await prisma.deliberationSession.update({
where: { id: sessionId },
data: {
finalWinnerId: winnerId,
wasOverridden: true,
overrideReason: reason,
overrideByUserId: actorId,
overrideAt: new Date(),
status: 'COMPLETED'
}
})
// Audit log
await createAuditLog({
action: 'DELIBERATION_ADMIN_OVERRIDE',
userId: actorId,
entityType: 'DeliberationSession',
entityId: sessionId,
metadata: { winnerId, reason }
})
}
```
**Result Lock:**
```typescript
// After deliberation finalized, create immutable snapshot
async function finalizeDeliberation(sessionId: string, actorId: string) {
const session = await prisma.deliberationSession.findUnique({
where: { id: sessionId },
include: { votes: true, finalWinner: true }
})
// Create ResultLock snapshot
const resultLock = await prisma.resultLock.create({
data: {
roundId: round8.id,
category: session.category,
winnerId: session.finalWinnerId,
snapshotJson: {
sessionId: session.id,
votes: session.votes,
finalWinner: session.finalWinner,
tieBreakMethod: session.tieBreakMethod,
wasOverridden: session.wasOverridden,
timestamp: new Date()
},
lockedBy: actorId,
lockedAt: new Date()
}
})
// Update Project.status → WINNER
await prisma.project.update({
where: { id: session.finalWinnerId },
data: { status: 'WINNER' }
})
return resultLock
}
```
**Result Unlock (Super-Admin Only):**
```typescript
async function unlockResult(resultLockId: string, actorId: string, reason: string) {
// Check: Is user super-admin?
const user = await prisma.user.findUnique({ where: { id: actorId } })
if (user.role !== 'SUPER_ADMIN') {
throw new Error('Only super-admins can unlock results')
}
// Mandatory reason
if (!reason || reason.trim().length < 10) {
throw new Error('Result unlock requires a detailed reason (min 10 characters)')
}
// Create unlock event (audit)
await prisma.resultUnlockEvent.create({
data: {
resultLockId,
unlockedBy: actorId,
unlockedAt: new Date(),
reason
}
})
// Update result lock
await prisma.resultLock.update({
where: { id: resultLockId },
data: {
isUnlocked: true,
unlockedBy: actorId,
unlockedAt: new Date(),
unlockReason: reason
}
})
// Audit log
await createAuditLog({
action: 'RESULT_UNLOCKED',
userId: actorId,
entityType: 'ResultLock',
entityId: resultLockId,
metadata: { reason }
})
}
```
### 10.5 Admin Controls
**Deliberation Dashboard:**
- View deliberation session status per category
- Monitor jury votes (submitted / total)
- View collective rankings (if enabled)
- Resolve ties (runoff vote or admin break)
- Override result (with mandatory reason)
- Finalize and lock results
- Unlock results (super-admin only, mandatory reason)
### 10.6 Output & Advancement Criteria
**Round 8 Completes:**
- Final winners selected per category
- Winners: `Project.status = WINNER`
- Non-winners: `Project.status = NOT_SELECTED` (or remain FINALIST)
- ResultLock snapshots created (immutable audit trail)
- Special award winners: Separate tracking (SpecialAwardWinner records)
**End of Competition:**
- Winners announced publicly
- Prizes awarded
- Case studies published
---
## 11. Cross-Cutting Behaviors
These behaviors apply across multiple rounds and are fundamental to the competition system.
### 11.1 Time Windows & Reminders
**Deadline Countdown Display:**
- Every round with a deadline shows countdown on relevant dashboards
- Color-coded urgency:
- **Green** (>7 days): "14 days remaining"
- **Yellow** (3-7 days): "⚠️ 5 days remaining — Please submit soon!"
- **Orange** (<3 days): "🚨 URGENT: 2 days remaining"
- **Red** (<24 hours): "🚨 CRITICAL: 12 hours remaining" (pulsing animation)
**Email Reminder Schedule:**
```typescript
// Configured per round in IntakeConfig, SubmissionConfig, EvaluationConfig
reminderEmailSchedule: [7, 3, 1] // Days before deadline
// Example reminder emails:
// - 7 days before: "Reminder: Deadline in 7 days"
// - 3 days before: "⚠️ Important: Deadline in 3 days"
// - 1 day before: "🚨 URGENT: Deadline tomorrow"
```
### 11.2 Admin Override
**Override Authority:**
- Admins can intervene at ANY point in the competition
- Override types:
- Eligibility decisions (R2 filtering)
- Assignment changes (R3/R5 jury assignments)
- Stage advancement (manually advance/reject projects)
- Finalist selection (R3/R5 override AI recommendations)
- Winner confirmation (R8 override deliberation result)
**Mandatory Audit:**
```typescript
// All admin overrides require audit justification
type AdminOverride = {
action: string // "ELIGIBILITY_OVERRIDE", "ADVANCEMENT_OVERRIDE", etc.
entityType: string // "Project", "FilteringResult", etc.
entityId: string
overrideType: 'APPROVE' | 'REJECT' | 'MODIFY'
reason: string // Mandatory, min 10 characters
previousValue: unknown // State before override
newValue: unknown // State after override
actorId: string
timestamp: Date
}
// Stored in DecisionAuditLog table
await prisma.decisionAuditLog.create({
data: {
action: 'ADMIN_OVERRIDE',
userId: actorId,
entityType,
entityId,
metadata: {
overrideType,
reason,
previousValue,
newValue
}
}
})
```
### 11.3 AI Ranked Shortlist
**When Generated:**
- End of Round 3 (Jury 1 evaluation)
- End of Round 5 (Jury 2 evaluation)
- End of any special award evaluation round
**How It Works:**
```typescript
async function generateAIShortlist(roundId: string, category: 'STARTUP' | 'BUSINESS_CONCEPT') {
// Fetch all projects with evaluations
const projects = await prisma.project.findMany({
where: {
competitionCategory: category,
projectRoundStates: {
some: { roundId, status: 'IN_PROGRESS' }
}
},
include: {
evaluations: {
where: { assignmentId: { in: assignmentIds } },
include: { assignment: { include: { user: true } } }
}
}
})
// Anonymize project data
const anonymized = anonymizeProjects(projects)
// Send to OpenAI
const prompt = `
You are evaluating projects for the Monaco Ocean Protection Challenge.
For each project, you have:
- Average jury score (1-10)
- Individual jury scores
- Jury feedback (anonymized)
- Project description (anonymized)
Task:
1. Rank all projects from best to worst
2. For each project, provide:
- Recommended rank (1st, 2nd, 3rd, etc.)
- Key strengths (2-3 bullet points)
- Key weaknesses (2-3 bullet points)
- Overall assessment (1-2 sentences)
- Recommendation: "Advance" / "Borderline" / "Do not advance"
Return JSON: { rankings: [ { project_id, rank, strengths, weaknesses, assessment, recommendation } ] }
`
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{ role: 'system', content: prompt },
{ role: 'user', content: JSON.stringify(anonymized) }
],
response_format: { type: 'json_object' }
})
// Parse and de-anonymize
const aiShortlist = parseAIShortlist(response, anonymized.mappings)
// Store in database
await prisma.aiShortlist.create({
data: {
roundId,
category,
rankingsJson: aiShortlist,
generatedAt: new Date()
}
})
return aiShortlist
}
```
**Admin UI:**
```
┌─ AI RECOMMENDED SHORTLIST (STARTUP) ────────────────┐
│ │
│ 1. OceanClean AI │
│ Strengths: │
│ • Strong technical innovation │
│ • Clear market need │
│ • Experienced team │
│ Weaknesses: │
│ • Financial projections overly optimistic │
│ • Limited pilot data │
│ Recommendation: Advance │
│ │
│ 2. BlueCarbon Solutions │
│ Strengths: │
│ • Proven business model │
│ • Strong partnerships │
│ Weaknesses: │
│ • Incremental innovation │
│ Recommendation: Advance │
│ │
│ ... (continues for all projects) │
│ │
│ [Accept AI Recommendation] [Edit Shortlist] │
└──────────────────────────────────────────────────────┘
```
**Admin Can:**
- Accept AI recommendation
- Override AI recommendation (drag to reorder, add/remove projects)
- Ignore AI recommendation (manual selection only)
### 11.4 Multi-Round Document Handling
**Applicant View:**
```
┌─ MY DOCUMENTS ───────────────────────────────────────┐
│ │
│ 🔒 Round 1: Application Documents (LOCKED) │
│ Submitted: Jan 25, 2026 │
│ Status: Locked (read-only) │
│ [View Files] │
│ │
│ 🔒 Round 2: Semi-Finalist Materials (LOCKED) │
│ Submitted: Mar 15, 2026 │
│ Status: Locked (read-only) │
│ [View Files] │
│ │
Locked documents cannot be edited. │
│ Contact admin if you need to make changes. │
└──────────────────────────────────────────────────────┘
```
**Jury View (Multi-Window):**
```
┌─ PROJECT DOCUMENTS ──────────────────────────────────┐
│ [ Round 1 ] [ Round 2 ] [ Mentoring Promoted ] │
│ ▲ │
└──────────────────────────────────────────────────────┘
Tab 1: Round 1 Application (Window 1 files)
Tab 2: Semi-Final Submission (Window 2 files)
Tab 3: Mentoring Promoted (Files promoted from mentoring workspace)
```
**Admin View:**
```
┌─ ADMIN FILE MANAGEMENT ──────────────────────────────┐
│ │
│ Project: OceanClean AI │
│ │
│ Window 1: Application Documents (LOCKED) │
│ ├─ Pitch Deck.pdf │
│ │ [View] [Download] [Replace] [Delete] [History] │
│ ├─ Budget.xlsx │
│ │ [View] [Download] [Replace] [Delete] [History] │
│ └─ Team CV.pdf │
│ [View] [Download] [Replace] [Delete] [History] │
│ │
│ Window 2: Semi-Finalist Materials (LOCKED) │
│ ├─ Updated Pitch.pdf (v2, supersedes v1) │
│ │ [View] [Download] [Replace] [Delete] [History] │
│ └─ Video Pitch.mp4 │
│ [View] [Download] [Replace] [Delete] [History] │
│ │
│ ✅ Admin can manage files in ANY window regardless │
│ of lock state. All changes are audited. │
└──────────────────────────────────────────────────────┘
```
**File Replacement Provenance:**
```typescript
// When admin replaces a file:
1. Old file: supersededBy = new file ID, supersededAt = now
2. New file: version = old file version + 1
3. Audit log: action = "FILE_REPLACED_BY_ADMIN", metadata = { oldFileId, newFileId, reason }
4. Both files remain in database (full audit trail)
5. Jury/applicant views show new file only (unless admin enables version history view)
```
### 11.5 Multi-Jury Structure
**Jury Groups:**
```
Jury 1 (Jury Group ID: jury-1)
├─ Purpose: Semi-finalist selection (R3)
├─ Members: 8 judges (e.g., Alice, Bob, Carol, David, Emma, Frank, Grace, Henry)
├─ Assignment: 3 evaluations per project
└─ Scoring: Criteria-based (Innovation, Feasibility, Impact, Team)
Jury 2 (Jury Group ID: jury-2)
├─ Purpose: Finalist selection (R5)
├─ Members: 12 judges (may overlap with Jury 1)
│ Example: Alice, Bob, Carol, David (from Jury 1) + 8 new judges
├─ Assignment: 3-5 evaluations per project
└─ Scoring: Criteria-based (Business Model, Team, Presentation, Viability)
Jury 3 (Jury Group ID: jury-3)
├─ Purpose: Live Finals voting (R7)
├─ Members: 8 judges (may overlap with Jury 1/2)
│ Example: Alice, Bob, Carol, David (from Jury 1) + 4 new judges
├─ Voting: Numeric (1-10) or ranking
└─ Deliberation: Final winner confirmation (R8)
Special Award Juries (Multiple)
├─ Innovation Award Jury (jury-award-innovation)
│ ├─ Purpose: Select Innovation Award winner
│ └─ Members: Subset of Jury 2 or dedicated judges
└─ Impact Award Jury (jury-award-impact)
├─ Purpose: Select Impact Award winner
└─ Members: Dedicated judges (may overlap with main juries)
```
**Judge Can Be in Multiple Juries:**
```
Judge Alice:
├─ Member of Jury 1 (R3)
├─ Member of Jury 2 (R5)
├─ Member of Jury 3 (R7)
└─ Member of Innovation Award Jury (R5 parallel)
Alice's Dashboard:
├─ Round 3 Assignments (Jury 1): 20 projects
├─ Round 5 Assignments (Jury 2): 15 projects
├─ Round 5 Award Assignments (Innovation Award): 5 projects
└─ Round 7 Live Finals (Jury 3): 10 projects (voting interface)
```
**Independent Jury Assignments:**
- Each jury group has its own assignment algorithm run
- Cap/quota limits are per-jury-group (not global per judge)
- Example: Alice has cap 20 for Jury 1, cap 15 for Jury 2, cap 5 for Award Jury
- Total workload across all juries tracked but not enforced (admin's responsibility to balance)
### 11.6 Score Independence
**During Active Evaluation:**
```
Round 3 (Jury 1) evaluation:
├─ Jury 1 scores are HIDDEN from Jury 2 and Jury 3
├─ Jury 1 members cannot see each other's scores until submission
└─ Admin can see all scores at any time
Round 5 (Jury 2) evaluation:
├─ Jury 2 scores are HIDDEN from Jury 3
├─ Jury 2 can optionally see Jury 1 scores (if config.showPriorJuryData = true)
├─ Jury 2 members cannot see each other's scores until submission
└─ Admin can see all scores at any time
Round 7 (Live Finals) evaluation:
├─ Jury 3 scores are HIDDEN from audience (if config.anonymizeJuryVotes = true)
├─ Jury 3 can optionally see Jury 1/2 scores (if config.showPriorJuryData = true)
├─ Jury 3 can see each other's scores AFTER voting (peer review)
└─ Admin can see all scores at any time
```
**showPriorJuryData Toggle:**
```typescript
// In EvaluationConfig for R5:
{
showPriorJuryData: true // Jury 2 can see Jury 1 scores + feedback
}
// In LiveFinalConfig for R7:
{
showPriorJuryData: true // Jury 3 can see Jury 1 + Jury 2 scores + feedback
}
```
**Jury View with Prior Data:**
```
┌─ EVALUATING: OceanClean AI ──────────────────────────┐
│ │
│ [Documents] [Scoring] [Feedback] [Prior Jury Data] │
│ ▲ New tab │
└──────────────────────────────────────────────────────┘
┌─ PRIOR JURY DATA ────────────────────────────────────┐
│ │
│ Jury 1 Results (Round 3 — Semi-Finalist Selection) │
│ ├─ Average Score: 8.2 / 10 │
│ ├─ Consensus: 0.85 (high agreement) │
│ ├─ Individual Scores: 9, 8, 8 (3 evaluations) │
│ └─ Feedback Summary: │
│ "Strong technical innovation, clear market need, │
│ experienced team. Financial projections overly │
│ optimistic, limited pilot data." │
│ │
│ Jury 2 Results (Round 5 — Finalist Selection) │
│ ├─ Average Score: 8.5 / 10 │
│ ├─ Consensus: 0.90 (very high agreement) │
│ ├─ Individual Scores: 9, 8, 9, 8, 8 (5 evaluations) │
│ └─ Feedback Summary: │
│ "Excellent business model, strong partnerships, │
│ impressive presentation. Some concerns about │
│ scalability." │
│ │
This data is for context only. Your evaluation │
│ should be independent. │
└──────────────────────────────────────────────────────┘
```
**Admin Reports (Cross-Jury Comparison):**
```
┌─ CROSS-JURY SCORE ANALYSIS ──────────────────────────┐
│ │
│ Project: OceanClean AI │
│ │
│ ┌────────┬──────────┬───────────┬──────────────┐ │
│ │ Jury │ Avg Score│ Consensus │ Trend │ │
│ ├────────┼──────────┼───────────┼──────────────┤ │
│ │ Jury 1 │ 8.2 │ 0.85 │ — │ │
│ │ Jury 2 │ 8.5 │ 0.90 │ ↗ +0.3 │ │
│ │ Jury 3 │ 8.8 │ 0.95 │ ↗ +0.3 │ │
│ └────────┴──────────┴───────────┴──────────────┘ │
│ │
│ Interpretation: │
│ ✅ Consistent upward trend across juries │
│ ✅ High consensus in all rounds │
│ ✅ Strong candidate for winner │
└──────────────────────────────────────────────────────┘
```
---
## 12. Monaco 2026 Reference Configuration
This section provides a concrete example of how the Monaco Ocean Protection Challenge 2026 competition is configured using all eight rounds.
### 12.1 Competition Profile
```typescript
Competition {
id: "comp-mopc-2026"
name: "Monaco Ocean Protection Challenge 2026"
slug: "mopc-2026"
description: "Annual competition for ocean conservation innovation"
startDate: "2026-02-01"
endDate: "2026-09-30"
status: "ACTIVE"
categories: ["STARTUP", "BUSINESS_CONCEPT"]
programId: "program-mopc"
}
```
### 12.2 Round Configuration
```typescript
// Round 1: Application Window (INTAKE)
Round {
id: "round-1-intake"
competitionId: "comp-mopc-2026"
name: "Application Window"
slug: "application-window"
roundType: "INTAKE"
sortOrder: 0
windowOpenAt: "2026-02-01T00:00:00Z"
windowCloseAt: "2026-05-31T23:59:59Z"
submissionWindowId: "window-1"
configJson: {
deadlinePolicy: "FLAG",
gracePeriodMinutes: null,
allowDraftSubmissions: true,
draftExpiryDays: 30,
requireTeamProfile: true,
maxTeamSize: 5,
minTeamSize: 1,
autoConfirmReceipt: true,
reminderEmailSchedule: [7, 3, 1],
publicFormEnabled: true,
categoryQuotasEnabled: false
}
}
// Linked SubmissionWindow 1
SubmissionWindow {
id: "window-1"
competitionId: "comp-mopc-2026"
roundId: "round-1-intake"
name: "Application Documents"
description: "Initial application materials for all applicants"
openDate: "2026-02-01T00:00:00Z"
closeDate: "2026-05-31T23:59:59Z"
latePolicy: "FLAG"
gracePeriodHours: null
lockOnClose: false // Not locked yet (will lock when R4 opens)
fileRequirements: [
{
label: "Executive Summary",
description: "PDF executive summary (max 2 pages)",
isRequired: true,
allowedFileTypes: ["pdf"],
maxSizeMB: 10,
displayOrder: 0
},
{
label: "Business Plan",
description: "Full business plan or project proposal (PDF)",
isRequired: true,
allowedFileTypes: ["pdf"],
maxSizeMB: 50,
displayOrder: 1
},
{
label: "Team CV",
description: "Team member biographies (PDF)",
isRequired: false,
allowedFileTypes: ["pdf"],
maxSizeMB: 5,
displayOrder: 2
}
]
}
// Round 2: AI Screening (FILTERING)
Round {
id: "round-2-filtering"
competitionId: "comp-mopc-2026"
name: "AI Screening & Eligibility Check"
slug: "ai-screening"
roundType: "FILTERING"
sortOrder: 1
windowOpenAt: null // No user-facing window (automated)
windowCloseAt: null
configJson: {
rules: [
{
name: "Startups Must Be < 5 Years Old",
ruleType: "FIELD_CHECK",
config: {
conditions: [
{ field: "competitionCategory", operator: "equals", value: "STARTUP" },
{ field: "foundedAt", operator: "newer_than_years", value: 5 }
],
logic: "AND"
},
priority: 10,
isActive: true,
action: "REJECT"
},
{
name: "Must Upload Executive Summary",
ruleType: "DOCUMENT_CHECK",
config: {
requiredFileTypes: ["pdf"],
minFileCount: 2
},
priority: 20,
isActive: true,
action: "FLAG"
}
],
aiScreeningEnabled: true,
aiRubricPrompt: "Project must demonstrate measurable ocean conservation impact with clear metrics and realistic timeline. Reject spam or unrelated projects.",
aiConfidenceThresholds: { high: 0.85, medium: 0.6, low: 0.4 },
aiBatchSize: 20,
aiParallelBatches: 2,
duplicateDetectionEnabled: true,
duplicateThreshold: 1.0,
duplicateAction: "FLAG",
autoAdvancePassingProjects: false,
manualReviewRequired: true
}
}
// Round 3: Jury 1 — Semi-Finalist Selection (EVALUATION)
Round {
id: "round-3-jury-1"
competitionId: "comp-mopc-2026"
name: "Jury 1 — Semi-Finalist Selection"
slug: "jury-1-evaluation"
roundType: "EVALUATION"
sortOrder: 2
windowOpenAt: "2026-06-05T00:00:00Z"
windowCloseAt: "2026-06-25T23:59:59Z"
juryGroupId: "jury-1"
configJson: {
requiredReviewsPerProject: 3,
scoringMode: "criteria",
requireFeedback: true,
coiRequired: true,
peerReviewEnabled: false,
anonymizationLevel: "fully_anonymous",
aiSummaryEnabled: true,
aiAssignmentEnabled: true,
advancementMode: "admin_selection",
advancementConfig: {
perCategory: true,
startupCount: 20,
conceptCount: 20,
tieBreaker: "admin_decides"
}
}
}
// Jury 1 Group
JuryGroup {
id: "jury-1"
name: "Jury 1 — Technical Panel"
defaultMaxAssignments: 25
defaultCapMode: "SOFT"
softCapBuffer: 10
categoryQuotasEnabled: true
defaultCategoryQuotas: {
STARTUP: { min: 3, max: 15 },
BUSINESS_CONCEPT: { min: 3, max: 15 }
}
allowJurorCapAdjustment: true
allowJurorRatioAdjustment: true
}
// RoundSubmissionVisibility for R3 (sees Window 1 only)
RoundSubmissionVisibility {
id: "vis-r3-w1"
evaluationRoundId: "round-3-jury-1"
submissionWindowId: "window-1"
displayLabel: "Application Documents"
displayOrder: 1
}
// Round 4: Semi-Finalist Submission (SUBMISSION)
Round {
id: "round-4-submission"
competitionId: "comp-mopc-2026"
name: "Semi-Finalist Materials"
slug: "semi-finalist-submission"
roundType: "SUBMISSION"
sortOrder: 3
windowOpenAt: "2026-06-28T00:00:00Z"
windowCloseAt: "2026-07-20T23:59:59Z"
submissionWindowId: "window-2"
configJson: {
eligibleStatuses: ["PASSED"],
notifyEligibleTeams: true,
lockPreviousWindows: true, // Window 1 auto-locks when R4 opens
windowConfig: {
name: "Semi-Finalist Materials",
description: "Additional documents for semi-finalists",
openDate: "2026-06-28T00:00:00Z",
closeDate: "2026-07-20T23:59:59Z",
latePolicy: "HARD",
gracePeriodHours: null
},
fileRequirements: [
{
label: "Updated Pitch Deck",
description: "PDF pitch deck with updates since Round 1",
isRequired: true,
allowedFileTypes: ["pdf"],
maxSizeMB: 15,
displayOrder: 0
},
{
label: "Video Pitch",
description: "3-minute video pitch (MP4 or MOV)",
isRequired: true,
allowedFileTypes: ["mp4", "mov"],
maxSizeMB: 100,
displayOrder: 1
},
{
label: "Financial Projections",
description: "3-year revenue and cost forecast (Excel or PDF)",
isRequired: true,
allowedFileTypes: ["xlsx", "pdf"],
maxSizeMB: 10,
displayOrder: 2
}
]
}
}
// Linked SubmissionWindow 2
SubmissionWindow {
id: "window-2"
competitionId: "comp-mopc-2026"
roundId: "round-4-submission"
name: "Semi-Finalist Materials"
description: "Additional documents for semi-finalists"
openDate: "2026-06-28T00:00:00Z"
closeDate: "2026-07-20T23:59:59Z"
latePolicy: "HARD"
gracePeriodHours: null
lockOnClose: true // Auto-locks after deadline
fileRequirements: [
{
label: "Updated Pitch Deck",
description: "PDF pitch deck with updates since Round 1",
isRequired: true,
allowedFileTypes: ["pdf"],
maxSizeMB: 15,
displayOrder: 0
},
{
label: "Video Pitch",
description: "3-minute video pitch (MP4 or MOV)",
isRequired: true,
allowedFileTypes: ["mp4", "mov"],
maxSizeMB: 100,
displayOrder: 1
},
{
label: "Financial Projections",
description: "3-year revenue and cost forecast (Excel or PDF)",
isRequired: true,
allowedFileTypes: ["xlsx", "pdf"],
maxSizeMB: 10,
displayOrder: 2
}
]
}
// Round 5: Jury 2 — Finalist Selection (EVALUATION)
Round {
id: "round-5-jury-2"
competitionId: "comp-mopc-2026"
name: "Jury 2 — Finalist Selection"
slug: "jury-2-evaluation"
roundType: "EVALUATION"
sortOrder: 4
windowOpenAt: "2026-07-24T00:00:00Z"
windowCloseAt: "2026-08-10T23:59:59Z"
juryGroupId: "jury-2"
configJson: {
requiredReviewsPerProject: 5, // More depth than R3
scoringMode: "criteria",
requireFeedback: true,
coiRequired: true,
peerReviewEnabled: true, // Jurors can see anonymized peer evaluations
anonymizationLevel: "show_initials",
aiSummaryEnabled: true,
aiAssignmentEnabled: true,
advancementMode: "ai_recommended",
advancementConfig: {
perCategory: true,
startupCount: 10,
conceptCount: 10,
tieBreaker: "admin_decides"
}
}
}
// Jury 2 Group
JuryGroup {
id: "jury-2"
name: "Jury 2 — Selection Panel"
defaultMaxAssignments: 15
defaultCapMode: "SOFT"
softCapBuffer: 5
categoryQuotasEnabled: true
defaultCategoryQuotas: {
STARTUP: { min: 2, max: 10 },
BUSINESS_CONCEPT: { min: 2, max: 10 }
}
allowJurorCapAdjustment: true
allowJurorRatioAdjustment: true
}
// RoundSubmissionVisibility for R5 (sees Window 1 + Window 2)
RoundSubmissionVisibility {
id: "vis-r5-w1"
evaluationRoundId: "round-5-jury-2"
submissionWindowId: "window-1"
displayLabel: "Round 1 Application"
displayOrder: 1
}
RoundSubmissionVisibility {
id: "vis-r5-w2"
evaluationRoundId: "round-5-jury-2"
submissionWindowId: "window-2"
displayLabel: "Semi-Final Submissions"
displayOrder: 2
}
// Round 6: Mentoring (MENTORING)
Round {
id: "round-6-mentoring"
competitionId: "comp-mopc-2026"
name: "Finalist Mentoring"
slug: "mentoring"
roundType: "MENTORING"
sortOrder: 5
windowOpenAt: "2026-08-15T00:00:00Z"
windowCloseAt: "2026-08-31T23:59:59Z"
configJson: {
eligibility: "requested_only",
chatEnabled: true,
fileUploadEnabled: true,
fileCommentsEnabled: true,
filePromotionEnabled: true,
promotionTargetWindowId: "window-2",
autoAssignMentors: false,
maxProjectsPerMentor: 3,
notifyTeamsOnOpen: true,
notifyMentorsOnAssign: true,
reminderBeforeClose: [7, 3, 1]
}
}
// Round 7: Live Finals (LIVE_FINAL)
Round {
id: "round-7-live-finals"
competitionId: "comp-mopc-2026"
name: "Live Finals Ceremony"
slug: "live-finals"
roundType: "LIVE_FINAL"
sortOrder: 6
windowOpenAt: "2026-09-15T00:00:00Z" // Event day
windowCloseAt: "2026-09-15T23:59:59Z"
juryGroupId: "jury-3"
configJson: {
juryGroupId: "jury-3",
votingMode: "NUMERIC",
numericScale: { min: 1, max: 10, allowDecimals: false },
criteriaEnabled: false,
audienceVotingEnabled: true,
audienceVotingWeight: 20,
juryVotingWeight: 80,
audienceVotingMode: "CATEGORY_FAVORITES",
audienceMaxFavorites: 3,
audienceRequireIdentification: true,
audienceAntiSpamMeasures: {
ipRateLimit: true,
deviceFingerprint: true,
emailVerification: false
},
presentationDurationMinutes: 8,
qaDurationMinutes: 5,
deliberationEnabled: true,
deliberationDurationMinutes: 30,
deliberationAllowsVoteRevision: false,
categoryWindowsEnabled: true,
categoryWindows: [
{
category: "STARTUP",
projectOrder: ["proj-1", "proj-2", "proj-3", "proj-4", "proj-5", "proj-6", "proj-7", "proj-8", "proj-9", "proj-10"],
startTime: "2026-09-15T18:00:00Z",
deliberationMinutes: 30
},
{
category: "BUSINESS_CONCEPT",
projectOrder: ["proj-11", "proj-12", "proj-13", "proj-14", "proj-15", "proj-16", "proj-17", "proj-18", "proj-19", "proj-20"],
startTime: "2026-09-15T20:00:00Z",
deliberationMinutes: 30
}
],
showLiveResults: true,
showLiveScores: false, // Show rankings but not actual scores
anonymizeJuryVotes: true,
requireAllJuryVotes: true,
adminCanOverrideVotes: true,
adminCanAdjustWeights: false,
presentationOrderMode: "MANUAL"
}
}
// Jury 3 Group
JuryGroup {
id: "jury-3"
name: "Jury 3 — Live Finals Panel"
// No assignment caps (voting only)
}
// RoundSubmissionVisibility for R7 (sees Window 1 + Window 2)
RoundSubmissionVisibility {
id: "vis-r7-w1"
evaluationRoundId: "round-7-live-finals"
submissionWindowId: "window-1"
displayLabel: "Original Application (January)"
displayOrder: 1
}
RoundSubmissionVisibility {
id: "vis-r7-w2"
evaluationRoundId: "round-7-live-finals"
submissionWindowId: "window-2"
displayLabel: "Semi-Final Submission (July)"
displayOrder: 2
}
// Round 8: Deliberation (CONFIRMATION)
Round {
id: "round-8-deliberation"
competitionId: "comp-mopc-2026"
name: "Final Winner Confirmation"
slug: "deliberation"
roundType: "CONFIRMATION"
sortOrder: 7
windowOpenAt: "2026-09-15T22:00:00Z" // Immediately after R7
windowCloseAt: "2026-09-16T23:59:59Z"
configJson: {
mode: "SINGLE_WINNER_VOTE",
showCollectiveRankings: true,
tieBreakMethod: "RUNOFF_VOTE",
adminCanOverride: true,
adminOverrideRequiresReason: true,
autoLockOnFinalize: true,
unlockRequiresSuperAdmin: true
}
}
```
### 12.3 Expected Project Flow
```
150 applicants submit (R1)
↓ (filtering)
120 projects pass eligibility (R2)
↓ (jury 1 evaluation)
40 semi-finalists selected (20 STARTUP, 20 BUSINESS_CONCEPT) (R3)
↓ (semi-finalist submission)
40 semi-finalists submit Round 2 docs (R4)
↓ (jury 2 evaluation)
20 finalists selected (10 STARTUP, 10 BUSINESS_CONCEPT) (R5)
↓ (mentoring)
15 finalists receive mentoring (5 opt out) (R6)
↓ (live finals)
20 finalists present live (R7)
↓ (deliberation)
2 winners confirmed (1 STARTUP, 1 BUSINESS_CONCEPT) (R8)
```
---
**End of Document**
This comprehensive specification defines the complete operational flow of a Monaco Ocean Protection Challenge competition across eight distinct rounds. Each round type is fully detailed with purpose, configuration, behaviors, admin controls, and advancement criteria. The cross-cutting behaviors section documents universal patterns that apply across all rounds.