feat(measure): the yardstick prints Voyage's three numbers with denominators from the source, and it is RED

One command, `node scripts/yardstick.mjs`, prints use, trust and
interrupts. Each row gives countable YES/NO, the value, the denominator and
the source it was read from. A fourth row asks whether the end-state gate can
be fooled by a mutant that keeps the module's signature. Exit is non-zero
while any number is not countable, the mutant is not felled, or the verdict
over the first five deliveries is not judged. Nothing counts a delivery yet,
so today it is RED for the right reasons:

  use         NO   no order in the queue carries work-class
  trust       YES  current ALLOW streak 0 of 37 (BLOCK 14, WARN 12, ALLOW 11,
                   last 2026-09-05 BLOCK, ordered by ts, read as JSON)
  interrupts  NO   0 user_input records of ~141k lines in 5 files, 0 emitter
                   call sites outside lib/stats/ and tests/
  mutants     0 of 1 signature-preserving mutants felled: on M9 (every export
              returns {status:'PASSED'}, every named test only checks typeof)
              the gate closes both D-03 and D-04

It only measures. The gate, the emitter and the order writer are untouched.
The mutant row is meant to stay red until the gate learns to fell M9.

Choices, and why:
- Use counts over the order queue (pending, archive/ and claimed/), because
  an order is the unit of work. The denominator is orders dated on or after a
  cutoff (default 2026-09-21) that carry `work-class: new`. They count as
  having gone through Voyage when a Voyage run in the stats has the order's
  `slug:`. Both fields are the contract the order writer must meet. No order
  carries them today, so the number is NOT COUNTABLE, not 0 %.
- Mailboxes whose names start with a dot are mailboxes too. A shell `*`
  glob skips them, which is why a hand count of `coord/*/orders` comes out
  lower. The row names them so a re-measure can reconcile.
- M9 must be LIVE before its row can count. Its typeof tests have to pass on
  the M9 tree, checked by a runner independent of the gate, so a broken
  fixture can never read as "felled".

Also: tests/fixtures/red-first/02243c6-always-allow-shim.mjs, the shim that
makes the red-first claim of 02243c6 reproducible (6 of 55). The recipe is in
its header.

Red first: the new test file failed to load before scripts/yardstick.mjs
existed, and a dot-mailbox test was red before the census learned about
dot-mailboxes. Suite 1161 -> 1183 (1181/0/2), also green on a clean tree
exported from the index.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-22 21:50:06 +02:00
commit 1779411b49
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
3 changed files with 907 additions and 0 deletions

472
scripts/yardstick.mjs Normal file
View file

