#!/usr/bin/env python3 """check_bindings.py -- this lane's own gate for where its tests bind. WHY THIS EXISTS `tools/import_resolve_check.py` grades the whole tree and is a leg of the repo gate, so a beam test that binds a module to a host path reddens the repository *after* it lands. That has now happened four times in this lane: `test_canonical_url.py` and `test_median.py` (Q98), `test_damerau_levenshtein.py` and `test_expr.py` (Q133), and `test_next_greater.py` -- opened 2026-11-02 as Q153, committed at `096ba9b` with `sys.path.insert(0, "/root/.hermes/wizard-outputs/beam")`. Each one read green when run, because the host copy and the tree copy were byte-identical; each one graded bytes the repository does not control, and on any other host it could not grade at all (`ModuleNotFoundError`). The repo tool is right and stays the authority. What the lane lacked is a reading it can run before committing: `wizard-work/beam/CHECK` collected all 681 cells and read rc 0 while `test_next_greater.py` was bound outside the tree. This wraps the authority, scopes its finding rows to this lane, and turns them into the lane gate's rc. WHAT IT MEASURES One subprocess: `tools/import_resolve_check.py --repo --rev --json --quiet`, which parses every tracked test file with `ast` and names the ones binding an absolute constant outside the repo (OUTSIDE_REPO) or at a fixed in-tree path (ABS_PATH_IN_REPO). Both classes are faults here. The finding rows are then filtered to paths under this file's own directory, so another lane's in-flight defect does not redden this lane's gate -- the repo gate already grades that, for everybody, once. THE EXIT CONTRACT -- the house three codes 0 the tool graded the tree and no file in THIS lane binds outside it 1 at least one lane file binds outside the repo (each row printed) 2 could not grade: the repo tool is absent, the tool itself returned 2 (its inventory or a file was unreadable), or its JSON could not be read rc 2 is not a pass and not a fault: a lane gate that could not read the tree has not earned a claim about it. NOT CLAIMED That the graded files test their modules well -- this grades the binding, no assertion. That a green lane means the repo is green: this lane's rows are the only ones read here, and `tools/verify.py` is the whole-tree gate. That a computed path (`os.path.join(os.environ["HOME"], ...)`) is caught: the underlying tool's `--import-probe` is what sees those, and it is not run here because it executes test code. USAGE python3 check_bindings.py # this lane, the work tree python3 check_bindings.py --rev HEAD # the committed blobs python3 check_bindings.py --repo /path --lane wizard-work/beam python3 check_bindings.py --selftest # 6 cells """ from __future__ import annotations import argparse import contextlib import glob import io import json import os import subprocess import sys import tempfile LANE = os.path.dirname(os.path.abspath(__file__)) DEFAULT_REPO = os.path.dirname(os.path.dirname(LANE)) TOOL_REL = os.path.join("tools", "import_resolve_check.py") class CannotGrade(Exception): """The binding reading could not be taken.""" def lane_prefix(repo, lane): """The lane's path relative to the repo root, with a trailing slash.""" rel = os.path.relpath(os.path.abspath(lane), os.path.abspath(repo)) if rel.startswith(".."): raise CannotGrade("lane %s is outside repo %s" % (lane, repo)) return rel.rstrip("/") + "/" def grade(repo, lane, rev): """(rc, faults, graded, lane_files) for one reading.""" repo = os.path.abspath(repo) tool = os.path.join(repo, TOOL_REL) if not os.path.isfile(tool): raise CannotGrade("no %s under %s" % (TOOL_REL, repo)) prefix = lane_prefix(repo, lane) lane_files = sorted(os.path.basename(p) for p in glob.glob(os.path.join(repo, prefix, "test_*.py"))) with tempfile.TemporaryDirectory(prefix="bindings-") as td: out = os.path.join(td, "rows.json") try: proc = subprocess.run( [sys.executable, tool, "--repo", repo, "--rev", rev, "--json", out, "--quiet"], capture_output=True, text=True, timeout=600) except (OSError, subprocess.TimeoutExpired) as exc: raise CannotGrade("could not run %s: %s" % (TOOL_REL, exc)) if proc.returncode == 2: why = (proc.stderr or proc.stdout or "").strip().splitlines() raise CannotGrade("%s could not grade: %s" % (TOOL_REL, why[-1] if why else "rc 2")) if proc.returncode not in (0, 1): raise CannotGrade("%s exited %d: %s" % (TOOL_REL, proc.returncode, (proc.stderr or "").strip()[-200:])) try: with open(out) as fh: row = json.load(fh) except (OSError, ValueError) as exc: raise CannotGrade("could not read %s's --json: %s" % (TOOL_REL, exc)) faults = [f for f in row.get("faults", []) if str(f[0]).startswith(prefix)] return (1 if faults else 0), faults, row.get("graded", 0), lane_files def run(repo, lane, rev, quiet=False): try: rc, faults, graded, lane_files = grade(repo, lane, rev) except CannotGrade as exc: print("bindings: cannot grade -- %s" % exc, file=sys.stderr) return 2 prefix = lane_prefix(repo, lane) if not quiet: for path, line, cls, how, raw, is_prefix in faults: print("FAIL bindings: %s:%s %s -> %s%s" % (path, line, how, raw, " (f-string prefix)" if is_prefix else "")) print("bindings: %d file(s) graded repo-wide at %s, %d test file(s) in " "%s" % (graded, rev, len(lane_files), prefix)) if faults: print("bindings: %d lane file(s) bind a module outside the repo" % len(faults)) return 1 if not quiet: print("bindings: PASS -- no test file in %s binds outside the repo" % prefix) return 0 # -------------------------------------------------------------------- selftest def _write(path, text): os.makedirs(os.path.dirname(path) or ".", exist_ok=True) with open(path, "w") as fh: fh.write(text) def _fixture(td, name): """A throwaway git repo shaped like this one: tools/ + wizard-work//.""" repo = os.path.join(td, name) os.makedirs(repo) src = os.path.join(DEFAULT_REPO, TOOL_REL) with open(src) as fh: _write(os.path.join(repo, TOOL_REL), fh.read()) lane = os.path.join(repo, "wizard-work", "beam") _write(os.path.join(lane, "target.py"), "thing = 1\n") _write(os.path.join(lane, "test_ok.py"), "import pathlib, sys\n" "HERE = pathlib.Path(__file__).resolve().parent\n" "sys.path.insert(0, str(HERE))\n" "from target import thing\n") subprocess.run(["git", "init", "-q", repo], check=True, capture_output=True, text=True) subprocess.run(["git", "-C", repo, "add", "-A"], check=True, capture_output=True, text=True) return repo, lane def selftest(): cells, passed, failures = 0, 0, [] def check(label, got, want): nonlocal cells, passed cells += 1 if got == want: passed += 1 else: failures.append("%s\n got: %r\n want: %r" % (label, got, want)) def call(*args, **kw): """run() with its report swallowed; a refusal is its own rc (2).""" with contextlib.redirect_stdout(io.StringIO()), \ contextlib.redirect_stderr(io.StringIO()): return run(*args, **kw) with tempfile.TemporaryDirectory(prefix="bindings-st-") as td: # 1. a __file__ binding in the lane is clean repo, lane = _fixture(td, "c1") check("1 a lane file bound from __file__ -> rc 0", call(repo, lane, "WORKTREE"), 0) # 2. the defect Q153 opened: a lane file bound to the host copy _write(os.path.join(lane, "test_bad.py"), 'import sys\nsys.path.insert(0, "/root/.hermes/wizard-outputs/beam")\n') subprocess.run(["git", "-C", repo, "add", "-A"], check=True, capture_output=True, text=True) rc, faults, graded, files = grade(repo, lane, "WORKTREE") check("2 a host binding in the lane -> rc 1", rc, 1) check("2b and the row names the file", (faults[0][0], faults[0][2]), ("wizard-work/beam/test_bad.py", "OUTSIDE_REPO")) os.remove(os.path.join(lane, "test_bad.py")) subprocess.run(["git", "-C", repo, "add", "-A"], check=True, capture_output=True, text=True) # 3. another lane's defect is NOT this lane's fault: scope, not silence other = os.path.join(repo, "wizard-work", "spark") _write(os.path.join(other, "test_spark.py"), 'import sys\nsys.path.insert(0, "/root/.hermes/skills")\n') subprocess.run(["git", "-C", repo, "add", "-A"], check=True, capture_output=True, text=True) check("3 another lane's host binding leaves this lane rc 0", call(repo, lane, "WORKTREE"), 0) # 4. the authority missing is rc 2, not a green lane os.remove(os.path.join(repo, TOOL_REL)) check("4 a missing authority -> rc 2", call(repo, lane, "WORKTREE"), 2) # 5. and the whole-tree reading still sees what this lane scoped out repo2, lane2 = _fixture(td, "c5") _write(os.path.join(repo2, "wizard-work", "spark", "test_spark.py"), 'import sys\nsys.path.insert(0, "/root/.hermes/skills")\n') subprocess.run(["git", "-C", repo2, "add", "-A"], check=True, capture_output=True, text=True) proc = subprocess.run( [sys.executable, os.path.join(repo2, TOOL_REL), "--repo", repo2, "--rev", "WORKTREE", "--quiet"], capture_output=True, text=True) check("5 the authority still reds the tree (rc 1)", proc.returncode, 1) print("selftest: %d cells, %d passed, %d failed" % (cells, passed, len(failures))) for f in failures: print(" FAIL %s" % f) return 0 if passed == cells else 1 def main(argv=None): ap = argparse.ArgumentParser( description="Grade where THIS lane's test files bind the modules they " "import, by scoping tools/import_resolve_check.py to the " "lane directory.") ap.add_argument("--repo", default=DEFAULT_REPO, help="git work tree root (default: the checkout holding " "this file)") ap.add_argument("--lane", default=LANE, help="lane directory to scope to (default: this file's)") ap.add_argument("--rev", default="WORKTREE", metavar="REV", help="WORKTREE (the index, default) or a committed revision") ap.add_argument("--quiet", action="store_true") ap.add_argument("--selftest", action="store_true") a = ap.parse_args(argv) if a.selftest: return selftest() return run(a.repo, a.lane, a.rev, a.quiet) if __name__ == "__main__": sys.exit(main())