MOPC-App/docs/claude-architecture-redesign/06-round-evaluation.md

699 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Round: Evaluation (Jury 1 & Jury 2)
## 1. Purpose & Position in Flow
The EVALUATION round is the core judging mechanism of the competition. It appears **twice** in the standard flow:
| Instance | Name | Position | Jury | Purpose | Output |
|----------|------|----------|------|---------|--------|
| Round 3 | "Jury 1 — Semi-finalist Selection" | After FILTERING | Jury 1 | Score projects, select semi-finalists | Semi-finalists per category |
| Round 5 | "Jury 2 — Finalist Selection" | After SUBMISSION Round 2 | Jury 2 | Score semi-finalists, select finalists + awards | Finalists per category |
Both instances use the same `RoundType.EVALUATION` but are configured independently with:
- Different jury groups (Jury 1 vs Jury 2)
- Different evaluation forms/rubrics
- Different visible submission windows (Jury 1 sees Window 1 only; Jury 2 sees Windows 1+2)
- Different advancement counts
---
## 2. Data Model
### Round Record
```
Round {
id: "round-jury-1"
competitionId: "comp-2026"
name: "Jury 1 — Semi-finalist Selection"
roundType: EVALUATION
status: ROUND_DRAFT → ROUND_ACTIVE → ROUND_CLOSED
sortOrder: 2
windowOpenAt: "2026-04-01" // Evaluation window start
windowCloseAt: "2026-04-30" // Evaluation window end
juryGroupId: "jury-group-1" // Links to Jury 1
submissionWindowId: null // EVALUATION rounds don't collect submissions
configJson: { ...EvaluationConfig }
}
```
### EvaluationConfig
```typescript
type EvaluationConfig = {
// --- Assignment Settings ---
requiredReviewsPerProject: number // How many jurors review each project (default: 3)
// --- Scoring Mode ---
scoringMode: "criteria" | "global" | "binary"
// criteria: Score per criterion + weighted total
// global: Single 1-10 score
// binary: Yes/No decision (semi-finalist worthy?)
requireFeedback: boolean // Must provide text feedback (default: true)
// --- COI ---
coiRequired: boolean // Must declare COI before evaluating (default: true)
// --- Peer Review ---
peerReviewEnabled: boolean // Jurors can see anonymized peer evaluations after submission
anonymizationLevel: "fully_anonymous" | "show_initials" | "named"
// --- AI Features ---
aiSummaryEnabled: boolean // Generate AI-powered evaluation summaries
aiAssignmentEnabled: boolean // Allow AI-suggested jury-project matching
// --- Advancement ---
advancementMode: "auto_top_n" | "admin_selection" | "ai_recommended"
advancementConfig: {
perCategory: boolean // Separate counts per STARTUP / BUSINESS_CONCEPT
startupCount: number // How many startups advance (default: 10 for Jury 1, 3 for Jury 2)
conceptCount: number // How many concepts advance
tieBreaker: "admin_decides" | "highest_individual" | "revote"
}
}
```
### Related Models
| Model | Role |
|-------|------|
| `JuryGroup` | Named jury entity linked to this round |
| `JuryGroupMember` | Members of the jury with per-juror overrides |
| `Assignment` | Juror-project pairing for this round, linked to JuryGroup |
| `Evaluation` | Score/feedback submitted by a juror for one project |
| `EvaluationForm` | Rubric/criteria definition for this round |
| `ConflictOfInterest` | COI declaration per assignment |
| `GracePeriod` | Per-juror deadline extension |
| `EvaluationSummary` | AI-generated insights per project per round |
| `EvaluationDiscussion` | Peer review discussion threads |
| `RoundSubmissionVisibility` | Which submission windows' docs jury can see |
| `AdvancementRule` | How projects advance after evaluation |
| `ProjectRoundState` | Per-project state in this round |
---
## 3. Setup Phase (Before Window Opens)
### 3.1 Admin Creates the Evaluation Round
Admin uses the competition wizard or round management UI to:
1. **Create the Round** with type EVALUATION
2. **Link a JuryGroup** — select "Jury 1" (or create a new jury group)
3. **Set the evaluation window** — start and end dates
4. **Configure the evaluation form** — scoring criteria, weights, scales
5. **Set visibility** — which submission windows jury can see (via RoundSubmissionVisibility)
6. **Configure advancement rules** — how many advance per category
### 3.2 Jury Group Configuration
The linked JuryGroup has:
```
JuryGroup {
name: "Jury 1"
defaultMaxAssignments: 20 // Default cap per juror
defaultCapMode: SOFT // HARD | SOFT | NONE
softCapBuffer: 2 // Can exceed by 2 for load balancing
categoryQuotasEnabled: true
defaultCategoryQuotas: {
"STARTUP": { "min": 3, "max": 15 },
"BUSINESS_CONCEPT": { "min": 3, "max": 15 }
}
allowJurorCapAdjustment: true // Jurors can adjust their cap during onboarding
allowJurorRatioAdjustment: true // Jurors can adjust their category preference
}
```
### 3.3 Per-Juror Overrides
Each `JuryGroupMember` can override group defaults:
```
JuryGroupMember {
juryGroupId: "jury-group-1"
userId: "judge-alice"
maxAssignmentsOverride: 25 // Alice wants more projects
capModeOverride: HARD // Alice: hard cap, no exceptions
categoryQuotasOverride: {
"STARTUP": { "min": 5, "max": 20 }, // Alice prefers startups
"BUSINESS_CONCEPT": { "min": 0, "max": 5 }
}
preferredStartupRatio: 0.8 // 80% startups
}
```
### 3.4 Juror Onboarding (Optional)
If `allowJurorCapAdjustment` or `allowJurorRatioAdjustment` is true:
1. When a juror first opens their jury dashboard after being added to the group
2. A one-time onboarding dialog appears:
- "Your default maximum is 20 projects. Would you like to adjust?" (slider)
- "Your default startup/concept ratio is 50/50. Would you like to adjust?" (slider)
3. Juror saves preferences → stored in `JuryGroupMember.maxAssignmentsOverride` and `preferredStartupRatio`
4. Dialog doesn't appear again (tracked via `JuryGroupMember.updatedAt` or a flag)
---
## 4. Assignment System (Enhanced)
### 4.1 Assignment Algorithm — Jury-Group-Aware
The current `stage-assignment.ts` algorithm is enhanced to:
1. **Filter jury pool by JuryGroup** — only members of the linked jury group are considered
2. **Apply hard/soft cap logic** per juror
3. **Apply category quotas** per juror
4. **Score candidates** using existing expertise matching + workload balancing + geo-diversity
#### Effective Limits Resolution
```typescript
function getEffectiveLimits(member: JuryGroupMember, group: JuryGroup): EffectiveLimits {
return {
maxAssignments: member.maxAssignmentsOverride ?? group.defaultMaxAssignments,
capMode: member.capModeOverride ?? group.defaultCapMode,
softCapBuffer: group.softCapBuffer, // Group-level only (not per-juror)
categoryQuotas: member.categoryQuotasOverride ?? group.defaultCategoryQuotas,
categoryQuotasEnabled: group.categoryQuotasEnabled,
preferredStartupRatio: member.preferredStartupRatio,
}
}
```
#### Cap Enforcement Logic
```typescript
function canAssignMore(
jurorId: string,
projectCategory: CompetitionCategory,
currentLoad: LoadTracker,
limits: EffectiveLimits
): { allowed: boolean; penalty: number; reason?: string } {
const total = currentLoad.total(jurorId)
const catLoad = currentLoad.byCategory(jurorId, projectCategory)
// 1. HARD cap check
if (limits.capMode === "HARD" && total >= limits.maxAssignments) {
return { allowed: false, penalty: 0, reason: "Hard cap reached" }
}
// 2. SOFT cap check (can exceed by buffer)
let overflowPenalty = 0
if (limits.capMode === "SOFT") {
if (total >= limits.maxAssignments + limits.softCapBuffer) {
return { allowed: false, penalty: 0, reason: "Soft cap + buffer exceeded" }
}
if (total >= limits.maxAssignments) {
// In buffer zone — apply increasing penalty
overflowPenalty = (total - limits.maxAssignments + 1) * 15
}
}
// 3. Category quota check
if (limits.categoryQuotasEnabled && limits.categoryQuotas) {
const quota = limits.categoryQuotas[projectCategory]
if (quota) {
if (catLoad >= quota.max) {
return { allowed: false, penalty: 0, reason: `Category ${projectCategory} max reached (${quota.max})` }
}
// Bonus for under-min
if (catLoad < quota.min) {
overflowPenalty -= 15 // Negative penalty = bonus
}
}
}
// 4. Ratio preference alignment
if (limits.preferredStartupRatio != null && total > 0) {
const currentStartupRatio = currentLoad.byCategory(jurorId, "STARTUP") / total
const isStartup = projectCategory === "STARTUP"
const wantMore = isStartup
? currentStartupRatio < limits.preferredStartupRatio
: currentStartupRatio > limits.preferredStartupRatio
if (wantMore) overflowPenalty -= 10 // Bonus for aligning with preference
else overflowPenalty += 10 // Penalty for diverging
}
return { allowed: true, penalty: overflowPenalty }
}
```
### 4.2 Assignment Flow
```
1. Admin opens Assignment panel for Round 3 (Jury 1)
2. System loads:
- Projects with ProjectRoundState PENDING/IN_PROGRESS in this round
- JuryGroup members (with effective limits)
- Existing assignments (to avoid duplicates)
- COI records (to skip conflicted pairs)
3. Admin clicks "Generate Suggestions"
4. Algorithm runs:
a. For each project (sorted by fewest current assignments):
- Score each eligible juror (tag matching + workload + geo + cap/quota penalties)
- Select top N jurors (N = requiredReviewsPerProject - existing reviews)
- Track load in jurorLoadMap
b. Report unassigned projects (jurors at capacity)
5. Admin reviews preview:
- Assignment matrix (juror × project grid)
- Load distribution chart
- Unassigned projects list
- Category distribution per juror
6. Admin can:
- Accept all suggestions
- Modify individual assignments (drag-drop or manual add/remove)
- Re-run with different parameters
7. Admin clicks "Apply Assignments"
8. System creates Assignment records with juryGroupId set
9. Notifications sent to jurors
```
### 4.3 AI-Powered Assignment (Optional)
If `aiAssignmentEnabled` is true in config:
1. Admin clicks "AI Assignment Suggestions"
2. System calls `ai-assignment.ts`:
- Anonymizes juror profiles and project descriptions
- Sends to GPT with matching instructions
- Returns confidence scores and reasoning
3. AI suggestions shown alongside algorithm suggestions
4. Admin picks which to use or mixes both
### 4.4 Handling Unassigned Projects
When all jurors with SOFT cap reach cap+buffer:
1. Remaining projects become "unassigned"
2. Admin dashboard highlights these prominently
3. Admin can:
- Manually assign to specific jurors (bypasses cap — manual override)
- Increase a juror's cap
- Add more jurors to the jury group
- Reduce `requiredReviewsPerProject` for remaining projects
---
## 5. Jury Evaluation Experience
### 5.1 Jury Dashboard
When a Jury 1 member opens their dashboard:
```
┌─────────────────────────────────────────────────────┐
│ JURY 1 — Semi-finalist Selection │
│ ─────────────────────────────────────────────────── │
│ Evaluation Window: April 1 April 30 │
│ ⏱ 12 days remaining │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────┐ │
│ │ 15 │ │ 8 │ │ 2 │ │ 5 │ │
│ │ Total │ │ Complete │ │ In Draft │ │ Pending│ │
│ └──────────┘ └──────────┘ └──────────┘ └────────┘ │
│ │
│ [Continue Next Evaluation →] │
│ │
│ Recent Assignments │
│ ┌──────────────────────────────────────────────┐ │
│ │ OceanClean AI │ Startup │ ✅ Done │ View │ │
│ │ Blue Carbon Hub │ Concept │ ⏳ Draft │ Cont │ │
│ │ SeaWatch Monitor │ Startup │ ⬜ Pending│ Start│ │
│ │ ... │ │
│ └──────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
```
Key elements:
- **Deadline countdown** — prominent timer showing days/hours remaining
- **Progress stats** — total, completed, in-draft, pending
- **Quick action CTA** — jump to next unevaluated project
- **Assignment list** — sorted by status (pending first, then drafts, then done)
### 5.2 COI Declaration (Blocking)
Before evaluating any project, the juror MUST declare COI:
```
┌───────────────────────────────────────────┐
│ Conflict of Interest Declaration │
│ │
│ Do you have a conflict of interest with │
│ "OceanClean AI" (Startup)? │
│ │
│ ○ No conflict — I can evaluate fairly │
│ ○ Yes, I have a conflict: │
│ Type: [Financial ▾] │
│ Description: [________________] │
│ │
│ [Submit Declaration] │
└───────────────────────────────────────────┘
```
- If **No conflict**: Proceed to evaluation form
- If **Yes**: Assignment flagged, admin notified, juror may be reassigned
- COI declaration is logged in `ConflictOfInterest` model
- Admin can review and take action (cleared / reassigned / noted)
### 5.3 Evaluation Form
The form adapts to the `scoringMode`:
#### Criteria Mode (default for Jury 1 and Jury 2)
```
┌───────────────────────────────────────────────────┐
│ Evaluating: OceanClean AI (Startup) │
│ ──────────────────────────────────────────────── │
│ │
│ [📄 Documents] [📊 Scoring] [💬 Feedback] │
│ │
│ ── DOCUMENTS TAB ── │
│ ┌─ Round 1 Application Docs ─────────────────┐ │
│ │ 📄 Executive Summary.pdf [Download] │ │
│ │ 📄 Business Plan.pdf [Download] │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ (Jury 2 also sees:) │
│ ┌─ Round 2 Semi-finalist Docs ────────────────┐ │
│ │ 📄 Updated Business Plan.pdf [Download] │ │
│ │ 🎥 Video Pitch.mp4 [Play] │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ ── SCORING TAB ── │
│ Innovation & Impact [1] [2] [3] [4] [5] (w:30%)│
│ Feasibility [1] [2] [3] [4] [5] (w:25%)│
│ Team & Execution [1] [2] [3] [4] [5] (w:25%)│
│ Ocean Relevance [1] [2] [3] [4] [5] (w:20%)│
│ │
│ Overall Score: 3.8 / 5.0 (auto-calculated) │
│ │
│ ── FEEDBACK TAB ── │
│ Feedback: [________________________________] │
│ │
│ [💾 Save Draft] [✅ Submit Evaluation] │
│ (Auto-saves every 30s) │
└───────────────────────────────────────────────────┘
```
#### Binary Mode (optional for quick screening)
```
Should this project advance to the semi-finals?
[✅ Yes] [❌ No]
Justification (required): [________________]
```
#### Global Score Mode
```
Overall Score: [1] [2] [3] [4] [5] [6] [7] [8] [9] [10]
Feedback (required): [________________]
```
### 5.4 Document Visibility (Cross-Round)
Controlled by `RoundSubmissionVisibility`:
| Round | Sees Window 1 ("Application Docs") | Sees Window 2 ("Semi-finalist Docs") |
|-------|------------------------------------|-----------------------------------------|
| Jury 1 (Round 3) | Yes | No (doesn't exist yet) |
| Jury 2 (Round 5) | Yes | Yes |
| Jury 3 (Round 7) | Yes | Yes |
In the evaluation UI:
- Documents are grouped by submission window
- Each group has a label (from `RoundSubmissionVisibility.displayLabel`)
- Clear visual separation (tabs, accordion sections, or side panels)
### 5.5 Auto-Save and Submission
- **Auto-save**: Client debounces and calls `evaluation.autosave` every 30 seconds while draft is open
- **Draft status**: Evaluation starts as NOT_STARTED → DRAFT on first save → SUBMITTED on explicit submit
- **Submission validation**:
- All required criteria scored (if criteria mode)
- Global score provided (if global mode)
- Binary decision selected (if binary mode)
- Feedback text provided (if `requireFeedback`)
- Window is open (or juror has grace period)
- **After submission**: Evaluation becomes read-only for juror (status = SUBMITTED)
- **Admin can lock**: Set status to LOCKED to prevent any further changes
### 5.6 Grace Periods
```
GracePeriod {
roundId: "round-jury-1"
userId: "judge-alice"
projectId: null // Applies to ALL Alice's assignments in this round
extendedUntil: "2026-05-02" // 2 days after official close
reason: "Travel conflict"
grantedById: "admin-1"
}
```
- Admin can grant per-juror or per-juror-per-project grace periods
- Evaluation submission checks grace period before rejecting past-window submissions
- Dashboard shows "(Grace period: 2 extra days)" badge for affected jurors
---
## 6. End of Evaluation — Results & Advancement
### 6.1 Results Visualization
When the evaluation window closes, the admin sees:
```
┌──────────────────────────────────────────────────────────────┐
│ Jury 1 Results │
│ ─────────────────────────────────────────────────────────── │
│ │
│ Completion: 142/150 evaluations submitted (94.7%) │
│ Outstanding: 8 (3 jurors have pending evaluations) │
│ │
│ ┌─ STARTUPS (Top 10) ──────────────────────────────────────┐│
│ │ # Project Avg Score Consensus Reviews Status ││
│ │ 1 OceanClean AI 4.6/5 0.92 3/3 ✅ ││
│ │ 2 SeaWatch 4.3/5 0.85 3/3 ✅ ││
│ │ 3 BlueCarbon 4.1/5 0.78 3/3 ✅ ││
│ │ ... ││
│ │ 10 TidalEnergy 3.2/5 0.65 3/3 ✅ ││
│ │ ── cutoff line ────────────────────────────────────────── ││
│ │ 11 WavePower 3.1/5 0.71 3/3 ⬜ ││
│ │ 12 CoralGuard 2.9/5 0.55 2/3 ⚠️ ││
│ └──────────────────────────────────────────────────────────┘│
│ │
│ ┌─ CONCEPTS (Top 10) ──────────────────────────────────────┐│
│ │ (same layout) ││
│ └──────────────────────────────────────────────────────────┘│
│ │
│ [🤖 AI Recommendation] [📊 Score Distribution] [Export] │
│ │
│ [✅ Approve Shortlist] [✏️ Edit Shortlist] │
└──────────────────────────────────────────────────────────────┘
```
**Metrics shown:**
- Average global score (or weighted criteria average)
- Consensus score (1 - normalized stddev, where 1.0 = full agreement)
- Review count / required
- Per-criterion averages (expandable)
### 6.2 AI Recommendation
When admin clicks "AI Recommendation":
1. System calls `ai-evaluation-summary.ts` for each project in bulk
2. AI generates:
- Ranked shortlist per category based on scores + feedback analysis
- Strengths, weaknesses, themes per project
- Recommendation: "Advance" / "Borderline" / "Do not advance"
3. Admin sees AI recommendation alongside actual scores
4. AI recommendations are suggestions only — admin has final say
### 6.3 Advancement Decision
```
Advancement Mode: admin_selection (with AI recommendation)
1. System shows ranked list per category
2. AI highlights recommended top N per category
3. Admin can:
- Accept AI recommendation
- Drag projects to reorder
- Add/remove projects from advancement list
- Set custom cutoff line
4. Admin clicks "Confirm Advancement"
5. System:
a. Sets ProjectRoundState to PASSED for advancing projects
b. Sets ProjectRoundState to REJECTED for non-advancing projects
c. Updates Project.status to SEMIFINALIST (Jury 1) or FINALIST (Jury 2)
d. Logs all decisions in DecisionAuditLog
e. Sends notifications to all teams (advanced / not selected)
```
### 6.4 Advancement Modes
| Mode | Behavior |
|------|----------|
| `auto_top_n` | Top N per category automatically advance when window closes |
| `admin_selection` | Admin manually selects who advances (with AI/score guidance) |
| `ai_recommended` | AI proposes list, admin must approve/modify |
---
## 7. Special Awards Integration (Jury 2 Only)
During the Jury 2 evaluation round, special awards can run alongside:
### 7.1 How It Works
```
Round 5: "Jury 2 — Finalist Selection"
├── Main evaluation (all semi-finalists scored by Jury 2)
└── Special Awards (run in parallel):
├── "Innovation Award" — STAY_IN_MAIN mode
│ Projects remain in main eval, flagged as eligible
│ Award jury (subset of Jury 2 or separate) votes
└── "Impact Award" — SEPARATE_POOL mode
AI filters eligible projects into award pool
Dedicated jury evaluates and votes
```
### 7.2 SpecialAward.evaluationRoundId
Each award links to the evaluation round it runs alongside:
```
SpecialAward {
evaluationRoundId: "round-jury-2" // Runs during Jury 2
eligibilityMode: STAY_IN_MAIN
juryGroupId: "jury-group-innovation" // Can be same or different jury
}
```
### 7.3 Award Evaluation Flow
1. Before Jury 2 window opens: Admin runs award eligibility (AI or manual)
2. During Jury 2 window: Award jury members see their award assignments alongside regular evaluations
3. Award jury submits award votes (PICK_WINNER, RANKED, or SCORED)
4. After Jury 2 closes: Award results finalized alongside main results
---
## 8. Differences Between Jury 1 and Jury 2
| Aspect | Jury 1 (Round 3) | Jury 2 (Round 5) |
|--------|-------------------|-------------------|
| Input projects | All eligible (post-filtering) | Semi-finalists only |
| Visible docs | Window 1 only | Window 1 + Window 2 |
| Output | Semi-finalists | Finalists |
| Project.status update | → SEMIFINALIST | → FINALIST |
| Special awards | No | Yes (alongside) |
| Jury group | Jury 1 | Jury 2 (different members, possible overlap) |
| Typical project count | 50-100+ | 10-20 |
| Required reviews | 3 (more projects, less depth) | 3-5 (fewer projects, more depth) |
---
## 9. API Changes
### Preserved Procedures (renamed stageId → roundId)
| Procedure | Change |
|-----------|--------|
| `evaluation.get` | roundId via assignment |
| `evaluation.start` | No change |
| `evaluation.autosave` | No change |
| `evaluation.submit` | Window check uses round.windowCloseAt + grace periods |
| `evaluation.declareCOI` | No change |
| `evaluation.getCOIStatus` | No change |
| `evaluation.getProjectStats` | No change |
| `evaluation.listByRound` | Renamed from listByStage |
| `evaluation.generateSummary` | roundId instead of stageId |
| `evaluation.generateBulkSummaries` | roundId instead of stageId |
### New Procedures
| Procedure | Purpose |
|-----------|---------|
| `assignment.previewWithJuryGroup` | Preview assignments filtered by jury group with cap/quota logic |
| `assignment.getJuryGroupStats` | Per-member stats: load, category distribution, cap utilization |
| `evaluation.getResultsOverview` | Rankings, scores, consensus, AI recommendations per category |
| `evaluation.confirmAdvancement` | Admin confirms which projects advance |
| `evaluation.getAdvancementPreview` | Preview advancement impact before confirming |
### Modified Procedures
| Procedure | Modification |
|-----------|-------------|
| `assignment.getSuggestions` | Now filters by JuryGroup, applies hard/soft caps, category quotas |
| `assignment.create` | Now sets `juryGroupId` on Assignment |
| `assignment.bulkCreate` | Now validates against jury group caps |
| `file.listByProjectForRound` | Uses RoundSubmissionVisibility to filter docs |
---
## 10. Service Layer Changes
### `stage-assignment.ts` → `round-assignment.ts`
Key changes to `previewStageAssignment``previewRoundAssignment`:
1. **Load jury pool from JuryGroup** instead of all JURY_MEMBER users:
```typescript
const juryGroup = await prisma.juryGroup.findUnique({
where: { id: round.juryGroupId },
include: { members: { include: { user: true } } }
})
const jurors = juryGroup.members.map(m => ({
...m.user,
effectiveLimits: getEffectiveLimits(m, juryGroup),
}))
```
2. **Replace simple max check** with cap mode logic (hard/soft/none)
3. **Add category quota tracking** per juror
4. **Add ratio preference scoring** in candidate ranking
5. **Report overflow** — projects that couldn't be assigned because all jurors hit caps
### `stage-engine.ts` → `round-engine.ts`
Simplified:
- Remove trackId from all transitions
- `executeTransition` now takes `fromRoundId` + `toRoundId` (or auto-advance to next sortOrder)
- `validateTransition` simplified — no StageTransition lookup, just checks next round exists and is active
- Guard evaluation simplified — AdvancementRule.configJson replaces arbitrary guardJson
---
## 11. Edge Cases
### More projects than jurors can handle
- Algorithm assigns up to hard/soft cap for all jurors
- Remaining projects flagged as "unassigned" in admin dashboard
- Admin must: add jurors, increase caps, or manually assign
### Juror doesn't complete by deadline
- Dashboard shows overdue assignments prominently
- Admin can: extend via GracePeriod, reassign to another juror, or mark as incomplete
### Tie in scores at cutoff
- Depending on `tieBreaker` config:
- `admin_decides`: Admin manually picks from tied projects
- `highest_individual`: Project with highest single-evaluator score wins
- `revote`: Tied projects sent back for quick re-evaluation
### Category imbalance
- If one category has far more projects, quotas ensure jurors still get a mix
- If quotas can't be satisfied (not enough of one category), system relaxes quota for that category
### Juror in multiple jury groups
- Juror Alice is in Jury 1 and Jury 2
- Her assignments for each round are independent
- Her caps are per-jury-group (20 for Jury 1, 15 for Jury 2)
- No cross-round cap — each round manages its own workload