40 KiB
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
- Feature-by-Feature Comparison Table
- Per-Step Deep Analysis
- Cross-Cutting Gap Analysis
- Integration Gaps
- 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,isDraftflag allows partial saves - ✅ Deadline enforcement:
Stage.windowOpenAt/windowCloseAtenforced inevaluationRouter.submit()and applicant submission logic - ✅ Grace periods:
GracePeriodmodel per stage/user,extendedUntiloverrides default deadline - ✅ File requirements:
FileRequirementlinked to stages, definesacceptedMimeTypes,maxSizeMB,isRequired - ✅ Late submission tracking:
ProjectFile.isLateflag 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.tsruns 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()setsfinalOutcome - ✅ Duplicate detection: Built-in check by
submittedByEmail, groups duplicates, always flags (never auto-rejects) - ✅ FilteringResult model: Stores
outcome(PASSED/FILTERED_OUT/FLAGGED),ruleResultsJson,aiScreeningJson,finalOutcomeafter 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()acceptsbinaryDecisionfor yes/no semifinalist vote - ✅ Assignment system:
stage-assignment.tsgenerates assignments with workload balancing - ⚠️ Per-juror max:
User.maxAssignmentsexists but treated as global limit across all stages, not stage-specific hard cap - ⚠️ Workload scoring:
calculateWorkloadScore()instage-assignment.tsusespreferredWorkloadbut 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.preferredCategoryRatioJsonor similar field - ❌ Named jury groups: All
JURY_MEMBERusers are a global pool, noJuryGroupmodel to create Jury 1, Jury 2, Jury 3 as separate entities
What's Missing:
- Soft cap + buffer: Need
User.targetAssignments(soft cap) andUser.maxAssignments(hard cap), with UI warning when juror is in buffer zone - Category ratio preferences: Need
User.preferredCategoryRatioJson: { STARTUP: 0.6, BUSINESS_CONCEPT: 0.4 }and assignment scoring that respects ratios - Named jury groups: Need
JuryGroupmodel withname,stageId,members[], so assignment can be scoped to "Jury 1" vs "Jury 2"
What Needs Modification:
- Assignment service: Update
stage-assignment.tsto:- Filter jury pool by
JuryGroup.membersfor the stage - Check both soft cap (warning) and hard cap (reject) when assigning
- Score assignments based on
preferredCategoryRatioJsonto balance category distribution per juror
- Filter jury pool by
- Schema: Add
JuryGroup,JuryMembership, modifyUserto havetargetAssignmentsandpreferredCategoryRatioJson - 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
FileRequirementlist - ✅ Multi-round windows:
Stage.windowOpenAt/windowCloseAtper 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.isReadOnlyorFileRequirement.allowEditsfield - ❌ Cumulative view: No explicit "show all files from all prior stages" flag on stages
What's Missing:
- Read-only flag: Need
ProjectFile.isReadOnlyForApplicant: Booleanset when stage transitions, orFileRequirement.allowEdits: Booleanto control mutability - Cumulative view: Need
Stage.showPriorStageFiles: BooleanorStage.cumulativeFileView: Booleanto make jury file access explicit - File versioning: Current
replacedByIdallows versioning but doesn't enforce read-only from prior rounds
What Needs Modification:
- Applicant file upload: Check
isReadOnlyForApplicantbefore allowing delete/replace - File router: Simplify jury file access by checking
Stage.cumulativeFileViewinstead 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,
binaryDecisionfor finalist vote - ✅ Special awards: Full system with
SpecialAward,AwardEligibility,AwardJuror,AwardVote, AI eligibility screening - ✅ Award tracks:
Track.kind: AWARDallows 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:
MentorAssignmentmodel, AI-suggested matching, manual assignment - ✅ Mentor messages:
MentorMessagemodel for chat messages between mentor and team - ❌ Mentor file upload: No
ProjectFile.uploadedByMentorIdor mentor file upload endpoint - ❌ Threaded file comments: No
FileCommentmodel withparentCommentIdfor threading - ❌ File promotion: No workflow to promote mentor-uploaded file to official project submission
What's Missing:
- Mentor file upload: Need
ProjectFile.uploadedByMentorId: String?, extendfileRouter.getUploadUrl()to allow mentors to upload - File comments: Need
FileCommentmodel: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()) } - File promotion: Need
ProjectFile.promotedFromMentorFileId: String?and a promotion workflow (admin/team approves mentor file as official doc)
What Needs Modification:
- File router: Add
mentorUploadFilemutation, authorization check for mentor role - Mentor router: Add
addFileComment,promoteFileToOfficialmutations - Schema: Add
FileComment, modifyProjectFileto 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.tswithLiveProgressCursor, session management, cursor navigation, queue reordering - ✅ Live voting:
LiveVotemodel, jury/audience voting, criteria-based scoring - ✅ Cohort management:
Cohortgroups projects for voting windows - ⚠️ Vote notes:
LiveVotehas nonotesorcommentaryfield for per-vote notes - ❌ Deliberation period: No
Cohort.deliberationDurationMinutesor deliberation status - ❌ Named Jury 3 group: Same global jury pool issue
What's Missing:
- Vote notes: Add
LiveVote.notes: String?for jury commentary during voting - Deliberation period: Add
Cohort.deliberationDurationMinutes: Int?,Cohort.deliberationStartedAt: DateTime?,Cohort.deliberationEndedAt: DateTime? - Named Jury 3 group: (Same as Jury 1/Jury 2)
What Needs Modification:
- LiveVote model: Add
notesfield - 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.winnerOverriddenflag,OverrideActionlogs admin actions, but no explicit "force majority" vs "choose winner" distinction - ⚠️ Audit trail:
DecisionAuditLog,OverrideActioncomprehensive, but no explicitResultsSnapshotor freeze mechanism - ❌ Individual jury confirmation: No
JuryConfirmationmodel for per-user digital signatures
What's Missing:
- Jury confirmation: Need
JuryConfirmationmodel: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? } - Results freeze: Need
Stage.resultsFrozenAt: DateTime?to mark results as immutable - 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, checkresultsFrozenAtbefore allowing further changes - Override service: Update
OverrideActioncreation to includeoverrideMode
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_MEMBERare treated as a global pool - No scoping of jury to specific stages or rounds
stage-assignment.tsqueries all active jury members without filtering by group
Gap:
- ❌ No
JuryGroupmodel - ❌ No
JuryMembershipmodel to link users to groups - ❌ No stage-level configuration to specify which jury group evaluates that stage
Required Schema Changes:
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.isReadOnlyForApplicantfield - ❌ No
Stage.cumulativeFileViewflag for jury access - ❌ No automated mechanism to mark files as read-only on stage transition
Required Schema Changes:
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.maxAssignmentsexists but treated as global hard cap - ⚠️
User.preferredWorkloadused 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:
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 betweentargetAssignmentsandmaxAssignments(buffer zone) - UI shows warning icon for jurors in buffer zone (target < current < max)
- Filter candidates to exclude jurors at
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.preferredCategoryRatioJsonfield - ❌ Assignment algorithm doesn't score based on category distribution
Required Schema Changes:
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.windowCloseAttimestamp - ❌ 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:
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 - ⚠️
ReminderLogtracks 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: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
ReminderLogmodel 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.winnerOverriddenflag 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: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.tsexecuteTransition():// 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
JuryGroupis introduced, must update every jury-related query/mutation to filter byStage.juryGroupId.
Affected Areas:
- Assignment generation:
stage-assignment.tspreviewStageAssignment()must filterprisma.user.findMany({ where: { role: 'JURY_MEMBER', ... } })toprisma.juryMembership.findMany({ where: { juryGroupId: stage.juryGroupId } }). - Evaluation authorization:
evaluationRouter.submit()must verifyassignment.userIdis a member ofstage.juryGroupId. - Live voting authorization:
liveVotingRouter.submitVote()must verify juror is instage.juryGroupId. - 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.windowCloseAtdirectly 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()withrefetchInterval: 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:
- Multi-jury support (named jury entities with overlap) — highest priority, affects all jury-related features
- Per-juror caps and category ratio preferences — essential for workload balancing
- Round 1 read-only enforcement + cumulative file view — data integrity and jury UX
- Individual jury confirmation + results freeze — compliance and audit requirements
- 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