13 KiB
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
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
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
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
- Assign projects to all judges up to their effective cap, respecting category bias
- Count remaining unassigned projects
- If remaining > 0 and SOFT-cap judges exist:
- Distribute remaining projects evenly among SOFT judges
- Stop when each SOFT judge reaches
effectiveCap + softCapBuffer
- Any projects still unassigned go to the Unassigned Queue
Category Quotas & Ratio Bias
CategoryQuotas (per JuryGroup)
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.
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 conceptsnull— 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:
- Judge receives invitation email with link to accept
- During onboarding, judge sees their assigned cap and category preference
- Judge can adjust:
- Max projects: up or down within admin-defined bounds
- Category preference: ratio of startups vs concepts
- Adjusted values stored in
JuryGroupMember.selfServiceCapandJuryGroupMember.selfServiceBias - 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
- COI Check: For each judge-project pair, check COI declarations. Skip if COI exists.
- Category Allocation: Divide projects into STARTUP and BUSINESS_CONCEPT pools.
- 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
- Soft Cap Overflow (if unassigned projects remain):
- Identify SOFT-cap judges
- Distribute remaining projects evenly up to cap + buffer
- 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.
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
Assignmentrecord matching this intent's judge+project pair. TheAssignmentIntent.idis stored on theAssignmentas 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:
- Before algorithmic assignment, load all PENDING intents for the round
- For each intent, attempt to create the assignment (respecting COI, cap checks)
- If assignment succeeds → mark intent HONORED
- If assignment fails (COI, cap exceeded) → keep PENDING, add to unassigned queue with reason code
INTENT_BLOCKED - 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:
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 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.