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:
262
src/server/services/assignment-policy.ts
Normal file
262
src/server/services/assignment-policy.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user