Files
felhom.eu/documentation/audits/the-28-2026-09-22/mktable.py
T
admin 186546d562
gates / gates (push) Successful in 27s
THE TWENTY-EIGHT: every app no drill had touched, walked in one night
All 28 walked on scratch guest 9202 against the private drill catalog. 26 deployed, 6 proven,
5 inconclusive, 14 with no upstream edge, 1 failed honestly (outline 1.9.1->1.10.1, HELD with the
right sentence), 2 undeployable - one (plant-it) by design, refused by the lifecycle gate, proven
live for the first time. Each app also got the half the update night skipped: a restore from its own
copy with the seed read back again - 21 restored, 2 correctly REFUSED per 07 6.2.

R-630 RAISED TO P1 by measurement: a stack with NO probe container does not skip verifying - it
waits out the full health timeout and HOLDS, stopping an app whose three containers read healthy.
The controller's own words: "not healthy within 5m0s (last: no probe container)".

R-633 opened: a remove sent during a restore reports success and leaves a container restarting with
a live public route. The product already refuses that clash for update and for restore, naming the
blocker; remove has no such guard.

R-634 opened: an app can be running, healthy and serving while recorded as deployed=false, and is
then unremovable. Reproducible alone on sparkyfitness; concurrency-linked on two others.

R-631 and R-632 CLOSED. Register 321 -> 323. Seven interventions, six of them my own harness -
named, with what each cost. No product code. The live catalog's image: lines are byte-identical to
the start of the night.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-09-22 16:00:56 +02:00

57 lines
3.0 KiB
Python

#!/usr/bin/env python3
"""Build the report's table FROM the verdict records, so it cannot drift from the evidence."""
import glob, json, io, os
HERE = os.path.dirname(os.path.abspath(__file__))
THE28 = ("calcom calibre-web claper code-server crafty-controller emby ghost gokapi gramps-web "
"homebox homepage immich jellyfin kimai komga onlyoffice outline paperless-ngx plant-it "
"plex radarr rallly recipe-importer seerr sonarr sparkyfitness termix wanderer").split()
# CLASSES READ FROM `07-backup-architecture.md` §6.2, not re-derived tonight (the brief says so,
# and re-deriving is exactly how the brief's own list went wrong). A = at least one readable file
# leg; B = data entirely in named volumes. Of the 28, four are A and the rest are B.
CLASS_A = {"calibre-web", "immich", "komga", "paperless-ngx"}
# which apps carry a database/search SERVICE of their own, read from each compose file
ENGINES = {"calcom": "postgres", "claper": "postgres", "outline": "postgres+redis",
"paperless-ngx": "postgres+redis", "rallly": "postgres", "sparkyfitness": "postgres",
"kimai": "mariadb", "immich": "postgres+redis", "wanderer": "meilisearch"}
def klass(a):
c = "A file-leg" if a in CLASS_A else "B volumes-only"
return c + (" + " + ENGINES[a] if a in ENGINES else "")
recs = {}
for f in glob.glob(os.path.join(HERE, "apps", "*", "verdict.json")):
d = json.load(open(f, encoding="utf-8"))
recs[d["app"]] = d
rows, tot = [], {}
rows.append("| app | class | deployed | seeded | backup | edge | update | restore | removed clean | s | evidence |")
rows.append("|---|---|---|---|---|---|---|---|---|---|---|")
for a in THE28:
d = recs.get(a)
if not d:
rows.append(f"| `{a}` | — | **NOT WALKED** | — | — | — | — | — | — | — | — |")
tot["not-walked"] = tot.get("not-walked", 0) + 1
continue
e = d.get("edge")
edge = (f"`{e['from'].rsplit(':',1)[1]}` → `{e['to'].rsplit(':',1)[1]}`" if e else "none upstream")
seeded = ("yes" if d["seed_read_before"] else
("no route" if d.get("seed_route") in (None, "none written") or not d["deployed"]
else "route failed"))
upd = d.get("update_phase_final") or ("—" if not e else "not reached")
bk = (str(d["backup"]["snapshots_offered"]) + " copy" if d.get("backup") else "—")
rows.append("| `%s` | %s | %s | %s | %s | %s | %s | %s | %s | %s | `apps/%s/` |" % (
a, klass(a), "yes" if d["deployed"] else "**no**", seeded, bk, edge, upd,
d.get("restore_verdict", "—"),
{True: "yes", False: "**no**", None: "—"}.get(d.get("removed_clean"), "—"),
d.get("duration_s"), a))
tot[d["verdict"]] = tot.get(d["verdict"], 0) + 1
out = "\n".join(rows) + "\n\n**Totals:** " + " · ".join(
f"**{v}** {k}" for k, v in sorted(tot.items(), key=lambda kv: -kv[1])) + \
f" — {sum(tot.values())} of 28 recorded.\n"
io.open(os.path.join(HERE, "TABLE.md"), "w", encoding="utf-8").write(out)
print(out)