6db08a5eb3
gates / gates (push) Successful in 1s
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
141 lines
6.8 KiB
Python
141 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""ladder_backfill.py — ONE-OFF (night 2026-09-23): the first ladder entry for every image move that
|
|
went live BEFORE the test-record gate existed, written from the verdict record that move cited.
|
|
|
|
python3 scripts/ladder_backfill.py --workspace /mnt/5_hdd/felhom.eu/git [--write] <commit> ...
|
|
|
|
For each commit: the app is the one template whose images it moved; `from`/`to` are the per-service
|
|
images at <commit>~1 and <commit>; the evidence is the `Evidence:` path in the commit message. The
|
|
entry is `proven` ONLY when that record exists, says `proven`, and names the same `to` refs;
|
|
otherwise it is written `unrecorded` and NAMED — never invented. Every entry carries
|
|
`backfilled: <today>` and the digest the registry serves for its refs TODAY (decision 17 has no
|
|
earlier measurement to cite; the entry says so in `note`). An `extra` record may be given per app
|
|
(`--extra app=path`) for a later measurement of the SAME step — romm's memory watch (M1) is one. A
|
|
commit that cited no record may be given one (`--evidence app=path`); it is used only if its verdict
|
|
and refs match, and the entry says the backfill named it.
|
|
|
|
It refuses to write an app whose compose no longer stands at the move's `to` (a later move would
|
|
need its own entry first) — the static gate would convict that ladder anyway.
|
|
"""
|
|
import datetime
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import image_digest # noqa: E402
|
|
import ladder # noqa: E402
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
|
|
def git(*a):
|
|
return subprocess.run(["git", "-C", ROOT] + list(a), capture_output=True, text=True)
|
|
|
|
|
|
def main(argv):
|
|
ws = argv[argv.index("--workspace") + 1]
|
|
write = "--write" in argv
|
|
extra, cite = {}, {}
|
|
for i, a in enumerate(argv):
|
|
if a == "--extra":
|
|
k, _, v = argv[i + 1].partition("=")
|
|
extra[k] = v
|
|
if a == "--evidence":
|
|
k, _, v = argv[i + 1].partition("=")
|
|
cite[k] = v
|
|
commits = [a for i, a in enumerate(argv) if re.match(r"^[0-9a-f]{7,40}$", a)]
|
|
today = datetime.date.today().isoformat()
|
|
rows, rc = [], 0
|
|
for c in commits:
|
|
files = [f for f in git("show", "--name-only", "--format=", c).stdout.split()
|
|
if re.match(r"^templates/[^/]+/docker-compose\.yml$", f)]
|
|
if len(files) != 1:
|
|
print("SKIP %s: moves %d templates, expected exactly one" % (c, len(files)))
|
|
rc = 1
|
|
continue
|
|
app = files[0].split("/")[1]
|
|
frm = ladder.images_in(git("show", "%s~1:%s" % (c, files[0])).stdout)
|
|
to = ladder.images_in(git("show", "%s:%s" % (c, files[0])).stdout)
|
|
cur = ladder.images_in(open(os.path.join(ROOT, files[0]), encoding="utf-8").read())
|
|
if cur != to:
|
|
print("REFUSE %s (%s): the compose has moved since (%s) — not backfilled" % (app, c, cur))
|
|
rc = 1
|
|
continue
|
|
body = git("show", "-s", "--format=%B", c).stdout
|
|
m = re.search(r"Evidence:\s*(\S+verdict\.json)", body)
|
|
ev = m.group(1) if m else None
|
|
cited_here = False
|
|
if ev is None and app in cite:
|
|
ev, cited_here = cite[app], True
|
|
verdict, why, tested_at = "unrecorded", None, None
|
|
if ev and os.path.exists(os.path.join(ws, ev)):
|
|
rec = json.load(open(os.path.join(ws, ev)))
|
|
rto = {k: v for k, v in (rec.get("to") or {}).items() if k in to}
|
|
if rec.get("verdict") == "proven" and all(to.get(k) == v for k, v in rto.items()) and rto:
|
|
verdict, tested_at = "proven", rec.get("measured_at")
|
|
else:
|
|
why = "the cited record says verdict=%r to=%r" % (rec.get("verdict"), rec.get("to"))
|
|
else:
|
|
why = "no verdict record found (commit cites %r)" % ev
|
|
digests = {}
|
|
for svc, ref in sorted(to.items()):
|
|
d, err = image_digest.resolve(ref)
|
|
if not d:
|
|
print("INCONCLUSIVE %s: %s %s: %s" % (app, svc, ref, err))
|
|
return 2
|
|
digests[svc] = d
|
|
if tested_at and not ladder.TS_RE.match(tested_at):
|
|
tested_at = tested_at.split(".")[0].replace("+00:00", "") + "Z"
|
|
e = {"from": frm, "to": to, "digest": digests, "verdict": verdict, "tested_at": tested_at,
|
|
"harness_version": 1 if verdict == "proven" else None, "evidence": ev,
|
|
"memory_peak_pct": None,
|
|
"marks": {"files_may_change": False, "needs_person": None, "memory_tight": False},
|
|
"backfilled": today,
|
|
"note": "backfilled from catalog commit %s; box walk only (harness v1, no memory watch); "
|
|
"digest = what the registry served on %s, not a measurement of the tested image"
|
|
% (c, today)}
|
|
if why:
|
|
e["note"] += "; UNRECORDED because " + why
|
|
if cited_here:
|
|
e["note"] += ("; the commit cited no record — this one was named by the backfill because its "
|
|
"from/to refs are the commit's and the commit describes the same walk")
|
|
if app in extra:
|
|
x = json.load(open(os.path.join(ws, extra[app])))
|
|
peaks = [p.get("peak_pct") for p in ((x.get("memory") or {}).get("containers") or {}).values()
|
|
if isinstance(p.get("peak_pct"), (int, float))]
|
|
if x.get("verdict") == "proven" and peaks and x.get("to", {}).get(next(iter(x["to"]))) == \
|
|
to.get(next(iter(x["to"]))):
|
|
pk = round(max(peaks) * 100, 1)
|
|
e["memory_peak_pct"] = pk
|
|
e["marks"]["memory_tight"] = pk > ladder.MEMORY_TIGHT_PCT
|
|
e["note"] += "; memory watch from %s (bench, harness v%s): peak %s%%" % (
|
|
extra[app], x.get("harness_version"), pk)
|
|
probs = ladder.check_entry(e)
|
|
if probs:
|
|
print("REFUSE %s: entry not well-formed: %s" % (app, probs))
|
|
rc = 1
|
|
continue
|
|
rows.append((app, c, verdict, ev, why))
|
|
if write:
|
|
p = os.path.join(ROOT, "templates", app, ".felhom.yml")
|
|
text = open(p, encoding="utf-8").read()
|
|
if "update_ladder:" in text:
|
|
print("REFUSE %s: already carries a ladder" % app)
|
|
rc = 1
|
|
continue
|
|
open(p, "w", encoding="utf-8").write(ladder.append_entry(text, e))
|
|
for app, c, v, ev, why in rows:
|
|
print("%-16s %s %-10s %s%s" % (app, c, v, ev or "-", (" (" + why + ")") if why else ""))
|
|
print("%d backfilled (%d proven, %d unrecorded)%s" % (
|
|
len(rows), sum(1 for r in rows if r[2] == "proven"), sum(1 for r in rows if r[2] != "proven"),
|
|
"" if write else " — DRY RUN, nothing written"))
|
|
return rc
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv[1:]))
|