MOPC-App/docs/claude-architecture-redesign/19-api-router-reference.md

1735 lines
43 KiB
Markdown

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