# API Router Reference: Architecture Redesign

## Overview

This document provides a complete reference of all tRPC router changes for the MOPC architecture redesign. Every procedure — new, modified, preserved, and removed — is documented with full Zod schemas.

**Key Changes Summary:**
- **Renamed Routers**: `pipeline` → `competition`, Stage CRUD split between `competition` and `round` routers
- **New Routers**: `jury-group`, `winner-confirmation`, `submission-window`, `mentor-workspace`
- **Enhanced Routers**: `assignment`, `evaluation`, `file`, `applicant`, `award`, `live-control`, `mentor`
- **Eliminated**: `stageTransition` procedures (replaced by linear advancement rules)
- **Procedure Count**: ~180 existing procedures, ~60 new, ~30 modified, ~20 removed

---

## Router Rename Map

| Current Router | Redesigned Router | Reason |
|----------------|-------------------|--------|
| `pipeline.ts` | `competition.ts` | Domain-specific naming |
| `stage.ts` | Split: `competition.ts` (CRUD) + `round.ts` (runtime) | Clearer separation of concerns |
| `assignment.ts` | `assignment.ts` (enhanced) | Add jury group awareness |
| `specialAward.ts` | `award.ts` | Enhanced for two-mode awards |
| `mentor.ts` | `mentor.ts` + `mentor-workspace.ts` | Separate workspace features |
| *(new)* | `jury-group.ts` | Explicit jury management |
| *(new)* | `winner-confirmation.ts` | Multi-party winner approval |
| *(new)* | `submission-window.ts` | Multi-round document requirements |

---

## 1. Competition Router (replaces Pipeline)

**File:** `src/server/routers/competition.ts` (renamed from `pipeline.ts`)

### Removed Procedures

All Track-related procedures are eliminated (Track model is removed):
- ~~`pipeline.createStructure`~~ — Replaced by `competition.createStructure` (no tracks)
- ~~`pipeline.getDraft`~~ — Replaced by `competition.getDraft` (returns rounds, not tracks)
- ~~`pipeline.updateStructure`~~ — Replaced by `competition.updateStructure` (rounds only)
- ~~`pipeline.getApplicantView`~~ — Moved to `applicant.getCompetitionView`

### Modified Procedures

#### `create` → `competition.create`
**Changes:** Rename references, simplified settings (no track concept)

```typescript
// Input schema
z.object({
  programId: z.string(),
  name: z.string().min(1).max(255),
  slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),

  // NEW: Competition-wide settings (typed, not generic JSON)
  categoryMode: z.enum(['SHARED', 'SPLIT']).default('SHARED'),
  startupFinalistCount: z.number().int().min(1).default(3),
  conceptFinalistCount: z.number().int().min(1).default(3),

  // Notification preferences
  notifyOnRoundAdvance: z.boolean().default(true),
  notifyOnDeadlineApproach: z.boolean().default(true),
  deadlineReminderDays: z.array(z.number().int()).default([7, 3, 1]),
})

// Output: Competition
```

#### `update` → `competition.update`
**Changes:** Add typed settings fields

```typescript
// Input schema
z.object({
  id: z.string(),
  name: z.string().min(1).max(255).optional(),
  slug: z.string().regex(/^[a-z0-9-]+$/).optional(),
  status: z.enum(['DRAFT', 'ACTIVE', 'CLOSED', 'ARCHIVED']).optional(),

  // NEW: Typed settings
  categoryMode: z.enum(['SHARED', 'SPLIT']).optional(),
  startupFinalistCount: z.number().int().min(1).optional(),
  conceptFinalistCount: z.number().int().min(1).optional(),
  notifyOnRoundAdvance: z.boolean().optional(),
  notifyOnDeadlineApproach: z.boolean().optional(),
  deadlineReminderDays: z.array(z.number().int()).optional(),
})

// Output: Competition
```

#### `get` → `competition.get`
**Changes:** Returns rounds array (flat), not tracks

```typescript
// Input schema
z.object({ id: z.string() })

// Output
{
  ...competition,
  program: { id, name },
  rounds: Array<{
    id, name, slug, roundType, status, sortOrder,
    windowOpenAt, windowCloseAt,
    juryGroup: { id, name } | null,
    submissionWindow: { id, name } | null,
    _count: { projectRoundStates, cohorts }
  }>,
  juryGroups: Array<{ id, name, _count: { members } }>,
  submissionWindows: Array<{ id, name, roundNumber }>,
  specialAwards: Array<{ id, name, status }>,
}
```

#### `list` → `competition.list`
**Changes:** Returns competition counts (no tracks)

```typescript
// Input schema
z.object({ programId: z.string() })

// Output
Array<{
  ...competition,
  _count: { rounds, juryGroups, submissionWindows, specialAwards }
}>
```

#### `publish` → `competition.publish`
**Changes:** Validates rounds instead of tracks

```typescript
// Input schema
z.object({ id: z.string() })

// Validation: Must have at least one round, all rounds must have configs
// Output: Competition with status = ACTIVE
```

### New Procedures

#### `createStructure`
**Purpose:** Atomically create Competition + Rounds + Jury Groups + Submission Windows

