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,113 @@
import { z } from 'zod'
import { router, adminProcedure } from '../trpc'
import { resolveMemberContext } from '@/server/services/competition-context'
import { evaluateAssignmentPolicy } from '@/server/services/assignment-policy'
export const assignmentPolicyRouter = router({
/**
* Get the fully-resolved assignment policy for a specific member in a round.
* Returns cap, cap mode, buffer, category bias — all with provenance.
*/
getMemberPolicy: adminProcedure
.input(z.object({ roundId: z.string(), userId: z.string() }))
.query(async ({ input }) => {
const ctx = await resolveMemberContext(input.roundId, input.userId)
return evaluateAssignmentPolicy(ctx)
}),
/**
* Get policy summary for all members in a jury group for a given round.
* Useful for admin dashboards showing cap compliance across the group.
*/
getGroupPolicySummary: adminProcedure
.input(z.object({ juryGroupId: z.string(), roundId: z.string() }))
.query(async ({ ctx, input }) => {
const members = await ctx.prisma.juryGroupMember.findMany({
where: { juryGroupId: input.juryGroupId },
include: {
user: { select: { id: true, name: true, email: true } },
},
})
const results = await Promise.all(
members.map(async (member) => {
try {
const memberCtx = await resolveMemberContext(input.roundId, member.userId)
const policy = evaluateAssignmentPolicy(memberCtx)
return {
userId: member.userId,
userName: member.user.name,
userEmail: member.user.email,
role: member.role,
policy,
}
} catch {
// Member may not be linked to this round's jury group
return null
}
}),
)
return results.filter(Boolean)
}),
/**
* Get cap compliance report for a round.
* Groups members into overCap, atCap, belowCap, and noCap buckets.
*/
getCapComplianceReport: adminProcedure
.input(z.object({ roundId: z.string() }))
.query(async ({ ctx, input }) => {
const round = await ctx.prisma.round.findUniqueOrThrow({
where: { id: input.roundId },
select: { juryGroupId: true },
})
if (!round.juryGroupId) {
return { overCap: [], atCap: [], belowCap: [], noCap: [] }
}
const members = await ctx.prisma.juryGroupMember.findMany({
where: { juryGroupId: round.juryGroupId },
include: {
user: { select: { id: true, name: true, email: true } },
},
})
const report: {
overCap: Array<{ userId: string; userName: string | null; overCapBy: number }>
atCap: Array<{ userId: string; userName: string | null }>
belowCap: Array<{ userId: string; userName: string | null; remaining: number }>
noCap: Array<{ userId: string; userName: string | null }>
} = { overCap: [], atCap: [], belowCap: [], noCap: [] }
for (const member of members) {
try {
const memberCtx = await resolveMemberContext(input.roundId, member.userId)
const policy = evaluateAssignmentPolicy(memberCtx)
if (policy.effectiveCapMode.value === 'NONE') {
report.noCap.push({ userId: member.userId, userName: member.user.name })
} else if (policy.isOverCap) {
report.overCap.push({
userId: member.userId,
userName: member.user.name,
overCapBy: policy.overCapBy,
})
} else if (policy.remainingCapacity === 0) {
report.atCap.push({ userId: member.userId, userName: member.user.name })
} else {
report.belowCap.push({
userId: member.userId,
userName: member.user.name,
remaining: policy.remainingCapacity,
})
}
} catch {
// Skip members that can't be resolved
}
}
return report
}),
})