350 lines
13 KiB
Markdown
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.
|