```typescript
// Input schema
z.object({
  programId: z.string().min(1),
  name: z.string().min(1).max(255),
  slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),

  // Competition-wide settings
  categoryMode: z.enum(['SHARED', 'SPLIT']).default('SHARED'),
  startupFinalistCount: z.number().int().default(3),
  conceptFinalistCount: z.number().int().default(3),

  // Rounds (linear array, no tracks)
  rounds: z.array(z.object({
    name: z.string().min(1).max(255),
    slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),
    roundType: z.enum([
      'INTAKE', 'FILTERING', 'EVALUATION',
      'SUBMISSION', 'MENTORING', 'LIVE_FINAL', 'CONFIRMATION'
    ]),
    sortOrder: z.number().int().min(0),
    configJson: z.record(z.unknown()).optional(),
    windowOpenAt: z.date().optional(),
    windowCloseAt: z.date().optional(),

    // Links to other entities
    juryGroupSlug: z.string().optional(),  // Will link to JuryGroup after creation
    submissionWindowSlug: z.string().optional(),
  })),

  // Jury groups
  juryGroups: z.array(z.object({
    name: z.string(),
    slug: z.string(),
    description: z.string().optional(),
    sortOrder: z.number().int(),
    defaultMaxAssignments: z.number().int().default(20),
    defaultCapMode: z.enum(['HARD', 'SOFT', 'NONE']).default('SOFT'),
    softCapBuffer: z.number().int().default(2),
    categoryQuotasEnabled: z.boolean().default(false),
    defaultCategoryQuotas: z.record(z.object({
      min: z.number().int(),
      max: z.number().int(),
    })).optional(),
  })),

  // Submission windows
  submissionWindows: z.array(z.object({
    name: z.string(),
    slug: z.string(),
    roundNumber: z.number().int(),
    sortOrder: z.number().int(),
    windowOpenAt: z.date().optional(),
    windowCloseAt: z.date().optional(),
    deadlinePolicy: z.enum(['HARD', 'FLAG', 'GRACE']).default('FLAG'),
    graceHours: z.number().int().optional(),
    lockOnClose: z.boolean().default(true),

    fileRequirements: z.array(z.object({
      name: z.string(),
      description: z.string().optional(),
      acceptedMimeTypes: z.array(z.string()),
      maxSizeMB: z.number().int().optional(),
      isRequired: z.boolean().default(true),
      sortOrder: z.number().int(),
    })),
  })),

  // Advancement rules (auto-generated linear flow by default)
  autoAdvancement: z.boolean().default(true),
})

// Output
{
  competition: Competition,
  rounds: Array<Round>,
  juryGroups: Array<JuryGroup>,
  submissionWindows: Array<SubmissionWindow>,
}
```

#### `updateStructure`
**Purpose:** Diff-based update (create/update/delete rounds, jury groups, windows)

```typescript
// Input schema
z.object({
  id: z.string(),
  name: z.string().optional(),
  slug: z.string().optional(),
  status: z.enum(['DRAFT', 'ACTIVE', 'CLOSED', 'ARCHIVED']).optional(),

  // Same nested arrays as createStructure, but with optional `id` fields
  rounds: z.array(z.object({
    id: z.string().optional(),  // Present = update, absent = create
    name: z.string(),
    // ... all other round fields
  })),

  juryGroups: z.array(z.object({
    id: z.string().optional(),
    name: z.string(),
    // ... all other jury group fields
  })),

  submissionWindows: z.array(z.object({
    id: z.string().optional(),
    name: z.string(),
    // ... all other submission window fields
  })),
})

// Logic:
// - Rounds/groups/windows with ID → update
// - Rounds/groups/windows without ID → create
// - Existing IDs not in input → delete (with safety checks: no active ProjectRoundStates)
```

#### `getDraft`
**Purpose:** Get full structure for wizard editing

```typescript
// Input schema
z.object({ id: z.string() })

// Output
{
  ...competition,
  program: { id, name },
  rounds: Array<{
    ...round,
    juryGroup: JuryGroup | null,
    submissionWindow: SubmissionWindow | null,
    visibleSubmissionWindows: Array<RoundSubmissionVisibility>,
    advancementRules: Array<AdvancementRule>,
    _count: { projectRoundStates },
  }>,
  juryGroups: Array<{
    ...juryGroup,
    members: Array<JuryGroupMember>,
    _count: { rounds, assignments },
  }>,
  submissionWindows: Array<{
    ...submissionWindow,
    fileRequirements: Array<SubmissionFileRequirement>,
    rounds: Array<{ id, name }>,
  }>,
  specialAwards: Array<{
    ...award,
    juryGroup: { id, name } | null,
    evaluationRound: { id, name } | null,
  }>,
}
```

#### `simulate`
**Purpose:** Dry-run project routing through competition

```typescript
// Input schema
z.object({
  id: z.string(),
  projectIds: z.array(z.string()).min(1).max(500),
})

// Output
{
  competitionId: string,
  competitionName: string,
  projectCount: number,
  simulations: Array<{
    projectId: string,
    projectTitle: string,
    currentStatus: ProjectStatus,
    targetRoundId: string | null,
    targetRoundName: string,
  }>,
}
```

### Preserved Procedures (renamed only)

- `delete` → `competition.delete` (archive)
- `getSummary` → `competition.getSummary` (aggregated stats)
- `getStageAnalytics` → `competition.getRoundAnalytics` (per-round stats)

---

## 2. Round Router (new, split from Stage)

**File:** `src/server/routers/round.ts`

Runtime operations for rounds (open/close windows, transition status, project states).

### New Procedures

#### `get`
```typescript
// Input schema
z.object({ id: z.string() })

// Output
{
  ...round,
  competition: { id, name, programId },
  juryGroup: JuryGroup | null,
  submissionWindow: SubmissionWindow | null,
  visibleSubmissionWindows: Array<RoundSubmissionVisibility>,
  cohorts: Array<Cohort>,
  advancementRules: Array<AdvancementRule>,
  stateDistribution: Record<ProjectRoundStateValue, number>,
  _count: { projectRoundStates, assignments, evaluationForms },
}
```

#### `transition`
**Purpose:** State machine for round status

```typescript
// Input schema
z.object({
  id: z.string(),
  targetStatus: z.enum(['ROUND_DRAFT', 'ROUND_ACTIVE', 'ROUND_CLOSED', 'ROUND_ARCHIVED']),
})

// Validation: Enforces state machine rules
// ROUND_DRAFT → ROUND_ACTIVE
// ROUND_ACTIVE → ROUND_CLOSED
// ROUND_CLOSED → ROUND_ARCHIVED | ROUND_ACTIVE (reopen)

// Output: Round with new status
```

#### `openWindow`
```typescript
// Input schema
z.object({
  id: z.string(),
  windowCloseAt: z.date().optional(),
})

// Sets windowOpenAt = now, optionally sets windowCloseAt
// Output: Round
```

#### `closeWindow`
```typescript
// Input schema
z.object({ id: z.string() })

// Sets windowCloseAt = now
// Output: Round
```

#### `getProjectStates`
**Purpose:** Paginated list of project states in a round

