Files
app-catalog-felhom.eu/REPORT.md
T
admin fd7747d129 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.
2026-08-02 14:03:55 +02:00

3.7 KiB

REPORT — one entry point for the catalog's gates (2026-08-02)

Change: scripts/catalog_gates.py — runs all three catalog gates, non-zero exit on any failure — plus its mandate in CLAUDE.md and a REUSE.md row. No gate logic changed; no template touched.

The previous REPORT.md described the catalog persistence sweep (43 CLEAN · 3 BROKEN · 7 UNDETERMINED over 53 templates). It is not lost: the full report, per-app evidence and proofs are at audits/persistence-sweep-2026-08-02/README.md, and its CHANGELOG entry sits directly below this one. This file is overwritten per the repo convention.

What was built

python3 scripts/catalog_gates.py                 # every AVAILABLE app, all three gates
python3 scripts/catalog_gates.py papra wishlist  # only these app dirs — the normal case
python3 scripts/catalog_gates.py --all           # include hidden/abandoned apps too
Gate Kind Scoped by app name
check-image-pins.py static, instant, whole repo no
check-image-resolvable.py network yes
check-volume-persistence.py runtime yes

Exit: 0 all clean · 1 convicted · 2 UNDETERMINED. A conviction outranks an undetermined result in the summary, and 2 is never folded into a pass — an app that wrote nothing has not been shown correct, and a throttled registry has not shown an image alive. Gate output is streamed rather than captured: a runner that swallows diagnostics makes a conviction unreadable.

Why a runner, and not the two alternatives (operator ruling, R-161)

  • Controller-side at template load — rejected, and this is the substantive reason. Such a check can only read the file. A static audit of all 53 templates reports the catalog clean including papra, whose compose is well-formed while its database goes to the container's writable layer. It would pass on the exact defect it exists to catch. The property is decidable only at runtime.
  • CI — rejected for now. Neither repo has any CI to build on, and there are no users yet.
  • A named single entry point — chosen, because it is the shape that works here. Of this project's gates, the only ones that ever get run are those with one entry point named in a CLAUDE.md: felhom.eu/scripts/site_gates.py is run; R-29's three orphaned gates are named nowhere and have stopped nothing. This copies that shape rather than adding a fourth gate nobody invokes.

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.

Verification — and what was deliberately not run

Check Result
syntax OK
check-image-pins.py standalone OK — 53 templates, 0 unpinned images (exit 0)
unknown-option path exit 2
aggregation, unit-checked over 5 gate-code combinations (0,0,0)→0 · (0,0,1)→1 · (0,2,0)→2 · (0,2,1)→1 · (1,2,0)→1

The runtime leg was NOT executed here, deliberately. check-volume-persistence.py deploys each template with docker compose on the invoking host; DooPlex is Tier 2 — the recovery chain — and the gate's own documentation says scratch host, never a customer box. Its correctness was already established by the sweep that wrote it (53 templates, canary self-test in both directions).

What is therefore unproven here: the runner's end-to-end invocation of that third gate. The plumbing is identical to the two it did invoke and the argument passing is unit-checked, but that is an inference, not a measurement. Run it once on a scratch host at the start of the next catalog campaign — that is the cheapest moment to close it.