#!/usr/bin/env python3 # -*- coding: utf-8 -*- """controller_gates.py — THE entry point for this repo's gates. Run from `controller/`: python3 scripts/controller_gates.py # every gate python3 scripts/controller_gates.py --fast # only gates that touch no network and no # container runtime (what .githooks/pre-push runs) Gates, in order (all must pass; **non-zero exit on any failure**): 1. template-id every template id/handle referenced by the dashboard resolves 2. emoji no emoji in the Hungarian UI (design-system v2) 3. native-confirm no native confirm()/prompt() — they freeze browser automation 4. offbox-rename the persisted `offbox` key is never re-guessed as `offbox_target` 5. app-row-dedup no duplicated app-row markup in the dashboard templates 6. mojibake no double-encoded UTF-8 in Hungarian copy 7. docker-v every `docker … -v` mount is a named volume or a proven host path 8. reuse-refs every path cited by this repo's REUSE.md still resolves WHY THIS FILE EXISTS (2026-08-02, closing R-29 leg (a) and half of leg (b)). A census of all thirteen gate scripts across the four felhom repos found one clean correlation: **every check a CLAUDE.md tells a person to run was passing, and two of the four nobody is told to run were failing.** Of the seven gates above, this repo's CLAUDE.md named exactly two; four were reachable only through a line in REUSE.md, and `docker_run_volume_path_gate.py` — RED at the time of the census — through one line in REUSE.md and nothing else. The fix is not more gates, it is one place to run them from. `app-catalog-felhom.eu/scripts/catalog_gates.py` is the canonical shape (R-161) and this copies it deliberately rather than inventing a second one. THE SHARED CHECKER. `reuse_refs_check.py` lives in ONE place — `felhom.eu/scripts/` — and is invoked here across the workspace at `/../felhom.eu/scripts/`. It is deliberately NOT copied into this repo: duplicating it would recreate exactly the drift it exists to detect. If the sibling clone is absent the gate FAILS and prints the path it tried — fail-closed, because a runner that quietly skips a gate is the inert-seam failure this project has shipped four times. EXIT CODES. Each gate returns 0 clean / 1 convicted / 2 inconclusive. This runner exits non-zero if any gate is non-zero, and reports 2 distinctly as INCONCLUSIVE — an undetermined result is never a pass, but it is not a conviction either. """ import os import subprocess import sys SCRIPTS = os.path.dirname(os.path.abspath(__file__)) CTRL = os.path.dirname(SCRIPTS) # /controller — every gate's cwd REPO = os.path.dirname(CTRL) # — the root REUSE.md lives here SHARED_REUSE = os.path.join(os.path.dirname(REPO), "felhom.eu", "scripts", "reuse_refs_check.py") SHARED_INSTRUCTIONS = os.path.join( os.path.dirname(REPO), "felhom.eu", "scripts", "instructions_gate.py") # (label, absolute script path, args, fast) GATES = [ ("template-id", os.path.join(SCRIPTS, "template_id_gate.py"), [], True), ("emoji", os.path.join(SCRIPTS, "emoji_gate.py"), [], True), ("native-confirm", os.path.join(SCRIPTS, "native_confirm_gate.py"), [], True), ("offbox-rename", os.path.join(SCRIPTS, "offbox_rename_gate.py"), [], True), ("app-row-dedup", os.path.join(SCRIPTS, "app_row_dedup_gate.py"), [], True), ("mojibake", os.path.join(SCRIPTS, "mojibake_gate.py"), [], True), ("docker-v", os.path.join(SCRIPTS, "docker_run_volume_path_gate.py"), [], True), ("reuse-refs", SHARED_REUSE, [REPO], True), ("instructions", SHARED_INSTRUCTIONS, [REPO], True), ] VERDICT = {0: "OK", 1: "FAILED", 2: "INCONCLUSIVE"} def hooks_armed_note(root): """Print a WARNING (never a failure) when this clone's pre-push hook is not switched on. core.hooksPath is local config and a clone does not carry it, so an unarmed clone is silent by construction — this is the only place it becomes visible. """ try: val = subprocess.check_output(["git", "config", "--get", "core.hooksPath"], cwd=root, stderr=subprocess.DEVNULL).decode().strip() except Exception: val = "" norm = val.replace("\\", "/").rstrip("/") if norm == ".githooks" or norm.endswith("/.githooks"): return print("WARNING: this clone is UNARMED — core.hooksPath is %s, so the pre-push hook will not\n" " run here. Switch it on once with: git config core.hooksPath .githooks" % (("'" + val + "'") if val else "unset")) def run_gate(label, path, args): if not os.path.exists(path): print("\nFAIL: gate '%s' is MISSING — tried %s" % (label, path)) print(" A missing gate is a failure, never a skip (fail-closed). The reuse-refs") print(" checker is shared and lives in the felhom.eu sibling clone; it is never copied.") return 1 print("\n" + "=" * 78) print("== gate: %s (%s%s)" % (label, os.path.basename(path), (" " + " ".join(args)) if args else "")) print("=" * 78, flush=True) # stream the gate's own output rather than capturing it — its diagnostics are the point. return subprocess.call([sys.executable, path] + args, cwd=CTRL) def main(argv): fast = "--fast" in argv unknown = [a for a in argv if a != "--fast"] if unknown: print("unknown argument(s): %s" % " ".join(unknown)) print("usage: python3 scripts/controller_gates.py [--fast] (run from controller/)") return 2 selected = [g for g in GATES if g[3] or not fast] skipped = [g[0] for g in GATES if not (g[3] or not fast)] print("controller_gates — %d gate(s)%s" % (len(selected), " [--fast]" if fast else "")) if skipped: print(" --fast SKIPPED (deliberate periodic runs, never in a hook): %s" % ", ".join(skipped)) hooks_armed_note(REPO) results = [(label, run_gate(label, path, args)) for label, path, args, _f in selected] print("\n" + "=" * 78) print("== summary") print("=" * 78) worst = 0 for label, rc in results: print(" %-18s %-13s (exit %d)" % (label, VERDICT.get(rc, "ERROR"), rc)) if rc != 0: worst = 1 if rc == 1 or worst == 1 else 2 if worst == 0: print("\nall controller gates OK") return 0 convicted = [l for l, rc in results if rc == 1] undecided = [l for l, rc in results if rc not in (0, 1)] if convicted: print("\nCONVICTED: %s" % ", ".join(convicted)) if undecided: print("UNDETERMINED (never a pass): %s" % ", ".join(undecided)) return worst if __name__ == "__main__": sys.exit(main(sys.argv[1:]))