diff --git a/tools/ci/batches/kills-20261008.json b/tools/ci/batches/kills-20261008.json index 12a847760..f2484f46e 100644 --- a/tools/ci/batches/kills-20261008.json +++ b/tools/ci/batches/kills-20261008.json @@ -7,12 +7,13 @@ { "cell": "experiment:connected-state", "status": "FAIL", - "evidence": "docs/analysis/class-v6" + "evidence": "docs/analysis/class-v6/connected-state.md" }, { "cell": "experiment:mixed-fp32", "status": "FAIL", - "evidence": "docs/analysis/class-v6" + "evidence": "docs/analysis/class-v6/mixed-fp32.md; docs/analysis/class-v6/rows/pow-07-fp32-unreachable.md" } - ] + ], + "note": " Evidence narrowed at 21:3x UK from the class-v6 directory to the two experiments' own files (a directory citation bound every file beneath it to these rows and refused every other lane's class-v6 landing); the verdicts are unchanged." } diff --git a/tools/ci/registry-evidence-check.sh b/tools/ci/registry-evidence-check.sh index a7118073d..caa475bf6 100755 --- a/tools/ci/registry-evidence-check.sh +++ b/tools/ci/registry-evidence-check.sh @@ -20,11 +20,11 @@ check() { # ; prints "refused ..." lines; returns 1 when any local base="$1" head="$2" rc=0 git cat-file -e "$head:$REG" 2>/dev/null || return 0 local tmp_h tmp_b; tmp_h=$(mktemp); tmp_b=$(mktemp); git show "$head:$REG" > "$tmp_h"; git show "$base:$REG" > "$tmp_b" 2>/dev/null || : > "$tmp_b" - local touched; touched=$(git diff --name-only "$base" "$head" --) - python3 - "$tmp_h" "$tmp_b" "$head" "$touched" <<'PY' || rc=1 + local touched added; touched=$(git diff --name-only "$base" "$head" --); added=$(git diff --name-only --diff-filter=A "$base" "$head" --) + python3 - "$tmp_h" "$tmp_b" "$head" "$touched" "$added" <<'PY' || rc=1 import json, sys, subprocess, os h = json.load(open(sys.argv[1])); b = json.load(open(sys.argv[2])) if os.path.getsize(sys.argv[2]) else {} -head = sys.argv[3]; touched = [t for t in sys.argv[4].split('\n') if t] +head = sys.argv[3]; touched = [t for t in sys.argv[4].split('\n') if t]; added = set(t for t in sys.argv[5].split('\n') if t) cases = lambda r: [t for s in r.get('suites', []) for t in s.get('tests', [])] hb = {c['id']: c for c in cases(b)}; refused = [] approved = bool(h.get('approval')); pinned = h.get('pinned_manifest_sha') @@ -62,6 +62,7 @@ for c in cases(h): REG_PATH = os.environ.get('REGISTRY_PATH', 'docs/plans/igneum-2.0-test-registry.json') for t in touched: if t == REG_PATH: continue # the registry names itself as GOV-02's evidence (the approval recorded in it); it always moves with itself + if t in added: continue # a file first appearing in the tree is the evidence arriving, not evidence changing under a row: a row that cited it before it landed stands (lane D's class, 8 October 2026 21:2x UK); only a MODIFIED or deleted evidence file must move its rows owners = [ids for p, ids in named.items() if t == p or t.startswith(p + '/')] if not owners and not t.startswith('docs/analysis/'): continue ids = sorted({i for ids in owners for i in ids}) @@ -97,7 +98,9 @@ PY git checkout -q -b e base; echo r2 > docs/analysis/row.md; git -c user.name=t -c user.email=t@t commit -qam touched && git tag touched # evidence-with-row: the same change with X-2's updated moved git checkout -q -b e2 base; echo r2 > docs/analysis/row.md; mk docs/plans/igneum-2.0-test-registry.json 1 - '{"X-1": {}, "X-2": {"run_status": "RUNNING", "evidence_path": "docs/analysis/row.md", "updated": "t1"}}'; git -c user.name=t -c user.email=t@t commit -qam with-row && git tag with-row - # stale: PASS on evidence pinned to another manifest + # added: a NEW evidence file another lane's row already cites lands free (the row cited it before it existed; rule 2 binds modified files only) + git checkout -q -b a base; mkdir -p docs/analysis/new; echo n1 > docs/analysis/new/log.tsv; mk docs/plans/igneum-2.0-test-registry.json 1 - '{"X-1": {}, "X-2": {"run_status": "RUNNING", "evidence_path": "docs/analysis/row.md; docs/analysis/new/log.tsv", "updated": "t0"}}'; git add -A; git -c user.name=t -c user.email=t@t commit -qm added && git tag added + git checkout -q -b a2 added; echo n2 > docs/analysis/new/log.tsv; git -c user.name=t -c user.email=t@t commit -qam added-then-touched && git tag added-touched git checkout -q -b s base; mk docs/plans/igneum-2.0-test-registry.json 1 aaaaaaaa '{"X-1": {"run_status": "PASS", "evidence_path": "evidence/a.log", "updated": "t1", "evidence_record": {"manifest_sha": "bbbbbbbb"}}, "X-2": {}}'; git -c user.name=t -c user.email=t@t commit -qam stale && git tag stale # self-ref: a row names the registry itself as its evidence; a registry change must not trip rule 2 on it git checkout -q -b r base; mk docs/plans/igneum-2.0-test-registry.json 1 - '{"X-1": {"run_status": "RUNNING", "evidence_path": "evidence/a.log", "updated": "t1"}, "X-2": {"run_status": "RUNNING", "evidence_path": "docs/plans/igneum-2.0-test-registry.json", "updated": "t0"}}'; git -c user.name=t -c user.email=t@t commit -qam self-ref && git tag self-ref @@ -108,10 +111,12 @@ PY out=$(bash "$ME" base pass-missing 2>&1) && { echo "self-test failed: a PASS naming a missing path passed"; fails=1; }; case "$out" in *"not in the tree"*) ;; *) echo "self-test failed: the missing path was not named: $out"; fails=1 ;; esac out=$(bash "$ME" base touched 2>&1) && { echo "self-test failed: a touched evidence file without its row passed"; fails=1; }; case "$out" in *"X-2"*"move together"*) ;; *) echo "self-test failed: the unmoved row was not named: $out"; fails=1 ;; esac bash "$ME" base with-row >/dev/null 2>&1 || { echo "self-test failed: a touched evidence file with its row updated was refused: $(bash "$ME" base with-row 2>&1)"; fails=1; } + bash "$ME" base added >/dev/null 2>&1 || { echo "self-test failed: a new evidence file a row already cites was refused on arrival: $(bash "$ME" base added 2>&1)"; fails=1; } + out=$(bash "$ME" added added-touched 2>&1) && { echo "self-test failed: the same file modified after it landed passed without its row"; fails=1; } out=$(bash "$ME" base stale 2>&1) && { echo "self-test failed: a PASS on stale evidence passed"; fails=1; }; case "$out" in *"stale evidence"*) ;; *) echo "self-test failed: the stale evidence was not named: $out"; fails=1 ;; esac bash "$ME" base self-ref >/dev/null 2>&1 || { echo "self-test failed: a row naming the registry itself as evidence tripped the move-together rule on a registry change: $(bash "$ME" base self-ref 2>&1)"; fails=1; } out=$(bash "$ME" base unapproved 2>&1) && { echo "self-test failed: a run_status without approval passed"; fails=1; }; case "$out" in *"thresholds before results"*) ;; *) echo "self-test failed: the missing approval was not named: $out"; fails=1 ;; esac - [ "$fails" = 0 ] && echo "self-test passed: a PASS must name evidence that exists (tree or box); a touched evidence file moves with its registry row; evidence pinned to another manifest cannot be PASS; a run_status needs the registry's approval" + [ "$fails" = 0 ] && echo "self-test passed: a PASS must name evidence that exists (tree or box); a modified evidence file moves with its registry row and a new one lands free; evidence pinned to another manifest cannot be PASS; a run_status needs the registry's approval" exit $fails fi [ $# -eq 2 ] || { echo "usage: registry-evidence-check.sh | --self-test" >&2; exit 2; } diff --git a/tools/ci/test-record.mjs b/tools/ci/test-record.mjs index e5836dfb7..8c16570c9 100644 --- a/tools/ci/test-record.mjs +++ b/tools/ci/test-record.mjs @@ -74,6 +74,16 @@ function record(reg, map, batch) { if (status === 'DEFERRED') { status = 'NOT RUN'; deferral = cell.deferral || 'deferred by the founder'; } if (!DECISIONS.has(status)) throw new Error(`cell ${cell.cell}: status ${cell.status} is not one of NOT RUN, BLOCKED, FAIL, PASS (RUNNING reads as NOT RUN in progress; DEFERRED as NOT RUN with a deferral note)`); const method = cell.method || batch.method; if (!METHODS.has(method)) throw new Error(`cell ${cell.cell}: method must be one of ${[...METHODS].join(', ')} (never conflated); got ${method}`); + // evidence paths (the coordinator's and lane D's classes, 8 October 2026 21:3x UK): a tree path must exist at record time and name a file, + // never a directory (a directory citation makes every file beneath it the row's evidence and trips the move-together rule on every + // other lane's landing); an absolute path or a box path (host:/path) is outside the tree and is not checked here (rule 1 reads it at the merge) + for (const one of String(cell.evidence || '').split(';').map((x) => x.trim()).filter(Boolean)) { + if (one.startsWith('/') || one.includes(':/')) continue; + if (process.env.TEST_RECORD_NO_TREE === '1') continue; + const abs = path.resolve(ROOT, one); + if (!fs.existsSync(abs)) throw new Error(`cell ${cell.cell}: evidence ${one} is not in the tree at record time (cite only files that exist here; another lane's unlanded file is cited after it lands)`); + if (fs.statSync(abs).isDirectory()) throw new Error(`cell ${cell.cell}: evidence ${one} is a directory; name the file (a directory citation binds every file beneath it to this row)`); + } const missing = Object.entries(cell.prereqs || {}).filter(([, v]) => v !== 'present').map(([k]) => k); if (missing.length) status = 'BLOCKED'; // a cell may narrow its write to named cases of the cell ("cases": [...]) so an incident on one case never writes a cell's other cases @@ -112,7 +122,7 @@ function record(reg, map, batch) { } 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 d = fs.mkdtempSync('/tmp/test-record-'); let fails = 0; process.env.TEST_RECORD_NO_TREE = '1'; // the fixture cells cite fake paths; the tree rule is exercised by its own cases below 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' } ] }; @@ -128,6 +138,14 @@ if (args.includes('--self-test')) { record(reg, map, { run_id: 'r1r', manifest_sha: 'abc', method: 'native', cells: [{ cell: 'pow', status: 'RUNNING', evidence: '/e/pow.log' }] }); if (!(reg.cases[0].run_status === 'NOT RUN' && reg.cases[0].in_progress_since)) { console.log('self-test failed: RUNNING did not become NOT RUN with in_progress_since'); fails = 1; } let badStatus = false; try { record(reg, map, { run_id: 'x', manifest_sha: 'abc', method: 'native', cells: [{ cell: 'pow', status: 'GREEN' }] }); } catch { badStatus = true; } + delete process.env.TEST_RECORD_NO_TREE; let badPath = ''; try { record(reg, map, { run_id: 'x', manifest_sha: 'abc', method: 'native', cells: [{ cell: 'pow', status: 'FAIL', evidence: `${d}/missing.log`.replace(/^\//, 'rel/') }] }); } catch (e) { badPath = String(e.message); } + if (!/not in the tree at record time/.test(badPath)) { console.log(`self-test failed: an evidence path not in the tree was accepted at record time: ${badPath}`); fails = 1; } + fs.mkdirSync(`${d}/dir`); const relDir = path.relative(ROOT, `${d}/dir`); let badDir = ''; + try { record(reg, map, { run_id: 'x', manifest_sha: 'abc', method: 'native', cells: [{ cell: 'pow', status: 'FAIL', evidence: relDir }] }); } catch (e) { badDir = String(e.message); } + if (!/is a directory/.test(badDir)) { console.log(`self-test failed: a directory cited as evidence was accepted: ${badDir}`); fails = 1; } + fs.writeFileSync(`${d}/dir/f.log`, 'x'); let okFile = true; try { record(reg, map, { run_id: 'x', manifest_sha: 'abc', method: 'native', cells: [{ cell: 'pow', status: 'FAIL', evidence: `${relDir}/f.log` }] }); } catch { okFile = false; } + if (!okFile) { console.log('self-test failed: a tree file that exists was refused as evidence'); fails = 1; } + process.env.TEST_RECORD_NO_TREE = '1'; if (!badStatus) { console.log('self-test failed: a status outside the vocabulary was accepted'); fails = 1; } let badMethod = false; try { record(reg, map, { run_id: 'x', manifest_sha: 'abc', cells: [{ cell: 'pow', status: 'PASS' }] }); } catch { badMethod = true; } if (!badMethod) { console.log('self-test failed: a batch with no method was accepted (methods are never conflated)'); fails = 1; }