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

@@ -5,12 +5,12 @@ import { logAudit } from '@/server/utils/audit'
export const cohortRouter = router({
/**
* Create a new cohort within a stage
* Create a new cohort within a round
*/
create: adminProcedure
.input(
z.object({
stageId: z.string(),
roundId: z.string(),
name: z.string().min(1).max(255),
votingMode: z.enum(['simple', 'criteria', 'ranked']).default('simple'),
windowOpenAt: z.date().optional(),
@@ -18,18 +18,11 @@ export const cohortRouter = router({
})
)
.mutation(async ({ ctx, input }) => {
// Verify stage exists and is of a type that supports cohorts
const stage = await ctx.prisma.stage.findUniqueOrThrow({
where: { id: input.stageId },
// Verify round exists
const round = await ctx.prisma.round.findUniqueOrThrow({
where: { id: input.roundId },
})
if (stage.stageType !== 'LIVE_FINAL' && stage.stageType !== 'SELECTION') {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Cohorts can only be created in LIVE_FINAL or SELECTION stages',
})
}
// Validate window dates
if (input.windowOpenAt && input.windowCloseAt) {
if (input.windowCloseAt <= input.windowOpenAt) {
@@ -43,7 +36,7 @@ export const cohortRouter = router({
const cohort = await ctx.prisma.$transaction(async (tx) => {
const created = await tx.cohort.create({
data: {
stageId: input.stageId,
roundId: input.roundId,
name: input.name,
votingMode: input.votingMode,
windowOpenAt: input.windowOpenAt ?? null,
@@ -58,7 +51,7 @@ export const cohortRouter = router({
entityType: 'Cohort',
entityId: created.id,
detailsJson: {
stageId: input.stageId,
roundId: input.roundId,
name: input.name,
votingMode: input.votingMode,
},
@@ -244,13 +237,13 @@ export const cohortRouter = router({
}),
/**
* List cohorts for a stage
* List cohorts for a round
*/
list: protectedProcedure
.input(z.object({ stageId: z.string() }))
.input(z.object({ roundId: z.string() }))
.query(async ({ ctx, input }) => {
return ctx.prisma.cohort.findMany({
where: { stageId: input.stageId },
where: { roundId: input.roundId },
orderBy: { createdAt: 'asc' },
include: {
_count: { select: { projects: true } },
@@ -267,18 +260,11 @@ export const cohortRouter = router({
const cohort = await ctx.prisma.cohort.findUniqueOrThrow({
where: { id: input.id },
include: {
stage: {
round: {
select: {
id: true,
name: true,
stageType: true,
track: {
select: {
id: true,
name: true,
pipeline: { select: { id: true, name: true } },
},
},
competition: { select: { id: true, name: true } },
},
},
projects: {
@@ -298,7 +284,7 @@ export const cohortRouter = router({
},
})
// Get vote counts per project in the cohort's stage session
// Get vote counts per project in the cohort's round session
const projectIds = cohort.projects.map((p) => p.projectId)
const voteSummary =
projectIds.length > 0
@@ -306,7 +292,7 @@ export const cohortRouter = router({
by: ['projectId'],
where: {
projectId: { in: projectIds },
session: { stageId: cohort.stage.id },
session: { roundId: cohort.round.id },
},
_count: true,
_avg: { score: true },