```typescript
// Input schema
z.object({
  roundId: z.string(),
  state: z.enum([
    'PENDING', 'IN_PROGRESS', 'PASSED',
    'REJECTED', 'COMPLETED', 'WITHDRAWN'
  ]).optional(),
  cursor: z.string().optional(),
  limit: z.number().int().min(1).max(100).default(50),
})

// Output
{
  items: Array<{
    ...projectRoundState,
    project: { id, title, status, tags, teamName, competitionCategory },
  }>,
  nextCursor: string | undefined,
}
```

#### `getForJury`
**Purpose:** Jury-facing round details

```typescript
// Input schema
z.object({ id: z.string() })

// Output
{
  ...round,
  competition: { id, name, programId },
  juryGroup: JuryGroup | null,
  evaluationForms: Array<{ id, criteriaJson, scalesJson }>,
  isWindowOpen: boolean,
  windowTimeRemaining: number | null,  // milliseconds
  myAssignmentCount: number,
  myCompletedCount: number,
}
```

#### `getApplicantTimeline`
**Purpose:** Show a project's journey through rounds

```typescript
// Input schema
z.object({
  projectId: z.string(),
  competitionId: z.string(),
})

// Output
Array<{
  roundId: string,
  roundName: string,
  roundType: RoundType,
  state: ProjectRoundStateValue,
  enteredAt: DateTime,
  exitedAt: DateTime | null,
  isCurrent: boolean,
}>
```

#### `getRequirements`
**Purpose:** File requirements and upload status for a round

```typescript
// Input schema
z.object({
  roundId: z.string(),
  projectId: z.string(),
})

// Output
{
  submissionWindow: {
    id: string,
    name: string,
    windowOpenAt: DateTime | null,
    windowCloseAt: DateTime | null,
    deadlinePolicy: DeadlinePolicy,
  } | null,
  fileRequirements: Array<SubmissionFileRequirement>,
  uploadedFiles: Array<ProjectFile>,
  windowStatus: {
    isOpen: boolean,
    closesAt: DateTime | null,
    isLate: boolean,
  },
  deadlineInfo: {
    windowOpenAt: DateTime | null,
    windowCloseAt: DateTime | null,
    graceHours: number,
  },
}
```

---

## 3. Jury Group Router (new)

**File:** `src/server/routers/jury-group.ts`

### New Procedures

#### `create`
```typescript
// Input schema
z.object({
  competitionId: z.string(),
  name: z.string().min(1).max(255),
  slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),
  description: z.string().optional(),
  sortOrder: z.number().int().min(0).optional(),

  // Default assignment configuration
  defaultMaxAssignments: z.number().int().min(1).default(20),
  defaultCapMode: z.enum(['HARD', 'SOFT', 'NONE']).default('SOFT'),
  softCapBuffer: z.number().int().min(0).default(2),

  // Category quotas
  categoryQuotasEnabled: z.boolean().default(false),
  defaultCategoryQuotas: z.record(z.object({
    min: z.number().int().min(0),
    max: z.number().int().min(1),
  })).optional(),

  // Onboarding settings
  allowJurorCapAdjustment: z.boolean().default(false),
  allowJurorRatioAdjustment: z.boolean().default(false),
})

// Output: JuryGroup
```

#### `update`
```typescript
// Input schema
z.object({
  id: z.string(),
  name: z.string().optional(),
  description: z.string().optional(),
  sortOrder: z.number().int().optional(),
  defaultMaxAssignments: z.number().int().optional(),
  defaultCapMode: z.enum(['HARD', 'SOFT', 'NONE']).optional(),
  softCapBuffer: z.number().int().optional(),
  categoryQuotasEnabled: z.boolean().optional(),
  defaultCategoryQuotas: z.record(z.object({
    min: z.number().int(),
    max: z.number().int(),
  })).optional(),
  allowJurorCapAdjustment: z.boolean().optional(),
  allowJurorRatioAdjustment: z.boolean().optional(),
})

// Output: JuryGroup
```

#### `delete`
```typescript
// Input schema
z.object({ id: z.string() })

// Validation: Cannot delete if linked to any rounds or assignments
// Output: { success: true }
```

#### `get`
```typescript
// Input schema
z.object({ id: z.string() })

// Output
{
  ...juryGroup,
  competition: { id, name },
  members: Array<{
    ...juryGroupMember,
    user: { id, name, email, expertiseTags },
  }>,
  rounds: Array<{ id, name, roundType }>,
  _count: { members, assignments },
}
```

#### `list`
```typescript
// Input schema
z.object({ competitionId: z.string() })

// Output
Array<{
  ...juryGroup,
  _count: { members, rounds, assignments },
}>
```

#### `addMember`
```typescript
// Input schema
z.object({
  juryGroupId: z.string(),
  userId: z.string(),
  isLead: z.boolean().default(false),

  // Per-juror overrides
  maxAssignmentsOverride: z.number().int().optional(),
  capModeOverride: z.enum(['HARD', 'SOFT', 'NONE']).optional(),
  categoryQuotasOverride: z.record(z.object({
    min: z.number().int(),
    max: z.number().int(),
  })).optional(),
})

// Validation: User must have role JURY_MEMBER
// Output: JuryGroupMember
```

#### `updateMember`
```typescript
// Input schema
z.object({
  id: z.string(),  // JuryGroupMember ID
  isLead: z.boolean().optional(),
  maxAssignmentsOverride: z.number().int().optional().nullable(),
  capModeOverride: z.enum(['HARD', 'SOFT', 'NONE']).optional().nullable(),
  categoryQuotasOverride: z.record(z.object({
    min: z.number().int(),
    max: z.number().int(),
  })).optional().nullable(),

  // Juror preferences (self-service during onboarding)
  preferredStartupRatio: z.number().min(0).max(1).optional(),
  availabilityNotes: z.string().optional(),
})

// Output: JuryGroupMember
```

#### `removeMember`
```typescript
// Input schema
z.object({ id: z.string() })

// Validation: Cannot remove if has active assignments
// Output: { success: true }
```

#### `bulkAddMembers`
```typescript
// Input schema
z.object({
  juryGroupId: z.string(),
  userIds: z.array(z.string()).min(1).max(100),
})

// Creates JuryGroupMember for each user with default settings
// Output: { created: number, skipped: number }
```

