787 lines
40 KiB
Markdown
787 lines
40 KiB
Markdown
# Gap Analysis: Current System vs. Target 8-Step Competition Flow
|
||
|
||
**Document Version:** 1.0
|
||
**Date:** 2026-02-15
|
||
**Author:** Architecture Review (Claude)
|
||
|
||
---
|
||
|
||
## Executive Summary
|
||
|
||
This gap analysis compares the **current MOPC platform** (pipeline-based, stage-engine architecture) against the **target 8-step competition flow** required for the 2026 Monaco Ocean Protection Challenge.
|
||
|
||
**Key Findings:**
|
||
- **Foundation is Strong**: Pipeline/Track/Stage architecture, stage-engine transitions, AI filtering, jury assignment, and live voting infrastructure are all in place.
|
||
- **Critical Gaps**: Multi-jury support (named jury groups with overlap), multi-round submission windows with read-only enforcement, per-juror capacity constraints (hard cap vs soft cap + buffer), category ratio preferences, countdown timers, and mentoring workspace features are **missing or incomplete**.
|
||
- **Integration Gaps**: The current system treats each stage independently; the target flow requires **cross-stage coordination** (e.g., Round 1 docs become read-only in Round 2, jury sees cumulative files).
|
||
|
||
---
|
||
|
||
## Table of Contents
|
||
|
||
1. [Feature-by-Feature Comparison Table](#1-feature-by-feature-comparison-table)
|
||
2. [Per-Step Deep Analysis](#2-per-step-deep-analysis)
|
||
3. [Cross-Cutting Gap Analysis](#3-cross-cutting-gap-analysis)
|
||
4. [Integration Gaps](#4-integration-gaps)
|
||
5. [Priority Matrix](#5-priority-matrix)
|
||
|
||
---
|
||
|
||
## 1. Feature-by-Feature Comparison Table
|
||
|
||
| Feature | Required by Flow | Current Status | Gap Level | Notes | File References |
|
||
|---------|-----------------|----------------|-----------|-------|-----------------|
|
||
| **Intake (Submission Round 1)** |
|
||
| Public submission form | Applicants upload Round 1 docs, deadline enforcement | ✅ Exists | **None** | `applicantRouter.saveSubmission()` handles create/update, deadline checked via `Stage.windowCloseAt` | `src/server/routers/applicant.ts:126` |
|
||
| Configurable deadline behavior | Grace periods, late submission flags | ✅ Exists | **None** | `GracePeriod` model, `isLate` flag on `ProjectFile` | `prisma/schema.prisma:703-728`, `ProjectFile.isLate:606` |
|
||
| File requirements per stage | Specify required file types, max size, mime types | ✅ Exists | **None** | `FileRequirement` model linked to stages | `prisma/schema.prisma:569-588` |
|
||
| Draft support | Save progress without submitting | ✅ Exists | **None** | `isDraft`, `draftDataJson`, `draftExpiresAt` on `Project` | `prisma/schema.prisma:528-530` |
|
||
| **AI Filtering** |
|
||
| Automated eligibility screening | Run deterministic + AI rules, band by confidence | ✅ Exists | **None** | `stage-filtering.ts` with banding logic, `FilteringResult` outcome | `src/server/services/stage-filtering.ts:173-191` |
|
||
| Admin override capability | Manually resolve flagged projects | ✅ Exists | **None** | `resolveManualDecision()` updates `finalOutcome`, logs override in `OverrideAction` | `src/server/services/stage-filtering.ts:529-611` |
|
||
| Duplicate detection | Flag duplicate submissions (same email) | ✅ Exists | **None** | Built-in duplicate check by `submittedByEmail`, always flags (never auto-rejects) | `src/server/services/stage-filtering.ts:267-289` |
|
||
| **Jury 1 (Evaluation Round 1)** |
|
||
| Semi-finalist selection | Jury evaluates and votes Yes/No | ✅ Exists | **None** | `Evaluation.binaryDecision` field, evaluation submission flow | `src/server/routers/evaluation.ts:130-200` |
|
||
| Hard cap per juror | Max N projects per juror (enforced) | ⚠️ **Partial** | **Partial** | `User.maxAssignments` exists but used as global limit, not stage-specific hard cap | `prisma/schema.prisma:249` |
|
||
| Soft cap + buffer | Target N, allow up to N+buffer with warning | ❌ **Missing** | **Missing** | No concept of soft cap vs hard cap, no buffer configuration | — |
|
||
| Category ratio preferences per juror | Juror wants X% Startup / Y% Concept | ❌ **Missing** | **Missing** | No `User.preferredCategoryRatio` or equivalent | — |
|
||
| Explicit Jury 1 group | Named jury entity with members | ❌ **Missing** | **Missing** | All JURY_MEMBER users are global pool, no stage-scoped jury groups | — |
|
||
| **Semi-finalist Submission (Submission Round 2)** |
|
||
| New doc requirements | Round 2 has different file requirements | ✅ Exists | **None** | Each stage can have its own `FileRequirement` list | `prisma/schema.prisma:569-588` |
|
||
| Round 1 docs become read-only | Applicants can't edit/delete Round 1 files | ❌ **Missing** | **Missing** | No `ProjectFile.isReadOnly` or `FileRequirement.allowEdits` field | — |
|
||
| Jury sees both rounds | Jury can access Round 1 + Round 2 files | ⚠️ **Partial** | **Partial** | File access checks in `fileRouter.getDownloadUrl()` allow prior stages but complex logic, no explicit "cumulative view" | `src/server/routers/file.ts:66-108` |
|
||
| Multi-round submission windows | Distinct open/close dates for Round 1 vs Round 2 | ✅ Exists | **None** | Each stage has `windowOpenAt` / `windowCloseAt` | `prisma/schema.prisma:1888-1889` |
|
||
| **Jury 2 (Evaluation Round 2)** |
|
||
| Finalist selection | Jury evaluates semifinalists, selects finalists | ✅ Exists | **None** | Same evaluation flow, can configure different form per stage | `prisma/schema.prisma:450-472` |
|
||
| Special awards alongside | Run award eligibility + voting in parallel | ✅ Exists | **None** | `SpecialAward` system with `AwardEligibility`, `AwardJuror`, `AwardVote` | `prisma/schema.prisma:1363-1481` |
|
||
| Explicit Jury 2 group | Named jury entity, possibly overlapping with Jury 1 | ❌ **Missing** | **Missing** | Same global jury pool issue | — |
|
||
| Same cap/ratio features | Per-juror hard cap, soft cap, category ratios | ❌ **Missing** | **Missing** | (Same as Jury 1) | — |
|
||
| **Mentoring** |
|
||
| Private mentor-team workspace | Chat, file upload, threaded discussions | ⚠️ **Partial** | **Partial** | `MentorMessage` exists but no threading, no file comments, no promotion mechanism | `prisma/schema.prisma:1577-1590` |
|
||
| Mentor file upload | Mentor can upload files to project | ❌ **Missing** | **Missing** | No `ProjectFile.uploadedByMentorId` or mentor file upload router endpoint | — |
|
||
| Threaded file comments | Comment on specific files with replies | ❌ **Missing** | **Missing** | No `FileComment` model | — |
|
||
| File promotion to official submission | Mentor-uploaded file becomes part of official docs | ❌ **Missing** | **Missing** | No promotion workflow or `ProjectFile.promotedFromMentorFileId` | — |
|
||
| **Jury 3 Live Finals** |
|
||
| Stage manager admin controls | Cursor navigation, pause/resume, queue reorder | ✅ Exists | **None** | `live-control.ts` service with `LiveProgressCursor`, `Cohort` | `src/server/services/live-control.ts:1-619` |
|
||
| Jury live voting with notes | Vote during presentation, add notes | ⚠️ **Partial** | **Partial** | `LiveVote` exists but no `notes` field for per-vote commentary | `prisma/schema.prisma:1073-1099` |
|
||
| Audience voting | Audience can vote with configurable weight | ✅ Exists | **None** | `AudienceVoter`, `allowAudienceVotes`, `audienceVoteWeight` | `prisma/schema.prisma:1051-1060, 1101-1117` |
|
||
| Deliberation period | Time for jury discussion before final vote | ❌ **Missing** | **Missing** | No stage-specific `deliberationDurationMinutes` or deliberation status | — |
|
||
| Explicit Jury 3 group | Named jury entity for live finals | ❌ **Missing** | **Missing** | (Same global pool issue) | — |
|
||
| **Winner Confirmation** |
|
||
| Individual jury member confirmation | Each juror digitally signs off on results | ❌ **Missing** | **Missing** | No `JuryConfirmation` model or per-user signature workflow | — |
|
||
| Admin override to force majority | Admin can override and pick winner | ⚠️ **Partial** | **Partial** | `SpecialAward.winnerOverridden` exists, `OverrideAction` logs admin actions, but no explicit "force majority" vs "choose winner" distinction | `prisma/schema.prisma:1388-1389, 2024-2040` |
|
||
| Results frozen with audit trail | Immutable record of final decision | ⚠️ **Partial** | **Partial** | `DecisionAuditLog` exists, `OverrideAction` tracks changes, but no `ResultsSnapshot` or explicit freeze mechanism | `prisma/schema.prisma:2042-2057` |
|
||
| **Cross-Cutting Features** |
|
||
| Multi-jury support (named entities) | Jury 1, Jury 2, Jury 3 with overlapping members | ❌ **Missing** | **Missing** | No `JuryGroup` or `JuryMembership` model | — |
|
||
| Countdown timers on dashboards | Show time remaining until deadline | ❌ **Missing** | **Missing** | Backend has `windowCloseAt` but no tRPC endpoint for countdown state | — |
|
||
| Email reminders as deadlines approach | Automated reminders at 72h, 24h, 1h | ⚠️ **Partial** | **Partial** | `processEvaluationReminders()` exists for jury, `ReminderLog` tracks sent reminders, but no applicant deadline reminders | `prisma/schema.prisma:1487-1501` |
|
||
| Full audit trail for all decisions | Every action logged, immutable | ✅ Exists | **None** | `DecisionAuditLog`, `OverrideAction`, `AuditLog` comprehensive | `prisma/schema.prisma:754-783, 2024-2057` |
|
||
|
||
**Legend:**
|
||
- ✅ **Exists** = Feature fully implemented
|
||
- ⚠️ **Partial** = Feature partially implemented, needs extension
|
||
- ❌ **Missing** = Feature does not exist
|
||
|
||
---
|
||
|
||
## 2. Per-Step Deep Analysis
|
||
|
||
### Step 1: Intake (Submission Round 1)
|
||
|
||
**What the Flow Requires:**
|
||
- Applicants submit initial docs (executive summary, pitch deck, video)
|
||
- Public submission form with deadline enforcement
|
||
- Configurable grace periods for late submissions
|
||
- Draft support to save progress without submitting
|
||
- File type/size validation per requirement
|
||
|
||
**What Currently Exists:**
|
||
- ✅ **Public submission form**: `applicantRouter.saveSubmission()` creates/updates projects, `isDraft` flag allows partial saves
|
||
- ✅ **Deadline enforcement**: `Stage.windowOpenAt` / `windowCloseAt` enforced in `evaluationRouter.submit()` and applicant submission logic
|
||
- ✅ **Grace periods**: `GracePeriod` model per stage/user, `extendedUntil` overrides default deadline
|
||
- ✅ **File requirements**: `FileRequirement` linked to stages, defines `acceptedMimeTypes`, `maxSizeMB`, `isRequired`
|
||
- ✅ **Late submission tracking**: `ProjectFile.isLate` flag set if uploaded after deadline
|
||
|
||
**What's Missing:**
|
||
- (None — intake is fully functional)
|
||
|
||
**What Needs Modification:**
|
||
- (None — intake meets requirements)
|
||
|
||
**File References:**
|
||
- `src/server/routers/applicant.ts:126-200` (saveSubmission)
|
||
- `prisma/schema.prisma:569-588` (FileRequirement)
|
||
- `prisma/schema.prisma:703-728` (GracePeriod)
|
||
|
||
---
|
||
|
||
### Step 2: AI Filtering
|
||
|
||
**What the Flow Requires:**
|
||
- Automated eligibility screening using deterministic rules (field checks, doc checks) + AI rubric
|
||
- Confidence banding: high confidence auto-pass, low confidence auto-reject, medium confidence flagged for manual review
|
||
- Admin override capability to resolve flagged projects
|
||
- Duplicate submission detection (never auto-reject, always flag)
|
||
|
||
**What Currently Exists:**
|
||
- ✅ **Filtering service**: `stage-filtering.ts` runs deterministic rules first, then AI screening if deterministic passes
|
||
- ✅ **Confidence banding**: `bandByConfidence()` function with thresholds 0.75 (pass) / 0.25 (reject), middle = flagged
|
||
- ✅ **Manual queue**: `getManualQueue()` returns flagged projects, `resolveManualDecision()` sets `finalOutcome`
|
||
- ✅ **Duplicate detection**: Built-in check by `submittedByEmail`, groups duplicates, always flags (never auto-rejects)
|
||
- ✅ **FilteringResult model**: Stores `outcome` (PASSED/FILTERED_OUT/FLAGGED), `ruleResultsJson`, `aiScreeningJson`, `finalOutcome` after override
|
||
|
||
**What's Missing:**
|
||
- (None — filtering is fully functional)
|
||
|
||
**What Needs Modification:**
|
||
- (None — filtering meets requirements)
|
||
|
||
**File References:**
|
||
- `src/server/services/stage-filtering.ts:1-647` (full filtering pipeline)
|
||
- `prisma/schema.prisma:1190-1237` (FilteringRule, FilteringResult)
|
||
|
||
---
|
||
|
||
### Step 3: Jury 1 (Evaluation Round 1)
|
||
|
||
**What the Flow Requires:**
|
||
- Semi-finalist selection with hard/soft caps per juror
|
||
- Per-juror hard cap (e.g., max 20 projects, enforced)
|
||
- Per-juror soft cap + buffer (e.g., target 15, allow up to 18 with warning)
|
||
- Per-juror category ratio preferences (e.g., "I want 60% Startup / 40% Concept")
|
||
- Explicit Jury 1 group (named entity, distinct from Jury 2/Jury 3)
|
||
|
||
**What Currently Exists:**
|
||
- ✅ **Evaluation flow**: `evaluationRouter.submit()` accepts `binaryDecision` for yes/no semifinalist vote
|
||
- ✅ **Assignment system**: `stage-assignment.ts` generates assignments with workload balancing
|
||
- ⚠️ **Per-juror max**: `User.maxAssignments` exists but treated as global limit across all stages, not stage-specific hard cap
|
||
- ⚠️ **Workload scoring**: `calculateWorkloadScore()` in `stage-assignment.ts` uses `preferredWorkload` but not distinct soft vs hard cap
|
||
- ❌ **Soft cap + buffer**: No configuration for soft cap + buffer (e.g., target 15, allow up to 18)
|
||
- ❌ **Category ratio preferences**: No `User.preferredCategoryRatioJson` or similar field
|
||
- ❌ **Named jury groups**: All `JURY_MEMBER` users are a global pool, no `JuryGroup` model to create Jury 1, Jury 2, Jury 3 as separate entities
|
||
|
||
**What's Missing:**
|
||
1. **Soft cap + buffer**: Need `User.targetAssignments` (soft cap) and `User.maxAssignments` (hard cap), with UI warning when juror is in buffer zone
|
||
2. **Category ratio preferences**: Need `User.preferredCategoryRatioJson: { STARTUP: 0.6, BUSINESS_CONCEPT: 0.4 }` and assignment scoring that respects ratios
|
||
3. **Named jury groups**: Need `JuryGroup` model with `name`, `stageId`, `members[]`, so assignment can be scoped to "Jury 1" vs "Jury 2"
|
||
|
||
**What Needs Modification:**
|
||
- **Assignment service**: Update `stage-assignment.ts` to:
|
||
- Filter jury pool by `JuryGroup.members` for the stage
|
||
- Check both soft cap (warning) and hard cap (reject) when assigning
|
||
- Score assignments based on `preferredCategoryRatioJson` to balance category distribution per juror
|
||
- **Schema**: Add `JuryGroup`, `JuryMembership`, modify `User` to have `targetAssignments` and `preferredCategoryRatioJson`
|
||
- **Admin UI**: Jury group management, per-juror cap/ratio configuration
|
||
|
||
**File References:**
|
||
- `src/server/services/stage-assignment.ts:1-777` (assignment algorithm)
|
||
- `src/server/routers/evaluation.ts:130-200` (evaluation submission)
|
||
- `prisma/schema.prisma:241-357` (User model)
|
||
|
||
---
|
||
|
||
### Step 4: Semi-finalist Submission (Submission Round 2)
|
||
|
||
**What the Flow Requires:**
|
||
- New doc requirements (e.g., detailed business plan, updated pitch deck)
|
||
- Round 1 docs become **read-only** for applicants (no edit/delete)
|
||
- Jury sees **both rounds** (cumulative file view)
|
||
- Multi-round submission windows (Round 2 opens after Jury 1 closes)
|
||
|
||
**What Currently Exists:**
|
||
- ✅ **Multi-round file requirements**: Each stage can define its own `FileRequirement` list
|
||
- ✅ **Multi-round windows**: `Stage.windowOpenAt` / `windowCloseAt` per stage
|
||
- ⚠️ **Jury file access**: `fileRouter.getDownloadUrl()` checks if juror has assignment to project, allows access to files from prior stages in same track (lines 66-108), but logic is implicit and complex
|
||
- ❌ **Read-only enforcement**: No `ProjectFile.isReadOnly` or `FileRequirement.allowEdits` field
|
||
- ❌ **Cumulative view**: No explicit "show all files from all prior stages" flag on stages
|
||
|
||
**What's Missing:**
|
||
1. **Read-only flag**: Need `ProjectFile.isReadOnlyForApplicant: Boolean` set when stage transitions, or `FileRequirement.allowEdits: Boolean` to control mutability
|
||
2. **Cumulative view**: Need `Stage.showPriorStageFiles: Boolean` or `Stage.cumulativeFileView: Boolean` to make jury file access explicit
|
||
3. **File versioning**: Current `replacedById` allows versioning but doesn't enforce read-only from prior rounds
|
||
|
||
**What Needs Modification:**
|
||
- **Applicant file upload**: Check `isReadOnlyForApplicant` before allowing delete/replace
|
||
- **File router**: Simplify jury file access by checking `Stage.cumulativeFileView` instead of complex prior-stage logic
|
||
- **Stage transition**: When project moves from Round 1 to Round 2, mark all Round 1 files as `isReadOnlyForApplicant: true`
|
||
- **Schema**: Add `ProjectFile.isReadOnlyForApplicant`, `Stage.cumulativeFileView`
|
||
|
||
**File References:**
|
||
- `src/server/routers/file.ts:12-125` (file download authorization)
|
||
- `prisma/schema.prisma:590-624` (ProjectFile)
|
||
- `prisma/schema.prisma:1879-1922` (Stage)
|
||
|
||
---
|
||
|
||
### Step 5: Jury 2 (Evaluation Round 2)
|
||
|
||
**What the Flow Requires:**
|
||
- Finalist selection (same evaluation mechanics as Jury 1)
|
||
- Special awards eligibility + voting alongside main track
|
||
- Explicit Jury 2 group (named entity, may overlap with Jury 1)
|
||
- Same per-juror caps and category ratio features as Jury 1
|
||
|
||
**What Currently Exists:**
|
||
- ✅ **Evaluation flow**: Identical to Jury 1, `binaryDecision` for finalist vote
|
||
- ✅ **Special awards**: Full system with `SpecialAward`, `AwardEligibility`, `AwardJuror`, `AwardVote`, AI eligibility screening
|
||
- ✅ **Award tracks**: `Track.kind: AWARD` allows award-specific stages to run in parallel
|
||
- ❌ **Named Jury 2 group**: Same global jury pool issue as Jury 1
|
||
|
||
**What's Missing:**
|
||
- (Same as Jury 1: named jury groups, soft cap + buffer, category ratio preferences)
|
||
|
||
**What Needs Modification:**
|
||
- (Same as Jury 1: jury group scoping, cap/ratio logic in assignment service)
|
||
|
||
**File References:**
|
||
- `src/server/routers/specialAward.ts:1-150` (award management)
|
||
- `prisma/schema.prisma:1363-1481` (award models)
|
||
|
||
---
|
||
|
||
### Step 6: Mentoring
|
||
|
||
**What the Flow Requires:**
|
||
- Private mentor-team workspace with:
|
||
- Chat/messaging (already exists)
|
||
- Mentor file upload (mentor uploads docs for team to review)
|
||
- Threaded file comments (comment on specific files with replies)
|
||
- File promotion (mentor-uploaded file becomes part of official submission)
|
||
|
||
**What Currently Exists:**
|
||
- ✅ **Mentor assignment**: `MentorAssignment` model, AI-suggested matching, manual assignment
|
||
- ✅ **Mentor messages**: `MentorMessage` model for chat messages between mentor and team
|
||
- ❌ **Mentor file upload**: No `ProjectFile.uploadedByMentorId` or mentor file upload endpoint
|
||
- ❌ **Threaded file comments**: No `FileComment` model with `parentCommentId` for threading
|
||
- ❌ **File promotion**: No workflow to promote mentor-uploaded file to official project submission
|
||
|
||
**What's Missing:**
|
||
1. **Mentor file upload**: Need `ProjectFile.uploadedByMentorId: String?`, extend `fileRouter.getUploadUrl()` to allow mentors to upload
|
||
2. **File comments**: Need `FileComment` model:
|
||
```prisma
|
||
model FileComment {
|
||
id String @id @default(cuid())
|
||
fileId String
|
||
file ProjectFile @relation(...)
|
||
authorId String
|
||
author User @relation(...)
|
||
content String @db.Text
|
||
parentCommentId String?
|
||
parentComment FileComment? @relation("CommentReplies", ...)
|
||
replies FileComment[] @relation("CommentReplies")
|
||
createdAt DateTime @default(now())
|
||
}
|
||
```
|
||
3. **File promotion**: Need `ProjectFile.promotedFromMentorFileId: String?` and a promotion workflow (admin/team approves mentor file as official doc)
|
||
|
||
**What Needs Modification:**
|
||
- **File router**: Add `mentorUploadFile` mutation, authorization check for mentor role
|
||
- **Mentor router**: Add `addFileComment`, `promoteFileToOfficial` mutations
|
||
- **Schema**: Add `FileComment`, modify `ProjectFile` to link mentor uploads and promotions
|
||
|
||
**File References:**
|
||
- `src/server/routers/mentor.ts:1-200` (mentor operations)
|
||
- `prisma/schema.prisma:1145-1172` (MentorAssignment)
|
||
- `prisma/schema.prisma:1577-1590` (MentorMessage)
|
||
|
||
---
|
||
|
||
### Step 7: Jury 3 Live Finals
|
||
|
||
**What the Flow Requires:**
|
||
- Stage manager admin controls (cursor navigation, pause/resume, queue reorder) — **ALREADY EXISTS**
|
||
- Jury live voting with notes (vote + add commentary per vote)
|
||
- Audience voting — **ALREADY EXISTS**
|
||
- Deliberation period (pause for jury discussion before final vote)
|
||
- Explicit Jury 3 group (named entity for live finals)
|
||
|
||
**What Currently Exists:**
|
||
- ✅ **Live control service**: `live-control.ts` with `LiveProgressCursor`, session management, cursor navigation, queue reordering
|
||
- ✅ **Live voting**: `LiveVote` model, jury/audience voting, criteria-based scoring
|
||
- ✅ **Cohort management**: `Cohort` groups projects for voting windows
|
||
- ⚠️ **Vote notes**: `LiveVote` has no `notes` or `commentary` field for per-vote notes
|
||
- ❌ **Deliberation period**: No `Cohort.deliberationDurationMinutes` or deliberation status
|
||
- ❌ **Named Jury 3 group**: Same global jury pool issue
|
||
|
||
**What's Missing:**
|
||
1. **Vote notes**: Add `LiveVote.notes: String?` for jury commentary during voting
|
||
2. **Deliberation period**: Add `Cohort.deliberationDurationMinutes: Int?`, `Cohort.deliberationStartedAt: DateTime?`, `Cohort.deliberationEndedAt: DateTime?`
|
||
3. **Named Jury 3 group**: (Same as Jury 1/Jury 2)
|
||
|
||
**What Needs Modification:**
|
||
- **LiveVote model**: Add `notes` field
|
||
- **Cohort model**: Add deliberation fields
|
||
- **Live voting router**: Add `startDeliberation()`, `endDeliberation()` procedures
|
||
- **Live control service**: Add deliberation status checks to prevent voting during deliberation
|
||
|
||
**File References:**
|
||
- `src/server/services/live-control.ts:1-619` (live session management)
|
||
- `src/server/routers/live-voting.ts:1-150` (live voting procedures)
|
||
- `prisma/schema.prisma:1035-1071, 1969-2006` (LiveVotingSession, Cohort)
|
||
|
||
---
|
||
|
||
### Step 8: Winner Confirmation
|
||
|
||
**What the Flow Requires:**
|
||
- Individual jury member confirmation (each juror digitally signs off on results)
|
||
- Admin override to force majority or choose winner
|
||
- Results frozen with immutable audit trail
|
||
|
||
**What Currently Exists:**
|
||
- ⚠️ **Admin override**: `SpecialAward.winnerOverridden` flag, `OverrideAction` logs admin actions, but no explicit "force majority" vs "choose winner" distinction
|
||
- ⚠️ **Audit trail**: `DecisionAuditLog`, `OverrideAction` comprehensive, but no explicit `ResultsSnapshot` or freeze mechanism
|
||
- ❌ **Individual jury confirmation**: No `JuryConfirmation` model for per-user digital signatures
|
||
|
||
**What's Missing:**
|
||
1. **Jury confirmation**: Need `JuryConfirmation` model:
|
||
```prisma
|
||
model JuryConfirmation {
|
||
id String @id @default(cuid())
|
||
stageId String
|
||
stage Stage @relation(...)
|
||
userId String
|
||
user User @relation(...)
|
||
confirmedAt DateTime @default(now())
|
||
signature String // Digital signature or consent hash
|
||
ipAddress String?
|
||
userAgent String?
|
||
}
|
||
```
|
||
2. **Results freeze**: Need `Stage.resultsFrozenAt: DateTime?` to mark results as immutable
|
||
3. **Override modes**: Add `OverrideAction.overrideMode: Enum(FORCE_MAJORITY, CHOOSE_WINNER)` for clarity
|
||
|
||
**What Needs Modification:**
|
||
- **Live voting router**: Add `confirmResults()` procedure for jury members to sign off
|
||
- **Admin router**: Add `freezeResults()` procedure, check `resultsFrozenAt` before allowing further changes
|
||
- **Override service**: Update `OverrideAction` creation to include `overrideMode`
|
||
|
||
**File References:**
|
||
- `prisma/schema.prisma:1363-1418` (SpecialAward with winner override)
|
||
- `prisma/schema.prisma:2024-2040` (OverrideAction)
|
||
- `prisma/schema.prisma:2042-2057` (DecisionAuditLog)
|
||
|
||
---
|
||
|
||
## 3. Cross-Cutting Gap Analysis
|
||
|
||
### Multi-Jury Support (Named Jury Entities with Overlap)
|
||
|
||
**Requirement:**
|
||
- Create named jury groups (Jury 1, Jury 2, Jury 3) with explicit membership lists
|
||
- Allow jurors to be members of multiple groups (e.g., Juror A is in Jury 1 and Jury 3 but not Jury 2)
|
||
- Scope assignments, evaluations, and live voting to specific jury groups
|
||
|
||
**Current State:**
|
||
- All users with `role: JURY_MEMBER` are treated as a global pool
|
||
- No scoping of jury to specific stages or rounds
|
||
- `stage-assignment.ts` queries all active jury members without filtering by group
|
||
|
||
**Gap:**
|
||
- ❌ No `JuryGroup` model
|
||
- ❌ No `JuryMembership` model to link users to groups
|
||
- ❌ No stage-level configuration to specify which jury group evaluates that stage
|
||
|
||
**Required Schema Changes:**
|
||
```prisma
|
||
model JuryGroup {
|
||
id String @id @default(cuid())
|
||
programId String
|
||
program Program @relation(...)
|
||
name String // "Jury 1", "Jury 2", "Jury 3"
|
||
description String?
|
||
createdAt DateTime @default(now())
|
||
|
||
memberships JuryMembership[]
|
||
stages Stage[] // One-to-many: stages can specify which jury group evaluates them
|
||
}
|
||
|
||
model JuryMembership {
|
||
id String @id @default(cuid())
|
||
juryGroupId String
|
||
juryGroup JuryGroup @relation(...)
|
||
userId String
|
||
user User @relation(...)
|
||
joinedAt DateTime @default(now())
|
||
|
||
@@unique([juryGroupId, userId])
|
||
}
|
||
|
||
// Extend Stage model:
|
||
model Stage {
|
||
// ... existing fields
|
||
juryGroupId String?
|
||
juryGroup JuryGroup? @relation(...)
|
||
}
|
||
```
|
||
|
||
**Impact:**
|
||
- **High** — Affects assignment generation, evaluation authorization, live voting eligibility
|
||
- **Requires**: New admin UI for jury group management, updates to all jury-related queries/mutations
|
||
|
||
---
|
||
|
||
### Multi-Round Submission Windows
|
||
|
||
**Requirement:**
|
||
- Distinct submission windows for Round 1 (Intake), Round 2 (Semi-finalist submission)
|
||
- Round 1 files become read-only after Round 1 closes
|
||
- Jury sees cumulative files from all prior rounds
|
||
|
||
**Current State:**
|
||
- ✅ Each stage has `windowOpenAt` / `windowCloseAt` (multi-round windows exist)
|
||
- ⚠️ File access is complex and implicit (checks prior stages in track but no clear flag)
|
||
- ❌ No read-only enforcement for applicants after stage transition
|
||
|
||
**Gap:**
|
||
- ❌ No `ProjectFile.isReadOnlyForApplicant` field
|
||
- ❌ No `Stage.cumulativeFileView` flag for jury access
|
||
- ❌ No automated mechanism to mark files as read-only on stage transition
|
||
|
||
**Required Schema Changes:**
|
||
```prisma
|
||
model ProjectFile {
|
||
// ... existing fields
|
||
isReadOnlyForApplicant Boolean @default(false)
|
||
}
|
||
|
||
model Stage {
|
||
// ... existing fields
|
||
cumulativeFileView Boolean @default(false) // If true, jury sees files from all prior stages in track
|
||
}
|
||
```
|
||
|
||
**Impact:**
|
||
- **Medium** — Affects file upload/delete authorization, jury file listing queries
|
||
- **Requires**: Stage transition hook to mark files as read-only, applicant file UI updates, jury file view updates
|
||
|
||
---
|
||
|
||
### Per-Juror Hard Cap vs Soft Cap + Buffer
|
||
|
||
**Requirement:**
|
||
- **Hard cap**: Max N projects (e.g., 20), enforced, cannot exceed
|
||
- **Soft cap**: Target N projects (e.g., 15), preferred, can exceed with warning
|
||
- **Buffer**: Soft cap to hard cap range (e.g., 15-18), shows warning in UI
|
||
|
||
**Current State:**
|
||
- ⚠️ `User.maxAssignments` exists but treated as global hard cap
|
||
- ⚠️ `User.preferredWorkload` used in assignment scoring but not enforced as soft cap
|
||
- ❌ No buffer concept, no UI warning when juror is over target
|
||
|
||
**Gap:**
|
||
- ❌ No distinction between soft cap and hard cap
|
||
- ❌ No buffer configuration or warning mechanism
|
||
|
||
**Required Schema Changes:**
|
||
```prisma
|
||
model User {
|
||
// ... existing fields
|
||
targetAssignments Int? // Soft cap (preferred target)
|
||
maxAssignments Int? // Hard cap (absolute max, enforced)
|
||
// preferredWorkload is deprecated in favor of targetAssignments
|
||
}
|
||
```
|
||
|
||
**Assignment Logic Changes:**
|
||
- Update `stage-assignment.ts`:
|
||
- Filter candidates to exclude jurors at `maxAssignments`
|
||
- Score jurors higher if below `targetAssignments`, lower if between `targetAssignments` and `maxAssignments` (buffer zone)
|
||
- UI shows warning icon for jurors in buffer zone (target < current < max)
|
||
|
||
**Impact:**
|
||
- **Medium** — Affects assignment generation and admin UI for jury workload
|
||
- **Requires**: Update assignment service, admin assignment UI to show soft/hard cap status
|
||
|
||
---
|
||
|
||
### Per-Juror Category Ratio Preferences
|
||
|
||
**Requirement:**
|
||
- Juror specifies preferred category distribution (e.g., "I want 60% Startup / 40% Business Concept")
|
||
- Assignment algorithm respects these preferences when assigning projects
|
||
|
||
**Current State:**
|
||
- ❌ No category ratio configuration per juror
|
||
- ⚠️ Assignment scoring uses tag overlap and workload but not category distribution
|
||
|
||
**Gap:**
|
||
- ❌ No `User.preferredCategoryRatioJson` field
|
||
- ❌ Assignment algorithm doesn't score based on category distribution
|
||
|
||
**Required Schema Changes:**
|
||
```prisma
|
||
model User {
|
||
// ... existing fields
|
||
preferredCategoryRatioJson Json? @db.JsonB // { "STARTUP": 0.6, "BUSINESS_CONCEPT": 0.4 }
|
||
}
|
||
```
|
||
|
||
**Assignment Logic Changes:**
|
||
- Update `stage-assignment.ts`:
|
||
- For each juror, calculate current category distribution of assigned projects
|
||
- Score candidates higher if assigning this project would bring juror's distribution closer to `preferredCategoryRatioJson`
|
||
- Example: Juror wants 60/40 Startup/Concept, currently has 70/30, algorithm prefers assigning Concept projects to rebalance
|
||
|
||
**Impact:**
|
||
- **Medium** — Affects assignment generation quality, requires juror onboarding to set preferences
|
||
- **Requires**: Update assignment algorithm, admin UI for juror profile editing, onboarding flow
|
||
|
||
---
|
||
|
||
### Countdown Timers on Dashboards
|
||
|
||
**Requirement:**
|
||
- Applicant dashboard shows countdown to submission deadline
|
||
- Jury dashboard shows countdown to evaluation deadline
|
||
- Admin dashboard shows countdown to stage window close
|
||
|
||
**Current State:**
|
||
- ✅ Backend has `Stage.windowCloseAt` timestamp
|
||
- ❌ No tRPC endpoint to fetch countdown state (time remaining, status: open/closing soon/closed)
|
||
- ❌ Frontend has no countdown component
|
||
|
||
**Gap:**
|
||
- ❌ No `stageRouter.getCountdown()` or similar procedure
|
||
- ❌ No frontend countdown component
|
||
|
||
**Required Changes:**
|
||
- Add tRPC procedure:
|
||
```typescript
|
||
stageRouter.getCountdown: protectedProcedure
|
||
.input(z.object({ stageId: z.string() }))
|
||
.query(async ({ ctx, input }) => {
|
||
const stage = await ctx.prisma.stage.findUniqueOrThrow({ where: { id: input.stageId } })
|
||
const now = new Date()
|
||
const closeAt = stage.windowCloseAt
|
||
if (!closeAt) return { status: 'no_deadline', timeRemaining: null }
|
||
const remaining = closeAt.getTime() - now.getTime()
|
||
if (remaining <= 0) return { status: 'closed', timeRemaining: 0 }
|
||
return {
|
||
status: remaining < 3600000 ? 'closing_soon' : 'open', // < 1 hour = closing soon
|
||
timeRemaining: remaining,
|
||
closeAt,
|
||
}
|
||
})
|
||
```
|
||
- Frontend: Countdown component that polls `getCountdown()` and displays "X days Y hours Z minutes remaining"
|
||
|
||
**Impact:**
|
||
- **Low** — UX improvement, no data model changes
|
||
- **Requires**: New tRPC procedure, frontend countdown component, dashboard integration
|
||
|
||
---
|
||
|
||
### Email Reminders as Deadlines Approach
|
||
|
||
**Requirement:**
|
||
- Automated email reminders at 72 hours, 24 hours, 1 hour before deadline
|
||
- For applicants (submission deadlines) and jury (evaluation deadlines)
|
||
|
||
**Current State:**
|
||
- ⚠️ `processEvaluationReminders()` exists for jury reminders
|
||
- ⚠️ `ReminderLog` tracks sent reminders to prevent duplicates
|
||
- ❌ No applicant deadline reminder cron job
|
||
- ❌ No configurable reminder intervals (hardcoded to 3 days, 24h, 1h in evaluation reminders)
|
||
|
||
**Gap:**
|
||
- ❌ No applicant reminder service
|
||
- ❌ No configurable reminder intervals per stage
|
||
|
||
**Required Changes:**
|
||
- Add `Stage.reminderIntervalsJson: Json?` // `[72, 24, 1]` (hours before deadline)
|
||
- Add `src/server/services/applicant-reminders.ts`:
|
||
```typescript
|
||
export async function processApplicantReminders(prisma: PrismaClient) {
|
||
const now = new Date()
|
||
const stages = await prisma.stage.findMany({
|
||
where: { status: 'STAGE_ACTIVE', windowCloseAt: { gte: now } },
|
||
})
|
||
for (const stage of stages) {
|
||
const intervals = (stage.reminderIntervalsJson as number[]) ?? [72, 24, 1]
|
||
for (const hoursBeforeDeadline of intervals) {
|
||
const reminderTime = new Date(stage.windowCloseAt!.getTime() - hoursBeforeDeadline * 3600000)
|
||
if (now >= reminderTime && now < new Date(reminderTime.getTime() + 3600000)) {
|
||
// Send reminders to all applicants with draft projects in this stage
|
||
// Check ReminderLog to avoid duplicates
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
- Add cron job in `src/app/api/cron/applicant-reminders/route.ts`
|
||
|
||
**Impact:**
|
||
- **Medium** — Improves applicant engagement, reduces late submissions
|
||
- **Requires**: New service, new cron endpoint, extend `ReminderLog` model if needed
|
||
|
||
---
|
||
|
||
### Admin Override Capability at Every Step
|
||
|
||
**Requirement:**
|
||
- Admin can override any automated decision (filtering, assignment, voting results)
|
||
- Override is logged with reason code and reason text in `OverrideAction`
|
||
|
||
**Current State:**
|
||
- ✅ Filtering: `resolveManualDecision()` overrides flagged projects
|
||
- ✅ Assignment: Manual assignment creation bypasses AI
|
||
- ⚠️ Live voting: `SpecialAward.winnerOverridden` flag exists but no explicit override flow for live voting results
|
||
- ⚠️ Stage transitions: No override capability to force projects between stages
|
||
|
||
**Gap:**
|
||
- ❌ No admin UI to override stage transitions (force project to next stage even if guard fails)
|
||
- ❌ No admin override for live voting results (admin can pick winner but not documented as override)
|
||
|
||
**Required Changes:**
|
||
- Add `stageRouter.overrideTransition()` procedure:
|
||
```typescript
|
||
overrideTransition: adminProcedure
|
||
.input(z.object({
|
||
projectId: z.string(),
|
||
fromStageId: z.string(),
|
||
toStageId: z.string(),
|
||
reasonCode: z.nativeEnum(OverrideReasonCode),
|
||
reasonText: z.string(),
|
||
}))
|
||
.mutation(async ({ ctx, input }) => {
|
||
// Force executeTransition() without validation
|
||
// Log in OverrideAction
|
||
})
|
||
```
|
||
- Add `liveVotingRouter.overrideWinner()` procedure (similar flow)
|
||
|
||
**Impact:**
|
||
- **Low** — Fills gaps in admin control, already mostly exists
|
||
- **Requires**: New admin procedures, UI buttons for override actions
|
||
|
||
---
|
||
|
||
## 4. Integration Gaps
|
||
|
||
### Cross-Stage File Visibility
|
||
|
||
**Issue:**
|
||
- Current file access is stage-scoped. Jury assigned to Round 2 can technically access Round 1 files (via complex `fileRouter.getDownloadUrl()` logic checking prior stages), but this is implicit and fragile.
|
||
- No clear flag to say "Round 2 jury should see Round 1 + Round 2 files" vs "Round 2 jury should only see Round 2 files".
|
||
|
||
**Required:**
|
||
- Add `Stage.cumulativeFileView: Boolean` — if true, jury sees files from all prior stages in the track.
|
||
- Simplify `fileRouter.getDownloadUrl()` authorization logic to check this flag instead of manual prior-stage traversal.
|
||
|
||
**Impact:**
|
||
- **Medium** — Simplifies file access logic, makes jury file view behavior explicit.
|
||
|
||
---
|
||
|
||
### Round 1 to Round 2 Transition (File Read-Only Enforcement)
|
||
|
||
**Issue:**
|
||
- When a project transitions from Round 1 (Intake) to Round 2 (Semi-finalist submission), Round 1 files should become read-only for applicants.
|
||
- Currently, no mechanism enforces this. Applicants could theoretically delete/replace Round 1 files during Round 2.
|
||
|
||
**Required:**
|
||
- Stage transition hook in `stage-engine.ts` `executeTransition()`:
|
||
```typescript
|
||
// After creating destination PSS:
|
||
if (fromStage.stageType === 'INTAKE' && toStage.stageType === 'INTAKE') {
|
||
// Mark all project files uploaded in fromStage as read-only for applicant
|
||
await tx.projectFile.updateMany({
|
||
where: { projectId, roundId: fromStageRoundId },
|
||
data: { isReadOnlyForApplicant: true },
|
||
})
|
||
}
|
||
```
|
||
- Applicant file upload/delete checks: Reject if `ProjectFile.isReadOnlyForApplicant: true`.
|
||
|
||
**Impact:**
|
||
- **High** — Ensures data integrity, prevents applicants from tampering with prior round submissions.
|
||
|
||
---
|
||
|
||
### Jury Group Scoping Across All Jury-Related Operations
|
||
|
||
**Issue:**
|
||
- Assignments, evaluations, live voting all currently use global jury pool.
|
||
- Once `JuryGroup` is introduced, must update every jury-related query/mutation to filter by `Stage.juryGroupId`.
|
||
|
||
**Affected Areas:**
|
||
1. **Assignment generation**: `stage-assignment.ts` `previewStageAssignment()` must filter `prisma.user.findMany({ where: { role: 'JURY_MEMBER', ... } })` to `prisma.juryMembership.findMany({ where: { juryGroupId: stage.juryGroupId } })`.
|
||
2. **Evaluation authorization**: `evaluationRouter.submit()` must verify `assignment.userId` is a member of `stage.juryGroupId`.
|
||
3. **Live voting authorization**: `liveVotingRouter.submitVote()` must verify juror is in `stage.juryGroupId`.
|
||
4. **Admin assignment UI**: Dropdown to select jurors must filter by jury group.
|
||
|
||
**Impact:**
|
||
- **High** — Pervasive change across all jury-related features.
|
||
- **Requires**: Careful migration plan, extensive testing.
|
||
|
||
---
|
||
|
||
### Countdown Timer Backend Support
|
||
|
||
**Issue:**
|
||
- Dashboards need real-time countdown to deadlines, but no backend service provides this.
|
||
- Frontend would need to poll `Stage.windowCloseAt` directly and calculate client-side, or use a tRPC subscription.
|
||
|
||
**Required:**
|
||
- Add `stageRouter.getCountdown()` procedure (described in Cross-Cutting section).
|
||
- Frontend uses `trpc.stage.getCountdown.useQuery()` with `refetchInterval: 60000` (1 minute polling).
|
||
- Optionally: WebSocket subscription for real-time updates (out of scope for now, polling is sufficient).
|
||
|
||
**Impact:**
|
||
- **Low** — Backend is simple, frontend polling handles real-time updates.
|
||
|
||
---
|
||
|
||
## 5. Priority Matrix
|
||
|
||
Features ranked by **Business Impact** (High/Medium/Low) x **Implementation Effort** (High/Medium/Low).
|
||
|
||
| Feature | Business Impact | Implementation Effort | Priority Quadrant | Notes |
|
||
|---------|----------------|----------------------|-------------------|-------|
|
||
| **Multi-jury support (named groups)** | **High** | **High** | **Critical** | Required for all 3 jury rounds, affects assignments/evaluations/voting |
|
||
| **Round 1 docs read-only enforcement** | **High** | **Low** | **Quick Win** | Data integrity essential, simple flag + hook |
|
||
| **Per-juror hard cap vs soft cap + buffer** | **High** | **Medium** | **Critical** | Ensures balanced workload, prevents burnout |
|
||
| **Per-juror category ratio preferences** | **Medium** | **Medium** | **Important** | Improves assignment quality, enhances juror satisfaction |
|
||
| **Jury vote notes (live finals)** | **Medium** | **Low** | **Quick Win** | Enhances deliberation, simple schema change |
|
||
| **Deliberation period (live finals)** | **Medium** | **Low** | **Quick Win** | Required for live finals flow, simple cohort fields |
|
||
| **Individual jury confirmation** | **High** | **Medium** | **Critical** | Legal/compliance requirement for final results |
|
||
| **Results freeze mechanism** | **High** | **Low** | **Quick Win** | Immutable audit trail, simple timestamp flag |
|
||
| **Cumulative file view flag** | **Medium** | **Low** | **Quick Win** | Simplifies jury file access logic |
|
||
| **Mentor file upload** | **Medium** | **Medium** | **Important** | Enhances mentoring, requires file router extension |
|
||
| **Threaded file comments** | **Low** | **Medium** | **Nice to Have** | Improves collaboration, but not blocking |
|
||
| **File promotion workflow** | **Low** | **Medium** | **Nice to Have** | Advanced feature, can defer to later phase |
|
||
| **Countdown timers (UI)** | **Low** | **Low** | **Nice to Have** | UX improvement, no data model changes |
|
||
| **Applicant deadline reminders** | **Medium** | **Low** | **Quick Win** | Reduces late submissions, simple cron job |
|
||
| **Admin override for stage transitions** | **Low** | **Low** | **Nice to Have** | Edge case, manual workaround exists |
|
||
|
||
**Priority Quadrants:**
|
||
- **Critical (High Impact / High Effort)**: Multi-jury support, jury confirmation — **must do**, high planning required
|
||
- **Quick Wins (High Impact / Low Effort)**: Read-only enforcement, results freeze, deliberation period — **do first**
|
||
- **Important (Medium Impact / Medium Effort)**: Caps/ratios, mentor file upload — **do after quick wins**
|
||
- **Nice to Have (Low Impact / Any Effort)**: File comments threading, countdown timers — **defer or phase 2**
|
||
|
||
---
|
||
|
||
## Conclusion
|
||
|
||
The current MOPC platform has a **solid foundation** with the pipeline/track/stage architecture, stage-engine transitions, AI filtering, jury assignment, and live voting infrastructure fully implemented. The **critical gaps** are:
|
||
|
||
1. **Multi-jury support** (named jury entities with overlap) — **highest priority**, affects all jury-related features
|
||
2. **Per-juror caps and category ratio preferences** — **essential for workload balancing**
|
||
3. **Round 1 read-only enforcement + cumulative file view** — **data integrity and jury UX**
|
||
4. **Individual jury confirmation + results freeze** — **compliance and audit requirements**
|
||
5. **Mentoring workspace features** (file upload, comments, promotion) — **enhances mentoring but lower priority**
|
||
|
||
**Recommended Approach:**
|
||
- **Phase 1 (Quick Wins)**: Read-only enforcement, results freeze, deliberation period, vote notes, applicant reminders — **2-3 weeks**
|
||
- **Phase 2 (Critical)**: Multi-jury support, jury confirmation — **4-6 weeks** (complex, pervasive changes)
|
||
- **Phase 3 (Important)**: Caps/ratios, mentor file upload — **3-4 weeks**
|
||
- **Phase 4 (Nice to Have)**: Threaded comments, file promotion, countdown timers — **defer to post-MVP**
|
||
|
||
Total estimated effort for Phases 1-3: **9-13 weeks** (assumes single developer, includes testing).
|
||
|
||
---
|
||
|
||
**End of Gap Analysis Document**
|