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