From a17db0c3c808731629078ec6eb1ab9788681efba Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Mon, 19 Oct 2026 10:34:41 +0200 Subject: [PATCH] hub-gofmt gate in repo_gates.py with its decoys (R-1160) --- scripts/hub_gofmt_gate.py | 82 +++++++++++++++++++++++++++++++++++++ scripts/repo_gates.py | 4 ++ scripts/test_gate_decoys.py | 42 +++++++++++++++++++ 3 files changed, 128 insertions(+) create mode 100644 scripts/hub_gofmt_gate.py diff --git a/scripts/hub_gofmt_gate.py b/scripts/hub_gofmt_gate.py new file mode 100644 index 00000000..1428197d --- /dev/null +++ b/scripts/hub_gofmt_gate.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""hub_gofmt_gate.py — every Go file in the hub module is gofmt-clean (R-1160; the controller's R-454 gate, same shape). + +MEASURED 2026-10-19: 22 hub files were not gofmt-clean and nothing noticed (fixed by one formatting-only commit, +d5e3c8b0). The controller's history below is why a gate and not a one-off clean-up. + +MEASURED 2026-09-02: five internal/web test files had been gofmt-unclean for an unknown length of +time and nothing noticed — `go vet` does not check formatting and no gate did. By 2026-10-05 the five +were clean and TWELVE others were not: the count only grows while nothing looks, and every `gofmt -l` +someone runs by hand is then noise that hides a real one. + +Run from the repo root: python3 scripts/hub_gofmt_gate.py [] (default: hub/) +Exit 0 clean · 1 convicted (lists the files) · 2 inconclusive (gofmt not found — never a pass). + +gofmt is the Go toolchain's own formatter (not a shell utility), found on PATH or under +`go env GOROOT`/bin. Decoy: scripts/test_gate_decoys.py ("hub-gofmt"), on a scratch module dir. +""" +import os +import shutil +import subprocess +import sys + +ROOT = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "hub") +ROOTS = ["cmd", "internal"] + + +def find_gofmt(): + p = shutil.which("gofmt") + if p: + return p + go = shutil.which("go") + if go: + try: + root = subprocess.check_output([go, "env", "GOROOT"], text=True).strip() + cand = os.path.join(root, "bin", "gofmt") + if os.path.isfile(cand): + return cand + except Exception: + pass + return None + + +def in_ci(): + """True on the Gitea Actions runner (act sets GITHUB_ACTIONS, Gitea sets GITEA_ACTIONS).""" + return os.environ.get("GITEA_ACTIONS") == "true" or os.environ.get("GITHUB_ACTIONS") == "true" + + +def main(): + global ROOT + if len(sys.argv) > 1: + ROOT = os.path.abspath(sys.argv[1]) + gofmt = find_gofmt() + if not gofmt: + # The CI runner may have no Go toolchain (felhom.eu CLAUDE.md, R-887 note). There the gate + # says so OUT LOUD and passes: the pre-push hook on the developer's machine, where Go always exists, is where + # formatting is enforced. Anywhere else a missing gofmt stays INCONCLUSIVE — never a silent pass. + # Pinned by the "hub-gofmt/ci-without-go" and "hub-gofmt/dev-without-go" decoys in test_gate_decoys.py. + if in_ci(): + print("gofmt gate NOT CHECKED in CI: no Go toolchain on the runner — the pre-push hook (with Go) enforces it") + return 0 + print("gofmt gate INCONCLUSIVE: gofmt not found on PATH or under `go env GOROOT`/bin") + return 2 + roots = [r for r in ROOTS if os.path.isdir(os.path.join(ROOT, r))] + if not roots: + print("gofmt gate INCONCLUSIVE: none of %s exists under %s" % (ROOTS, ROOT)) + return 2 + p = subprocess.run([gofmt, "-l"] + roots, cwd=ROOT, capture_output=True, text=True) + if p.returncode != 0: + print("gofmt gate INCONCLUSIVE: gofmt exited %d: %s" % (p.returncode, p.stderr.strip()[:400])) + return 2 + bad = [l for l in p.stdout.splitlines() if l.strip()] + if bad: + for f in bad: + print(" not gofmt-clean: %s" % f) + print("GOFMT GATE FAILED: %d file(s) — run `gofmt -w ` (formatting only, no behaviour change)" % len(bad)) + return 1 + print("gofmt gate OK — every Go file under %s is gofmt-clean" % ", ".join(roots)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/repo_gates.py b/scripts/repo_gates.py index e1f57783..00c00a7b 100644 --- a/scripts/repo_gates.py +++ b/scripts/repo_gates.py @@ -32,6 +32,7 @@ Gates, in order (all must pass; **non-zero exit on any failure**): decoy — FULL RUNS ONLY, "not checked" (exit 2) without docker (R-502, decision 147) 16. decoy-coverage every registered gate in all four repos has a decoy, or a named exemption (R-421) 17. poster-facts WARNS (never fails) when felhom-system-poster.facts.md is newer than the poster + 17b. hub-gofmt every hub Go file is gofmt-clean (R-1160; the controller's R-454 shape) **THE `GATES` TABLE BELOW IS THE LIST; THIS IS A POINTER TO IT.** It drifted once already — it read eleven while thirteen were registered, from 2026-08-24 until 2026-09-01, so @@ -198,6 +199,9 @@ GATES = [ # until a human with another tool was free -- and `--no-verify` is forbidden here, so the only # way out would be deleting the gate. Fast: two `git log -1` calls. ("poster-facts", os.path.join(SCRIPTS, "poster_facts_gate.py"), [ROOT], True, False), + # R-1160 (2026-10-19) — every hub Go file is gofmt-clean; the controller's gofmt_gate.py shape. Fast: one gofmt -l. + # The CI runner has no Go: there it says NOT CHECKED out loud and passes; the pre-push hook (with Go) enforces it. + ("hub-gofmt", os.path.join(SCRIPTS, "hub_gofmt_gate.py"), [], True, False), ] VERDICT = {0: "OK", 1: "FAILED", 2: "INCONCLUSIVE"} diff --git a/scripts/test_gate_decoys.py b/scripts/test_gate_decoys.py index 8c7af5ab..a3919f0b 100644 --- a/scripts/test_gate_decoys.py +++ b/scripts/test_gate_decoys.py @@ -110,6 +110,9 @@ COVERS = { "one-register": ("R-426: an open roadmap row whose state NAMES another row's CLOSED (a live hole until " "2026-10-06), its id only in register prose, a suffix id; genuine: a row with a counterpart, " "and an `idea` row (R-424's accepted hole, asserted BY DESIGN)"), + "hub-gofmt": ("R-1160: an unformatted .go file in a SCRATCH module dir is convicted by name; its " + "formatted twin passes; with no gofmt reachable, CI passes OUT LOUD and a dev machine is " + "INCONCLUSIVE, never a silent pass"), "hub-copy": ("R-558: an English retrieval promise in the NEW bundle (the sentences moved " "out of templates.go, so the surface list had to move with them), the same " "in Hungarian, and an INNOCENT control using the identical verbs without the " @@ -1079,6 +1082,45 @@ else: print("ok poster-facts/output-is-ascii") +# --- hub-gofmt (R-1160): the gate on a scratch module dir, never the real hub tree. ---------------------------- +import tempfile as _tf +_gd = _tf.mkdtemp(prefix="hub-gofmt-decoy-") +try: + os.makedirs(os.path.join(_gd, "internal", "x")) + _gp = os.path.join(_gd, "internal", "x", "zz_gofmt_decoy.go") + io.open(_gp, "w", encoding="utf-8").write(u"package x\n\nfunc F( ) int {return 1}\n") + if shutil.which("gofmt") or shutil.which("go"): + ran += 1 + _rc, _out = gate("hub_gofmt_gate.py", [_gd]) + if _rc != 1 or "zz_gofmt_decoy.go" not in _out: + fails.append("hub-gofmt: an unformatted planted file was not convicted (rc=%d)\n%s" % (_rc, _out[-400:])) + else: + print("ok hub-gofmt/unformatted-convicted") + ran += 1 + io.open(_gp, "w", encoding="utf-8").write(u"package x\n\nfunc F() int { return 1 }\n") + _rc, _out = gate("hub_gofmt_gate.py", [_gd]) + if _rc != 0: + fails.append("hub-gofmt: the formatted twin did not pass (rc=%d)\n%s" % (_rc, _out[-400:])) + else: + print("ok hub-gofmt/formatted-passes") + else: + print(" -- hub-gofmt not runnable here (no Go toolchain) — the two cases below pin that mode") + for _ci, _want_rc, _want_txt, _name in ((True, 0, "NOT CHECKED in CI", "hub-gofmt/ci-without-go"), + (False, 2, "INCONCLUSIVE", "hub-gofmt/dev-without-go")): + ran += 1 + _env = {"PATH": os.path.join(_gd, "empty-path"), "HOME": _gd} + if _ci: + _env["GITEA_ACTIONS"] = "true" + _p = subprocess.run([sys.executable, os.path.join("scripts", "hub_gofmt_gate.py"), _gd], cwd=ROOT, env=_env, + capture_output=True, text=True) + if _p.returncode != _want_rc or _want_txt not in _p.stdout: + fails.append("%s: rc=%d, want %d with %r\n%s" % (_name, _p.returncode, _want_rc, _want_txt, _p.stdout[-300:])) + else: + print("ok %s" % _name) +finally: + shutil.rmtree(_gd, ignore_errors=True) + + print() if fails: for f in fails: