15 batcher (Opus 4.8 xhigh, read-only) verifiserte 55 sample-frame-filer mot live MS Learn (strikt v2 bevis-krav: correct/outdated/wrong krever hentet learn-sitat, ellers unsourced). 373 påstander: 259 correct, 29 outdated, 11 wrong, 74 unsourced. 40 reelle feil. lastmod_changed joinet i kode (registry sitemap_lastmod vs filens dato): 0 filer ville staleness-loopen flagget -> alle 40 feil er judge-unike (rå funn, krever tolkning i base-rate-rapporten). lib/base-rate.mjs (Wilson, lastmod, aggregering) TDD 15 tester. Suite 538/538. Steg 3 (compute-base-rate + gate) gjenstår.
133 lines
6.1 KiB
JavaScript
133 lines
6.1 KiB
JavaScript
// tests/kb-eval/test-base-rate.test.mjs
|
|
// Unit tests for the Fase 0 base-rate aggregation lib.
|
|
//
|
|
// These functions turn the per-claim subagent verdicts (gold-correctness-set)
|
|
// into a defensible base-rate report: a verifiable error rate with a Wilson 95%
|
|
// score interval, broken down per skill / stratum / claim_type, plus the gate-
|
|
// critical "errors a correctness judge would catch over the existing staleness
|
|
// loop" (= genuine errors whose cited source lastmod did NOT change since the
|
|
// file's date). All computation is deterministic.
|
|
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
parseLastUpdated,
|
|
wilson,
|
|
fileLastmodChanged,
|
|
computeBaseRate,
|
|
} from '../../scripts/kb-eval/lib/base-rate.mjs';
|
|
|
|
// --- parseLastUpdated --------------------------------------------------------
|
|
test('parseLastUpdated — full YYYY-MM-DD header', () => {
|
|
assert.equal(
|
|
parseLastUpdated('# Title\n\n**Last updated:** 2026-06-24 (research via MCP)\n'),
|
|
'2026-06-24',
|
|
);
|
|
});
|
|
test('parseLastUpdated — month-only header normalises to first of month', () => {
|
|
assert.equal(
|
|
parseLastUpdated('**Last updated:** 2026-06 | Priser verifisert juni 2026\n'),
|
|
'2026-06-01',
|
|
);
|
|
});
|
|
test('parseLastUpdated — no header returns null', () => {
|
|
assert.equal(parseLastUpdated('# Title\n\nNo date here.\n'), null);
|
|
});
|
|
|
|
// --- wilson ------------------------------------------------------------------
|
|
test('wilson — empty sample is all-zero, never NaN', () => {
|
|
const w = wilson(0, 0);
|
|
assert.equal(w.p, 0);
|
|
assert.equal(w.low, 0);
|
|
assert.equal(w.high, 0);
|
|
});
|
|
test('wilson — all successes: point estimate 1, upper bound 1, lower < 1', () => {
|
|
const w = wilson(10, 10);
|
|
assert.equal(w.p, 1);
|
|
assert.ok(Math.abs(w.high - 1) < 1e-9);
|
|
assert.ok(w.low < 1 && w.low > 0.6);
|
|
});
|
|
test('wilson — 50/100 matches known 95% interval [0.404, 0.596]', () => {
|
|
const w = wilson(50, 100);
|
|
assert.equal(w.p, 0.5);
|
|
assert.ok(Math.abs(w.low - 0.4038) < 0.01, `low=${w.low}`);
|
|
assert.ok(Math.abs(w.high - 0.5962) < 0.01, `high=${w.high}`);
|
|
});
|
|
|
|
// --- fileLastmodChanged ------------------------------------------------------
|
|
const REG = {
|
|
urls: {
|
|
'https://l/a': { sitemap_lastmod: '2026-05-01' },
|
|
'https://l/b': { sitemap_lastmod: '2024-01-01' },
|
|
},
|
|
};
|
|
test('fileLastmodChanged — a cited source changed after the file date -> true', () => {
|
|
assert.equal(fileLastmodChanged(['https://l/a', 'https://l/b'], REG, '2026-01-01'), true);
|
|
});
|
|
test('fileLastmodChanged — no cited source newer than file date -> false', () => {
|
|
assert.equal(fileLastmodChanged(['https://l/b'], REG, '2026-01-01'), false);
|
|
});
|
|
test('fileLastmodChanged — unknown file date -> null', () => {
|
|
assert.equal(fileLastmodChanged(['https://l/a'], REG, null), null);
|
|
});
|
|
test('fileLastmodChanged — cited url absent from registry is ignored', () => {
|
|
assert.equal(fileLastmodChanged(['https://l/missing'], REG, '2026-01-01'), false);
|
|
});
|
|
|
|
// --- computeBaseRate ---------------------------------------------------------
|
|
// Synthetic gold set: 10 claims across two skills and two strata.
|
|
const CLAIMS = [
|
|
// skill X, volatile
|
|
{ skill: 'x', stratum: 'volatile', claim_type: 'price', verdict: 'correct', lastmod_changed: false },
|
|
{ skill: 'x', stratum: 'volatile', claim_type: 'version', verdict: 'outdated', lastmod_changed: true }, // staleness-catchable
|
|
{ skill: 'x', stratum: 'volatile', claim_type: 'sku', verdict: 'wrong', lastmod_changed: false }, // judge-unique
|
|
{ skill: 'x', stratum: 'volatile', claim_type: 'price', verdict: 'unsourced', lastmod_changed: false },
|
|
{ skill: 'x', stratum: 'volatile', claim_type: 'status', verdict: 'outdated', lastmod_changed: false }, // judge-unique
|
|
// skill y, volatile
|
|
{ skill: 'y', stratum: 'volatile', claim_type: 'tpm', verdict: 'correct', lastmod_changed: true },
|
|
{ skill: 'y', stratum: 'volatile', claim_type: 'price', verdict: 'unsourced', lastmod_changed: null },
|
|
// skill y, control
|
|
{ skill: 'y', stratum: 'control', claim_type: 'taxonomy', verdict: 'correct', lastmod_changed: false },
|
|
{ skill: 'y', stratum: 'control', claim_type: 'taxonomy', verdict: 'correct', lastmod_changed: false },
|
|
{ skill: 'y', stratum: 'control', claim_type: 'status', verdict: 'wrong', lastmod_changed: true }, // staleness-catchable
|
|
];
|
|
|
|
test('computeBaseRate — overall verdict tally and totals', () => {
|
|
const r = computeBaseRate(CLAIMS);
|
|
assert.equal(r.overall.total, 10);
|
|
assert.equal(r.overall.byVerdict.correct, 4);
|
|
assert.equal(r.overall.byVerdict.outdated, 2);
|
|
assert.equal(r.overall.byVerdict.wrong, 2);
|
|
assert.equal(r.overall.byVerdict.unsourced, 2);
|
|
});
|
|
test('computeBaseRate — verifiable error rate excludes unsourced from denominator', () => {
|
|
const r = computeBaseRate(CLAIMS);
|
|
// errors = outdated+wrong = 4; verifiable = total - unsourced = 8
|
|
assert.equal(r.overall.errors, 4);
|
|
assert.equal(r.overall.verifiable, 8);
|
|
assert.ok(Math.abs(r.overall.errorRate - 0.5) < 1e-9);
|
|
assert.equal(r.overall.unsourced, 2);
|
|
assert.ok(Math.abs(r.overall.unsourcedRate - 0.2) < 1e-9);
|
|
// Wilson band present and brackets the point estimate
|
|
assert.ok(r.overall.errorRateWilson.low < 0.5 && r.overall.errorRateWilson.high > 0.5);
|
|
});
|
|
test('computeBaseRate — judge-unique errors = errors with lastmod_changed !== true', () => {
|
|
const r = computeBaseRate(CLAIMS);
|
|
// errors: outdated(true)=staleness, wrong(false)=judge, outdated(false)=judge, wrong(true)=staleness
|
|
assert.equal(r.overall.errorsStalenessCatchable, 2); // lastmod_changed === true
|
|
assert.equal(r.overall.errorsJudgeUnique, 2); // lastmod_changed !== true
|
|
});
|
|
test('computeBaseRate — per-stratum split (volatile vs control)', () => {
|
|
const r = computeBaseRate(CLAIMS);
|
|
assert.equal(r.byStratum.volatile.total, 7);
|
|
assert.equal(r.byStratum.volatile.errors, 3);
|
|
assert.equal(r.byStratum.control.total, 3);
|
|
assert.equal(r.byStratum.control.errors, 1);
|
|
});
|
|
test('computeBaseRate — per-skill breakdown present', () => {
|
|
const r = computeBaseRate(CLAIMS);
|
|
assert.equal(r.bySkill.x.total, 5);
|
|
assert.equal(r.bySkill.x.errors, 3);
|
|
assert.equal(r.bySkill.y.total, 5);
|
|
assert.equal(r.bySkill.y.errors, 1);
|
|
});
|