feat: weighted criteria in AI ranking, z-score normalization, threshold advancement, CSV export
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m16s
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m16s
- Add criteriaWeights to EvaluationConfig for per-criterion weight assignment (0-10) - Rewrite ai-ranking service: fetch eval form criteria, compute per-criterion averages, z-score normalize juror scores to correct grading bias, send weighted criteria to AI - Update AI prompts with criteria_definitions and per-project criteria_scores - compositeScore uses weighted criteria when configured, falls back to globalScore - Add collapsible ranking config section to dashboard (criteria text + weight sliders) - Move rankingCriteria textarea from eval config tab to ranking dashboard - Store criteriaWeights in ranking snapshot parsedRulesJson for audit - Enhance projectScores CSV export with per-criterion averages, category, country - Add Export CSV button to ranking dashboard header - Add threshold-based advancement mode (decimal score threshold, e.g. 6.5) alongside existing top-N mode in advance dialog Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -98,20 +98,36 @@ export const exportRouter = router({
|
||||
}),
|
||||
|
||||
/**
|
||||
* Export project scores summary
|
||||
* Export project scores summary with per-criterion averages
|
||||
*/
|
||||
projectScores: adminProcedure
|
||||
.input(z.object({ roundId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
// Fetch evaluation form to get criteria labels
|
||||
const evalForm = await ctx.prisma.evaluationForm.findFirst({
|
||||
where: { roundId: input.roundId, isActive: true },
|
||||
select: { criteriaJson: true },
|
||||
})
|
||||
const criteria = (evalForm?.criteriaJson as Array<{
|
||||
id: string; label: string; type?: string
|
||||
}> | null) ?? []
|
||||
const numericCriteria = criteria.filter((c) => !c.type || c.type === 'numeric')
|
||||
|
||||
const projects = await ctx.prisma.project.findMany({
|
||||
where: {
|
||||
assignments: { some: { roundId: input.roundId } },
|
||||
},
|
||||
include: {
|
||||
assignments: {
|
||||
where: { roundId: input.roundId },
|
||||
include: {
|
||||
evaluation: {
|
||||
where: { status: 'SUBMITTED' },
|
||||
select: {
|
||||
globalScore: true,
|
||||
binaryDecision: true,
|
||||
criterionScoresJson: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -132,9 +148,24 @@ export const exportRouter = router({
|
||||
(e) => e?.binaryDecision === true
|
||||
).length
|
||||
|
||||
// Per-criterion averages
|
||||
const criterionAvgs: Record<string, string | null> = {}
|
||||
for (const c of numericCriteria) {
|
||||
const values: number[] = []
|
||||
for (const e of evaluations) {
|
||||
const scores = e?.criterionScoresJson as Record<string, number> | null
|
||||
if (scores && typeof scores[c.id] === 'number') values.push(scores[c.id])
|
||||
}
|
||||
criterionAvgs[c.label] = values.length > 0
|
||||
? (values.reduce((a, b) => a + b, 0) / values.length).toFixed(2)
|
||||
: null
|
||||
}
|
||||
|
||||
return {
|
||||
title: p.title,
|
||||
teamName: p.teamName,
|
||||
category: p.competitionCategory ?? '',
|
||||
country: p.country ?? '',
|
||||
status: p.status,
|
||||
tags: p.tags.join(', '),
|
||||
totalEvaluations: evaluations.length,
|
||||
@@ -146,6 +177,7 @@ export const exportRouter = router({
|
||||
: null,
|
||||
minScore: globalScores.length > 0 ? Math.min(...globalScores) : null,
|
||||
maxScore: globalScores.length > 0 ? Math.max(...globalScores) : null,
|
||||
...criterionAvgs,
|
||||
yesVotes,
|
||||
noVotes: evaluations.length - yesVotes,
|
||||
yesPercentage:
|
||||
@@ -171,12 +203,15 @@ export const exportRouter = router({
|
||||
columns: [
|
||||
'title',
|
||||
'teamName',
|
||||
'category',
|
||||
'country',
|
||||
'status',
|
||||
'tags',
|
||||
'totalEvaluations',
|
||||
'averageScore',
|
||||
'minScore',
|
||||
'maxScore',
|
||||
...numericCriteria.map((c) => c.label),
|
||||
'yesVotes',
|
||||
'noVotes',
|
||||
'yesPercentage',
|
||||
|
||||
@@ -85,7 +85,16 @@ export const rankingRouter = router({
|
||||
fetchAndRankCategory('BUSINESS_CONCEPT', rules, input.roundId, ctx.prisma, ctx.user.id),
|
||||
])
|
||||
|
||||
// Persist snapshot
|
||||
// Read criteria weights for snapshot audit trail
|
||||
const round = await ctx.prisma.round.findUniqueOrThrow({
|
||||
where: { id: input.roundId },
|
||||
select: { configJson: true },
|
||||
})
|
||||
const evalConfig = (round.configJson as EvaluationConfig | null) ?? ({} as EvaluationConfig)
|
||||
const criteriaWeights = evalConfig.criteriaWeights ?? undefined
|
||||
|
||||
// Persist snapshot — embed weights alongside rules for audit
|
||||
const parsedRulesWithWeights = { rules, weights: criteriaWeights } as unknown as Prisma.InputJsonValue
|
||||
const snapshot = await ctx.prisma.rankingSnapshot.create({
|
||||
data: {
|
||||
roundId: input.roundId,
|
||||
@@ -94,7 +103,7 @@ export const rankingRouter = router({
|
||||
mode: 'CONFIRMED',
|
||||
status: 'COMPLETED',
|
||||
criteriaText: input.criteriaText,
|
||||
parsedRulesJson: rules as unknown as Prisma.InputJsonValue,
|
||||
parsedRulesJson: parsedRulesWithWeights,
|
||||
startupRankingJson: startup.rankedProjects as unknown as Prisma.InputJsonValue,
|
||||
conceptRankingJson: concept.rankedProjects as unknown as Prisma.InputJsonValue,
|
||||
},
|
||||
@@ -271,13 +280,16 @@ export const rankingRouter = router({
|
||||
|
||||
const result = await aiQuickRank(criteriaText, roundId, ctx.prisma, ctx.user.id)
|
||||
|
||||
// Embed weights alongside rules for audit
|
||||
const criteriaWeights = config.criteriaWeights ?? undefined
|
||||
const parsedRulesWithWeights = { rules: result.parsedRules, weights: criteriaWeights } as unknown as Prisma.InputJsonValue
|
||||
const snapshot = await ctx.prisma.rankingSnapshot.create({
|
||||
data: {
|
||||
roundId,
|
||||
triggeredById: ctx.user.id,
|
||||
triggerType: 'MANUAL',
|
||||
criteriaText,
|
||||
parsedRulesJson: result.parsedRules as unknown as Prisma.InputJsonValue,
|
||||
parsedRulesJson: parsedRulesWithWeights,
|
||||
startupRankingJson: result.startup.rankedProjects as unknown as Prisma.InputJsonValue,
|
||||
conceptRankingJson: result.concept.rankedProjects as unknown as Prisma.InputJsonValue,
|
||||
mode: 'QUICK',
|
||||
|
||||
Reference in New Issue
Block a user