#### `getOnboarding`
**Purpose:** Jury member views their onboarding settings for a group

```typescript
// Input schema (protectedProcedure)
z.object({ juryGroupId: z.string() })

// Output
{
  juryGroup: JuryGroup,
  membership: JuryGroupMember,
  allowCapAdjustment: boolean,
  allowRatioAdjustment: boolean,
  hasCompleted: boolean,  // Has filled preferences
}
```

#### `submitOnboarding`
**Purpose:** Jury member submits preferences

```typescript
// Input schema (protectedProcedure)
z.object({
  juryGroupId: z.string(),

  // Conditionally allowed based on juryGroup.allowJurorCapAdjustment
  maxAssignmentsOverride: z.number().int().optional(),

  // Conditionally allowed based on juryGroup.allowJurorRatioAdjustment
  preferredStartupRatio: z.number().min(0).max(1).optional(),

  availabilityNotes: z.string().optional(),
})

// Output: JuryGroupMember
```

---

## 4. Submission Window Router (new)

**File:** `src/server/routers/submission-window.ts`

### New Procedures

#### `create`
```typescript
// Input schema
z.object({
  competitionId: z.string(),
  name: z.string().min(1),
  slug: z.string().regex(/^[a-z0-9-]+$/),
  roundNumber: z.number().int().min(1),
  sortOrder: z.number().int().optional(),

  // Window timing
  windowOpenAt: z.date().optional(),
  windowCloseAt: z.date().optional(),

  // Deadline behavior
  deadlinePolicy: z.enum(['HARD', 'FLAG', 'GRACE']).default('FLAG'),
  graceHours: z.number().int().min(0).optional(),
  lockOnClose: z.boolean().default(true),
})

// Output: SubmissionWindow
```

#### `update`
```typescript
// Input schema
z.object({
  id: z.string(),
  name: z.string().optional(),
  windowOpenAt: z.date().optional().nullable(),
  windowCloseAt: z.date().optional().nullable(),
  deadlinePolicy: z.enum(['HARD', 'FLAG', 'GRACE']).optional(),
  graceHours: z.number().int().optional(),
  lockOnClose: z.boolean().optional(),
})

// Output: SubmissionWindow
```

#### `delete`
```typescript
// Input schema
z.object({ id: z.string() })

// Validation: Cannot delete if has uploaded files
// Output: { success: true }
```

#### `get`
```typescript
// Input schema
z.object({ id: z.string() })

// Output
{
  ...submissionWindow,
  competition: { id, name },
  fileRequirements: Array<SubmissionFileRequirement>,
  rounds: Array<{ id, name, roundType }>,
  _count: { fileRequirements, projectFiles },
}
```

#### `list`
```typescript
// Input schema
z.object({ competitionId: z.string() })

// Output
Array<{
  ...submissionWindow,
  _count: { fileRequirements, projectFiles, rounds },
}>
```

#### `addFileRequirement`
```typescript
// Input schema
z.object({
  submissionWindowId: z.string(),
  name: z.string().min(1),
  description: z.string().optional(),
  acceptedMimeTypes: z.array(z.string()).min(1),
  maxSizeMB: z.number().int().min(1).optional(),
  isRequired: z.boolean().default(true),
  sortOrder: z.number().int().optional(),
})

// Output: SubmissionFileRequirement
```

#### `updateFileRequirement`
```typescript
// Input schema
z.object({
  id: z.string(),
  name: z.string().optional(),
  description: z.string().optional(),
  acceptedMimeTypes: z.array(z.string()).optional(),
  maxSizeMB: z.number().int().optional(),
  isRequired: z.boolean().optional(),
  sortOrder: z.number().int().optional(),
})

// Output: SubmissionFileRequirement
```

#### `deleteFileRequirement`
```typescript
// Input schema
z.object({ id: z.string() })

// Validation: Cannot delete if has uploaded files against this requirement
// Output: { success: true }
```

#### `setRoundVisibility`
**Purpose:** Configure which submission windows a jury round can see

```typescript
// Input schema
z.object({
  roundId: z.string(),
  submissionWindowIds: z.array(z.string()),  // Windows jury can see
  displayLabels: z.record(z.string()).optional(),  // submissionWindowId → label
})

// Creates/updates RoundSubmissionVisibility records
// Output: Array<RoundSubmissionVisibility>
```

---

## 5. Winner Confirmation Router (new)

**File:** `src/server/routers/winner-confirmation.ts`

### New Procedures

#### `createProposal`
**Purpose:** Admin or system generates a winner proposal

```typescript
// Input schema
z.object({
  competitionId: z.string(),
  category: z.enum(['STARTUP', 'BUSINESS_CONCEPT']),

  // Proposed rankings (ordered project IDs)
  rankedProjectIds: z.array(z.string()).min(1).max(10),

  // Evidence/basis
  sourceRoundId: z.string(),
  selectionBasis: z.object({
    method: z.enum(['SCORE_BASED', 'VOTE_BASED', 'AI_RECOMMENDED', 'ADMIN_MANUAL']),
    scores: z.record(z.number()).optional(),
    aiRecommendation: z.string().optional(),
    reasoning: z.string().optional(),
  }),
})

// Output: WinnerProposal (status: PENDING)
```

#### `getProposal`
```typescript
// Input schema
z.object({ id: z.string() })

// Output
{
  ...winnerProposal,
  competition: { id, name },
  sourceRound: { id, name, roundType },
  proposedBy: { id, name, email },
  frozenBy: { id, name } | null,
  overrideBy: { id, name } | null,
  approvals: Array<{
    ...winnerApproval,
    user: { id, name, email },
  }>,
  projects: Array<{  // Ordered by rankedProjectIds
    id, title, teamName, rank,
  }>,
}
```

#### `listProposals`
```typescript
// Input schema
z.object({
  competitionId: z.string(),
  status: z.enum([
    'PENDING', 'APPROVED', 'REJECTED', 'OVERRIDDEN', 'FROZEN'
  ]).optional(),
})

// Output
Array<{
  ...winnerProposal,
  approvalProgress: {
    total: number,
    approved: number,
    rejected: number,
    pending: number,
  },
}>
```

