The registry's structural transforms replay at the merge like batches: the REV generator and the notes; a NOT RUN row is stamped only on change

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>
This commit is contained in:
igneum-labs 2026-10-08 19:16:03 +00:00
parent 703f9389c9
commit 714fc79e7d
5 changed files with 33 additions and 3 deletions

View file

@ -115,7 +115,7 @@ mirror_master() { # <sha> [landed-remote-url]: the landed master to every othe
push_race() { case "$1" in *"REFUSED"*|*" RED "*) return 1 ;; *"cannot lock ref"*|*"failed to update ref"*|*"fetch first"*|*"non-fast-forward"*) return 0 ;; *) return 1 ;; esac; }
merge_with_batches() {
local tip="$1" sha="$2" msg="$3" conflicts f
local MAP_CHANGED="${MAP_CHANGED:-0}" MAP_PATH="${MAP_PATH:-tools/ci/test-map.json}" PAGE_PATH="${PAGE_PATH:-docs/plans/igneum-2.0-test-harness-map.md}" BATCHES="${BATCHES:-}" REGISTRY_PATH="${REGISTRY_PATH:-docs/plans/igneum-2.0-test-registry.json}"
local MAP_CHANGED="${MAP_CHANGED:-0}" MAP_PATH="${MAP_PATH:-tools/ci/test-map.json}" PAGE_PATH="${PAGE_PATH:-docs/plans/igneum-2.0-test-harness-map.md}" BATCHES="${BATCHES:-}" NOTES="${NOTES:-}" REGISTRY_PATH="${REGISTRY_PATH:-docs/plans/igneum-2.0-test-registry.json}"
if git "${AUTHOR[@]}" merge -q --no-ff --no-commit "$sha" >/dev/null 2>&1; then :; else
conflicts=$(git diff --name-only --diff-filter=U)
local c ok=1
@ -143,11 +143,21 @@ merge_with_batches() {
fi
if [ -n "$BATCHES" ]; then
git checkout -q "$tip" -- "$REGISTRY_PATH" # master's copy, never the branch's
if [ -f tools/ci/review-suite.mjs ] && [ -f docs/analysis/review-2026-10-08-b/findings.json ]; then
node tools/ci/review-suite.mjs --findings docs/analysis/review-2026-10-08-b/findings.json --dispatch docs/analysis/review-2026-10-08-b/dispatch.md --prefix REV --write >/dev/null || { echo "merge-to-master: the REV suite does not regenerate on the merged tree" >&2; git merge --abort 2>/dev/null; return 1; }
echo "merge-to-master: regenerated the REV suite on master's registry"
fi
for f in $BATCHES; do
[ "$f" = . ] && continue
git checkout -q "$sha" -- "$f"
node tools/ci/test-record.mjs --record "$f" >/dev/null || { echo "merge-to-master: the batch $f does not replay onto master's registry" >&2; git merge --abort 2>/dev/null; return 1; }
echo "merge-to-master: replayed $f onto master's registry"
done
for f in ${NOTES:-}; do
git checkout -q "$sha" -- "$f"
node tools/ci/test-record.mjs --note-file "$f" >/dev/null || { echo "merge-to-master: the note $f does not apply" >&2; git merge --abort 2>/dev/null; return 1; }
echo "merge-to-master: replayed the note $f"
done
git add -A
fi
git "${AUTHOR[@]}" commit -q -m "$msg" || return 1
@ -290,6 +300,9 @@ bash tools/ci/rule24-crate-gate.sh "$BASE" "$SHA" || { echo "merge-to-master: RE
# copy is never what lands and rule 26 does not bind the registry path for such a branch (the evidence rules run on the merged result)
REGISTRY_PATH="${REGISTRY_PATH:-docs/plans/igneum-2.0-test-registry.json}"
BATCHES=$(git diff --name-only --diff-filter=A "$BASE" "$SHA" -- tools/ci/batches/ | grep -E '\.json$' || true)
NOTES=$(git diff --name-only --diff-filter=A "$BASE" "$SHA" -- tools/ci/notes/ | grep -E '\.json$' || true) # {suite, text}, replayed through test-record.mjs --note-file
REVGEN=0; git diff --quiet "$BASE" "$SHA" -- tools/ci/review-suite.mjs docs/analysis/review-2026-10-08-b/findings.json docs/analysis/review-2026-10-08-b/dispatch.md 2>/dev/null || REVGEN=1 # the REV suite regenerates on the merged tree
[ -n "$NOTES" ] || [ "$REVGEN" = 1 ] && BATCHES="${BATCHES:-.}" # the registry is rebuilt from master's copy whenever any transform rides
RULE26_SKIP_PATHS=""; [ -n "$BATCHES" ] && RULE26_SKIP_PATHS="$REGISTRY_PATH"
# the harness map page is generated from tools/ci/test-map.json (test-map-doc.mjs); a branch that changed the map regenerated the
# whole page, and master's page moves under every lane (8 October 2026, 19:5x UK: the enforced-proving lane lost rule 26's race

View file

@ -0,0 +1,4 @@
{
"suite": "FIN",
"text": "F04 (Review B), the founder's ruling 8 October 19:57 UK: the recovery lock is kept and is always labelled 'recovery', never 'final', on every surface (the checkpoint field, the explorer, receipts, the light client, the oracle, the site)"
}

View file

@ -0,0 +1,4 @@
{
"suite": "OPS",
"text": "F14 (Review B), the founder's ruling 8 October 19:57 UK: the public miner ships from 2.0.2 without remote jobs; our own fleet runs the lab build with its own signing root"
}

View file

@ -0,0 +1,4 @@
{
"suite": "UX",
"text": "F14 (Review B), the founder's ruling 8 October 19:57 UK: the public miner ships from 2.0.2 without remote jobs; our own fleet runs the lab build with its own signing root"
}

View file

@ -47,7 +47,9 @@ function record(reg, map, batch) {
touched.push(id);
}
}
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, at: now }; c.updated = now; } }
// 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; }));
@ -65,6 +67,8 @@ if (args.includes('--self-test')) {
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));
@ -74,12 +78,13 @@ if (args.includes('--self-test')) {
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');
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); }