2026-02-04 14:15:06 +01:00
|
|
|
/**
|
|
|
|
|
* Smart Assignment Scoring Service
|
|
|
|
|
*
|
|
|
|
|
* Calculates scores for jury/mentor-project matching based on:
|
|
|
|
|
* - Tag overlap (expertise match)
|
2026-02-04 15:27:28 +01:00
|
|
|
* - Bio/description match (text similarity)
|
2026-02-04 14:15:06 +01:00
|
|
|
* - Workload balance
|
|
|
|
|
* - Country match (mentors only)
|
Implement 10 platform features: evaluation UX, admin tools, AI summaries, applicant portal
Batch 1 - Quick Wins:
- F1: Evaluation progress indicator with touch tracking in sticky status bar
- F2: Export filtering results as CSV with dynamic AI column flattening
- F3: Observer access to analytics dashboards (8 procedures changed to observerProcedure)
Batch 2 - Jury Experience:
- F4: Countdown timer component with urgency colors + email reminder service with cron endpoint
- F5: Conflict of interest declaration system (dialog, admin management, review workflow)
Batch 3 - Admin & AI Enhancements:
- F6: Bulk status update UI with selection checkboxes, floating toolbar, status history recording
- F7: AI-powered evaluation summary with anonymized data, OpenAI integration, scoring patterns
- F8: Smart assignment improvements (geo diversity penalty, round familiarity bonus, COI blocking)
Batch 4 - Form Flexibility & Applicant Portal:
- F9: Evaluation form flexibility (text, boolean, section_header types, conditional visibility)
- F10: Applicant portal (status timeline, per-round documents, mentor messaging)
Schema: 5 new models (ReminderLog, ConflictOfInterest, EvaluationSummary, ProjectStatusHistory, MentorMessage), ProjectFile extended with roundId + isLate.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 21:58:27 +01:00
|
|
|
* - Geographic diversity penalty (prevents clustering by country)
|
|
|
|
|
* - Previous round familiarity bonus (continuity across rounds)
|
|
|
|
|
* - COI penalty (conflict of interest hard-block)
|
2026-02-04 14:15:06 +01:00
|
|
|
*
|
Implement 10 platform features: evaluation UX, admin tools, AI summaries, applicant portal
Batch 1 - Quick Wins:
- F1: Evaluation progress indicator with touch tracking in sticky status bar
- F2: Export filtering results as CSV with dynamic AI column flattening
- F3: Observer access to analytics dashboards (8 procedures changed to observerProcedure)
Batch 2 - Jury Experience:
- F4: Countdown timer component with urgency colors + email reminder service with cron endpoint
- F5: Conflict of interest declaration system (dialog, admin management, review workflow)
Batch 3 - Admin & AI Enhancements:
- F6: Bulk status update UI with selection checkboxes, floating toolbar, status history recording
- F7: AI-powered evaluation summary with anonymized data, OpenAI integration, scoring patterns
- F8: Smart assignment improvements (geo diversity penalty, round familiarity bonus, COI blocking)
Batch 4 - Form Flexibility & Applicant Portal:
- F9: Evaluation form flexibility (text, boolean, section_header types, conditional visibility)
- F10: Applicant portal (status timeline, per-round documents, mentor messaging)
Schema: 5 new models (ReminderLog, ConflictOfInterest, EvaluationSummary, ProjectStatusHistory, MentorMessage), ProjectFile extended with roundId + isLate.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 21:58:27 +01:00
|
|
|
* Score Breakdown:
|
2026-02-04 15:27:28 +01:00
|
|
|
* - Tag overlap: 0-40 points (weighted by confidence)
|
|
|
|
|
* - Bio match: 0-15 points (if bio exists)
|
2026-02-04 14:15:06 +01:00
|
|
|
* - Workload balance: 0-25 points
|
|
|
|
|
* - Country match: 0-15 points (mentors only)
|
Implement 10 platform features: evaluation UX, admin tools, AI summaries, applicant portal
Batch 1 - Quick Wins:
- F1: Evaluation progress indicator with touch tracking in sticky status bar
- F2: Export filtering results as CSV with dynamic AI column flattening
- F3: Observer access to analytics dashboards (8 procedures changed to observerProcedure)
Batch 2 - Jury Experience:
- F4: Countdown timer component with urgency colors + email reminder service with cron endpoint
- F5: Conflict of interest declaration system (dialog, admin management, review workflow)
Batch 3 - Admin & AI Enhancements:
- F6: Bulk status update UI with selection checkboxes, floating toolbar, status history recording
- F7: AI-powered evaluation summary with anonymized data, OpenAI integration, scoring patterns
- F8: Smart assignment improvements (geo diversity penalty, round familiarity bonus, COI blocking)
Batch 4 - Form Flexibility & Applicant Portal:
- F9: Evaluation form flexibility (text, boolean, section_header types, conditional visibility)
- F10: Applicant portal (status timeline, per-round documents, mentor messaging)
Schema: 5 new models (ReminderLog, ConflictOfInterest, EvaluationSummary, ProjectStatusHistory, MentorMessage), ProjectFile extended with roundId + isLate.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 21:58:27 +01:00
|
|
|
* - Geo diversity: -15 per excess same-country assignment (threshold: 2)
|
|
|
|
|
* - Previous round familiarity: +10 if reviewed in earlier round
|
|
|
|
|
* - COI: juror skipped entirely if conflict declared
|
2026-02-04 14:15:06 +01:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { prisma } from '@/lib/prisma'
|
|
|
|
|
|
|
|
|
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export interface ScoreBreakdown {
|
|
|
|
|
tagOverlap: number
|
2026-02-04 15:27:28 +01:00
|
|
|
bioMatch: number
|
2026-02-04 14:15:06 +01:00
|
|
|
workloadBalance: number
|
|
|
|
|
countryMatch: number
|
Implement 10 platform features: evaluation UX, admin tools, AI summaries, applicant portal
Batch 1 - Quick Wins:
- F1: Evaluation progress indicator with touch tracking in sticky status bar
- F2: Export filtering results as CSV with dynamic AI column flattening
- F3: Observer access to analytics dashboards (8 procedures changed to observerProcedure)
Batch 2 - Jury Experience:
- F4: Countdown timer component with urgency colors + email reminder service with cron endpoint
- F5: Conflict of interest declaration system (dialog, admin management, review workflow)
Batch 3 - Admin & AI Enhancements:
- F6: Bulk status update UI with selection checkboxes, floating toolbar, status history recording
- F7: AI-powered evaluation summary with anonymized data, OpenAI integration, scoring patterns
- F8: Smart assignment improvements (geo diversity penalty, round familiarity bonus, COI blocking)
Batch 4 - Form Flexibility & Applicant Portal:
- F9: Evaluation form flexibility (text, boolean, section_header types, conditional visibility)
- F10: Applicant portal (status timeline, per-round documents, mentor messaging)
Schema: 5 new models (ReminderLog, ConflictOfInterest, EvaluationSummary, ProjectStatusHistory, MentorMessage), ProjectFile extended with roundId + isLate.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 21:58:27 +01:00
|
|
|
geoDiversityPenalty: number
|
|
|
|
|
previousRoundFamiliarity: number
|
|
|
|
|
coiPenalty: number
|
2026-02-04 14:15:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface AssignmentScore {
|
|
|
|
|
userId: string
|
|
|
|
|
userName: string
|
|
|
|
|
userEmail: string
|
|
|
|
|
projectId: string
|
|
|
|
|
projectTitle: string
|
|
|
|
|
score: number
|
|
|
|
|
breakdown: ScoreBreakdown
|
|
|
|
|
reasoning: string[]
|
|
|
|
|
matchingTags: string[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ProjectTagData {
|
|
|
|
|
tagId: string
|
|
|
|
|
tagName: string
|
|
|
|
|
confidence: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
2026-02-04 15:27:28 +01:00
|
|
|
const MAX_TAG_OVERLAP_SCORE = 40
|
|
|
|
|
const MAX_BIO_MATCH_SCORE = 15
|
2026-02-04 14:15:06 +01:00
|
|
|
const MAX_WORKLOAD_SCORE = 25
|
|
|
|
|
const MAX_COUNTRY_SCORE = 15
|
2026-02-04 15:27:28 +01:00
|
|
|
const POINTS_PER_TAG_MATCH = 8
|
|
|
|
|
|
Implement 10 platform features: evaluation UX, admin tools, AI summaries, applicant portal
Batch 1 - Quick Wins:
- F1: Evaluation progress indicator with touch tracking in sticky status bar
- F2: Export filtering results as CSV with dynamic AI column flattening
- F3: Observer access to analytics dashboards (8 procedures changed to observerProcedure)
Batch 2 - Jury Experience:
- F4: Countdown timer component with urgency colors + email reminder service with cron endpoint
- F5: Conflict of interest declaration system (dialog, admin management, review workflow)
Batch 3 - Admin & AI Enhancements:
- F6: Bulk status update UI with selection checkboxes, floating toolbar, status history recording
- F7: AI-powered evaluation summary with anonymized data, OpenAI integration, scoring patterns
- F8: Smart assignment improvements (geo diversity penalty, round familiarity bonus, COI blocking)
Batch 4 - Form Flexibility & Applicant Portal:
- F9: Evaluation form flexibility (text, boolean, section_header types, conditional visibility)
- F10: Applicant portal (status timeline, per-round documents, mentor messaging)
Schema: 5 new models (ReminderLog, ConflictOfInterest, EvaluationSummary, ProjectStatusHistory, MentorMessage), ProjectFile extended with roundId + isLate.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 21:58:27 +01:00
|
|
|
// New scoring factors
|
|
|
|
|
const GEO_DIVERSITY_THRESHOLD = 2
|
|
|
|
|
const GEO_DIVERSITY_PENALTY_PER_EXCESS = -15
|
|
|
|
|
const PREVIOUS_ROUND_FAMILIARITY_BONUS = 10
|
|
|
|
|
// COI jurors are skipped entirely rather than penalized (effectively -Infinity)
|
|
|
|
|
|
2026-02-04 15:27:28 +01:00
|
|
|
// Common words to exclude from bio matching
|
|
|
|
|
const STOP_WORDS = new Set([
|
|
|
|
|
'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with',
|
|
|
|
|
'by', 'from', 'as', 'is', 'was', 'are', 'were', 'been', 'be', 'have', 'has', 'had',
|
|
|
|
|
'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'must',
|
|
|
|
|
'that', 'which', 'who', 'whom', 'this', 'these', 'those', 'it', 'its', 'i', 'we',
|
|
|
|
|
'you', 'he', 'she', 'they', 'them', 'their', 'our', 'my', 'your', 'his', 'her',
|
|
|
|
|
'am', 'about', 'into', 'through', 'during', 'before', 'after', 'above', 'below',
|
|
|
|
|
'between', 'under', 'again', 'further', 'then', 'once', 'here', 'there', 'when',
|
|
|
|
|
'where', 'why', 'how', 'all', 'each', 'few', 'more', 'most', 'other', 'some',
|
|
|
|
|
'such', 'no', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 'can',
|
|
|
|
|
'just', 'being', 'over', 'both', 'up', 'down', 'out', 'also', 'new', 'any',
|
|
|
|
|
])
|
2026-02-04 14:15:06 +01:00
|
|
|
|
|
|
|
|
// ─── Scoring Functions ───────────────────────────────────────────────────────
|
|
|
|
|
|
2026-02-04 15:27:28 +01:00
|
|
|
/**
|
|
|
|
|
* Extract meaningful keywords from text
|
|
|
|
|
*/
|
|
|
|
|
function extractKeywords(text: string | null | undefined): Set<string> {
|
|
|
|
|
if (!text) return new Set()
|
|
|
|
|
|
|
|
|
|
// Tokenize, lowercase, and filter
|
|
|
|
|
const words = text
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
.replace(/[^\w\s]/g, ' ') // Remove punctuation
|
|
|
|
|
.split(/\s+/)
|
|
|
|
|
.filter((word) => word.length >= 3 && !STOP_WORDS.has(word))
|
|
|
|
|
|
|
|
|
|
return new Set(words)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate bio match score between user bio and project description
|
|
|
|
|
* Only applies if user has a bio
|
|
|
|
|
*/
|
|
|
|
|
export function calculateBioMatchScore(
|
|
|
|
|
userBio: string | null | undefined,
|
|
|
|
|
projectDescription: string | null | undefined
|
|
|
|
|
): { score: number; matchingKeywords: string[] } {
|
|
|
|
|
// If no bio, return 0 (not penalized, just no bonus)
|
|
|
|
|
if (!userBio || userBio.trim().length === 0) {
|
|
|
|
|
return { score: 0, matchingKeywords: [] }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If no project description, can't match
|
|
|
|
|
if (!projectDescription || projectDescription.trim().length === 0) {
|
|
|
|
|
return { score: 0, matchingKeywords: [] }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const bioKeywords = extractKeywords(userBio)
|
|
|
|
|
const projectKeywords = extractKeywords(projectDescription)
|
|
|
|
|
|
|
|
|
|
if (bioKeywords.size === 0 || projectKeywords.size === 0) {
|
|
|
|
|
return { score: 0, matchingKeywords: [] }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find matching keywords
|
|
|
|
|
const matchingKeywords: string[] = []
|
|
|
|
|
for (const keyword of bioKeywords) {
|
|
|
|
|
if (projectKeywords.has(keyword)) {
|
|
|
|
|
matchingKeywords.push(keyword)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (matchingKeywords.length === 0) {
|
|
|
|
|
return { score: 0, matchingKeywords: [] }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate score based on match ratio
|
|
|
|
|
// Use Jaccard-like similarity: matches / (bio keywords + project keywords - matches)
|
|
|
|
|
const unionSize = bioKeywords.size + projectKeywords.size - matchingKeywords.length
|
|
|
|
|
const similarity = matchingKeywords.length / unionSize
|
|
|
|
|
|
|
|
|
|
// Scale to max score (15 points)
|
|
|
|
|
// A good match (20%+ overlap) should get near max
|
|
|
|
|
const score = Math.min(MAX_BIO_MATCH_SCORE, Math.round(similarity * 100))
|
|
|
|
|
|
|
|
|
|
return { score, matchingKeywords }
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 14:15:06 +01:00
|
|
|
/**
|
|
|
|
|
* Calculate tag overlap score between user expertise and project tags
|
|
|
|
|
*/
|
|
|
|
|
export function calculateTagOverlapScore(
|
|
|
|
|
userTagNames: string[],
|
|
|
|
|
projectTags: ProjectTagData[]
|
|
|
|
|
): { score: number; matchingTags: string[] } {
|
|
|
|
|
if (projectTags.length === 0 || userTagNames.length === 0) {
|
|
|
|
|
return { score: 0, matchingTags: [] }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const userTagSet = new Set(userTagNames.map((t) => t.toLowerCase()))
|
|
|
|
|
const matchingTags: string[] = []
|
|
|
|
|
let weightedScore = 0
|
|
|
|
|
|
|
|
|
|
for (const pt of projectTags) {
|
|
|
|
|
if (userTagSet.has(pt.tagName.toLowerCase())) {
|
|
|
|
|
matchingTags.push(pt.tagName)
|
|
|
|
|
// Weight by confidence - higher confidence = more points
|
|
|
|
|
weightedScore += POINTS_PER_TAG_MATCH * pt.confidence
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cap at max score
|
|
|
|
|
const score = Math.min(MAX_TAG_OVERLAP_SCORE, Math.round(weightedScore))
|
|
|
|
|
return { score, matchingTags }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate workload balance score
|
|
|
|
|
* Full points if under target, decreasing as over target
|
|
|
|
|
*/
|
|
|
|
|
export function calculateWorkloadScore(
|
|
|
|
|
currentAssignments: number,
|
|
|
|
|
targetAssignments: number,
|
|
|
|
|
maxAssignments?: number | null
|
|
|
|
|
): number {
|
|
|
|
|
// If user is at or over their personal max, return 0
|
|
|
|
|
if (maxAssignments !== null && maxAssignments !== undefined) {
|
|
|
|
|
if (currentAssignments >= maxAssignments) {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If under target, full points
|
|
|
|
|
if (currentAssignments < targetAssignments) {
|
|
|
|
|
return MAX_WORKLOAD_SCORE
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Over target - decrease score
|
|
|
|
|
const overload = currentAssignments - targetAssignments
|
|
|
|
|
return Math.max(0, MAX_WORKLOAD_SCORE - overload * 5)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate country match score (mentors only)
|
|
|
|
|
* Same country = bonus points
|
|
|
|
|
*/
|
|
|
|
|
export function calculateCountryMatchScore(
|
|
|
|
|
userCountry: string | null | undefined,
|
|
|
|
|
projectCountry: string | null | undefined
|
|
|
|
|
): number {
|
|
|
|
|
if (!userCountry || !projectCountry) {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Normalize for comparison
|
|
|
|
|
const normalizedUser = userCountry.toLowerCase().trim()
|
|
|
|
|
const normalizedProject = projectCountry.toLowerCase().trim()
|
|
|
|
|
|
|
|
|
|
if (normalizedUser === normalizedProject) {
|
|
|
|
|
return MAX_COUNTRY_SCORE
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Main Scoring Function ───────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get smart assignment suggestions for a round
|
|
|
|
|
*/
|
|
|
|
|
export async function getSmartSuggestions(options: {
|
|
|
|
|
roundId: string
|
|
|
|
|
type: 'jury' | 'mentor'
|
|
|
|
|
limit?: number
|
|
|
|
|
aiMaxPerJudge?: number
|
|
|
|
|
}): Promise<AssignmentScore[]> {
|
|
|
|
|
const { roundId, type, limit = 50, aiMaxPerJudge = 20 } = options
|
|
|
|
|
|
2026-02-04 15:27:28 +01:00
|
|
|
// Get projects in round with their tags and description
|
2026-02-04 14:15:06 +01:00
|
|
|
const projects = await prisma.project.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
roundId,
|
|
|
|
|
status: { not: 'REJECTED' },
|
|
|
|
|
},
|
2026-02-04 15:27:28 +01:00
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
title: true,
|
|
|
|
|
teamName: true,
|
|
|
|
|
description: true,
|
|
|
|
|
country: true,
|
|
|
|
|
status: true,
|
2026-02-04 14:15:06 +01:00
|
|
|
projectTags: {
|
|
|
|
|
include: { tag: true },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (projects.length === 0) {
|
|
|
|
|
return []
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 15:27:28 +01:00
|
|
|
// Get users of the appropriate role with bio for matching
|
2026-02-04 14:15:06 +01:00
|
|
|
const role = type === 'jury' ? 'JURY_MEMBER' : 'MENTOR'
|
|
|
|
|
const users = await prisma.user.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
role,
|
|
|
|
|
status: 'ACTIVE',
|
|
|
|
|
},
|
2026-02-04 15:27:28 +01:00
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
name: true,
|
|
|
|
|
email: true,
|
|
|
|
|
bio: true,
|
|
|
|
|
expertiseTags: true,
|
|
|
|
|
maxAssignments: true,
|
|
|
|
|
country: true,
|
2026-02-04 14:15:06 +01:00
|
|
|
_count: {
|
|
|
|
|
select: {
|
|
|
|
|
assignments: {
|
|
|
|
|
where: { roundId },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (users.length === 0) {
|
|
|
|
|
return []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get existing assignments to avoid duplicates
|
|
|
|
|
const existingAssignments = await prisma.assignment.findMany({
|
|
|
|
|
where: { roundId },
|
|
|
|
|
select: { userId: true, projectId: true },
|
|
|
|
|
})
|
|
|
|
|
const assignedPairs = new Set(
|
|
|
|
|
existingAssignments.map((a) => `${a.userId}:${a.projectId}`)
|
|
|
|
|
)
|
|
|
|
|
|
Implement 10 platform features: evaluation UX, admin tools, AI summaries, applicant portal
Batch 1 - Quick Wins:
- F1: Evaluation progress indicator with touch tracking in sticky status bar
- F2: Export filtering results as CSV with dynamic AI column flattening
- F3: Observer access to analytics dashboards (8 procedures changed to observerProcedure)
Batch 2 - Jury Experience:
- F4: Countdown timer component with urgency colors + email reminder service with cron endpoint
- F5: Conflict of interest declaration system (dialog, admin management, review workflow)
Batch 3 - Admin & AI Enhancements:
- F6: Bulk status update UI with selection checkboxes, floating toolbar, status history recording
- F7: AI-powered evaluation summary with anonymized data, OpenAI integration, scoring patterns
- F8: Smart assignment improvements (geo diversity penalty, round familiarity bonus, COI blocking)
Batch 4 - Form Flexibility & Applicant Portal:
- F9: Evaluation form flexibility (text, boolean, section_header types, conditional visibility)
- F10: Applicant portal (status timeline, per-round documents, mentor messaging)
Schema: 5 new models (ReminderLog, ConflictOfInterest, EvaluationSummary, ProjectStatusHistory, MentorMessage), ProjectFile extended with roundId + isLate.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 21:58:27 +01:00
|
|
|
// ── Batch-query data for new scoring factors ──────────────────────────────
|
|
|
|
|
|
|
|
|
|
// 1. Geographic diversity: per-juror country distribution for existing assignments
|
|
|
|
|
const assignmentsWithCountry = await prisma.assignment.findMany({
|
|
|
|
|
where: { roundId },
|
|
|
|
|
select: {
|
|
|
|
|
userId: true,
|
|
|
|
|
project: { select: { country: true } },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Build map: userId -> { country -> count }
|
|
|
|
|
const userCountryDistribution = new Map<string, Map<string, number>>()
|
|
|
|
|
for (const a of assignmentsWithCountry) {
|
|
|
|
|
const country = a.project.country?.toLowerCase().trim()
|
|
|
|
|
if (!country) continue
|
|
|
|
|
let countryMap = userCountryDistribution.get(a.userId)
|
|
|
|
|
if (!countryMap) {
|
|
|
|
|
countryMap = new Map()
|
|
|
|
|
userCountryDistribution.set(a.userId, countryMap)
|
|
|
|
|
}
|
|
|
|
|
countryMap.set(country, (countryMap.get(country) || 0) + 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 2. Previous round familiarity: find assignments in earlier rounds of the same program
|
|
|
|
|
const currentRound = await prisma.round.findUnique({
|
|
|
|
|
where: { id: roundId },
|
|
|
|
|
select: { programId: true, sortOrder: true },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const previousRoundAssignmentPairs = new Set<string>()
|
|
|
|
|
if (currentRound) {
|
|
|
|
|
const previousAssignments = await prisma.assignment.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
round: {
|
|
|
|
|
programId: currentRound.programId,
|
|
|
|
|
sortOrder: { lt: currentRound.sortOrder },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
select: { userId: true, projectId: true },
|
|
|
|
|
})
|
|
|
|
|
for (const pa of previousAssignments) {
|
|
|
|
|
previousRoundAssignmentPairs.add(`${pa.userId}:${pa.projectId}`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. COI declarations: all active conflicts for this round
|
|
|
|
|
const coiRecords = await prisma.conflictOfInterest.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
roundId,
|
|
|
|
|
hasConflict: true,
|
|
|
|
|
},
|
|
|
|
|
select: { userId: true, projectId: true },
|
|
|
|
|
})
|
|
|
|
|
const coiPairs = new Set(
|
|
|
|
|
coiRecords.map((c) => `${c.userId}:${c.projectId}`)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ── Calculate target assignments per user ─────────────────────────────────
|
2026-02-04 14:15:06 +01:00
|
|
|
const targetPerUser = Math.ceil(projects.length / users.length)
|
|
|
|
|
|
Implement 10 platform features: evaluation UX, admin tools, AI summaries, applicant portal
Batch 1 - Quick Wins:
- F1: Evaluation progress indicator with touch tracking in sticky status bar
- F2: Export filtering results as CSV with dynamic AI column flattening
- F3: Observer access to analytics dashboards (8 procedures changed to observerProcedure)
Batch 2 - Jury Experience:
- F4: Countdown timer component with urgency colors + email reminder service with cron endpoint
- F5: Conflict of interest declaration system (dialog, admin management, review workflow)
Batch 3 - Admin & AI Enhancements:
- F6: Bulk status update UI with selection checkboxes, floating toolbar, status history recording
- F7: AI-powered evaluation summary with anonymized data, OpenAI integration, scoring patterns
- F8: Smart assignment improvements (geo diversity penalty, round familiarity bonus, COI blocking)
Batch 4 - Form Flexibility & Applicant Portal:
- F9: Evaluation form flexibility (text, boolean, section_header types, conditional visibility)
- F10: Applicant portal (status timeline, per-round documents, mentor messaging)
Schema: 5 new models (ReminderLog, ConflictOfInterest, EvaluationSummary, ProjectStatusHistory, MentorMessage), ProjectFile extended with roundId + isLate.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 21:58:27 +01:00
|
|
|
// ── Calculate scores for all user-project pairs ───────────────────────────
|
2026-02-04 14:15:06 +01:00
|
|
|
const suggestions: AssignmentScore[] = []
|
|
|
|
|
|
|
|
|
|
for (const user of users) {
|
|
|
|
|
// Skip users at AI max (they won't appear in suggestions)
|
|
|
|
|
const currentCount = user._count.assignments
|
|
|
|
|
if (currentCount >= aiMaxPerJudge) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const project of projects) {
|
|
|
|
|
// Skip if already assigned
|
|
|
|
|
const pairKey = `${user.id}:${project.id}`
|
|
|
|
|
if (assignedPairs.has(pairKey)) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
Implement 10 platform features: evaluation UX, admin tools, AI summaries, applicant portal
Batch 1 - Quick Wins:
- F1: Evaluation progress indicator with touch tracking in sticky status bar
- F2: Export filtering results as CSV with dynamic AI column flattening
- F3: Observer access to analytics dashboards (8 procedures changed to observerProcedure)
Batch 2 - Jury Experience:
- F4: Countdown timer component with urgency colors + email reminder service with cron endpoint
- F5: Conflict of interest declaration system (dialog, admin management, review workflow)
Batch 3 - Admin & AI Enhancements:
- F6: Bulk status update UI with selection checkboxes, floating toolbar, status history recording
- F7: AI-powered evaluation summary with anonymized data, OpenAI integration, scoring patterns
- F8: Smart assignment improvements (geo diversity penalty, round familiarity bonus, COI blocking)
Batch 4 - Form Flexibility & Applicant Portal:
- F9: Evaluation form flexibility (text, boolean, section_header types, conditional visibility)
- F10: Applicant portal (status timeline, per-round documents, mentor messaging)
Schema: 5 new models (ReminderLog, ConflictOfInterest, EvaluationSummary, ProjectStatusHistory, MentorMessage), ProjectFile extended with roundId + isLate.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 21:58:27 +01:00
|
|
|
// COI check - skip juror entirely for this project if COI declared
|
|
|
|
|
if (coiPairs.has(pairKey)) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 14:15:06 +01:00
|
|
|
// Get project tags data
|
|
|
|
|
const projectTags: ProjectTagData[] = project.projectTags.map((pt) => ({
|
|
|
|
|
tagId: pt.tagId,
|
|
|
|
|
tagName: pt.tag.name,
|
|
|
|
|
confidence: pt.confidence,
|
|
|
|
|
}))
|
|
|
|
|
|
Implement 10 platform features: evaluation UX, admin tools, AI summaries, applicant portal
Batch 1 - Quick Wins:
- F1: Evaluation progress indicator with touch tracking in sticky status bar
- F2: Export filtering results as CSV with dynamic AI column flattening
- F3: Observer access to analytics dashboards (8 procedures changed to observerProcedure)
Batch 2 - Jury Experience:
- F4: Countdown timer component with urgency colors + email reminder service with cron endpoint
- F5: Conflict of interest declaration system (dialog, admin management, review workflow)
Batch 3 - Admin & AI Enhancements:
- F6: Bulk status update UI with selection checkboxes, floating toolbar, status history recording
- F7: AI-powered evaluation summary with anonymized data, OpenAI integration, scoring patterns
- F8: Smart assignment improvements (geo diversity penalty, round familiarity bonus, COI blocking)
Batch 4 - Form Flexibility & Applicant Portal:
- F9: Evaluation form flexibility (text, boolean, section_header types, conditional visibility)
- F10: Applicant portal (status timeline, per-round documents, mentor messaging)
Schema: 5 new models (ReminderLog, ConflictOfInterest, EvaluationSummary, ProjectStatusHistory, MentorMessage), ProjectFile extended with roundId + isLate.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 21:58:27 +01:00
|
|
|
// Calculate existing scores
|
2026-02-04 14:15:06 +01:00
|
|
|
const { score: tagScore, matchingTags } = calculateTagOverlapScore(
|
|
|
|
|
user.expertiseTags,
|
|
|
|
|
projectTags
|
|
|
|
|
)
|
|
|
|
|
|
2026-02-04 15:27:28 +01:00
|
|
|
const { score: bioScore, matchingKeywords } = calculateBioMatchScore(
|
|
|
|
|
user.bio,
|
|
|
|
|
project.description
|
|
|
|
|
)
|
|
|
|
|
|
2026-02-04 14:15:06 +01:00
|
|
|
const workloadScore = calculateWorkloadScore(
|
|
|
|
|
currentCount,
|
|
|
|
|
targetPerUser,
|
|
|
|
|
user.maxAssignments
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const countryScore =
|
|
|
|
|
type === 'mentor'
|
2026-02-04 15:27:28 +01:00
|
|
|
? calculateCountryMatchScore(user.country, project.country)
|
2026-02-04 14:15:06 +01:00
|
|
|
: 0
|
|
|
|
|
|
Implement 10 platform features: evaluation UX, admin tools, AI summaries, applicant portal
Batch 1 - Quick Wins:
- F1: Evaluation progress indicator with touch tracking in sticky status bar
- F2: Export filtering results as CSV with dynamic AI column flattening
- F3: Observer access to analytics dashboards (8 procedures changed to observerProcedure)
Batch 2 - Jury Experience:
- F4: Countdown timer component with urgency colors + email reminder service with cron endpoint
- F5: Conflict of interest declaration system (dialog, admin management, review workflow)
Batch 3 - Admin & AI Enhancements:
- F6: Bulk status update UI with selection checkboxes, floating toolbar, status history recording
- F7: AI-powered evaluation summary with anonymized data, OpenAI integration, scoring patterns
- F8: Smart assignment improvements (geo diversity penalty, round familiarity bonus, COI blocking)
Batch 4 - Form Flexibility & Applicant Portal:
- F9: Evaluation form flexibility (text, boolean, section_header types, conditional visibility)
- F10: Applicant portal (status timeline, per-round documents, mentor messaging)
Schema: 5 new models (ReminderLog, ConflictOfInterest, EvaluationSummary, ProjectStatusHistory, MentorMessage), ProjectFile extended with roundId + isLate.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 21:58:27 +01:00
|
|
|
// ── New scoring factors ─────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
// Geographic diversity penalty
|
|
|
|
|
let geoDiversityPenalty = 0
|
|
|
|
|
const projectCountry = project.country?.toLowerCase().trim()
|
|
|
|
|
if (projectCountry) {
|
|
|
|
|
const countryMap = userCountryDistribution.get(user.id)
|
|
|
|
|
const sameCountryCount = countryMap?.get(projectCountry) || 0
|
|
|
|
|
if (sameCountryCount >= GEO_DIVERSITY_THRESHOLD) {
|
|
|
|
|
geoDiversityPenalty =
|
|
|
|
|
GEO_DIVERSITY_PENALTY_PER_EXCESS *
|
|
|
|
|
(sameCountryCount - GEO_DIVERSITY_THRESHOLD + 1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Previous round familiarity bonus
|
|
|
|
|
let previousRoundFamiliarity = 0
|
|
|
|
|
if (previousRoundAssignmentPairs.has(pairKey)) {
|
|
|
|
|
previousRoundFamiliarity = PREVIOUS_ROUND_FAMILIARITY_BONUS
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const totalScore =
|
|
|
|
|
tagScore +
|
|
|
|
|
bioScore +
|
|
|
|
|
workloadScore +
|
|
|
|
|
countryScore +
|
|
|
|
|
geoDiversityPenalty +
|
|
|
|
|
previousRoundFamiliarity
|
2026-02-04 14:15:06 +01:00
|
|
|
|
|
|
|
|
// Build reasoning
|
|
|
|
|
const reasoning: string[] = []
|
|
|
|
|
if (matchingTags.length > 0) {
|
|
|
|
|
reasoning.push(`Expertise match: ${matchingTags.length} tag(s)`)
|
|
|
|
|
}
|
2026-02-04 15:27:28 +01:00
|
|
|
if (bioScore > 0) {
|
|
|
|
|
reasoning.push(`Bio match: ${matchingKeywords.length} keyword(s)`)
|
|
|
|
|
}
|
2026-02-04 14:15:06 +01:00
|
|
|
if (workloadScore === MAX_WORKLOAD_SCORE) {
|
|
|
|
|
reasoning.push('Available capacity')
|
|
|
|
|
} else if (workloadScore > 0) {
|
|
|
|
|
reasoning.push('Moderate workload')
|
|
|
|
|
}
|
|
|
|
|
if (countryScore > 0) {
|
|
|
|
|
reasoning.push('Same country')
|
|
|
|
|
}
|
Implement 10 platform features: evaluation UX, admin tools, AI summaries, applicant portal
Batch 1 - Quick Wins:
- F1: Evaluation progress indicator with touch tracking in sticky status bar
- F2: Export filtering results as CSV with dynamic AI column flattening
- F3: Observer access to analytics dashboards (8 procedures changed to observerProcedure)
Batch 2 - Jury Experience:
- F4: Countdown timer component with urgency colors + email reminder service with cron endpoint
- F5: Conflict of interest declaration system (dialog, admin management, review workflow)
Batch 3 - Admin & AI Enhancements:
- F6: Bulk status update UI with selection checkboxes, floating toolbar, status history recording
- F7: AI-powered evaluation summary with anonymized data, OpenAI integration, scoring patterns
- F8: Smart assignment improvements (geo diversity penalty, round familiarity bonus, COI blocking)
Batch 4 - Form Flexibility & Applicant Portal:
- F9: Evaluation form flexibility (text, boolean, section_header types, conditional visibility)
- F10: Applicant portal (status timeline, per-round documents, mentor messaging)
Schema: 5 new models (ReminderLog, ConflictOfInterest, EvaluationSummary, ProjectStatusHistory, MentorMessage), ProjectFile extended with roundId + isLate.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 21:58:27 +01:00
|
|
|
if (geoDiversityPenalty < 0) {
|
|
|
|
|
reasoning.push(`Geo diversity penalty (${geoDiversityPenalty})`)
|
|
|
|
|
}
|
|
|
|
|
if (previousRoundFamiliarity > 0) {
|
|
|
|
|
reasoning.push('Reviewed in previous round (+10)')
|
|
|
|
|
}
|
2026-02-04 14:15:06 +01:00
|
|
|
|
|
|
|
|
suggestions.push({
|
|
|
|
|
userId: user.id,
|
|
|
|
|
userName: user.name || 'Unknown',
|
|
|
|
|
userEmail: user.email,
|
|
|
|
|
projectId: project.id,
|
|
|
|
|
projectTitle: project.title,
|
|
|
|
|
score: totalScore,
|
|
|
|
|
breakdown: {
|
|
|
|
|
tagOverlap: tagScore,
|
2026-02-04 15:27:28 +01:00
|
|
|
bioMatch: bioScore,
|
2026-02-04 14:15:06 +01:00
|
|
|
workloadBalance: workloadScore,
|
|
|
|
|
countryMatch: countryScore,
|
Implement 10 platform features: evaluation UX, admin tools, AI summaries, applicant portal
Batch 1 - Quick Wins:
- F1: Evaluation progress indicator with touch tracking in sticky status bar
- F2: Export filtering results as CSV with dynamic AI column flattening
- F3: Observer access to analytics dashboards (8 procedures changed to observerProcedure)
Batch 2 - Jury Experience:
- F4: Countdown timer component with urgency colors + email reminder service with cron endpoint
- F5: Conflict of interest declaration system (dialog, admin management, review workflow)
Batch 3 - Admin & AI Enhancements:
- F6: Bulk status update UI with selection checkboxes, floating toolbar, status history recording
- F7: AI-powered evaluation summary with anonymized data, OpenAI integration, scoring patterns
- F8: Smart assignment improvements (geo diversity penalty, round familiarity bonus, COI blocking)
Batch 4 - Form Flexibility & Applicant Portal:
- F9: Evaluation form flexibility (text, boolean, section_header types, conditional visibility)
- F10: Applicant portal (status timeline, per-round documents, mentor messaging)
Schema: 5 new models (ReminderLog, ConflictOfInterest, EvaluationSummary, ProjectStatusHistory, MentorMessage), ProjectFile extended with roundId + isLate.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 21:58:27 +01:00
|
|
|
geoDiversityPenalty,
|
|
|
|
|
previousRoundFamiliarity,
|
|
|
|
|
coiPenalty: 0, // COI jurors are skipped entirely
|
2026-02-04 14:15:06 +01:00
|
|
|
},
|
|
|
|
|
reasoning,
|
|
|
|
|
matchingTags,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort by score descending and limit
|
|
|
|
|
return suggestions.sort((a, b) => b.score - a.score).slice(0, limit)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get mentor suggestions for a specific project
|
|
|
|
|
*/
|
|
|
|
|
export async function getMentorSuggestionsForProject(
|
|
|
|
|
projectId: string,
|
|
|
|
|
limit: number = 10
|
|
|
|
|
): Promise<AssignmentScore[]> {
|
|
|
|
|
const project = await prisma.project.findUnique({
|
|
|
|
|
where: { id: projectId },
|
|
|
|
|
include: {
|
|
|
|
|
projectTags: {
|
|
|
|
|
include: { tag: true },
|
|
|
|
|
},
|
|
|
|
|
mentorAssignment: true,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!project) {
|
|
|
|
|
throw new Error(`Project not found: ${projectId}`)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 15:27:28 +01:00
|
|
|
// Get all active mentors with bio for matching
|
2026-02-04 14:15:06 +01:00
|
|
|
const mentors = await prisma.user.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
role: 'MENTOR',
|
|
|
|
|
status: 'ACTIVE',
|
|
|
|
|
},
|
2026-02-04 15:27:28 +01:00
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
name: true,
|
|
|
|
|
email: true,
|
|
|
|
|
bio: true,
|
|
|
|
|
expertiseTags: true,
|
|
|
|
|
maxAssignments: true,
|
|
|
|
|
country: true,
|
2026-02-04 14:15:06 +01:00
|
|
|
_count: {
|
|
|
|
|
select: { mentorAssignments: true },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (mentors.length === 0) {
|
|
|
|
|
return []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const projectTags: ProjectTagData[] = project.projectTags.map((pt) => ({
|
|
|
|
|
tagId: pt.tagId,
|
|
|
|
|
tagName: pt.tag.name,
|
|
|
|
|
confidence: pt.confidence,
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
const targetPerMentor = 5 // Target 5 projects per mentor
|
|
|
|
|
|
|
|
|
|
const suggestions: AssignmentScore[] = []
|
|
|
|
|
|
|
|
|
|
for (const mentor of mentors) {
|
|
|
|
|
// Skip if already assigned to this project
|
|
|
|
|
if (project.mentorAssignment?.mentorId === mentor.id) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { score: tagScore, matchingTags } = calculateTagOverlapScore(
|
|
|
|
|
mentor.expertiseTags,
|
|
|
|
|
projectTags
|
|
|
|
|
)
|
|
|
|
|
|
2026-02-04 15:27:28 +01:00
|
|
|
// Bio match (only if mentor has a bio)
|
|
|
|
|
const { score: bioScore, matchingKeywords } = calculateBioMatchScore(
|
|
|
|
|
mentor.bio,
|
|
|
|
|
project.description
|
|
|
|
|
)
|
|
|
|
|
|
2026-02-04 14:15:06 +01:00
|
|
|
const workloadScore = calculateWorkloadScore(
|
|
|
|
|
mentor._count.mentorAssignments,
|
|
|
|
|
targetPerMentor,
|
|
|
|
|
mentor.maxAssignments
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const countryScore = calculateCountryMatchScore(
|
2026-02-04 15:27:28 +01:00
|
|
|
mentor.country,
|
2026-02-04 14:15:06 +01:00
|
|
|
project.country
|
|
|
|
|
)
|
|
|
|
|
|
2026-02-04 15:27:28 +01:00
|
|
|
const totalScore = tagScore + bioScore + workloadScore + countryScore
|
2026-02-04 14:15:06 +01:00
|
|
|
|
|
|
|
|
const reasoning: string[] = []
|
|
|
|
|
if (matchingTags.length > 0) {
|
|
|
|
|
reasoning.push(`${matchingTags.length} matching expertise tag(s)`)
|
|
|
|
|
}
|
2026-02-04 15:27:28 +01:00
|
|
|
if (bioScore > 0) {
|
|
|
|
|
reasoning.push(`Bio match: ${matchingKeywords.length} keyword(s)`)
|
|
|
|
|
}
|
2026-02-04 14:15:06 +01:00
|
|
|
if (countryScore > 0) {
|
|
|
|
|
reasoning.push('Same country of origin')
|
|
|
|
|
}
|
|
|
|
|
if (workloadScore === MAX_WORKLOAD_SCORE) {
|
|
|
|
|
reasoning.push('Available capacity')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
suggestions.push({
|
|
|
|
|
userId: mentor.id,
|
|
|
|
|
userName: mentor.name || 'Unknown',
|
|
|
|
|
userEmail: mentor.email,
|
|
|
|
|
projectId: project.id,
|
|
|
|
|
projectTitle: project.title,
|
|
|
|
|
score: totalScore,
|
|
|
|
|
breakdown: {
|
|
|
|
|
tagOverlap: tagScore,
|
2026-02-04 15:27:28 +01:00
|
|
|
bioMatch: bioScore,
|
2026-02-04 14:15:06 +01:00
|
|
|
workloadBalance: workloadScore,
|
|
|
|
|
countryMatch: countryScore,
|
Implement 10 platform features: evaluation UX, admin tools, AI summaries, applicant portal
Batch 1 - Quick Wins:
- F1: Evaluation progress indicator with touch tracking in sticky status bar
- F2: Export filtering results as CSV with dynamic AI column flattening
- F3: Observer access to analytics dashboards (8 procedures changed to observerProcedure)
Batch 2 - Jury Experience:
- F4: Countdown timer component with urgency colors + email reminder service with cron endpoint
- F5: Conflict of interest declaration system (dialog, admin management, review workflow)
Batch 3 - Admin & AI Enhancements:
- F6: Bulk status update UI with selection checkboxes, floating toolbar, status history recording
- F7: AI-powered evaluation summary with anonymized data, OpenAI integration, scoring patterns
- F8: Smart assignment improvements (geo diversity penalty, round familiarity bonus, COI blocking)
Batch 4 - Form Flexibility & Applicant Portal:
- F9: Evaluation form flexibility (text, boolean, section_header types, conditional visibility)
- F10: Applicant portal (status timeline, per-round documents, mentor messaging)
Schema: 5 new models (ReminderLog, ConflictOfInterest, EvaluationSummary, ProjectStatusHistory, MentorMessage), ProjectFile extended with roundId + isLate.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 21:58:27 +01:00
|
|
|
geoDiversityPenalty: 0,
|
|
|
|
|
previousRoundFamiliarity: 0,
|
|
|
|
|
coiPenalty: 0,
|
2026-02-04 14:15:06 +01:00
|
|
|
},
|
|
|
|
|
reasoning,
|
|
|
|
|
matchingTags,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return suggestions.sort((a, b) => b.score - a.score).slice(0, limit)
|
|
|
|
|
}
|