Files

87 lines
3.8 KiB
JavaScript

/**
* Die harten Grenzen der Modellwahl, an einer Stelle und erzwingbar.
*
* Befund 6 des unabhaengigen Audits vom 2026-08-29: `docs/adr/0001-modellwahl.md`
* spricht von "harten Grenzen 0.6/0.9", und die standen bis dahin ausschliesslich
* im eingefrorenen Abnahmetest (tests/scaffold/w5b.test.mjs). Ein Abnahmetest
* liest eine aufgezeichnete Datei; er kann einen Lauf nicht anhalten. `npm run
* eval:llm` rankte darum sechs Modelle, kroente einen Sieger je Klasse und endete
* mit 0, auch wenn dieser Sieger die Grenzen gerissen haette. Die Grenzen waren
* damit eine Behauptung im ADR und kein Gate.
*
* Diese Datei ist die eine Quelle. Sie wird von drei Stellen gelesen:
*
* - `tools/eval-llm.mjs` schreibt sie als `bounds` in verification/w5/eval.json
* und bricht ab, wenn ein Klassensieger sie reisst,
* - `tools/eval-check.mjs` prueft die zwei aufgezeichneten Sieger erneut gegen
* dieselben Zahlen,
* - `tools/lib/eval-bounds.test.mjs` beweist die Regel an erfundenen
* Ergebnissen, ohne ein Modell zu laden.
*
* ## Warum es zwei Zahlen sind und nicht eine
*
* Die Trefferquote sagt, wie oft die Antwort stimmt; die Zitattreue sagt, ob man
* es nachsehen kann. Ein Modell mit hoher Trefferquote und schlechter Zitattreue
* hat fuer dieses Produkt nichts geleistet: seine richtigen Antworten sind von
* seinen falschen nicht zu unterscheiden. Darum ist die Zitattreue die haertere
* Schranke (0.9 gegen 0.6), und darum sind es zwei Bedingungen und nicht ein
* gemittelter Punktwert, in dem eine schlechte Haelfte von einer guten
* ausgeglichen werden koennte.
*/
/**
* Die Grenzen, wortgleich mit ADR 0001 und mit tests/scaffold/w5b.test.mjs.
*
* `source` steht mit im Objekt, weil es in eval.json landet: eine Zahl in einem
* Beweisartefakt, deren Herkunft man nicht nachsehen kann, ist eine Zahl, die
* beim naechsten Lauf jemand anpasst.
*/
export const EVAL_BOUNDS = Object.freeze({
passRate: 0.6,
citationCompliance: 0.9,
appliesTo: 'der Sieger JEDER Modellklasse, nicht das Feld im Schnitt',
source: 'docs/adr/0001-modellwahl.md (Evaluation limits), '
+ 'tests/scaffold/w5b.test.mjs',
});
/**
* Ob ein einzelnes Ergebnis die Grenzen haelt.
*
* Fehlende Zahlen gelten als Verletzung und nicht als "unbekannt": ein Lauf, der
* keinen Wert gemessen hat, hat auch keinen bestanden.
*/
export function violationsOf(label, result) {
const found = [];
const passRate = Number(result?.passRate);
const citation = Number(result?.citationCompliance);
if (!Number.isFinite(passRate) || passRate < EVAL_BOUNDS.passRate) {
found.push(`${label}: passRate ${Number.isFinite(passRate) ? passRate : 'nicht gemessen'} `
+ `unter der Grenze ${EVAL_BOUNDS.passRate}`);
}
if (!Number.isFinite(citation) || citation < EVAL_BOUNDS.citationCompliance) {
found.push(`${label}: Zitattreue ${Number.isFinite(citation) ? citation : 'nicht gemessen'} `
+ `unter der Grenze ${EVAL_BOUNDS.citationCompliance}`);
}
return found;
}
/**
* Die Verletzungen aller Klassensieger, als flache Liste von Saetzen.
*
* `winners` ist ein Objekt Klasse -> Ergebnis, so wie es in eval.json steht.
* Eine Klasse ohne Sieger ist selbst eine Verletzung: der Lauf hat dann fuer
* eine der beiden Budgetklassen kein Modell gefunden, und ein Release, das eine
* Klasse still weglaesst, waere ein Release mit einer halben Modellwahl.
*/
export function boundViolations(winners) {
const found = [];
for (const [modelClass, winner] of Object.entries(winners)) {
if (winner === undefined || winner === null) {
found.push(`Klasse ${modelClass}: kein Sieger ermittelt`);
continue;
}
found.push(...violationsOf(`Klasse ${modelClass} (${winner.name ?? 'ohne Namen'})`, winner));
}
return found;
}