MOPC-App/docs/unified-architecture-redesign/04-jury-groups-and-assignme...

350 lines
13 KiB
Markdown

# Jury Groups & Assignment Policy
## Overview
Jury groups are first-class entities in the redesigned system. Each jury (Jury 1, Jury 2, Jury 3, award juries) is an explicit `JuryGroup` with named members, configurable assignment caps, category ratio preferences, and policy overrides. This document covers the data model, assignment algorithm, policy precedence, and admin controls.
---
## Data Model
### JuryGroup
```prisma
model JuryGroup {
id String @id @default(cuid())
competitionId String
label String // "Jury 1", "Jury 2", "Live Finals Jury", custom per program
description String?
roundId String? // which round this jury evaluates (nullable for award juries)
// Default policies for this group
defaultCapMode CapMode @default(SOFT)
defaultMaxProjects Int @default(15)
softCapBuffer Int @default(10)
defaultCategoryBias Json? // { STARTUP: 0.6, BUSINESS_CONCEPT: 0.4 } or null for no preference
allowOnboardingSelfService Boolean @default(true) // judges can adjust cap/ratio during onboarding
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
competition Competition @relation(fields: [competitionId], references: [id])
round Round? @relation(fields: [roundId], references: [id])
members JuryGroupMember[]
@@index([competitionId])
}
```
### JuryGroupMember
```prisma
model JuryGroupMember {
id String @id @default(cuid())
juryGroupId String
userId String
role JuryGroupMemberRole @default(MEMBER) // CHAIR, MEMBER, OBSERVER
// Per-member overrides (null = use group default)
capMode CapMode? // override group default
maxProjects Int? // override group default
categoryBias Json? // override group default ratio
// Onboarding self-service values (set by judge during onboarding if allowed)
selfServiceCap Int?
selfServiceBias Json?
availabilityNotes String? // Free-text availability info
joinedAt DateTime @default(now())
juryGroup JuryGroup @relation(fields: [juryGroupId], references: [id])
user User @relation(fields: [userId], references: [id])
@@unique([juryGroupId, userId])
@@index([userId])
}
enum JuryGroupMemberRole {
CHAIR // Jury lead — can manage session, see aggregate data, moderate deliberation
MEMBER // Regular juror — votes, scores, provides feedback
OBSERVER // View-only — can see evaluations/deliberations but cannot vote or score
}
```
**Role Permissions:**
| Permission | CHAIR | MEMBER | OBSERVER |
|-----------|-------|--------|----------|
| Submit evaluations | Yes | Yes | No |
| Vote in deliberation | Yes | Yes | No |
| See aggregate scores (during deliberation) | Yes | No* | No |
| Moderate deliberation discussion | Yes | No | No |
| View all assigned projects | Yes | Yes | Yes (read-only) |
| Be counted toward quorum | Yes | Yes | No |
*Members see aggregates only if `showCollectiveRankings` is enabled in DeliberationConfig.
### Supporting Enums
```prisma
enum CapMode {
HARD // AI cannot assign more than maxProjects
SOFT // AI tries to stay under maxProjects, can go up to maxProjects + softCapBuffer
NONE // No cap (unlimited assignments)
}
```
---
## Cap Mode Behavior
| Cap Mode | AI Assignment Behavior | Manual Assignment |
|----------|----------------------|-------------------|
| **HARD** | AI will NOT assign more than `maxProjects`. Period. | Admin can still force-assign beyond cap (creates `AssignmentException`). |
| **SOFT** | AI tries to stay at `maxProjects`. If excess projects remain after distributing to all judges at their cap, excess is distributed evenly among SOFT judges up to `maxProjects + softCapBuffer`. | Same as HARD — admin can override. |
| **NONE** | No limit. AI distributes projects as needed. | No limit. |
### Effective Cap Calculation
The effective cap for a judge considers the 5-layer policy precedence:
```
effectiveCap(member) =
member.selfServiceCap // Layer 4: onboarding self-service (if allowed and set)
?? member.maxProjects // Layer 4: admin per-member override
?? member.juryGroup.defaultMaxProjects // Layer 3: jury group default
?? program.defaultMaxProjects // Layer 2: program default
?? 15 // Layer 1: system default
```
Admin override (Layer 5) bypasses this entirely via `AssignmentException`.
### Soft Cap Overflow Algorithm
1. Assign projects to all judges up to their effective cap, respecting category bias
2. Count remaining unassigned projects
3. If remaining > 0 and SOFT-cap judges exist:
- Distribute remaining projects evenly among SOFT judges
- Stop when each SOFT judge reaches `effectiveCap + softCapBuffer`
4. Any projects still unassigned go to the **Unassigned Queue**
---
## Category Quotas & Ratio Bias
### CategoryQuotas (per JuryGroup)
```typescript
type CategoryQuotas = {
STARTUP: { min: number; max: number }
BUSINESS_CONCEPT: { min: number; max: number }
}
```
### Category Bias (per member)
Category bias is a **soft preference**, not a hard constraint. It influences the assignment algorithm's project selection but does not guarantee exact ratios.
```typescript
type CategoryBias = {
STARTUP: number // e.g., 0.6 = prefer 60% startups
BUSINESS_CONCEPT: number // e.g., 0.4 = prefer 40% concepts
}
```
Special cases:
- `{ STARTUP: 1.0, BUSINESS_CONCEPT: 0.0 }` — judge reviews only startups
- `{ STARTUP: 0.0, BUSINESS_CONCEPT: 1.0 }` — judge reviews only concepts
- `null` — no preference, algorithm decides
**Disclosure:** When bias is set, it is disclosed to judges on their dashboard (e.g., "You have been assigned primarily Startup projects based on your preference").
---
## 5-Layer Policy Precedence
| Layer | Scope | Who Sets It | Override Behavior |
|-------|-------|-------------|-------------------|
| 1 | System default | Platform config | Baseline for all programs |
| 2 | Program default | Program admin | Overrides system default for this program |
| 3 | Jury group default | Program admin | Overrides program default for this jury |
| 4a | Per-member override | Program admin | Overrides jury group default for this judge |
| 4b | Self-service override | Judge (during onboarding) | Only if `allowOnboardingSelfService` is true. Cannot exceed admin-set maximum. |
| 5 | Admin override | Program admin / Super admin | Always wins. Creates `AssignmentException` with audit trail. |
Policy resolution proceeds from Layer 5 → Layer 1. The first non-null value wins.
---
## Judge Onboarding Self-Service
When `allowOnboardingSelfService` is enabled on the JuryGroup:
1. Judge receives invitation email with link to accept
2. During onboarding, judge sees their assigned cap and category preference
3. Judge can adjust:
- **Max projects**: up or down within admin-defined bounds
- **Category preference**: ratio of startups vs concepts
4. Adjusted values stored in `JuryGroupMember.selfServiceCap` and `JuryGroupMember.selfServiceBias`
5. Admin can review and override self-service values at any time
---
## Assignment Algorithm
### Input
- List of eligible projects (with category)
- List of JuryGroupMembers (with effective caps, biases, COI declarations)
- CategoryQuotas for the JuryGroup
### Steps
1. **COI Check**: For each judge-project pair, check COI declarations. Skip if COI exists.
2. **Category Allocation**: Divide projects into STARTUP and BUSINESS_CONCEPT pools.
3. **Primary Assignment** (up to effective cap):
- For each judge, select projects matching their category bias
- Apply geo-diversity penalty and familiarity bonus (existing algorithm)
- Stop when judge reaches effective cap
4. **Soft Cap Overflow** (if unassigned projects remain):
- Identify SOFT-cap judges
- Distribute remaining projects evenly up to cap + buffer
5. **Unassigned Queue**: Any remaining projects enter the queue with reason codes
### Reason Codes for Unassigned Queue
| Code | Meaning |
|------|---------|
| `ALL_HARD_CAPPED` | All judges at hard cap |
| `SOFT_BUFFER_EXHAUSTED` | All soft-cap judges at cap + buffer |
| `COI_CONFLICT` | Project has COI with all available judges |
| `CATEGORY_IMBALANCE` | No judges available for this category |
| `MANUAL_ONLY` | Project flagged for manual assignment |
---
## AssignmentIntent (Pre-Assignment at Invite Time)
From the invite/onboarding integration: when a jury member is invited, the admin can optionally pre-assign specific projects.
```prisma
model AssignmentIntent {
id String @id @default(cuid())
juryGroupMemberId String
roundId String
projectId String
source AssignmentIntentSource // INVITE | ADMIN | SYSTEM
status AssignmentIntentStatus @default(PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
juryGroupMember JuryGroupMember @relation(...)
round Round @relation(...)
project Project @relation(...)
@@unique([juryGroupMemberId, roundId, projectId])
@@index([status])
}
enum AssignmentIntentSource {
INVITE // set during invitation
ADMIN // set by admin after invite
SYSTEM // set by AI suggestion
}
enum AssignmentIntentStatus {
PENDING // Created, awaiting assignment algorithm execution
HONORED // Algorithm materialized this intent into an Assignment record
OVERRIDDEN // Admin changed the assignment, superseding this intent
EXPIRED // Round completed without this intent being honored
CANCELLED // Explicitly cancelled by admin or system
}
```
### Intent Lifecycle State Machine
```
PENDING ──── [algorithm runs, intent matched] ──── → HONORED
├──── [admin changes assignment] ──────────── → OVERRIDDEN
├──── [round completes unmatched] ─────────── → EXPIRED
└──── [admin/system cancels] ──────────────── → CANCELLED
```
**Lifecycle rules:**
- **PENDING → HONORED**: When the assignment algorithm runs and creates an `Assignment` record matching this intent's judge+project pair. The `AssignmentIntent.id` is stored on the `Assignment` as provenance.
- **PENDING → OVERRIDDEN**: When an admin manually assigns the project to a different judge, or removes the intended judge from the JuryGroup. The override is logged to `DecisionAuditLog`.
- **PENDING → EXPIRED**: When the round transitions to CLOSED and this intent was never materialized. Automatic batch update on round close.
- **PENDING → CANCELLED**: When an admin explicitly removes the intent, or when the judge is removed from the JuryGroup. Source logged.
- **Terminal states**: HONORED, OVERRIDDEN, EXPIRED, CANCELLED are all terminal. No transitions out.
**Algorithm integration:**
1. Before algorithmic assignment, load all PENDING intents for the round
2. For each intent, attempt to create the assignment (respecting COI, cap checks)
3. If assignment succeeds → mark intent HONORED
4. If assignment fails (COI, cap exceeded) → keep PENDING, add to unassigned queue with reason code `INTENT_BLOCKED`
5. Admin can manually resolve blocked intents
---
## AssignmentException (Audited Over-Cap Assignment)
When an admin manually assigns a project to a judge who is at or beyond their cap:
```prisma
model AssignmentException {
id String @id @default(cuid())
assignmentId String // the actual assignment record
reason String // admin must provide justification
overCapBy Int // how many over the effective cap
approvedById String // admin who approved
createdAt DateTime @default(now())
approvedBy User @relation(...)
}
```
All exceptions are visible in the assignment audit view and included in compliance reports.
---
## Cross-Jury Membership
Judges can belong to multiple JuryGroups simultaneously:
- Judge A is on Jury 1 AND Jury 2
- Judge B is on Jury 2 AND the Innovation Award jury
- Judge C is on Jury 1, Jury 2, AND Jury 3
Each membership has independent cap/bias configuration. A judge's Jury 1 cap of 20 doesn't affect their Jury 2 cap of 10.
---
## Admin "Juries" UI Section
A dedicated admin section for managing JuryGroups:
- **Create Jury Group**: name, description, linked round, default policies
- **Manage Members**: add/remove members, set per-member overrides, view self-service adjustments
- **View Assignments**: see all assignments for the group, identify unassigned projects
- **Manual Assignment**: drag-and-drop or bulk-assign unassigned projects
- **Assignment Audit**: view all exceptions, overrides, and intent fulfillment
This is separate from the round configuration — JuryGroups are entities that exist independently and are linked to rounds.
See [08-platform-integration-matrix.md](./08-platform-integration-matrix.md) for full page mapping.
---
## Manual Override
Admin can override ANY assignment decision at ANY time:
- Reassign a project from one judge to another
- Assign a project to a judge beyond their cap (creates `AssignmentException`)
- Remove an assignment
- Change a judge's cap or bias mid-round
- Override the algorithm's category allocation
All overrides are logged to `DecisionAuditLog` with the admin's identity, timestamp, and reason.