#### `requestApproval`
**Purpose:** Admin adds jury members to approval list

```typescript
// Input schema
z.object({
  winnerProposalId: z.string(),
  userIds: z.array(z.string()).min(1),  // Jury members to request approval from
  role: z.enum(['JURY_MEMBER', 'ADMIN']).default('JURY_MEMBER'),
})

// Creates WinnerApproval records (approved: null)
// Output: Array<WinnerApproval>
```

#### `approve`
**Purpose:** Jury member or admin approves the proposal

```typescript
// Input schema (protectedProcedure)
z.object({
  winnerProposalId: z.string(),
  approved: z.boolean(),
  comments: z.string().optional(),
})

// Updates WinnerApproval.approved = true/false, respondedAt = now
// If all approvals received and all true → sets WinnerProposal.status = APPROVED
// If any approval is false → sets WinnerProposal.status = REJECTED
// Output: WinnerApproval
```

#### `getMyPendingApprovals`
**Purpose:** Jury member sees proposals awaiting their approval

```typescript
// Input schema (protectedProcedure)
z.object({ competitionId: z.string().optional() })

// Output
Array<{
  ...winnerApproval,
  proposal: {
    ...winnerProposal,
    competition: { id, name },
    projects: Array<{ id, title, rank }>,
  },
}>
```

#### `override`
**Purpose:** Admin forces a result (bypasses jury approval)

```typescript
// Input schema
z.object({
  winnerProposalId: z.string(),
  mode: z.enum(['FORCE_MAJORITY', 'ADMIN_DECISION']),
  reason: z.string().min(10),
})

// Sets WinnerProposal.status = OVERRIDDEN
// Sets overrideUsed = true, overrideMode, overrideReason, overrideById
// Output: WinnerProposal
```

#### `freeze`
**Purpose:** Lock the results (official, no further changes)

```typescript
// Input schema
z.object({ winnerProposalId: z.string() })

// Validation: Must be status APPROVED or OVERRIDDEN
// Sets WinnerProposal.status = FROZEN, frozenAt = now, frozenById
// Updates Competition with winnerProjectIds
// Output: WinnerProposal
```

#### `unfreeze`
**Purpose:** Admin unlocks results (rare, exceptional circumstances)

```typescript
// Input schema
z.object({
  winnerProposalId: z.string(),
  reason: z.string().min(10),
})

// Sets status = PENDING, frozenAt = null
// Creates audit log with reason
// Output: WinnerProposal
```

---

## 6. Assignment Router (enhanced)

**File:** `src/server/routers/assignment.ts`

### Modified Procedures

#### `create` → Enhanced with jury group awareness
**Changes:** Add `juryGroupId` parameter, enforce per-juror caps from JuryGroupMember

```typescript
// Input schema
z.object({
  userId: z.string(),
  projectId: z.string(),
  roundId: z.string(),  // RENAMED from stageId
  juryGroupId: z.string().optional(),  // NEW: Link to jury group
  isRequired: z.boolean().default(true),
  forceOverride: z.boolean().default(false),
})

// Validation:
// - Checks JuryGroupMember.maxAssignmentsOverride if set
// - Falls back to JuryGroup.defaultMaxAssignments
// - Falls back to User.maxAssignments
// - Enforces CapMode (HARD vs SOFT)

// Output: Assignment (with juryGroupId)
```

#### `bulkCreate` → Enhanced with capacity checking
**Changes:** Respect juryGroupId, apply category quotas

```typescript
// Input schema
z.object({
  roundId: z.string(),  // RENAMED from stageId
  juryGroupId: z.string().optional(),  // NEW
  assignments: z.array(z.object({
    userId: z.string(),
    projectId: z.string(),
  })),
})

// Logic:
// - For each juror, checks current count vs cap
// - If CapMode = HARD → rejects over-cap assignments
// - If CapMode = SOFT → allows up to softCapBuffer extra
// - Applies category quotas if categoryQuotasEnabled
// - Skips assignments that would violate quotas

// Output
{
  created: number,
  requested: number,
  skipped: number,
  skippedDueToCapacity: number,
  skippedDueToCategoryQuota: number,
}
```

#### `getSuggestions` → Enhanced with category quotas
**Changes:** Add quota scoring, penalty for over-quota categories

```typescript
// Input schema
z.object({
  roundId: z.string(),  // RENAMED from stageId
  juryGroupId: z.string().optional(),  // NEW
})

// Algorithm enhancements:
// - Fetches JuryGroup.defaultCategoryQuotas
// - Tracks per-juror category distribution
// - Penalizes assignments to over-quota categories (-25 score)
// - Bonuses assignments to under-quota categories (+10 score)

// Output: Array of suggestions with quota awareness
```

### New Procedures

#### `getJuryGroupStats`
**Purpose:** Per-group assignment coverage stats

```typescript
// Input schema
z.object({
  juryGroupId: z.string(),
  roundId: z.string(),
})

// Output
{
  juryGroup: { id, name },
  round: { id, name },
  members: Array<{
    user: { id, name, email },
    currentAssignments: number,
    cap: number,
    capMode: CapMode,
    categoryDistribution: Record<string, number>,  // Category → count
    quotaStatus: Record<string, { current, min, max, status }>,
  }>,
  totalAssignments: number,
  averageLoad: number,
  minLoad: number,
  maxLoad: number,
}
```

---

## 7. Evaluation Router (enhanced)

**File:** `src/server/routers/evaluation.ts`

### Modified Procedures

#### All procedures: Rename `stageId` → `roundId` in inputs/outputs

#### `get`, `start`, `autosave`, `submit` — No functional changes, just renames

### New Procedures

#### `getCrossRoundDocs`
**Purpose:** Jury sees documents from multiple submission windows

```typescript
// Input schema (protectedProcedure)
z.object({
  projectId: z.string(),
  roundId: z.string(),
})

// Logic:
// - Fetches RoundSubmissionVisibility records for roundId
// - Returns files from all visible submission windows

// Output
{
  round: { id, name },
  submissionWindows: Array<{
    window: { id, name, roundNumber },
    displayLabel: string,
    files: Array<ProjectFile>,
    requirements: Array<SubmissionFileRequirement>,
  }>,
}
```

---

