Files
app-catalog-felhom.eu/scripts/check-test-record.py
T
admin 6db08a5eb3
gates / gates (push) Successful in 1s
test record: an image move must carry its proof (09 decision 13, part 4)
update_ladder: in .felhom.yml, one JSON entry per line (spiked live on
controller v0.266.0 and v0.267.0 first). Two gates: check-test-record.py
(static, CI too) and check-test-record-move.py (history + registry for
moved refs only). 16 decoys, 3 red-proofs. The ONLY writer is
upgrade-test.py --write-ladder (bench AND box proven, digests resolved).
Harness v3: box fixtures on the bench, files_may_change.
Backfill: the 21 moves of 2026-09-22, 21 proven from their records.
No image: line moved.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-23 20:52:32 +02:00

99 lines
4.3 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""check-test-record.py — catalog gate: every ladder is well-formed and agrees with its compose.
python3 scripts/check-test-record.py # every template
python3 scripts/check-test-record.py navidrome romm # only these
python3 scripts/check-test-record.py --root DIR ... # another checkout (decoy tests)
`09-update-architecture.md` §3 decision 13 (the test decides, not the tag) and §6.4 part 4. This is
the STATIC half: no git history, no network — so it runs in the pre-push hook AND in CI, whose clone
is shallow (the R-452 gap that skips every history gate there). Its twin
`check-test-record-move.py` is the half that needs history: an image MOVE must add a proven entry.
WHAT IT CHECKS, per template that carries `update_ladder:` (format and field rules: ladder.py):
1. every line of the block is a one-line JSON entry, and every entry is well-formed
(verdict proven|unrecorded only; a sha256 digest per `to` service; the memory watch's peak on
any entry not backfilled; marks.memory_tight agrees with the peak);
2. the ladder is CONTINUOUS — each entry's `from` is the previous entry's `to`;
3. the NEWEST entry's `to` is EXACTLY the compose's current image per service. This is the fact
that makes the rule hold without history: a compose moved without a new entry no longer
matches its ladder's head, whoever pushed it and however.
A template WITHOUT a ladder passes here — it has never been moved since the gate existed, and its
first move is refused by the twin unless that move brings the first entry.
Exit 0 clean · 1 convicted · 2 inconclusive (nothing to read).
"""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import ladder # noqa: E402
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def check_app(app_dir):
"""List of problem sentences for one template directory ([] = clean or no ladder)."""
fy = os.path.join(app_dir, ".felhom.yml")
comp = os.path.join(app_dir, "docker-compose.yml")
if not os.path.exists(fy) or not os.path.exists(comp):
return []
entries, _raws, errors = ladder.parse(open(fy, encoding="utf-8").read())
if not entries and not errors:
return []
problems = list(errors)
for i, e in enumerate(entries):
for p in ladder.check_entry(e):
problems.append("entry %d: %s" % (i + 1, p))
for i in range(1, len(entries)):
if entries[i].get("from") != entries[i - 1].get("to"):
problems.append("entry %d's `from` is not entry %d's `to` — the ladder has a gap" % (i + 1, i))
if entries:
current = ladder.images_in(open(comp, encoding="utf-8").read())
head = entries[-1].get("to")
if head != current:
diff = sorted(set(current.items()) ^ set((head or {}).items()))
problems.append("the compose's images are not the ladder's newest step — an image moved "
"without a test record, or the record names other refs: %s" % diff)
return problems
def main(argv):
root = ROOT
if "--root" in argv:
i = argv.index("--root")
root = argv[i + 1]
argv = argv[:i] + argv[i + 2:]
only = [a for a in argv if not a.startswith("-")]
tdir = os.path.join(root, "templates")
apps = sorted(only) if only else sorted(os.listdir(tdir))
seen = with_ladder = 0
convicted = []
for app in apps:
d = os.path.join(tdir, app)
if not os.path.isdir(d):
print("test-record: no such template %r" % app)
return 2
seen += 1
text = open(os.path.join(d, ".felhom.yml"), encoding="utf-8").read() if os.path.exists(
os.path.join(d, ".felhom.yml")) else ""
if "update_ladder:" in text:
with_ladder += 1
probs = check_app(d)
if probs:
convicted.append(app)
for p in probs:
print("FAIL %s: %s" % (app, p))
if seen == 0:
print("TEST-RECORD GATE INCONCLUSIVE: no template read")
return 2
print("test-record gate — %d template(s) read, %d carry a ladder, %d convicted"
% (seen, with_ladder, len(convicted)))
return 1 if convicted else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))