Competition/Round architecture: full platform rewrite (Phases 1-9)
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s
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:
113
src/server/routers/assignmentPolicy.ts
Normal file
113
src/server/routers/assignmentPolicy.ts
Normal 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
|
||||
}),
|
||||
})
|
||||
Reference in New Issue
Block a user