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

@@ -0,0 +1,358 @@
/**
* Submission Round Manager Service
*
* Manages SubmissionWindow lifecycle, file requirement enforcement,
* and deadline policies.
*/
import type { PrismaClient, DeadlinePolicy, Prisma } from '@prisma/client'
import { logAudit } from '@/server/utils/audit'
// ─── Types ──────────────────────────────────────────────────────────────────
export type WindowLifecycleResult = {
success: boolean
errors?: string[]
}
export type DeadlineStatus = {
status: 'OPEN' | 'GRACE' | 'CLOSED' | 'LOCKED'
graceExpiresAt?: Date
deadlinePolicy: DeadlinePolicy
}
export type SubmissionValidationResult = {
valid: boolean
errors: string[]
}
// ─── Window Lifecycle ───────────────────────────────────────────────────────
/**
* Open a submission window for accepting files.
*/
export async function openWindow(
windowId: string,
actorId: string,
prisma: PrismaClient | any,
): Promise<WindowLifecycleResult> {
try {
const window = await prisma.submissionWindow.findUnique({ where: { id: windowId } })
if (!window) {
return { success: false, errors: [`Submission window ${windowId} not found`] }
}
if (window.isLocked) {
return { success: false, errors: ['Cannot open a locked window'] }
}
await prisma.$transaction(async (tx: any) => {
await tx.submissionWindow.update({
where: { id: windowId },
data: {
windowOpenAt: new Date(),
isLocked: false,
},
})
await tx.decisionAuditLog.create({
data: {
eventType: 'submission_window.opened',
entityType: 'SubmissionWindow',
entityId: windowId,
actorId,
detailsJson: { windowName: window.name },
snapshotJson: { timestamp: new Date().toISOString(), emittedBy: 'submission-manager' },
},
})
await logAudit({
prisma: tx,
userId: actorId,
action: 'SUBMISSION_WINDOW_OPEN',
entityType: 'SubmissionWindow',
entityId: windowId,
detailsJson: { name: window.name },
})
})
return { success: true }
} catch (error) {
console.error('[SubmissionManager] openWindow failed:', error)
return {
success: false,
errors: [error instanceof Error ? error.message : 'Unknown error'],
}
}
}
/**
* Close a submission window (respects deadline policy).
*/
export async function closeWindow(
windowId: string,
actorId: string,
prisma: PrismaClient | any,
): Promise<WindowLifecycleResult> {
try {
const window = await prisma.submissionWindow.findUnique({ where: { id: windowId } })
if (!window) {
return { success: false, errors: [`Submission window ${windowId} not found`] }
}
await prisma.$transaction(async (tx: any) => {
const data: Record<string, unknown> = {
windowCloseAt: new Date(),
}
// Auto-lock on close if configured
if (window.lockOnClose && window.deadlinePolicy === 'HARD_DEADLINE') {
data.isLocked = true
}
await tx.submissionWindow.update({ where: { id: windowId }, data })
await tx.decisionAuditLog.create({
data: {
eventType: 'submission_window.closed',
entityType: 'SubmissionWindow',
entityId: windowId,
actorId,
detailsJson: {
windowName: window.name,
deadlinePolicy: window.deadlinePolicy,
autoLocked: data.isLocked === true,
},
snapshotJson: { timestamp: new Date().toISOString(), emittedBy: 'submission-manager' },
},
})
await logAudit({
prisma: tx,
userId: actorId,
action: 'SUBMISSION_WINDOW_CLOSE',
entityType: 'SubmissionWindow',
entityId: windowId,
detailsJson: { name: window.name },
})
})
return { success: true }
} catch (error) {
console.error('[SubmissionManager] closeWindow failed:', error)
return {
success: false,
errors: [error instanceof Error ? error.message : 'Unknown error'],
}
}
}
/**
* Lock a submission window (no further uploads allowed).
*/
export async function lockWindow(
windowId: string,
actorId: string,
prisma: PrismaClient | any,
): Promise<WindowLifecycleResult> {
try {
const window = await prisma.submissionWindow.findUnique({ where: { id: windowId } })
if (!window) {
return { success: false, errors: [`Submission window ${windowId} not found`] }
}
if (window.isLocked) {
return { success: false, errors: ['Window is already locked'] }
}
await prisma.$transaction(async (tx: any) => {
await tx.submissionWindow.update({
where: { id: windowId },
data: { isLocked: true },
})
await tx.decisionAuditLog.create({
data: {
eventType: 'submission_window.locked',
entityType: 'SubmissionWindow',
entityId: windowId,
actorId,
detailsJson: { windowName: window.name },
snapshotJson: { timestamp: new Date().toISOString(), emittedBy: 'submission-manager' },
},
})
await logAudit({
prisma: tx,
userId: actorId,
action: 'SUBMISSION_WINDOW_LOCK',
entityType: 'SubmissionWindow',
entityId: windowId,
detailsJson: { name: window.name },
})
})
return { success: true }
} catch (error) {
console.error('[SubmissionManager] lockWindow failed:', error)
return {
success: false,
errors: [error instanceof Error ? error.message : 'Unknown error'],
}
}
}
// ─── Deadline Enforcement ───────────────────────────────────────────────────
/**
* Check the current deadline status of a submission window.
*/
export async function checkDeadlinePolicy(
windowId: string,
prisma: PrismaClient | any,
): Promise<DeadlineStatus> {
const window = await prisma.submissionWindow.findUnique({ where: { id: windowId } })
if (!window) {
return { status: 'LOCKED', deadlinePolicy: 'HARD_DEADLINE' }
}
if (window.isLocked) {
return { status: 'LOCKED', deadlinePolicy: window.deadlinePolicy }
}
const now = new Date()
// Not yet open
if (window.windowOpenAt && now < window.windowOpenAt) {
return { status: 'CLOSED', deadlinePolicy: window.deadlinePolicy }
}
// No close time or still before close
if (!window.windowCloseAt || now < window.windowCloseAt) {
return { status: 'OPEN', deadlinePolicy: window.deadlinePolicy }
}
// Past the close time — policy determines behavior
switch (window.deadlinePolicy) {
case 'HARD_DEADLINE':
return { status: 'CLOSED', deadlinePolicy: window.deadlinePolicy }
case 'FLAG':
// Allow uploads but flag them
return { status: 'OPEN', deadlinePolicy: window.deadlinePolicy }
case 'GRACE': {
if (window.graceHours) {
const graceEnd = new Date(window.windowCloseAt.getTime() + window.graceHours * 60 * 60 * 1000)
if (now < graceEnd) {
return {
status: 'GRACE',
graceExpiresAt: graceEnd,
deadlinePolicy: window.deadlinePolicy,
}
}
}
return { status: 'CLOSED', deadlinePolicy: window.deadlinePolicy }
}
default:
return { status: 'CLOSED', deadlinePolicy: window.deadlinePolicy }
}
}
// ─── File Requirement Validation ────────────────────────────────────────────
/**
* Validate a project's submission against the window's file requirements.
*/
export async function validateSubmission(
projectId: string,
windowId: string,
files: Array<{ mimeType: string; size: number; requirementId?: string }>,
prisma: PrismaClient | any,
): Promise<SubmissionValidationResult> {
const errors: string[] = []
const requirements = await prisma.submissionFileRequirement.findMany({
where: { submissionWindowId: windowId },
orderBy: { sortOrder: 'asc' },
})
// Check required files are present
for (const req of requirements) {
if (!req.required) continue
const matchingFiles = files.filter((f) => f.requirementId === req.id)
if (matchingFiles.length === 0) {
errors.push(`Missing required file: ${req.label}`)
}
}
// Validate each file against its requirement
for (const file of files) {
if (!file.requirementId) continue
const req = requirements.find((r: any) => r.id === file.requirementId)
if (!req) {
errors.push(`Unknown file requirement: ${file.requirementId}`)
continue
}
// Check mime type
if (req.mimeTypes.length > 0 && !req.mimeTypes.includes(file.mimeType)) {
errors.push(
`File for "${req.label}" has invalid type ${file.mimeType}. Allowed: ${req.mimeTypes.join(', ')}`,
)
}
// Check size
if (req.maxSizeMb && file.size > req.maxSizeMb * 1024 * 1024) {
errors.push(
`File for "${req.label}" exceeds max size of ${req.maxSizeMb}MB`,
)
}
}
return { valid: errors.length === 0, errors }
}
// ─── Read-Only Enforcement ──────────────────────────────────────────────────
/**
* Check if a window is read-only (locked or hard-closed).
*/
export async function isWindowReadOnly(
windowId: string,
prisma: PrismaClient | any,
): Promise<boolean> {
const status = await checkDeadlinePolicy(windowId, prisma)
return status.status === 'LOCKED' || status.status === 'CLOSED'
}
// ─── Visibility Helpers ─────────────────────────────────────────────────────
/**
* Get visible submission windows for a round.
*/
export async function getVisibleWindows(
roundId: string,
prisma: PrismaClient | any,
) {
const visibility = await prisma.roundSubmissionVisibility.findMany({
where: { roundId, canView: true },
include: {
submissionWindow: {
include: { fileRequirements: { orderBy: { sortOrder: 'asc' } } },
},
},
})
return visibility.map((v: any) => ({
...v.submissionWindow,
displayLabel: v.displayLabel,
}))
}