Same-tag security fixes as tested steps (09 decision 52, R-740): re-test entries, their gates, the monthly command
gates / gates (push) Successful in 2s

- A RE-TEST entry: from == to, digest = the registry's new digest, digest_from = the tested one, box_evidence.
  ladder.check_entry refuses one with no new digest, no digest_from or no box proof; check-test-record rule 2b
  ties digest_from to the previous entry's digest; check-test-record-move now judges re-tests too (they change
  .felhom.yml only — the gate looked at compose moves alone) and refuses a digest the registry no longer serves.
  Decoys: 8 cases in test_gate_decoys.py, seen red with the rules switched off.
- upgrade-test.py --retest <app> [svc]: FROM the ladder head's tested digest TO the registry's current one, the
  full method; --write-ladder writes a re-test entry (plain refs + digest_from), refusing without the box venue or
  when the registry moved again. Writer tests, red-proofed.
- scripts/retest-floating.py — ONE command: --dry-run lists, --engines-only is the ruled start; bench, box
  (retest_box.py on 9202 via the drill catalog), writer, gates, one commit per app. box_walk.py moves the box
  client into the catalog. Run today: nothing to re-test on the database/redis lines.
- End to end on 9202 (drill): docmost at the OLD redis digest, the re-test, "run tonight's chain now" -> the leg
  pressed it, the new digest runs, read back, badge current.
- Also: upgrade-test.py BENCH_ENV_OVERRIDES (R-739, wanderer's DB address on the bench, recorded per verdict);
  test_gate_decoys.py read kimai's tag and date from the clone (red on main since kimai moved).

Evidence: felhom.eu/documentation/audits/night-rulings-2026-09-30/A/

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-30 23:06:09 +02:00
parent 45d84827ad
commit 6a3ead9ebe
10 changed files with 1226 additions and 21 deletions
+17
View File
@@ -32,6 +32,13 @@ AN ENTRY (all keys required unless marked):
`upgrade-test.py --write-ladder` only when the bench AND the box both converted it
backfilled (optional) "YYYY-MM-DD" — written by the backfill from an EXISTING record,
never by a new test; a new move may not carry it
digest_from (a RE-TEST only, `09` §3 decision 52) {service: "sha256:…"} — the digest the re-test
was run FROM. A re-test is an entry whose `from` equals its `to` (the same tags): the
catalog proved the same tag at a NEW digest (an upstream same-name fix). It needs
`digest_from` for every service, at least one service whose `digest` differs from it,
and `box_evidence` (both venues); `check-test-record.py` rule 2b ties `digest_from` to
the previous entry's `digest`. Written by `upgrade-test.py --write-ladder` from a
`--retest` verdict, never by hand.
STEP DEFINITIONS (`09` §6.4 part 5, controller v0.268.0): every entry but the NEWEST carries its own
complete compose file at `templates/<app>/steps/<step_key(to)>.yml` — the box climbs one step at a time
@@ -138,6 +145,16 @@ def check_entry(e):
for svc in e["digest"]:
if svc not in e["to"]:
p.append("digest names a service %r that `to` does not" % svc)
if e["from"] == e["to"] and v == "proven" and backfilled is None: # a RE-TEST (decision 52)
df = e.get("digest_from")
if not isinstance(df, dict) or set(df) != set(e["to"]) or not all(isinstance(x, str) and DIGEST_RE.match(x) for x in df.values()):
p.append("a re-test (from == to) needs digest_from: {service: sha256} for every service — the digest it was tested FROM")
elif all(df[s] == e["digest"].get(s) for s in e["to"]):
p.append("a re-test (from == to) whose digest is the same as its digest_from tests nothing new — no new digest")
if not (isinstance(e.get("box_evidence"), str) and e["box_evidence"].strip()):
p.append("a re-test (from == to) must cite BOTH venues: box_evidence is missing")
elif e.get("digest_from") is not None:
p.append("digest_from belongs only to a re-test (from == to)")
conv = e.get("engine_conversion")
if conv is not None: # `09` §6.4 part 10 — the box converts ONLY on this mark
if not (isinstance(conv, dict) and set(conv) == {"service", "engine", "from", "to"}