Files
felhom.eu/scripts/repo_gates.py
T
admin b03a105375
gates / gates (push) Successful in 17s
hub v0.105.0: the third name, a machine told to be quiet, and a guard for the hub's own words
Hub only. No controller change, no agent change, no wire change — nothing to bake.
demo-hp untouched: the operator is re-deploying it this evening.

R-323 — the five-word phrase is „Tulajdonosi jelmondat". It was „Visszaállító
jelszó": one word from the name retired last week, and false besides — it restores
nothing, it proves the account owns the box being bound. Five sites, all in the hub;
felhom-controller and felhom-agent carry the name nowhere, so no halt and no bake.
Both suggested names were rejected with reasons: „Fiókjelszó" would collide with the
dashboard login (a DIFFERENT real secret), and „Összekötési jelszó" would leave the
two factors on this page separated only by kód-versus-jelszó — the exact shape being
removed, since the other factor is the „Párosító kód". The chosen name differs on
both axes, stem and noun. Naming only; the acceptance pin drives the real handler.

R-324 — the hub's customer copy is under a guard for the first time. Retired names
banned across all 95 hub files; retrieval stems registered in four declared customer
surfaces. The selftest found a defect in its own instrument on the first run. One
shared vocabulary in scripts/, drift-checked into the controller gate rather than
copied (R-325 removes the scaffold).

R-321 — a machine we told to be quiet is no longer reported as dead, and it was two
doors, not one: because the state is RECORDED rather than deleted, the morning
deadline check can skip it too. A deleted state returns "", which is not "down" —
R-195's shape returning through a second door. The clock runs from the report the hub
can see, so re-enabling starts it there and emits no recovery for an outage that never
happened. Three red-proofs; the one that matters showed a genuinely dead machine
sitting at "disabled" when the suppression was made unconditional.

R-326 — "which claims are unproven" is answerable by a command now. The nine I have
been repeating was the count of claims the 9 August pass DOWNGRADED, not the count of
unproven ones. The real figures: 55 claims, 23 walked, 32 not — and only 6 of those 32
cite evidence. Its first run found a stale claim (R-327).
2026-08-13 15:50:32 +02:00

152 lines
7.7 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""repo_gates.py — THE entry point for this repo's gates. Run from the repo root:
python3 scripts/repo_gates.py # every gate
python3 scripts/repo_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. site website HTML: BOM, emoji, nav/footer, analytics, CDN, tokens, cache-busting
2. hostinstall felhom-host-install.sh's five drill-swept invariants (+ R-94's absent-version)
3. hub-confirm no native confirm()/prompt() in hub templates
4. manifest-bearer no bearer-shaped literal anywhere in manifests/
5. reuse-refs every path cited by this repo's REUSE.md still resolves
6. instructions CLAUDE.md length/versions/TEMPORARY, rule-file scoping, workspace-copy identity
7. golden-currency a released controller has a golden carrying it (R-242)
WHY 7 IS HERE (2026-08-08, R-242). R-242 was filed as a rule with no mechanism — *a controller
release is not finished until a golden carries it* — and RECURRED THE NEXT DAY: v0.206.0 shipped
while the vouched golden still carried 0.205.0, so a machine installed that morning would have got
neither of the R-241 fixes. Two occurrences in two days, the first (R-239) invisible until a walk
measured it from the customer's side. It is `--fast` because both the pre-push hook and CI run only
`--fast`; a non-fast gate would run in neither, which is the R-29 failure this runner ended. That
constraint is why it checks the BAKE and not the vouch — the full reasoning is in its docstring.
WHY 6 IS HERE AND WAS NOT (2026-08-06, R-229 deferred leg). instructions_gate.py LIVES in this
repo's scripts/ and was registered in the controller and agent runners on the day it was written —
but not in this one, because this repo's own CLAUDE.md was still 27 lines over the ceiling and a
registered-but-failing gate refuses every push through .githooks/pre-push. The file was trimmed
(227 -> 115 effective lines, core + .claude/rules/) and the gate registered in the same session.
A check that does not run in the place it applies is the exact failure the R-29 gate census found.
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** — one since 14 July. Neither failure was harmful in effect, which was
checked line by line; nothing would have said so if they had been. 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.
`site_gates.py` is a GATE — eight assertions in one file — and is NOT the model for this file. A
runner that invokes separate gates is the shape that survives; copying site_gates would just add
a ninth monolith.
FAIL-CLOSED. A gate script that is missing is a FAILURE, never a skip, and the exact path tried
is printed. A runner that quietly drops 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, and the operator needs to know which they have.
"""
import os
import subprocess
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
SCRIPTS = os.path.join(ROOT, "scripts")
# (label, absolute script path, args, fast)
GATES = [
("site", os.path.join(SCRIPTS, "site_gates.py"), [], True),
("hostinstall", os.path.join(SCRIPTS, "hostinstall_gates.py"), [], True),
("hub-confirm", os.path.join(SCRIPTS, "hub_confirm_gate.py"), [], True),
("manifest-bearer", os.path.join(SCRIPTS, "manifest_bearer_gate.py"), [], True),
("reuse-refs", os.path.join(SCRIPTS, "reuse_refs_check.py"), [ROOT], True),
("instructions", os.path.join(SCRIPTS, "instructions_gate.py"), [ROOT], True),
("golden-currency", os.path.join(SCRIPTS, "golden_currency_gate.py"), [], True),
("wire-contract", os.path.join(SCRIPTS, "wire_contract_gate.py"), [], True),
# R-324 — the hub composes every customer e-mail and renders the binding pages, and until
# 2026-08-13 no guard in either repo had ever looked at them. Fast: pure file reads.
("hub-copy", os.path.join(SCRIPTS, "hub_copy_gate.py"), [], 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).")
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,
# and a runner that swallows them makes a conviction unreadable.
return subprocess.call([sys.executable, path] + args, cwd=ROOT)
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/repo_gates.py [--fast]")
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("repo_gates (felhom.eu) — %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(ROOT)
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 felhom.eu 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:]))