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