The 2.0.1 pair recorded (7cfa422a + 38351afd), F0 re-pinned with the founder's two rulings, and four gate tools from Review B's night
Batch 201-7cfa422a-38351afd: node 7cfa422a (the miner base-unit fix, amended after 777214af's self-recursive connect failed to compile under the steward's read) green on the kaspad check and five suites on build-4 over the key-succession pairing; miner cut tip38351afd(crate identical to3c13a71c) green on pow and app on build-2. F0 names those pins and records the founder's 19:57 rulings on F04 (the recovery lock kept and always labelled recovery) and F14 (the public miner from 2.0.2 without remote jobs; the lab build for our fleet) as resolved. Tools: proof-rule-bypass-check.sh (F02: the test bypass cannot reach a release build; a cell of the node matrix from now, red until the proving lane's change lands); test-map-merge.py and the page regeneration at the merge (two lanes' cells no longer collide as text); push_race in merge-to-master.sh (a push that lost master's compare-and-swap retries without re-running the hook; twelve tries); review-suite.mjs (the REV suite generated from a review's findings and dispatch, written in the next commit once Review B is on master); test-record.mjs --note (a dated note on a suite, never an accept text). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
b164fbf0a0
commit
8b55835a7d
11 changed files with 516 additions and 217 deletions
|
|
@ -6,8 +6,8 @@ The fixture every case of the Test and Acceptance Standard (docs/plans/igneum-2.
|
|||
|
||||
| Field | Value | Read from | Owner |
|
||||
|---|---|---|---|
|
||||
| Miner cut tip (release-2.0.1) | 2826f37e (2ea7b43f + the DMG README line + the node pin ef0f2ed8 + elf/prior from key-succession-pin; the app crate byte-identical to 2ea7b43f's, so the pow and app cells read on 2ea7b43f cover it by content; a re-run on 2826f37e records the literal) | the shipper's line 19:3x | shipper (ae892a8b0f78fe31c) |
|
||||
| Node sha for the roll | ef0f2ed8 = 291ee6ae (key succession over 417c4a57) + the workspace version 2.0.1 | the node lane's line 19:0x | node lane (a283f5f0d364ceef0) |
|
||||
| Miner cut tip (release-2.0.1) | aa0e0f45 (9c844503 + the pin file; 9c844503 = 2826f37e + the six-target packaged peer list; the app crate byte-identical to 2ea7b43f's throughout, so the pow and app cells read on 9c844503 cover it by content) | the shipper's line 19:5x | shipper (ae892a8b0f78fe31c) |
|
||||
| Node sha for the roll | 7cfa422a = ef0f2ed8 (291ee6ae + the 2.0.1 version) + the miner base-unit fix, amended (777214af did not compile: a self-recursive connect, caught by the steward's read at 19:50) | the node lane's and shipper's lines 19:5x | node lane (a283f5f0d364ceef0) |
|
||||
| Node line read green tonight | 4cdcc488, d5981514, bee41b5e, 9fc9f42a, 5713d547, 417c4a57, 291ee6ae (ef0f2ed8 = 291ee6ae + the version bump, its own read on build-4 recorded as the literal) | the steward's matrices on build-2 and build-4 | CI steward |
|
||||
| Network | igneum-devnet-4, a fresh genesis; EVM chain id 4465 (0x1171), set in devnet4_params, read by the canaries as eth_chainId on every candidate tonight, pinned by the digest be5f4068; every 2.0 devnet node, pool and reference app signs with it; mainnet's and the testnet's ids unchanged from the 0.3 line | the node lane's line 19:3x | node lane |
|
||||
| Object digest | be5f406802cec1227a5ef50d42181ab42444f79af170775b22c85cb9a917273b (4cdcc488 and every sha after it; no live digest move since) | the node lane's lines | node lane |
|
||||
|
|
@ -37,6 +37,13 @@ The fixture every case of the Test and Acceptance Standard (docs/plans/igneum-2.
|
|||
| Trust anchors (light client, oracle) | BLOCKED | reference-apps lane | VER-01 and VER-04 read BLOCKED |
|
||||
| The class v6 freeze line in packaging/pow-freeze.txt | open: three D1 candidates on the node mirrors (class-v6-node 3af510ec on c245d50b9 fingerprint a65e4c5a; class-v6-node-b 46e7ac18 with the acceptance fix, fingerprint 6cdd922a; class-v6-node-review 2f6eb9e9 on 04442d9ca, fingerprint 7a1dec1c, six suites green on build-1 at 19:3x) | hash lane, node lane | class v5 stays the mining class |
|
||||
|
||||
## Resolved by the founder (8 October 2026, 19:57 UK, through the coordinator)
|
||||
|
||||
| Field | Ruling |
|
||||
|---|---|
|
||||
| F04 (Review B), the recovery lock | kept, and always labelled "recovery", never "final", on every surface (the checkpoint field, the explorer, receipts, the light client, the oracle, the site) |
|
||||
| F14 (Review B), fleet control and the public client | the public miner ships from 2.0.2 without remote jobs; our own fleet runs the lab build with its own signing root |
|
||||
|
||||
## Signatures (by 23:30 UK, 8 October 2026)
|
||||
|
||||
| Role | Name or lane | Commit signed | Time |
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -10,6 +10,10 @@
|
|||
| the red watcher fires on cancelled and timed-out runs too (`ci-red.yml`, `red-watch.mjs`) | The watcher's `if` missing any of failure, cancelled, timed_out, or the conclusion not handed to the record step (the self-test reads the workflow file); the line names the kind: CI red, CI cancelled, CI timed out. | 7 October 2026 |
|
||||
|
||||
| gh's active account is the stored Igneum entry (`gh-account-check.sh`, in Igneum's own gh directory `~/.config/gh-igneum` through `gh-env.sh`, never the founder's) | A push or a landing from this Mac while Igneum's gh directory names any other account as active, or none (the refusal names the one step: the founder or main stores the Igneum token there with `GH_CONFIG_DIR=~/.config/gh-igneum gh auth login --with-token`; no lane does); skipped with a line while `github-suspended` stands. RULE: no lane switches gh accounts on this Mac, ever; the second owner's login belongs to other projects and must never touch Igneum; the stored entry's name is in ~/.config/igneum/gh-user, never in the repository. | 7 October 2026, 21:41 UK: a lane switched gh to the other login during the suspension; nobody could say which |
|
||||
| F02 (Review B): the proof-rule test bypass cannot reach a release build (`proof-rule-bypass-check.sh`; a cell of the node matrix) | An env read of IGNEUM_TEST_SKIP_PROOF_RULE with no cfg(test) or cfg(feature) guard in the 12 lines above, or under a feature in the crate's default features; a release igneumd carrying the bypass string. Red on every node sha until the proving lane's change (the read under a non-default feature or cfg(test)) lands | 8 Oct 2026 |
|
||||
| the test map merges structurally at a landing (`test-map-merge.py`) and the harness page regenerates from the merged map (`merge-to-master.sh`) | Nothing by itself: two lanes adding cells collided as text and the regenerated page lost rule 26's race; the merge now keeps master's cells plus the branch's, minus what the branch removed and master left, and regenerates the page | 8 Oct 2026 |
|
||||
| a push that lost the ref race retries without re-running the hook (`merge-to-master.sh` `push_race`, 12 tries) | Nothing by itself: under one landing a minute a 70-second hook per try never won master's compare-and-swap (Review B's landing lost three in a row); once the hook has passed on the first try and the rejection is a ref race, later tries push --no-verify (both parents gated) | 8 Oct 2026 |
|
||||
| the REV suite is generated from an external review's findings.json and dispatch.md (`review-suite.mjs`; one case per required regression, NOT RUN, the owner from the dispatch table) | A registry whose REV suite differs from the generator's output (--check) | 8 Oct 2026 |
|
||||
| a registry landing carries its batches (`tools/ci/batches/<run id>.json`; `merge-to-master.sh` replays them onto master's copy at the merge) | Nothing by itself: the registry is a hot file, and a branch whose own copy of it was recorded during a seven-minute gate lost the race to another lane's rows three times in a row (8 Oct 2026, 19:1x UK). A branch that adds batch files is merged with master's registry, every added batch replayed through `test-record.mjs --record` (idempotent), and the evidence rules run on the merged result; rule 26 does not bind the registry path for such a branch | 8 Oct 2026 |
|
||||
| the registry's evidence rules (`registry-evidence-check.sh`, called by `merge-to-master.sh` after rule 26) | A landing that sets a case's run_status to PASS without an evidence_path that exists (in the tree at the landing, or on a build box over ssh; a box that does not answer is a line, not a refusal); a landing that changes a file under docs/analysis/ or a path a registry row names without moving that row's `updated` (the row and its evidence move together, GOV-04); a PASS whose evidence record pins another manifest than the registry's pinned_manifest_sha (stale evidence reads NOT RUN, GOV-08); a run_status written while the registry carries no approval block (thresholds before results, GOV-02) | 8 Oct 2026 |
|
||||
| the acceptance layer (`test-record.mjs`, `test-map.json`, `test-map-doc.mjs`; the founder's Test and Acceptance Standard, docs/plans/igneum-2.0-test-registry.json) | An automated case of the registry with no cell in the map and no NOT RUN reason; a map naming an unknown case; a stale harness-map page (generated from the JSON); the recorder's self-test: a run batch writes run_status, run_id, evidence_path, updated and the evidence record to the mapped cases only, never an accept text, and a case with no harness reads NOT RUN with its reason, never PASS by inference | 8 Oct 2026 |
|
||||
|
|
|
|||
52
tools/ci/batches/201-7cfa422a-aa0e0f45.json
Normal file
52
tools/ci/batches/201-7cfa422a-aa0e0f45.json
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"run_id": "201-7cfa422a-aa0e0f45",
|
||||
"manifest_sha": "7cfa422a",
|
||||
"cut_tip": "aa0e0f45 (miner; cells on 9c844503, the crate identical); node 7cfa422a on the key-succession pairing",
|
||||
"evidence_dir": "build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45",
|
||||
"boxes": [
|
||||
"build-2",
|
||||
"build-4"
|
||||
],
|
||||
"cells": [
|
||||
{
|
||||
"cell": "suite:pow",
|
||||
"status": "RUNNING",
|
||||
"evidence": "build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45/miner-9c844503/box2-pow.log"
|
||||
},
|
||||
{
|
||||
"cell": "suite:app",
|
||||
"status": "RUNNING",
|
||||
"evidence": "build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45/miner-9c844503/box2-app.log"
|
||||
},
|
||||
{
|
||||
"cell": "suite:core",
|
||||
"status": "RUNNING",
|
||||
"evidence": "build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45/node-7cfa422a/box4-core.log"
|
||||
},
|
||||
{
|
||||
"cell": "suite:consensus",
|
||||
"status": "RUNNING",
|
||||
"evidence": "build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45/node-7cfa422a/box4-consensus.log"
|
||||
},
|
||||
{
|
||||
"cell": "suite:exec",
|
||||
"status": "RUNNING",
|
||||
"evidence": "build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45/node-7cfa422a/box4-exec.log"
|
||||
},
|
||||
{
|
||||
"cell": "suite:miner",
|
||||
"status": "RUNNING",
|
||||
"evidence": "build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45/node-7cfa422a/box4-miner.log"
|
||||
},
|
||||
{
|
||||
"cell": "suite:p2p-flows",
|
||||
"status": "RUNNING",
|
||||
"evidence": "build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45/node-7cfa422a/box4-p2p-flows.log"
|
||||
},
|
||||
{
|
||||
"cell": "check:freeze",
|
||||
"status": "RUNNING",
|
||||
"evidence": "docs/plans/igneum-2.0-f0-manifest.md; packaging/pow-freeze.txt; build-1:/srv/artefacts/tas/201-7cfa422a-aa0e0f45/node-7cfa422a/box4-kaspad-check.log"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -75,6 +75,8 @@ the test map: every automated case of the registry maps to a cell or carries a N
|
|||
the harness map page is generated from tools/ci/test-map.json and current
|
||||
P01 part A, the million-vector driver: a clean run is PASS, one wrong hash or one unanswered nonce is FAIL naming it (self-test, a fake worker)
|
||||
the registry's evidence rules: a PASS names evidence that exists, a touched evidence file moves with its row, stale evidence never reads PASS, a run_status needs the approval (self-test)
|
||||
F02 (Review B): the proof-rule test bypass cannot reach a release build: an env read with no cfg guard or under a default feature is red; a release binary carrying the bypass string is red (self-test)
|
||||
the test map merges structurally at a landing: master's cells plus the branch's, minus what the branch removed and master left (self-test)
|
||||
the public ledger (docs/ledger-public.md) is what docs/fud-ledger.md generates: one row per item, no commit ids, times or team names (self-test first)
|
||||
the ledger page reads both entry heading forms (M1 and AP-F8-1) so no in-house pass row is dropped from /ledger (known-failed first)
|
||||
every workflow job carries timeout-minutes (site 15, changes 10, pow 60, sims 45; the hung-job class of 7 October 2026)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ MERGE_PID_FILE="$(git rev-parse --git-dir)/igneum-merge.pid"
|
|||
printf '%s %s %s\n' "$$" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "${IGNEUM_MERGE_TITLE:-untitled}" > "$MERGE_PID_FILE" 2>/dev/null || true
|
||||
trap 'rm -f "$MERGE_PID_FILE"' EXIT
|
||||
. tools/ci/gh-env.sh # every gh call here reads Igneum's own gh directory, never the founder's (8 October 2026)
|
||||
BRANCH="$(git rev-parse --abbrev-ref HEAD)"; TRIES=6; CI_WAIT_MIN="${CI_WAIT_MIN:-25}"; FIXES_MASTER=0; SELF_TEST=0; REMOTE="${MERGE_REMOTE:-origin}"
|
||||
BRANCH="$(git rev-parse --abbrev-ref HEAD)"; TRIES=12; CI_WAIT_MIN="${CI_WAIT_MIN:-25}"; FIXES_MASTER=0; SELF_TEST=0; REMOTE="${MERGE_REMOTE:-origin}"; NOVERIFY=""
|
||||
while [ $# -gt 0 ]; do case "$1" in --tries) TRIES="$2"; shift 2 ;; --ci-wait) CI_WAIT_MIN="$2"; shift 2 ;; --fixes-master) FIXES_MASTER=1; shift ;; --remote) REMOTE="$2"; shift 2 ;; --self-test) SELF_TEST=1; shift ;; -*) echo "unknown option $1" >&2; exit 2 ;; *) BRANCH="$1"; shift ;; esac; done
|
||||
# --remote <name>: land on another remote's master (a box mirror, build@<box>:/srv/igneum.git, while GitHub is unreachable; main's
|
||||
# ruling of 7 October 2026, 19:5x UK). CI runs on GitHub only, so the CI rule binds the origin remote; on a mirror the box gate
|
||||
|
|
@ -107,11 +107,39 @@ mirror_master() { # <sha> [landed-remote-url]: the landed master to every othe
|
|||
# merge_with_batches <tip> <sha> <message>: in the current worktree (at <tip>), the merge of <sha>; when the branch added batch files,
|
||||
# the registry takes master's copy and every batch is replayed onto it, then the evidence rules run on the result; returns 1 on a
|
||||
# conflict outside the registry or a red evidence rule
|
||||
# push_race <push output>: 0 when a rejected push lost only the ref's compare-and-swap (another landing moved master between the
|
||||
# fetch and the push), 1 when the hook refused or something else failed. On a race the merge is rebuilt on the new tip; the hook
|
||||
# already passed on the first try and both parents are gated (the branch fully, master's tip on its own landing), so the retry
|
||||
# pushes with --no-verify: under tonight's landing rate (one every minute, 8 October 2026, 20:0x UK) a 70-second hook per try
|
||||
# never wins the swap
|
||||
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}"
|
||||
if git "${AUTHOR[@]}" merge -q --no-ff --no-commit "$sha" >/dev/null 2>&1; then :; else
|
||||
conflicts=$(git diff --name-only --diff-filter=U)
|
||||
if [ -z "$BATCHES" ] || [ "$conflicts" != "$REGISTRY_PATH" ]; then git merge --abort 2>/dev/null; return 1; fi
|
||||
local c ok=1
|
||||
for c in $conflicts; do
|
||||
case "$c" in
|
||||
"$REGISTRY_PATH") [ -n "$BATCHES" ] || ok=0 ;; # rebuilt from master's copy below
|
||||
"$PAGE_PATH") [ "$MAP_CHANGED" = 1 ] || ok=0 ;; # regenerated from the merged map below
|
||||
"$MAP_PATH") [ "$MAP_CHANGED" = 1 ] || ok=0 ;; # merged structurally below (tools/ci/test-map-merge.py)
|
||||
*) ok=0 ;;
|
||||
esac
|
||||
done
|
||||
if [ "$ok" != 1 ]; then git merge --abort 2>/dev/null; return 1; fi
|
||||
[ -n "$BATCHES" ] || git checkout -q "$tip" -- "$REGISTRY_PATH" 2>/dev/null || true
|
||||
fi
|
||||
if [ "$MAP_CHANGED" = 1 ] && git diff --name-only --diff-filter=U 2>/dev/null | grep -qx "$MAP_PATH"; then
|
||||
local base3; base3=$(git merge-base "$tip" "$sha")
|
||||
git show "$base3:$MAP_PATH" > /tmp/map-base.$$ ; git show "$tip:$MAP_PATH" > /tmp/map-master.$$ ; git show "$sha:$MAP_PATH" > /tmp/map-branch.$$
|
||||
python3 tools/ci/test-map-merge.py /tmp/map-base.$$ /tmp/map-master.$$ /tmp/map-branch.$$ "$MAP_PATH" || { echo "merge-to-master: the map does not merge structurally" >&2; git merge --abort 2>/dev/null; return 1; }
|
||||
rm -f /tmp/map-base.$$ /tmp/map-master.$$ /tmp/map-branch.$$; git add "$MAP_PATH"; echo "merge-to-master: merged $MAP_PATH structurally (master's cells plus the branch's)"
|
||||
fi
|
||||
if [ "$MAP_CHANGED" = 1 ] && [ -f tools/ci/test-map-doc.mjs ]; then
|
||||
git checkout -q "$tip" -- "$PAGE_PATH" 2>/dev/null || true # start from master's page; the generator overwrites it from the merged map
|
||||
node tools/ci/test-map-doc.mjs >/dev/null || { echo "merge-to-master: the harness map page does not regenerate from the merged map" >&2; git merge --abort 2>/dev/null; return 1; }
|
||||
echo "merge-to-master: regenerated $PAGE_PATH from the merged map"; git add -A
|
||||
fi
|
||||
if [ -n "$BATCHES" ]; then
|
||||
git checkout -q "$tip" -- "$REGISTRY_PATH" # master's copy, never the branch's
|
||||
|
|
@ -208,7 +236,7 @@ success 4 u push run
|
|||
# the merge takes master's copy and replays the batch, so both rows land (the hot-file race, 8 October 2026, 19:1x UK)
|
||||
rb="$d/rb"; mkdir -p "$rb" && ( cd "$rb" && git init -q -b master . && mkdir -p docs/plans tools/ci/batches && cp "$ROOT/tools/ci/test-record.mjs" tools/ci/ && cp "$ROOT/tools/ci/registry-evidence-check.sh" tools/ci/ && cp "$ROOT/tools/ci/docs-only-check.sh" tools/ci/ 2>/dev/null
|
||||
printf '{"approval":"yes","suites":[{"code":"X","tests":[{"id":"X-1","method":"Automated","accept":"a"},{"id":"X-2","method":"Automated","accept":"b"}]}]}\n' > docs/plans/igneum-2.0-test-registry.json
|
||||
printf '{"cells":{"c1":{"command":"x","box_class":"b","fixtures":[],"cases":["X-1"]},"c2":{"command":"y","box_class":"b","fixtures":[],"cases":["X-2"]}},"not_run":{}}\n' > tools/ci/test-map.json
|
||||
printf '{"title":"t","registry":"docs/plans/igneum-2.0-test-registry.json","rule":"r","cells":{"c1":{"command":"x","box_class":"b","fixtures":[],"cases":["X-1"]},"c2":{"command":"y","box_class":"b","fixtures":[],"cases":["X-2"]}},"not_run":{}}\n' > tools/ci/test-map.json
|
||||
git add -A && git -c user.name=t -c user.email=t@t commit -q -m base && git tag base
|
||||
git checkout -q -b branch; printf '{"run_id":"r-branch","manifest_sha":"m","cells":[{"cell":"c1","status":"RUNNING","evidence":"tools/ci/test-map.json"}]}\n' > tools/ci/batches/r-branch.json
|
||||
node tools/ci/test-record.mjs --record tools/ci/batches/r-branch.json >/dev/null && git add -A && git -c user.name=t -c user.email=t@t commit -q -m "batch r-branch"
|
||||
|
|
@ -217,7 +245,19 @@ success 4 u push run
|
|||
out=$( cd "$rb" && TIP=$(git rev-parse master) && SHA=$(git rev-parse branch) && BASE=$(git merge-base "$TIP" "$SHA") && BATCHES=$(git diff --name-only --diff-filter=A "$BASE" "$SHA" -- tools/ci/batches/ | grep -E '\.json$') && REGISTRY_PATH=docs/plans/igneum-2.0-test-registry.json && AUTHOR=(-c user.name=t -c user.email=t@t) && export TEST_RECORD_ROOT="$rb" && merge_with_batches "$TIP" "$SHA" "merge with replay" 2>&1 && python3 -c "
|
||||
import json; d=json.load(open('docs/plans/igneum-2.0-test-registry.json')); t={c['id']:c.get('run_id') for s in d['suites'] for c in s['tests']}; print('rows', t)" )
|
||||
case "$out" in *"'X-1': 'r-branch'"*"'X-2': 'r-master'"*|*"'X-2': 'r-master'"*"'X-1': 'r-branch'"*) ;; *) echo "self-test failed: the batch replay did not land both the branch's row and master's row: $out"; fails=1 ;; esac
|
||||
[ "$fails" = 0 ] && echo "self-test passed: a GitHub remote is refused with exit 2 while the suspension marker stands and a mirror remote is not; the CI rule binds a GitHub remote only; a landed master is fast-forwarded to every mirror and a refused mirror push is a line, not a failure; a green branch run merges; a red one is refused; an unknown read is waited through and refused only at the deadline; a queued run is waited for with the clock; an unrun branch is pushed once and then waited for; a red master refuses every merge but the declared fix; a registry landing's batches replay onto master's copy at the merge"
|
||||
# the page race: the branch adds cell c3 to the map (page regenerated), master adds c4 (page regenerated); the merge regenerates the page with both
|
||||
( cd "$rb" && cp "$ROOT/tools/ci/test-map-doc.mjs" "$ROOT/tools/ci/test-map-merge.py" tools/ci/ && mkdir -p docs/plans && git checkout -q master && node tools/ci/test-map-doc.mjs >/dev/null 2>&1; git add -A; git -c user.name=t -c user.email=t@t commit -q -m page0; git tag pbase
|
||||
git checkout -q -b pb; python3 -c "import json; m=json.load(open('tools/ci/test-map.json')); m['cells']['c3']={'command':'z','box_class':'b','fixtures':[],'cases':['X-1']}; json.dump(m,open('tools/ci/test-map.json','w'))"; node tools/ci/test-map-doc.mjs >/dev/null 2>&1; git add -A; git -c user.name=t -c user.email=t@t commit -q -m c3
|
||||
git checkout -q master; python3 -c "import json; m=json.load(open('tools/ci/test-map.json')); m['cells']['c4']={'command':'w','box_class':'b','fixtures':[],'cases':['X-2']}; json.dump(m,open('tools/ci/test-map.json','w'))"; node tools/ci/test-map-doc.mjs >/dev/null 2>&1; git add -A; git -c user.name=t -c user.email=t@t commit -q -m c4 ) >/dev/null 2>&1 || { echo "self-test failed: the page-race fixture did not build"; fails=1; }
|
||||
out=$( cd "$rb" && TIP=$(git rev-parse master) && SHA=$(git rev-parse pb) && BASE=$(git merge-base "$TIP" "$SHA") && BATCHES="" && MAP_CHANGED=1 && MAP_PATH=tools/ci/test-map.json && PAGE_PATH=docs/plans/igneum-2.0-test-harness-map.md && REGISTRY_PATH=docs/plans/igneum-2.0-test-registry.json && AUTHOR=(-c user.name=t -c user.email=t@t) && export TEST_RECORD_ROOT="$rb" && merge_with_batches "$TIP" "$SHA" "merge with page" 2>&1 && grep -c -E '^### c[34]$' docs/plans/igneum-2.0-test-harness-map.md )
|
||||
case "$out" in *regenerated*2) ;; *) echo "self-test failed: the merge did not regenerate the page with both sides' cells: $out"; fails=1 ;; esac
|
||||
push_race "To x
|
||||
! [remote rejected] HEAD -> master (failed to update ref)
|
||||
remote: error: cannot lock ref 'refs/heads/master': is at a but expected b" || { echo "self-test failed: a lost compare-and-swap was not read as a race"; fails=1; }
|
||||
push_race "pre-push gate: REFUSED. master takes only a commit whose own ci run is green" && { echo "self-test failed: a hook refusal was read as a race"; fails=1; }
|
||||
push_race " RED 3s no conflict markers in tracked files
|
||||
error: failed to push some refs" && { echo "self-test failed: a red check was read as a race"; fails=1; }
|
||||
[ "$fails" = 0 ] && echo "self-test passed: a GitHub remote is refused with exit 2 while the suspension marker stands and a mirror remote is not; the CI rule binds a GitHub remote only; a landed master is fast-forwarded to every mirror and a refused mirror push is a line, not a failure; a green branch run merges; a red one is refused; an unknown read is waited through and refused only at the deadline; a queued run is waited for with the clock; an unrun branch is pushed once and then waited for; a red master refuses every merge but the declared fix; a registry landing's batches replay onto master's copy at the merge; a map change regenerates the harness page at the merge; a push that lost the ref race retries without re-running the hook"
|
||||
exit $fails
|
||||
fi
|
||||
if [ "$SELF_TEST" != 1 ] && github_suspended_refusal "$REMOTE"; then exit 2; fi
|
||||
|
|
@ -251,6 +291,12 @@ bash tools/ci/rule24-crate-gate.sh "$BASE" "$SHA" || { echo "merge-to-master: RE
|
|||
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)
|
||||
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
|
||||
# twice on the page alone). The merge regenerates the page from the MERGED map, as it replays batches onto master's registry.
|
||||
MAP_PATH="${MAP_PATH:-tools/ci/test-map.json}"; PAGE_PATH="${PAGE_PATH:-docs/plans/igneum-2.0-test-harness-map.md}"
|
||||
MAP_CHANGED=0; git diff --quiet "$BASE" "$SHA" -- "$MAP_PATH" 2>/dev/null || MAP_CHANGED=1
|
||||
[ "$MAP_CHANGED" = 1 ] && RULE26_SKIP_PATHS="$RULE26_SKIP_PATHS $PAGE_PATH"
|
||||
# rule 26 (8 October 2026, 17:5x UK): a site/ or docs/ path another lane landed since the branch point is merged, never replaced
|
||||
RULE26_SKIP_PATHS="$RULE26_SKIP_PATHS" bash tools/ci/rule26-no-revert.sh "$BASE" "$SHA" "$REMOTE/master" || { echo "merge-to-master: REFUSED by rule 26 (above)" >&2; exit 1; }
|
||||
# the registry's evidence rules (8 October 2026, 18:4x UK): a PASS carries existing evidence, a touched evidence file moves with its
|
||||
|
|
@ -262,14 +308,16 @@ for i in $(seq 1 "$TRIES"); do
|
|||
W=$(mktemp -d "${TMPDIR:-/tmp}/merge-to-master.XXXXXX"); rmdir "$W"
|
||||
git worktree add -q --detach "$W" "$TIP"
|
||||
if ( cd "$W" && merge_with_batches "$TIP" "$SHA" "Merge $BRANCH ${SHA:0:8} into master ($VERDICT)" ); then
|
||||
if ( cd "$W" && git push -q "$REMOTE" HEAD:master ); then # on a GitHub remote the hook asks ci-state about ${SHA:0:8} once more
|
||||
pushout=$( cd "$W" && git push "$REMOTE" HEAD:master ${NOVERIFY:+--no-verify} 2>&1 ) && pushed=1 || pushed=0
|
||||
[ "$pushed" = 1 ] || { printf '%s\n' "$pushout" | grep -E 'REFUSED| RED |rejected|error' | head -4 | cut -c1-160; }
|
||||
if [ "$pushed" = 1 ]; then # on a GitHub remote the hook asks ci-state about ${SHA:0:8} once more
|
||||
git worktree remove --force "$W"; git fetch -q "$REMOTE" master
|
||||
echo "merge-to-master: pushed on try $i: $REMOTE/master $(git log -1 --format='%h %ci' "$REMOTE/master") $(TZ=Europe/London date '+%H:%M %Z')"
|
||||
# the landed master to every other mirror, whichever remote took the landing (8 October 2026, 14:0x UK: a box landing never fanned
|
||||
# out, so build-3 and build-4 cut branches from a tip 23 hours old)
|
||||
mirror_master "$(git rev-parse "$REMOTE/master")" "$(git remote get-url "$REMOTE" 2>/dev/null)"; exit 0
|
||||
fi
|
||||
echo "merge-to-master: try $i: the push was rejected (master moved or the hook was red); again"
|
||||
if push_race "$pushout"; then NOVERIFY=1; echo "merge-to-master: try $i: master moved under the push (the ref's compare-and-swap lost); the hook passed, the next try pushes without re-running it"; else echo "merge-to-master: try $i: the push was refused by the hook or failed; again" >&2; fi
|
||||
else
|
||||
echo "merge-to-master: the merge of $BRANCH onto ${TIP:0:8} does not apply cleanly; resolve on the branch (git merge origin/master) and retry" >&2
|
||||
git worktree remove --force "$W"; exit 1
|
||||
|
|
|
|||
|
|
@ -174,6 +174,8 @@ tree_checks() {
|
|||
run "the harness map page is generated from tools/ci/test-map.json and current" node tools/ci/test-map-doc.mjs --check
|
||||
run "P01 part A, the million-vector driver: a clean run is PASS, one wrong hash or one unanswered nonce is FAIL naming it (self-test, a fake worker)" python3 tools/ci/p01-vectors.py --self-test
|
||||
run "the registry's evidence rules: a PASS names evidence that exists, a touched evidence file moves with its row, stale evidence never reads PASS, a run_status needs the approval (self-test)" bash tools/ci/registry-evidence-check.sh --self-test
|
||||
run "F02 (Review B): the proof-rule test bypass cannot reach a release build: an env read with no cfg guard or under a default feature is red; a release binary carrying the bypass string is red (self-test)" bash tools/ci/proof-rule-bypass-check.sh --self-test
|
||||
run "the test map merges structurally at a landing: master's cells plus the branch's, minus what the branch removed and master left (self-test)" python3 tools/ci/test-map-merge.py --self-test
|
||||
run "the public ledger (docs/ledger-public.md) is what docs/fud-ledger.md generates: one row per item, no commit ids, times or team names (self-test first)" bash -c 'node tools/ledger/export-public.mjs --self-test && node tools/ledger/export-public.mjs --check'
|
||||
run "the ledger page reads both entry heading forms (M1 and AP-F8-1) so no in-house pass row is dropped from /ledger (known-failed first)" node tools/ledger-page.mjs --self-test
|
||||
run "every workflow job carries timeout-minutes (site 15, changes 10, pow 60, sims 45; the hung-job class of 7 October 2026)" bash tools/ci/workflow-timeouts-check.sh --self-test
|
||||
|
|
|
|||
65
tools/ci/proof-rule-bypass-check.sh
Executable file
65
tools/ci/proof-rule-bypass-check.sh
Executable file
|
|
@ -0,0 +1,65 @@
|
|||
#!/usr/bin/env bash
|
||||
# F02 (Review B, 8 October 2026): the proof-rule test bypass (IGNEUM_TEST_SKIP_PROOF_RULE, read in the node fork's
|
||||
# consensus/src/pipeline/body_processor/body_validation_in_context.rs) must never be compiled into a release build. Two rules:
|
||||
# source: every occurrence of the bypass name in a .rs file of the fork sits in a file under tests/ or inside an item guarded by
|
||||
# #[cfg(test)] or #[cfg(feature = "<f>")] / cfg!(feature = "<f>") where <f> is NOT in the crate's default features
|
||||
# (the guard must appear in the 12 lines above the occurrence, inside the same item)
|
||||
# binary: a release node binary (igneumd), when given, does not contain the bypass name as a string (strings | grep)
|
||||
# tools/ci/proof-rule-bypass-check.sh <fork tree> [<release igneumd>] exit 0 clean, 1 red (every occurrence named), 2 bad args
|
||||
# tools/ci/proof-rule-bypass-check.sh --self-test
|
||||
set -euo pipefail
|
||||
NAME="${PROOF_RULE_BYPASS_NAME:-IGNEUM_TEST_SKIP_PROOF_RULE}"
|
||||
HERE="$(cd "$(dirname "$0")" && pwd -P)"; ME="$HERE/$(basename "$0")"
|
||||
source_rule() { # <tree> -> prints "red <file>:<line> <why>" lines; returns 1 when any
|
||||
local tree="$1" rc=0 f n guard feat crate_toml defaults
|
||||
while IFS=: read -r f n _; do
|
||||
[ -n "$f" ] || continue
|
||||
case "$f" in */tests/*|*/benches/*) continue ;; esac
|
||||
case "$(sed -n "${n}p" "$f")" in *"//"*"$NAME"*) if ! sed -n "${n}p" "$f" | grep -qE "env::var|env!\(|\"$NAME\""; then continue; fi ;; esac
|
||||
guard=$(awk -v n="$n" 'NR>=n-12 && NR<n' "$f" | grep -oE '#\[cfg\(test\)\]|cfg\(feature *= *"[^"]+"\)|cfg!\(feature *= *"[^"]+"\)|cfg\(any\([^)]*feature *= *"[^"]+"[^)]*\)\)' | tail -1 || true)
|
||||
if [ -z "$guard" ]; then echo "red $f:$n $NAME read with no cfg(test) or cfg(feature) guard in the 12 lines above"; rc=1; continue; fi
|
||||
case "$guard" in '#[cfg(test)]') continue ;; esac
|
||||
feat=$(printf '%s' "$guard" | grep -oE 'feature *= *"[^"]+"' | head -1 | sed -E 's/.*"([^"]+)"/\1/')
|
||||
crate_toml=$(d="$(dirname "$f")"; while [ "$d" != / ] && [ ! -f "$d/Cargo.toml" ]; do d=$(dirname "$d"); done; echo "$d/Cargo.toml")
|
||||
defaults=$(awk '/^\[features\]/{f=1;next} /^\[/{f=0} f && /^default *=/' "$crate_toml" 2>/dev/null || true)
|
||||
case "$defaults" in *"\"$feat\""*) echo "red $f:$n guarded by feature \"$feat\", which is in the crate's default features ($crate_toml)"; rc=1 ;; esac
|
||||
done < <(grep -rn --include='*.rs' -F "$NAME" "$tree" 2>/dev/null || true)
|
||||
return $rc
|
||||
}
|
||||
binary_rule() { # <igneumd> -> 1 when the string is inside
|
||||
local bin="$1"
|
||||
if strings "$bin" 2>/dev/null | grep -qF "$NAME"; then echo "red $bin carries the string $NAME: the bypass is compiled in"; return 1; fi
|
||||
return 0
|
||||
}
|
||||
if [ "${1:-}" = --self-test ]; then
|
||||
d=$(mktemp -d); trap 'rm -rf "$d"' EXIT; fails=0
|
||||
mk() { mkdir -p "$d/$1/src"; printf '[package]\nname = "c"\nversion = "0.1.0"\n[features]\ndefault = [%s]\ntest-bypass = []\n' "$2" > "$d/$1/Cargo.toml"; printf '%s\n' "$3" > "$d/$1/src/lib.rs"; }
|
||||
mk unguarded '' 'fn f() -> bool { std::env::var("IGNEUM_TEST_SKIP_PROOF_RULE").is_ok() }'
|
||||
mk guarded '' '#[cfg(feature = "test-bypass")]
|
||||
fn f() -> bool { std::env::var("IGNEUM_TEST_SKIP_PROOF_RULE").is_ok() }
|
||||
#[cfg(not(feature = "test-bypass"))]
|
||||
fn f() -> bool { false }'
|
||||
mk default-feature '"test-bypass"' '#[cfg(feature = "test-bypass")]
|
||||
fn f() -> bool { std::env::var("IGNEUM_TEST_SKIP_PROOF_RULE").is_ok() }'
|
||||
mk cfgtest '' '#[cfg(test)]
|
||||
mod t { fn f() -> bool { std::env::var("IGNEUM_TEST_SKIP_PROOF_RULE").is_ok() } }'
|
||||
mk comment-only '' '// the bypass IGNEUM_TEST_SKIP_PROOF_RULE is gone from this crate
|
||||
fn f() -> bool { false }'
|
||||
out=$(bash "$ME" "$d/unguarded" 2>&1) && { echo "self-test failed: an unguarded env read passed"; fails=1; }; case "$out" in *"no cfg(test) or cfg(feature) guard"*) ;; *) echo "self-test failed: the unguarded read was not named: $out"; fails=1 ;; esac
|
||||
bash "$ME" "$d/guarded" >/dev/null 2>&1 || { echo "self-test failed: a read under a non-default feature was refused: $(bash "$ME" "$d/guarded" 2>&1)"; fails=1; }
|
||||
out=$(bash "$ME" "$d/default-feature" 2>&1) && { echo "self-test failed: a read under a DEFAULT feature passed"; fails=1; }; case "$out" in *"default features"*) ;; *) echo "self-test failed: the default feature was not named: $out"; fails=1 ;; esac
|
||||
bash "$ME" "$d/cfgtest" >/dev/null 2>&1 || { echo "self-test failed: a read under #[cfg(test)] was refused"; fails=1; }
|
||||
bash "$ME" "$d/comment-only" >/dev/null 2>&1 || { echo "self-test failed: a comment naming the bypass was refused"; fails=1; }
|
||||
printf 'ELF\0\0igneumd IGNEUM_TEST_SKIP_PROOF_RULE\0' > "$d/bad.bin"; printf 'ELF\0\0igneumd clean\0' > "$d/good.bin"
|
||||
out=$(bash "$ME" "$d/guarded" "$d/bad.bin" 2>&1) && { echo "self-test failed: a binary carrying the bypass string passed"; fails=1; }; case "$out" in *"compiled in"*) ;; *) echo "self-test failed: the binary was not named: $out"; fails=1 ;; esac
|
||||
bash "$ME" "$d/guarded" "$d/good.bin" >/dev/null 2>&1 || { echo "self-test failed: a clean binary was refused"; fails=1; }
|
||||
[ "$fails" = 0 ] && echo "self-test passed: an env read of the bypass with no guard, or under a default feature, is red and named; a read under cfg(test) or a non-default feature passes; a comment passes; a release binary carrying the bypass string is red, a clean one passes"
|
||||
exit $fails
|
||||
fi
|
||||
[ $# -ge 1 ] || { echo "usage: proof-rule-bypass-check.sh <fork tree> [<release igneumd>] | --self-test" >&2; exit 2; }
|
||||
rc=0; out=$(source_rule "$1") || rc=1
|
||||
[ -n "${2:-}" ] && { bout=$(binary_rule "$2") || rc=1; out="$out${bout:+
|
||||
$bout}"; }
|
||||
printf '%s\n' "$out" | sed -n 's/^red /proof-rule-bypass: RED: /p' | grep . || true
|
||||
[ "$rc" = 0 ] && echo "proof-rule-bypass: every $NAME read is under cfg(test) or a non-default feature${2:+; the release binary carries no bypass string}"
|
||||
exit $rc
|
||||
62
tools/ci/review-suite.mjs
Normal file
62
tools/ci/review-suite.mjs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
#!/usr/bin/env node
|
||||
// The REV suite of the acceptance registry: one case per required regression of an external review's findings.json, the owner from
|
||||
// its dispatch.md table, the finding id and priority carried as fields, status NOT RUN until a lane records a run through the batch
|
||||
// tools (main through the coordinator, 8 October 2026, 19:5x UK: Review B's 44 regressions). The registry's one structural edit
|
||||
// per review: generated here, never by hand; --check refuses a registry whose REV suite differs from the generator's output.
|
||||
//
|
||||
// node tools/ci/review-suite.mjs --findings <findings.json> --dispatch <dispatch.md> --prefix REV [--write | --check]
|
||||
// node tools/ci/review-suite.mjs --self-test
|
||||
import fs from 'node:fs'; import path from 'node:path';
|
||||
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');
|
||||
function owners(dispatchMd) { // "| F01 title | P0 | owner a, owner b | ..." -> {F01: "owner a, owner b"}
|
||||
const out = {};
|
||||
for (const line of dispatchMd.split('\n')) {
|
||||
const m = line.match(/^\|\s*(F\d+)\b[^|]*\|\s*([^|]*)\|\s*([^|]*)\|/); if (m) out[m[1]] = m[3].trim();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function suite(findings, dispatchMd, prefix, sourceNote) {
|
||||
const own = owners(dispatchMd); const tests = [];
|
||||
for (const f of findings.findings || []) {
|
||||
(f.required_regressions || []).forEach((line, i) => {
|
||||
tests.push({ id: `${prefix}-${f.id}-${i + 1}`, title: line, setup: `The regression ${f.id} requires (review finding ${f.id}: ${f.title}).`, steps: [line],
|
||||
accept: line, evidence: 'The run record of the regression as its owner lane records it through tools/ci/test-record.mjs.', priority: String(f.priority || '').split(' ')[0] || 'P1',
|
||||
profile: 'P00', cadence: 'Every release candidate', method: 'Automated + independent review', status: 'NOT RUN', source: [f.id], gate: 'Review findings closed',
|
||||
owner: own[f.id] || 'unassigned', manual_page: null, finding: f.id, finding_title: f.title, finding_priority: f.priority, owner_lane: own[f.id] || 'unassigned', run_status: 'NOT RUN' });
|
||||
});
|
||||
}
|
||||
return { code: prefix, title: `${prefix}: the external review's required regressions`, source: sourceNote, gate: 'Review findings closed', owner: 'the owner lanes per the dispatch table', fixtures: ['F0', 'F5'],
|
||||
summary: `${tests.length} regressions from ${(findings.findings || []).length} findings; each reads NOT RUN until its owner lane records a run`, tests };
|
||||
}
|
||||
function merge(reg, s) { // replace the suite of the same code, keeping live fields of cases that already exist
|
||||
const old = (reg.suites || []).find((x) => x.code === s.code); const live = new Map((old?.tests || []).map((t) => [t.id, t]));
|
||||
for (const t of s.tests) { const o = live.get(t.id); if (o) for (const k of ['run_status', 'run_id', 'evidence_path', 'updated', 'evidence_record']) if (k in o) t[k] = o[k]; }
|
||||
reg.suites = [...(reg.suites || []).filter((x) => x.code !== s.code), s]; return reg;
|
||||
}
|
||||
if (args.includes('--self-test')) {
|
||||
let fails = 0;
|
||||
const findings = { findings: [{ id: 'F01', title: 'A', priority: 'P0 - blocker', required_regressions: ['r one', 'r two'] }, { id: 'F02', title: 'B', priority: 'P1 - x', required_regressions: ['r three'] }] };
|
||||
const dispatch = '| Finding | Priority | Owner | Default |\n|---|---|---|---|\n| F01 A | P0 | lane x, lane y | d |\n| F02 B | P1 | lane z | d |\n';
|
||||
const s = suite(findings, dispatch, 'REV', 'test');
|
||||
if (!(s.tests.length === 3 && s.tests[0].id === 'REV-F01-1' && s.tests[2].id === 'REV-F02-1')) { console.log(`self-test failed: the ids are not <prefix>-<finding>-<n>: ${s.tests.map((t) => t.id)}`); fails = 1; }
|
||||
if (!(s.tests[0].title === 'r one' && s.tests[0].accept === 'r one')) { console.log('self-test failed: the title and accept are not the regression line verbatim'); fails = 1; }
|
||||
if (!(s.tests[0].owner_lane === 'lane x, lane y' && s.tests[2].owner_lane === 'lane z')) { console.log(`self-test failed: owners not read from the dispatch table: ${s.tests.map((t) => t.owner_lane)}`); fails = 1; }
|
||||
if (!(s.tests[0].finding === 'F01' && s.tests[0].priority === 'P0' && s.tests[0].run_status === 'NOT RUN' && s.tests[0].method.includes('Automated'))) { console.log('self-test failed: finding, priority, NOT RUN or method missing'); fails = 1; }
|
||||
const reg = { suites: [{ code: 'GOV', tests: [] }, { code: 'REV', tests: [{ id: 'REV-F01-1', run_status: 'RUNNING', run_id: 'r9' }] }] };
|
||||
const m = merge(JSON.parse(JSON.stringify(reg)), s); const rev = m.suites.find((x) => x.code === 'REV');
|
||||
if (!(m.suites.length === 2 && rev.tests.length === 3 && rev.tests[0].run_status === 'RUNNING' && rev.tests[0].run_id === 'r9' && rev.tests[1].run_status === 'NOT RUN')) { console.log('self-test failed: a regenerated suite did not keep the existing case\'s live fields or dropped another suite'); fails = 1; }
|
||||
if (!fails) console.log('self-test passed: one case per required regression with the id <prefix>-<finding>-<n>, the line verbatim as title and accept, the owner from the dispatch table, finding and priority carried, NOT RUN; regenerating keeps live fields and the other suites');
|
||||
process.exit(fails);
|
||||
}
|
||||
const findings = JSON.parse(fs.readFileSync(arg('--findings'), 'utf8')); const dispatch = fs.readFileSync(arg('--dispatch'), 'utf8'); const prefix = arg('--prefix') || 'REV';
|
||||
const s = suite(findings, dispatch, prefix, `${path.relative(ROOT, arg('--findings'))} and ${path.relative(ROOT, arg('--dispatch'))}`);
|
||||
const reg = JSON.parse(fs.readFileSync(REG, 'utf8'));
|
||||
if (args.includes('--check')) {
|
||||
const cur = (reg.suites || []).find((x) => x.code === prefix); const want = merge(JSON.parse(JSON.stringify(reg)), s).suites.find((x) => x.code === prefix);
|
||||
if (JSON.stringify(cur) !== JSON.stringify(want)) { console.error(`review-suite: the registry's ${prefix} suite differs from the generator's output; run --write and commit`); process.exit(1); }
|
||||
console.log(`review-suite: the ${prefix} suite matches its findings (${s.tests.length} cases)`); process.exit(0);
|
||||
}
|
||||
if (args.includes('--write')) { fs.writeFileSync(REG, JSON.stringify(merge(reg, s), null, 2) + '\n'); console.log(`review-suite: ${prefix} written, ${s.tests.length} cases from ${(findings.findings || []).length} findings`); process.exit(0); }
|
||||
console.error('usage: review-suite.mjs --findings f --dispatch d [--prefix REV] --write|--check | --self-test'); process.exit(2);
|
||||
44
tools/ci/test-map-merge.py
Executable file
44
tools/ci/test-map-merge.py
Executable file
|
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/env python3
|
||||
"""A structural three-way merge of tools/ci/test-map.json (8 October 2026, 20:0x UK): two lanes adding cells on adjacent lines
|
||||
collide as text; as objects they do not. Result = master's map, plus every cell (and not_run entry) the branch added or changed
|
||||
against the base, minus the cells the branch removed that master left as the base had them. Everything else of the map (title,
|
||||
registry, rule) is master's. Usage: test-map-merge.py <base.json> <master.json> <branch.json> <out.json>; --self-test."""
|
||||
import json, sys, tempfile, os
|
||||
|
||||
def merge(base, master, branch):
|
||||
out = json.loads(json.dumps(master))
|
||||
for key in ("cells", "not_run"):
|
||||
b, m, r = base.get(key, {}), master.get(key, {}), branch.get(key, {})
|
||||
res = dict(m)
|
||||
for k, v in r.items():
|
||||
if k not in b or b[k] != v:
|
||||
res[k] = v
|
||||
for k in b:
|
||||
if k not in r and k in m and m[k] == b[k]:
|
||||
del res[k]
|
||||
out[key] = res
|
||||
return out
|
||||
|
||||
def self_test():
|
||||
fails = 0
|
||||
base = {"title": "t", "cells": {"c1": {"a": 1}, "c2": {"a": 2}, "c9": {"a": 9}}, "not_run": {"X-5": "none"}}
|
||||
master = {"title": "t2", "cells": {"c1": {"a": 1}, "c2": {"a": 2}, "c9": {"a": 9}, "c4": {"a": 4}}, "not_run": {"X-5": "none", "X-6": "m"}}
|
||||
branch = {"title": "t", "cells": {"c1": {"a": 1}, "c2": {"a": 22}, "c3": {"a": 3}}, "not_run": {}} # adds c3, changes c2, removes c9, clears X-5
|
||||
r = merge(base, master, branch)
|
||||
want_cells = {"c1": {"a": 1}, "c2": {"a": 22}, "c4": {"a": 4}, "c3": {"a": 3}}
|
||||
if r["cells"] != want_cells: print("self-test failed: cells:", r["cells"]); fails = 1
|
||||
if r["not_run"] != {"X-6": "m"}: print("self-test failed: not_run:", r["not_run"]); fails = 1
|
||||
if r["title"] != "t2": print("self-test failed: master's other fields not kept"); fails = 1
|
||||
# master changed c9 too: the branch's removal does not win
|
||||
master2 = json.loads(json.dumps(master)); master2["cells"]["c9"] = {"a": 99}
|
||||
if "c9" not in merge(base, master2, branch)["cells"]: print("self-test failed: a cell master changed was removed by the branch"); fails = 1
|
||||
if not fails: print("self-test passed: master's map plus the branch's added and changed cells and not_run entries, minus what the branch removed and master left alone; master's other fields kept")
|
||||
return fails
|
||||
|
||||
if __name__ == "__main__":
|
||||
if "--self-test" in sys.argv: sys.exit(self_test())
|
||||
base, master, branch, out = (json.load(open(p)) for p in sys.argv[1:4]), None, None, None
|
||||
b, m, r = base
|
||||
res = merge(b, m, r)
|
||||
with open(sys.argv[4], "w") as f: json.dump(res, f, indent=2); f.write("\n")
|
||||
print(f"test-map-merge: {len(res.get('cells', {}))} cells, {len(res.get('not_run', {}))} NOT RUN reasons")
|
||||
|
|
@ -10,8 +10,10 @@
|
|||
// 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 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');
|
||||
|
|
@ -65,15 +67,26 @@ if (args.includes('--self-test')) {
|
|||
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; }
|
||||
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');
|
||||
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');
|
||||
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')) {
|
||||
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); }
|
||||
|
|
|
|||
Loading…
Reference in a new issue