## 8. File Router (enhanced)

**File:** `src/server/routers/file.ts`

### Modified Procedures

#### `getDownloadUrl` → Enhanced for multi-round visibility
**Changes:** Check RoundSubmissionVisibility for jury access

```typescript
// Input schema (no changes)
z.object({
  bucket: z.string(),
  objectKey: z.string(),
})

// Logic changes:
// - If user is jury, checks Assignment.roundId
// - Looks up RoundSubmissionVisibility for that round
// - Grants access if file.submissionWindowId is in visible windows

// Output: { url }
```

#### `getUploadUrl` → Enhanced for submission window association
**Changes:** Link file to submissionWindowId

```typescript
// Input schema
z.object({
  projectId: z.string(),
  fileName: z.string(),
  fileType: z.enum(['EXEC_SUMMARY', 'PRESENTATION', 'VIDEO', 'OTHER']),
  mimeType: z.string(),
  size: z.number().int().positive(),

  submissionWindowId: z.string().optional(),  // NEW: Which window is this for?
  requirementId: z.string().optional(),  // Links to SubmissionFileRequirement
})

// Creates ProjectFile with submissionWindowId
// Output: { uploadUrl, fileId }
```

### New Procedures

#### `promoteFromMentor`
**Purpose:** Promote a MentorFile to official ProjectFile

```typescript
// Input schema
z.object({
  mentorFileId: z.string(),
  submissionWindowId: z.string(),
  requirementId: z.string().optional(),
})

// Logic:
// - Copies object in MinIO to new key
// - Creates ProjectFile linked to submissionWindow
// - Sets MentorFile.isPromoted = true, promotedToFileId

// Output: ProjectFile
```

---

## 9. Applicant Router (enhanced)

**File:** `src/server/routers/applicant.ts`

### Modified Procedures

#### `getMySubmission` → Enhanced for multi-round
**Changes:** Returns submission status for all rounds

```typescript
// Input schema
z.object({
  competitionId: z.string().optional(),  // NEW: Filter by competition
  programId: z.string().optional(),
})

// Output
{
  ...project,
  roundStates: Array<{
    round: { id, name, roundType, sortOrder },
    state: ProjectRoundStateValue,
    enteredAt: DateTime,
    exitedAt: DateTime | null,
  }>,
  currentRound: { id, name, roundType } | null,
  eligibleForNextSubmission: boolean,
  nextSubmissionWindow: SubmissionWindow | null,
}
```

### New Procedures

#### `getCompetitionView`
**Purpose:** Applicant's view of their journey through a competition

```typescript
// Input schema (protectedProcedure)
z.object({
  competitionId: z.string(),
  projectId: z.string(),
})

// Output
{
  competition: { id, name, status },
  project: { id, title, teamName, status },
  rounds: Array<{
    round: { id, name, roundType, sortOrder, windowOpenAt, windowCloseAt },
    projectState: ProjectRoundState | null,
    isCurrent: boolean,
    isPassed: boolean,
    canSubmit: boolean,
    submissionWindow: SubmissionWindow | null,
  }>,
  timeline: Array<{
    date: DateTime,
    event: string,
    description: string,
  }>,
}
```

#### `submitToWindow`
**Purpose:** Submit files for a specific submission window

```typescript
// Input schema (protectedProcedure)
z.object({
  projectId: z.string(),
  submissionWindowId: z.string(),
  fileIds: z.array(z.string()),  // ProjectFile IDs to mark as submitted
})

// Validation:
// - Window must be open
// - All required FileRequirements must have files
// - User must own project or be team member

// Output: { success: true, submittedAt: DateTime }
```

---

## 10. Live Control Router (enhanced)

**File:** `src/server/routers/live-control.ts` (or `live.ts`)

### Modified Procedures

#### All procedures: Rename `stageId` → `roundId`

### New Procedures

#### `getStageManager`
**Purpose:** Admin view of live ceremony control panel

```typescript
// Input schema
z.object({ roundId: z.string() })

// Output
{
  round: { id, name, roundType },
  liveCursor: LiveProgressCursor | null,
  liveVotingSession: LiveVotingSession | null,

  // Cohorts (project presentation order)
  cohorts: Array<{
    ...cohort,
    projects: Array<{
      project: { id, title, teamName },
      presentationOrder: number,
    }>,
  }>,

  // Jury voting status
  juryVotes: Array<{
    juror: { id, name },
    votedProjects: Array<string>,  // Project IDs
    hasCompleted: boolean,
  }>,

  // Audience voting status
  audienceVotes: {
    total: number,
    byProject: Record<string, number>,
  },

  // Controls
  canStartVoting: boolean,
  canCloseVoting: boolean,
  canRevealResults: boolean,
}
```

#### `setCursor`
**Purpose:** Move live ceremony to a specific project/cohort

```typescript
// Input schema
z.object({
  roundId: z.string(),
  cohortId: z.string().optional(),
  projectId: z.string().optional(),
})

// Updates LiveProgressCursor.currentCohortId, currentProjectId
// Output: LiveProgressCursor
```

---

## 11. Mentor Workspace Router (new)

**File:** `src/server/routers/mentor-workspace.ts`

### New Procedures

#### `getWorkspace`
**Purpose:** Mentor or applicant views the shared workspace

```typescript
// Input schema (protectedProcedure)
z.object({ mentorAssignmentId: z.string() })

// Validation: User must be mentor or project team member
// Output
{
  mentorAssignment: {
    ...assignment,
    mentor: { id, name, email },
    project: { id, title, teamName },
    workspaceEnabled: boolean,
    workspaceOpenAt: DateTime | null,
    workspaceCloseAt: DateTime | null,
  },
  files: Array<{
    ...mentorFile,
    uploadedBy: { id, name },
    comments: Array<MentorFileComment>,
    isPromoted: boolean,
    promotedToFile: { id, fileName } | null,
  }>,
  canUpload: boolean,
  canComment: boolean,
  canPromote: boolean,
}
```

#### `uploadFile`
```typescript
// Input schema (protectedProcedure)
z.object({
  mentorAssignmentId: z.string(),
  fileName: z.string(),
  mimeType: z.string(),
  size: z.number().int().positive(),
  description: z.string().optional(),
})

// Creates MentorFile, returns pre-signed upload URL
// Output: { uploadUrl, fileId }
```

