// tests/scripts/comparison-gate.test.mjs // The comparison gate counts the areas where Voyage + the operator's way of working is // level with or ahead of every reference framework, and fells the table when a claim // has no evidence behind it. It also counts the "tidy" denominator: every Voyage part a // documented Claude Code primitive covers, and how many of those are resolved. // // Like the yardstick's tests, these pin the MECHANICS in both directions against // throwaway fixtures. They deliberately do NOT assert that the real table is red today: // the gate is supposed to turn green, and a test that pins today's distance would fail // on the day the work is done. Nothing here reads the real table or the real source. import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, dirname } from 'node:path'; import { execFileSync } from 'node:child_process'; import { parseLoc, parseSourceAreas, listParts, evaluate, render, main, } from '../../scripts/comparison-gate.mjs'; // --- helpers --------------------------------------------------------------- const lines = (n) => `${Array.from({ length: n }, (_, i) => `line ${i + 1}`).join('\n')}\n`; const SOURCE = [ '# report', '', '## 2. Per kapabilitet', '', '| Kapabilitet | A | B |', '|---|---|---|', '| Alpha | x | y |', '| Beta | x | y |', '', '## 3. Next section', '', '| Not | an | area |', '|---|---|---|', '| Gamma | x | y |', '', ].join('\n'); function world() { const dir = mkdtempSync(join(tmpdir(), 'comparison-gate-')); const files = { 'vy/own.md': lines(5), 'pm/source.md': SOURCE, 'pm/why.md': lines(3), 'sp/ref.md': lines(10), }; for (const [rel, body] of Object.entries(files)) { const p = join(dir, rel); mkdirSync(dirname(p), { recursive: true }); writeFileSync(p, body); } return { dir, roots: { VY: join(dir, 'vy'), PM: join(dir, 'pm'), SP: join(dir, 'sp') } }; } const cleanup = (w) => rmSync(w.dir, { recursive: true, force: true }); // A table that is fully green: every area level or ahead, every cell with evidence, // an independent reader, and the one covered part resolved with a measured reason. function greenData(w) { return { schema: 'voyage-comparison/1', source: { loc: 'PM:source.md', heading: '## 2. Per kapabilitet' }, roots: w.roots, references: [{ id: 'SP', name: 'obra/superpowers' }], reader: { kind: 'uavhengig', who: 'fixture', date: '2026-10-02' }, areas: [ { id: 'alpha', title: 'Alpha', own: { evidence: [{ loc: 'VY:own.md:1-2', says: 'own side' }] }, cells: { SP: { verdict: 'likt', why: 'level', evidence: [{ loc: 'SP:ref.md:3-10', says: 'ref side' }] }, }, }, { id: 'beta', title: 'Beta', own: { evidence: [{ loc: 'VY:own.md:5', says: 'own side' }] }, cells: { SP: { verdict: 'foran', why: 'the reference has nothing here', evidence: [{ absence: { query: 'grep -rn beta .', hits: 0, known_positive: 'grep -rn line . finds 10' }, says: 'no beta in the reference', }], }, }, }, ], tidy: { primitives: [{ id: 'subagents', name: 'Subagents', doc: 'https://code.claude.com/docs/en/sub-agents', quote: 'q' }], parts: [ { path: 'commands/x.md', covered_by: ['subagents'], coverage: 'delvis', why: 'w', resolution: 'maalt-grunn', reason: { loc: 'PM:why.md:1-3', says: 'measured' }, }, { path: 'lib/y.mjs', covered_by: [], coverage: 'ikke-dekket', why: 'w', resolution: null, reason: null }, ], }, }; } const PARTS = ['commands/x.md', 'lib/y.mjs']; const run = (w, data) => evaluate(data, { parts: PARTS }); const problemText = (r) => r.problems.join('\n'); // --- parsing ----------------------------------------------------------------- test('parseLoc: root, path and a line or a line range', () => { assert.deepEqual(parseLoc('SP:skills/a/SKILL.md:38-88'), { root: 'SP', path: 'skills/a/SKILL.md', start: 38, end: 88 }); assert.deepEqual(parseLoc('VY:lib/x.mjs:7'), { root: 'VY', path: 'lib/x.mjs', start: 7, end: 7 }); assert.equal(parseLoc('lib/x.mjs:7'), null, 'a loc without a root is not evidence'); assert.equal(parseLoc('VY:lib/x.mjs'), null, 'a loc without a line is not evidence'); assert.equal(parseLoc('VY:lib/x.mjs:9-3'), null, 'a range that runs backwards is not evidence'); assert.equal(parseLoc('VY:lib/x.mjs:0'), null, 'lines count from 1'); }); test('parseSourceAreas: reads the first column of the table under the heading, and only that table', () => { assert.deepEqual(parseSourceAreas(SOURCE, '## 2. Per kapabilitet'), ['Alpha', 'Beta']); assert.equal(parseSourceAreas(SOURCE, '## 9. Missing'), null); }); test('listParts: tracked files in the scope, eval cases as one part each, nothing outside the scope', () => { const dir = mkdtempSync(join(tmpdir(), 'comparison-gate-parts-')); try { for (const rel of ['commands/a.md', 'lib/u/b.mjs', 'evals/case-1/eval.json', 'evals/case-1/fixture/x.md', 'evals/case-2/eval.json', 'docs/not-a-part.md', 'settings.json', 'untracked/z.md']) { mkdirSync(dirname(join(dir, rel)), { recursive: true }); writeFileSync(join(dir, rel), 'x\n'); } execFileSync('git', ['init', '-q'], { cwd: dir }); execFileSync('git', ['add', 'commands', 'lib', 'evals', 'docs', 'settings.json'], { cwd: dir }); assert.deepEqual(listParts(dir), ['commands/a.md', 'evals/case-1/', 'evals/case-2/', 'lib/u/b.mjs', 'settings.json']); } finally { rmSync(dir, { recursive: true, force: true }); } }); // --- the comparison ------------------------------------------------------------ test('a fully evidenced table, every area level or ahead, read by an independent reader, is green', () => { const w = world(); try { const r = run(w, greenData(w)); assert.deepEqual(r.problems, []); assert.equal(r.comparison.level_or_ahead, 2); assert.equal(r.comparison.total, 2); assert.equal(r.green, true); } finally { cleanup(w); } }); test('a cell with NO evidence fells the table, whatever its verdict says', () => { const w = world(); try { const data = greenData(w); data.areas[0].cells.SP.evidence = []; const r = run(w, data); assert.match(problemText(r), /alpha.*SP.*no evidence/); assert.equal(r.green, false); } finally { cleanup(w); } }); test('an area with no evidence on our own side fells the table', () => { const w = world(); try { const data = greenData(w); delete data.areas[1].own; const r = run(w, data); assert.match(problemText(r), /beta.*own.*no evidence/); assert.equal(r.green, false); } finally { cleanup(w); } }); test('a cited line past the end of the file is not evidence', () => { const w = world(); try { const data = greenData(w); data.areas[0].cells.SP.evidence[0].loc = 'SP:ref.md:3-11'; const r = run(w, data); assert.match(problemText(r), /SP:ref\.md:3-11.*10 lines/); assert.equal(r.green, false); } finally { cleanup(w); } }); test('a cited file that does not exist, or that escapes its root, is not evidence', () => { const w = world(); try { const data = greenData(w); data.areas[0].cells.SP.evidence[0].loc = 'SP:missing.md:1'; data.areas[0].own.evidence[0].loc = 'VY:../pm/source.md:1'; const r = run(w, data); assert.match(problemText(r), /SP:missing\.md:1.*does not exist/); assert.match(problemText(r), /VY:\.\.\/pm\/source\.md:1.*outside its root/); } finally { cleanup(w); } }); test('an absence without a known-positive control is not evidence, and neither is one with hits', () => { const w = world(); try { const data = greenData(w); delete data.areas[1].cells.SP.evidence[0].absence.known_positive; let r = run(w, data); assert.match(problemText(r), /beta.*SP.*known_positive/); const again = greenData(w); again.areas[1].cells.SP.evidence[0].absence.hits = 2; r = run(w, again); assert.match(problemText(r), /beta.*SP.*hits/); } finally { cleanup(w); } }); test('a web citation needs a verbatim quote', () => { const w = world(); try { const data = greenData(w); data.areas[0].cells.SP.evidence = [{ url: 'https://example.org/playbook#review', says: 'x' }]; let r = run(w, data); assert.match(problemText(r), /alpha.*SP.*quote/); data.areas[0].cells.SP.evidence = [{ url: 'https://example.org/playbook#review', quote: 'every change is reviewed', says: 'x' }]; r = run(w, data); assert.deepEqual(r.problems, []); } finally { cleanup(w); } }); test('the denominator comes from the SOURCE: an area the source lists but the table drops fells it', () => { const w = world(); try { const data = greenData(w); data.areas.pop(); const r = run(w, data); assert.match(problemText(r), /Beta.*missing from the table/); assert.equal(r.comparison.total, 2, 'the total is the source count, not the table count'); assert.equal(r.green, false); } finally { cleanup(w); } }); test('an area the source does not list is not counted and is reported', () => { const w = world(); try { const data = greenData(w); data.areas.push({ ...data.areas[0], id: 'gamma', title: 'Gamma' }); const r = run(w, data); assert.match(problemText(r), /Gamma.*not in the source/); assert.equal(r.comparison.total, 2); } finally { cleanup(w); } }); test('a missing reference cell fells the table; an unknown verdict fells it', () => { const w = world(); try { const data = greenData(w); data.references.push({ id: 'PS', name: 'mattpocock/skills' }); let r = run(w, data); assert.match(problemText(r), /alpha.*PS.*missing/); const again = greenData(w); again.areas[0].cells.SP.verdict = 'probably fine'; r = run(w, again); assert.match(problemText(r), /alpha.*SP.*verdict/); } finally { cleanup(w); } }); test('one reference ahead of us in one area: that area is not counted, and it is named', () => { const w = world(); try { const data = greenData(w); data.areas[0].cells.SP.verdict = 'bak'; const r = run(w, data); assert.deepEqual(r.problems, []); assert.equal(r.comparison.level_or_ahead, 1); assert.deepEqual(r.comparison.behind, [{ area: 'alpha', references: ['SP'] }]); assert.equal(r.green, false); } finally { cleanup(w); } }); test('the count per reference shows which framework pulls the total down', () => { const w = world(); try { const data = greenData(w); data.references.push({ id: 'PS', name: 'mattpocock/skills' }); for (const a of data.areas) a.cells.PS = { ...a.cells.SP }; data.areas[0].cells.PS = { ...data.areas[0].cells.SP, verdict: 'bak' }; const r = run(w, data); assert.deepEqual(r.problems, []); assert.deepEqual(r.comparison.per_reference, { SP: 2, PS: 1 }); assert.equal(r.comparison.level_or_ahead, 1); assert.match(render(r), /PS: 1 of 2/); } finally { cleanup(w); } }); test('14 of 14 is not green until an INDEPENDENT reader judged it', () => { const w = world(); try { const data = greenData(w); data.reader.kind = 'pm-rapport'; const r = run(w, data); assert.deepEqual(r.problems, []); assert.equal(r.comparison.level_or_ahead, 2); assert.equal(r.green, false); assert.match(r.reasons.join('\n'), /independent reader/); } finally { cleanup(w); } }); test('a pinned reference whose clone has moved is not evidence for the pinned revision', () => { const w = world(); try { execFileSync('git', ['init', '-q'], { cwd: w.roots.SP }); execFileSync('git', ['add', '.'], { cwd: w.roots.SP }); execFileSync('git', ['-c', 'user.name=t', '-c', 'user.email=t@t', 'commit', '-qm', 'x'], { cwd: w.roots.SP }); const data = greenData(w); data.references[0].pin = '0000000'; const r = run(w, data); assert.match(problemText(r), /SP.*pinned 0000000.*HEAD/); data.references[0].pin = execFileSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: w.roots.SP, encoding: 'utf-8' }).trim(); assert.deepEqual(run(w, data).problems, []); } finally { cleanup(w); } }); // --- tidy ------------------------------------------------------------------------ test('tidy: a tracked part the list leaves out fells it; the denominator is the tree, not the list', () => { const w = world(); try { const r = evaluate(greenData(w), { parts: [...PARTS, 'agents/new.md'] }); assert.match(problemText(r), /agents\/new\.md.*missing from the tidy list/); assert.equal(r.tidy.parts, 3); assert.equal(r.green, false); } finally { cleanup(w); } }); test('tidy: a covered part counts as resolved only with a measured reason or as removed from the tree', () => { const w = world(); try { const data = greenData(w); data.tidy.parts[0].resolution = 'aapen'; data.tidy.parts[0].reason = null; let r = run(w, data); assert.deepEqual(r.problems, []); assert.equal(r.tidy.covered, 1); assert.equal(r.tidy.resolved, 0); assert.equal(r.green, false); const noReason = greenData(w); noReason.tidy.parts[0].reason = null; r = run(w, noReason); assert.match(problemText(r), /commands\/x\.md.*maalt-grunn.*reason/); const removed = greenData(w); removed.tidy.parts[0] = { ...removed.tidy.parts[0], resolution: 'fjernet', reason: null, commit: 'abc1234' }; r = run(w, removed); assert.match(problemText(r), /commands\/x\.md.*fjernet.*still in the tree/); r = evaluate(removed, { parts: ['lib/y.mjs'] }); assert.deepEqual(r.problems, []); assert.equal(r.tidy.resolved, 1); } finally { cleanup(w); } }); test('tidy: a primitive without a documentation URL, or a part covered by an unknown primitive, fells it', () => { const w = world(); try { const data = greenData(w); data.tidy.primitives[0].doc = 'somewhere'; data.tidy.parts[0].covered_by = ['subagents', 'nonexistent']; const r = run(w, data); assert.match(problemText(r), /subagents.*documentation URL/); assert.match(problemText(r), /commands\/x\.md.*nonexistent/); } finally { cleanup(w); } }); // --- output and CLI ------------------------------------------------------------------ test('render names the count, the behind areas and the reader', () => { const w = world(); try { const data = greenData(w); data.areas[0].cells.SP.verdict = 'bak'; data.reader.kind = 'pm-rapport'; const text = render(run(w, data)); assert.match(text, /1 of 2/); assert.match(text, /alpha \(SP\)/); assert.match(text, /pm-rapport/); assert.match(text, /RED/); } finally { cleanup(w); } }); test('main: a missing table is NOT MEASURED and exits 1, never 0; an unknown flag exits 2', () => { const w = world(); try { assert.equal(main(['--data', join(w.dir, 'nope.json'), '--json']), 1); assert.equal(main(['--nope']), 2); } finally { cleanup(w); } }); test('main: exit 0 only for a green table, 1 for a red one', () => { const w = world(); try { const file = join(w.dir, 'table.json'); writeFileSync(file, JSON.stringify(greenData(w))); // --parts-from pins the denominator to a fixture repo instead of the real tree const repo = join(w.dir, 'repo'); for (const rel of PARTS) { mkdirSync(dirname(join(repo, rel)), { recursive: true }); writeFileSync(join(repo, rel), 'x\n'); } execFileSync('git', ['init', '-q'], { cwd: repo }); execFileSync('git', ['add', '.'], { cwd: repo }); assert.equal(main(['--data', file, '--parts-from', repo, '--json']), 0); const red = greenData(w); red.areas[0].cells.SP.evidence = []; writeFileSync(file, JSON.stringify(red)); assert.equal(main(['--data', file, '--parts-from', repo, '--json']), 1); } finally { cleanup(w); } });