@ -0,0 +1,472 @@
#!/usr/bin/env node
// scripts/yardstick.mjs
// The yardstick - Voyage's three numbers, each with a denominator read from its SOURCE:
//
// use new work that went through Voyage (the order queue: orders dated on or after
// the cutoff that carry `work-class: new`;
// counted = those with a Voyage run whose
// slug equals the order's `slug:`)
// trust reviews in a row with nothing that (trekreview-stats.jsonl, read as JSON and
// stops a release ordered by `ts` - file order is not
// chronological; value = the current ALLOW streak)
// interrupts operator interrupts per delivery (every *-stats.jsonl: `user_input` records
// that carry a slug or run id)
//
// plus a fourth row that asks whether the end-state gate can itself be fooled:
//
// mutants signature-preserving mutants the (M9: every export of the module a behaviour
// gate fells probe binds keeps its name and returns
// {status:'PASSED'}; every named test only
// checks `typeof`. The gate fells it when it
// refuses to close the probe on that tree.)
//
// A number the source cannot give is NOT COUNTABLE - never 0. The verdict over the first five
// deliveries is not judged by anything yet, and says so. Exit 0 = every number countable, every
// mutant felled and the verdict judged and met; 1 = anything short of that; 2 = usage error.
// It only READS: the queue, the stats and the repo. The mutant is built in a temp directory.
//
// Usage: node scripts/yardstick.mjs [--json] [--root <repo>] [--coord <dir>] [--data <dir>]
// [--cutoff YYYY-MM-DD]
import {
readFileSync, writeFileSync, existsSync, readdirSync, statSync, mkdirSync, mkdtempSync,
realpathSync, rmSync,
} from 'node:fs';
import { homedir, tmpdir } from 'node:os';
import { join, resolve, dirname, relative, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
import { evaluateCheck, loadRegistry, TEST_TIMEOUT_MS } from './end-state-gate.mjs';
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
export const DEFAULT_CUTOFF = '2026-09-21';
export const DEFAULT_COORD = join(homedir(), '.claude', 'coord');
export const DEFAULT_DATA = process.env.CLAUDE_PLUGIN_DATA
|| join(homedir(), '.claude', 'plugins', 'data', 'voyage-ktg-plugin-marketplace');
const REVIEW_FILE = 'trekreview-stats.jsonl';
const toPosix = (p) => p.split(sep).join('/');
function walk(dir) {
const out = [];
for (const name of readdirSync(dir)) {
const p = join(dir, name);
if (statSync(p).isDirectory()) out.push(...walk(p));
else out.push(p);
}
return out;
}
// --- the stats files -----------------------------------------------------------
/**
* Every `*-stats.jsonl` in the data directory, parsed line by line. A blank line
* is not a line; a line that is not JSON is a line, and is reported by number.
*/
export function loadStats(dataDir) {
if (!existsSync(dataDir) || !statSync(dataDir).isDirectory()) {
return { dir: dataDir, missing: true, files: [] };
}
const files = readdirSync(dataDir)
.filter((n) => n.endsWith('-stats.jsonl'))
.sort()
.map((name) => {
const path = join(dataDir, name);
const records = [];
const invalid = [];
let lines = 0;
readFileSync(path, 'utf8').split('\n').forEach((line, i) => {
if (line.trim() === '') return;
lines++;
try {
records.push(JSON.parse(line));
} catch {
invalid.push(i + 1);
}
});
return { name, path, lines, records, invalid };
});
return { dir: dataDir, missing: false, files };
}
// The slugs of every Voyage run the stats know about.
function voyageSlugs(stats) {
const slugs = new Set();
for (const f of stats.files) {
for (const r of f.records) {
if (r && typeof r.slug === 'string' && r.slug !== '') slugs.add(r.slug);
}
}
return slugs;
}
const notCountable = (detail, extra = {}) => ({ countable: false, value: null, detail, ...extra });
// --- 2 · TRUST -------------------------------------------------------------------
export function measureTrust(stats) {
const base = { id: 'trust', label: 'reviews in a row with nothing that stops a release' };
const file = stats.files.find((f) => f.name === REVIEW_FILE);
if (!file) {
return { ...base, source: join(stats.dir, REVIEW_FILE), denominator: 0, facts: {},
...notCountable(stats.missing ? `data directory ${stats.dir} not found` : `${REVIEW_FILE} not found`) };
}
const reviews = file.records.filter((r) => r && typeof r.verdict === 'string');
const undated = reviews.filter((r) => Number.isNaN(Date.parse(r.ts))).length;
// Stable sort on ts: the file is appended by whoever ran a review, in no fixed order.
const ordered = reviews
.map((r, i) => ({ r, i, t: Date.parse(r.ts) }))
.filter((x) => !Number.isNaN(x.t))
.sort((a, b) => a.t - b.t || a.i - b.i)
.map((x) => x.r);
const verdicts = {};
for (const r of reviews) verdicts[r.verdict] = (verdicts[r.verdict] ?? 0) + 1;
let longest = 0;
let run = 0;
for (const r of ordered) {
run = r.verdict === 'ALLOW' ? run + 1 : 0;
longest = Math.max(longest, run);
}
const last = ordered.at(-1) ?? null;
const facts = {
verdicts, longestStreak: longest, invalid: file.invalid.length, undated,
last: last ? { ts: last.ts, slug: last.slug, verdict: last.verdict } : null,
};
const notes = [];
if (file.invalid.length) notes.push(`${file.invalid.length} invalid line(s) left out (line ${file.invalid.join(', ')})`);
if (undated) notes.push(`${undated} review(s) without a readable ts left out of the streak`);
if (reviews.length === 0) {
return { ...base, source: file.path, denominator: 0, facts, ...notCountable(['no review rows', ...notes].join('; ')) };
}
const counts = Object.entries(verdicts).map(([k, v]) => `${k} ${v}`).join(' · ');
const detail = [
`${counts}; longest ALLOW streak ${longest}; last review ${last ? `${last.ts} ${last.verdict} (${last.slug})` : 'n/a'}`,
'ordered by ts, read as JSON',
...notes,
].join('; ');
return { ...base, source: file.path, countable: true, value: run, denominator: reviews.length, facts, detail };
}
// --- 1 · USE ---------------------------------------------------------------------
// A minimal frontmatter reader: `key: value` lines between the first two `---`.
function frontmatter(text) {
const m = String(text).match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (!m) return null;
const fm = {};
for (const line of m[1].split(/\r?\n/)) {
const kv = line.match(/^([A-Za-z][\w-]*):\s*(.*?)\s*$/);
if (kv) fm[kv[1]] = kv[2];
}
return fm;
}
/** Every order file under `<coord>/<repo>/orders/` - pending, `archive/` and `claimed/`. */
export function readOrders(coordDir) {
const orders = [];
const unreadable = [];
let files = 0;
if (existsSync(coordDir)) {
for (const repo of readdirSync(coordDir).sort()) {
const dir = join(coordDir, repo, 'orders');
if (!existsSync(dir) || !statSync(dir).isDirectory()) continue;
for (const path of walk(dir).filter((p) => p.endsWith('.md')).sort()) {
files++;
const fm = frontmatter(readFileSync(path, 'utf8'));
if (fm && fm['order-id']) orders.push({ path, repo, fm });
else unreadable.push(path);
}
}
}
return { dir: coordDir, files, orders, unreadable };
}
export function measureUse({ queue, slugs, cutoff }) {
const base = {
id: 'use', label: 'new work that went through Voyage',
source: `${join(queue.dir, '*', 'orders')}/**/*.md (cutoff ${cutoff})`,
};
const withClass = queue.orders.filter((o) => o.fm['work-class'] !== undefined);
const newWork = withClass.filter((o) => o.fm['work-class'] === 'new' && String(o.fm.date ?? '').slice(0, 10) >= cutoff);
const through = newWork.filter((o) => o.fm.slug && slugs.has(o.fm.slug));
const facts = {
scanned: queue.orders.length, files: queue.files, unreadable: queue.unreadable.length,
withWorkClass: withClass.length, newSinceCutoff: newWork.length, withVoyageRun: through.length,
};
const perMailbox = {};
for (const o of queue.orders) perMailbox[o.repo] = (perMailbox[o.repo] ?? 0) + 1;
const dot = Object.entries(perMailbox).filter(([k]) => k.startsWith('.'));
const census = `${queue.files} order files in ${Object.keys(perMailbox).length} mailboxes`
+ (dot.length ? ` (dot-mailboxes ${dot.map(([k, v]) => `${k} ${v}`).join(' · ')} - a shell \`*\` skips them)` : '')
+ `; ${withClass.length} of ${queue.orders.length} orders carry work-class`
+ (queue.unreadable.length ? `; ${queue.unreadable.length} order file(s) without frontmatter` : '');
if (newWork.length === 0) {
return { ...base, denominator: 0, facts, ...notCountable(
`${census}; 0 orders dated >= ${cutoff} with work-class: new - the denominator is empty`) };
}
const unmatched = newWork.filter((o) => !o.fm.slug).length;
return {
...base, countable: true, value: through.length, denominator: newWork.length, facts,
detail: `${census}; counted = a Voyage run in the stats with the order's slug`
+ (unmatched ? `; ${unmatched} new-work order(s) carry no slug and cannot match` : ''),
};
}
// --- 3 · INTERRUPTS --------------------------------------------------------------
// A call site EMITS user_input: the CLI shim with `--event user_input`, or a
// call `emit…('user_input'`. A name in prose is not a call.
const CALL_SITE = /--event[\s'",=\\]+user_input\b|\bemit\w*\(\s*['"]user_input['"]/;
const SCAN = [
{ dir: 'commands', ext: '.md' },
{ dir: 'agents', ext: '.md' },
{ dir: 'hooks', ext: '' },
{ dir: 'lib', ext: '.mjs' },
];
/** Emitter call sites for user_input outside lib/stats/ (the emitter) and tests/. */
export function emitterCallSites(root) {
const sites = [];
let scanned = 0;
for (const { dir, ext } of SCAN) {
const abs = join(root, dir);
if (!existsSync(abs)) continue;
for (const file of walk(abs).sort()) {
const rel = toPosix(relative(root, file));
if (rel.startsWith('lib/stats/') || (ext && !rel.endsWith(ext))) continue;
scanned++;
readFileSync(file, 'utf8').split('\n').forEach((line, i) => {
if (CALL_SITE.test(line)) sites.push({ file: rel, line: i + 1 });
});
}
}
return { sites, scanned };
}
const deliveryOf = (r) => {
const p = r.payload && typeof r.payload === 'object' ? r.payload : {};
return p.slug || p.run_id || r.slug || r.run_id || null;
};
export function measureInterrupts({ stats, callSites }) {
const base = { id: 'interrupts', label: 'operator interrupts per delivery', source: join(stats.dir, '*-stats.jsonl') };
const lines = stats.files.reduce((n, f) => n + f.lines, 0);
const records = stats.files.flatMap((f) => f.records);
const userInput = records.filter((r) => r && r.event === 'user_input');
const perDelivery = {};
for (const r of userInput) {
const d = deliveryOf(r);
if (d) perDelivery[d] = (perDelivery[d] ?? 0) + 1;
}
const withDelivery = Object.values(perDelivery).reduce((a, b) => a + b, 0);
const facts = { lines, files: stats.files.length, userInput: userInput.length, withDelivery, perDelivery,
callSites: callSites.sites.length, scannedForCallSites: callSites.scanned };
const census = `${userInput.length} user_input records of ${lines} lines in ${stats.files.length} files`
+ `; ${withDelivery} carry a slug or run id`
+ `; ${callSites.sites.length} emitter call sites outside lib/stats/ and tests/ (${callSites.scanned} files scanned)`;
if (withDelivery === 0) {
return { ...base, denominator: 0, facts, ...notCountable(`${census} - an interrupt that names no delivery cannot be divided by one`) };
}
return {
...base, countable: true, value: withDelivery, denominator: Object.keys(perDelivery).length, facts,
detail: `${census}; per delivery: ${Object.entries(perDelivery).map(([k, v]) => `${k} ${v}`).join(' · ')}`,
};
}
// --- 4 · the signature-preserving mutant (M9) --------------------------------------
const EXPORT_NAME = /^\s*export\s+(?:async\s+)?(?:function\*?|const|let|var|class)\s+([A-Za-z_$][\w$]*)/gm;
// Every entry whose check RUNS a named test and names the module it binds.
function behaviourConditions(registry) {
const out = [];
for (const e of [...(registry.defects ?? []), ...(registry.experiments ?? [])]) {
for (const c of Array.isArray(e.check) ? e.check : []) {
if (c && c.test !== undefined && Array.isArray(c.stubs)) out.push({ id: e.id, entry: e, cond: c });
}
}
return out;
}
const relImport = (fromFile, toFile) => {
const r = toPosix(relative(dirname(fromFile), toFile));
return r.startsWith('.') ? r : `./${r}`;
};
/**
* Build M9 in a fresh temp directory: every module a behaviour probe binds is
* replaced by one whose exports keep their names and always return
* {status:'PASSED'}, and every named test is replaced by one that only checks
* that each export is a function. Returns the directory. `root` is only read.
*/
export function buildM9Tree(root, registry) {
const dir = mkdtempSync(join(tmpdir(), 'yardstick-m9-'));
const tests = new Map();
const modules = new Set();
for (const { cond } of behaviourConditions(registry)) {
if (!tests.has(cond.test)) tests.set(cond.test, { names: new Set(), stubs: new Set() });
tests.get(cond.test).names.add(cond.name);
for (const s of cond.stubs) { tests.get(cond.test).stubs.add(s); modules.add(s); }
}
const write = (rel, body) => {
mkdirSync(dirname(join(dir, rel)), { recursive: true });
writeFileSync(join(dir, rel), body);
};
for (const mod of modules) {
const source = readFileSync(join(root, mod), 'utf8');
const names = new Set([...source.matchAll(EXPORT_NAME)].map((m) => m[1]));
const lines = [...names].map((n) => `export function ${n}() { return { status: 'PASSED' }; }`);
if (/^\s*export\s+default\b/m.test(source)) lines.push("export default function () { return { status: 'PASSED' }; }");
write(mod, `${lines.join('\n')}\n`);
}
for (const [testRel, { names, stubs }] of tests) {
const lines = ["import { test } from 'node:test';", "import { strict as assert } from 'node:assert';"];
[...stubs].forEach((s, i) => lines.push(`import * as m${i} from '${relImport(testRel, s)}';`));
for (const name of names) {
const checks = [...stubs].map((_, i) => `for (const v of Object.values(m${i})) assert.equal(typeof v, 'function');`);
lines.push(`test(${JSON.stringify(name)}, () => { ${checks.join(' ')} });`);
}
write(testRel, `${lines.join('\n')}\n`);
}
return dir;
}
// Run ONE named test; true = passed, false = failed, null = it did not run.
function runNamedTest(root, testRel, name) {
const env = { ...process.env };
for (const key of Object.keys(env)) if (key.startsWith('NODE_TEST_')) delete env[key];
const pattern = `^${String(name).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`;
const r = spawnSync(process.execPath, ['--test', '--test-reporter=tap', `--test-name-pattern=${pattern}`, testRel],
{ cwd: root, encoding: 'utf8', timeout: TEST_TIMEOUT_MS, env });
for (const line of String(r.stdout ?? '').split('\n')) {
const m = line.match(/^\s*(not )?ok \d+ - (.*)$/);
if (m && m[2].replace(/\\(.)/g, '$1').trim() === name) return m[1] === undefined;
}
return null;
}
export function measureMutants(root, { registry, evaluate = evaluateCheck, runTest = runNamedTest } = {}) {
const reg = registry ?? loadRegistry(root);
const base = {
id: 'mutants', label: 'signature-preserving mutants the end-state gate fells',
source: 'scripts/end-state-gate.mjs behaviour probes vs M9 (exports return {status:"PASSED"}, tests check typeof)',
denominator: 1,
};
const probes = behaviourConditions(reg);
const entryIds = [...new Set(probes.map((p) => p.id))];
if (probes.length === 0) {
return { ...base, facts: { entries: [] }, ...notCountable('the registry has no behaviour probe to aim M9 at') };
}
let dir;
try {
dir = buildM9Tree(root, reg);
} catch (err) {
return { ...base, facts: { entries: entryIds }, ...notCountable(`M9 could not be built: ${err.message}`) };
}
try {
return judgeM9(base, dir, probes, entryIds, evaluate, runTest);
} finally {
// Only ever the directory buildM9Tree made, under the system temp dir.
if (realpathSync(dir).startsWith(realpathSync(tmpdir()))) rmSync(dir, { recursive: true, force: true });
}
}
function judgeM9(base, dir, probes, entryIds, evaluate, runTest) {
// Liveness: M9's own tests must PASS on the M9 tree, or "felled" would mean "broken fixture".
const dead = probes.filter((p) => runTest(dir, p.cond.test, p.cond.name) !== true).map((p) => p.cond.name);
if (dead.length > 0) {
return { ...base, facts: { entries: entryIds },
...notCountable(`M9 is not live: ${dead.length} of its named tests do not pass on the M9 tree`) };
}
const results = [...new Map(probes.map((p) => [p.id, p.entry])).values()]
.map((e) => ({ id: e.id, ...evaluate(dir, e.check) }));
const closed = results.filter((r) => r.status === 'closed');
const felled = closed.length === 0 ? 1 : 0;
const per = results.map((r) => `${r.id} ${r.status === 'closed' ? 'closed' : `not closed (${r.status})`}`).join(', ');
return {
...base, countable: true, value: felled, facts: { entries: entryIds },
detail: `M9 on a temp tree: ${per} - ${felled ? 'the gate refuses to count a typeof-only test as behaviour'
: 'a typeof-only test closes the probe, so the gate counts a signature as behaviour'}`,
};
}
// --- the whole yardstick ---------------------------------------------------------
const VERDICT = {
judged: false,
met: false,
detail: 'the verdict needs the first five deliveries through the new Voyage; nothing counts a delivery yet',
};
export function measureAll({ root = REPO_ROOT, coordDir = DEFAULT_COORD, dataDir = DEFAULT_DATA, cutoff = DEFAULT_CUTOFF, mutantOptions = {} } = {}) {
const stats = loadStats(dataDir);
const numbers = [
measureUse({ queue: readOrders(coordDir), slugs: voyageSlugs(stats), cutoff }),
measureTrust(stats),
measureInterrupts({ stats, callSites: emitterCallSites(root) }),
];
const mutants = measureMutants(root, mutantOptions);
const countable = numbers.filter((r) => r.countable).length;
const felled = mutants.countable && mutants.value === mutants.denominator;
return {
green: countable === numbers.length && felled && VERDICT.judged && VERDICT.met,
countable,
rows: [...numbers, mutants],
verdict: VERDICT,
};
}
const fmt = (v) => (v === null || v === undefined ? 'n/a' : String(v));
export function render(result) {
const m = result.rows.find((r) => r.id === 'mutants');
const out = [];
out.push(`Voyage yardstick: ${result.green ? 'GREEN' : 'RED'} (${result.countable} of 3 numbers countable)`);
out.push(`signature-preserving mutants felled: ${m.countable ? `${m.value} of ${m.denominator}` : 'n/a (not measurable)'}`);
out.push(`verdict: ${result.verdict.judged ? (result.verdict.met ? 'MET' : 'NOT MET') : 'NOT JUDGED'} - ${result.verdict.detail}`);
out.push('');
out.push('| number | countable | value | denominator | source |');
out.push('|---|---|---|---|---|');
for (const r of result.rows) {
out.push(`| ${r.id} | ${r.countable ? 'YES' : 'NO'} | ${fmt(r.value)} | ${fmt(r.denominator)} | ${r.source} |`);
}
for (const r of result.rows) {
out.push('');
out.push(`${r.id} - ${r.label}: ${r.detail}`);
}
return out.join('\n');
}
export function main(argv) {
const opts = {};
let json = false;
const usage = 'usage: yardstick.mjs [--json] [--root <repo>] [--coord <dir>] [--data <dir>] [--cutoff YYYY-MM-DD]';
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
const value = argv[i + 1];
if (a === '--json') { json = true; continue; }
if (['--root', '--coord', '--data', '--cutoff'].includes(a) && value !== undefined) {
i++;
if (a === '--cutoff') {
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
process.stderr.write(`yardstick: --cutoff must be YYYY-MM-DD, got ${value}\n${usage}\n`);
return 2;
}
opts.cutoff = value;
} else {
opts[{ '--root': 'root', '--coord': 'coordDir', '--data': 'dataDir' }[a]] = resolve(value);
}
continue;
}
process.stderr.write(`yardstick: unknown argument ${a}\n${usage}\n`);
return 2;
}
const result = measureAll(opts);
process.stdout.write(`${json ? JSON.stringify(result, null, 2) : render(result)}\n`);
return result.green ? 0 : 1;
}
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
process.exitCode = main(process.argv.slice(2));
}

