2d88776227
gates / gates (push) Successful in 20s
decoy_coverage_gate.py identified a repository by os.path.basename(root), looked up in a RUNNERS map. Gitea's act-runner checks the repo out into a directory called `hostexecutor`, so on its FIRST CI run the gate reported "unknown repo 'hostexecutor'" and went INCONCLUSIVE - correctly refusing to pass, and blind. A NAME standing in for a FACT, in the first ten lines of the main loop of the gate written that same morning to catch exactly that, by a session with the four shapes on screen. That is the point of R-428 and why it is recorded rather than quietly patched: this class is not carelessness. FIXED: the repo is now identified by which registered runner FILE exists under the root. Verified under a renamed directory - 14 gates found where the name-based version found none. CI also now fetches app-catalog-felhom.eu. The meta-gate walks all four runners, and a gate that cannot see part of its subject must not report a pass on it - the same reasoning, and the same fix, as the two sibling fetches already in the workflow. NOTE ON THE OTHER THREE RED RUNS, all mine and all ordering: felhom-agent and felhom-controller cite R-421, and I pushed them BEFORE felhom.eu carried that row, so instructions_gate correctly convicted "cites R-421, which appears in neither register". The register lives in felhom.eu; any repo citing a new row must be pushed after it. Re-run below.
230 lines
11 KiB
Python
230 lines
11 KiB
Python
#!/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 <repo-root> [<repo-root> ...]
|
|
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)
|
|
# ⚠ IDENTIFY THE REPO BY ITS RUNNER, NOT BY ITS DIRECTORY NAME.
|
|
#
|
|
# This gate asked `os.path.basename(root)` and looked the answer up in RUNNERS — a NAME
|
|
# standing in for a FACT, which is the exact class this gate was written to catch. It went
|
|
# INCONCLUSIVE on its first CI run because Gitea's act-runner checks the repo out into a
|
|
# directory called `hostexecutor`. **The gate that hunts label-matching was matching a
|
|
# label.** Measured 2026-09-01, CI job 490. Now: whichever registered runner FILE actually
|
|
# exists under this root is what the repo is.
|
|
name, runner = None, None
|
|
for cand, rel in RUNNERS.items():
|
|
if os.path.isfile(os.path.join(root, rel)):
|
|
name, runner = cand, os.path.join(root, rel)
|
|
break
|
|
if name is None:
|
|
inconclusive.append("no known gate runner found under %s — tried %s"
|
|
% (root, ", ".join(sorted(RUNNERS.values()))))
|
|
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))
|