#!/usr/bin/env python3 """q93_axis_negative_probe.py -- Q93's two negative cells, at the AXIS level. Q93 (2026-10-27) changed the malformed-input axis of `tools/selftest_sweep.py` from PROSE grading to CONTRACT grading, and gave the run one verdict line whose text and exit code come from the same function. A green axis after such a change proves only that the corpus agrees with the new rule; it does not prove the axis can still FAIL. This probe supplies the two negative shapes Q93 asks for, through the real subprocess path (`discover` -> `read_axis` -> `report_read`), against scratch copies of `tools/`: A answers-zero : a tool that exits 0 on a malformed input EXPECT rc 1, malformed-input 1 fault, verdict RC_WRONG, and `citation_hash_class.py/not-a-repo` named on the summary line (Q93: UNEXPLAINED never appears unnamed). B odd-prose : a tool that refuses correctly at rc 2 with prose no table ever pinned EXPECT rc 0, 0 fault -- the axis grades the contract, so a sound refusal with different wording is no longer red. Under the pre-Q93 table this same tool was UNEXPLAINED, which is the defect: the axis would have gone green only if the tool's message were degraded to match a pinned phrase. Root is resolved from this file (`wizard-work/volt/` -> repo root), not written down, so a second checkout re-verifies itself. Scratch copies live under $TMPDIR and are removed. No file in the working tree is modified. rc 0 both shapes behaved as stated (the axis can fail, and for the right reason) rc 1 a shape behaved differently: the axis is blind, or still grades wording rc 2 could not grade: no selftest_sweep.py, no tools/, or a scratch build failed """ from __future__ import annotations import contextlib import importlib.util import io import os import shutil import sys import tempfile ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) SWEEP = os.path.join(ROOT, "tools", "selftest_sweep.py") TOOLS = os.path.join(ROOT, "tools") SHIM_ZERO = ("import sys as _s, os as _o\n" "if '--repo' in _s.argv:\n" " _r = _s.argv[_s.argv.index('--repo') + 1]\n" " if not _o.path.isdir(_o.path.join(_r, '.git')):\n" " print('0 citations classified; nothing to do')\n" " raise SystemExit(0)\n") SHIM_REFUSE = ("import sys as _s, os as _o\n" "if '--repo' in _s.argv:\n" " _r = _s.argv[_s.argv.index('--repo') + 1]\n" " if not _o.path.isdir(_o.path.join(_r, '.git')):\n" " _s.stderr.write('totally different words, same refusal\\n')\n" " raise SystemExit(2)\n") def load_sweep(): spec = importlib.util.spec_from_file_location("q93_sweep", SWEEP) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) # side-effect-free: guarded by __main__ return mod def build(base: str, name: str, shim: str) -> str: """A copy of tools/ whose citation_hash_class.py opens with `shim`.""" d = os.path.join(base, name) os.makedirs(d) for fn in sorted(os.listdir(TOOLS)): if fn.endswith(".py"): shutil.copy(os.path.join(TOOLS, fn), os.path.join(d, fn)) target = os.path.join(d, "citation_hash_class.py") with open(target, encoding="utf-8") as fh: body = fh.read() with open(target, "w", encoding="utf-8") as fh: fh.write(shim + body) return d def probe(mod, tools_dir: str): """Run both input axes against `tools_dir`; return (rc, read, malformed, malformed summary line, named faults).""" rows, _source, _untracked = mod.discover(None, tools_dir) axis = mod.read_axis(rows, 120) named = [(r["tool"], r["probe"], r["verdict"]) for r in mod.axis_faults(axis)] buf = io.StringIO() with contextlib.redirect_stdout(buf): rc, read_fault, malformed_fault = mod.report_read(axis, quiet=True) lines = [ln for ln in buf.getvalue().splitlines() if ln.startswith("malformed-input:")] return rc, read_fault, malformed_fault, (lines[0] if lines else ""), named def main() -> int: if not os.path.isfile(SWEEP): print("cannot grade: no tools/selftest_sweep.py under " + ROOT) return 2 if not os.path.isdir(TOOLS): print("cannot grade: no tools/ under " + ROOT) return 2 mod = load_sweep() ok = True with tempfile.TemporaryDirectory(prefix="q93neg.") as base: d = build(base, "answers-zero", SHIM_ZERO) rc, _rf, mf, line, named = probe(mod, d) a_ok = (rc == 1 and mf == 1 and ("citation_hash_class.py", "not-a-repo", "RC_WRONG") in named and "citation_hash_class.py/not-a-repo" in line) ok &= a_ok print("A answers-zero rc=%d malformed_fault=%d %s" % (rc, mf, "EXPECTED" if a_ok else "UNEXPECTED")) print(" %s" % line) d = build(base, "odd-prose", SHIM_REFUSE) rc, _rf, mf, line, named = probe(mod, d) b_ok = (rc == 0 and mf == 0 and not named) ok &= b_ok print("B odd-prose rc=%d malformed_fault=%d %s" % (rc, mf, "EXPECTED" if b_ok else "UNEXPECTED")) print(" %s" % line) print("q93 axis negative cells: %s" % ("2/2 as expected" if ok else "FAILED")) return 0 if ok else 1 if __name__ == "__main__": sys.exit(main())