catalog gates: one entry point, mandated in CLAUDE.md (R-161 ruling)

scripts/catalog_gates.py runs all three gates - image-pins, image-resolvable,
volume-persistence - and exits non-zero if any fails. Mandated in CLAUDE.md the way
felhom.eu/scripts/site_gates.py is: run it after any template change, naming the
app(s) you touched.

Operator ruling, recorded because both alternatives were rejected for measured
reasons. Controller-side enforcement at template load was rejected because such a
check can only read the file, and a static audit of all 53 templates reports the
catalog clean INCLUDING papra - it would pass on the exact defect it exists to
catch; the property is decidable only at runtime. CI was rejected for now: neither
repo has any, and there are no users yet. What was chosen copies the shape that
demonstrably works here - of this project's gates, the only ones that ever get run
are the ones with a single entry point named in a CLAUDE.md; site_gates.py is run,
and R-29's three orphans are named nowhere and have stopped nothing.

Behaviour: 0 all clean / 1 convicted / 2 UNDETERMINED, never a pass; a conviction
outranks an undetermined result so the reader knows which they have. Gate output is
streamed, not captured. App names scope the two gates that accept scoping; with no
names the runtime gate deploys every template and belongs on a scratch host.
Adding a fourth gate means one line in GATES.

R-161 stays OPEN at reduced scope: this is convention, run by a person. Real
automatic enforcement is owed when a second person touches templates.

Verified: image-pins passes standalone (53 templates, 0 unpinned), the
unknown-option path exits 2, and the aggregation was unit-checked over five
gate-code combinations. The runtime leg was deliberately NOT executed - it deploys
templates via docker compose and DooPlex is the recovery chain - so the runner's
end-to-end invocation of that third gate is inferred, not measured, and is flagged
in REPORT.md to be closed on a scratch host at the next campaign.

REPORT.md overwritten per convention; the persistence sweep's report is preserved
at audits/persistence-sweep-2026-08-02/ and pointed to from the new one.
This commit is contained in:
2026-08-02 14:03:55 +02:00
parent 6d45b60f94
commit fd7747d129
5 changed files with 213 additions and 35 deletions
+28
View File
@@ -1,5 +1,33 @@
# Changelog
## 2026-08-02 — one entry point for the catalog's gates (R-161 ruling)
`scripts/catalog_gates.py` runs all three gates — image-pins, image-resolvable, volume-persistence —
and exits non-zero if any fails. Mandated in `CLAUDE.md` the way `felhom.eu/scripts/site_gates.py` is:
**run it after any template change**, naming the app(s) you touched.
**Operator ruling, recorded because the alternatives were rejected for measured reasons.**
Controller-side enforcement at template load was rejected: such a check can only read the file, and a
static audit of all 53 templates reports the catalog clean **including papra** — it would pass on the
exact defect it exists to catch. CI was rejected for now: neither repo has any, and there are no users
yet. What was chosen copies the shape that demonstrably works here — of this project's gates, the only
ones that ever get run are the ones with a single entry point named in a CLAUDE.md; `site_gates.py` is
run, and R-29's three orphaned gates are named nowhere and have stopped nothing.
Behaviour: `0` all clean · `1` convicted · `2` UNDETERMINED, **never a pass**; a conviction outranks an
undetermined result in the summary so the reader knows which they have. Gate output is streamed, not
captured — a runner that swallows diagnostics makes a conviction unreadable. Scoping passes app names
through to the two gates that accept them; with no names the runtime gate deploys every template and
belongs on a scratch host.
**R-161 stays OPEN at reduced scope:** this is convention, run by a person. Real automatic enforcement
is owed when a second person touches templates.
Verified: `image-pins` passes standalone (53 templates, 0 unpinned); the unknown-option path exits 2;
the aggregation was unit-checked over five gate-code combinations. **The runtime leg was deliberately
NOT executed on DooPlex** — it deploys templates via `docker compose`, and DooPlex is the recovery
chain; it belongs on a scratch host.
## 2026-08-02 — persistence sweep: does every app's data land in a folder the template preserves?
Campaign 10's R-156 found papra writing its database into the container's writable layer while the