#!/usr/bin/env python3 # -*- coding: utf-8 -*- """decoy_coverage_gate.py — every registered gate ships with a decoy test (R-421). Usage: python3 scripts/decoy_coverage_gate.py [ ...] Exit 0 covered-or-registered · 1 a gate has neither a decoy nor an exemption · 2 inconclusive. WHY THIS EXISTS. Four times in one week a gate turned out to be matching a NAME instead of the thing it named — R-410 (a `mkdir` turned the release gate green), R-400 (seven debug controls answering nothing), R-378 (a status word inside a sentence), R-419 (a phrase inside prose, including prose saying the marker was absent). All four were found by accident. **The gates are the machinery that enforces everything else here, and they were the one part nothing checked.** The 2026-09-01 sweep read all 29 and fooled 16 of them. A survey fixes what it finds once. This makes the next one impossible to add quietly: **a NEW gate with no decoy fails immediately**, and the gates not yet covered are listed BY NAME below, each with its row, so the remaining debt is visible and shrinking rather than forgotten in a Python literal. R-329's shape, deliberately, because this project already trusts it: walk everything, register the exceptions by name, and let a new one fail rather than slip through. ⚠ COVERAGE IS A DECLARATION, NOT A GREP. Each decoy suite exports `COVERS = {gate: why}` and this gate AST-parses that literal. Searching the test file for a gate's name would be exactly the substring-for-fact shape this whole sweep exists to find — the gate that checks for label-matching must not itself match on a label. """ import ast import io import os import sys # ── EXEMPTIONS — each carries its row and one line saying why it is not covered YET ───────────── # Dated 2026-09-01. This list is DEBT, not a settled state: R-426 owns it and names every entry. EXEMPT = { # felhom.eu ("felhom.eu", "site"): "R-423 — PAGES is a hardcoded list of 7 files; a new page is unscanned. The decoy passes " "TODAY, so a test asserting rejection would be a lie. Fix is to glob website/*.html.", ("felhom.eu", "one-register"): "R-424 — a real defect parked under state `idea` is invisible. Declared in the gate's own " "docstring as residual hole 1; the decoy passes today.", ("felhom.eu", "hostinstall"): "R-426 — no plausible decoy constructed yet. It asserts installer invariants against a " "shell script; a legitimate decoy needs a shape a real edit would produce.", ("felhom.eu", "wire-contract"): "R-426 — 607 lines comparing emitted fields to receiver structs across two repos; a decoy " "needs a Go edit, which this sweep was forbidden from making.", ("felhom.eu", "hub-copy"): "COVERED IN THE SWEEP, not yet in a suite: a retired name planted in a NEW hub template was " "REJECTED (it uses os.walk). R-426 tracks moving that decoy into the suite.", ("felhom.eu", "due-checks"): "R-426 — no legitimate decoy constructed; the attempt in the sweep was a no-op and its " "verdict was withdrawn rather than reported.", ("felhom.eu", "instructions"): "COVERED IN THE SWEEP, not yet in a suite: a version literal in effective text was REJECTED. " "R-426 tracks moving it in.", # felhom-controller ("felhom-controller", "docker-v"): "COVERED IN THE SWEEP, not yet in a suite: an unallowlisted `-v` host path in a new Go file " "was REJECTED (it uses os.walk). R-426 tracks moving it in.", ("felhom-controller", "offbox-rename"): "R-425 — the FILES list is fixed, so banned branding in a NEW offbox file is unscanned. The " "decoy passes today.", ("felhom-controller", "reuse-refs"): "shared script; its decoy lives in felhom.eu/scripts/test_gate_decoys.py.", ("felhom-controller", "instructions"): "shared script; see felhom.eu. R-426.", ("felhom-controller", "observations"): "shared script; its decoy (R-419) lives in felhom.eu/scripts/test_gate_decoys.py.", # felhom-agent — no decoy suite yet ("felhom-agent", "reuse-refs"): "shared script; decoy in felhom.eu. R-426.", ("felhom-agent", "instructions"): "shared script; decoy in felhom.eu. R-426.", ("felhom-agent", "observations"): "shared script; decoy in felhom.eu. R-426.", ("felhom-agent", "published"): "R-426 — a network gate; a decoy needs a fake registry, which this sweep did not build.", ("felhom-agent", "release-complete"): "R-426 — the sweep's decoy (a non-version heading on top of CHANGELOG.md) was WITHDRAWN: " "HEAD_RE.search scans the whole file, so the real release is still found and named. The " "gate looked sound; no legitimate decoy has been constructed yet.", # app-catalog ("app-catalog-felhom.eu", "image-pins"): "COVERED IN THE SWEEP: a real untagged `image:` line was REJECTED, and the `x-image:` decoy " "was WITHDRAWN as illegitimate (x- fields are inert in Compose). R-426 tracks a suite.", ("app-catalog-felhom.eu", "image-resolvable"): "R-426 — needs a container runtime and the network; not a --fast gate.", ("app-catalog-felhom.eu", "volume-persistence"): "R-426 — 877 lines that actually run containers and diff them; not a --fast gate.", } RUNNERS = { "felhom.eu": os.path.join("scripts", "repo_gates.py"), "felhom-controller": os.path.join("controller", "scripts", "controller_gates.py"), "felhom-agent": os.path.join("scripts", "agent_gates.py"), "app-catalog-felhom.eu": os.path.join("scripts", "catalog_gates.py"), } SUITES = { "felhom.eu": os.path.join("scripts", "test_gate_decoys.py"), "felhom-controller": os.path.join("controller", "scripts", "test_gate_decoys.py"), "felhom-agent": os.path.join("scripts", "test_gate_decoys.py"), "app-catalog-felhom.eu": os.path.join("scripts", "test_gate_decoys.py"), } def literal_from(path, name): """AST-parse a module-level literal assignment. Never imports — importing a test RUNS it.""" if not os.path.isfile(path): return None try: tree = ast.parse(io.open(path, encoding="utf-8").read()) except SyntaxError as e: return ("ERROR", "%s does not parse: %s" % (path, e)) for node in tree.body: if isinstance(node, ast.Assign): for t in node.targets: if isinstance(t, ast.Name) and t.id == name: try: return ast.literal_eval(node.value) except Exception: return ("ERROR", "%s in %s is not a literal" % (name, path)) return None def gates_of(runner_path): """The GATES table's LABELS, AST-read — the runner is never imported and never literal_eval'd. Only the label is taken, deliberately. Three of the four runners build their script paths with `os.path.join(...)`, so the table as a whole is not a literal — but every label is a plain string constant, which is all this gate needs. Evaluating the whole row would make this gate fail on three repos for a reason that has nothing to do with coverage. """ if not os.path.isfile(runner_path): return None try: tree = ast.parse(io.open(runner_path, encoding="utf-8").read()) except SyntaxError: return None for node in tree.body: if not isinstance(node, ast.Assign): continue if not any(isinstance(t, ast.Name) and t.id == "GATES" for t in node.targets): continue if not isinstance(node.value, (ast.List, ast.Tuple)): return None out = [] for row in node.value.elts: if isinstance(row, (ast.Tuple, ast.List)) and row.elts: first = row.elts[0] if isinstance(first, ast.Constant) and isinstance(first.value, str): out.append(first.value) return out return None def main(argv): roots = argv[1:] or ["."] problems, inconclusive, lines = [], [], [] total = covered = exempt = 0 for root in roots: root = os.path.abspath(root) name = os.path.basename(root) if name not in RUNNERS: inconclusive.append("unknown repo %r — no runner registered for it" % name) continue runner = os.path.join(root, RUNNERS[name]) if not os.path.isfile(runner): inconclusive.append("%s: runner not found at %s" % (name, runner)) continue labels = gates_of(runner) if labels is None: inconclusive.append("%s: could not read the GATES table from %s" % (name, runner)) continue covers = literal_from(os.path.join(root, SUITES[name]), "COVERS") or {} if isinstance(covers, tuple): inconclusive.append("%s: %s" % (name, covers[1])) covers = {} for label in labels: total += 1 if label in covers: covered += 1 lines.append(" COVERED %-22s %-20s %s" % (name, label, covers[label][:60])) elif (name, label) in EXEMPT: exempt += 1 lines.append(" exempt %-22s %-20s %s" % (name, label, EXEMPT[(name, label)][:60])) else: problems.append((name, label)) print("decoy-coverage gate — %d registered gate(s): %d with a decoy, %d registered exempt, " "%d UNACCOUNTED" % (total, covered, exempt, len(problems))) for l in sorted(lines): print(l) if inconclusive: print() for i in inconclusive: print(" INCONCLUSIVE: %s" % i) if not problems: print("\ndecoy-coverage gate INCONCLUSIVE — an undetermined result is never a pass") return 2 if problems: print() print("CONVICTED — these gates have neither a decoy test nor a registered exemption:") for repo, label in problems: print(" %s / %s" % (repo, label)) print() print("A gate ships with a decoy test that has been SEEN TO FAIL. Construct the label") print("without the fact, run the gate, and assert it convicts — then name the gate in that") print("repo's scripts/test_gate_decoys.py COVERS map.") print("If no plausible decoy exists, say so: add it to EXEMPT here with a row number and one") print("line of reason. An honest exemption is a result; a silent gap is how R-410 happened.") return 1 print("\ndecoy-coverage gate OK — every registered gate has a decoy or a named exemption (R-426)") return 0 if __name__ == "__main__": sys.exit(main(sys.argv))