The gate manifest regenerated
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
a6cf4ada87
commit
43e2b571b9
1 changed files with 121 additions and 0 deletions
121
tools/ci/registry-evidence-check.sh
Executable file
121
tools/ci/registry-evidence-check.sh
Executable file
|
|
@ -0,0 +1,121 @@
|
|||
#!/usr/bin/env bash
|
||||
# The registry's evidence rules (the founder's standard: GOV-02 thresholds before results, GOV-04 raw evidence preserved, GOV-08
|
||||
# stale evidence invalidated; main through the coordinator, 8 October 2026, 18:4x UK). Run by merge-to-master.sh before the
|
||||
# merge on <base> <head>:
|
||||
# 1. a landing that sets a case's run_status to PASS must carry the evidence_path it names: a path in the tree at <head>, or
|
||||
# an artefact on a build box (build-N:/srv/artefacts/... or /srv/artefacts/..., checked over ssh when the box answers;
|
||||
# a box that does not answer is a line, not a refusal); an empty or missing path is refused
|
||||
# 2. a landing that touches a file under docs/analysis/ or an evidence directory that a registry row names, without
|
||||
# updating that row (its `updated` field) in the same diff, is named (a refusal: the row and its evidence move together)
|
||||
# 3. a case whose evidence_record.manifest_sha is set and differs from the registry's pinned manifest (top-level
|
||||
# `pinned_manifest_sha`, when present) reads NOT RUN, never PASS: stale evidence (GOV-08)
|
||||
# 4. a run_status may be written only while the registry's approval stands (top-level `approval` present): thresholds are
|
||||
# frozen before results (GOV-02)
|
||||
# tools/ci/registry-evidence-check.sh <base> <head> exit 0 clean, 1 refused (the rows named), 2 bad args
|
||||
# tools/ci/registry-evidence-check.sh --self-test
|
||||
set -euo pipefail
|
||||
HERE="$(cd "$(dirname "$0")" && pwd -P)"; ME="$HERE/$(basename "$0")"
|
||||
REG="${REGISTRY_PATH:-docs/plans/igneum-2.0-test-registry.json}"
|
||||
check() { # <base> <head> ; 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
|
||||
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]
|
||||
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')
|
||||
def in_tree(p):
|
||||
return subprocess.run(['git', 'cat-file', '-e', f'{head}:{p}'], capture_output=True).returncode == 0
|
||||
def on_box(p):
|
||||
box, _, path = p.partition(':') if ':' in p else ('build-1', '', p)
|
||||
ip = {'build-1': '188.40.146.49', 'build-2': '142.132.249.238', 'build-3': '62.238.91.43', 'build-4': '178.63.182.105'}.get(box)
|
||||
if not ip: return 'unknown-box'
|
||||
r = subprocess.run(['ssh', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=6', f'build@{ip}', f'test -e {path!r} && echo yes || echo no'], capture_output=True, text=True)
|
||||
if r.returncode != 0: return 'no-answer'
|
||||
return 'yes' if 'yes' in r.stdout else 'no'
|
||||
for c in cases(h):
|
||||
cid = c['id']; old = hb.get(cid, {}); st = c.get('run_status'); changed = (st != old.get('run_status')) or (c.get('run_id') != old.get('run_id')) or (c.get('evidence_path') != old.get('evidence_path'))
|
||||
if changed and st and st != 'NOT RUN' and not approved:
|
||||
refused.append(f'refused {cid}: run_status {st} written while the registry carries no approval (GOV-02: thresholds before results)')
|
||||
rec = c.get('evidence_record') or {}
|
||||
if pinned and rec.get('manifest_sha') and not str(pinned).startswith(str(rec['manifest_sha'])[:8]) and not str(rec['manifest_sha']).startswith(str(pinned)[:8]) and st == 'PASS':
|
||||
refused.append(f'refused {cid}: PASS on evidence pinned to manifest {rec["manifest_sha"]} while the registry pins {pinned} (GOV-08: stale evidence reads NOT RUN)')
|
||||
if changed and st == 'PASS':
|
||||
p = (c.get('evidence_path') or '').strip()
|
||||
if not p: refused.append(f'refused {cid}: PASS with no evidence_path'); continue
|
||||
for one in [x.strip() for x in p.split(';') if x.strip()]:
|
||||
if one.startswith('/srv/') or ':/srv/' in one:
|
||||
if os.environ.get('REGISTRY_CHECK_NO_SSH') == '1': continue
|
||||
v = on_box(one)
|
||||
if v == 'no': refused.append(f'refused {cid}: PASS names {one}, which is not on the box')
|
||||
elif v in ('no-answer', 'unknown-box'): print(f'registry-evidence: {cid}: {one} could not be checked ({v}); not a refusal')
|
||||
elif not in_tree(one): refused.append(f'refused {cid}: PASS names {one}, which is not in the tree at {head[:8]}')
|
||||
# rule 2: a touched evidence file under docs/analysis or named by a row, without that row's `updated` moving
|
||||
named = {}
|
||||
for c in cases(h):
|
||||
for one in [x.strip() for x in (c.get('evidence_path') or '').split(';') if x.strip()]:
|
||||
if not one.startswith('/') and ':/' not in one: named.setdefault(one.rstrip('/'), []).append(c['id'])
|
||||
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
|
||||
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})
|
||||
if not ids: continue # docs/analysis file no row names: free
|
||||
moved = [i for i in ids if (h and {c['id']: c for c in cases(h)}[i].get('updated')) != hb.get(i, {}).get('updated')]
|
||||
if not moved: refused.append(f'refused: {t} changed but its registry row(s) {", ".join(ids)} did not (the row and its evidence move together)')
|
||||
for r in refused: print(r)
|
||||
sys.exit(1 if refused else 0)
|
||||
PY
|
||||
rm -f "$tmp_h" "$tmp_b"; return $rc
|
||||
}
|
||||
if [ "${1:-}" = --self-test ]; then
|
||||
d=$(mktemp -d); trap 'rm -rf "$d"' EXIT; fails=0; export REGISTRY_CHECK_NO_SSH=1
|
||||
mk() { python3 - "$@" <<'PY'
|
||||
import json, sys
|
||||
path, approval, pinned, rows = sys.argv[1], sys.argv[2] == '1', sys.argv[3], json.loads(sys.argv[4])
|
||||
reg = {'title': 't', 'suites': [{'code': 'X', 'tests': [dict({'id': i, 'method': 'Automated', 'accept': 'a'}, **r) for i, r in rows.items()]}]}
|
||||
if approval: reg['approval'] = 'yes'
|
||||
if pinned != '-': reg['pinned_manifest_sha'] = pinned
|
||||
json.dump(reg, open(path, 'w'), indent=1)
|
||||
PY
|
||||
}
|
||||
( cd "$d" && git init -q -b master . && mkdir -p docs/plans docs/analysis evidence && echo e > evidence/a.log && echo r > 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": "t0"}}'
|
||||
git add -A && git -c user.name=t -c user.email=t@t commit -q -m base && git tag base
|
||||
# pass-ok: PASS with an evidence path in the tree
|
||||
mk docs/plans/igneum-2.0-test-registry.json 1 - '{"X-1": {"run_status": "PASS", "evidence_path": "evidence/a.log", "updated": "t1"}, "X-2": {"run_status": "RUNNING", "evidence_path": "docs/analysis/row.md", "updated": "t0"}}'
|
||||
git -c user.name=t -c user.email=t@t commit -qam pass-ok && git tag pass-ok
|
||||
# pass-missing: PASS naming a path not in the tree
|
||||
git checkout -q -b m base; mk docs/plans/igneum-2.0-test-registry.json 1 - '{"X-1": {"run_status": "PASS", "evidence_path": "evidence/none.log", "updated": "t1"}, "X-2": {"run_status": "RUNNING", "evidence_path": "docs/analysis/row.md", "updated": "t0"}}'
|
||||
git -c user.name=t -c user.email=t@t commit -qam pass-missing && git tag pass-missing
|
||||
# evidence-touched: docs/analysis/row.md changed, X-2's updated did not
|
||||
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
|
||||
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
|
||||
# unapproved: a run_status written with no approval
|
||||
git checkout -q -b u base; mk docs/plans/igneum-2.0-test-registry.json 0 - '{"X-1": {"run_status": "RUNNING", "evidence_path": "evidence/a.log", "updated": "t1"}, "X-2": {}}'; git -c user.name=t -c user.email=t@t commit -qam unapproved && git tag unapproved ) >/dev/null 2>&1
|
||||
cd "$d"
|
||||
bash "$ME" base pass-ok >/dev/null 2>&1 || { echo "self-test failed: a PASS with its evidence in the tree was refused: $(bash "$ME" base pass-ok 2>&1)"; fails=1; }
|
||||
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; }
|
||||
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"
|
||||
exit $fails
|
||||
fi
|
||||
[ $# -eq 2 ] || { echo "usage: registry-evidence-check.sh <base> <head> | --self-test" >&2; exit 2; }
|
||||
rc=0; out=$(check "$1" "$2") || rc=$?
|
||||
printf '%s\n' "$out" | sed -n 's/^refused/registry-evidence: REFUSED:/p; /^registry-evidence:/p' | grep . || true
|
||||
[ "$rc" = 0 ] && echo "registry-evidence: every PASS names existing evidence, every touched evidence file moves with its row, no stale evidence reads PASS"
|
||||
exit $rc
|
||||
Loading…
Reference in a new issue