Add per-round assignment constraints (min/max per judge)
- Add minAssignmentsPerJuror and maxAssignmentsPerJuror fields to Round model - Update assignment router: - Calculate effective max from user override or round default - Add forceOverride parameter for manual assignment beyond limits - Update getSuggestions to use round constraints with min target bonus - Update getAISuggestions to pass constraints to AI service - Update AI assignment service: - Add minAssignmentsPerJuror to constraints interface - Update fallback algorithm with under-min bonus scoring - New score weights: 50% expertise, 30% load, 20% under-min bonus - Update round router: - Add new constraint fields to create/update schemas - Add validation for min <= max constraint - Update admin UI: - Add min/max constraint fields to round edit page - Remove hardcoded maxPerJuror from assignments page - Add migration files for production deployment: - User.bio field for judge/mentor profiles - Round assignment constraint fields Constraint hierarchy: 1. User.maxAssignments (if set) overrides round default 2. Round.maxAssignmentsPerJuror is the default cap 3. Admin can force-override any limit with confirmation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -78,6 +78,7 @@ interface ProjectForAssignment {
|
||||
|
||||
interface AssignmentConstraints {
|
||||
requiredReviewsPerProject: number
|
||||
minAssignmentsPerJuror?: number
|
||||
maxAssignmentsPerJuror?: number
|
||||
existingAssignments: Array<{
|
||||
jurorId: string
|
||||
@@ -412,18 +413,22 @@ export function generateFallbackAssignments(
|
||||
|
||||
return true
|
||||
})
|
||||
.map((juror) => ({
|
||||
juror,
|
||||
score: calculateExpertiseScore(juror.expertiseTags, project.tags),
|
||||
loadScore: calculateLoadScore(
|
||||
jurorAssignments.get(juror.id) || 0,
|
||||
juror.maxAssignments ?? constraints.maxAssignmentsPerJuror ?? 10
|
||||
),
|
||||
}))
|
||||
.map((juror) => {
|
||||
const currentLoad = jurorAssignments.get(juror.id) || 0
|
||||
const maxLoad = juror.maxAssignments ?? constraints.maxAssignmentsPerJuror ?? 20
|
||||
const minTarget = constraints.minAssignmentsPerJuror ?? 5
|
||||
|
||||
return {
|
||||
juror,
|
||||
score: calculateExpertiseScore(juror.expertiseTags, project.tags),
|
||||
loadScore: calculateLoadScore(currentLoad, maxLoad),
|
||||
underMinBonus: calculateUnderMinBonus(currentLoad, minTarget),
|
||||
}
|
||||
})
|
||||
.sort((a, b) => {
|
||||
// Combined score: 60% expertise, 40% load balancing
|
||||
const aTotal = a.score * 0.6 + a.loadScore * 0.4
|
||||
const bTotal = b.score * 0.6 + b.loadScore * 0.4
|
||||
// Combined score: 50% expertise, 30% load balancing, 20% under-min bonus
|
||||
const aTotal = a.score * 0.5 + a.loadScore * 0.3 + a.underMinBonus * 0.2
|
||||
const bTotal = b.score * 0.5 + b.loadScore * 0.3 + b.underMinBonus * 0.2
|
||||
return bTotal - aTotal
|
||||
})
|
||||
|
||||
@@ -494,6 +499,16 @@ function calculateLoadScore(currentLoad: number, maxLoad: number): number {
|
||||
return Math.max(0, 1 - utilization)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate bonus for jurors under their minimum target
|
||||
* Returns 1.0 if under min, scaled down as approaching min
|
||||
*/
|
||||
function calculateUnderMinBonus(currentLoad: number, minTarget: number): number {
|
||||
if (currentLoad >= minTarget) return 0
|
||||
// Scale bonus based on how far under min (1.0 at 0 load, decreasing as approaching min)
|
||||
return (minTarget - currentLoad) / minTarget
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate reasoning for fallback assignments
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user