#### `addComment`
```typescript
// Input schema (protectedProcedure)
z.object({
  mentorFileId: z.string(),
  content: z.string().min(1).max(2000),
  parentCommentId: z.string().optional(),  // For threading
})

// Output: MentorFileComment
```

#### `promoteFile`
**Purpose:** Mentor promotes a workspace file to official submission

```typescript
// Input schema (mentorProcedure)
z.object({
  mentorFileId: z.string(),
  submissionWindowId: z.string(),
  requirementId: z.string().optional(),
})

// Calls file.promoteFromMentor internally
// Output: ProjectFile
```

---

## 12. Special Award Router (renamed to Award, enhanced)

**File:** `src/server/routers/award.ts` (renamed from `specialAward.ts`)

### Modified Procedures

#### `create` → Enhanced for two-mode awards
**Changes:** Add `eligibilityMode`, `juryGroupId`, `evaluationRoundId`

```typescript
// Input schema
z.object({
  competitionId: z.string(),  // CHANGED from programId
  name: z.string().min(1),
  description: z.string().optional(),
  criteriaText: z.string().optional(),

  // NEW: Award mode
  eligibilityMode: z.enum(['SEPARATE_POOL', 'STAY_IN_MAIN']).default('STAY_IN_MAIN'),

  // Scoring/voting
  scoringMode: z.enum(['PICK_WINNER', 'RANKED', 'SCORED']),
  maxRankedPicks: z.number().int().optional(),

  // Decision
  decisionMode: z.enum(['JURY_VOTE', 'AWARD_MASTER_DECISION', 'ADMIN_DECISION']).default('JURY_VOTE'),

  // NEW: Links to other entities
  evaluationRoundId: z.string().optional(),  // Which round this award runs alongside
  juryGroupId: z.string().optional(),  // Dedicated or shared jury group

  // AI eligibility
  useAiEligibility: z.boolean().default(false),
  autoTagRulesJson: z.record(z.unknown()).optional(),
})

// Output: SpecialAward
```

#### `update` → Add new fields
```typescript
// Input schema (all fields optional)
z.object({
  id: z.string(),
  name: z.string().optional(),
  description: z.string().optional(),
  criteriaText: z.string().optional(),
  eligibilityMode: z.enum(['SEPARATE_POOL', 'STAY_IN_MAIN']).optional(),  // NEW
  scoringMode: z.enum(['PICK_WINNER', 'RANKED', 'SCORED']).optional(),
  maxRankedPicks: z.number().int().optional(),
  decisionMode: z.enum(['JURY_VOTE', 'AWARD_MASTER_DECISION', 'ADMIN_DECISION']).optional(),
  evaluationRoundId: z.string().optional(),  // NEW
  juryGroupId: z.string().optional(),  // NEW
  useAiEligibility: z.boolean().optional(),
  votingStartAt: z.date().optional(),
  votingEndAt: z.date().optional(),
})

// Output: SpecialAward
```

### New Procedures

#### `getEligibilityByMode`
**Purpose:** View eligible projects with mode-specific filtering

```typescript
// Input schema
z.object({ awardId: z.string() })

// Output
{
  award: { ...specialAward, eligibilityMode },
  eligibleProjects: Array<{
    project: { id, title, teamName, competitionCategory },
    eligibility: AwardEligibility,

    // For SEPARATE_POOL mode
    wasRemovedFromMain: boolean,

    // For STAY_IN_MAIN mode
    mainRoundStatus: ProjectRoundStateValue,
  }>,
}
```

---

## 13. Auth & User Router

**File:** `src/server/routers/user.ts`

### New Procedures

#### `listJuryMembers`
**Purpose:** Admin lists all jury members for jury group assignment

```typescript
// Input schema
z.object({
  programId: z.string().optional(),
  onlyActive: z.boolean().default(true),
})

// Output
Array<{
  ...user,
  juryGroupMemberships: Array<{
    juryGroup: { id, name, competition: { id, name } },
    isLead: boolean,
  }>,
  _count: { assignments, juryGroupMemberships },
}>
```

---

## Procedure Access Matrix

