hub-gofmt gate in repo_gates.py with its decoys (R-1160)
gates / gates (push) Successful in 7m14s

This commit is contained in:
2026-10-19 10:34:41 +02:00
parent 503c93fada
commit a17db0c3c8
3 changed files with 128 additions and 0 deletions
+82
View File
@@ -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 [<module dir>] (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 <file>` (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())
+4
View File
@@ -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"}
+42
View File
@@ -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: