Operatørvalg (c): én målrettet, prinsipiell prompt-iterasjon på den diagnostiserte grounded-men-feil-feilmoden (eksakt-verdi-entailment). Terskel uendret; v1 frosset. Full blind v2 fan-out (15 batcher Opus 4.8 xhigh, 255 P-påstander dekket): - judge v2: recall 84.2% (32/38, PASS >=0.80, Wilson [69.6-92.6%]), presisjon 84.2% (PASS >=0.70), F1 0.842, slår staleness 0/38. - Fiksen løste målet: sku recall 37.5%->75.0%, taxonomy 66.7%->100%. - +6 ekte fangster (26->32) uten netto nye FP (6->6) => recall OG presisjon opp. Forbehold (ærlig): andre måling på samme frosne sett etter v1 (erkjent); Wilson nedre grense 69.6% < 0.80 ved n=38; én iterasjon. Gate-logikk => vei mot S3. Stoppet for operatør-beslutning (S2/S3), eskalerer ikke selv. run-judge-bakeoff.mjs: --results/--report-prefix flagg (v2 uten å klobbe v1). Suite 552/552.
158 lines
6.9 KiB
JavaScript
158 lines
6.9 KiB
JavaScript
#!/usr/bin/env node
|
|
// run-judge-bakeoff.mjs — S1 final glue: join the blind judge results back to the
|
|
// frozen gold set, grade the judge / staleness / hybrid arms, apply the
|
|
// PRE-REGISTERED gate, and write the bake-off report. All math lives in tested
|
|
// lib/judge-bakeoff.mjs; this CLI is thin wiring (same shape as compute-base-rate.mjs).
|
|
//
|
|
// The thresholds are required flags, not defaults: the gate must be locked BEFORE the
|
|
// judge fan-out runs (pre-registration), and the locked values are recorded in the
|
|
// report _meta so the decision is auditable.
|
|
//
|
|
// Usage:
|
|
// node scripts/kb-eval/run-judge-bakeoff.mjs --min-recall 0.70 --min-precision 0.60 [--write]
|
|
//
|
|
// Inputs: data/gold-correctness-set.json (answer key)
|
|
// data/judge-bakeoff-results.json ({ results: [{id, judge_verdict, ...}] })
|
|
// Outputs: data/judge-bakeoff-report.{json,md} (with --write)
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { computeBakeoff, evalPopulation } from './lib/judge-bakeoff.mjs';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const DATA = path.join(__dirname, 'data');
|
|
|
|
function flag(name) {
|
|
const i = process.argv.indexOf(name);
|
|
return i >= 0 ? process.argv[i + 1] : undefined;
|
|
}
|
|
const minRecall = Number(flag('--min-recall'));
|
|
const minPrecision = Number(flag('--min-precision'));
|
|
if (!Number.isFinite(minRecall) || !Number.isFinite(minPrecision)) {
|
|
console.error('error: --min-recall and --min-precision are required (pre-registered gate)');
|
|
process.exit(2);
|
|
}
|
|
const thresholds = { minRecall, minPrecision };
|
|
|
|
const gold = JSON.parse(fs.readFileSync(path.join(DATA, 'gold-correctness-set.json'), 'utf8'));
|
|
// --results / --report-prefix let a second iteration (v2) be graded without
|
|
// clobbering the frozen v1 artifacts. Defaults preserve the v1 file names.
|
|
const resultsPath = path.join(DATA, flag('--results') || 'judge-bakeoff-results.json');
|
|
const reportPrefix = flag('--report-prefix') || 'judge-bakeoff-report';
|
|
if (!fs.existsSync(resultsPath)) {
|
|
console.error(`error: ${resultsPath} not found — run the judge fan-out first`);
|
|
process.exit(2);
|
|
}
|
|
const judge = JSON.parse(fs.readFileSync(resultsPath, 'utf8'));
|
|
|
|
// Join judge verdicts back to gold by id.
|
|
const verdictById = new Map((judge.results || []).map((r) => [r.id, r.judge_verdict]));
|
|
const joined = gold.claims.map((c) => ({ ...c, judge_verdict: verdictById.get(c.id) }));
|
|
|
|
// Completeness check: every claim in P must have a judge verdict.
|
|
const missing = evalPopulation(joined).filter((c) => c.judge_verdict == null);
|
|
if (missing.length && !process.argv.includes('--allow-incomplete')) {
|
|
console.error(`error: ${missing.length} P-claims have no judge verdict (incomplete fan-out).`);
|
|
console.error('first few:', missing.slice(0, 5).map((c) => c.id).join(', '));
|
|
console.error('pass --allow-incomplete to grade anyway (missing claims count as not-flagged).');
|
|
process.exit(2);
|
|
}
|
|
|
|
const r = computeBakeoff(joined, thresholds);
|
|
|
|
const pct = (x) => (x == null ? 'n/a' : `${(x * 100).toFixed(1)}%`);
|
|
const num = (x) => (x == null ? 'n/a' : x.toFixed(3));
|
|
const band = (w) => (w == null ? 'n/a' : `[${pct(w.low)}, ${pct(w.high)}]`);
|
|
|
|
function armRow(name, a) {
|
|
return `| ${name} | ${a.tp} | ${a.fp} | ${a.fn} | ${a.tn} | ${pct(a.precision)} | ${pct(a.recall)} | ${band(a.recallWilson)} | ${num(a.f1)} |`;
|
|
}
|
|
function typeRow(t, a) {
|
|
return `| ${t} | ${a.positives} | ${a.tp} | ${a.fp} | ${a.fn} | ${pct(a.precision)} | ${pct(a.recall)} |`;
|
|
}
|
|
|
|
const p = r.population;
|
|
const g = r.gate;
|
|
const ss = r.sourceSilent;
|
|
const verdict = g.pass ? '✅ PASS — bygg S3' : '❌ FAIL — stopp, ikke bygg S3';
|
|
|
|
const md = `# Judge bake-off-rapport — S1 (Fase 3 de-risk)
|
|
|
|
_Generert deterministisk av \`run-judge-bakeoff.mjs\` over \`gold-correctness-set.json\` + \`judge-bakeoff-results.json\`. Tall fra testet \`lib/judge-bakeoff.mjs\`. Ikke rediger for hånd — regenerer._
|
|
|
|
**Forhåndsregistrert gate (låst FØR fan-out):** recall ≥ ${minRecall}, presisjon ≥ ${minPrecision}, OG judge-recall > staleness-recall.
|
|
|
|
## Evaluerings-populasjon (P)
|
|
|
|
Volatil stratum + fetchbare claim_types (price ekskludert) — der feilene bor; unngår «invertert leverage».
|
|
|
|
| metrikk | verdi |
|
|
|---|---|
|
|
| P totalt | ${p.total} |
|
|
| Verifiserbare (correct/outdated/wrong) | ${p.verifiable} |
|
|
| Positive (reelle feil å fange) | ${p.positives} |
|
|
| Negative (correct) | ${p.negatives} |
|
|
| Unsourced i P (kjørt, men utenfor P/R) | ${p.unsourcedInP} |
|
|
|
|
## Arm-sammenligning (detektering over de ${p.verifiable} verifiserbare)
|
|
|
|
| arm | TP | FP | FN | TN | presisjon | recall | recall Wilson 95% | F1 |
|
|
|---|---|---|---|---|---|---|---|---|
|
|
${armRow('staleness (billig baseline)', r.arms.staleness)}
|
|
${armRow('judge (per-påstand groundedness)', r.arms.judge)}
|
|
${armRow('hybrid (union)', r.arms.hybrid)}
|
|
|
|
## Judge per claim_type (verifiserbar delmengde)
|
|
|
|
| claim_type | positive | TP | FP | FN | presisjon | recall |
|
|
|---|---|---|---|---|---|---|
|
|
${Object.entries(r.byClaimType).sort((a, b) => b[1].positives - a[1].positives).map(([t, a]) => typeRow(t, a)).join('\n')}
|
|
|
|
## source_silent-diagnostikk
|
|
|
|
Judgen hentet siden men fant ikke verdien. Diagnostisk, ikke et flagg.
|
|
|
|
| signal | antall | tolkning |
|
|
|---|---|---|
|
|
| På verifiserbar feil | ${ss.onVerifiableError} | judge-bom: reell feil oversett via «kan ikke verifisere» |
|
|
| På verifiserbar correct | ${ss.onVerifiableNegative} | judge reproduserte ikke et korrekt faktum mennesket fant |
|
|
| Enig med unsourced | ${ss.agreesWithUnsourced} | judge reproduserer den uverifiserbare grensen (godt) |
|
|
| Uenig med unsourced | ${ss.disagreesWithUnsourced} | judge hevdet grunnet/ugrunnet der mennesket ikke fant kilde |
|
|
|
|
## GATE: ${verdict}
|
|
|
|
- recall ${num(r.arms.judge.recall)} ≥ ${minRecall}? **${g.recallOk ? 'ja' : 'nei'}**
|
|
- presisjon ${num(r.arms.judge.precision)} ≥ ${minPrecision}? **${g.precisionOk ? 'ja' : 'nei'}**
|
|
- slår staleness (recall ${num(r.arms.staleness.recall)})? **${g.beatsStaleness ? 'ja' : 'nei'}**
|
|
- begrunnelse: ${g.reasons.join('; ')}
|
|
`;
|
|
|
|
if (process.argv.includes('--write')) {
|
|
const jsonOut = path.join(DATA, `${reportPrefix}.json`);
|
|
const mdOut = path.join(DATA, `${reportPrefix}.md`);
|
|
fs.writeFileSync(
|
|
jsonOut,
|
|
JSON.stringify(
|
|
{
|
|
_meta: {
|
|
source: 'gold-correctness-set.json + judge-bakeoff-results.json',
|
|
thresholds,
|
|
judged: (judge.results || []).length,
|
|
},
|
|
...r,
|
|
},
|
|
null,
|
|
2,
|
|
) + '\n',
|
|
);
|
|
fs.writeFileSync(mdOut, md);
|
|
console.log(`wrote ${jsonOut}`);
|
|
console.log(`wrote ${mdOut}`);
|
|
} else {
|
|
console.log(`P=${p.total} verifiable=${p.verifiable} positives=${p.positives}`);
|
|
console.log(`judge: precision=${num(r.arms.judge.precision)} recall=${num(r.arms.judge.recall)} f1=${num(r.arms.judge.f1)}`);
|
|
console.log(`staleness: recall=${num(r.arms.staleness.recall)}`);
|
|
console.log(`GATE: ${g.pass ? 'PASS' : 'FAIL'} — ${g.reasons.join('; ')}`);
|
|
console.log('(dry run — pass --write to persist judge-bakeoff-report.json + .md)');
|
|
}
|