Round system redesign: Phases 1-7 complete

Full pipeline/track/stage architecture replacing the legacy round system.

Schema: 11 new models (Pipeline, Track, Stage, StageTransition,
ProjectStageState, RoutingRule, Cohort, CohortProject, LiveProgressCursor,
OverrideAction, AudienceVoter) + 8 new enums.

Backend: 9 new routers (pipeline, stage, routing, stageFiltering,
stageAssignment, cohort, live, decision, award) + 6 new services
(stage-engine, routing-engine, stage-filtering, stage-assignment,
stage-notifications, live-control).

Frontend: Pipeline wizard (17 components), jury stage pages (7),
applicant pipeline pages (3), public stage pages (2), admin pipeline
pages (5), shared stage components (3), SSE route, live hook.

Phase 6 refit: 23 routers/services migrated from roundId to stageId,
all frontend components refitted. Deleted round.ts (985 lines),
roundTemplate.ts, round-helpers.ts, round-settings.ts, round-type-settings.tsx,
10 legacy admin pages, 7 legacy jury pages, 3 legacy dialogs.

Phase 7 validation: 36 tests (10 unit + 8 integration files) all passing,
TypeScript 0 errors, Next.js build succeeds, 13 integrity checks,
legacy symbol sweep clean, auto-seed on first Docker startup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 13:57:09 +01:00
parent 8a328357e3
commit 331b67dae0
256 changed files with 29117 additions and 21424 deletions

View File

@@ -0,0 +1,132 @@
import type { StageType, TrackKind, RoutingMode, DecisionMode, AwardScoringMode } from '@prisma/client'
// ============================================================================
// Stage Config Discriminated Unions
// ============================================================================
export type IntakeConfig = {
submissionWindowEnabled: boolean
lateSubmissionPolicy: 'reject' | 'flag' | 'accept'
lateGraceHours: number
fileRequirements: FileRequirementConfig[]
}
export type FileRequirementConfig = {
name: string
description?: string
acceptedMimeTypes: string[]
maxSizeMB?: number
isRequired: boolean
}
export type FilterConfig = {
rules: FilterRuleConfig[]
aiRubricEnabled: boolean
aiConfidenceThresholds: {
high: number
medium: number
low: number
}
manualQueueEnabled: boolean
}
export type FilterRuleConfig = {
field: string
operator: string
value: string | number | boolean
weight: number
}
export type EvaluationConfig = {
requiredReviews: number
maxLoadPerJuror: number
minLoadPerJuror: number
availabilityWeighting: boolean
overflowPolicy: 'queue' | 'expand_pool' | 'reduce_reviews'
}
export type SelectionConfig = {
finalistCount?: number
rankingMethod: 'score_average' | 'weighted_criteria' | 'binary_pass'
tieBreaker: 'admin_decides' | 'highest_individual' | 'revote'
}
export type LiveFinalConfig = {
juryVotingEnabled: boolean
audienceVotingEnabled: boolean
audienceVoteWeight: number
cohortSetupMode: 'auto' | 'manual'
revealPolicy: 'immediate' | 'delayed' | 'ceremony'
}
export type ResultsConfig = {
publicationMode: 'manual' | 'auto_on_close'
showDetailedScores: boolean
showRankings: boolean
}
export type StageConfigMap = {
INTAKE: IntakeConfig
FILTER: FilterConfig
EVALUATION: EvaluationConfig
SELECTION: SelectionConfig
LIVE_FINAL: LiveFinalConfig
RESULTS: ResultsConfig
}
// ============================================================================
// Wizard Stage / Track / State Types
// ============================================================================
export type WizardStageConfig = {
id?: string
name: string
slug: string
stageType: StageType
sortOrder: number
configJson: Record<string, unknown>
windowOpenAt?: Date | null
windowCloseAt?: Date | null
}
export type WizardTrackConfig = {
id?: string
name: string
slug: string
kind: TrackKind
sortOrder: number
routingModeDefault?: RoutingMode
decisionMode?: DecisionMode
stages: WizardStageConfig[]
awardConfig?: {
name: string
description?: string
scoringMode?: AwardScoringMode
}
}
export type WizardState = {
name: string
slug: string
programId: string
settingsJson: Record<string, unknown>
tracks: WizardTrackConfig[]
notificationConfig: Record<string, boolean>
overridePolicy: Record<string, unknown>
}
// ============================================================================
// Validation Result
// ============================================================================
export type ValidationResult = {
valid: boolean
errors: string[]
warnings: string[]
}
export type SectionValidation = {
basics: ValidationResult
tracks: ValidationResult
notifications: ValidationResult
}

