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

2950 lines
99 KiB
Markdown
Raw Permalink Normal View History

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