import type { Ampelfarbe, ScoringDimensionDefinition, ScoringFarbe } from "./scoring-model"; export interface LlmSubcriterionAssessment { id: string; farbe: ScoringFarbe; evidence: string; begruendung: string; confidence: number; missingReason: string; } export interface LlmDimensionAssessment { id: string; subcriteria: LlmSubcriterionAssessment[]; } export interface LlmScoringAssessment { ausschlussgruende: string[]; dimensionen: LlmDimensionAssessment[]; } export interface ScoredSubcriterion extends LlmSubcriterionAssessment { name: string; indikator: string; weight: number; score: number | null; weightedScore: number; } export interface ScoringDimension { id: string; name: string; weight: number; farbe: ScoringFarbe; score: number; weightedScore: number; scorableWeight: number; subcriteria: ScoredSubcriterion[]; } export interface ScoringResult { farbe: Ampelfarbe; gesamtScore: number; unbewertbareKriterien: number; missingDataWarnings: string[]; dimensionen: ScoringDimension[]; } const COLOR_SCORE: Record = { gruen: 100, gelb: 50, rot: 0, unbewertbar: null, }; export function normalizeScoringFarbe(value: unknown): ScoringFarbe { return value === "gruen" || value === "gelb" || value === "rot" || value === "unbewertbar" ? value : "unbewertbar"; } export function scoreToFarbe(score: number): Ampelfarbe { if (score >= 75) return "gruen"; return "gelb"; } export function calculateScoringResult( model: ScoringDimensionDefinition[], assessment: LlmScoringAssessment, ): ScoringResult { const assessmentByDimension = new Map(assessment.dimensionen.map((dimension) => [dimension.id, dimension])); let totalWeightedScore = 0; let unbewertbareKriterien = 0; const missingDataWarnings: string[] = []; const dimensionen = model.map((dimensionDefinition): ScoringDimension => { const dimensionAssessment = assessmentByDimension.get(dimensionDefinition.id); const assessmentBySubcriterion = new Map( (dimensionAssessment?.subcriteria ?? []).map((subcriterion) => [subcriterion.id, subcriterion]), ); let achievedScore = 0; let scorableWeight = 0; const subcriteria = dimensionDefinition.subcriteria.map((subcriterionDefinition): ScoredSubcriterion => { const rawAssessment = assessmentBySubcriterion.get(subcriterionDefinition.id); const farbe = normalizeScoringFarbe(rawAssessment?.farbe); const score = COLOR_SCORE[farbe]; const confidence = Number.isFinite(rawAssessment?.confidence) ? Math.max(0, Math.min(1, Number(rawAssessment?.confidence))) : 0; const missingReason = String(rawAssessment?.missingReason ?? "").trim(); const evidence = String(rawAssessment?.evidence ?? "").trim(); if (score == null) { unbewertbareKriterien += 1; if (missingReason) { missingDataWarnings.push(`${dimensionDefinition.name} / ${subcriterionDefinition.name}: ${missingReason}`); } } else { scorableWeight += subcriterionDefinition.weight; } achievedScore += ((score ?? 0) * subcriterionDefinition.weight) / 100; return { id: subcriterionDefinition.id, name: subcriterionDefinition.name, indikator: subcriterionDefinition.indikator, farbe, evidence, begruendung: String(rawAssessment?.begruendung ?? "").trim(), confidence, missingReason, weight: subcriterionDefinition.weight, score, weightedScore: 0, }; }); const roundedScorableWeight = Number(scorableWeight.toFixed(2)); const roundedDimensionScore = roundedScorableWeight ? Number(((achievedScore / roundedScorableWeight) * 100).toFixed(2)) : 0; const weightedScore = roundedScorableWeight ? (roundedDimensionScore * dimensionDefinition.weight) / 100 : 0; if (roundedScorableWeight) { totalWeightedScore += weightedScore; } const normalizedSubcriteria = subcriteria.map((subcriterion) => ({ ...subcriterion, weightedScore: subcriterion.score == null || !roundedScorableWeight ? 0 : Number(((subcriterion.score * subcriterion.weight) / roundedScorableWeight).toFixed(2)), })); return { id: dimensionDefinition.id, name: dimensionDefinition.name, weight: dimensionDefinition.weight, farbe: scoreToFarbe(roundedDimensionScore), score: roundedDimensionScore, weightedScore: Number(weightedScore.toFixed(2)), scorableWeight: roundedScorableWeight, subcriteria: normalizedSubcriteria, }; }); const scorableDimensionWeight = dimensionen .filter((dimension) => dimension.scorableWeight > 0) .reduce((sum, dimension) => sum + dimension.weight, 0); const gesamtScore = scorableDimensionWeight ? Number(((totalWeightedScore / scorableDimensionWeight) * 100).toFixed(2)) : 0; const hasWeakDimension = dimensionen.some((dimension) => dimension.scorableWeight > 0 && dimension.score < 35); const rawFarbe = scoreToFarbe(gesamtScore); const farbe = rawFarbe === "gruen" && hasWeakDimension ? "gelb" : rawFarbe; return { farbe, gesamtScore, unbewertbareKriterien, missingDataWarnings, dimensionen, }; } export function deriveTrafficLight( scoring: ScoringResult, ausschlussgruende: string[], ): Ampelfarbe { return ausschlussgruende.length ? "rot" : scoring.farbe; }