chore(cleanup): R-b R1 — Voyage's own stats, events and OTel export removed
Removes Voyage's measurement, statistics and export apparatus (decision 189; condition 175 met in step 1) and the R-a leftovers: lib/stats/, lib/exporters/, the otel-export and post-bash-stats hooks, yardstick, end-state and comparison gates, synthesis-measure, gold-scorer, docs/observability.md, examples/observability/, docs/T1-cc26-delegated-orchestration.md and their tests and fixtures. The six command stats phases and the /trekexecute main-merge event blocks go; intent-approval --stamp no longer emits brief-approved. README and docs point to Claude Code's own OpenTelemetry. Files removed: 52. Files edited: 37. npm test: 2930 tests (2928 pass / 0 fail / 2 skipped) before, 2600 tests (2598 pass / 0 fail / 2 skipped) after. removed-surfaces group R1 green. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
92010d20de
commit
21480e8627
89 changed files with 94 additions and 10290 deletions
|
|
@ -1,422 +0,0 @@
|
|||
// 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); }
|
||||
});
|
||||
|
|
@ -1,950 +0,0 @@
|
|||
// tests/scripts/end-state-gate.test.mjs
|
||||
// The end-state gate counts the distance to "Voyage is finished" as five tallies:
|
||||
// open pipeline defects, unresolved experiments/opt-ins, dormant agents, open operator
|
||||
// decisions, and feat commits after the freeze tag. "Finished" = all five at 0 AND the
|
||||
// registry the gate counts from is intact against its frozen denominator.
|
||||
//
|
||||
// These tests pin the gate's MECHANICS in both directions — every tally can be 0 and can
|
||||
// be > 0 — against throwaway fixture trees. They deliberately do NOT assert that the real
|
||||
// repo is red today: the gate is supposed to go green, and a test that pins the current
|
||||
// distance would fail on the day the work is done. What they DO pin about the real repo
|
||||
// is the denominator itself (see "the frozen denominator" below).
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, readFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, dirname, resolve } from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
evaluateCondition,
|
||||
evaluateCheck,
|
||||
measure,
|
||||
loadRegistry,
|
||||
loadFrozen,
|
||||
spawnedNames,
|
||||
checkSignature,
|
||||
verifyIntegrity,
|
||||
render,
|
||||
PROBE_NOTES,
|
||||
} from '../../scripts/end-state-gate.mjs';
|
||||
|
||||
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||
const GATE = join(ROOT, 'scripts', 'end-state-gate.mjs');
|
||||
|
||||
// --- the frozen denominator ------------------------------------------------
|
||||
// Denominator frozen 2026-09-17, the day the end-state
|
||||
// direction was chosen. An entry may CLOSE; it may never be removed or have its check
|
||||
// changed, and the gate's agent/decision/freeze configuration may not move. To change
|
||||
// anything below you must change this test — say in the commit message who decided it.
|
||||
const FROZEN = {
|
||||
defects: {
|
||||
'D-01': '48f7f5cca070091e8f9b9ca0c7cdb7a1880733691935c25d07ea1d3e8d508fc9',
|
||||
'D-02': 'dfc94dba04a9116ae9be2097a0c5a9d8313b02c7de1ca6b49a472c1823a6d1e1',
|
||||
// Amended 2026-09-18 (operator decision): D-03/D-04 became behaviour probes.
|
||||
// Amended a second time the same day, as a maintenance decision: those
|
||||
// behaviour probes were greps for an export name, and a two-line stub
|
||||
// exporting both names closed them on an unfixed tree (mutant M6, pinned
|
||||
// below). They now RUN a named test.
|
||||
// Amended a third time the same day, same standing: a named test can be
|
||||
// EMPTY, and two empty tests with the right names closed both defects on a
|
||||
// tree without lib/verification/ (mutant M7, pinned below). Each condition
|
||||
// now also names the module whose STUB its test must fell. Every amendment
|
||||
// moved a signature on purpose and removed nothing.
|
||||
'D-03': '98698ce74c243353713786b8604160f6672568b090884e9428d9e5e9fbcad89d',
|
||||
'D-04': 'df11a2e95db7b014ce077db0473e5dde4195ace92d8077cacdc1cee273947b47',
|
||||
'D-05': '8fe6df52b43496c3302400507cb98002dcf8d2a12d92085e14bda4c7819bc7ba',
|
||||
'D-06': '72a787c8154d1c18849e9856dcf1a67703d6b5541f88db81d6bab13cab14b418',
|
||||
'D-07': '1bf3e0cd36c621720697475b0a3fd7db78611d66b58a9ce035e7695e57c69f1b',
|
||||
},
|
||||
experiments: {
|
||||
'E-01': 'f7718897cec8ace6879fe79f5f32760ec957c3642e7186f8aa1688ce77b3f4e8',
|
||||
'E-02': 'c8ff12c504ba7a9ac36f11bb8f9b713263e9b4eb28c07d984d9e2115980e740d',
|
||||
'E-03': '17153ad8808615a833a49ae0c3523264ce055aa690e5542a75bae1874495e7e8',
|
||||
},
|
||||
agents: {
|
||||
dir: 'agents',
|
||||
spawnSites: 'commands/*.md',
|
||||
referenceMarker: 'Reference document, not a spawnable capability',
|
||||
},
|
||||
decisions: {
|
||||
file: 'STATE.md',
|
||||
section: '## Åpne operatørbeslutninger',
|
||||
open: '^- \\[ \\] ',
|
||||
closed: '^- \\[x\\] ',
|
||||
},
|
||||
freeze: { tag: 'end-state-freeze', featPattern: '^feat(\\(|!|:)' },
|
||||
};
|
||||
|
||||
// --- helpers ---------------------------------------------------------------
|
||||
|
||||
function fixture(files) {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'end-state-gate-'));
|
||||
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 DECISIONS = { file: 'STATE.md', section: '## Open decisions', open: '^- \\[ \\] ', closed: '^- \\[x\\] ' };
|
||||
const AGENTS = { dir: 'agents', spawnSites: 'commands/*.md', referenceMarker: 'Reference document, not a spawnable capability' };
|
||||
const FREEZE = { tag: 'end-state-freeze', featPattern: '^feat(\\(|!|:)' };
|
||||
|
||||
function registry(overrides = {}) {
|
||||
return { defects: [], experiments: [], agents: AGENTS, decisions: DECISIONS, freeze: FREEZE, ...overrides };
|
||||
}
|
||||
|
||||
// A frozen manifest that matches `reg` exactly.
|
||||
function frozenFor(reg) {
|
||||
const sigs = (list) => Object.fromEntries(list.map((e) => [e.id, checkSignature(e.check)]));
|
||||
return { defects: sigs(reg.defects), experiments: sigs(reg.experiments), agents: reg.agents, decisions: reg.decisions, freeze: reg.freeze };
|
||||
}
|
||||
|
||||
function git(dir, ...args) {
|
||||
const r = spawnSync('git', [
|
||||
'-c', 'user.name=fixture', '-c', 'user.email=fixture@example.invalid',
|
||||
'-c', 'core.hooksPath=/dev/null', '-c', 'commit.gpgsign=false', '-c', 'tag.gpgsign=false',
|
||||
...args,
|
||||
], { cwd: dir, encoding: 'utf8' });
|
||||
assert.equal(r.status, 0, `git ${args.join(' ')}: ${r.stderr}`);
|
||||
return r.stdout;
|
||||
}
|
||||
|
||||
function gitInit(dir) {
|
||||
git(dir, 'init', '-q');
|
||||
git(dir, 'add', '-A');
|
||||
git(dir, 'commit', '-q', '--allow-empty', '-m', 'chore: fixture baseline');
|
||||
}
|
||||
|
||||
function gitCommit(dir, subject) {
|
||||
git(dir, 'commit', '-q', '--allow-empty', '-m', subject);
|
||||
}
|
||||
|
||||
const rowById = (result, id) => result.rows.find((r) => r.id === id);
|
||||
|
||||
// --- conditions -----------------------------------------------------------
|
||||
|
||||
test('evaluateCondition: match / no-match on a single file, multiline regex', () => {
|
||||
const dir = fixture({ 'a.md': 'intro\n### Heading\nbody\n' });
|
||||
try {
|
||||
assert.equal(evaluateCondition(dir, { path: 'a.md', pattern: '^### Heading', flags: 'm', expect: 'match' }), true);
|
||||
assert.equal(evaluateCondition(dir, { path: 'a.md', pattern: '^### Other', flags: 'm', expect: 'match' }), false);
|
||||
assert.equal(evaluateCondition(dir, { path: 'a.md', pattern: '^### Other', flags: 'm', expect: 'no-match' }), true);
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
test('evaluateCondition: globs (dir/*.ext, dir/**/*.ext) and path arrays match ANY file', () => {
|
||||
const dir = fixture({
|
||||
'commands/x.md': 'nothing here\n',
|
||||
'lib/deep/y.mjs': 'const FLAG = process.env.SOME_FLAG;\n',
|
||||
});
|
||||
try {
|
||||
assert.equal(evaluateCondition(dir, { path: 'commands/*.md', pattern: 'SOME_FLAG', expect: 'match' }), false);
|
||||
assert.equal(evaluateCondition(dir, { path: 'lib/**/*.mjs', pattern: 'SOME_FLAG', expect: 'match' }), true);
|
||||
assert.equal(evaluateCondition(dir, { path: ['commands/*.md', 'lib/**/*.mjs'], pattern: 'SOME_FLAG', expect: 'match' }), true);
|
||||
assert.equal(evaluateCondition(dir, { path: ['commands/*.md', 'lib/**/*.mjs'], pattern: 'SOME_FLAG', expect: 'no-match' }), false);
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
test('evaluateCondition: section scoping reads only from the heading to the next "## "', () => {
|
||||
const dir = fixture({
|
||||
'doc.md': '## 5. Design\nno status here\n## 6. PoC\n> **STATUS: RUN AND DECLINED**\n',
|
||||
});
|
||||
try {
|
||||
const inFive = { path: 'doc.md', section: '## 5.', pattern: '^> \\*\\*STATUS:', flags: 'm', expect: 'no-match' };
|
||||
const inSix = { ...inFive, section: '## 6.' };
|
||||
assert.equal(evaluateCondition(dir, inFive), true, 'section 5 has no status block');
|
||||
assert.equal(evaluateCondition(dir, inSix), false, 'section 6 has one');
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
test('evaluateCondition: a literal NUL byte is detectable', () => {
|
||||
const dir = fixture({ 'nul.mjs': "const SEP = '\u0000';\n", 'clean.mjs': "const SEP = '\\x00';\n" });
|
||||
try {
|
||||
assert.equal(evaluateCondition(dir, { path: 'nul.mjs', pattern: '\\u0000', expect: 'match' }), true);
|
||||
assert.equal(evaluateCondition(dir, { path: 'clean.mjs', pattern: '\\u0000', expect: 'match' }), false);
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
test('evaluateCondition: a missing file, a missing glob base or a missing section throws', () => {
|
||||
const dir = fixture({ 'a.md': 'x\n' });
|
||||
try {
|
||||
assert.throws(() => evaluateCondition(dir, { path: 'missing.md', pattern: 'x', expect: 'no-match' }), /missing\.md/);
|
||||
assert.throws(() => evaluateCondition(dir, { path: 'nodir/*.md', pattern: 'x', expect: 'no-match' }), /nodir/);
|
||||
assert.throws(() => evaluateCondition(dir, { path: 'a.md', section: '## 9.', pattern: 'x', expect: 'no-match' }), /## 9\./);
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
test('evaluateCondition: a glob over an EXISTING dir with no matching files throws (no silent no-match)', () => {
|
||||
// In a no-match condition, "silently nothing matched" would read as the condition holding.
|
||||
const dir = fixture({ 'docs/readme.txt': 'x\n' });
|
||||
try {
|
||||
assert.throws(() => evaluateCondition(dir, { path: 'docs/*.md', pattern: 'x', expect: 'no-match' }), /docs\/\*\.md/);
|
||||
assert.throws(() => evaluateCondition(dir, { path: 'docs/**/*.md', pattern: 'x', expect: 'no-match' }), /docs\/\*\*\/\*\.md/);
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
test('evaluateCheck: open only when ALL conditions hold; null check is not fellable', () => {
|
||||
const dir = fixture({
|
||||
'r.md': 'run the `### Bridge agent` block\n',
|
||||
'fixed.md': 'run the `### Bridge agent` block\n### Bridge agent\n',
|
||||
});
|
||||
const dangling = (file) => [
|
||||
{ path: file, pattern: '`### Bridge agent`', expect: 'match' },
|
||||
{ path: file, pattern: '^### Bridge agent', flags: 'm', expect: 'no-match' },
|
||||
];
|
||||
try {
|
||||
assert.equal(evaluateCheck(dir, dangling('r.md')).status, 'open');
|
||||
assert.equal(evaluateCheck(dir, dangling('fixed.md')).status, 'closed');
|
||||
assert.equal(evaluateCheck(dir, null).status, 'not-fellable');
|
||||
const broken = evaluateCheck(dir, dangling('gone.md'));
|
||||
assert.equal(broken.status, 'not-fellable');
|
||||
assert.match(broken.detail, /gone\.md/);
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
// --- spawn instructions (what "referenced by a command" means) -------------
|
||||
|
||||
test('spawnedNames: a row of a table headed `Agent` is a spawn instruction; other tables are not', () => {
|
||||
const md = [
|
||||
'Launch the new agents:', '',
|
||||
'| Agent | Purpose |', '|-------|---------|', '| `alpha-agent` | does a |', '| `beta-agent` | does b |', '',
|
||||
'| Name | Purpose |', '|------|---------|', '| `gamma-agent` | just a list |', '',
|
||||
].join('\n');
|
||||
const names = spawnedNames(md);
|
||||
assert.ok(names.has('alpha-agent') && names.has('beta-agent'));
|
||||
assert.ok(!names.has('gamma-agent'), 'a table not headed "Agent" is not a roster');
|
||||
});
|
||||
|
||||
test('spawnedNames: an imperative Launch/Spawn line names the agents it launches', () => {
|
||||
const md = [
|
||||
'Launch the **alpha-agent** agent:',
|
||||
'- Launch `beta-agent` (Agent tool) with the findings.',
|
||||
'3. Spawn `voyage:gamma-agent` in the background.',
|
||||
'**Launch** the `delta-agent` agent now.',
|
||||
'launch `epsilon-agent` too',
|
||||
].join('\n');
|
||||
const names = spawnedNames(md);
|
||||
for (const n of ['alpha-agent', 'beta-agent', 'gamma-agent', 'delta-agent', 'epsilon-agent']) {
|
||||
assert.ok(names.has(n), `${n} should count as spawned`);
|
||||
}
|
||||
});
|
||||
|
||||
test('spawnedNames: an agent block with a Prompt: line is a spawn instruction; a bare bold name is not', () => {
|
||||
const md = [
|
||||
'**alpha-agent** — adversarial review of the plan.',
|
||||
'Prompt: "Review this plan."',
|
||||
'',
|
||||
'**beta-agent** — mentioned for context only.',
|
||||
'It is not launched here.',
|
||||
].join('\n');
|
||||
const names = spawnedNames(md);
|
||||
assert.ok(names.has('alpha-agent'));
|
||||
assert.ok(!names.has('beta-agent'));
|
||||
});
|
||||
|
||||
test('spawnedNames: prose, negation, comments, fences and quotes are never spawn instructions', () => {
|
||||
const md = [
|
||||
'The alpha-agent is dormant and is never spawned.',
|
||||
'Never launch `beta-agent`.',
|
||||
'In quick mode we used to launch `gamma-agent`.',
|
||||
'<!-- Launch the `delta-agent` agent -->',
|
||||
'<!--',
|
||||
'Launch the `epsilon-agent` agent',
|
||||
'-->',
|
||||
'```',
|
||||
'Launch the `zeta-agent` agent',
|
||||
'| Agent |',
|
||||
'|---|',
|
||||
'| `eta-agent` |',
|
||||
'```',
|
||||
'> Launch the `theta-agent` agent',
|
||||
].join('\n');
|
||||
const names = spawnedNames(md);
|
||||
for (const n of ['alpha-agent', 'beta-agent', 'gamma-agent', 'delta-agent', 'epsilon-agent', 'zeta-agent', 'eta-agent', 'theta-agent']) {
|
||||
assert.ok(!names.has(n), `${n} must not count as spawned`);
|
||||
}
|
||||
});
|
||||
|
||||
// --- the tallies, both directions ------------------------------------------
|
||||
|
||||
test('defects tally: > 0 while a check holds, 0 once fixed; not-fellable counts as open', () => {
|
||||
const defect = {
|
||||
id: 'D-X',
|
||||
summary: 'removed tool still referenced',
|
||||
probe: 'phrase',
|
||||
check: [{ path: 'commands/plan.md', pattern: '\\bTeamCreate\\b', expect: 'match' }],
|
||||
};
|
||||
const unfellable = { id: 'D-Y', summary: 'no runnable check yet', check: null };
|
||||
const base = { 'STATE.md': '## Open decisions\n', 'agents/a.md': 'x\n', 'commands/plan.md': 'Launch `a`\n' };
|
||||
|
||||
const red = fixture({ ...base, 'commands/plan.md': 'Launch `a` and TeamCreate\n' });
|
||||
const green = fixture(base);
|
||||
try {
|
||||
let row = rowById(measure(red, registry({ defects: [defect] })), 'defects');
|
||||
assert.equal(row.open, 1);
|
||||
assert.equal(row.total, 1);
|
||||
row = rowById(measure(green, registry({ defects: [defect] })), 'defects');
|
||||
assert.equal(row.open, 0);
|
||||
row = rowById(measure(green, registry({ defects: [defect, unfellable] })), 'defects');
|
||||
assert.equal(row.open, 1, 'a defect without a runnable check stays open');
|
||||
assert.equal(row.items.find((i) => i.id === 'D-Y').status, 'not-fellable');
|
||||
} finally { cleanup(red, green); }
|
||||
});
|
||||
|
||||
test('experiments tally: an env-gated opt-in is open until its gate is gone', () => {
|
||||
const exp = {
|
||||
id: 'E-X',
|
||||
summary: 'default-off loop',
|
||||
probe: 'phrase',
|
||||
check: [{ path: ['commands/*.md', 'lib/**/*.mjs'], pattern: 'LOOP_ENABLED', expect: 'match' }],
|
||||
};
|
||||
const base = { 'STATE.md': '## Open decisions\n', 'agents/a.md': 'x\n', 'commands/r.md': 'Launch `a`\n' };
|
||||
const red = fixture({ ...base, 'lib/util/cap.mjs': 'if (process.env.LOOP_ENABLED) {}\n' });
|
||||
const green = fixture({ ...base, 'lib/util/cap.mjs': 'export const x = 1;\n' });
|
||||
try {
|
||||
assert.equal(rowById(measure(red, registry({ experiments: [exp] })), 'experiments').open, 1);
|
||||
assert.equal(rowById(measure(green, registry({ experiments: [exp] })), 'experiments').open, 0);
|
||||
} finally { cleanup(red, green); }
|
||||
});
|
||||
|
||||
test('dormant-agents tally: an agent no command spawns counts; reference docs never do', () => {
|
||||
const files = {
|
||||
'STATE.md': '## Open decisions\n',
|
||||
'agents/used-agent.md': '---\nname: used-agent\ndescription: |\n does work\n---\n',
|
||||
'agents/idle-agent.md': '---\nname: idle-agent\ndescription: |\n never wired\n---\n',
|
||||
'agents/some-orchestrator.md': '---\nname: some-orchestrator\ndescription: Reference document, not a spawnable capability — docs\n---\n',
|
||||
'commands/run.md': 'Launch the **used-agent** agent.\n',
|
||||
};
|
||||
const red = fixture(files);
|
||||
const green = fixture({ ...files, 'commands/other.md': 'Spawn `idle-agent` in parallel.\n' });
|
||||
try {
|
||||
let row = rowById(measure(red, registry()), 'dormant-agents');
|
||||
assert.equal(row.open, 1);
|
||||
assert.equal(row.total, 2, 'denominator = spawnable agents (reference docs excluded)');
|
||||
assert.deepEqual(row.items.filter((i) => i.status === 'open').map((i) => i.id), ['idle-agent']);
|
||||
row = rowById(measure(green, registry()), 'dormant-agents');
|
||||
assert.equal(row.open, 0);
|
||||
} finally { cleanup(red, green); }
|
||||
});
|
||||
|
||||
test('dormant-agents tally: a prose sentence or an HTML comment does not wake a dormant agent', () => {
|
||||
const files = {
|
||||
'STATE.md': '## Open decisions\n',
|
||||
'agents/idle-agent.md': '---\nname: idle-agent\ndescription: |\n never wired\n---\n',
|
||||
'commands/plan.md': 'The idle-agent is dormant and is never spawned.\n',
|
||||
'commands/end.md': '<!-- idle-agent -->\nThe research-idle-agents phase.\n',
|
||||
};
|
||||
const dir = fixture(files);
|
||||
try {
|
||||
assert.equal(rowById(measure(dir, registry()), 'dormant-agents').open, 1);
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
test('dormant-agents tally: an unmeasurable agents dir is n/a (null), never 0', () => {
|
||||
const dir = fixture({ 'STATE.md': '## Open decisions\n', 'commands/r.md': 'x\n' });
|
||||
try {
|
||||
const result = measure(dir, registry());
|
||||
const row = rowById(result, 'dormant-agents');
|
||||
assert.equal(row.open, null);
|
||||
assert.equal(row.total, null);
|
||||
assert.match(row.detail, /not measurable/);
|
||||
assert.equal(result.green, false);
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
test('decisions tally: counts open markers inside the section only, and says it counts ticks', () => {
|
||||
const state = [
|
||||
'# STATE',
|
||||
'- [ ] outside the section, ignored',
|
||||
'## Open decisions',
|
||||
'- [ ] first open',
|
||||
'- [x] already decided',
|
||||
'- [ ] second open',
|
||||
'## Next section',
|
||||
'- [ ] also ignored',
|
||||
'',
|
||||
].join('\n');
|
||||
const red = fixture({ 'STATE.md': state, 'agents/a.md': 'x\n', 'commands/r.md': 'Launch `a`\n' });
|
||||
const green = fixture({
|
||||
'STATE.md': '## Open decisions\n- [x] done\n## Next\n',
|
||||
'agents/a.md': 'x\n',
|
||||
'commands/r.md': 'Launch `a`\n',
|
||||
});
|
||||
try {
|
||||
let row = rowById(measure(red, registry()), 'decisions');
|
||||
assert.equal(row.open, 2);
|
||||
assert.equal(row.total, 3);
|
||||
assert.match(row.detail, /tick/i, 'the output must say a tick is not verified as a decision');
|
||||
row = rowById(measure(green, registry()), 'decisions');
|
||||
assert.equal(row.open, 0);
|
||||
assert.equal(row.total, 1);
|
||||
} finally { cleanup(red, green); }
|
||||
});
|
||||
|
||||
test('decisions tally: a missing STATE.md or section is NOT MEASURABLE (null), never 0', () => {
|
||||
const noState = fixture({ 'agents/a.md': 'x\n', 'commands/r.md': 'Launch `a`\n' });
|
||||
const noSection = fixture({ 'STATE.md': '# STATE\n- [ ] x\n', 'agents/a.md': 'x\n', 'commands/r.md': 'Launch `a`\n' });
|
||||
try {
|
||||
for (const dir of [noState, noSection]) {
|
||||
const result = measure(dir, registry());
|
||||
const row = rowById(result, 'decisions');
|
||||
assert.equal(row.open, null);
|
||||
assert.ok(row.detail.length > 0, 'the reason is reported');
|
||||
assert.equal(result.green, false, 'an unmeasurable tally keeps the gate red');
|
||||
}
|
||||
} finally { cleanup(noState, noSection); }
|
||||
});
|
||||
|
||||
test('decisions tally: an unmarked list item in the section is format drift → NOT MEASURABLE', () => {
|
||||
// A STATE written without the marker convention must not read as "0 open".
|
||||
for (const item of ['1. **Some decision?** prose', '- plain bullet decision', '* star bullet']) {
|
||||
const dir = fixture({
|
||||
'STATE.md': `## Open decisions\n- [ ] marked\n${item}\n## Next\n`,
|
||||
'agents/a.md': 'x\n',
|
||||
'commands/r.md': 'Launch `a`\n',
|
||||
});
|
||||
try {
|
||||
const row = rowById(measure(dir, registry()), 'decisions');
|
||||
assert.equal(row.open, null, `unmarked "${item}" must make the tally unmeasurable`);
|
||||
assert.match(row.detail, /unmarked/);
|
||||
} finally { cleanup(dir); }
|
||||
}
|
||||
});
|
||||
|
||||
test('feat-after-freeze tally: not a git repo → n/a (red), never 0', () => {
|
||||
const dir = fixture({ 'STATE.md': '## Open decisions\n' });
|
||||
try {
|
||||
const result = measure(dir, registry());
|
||||
const row = rowById(result, 'feat-after-freeze');
|
||||
assert.equal(row.open, null);
|
||||
assert.match(row.detail, /not measured/);
|
||||
assert.equal(result.green, false);
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
test('feat-after-freeze tally: no freeze tag yet → n/a (red), and the detail names the tag', () => {
|
||||
const dir = fixture({ 'STATE.md': '## Open decisions\n' });
|
||||
try {
|
||||
gitInit(dir);
|
||||
const row = rowById(measure(dir, registry()), 'feat-after-freeze');
|
||||
assert.equal(row.open, null);
|
||||
assert.match(row.detail, /end-state-freeze/);
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
test('feat-after-freeze tally: counts feat commits after the tag, 0 when there are none', () => {
|
||||
const dir = fixture({ 'STATE.md': '## Open decisions\n' });
|
||||
try {
|
||||
gitInit(dir);
|
||||
gitCommit(dir, 'feat: before the freeze does not count');
|
||||
git(dir, 'tag', 'end-state-freeze');
|
||||
let row = rowById(measure(dir, registry()), 'feat-after-freeze');
|
||||
assert.equal(row.open, 0);
|
||||
assert.equal(row.total, 0);
|
||||
gitCommit(dir, 'fix(x): a defect fix is allowed');
|
||||
gitCommit(dir, 'feature: not a conventional feat');
|
||||
row = rowById(measure(dir, registry()), 'feat-after-freeze');
|
||||
assert.equal(row.open, 0);
|
||||
assert.equal(row.total, 2);
|
||||
gitCommit(dir, 'feat(research): a new capability');
|
||||
gitCommit(dir, 'feat!: a breaking capability');
|
||||
row = rowById(measure(dir, registry()), 'feat-after-freeze');
|
||||
assert.equal(row.open, 2);
|
||||
assert.equal(row.total, 4);
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
// --- registry integrity (the judged party must not own the ledger) ---------
|
||||
|
||||
const SAMPLE = registry({
|
||||
defects: [
|
||||
{ id: 'D-01', summary: 'first defect', probe: 'phrase', check: [{ path: 'a.md', pattern: 'bad', expect: 'match' }] },
|
||||
{ id: 'D-02', summary: 'second defect', probe: 'phrase', check: [{ path: 'a.md', pattern: 'worse', expect: 'match' }] },
|
||||
],
|
||||
experiments: [
|
||||
{ id: 'E-01', summary: 'first experiment', probe: 'phrase', check: [{ path: 'a.md', pattern: 'FLAG', expect: 'match' }] },
|
||||
{ id: 'E-02', summary: 'second experiment', probe: 'phrase', check: [{ path: 'a.md', pattern: 'OPT', expect: 'match' }] },
|
||||
],
|
||||
});
|
||||
const clone = (v) => JSON.parse(JSON.stringify(v));
|
||||
|
||||
test('verifyIntegrity: the frozen registry itself is intact; a new entry may be added', () => {
|
||||
const frozen = frozenFor(SAMPLE);
|
||||
assert.deepEqual(verifyIntegrity(SAMPLE, frozen), { ok: true, violations: [] });
|
||||
const grown = clone(SAMPLE);
|
||||
grown.defects.push({ id: 'D-03', summary: 'found later', probe: 'phrase', check: [{ path: 'a.md', pattern: 'new', expect: 'match' }] });
|
||||
assert.equal(verifyIntegrity(grown, frozen).ok, true);
|
||||
});
|
||||
|
||||
test('verifyIntegrity: every registry attack is a violation that names what moved', () => {
|
||||
const frozen = frozenFor(SAMPLE);
|
||||
const attacks = {
|
||||
'delete a defect': (r) => { r.defects = r.defects.filter((d) => d.id !== 'D-02'); },
|
||||
'delete an experiment': (r) => { r.experiments = r.experiments.filter((e) => e.id !== 'E-01'); },
|
||||
'weaken a defect check': (r) => { r.defects[0].check[0].pattern = '^NEVER MATCHES$'; },
|
||||
'weaken an experiment check': (r) => { r.experiments[1].check[0].expect = 'no-match'; },
|
||||
'drop a condition': (r) => { r.defects[0].check = []; },
|
||||
'point spawnSites at agents': (r) => { r.agents = { ...r.agents, spawnSites: 'agents/*.md' }; },
|
||||
'change the reference marker': (r) => { r.agents = { ...r.agents, referenceMarker: 'DORMANT' }; },
|
||||
'move the decisions section': (r) => { r.decisions = { ...r.decisions, section: '## Elsewhere' }; },
|
||||
'change the open marker': (r) => { r.decisions = { ...r.decisions, open: '^NEVER' }; },
|
||||
'rename the freeze tag': (r) => { r.freeze = { ...r.freeze, tag: 'some-future-tag' }; },
|
||||
'duplicate an id': (r) => { r.defects.push(clone(r.defects[0])); },
|
||||
};
|
||||
for (const [name, attack] of Object.entries(attacks)) {
|
||||
const reg = clone(SAMPLE);
|
||||
attack(reg);
|
||||
const v = verifyIntegrity(reg, frozen);
|
||||
assert.equal(v.ok, false, `${name} must be caught`);
|
||||
assert.ok(v.violations.length > 0 && v.violations.every((s) => typeof s === 'string' && s.length > 0), name);
|
||||
}
|
||||
});
|
||||
|
||||
test('verifyIntegrity: spawnSites may never include agents/, even if the manifest says so', () => {
|
||||
const reg = clone(SAMPLE);
|
||||
reg.agents = { ...reg.agents, spawnSites: 'agents/*.md' };
|
||||
const v = verifyIntegrity(reg, frozenFor(reg));
|
||||
assert.equal(v.ok, false);
|
||||
assert.ok(v.violations.some((s) => /agents\//.test(s)));
|
||||
});
|
||||
|
||||
test('checkSignature: key order does not matter; every field of a condition does', () => {
|
||||
const a = [{ path: 'a.md', pattern: 'x', flags: 'm', expect: 'match' }];
|
||||
const reordered = [{ expect: 'match', flags: 'm', pattern: 'x', path: 'a.md' }];
|
||||
assert.equal(checkSignature(a), checkSignature(reordered));
|
||||
for (const change of [{ path: 'b.md' }, { pattern: 'y' }, { flags: '' }, { expect: 'no-match' }, { section: '## 1.' }]) {
|
||||
assert.notEqual(checkSignature([{ ...a[0], ...change }]), checkSignature(a), JSON.stringify(change));
|
||||
}
|
||||
assert.match(checkSignature(a), /^[0-9a-f]{64}$/);
|
||||
});
|
||||
|
||||
test('measure: a missing frozen manifest makes integrity n/a and keeps the gate red', () => {
|
||||
const dir = fixture({ 'STATE.md': '## Open decisions\n' });
|
||||
try {
|
||||
const result = measure(dir, registry());
|
||||
assert.equal(result.integrity.ok, null);
|
||||
assert.match(result.integrity.detail, /end-state-frozen\.json/);
|
||||
assert.equal(result.green, false);
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
// --- render ----------------------------------------------------------------
|
||||
|
||||
test('render: the header counts only tallies that ARE 0, never n/a ones', () => {
|
||||
const result = {
|
||||
green: false,
|
||||
integrity: { ok: true, violations: [], detail: '' },
|
||||
rows: [
|
||||
{ id: 'defects', label: 'l', source: 's', open: 0, total: 1, target: 0, items: [], detail: '' },
|
||||
{ id: 'experiments', label: 'l', source: 's', open: null, total: null, target: 0, items: [], detail: 'not measurable: x' },
|
||||
{ id: 'dormant-agents', label: 'l', source: 's', open: 0, total: 3, target: 0, items: [], detail: '' },
|
||||
],
|
||||
};
|
||||
const text = render(result);
|
||||
assert.match(text, /RED \(2 of 3 tallies at 0\)/);
|
||||
assert.match(text, /\| experiments \| n\/a \| n\/a \|/);
|
||||
});
|
||||
|
||||
test('render: open phrase probes are labelled, closed ids are listed, integrity is shown', () => {
|
||||
const result = {
|
||||
green: false,
|
||||
integrity: { ok: false, violations: ['D-02 removed from the registry'], detail: '' },
|
||||
rows: [{
|
||||
id: 'defects', label: 'open pipeline defects', source: 's', open: 1, total: 2, target: 0, detail: '',
|
||||
items: [
|
||||
{ id: 'D-01', summary: 'still open', probe: 'phrase', status: 'open', detail: '' },
|
||||
{ id: 'D-06', summary: 'fixed', probe: 'byte', status: 'closed', detail: '' },
|
||||
],
|
||||
}],
|
||||
};
|
||||
const text = render(result);
|
||||
assert.match(text, /\[open · phrase probe\] D-01/);
|
||||
assert.match(text, /closed: D-06/);
|
||||
assert.match(text, /registry integrity: VIOLATED/);
|
||||
assert.match(text, /D-02 removed from the registry/);
|
||||
});
|
||||
|
||||
// --- CLI -------------------------------------------------------------------
|
||||
|
||||
function writeGateFiles(dir, reg, frozen = frozenFor(reg)) {
|
||||
mkdirSync(join(dir, 'scripts'), { recursive: true });
|
||||
writeFileSync(join(dir, 'scripts', 'end-state-registry.json'), JSON.stringify(reg, null, 2));
|
||||
mkdirSync(join(dir, 'tests', 'fixtures'), { recursive: true });
|
||||
writeFileSync(join(dir, 'tests', 'fixtures', 'end-state-frozen.json'), JSON.stringify(frozen, null, 2));
|
||||
}
|
||||
|
||||
function runGate(dir, extra = []) {
|
||||
return spawnSync(process.execPath, [GATE, '--root', dir, ...extra], { encoding: 'utf8' });
|
||||
}
|
||||
|
||||
const ALL_IDS = ['defects', 'experiments', 'dormant-agents', 'decisions', 'feat-after-freeze'];
|
||||
|
||||
test('CLI: all five tallies at 0 with an intact registry → green, exit 0; --json carries the rows', () => {
|
||||
const dir = fixture({
|
||||
'STATE.md': '## Open decisions\n- [x] done\n',
|
||||
'agents/a.md': '---\nname: a\n---\n',
|
||||
'commands/r.md': 'Spawn `a`\n',
|
||||
});
|
||||
try {
|
||||
writeGateFiles(dir, registry());
|
||||
gitInit(dir);
|
||||
git(dir, 'tag', 'end-state-freeze');
|
||||
const r = runGate(dir, ['--json']);
|
||||
assert.equal(r.status, 0, r.stdout + r.stderr);
|
||||
const out = JSON.parse(r.stdout);
|
||||
assert.equal(out.green, true);
|
||||
assert.equal(out.integrity.ok, true);
|
||||
assert.deepEqual(out.rows.map((x) => x.id), ALL_IDS);
|
||||
assert.ok(out.rows.every((x) => x.open === 0 && x.target === 0 && typeof x.source === 'string'));
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
test('CLI: an intact registry with every measurable tally at 0 but no freeze tag → red, exit 1', () => {
|
||||
// n/a must never be read as 0 — also when nothing else is wrong.
|
||||
const dir = fixture({
|
||||
'STATE.md': '## Open decisions\n- [x] done\n',
|
||||
'agents/a.md': '---\nname: a\n---\n',
|
||||
'commands/r.md': 'Spawn `a`\n',
|
||||
});
|
||||
try {
|
||||
writeGateFiles(dir, registry());
|
||||
gitInit(dir);
|
||||
const r = runGate(dir, ['--json']);
|
||||
assert.equal(r.status, 1, r.stdout + r.stderr);
|
||||
const out = JSON.parse(r.stdout);
|
||||
assert.equal(out.integrity.ok, true);
|
||||
assert.equal(out.green, false);
|
||||
assert.equal(out.rows.find((x) => x.id === 'feat-after-freeze').open, null);
|
||||
assert.ok(out.rows.filter((x) => x.id !== 'feat-after-freeze').every((x) => x.open === 0));
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
test('CLI: all tallies at 0 but a tampered registry → red, exit 1 (the combined green attack)', () => {
|
||||
const dir = fixture({
|
||||
'STATE.md': '## Open decisions\n- [x] ticked\n',
|
||||
'agents/a.md': '---\nname: a\n---\n',
|
||||
'commands/r.md': 'Spawn `a`\n',
|
||||
});
|
||||
try {
|
||||
const honest = registry({ defects: [{ id: 'D-01', summary: 'a real defect', probe: 'phrase', check: [{ path: 'commands/r.md', pattern: 'Spawn', expect: 'match' }] }] });
|
||||
const tampered = registry(); // the open defect simply left the ledger
|
||||
writeGateFiles(dir, tampered, frozenFor(honest));
|
||||
gitInit(dir);
|
||||
git(dir, 'tag', 'end-state-freeze');
|
||||
const r = runGate(dir);
|
||||
assert.equal(r.status, 1, r.stdout + r.stderr);
|
||||
assert.match(r.stdout, /RED/);
|
||||
assert.match(r.stdout, /VIOLATED/);
|
||||
assert.match(r.stdout, /D-01/);
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
test('CLI: any tally > 0 → red, exit 1; the table names every row', () => {
|
||||
const dir = fixture({
|
||||
'STATE.md': '## Open decisions\n- [ ] undecided\n',
|
||||
'agents/a.md': '---\nname: a\n---\n',
|
||||
'commands/r.md': 'Spawn `a`\n',
|
||||
});
|
||||
try {
|
||||
writeGateFiles(dir, registry());
|
||||
const r = runGate(dir);
|
||||
assert.equal(r.status, 1, r.stderr);
|
||||
for (const label of ALL_IDS) assert.match(r.stdout, new RegExp(label));
|
||||
assert.match(r.stdout, /RED/);
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
test('CLI: an unreadable registry is a usage error (exit 2), not a verdict', () => {
|
||||
const dir = fixture({ 'STATE.md': '## Open decisions\n' });
|
||||
try {
|
||||
const r = runGate(dir);
|
||||
assert.equal(r.status, 2);
|
||||
assert.match(r.stderr, /end-state-registry\.json/);
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
// --- the real repo: denominator pinned, state never pinned -----------------
|
||||
|
||||
test('the frozen denominator: tests/fixtures/end-state-frozen.json is exactly the literal above', () => {
|
||||
const file = loadFrozen(ROOT);
|
||||
const { why, ...rest } = file;
|
||||
assert.ok(typeof why === 'string' && why.length > 20, 'the manifest says why it is frozen');
|
||||
assert.deepEqual(rest, FROZEN);
|
||||
});
|
||||
|
||||
test('the frozen denominator: the real registry is intact against it', () => {
|
||||
const v = verifyIntegrity(loadRegistry(ROOT), FROZEN);
|
||||
assert.deepEqual(v, { ok: true, violations: [] }, v.violations.join('\n'));
|
||||
});
|
||||
|
||||
test('real registry: every counted entry has an id, a summary, a probe kind and a check that RUNS here', () => {
|
||||
const reg = loadRegistry(ROOT);
|
||||
const entries = [...reg.defects, ...reg.experiments];
|
||||
const ids = new Set();
|
||||
for (const e of entries) {
|
||||
assert.match(e.id, /^[DE]-\d{2}$/, `bad id ${e.id}`);
|
||||
assert.ok(!ids.has(e.id), `duplicate id ${e.id}`);
|
||||
ids.add(e.id);
|
||||
assert.ok(typeof e.summary === 'string' && e.summary.length > 10, `${e.id} needs a summary`);
|
||||
assert.ok(Object.keys(PROBE_NOTES).includes(e.probe), `${e.id} must declare a probe kind the gate can explain`);
|
||||
// A check that cannot evaluate against this very repo is a typo, not a finding.
|
||||
assert.notEqual(evaluateCheck(ROOT, e.check).status, 'not-fellable', `${e.id} check does not run here`);
|
||||
}
|
||||
});
|
||||
|
||||
test('real repo: five rows, open is a count or null, exit code agrees with rows + integrity', () => {
|
||||
const r = spawnSync(process.execPath, [GATE, '--json'], { encoding: 'utf8', cwd: ROOT });
|
||||
assert.ok(r.status === 0 || r.status === 1, r.stderr);
|
||||
const out = JSON.parse(r.stdout);
|
||||
assert.deepEqual(out.rows.map((x) => x.id), ALL_IDS);
|
||||
for (const row of out.rows) {
|
||||
assert.ok(row.open === null || (Number.isInteger(row.open) && row.open >= 0), `${row.id}: ${row.open}`);
|
||||
}
|
||||
const green = out.rows.every((row) => row.open === 0) && out.integrity.ok === true;
|
||||
assert.equal(out.green, green);
|
||||
assert.equal(r.status, green ? 0 : 1);
|
||||
assert.ok(existsSync(join(ROOT, 'scripts', 'end-state-registry.json')));
|
||||
assert.ok(readFileSync(join(ROOT, 'tests', 'fixtures', 'end-state-frozen.json'), 'utf8').length > 0);
|
||||
});
|
||||
|
||||
// --- probe labelling -------------------------------------------------------
|
||||
//
|
||||
// The gate labelled only `phrase` probes, so every other probe kind rendered
|
||||
// unlabelled — indistinguishable from an entry that declared nothing. Once
|
||||
// D-03/D-04 became behaviour probes (operator decision 2026-09-18) the reader
|
||||
// needs to see WHICH kind of evidence each closed entry rests on.
|
||||
|
||||
test('PROBE_NOTES: every probe kind the registry may declare carries a note', () => {
|
||||
assert.deepEqual(Object.keys(PROBE_NOTES).sort(), ['behaviour', 'byte', 'phrase']);
|
||||
for (const [kind, note] of Object.entries(PROBE_NOTES)) {
|
||||
assert.ok(note.length > 20, `${kind} needs a note that says what the probe can and cannot prove`);
|
||||
}
|
||||
});
|
||||
|
||||
test('render: every probe kind is labelled, open and closed — not just phrase', () => {
|
||||
const text = render({
|
||||
green: false,
|
||||
integrity: { ok: true, violations: [], detail: '' },
|
||||
rows: [{
|
||||
id: 'defects', label: 'open pipeline defects', source: 's', open: 2, total: 3, target: 0, detail: '',
|
||||
items: [
|
||||
{ id: 'D-01', summary: 'still open', probe: 'phrase', status: 'open', detail: '' },
|
||||
{ id: 'D-03', summary: 'still open', probe: 'behaviour', status: 'open', detail: '' },
|
||||
{ id: 'D-06', summary: 'fixed', probe: 'byte', status: 'closed', detail: '' },
|
||||
],
|
||||
}],
|
||||
});
|
||||
assert.match(text, /\[open · phrase probe\] D-01/);
|
||||
assert.match(text, /\[open · behaviour probe\] D-03/);
|
||||
assert.match(text, /closed: D-06 \(byte probe\)/);
|
||||
});
|
||||
|
||||
test('the detail line carries one note per probe kind PRESENT, and none for kinds absent', () => {
|
||||
const dir = fixture({
|
||||
'a.md': 'bad\n',
|
||||
'STATE.md': '## Open decisions\n- [x] done\n',
|
||||
'agents/a.md': '---\nname: a\n---\n',
|
||||
'commands/r.md': 'Spawn `a`\n',
|
||||
});
|
||||
try {
|
||||
writeGateFiles(dir, registry({
|
||||
defects: [
|
||||
{ id: 'D-01', summary: 'a phrase defect', probe: 'phrase', check: [{ path: 'a.md', pattern: 'bad', expect: 'match' }] },
|
||||
{ id: 'D-02', summary: 'a behaviour defect', probe: 'behaviour', check: [{ path: 'a.md', pattern: 'nope', expect: 'match' }] },
|
||||
],
|
||||
}));
|
||||
gitInit(dir);
|
||||
const out = JSON.parse(runGate(dir, ['--json']).stdout);
|
||||
const detail = rowById(out, 'defects').detail;
|
||||
assert.match(detail, /phrase probe closes on rewording/);
|
||||
assert.match(detail, /behaviour probe/);
|
||||
assert.ok(!/byte probe/.test(detail), 'a kind no entry declares must not be explained');
|
||||
assert.equal(rowById(out, 'experiments').detail, '', 'an empty row explains nothing');
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
// --- a probe that RUNS a test, and the M6 mutant ---------------------------
|
||||
//
|
||||
// Measured 2026-09-18 (re-measured on 6cafb4c): a two-line file
|
||||
// export function runPlanVerification() { return { ok: true }; }
|
||||
// export function formatCriteriaEvidence() { return ''; }
|
||||
// dropped into an otherwise UNFIXED tree closed both D-03 and D-04, and the
|
||||
// gate reported "defects 0 of 7, registry intact". A grep condition can only
|
||||
// ask whether a string is present, so a behaviour probe built from one is a
|
||||
// phrase probe pointed at code. A condition that RUNS a named test and reads
|
||||
// its TAP result cannot be closed that way.
|
||||
|
||||
const M6_STUB = [
|
||||
'export function runPlanVerification() { return { ok: true }; }',
|
||||
"export function formatCriteriaEvidence() { return ''; }",
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
// A fixture pair: a module, and a test that BINDS it. `green one` passes on
|
||||
// the tree and fails against a stub of the module - which is what makes it a
|
||||
// behaviour probe rather than a name.
|
||||
const MODULE = 'export function answer() { return 42; }\n';
|
||||
|
||||
const TEST_FILE = [
|
||||
"import { test } from 'node:test';",
|
||||
"import { strict as assert } from 'node:assert';",
|
||||
"import { answer } from '../lib/m.mjs';",
|
||||
"test('green one', () => { assert.equal(answer(), 42); });",
|
||||
"test('red one', () => { assert.ok(false, 'red on purpose'); });",
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
test('a test condition RUNS the named test and reads its result', () => {
|
||||
const dir = fixture({ 'tests/t.test.mjs': TEST_FILE, 'lib/m.mjs': MODULE });
|
||||
try {
|
||||
const cond = (name, expect) => ({ test: 'tests/t.test.mjs', name, expect, stubs: ['lib/m.mjs'] });
|
||||
assert.equal(evaluateCondition(dir, cond('green one', 'passes')), true);
|
||||
assert.equal(evaluateCondition(dir, cond('green one', 'fails')), false);
|
||||
assert.equal(evaluateCondition(dir, cond('red one', 'passes')), false);
|
||||
assert.equal(evaluateCondition(dir, cond('red one', 'fails')), true);
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
test('a test condition naming a test that does not run is NOT FELLABLE, never closed', () => {
|
||||
const dir = fixture({ 'tests/t.test.mjs': TEST_FILE, 'lib/m.mjs': MODULE });
|
||||
try {
|
||||
for (const cond of [
|
||||
{ test: 'tests/t.test.mjs', name: 'no such test', expect: 'fails', stubs: ['lib/m.mjs'] },
|
||||
{ test: 'tests/missing.test.mjs', name: 'green one', expect: 'fails', stubs: ['lib/m.mjs'] },
|
||||
]) {
|
||||
const r = evaluateCheck(dir, [cond]);
|
||||
assert.equal(r.status, 'not-fellable', JSON.stringify(cond));
|
||||
assert.ok(r.detail.length > 0, 'the gate must say WHY the check could not fire');
|
||||
}
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
test('M6 mutant: a stub exporting both symbols does NOT close D-03 or D-04', () => {
|
||||
const dir = fixture({ 'lib/cosmetic/stub.mjs': M6_STUB });
|
||||
try {
|
||||
const entries = loadRegistry(ROOT).defects.filter((e) => e.id === 'D-03' || e.id === 'D-04');
|
||||
assert.equal(entries.length, 2, 'D-03 and D-04 are still in the registry');
|
||||
for (const e of entries) {
|
||||
assert.notEqual(
|
||||
evaluateCheck(dir, e.check).status, 'closed',
|
||||
`${e.id} closed on a tree where nothing is fixed: ${JSON.stringify(e.check)}`,
|
||||
);
|
||||
}
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
test('the gate states out loud that the wiring is pinned by text, not proven', () => {
|
||||
const r = spawnSync(process.execPath, [GATE], { encoding: 'utf8', cwd: ROOT });
|
||||
assert.ok(r.status === 0 || r.status === 1, r.stderr);
|
||||
assert.match(r.stdout, /wiring: /);
|
||||
assert.match(r.stdout, /pinned by TEXT/);
|
||||
assert.match(r.stdout, /plugin-eval/);
|
||||
});
|
||||
|
||||
// --- M7: a probe must bind the test's CONTENT, not its name ---------------
|
||||
//
|
||||
// Measured 2026-09-18 (re-measured on e1e7bdf): the cosmetic close came back
|
||||
// one level up. The probe reads the test file from the SAME tree it measures,
|
||||
// so a four-line file holding two EMPTY tests with the two named names —
|
||||
// test('runPlanVerification: a plan whose success criterion FAILS fells the run', () => {});
|
||||
// test('formatCriteriaEvidence: one row per criterion, with command and exit code', () => {});
|
||||
// — closed D-03 and D-04 on a tree where `lib/verification/` did not exist at
|
||||
// all: "defects 0 of 7, registry intact". An empty test is the new two-line
|
||||
// stub. The only form that cannot be closed by a name is one that requires the
|
||||
// named test to FELL a mutant: the gate runs it a second time against a tree
|
||||
// where the module it binds is replaced by a stub exporting the same names and
|
||||
// doing nothing. A test that passes against both is not a behaviour probe.
|
||||
|
||||
const EMPTY_NAMED_TESTS = [
|
||||
"import test from 'node:test';",
|
||||
"test('runPlanVerification: a plan whose success criterion FAILS fells the run', () => {});",
|
||||
"test('formatCriteriaEvidence: one row per criterion, with command and exit code', () => {});",
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
const REAL_RUNNER = join(ROOT, 'lib', 'verification', 'criteria-runner.mjs');
|
||||
const REAL_RUNNER_TEST = join(ROOT, 'tests', 'lib', 'criteria-runner.test.mjs');
|
||||
|
||||
test('M7 mutant: two EMPTY tests with the right names do NOT close D-03 or D-04', () => {
|
||||
// The checkpoint's reproduction: an unfixed tree (no lib/verification/) plus
|
||||
// the four-line test file.
|
||||
const dir = fixture({ 'tests/lib/criteria-runner.test.mjs': EMPTY_NAMED_TESTS });
|
||||
try {
|
||||
const entries = loadRegistry(ROOT).defects.filter((e) => e.id === 'D-03' || e.id === 'D-04');
|
||||
assert.equal(entries.length, 2, 'D-03 and D-04 are still in the registry');
|
||||
for (const e of entries) {
|
||||
const r = evaluateCheck(dir, e.check);
|
||||
assert.equal(r.status, 'not-fellable', `${e.id} closed on a tree where nothing is fixed`);
|
||||
assert.ok(r.detail.length > 0, 'the gate must say WHY the check could not fire');
|
||||
}
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
test('M7 mutant: an empty named test does not close the defect even when the module IS present', () => {
|
||||
// The harder case: the real module is there, so the stub CAN be built — and
|
||||
// the empty test passes against it, which is exactly the proof that the test
|
||||
// binds the name and nothing else.
|
||||
const dir = fixture({
|
||||
'tests/lib/criteria-runner.test.mjs': EMPTY_NAMED_TESTS,
|
||||
'lib/verification/criteria-runner.mjs': readFileSync(REAL_RUNNER, 'utf8'),
|
||||
});
|
||||
try {
|
||||
for (const e of loadRegistry(ROOT).defects.filter((x) => x.id === 'D-03' || x.id === 'D-04')) {
|
||||
const r = evaluateCheck(dir, e.check);
|
||||
assert.equal(r.status, 'not-fellable', `${e.id} closed on an empty test`);
|
||||
assert.match(r.detail, /stub/i, 'the gate must name the mutant the test failed to fell');
|
||||
}
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
test("D-03/D-04 still CLOSE on the real tree — the named tests do fell a stub", () => {
|
||||
// The positive control. Without it, "not closed" everywhere would read as a
|
||||
// working probe when the probe is simply broken.
|
||||
for (const e of loadRegistry(ROOT).defects.filter((x) => x.id === 'D-03' || x.id === 'D-04')) {
|
||||
const r = evaluateCheck(ROOT, e.check);
|
||||
assert.equal(r.status, 'closed', `${e.id} is not closed on the real tree: ${r.detail}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('a test condition must name the module its test binds, or it cannot fire', () => {
|
||||
const dir = fixture({ 'tests/t.test.mjs': TEST_FILE, 'lib/m.mjs': MODULE });
|
||||
try {
|
||||
for (const cond of [
|
||||
{ test: 'tests/t.test.mjs', name: 'green one', expect: 'passes' },
|
||||
{ test: 'tests/t.test.mjs', name: 'green one', expect: 'passes', stubs: [] },
|
||||
{ test: 'tests/t.test.mjs', name: 'green one', expect: 'passes', stubs: ['lib/gone.mjs'] },
|
||||
]) {
|
||||
const r = evaluateCheck(dir, [cond]);
|
||||
assert.equal(r.status, 'not-fellable', JSON.stringify(cond));
|
||||
assert.ok(r.detail.length > 0, 'the gate must say WHY');
|
||||
}
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
test('the stub mutant leaves the measured tree untouched', () => {
|
||||
const dir = fixture({ 'tests/t.test.mjs': TEST_FILE, 'lib/m.mjs': MODULE });
|
||||
try {
|
||||
const before = readFileSync(join(dir, 'lib', 'm.mjs'), 'utf8');
|
||||
evaluateCheck(dir, [{ test: 'tests/t.test.mjs', name: 'green one', expect: 'passes', stubs: ['lib/m.mjs'] }]);
|
||||
assert.equal(readFileSync(join(dir, 'lib', 'm.mjs'), 'utf8'), before, 'the mutation must happen in a copy, never in place');
|
||||
} finally { cleanup(dir); }
|
||||
});
|
||||
|
||||
test('the frozen file claims no authority it does not have', () => {
|
||||
// Measured the same day: the tracked frozen file claimed its second
|
||||
// amendment was operator-authorised. No operator authorised it; it followed
|
||||
// from a maintenance decision. A file whose whole job is to be an explicit
|
||||
// decision trail may not overclaim in either direction.
|
||||
const why = loadFrozen(ROOT).why;
|
||||
assert.ok(why.length > 0, 'the frozen manifest must say WHY it changed');
|
||||
assert.ok(
|
||||
!/operator-authoris|operator-authoriz|by work order|PM checkpoint/i.test(why),
|
||||
'the frozen file may not claim operator authorisation, and coordination metadata belongs in the local plan',
|
||||
);
|
||||
});
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
// - a stats file with no `effort` field is a loud error, never a silently
|
||||
// empty group that reads as "no gain".
|
||||
//
|
||||
// Pattern: tests/scripts/synthesis-measure.test.mjs (pure core, no fixtures on disk).
|
||||
// Pure core, no fixtures on disk.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
|
|
@ -248,7 +248,7 @@ test('decideVerdict: a null gain is insufficient data, never a decline', () => {
|
|||
// appends; nothing read it.
|
||||
//
|
||||
// The stats record carries counts, not names — names are free prose and
|
||||
// field-allowlist.mjs denies prose by omission — so the run attests membership
|
||||
// the record denies prose by omission — so the run attests membership
|
||||
// with a low-cardinality boolean instead, and the gate refuses to call
|
||||
// activation OK without it.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -1,101 +0,0 @@
|
|||
// tests/scripts/synthesis-measure.test.mjs
|
||||
// NW3 (S12) — deterministic Δ main-context measurement core.
|
||||
//
|
||||
// The gate metric (T1 §2) is Δ main-context tokens for an equivalent-quality
|
||||
// digest. This pins the pure accounting that turns fixture token counts into a
|
||||
// verdict, under the two framings that decide NW3:
|
||||
//
|
||||
// - FAITHFUL (current flow): Phase 5 swarm runs FOREGROUND, so its outputs are
|
||||
// already RESIDENT in main before Phase 7. Delegating only the synthesis read
|
||||
// cannot evict them → main holds base+out+dig in BOTH arms → Δ ≈ 0.
|
||||
// - DISK-POTENTIAL (upper bound): IF outputs were on disk (a separate Phase-5
|
||||
// change, out of NW3 scope), the delegated arm holds base+dig only → Δ = out/(base+out+dig).
|
||||
//
|
||||
// POSITIVE adopt requires Δ ≥ 30% AND quality ≥ inline (T1 §5 thresholds).
|
||||
|
||||
import { test } from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import {
|
||||
estimateTokens,
|
||||
mainContextTokens,
|
||||
deltaPct,
|
||||
decideVerdict,
|
||||
analyze,
|
||||
} from '../../scripts/synthesis-measure.mjs';
|
||||
|
||||
test('estimateTokens: ~chars/4 heuristic, monotonic, non-negative', () => {
|
||||
assert.equal(estimateTokens(''), 0);
|
||||
assert.equal(estimateTokens('abcd'), 1);
|
||||
assert.equal(estimateTokens('abcde'), 2); // ceil(5/4)
|
||||
assert.ok(estimateTokens('a'.repeat(400)) === 100);
|
||||
assert.ok(estimateTokens('x'.repeat(1000)) > estimateTokens('x'.repeat(500)));
|
||||
});
|
||||
|
||||
test('mainContextTokens: inline arm holds base + out + dig', () => {
|
||||
assert.equal(mainContextTokens({ base: 50000, out: 12000, dig: 1500, arm: 'inline' }), 63500);
|
||||
});
|
||||
|
||||
test('mainContextTokens: delegated_faithful still holds out (Phase-5 resident) + dig', () => {
|
||||
// The decisive structural fact: foreground swarm delivery already made `out`
|
||||
// resident; delegating Phase 7 does not evict it.
|
||||
assert.equal(
|
||||
mainContextTokens({ base: 50000, out: 12000, dig: 1500, arm: 'delegated_faithful' }),
|
||||
63500,
|
||||
);
|
||||
});
|
||||
|
||||
test('mainContextTokens: delegated_disk holds base + dig only (out lives in the sub-agent)', () => {
|
||||
assert.equal(
|
||||
mainContextTokens({ base: 50000, out: 12000, dig: 1500, arm: 'delegated_disk' }),
|
||||
51500,
|
||||
);
|
||||
});
|
||||
|
||||
test('mainContextTokens: unknown arm throws (no silent default)', () => {
|
||||
assert.throws(() => mainContextTokens({ base: 1, out: 1, dig: 1, arm: 'nope' }));
|
||||
});
|
||||
|
||||
test('deltaPct: (A-B)/A; equal arms → 0; B smaller → positive fraction', () => {
|
||||
assert.equal(deltaPct(100, 100), 0);
|
||||
assert.equal(deltaPct(100, 75), 0.25);
|
||||
assert.equal(deltaPct(0, 0), 0); // guard divide-by-zero
|
||||
});
|
||||
|
||||
test('decideVerdict: ≥30% AND quality-ok → POSITIVE', () => {
|
||||
assert.equal(decideVerdict(0.30, true), 'POSITIVE');
|
||||
assert.equal(decideVerdict(0.45, true), 'POSITIVE');
|
||||
});
|
||||
|
||||
test('decideVerdict: quality loss forces NEGATIVE even at a large Δ', () => {
|
||||
assert.equal(decideVerdict(0.60, false), 'NEGATIVE');
|
||||
});
|
||||
|
||||
test('decideVerdict: <15% → NEGATIVE; the [15%,30%) band → INCONCLUSIVE', () => {
|
||||
assert.equal(decideVerdict(0.149, true), 'NEGATIVE');
|
||||
assert.equal(decideVerdict(0.00, true), 'NEGATIVE');
|
||||
assert.equal(decideVerdict(0.15, true), 'INCONCLUSIVE');
|
||||
assert.equal(decideVerdict(0.2999, true), 'INCONCLUSIVE');
|
||||
});
|
||||
|
||||
test('analyze: faithful arm is structurally Δ=0 → NEGATIVE regardless of sizes', () => {
|
||||
const a = analyze({ baseTokens: 50000, outTokens: 12000, digTokens: 1500, qualityOK: true });
|
||||
assert.equal(a.faithful.deltaPct, 0);
|
||||
assert.equal(a.faithful.verdict, 'NEGATIVE');
|
||||
assert.equal(a.faithful.armA, a.faithful.armB);
|
||||
});
|
||||
|
||||
test('analyze: disk arm Δ = out/(base+out+dig)', () => {
|
||||
const a = analyze({ baseTokens: 50000, outTokens: 12000, digTokens: 1500, qualityOK: true });
|
||||
const expected = 12000 / (50000 + 12000 + 1500);
|
||||
assert.ok(Math.abs(a.disk.deltaPct - expected) < 1e-9);
|
||||
assert.equal(a.disk.armB, 51500);
|
||||
});
|
||||
|
||||
test('analyze: disk verdict tracks BASE — large fixed baseline can sink the upper bound below 30%', () => {
|
||||
// Small base → disk Δ clears 30%; large base → it does not. Demonstrates the
|
||||
// upper bound is itself BASE-sensitive (sweep, do not assert one number).
|
||||
const small = analyze({ baseTokens: 10000, outTokens: 12000, digTokens: 1500, qualityOK: true });
|
||||
const large = analyze({ baseTokens: 200000, outTokens: 12000, digTokens: 1500, qualityOK: true });
|
||||
assert.equal(small.disk.verdict, 'POSITIVE'); // 12000/23500 ≈ 0.51
|
||||
assert.equal(large.disk.verdict, 'NEGATIVE'); // 12000/213500 ≈ 0.056
|
||||
});
|
||||
|
|
@ -1,660 +0,0 @@
|
|||
// 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, symlinkSync } from 'node:fs';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join, dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import {
|
||||
DEFAULT_CUTOFF,
|
||||
MARKER_SINCE,
|
||||
loadStats,
|
||||
readOrders,
|
||||
measureUse,
|
||||
measureTrust,
|
||||
measureInterrupts,
|
||||
emitterCallSites,
|
||||
buildM9Tree,
|
||||
measureMutants,
|
||||
measureAll,
|
||||
render,
|
||||
main,
|
||||
} from '../../scripts/yardstick.mjs';
|
||||
// New exports are read through the namespace, so a missing one fails its own
|
||||
// tests instead of failing this whole file at link time.
|
||||
import * as Y 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`;
|
||||
|
||||
// `marker` writes the order's solution-choice marker the way dispatch does: an
|
||||
// HTML comment on its own line in the body, `<!-- loesningsvalg: <value>; grunn=... -->`.
|
||||
// `arbeidsform` writes `<!-- arbeidsform: <value> -->` on its own line, as dispatch does.
|
||||
function order({ id, date, workClass, slug, marker, arbeidsform, 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('---');
|
||||
if (arbeidsform !== undefined) lines.push(`<!-- arbeidsform: ${arbeidsform} -->`);
|
||||
if (marker !== undefined) lines.push(`<!-- loesningsvalg: ${marker}; grunn=fixture -->`);
|
||||
lines.push('', `# ORDER ${id}`, extra, '');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
const review = (ts, verdict, slug = 's') => ({ ts, slug, verdict });
|
||||
|
||||
// A /trekbrief stats record naming the order it ran (the writer adds order_*).
|
||||
const briefRun = ({ order_id, slug = 's', ts = '2026-10-04T10:00:00Z' }) => ({ ts, slug, order_id, order_claimed_at: ts, order_reason: order_id ? null : 'none-claimed' });
|
||||
|
||||
// The runs index the yardstick joins on, built from stats files `{ name: [records] }`.
|
||||
function runsFrom(files = {}) {
|
||||
const dir = fixture(Object.fromEntries(Object.entries(files).map(([n, recs]) => [n, jsonl(...recs)])));
|
||||
try { return Y.orderRuns(loadStats(dir)); } finally { cleanup(dir); }
|
||||
}
|
||||
const runsOf = (...records) => runsFrom(records.length ? { 'trekbrief-stats.jsonl': records } : {});
|
||||
|
||||
// measureUse over a fixture queue; counting starts at MARKER_SINCE unless the test says otherwise.
|
||||
function use(files, { records = [], runs, cutoff = DEFAULT_CUTOFF, countingSince = MARKER_SINCE } = {}) {
|
||||
const coord = fixture(files);
|
||||
try {
|
||||
return measureUse({ queue: readOrders(coord), runs: runs ?? runsOf(...records), cutoff, countingSince });
|
||||
} finally { cleanup(coord); }
|
||||
}
|
||||
|
||||
// --- 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, runs: runsOf(), cutoff: DEFAULT_CUTOFF, countingSince: MARKER_SINCE });
|
||||
assert.match(row.detail, /2 order files in 2 mailboxes/);
|
||||
assert.match(row.detail, /dot-mailboxes .pm 1/);
|
||||
} finally { cleanup(coord); }
|
||||
});
|
||||
|
||||
test('use: the marker is counted from 2026-09-28, the day pre-order-loesningsvalg.sh began enforcing it', () => {
|
||||
assert.equal(MARKER_SINCE, '2026-09-28');
|
||||
});
|
||||
|
||||
test('use: no order carrying a loesningsvalg marker means NOT COUNTABLE, not 0 %', () => {
|
||||
const row = use({
|
||||
'r/orders/o1.md': order({ id: 'o1', date: '2026-09-29T10:00:00Z' }),
|
||||
'r/orders/o2.md': order({ id: 'o2', date: '2026-09-30T10:00:00Z' }),
|
||||
});
|
||||
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.withMarker, 0);
|
||||
assert.equal(row.facts.notMeasured, 2, 'an unmarked order is not measured - never a no');
|
||||
assert.match(row.detail, /0 of 2 orders carry a loesningsvalg marker/);
|
||||
assert.match(row.detail, /2 not measured/);
|
||||
});
|
||||
|
||||
test('use: plan and prove are NEW work, ikke-nytt is not; unmarked and pre-2026-09-28 orders are not measured', () => {
|
||||
const row = use({
|
||||
'r/orders/plan.md': order({ id: 'plan', date: '2026-09-28T00:00:00Z', marker: 'plan' }),
|
||||
'r/orders/archive/prove.md': order({ id: 'prove', date: '2026-10-01T00:00:00Z', marker: 'prove' }),
|
||||
'r/orders/claimed/ikke.md': order({ id: 'ikke', date: '2026-10-01T00:00:00Z', marker: 'ikke-nytt' }),
|
||||
'r/orders/none.md': order({ id: 'none', date: '2026-10-01T00:00:00Z' }),
|
||||
'r/orders/early.md': order({ id: 'early', date: '2026-09-27T23:59:59Z', marker: 'plan' }),
|
||||
}, { records: [briefRun({ order_id: 'plan' }), briefRun({ order_id: 'ikke', slug: 'other' }), briefRun({ order_id: 'early', slug: 'old' })] });
|
||||
assert.equal(row.facts.newWork, 2, 'plan and prove');
|
||||
assert.equal(row.facts.notNew, 1);
|
||||
assert.equal(row.facts.beforeMarker, 1);
|
||||
assert.equal(row.facts.unmarked, 1);
|
||||
assert.equal(row.facts.withVoyageRun, 1, 'plan has a trekbrief record naming it');
|
||||
assert.equal(row.facts.noRecord, 1, 'prove has none - not measured, never a no');
|
||||
assert.equal(row.countable, true);
|
||||
assert.equal(row.value, 1);
|
||||
assert.equal(row.denominator, 1, 'YES + declared NO; prove is not measured');
|
||||
assert.equal(row.facts.notMeasured, 3, 'unmarked, before 2026-09-28, and prove without a record');
|
||||
assert.match(row.detail, /value = 1 of 1 measured/);
|
||||
});
|
||||
|
||||
test('use: work-class in the frontmatter no longer decides anything - only the marker does', () => {
|
||||
const row = use({
|
||||
'r/orders/a.md': order({ id: 'a', date: '2026-09-29T00:00:00Z', workClass: 'new' }),
|
||||
'r/orders/b.md': order({ id: 'b', date: '2026-09-29T00:00:00Z', workClass: 'maintenance', marker: 'prove' }),
|
||||
}, { records: [briefRun({ order_id: 'a' }), briefRun({ order_id: 'b', slug: 'b' })] });
|
||||
assert.equal(row.facts.newWork, 1, 'b is new by its marker; a has no marker and is not measured');
|
||||
assert.equal(row.value, 1);
|
||||
assert.equal(row.denominator, 1);
|
||||
assert.equal(row.facts.notMeasured, 1);
|
||||
});
|
||||
|
||||
test('use: only a marker on its own line counts - a mention in prose or an unknown value is not measured', () => {
|
||||
const row = use({
|
||||
'r/orders/prose.md': order({ id: 'prose', date: '2026-09-29T00:00:00Z', extra: 'Count from the marker `<!-- loesningsvalg: plan -->` in the body.' }),
|
||||
'r/orders/odd.md': order({ id: 'odd', date: '2026-09-29T00:00:00Z', marker: 'kanskje' }),
|
||||
'r/orders/both.md': order({ id: 'both', date: '2026-09-29T00:00:00Z', marker: 'ikke-nytt', extra: 'Prose names `<!-- loesningsvalg: plan -->` too.' }),
|
||||
});
|
||||
assert.equal(row.denominator, 0);
|
||||
assert.equal(row.facts.notNew, 1, 'both: the line marker wins over the prose mention');
|
||||
assert.equal(row.facts.notMeasured, 2, 'prose has no marker; kanskje is no known value');
|
||||
assert.equal(row.countable, false);
|
||||
});
|
||||
|
||||
test('use: a cutoff later than 2026-09-28 moves the start of the count', () => {
|
||||
const row = use({
|
||||
'r/orders/a.md': order({ id: 'a', date: '2026-09-29T00:00:00Z', marker: 'plan' }),
|
||||
'r/orders/b.md': order({ id: 'b', date: '2026-10-02T00:00:00Z', marker: 'plan' }),
|
||||
}, { cutoff: '2026-10-01', records: [briefRun({ order_id: 'a' }), briefRun({ order_id: 'b', slug: 'b' })] });
|
||||
assert.equal(row.facts.newWork, 1);
|
||||
assert.equal(row.facts.beforeMarker, 1);
|
||||
assert.equal(row.value, 1, 'b only');
|
||||
assert.match(row.source, /from 2026-10-01/);
|
||||
});
|
||||
|
||||
// --- use: the order-id join (source C) -------------------------------------------
|
||||
|
||||
test('use: a new-work order is YES only when a trekbrief record names its order id', () => {
|
||||
const row = use({ 'r/orders/o.md': order({ id: 'o', date: '2026-09-29T00:00:00Z', marker: 'plan' }) },
|
||||
{ records: [briefRun({ order_id: 'o' })] });
|
||||
assert.equal(row.countable, true);
|
||||
assert.equal(row.value, 1);
|
||||
assert.equal(row.denominator, 1);
|
||||
assert.equal(row.facts.withVoyageRun, 1);
|
||||
assert.deepEqual(row.facts.matched, ['o']);
|
||||
});
|
||||
|
||||
test('use: no record, or a record naming another id, is NOT MEASURED - never a no', () => {
|
||||
const row = use({ 'r/orders/a.md': order({ id: 'a', date: '2026-09-29T00:00:00Z', marker: 'plan' }) },
|
||||
{ records: [briefRun({ order_id: 'zzz' })] });
|
||||
assert.equal(row.facts.noRecord, 1);
|
||||
assert.equal(row.facts.withVoyageRun, 0);
|
||||
assert.equal(row.countable, false);
|
||||
assert.equal(row.value, null);
|
||||
assert.equal(row.denominator, 0);
|
||||
assert.equal(row.facts.notMeasured, 1);
|
||||
});
|
||||
|
||||
test('use: an equal slug alone no longer matches - only the order id does', () => {
|
||||
const row = use({ 'r/orders/a.md': order({ id: 'a', date: '2026-09-29T00:00:00Z', marker: 'plan', slug: 'feat-a' }) },
|
||||
{ records: [briefRun({ order_id: null, slug: 'feat-a' }), briefRun({ order_id: 'other', slug: 'feat-a' })] });
|
||||
assert.equal(row.facts.withVoyageRun, 0);
|
||||
assert.equal(row.facts.noRecord, 1);
|
||||
});
|
||||
|
||||
test('use: an order id in another stats file does not count - only /trekbrief records the order', () => {
|
||||
const row = use({ 'r/orders/a.md': order({ id: 'a', date: '2026-09-29T00:00:00Z', marker: 'plan' }) },
|
||||
{ runs: runsFrom({ 'trekplan-stats.jsonl': [{ ts: 't', slug: 's', order_id: 'a' }] }) });
|
||||
assert.equal(row.facts.withVoyageRun, 0);
|
||||
assert.equal(row.facts.noRecord, 1);
|
||||
});
|
||||
|
||||
test('use: two records with the same id and slug count once; the same id under two slugs is disputed', () => {
|
||||
const once = use({ 'r/orders/a.md': order({ id: 'a', date: '2026-09-29T00:00:00Z', marker: 'plan' }) },
|
||||
{ records: [briefRun({ order_id: 'a' }), briefRun({ order_id: 'a', ts: '2026-10-05T00:00:00Z' })] });
|
||||
assert.equal(once.value, 1);
|
||||
assert.equal(once.denominator, 1);
|
||||
const two = use({ 'r/orders/a.md': order({ id: 'a', date: '2026-09-29T00:00:00Z', marker: 'plan' }) },
|
||||
{ records: [briefRun({ order_id: 'a', slug: 'x' }), briefRun({ order_id: 'a', slug: 'y' })] });
|
||||
assert.equal(two.facts.disputed, 1);
|
||||
assert.equal(two.facts.withVoyageRun, 0);
|
||||
assert.equal(two.countable, false, 'a disputed order is not measured');
|
||||
});
|
||||
|
||||
test('use: one slug naming two order ids makes both disputed - a brief cannot run two orders', () => {
|
||||
const row = use({
|
||||
'r/orders/a.md': order({ id: 'a', date: '2026-09-29T00:00:00Z', marker: 'plan' }),
|
||||
'r/orders/b.md': order({ id: 'b', date: '2026-09-29T00:00:00Z', marker: 'plan' }),
|
||||
}, { records: [briefRun({ order_id: 'a', slug: 'one' }), briefRun({ order_id: 'b', slug: 'one' })] });
|
||||
assert.equal(row.facts.disputed, 2);
|
||||
assert.equal(row.facts.withVoyageRun, 0);
|
||||
});
|
||||
|
||||
test('use: arbeidsform: ordre is a declared NO even with a record; arbeidsform: voyage with a record is YES', () => {
|
||||
const row = use({
|
||||
'r/orders/a.md': order({ id: 'a', date: '2026-09-29T00:00:00Z', marker: 'plan', arbeidsform: 'ordre' }),
|
||||
'r/orders/b.md': order({ id: 'b', date: '2026-09-29T00:00:00Z', marker: 'plan', arbeidsform: 'voyage' }),
|
||||
}, { records: [briefRun({ order_id: 'a', slug: 'a' }), briefRun({ order_id: 'b', slug: 'b' })] });
|
||||
assert.equal(row.facts.declaredNo, 1);
|
||||
assert.equal(row.facts.withVoyageRun, 1);
|
||||
assert.deepEqual(row.facts.matched, ['b']);
|
||||
assert.equal(row.value, 1);
|
||||
assert.equal(row.denominator, 2);
|
||||
});
|
||||
|
||||
test('use: a veto marker mentioned mid-line in prose does not veto', () => {
|
||||
const row = use({ 'r/orders/a.md': order({ id: 'a', date: '2026-09-29T00:00:00Z', marker: 'plan', extra: 'Not this: `<!-- arbeidsform: ordre -->` in a sentence.' }) },
|
||||
{ records: [briefRun({ order_id: 'a' })] });
|
||||
assert.equal(row.facts.declaredNo, 0);
|
||||
assert.equal(row.value, 1);
|
||||
});
|
||||
|
||||
test('use: arbeidsform: ordre on an ikke-nytt or unmarked order adds nothing to the denominator', () => {
|
||||
const row = use({
|
||||
'r/orders/y.md': order({ id: 'y', date: '2026-09-29T00:00:00Z', marker: 'plan' }),
|
||||
'r/orders/i.md': order({ id: 'i', date: '2026-09-29T00:00:00Z', marker: 'ikke-nytt', arbeidsform: 'ordre' }),
|
||||
'r/orders/u.md': order({ id: 'u', date: '2026-09-29T00:00:00Z', arbeidsform: 'ordre' }),
|
||||
}, { records: [briefRun({ order_id: 'y' })] });
|
||||
assert.equal(row.facts.declaredNo, 0);
|
||||
assert.equal(row.denominator, 1);
|
||||
});
|
||||
|
||||
test('use: arbeidsform: voyage without a record is NOT MEASURED - the declaration is never the source', () => {
|
||||
const row = use({ 'r/orders/a.md': order({ id: 'a', date: '2026-09-29T00:00:00Z', marker: 'plan', arbeidsform: 'voyage' }) });
|
||||
assert.equal(row.facts.withVoyageRun, 0);
|
||||
assert.equal(row.facts.noRecord, 1);
|
||||
assert.equal(row.countable, false);
|
||||
});
|
||||
|
||||
test('use: orders before the counting start are NOT MEASURED; an order dated exactly on it counts', () => {
|
||||
const row = use({
|
||||
'r/orders/early.md': order({ id: 'early', date: '2026-10-01T23:59:59Z', marker: 'plan' }),
|
||||
'r/orders/on.md': order({ id: 'on', date: '2026-10-02T00:00:00Z', marker: 'plan' }),
|
||||
}, { countingSince: '2026-10-02', records: [briefRun({ order_id: 'early', slug: 'e' }), briefRun({ order_id: 'on' })] });
|
||||
assert.equal(row.facts.beforeMarker, 1);
|
||||
assert.equal(row.value, 1);
|
||||
assert.equal(row.denominator, 1);
|
||||
assert.deepEqual(row.facts.matched, ['on']);
|
||||
});
|
||||
|
||||
test('use: with no counting start the row is NOT COUNTABLE and every order is NOT MEASURED', () => {
|
||||
const row = use({
|
||||
'r/orders/a.md': order({ id: 'a', date: '2026-09-29T00:00:00Z', marker: 'plan' }),
|
||||
'r/orders/b.md': order({ id: 'b', date: '2026-09-29T00:00:00Z', marker: 'plan', arbeidsform: 'ordre' }),
|
||||
}, { countingSince: null, records: [briefRun({ order_id: 'a' })] });
|
||||
assert.equal(row.countable, false);
|
||||
assert.equal(row.value, null);
|
||||
assert.equal(row.facts.notMeasured, row.facts.scanned);
|
||||
assert.equal(row.facts.countingSince, null);
|
||||
assert.match(row.detail, /counting has not started/);
|
||||
assert.match(row.source, /counting not started/);
|
||||
});
|
||||
|
||||
test('use: the window starts at the latest of the cutoff, MARKER_SINCE and the counting start', () => {
|
||||
const files = { 'r/orders/a.md': order({ id: 'a', date: '2026-10-04T00:00:00Z', marker: 'plan' }) };
|
||||
assert.match(use(files, { cutoff: '2026-10-05', countingSince: '2026-10-03' }).source, /from 2026-10-05/);
|
||||
assert.match(use(files, { cutoff: '2026-09-21', countingSince: '2026-10-03' }).source, /from 2026-10-03/);
|
||||
});
|
||||
|
||||
test('use: the exact outcome over YES, declared NO, no record and disputed', () => {
|
||||
const row = use({
|
||||
'r/orders/y.md': order({ id: 'y', date: '2026-09-29T00:00:00Z', marker: 'plan' }),
|
||||
'r/orders/n.md': order({ id: 'n', date: '2026-09-29T00:00:00Z', marker: 'prove', arbeidsform: 'ordre' }),
|
||||
'r/orders/r.md': order({ id: 'r', date: '2026-09-29T00:00:00Z', marker: 'plan', arbeidsform: 'voyage' }),
|
||||
'r/orders/d.md': order({ id: 'd', date: '2026-09-29T00:00:00Z', marker: 'plan' }),
|
||||
'r/orders/u.md': order({ id: 'u', date: '2026-09-29T00:00:00Z' }),
|
||||
'r/orders/e.md': order({ id: 'e', date: '2026-09-01T00:00:00Z', marker: 'plan' }),
|
||||
}, { records: [briefRun({ order_id: 'y', slug: 'y' }), briefRun({ order_id: 'd', slug: 'd1' }), briefRun({ order_id: 'd', slug: 'd2' })] });
|
||||
assert.equal(row.countable, true);
|
||||
assert.equal(row.value, 1);
|
||||
assert.equal(row.denominator, 2, 'y (YES) + n (declared NO)');
|
||||
assert.equal(row.facts.withVoyageRun, 1);
|
||||
assert.equal(row.facts.declaredNo, 1);
|
||||
assert.equal(row.facts.noRecord, 1);
|
||||
assert.equal(row.facts.disputed, 1);
|
||||
assert.equal(row.facts.notMeasured, 4, 'r (no record) + d (disputed) + u (unmarked) + e (before the window)');
|
||||
assert.match(row.detail, /value = 1 of 2 measured/);
|
||||
});
|
||||
|
||||
test('use: ordersNamedByARecord counts named orders even before counting starts (the known-positive check)', () => {
|
||||
const row = use({
|
||||
'r/orders/a.md': order({ id: 'a', date: '2026-09-29T00:00:00Z', marker: 'plan' }),
|
||||
'r/orders/b.md': order({ id: 'b', date: '2026-09-29T00:00:00Z', marker: 'plan' }),
|
||||
}, { countingSince: null, records: [briefRun({ order_id: 'a' })] });
|
||||
assert.equal(row.countable, false);
|
||||
assert.equal(row.facts.ordersNamedByARecord, 1);
|
||||
});
|
||||
|
||||
// --- 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-29T00:00:00Z', marker: 'plan' }) },
|
||||
stats: {
|
||||
'trekbrief-stats.jsonl': jsonl(briefRun({ order_id: 'o', slug: 'a' })),
|
||||
'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, countingSince: MARKER_SINCE,
|
||||
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);
|
||||
});
|
||||
|
||||
test('measureAll: without countingSince it uses USE_COUNTING_SINCE, whatever its value', () => {
|
||||
assert.ok('USE_COUNTING_SINCE' in Y, 'scripts/yardstick.mjs exports USE_COUNTING_SINCE');
|
||||
const { coord, data } = world({
|
||||
orders: { 'r/orders/o.md': order({ id: 'o', date: '2026-10-29T00:00:00Z', marker: 'plan' }) },
|
||||
stats: { 'trekbrief-stats.jsonl': jsonl(briefRun({ order_id: 'o' })) },
|
||||
});
|
||||
try {
|
||||
const all = measureAll({ root: ROOT, coordDir: coord, dataDir: data, cutoff: DEFAULT_CUTOFF,
|
||||
mutantOptions: { evaluate: () => ({ status: 'not-fellable', detail: '' }) } });
|
||||
const direct = measureUse({ queue: readOrders(coord), runs: Y.orderRuns(loadStats(data)), cutoff: DEFAULT_CUTOFF,
|
||||
countingSince: Y.USE_COUNTING_SINCE });
|
||||
assert.deepEqual(all.rows.find((r) => r.id === 'use'), direct);
|
||||
} finally { cleanup(coord, data); }
|
||||
});
|
||||
|
||||
test('main: the CLI prints its result when run through a symlink', () => {
|
||||
const { coord, data } = world();
|
||||
const links = mkdtempSync(join(tmpdir(), 'yardstick-link-'));
|
||||
try {
|
||||
const link = join(links, 'yardstick.mjs');
|
||||
symlinkSync(join(ROOT, 'scripts', 'yardstick.mjs'), link);
|
||||
const r = spawnSync(process.execPath, [link, '--coord', coord, '--data', data, '--json'], { encoding: 'utf8', timeout: 120000 });
|
||||
assert.ok(r.stdout.trim().length > 0, `no output through a symlink (exit ${r.status})`);
|
||||
assert.ok(Array.isArray(JSON.parse(r.stdout).rows));
|
||||
} finally { cleanup(coord, data, links); }
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue