186546d562
gates / gates (push) Successful in 27s
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
57 lines
3.0 KiB
Python
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)
|