feat(measure): the comparison gate counts the 14 areas against five frameworks and the tidy denominator, and it is RED [skip-docs]
The lifted target (operator, 2026-09-27; PM order 44111111): Voyage + the
operator's way of working level with or ahead of related frameworks in all
14 areas, and every Voyage part a documented Claude Code primitive covers
removed or with a measured reason to stay.
scripts/comparison-gate.mjs reads both denominators from their source, never
from the table: the areas from the first column of the PM report's table,
the parts from git ls-files (one part per eval case). It fells the table on
missing evidence, never on a verdict: a cited file must exist inside its
root and hold the cited lines, a pinned clone must sit at its pin, a web
citation needs a verbatim quote, an absence needs zero hits and a
known-positive control. Green also requires an independent reader. The
table itself is local-only (docs/comparison.local.json).
Measured today on 7439dac: RED, 0 evidence problems; 2 of 14 areas against
all five references (Superpowers 11, Pocock 14, agent-skills 11, Cherny 10,
playbook 2); tidy 5 of 64 covered parts resolved.
23 tests in tests/scripts/comparison-gate.test.mjs; six mutants felled (no
evidence check, no line check, no reader check, denominator from the table,
no tidy completeness, no pin check). Suite 1278 -> 1301 (1299/0/2).
[skip-docs]: an operator measurement instrument like scripts/yardstick.mjs,
whose table is local-only - no user-facing behaviour changes, so README and
CLAUDE.md have nothing true to say about it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7439dac927
commit
e9a4abc34f
2 changed files with 763 additions and 0 deletions
422
tests/scripts/comparison-gate.test.mjs
Normal file
422
tests/scripts/comparison-gate.test.mjs
Normal file
|
|
@ -0,0 +1,422 @@
|
|||
// 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); }
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue