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

@@ -10,10 +10,10 @@ import { randomUUID } from 'crypto'
import { prisma } from './setup'
import type {
UserRole,
StageType,
StageStatus,
TrackKind,
ProjectStageStateValue,
RoundType,
RoundStatus,
CompetitionStatus,
ProjectRoundStateValue,
AssignmentMethod,
} from '@prisma/client'
@@ -65,76 +65,52 @@ export async function createTestProgram(
})
}
// ─── Pipeline Factory ──────────────────────────────────────────────────────
// ─── Competition Factory ───────────────────────────────────────────────────
export async function createTestPipeline(
export async function createTestCompetition(
programId: string,
overrides: Partial<{ name: string; slug: string; status: string }> = {},
overrides: Partial<{
name: string
slug: string
status: CompetitionStatus
}> = {},
) {
const id = uid('pipe')
return prisma.pipeline.create({
const id = uid('comp')
return prisma.competition.create({
data: {
id,
programId,
name: overrides.name ?? `Pipeline ${id}`,
name: overrides.name ?? `Competition ${id}`,
slug: overrides.slug ?? id,
status: overrides.status ?? 'DRAFT',
},
})
}
// ─── Track Factory ─────────────────────────────────────────────────────────
// ─── Round Factory ─────────────────────────────────────────────────────────
export async function createTestTrack(
pipelineId: string,
export async function createTestRound(
competitionId: string,
overrides: Partial<{
name: string
slug: string
kind: TrackKind
sortOrder: number
routingMode: string
decisionMode: string
}> = {},
) {
const id = uid('track')
return prisma.track.create({
data: {
id,
pipelineId,
name: overrides.name ?? `Track ${id}`,
slug: overrides.slug ?? id,
kind: overrides.kind ?? 'MAIN',
sortOrder: overrides.sortOrder ?? 0,
routingMode: (overrides.routingMode as any) ?? null,
decisionMode: (overrides.decisionMode as any) ?? null,
},
})
}
// ─── Stage Factory ─────────────────────────────────────────────────────────
export async function createTestStage(
trackId: string,
overrides: Partial<{
name: string
slug: string
stageType: StageType
status: StageStatus
roundType: RoundType
status: RoundStatus
sortOrder: number
configJson: Record<string, unknown>
windowOpenAt: Date
windowCloseAt: Date
}> = {},
) {
const id = uid('stage')
return prisma.stage.create({
const id = uid('round')
return prisma.round.create({
data: {
id,
trackId,
name: overrides.name ?? `Stage ${id}`,
competitionId,
name: overrides.name ?? `Round ${id}`,
slug: overrides.slug ?? id,
stageType: overrides.stageType ?? 'EVALUATION',
status: overrides.status ?? 'STAGE_ACTIVE',
roundType: overrides.roundType ?? 'EVALUATION',
status: overrides.status ?? 'ROUND_ACTIVE',
sortOrder: overrides.sortOrder ?? 0,
configJson: (overrides.configJson as any) ?? undefined,
windowOpenAt: overrides.windowOpenAt ?? null,
@@ -143,23 +119,6 @@ export async function createTestStage(
})
}
// ─── Stage Transition Factory ──────────────────────────────────────────────
export async function createTestTransition(
fromStageId: string,
toStageId: string,
overrides: Partial<{ isDefault: boolean; guardJson: Record<string, unknown> }> = {},
) {
return prisma.stageTransition.create({
data: {
fromStageId,
toStageId,
isDefault: overrides.isDefault ?? true,
guardJson: (overrides.guardJson as any) ?? undefined,
},
})
}
// ─── Project Factory ───────────────────────────────────────────────────────
export async function createTestProject(
@@ -188,22 +147,20 @@ export async function createTestProject(
})
}
// ─── ProjectStageState Factory ─────────────────────────────────────────────
// ─── ProjectRoundState Factory ─────────────────────────────────────────────
export async function createTestPSS(
export async function createTestProjectRoundState(
projectId: string,
trackId: string,
stageId: string,
roundId: string,
overrides: Partial<{
state: ProjectStageStateValue
state: ProjectRoundStateValue
exitedAt: Date | null
}> = {},
) {
return prisma.projectStageState.create({
return prisma.projectRoundState.create({
data: {
projectId,
trackId,
stageId,
roundId,
state: overrides.state ?? 'PENDING',
exitedAt: overrides.exitedAt ?? null,
},
@@ -215,7 +172,7 @@ export async function createTestPSS(
export async function createTestAssignment(
userId: string,
projectId: string,
stageId: string,
roundId: string,
overrides: Partial<{
method: AssignmentMethod
isCompleted: boolean
@@ -225,7 +182,7 @@ export async function createTestAssignment(
data: {
userId,
projectId,
stageId,
roundId,
method: overrides.method ?? 'MANUAL',
isCompleted: overrides.isCompleted ?? false,
},
@@ -235,7 +192,7 @@ export async function createTestAssignment(
// ─── Evaluation Form Factory ───────────────────────────────────────────────
export async function createTestEvaluationForm(
stageId: string,
roundId: string,
criteria: Array<{
id: string
label: string
@@ -245,7 +202,7 @@ export async function createTestEvaluationForm(
) {
return prisma.evaluationForm.create({
data: {
stageId,
roundId,
criteriaJson: criteria.length > 0
? criteria
: [
@@ -261,7 +218,7 @@ export async function createTestEvaluationForm(
// ─── Filtering Rule Factory ────────────────────────────────────────────────
export async function createTestFilteringRule(
stageId: string,
roundId: string,
overrides: Partial<{
name: string
ruleType: string
@@ -271,7 +228,7 @@ export async function createTestFilteringRule(
) {
return prisma.filteringRule.create({
data: {
stageId,
roundId,
name: overrides.name ?? 'Test Filter Rule',
ruleType: (overrides.ruleType as any) ?? 'DOCUMENT_CHECK',
configJson: (overrides.configJson ?? { requiredFileTypes: ['EXEC_SUMMARY'], action: 'REJECT' }) as any,
@@ -304,7 +261,7 @@ export async function createTestCOI(
// ─── Cohort + CohortProject Factory ────────────────────────────────────────
export async function createTestCohort(
stageId: string,
roundId: string,
overrides: Partial<{
name: string
isOpen: boolean
@@ -315,7 +272,7 @@ export async function createTestCohort(
return prisma.cohort.create({
data: {
id,
stageId,
roundId,
name: overrides.name ?? `Cohort ${id}`,
isOpen: overrides.isOpen ?? false,
votingMode: overrides.votingMode ?? 'simple',
@@ -350,31 +307,31 @@ export async function cleanupTestData(programId: string, userIds: string[] = [])
await prisma.overrideAction.deleteMany({ where: { actorId: { in: userIds } } })
await prisma.auditLog.deleteMany({ where: { userId: { in: userIds } } })
}
await prisma.cohortProject.deleteMany({ where: { cohort: { stage: { track: { pipeline: { programId } } } } } })
await prisma.cohort.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.liveProgressCursor.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.filteringResult.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.filteringRule.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.filteringJob.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.assignmentJob.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.conflictOfInterest.deleteMany({ where: { assignment: { stage: { track: { pipeline: { programId } } } } } })
await prisma.evaluation.deleteMany({ where: { assignment: { stage: { track: { pipeline: { programId } } } } } })
await prisma.assignment.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.evaluationForm.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.fileRequirement.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.gracePeriod.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.reminderLog.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.evaluationSummary.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.evaluationDiscussion.deleteMany({ where: { stage: { track: { pipeline: { programId } } } } })
await prisma.projectStageState.deleteMany({ where: { track: { pipeline: { programId } } } })
await prisma.stageTransition.deleteMany({ where: { fromStage: { track: { pipeline: { programId } } } } })
// Competition/Round cascade cleanup
await prisma.cohortProject.deleteMany({ where: { cohort: { round: { competition: { programId } } } } })
await prisma.cohort.deleteMany({ where: { round: { competition: { programId } } } })
await prisma.liveProgressCursor.deleteMany({ where: { round: { competition: { programId } } } })
await prisma.filteringResult.deleteMany({ where: { round: { competition: { programId } } } })
await prisma.filteringRule.deleteMany({ where: { round: { competition: { programId } } } })
await prisma.filteringJob.deleteMany({ where: { round: { competition: { programId } } } })
await prisma.assignmentJob.deleteMany({ where: { round: { competition: { programId } } } })
await prisma.conflictOfInterest.deleteMany({ where: { assignment: { round: { competition: { programId } } } } })
await prisma.evaluation.deleteMany({ where: { assignment: { round: { competition: { programId } } } } })
await prisma.assignment.deleteMany({ where: { round: { competition: { programId } } } })
await prisma.evaluationForm.deleteMany({ where: { round: { competition: { programId } } } })
await prisma.fileRequirement.deleteMany({ where: { round: { competition: { programId } } } })
await prisma.gracePeriod.deleteMany({ where: { round: { competition: { programId } } } })
await prisma.reminderLog.deleteMany({ where: { round: { competition: { programId } } } })
await prisma.evaluationSummary.deleteMany({ where: { round: { competition: { programId } } } })
await prisma.evaluationDiscussion.deleteMany({ where: { round: { competition: { programId } } } })
await prisma.projectRoundState.deleteMany({ where: { round: { competition: { programId } } } })
await prisma.advancementRule.deleteMany({ where: { round: { competition: { programId } } } })
await prisma.awardEligibility.deleteMany({ where: { award: { program: { id: programId } } } })
await prisma.awardVote.deleteMany({ where: { award: { program: { id: programId } } } })
await prisma.awardJuror.deleteMany({ where: { award: { program: { id: programId } } } })
await prisma.specialAward.deleteMany({ where: { programId } })
await prisma.stage.deleteMany({ where: { track: { pipeline: { programId } } } })
await prisma.track.deleteMany({ where: { pipeline: { programId } } })
await prisma.pipeline.deleteMany({ where: { programId } })
await prisma.round.deleteMany({ where: { competition: { programId } } })
await prisma.competition.deleteMany({ where: { programId } })
await prisma.projectStatusHistory.deleteMany({ where: { project: { programId } } })
await prisma.projectFile.deleteMany({ where: { project: { programId } } })
await prisma.projectTag.deleteMany({ where: { project: { programId } } })