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

@@ -12,7 +12,7 @@ import { logAudit } from '../utils/audit'
export const projectPoolRouter = router({
/**
* List unassigned projects with filtering and pagination
* Projects not assigned to any stage
* Projects not assigned to any round
*/
listUnassigned: adminProcedure
.input(
@@ -33,7 +33,7 @@ export const projectPoolRouter = router({
// Build where clause
const where: Record<string, unknown> = {
programId,
stageStates: { none: {} }, // Only unassigned projects (not in any stage)
projectRoundStates: { none: {} }, // Only unassigned projects (not in any round)
}
// Filter by competition category
@@ -92,27 +92,27 @@ export const projectPoolRouter = router({
}),
/**
* Bulk assign projects to a stage
* Bulk assign projects to a round
*
* Validates that:
* - All projects exist
* - Stage exists
* - Round exists
*
* Creates:
* - ProjectStageState entries for each project
* - RoundAssignment entries for each project
* - Project.status updated to 'ASSIGNED'
* - ProjectStatusHistory records for each project
* - Audit log
*/
assignToStage: adminProcedure
assignToRound: adminProcedure
.input(
z.object({
projectIds: z.array(z.string()).min(1).max(200), // Max 200 projects at once
stageId: z.string(),
roundId: z.string(),
})
)
.mutation(async ({ ctx, input }) => {
const { projectIds, stageId } = input
const { projectIds, roundId } = input
// Step 1: Fetch all projects to validate
const projects = await ctx.prisma.project.findMany({
@@ -136,24 +136,22 @@ export const projectPoolRouter = router({
})
}
// Verify stage exists and get its trackId
const stage = await ctx.prisma.stage.findUniqueOrThrow({
where: { id: stageId },
select: { id: true, trackId: true },
// Verify round exists
const round = await ctx.prisma.round.findUniqueOrThrow({
where: { id: roundId },
select: { id: true },
})
// Step 2: Perform bulk assignment in a transaction
const result = await ctx.prisma.$transaction(async (tx) => {
// Create ProjectStageState entries for each project (skip existing)
const stageStateData = projectIds.map((projectId) => ({
// Create ProjectRoundState entries for each project (skip existing)
const assignmentData = projectIds.map((projectId) => ({
projectId,
stageId,
trackId: stage.trackId,
state: 'PENDING' as const,
roundId,
}))
await tx.projectStageState.createMany({
data: stageStateData,
await tx.projectRoundState.createMany({
data: assignmentData,
skipDuplicates: true,
})
@@ -180,10 +178,10 @@ export const projectPoolRouter = router({
await logAudit({
prisma: tx,
userId: ctx.user?.id,
action: 'BULK_ASSIGN_TO_STAGE',
action: 'BULK_ASSIGN_TO_ROUND',
entityType: 'Project',
detailsJson: {
stageId,
roundId,
projectCount: projectIds.length,
projectIds,
},
@@ -197,7 +195,7 @@ export const projectPoolRouter = router({
return {
success: true,
assignedCount: result.count,
stageId,
roundId,
}
}),
})