Files
volt 52784d4799 volt: Q65 cycle 5 -- 15 of 106 committed verifiers have no failure path; 14 runner PASS rows cannot fail
q65_no_verdict.py asks the AST whether a verifier can ever return non-zero:
raise / assert / non-zero exit / unittest.main. 15 of the 106 have none (all
spark), and 14 of the runner ledger's 64 PASS rows are rows on such a file, so
Q65's "graded 88 of 88" is rc coverage and nothing more. Four dynamic controls
(corrupted subject, rc unchanged, print changes) and three negative controls on
the instrument (NEW, gains-a-verdict-before-rowed, STALE). Wired as CHECK's
fourth leg. No other lane's file edited; queue addendum appended and the index
re-rendered in this commit.
2026-10-25 14:18:21 +00:00

327 lines
14 KiB
Python

#!/usr/bin/env python3
"""q65_no_verdict.py -- does a committed verifier have a failure path at all?
Q65's census grades each committed `wizard-work/**/verify_*.py` by running it and
recording its rc. A verifier whose source has no failure path returns rc 0 for
every input, so its row is a constant: it can never be a FAIL, and `graded 88 of
88` counts it as graded. Q65 cycle 4 found one such file by hand
(`ledger/verify_dates.py`: prints six lines and falls off the end of main());
this instrument is the static question asked of all of them.
WHAT COUNTS AS A VERDICT PATH (ast, so docstrings and comments are invisible --
which is the defect being measured: prose about failing is not a failure path):
raise <anything> a Raise node
assert <expr> an Assert node
sys.exit(<nonzero>) / exit(<nonzero>) / raise SystemExit(<nonzero>)
unittest.main() / pytest.main() the runner exits (pytest.main: see WEAK)
subprocess.run(..., check=True) raises on a bad rc -- WEAK, see below
WEAK-SUBPROCESS is its own class and not a verdict: a `check=True` grades the
child process, not the subject. `ledger/verify_dates.py` carries exactly one
(its `git log` call) and scores WEAK here while its comparison loop prints and
returns -- so WEAK is printed per-file for a human to read and is never a fault
on its own.
WHAT THIS CANNOT SEE, stated so the numbers are not read as stronger than they
are: a `raise` inside a swallowed `try/except` counts as a verdict path (false
negative for the printer class); a comparison whose result is printed and
`sys.exit(0 if ok else 1)`-ed counts correctly; a file that never runs to
completion is the runner's question (its row is TIMEOUT, not mine).
Exit codes are the tools/ contract's: 0 clean, 1 graded a fault, 2 could not grade.
--check ledger vs tree at --rev; NEW no-verdict file or a STALE ledger row = rc 1
--emit rewrite the ledger from the tree
--json the full classification, for re-derivation
"""
import argparse
import ast
import json
import os
import subprocess
import sys
import warnings
# several committed verifiers carry a "\u" in a docstring; that is a SyntaxWarning
# on parse and not this instrument's business.
warnings.filterwarnings("ignore", category=SyntaxWarning)
DEFAULT_LEDGER = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"q65_no_verdict.tsv")
RUNNER_LEDGER = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"q65_runner_baseline.tsv")
EXIT_NAMES = {"exit", "_exit", "quit"}
WEAK_KW = {"check_call", "check_output"}
def git(repo, *args):
p = subprocess.run(["git", "-C", repo, *args], capture_output=True, text=True)
if p.returncode != 0:
raise RuntimeError((p.stderr or p.stdout).strip())
return p.stdout
def _nonzero(arg):
"""Is this exit() argument a non-zero code? Unknown expressions count as
non-zero: an exit(<expr>) is a deliberate status, and the defect class is the
file with no status at all."""
if arg is None:
return False
if isinstance(arg, ast.Constant):
if arg.value is None or arg.value == 0 or arg.value is False:
return False
return True
return True
def classify(src):
"""Return (klass, tokens) for one verifier's source text."""
t = {"raise": 0, "assert": 0, "exit": 0, "runner_main": 0, "weak": 0,
"exit_zero": 0}
try:
tree = ast.parse(src)
except SyntaxError as e:
return "UNPARSEABLE", dict(t, error=str(e))
for node in ast.walk(tree):
if isinstance(node, ast.Raise):
t["raise"] += 1
elif isinstance(node, ast.Assert):
t["assert"] += 1
elif isinstance(node, ast.Call):
f = node.func
name = (f.attr if isinstance(f, ast.Attribute)
else f.id if isinstance(f, ast.Name) else "")
if name in EXIT_NAMES:
arg = node.args[0] if node.args else None
if _nonzero(arg):
t["exit"] += 1
else:
t["exit_zero"] += 1 # sys.exit() / exit(0) is rc 0
elif name in WEAK_KW:
t["weak"] += 1
elif name == "run":
for kw in node.keywords:
if kw.arg == "check" and isinstance(kw.value, ast.Constant) \
and kw.value.value:
t["weak"] += 1
elif name == "main" and isinstance(f, ast.Attribute) and \
isinstance(f.value, ast.Name) and f.value.id == "unittest":
t["runner_main"] += 1
elif name == "main" and isinstance(f, ast.Attribute) and \
isinstance(f.value, ast.Name) and f.value.id == "pytest":
t["weak"] += 1 # pytest.main returns an int, does not exit
strong = t["raise"] + t["assert"] + t["exit"] + t["runner_main"]
if strong and not t["weak"]:
return "has-verdict", t
if strong:
return "has-verdict+weak", t
if t["weak"]:
return "weak-subprocess", t
return "no-verdict", t
def verifiers(repo, rev):
out = git(repo, "ls-tree", "-r", "--name-only", rev).split()
return sorted(p for p in out
if p.startswith("wizard-work/") and "/verify_" in p
and p.endswith(".py"))
def census(repo, rev):
rows = []
for p in verifiers(repo, rev):
src = git(repo, "show", f"{rev}:{p}")
klass, t = classify(src)
rows.append({"path": p, "class": klass, "tokens": t,
"loc": src.count("\n") + 1,
"blob": git(repo, "rev-parse", f"{rev}:{p}").strip()[:12]})
return rows
def read_ledger(path):
led = {}
if not os.path.isfile(path):
return led
for ln in open(path, encoding="utf-8"):
if not ln.strip() or ln.lstrip().startswith("#"):
continue
parts = ln.rstrip("\n").split("\t")
if len(parts) >= 2:
led[parts[1]] = parts
return led
def read_runner(path):
"""path -> verdict, for rows whose rc agrees with the verdict."""
rows = {}
if not os.path.isfile(path):
return rows
for ln in open(path, encoding="utf-8"):
if not ln.strip() or ln.lstrip().startswith("#"):
continue
parts = ln.rstrip("\n").split("\t")
if len(parts) >= 4:
rows[parts[1]] = (parts[2], parts[3])
return rows
def emit(rows, path, rev):
bad = [r for r in rows if r["class"] in ("no-verdict", "weak-subprocess",
"UNPARSEABLE")]
with open(path, "w", encoding="utf-8") as f:
f.write("# Q65 no-verdict census -- one row per committed verifier with no\n")
f.write("# failure path of its own (or only a subprocess check). Written by\n")
f.write("# q65_no_verdict.py --emit; a row states the finding, it is not an\n")
f.write("# exemption: --check faults on a no-verdict file this file does not\n")
f.write("# name, and on a row whose file has since gained a verdict path.\n")
f.write(f"# rev {rev} committed verifiers {len(rows)} rows {len(bad)}\n")
f.write("# class\tpath\tloc\tblob\ttokens\tevidence\n")
for r in bad:
tok = ",".join(f"{k}={v}" for k, v in sorted(r["tokens"].items()) if v)
f.write("\t".join([r["class"], r["path"], str(r["loc"]), r["blob"],
tok or "no-failure-token", ""]) + "\n")
return bad
def check(rows, led, runner):
nov = [r for r in rows if r["class"] == "no-verdict"]
weak = [r for r in rows if r["class"] == "weak-subprocess"]
known = {p: v for p, v in led.items() if v[0] == "no-verdict"}
new = [r for r in nov if r["path"] not in known]
stale = [p for p in known if p not in {r["path"] for r in nov}]
# a PASS row on a file with no verdict path is a constant, not a grade
vacuous = [r for r in nov
if runner.get(r["path"], ("", ""))[0] in ("PASS",)]
return nov, weak, new, stale, vacuous
def report(repo, rev, led, runner, verbose=False):
rows = census(repo, rev)
nov, weak, new, stale, vacuous = check(rows, led, runner)
lanes = {}
for r in nov:
lane = r["path"].split("/")[1]
lanes[lane] = lanes.get(lane, 0) + 1
print(f"q65_no_verdict -- rev {rev}")
print(" committed verifiers : %d" % len(rows))
print(" no failure path : %d (%s)"
% (len(nov), " ".join(f"{k} {v}" for k, v in sorted(lanes.items(),
key=lambda kv: -kv[1]))
or "none"))
for r in nov:
tok = ",".join(f"{k}={v}" for k, v in sorted(r["tokens"].items()) if v) or "-"
print(f" NO-VERDICT {r['path']:64s} loc {r['loc']:3d} {tok}")
print(" weak only (subprocess) : %d" % len(weak))
for r in weak:
print(f" WEAK {r['path']}")
print(" runner PASS rows that cannot fail: %d of %d"
% (len(vacuous), sum(1 for v in runner.values() if v[0] == "PASS")))
if verbose:
for r in vacuous:
print(f" VACUOUS {r['path']} "
f"(runner row {runner[r['path']][0]} rc {runner[r['path']][1]})")
for r in new:
print(f" NEW {r['path']} (no-verdict, not in {os.path.basename(DEFAULT_LEDGER)})")
for p in stale:
print(f" STALE {p} (ledger row, file now has a verdict path or is gone)")
faults = len(new) + len(stale)
print("q65_no_verdict: %d fault(s)" % faults)
return faults
FIXTURES = [
("printer", "print('all good')\nopen('/tmp/x','w').write('ok')\n",
"no-verdict"),
("bare exit", "import sys\nprint('fine')\nsys.exit()\n", "no-verdict"),
("exit 0", "import sys\nsys.exit(0)\n", "no-verdict"),
("assert only", "assert 1 + 1 == 2\n", "has-verdict"),
("sys.exit(nonzero)", "import sys\nsys.exit(1)\n", "has-verdict"),
("raise SystemExit", "raise SystemExit(2)\n", "has-verdict"),
("subprocess only", "import subprocess\nsubprocess.run(['true'], check=True)\n",
"weak-subprocess"),
("unittest.main", "import unittest\nclass T(unittest.TestCase):\n"
" def test_a(self):\n self.assertTrue(True)\n"
"if __name__ == '__main__':\n unittest.main()\n",
"has-verdict"),
("assert + swallowed", "try:\n assert False\nexcept Exception:\n pass\n",
"has-verdict"),
]
def selftest():
bad = 0
for name, src, want in FIXTURES:
got, t = classify(src)
ok = got == want
bad += 0 if ok else 1
print(f" {'ok ' if ok else 'FAIL'} {name:22s} -> {got} (want {want})")
# check(): a NEW no-verdict file faults, a STALE row faults, a match is clean
rows = [{"path": "wizard-work/spark/verify_a.py", "class": "no-verdict",
"tokens": {}, "loc": 1, "blob": "x"},
{"path": "wizard-work/spark/verify_b.py", "class": "has-verdict",
"tokens": {}, "loc": 1, "blob": "y"}]
led = {"wizard-work/spark/verify_a.py": ["no-verdict", "wizard-work/spark/verify_a.py"]}
nov, weak, new, stale, vac = check(rows, led, {})
ok = (len(nov) == 1 and len(new) == 0 and len(stale) == 0)
print(f" {'ok ' if ok else 'FAIL'} ledger match -> new {len(new)} stale {len(stale)}")
bad += 0 if ok else 1
rows[0]["class"] = "has-verdict"
nov, weak, new, stale, vac = check(rows, led, {})
ok = len(stale) == 1
print(f" {'ok ' if ok else 'FAIL'} fixed file is STALE -> stale {len(stale)}")
bad += 0 if ok else 1
rows[0]["class"] = "no-verdict"
nov, weak, new, stale, vac = check(rows, {}, {})
ok = len(new) == 1
print(f" {'ok ' if ok else 'FAIL'} unrowed file is NEW -> new {len(new)}")
bad += 0 if ok else 1
vac_rows = check(rows, led, {"wizard-work/spark/verify_a.py": ("PASS", "0")})
ok = len(vac_rows[4]) == 1
print(f" {'ok ' if ok else 'FAIL'} PASS row is vacuous -> vacuous {len(vac_rows[4])}")
bad += 0 if ok else 1
print(f"q65_no_verdict selftest: {'PASS' if not bad else 'FAIL'} "
f"({len(FIXTURES) + 4 - bad}/{len(FIXTURES) + 4})")
return bad
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--repo", default=".")
ap.add_argument("--rev", default="HEAD")
ap.add_argument("--ledger", default=DEFAULT_LEDGER)
ap.add_argument("--runner-ledger", default=RUNNER_LEDGER)
ap.add_argument("--check", action="store_true")
ap.add_argument("--emit", action="store_true")
ap.add_argument("--json")
ap.add_argument("--verbose", action="store_true")
ap.add_argument("--selftest", action="store_true")
a = ap.parse_args()
if a.selftest:
return 0 if selftest() == 0 else 1
try:
git(a.repo, "rev-parse", a.rev)
except (RuntimeError, FileNotFoundError) as e:
print(f"q65_no_verdict: cannot grade -- {e}", file=sys.stderr)
return 2
try:
if a.emit:
rows = census(a.repo, a.rev)
bad = emit(rows, a.ledger, a.rev)
print(f"q65_no_verdict: {len(bad)} row(s) -> {a.ledger} "
f"({len(rows)} committed verifiers at {a.rev})")
return 0
faults = report(a.repo, a.rev, read_ledger(a.ledger),
read_runner(a.runner_ledger), a.verbose)
if a.json:
with open(a.json, "w", encoding="utf-8") as f:
json.dump(census(a.repo, a.rev), f, indent=1)
return 0 if faults == 0 else 1
except (RuntimeError, FileNotFoundError) as e:
print(f"q65_no_verdict: cannot grade -- {e}", file=sys.stderr)
return 2
if __name__ == "__main__":
sys.exit(main())