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

61 lines
3.7 KiB
Markdown

# 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.