MOPC-App/docs/unified-architecture-redesign/07-live-finals-and-delibera...

70 KiB

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:

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:

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:

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

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

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)

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

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:

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):

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:

{
  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 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 for:

  • Round sequencing and dependencies
  • Status transitions (SETUP → ACTIVE → COMPLETED)
  • Round advancement triggers
  • Competition-level state machine

API & Service Functions

New tRPC Procedures

// 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

// 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


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