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

@@ -22,42 +22,36 @@ export const applicantRouter = router({
getSubmissionBySlug: publicProcedure
.input(z.object({ slug: z.string() }))
.query(async ({ ctx, input }) => {
const stage = await ctx.prisma.stage.findFirst({
const round = await ctx.prisma.round.findFirst({
where: { slug: input.slug },
include: {
track: {
competition: {
include: {
pipeline: {
include: {
program: { select: { id: true, name: true, year: true, description: true } },
},
},
program: { select: { id: true, name: true, year: true, description: true } },
},
},
},
})
if (!stage) {
if (!round) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Stage not found',
message: 'Round not found',
})
}
const now = new Date()
const isOpen = stage.windowCloseAt
? now < stage.windowCloseAt
: stage.status === 'STAGE_ACTIVE'
const isOpen = round.status === 'ROUND_ACTIVE'
return {
stage: {
id: stage.id,
name: stage.name,
slug: stage.slug,
windowCloseAt: stage.windowCloseAt,
id: round.id,
name: round.name,
slug: round.slug,
windowCloseAt: null,
isOpen,
},
program: stage.track.pipeline.program,
program: round.competition.program,
}
}),
@@ -65,7 +59,7 @@ export const applicantRouter = router({
* Get the current user's submission for a round (as submitter or team member)
*/
getMySubmission: protectedProcedure
.input(z.object({ stageId: z.string().optional(), programId: z.string().optional() }))
.input(z.object({ roundId: z.string().optional(), programId: z.string().optional() }))
.query(async ({ ctx, input }) => {
// Only applicants can use this
if (ctx.user.role !== 'APPLICANT') {
@@ -86,8 +80,8 @@ export const applicantRouter = router({
],
}
if (input.stageId) {
where.stageStates = { some: { stageId: input.stageId } }
if (input.roundId) {
where.roundAssignments = { some: { roundId: input.roundId } }
}
if (input.programId) {
where.programId = input.programId
@@ -239,7 +233,7 @@ export const applicantRouter = router({
fileName: z.string(),
mimeType: z.string(),
fileType: z.enum(['EXEC_SUMMARY', 'BUSINESS_PLAN', 'VIDEO_PITCH', 'PRESENTATION', 'SUPPORTING_DOC', 'OTHER']),
stageId: z.string().optional(),
roundId: z.string().optional(),
requirementId: z.string().optional(),
})
)
@@ -323,7 +317,7 @@ export const applicantRouter = router({
bucket: SUBMISSIONS_BUCKET,
objectKey,
isLate,
stageId: input.stageId || null,
roundId: input.roundId || null,
}
}),
@@ -340,7 +334,7 @@ export const applicantRouter = router({
fileType: z.enum(['EXEC_SUMMARY', 'BUSINESS_PLAN', 'VIDEO_PITCH', 'PRESENTATION', 'SUPPORTING_DOC', 'OTHER']),
bucket: z.string(),
objectKey: z.string(),
stageId: z.string().optional(),
roundId: z.string().optional(),
isLate: z.boolean().optional(),
requirementId: z.string().optional(),
})
@@ -378,7 +372,7 @@ export const applicantRouter = router({
})
}
const { projectId, stageId, isLate, requirementId, ...fileData } = input
const { projectId, roundId, isLate, requirementId, ...fileData } = input
// Delete existing file: by requirementId if provided, otherwise by fileType
if (requirementId) {
@@ -397,12 +391,12 @@ export const applicantRouter = router({
})
}
// Create new file record (roundId column kept null for new data)
// Create new file record
const file = await ctx.prisma.projectFile.create({
data: {
projectId,
...fileData,
roundId: null,
roundId: roundId || null,
isLate: isLate || false,
requirementId: requirementId || null,
},
@@ -1153,7 +1147,7 @@ export const applicantRouter = router({
})
if (!project) {
return { project: null, openStages: [], timeline: [], currentStatus: null }
return { project: null, openRounds: [], timeline: [], currentStatus: null }
}
const currentStatus = project.status ?? 'SUBMITTED'
@@ -1239,19 +1233,17 @@ export const applicantRouter = router({
}
const programId = project.programId
const openStages = programId
? await ctx.prisma.stage.findMany({
const openRounds = programId
? await ctx.prisma.round.findMany({
where: {
track: { pipeline: { programId } },
status: 'STAGE_ACTIVE',
competition: { programId },
status: 'ROUND_ACTIVE',
},
orderBy: { sortOrder: 'asc' },
select: {
id: true,
name: true,
slug: true,
stageType: true,
windowOpenAt: true,
windowCloseAt: true,
},
})
@@ -1267,7 +1259,7 @@ export const applicantRouter = router({
isTeamLead,
userRole: userMembership?.role || (project.submittedByUserId === ctx.user.id ? 'LEAD' : null),
},
openStages,
openRounds,
timeline,
currentStatus,
}