Files
syntaxbullet 99569d2cf3 initial commit
2026-05-13 17:26:13 +02:00

173 lines
5.4 KiB
TypeScript

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<ScoringFarbe, number | null> = {
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;
}