MOPC-App/docs/claude-architecture-redesign/02-gap-analysis.md

787 lines
40 KiB
Markdown
Raw Permalink Normal View History

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