#!/usr/bin/env python3 """verifier_gate.py -- the runner `wizard-work/forge` never had (Q87, 2026-10-31). WHY THIS EXISTS At `431bca1` the pipe measured 34 `verify_*.py` in this lane and no `CHECK`; the correction at `d56105a` moved the count to 36 and the runner count stayed zero. At `7b42050` the lane holds 39 and `tools/verify.py` still names `wizard-work/forge` 0 times. A verifier nothing runs is an unread claim: its verdict is whatever its author last saw by hand, and this lane would not notice if all 39 were broken at once. This gate executes them, keeps one row per file bound to the blob the rc was measured against, and re-grades that ledger on every run without re-executing it. WHAT IT MEASURES -- one row per committed verifier, one verdict per row PASS rc 0: the file ran and its checks held. FAIL rc 1: the file ran and its checks did NOT hold. A finding, not a bookkeeping error -- the file's claim is false at this tree. REFUSED rc 2: the file declined to grade. Not a pass. OTHER_RC rc above 2 that the house contract does not define. TIMEOUT no finish inside its per-file window (see WINDOWS below). ERROR the interpreter could not run the file at all (no rc to grade). MISSING the row's path is not in the tree at --rev. A non-PASS row must carry a reason in its note column. A red with no recorded cause is a FAULT (`CAUSELESS`), because "it is red and nobody wrote down why" is the state this lane has been in since 2026-10-27. THE LEDGER IS BOUND TO CONTENT Column 7 is the git blob id each rc was measured against. `--check` recomputes it at its own `--rev`; a mismatch is DRIFT, so editing a verifier is a request for `--emit` rather than a free pass on an old green. Column 8 is a static host-pin probe: the committed blob is parsed as an AST and every non-docstring string constant is tested for an absolute host root (`/root/troupe-standup`, `/root/.hermes`). A PASS carrying one prints as `PASS*` and is reported separately, because a verifier that reads this host's copy graded this host and not the tree that invoked it (Q62/Q106's class). WHAT IT DELIBERATELY DOES NOT DO `--check` (the default, what `CHECK` runs) does NOT execute anything: it grades the ledger against the tree in well under a second. Execution is `--emit`, it takes minutes here (measured: 470 s for 39 files at a 75 s default window, plus three files that need longer), and a gate that takes minutes is a gate that gets skipped. `--strict` promotes any non-PASS row to a fatal fault, which is the switch between "are the books consistent?" and "are the lane's verifiers true?" -- today the honest answer to the second is no, and the rows say which ones. It does not decide whether a verifier is *good*, only whether it exits 0. A verifier that asserts nothing exits 0. That is the same boundary q65_runner draws for the repo-wide population and it is not moved here. WINDOWS A single `--timeout` for the whole lane is wrong: three files here need more than 75 s and one of them is a recursive sweep. `VERIFIER_WINDOWS.tsv` names one window per path with the reason; `--timeout` stays the fallback. A TIMEOUT row's note names the window it actually got, so the ledger cannot quietly disagree with the run. EXIT CODES -- the house contract 0 clean / 1 graded a fault / 2 could not grade (no git, no python3, no lane). Usage: python3 verifier_gate.py --check # grade the ledger (fast, no runs) python3 verifier_gate.py --check --strict # ...and any non-PASS row is fatal python3 verifier_gate.py --emit # execute every verifier, rewrite python3 verifier_gate.py --census # who else runs each file python3 verifier_gate.py --selftest # the gate's own cells """ import argparse import ast import os import re import shutil import subprocess import sys import tempfile import time from pathlib import Path LANE = "wizard-work/forge" HERE = Path(__file__).resolve().parent ROOT = HERE.parents[1] LEDGER = HERE / "VERIFIER_LEDGER.tsv" WINDOWS = HERE / "VERIFIER_WINDOWS.tsv" PASS, FAIL, REFUSED = "PASS", "FAIL", "REFUSED" OTHER_RC, TIMEOUT, ERROR, MISSING = "OTHER_RC", "TIMEOUT", "ERROR", "MISSING" VERDICTS = (PASS, FAIL, REFUSED, OTHER_RC, TIMEOUT, ERROR, MISSING) HOST_ROOTS = ("/root/troupe-standup", "/root/.hermes") VERDICT_OF_RC = {0: PASS, 1: FAIL, 2: REFUSED} def git(repo, *args): """(rc, stdout, stderr) -- never raises, so a missing git is rc 2 upstream.""" try: r = subprocess.run(["git", "-C", str(repo)] + list(args), capture_output=True, text=True, errors="replace", timeout=120) return r.returncode, r.stdout, r.stderr except (OSError, subprocess.SubprocessError) as exc: return 127, "", str(exc) def population(repo, rev): """Committed verify_*.py under the lane at `rev` -- from the tree, not a list. The population is read from git rather than from a directory glob because the question is "what did this commit ship", and an untracked scratch file beside the lane is not a claim about the commit. """ rc, out, err = git(repo, "ls-tree", "-r", "--name-only", rev, "--", LANE) if rc != 0: raise SystemExit("git ls-tree %s failed: %s" % (rev, (err or "").strip())) paths = [p.strip() for p in out.splitlines() if p.strip()] return sorted(p for p in paths if p.startswith(LANE + "/") and os.path.basename(p).startswith("verify_") and p.endswith(".py")) def blob_of(repo, rev, path): rc, out, _ = git(repo, "rev-parse", "%s:%s" % (rev, path)) return out.strip() if rc == 0 else "" def host_pin(repo, rev, path): """The first absolute host root this blob reads, or ''. Parsed rather than grepped: a docstring that *names* the host root is a description, and the Q85 lesson is that a text scan turns description into finding. Only string constants outside docstrings count. """ rc, blob, _ = git(repo, "show", "%s:%s" % (rev, path)) if rc != 0: return "" try: tree = ast.parse(blob) except SyntaxError: return "unparseable" docs = set() for node in ast.walk(tree): if isinstance(node, (ast.Module, ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): body = getattr(node, "body", []) if body and isinstance(body[0], ast.Expr) \ and isinstance(body[0].value, ast.Constant) \ and isinstance(body[0].value.value, str): docs.add(id(body[0].value)) for node in ast.walk(tree): if isinstance(node, ast.Constant) and isinstance(node.value, str): if id(node) in docs: continue for root in HOST_ROOTS: if root in node.value: hits = [l for l in node.value.splitlines() if root in l] return (hits[0] if hits else root).strip()[:120] return "" def read_windows(path): """{path: (seconds, why)} -- one window per file, the reason mandatory.""" win = {} p = Path(path) if not p.exists(): return win for line in p.read_text(encoding="utf-8").splitlines(): if not line.strip() or line.lstrip().startswith("#"): continue parts = line.split("\t") if len(parts) < 3: raise SystemExit("windows row needs pathsecondswhy: %r" % line) try: win[parts[0].strip()] = (float(parts[1]), parts[2].strip()) except ValueError: raise SystemExit("window seconds is not a number: %r" % line) return win def run_one(repo, rel, timeout, windows): """Execute one verifier in its own directory, under its own TMPDIR. THE SANDBOX IS NOT A LUXURY. Five files in this lane clean up with `shutil.rmtree(os.path.dirname())`, which is the PARENT of the directory they made -- i.e. `$TMPDIR` itself: verify_append_only_audit_dry_refactor.py:304, verify_append_only_owners_ dry_refactor.py:238, verify_gate_blame_dry_refactor.py:178, verify_served_ drift_summarise_dry.py:192, verify_write_probe_dry_refactor.py:133. Running them with the caller's TMPDIR deleted the caller's scratch tree (this item's own first measurement, at 09:27, died that way: the probe's TSV was gone mid-run) and every later file failed with FileNotFoundError naming a directory it had just created, because `mkdtemp` raises on the missing PARENT. Each file now gets `/tb` as TMPDIR, so a verifier that deletes its parent takes out a throwaway directory the gate removes itself. """ window, why = windows.get(rel, (timeout, "")) full = Path(repo) / rel if not full.exists(): return {"path": rel, "verdict": MISSING, "rc": None, "seconds": 0.0, "note": "not in worktree"} base = tempfile.mkdtemp(prefix="forge_gate_") tb = os.path.join(base, "tb") os.makedirs(tb, exist_ok=True) env = dict(os.environ, PYTHONDONTWRITEBYTECODE="1", PYTHONUNBUFFERED="1", TMPDIR=tb, TEMP=tb, TMP=tb) env.pop("PYTHONOPTIMIZE", None) t0 = time.time() try: r = subprocess.run([sys.executable, "-B", full.name], cwd=str(full.parent), env=env, capture_output=True, timeout=window, text=True, errors="replace") rc, so, se = r.returncode, r.stdout, r.stderr except subprocess.TimeoutExpired: return {"path": rel, "verdict": TIMEOUT, "rc": None, "seconds": round(time.time() - t0, 2), "note": "no finish in %ss%s" % (window, (" (%s)" % why) if why else "")} except OSError as exc: return {"path": rel, "verdict": ERROR, "rc": None, "seconds": 0.0, "note": str(exc)[:160]} finally: shutil.rmtree(base, ignore_errors=True) secs = round(time.time() - t0, 2) verdict = VERDICT_OF_RC.get(rc, OTHER_RC if rc > 2 else ERROR) note = verdict_line(so, se, rc) return {"path": rel, "verdict": verdict, "rc": rc, "seconds": secs, "note": note} def verdict_line(so, se, rc): """The file's own sentence, not whichever stream happened to end last. The first draft took stderr's last line, and `verify_append_only_audit_dry_ refactor.py` wrote its cause to stdout while stderr ended on a SyntaxWarning fragment -- the row read `(b"dash \\u2014 here\\nplain\\n", ...)` and lost the `VERDICT: MISMATCH` it had printed. A verdict-looking line is preferred, from stdout first; the last line of either stream is the fallback. """ keys = ("VERDICT", "FAIL", "checks", "PASS", "failed", "Error") for text in (so, se): lines = [l.strip() for l in (text or "").splitlines() if l.strip()] for line in reversed(lines): if line.startswith("[ok") or line.startswith("ok:"): continue # a passing cell is not this row's cause if any(k in line for k in keys): return line[:160] # Nothing verdict-shaped: prefer stderr's last line, because that is where a # traceback puts the reason, and stdout's last line is often a passing cell # the file printed before the thing that killed it. for text in (se, so): lines = [l.strip() for l in (text or "").splitlines() if l.strip()] if lines: return lines[-1][:160] return "" if rc == 0 else "exited rc %d with no stdout/stderr" % rc HEADER = """\ # Q87 verifier ledger -- one row per committed verify_*.py in wizard-work/forge, # with the exit code it actually returned at the stated rev. # row: \t\t\t\t\t\t\t # verdicts: PASS rc0 / FAIL rc1 / REFUSED rc2 / OTHER_RC >2 / TIMEOUT / ERROR / # MISSING. A non-PASS row must carry a note: the cause, in the file's words. # invocation is what runs the file: `gate` when this gate is the only runner, # `also-run` when another committed line invokes it (the census evidence is # `--census`'s job, not a column here). # blob is the git blob id the rc was measured against; --check recomputes it and # a mismatch is DRIFT, so editing a verifier is a request for --emit. # host is the absolute host root the blob reads (AST, non-docstring strings), or # empty. A PASS carrying one prints as PASS* and is counted separately. # seconds is wall clock under that file's window (VERIFIER_WINDOWS.tsv); a # TIMEOUT note names the window it got. """ def read_ledger(path): rows, meta = [], {} p = Path(path) if not p.exists(): return rows, meta, "no ledger at %s (run --emit)" % p for line in p.read_text(encoding="utf-8").splitlines(): if not line.strip(): continue if line.startswith("#"): m = re.match(r"#\s*(rev|emitted_at):\s*(\S+)", line) if m: meta[m.group(1)] = m.group(2) continue parts = line.split("\t") if len(parts) < 6: return rows, meta, "ledger row has %d column(s): %r" % (len(parts), line) while len(parts) < 8: parts.append("") rows.append({"invocation": parts[0], "path": parts[1], "verdict": parts[2], "rc": parts[3], "seconds": parts[4], "note": parts[5], "blob": parts[6], "host": parts[7]}) return rows, meta, "" def write_ledger(path, rows, repo, rev): rc, out, _ = git(repo, "rev-parse", "--short", rev) sha = out.strip() or rev body = [HEADER.rstrip("\n"), "# rev: %s" % sha, "# emitted_at: %sZ" % time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime())] for r in sorted(rows, key=lambda x: x["path"]): body.append("\t".join([r["invocation"], r["path"], r["verdict"], "" if r["rc"] is None else str(r["rc"]), "%.2f" % float(r["seconds"]), r["note"], r["blob"], r["host"]])) Path(path).write_text("\n".join(body) + "\n", encoding="utf-8") def rc_consistent(row): """Wording a verdict has to match. Returns a reason string or ''.""" v, rc = row["verdict"], row["rc"] if v not in VERDICTS: return "verdict %r is not one of %s" % (v, "/".join(VERDICTS)) if v in (TIMEOUT, ERROR, MISSING): return "" if rc == "" else "%s carries rc %s" % (v, rc) if rc == "" or not re.fullmatch(r"-?\d+", rc): return "%s carries rc %r" % (v, rc) n = int(rc) if v == PASS and n != 0: return "PASS with rc %d" % n if v == FAIL and n != 1: return "FAIL with rc %d" % n if v == REFUSED and n != 2: return "REFUSED with rc %d" % n if v == OTHER_RC and n <= 2: return "OTHER_RC with rc %d" % n return "" # ---------------------------------------------------------------- invocation DOC_SUFFIXES = (".md", ".txt", ".log", ".tsv", ".json", ".pyc") RUN_TOKENS = ("python3", "python ", "sys.executable", "subprocess", "os.system", "pytest", "bash ") def invocation_census(repo, rev, paths, exclude_rel): """{path: (class, evidence)} -- who runs this file, from committed content. A mention is not an invocation. A reference counts as a RUN only when it sits in an executable file (.py/.sh/CHECK*, not a document) on a line that also names an interpreter or a spawner; every other reference is prose. That is the distinction reply 21 asked for, and it is a rule with a test: --selftest holds one prose mention and one `python3 x.py` line and requires the classifier to separate them. """ rc, out, _ = git(repo, "ls-tree", "-r", "--name-only", rev) if rc != 0: raise SystemExit("git ls-tree failed at %s" % rev) files = [f.strip() for f in out.splitlines() if f.strip()] texts = {} for f in files: base = os.path.basename(f) if base.endswith(DOC_SUFFIXES): continue rc, blob, _ = git(repo, "show", "%s:%s" % (rev, f)) if rc == 0: texts[f] = blob res = {} for path in paths: name = os.path.basename(path) runs = [] named = 0 for f, blob in texts.items(): if f == path or f == exclude_rel: continue if name not in blob: continue for i, line in enumerate(blob.splitlines(), 1): if name not in line: continue if line.lstrip().startswith(("#", "//", "*", "--")): named += 1 continue if any(t in line for t in RUN_TOKENS): runs.append("%s:%d" % (f, i)) else: named += 1 if runs: res[path] = ("also-run", ", ".join(runs[:2])) elif named: res[path] = ("gate", "%d prose ref(s), no invocation" % named) else: res[path] = ("gate", "named by nothing else at all") return res def check(repo, rev, ledger_path, strict, quiet=False): try: pop = population(repo, rev) except SystemExit as exc: print("verifier_gate: %s" % exc) return 2 if not pop: print("verifier_gate: population is empty at %s -- could not grade" % rev) return 2 rows, meta, err = read_ledger(ledger_path) if err: print("verifier_gate: %s" % err) return 2 faults, notes = [], [] seen = {} for r in rows: seen[r["path"]] = seen.get(r["path"], 0) + 1 why = rc_consistent(r) if why: faults.append("INCONSISTENT %s: %s" % (r["path"], why)) if r["verdict"] != PASS and not r["note"].strip(): faults.append("CAUSELESS %s: %s row with no reason in the note" % (r["path"], r["verdict"])) if not r["blob"]: faults.append("UNBOUND %s: no blob column -- the row does not say " "which content its rc describes (re-emit)" % r["path"]) for p, n in sorted(seen.items()): if n > 1: faults.append("DUPLICATE %s: %d rows" % (p, n)) for r in rows: if r["path"] not in pop: continue now = blob_of(repo, rev, r["path"]) if r["blob"] and now and now != r["blob"]: faults.append("DRIFT %s: rc measured against blob %s, tree at %s has %s" % (r["path"], r["blob"][:10], rev, now[:10])) # A dirty worktree is not a commit, so it cannot be DRIFT -- but a verifier # whose working bytes are not the blob its rc describes is exactly the case # where the ledger's verdict is about content you are not running. Named, # not fatal: the fix is a commit (then DRIFT/UNGRADED decide) or --emit. for r in rows: if r["path"] not in pop or not r["blob"]: continue rc_w, out_w, _ = git(repo, "hash-object", "--", r["path"]) if rc_w == 0 and out_w.strip() and out_w.strip() != r["blob"]: notes.append("UNCOMMITTED %s: worktree bytes are not the blob its rc " "was measured against -- commit it, then re-emit or " "expect DRIFT" % r["path"]) ungraded = sorted(set(pop) - set(seen)) for p in ungraded: faults.append("UNGRADED %s: committed at %s, no ledger row" % (p, rev)) stale = sorted(p for p in set(seen) - set(pop) if not (Path(repo) / p).exists()) for p in stale: faults.append("STALE %s: ledger row, file not in the tree" % p) outside = sorted(p for p in set(seen) - set(pop) if (Path(repo) / p).exists()) for p in outside: notes.append("OUTSIDE %s: ledger row, not a committed verifier at %s " "(untracked beside the lane or renamed)" % (p, rev)) counts = {} for r in rows: counts[r["verdict"]] = counts.get(r["verdict"], 0) + 1 pinned = [r for r in rows if r["verdict"] == PASS and r["host"]] reds = [r for r in rows if r["verdict"] != PASS] clean = len(rows) - len(reds) - len(pinned) print("verifier_gate -- lane %s at %s, %d committed verifier(s), %d ledger row(s)" % (LANE, rev, len(pop), len(rows))) if meta.get("rev"): print(" ledger rev : %s emitted %s" % (meta["rev"], meta.get("emitted_at", "?"))) print(" rows : %s" % ", ".join("%s %d" % (v, counts[v]) for v in VERDICTS if counts.get(v))) print(" graded-clean : %d of %d (PASS on a host-independent blob)" % (clean, len(pop))) if pinned: print(" host-pinned : %d PASS row(s) read as PASS* -- they read an " "absolute host root" % len(pinned)) for r in pinned: print(" PASS* %s %s" % (r["path"], r["host"])) if reds: print(" known-red : %d of %d row(s) -- every one named, every one " "with its cause" % (len(reds), len(rows))) for r in sorted(reds, key=lambda x: x["path"]): print(" %-8s rc %-3s %s %s" % (r["verdict"], r["rc"] or "-", r["path"], r["note"][:100])) for n in notes: print(" note: " + n) if strict: for r in reds: faults.append("%s %s rc %s %s" % (r["verdict"], r["path"], r["rc"] or "-", r["note"][:80])) for f in faults: print(" " + f) print("verifier_gate: %d fault(s)%s" % (len(faults), " (strict: every non-PASS row counts)" if strict else "")) return 1 if faults else 0 def emit(repo, rev, ledger_path, timeout, windows, only=None, merge=False): pop = population(repo, rev) if only: pop = [p for p in pop if only in p] if not pop: print("verifier_gate: nothing to emit") return 2 keep = [] if merge and Path(ledger_path).exists(): have, _, err = read_ledger(ledger_path) if err: print("verifier_gate: %s" % err) return 2 keep = [r for r in have if r["path"] not in set(pop)] rc, before, _ = git(repo, "status", "--porcelain") t0 = time.time() rows = list(keep) for p in pop: r = run_one(repo, p, timeout, windows) r["blob"] = blob_of(repo, rev, p) r["host"] = host_pin(repo, rev, p) r["invocation"] = "gate" rows.append(r) print(" %-8s %6.2fs %s" % (r["verdict"], r["seconds"], p), flush=True) census = {} try: census = invocation_census(repo, rev, [r["path"] for r in rows], os.path.relpath(ledger_path, repo)) except SystemExit as exc: print("verifier_gate: census skipped (%s)" % exc) for r in rows: cls, ev = census.get(r["path"], ("gate", "")) r["invocation"] = "also-run" if cls == "also-run" else "gate" write_ledger(ledger_path, rows, repo, rev) rc2, after, _ = git(repo, "status", "--porcelain") led_rel = os.path.relpath(str(ledger_path), str(repo)) dirt = sorted(d for d in set(after.splitlines()) - set(before.splitlines()) if led_rel not in d) counts = {} for r in rows: counts[r["verdict"]] = counts.get(r["verdict"], 0) + 1 print("verifier_gate: %d row(s) in %.1fs -> %s" % (len(rows), time.time() - t0, ledger_path)) print(" " + ", ".join("%s %d" % (v, counts[v]) for v in VERDICTS if counts.get(v))) if dirt: print(" WARNING: the run changed the worktree (these verifiers write " "into it):") for d in dirt: print(" " + d) return 0 def census_report(repo, rev, ledger_path): pop = population(repo, rev) res = invocation_census(repo, rev, pop, os.path.relpath(str(ledger_path), str(repo))) run = sorted(p for p, (c, _) in res.items() if c == "also-run") print("verifier_census -- %d committed verifier(s) in %s at %s" % (len(pop), LANE, rev)) print(" invoked by another committed line : %d" % len(run)) for p in run: print(" %s %s" % (p, res[p][1])) print(" invoked by this gate only : %d" % (len(pop) - len(run))) for p in pop: if p in run: continue print(" %s %s" % (p, res[p][1])) return 0 # ------------------------------------------------------------------- selftest def selftest(): """Cells for the rules that can silently pass. Every cell builds a throwaway git repo, so the gate's logic is graded on a tree it did not ship with.""" cells = [] def cell(name, fn): try: ok, detail = fn() except Exception as exc: # noqa: BLE001 - a cell that raises is a cell that failed ok, detail = False, "%s: %s" % (type(exc).__name__, exc) cells.append((name, ok, detail)) def mk(tmp, files, commit=True): repo = Path(tmp) (repo / LANE).mkdir(parents=True, exist_ok=True) for rel, body in files.items(): p = repo / rel p.parent.mkdir(parents=True, exist_ok=True) p.write_text(body, encoding="utf-8") git(repo, "init", "-q") git(repo, "config", "user.email", "t@example.invalid") git(repo, "config", "user.name", "t") if commit: git(repo, "add", "-A") git(repo, "commit", "-qm", "cell") return repo V = LANE + "/verify_alpha.py" def c1(): bad = {"path": V, "verdict": PASS, "rc": "1", "note": "", "blob": "x", "host": "", "seconds": "0", "invocation": "gate"} good = dict(bad, verdict=FAIL) a, b = rc_consistent(bad), rc_consistent(good) return (bool(a) and not b, "PASS with rc 1") def c2(): with tempfile.TemporaryDirectory() as tmp: repo = mk(tmp, {V: "print(0)\n"}) n1 = len(population(repo, "HEAD")) (repo / LANE / "verify_beta.py").write_text("print(0)\n") git(repo, "add", "-A") git(repo, "commit", "-qm", "two") n2 = len(population(repo, "HEAD")) return (n1 == 1 and n2 == 2, "population follows the rev") def c3(): with tempfile.TemporaryDirectory() as tmp: repo = mk(tmp, {V: "print(0)\n"}) led = repo / LANE / "L.tsv" write_ledger(led, [{"path": V, "verdict": PASS, "rc": 0, "seconds": 0.0, "note": "", "blob": blob_of(repo, "HEAD", V), "host": "", "invocation": "gate"}], repo, "HEAD") r0 = check(repo, "HEAD", led, False) (repo / V).write_text("print(1)\n") git(repo, "add", "-A") git(repo, "commit", "-qm", "edit") r1 = check(repo, "HEAD", led, False) return (r0 == 0 and r1 == 1, "a drifted blob is a fault") def c4(): with tempfile.TemporaryDirectory() as tmp: repo = mk(tmp, {V: "print(0)\n"}) led = repo / LANE / "L.tsv" write_ledger(led, [], repo, "HEAD") return (check(repo, "HEAD", led, False) == 1, "a committed verifier with no row faults") def c5(): with tempfile.TemporaryDirectory() as tmp: repo = mk(tmp, {V: "print(0)\n"}) led = repo / LANE / "L.tsv" write_ledger(led, [{"path": LANE + "/verify_gone.py", "verdict": PASS, "rc": 0, "seconds": 0.0, "note": "", "blob": "abc", "host": "", "invocation": "gate"}], repo, "HEAD") return (check(repo, "HEAD", led, False) == 1, "a row for a deleted file faults") def c6(): with tempfile.TemporaryDirectory() as tmp: body = ('"""Reads /root/troupe-standup in a docstring."""\n' 'P = "/root/troupe-standup/inbox"\n') repo = mk(tmp, {V: body}) hit = host_pin(repo, "HEAD", V) repo2 = mk(tmp + "/b", {V: '"""Reads /root/troupe-standup."""\n'}) miss = host_pin(repo2, "HEAD", V) return (hit != "" and miss == "", "host pin is AST, not prose") def c7(): with tempfile.TemporaryDirectory() as tmp: repo = mk(tmp, {V: "print(0)\n", "CHECK": "python3 %s\n" % V, "notes/x.md": "see %s\n" % V}) res = invocation_census(repo, "HEAD", [V], "") return (res[V][0] == "also-run", "prose is not an invocation") def c8(): with tempfile.TemporaryDirectory() as tmp: repo = mk(tmp, {V: "import sys\nsys.exit(1)\n", LANE + "/verify_ok.py": "print(0)\n"}) led = repo / LANE / "L.tsv" rc = emit(repo, "HEAD", led, 30.0, {}, only=None) rows, _, err = read_ledger(led) by = {r["path"]: r for r in rows} ok = (rc == 0 and not err and by[V]["verdict"] == FAIL and by[V]["note"] != "" and by[LANE + "/verify_ok.py"]["verdict"] == PASS) return (ok, "emit records rc and a cause") def c9(): with tempfile.TemporaryDirectory() as tmp: repo = mk(tmp, {V: "print(0)\n"}) led = repo / LANE / "L.tsv" write_ledger(led, [{"path": V, "verdict": FAIL, "rc": 1, "seconds": 0.0, "note": "", "blob": blob_of(repo, "HEAD", V), "host": "", "invocation": "gate"}], repo, "HEAD") return (check(repo, "HEAD", led, False) == 1, "a red with no cause is a fault") def c10(): with tempfile.TemporaryDirectory() as tmp: repo = mk(tmp, {V: "print(0)\n"}) led = repo / LANE / "L.tsv" blob = blob_of(repo, "HEAD", V) write_ledger(led, [{"path": V, "verdict": FAIL, "rc": 1, "seconds": 0.0, "note": "known red", "blob": blob, "host": "", "invocation": "gate"}], repo, "HEAD") loose = check(repo, "HEAD", led, False) strict = check(repo, "HEAD", led, True) return (loose == 0 and strict == 1, "strict promotes a red row") def c11(): import contextlib import io with tempfile.TemporaryDirectory() as tmp: repo = mk(tmp, {V: "print(0)\n"}) led = repo / LANE / "L.tsv" write_ledger(led, [{"path": V, "verdict": PASS, "rc": 0, "seconds": 0.0, "note": "", "blob": blob_of(repo, "HEAD", V), "host": "", "invocation": "gate"}], repo, "HEAD") (repo / V).write_text("print(0) # uncommitted edit\n") buf = io.StringIO() with contextlib.redirect_stdout(buf): rc = check(repo, "HEAD", led, False) out = buf.getvalue() return (rc == 0 and "UNCOMMITTED" in out, "a dirty worktree is named, not fatal") def c12(): body = ("import sys\n" "sys.stderr.write('warning: bad escape \\\\u\\n')\n" "print('VERDICT: MISMATCH -- behavior NOT preserved')\n" "sys.exit(1)\n") with tempfile.TemporaryDirectory() as tmp: repo = mk(tmp, {V: body}) led = repo / LANE / "L.tsv" emit(repo, "HEAD", led, 30.0, {}, only=None) rows, _, err = read_ledger(led) by = {r["path"]: r for r in rows} note = by[V]["note"] if V in by else "" return (not err and "VERDICT: MISMATCH" in note, "the note is the file's verdict line: %r" % note[:60]) def c13(): body = ("import os, shutil, tempfile\n" "sandbox = tempfile.mkdtemp(prefix='x_', dir=os.environ['TMPDIR'])\n" "shutil.rmtree(os.path.dirname(sandbox), ignore_errors=True)\n" "print('cleaned up my parent')\n") with tempfile.TemporaryDirectory() as tmp: repo = mk(tmp, {V: body}) led = repo / LANE / "L.tsv" survive = tempfile.mkdtemp(prefix="survive_") emit(repo, "HEAD", led, 30.0, {}, only=None) rows, _, err = read_ledger(led) by = {r["path"]: r for r in rows} alive = os.path.isdir(survive) shutil.rmtree(survive, ignore_errors=True) return (not err and by[V]["verdict"] == PASS and alive, "a verifier that deletes its TMPDIR leaves the caller's alone") # the cell fixture needs a caller-level temp dir to watch for name, fn in (("c1 verdict/rc pairing", c1), ("c2 population from the rev", c2), ("c3 blob drift", c3), ("c4 ungraded file", c4), ("c5 stale row", c5), ("c6 host-pin AST", c6), ("c7 named-vs-run", c7), ("c8 emit records", c8), ("c9 causeless red", c9), ("c10 strict", c10), ("c11 uncommitted worktree", c11), ("c12 note is the verdict line", c12), ("c13 sandboxed TMPDIR", c13)): cell(name, fn) bad = [c for c in cells if not c[1]] for name, ok, detail in cells: print(" [%s] %s -- %s" % ("ok " if ok else "FAIL", name, detail)) print("verifier_gate selftest: %d cells, %d passed, %d failed" % (len(cells), len(cells) - len(bad), len(bad))) return 0 if not bad else 1 def main(): ap = argparse.ArgumentParser(add_help=True) ap.add_argument("--repo", default=str(ROOT)) ap.add_argument("--rev", default="HEAD") ap.add_argument("--ledger", default=str(LEDGER)) ap.add_argument("--windows", default=str(WINDOWS)) ap.add_argument("--timeout", type=float, default=90.0) ap.add_argument("--check", action="store_true") ap.add_argument("--emit", action="store_true") ap.add_argument("--census", action="store_true") ap.add_argument("--strict", action="store_true") ap.add_argument("--only") ap.add_argument("--merge", action="store_true", help="with --emit: keep the rows this run did not re-measure") ap.add_argument("--selftest", action="store_true") args = ap.parse_args() if args.selftest: return selftest() repo = Path(args.repo).resolve() if not (repo / ".git").exists(): print("verifier_gate: %s is not a git worktree -- could not grade" % repo) return 2 ledger = Path(args.ledger) if not ledger.is_absolute(): ledger = repo / ledger if args.emit: try: win = read_windows(args.windows) except SystemExit as exc: print("verifier_gate: %s" % exc) return 2 return emit(repo, args.rev, ledger, args.timeout, win, only=args.only, merge=args.merge) if args.census: return census_report(repo, args.rev, ledger) return check(repo, args.rev, ledger, args.strict) if __name__ == "__main__": sys.exit(main())