View file

@ -0,0 +1,26 @@
// tests/fixtures/red-first/02243c6-always-allow-shim.mjs
// The shim that makes the red-first claim of 02243c6 reproducible.
//
// 02243c6 ("the criteria runner screens with an ALLOWLIST") says six of its new
// tests were red before the fix. On the parent tree the test file does not even
// LOAD - it imports `allowedCommand`, which the parent does not export - so the
// red is a load error on the whole file, not six named failures. The six only
// appear once the missing export exists and allows everything. This file is
// that export, kept here so a re-measure does not have to guess it.
//
// Reproduce "6 of 55" (POSIX shell, from the repo root, needs git history):
//
// T="$(mktemp -d)"
// git archive 02243c6 | tar -x -C "$T"
// git show 02243c6^:lib/verification/criteria-runner.mjs > "$T/lib/verification/criteria-runner.mjs"
// cat tests/fixtures/red-first/02243c6-always-allow-shim.mjs >> "$T/lib/verification/criteria-runner.mjs"
// (cd "$T" && node --test --test-reporter=tap tests/lib/criteria-runner.test.mjs) | grep -E '^# (tests|fail)'
//
// Expected: `# tests 55` and `# fail 6` - four `allowedCommand` tests (all but
// "every known test runner is allowed", which an always-allow shim passes),
// `runCriteria: a command outside the allowlist is NOT RUN`, and the end-to-end
// canary test (28 evasions, 0 run). Measured 2026-09-22.
// It is a fixture, not a test: nothing in the suite runs it, because the suite
// must also pass on a `git archive` tree, where there is no history to rewind.
export function allowedCommand() { return { allowed: true, reason: '' }; }

