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,262 @@
import type { CapMode } from '@prisma/client'
import type { PolicyResolution } from '@/types/competition'
import type { MemberContext } from './competition-context'
// ============================================================================
// System Defaults (Layer 1)
// ============================================================================
export const SYSTEM_DEFAULT_CAP = 15
export const SYSTEM_DEFAULT_CAP_MODE: CapMode = 'SOFT'
export const SYSTEM_DEFAULT_SOFT_BUFFER = 2
// ============================================================================
// Effective Cap Resolution (5-layer precedence)
// ============================================================================
/**
* Resolves the effective assignment cap for a jury member.
*
* Precedence (first non-null wins):
* Layer 4b: selfServiceCap (bounded by admin max, only if allowJurorCapAdjustment)
* Layer 4a: maxAssignmentsOverride (admin per-member override)
* Layer 3: juryGroup.defaultMaxAssignments
* Layer 1: SYSTEM_DEFAULT_CAP (15)
*/
export function resolveEffectiveCap(ctx: MemberContext): PolicyResolution<number> {
const group = ctx.juryGroup
// Layer 4b: Self-service cap (juror-set during onboarding)
if (group?.allowJurorCapAdjustment && ctx.member.selfServiceCap != null) {
const adminMax =
ctx.member.maxAssignmentsOverride
?? group.defaultMaxAssignments
?? SYSTEM_DEFAULT_CAP
const bounded = Math.min(ctx.member.selfServiceCap, adminMax)
return {
value: bounded,
source: 'member',
explanation: `Self-service cap ${ctx.member.selfServiceCap} (bounded to ${adminMax})`,
}
}
// Layer 4a: Admin per-member override
if (ctx.member.maxAssignmentsOverride != null) {
return {
value: ctx.member.maxAssignmentsOverride,
source: 'member',
explanation: `Admin per-member override: ${ctx.member.maxAssignmentsOverride}`,
}
}
// Layer 3: Jury group default
if (group) {
return {
value: group.defaultMaxAssignments,
source: 'jury_group',
explanation: `Jury group "${group.name}" default: ${group.defaultMaxAssignments}`,
}
}
// Layer 1: System default
return {
value: SYSTEM_DEFAULT_CAP,
source: 'system',
explanation: `System default: ${SYSTEM_DEFAULT_CAP}`,
}
}
// ============================================================================
// Effective Cap Mode Resolution
// ============================================================================
/**
* Resolves the effective cap mode for a jury member.
*
* Precedence:
* Layer 4a: capModeOverride (admin per-member)
* Layer 3: juryGroup.defaultCapMode
* Layer 1: SYSTEM_DEFAULT_CAP_MODE (SOFT)
*/
export function resolveEffectiveCapMode(ctx: MemberContext): PolicyResolution<CapMode> {
// Layer 4a: Admin per-member override
if (ctx.member.capModeOverride != null) {
return {
value: ctx.member.capModeOverride,
source: 'member',
explanation: `Admin per-member cap mode override: ${ctx.member.capModeOverride}`,
}
}
// Layer 3: Jury group default
if (ctx.juryGroup) {
return {
value: ctx.juryGroup.defaultCapMode,
source: 'jury_group',
explanation: `Jury group "${ctx.juryGroup.name}" default: ${ctx.juryGroup.defaultCapMode}`,
}
}
// Layer 1: System default
return {
value: SYSTEM_DEFAULT_CAP_MODE,
source: 'system',
explanation: `System default: ${SYSTEM_DEFAULT_CAP_MODE}`,
}
}
// ============================================================================
// Effective Soft Cap Buffer Resolution
// ============================================================================
/**
* Resolves the effective soft cap buffer.
* Only meaningful when capMode is SOFT.
*
* Precedence:
* Layer 3: juryGroup.softCapBuffer
* Layer 1: SYSTEM_DEFAULT_SOFT_BUFFER (2)
*/
export function resolveEffectiveSoftCapBuffer(
ctx: MemberContext,
): PolicyResolution<number> {
if (ctx.juryGroup) {
return {
value: ctx.juryGroup.softCapBuffer,
source: 'jury_group',
explanation: `Jury group "${ctx.juryGroup.name}" buffer: ${ctx.juryGroup.softCapBuffer}`,
}
}
return {
value: SYSTEM_DEFAULT_SOFT_BUFFER,
source: 'system',
explanation: `System default buffer: ${SYSTEM_DEFAULT_SOFT_BUFFER}`,
}
}
// ============================================================================
// Effective Category Bias Resolution
// ============================================================================
/**
* Resolves the effective category bias (startup ratio) for a jury member.
*
* Precedence:
* Layer 4b: selfServiceRatio (if allowJurorRatioAdjustment)
* Layer 4a: preferredStartupRatio (admin per-member)
* Layer 3: juryGroup.defaultCategoryQuotas (derived ratio)
* Default: null (no preference)
*/
export function resolveEffectiveCategoryBias(
ctx: MemberContext,
): PolicyResolution<Record<string, number> | null> {
const group = ctx.juryGroup
// Layer 4b: Self-service ratio
if (group?.allowJurorRatioAdjustment && ctx.member.selfServiceRatio != null) {
const ratio = ctx.member.selfServiceRatio
return {
value: { STARTUP: ratio, BUSINESS_CONCEPT: 1 - ratio },
source: 'member',
explanation: `Self-service ratio: ${Math.round(ratio * 100)}% startup`,
}
}
// Layer 4a: Admin per-member preferred ratio
if (ctx.member.preferredStartupRatio != null) {
const ratio = ctx.member.preferredStartupRatio
return {
value: { STARTUP: ratio, BUSINESS_CONCEPT: 1 - ratio },
source: 'member',
explanation: `Admin-set ratio: ${Math.round(ratio * 100)}% startup`,
}
}
// Layer 3: Jury group default category quotas (derive ratio from quotas)
if (group?.categoryQuotasEnabled && group.defaultCategoryQuotas) {
const quotas = group.defaultCategoryQuotas as Record<
string,
{ min: number; max: number }
>
const totalMax = Object.values(quotas).reduce((sum, q) => sum + q.max, 0)
if (totalMax > 0) {
const bias: Record<string, number> = {}
for (const [cat, q] of Object.entries(quotas)) {
bias[cat] = q.max / totalMax
}
return {
value: bias,
source: 'jury_group',
explanation: `Derived from group category quotas`,
}
}
}
// No preference
return {
value: null,
source: 'system',
explanation: 'No category bias configured',
}
}
// ============================================================================
// Aggregate Policy Evaluation
// ============================================================================
export type AssignmentPolicyResult = {
effectiveCap: PolicyResolution<number>
effectiveCapMode: PolicyResolution<CapMode>
softCapBuffer: PolicyResolution<number>
categoryBias: PolicyResolution<Record<string, number> | null>
canAssignMore: boolean
remainingCapacity: number
isOverCap: boolean
overCapBy: number
}
/**
* Evaluates all assignment policies for a member in one call.
* Returns resolved values with provenance plus computed flags.
*/
export function evaluateAssignmentPolicy(
ctx: MemberContext,
): AssignmentPolicyResult {
const effectiveCap = resolveEffectiveCap(ctx)
const effectiveCapMode = resolveEffectiveCapMode(ctx)
const softCapBuffer = resolveEffectiveSoftCapBuffer(ctx)
const categoryBias = resolveEffectiveCategoryBias(ctx)
const cap = effectiveCap.value
const mode = effectiveCapMode.value
const buffer = softCapBuffer.value
const count = ctx.currentAssignmentCount
const isOverCap = count > cap
const overCapBy = Math.max(0, count - cap)
const remainingCapacity =
mode === 'NONE'
? Infinity
: mode === 'SOFT'
? Math.max(0, cap + buffer - count)
: Math.max(0, cap - count)
const canAssignMore =
mode === 'NONE'
? true
: mode === 'SOFT'
? count < cap + buffer
: count < cap
return {
effectiveCap,
effectiveCapMode,
softCapBuffer,
categoryBias,
canAssignMore,
remainingCapacity,
isOverCap,
overCapBy,
}
}