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

- 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:
2026-03-02 11:24:14 +01:00
parent c6ebd169dd
commit 19b58e4434
6 changed files with 674 additions and 107 deletions

View File

@@ -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',