| Router | Procedure | Public | Applicant | Jury | Mentor | Observer | Admin | Super Admin |
|--------|-----------|--------|-----------|------|--------|----------|-------|-------------|
| **competition** | create | | | | | | ✓ | ✓ |
| | update | | | | | | ✓ | ✓ |
| | delete | | | | | | ✓ | ✓ |
| | get | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| | list | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| | publish | | | | | | ✓ | ✓ |
| | createStructure | | | | | | ✓ | ✓ |
| | updateStructure | | | | | | ✓ | ✓ |
| | getDraft | | | | | | ✓ | ✓ |
| | simulate | | | | | | ✓ | ✓ |
| | getRoundAnalytics | | | | | ✓ | ✓ | ✓ |
| **round** | get | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| | transition | | | | | | ✓ | ✓ |
| | openWindow | | | | | | ✓ | ✓ |
| | closeWindow | | | | | | ✓ | ✓ |
| | getProjectStates | | | | | ✓ | ✓ | ✓ |
| | getForJury | | | ✓ | | | | |
| | getApplicantTimeline | | ✓ | ✓ | ✓ | | ✓ | ✓ |
| | getRequirements | | ✓ | ✓ | ✓ | | ✓ | ✓ |
| **jury-group** | create | | | | | | ✓ | ✓ |
| | update | | | | | | ✓ | ✓ |
| | delete | | | | | | ✓ | ✓ |
| | get | | | ✓* | | ✓ | ✓ | ✓ |
| | list | | | ✓* | | ✓ | ✓ | ✓ |
| | addMember | | | | | | ✓ | ✓ |
| | updateMember | | | | | | ✓ | ✓ |
| | removeMember | | | | | | ✓ | ✓ |
| | bulkAddMembers | | | | | | ✓ | ✓ |
| | getOnboarding | | | ✓* | | | | |
| | submitOnboarding | | | ✓* | | | | |
| **submission-window** | create | | | | | | ✓ | ✓ |
| | update | | | | | | ✓ | ✓ |
| | delete | | | | | | ✓ | ✓ |
| | get | | ✓* | ✓* | ✓* | ✓ | ✓ | ✓ |
| | list | | ✓* | ✓* | ✓* | ✓ | ✓ | ✓ |
| | addFileRequirement | | | | | | ✓ | ✓ |
| | updateFileRequirement | | | | | | ✓ | ✓ |
| | deleteFileRequirement | | | | | | ✓ | ✓ |
| | setRoundVisibility | | | | | | ✓ | ✓ |
| **winner-confirmation** | createProposal | | | | | | ✓ | ✓ |
| | getProposal | | | ✓* | | ✓ | ✓ | ✓ |
| | listProposals | | | | | ✓ | ✓ | ✓ |
| | requestApproval | | | | | | ✓ | ✓ |
| | approve | | | ✓* | | | ✓* | ✓ |
| | getMyPendingApprovals | | | ✓* | | | ✓* | ✓ |
| | override | | | | | | ✓ | ✓ |
| | freeze | | | | | | ✓ | ✓ |
| | unfreeze | | | | | | | ✓ |
| **assignment** | create | | | | | | ✓ | ✓ |
| | bulkCreate | | | | | | ✓ | ✓ |
| | delete | | | | | | ✓ | ✓ |
| | listByRound | | | | | ✓ | ✓ | ✓ |
| | listByProject | | | | | ✓ | ✓ | ✓ |
| | myAssignments | | | ✓ | | | | |
| | get | | | ✓* | ✓* | | ✓ | ✓ |
| | getStats | | | | | ✓ | ✓ | ✓ |
| | getSuggestions | | | | | | ✓ | ✓ |
| | getAISuggestions | | | | | | ✓ | ✓ |
| | applySuggestions | | | | | | ✓ | ✓ |
| | getJuryGroupStats | | | | | ✓ | ✓ | ✓ |
| **evaluation** | get | | | ✓* | | | ✓ | ✓ |
| | start | | | ✓* | | | | |
| | autosave | | | ✓* | | | | |
| | submit | | | ✓* | | | | |
| | getProjectStats | | | | | ✓ | ✓ | ✓ |
| | listByRound | | | | | ✓ | ✓ | ✓ |
| | myPastEvaluations | | | ✓ | | | | |
| | declareCOI | | | ✓* | | | | |
| | getCOIStatus | | | ✓* | | | ✓ | ✓ |
| | listCOIByRound | | | | | | ✓ | ✓ |
| | reviewCOI | | | | | | ✓ | ✓ |
| | generateSummary | | | | | | ✓ | ✓ |
| | getSummary | | | | | ✓ | ✓ | ✓ |
| | getCrossRoundDocs | | | ✓* | | | | |
| **file** | getDownloadUrl | | ✓* | ✓* | ✓* | | ✓ | ✓ |
| | getUploadUrl | | | | | | ✓ | ✓ |
| | promoteFromMentor | | | | ✓* | | ✓ | ✓ |
| **applicant** | getSubmissionBySlug | ✓ | ✓ | | | | | |
| | getMySubmission | | ✓ | | | | | |
| | saveSubmission | | ✓ | | | | | |
| | getCompetitionView | | ✓* | | | | | |
| | submitToWindow | | ✓* | | | | | |
| **mentor-workspace** | getWorkspace | | ✓* | | ✓* | | ✓ | ✓ |
| | uploadFile | | ✓* | | ✓* | | | |
| | addComment | | ✓* | | ✓* | | | |
| | promoteFile | | | | ✓ | | ✓ | ✓ |
| **award** | create | | | | | | ✓ | ✓ |
| | update | | | | | | ✓ | ✓ |
| | delete | | | | | | ✓ | ✓ |
| | get | | ✓* | ✓* | ✓* | ✓ | ✓ | ✓ |
| | list | | ✓* | ✓* | ✓* | ✓ | ✓ | ✓ |
| | updateStatus | | | | | | ✓ | ✓ |
| | runEligibility | | | | | | ✓ | ✓ |
| | getEligibilityByMode | | | | | ✓ | ✓ | ✓ |
| **live-control** | getStageManager | | | | | | ✓ | ✓ |
| | setCursor | | | | | | ✓ | ✓ |
| | startVoting | | | | | | ✓ | ✓ |
| | closeVoting | | | | | | ✓ | ✓ |
| | castJuryVote | | | ✓* | | | | |
| | castAudienceVote | | ✓* | ✓* | ✓* | ✓* | ✓* | ✓* |

*Legend:*
- `✓*` = Conditional access (must be assigned, own project, be in jury group, etc.)
- `✓` = Unconditional access for that role

---

## Migration Checklist: tRPC Updates

### Phase 1: Rename Existing Routers
- [ ] Rename `pipeline.ts` → `competition.ts`
- [ ] Update all imports in UI components
- [ ] Update all `trpc.pipeline.*` calls → `trpc.competition.*`

### Phase 2: Split Stage Router
- [ ] Create `round.ts` for runtime operations
- [ ] Move procedures: `transition`, `openWindow`, `closeWindow`, `getProjectStates`, `getForJury`, `getApplicantTimeline`, `getRequirements`
- [ ] Update all `trpc.stage.*` calls to reference correct router

### Phase 3: Create New Routers
- [ ] Create `jury-group.ts`
- [ ] Create `winner-confirmation.ts`
- [ ] Create `submission-window.ts`
- [ ] Create `mentor-workspace.ts`

### Phase 4: Enhance Existing Routers
- [ ] Update `assignment.ts` with jury group awareness
- [ ] Update `evaluation.ts` with cross-round doc visibility
- [ ] Update `file.ts` with submission window association
- [ ] Update `applicant.ts` with multi-round support
- [ ] Rename `specialAward.ts` → `award.ts` and enhance

### Phase 5: Update App Router
- [ ] Update `src/server/routers/_app.ts` with new routers
- [ ] Export all renamed routers
- [ ] Run type generation: `npm run trpc:generate`

---

**Total Procedures:**
- **Current**: ~180
- **New**: ~60
- **Modified**: ~30
- **Removed**: ~20
- **Final**: ~250

This comprehensive reference ensures every API change is documented with complete type safety.
