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

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

  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)

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 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.

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:

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.