merge-to-master.sh rebuilds the registry from master's copy whenever a landing carries any transform, then regenerates the REV suite from the merged tree's findings and dispatch (review-suite.mjs --write), replays every added batch, and replays every added note file (tools/ci/notes/<id>.json, {suite, text}, through test-record.mjs --note-file), then regenerates the harness page from the structurally merged map. The founder's two rulings ride as note files. test-record.mjs stamps a NOT RUN row only when its status or reason changes (every landing had been colliding on 39 untouched rows, the reference-apps lane's finding).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
100 lines
11 KiB
JavaScript
100 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
|
// The acceptance layer's recorder (the founder's Test and Acceptance Standard, 8 October 2026; main through the coordinator, 18:2x UK).
|
|
// The registry docs/plans/igneum-2.0-test-registry.json holds the cases; tools/ci/test-map.json maps every case whose method includes
|
|
// "Automated" to the harness or suite that answers it (the command, the box class, the fixture ids it needs). A run batch writes
|
|
// status, run id, evidence path and the pinned manifest sha back into the registry for the cases it covered; a case's accept text is
|
|
// never edited (the self-test diffs every accept field before and after); a case with no harness reads NOT RUN with the reason,
|
|
// never PASS by inference.
|
|
//
|
|
// node tools/ci/test-record.mjs --check every Automated case has a map entry or a NOT RUN reason; the map's ids exist
|
|
// node tools/ci/test-record.mjs --record <batch.json> batch: {run_id, manifest_sha, evidence_dir, cells:[{cell, status, evidence}]}
|
|
// each cell's case ids come from the map; the registry gains status/run/evidence/manifest
|
|
// node tools/ci/test-record.mjs --cases <cell> the case ids a matrix cell answers (for the matrix scripts' column)
|
|
// node tools/ci/test-record.mjs --note <suite code> "<text>" append a dated note to a suite's notes (a ruling, a review finding's
|
|
// disposition); never a case's accept text
|
|
// node tools/ci/test-record.mjs --self-test
|
|
import fs from 'node:fs'; import path from 'node:path'; import child_process from 'node:child_process';
|
|
const args = process.argv.slice(2); const arg = (n) => { const i = args.indexOf(n); return i >= 0 ? args[i + 1] : undefined; };
|
|
const ROOT = process.env.TEST_RECORD_ROOT || path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', '..');
|
|
const REG = process.env.TEST_REGISTRY || path.join(ROOT, 'docs/plans/igneum-2.0-test-registry.json');
|
|
const MAP = process.env.TEST_MAP || path.join(ROOT, 'tools/ci/test-map.json');
|
|
const ACCEPT_KEYS = ['accept', 'acceptance', 'accept_text', 'criteria', 'expected']; // whichever the registry uses; all are frozen
|
|
const load = (p) => JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
const casesOf = (reg) => Array.isArray(reg) ? reg : (reg.cases || reg.tests || (reg.suites || []).flatMap((s) => s.tests || s.cases || []));
|
|
const isAutomated = (c) => /automated/i.test(String(c.method ?? c.methods ?? ''));
|
|
const idOf = (c) => String(c.id ?? c.case ?? c.case_id);
|
|
function check(reg, map) {
|
|
const cases = casesOf(reg); const ids = new Set(cases.map(idOf)); const out = []; let bad = 0;
|
|
for (const [cell, m] of Object.entries(map.cells || {})) for (const id of m.cases || []) if (!ids.has(id)) { out.push(`map: cell ${cell} names an unknown case ${id}`); bad++; }
|
|
const mapped = new Set(Object.values(map.cells || {}).flatMap((m) => m.cases || []));
|
|
const notRun = map.not_run || {};
|
|
for (const c of cases) {
|
|
if (!isAutomated(c)) continue; const id = idOf(c);
|
|
if (!mapped.has(id) && !notRun[id]) { out.push(`case ${id} (Automated) has no harness in the map and no NOT RUN reason`); bad++; }
|
|
}
|
|
for (const [cell, m] of Object.entries(map.cells || {})) { if (!m.command) { out.push(`map: cell ${cell} has no command`); bad++; } if (!m.box_class) { out.push(`map: cell ${cell} has no box class`); bad++; } if (!Array.isArray(m.fixtures)) { out.push(`map: cell ${cell} has no fixtures list (F0 onward, or empty)`); bad++; } }
|
|
return { bad, lines: out, automated: cases.filter(isAutomated).length, mapped: [...mapped].length, notRun: Object.keys(notRun).length };
|
|
}
|
|
function record(reg, map, batch) {
|
|
const cases = casesOf(reg); const byId = new Map(cases.map((c) => [idOf(c), c])); const touched = [];
|
|
const now = new Date().toISOString();
|
|
for (const cell of batch.cells || []) {
|
|
const m = (map.cells || {})[cell.cell]; if (!m) throw new Error(`batch names a cell not in the map: ${cell.cell}`);
|
|
for (const id of m.cases || []) {
|
|
const c = byId.get(id); if (!c) throw new Error(`map names an unknown case ${id}`);
|
|
c.run_status = cell.status; c.run_id = batch.run_id; c.evidence_path = cell.evidence || batch.evidence_dir; c.updated = now;
|
|
c.evidence_record = { cell: cell.cell, manifest_sha: batch.manifest_sha, coverage: m.coverage || {}, at: now };
|
|
touched.push(id);
|
|
}
|
|
}
|
|
// a NOT RUN row is stamped only when its status or reason changes (8 October 2026, 20:1x UK: re-stamping every NOT RUN row on every
|
|
// run made each lane's landing collide on 39 rows it never touched)
|
|
for (const [id, reason] of Object.entries(map.not_run || {})) { const c = byId.get(id); if (c && (!c.run_status || c.run_status === 'NOT RUN') && !(c.run_status === 'NOT RUN' && c.evidence_record?.reason === reason)) { c.run_status = 'NOT RUN'; c.evidence_record = { reason, at: now }; c.updated = now; } }
|
|
return touched;
|
|
}
|
|
const acceptSnapshot = (reg) => JSON.stringify(casesOf(reg).map((c) => { const o = { id: idOf(c) }; for (const k of ACCEPT_KEYS) if (k in c) o[k] = c[k]; return o; }));
|
|
if (args.includes('--self-test')) {
|
|
const d = fs.mkdtempSync('/tmp/test-record-'); let fails = 0;
|
|
const reg = { cases: [
|
|
{ id: 'C1', method: 'Automated', accept: 'one million vectors agree' }, { id: 'C2', method: 'Automated', accept: 'parser refuses ten thousand malformed' },
|
|
{ id: 'C3', method: 'Manual review', accept: 'the reviewer signs' }, { id: 'C4', method: 'Automated; manual', accept: 'frozen verify time holds' } ] };
|
|
const map = { cells: { 'pow': { command: 'cargo test --release', box_class: 'suite', fixtures: ['F0'], cases: ['C1'] } }, not_run: { C2: 'no malformed-case corpus yet', C4: 'the hostile-load rig is not built' } };
|
|
fs.writeFileSync(`${d}/reg.json`, JSON.stringify(reg)); fs.writeFileSync(`${d}/map.json`, JSON.stringify(map));
|
|
let r = check(reg, map); if (r.bad !== 0) { console.log(`self-test failed: a complete map was refused: ${r.lines.join('; ')}`); fails = 1; }
|
|
const map2 = { cells: { pow: { ...map.cells.pow, cases: ['C1', 'C9'] } }, not_run: {} };
|
|
r = check(reg, map2); if (!(r.bad >= 2 && r.lines.some((l) => l.includes('unknown case C9')) && r.lines.some((l) => l.includes('C2') && l.includes('no harness')))) { console.log(`self-test failed: an unknown id or an unmapped Automated case was not refused: ${r.lines.join('; ')}`); fails = 1; }
|
|
const before = acceptSnapshot(reg); const touched = record(reg, map, { run_id: 'r1', manifest_sha: 'abc', evidence_dir: '/e', cells: [{ cell: 'pow', status: 'PASS', evidence: '/e/pow.log' }] });
|
|
if (acceptSnapshot(reg) !== before) { console.log('self-test failed: a record changed an accept text'); fails = 1; }
|
|
if (!(touched.length === 1 && reg.cases[0].run_status === 'PASS' && reg.cases[0].run_id === 'r1' && reg.cases[0].evidence_record.manifest_sha === 'abc' && reg.cases[0].evidence_path === '/e/pow.log' && reg.cases[0].updated)) { console.log(`self-test failed: the run was not written to the mapped case's live fields: ${JSON.stringify(reg.cases[0])}`); fails = 1; }
|
|
if (!(reg.cases[1].run_status === 'NOT RUN' && /corpus/.test(reg.cases[1].evidence_record.reason))) { console.log('self-test failed: an unmapped Automated case did not read NOT RUN with its reason'); fails = 1; }
|
|
const stamp1 = reg.cases[1].updated; record(reg, map, { run_id: 'r1b', manifest_sha: 'abc', evidence_dir: '/e', cells: [{ cell: 'pow', status: 'PASS', evidence: '/e/pow.log' }] });
|
|
if (reg.cases[1].updated !== stamp1) { console.log('self-test failed: an unchanged NOT RUN row was re-stamped on a later run'); fails = 1; }
|
|
if (reg.cases[2].run_status) { console.log('self-test failed: a manual case was given a run status'); fails = 1; }
|
|
const regS = { suites: [{ code: 'X', tests: [{ id: 'X-1', method: 'Automated', accept: 'a' }] }] }; if (casesOf(regS).length !== 1) { console.log('self-test failed: the suites/tests registry shape was not read'); fails = 1; }
|
|
const regN = { suites: [{ code: 'FIN', tests: [{ id: 'F-1', method: 'Automated', accept: 'keep' }] }] }; fs.writeFileSync(`${d}/regn.json`, JSON.stringify(regN));
|
|
const nr = child_process.spawnSync(process.execPath, [new URL(import.meta.url).pathname, '--note', 'FIN', 'the ruling'], { env: { ...process.env, TEST_REGISTRY: `${d}/regn.json`, TEST_MAP: `${d}/map.json` }, encoding: 'utf8' });
|
|
const regN2 = JSON.parse(fs.readFileSync(`${d}/regn.json`, 'utf8'));
|
|
if (!(nr.status === 0 && regN2.suites[0].notes?.length === 1 && regN2.suites[0].notes[0].text === 'the ruling' && regN2.suites[0].tests[0].accept === 'keep')) { console.log(`self-test failed: --note did not append a dated note to the suite and keep the accept text: ${nr.stdout} ${nr.stderr}`); fails = 1; }
|
|
let threw = false; try { record(reg, map, { run_id: 'r2', manifest_sha: 'x', cells: [{ cell: 'ghost', status: 'PASS' }] }); } catch { threw = true; }
|
|
if (!threw) { console.log('self-test failed: a batch naming a cell not in the map was accepted'); fails = 1; }
|
|
fs.rmSync(d, { recursive: true, force: true });
|
|
if (!fails) console.log('self-test passed: a complete map checks; an unknown case id and an unmapped Automated case are refused; a run batch writes run_status, run_id, evidence_path, updated and the evidence record to the mapped cases only, leaves every accept text byte-identical, gives an unmapped Automated case NOT RUN with its reason and a manual case nothing; a batch naming an unknown cell is refused; --note appends a dated note to a suite and never touches an accept text; an unchanged NOT RUN row is not re-stamped');
|
|
process.exit(fails);
|
|
}
|
|
const reg = load(REG); const map = load(MAP);
|
|
if (args.includes('--check')) { const r = check(reg, map); for (const l of r.lines) console.error(`test-record: ${l}`); console.log(`test-record: ${r.automated} Automated cases, ${r.mapped} mapped to cells, ${r.notRun} NOT RUN with a reason${r.bad ? `, ${r.bad} problems` : ''}`); process.exit(r.bad ? 1 : 0); }
|
|
if (arg('--cases')) { console.log(((map.cells || {})[arg('--cases')]?.cases || []).join(',')); process.exit(0); }
|
|
if (arg('--note-file')) { const n = load(arg('--note-file')); args.push('--note', n.suite, n.text); }
|
|
if (arg('--note')) {
|
|
const code = arg('--note'); const text = args[args.indexOf('--note') + 2]; const suite = (reg.suites || []).find((s) => s.code === code);
|
|
if (!suite || !text) { console.error(`test-record: --note needs a suite code in the registry and a text (got ${code}, ${text ? 'text' : 'no text'})`); process.exit(2); }
|
|
const before = acceptSnapshot(reg); suite.notes = suite.notes || []; suite.notes.push({ at: new Date().toISOString(), text });
|
|
if (acceptSnapshot(reg) !== before) { console.error('test-record: REFUSED: the note would change an accept text'); process.exit(1); }
|
|
fs.writeFileSync(REG, JSON.stringify(reg, null, 2) + '\n'); console.log(`test-record: note ${suite.notes.length} on ${code}: ${text.slice(0, 80)}`); process.exit(0);
|
|
}
|
|
if (arg('--record')) {
|
|
const batch = load(arg('--record')); const before = acceptSnapshot(reg); const touched = record(reg, map, batch);
|
|
if (acceptSnapshot(reg) !== before) { console.error('test-record: REFUSED: the record would change an accept text'); process.exit(1); }
|
|
fs.writeFileSync(REG, JSON.stringify(reg, null, 2) + '\n'); console.log(`test-record: run ${batch.run_id} (manifest ${String(batch.manifest_sha).slice(0, 8)}) written to ${touched.length} cases: ${touched.join(', ')}`); process.exit(0);
|
|
}
|
|
console.error('usage: test-record.mjs --check | --record <batch.json> | --cases <cell> | --self-test'); process.exit(2);
|