View File

@@ -1,148 +0,0 @@
/**
* Round type-specific settings interfaces
*/
// Filtering round settings (Round 1: High-volume screening)
export interface FilteringRoundSettings {
// Auto-elimination configuration
autoEliminationEnabled: boolean
autoEliminationThreshold: number // Minimum average score (e.g., 4)
autoEliminationMinReviews: number // Min reviews required before elimination
targetAdvancing: number // Target number of projects to advance (e.g., 60)
// Auto-run filtering when round closes
autoFilterOnClose: boolean
// Display options
showAverageScore: boolean
showRanking: boolean
}
// Evaluation round settings (Round 2: In-depth review)
export interface EvaluationRoundSettings {
// Requirements
detailedCriteriaRequired: boolean
minimumFeedbackLength: number // Minimum characters for feedback
targetFinalists: number // Target number of finalists (e.g., 6)
// Display options
showAverageScore: boolean
showRanking: boolean
requireAllCriteria: boolean
}
// Live event round settings (Round 3: Event day)
export interface LiveEventRoundSettings {
// Presentation
presentationDurationMinutes: number
presentationOrder: string[] // Project IDs in order
// Voting
votingWindowSeconds: number
showLiveScores: boolean
allowVoteChange: boolean
votingMode: 'simple' | 'criteria'
// Audience voting
audienceVotingMode: 'disabled' | 'per_project' | 'per_category' | 'favorites'
audienceMaxFavorites: number
audienceRequireId: boolean
audienceVotingDuration: number | null
// Display
displayMode: 'SCORES' | 'RANKING' | 'NONE'
}
// Union type for all round settings
export type RoundSettings =
| { type: 'FILTERING'; settings: FilteringRoundSettings }
| { type: 'EVALUATION'; settings: EvaluationRoundSettings }
| { type: 'LIVE_EVENT'; settings: LiveEventRoundSettings }
// Default settings for each round type
export const defaultFilteringSettings: FilteringRoundSettings = {
autoEliminationEnabled: false,
autoEliminationThreshold: 4,
autoEliminationMinReviews: 0,
targetAdvancing: 60,
autoFilterOnClose: true,
showAverageScore: true,
showRanking: true,
}
export const defaultEvaluationSettings: EvaluationRoundSettings = {
detailedCriteriaRequired: true,
minimumFeedbackLength: 50,
targetFinalists: 6,
showAverageScore: true,
showRanking: true,
requireAllCriteria: true,
}
export const defaultLiveEventSettings: LiveEventRoundSettings = {
presentationDurationMinutes: 5,
presentationOrder: [],
votingWindowSeconds: 30,
showLiveScores: true,
allowVoteChange: false,
votingMode: 'simple',
audienceVotingMode: 'disabled',
audienceMaxFavorites: 3,
audienceRequireId: false,
audienceVotingDuration: null,
displayMode: 'RANKING',
}
// Round type labels
export const roundTypeLabels: Record<string, string> = {
FILTERING: 'Filtering Round',
EVALUATION: 'Evaluation Round',
LIVE_EVENT: 'Live Event',
}
// Round type descriptions
export const roundTypeDescriptions: Record<string, string> = {
FILTERING: 'High-volume initial screening with auto-elimination options',
EVALUATION: 'In-depth evaluation with detailed criteria and feedback',
LIVE_EVENT: 'Real-time voting during presentations',
}
// Field visibility per round type
export const ROUND_FIELD_VISIBILITY: Record<string, {
showRequiredReviews: boolean
showAssignmentLimits: boolean
showVotingWindow: boolean
showSubmissionDates: boolean
showEvaluationForm: boolean
}> = {
FILTERING: {
showRequiredReviews: false,
showAssignmentLimits: false,
showVotingWindow: false,
showSubmissionDates: true,
showEvaluationForm: false,
},
EVALUATION: {
showRequiredReviews: true,
showAssignmentLimits: true,
showVotingWindow: true,
showSubmissionDates: true,
showEvaluationForm: true,
},
LIVE_EVENT: {
showRequiredReviews: false,
showAssignmentLimits: false,
showVotingWindow: false,
showSubmissionDates: false,
showEvaluationForm: false,
},
}
// Live voting criterion type
export interface LiveVotingCriterion {
id: string
label: string
description?: string
scale: number // max score (e.g. 10)
weight: number // 0-1, weights must sum to 1
}