diff --git a/tools/ci/merge-to-master.sh b/tools/ci/merge-to-master.sh index a573b1ad0..491917e45 100755 --- a/tools/ci/merge-to-master.sh +++ b/tools/ci/merge-to-master.sh @@ -115,7 +115,7 @@ mirror_master() { # [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 diff --git a/tools/ci/notes/f04-fin-20261008.json b/tools/ci/notes/f04-fin-20261008.json new file mode 100644 index 000000000..d1ed37843 --- /dev/null +++ b/tools/ci/notes/f04-fin-20261008.json @@ -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)" +} diff --git a/tools/ci/notes/f14-ops-20261008.json b/tools/ci/notes/f14-ops-20261008.json new file mode 100644 index 000000000..05ad8f2f8 --- /dev/null +++ b/tools/ci/notes/f14-ops-20261008.json @@ -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" +} diff --git a/tools/ci/notes/f14-ux-20261008.json b/tools/ci/notes/f14-ux-20261008.json new file mode 100644 index 000000000..689adbbc9 --- /dev/null +++ b/tools/ci/notes/f14-ux-20261008.json @@ -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" +} diff --git a/tools/ci/test-record.mjs b/tools/ci/test-record.mjs index cdaea54e4..aac60b034 100644 --- a/tools/ci/test-record.mjs +++ b/tools/ci/test-record.mjs @@ -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); }