681cc663ef
gates / gates (push) Failing after 13s
Every gate was DECOYED - the label constructed without the fact, the gate run, the verdict recorded.
No verdict here was reached by reading, because reading is exactly how the five prior instances hid.
SCOPE IS A FACT TOO, and it was the big one. Six gates decided what to look at with os.listdir - one
directory level. Every one was green AND CORRECT, because no template subdirectory exists today; every
one would have gone blind the moment anyone added templates/partials/, which is an ordinary act. A
single planted file carrying an emoji, a native confirm(), hand-rolled row markup, a dangling JS id
reference, a templated secret and an unregistered retrieval promise passed all six.
THE CONTROL IS WHAT MAKES THAT A MEASUREMENT: mojibake and docker-v already used os.walk, saw the
identical planted file, and convicted. So the cause was the listing, not the decoy.
COMMENTS ARE NOT CODE, AND COMMENTS ARE NOT CONTROLS. debug-routes matched `case subpath == "x"` in
raw text, so a case left in a commented-out block counted as a live handler - which is R-400's
original defect (seven dead controls on the page an operator opens when something is already wrong)
reached through the one door its own gate could not see. app-row-dedup's MUST_USE check had the same
shape: a commented-out {{template "app_list_row"}} satisfied it.
Stripping is deliberately crude in debug_route_gate, and that is correct there: its own docstring
insists on ten lines that cannot rot. A // inside a string literal truncates that line, which can
only ever HIDE a reference, never invent one - it fails in the safe direction.
NOT FIXED, and left open with its decoy rather than quietly patched: R-425, offbox-rename scans a
fixed three-entry FILES list, so banned NAS branding in a NEW offbox template passes. The scope was
correct when written and silently narrows every time the feature grows a file.
test_gate_decoys.py holds 10 decoys and declares COVERS, which felhom.eu's new decoy-coverage gate
AST-parses - a substring search for coverage would be the very shape this sweep exists to find.
No Go code. No version bump. No image. No golden owed.
Survey: felhom.eu/documentation/audits/AUDIT-gate-decoys-2026-09-01.md
79 lines
4.8 KiB
Markdown
79 lines
4.8 KiB
Markdown
---
|
|
paths: ["controller/**/*.go", "controller/**/*.html", "controller/**/*.css", "controller/scripts/**"]
|
|
---
|
|
|
|
# Gates and logging — felhom-controller
|
|
|
|
## The ONE entry point
|
|
|
|
**Run `python3 controller/scripts/controller_gates.py` (from `controller/`) after ANY change in this
|
|
repo.** It runs the local gates — `template_id_gate`, `emoji_gate`, `native_confirm_gate`,
|
|
`offbox_rename_gate`, `app_row_dedup_gate`, `mojibake_gate`, `docker_run_volume_path_gate`,
|
|
`secret_in_markup_gate`, `retrieval_promise_gate`, `debug_route_gate` — plus `reuse_refs_check`,
|
|
`instructions_gate` and `observations_gate` on the repo root, streaming each gate's own output and
|
|
exiting non-zero if any fails. **The runner's `GATES` table is the list; this sentence is a pointer to
|
|
it, not a second copy** — it has already drifted once (it said "seven" while nine were registered).
|
|
|
|
- `--fast` selects the gates that touch no network and no container runtime; today that is all of them.
|
|
- **A missing gate script is a FAILURE, never a skip.**
|
|
- **The shared `reuse_refs_check.py` and `instructions_gate.py` live in `felhom.eu/scripts/` and are
|
|
never copied here** — a copy would recreate the drift they detect; an absent sibling clone FAILS.
|
|
- **The pre-push hook** (`.githooks/pre-push`) runs it with `--fast` and refuses a failing push. It is
|
|
per-clone — switch it on once with `git config core.hooksPath .githooks`, and a manual run WARNS
|
|
when this clone is unarmed. `git push --no-verify` bypasses it deliberately; **say so in the session
|
|
report when you use it** — CI re-runs the same entry point on every push and **emails the operator
|
|
on failure**, so a bypass is noticed even though it is not blocked (R-168, CLOSED 2026-08-02).
|
|
|
|
<!--
|
|
WHY A RUNNER AND NOT SEVEN INVOCATIONS (2026-08-02, R-29) — rationale, not a directive.
|
|
A census of all thirteen gates across the four repos found that every check a CLAUDE.md named was
|
|
passing, and two of the four nobody is told to run were failing. This repo's CLAUDE.md used to name
|
|
two of the seven; the other five were reachable only through a line in REUSE.md, and
|
|
docker_run_volume_path_gate.py was RED. The single-entry-point shape is the only one that
|
|
demonstrably gets run. app-catalog-felhom.eu/scripts/catalog_gates.py is the canonical version of
|
|
the runner (R-161); repo_gates.py copies it. site_gates.py is a *gate*, not a runner — do not model
|
|
new work on it.
|
|
-->
|
|
|
|
## Logging
|
|
|
|
New leveled lines use `internal/logx` — DEBUG always reaches the debug ring; stdout respects
|
|
`logging.level`. English, keys-never-values, durations on outcomes. Full rules:
|
|
`felhom.eu/documentation/runbooks/logging-conventions.md`.
|
|
|
|
## Health checks issue no block I/O
|
|
|
|
A probe that touches a wedged device enters uninterruptible sleep, survives `SIGKILL`, and cannot be
|
|
recovered until the device returns or the host reboots — so `systemctl restart` hangs too. A timeout
|
|
protects the caller's control flow and nothing else: the blocked thread remains. Liveness is decided
|
|
from `/proc` and kernel state, never by reading or writing the filesystem.
|
|
|
|
<!--
|
|
Measured, R-117 spike §6.3 (felhom.eu/documentation/audits/SPIKE-r117-bind-liveness-2026-07-30.md):
|
|
a probe stayed in D state 3m50s after kill -9; a buffered write with no fsync blocked too (O_CREAT
|
|
needs journal access); and statfs/getdents returned HEALTHY on a namespace that EIOs every byte —
|
|
fast, and wrong.
|
|
-->
|
|
|
|
## A gate ships with a decoy test that has been seen to fail (R-421)
|
|
|
|
**A decoy is the LABEL without the FACT** — a directory with the right name and no bake log, a
|
|
handler case that exists only in a comment, a note whose prose mentions the marker it lacks. Write
|
|
one for every new gate, run it, and watch it convict. `scripts/decoy_coverage_gate.py` refuses a gate
|
|
registered without one, or without a named exemption carrying its row.
|
|
|
|
**Earned by five instances, every one found by accident:** R-410, R-400, R-378, R-419, R-94. The
|
|
2026-09-01 sweep read all 29 gate scripts and fooled 16 of them. The four shapes to test against:
|
|
|
|
1. **name-for-fact** — it matches a path or directory NAME while the fact lives inside the file.
|
|
2. **substring-for-field** — it matches a token anywhere in a body instead of in the field carrying it.
|
|
3. **declaration-for-reachability** — it checks a thing is declared, not that it RESOLVES.
|
|
4. **constant-for-measurement** — it compares a value against itself.
|
|
|
|
**Scope is a fact too.** Eight of the sixteen were `os.listdir` (one level) where `os.walk` was meant:
|
|
green and correct today, blind the moment anyone adds a subdirectory. Prefer `os.walk`, and prefer a
|
|
glob over a hand-maintained list of files.
|
|
|
|
**A decoy nobody would write proves nothing** — say the gate is sound and move on. Five of mine were
|
|
withdrawn as illegitimate and are named in `documentation/audits/AUDIT-gate-decoys-2026-09-01.md`.
|