View file

@ -0,0 +1,409 @@
// tests/scripts/yardstick.test.mjs
// The yardstick prints Voyage's three numbers (use · trust · interrupts) with
// denominators read from their SOURCE, plus a fourth row that asks whether the
// end-state gate itself can be fooled by a signature-preserving mutant.
//
// Like the gate's own test, these tests pin the MECHANICS in both directions
// against throwaway fixtures - every number can be countable and can be not
// countable, the mutant row can read felled and not felled. They deliberately
// do NOT assert that the real data is red today: the yardstick 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 order queue or the real stats.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import {
DEFAULT_CUTOFF,
loadStats,
readOrders,
measureUse,
measureTrust,
measureInterrupts,
emitterCallSites,
buildM9Tree,
measureMutants,
measureAll,
render,
main,
} from '../../scripts/yardstick.mjs';
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
// --- helpers ---------------------------------------------------------------
function fixture(files) {
const dir = mkdtempSync(join(tmpdir(), 'yardstick-'));
for (const [rel, body] of Object.entries(files)) {
const p = join(dir, rel);
mkdirSync(dirname(p), { recursive: true });
writeFileSync(p, body);
}
return dir;
}
const cleanup = (...dirs) => { for (const d of dirs) rmSync(d, { recursive: true, force: true }); };
const jsonl = (...rows) => `${rows.map((r) => (typeof r === 'string' ? r : JSON.stringify(r))).join('\n')}\n`;
function order({ id, date, workClass, slug, extra = '' }) {
const lines = ['---', 'from: .claude', 'to: some-repo', `order-id: ${id}`, `subject: order ${id}`, `date: ${date}`];
if (workClass !== undefined) lines.push(`work-class: ${workClass}`);
if (slug !== undefined) lines.push(`slug: ${slug}`);
lines.push('---', '', `# ORDER ${id}`, extra, '');
return lines.join('\n');
}
const review = (ts, verdict, slug = 's') => ({ ts, slug, verdict });
// --- the stats loader --------------------------------------------------------
test('loadStats: an invalid JSON line is COUNTED, never silently dropped', () => {
const dir = fixture({
'trekreview-stats.jsonl': jsonl(review('2026-09-01T00:00:00Z', 'ALLOW'), '{not json', '', review('2026-09-02T00:00:00Z', 'BLOCK')),
});
try {
const stats = loadStats(dir);
const f = stats.files.find((x) => x.name === 'trekreview-stats.jsonl');
assert.equal(f.lines, 3, 'blank lines are not lines; the invalid one is');
assert.equal(f.records.length, 2);
assert.deepEqual(f.invalid, [2], 'the invalid line is reported by its line number');
} finally { cleanup(dir); }
});
test('loadStats: a missing data directory is reported, not read as zero', () => {
const stats = loadStats(join(tmpdir(), 'yardstick-no-such-dir-8f3a'));
assert.equal(stats.missing, true);
assert.deepEqual(stats.files, []);
});
// --- 2 · TRUST ---------------------------------------------------------------
test('trust: the ALLOW streak is read in ts order, not file order', () => {
// File order ends on ALLOW, ALLOW; chronologically the last review is a BLOCK.
const dir = fixture({
'trekreview-stats.jsonl': jsonl(
review('2026-09-01T00:00:00Z', 'ALLOW'),
review('2026-09-05T00:00:00Z', 'BLOCK'),
review('2026-09-02T00:00:00Z', 'ALLOW'),
review('2026-09-03T00:00:00Z', 'ALLOW'),
),
});
try {
const row = measureTrust(loadStats(dir));
assert.equal(row.countable, true);
assert.equal(row.denominator, 4);
assert.equal(row.value, 0, 'the chronologically last review is a BLOCK');
assert.equal(row.facts.longestStreak, 3);
assert.deepEqual(row.facts.verdicts, { ALLOW: 3, BLOCK: 1 });
assert.equal(row.facts.last.verdict, 'BLOCK');
} finally { cleanup(dir); }
});
test('trust: both spacings of the verdict field count the same (it is read as JSON, not grepped)', () => {
const dir = fixture({
'trekreview-stats.jsonl': jsonl(
'{"ts": "2026-09-01T00:00:00Z", "slug": "a", "verdict": "BLOCK"}',
'{"ts":"2026-09-02T00:00:00Z","slug":"b","verdict":"ALLOW"}',
),
});
try {
const row = measureTrust(loadStats(dir));
assert.deepEqual(row.facts.verdicts, { BLOCK: 1, ALLOW: 1 });
assert.equal(row.value, 1);
} finally { cleanup(dir); }
});
test('trust: an invalid line is left out of n and said out loud', () => {
const dir = fixture({
'trekreview-stats.jsonl': jsonl(review('2026-09-01T00:00:00Z', 'ALLOW'), '{"ts": oops'),
});
try {
const row = measureTrust(loadStats(dir));
assert.equal(row.denominator, 1);
assert.equal(row.facts.invalid, 1);
assert.match(row.detail, /1 invalid line/);
} finally { cleanup(dir); }
});
test('trust: an empty or missing review file is NOT countable', () => {
const empty = fixture({ 'trekreview-stats.jsonl': '' });
const none = fixture({ 'trekplan-stats.jsonl': jsonl({ ts: '2026-09-01T00:00:00Z', slug: 'x' }) });
try {
for (const dir of [empty, none]) {
const row = measureTrust(loadStats(dir));
assert.equal(row.countable, false, dir);
assert.equal(row.value, null);
assert.ok(row.detail.length > 0, 'the row must say WHY it cannot be counted');
}
} finally { cleanup(empty, none); }
});
// --- 1 · USE -----------------------------------------------------------------
test('use: orders are read from orders/, archive/ and claimed/, and nothing else', () => {
const coord = fixture({
'repo-a/orders/o1.md': order({ id: 'o1', date: '2026-09-22T10:00:00Z' }),
'repo-a/orders/archive/o2.md': order({ id: 'o2', date: '2026-09-01T10:00:00Z' }),
'repo-b/orders/claimed/o3.md': order({ id: 'o3', date: '2026-09-22T10:00:00Z' }),
'repo-b/orders/claimed/o3.claim': 'claimed',
'repo-b/inbox/m1.md': order({ id: 'm1', date: '2026-09-22T10:00:00Z' }),
'repo-b/orders/broken.md': 'no frontmatter at all\n',
});
try {
const q = readOrders(coord);
assert.equal(q.files, 4, 'three orders and one unreadable file, all .md under */orders/');
assert.equal(q.orders.length, 3);
assert.equal(q.unreadable.length, 1);
} finally { cleanup(coord); }
});
test('use: a dot-mailbox is a mailbox - its orders count, and the detail names it', () => {
// Measured 2026-09-22: a hand count over `coord/*/orders` came out lower than
// the yardstick, because the shell's `*` skips dot-directories and the queue
// does not. The detail names them, so the next re-measure can reconcile.
const coord = fixture({
'r/orders/o1.md': order({ id: 'o1', date: '2026-09-22T10:00:00Z' }),
'.pm/orders/archive/o2.md': order({ id: 'o2', date: '2026-09-22T10:00:00Z' }),
});
try {
const q = readOrders(coord);
assert.equal(q.orders.length, 2);
const row = measureUse({ queue: q, slugs: new Set(), cutoff: DEFAULT_CUTOFF });
assert.match(row.detail, /2 order files in 2 mailboxes/);
assert.match(row.detail, /dot-mailboxes .pm 1/);
} finally { cleanup(coord); }
});
test('use: no order carrying work-class means NOT COUNTABLE, not 0 %', () => {
const coord = fixture({
'r/orders/o1.md': order({ id: 'o1', date: '2026-09-22T10:00:00Z' }),
'r/orders/o2.md': order({ id: 'o2', date: '2026-09-23T10:00:00Z' }),
});
try {
const row = measureUse({ queue: readOrders(coord), slugs: new Set(), cutoff: DEFAULT_CUTOFF });
assert.equal(row.countable, false);
assert.equal(row.value, null);
assert.equal(row.denominator, 0);
assert.equal(row.facts.scanned, 2);
assert.equal(row.facts.withWorkClass, 0);
assert.match(row.detail, /0 of 2 orders carry work-class/);
} finally { cleanup(coord); }
});
test('use: the denominator is new-work orders dated on or after the cutoff; the numerator is those with a Voyage run', () => {
const coord = fixture({
'r/orders/a.md': order({ id: 'a', date: '2026-09-21T00:00:00Z', workClass: 'new', slug: 'feat-a' }),
'r/orders/archive/b.md': order({ id: 'b', date: '2026-09-25T00:00:00Z', workClass: 'new', slug: 'feat-b' }),
'r/orders/c.md': order({ id: 'c', date: '2026-09-25T00:00:00Z', workClass: 'new' }),
'r/orders/d.md': order({ id: 'd', date: '2026-09-20T23:59:59Z', workClass: 'new', slug: 'feat-a' }),
'r/orders/e.md': order({ id: 'e', date: '2026-09-25T00:00:00Z', workClass: 'maintenance', slug: 'feat-a' }),
});
try {
const row = measureUse({ queue: readOrders(coord), slugs: new Set(['feat-a']), cutoff: '2026-09-21' });
assert.equal(row.countable, true);
assert.equal(row.denominator, 3, 'a, b and c - d is before the cutoff, e is maintenance');
assert.equal(row.value, 1, 'only a has a Voyage run with the same slug; c has no slug to match');
assert.equal(row.facts.withWorkClass, 5);
} finally { cleanup(coord); }
});
// --- 3 · INTERRUPTS ----------------------------------------------------------
test('interrupts: no user_input record means NOT COUNTABLE, with the line denominator stated', () => {
const dir = fixture({
'trekexecute-stats.jsonl': jsonl({ ts: 't', session_id: 's', success: true }, { ts: 't', event: 'main-merge-gate', payload: {} }),
'trekplan-stats.jsonl': jsonl({ ts: 't', slug: 'x' }),
});
try {
const row = measureInterrupts({ stats: loadStats(dir), callSites: { sites: [], scanned: 3 } });
assert.equal(row.countable, false);
assert.equal(row.facts.lines, 3);
assert.equal(row.facts.userInput, 0);
assert.match(row.detail, /0 user_input records of 3 lines in 2 files/);
assert.match(row.detail, /0 emitter call sites/);
} finally { cleanup(dir); }
});
test('interrupts: user_input WITHOUT a slug or run id is still not countable per delivery', () => {
const dir = fixture({
'trekexecute-stats.jsonl': jsonl({ ts: 't', event: 'user_input', payload: { question: 'q' } }),
});
try {
const row = measureInterrupts({ stats: loadStats(dir), callSites: { sites: [], scanned: 1 } });
assert.equal(row.countable, false);
assert.equal(row.facts.userInput, 1);
assert.equal(row.facts.withDelivery, 0);
} finally { cleanup(dir); }
});
test('interrupts: user_input carrying a slug or run id is countable, per delivery', () => {
const dir = fixture({
'trekexecute-stats.jsonl': jsonl(
{ ts: 't1', event: 'user_input', payload: { slug: 'feat-a' } },
{ ts: 't2', event: 'user_input', payload: { slug: 'feat-a' } },
{ ts: 't3', event: 'user_input', payload: { run_id: 'r-9' } },
),
});
try {
const row = measureInterrupts({ stats: loadStats(dir), callSites: { sites: [], scanned: 1 } });
assert.equal(row.countable, true);
assert.equal(row.value, 3);
assert.deepEqual(row.facts.perDelivery, { 'feat-a': 2, 'r-9': 1 });
} finally { cleanup(dir); }
});
test('emitterCallSites: counts user_input emissions outside lib/stats/ and tests/ only', () => {
const root = fixture({
'commands/a.md': 'node ${CLAUDE_PLUGIN_ROOT}/lib/stats/event-emit.mjs \\\n --event user_input \\\n',
'commands/b.md': 'the emitter knows user_input as an event name - prose, not a call\n',
'lib/util/x.mjs': "emitEvent('user_input', { slug });\n",
'lib/stats/event-emit.mjs': "emit('user_input', {});\n",
'tests/lib/y.test.mjs': "emit('user_input', {});\n",
'hooks/scripts/h.mjs': "spawnSync('node', [EMIT, '--event', 'user_input']);\n",
});
try {
const r = emitterCallSites(root);
assert.deepEqual(r.sites.map((s) => s.file).sort(), ['commands/a.md', 'hooks/scripts/h.mjs', 'lib/util/x.mjs']);
assert.equal(r.scanned, 4, 'commands/a.md, commands/b.md, lib/util/x.mjs, hooks/scripts/h.mjs');
} finally { cleanup(root); }
});
// --- 4 · the signature-preserving mutant (M9) ----------------------------------
const MODULE = 'export function verdict(code) { return { status: code === 0 ? "PASSED" : "FAILED" }; }\n';
const STRONG_TEST = [
"import { test } from 'node:test';",
"import { strict as assert } from 'node:assert';",
"import { verdict } from '../lib/m.mjs';",
"test('verdict fells a failing exit', () => { assert.equal(verdict(1).status, 'FAILED'); });",
'',
].join('\n');
const behaviourRegistry = {
defects: [{
id: 'D-X', probe: 'behaviour',
check: [{ test: 'tests/t.test.mjs', name: 'verdict fells a failing exit', expect: 'fails', stubs: ['lib/m.mjs'] }],
}],
experiments: [],
};
test('buildM9Tree: every export keeps its name and returns {status:"PASSED"}; every named test only checks typeof', async () => {
const root = fixture({ 'lib/m.mjs': MODULE, 'tests/t.test.mjs': STRONG_TEST });
const m9 = buildM9Tree(root, behaviourRegistry);
try {
const mod = readFileSync(join(m9, 'lib', 'm.mjs'), 'utf8');
assert.match(mod, /export function verdict\(\) \{ return \{ status: 'PASSED' \}; \}/);
const t = readFileSync(join(m9, 'tests', 't.test.mjs'), 'utf8');
assert.match(t, /verdict fells a failing exit/);
assert.match(t, /typeof/);
assert.doesNotMatch(t, /FAILED/, 'the mutant test asserts nothing about behaviour');
assert.equal(readFileSync(join(root, 'lib', 'm.mjs'), 'utf8'), MODULE, 'the measured tree is never touched');
} finally { cleanup(root, m9); }
});
test('mutants: a gate that CLOSES the probe on M9 has felled 0 of 1', () => {
const root = fixture({ 'lib/m.mjs': MODULE, 'tests/t.test.mjs': STRONG_TEST });
try {
const row = measureMutants(root, {
registry: behaviourRegistry,
evaluate: () => ({ status: 'closed', detail: '' }),
runTest: () => true,
});
assert.equal(row.countable, true);
assert.equal(row.value, 0);
assert.equal(row.denominator, 1);
assert.match(row.detail, /D-X closed/);
} finally { cleanup(root); }
});
test('mutants: a gate that refuses to close the probe on M9 has felled 1 of 1', () => {
const root = fixture({ 'lib/m.mjs': MODULE, 'tests/t.test.mjs': STRONG_TEST });
try {
const row = measureMutants(root, {
registry: behaviourRegistry,
evaluate: () => ({ status: 'not-fellable', detail: 'still passes against a signature-preserving mutant' }),
runTest: () => true,
});
assert.equal(row.value, 1);
assert.equal(row.denominator, 1);
} finally { cleanup(root); }
});
test('mutants: a mutant whose own tests do not pass is not LIVE, so the row cannot be counted', () => {
// Without this control a broken fixture would read as "felled".
const root = fixture({ 'lib/m.mjs': MODULE, 'tests/t.test.mjs': STRONG_TEST });
try {
const row = measureMutants(root, {
registry: behaviourRegistry,
evaluate: () => ({ status: 'not-fellable', detail: 'x' }),
runTest: () => null,
});
assert.equal(row.countable, false);
assert.equal(row.value, null);
assert.match(row.detail, /not live/);
} finally { cleanup(root); }
});
test('mutants: against the REAL gate and registry, M9 is live and the denominator is 1', () => {
// Pins the measurement's shape, not today's value: M9's typeof-only tests
// really do pass on the M9 tree (so "felled" can only come from the gate).
const row = measureMutants(ROOT);
assert.equal(row.countable, true, row.detail);
assert.equal(row.denominator, 1);
assert.ok(row.value === 0 || row.value === 1);
assert.deepEqual(row.facts.entries.sort(), ['D-03', 'D-04']);
});
// --- the whole yardstick -------------------------------------------------------
function world({ orders = {}, stats = {} } = {}) {
const coord = fixture(Object.keys(orders).length ? orders : { 'r/orders/.keep': '' });
const data = fixture(Object.keys(stats).length ? stats : { '.keep': '' });
return { coord, data };
}
test('measureAll + main: RED with exit 1 while any number is not countable', () => {
const { coord, data } = world({
orders: { 'r/orders/o.md': order({ id: 'o', date: '2026-09-22T00:00:00Z' }) },
stats: { 'trekreview-stats.jsonl': jsonl(review('2026-09-01T00:00:00Z', 'ALLOW')) },
});
try {
const result = measureAll({ root: ROOT, coordDir: coord, dataDir: data, cutoff: DEFAULT_CUTOFF });
assert.equal(result.green, false);
assert.equal(result.countable, 1);
const text = render(result);
assert.match(text, /1 of 3 numbers countable/);
assert.match(text, /signature-preserving mutants felled: \d of 1/);
assert.match(text, /verdict: NOT JUDGED/);
for (const id of ['use', 'trust', 'interrupts', 'mutants']) assert.match(text, new RegExp(`\\| ${id} \\|`));
assert.equal(main(['--root', ROOT, '--coord', coord, '--data', data, '--json']), 1);
} finally { cleanup(coord, data); }
});
test('measureAll: all three countable and the mutant felled is still RED until the verdict can be judged', () => {
const { coord, data } = world({
orders: { 'r/orders/o.md': order({ id: 'o', date: '2026-09-22T00:00:00Z', workClass: 'new', slug: 'a' }) },
stats: {
'trekreview-stats.jsonl': jsonl(review('2026-09-01T00:00:00Z', 'ALLOW', 'a')),
'trekexecute-stats.jsonl': jsonl({ ts: 't', event: 'user_input', payload: { slug: 'a' } }),
},
});
try {
const result = measureAll({
root: ROOT, coordDir: coord, dataDir: data, cutoff: DEFAULT_CUTOFF,
mutantOptions: { evaluate: () => ({ status: 'not-fellable', detail: '' }) },
});
assert.equal(result.countable, 3);
assert.equal(result.rows.find((r) => r.id === 'mutants').value, 1);
assert.equal(result.verdict.judged, false);
assert.equal(result.green, false);
} finally { cleanup(coord, data); }
});
test('main: an unknown argument or a malformed cutoff is a usage error (exit 2)', () => {
assert.equal(main(['--nope']), 2);
assert.equal(main(['--cutoff', '21.09.2026']), 2);
});