REPORT: catalog_gates --fast + pre-push hook
This commit is contained in:
@@ -1,60 +1,66 @@
|
||||
# REPORT — one entry point for the catalog's gates (2026-08-02)
|
||||
# REPORT — `catalog_gates --fast` and the pre-push hook (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.**
|
||||
**Overwritten** per the standing rule. The prior contents (the entry point itself, earlier the same
|
||||
day) have their durable record in `CHANGELOG.md`; nothing was lost.
|
||||
|
||||
> 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.
|
||||
**Change:** `scripts/catalog_gates.py` gains `--fast`; `.githooks/pre-push` (new) runs it;
|
||||
`scripts/test_catalog_gates.py` (new) pins its behaviour; `CLAUDE.md` records the hook.
|
||||
**No gate logic changed, no template touched, no version** (this repo carries none).
|
||||
|
||||
## 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 |
|
||||
| Commit | What |
|
||||
|---|---|
|
||||
| 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` |
|
||||
| `c3e4bb1` | `scripts/catalog_gates.py` `--fast` + `scripts/test_catalog_gates.py` + `.githooks/pre-push` + `CLAUDE.md` |
|
||||
| `340ff2a` | `CHANGELOG.md` |
|
||||
|
||||
**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).
|
||||
Baseline on arrival: `fd7747d1293c`, clean, `HEAD == origin/main` — matched the spec's anchor.
|
||||
|
||||
**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.
|
||||
## What `--fast` selects, and why the other two are excluded
|
||||
|
||||
`--fast` runs only gates that touch **no network and no container runtime**. Today that is gate 1,
|
||||
`check-image-pins.py`. `check-image-resolvable.py` (network) and `check-volume-persistence.py`
|
||||
(Docker, minutes per app) are **not** in it.
|
||||
|
||||
That exclusion is the point, not a compromise: **a push that pulls images and starts containers gets
|
||||
bypassed within a week, and the bypass becomes the habit.** Both remain deliberate periodic runs — at
|
||||
the start of a catalog campaign, before a publish train that vouches the catalog, and whenever a
|
||||
template's `volumes:` block or image tag changes — on a scratch host, never a customer box.
|
||||
|
||||
**The skip is announced**, with its reason and with what still owes a run. A silently narrowed run
|
||||
reads as "covered everything" when it did not. Default behaviour with no flag is **unchanged**: all
|
||||
three gates, exactly as before.
|
||||
|
||||
## `.githooks/pre-push`
|
||||
|
||||
Runs `catalog_gates.py --fast` and refuses the push, printing a line before and after because an
|
||||
absent log line is not evidence a hook ran. Limits, written into the hook: **per-clone**
|
||||
(`core.hooksPath` is local config; this clone is armed — `git config --get core.hooksPath` →
|
||||
`.githooks`) and **`git push --no-verify` bypasses it on purpose**. It was **not** used against this
|
||||
repo; both real pushes ran the hook and passed.
|
||||
|
||||
This makes R-161's convention half automatic-ish, not automatic. The unbypassable half is CI, which
|
||||
has no execution mechanism here yet and is now tracked as `felhom.eu` `OPEN-ITEMS.md` **R-168**.
|
||||
|
||||
## `test_catalog_gates.py` — pins the CONTENT, not just the exit code
|
||||
|
||||
Five tests: the static gate's own stdout must appear (an inert runner prints the summary while
|
||||
calling nothing), the runtime gates' must **not**, the skip must be announced, and the no-flag path
|
||||
must still select all three. **Red-proof:** replacing `run_gate`'s body with `return 0` turns it red.
|
||||
|
||||
## Verification
|
||||
|
||||
```
|
||||
catalog_gates — static gate only [--fast]
|
||||
--fast SKIPPED: image-resolvable, volume-persistence — …
|
||||
|
||||
image-pin gate OK — 53 templates, 0 unpinned images
|
||||
|
||||
image-pins OK (exit 0)
|
||||
|
||||
all catalog gates OK
|
||||
```
|
||||
|
||||
`python3 scripts/test_catalog_gates.py` → **5 tests, OK**.
|
||||
`python3 scripts/test_check_volume_persistence.py` — untouched, still green.
|
||||
|
||||
Full cross-repo detail, every red-proof, the `core.hooksPath` probe measurements and the hub
|
||||
deployment: `felhom.eu/REPORT.md`.
|
||||
|
||||
Reference in New Issue
Block a user