feat(eval): SKAL-1·4b offline gold-scored output eval
Scores committed agent-run fixtures against the golden corpus at (file, rule_key) granularity, building on the deterministic coordinator contract (4a). Offline: committed reviewer payloads, no live agent spawn, no LLM, no network (the LLM-in-the-loop grading is the separate 4c tier). - lib/review/gold-scorer.mjs: scoreFindings (precision/recall/f1 at (file,rule_key) granularity, line+severity ignored) + scoreVerdict; pure, with documented vacuous-set conventions. - tests/fixtures/bakeoff-rich/runs/run-perfect.json: committed run that reproduces all 5 seeded gold findings through runContract. - tests/lib/gold-eval.test.mjs: the scoring RUN (precision/recall/f1 = 1.0, verdict == expected_verdict BLOCK, nothing suppressed/skipped). - lib/util/test-census.mjs: third census category (goldEval) — a scoring run is neither behavior coverage nor a doc-pin; honest-count invariant now 3-way. - docs/eval-corpus/README.md: 4b moved from Future hardening to implemented. Suite 809 -> 822 (820/0/2). gold-scorer covers TP+FP+FN+degenerate paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJQYC5vpkJWxndS55vQQZ6
This commit is contained in:
parent
da418e653d
commit
440594f1b2
7 changed files with 255 additions and 19 deletions
BIN
lib/review/gold-scorer.mjs
Normal file
BIN
lib/review/gold-scorer.mjs
Normal file
Binary file not shown.
|
|
@ -1,10 +1,16 @@
|
|||
// lib/util/test-census.mjs
|
||||
// Census of the test suite: split top-level test() declarations into
|
||||
// "behavior" tests vs "doc-consistency pins" (string/existence assertions that
|
||||
// pin documentation against a source-of-truth). Makes the cited test count
|
||||
// honest — a prose-pin is not the same coverage as a behavior test, so a single
|
||||
// Census of the test suite: split top-level test() declarations into three
|
||||
// honest categories:
|
||||
// - "behavior" — ordinary behavior coverage (the default bucket).
|
||||
// - "docPins" — doc-consistency pins (string/existence assertions that pin
|
||||
// documentation against a source-of-truth).
|
||||
// - "goldEval" — the offline gold-scored output eval scoring RUN (SKAL-1·4b):
|
||||
// neither behavior nor prose-pin, but a run that scores a
|
||||
// committed agent-run fixture against the golden corpus.
|
||||
// Makes the cited test count honest — a prose-pin is not the same coverage as a
|
||||
// behavior test, and a scoring run is a third thing again, so a single
|
||||
// conflated total oversells behavior coverage.
|
||||
// Devil's-advocate audit §Top changes #8 (S19).
|
||||
// Devil's-advocate audit §Top changes #8 (S19); third bucket added in SKAL-1·4b.
|
||||
//
|
||||
// Metric: top-level `test(` declarations (static, deterministic, in-process).
|
||||
// This is distinct from node:test's runtime total, which additionally counts
|
||||
|
|
@ -18,6 +24,11 @@ import { join } from 'node:path';
|
|||
// is bucketed correctly without editing this module.
|
||||
export const PIN_FILE_RE = /(doc-consistency|prose-pins)\.test\.mjs$/;
|
||||
|
||||
// Files whose tests are the offline gold-scored output eval scoring run
|
||||
// (SKAL-1·4b). Kept as a regex (not a single filename) so a future split-out
|
||||
// is bucketed correctly without editing this module.
|
||||
export const GOLD_EVAL_FILE_RE = /gold-eval\.test\.mjs$/;
|
||||
|
||||
function walk(dir) {
|
||||
const out = [];
|
||||
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
||||
|
|
@ -32,18 +43,20 @@ function countTests(file) {
|
|||
return (readFileSync(file, 'utf-8').match(/^\s*test\(/gm) || []).length;
|
||||
}
|
||||
|
||||
// Returns { behavior, docPins, total, byFile } for all *.test.mjs under
|
||||
// testsRoot. behavior + docPins === total by construction.
|
||||
// Returns { behavior, docPins, goldEval, total, byFile } for all *.test.mjs
|
||||
// under testsRoot. behavior + docPins + goldEval === total by construction.
|
||||
export function censusTests(testsRoot) {
|
||||
const files = walk(testsRoot).sort();
|
||||
const byFile = {};
|
||||
let behavior = 0;
|
||||
let docPins = 0;
|
||||
let goldEval = 0;
|
||||
for (const f of files) {
|
||||
const n = countTests(f);
|
||||
byFile[f] = n;
|
||||
if (PIN_FILE_RE.test(f)) docPins += n;
|
||||
else if (GOLD_EVAL_FILE_RE.test(f)) goldEval += n;
|
||||
else behavior += n;
|
||||
}
|
||||
return { behavior, docPins, total: behavior + docPins, byFile };
|
||||
return { behavior, docPins, goldEval, total: behavior + docPins + goldEval, byFile };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue