2950 lines
99 KiB
Markdown
2950 lines
99 KiB
Markdown
|
|
# 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.
|