Files
rune/henxels_checks.py
Nick 0821ad28cb fix(governance): phantom-detection consistency for subpackage modules (RP10 CI)
The two lint-job henxel failures on PR #279 were pre-existing master debt
surfaced by the FIRST pull_request CI run in this repo's history (master
pushes only run merge-green):

1. umbrella manifest phantom check: umbrella_impact.module_of() routes
   subpackage modules by stem (orchestration/retrieval_benchmark.py ->
   'retrieval_benchmark') but the manifest completeness henxel's 'actual'
   set only globbed TOP-LEVEL src/rune/*.py — so a legitimately-mapped
   subpackage module looked phantom. Fix: phantom direction now checks
   existence across the whole tree (rglob); the 'unmapped' direction
   preserves the historical top-level contract unchanged. Test mirrored.

2. workflow registry: research-dispatcher.yml had no registry entry.
   Registered as SUPERSEDED-legacy (canonical pipeline replaced it; labels
   stripped 2026-09-08; retirement is RP11/#274's job).

Also re-baselined lint-baseline.json to measured master reality (82/356 —
ratchet doctrine preserved: floor may only shrink) and resynced the CI
dependency lock (platformdirs 4.11.7 -> 4.11.8 upstream drift).

Full suite: 1069 passed. henxels check --all: 0 failures locally.
2026-09-09 03:55:22 +01:00

192 lines
7.7 KiB
Python

from pathlib import Path
from henxels import statement
@statement("ci_runs_all_canaries", help="every src/rune/**/canary_*.py is listed in .gitea/workflows/ci.yml")
def ci_runs_all_canaries(param, file, root):
if not (file.startswith("src/rune/") and Path(file).name.startswith("canary_")):
return None
if not file.endswith(".py"):
return None
ci = root / ".gitea" / "workflows" / "ci.yml"
if not ci.exists():
return f"{file} is missing from .gitea/workflows/ci.yml (no ci.yml found)"
if Path(file).name in ci.read_text():
return None
return f"{file} is missing from .gitea/workflows/ci.yml — register it in the canary job's for-loop list"
# --- Umbrella impact-map contracts (T32): the map is the change-propagation
# graph. It must be complete (bidirectional), reference real canaries, and
# stay acyclic — otherwise the impact gate lies.
def _umbrella_manifest(root):
import json
p = root / "config" / "umbrellas.json"
if not p.exists():
return None
return json.loads(p.read_text()).get("umbrellas", {})
@statement(
"umbrella_manifest_is_complete",
help="every src/rune/*.py and shared/*.py module is in exactly one umbrella; every entry references a real file (bidirectional)",
)
def umbrella_manifest_is_complete(param, file, root):
manifest = _umbrella_manifest(root)
if manifest is None:
return "config/umbrellas.json is missing — the impact gate has no map"
mapped = {}
for umb, data in manifest.items():
for mod in data.get("modules", []):
mapped.setdefault(mod, []).append(umb)
dupes = {m: u for m, u in mapped.items() if len(u) > 1}
if dupes:
return f"modules in multiple umbrellas (map must be a partition): {dupes}"
# 'unmapped' preserves the historical top-level contract; 'phantom' checks
# existence across the whole tree (a mapped name is phantom only if NO
# file with that stem exists anywhere under src/rune or shared).
actual = {f.stem for f in (root / "src" / "rune").glob("*.py")
if not f.stem.startswith(("canary_", "test_", "__"))}
actual |= {f.stem for f in (root / "shared").glob("*.py")}
existing = {f.stem for f in (root / "src" / "rune").rglob("*.py")}
existing |= {f.stem for f in (root / "shared").rglob("*.py")}
unmapped = sorted(actual - set(mapped))
phantom = sorted(set(mapped) - existing)
debt = root / "docs" / "module-coverage-debt.md"
debt_ok = debt.exists()
if unmapped and not debt_ok:
return f"unmapped modules (add to config/umbrellas.json or docs/module-coverage-debt.md): {unmapped}"
if phantom:
return f"phantom manifest entries (files gone — remove from the map): {phantom}"
return None
@statement(
"umbrella_canaries_exist",
help="every canary referenced by the umbrella manifest exists in src/rune/ (CI registration enforced by ci_runs_all_canaries)",
)
def umbrella_canaries_exist(param, file, root):
manifest = _umbrella_manifest(root)
if manifest is None:
return None
phantom = []
for umb, data in manifest.items():
for c in data.get("canaries", []):
if not (root / "src" / "rune" / f"{c}.py").exists():
phantom.append(f"{umb}:{c}")
if phantom:
return f"manifest references canaries that do not exist: {phantom}"
return None
@statement(
"umbrella_dag_acyclic",
help="the umbrella depends_on graph is acyclic with known targets — reflexive updates cannot cycle",
)
def umbrella_dag_acyclic(param, file, root):
manifest = _umbrella_manifest(root)
if manifest is None:
return None
def has_cycle(node, seen, stack):
if node in stack:
return True
if node in seen:
return False
seen.add(node)
return any(has_cycle(d, seen, stack | {node})
for d in manifest.get(node, {}).get("depends_on", []))
for u in manifest:
if has_cycle(u, set(), frozenset()):
return f"dependency cycle through umbrella '{u}' — reflexive updates would recurse forever"
for d in manifest[u].get("depends_on", []):
if d not in manifest:
return f"umbrella '{u}' depends on unknown umbrella '{d}'"
return None
# --- Pipeline-expansion contracts (issue #204 doctrine): when a new process,
# feature, workflow, or backend surface is added, a governing contract must
# exist for it. These statements make "ungoverned surface" a check failure.
@statement(
"workflows_are_governed",
help="every .gitea/workflows/*.yml is referenced by docs/pipeline-registry.md — new pipeline machinery ships with documentation",
)
def workflows_are_governed(param, file, root):
if not file.startswith(".gitea/workflows/") or not file.endswith(".yml"):
return None
registry = root / "docs" / "pipeline-registry.md"
if not registry.exists():
return f"docs/pipeline-registry.md is missing — create it and register {file} (every workflow documents trigger/purpose/owner)"
if Path(file).name not in registry.read_text():
return f"{file} is not registered in docs/pipeline-registry.md — new pipeline processes ship with a registry entry (trigger, purpose, failure mode)"
return None
@statement(
"tui_panels_are_governed",
help="new TUI surfaces (src/rune/*tui*.py panels/widgets) are referenced in docs/tui-registry.md — no silent UI growth",
)
def tui_panels_are_governed(param, file, root):
if Path(file).stem.startswith("canary_"):
return None
if "tui" not in Path(file).name.lower():
return None
registry = root / "docs" / "tui-registry.md"
if not registry.exists():
return f"docs/tui-registry.md is missing — create it and register {file} (every TUI surface documents what it renders and who owns it)"
if Path(file).name in registry.read_text():
return None
# registry exists but this file is new/unregistered — check for a known-exempt core list
core = {"rune_tui.py"}
if Path(file).name in core:
return None
return f"{Path(file).name} is not registered in docs/tui-registry.md — new TUI surfaces ship with a registry entry"
def _tests_corpus(root) -> str:
tests_dir = root / "tests"
if not tests_dir.exists():
return ""
parts = []
for tf in sorted(tests_dir.glob("test_*.py")):
try:
parts.append(tf.read_text(errors="replace"))
except OSError:
continue
return "\n".join(parts)
@statement(
"new_rune_modules_have_canary_or_test_coverage",
help="every src/rune/*.py module is referenced by at least one test file or has a canary — no ungoverned code surfaces",
)
def new_rune_modules_have_canary_or_test_coverage(param, file, root):
if not file.startswith("src/rune/") or not file.endswith(".py"):
return None
name = Path(file).stem
if name.startswith(("canary_", "test_", "__")):
return None
tests_dir = root / "tests"
if (tests_dir / f"test_{name}.py").exists():
return None
canary = root / "src" / "rune" / f"canary_{name}.py"
if canary.exists():
return None
corpus = _tests_corpus(root)
if name in corpus:
return None
debt = root / "docs" / "module-coverage-debt.md"
if debt.exists():
debt_text = debt.read_text(errors="replace")
if name in debt_text:
return None # tracked debt — visible in docs/module-coverage-debt.md (list only shrinks)
return (
f"{name} is referenced by no test file and has no canary — new modules ship with coverage "
f"(a test importing/exercising it, or a canary) per docs/bugfix-contract.md; "
f"legacy debt goes in docs/module-coverage-debt.md (tracked, not silent)"
)