Competition/Round architecture: full platform rewrite (Phases 1-9)
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s

Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 23:04:15 +01:00
parent 9ab4717f96
commit 6ca39c976b
349 changed files with 69938 additions and 28767 deletions

View File

@@ -77,7 +77,7 @@ type Role = 'SUPER_ADMIN' | 'PROGRAM_ADMIN' | 'AWARD_MASTER' | 'JURY_MEMBER' | '
interface Assignment {
projectId: string
stageId: string
roundId: string
}
interface MemberRow {
@@ -271,7 +271,7 @@ export default function MemberInvitePage() {
} | null>(null)
// Pre-assignment state
const [selectedStageId, setSelectedStageId] = useState<string>('')
const [selectedRoundId, setSelectedRoundId] = useState<string>('')
const utils = trpc.useUtils()
@@ -297,27 +297,27 @@ export default function MemberInvitePage() {
},
})
// Fetch programs with stages for pre-assignment
// Fetch programs with rounds for pre-assignment
const { data: programsData } = trpc.program.list.useQuery({
status: 'ACTIVE',
includeStages: true,
})
// Flatten all stages from all programs
const stages = useMemo(() => {
// Flatten all rounds from all programs
const rounds = useMemo(() => {
if (!programsData) return []
return programsData.flatMap((program) =>
((program.stages ?? []) as Array<{ id: string; name: string }>).map((stage: { id: string; name: string }) => ({
id: stage.id,
name: stage.name,
((program.stages ?? []) as Array<{ id: string; name: string }>).map((round: { id: string; name: string }) => ({
id: round.id,
name: round.name,
programName: `${program.name} ${program.year}`,
}))
)
}, [programsData])
// Fetch projects for selected stage
// Fetch projects for selected round
const { data: projectsData, isLoading: projectsLoading } = trpc.project.list.useQuery(
{ stageId: selectedStageId, perPage: 200 },
{ enabled: !!selectedStageId }
{ roundId: selectedRoundId, perPage: 200 },
{ enabled: !!selectedRoundId }
)
const projects = projectsData?.projects || []
@@ -365,7 +365,7 @@ export default function MemberInvitePage() {
// Per-row project assignment management
const toggleProjectAssignment = (rowId: string, projectId: string) => {
if (!selectedStageId) return
if (!selectedRoundId) return
setRows((prev) =>
prev.map((r) => {
if (r.id !== rowId) return r
@@ -373,7 +373,7 @@ export default function MemberInvitePage() {
if (existing) {
return { ...r, assignments: r.assignments.filter((a) => a.projectId !== projectId) }
} else {
return { ...r, assignments: [...r.assignments, { projectId, stageId: selectedStageId }] }
return { ...r, assignments: [...r.assignments, { projectId, roundId: selectedRoundId }] }
}
})
)
@@ -599,21 +599,21 @@ export default function MemberInvitePage() {
<div className="flex-1 min-w-0">
<Label className="text-sm font-medium">Pre-assign Projects (Optional)</Label>
<p className="text-xs text-muted-foreground">
Select a stage to assign projects to jury members before they onboard
Select a round to assign projects to jury members before they onboard
</p>
</div>
<Select
value={selectedStageId || 'none'}
onValueChange={(v) => setSelectedStageId(v === 'none' ? '' : v)}
value={selectedRoundId || 'none'}
onValueChange={(v) => setSelectedRoundId(v === 'none' ? '' : v)}
>
<SelectTrigger className="w-[200px]">
<SelectValue placeholder="Select stage" />
<SelectValue placeholder="Select round" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">No pre-assignment</SelectItem>
{stages.map((stage) => (
<SelectItem key={stage.id} value={stage.id}>
{stage.programName} - {stage.name}
{rounds.map((round) => (
<SelectItem key={round.id} value={round.id}>
{round.programName} - {round.name}
</SelectItem>
))}
</SelectContent>
@@ -685,7 +685,7 @@ export default function MemberInvitePage() {
/>
{/* Per-member project pre-assignment (only for jury members) */}
{row.role === 'JURY_MEMBER' && selectedStageId && (
{row.role === 'JURY_MEMBER' && selectedRoundId && (
<Collapsible className="space-y-2">
<CollapsibleTrigger asChild>
<Button