1432 lines
70 KiB
Markdown
1432 lines
70 KiB
Markdown
|
|
# 07. Live Finals and Deliberation
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
|
||
|
|
This document specifies **Round 7 (Live Finals)** and **Round 8 (Deliberation)** — the climactic phases of the Monaco Ocean Protection Challenge competition. These rounds transform months of evaluation into official, immutable results through live ceremony management and formal deliberation voting.
|
||
|
|
|
||
|
|
### Why Live Finals and Deliberation Are Treated Together
|
||
|
|
|
||
|
|
These two rounds form an integrated workflow:
|
||
|
|
- **Live Finals (R7)**: Jury 3 evaluates finalist presentations in real-time during a live ceremony, with optional audience participation
|
||
|
|
- **Deliberation (R8)**: The same Jury 3 engages in formal consensus-building to confirm final rankings
|
||
|
|
|
||
|
|
**Critical architectural decision: Deliberation IS the confirmation.** There is no separate confirmation step after deliberation. The deliberation voting process produces the official, locked results. This replaces the old WinnerProposal → jury sign-off → admin approval flow with a unified deliberation session that serves as both decision-making and confirmation in one step.
|
||
|
|
|
||
|
|
### Position in Competition Flow
|
||
|
|
|
||
|
|
```
|
||
|
|
Round 1: Application Window (INTAKE)
|
||
|
|
Round 2: AI Screening (FILTERING)
|
||
|
|
Round 3: Jury 1 - Semi-finalist Selection (EVALUATION)
|
||
|
|
Round 4: Semi-finalist Submission (SUBMISSION)
|
||
|
|
Round 5: Jury 2 - Finalist Selection (EVALUATION)
|
||
|
|
Round 6: Finalist Mentoring (MENTORING)
|
||
|
|
Round 7: Live Finals (LIVE_FINAL) ← THIS DOCUMENT
|
||
|
|
Round 8: Deliberation (DELIBERATION) ← THIS DOCUMENT
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Live Finals (Round 7)
|
||
|
|
|
||
|
|
### Purpose & Context
|
||
|
|
|
||
|
|
Live Finals orchestrate the live ceremony where Jury 3 (the Grand Jury) evaluates finalist presentations in real-time. This is the public face of the competition — often conducted at a formal event venue with an audience, media, and stakeholders present.
|
||
|
|
|
||
|
|
**Key capabilities:**
|
||
|
|
- Real-time stage manager controls for ceremony orchestration
|
||
|
|
- Jury voting with multiple scoring modes (criteria-based or simple)
|
||
|
|
- Optional audience voting with configurable weight
|
||
|
|
- Per-category presentation windows (STARTUP first, then BUSINESS_CONCEPT)
|
||
|
|
- Live results display or deferred reveal
|
||
|
|
- Anti-fraud measures for audience participation
|
||
|
|
|
||
|
|
### LiveFinalConfig Schema
|
||
|
|
|
||
|
|
The `LiveFinalConfig` object (stored in `Round.configJson`) controls all behavior for Round 7:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { z } from 'zod'
|
||
|
|
|
||
|
|
const LiveVotingCriterionSchema = z.object({
|
||
|
|
id: z.string(),
|
||
|
|
label: z.string().min(1).max(100),
|
||
|
|
description: z.string().max(500).optional(),
|
||
|
|
maxScore: z.number().int().min(1).max(100),
|
||
|
|
weight: z.number().min(0).max(1).default(1),
|
||
|
|
})
|
||
|
|
|
||
|
|
const LiveFinalConfigSchema = z.object({
|
||
|
|
// ── Jury Configuration ──────────────────────────────────
|
||
|
|
juryGroupId: z.string(), // Links to JuryGroup (Jury 3)
|
||
|
|
|
||
|
|
// ── Scoring Mode ────────────────────────────────────────
|
||
|
|
scoringMode: z.enum(['CRITERIA_BASED', 'SIMPLE_SCORE']).default('CRITERIA_BASED'),
|
||
|
|
|
||
|
|
scoringCriteria: z.array(LiveVotingCriterionSchema).optional(),
|
||
|
|
// Only used if scoringMode = CRITERIA_BASED
|
||
|
|
// Example: [
|
||
|
|
// { id: 'innovation', label: 'Innovation', maxScore: 10, weight: 0.3 },
|
||
|
|
// { id: 'impact', label: 'Impact Potential', maxScore: 10, weight: 0.4 },
|
||
|
|
// { id: 'feasibility', label: 'Feasibility', maxScore: 10, weight: 0.3 }
|
||
|
|
// ]
|
||
|
|
|
||
|
|
simpleScoreRange: z.object({
|
||
|
|
min: z.number().int().default(1),
|
||
|
|
max: z.number().int().default(10),
|
||
|
|
}).optional(),
|
||
|
|
// Only used if scoringMode = SIMPLE_SCORE
|
||
|
|
|
||
|
|
// ── Audience Voting ─────────────────────────────────────
|
||
|
|
audienceVotingEnabled: z.boolean().default(false),
|
||
|
|
|
||
|
|
audienceRevealTiming: z.enum([
|
||
|
|
'REAL_TIME', // Show audience scores live as they vote
|
||
|
|
'AFTER_JURY_VOTE', // Show after jury completes voting for each project
|
||
|
|
'AT_DELIBERATION' // Hide until deliberation starts
|
||
|
|
]).default('AT_DELIBERATION'),
|
||
|
|
|
||
|
|
audienceBlendWeight: z.number().min(0).max(1).default(0),
|
||
|
|
// 0 = Jury only (audience vote has no impact on scores)
|
||
|
|
// 0.3 = 70% jury, 30% audience
|
||
|
|
// 1 = Audience only (rare)
|
||
|
|
|
||
|
|
// ── Presentation Timing ─────────────────────────────────
|
||
|
|
votingWindowDuration: z.number().min(30).max(600).default(120), // seconds
|
||
|
|
// How long voting is open per project after presentation
|
||
|
|
|
||
|
|
// ── Prior Data Visibility ───────────────────────────────
|
||
|
|
showPriorJuryData: z.boolean().default(false),
|
||
|
|
// If true: Jury 3 can see previous jury scores/feedback during Live Finals
|
||
|
|
// If false: Jury 3 evaluates independently without prior jury context
|
||
|
|
// This reconciles "independent evaluation" with "as allowed by admin" qualifier
|
||
|
|
|
||
|
|
// ── Presentation Order ──────────────────────────────────
|
||
|
|
categoryOrder: z.array(z.nativeEnum(ProjectCategory)).default(['STARTUP', 'BUSINESS_CONCEPT']),
|
||
|
|
// Order in which categories are presented
|
||
|
|
|
||
|
|
presentationOrder: z.enum(['ALPHABETICAL', 'RANDOM', 'MANUAL']).default('MANUAL'),
|
||
|
|
// Within each category, how are projects ordered?
|
||
|
|
})
|
||
|
|
.refine(
|
||
|
|
(data) => {
|
||
|
|
// If criteria-based, must have criteria
|
||
|
|
if (data.scoringMode === 'CRITERIA_BASED' && (!data.scoringCriteria || data.scoringCriteria.length === 0)) {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
},
|
||
|
|
{ message: 'Criteria-based scoring requires at least one criterion' }
|
||
|
|
)
|
||
|
|
.refine(
|
||
|
|
(data) => {
|
||
|
|
// Criteria weights must sum to 1.0 (within tolerance)
|
||
|
|
if (data.scoringCriteria && data.scoringCriteria.length > 0) {
|
||
|
|
const weightSum = data.scoringCriteria.reduce((sum, c) => sum + c.weight, 0)
|
||
|
|
return Math.abs(weightSum - 1.0) < 0.01
|
||
|
|
}
|
||
|
|
return true
|
||
|
|
},
|
||
|
|
{ message: 'Criteria weights must sum to 1.0' }
|
||
|
|
)
|
||
|
|
|
||
|
|
type LiveFinalConfig = z.infer<typeof LiveFinalConfigSchema>
|
||
|
|
```
|
||
|
|
|
||
|
|
### Ceremony State Machine
|
||
|
|
|
||
|
|
The ceremony progresses through distinct phases:
|
||
|
|
|
||
|
|
```
|
||
|
|
┌──────────────┐
|
||
|
|
│ NOT_STARTED │ — Session created, projects ordered, jury/audience links generated
|
||
|
|
└──────┬───────┘
|
||
|
|
│ (Admin clicks "Start Ceremony")
|
||
|
|
▼
|
||
|
|
┌──────────────┐
|
||
|
|
│ IN_PROGRESS │ — Presentations ongoing, per-project state advancing
|
||
|
|
└──────┬───────┘ (WAITING → PRESENTING → Q_AND_A → VOTING → VOTED)
|
||
|
|
│
|
||
|
|
│ (All category presentations complete)
|
||
|
|
▼
|
||
|
|
┌──────────────┐
|
||
|
|
│ DELIBERATION │ — Live Finals concluded, deliberation voting active (R8)
|
||
|
|
└──────┬───────┘ (See Deliberation section below)
|
||
|
|
│
|
||
|
|
│ (Deliberation voting complete + admin locks)
|
||
|
|
▼
|
||
|
|
┌──────────────┐
|
||
|
|
│ COMPLETED │ — Results locked, ceremony finished
|
||
|
|
└──────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
**State Descriptions:**
|
||
|
|
|
||
|
|
- **NOT_STARTED**: Ceremony configured but not yet live. Admin can preview setup, test links, adjust project order.
|
||
|
|
- **IN_PROGRESS**: Active ceremony. Projects advance through per-project states. Admin uses Stage Manager controls.
|
||
|
|
- **DELIBERATION**: Live Finals portion complete. Jury 3 now in deliberation voting session (Round 8).
|
||
|
|
- **COMPLETED**: All deliberation voting finished, results locked. Ceremony archived.
|
||
|
|
|
||
|
|
### Per-Project State Machine
|
||
|
|
|
||
|
|
Each finalist project progresses through these states during Live Finals:
|
||
|
|
|
||
|
|
```
|
||
|
|
WAITING — Queued, not yet presenting
|
||
|
|
│
|
||
|
|
▼
|
||
|
|
PRESENTING — Presentation in progress (timer: presentationDurationMinutes)
|
||
|
|
│
|
||
|
|
▼
|
||
|
|
Q_AND_A — Q&A session (timer: qaDurationMinutes)
|
||
|
|
│
|
||
|
|
▼
|
||
|
|
VOTING — Voting window open (jury + audience can vote)
|
||
|
|
│
|
||
|
|
▼
|
||
|
|
VOTED — Voting window closed, scores recorded
|
||
|
|
│
|
||
|
|
▼
|
||
|
|
COMPLETE — Move to next project
|
||
|
|
```
|
||
|
|
|
||
|
|
**Special states:**
|
||
|
|
- **SKIPPED**: Admin emergency override to skip a project (e.g., team no-show)
|
||
|
|
|
||
|
|
### Stage Manager Admin Controls
|
||
|
|
|
||
|
|
The **Stage Manager** is the live ceremony control panel. It provides real-time orchestration and emergency intervention capabilities.
|
||
|
|
|
||
|
|
#### Core Control Panel Layout (ASCII Mockup)
|
||
|
|
|
||
|
|
```
|
||
|
|
┌────────────────────────────────────────────────────────────────────┐
|
||
|
|
│ LIVE FINALS STAGE MANAGER Session: live-abc-123 │
|
||
|
|
├────────────────────────────────────────────────────────────────────┤
|
||
|
|
│ Status: IN_PROGRESS Category: STARTUP Jury: Jury 3 (8/8) │
|
||
|
|
│ │
|
||
|
|
│ [Pause Ceremony] [End Session] [Emergency Stop] │
|
||
|
|
└────────────────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
┌─ CURRENT PROJECT ──────────────────────────────────────────────────┐
|
||
|
|
│ Project #3 of 6 (STARTUP) │
|
||
|
|
│ Title: "OceanSense AI" — Team: AquaTech Solutions │
|
||
|
|
│ │
|
||
|
|
│ State: VOTING │
|
||
|
|
│ ┌─ Presentation Timer ────┐ ┌─ Q&A Timer ─────┐ ┌─ Voting Timer ─┐│
|
||
|
|
│ │ Completed: 8:00 / 8:00 │ │ Completed: 5:00 │ │ 0:45 remaining ││
|
||
|
|
│ └─────────────────────────┘ └──────────────────┘ └────────────────┘│
|
||
|
|
│ │
|
||
|
|
│ Jury Votes: 6 / 8 (75%) │
|
||
|
|
│ [✓] Alice Chen [✓] Bob Martin [ ] Carol Davis │
|
||
|
|
│ [✓] David Lee [✓] Emma Wilson [ ] Frank Garcia │
|
||
|
|
│ [✓] Grace Huang [✓] Henry Thompson │
|
||
|
|
│ │
|
||
|
|
│ Audience Votes: 142 │
|
||
|
|
│ │
|
||
|
|
│ [Skip Project] [Reset Votes] [Extend Time +1min] [Next Project] │
|
||
|
|
└─────────────────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
┌─ PROJECT QUEUE ────────────────────────────────────────────────────┐
|
||
|
|
│ [✓] 1. AquaClean Tech (STARTUP) — Score: 8.2 (Completed) │
|
||
|
|
│ [✓] 2. BlueCarbon Solutions (STARTUP) — Score: 7.8 (Completed) │
|
||
|
|
│ [>] 3. OceanSense AI (STARTUP) — Voting in progress │
|
||
|
|
│ [ ] 4. MarineTech Innovations (STARTUP) — Waiting │
|
||
|
|
│ [ ] 5. CoralGuard (STARTUP) — Waiting │
|
||
|
|
│ [ ] 6. DeepSea Robotics (STARTUP) — Waiting │
|
||
|
|
│ │
|
||
|
|
│ [Reorder Queue] [Jump to Project...] [Add Project] │
|
||
|
|
└─────────────────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
┌─ CATEGORY WINDOWS ─────────────────────────────────────────────────┐
|
||
|
|
│ Window 1: STARTUP (6 projects) │
|
||
|
|
│ Status: IN_PROGRESS (Project 3/6) │
|
||
|
|
│ Started: 2026-05-15 18:00:00 │
|
||
|
|
│ [Close Window & Start Deliberation] │
|
||
|
|
│ │
|
||
|
|
│ Window 2: BUSINESS_CONCEPT (6 projects) │
|
||
|
|
│ Status: WAITING │
|
||
|
|
│ Scheduled: 2026-05-15 19:30:00 │
|
||
|
|
│ [Start Window Early] │
|
||
|
|
└─────────────────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
┌─ LIVE LEADERBOARD (STARTUP) ───────────────────────────────────────┐
|
||
|
|
│ Rank | Project | Jury Avg | Audience | Weighted | Gap │
|
||
|
|
│------+-----------------------+----------+----------+----------+------│
|
||
|
|
│ 1 | AquaClean Tech | 8.5 | 7.2 | 8.2 | — │
|
||
|
|
│ 2 | BlueCarbon Solutions | 8.0 | 7.4 | 7.8 | -0.4 │
|
||
|
|
│ 3 | OceanSense AI | — | 6.8 | — | — │
|
||
|
|
│ 4 | MarineTech Innov. | — | — | — | — │
|
||
|
|
│ 5 | CoralGuard | — | — | — | — │
|
||
|
|
│ 6 | DeepSea Robotics | — | — | — | — │
|
||
|
|
└─────────────────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
┌─ CEREMONY LOG ─────────────────────────────────────────────────────┐
|
||
|
|
│ 18:43:22 — Voting opened for "OceanSense AI" │
|
||
|
|
│ 18:42:10 — Q&A period ended │
|
||
|
|
│ 18:37:05 — Q&A period started │
|
||
|
|
│ 18:29:00 — Presentation started: "OceanSense AI" │
|
||
|
|
│ 18:28:45 — Voting closed for "BlueCarbon Solutions" │
|
||
|
|
│ 18:27:30 — All jury votes received for "BlueCarbon Solutions" │
|
||
|
|
└─────────────────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Stage Manager Features
|
||
|
|
|
||
|
|
**1. Session Management**
|
||
|
|
- Start ceremony (initialize cursor, generate access links)
|
||
|
|
- Pause ceremony (freeze all timers, block new votes)
|
||
|
|
- Resume ceremony
|
||
|
|
- End session (trigger transition to Deliberation round)
|
||
|
|
|
||
|
|
**2. Project Navigation**
|
||
|
|
- Jump to specific project by index
|
||
|
|
- Skip project (emergency — logs reason)
|
||
|
|
- Reorder presentation queue (drag-and-drop or modal)
|
||
|
|
- Add project mid-ceremony (rare edge case — requires justification)
|
||
|
|
|
||
|
|
**3. Timer Controls**
|
||
|
|
- Extend voting window (+1 min, +5 min)
|
||
|
|
- Manual timer override (set specific time)
|
||
|
|
- Auto-advance to next project when voting closes
|
||
|
|
|
||
|
|
**4. Voting Window Management**
|
||
|
|
- Open voting for current project
|
||
|
|
- Close voting early (requires confirmation)
|
||
|
|
- Require all jury votes before closing (configurable)
|
||
|
|
- Reset votes (emergency undo — requires reason)
|
||
|
|
|
||
|
|
**5. Category Window Controls**
|
||
|
|
- Open category window (STARTUP or BUSINESS_CONCEPT)
|
||
|
|
- Close category window
|
||
|
|
- Advance to next category
|
||
|
|
|
||
|
|
**6. Emergency Controls**
|
||
|
|
- Skip project (with reason)
|
||
|
|
- Reset individual vote
|
||
|
|
- Reset all votes for project
|
||
|
|
- Force end session
|
||
|
|
|
||
|
|
**7. Real-Time Monitoring**
|
||
|
|
- Live vote count (jury + audience)
|
||
|
|
- Missing jury votes indicator
|
||
|
|
- Audience voter registration count
|
||
|
|
- Leaderboard (if `showLiveResults=true`)
|
||
|
|
- Ceremony event log with timestamps
|
||
|
|
|
||
|
|
### Jury 3 Experience
|
||
|
|
|
||
|
|
#### Jury Dashboard During Live Finals
|
||
|
|
|
||
|
|
Jury members access a dedicated voting interface during the ceremony:
|
||
|
|
|
||
|
|
```
|
||
|
|
┌────────────────────────────────────────────────────────────────────┐
|
||
|
|
│ LIVE FINALS VOTING — Jury 3 Alice Chen │
|
||
|
|
├────────────────────────────────────────────────────────────────────┤
|
||
|
|
│ Status: VOTING IN PROGRESS │
|
||
|
|
│ Category: STARTUP │
|
||
|
|
│ │
|
||
|
|
│ [View All Finalists] [My Notes] [Jury Discussion] │
|
||
|
|
└────────────────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
┌─ CURRENT PROJECT ──────────────────────────────────────────────────┐
|
||
|
|
│ Project 3 of 6 │
|
||
|
|
│ │
|
||
|
|
│ OceanSense AI │
|
||
|
|
│ Team: AquaTech Solutions │
|
||
|
|
│ Category: STARTUP (Marine Technology) │
|
||
|
|
│ │
|
||
|
|
│ Description: │
|
||
|
|
│ AI-powered ocean monitoring platform that detects pollution events │
|
||
|
|
│ in real-time using satellite imagery and underwater sensors. │
|
||
|
|
│ │
|
||
|
|
│ ┌─ Documents ─────────────────────────────────────────────────┐ │
|
||
|
|
│ │ Round 1 Application: │ │
|
||
|
|
│ │ • Executive Summary.pdf │ │
|
||
|
|
│ │ • Business Plan.pdf │ │
|
||
|
|
│ │ │ │
|
||
|
|
│ │ Round 4 Semi-finalist Submission: │ │
|
||
|
|
│ │ • Updated Business Plan.pdf │ │
|
||
|
|
│ │ • Pitch Video.mp4 │ │
|
||
|
|
│ │ • Technical Whitepaper.pdf │ │
|
||
|
|
│ │ │ │
|
||
|
|
│ │ ⚠️ Prior Jury Data: [Hidden by Admin] │ │
|
||
|
|
│ │ (showPriorJuryData = false) │ │
|
||
|
|
│ └──────────────────────────────────────────────────────────────┘ │
|
||
|
|
│ │
|
||
|
|
│ Voting closes in: 0:45 │
|
||
|
|
└─────────────────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
┌─ VOTING PANEL (Criteria-Based) ────────────────────────────────────┐
|
||
|
|
│ │
|
||
|
|
│ Innovation (Weight: 30%) │
|
||
|
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||
|
|
│ │ 1 2 3 4 5 6 7 8 9 10 │ │
|
||
|
|
│ │ ○ ○ ○ ○ ○ ○ ○ ○ ● ○ │ │
|
||
|
|
│ └──────────────────────────────────────────────────────────┘ │
|
||
|
|
│ Your score: 9 │
|
||
|
|
│ │
|
||
|
|
│ Impact Potential (Weight: 40%) │
|
||
|
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||
|
|
│ │ 1 2 3 4 5 6 7 8 9 10 │ │
|
||
|
|
│ │ ○ ○ ○ ○ ○ ○ ○ ● ○ ○ │ │
|
||
|
|
│ └──────────────────────────────────────────────────────────┘ │
|
||
|
|
│ Your score: 8 │
|
||
|
|
│ │
|
||
|
|
│ Feasibility (Weight: 30%) │
|
||
|
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
||
|
|
│ │ 1 2 3 4 5 6 7 8 9 10 │ │
|
||
|
|
│ │ ○ ○ ○ ○ ○ ○ ○ ○ ● ○ │ │
|
||
|
|
│ └──────────────────────────────────────────────────────────┘ │
|
||
|
|
│ Your score: 9 │
|
||
|
|
│ │
|
||
|
|
│ Weighted Average: 8.6 │
|
||
|
|
│ │
|
||
|
|
│ [Submit Vote] │
|
||
|
|
│ │
|
||
|
|
│ ⚠️ Votes cannot be changed after submission. │
|
||
|
|
└─────────────────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
┌─ YOUR VOTES THIS SESSION ──────────────────────────────────────────┐
|
||
|
|
│ [✓] 1. AquaClean Tech — Score: 9.0 │
|
||
|
|
│ [✓] 2. BlueCarbon Solutions — Score: 8.2 │
|
||
|
|
│ [ ] 3. OceanSense AI — Voting now │
|
||
|
|
│ [ ] 4. MarineTech Innovations — Waiting │
|
||
|
|
│ [ ] 5. CoralGuard — Waiting │
|
||
|
|
│ [ ] 6. DeepSea Robotics — Waiting │
|
||
|
|
└─────────────────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Jury Access to Prior Jury Data
|
||
|
|
|
||
|
|
**Critical design point:** The `showPriorJuryData` toggle resolves the tension between "fully independent evaluation" and "as allowed by admin":
|
||
|
|
|
||
|
|
- **During active Jury 1/2 evaluation (R3, R5)**: NO cross-jury visibility — juries evaluate independently
|
||
|
|
- **During Live Finals (R7)**: Admin-configurable via `showPriorJuryData` toggle
|
||
|
|
- If `true`: Jury 3 can see Jury 1 and Jury 2 scores, feedback, and notes
|
||
|
|
- If `false`: Jury 3 evaluates without access to prior jury context
|
||
|
|
|
||
|
|
**When enabled, prior data includes:**
|
||
|
|
- Jury 1 and Jury 2 average scores per project
|
||
|
|
- Criteria scores (if criteria-based evaluation was used)
|
||
|
|
- Written feedback/comments from previous rounds
|
||
|
|
- Notes from jury deliberations (if recorded)
|
||
|
|
|
||
|
|
**UI representation when enabled:**
|
||
|
|
```
|
||
|
|
┌─ Prior Jury Context ─────────────────────────────────────────────┐
|
||
|
|
│ Jury 1 (Semi-finalist Selection): │
|
||
|
|
│ Average Score: 8.4 / 10 │
|
||
|
|
│ Top Criterion: Innovation (9.2) │
|
||
|
|
│ Key Feedback: "Strong technical approach, clear market fit" │
|
||
|
|
│ │
|
||
|
|
│ Jury 2 (Finalist Selection): │
|
||
|
|
│ Average Score: 8.8 / 10 │
|
||
|
|
│ Top Criterion: Impact Potential (9.5) │
|
||
|
|
│ Key Feedback: "Excellent scalability plan" │
|
||
|
|
└───────────────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Jury Real-Time Notes
|
||
|
|
|
||
|
|
Jury members can take private notes during presentations:
|
||
|
|
|
||
|
|
- Notes are per-project, per-jury-member
|
||
|
|
- Notes are NOT visible to other jurors during Live Finals
|
||
|
|
- Notes become visible during Deliberation (R8) if admin enables note sharing
|
||
|
|
- Notes are included in the final deliberation record
|
||
|
|
|
||
|
|
### Audience Voting
|
||
|
|
|
||
|
|
When `audienceVotingEnabled = true`, registered audience members can vote on finalist projects.
|
||
|
|
|
||
|
|
#### Audience Registration & Anti-Fraud
|
||
|
|
|
||
|
|
Before voting, audience members must register via a unique voting token:
|
||
|
|
|
||
|
|
**Registration flow:**
|
||
|
|
1. Admin generates unique voting tokens (UUID-based)
|
||
|
|
2. Tokens distributed via QR codes, email, or physical cards at event
|
||
|
|
3. Audience member visits public voting page, enters token
|
||
|
|
4. System validates token, records device fingerprint and IP
|
||
|
|
5. Audience member can now vote
|
||
|
|
|
||
|
|
**Anti-fraud measures:**
|
||
|
|
- **IP rate limiting**: Max 3 votes per IP address (configurable)
|
||
|
|
- **Device fingerprinting**: Track browser/device signatures
|
||
|
|
- **Token uniqueness**: Each token can only vote once per project
|
||
|
|
- **Time window**: Voting only allowed during active voting windows
|
||
|
|
- **Admin monitoring**: Dashboard shows suspicious voting patterns (e.g., rapid-fire votes from same IP)
|
||
|
|
|
||
|
|
#### Audience Voting Interface
|
||
|
|
|
||
|
|
```
|
||
|
|
┌────────────────────────────────────────────────────────────────────┐
|
||
|
|
│ MONACO OCEAN PROTECTION CHALLENGE — AUDIENCE VOTE │
|
||
|
|
│ │
|
||
|
|
│ 🎤 Now Presenting: OceanSense AI (STARTUP) │
|
||
|
|
│ │
|
||
|
|
│ How would you rate this project? │
|
||
|
|
│ │
|
||
|
|
│ ┌────────────────────────────────────────────────────────────┐ │
|
||
|
|
│ │ 😞 😐 🙂 😊 🤩 │ │
|
||
|
|
│ │ 1 2 3 4 5 │ │
|
||
|
|
│ │ ○ ○ ○ ○ ● │ │
|
||
|
|
│ └────────────────────────────────────────────────────────────┘ │
|
||
|
|
│ │
|
||
|
|
│ Your vote: 5 stars │
|
||
|
|
│ │
|
||
|
|
│ [Submit Vote] │
|
||
|
|
│ │
|
||
|
|
│ Voting closes in: 1:23 │
|
||
|
|
│ │
|
||
|
|
│ ── Your Votes ──────────────────────────────────────────────── │
|
||
|
|
│ [✓] AquaClean Tech: ⭐⭐⭐⭐⭐ │
|
||
|
|
│ [✓] BlueCarbon Solutions: ⭐⭐⭐⭐ │
|
||
|
|
│ [ ] OceanSense AI: Voting now │
|
||
|
|
└────────────────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Audience Vote Reveal Timing
|
||
|
|
|
||
|
|
The `audienceRevealTiming` config controls when audience votes become visible:
|
||
|
|
|
||
|
|
**REAL_TIME**
|
||
|
|
- Audience vote totals update live as votes come in
|
||
|
|
- Visible to both jury and admin
|
||
|
|
- Creates dynamic leaderboard experience
|
||
|
|
- Risk: May influence jury voting
|
||
|
|
|
||
|
|
**AFTER_JURY_VOTE**
|
||
|
|
- Audience votes hidden until jury completes voting for each project
|
||
|
|
- Revealed after jury voting window closes
|
||
|
|
- Jury votes independently, then sees audience perspective
|
||
|
|
- Balanced approach
|
||
|
|
|
||
|
|
**AT_DELIBERATION** (recommended default)
|
||
|
|
- Audience votes completely hidden during Live Finals
|
||
|
|
- Revealed only when deliberation begins (R8)
|
||
|
|
- Jury 3 votes with complete independence
|
||
|
|
- Audience data informs deliberation discussion
|
||
|
|
|
||
|
|
### Score Calculation
|
||
|
|
|
||
|
|
Final scores blend jury and audience votes according to `audienceBlendWeight`:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
function calculateFinalScore(
|
||
|
|
juryAverage: number,
|
||
|
|
audienceAverage: number,
|
||
|
|
blendWeight: number
|
||
|
|
): number {
|
||
|
|
const juryWeight = 1 - blendWeight
|
||
|
|
return (juryAverage * juryWeight) + (audienceAverage * blendWeight)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Examples:
|
||
|
|
// blendWeight = 0 (jury only):
|
||
|
|
// Jury 8.5, Audience 7.0 → Final: 8.5
|
||
|
|
|
||
|
|
// blendWeight = 0.3 (70% jury, 30% audience):
|
||
|
|
// Jury 8.5, Audience 7.0 → Final: 8.5 * 0.7 + 7.0 * 0.3 = 8.05
|
||
|
|
|
||
|
|
// blendWeight = 0.5 (equal):
|
||
|
|
// Jury 8.5, Audience 7.0 → Final: 7.75
|
||
|
|
```
|
||
|
|
|
||
|
|
**Criteria-based jury scores** are converted to a weighted average before blending:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
function calculateCriteriaWeightedAverage(
|
||
|
|
criteriaScores: { criterionId: string; score: number }[],
|
||
|
|
criteria: LiveVotingCriterion[]
|
||
|
|
): number {
|
||
|
|
return criteriaScores.reduce((sum, cs) => {
|
||
|
|
const criterion = criteria.find(c => c.id === cs.criterionId)
|
||
|
|
const normalizedScore = cs.score / criterion.maxScore // Scale to 0-1
|
||
|
|
return sum + (normalizedScore * 10 * criterion.weight) // Convert back to 0-10 scale
|
||
|
|
}, 0)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Deliberation (Round 8)
|
||
|
|
|
||
|
|
### Purpose & Context
|
||
|
|
|
||
|
|
**Deliberation IS the confirmation.** There is no separate approval workflow after deliberation. The deliberation voting session produces the official, locked results.
|
||
|
|
|
||
|
|
After Live Finals conclude, Jury 3 enters a formal deliberation period to reach consensus on final rankings. The deliberation process replaces the old WinnerProposal → jury sign-off → admin approval flow.
|
||
|
|
|
||
|
|
**Key principles:**
|
||
|
|
- One deliberation session per category (STARTUP and BUSINESS_CONCEPT deliberate independently)
|
||
|
|
- Multiple voting modes supported (single-winner vote or full ranking)
|
||
|
|
- Tie-breaking mechanisms (runoff, admin decision, or score fallback)
|
||
|
|
- Admin can override entire result if deadlock occurs
|
||
|
|
- Results lock after deliberation completes — immutable official outcome
|
||
|
|
|
||
|
|
### DeliberationConfig Schema
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { z } from 'zod'
|
||
|
|
|
||
|
|
enum DeliberationMode {
|
||
|
|
SINGLE_WINNER_VOTE = 'SINGLE_WINNER_VOTE', // Each juror picks one winner
|
||
|
|
FULL_RANKING = 'FULL_RANKING', // Each juror ranks all projects (1st, 2nd, 3rd...)
|
||
|
|
}
|
||
|
|
|
||
|
|
enum TieBreakMethod {
|
||
|
|
RUNOFF = 'RUNOFF', // New vote with only tied projects
|
||
|
|
ADMIN_DECIDES = 'ADMIN_DECIDES', // Admin manually breaks tie
|
||
|
|
SCORE_FALLBACK = 'SCORE_FALLBACK', // Fall back to Jury 3 scores from Live Finals
|
||
|
|
}
|
||
|
|
|
||
|
|
const DeliberationConfigSchema = z.object({
|
||
|
|
// ── Jury ────────────────────────────────────────────────
|
||
|
|
juryGroupId: z.string(), // Typically same as LiveFinal juryGroupId (Jury 3)
|
||
|
|
|
||
|
|
// ── Voting Mode ─────────────────────────────────────────
|
||
|
|
mode: z.nativeEnum(DeliberationMode).default('SINGLE_WINNER_VOTE'),
|
||
|
|
|
||
|
|
// ── Visibility ──────────────────────────────────────────
|
||
|
|
showCollectiveRankings: z.boolean().default(false),
|
||
|
|
// If true: jurors see aggregated vote counts in real-time
|
||
|
|
// If false: blind voting until deliberation closes
|
||
|
|
|
||
|
|
showPriorJuryData: z.boolean().default(false),
|
||
|
|
// Same as LiveFinalConfig — show Jury 1/2 data during deliberation
|
||
|
|
|
||
|
|
// ── Tie-Breaking ────────────────────────────────────────
|
||
|
|
tieBreakMethod: z.nativeEnum(TieBreakMethod).default('ADMIN_DECIDES'),
|
||
|
|
|
||
|
|
// ── Timing ──────────────────────────────────────────────
|
||
|
|
votingDuration: z.number().min(60).optional(),
|
||
|
|
// Seconds. Null = unlimited until admin closes
|
||
|
|
|
||
|
|
// ── Admin Override ──────────────────────────────────────
|
||
|
|
allowAdminOverride: z.boolean().default(true),
|
||
|
|
|
||
|
|
// ── Winner Count ────────────────────────────────────────
|
||
|
|
topN: z.number().min(1).default(3),
|
||
|
|
// How many winners/ranked positions to determine (e.g., 1st, 2nd, 3rd)
|
||
|
|
})
|
||
|
|
|
||
|
|
type DeliberationConfig = z.infer<typeof DeliberationConfigSchema>
|
||
|
|
```
|
||
|
|
|
||
|
|
### Session Lifecycle
|
||
|
|
|
||
|
|
```
|
||
|
|
┌──────────┐
|
||
|
|
│ OPEN │ — Session created, jurors can review Live Finals results
|
||
|
|
└────┬─────┘
|
||
|
|
│ (Admin opens deliberation voting)
|
||
|
|
▼
|
||
|
|
┌──────────┐
|
||
|
|
│ VOTING │ — Jurors submit votes (SINGLE_WINNER_VOTE or FULL_RANKING)
|
||
|
|
└────┬─────┘
|
||
|
|
│ (All required votes received OR voting duration expires)
|
||
|
|
▼
|
||
|
|
┌───────────┐
|
||
|
|
│ TALLYING │ — System aggregates votes, checks for ties
|
||
|
|
└────┬──────┘
|
||
|
|
│
|
||
|
|
├── No tie → LOCKED
|
||
|
|
│
|
||
|
|
├── Tie detected & tieBreakMethod = RUNOFF
|
||
|
|
│ ▼
|
||
|
|
│ ┌────────┐
|
||
|
|
│ │ RUNOFF │ — New vote with only tied projects (runoffRound++)
|
||
|
|
│ └────┬───┘
|
||
|
|
│ └──→ Back to TALLYING
|
||
|
|
│
|
||
|
|
├── Tie detected & tieBreakMethod = ADMIN_DECIDES
|
||
|
|
│ → Admin manually resolves → LOCKED
|
||
|
|
│
|
||
|
|
└── Tie detected & tieBreakMethod = SCORE_FALLBACK
|
||
|
|
→ Use Jury 3 scores from Live Finals → LOCKED
|
||
|
|
|
||
|
|
┌──────────┐
|
||
|
|
│ LOCKED │ — Results frozen, immutable, official
|
||
|
|
└──────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
### Voting Modes
|
||
|
|
|
||
|
|
#### SINGLE_WINNER_VOTE
|
||
|
|
|
||
|
|
Each juror picks **one** winner project. The project with the most votes becomes the proposed winner. Other projects are ranked by vote count.
|
||
|
|
|
||
|
|
**Example:**
|
||
|
|
- 8 jurors vote
|
||
|
|
- Project A: 5 votes
|
||
|
|
- Project B: 2 votes
|
||
|
|
- Project C: 1 vote
|
||
|
|
|
||
|
|
**Result:**
|
||
|
|
1. Project A (winner — 5 votes)
|
||
|
|
2. Project B (2nd — 2 votes)
|
||
|
|
3. Project C (3rd — 1 vote)
|
||
|
|
|
||
|
|
**Tie scenario:**
|
||
|
|
- Project A: 4 votes
|
||
|
|
- Project B: 4 votes
|
||
|
|
|
||
|
|
→ Trigger tie-breaking mechanism
|
||
|
|
|
||
|
|
#### FULL_RANKING
|
||
|
|
|
||
|
|
Each juror submits an ordinal ranking of all projects (1st, 2nd, 3rd, etc.). Aggregated via **Borda count**.
|
||
|
|
|
||
|
|
**Example with 3 projects, 5 jurors:**
|
||
|
|
|
||
|
|
| Juror | 1st | 2nd | 3rd |
|
||
|
|
|-------|-----|-----|-----|
|
||
|
|
| Alice | A | B | C |
|
||
|
|
| Bob | A | C | B |
|
||
|
|
| Carol | B | A | C |
|
||
|
|
| David | A | B | C |
|
||
|
|
| Emma | B | A | C |
|
||
|
|
|
||
|
|
**Borda count calculation** (3 points for 1st, 2 for 2nd, 1 for 3rd):
|
||
|
|
|
||
|
|
- **Project A**: (3+3+2+3+2) = 13 points
|
||
|
|
- **Project B**: (2+1+3+2+3) = 11 points
|
||
|
|
- **Project C**: (1+2+1+1+1) = 6 points
|
||
|
|
|
||
|
|
**Final ranking:**
|
||
|
|
1. Project A (13 points)
|
||
|
|
2. Project B (11 points)
|
||
|
|
3. Project C (6 points)
|
||
|
|
|
||
|
|
### Aggregation Logic
|
||
|
|
|
||
|
|
#### SINGLE_WINNER_VOTE Aggregation
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
function aggregateSingleWinnerVotes(
|
||
|
|
votes: DeliberationVote[]
|
||
|
|
): { projectId: string; voteCount: number; rank: number }[] {
|
||
|
|
// Count votes per project
|
||
|
|
const voteCounts = new Map<string, number>()
|
||
|
|
votes.forEach(v => {
|
||
|
|
if (v.isWinnerPick) {
|
||
|
|
voteCounts.set(v.projectId, (voteCounts.get(v.projectId) || 0) + 1)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
// Sort by vote count descending
|
||
|
|
const results = Array.from(voteCounts.entries())
|
||
|
|
.map(([projectId, voteCount]) => ({ projectId, voteCount }))
|
||
|
|
.sort((a, b) => b.voteCount - a.voteCount)
|
||
|
|
|
||
|
|
// Assign ranks (handle ties at same rank)
|
||
|
|
let rank = 1
|
||
|
|
results.forEach((r, i) => {
|
||
|
|
if (i > 0 && results[i - 1].voteCount > r.voteCount) {
|
||
|
|
rank = i + 1
|
||
|
|
}
|
||
|
|
r.rank = rank
|
||
|
|
})
|
||
|
|
|
||
|
|
return results
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### FULL_RANKING Aggregation (Borda Count)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
function aggregateFullRankingVotes(
|
||
|
|
votes: DeliberationVote[],
|
||
|
|
projectCount: number
|
||
|
|
): { projectId: string; bordaScore: number; rank: number }[] {
|
||
|
|
// Calculate Borda points: 1st place = N points, 2nd = N-1, etc.
|
||
|
|
const bordaScores = new Map<string, number>()
|
||
|
|
|
||
|
|
votes.forEach(v => {
|
||
|
|
const points = projectCount - (v.rank - 1) // rank 1 → N points, rank 2 → N-1, etc.
|
||
|
|
bordaScores.set(v.projectId, (bordaScores.get(v.projectId) || 0) + points)
|
||
|
|
})
|
||
|
|
|
||
|
|
// Sort by Borda score descending
|
||
|
|
const results = Array.from(bordaScores.entries())
|
||
|
|
.map(([projectId, bordaScore]) => ({ projectId, bordaScore }))
|
||
|
|
.sort((a, b) => b.bordaScore - a.bordaScore)
|
||
|
|
|
||
|
|
// Assign final ranks
|
||
|
|
let rank = 1
|
||
|
|
results.forEach((r, i) => {
|
||
|
|
if (i > 0 && results[i - 1].bordaScore > r.bordaScore) {
|
||
|
|
rank = i + 1
|
||
|
|
}
|
||
|
|
r.rank = rank
|
||
|
|
})
|
||
|
|
|
||
|
|
return results
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Tie-Breaking Methods
|
||
|
|
|
||
|
|
#### RUNOFF
|
||
|
|
|
||
|
|
When multiple projects tie for a position, a new voting round is triggered with only the tied projects.
|
||
|
|
|
||
|
|
**Flow:**
|
||
|
|
1. Initial vote detects tie (e.g., Projects A and B both have 4 votes)
|
||
|
|
2. System creates runoff session with `runoffRound = 1`
|
||
|
|
3. Jurors vote again, choosing only between A and B
|
||
|
|
4. If still tied → runoff round 2 (or escalate to admin)
|
||
|
|
5. If resolved → final ranking locked
|
||
|
|
|
||
|
|
**Runoff limits:**
|
||
|
|
- Maximum 3 runoff rounds
|
||
|
|
- After 3 rounds, escalate to `ADMIN_DECIDES` automatically
|
||
|
|
|
||
|
|
#### ADMIN_DECIDES
|
||
|
|
|
||
|
|
Admin manually selects the winner or ranking order.
|
||
|
|
|
||
|
|
**UI:**
|
||
|
|
```
|
||
|
|
┌────────────────────────────────────────────────────────────────────┐
|
||
|
|
│ ⚠️ Deliberation Tie — Admin Decision Required │
|
||
|
|
│ │
|
||
|
|
│ Current Tie (for 1st place): │
|
||
|
|
│ • Project A: 4 votes │
|
||
|
|
│ • Project B: 4 votes │
|
||
|
|
│ │
|
||
|
|
│ Select the winner: │
|
||
|
|
│ ○ Project A — OceanSense AI │
|
||
|
|
│ ● Project B — BlueCarbon Solutions │
|
||
|
|
│ │
|
||
|
|
│ Reason for decision (required): │
|
||
|
|
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||
|
|
│ │ BlueCarbon Solutions demonstrated stronger implementation │ │
|
||
|
|
│ │ timeline and partnership commitments during Q&A. │ │
|
||
|
|
│ └──────────────────────────────────────────────────────────────┘ │
|
||
|
|
│ │
|
||
|
|
│ [Cancel] [Confirm Decision & Lock] │
|
||
|
|
└────────────────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
**Audit trail:**
|
||
|
|
- Admin decision recorded in `DeliberationResult.isAdminOverridden = true`
|
||
|
|
- Reason stored in `DeliberationResult.overrideReason`
|
||
|
|
- Full vote tallies preserved for transparency
|
||
|
|
|
||
|
|
#### SCORE_FALLBACK
|
||
|
|
|
||
|
|
Use Jury 3 scores from Live Finals (Round 7) to break the tie.
|
||
|
|
|
||
|
|
**Logic:**
|
||
|
|
1. Detect tie in deliberation votes
|
||
|
|
2. Fetch final weighted scores from Live Finals for tied projects
|
||
|
|
3. Project with higher Live Finals score wins the tie
|
||
|
|
4. If still tied (exact same score) → escalate to ADMIN_DECIDES
|
||
|
|
|
||
|
|
**Example:**
|
||
|
|
- Deliberation tie: Project A (4 votes), Project B (4 votes)
|
||
|
|
- Live Finals scores: Project A (8.5), Project B (8.2)
|
||
|
|
- Winner: Project A (score fallback)
|
||
|
|
|
||
|
|
### Admin Override
|
||
|
|
|
||
|
|
Admin can override the **entire deliberation result** at any point, even if no tie exists.
|
||
|
|
|
||
|
|
**Use cases:**
|
||
|
|
- Deadlocked deliberation (jury cannot reach consensus after multiple runoffs)
|
||
|
|
- Jury misconduct discovered (e.g., COI not disclosed)
|
||
|
|
- Extraordinary circumstances (e.g., project disqualified post-deliberation)
|
||
|
|
|
||
|
|
**Override UI:**
|
||
|
|
```
|
||
|
|
┌────────────────────────────────────────────────────────────────────┐
|
||
|
|
│ Override Deliberation Result — STARTUP Category │
|
||
|
|
│ │
|
||
|
|
│ Current Result (Deliberation Vote): │
|
||
|
|
│ 1st: Project A (8 votes) │
|
||
|
|
│ 2nd: Project B (0 votes) │
|
||
|
|
│ │
|
||
|
|
│ Override with new ranking: │
|
||
|
|
│ ┌────┬──────────────────────────────┬────────────────────────┐ │
|
||
|
|
│ │ # │ Project │ Actions │ │
|
||
|
|
│ ├────┼──────────────────────────────┼────────────────────────┤ │
|
||
|
|
│ │ 1 │ ≡ Project B — BlueCarbon │ ↑ ↓ │ │
|
||
|
|
│ │ 2 │ ≡ Project A — OceanSense │ ↑ ↓ │ │
|
||
|
|
│ └────┴──────────────────────────────┴────────────────────────┘ │
|
||
|
|
│ │
|
||
|
|
│ Reason for override (required): │
|
||
|
|
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||
|
|
│ │ Project A team member disclosed undisclosed COI after │ │
|
||
|
|
│ │ deliberation. Ethics committee recommends disqualification. │ │
|
||
|
|
│ └──────────────────────────────────────────────────────────────┘ │
|
||
|
|
│ │
|
||
|
|
│ ⚠ This action is audited and cannot be undone. │
|
||
|
|
│ │
|
||
|
|
│ [Cancel] [Apply Override & Lock] │
|
||
|
|
└────────────────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
**Override behavior:**
|
||
|
|
- `DeliberationResult.isAdminOverridden = true`
|
||
|
|
- Original vote tallies preserved in audit log
|
||
|
|
- New ranking stored in `DeliberationResult.finalRank`
|
||
|
|
- Override reason mandatory (min 20 chars)
|
||
|
|
|
||
|
|
### Participant Management
|
||
|
|
|
||
|
|
#### Participant Status Types
|
||
|
|
|
||
|
|
**REQUIRED**
|
||
|
|
- Active Jury 3 member expected to vote
|
||
|
|
- Counted in quorum calculations
|
||
|
|
|
||
|
|
**ABSENT_EXCUSED**
|
||
|
|
- Jury member cannot participate (e.g., illness, travel)
|
||
|
|
- NOT counted in quorum
|
||
|
|
- Requires admin approval + reason
|
||
|
|
|
||
|
|
**REPLACED**
|
||
|
|
- Original jury member replaced by another juror
|
||
|
|
- Replacement juror assumes voting rights
|
||
|
|
- Original juror cannot vote
|
||
|
|
|
||
|
|
**REPLACEMENT_ACTIVE**
|
||
|
|
- Replacement juror (linked to original via `replacedById`)
|
||
|
|
- Has active voting rights
|
||
|
|
- Counted in quorum
|
||
|
|
|
||
|
|
#### Quorum Calculation
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
function calculateQuorum(participants: DeliberationParticipant[]): {
|
||
|
|
requiredVotes: number;
|
||
|
|
activeParticipants: DeliberationParticipant[];
|
||
|
|
} {
|
||
|
|
const activeParticipants = participants.filter(p =>
|
||
|
|
p.status === 'REQUIRED' || p.status === 'REPLACEMENT_ACTIVE'
|
||
|
|
)
|
||
|
|
|
||
|
|
return {
|
||
|
|
requiredVotes: activeParticipants.length,
|
||
|
|
activeParticipants,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Example:**
|
||
|
|
- Jury 3 has 8 members
|
||
|
|
- 1 member marked ABSENT_EXCUSED
|
||
|
|
- 1 member REPLACED (replacement is REPLACEMENT_ACTIVE)
|
||
|
|
- Quorum: 7 votes required (8 - 1 absent + 0 net change from replacement)
|
||
|
|
|
||
|
|
#### Replacement Flow
|
||
|
|
|
||
|
|
Admin can replace a jury member mid-deliberation:
|
||
|
|
|
||
|
|
1. Admin marks original juror as REPLACED
|
||
|
|
2. Admin selects replacement juror (must be eligible Jury 3 alternate)
|
||
|
|
3. Replacement juror gets REPLACEMENT_ACTIVE status
|
||
|
|
4. Replacement votes count toward quorum
|
||
|
|
5. Original juror's existing vote (if any) is invalidated
|
||
|
|
|
||
|
|
### Juror Experience During Deliberation
|
||
|
|
|
||
|
|
#### Deliberation Dashboard
|
||
|
|
|
||
|
|
```
|
||
|
|
┌────────────────────────────────────────────────────────────────────┐
|
||
|
|
│ DELIBERATION — STARTUP Category Alice Chen │
|
||
|
|
├────────────────────────────────────────────────────────────────────┤
|
||
|
|
│ Status: VOTING │
|
||
|
|
│ Voting closes in: 45:00 (or when all 8 jurors vote) │
|
||
|
|
│ │
|
||
|
|
│ [Review Live Finals Scores] [My Notes] [Jury Discussion] │
|
||
|
|
└────────────────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
┌─ YOUR VOTE ─────────────────────────────────────────────────────────┐
|
||
|
|
│ │
|
||
|
|
│ Voting Mode: Single Winner Vote │
|
||
|
|
│ │
|
||
|
|
│ Select the winning project: │
|
||
|
|
│ │
|
||
|
|
│ ○ Project A — OceanSense AI │
|
||
|
|
│ Live Finals Score: 8.6 (Jury: 8.8, Audience: 7.9) │
|
||
|
|
│ Your Live Finals Score: 9.0 │
|
||
|
|
│ │
|
||
|
|
│ ● Project B — BlueCarbon Solutions │
|
||
|
|
│ Live Finals Score: 8.4 (Jury: 8.2, Audience: 9.1) │
|
||
|
|
│ Your Live Finals Score: 8.5 │
|
||
|
|
│ │
|
||
|
|
│ ○ Project C — AquaClean Tech │
|
||
|
|
│ Live Finals Score: 8.2 (Jury: 8.5, Audience: 7.2) │
|
||
|
|
│ Your Live Finals Score: 8.0 │
|
||
|
|
│ │
|
||
|
|
│ Comment (optional): │
|
||
|
|
│ ┌──────────────────────────────────────────────────────────────┐ │
|
||
|
|
│ │ BlueCarbon's implementation timeline is most realistic. │ │
|
||
|
|
│ └──────────────────────────────────────────────────────────────┘ │
|
||
|
|
│ │
|
||
|
|
│ [Submit Vote] │
|
||
|
|
│ │
|
||
|
|
│ ⚠️ Votes cannot be changed after submission. │
|
||
|
|
└─────────────────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
┌─ VOTING PROGRESS ───────────────────────────────────────────────────┐
|
||
|
|
│ 5 of 8 jurors have voted (62.5%) │
|
||
|
|
│ │
|
||
|
|
│ [✓] You (Alice Chen) [✓] Bob Martin [ ] Carol Davis │
|
||
|
|
│ [✓] David Lee [✓] Emma Wilson [ ] Frank Garcia │
|
||
|
|
│ [✓] Grace Huang [ ] Henry Thompson │
|
||
|
|
│ │
|
||
|
|
│ ⚠️ Results hidden until all jurors vote (showCollectiveRankings=false)│
|
||
|
|
└─────────────────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Review Own Scores
|
||
|
|
|
||
|
|
During deliberation, jurors can review:
|
||
|
|
- Their own Live Finals scores for each project
|
||
|
|
- Their own notes from Live Finals
|
||
|
|
- Project documents from all rounds
|
||
|
|
- Aggregated Live Finals scores (jury + audience)
|
||
|
|
- **If enabled:** Prior Jury 1/2 data (same toggle as Live Finals)
|
||
|
|
|
||
|
|
**NOT visible** (until deliberation closes):
|
||
|
|
- Other jurors' deliberation votes (unless `showCollectiveRankings = true`)
|
||
|
|
- Vote tallies or rankings
|
||
|
|
|
||
|
|
#### Real-Time Vote Counts (Optional)
|
||
|
|
|
||
|
|
If `showCollectiveRankings = true`, jurors see aggregated vote counts in real-time:
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─ CURRENT VOTE TALLY (LIVE) ─────────────────────────────────────────┐
|
||
|
|
│ │
|
||
|
|
│ Project A — OceanSense AI: ███████ 4 votes │
|
||
|
|
│ Project B — BlueCarbon Solutions: ████ 2 votes │
|
||
|
|
│ Project C — AquaClean Tech: ██ 1 vote │
|
||
|
|
│ │
|
||
|
|
│ 7 of 8 jurors have voted │
|
||
|
|
└─────────────────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
**Risk:** Real-time vote counts may influence later voters (bandwagon effect). Recommended default: `showCollectiveRankings = false`.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Result Lock
|
||
|
|
|
||
|
|
### ResultLock Model and Behavior
|
||
|
|
|
||
|
|
When deliberation completes and admin locks the results, a `ResultLock` record is created:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
model ResultLock {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
competitionId String
|
||
|
|
roundId String // Deliberation round ID
|
||
|
|
category ProjectCategory
|
||
|
|
lockedById String // Admin who locked
|
||
|
|
resultSnapshot Json @db.JsonB // Full snapshot of deliberation results
|
||
|
|
lockedAt DateTime @default(now())
|
||
|
|
|
||
|
|
competition Competition @relation(fields: [competitionId], references: [id])
|
||
|
|
round Round @relation(fields: [roundId], references: [id])
|
||
|
|
lockedBy User @relation(fields: [lockedById], references: [id])
|
||
|
|
unlockEvents ResultUnlockEvent[]
|
||
|
|
|
||
|
|
@@unique([competitionId, category]) // One lock per category
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Result snapshot includes:**
|
||
|
|
- Final rankings (1st, 2nd, 3rd, etc.)
|
||
|
|
- All deliberation votes (per juror)
|
||
|
|
- Tie-break history (if runoffs occurred)
|
||
|
|
- Override reason (if admin override applied)
|
||
|
|
- Participant status (active, absent, replaced)
|
||
|
|
- Timestamp of lock
|
||
|
|
- Cryptographic hash of snapshot (for integrity verification)
|
||
|
|
|
||
|
|
**Immutability enforcement:**
|
||
|
|
- Once locked, deliberation votes cannot be changed
|
||
|
|
- Rankings cannot be edited
|
||
|
|
- Admin cannot reorder winners
|
||
|
|
- **Only super-admin can unlock** (see below)
|
||
|
|
|
||
|
|
### ResultUnlockEvent (Super-Admin Only)
|
||
|
|
|
||
|
|
In rare circumstances, results must be unlocked (e.g., legal challenge, fraud discovery):
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
model ResultUnlockEvent {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
resultLockId String
|
||
|
|
unlockedById String // Super-admin only
|
||
|
|
reason String // Mandatory explanation
|
||
|
|
unlockedAt DateTime @default(now())
|
||
|
|
|
||
|
|
resultLock ResultLock @relation(fields: [resultLockId], references: [id])
|
||
|
|
unlockedBy User @relation(fields: [unlockedById], references: [id])
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Unlock flow:**
|
||
|
|
1. Super-admin initiates unlock
|
||
|
|
2. System prompts for mandatory reason (min 50 chars)
|
||
|
|
3. `ResultUnlockEvent` created with full audit trail
|
||
|
|
4. Deliberation session status reverts to TALLYING
|
||
|
|
5. Admin can now modify results or trigger new deliberation
|
||
|
|
6. When re-locked, new `ResultLock` created (old lock preserved for audit)
|
||
|
|
|
||
|
|
**Security:**
|
||
|
|
- Only users with role `SUPER_ADMIN` can unlock
|
||
|
|
- Unlock reason is mandatory and cannot be empty
|
||
|
|
- All unlock events logged to `DecisionAuditLog`
|
||
|
|
- Email notification sent to all program admins when unlock occurs
|
||
|
|
|
||
|
|
### Audit Trail
|
||
|
|
|
||
|
|
Every action in the deliberation and locking process is audited:
|
||
|
|
|
||
|
|
**Audited events:**
|
||
|
|
- Deliberation session created
|
||
|
|
- Juror submits vote
|
||
|
|
- Juror marked absent/excused
|
||
|
|
- Juror replaced
|
||
|
|
- Tie detected
|
||
|
|
- Runoff initiated
|
||
|
|
- Admin override applied
|
||
|
|
- Results locked
|
||
|
|
- Results unlocked (super-admin)
|
||
|
|
- Results re-locked
|
||
|
|
|
||
|
|
**Audit log format:**
|
||
|
|
```typescript
|
||
|
|
{
|
||
|
|
entityType: 'DELIBERATION_SESSION',
|
||
|
|
entityId: sessionId,
|
||
|
|
action: 'RESULTS_LOCKED',
|
||
|
|
userId: adminId,
|
||
|
|
timestamp: '2026-05-15T22:30:00Z',
|
||
|
|
details: {
|
||
|
|
category: 'STARTUP',
|
||
|
|
finalRankings: [
|
||
|
|
{ rank: 1, projectId: 'proj_abc', votes: 5 },
|
||
|
|
{ rank: 2, projectId: 'proj_def', votes: 3 },
|
||
|
|
],
|
||
|
|
tieBreaksApplied: [],
|
||
|
|
adminOverride: false,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Integration with Prior Rounds
|
||
|
|
|
||
|
|
### How R7/R8 Consume Data from R1-R6
|
||
|
|
|
||
|
|
**Round 7 (Live Finals) receives:**
|
||
|
|
- Finalist projects from Round 5 (Jury 2 selection)
|
||
|
|
- All project submissions from R1 and R4
|
||
|
|
- Mentored documents from Round 6 (if promoted)
|
||
|
|
- Jury 1 and Jury 2 scores (if `showPriorJuryData = true`)
|
||
|
|
|
||
|
|
**Round 8 (Deliberation) receives:**
|
||
|
|
- Live Finals scores from Round 7
|
||
|
|
- Deliberation votes from Jury 3
|
||
|
|
- Audience vote totals (if enabled)
|
||
|
|
|
||
|
|
**Data flow diagram:**
|
||
|
|
```
|
||
|
|
R1 (INTAKE) → R2 (FILTERING) → R3 (JURY 1) → R4 (SUBMISSION) → R5 (JURY 2) → R6 (MENTORING)
|
||
|
|
↓ ↓ ↓
|
||
|
|
scores scores documents
|
||
|
|
↓ ↓ ↓
|
||
|
|
└────────────────────────────────┴─────────────┘
|
||
|
|
↓
|
||
|
|
R7 (LIVE FINALS)
|
||
|
|
↓
|
||
|
|
Jury 3 scores
|
||
|
|
Audience votes
|
||
|
|
↓
|
||
|
|
R8 (DELIBERATION)
|
||
|
|
↓
|
||
|
|
ResultLock (final)
|
||
|
|
```
|
||
|
|
|
||
|
|
### showPriorJuryData Toggle Behavior
|
||
|
|
|
||
|
|
**Cross-reference:** [04-jury-groups-and-assignment-policy.md](./04-jury-groups-and-assignment-policy.md) for JuryGroup definitions.
|
||
|
|
|
||
|
|
**During active evaluation rounds (R3, R5):**
|
||
|
|
- Jury 1 and Jury 2 **cannot** see each other's scores/feedback
|
||
|
|
- Strict independent evaluation enforced
|
||
|
|
- Cross-jury visibility = BLOCKED at query level
|
||
|
|
|
||
|
|
**During Live Finals (R7) and Deliberation (R8):**
|
||
|
|
- `showPriorJuryData` toggle becomes active
|
||
|
|
- If `true`: Jury 3 sees Jury 1 and Jury 2 historical data
|
||
|
|
- If `false`: Jury 3 evaluates blind (recommended for independence)
|
||
|
|
|
||
|
|
**Data visibility matrix:**
|
||
|
|
|
||
|
|
| Round | Jury 1 Data | Jury 2 Data | Jury 3 Live Finals Data |
|
||
|
|
|-------|-------------|-------------|-------------------------|
|
||
|
|
| R3 (Jury 1 evaluation) | Own only | BLOCKED | BLOCKED |
|
||
|
|
| R5 (Jury 2 evaluation) | BLOCKED* | Own only | BLOCKED |
|
||
|
|
| R7 (Live Finals) | Configurable** | Configurable** | Own only |
|
||
|
|
| R8 (Deliberation) | Configurable** | Configurable** | All Jury 3 scores visible |
|
||
|
|
|
||
|
|
\* Unless explicitly enabled by admin for reference (rare)
|
||
|
|
\*\* Via `showPriorJuryData` toggle
|
||
|
|
|
||
|
|
### Cross-Reference to Competition Flow
|
||
|
|
|
||
|
|
See [03-competition-flow.md](./03-competition-flow.md) for:
|
||
|
|
- Round sequencing and dependencies
|
||
|
|
- Status transitions (SETUP → ACTIVE → COMPLETED)
|
||
|
|
- Round advancement triggers
|
||
|
|
- Competition-level state machine
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## API & Service Functions
|
||
|
|
|
||
|
|
### New tRPC Procedures
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// src/server/routers/live-finals.ts
|
||
|
|
|
||
|
|
export const liveFinalsRouter = router({
|
||
|
|
// ── Session Management ─────────────────────────────────
|
||
|
|
startCeremony: adminProcedure
|
||
|
|
.input(z.object({ roundId: z.string() }))
|
||
|
|
.mutation(async ({ input, ctx }) => { /* Initialize ceremony */ }),
|
||
|
|
|
||
|
|
pauseCeremony: adminProcedure
|
||
|
|
.input(z.object({ sessionId: z.string() }))
|
||
|
|
.mutation(async ({ input, ctx }) => { /* Pause all timers */ }),
|
||
|
|
|
||
|
|
// ── Project Navigation ─────────────────────────────────
|
||
|
|
setActiveProject: adminProcedure
|
||
|
|
.input(z.object({ sessionId: z.string(), projectId: z.string() }))
|
||
|
|
.mutation(async ({ input, ctx }) => { /* Set presenting project */ }),
|
||
|
|
|
||
|
|
reorderQueue: adminProcedure
|
||
|
|
.input(z.object({ sessionId: z.string(), projectIds: z.array(z.string()) }))
|
||
|
|
.mutation(async ({ input, ctx }) => { /* Reorder presentation */ }),
|
||
|
|
|
||
|
|
// ── Voting ─────────────────────────────────────────────
|
||
|
|
submitJuryVote: juryProcedure
|
||
|
|
.input(z.object({
|
||
|
|
sessionId: z.string(),
|
||
|
|
projectId: z.string(),
|
||
|
|
criteriaScores: z.array(z.object({
|
||
|
|
criterionId: z.string(),
|
||
|
|
score: z.number(),
|
||
|
|
})).optional(),
|
||
|
|
simpleScore: z.number().optional(),
|
||
|
|
}))
|
||
|
|
.mutation(async ({ input, ctx }) => { /* Record jury vote */ }),
|
||
|
|
|
||
|
|
submitAudienceVote: publicProcedure
|
||
|
|
.input(z.object({
|
||
|
|
sessionId: z.string(),
|
||
|
|
projectId: z.string(),
|
||
|
|
token: z.string(),
|
||
|
|
score: z.number(),
|
||
|
|
}))
|
||
|
|
.mutation(async ({ input, ctx }) => { /* Record audience vote */ }),
|
||
|
|
|
||
|
|
// ── Results ────────────────────────────────────────────
|
||
|
|
getLiveResults: publicProcedure
|
||
|
|
.input(z.object({ sessionId: z.string() }))
|
||
|
|
.query(async ({ input }) => { /* Return leaderboard */ }),
|
||
|
|
})
|
||
|
|
|
||
|
|
// src/server/routers/deliberation.ts
|
||
|
|
|
||
|
|
export const deliberationRouter = router({
|
||
|
|
// ── Session Management ─────────────────────────────────
|
||
|
|
createSession: adminProcedure
|
||
|
|
.input(z.object({
|
||
|
|
roundId: z.string(),
|
||
|
|
category: z.nativeEnum(ProjectCategory),
|
||
|
|
config: DeliberationConfigSchema,
|
||
|
|
}))
|
||
|
|
.mutation(async ({ input, ctx }) => { /* Create deliberation session */ }),
|
||
|
|
|
||
|
|
openVoting: adminProcedure
|
||
|
|
.input(z.object({ sessionId: z.string() }))
|
||
|
|
.mutation(async ({ input, ctx }) => { /* Open deliberation voting */ }),
|
||
|
|
|
||
|
|
closeVoting: adminProcedure
|
||
|
|
.input(z.object({ sessionId: z.string() }))
|
||
|
|
.mutation(async ({ input, ctx }) => { /* Close voting, tally results */ }),
|
||
|
|
|
||
|
|
// ── Voting ─────────────────────────────────────────────
|
||
|
|
submitVote: juryProcedure
|
||
|
|
.input(z.object({
|
||
|
|
sessionId: z.string(),
|
||
|
|
votes: z.array(z.object({
|
||
|
|
projectId: z.string(),
|
||
|
|
rank: z.number().optional(), // FULL_RANKING
|
||
|
|
isWinnerPick: z.boolean().optional(), // SINGLE_WINNER_VOTE
|
||
|
|
})),
|
||
|
|
comment: z.string().optional(),
|
||
|
|
}))
|
||
|
|
.mutation(async ({ input, ctx }) => { /* Record deliberation vote */ }),
|
||
|
|
|
||
|
|
// ── Participant Management ─────────────────────────────
|
||
|
|
markAbsent: adminProcedure
|
||
|
|
.input(z.object({
|
||
|
|
sessionId: z.string(),
|
||
|
|
juryMemberId: z.string(),
|
||
|
|
reason: z.string(),
|
||
|
|
}))
|
||
|
|
.mutation(async ({ input, ctx }) => { /* Mark juror absent */ }),
|
||
|
|
|
||
|
|
replaceJuror: adminProcedure
|
||
|
|
.input(z.object({
|
||
|
|
sessionId: z.string(),
|
||
|
|
originalJuryMemberId: z.string(),
|
||
|
|
replacementJuryMemberId: z.string(),
|
||
|
|
}))
|
||
|
|
.mutation(async ({ input, ctx }) => { /* Replace juror */ }),
|
||
|
|
|
||
|
|
// ── Tie-Breaking ───────────────────────────────────────
|
||
|
|
initiateRunoff: adminProcedure
|
||
|
|
.input(z.object({ sessionId: z.string(), tiedProjectIds: z.array(z.string()) }))
|
||
|
|
.mutation(async ({ input, ctx }) => { /* Create runoff vote */ }),
|
||
|
|
|
||
|
|
resolveAdminTieBreak: adminProcedure
|
||
|
|
.input(z.object({
|
||
|
|
sessionId: z.string(),
|
||
|
|
winnerId: z.string(),
|
||
|
|
reason: z.string(),
|
||
|
|
}))
|
||
|
|
.mutation(async ({ input, ctx }) => { /* Admin breaks tie */ }),
|
||
|
|
|
||
|
|
// ── Override ───────────────────────────────────────────
|
||
|
|
overrideResult: adminProcedure
|
||
|
|
.input(z.object({
|
||
|
|
sessionId: z.string(),
|
||
|
|
newRankings: z.array(z.object({ projectId: z.string(), rank: z.number() })),
|
||
|
|
reason: z.string().min(20),
|
||
|
|
}))
|
||
|
|
.mutation(async ({ input, ctx }) => { /* Override entire result */ }),
|
||
|
|
|
||
|
|
// ── Locking ────────────────────────────────────────────
|
||
|
|
lockResults: adminProcedure
|
||
|
|
.input(z.object({ sessionId: z.string() }))
|
||
|
|
.mutation(async ({ input, ctx }) => { /* Create ResultLock */ }),
|
||
|
|
|
||
|
|
unlockResults: superAdminProcedure
|
||
|
|
.input(z.object({ resultLockId: z.string(), reason: z.string().min(50) }))
|
||
|
|
.mutation(async ({ input, ctx }) => { /* Create ResultUnlockEvent */ }),
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
### Service Functions
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// src/server/services/live-finals.ts
|
||
|
|
|
||
|
|
export async function initializeCeremony(roundId: string): Promise<LiveVotingSession>
|
||
|
|
export async function advanceToNextProject(sessionId: string): Promise<void>
|
||
|
|
export async function calculateLiveScores(sessionId: string, category: ProjectCategory): Promise<ProjectScore[]>
|
||
|
|
export async function registerAudienceVoter(sessionId: string, token: string): Promise<AudienceVoter>
|
||
|
|
|
||
|
|
// src/server/services/deliberation.ts
|
||
|
|
|
||
|
|
export async function createDeliberationSession(roundId: string, category: ProjectCategory, config: DeliberationConfig): Promise<DeliberationSession>
|
||
|
|
export async function tallyVotes(sessionId: string): Promise<DeliberationResult[]>
|
||
|
|
export async function detectTies(results: DeliberationResult[]): Promise<{ rank: number; projectIds: string[] }[]>
|
||
|
|
export async function createRunoffSession(sessionId: string, tiedProjectIds: string[]): Promise<DeliberationSession>
|
||
|
|
export async function lockDeliberationResults(sessionId: string, lockedById: string): Promise<ResultLock>
|
||
|
|
export async function unlockResults(resultLockId: string, unlockedById: string, reason: string): Promise<ResultUnlockEvent>
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Edge Cases & Error Handling
|
||
|
|
|
||
|
|
| Scenario | Handling |
|
||
|
|
|----------|----------|
|
||
|
|
| **Jury member loses connection during vote** | Vote saved incrementally (optimistic UI). If connection lost, vote marked incomplete until juror reconnects. |
|
||
|
|
| **Audience member tries to vote twice** | Token + projectId uniqueness enforced. Second vote rejected with error. |
|
||
|
|
| **Admin closes voting before all jury votes** | Warning prompt: "Only X/Y jury votes received. Close anyway?" Audit log records early close. |
|
||
|
|
| **Tie persists after 3 runoff rounds** | Auto-escalate to ADMIN_DECIDES mode. Admin notified immediately. |
|
||
|
|
| **Deliberation vote submitted after session closed** | Rejected with error. Vote not recorded. Juror notified session is closed. |
|
||
|
|
| **Admin tries to lock results with tie** | Blocked. Must resolve tie first (runoff, admin decision, or score fallback). |
|
||
|
|
| **Super-admin unlocks results after 6 months** | Allowed. Unlock event audited with timestamp and reason. All stakeholders notified. |
|
||
|
|
| **Jury member replaced mid-deliberation, replacement votes** | Original vote invalidated. Replacement vote counted. Quorum recalculated. |
|
||
|
|
| **Live Finals scores tied exactly** | Score fallback tie-break uses additional decimal precision. If still tied → admin decides. |
|
||
|
|
| **Audience voting flood (bot attack)** | IP rate limiting + device fingerprinting blocks suspicious patterns. Admin alerted. |
|
||
|
|
| **Project disqualified during deliberation** | Admin removes project from deliberation. Votes for that project invalidated. Remaining projects re-ranked. |
|
||
|
|
| **Network partition during live ceremony** | Stage Manager persists state locally. On reconnect, state reconciled. Event log replayed. |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Security & Access Control
|
||
|
|
|
||
|
|
### Live Finals Access
|
||
|
|
|
||
|
|
| Role | Can View Ceremony | Can Vote (Jury) | Can Vote (Audience) | Can Control Stage Manager |
|
||
|
|
|------|-------------------|-----------------|---------------------|---------------------------|
|
||
|
|
| SUPER_ADMIN | ✓ | — | — | ✓ |
|
||
|
|
| PROGRAM_ADMIN | ✓ | — | — | ✓ |
|
||
|
|
| Jury 3 Member | ✓ | ✓ | — | — |
|
||
|
|
| Audience (token holder) | ✓ (public view) | — | ✓ | — |
|
||
|
|
| Public | ✓ (if public stream enabled) | — | — | — |
|
||
|
|
|
||
|
|
### Deliberation Access
|
||
|
|
|
||
|
|
| Role | Can View Session | Can Vote | Can Mark Absent | Can Override Result | Can Lock | Can Unlock |
|
||
|
|
|------|------------------|----------|-----------------|---------------------|----------|------------|
|
||
|
|
| SUPER_ADMIN | ✓ | — | ✓ | ✓ | ✓ | ✓ |
|
||
|
|
| PROGRAM_ADMIN | ✓ | — | ✓ | ✓ | ✓ | — |
|
||
|
|
| Jury 3 Member | ✓ (own data) | ✓ | — | — | — | — |
|
||
|
|
| Other roles | — | — | — | — | — | — |
|
||
|
|
|
||
|
|
### ResultLock Immutability
|
||
|
|
|
||
|
|
- Database constraints enforce no-update on locked results
|
||
|
|
- tRPC middleware blocks all mutation attempts on locked results
|
||
|
|
- Super-admin unlock requires 2FA confirmation (future enhancement)
|
||
|
|
- All unlock events trigger email alerts to compliance team
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Cross-References
|
||
|
|
|
||
|
|
- [02-data-model.md](./02-data-model.md) — Prisma models for `LiveVotingSession`, `DeliberationSession`, `ResultLock`
|
||
|
|
- [03-competition-flow.md](./03-competition-flow.md) — Round sequencing, status transitions, R7/R8 position in flow
|
||
|
|
- [04-jury-groups-and-assignment-policy.md](./04-jury-groups-and-assignment-policy.md) — JuryGroup (Jury 3), membership, binding to rounds
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Summary
|
||
|
|
|
||
|
|
This document specifies the integrated **Live Finals (R7)** and **Deliberation (R8)** workflow. Key takeaways:
|
||
|
|
|
||
|
|
1. **Live Finals** orchestrates real-time jury voting with optional audience participation, configurable scoring modes, and admin stage management.
|
||
|
|
|
||
|
|
2. **Deliberation IS confirmation** — there is no separate approval step. The deliberation voting session produces the official, locked results.
|
||
|
|
|
||
|
|
3. **Prior jury data visibility** is configurable via `showPriorJuryData`, reconciling independent evaluation with admin flexibility.
|
||
|
|
|
||
|
|
4. **Tie-breaking** supports runoffs, admin decisions, and score fallback, with full audit trails.
|
||
|
|
|
||
|
|
5. **Result immutability** is enforced via `ResultLock`, with super-admin-only unlock capability for extraordinary circumstances.
|
||
|
|
|
||
|
|
6. **Full audit trail** captures every vote, tie-break, override, lock, and unlock event for transparency and compliance.
|
||
|
|
|
||
|
|
This architecture supports both the current Monaco flow and future enhancements (e.g., multi-round deliberation, external judge integration, live broadcast features).
|