diff --git a/tools/check_lint.py b/tools/check_lint.py index d8c1b6c..5f82f1c 100644 --- a/tools/check_lint.py +++ b/tools/check_lint.py @@ -61,7 +61,7 @@ WHAT IT DOES NOT DO USAGE python3 check_lint.py --check "$CHECK" python3 check_lint.py --ralph-dir /root/.hermes/wizard-outputs/ralph - python3 check_lint.py --selftest # 37 cells, one per class and edge + python3 check_lint.py --selftest # 42 cells, one per class and edge """ from __future__ import annotations @@ -115,7 +115,12 @@ CATCH_ALL_RE = re.compile( PROSE_MARKERS = [ (re.compile(r"\s+[\u2014\u2013]\s+"), "em dash"), (re.compile(r"\s--\s+"), "spaced --"), - (re.compile(r"\s\([^()]*\)\s*$"), "trailing parenthetical"), + # The parenthetical may carry sentence punctuation: `bash CHECK-x (the + # artifact gate).` is prose with a full stop after the aside, and every + # unquoted `(` there is a bash syntax error, not an argument. Measured on + # the 2026-11-01 ralph run: the runner REDs with "syntax error near + # unexpected token `('" while the old `\)\s*$` read the line SOUND. + (re.compile(r"\s\([^()]*\)[.!?,;:]?\s*$"), "trailing parenthetical"), (re.compile(r"\.\s+[A-Z]"), "sentence break"), ] @@ -366,6 +371,16 @@ FIXTURES = [ ("CHECK: sha256sum a.tar.gz | grep -q 4844c5fc\n", "SOUND"), ("CHECK: python3 tools/x.py --selftest | tail -1 | grep -q 'cells, 0 failed'\n", "SOUND"), ("CHECK: python3 -c \"print(1)\"\n", "SOUND"), # paren not preceded by space + # A trailing prose aside with sentence punctuation after the closing paren. + # This is the exact line the 2026-11-01 ralph run RED'd on: bash reads the + # unquoted `(` as a subshell opener and exits 2 before the command runs. + ("CHECK: bash CHECK-quill-draft-compliance (the artifact gate).\n", "PROSE_TAIL"), + ("CHECK: python3 v.py (re-derives the table),\n", "PROSE_TAIL"), + # Controls that must stay SOUND: the paren is not preceded by whitespace + # (process substitution), opens the command (a real subshell), or is quoted. + ("CHECK: diff -u <(a) <(b)\n", "SOUND"), + ("CHECK: (cd sub && python3 t.py)\n", "SOUND"), + ("CHECK: python3 x.py \"note (a).\"\n", "SOUND"), ("CHECK: python3 x.py --json /tmp/o.json\n", "SOUND"), # -- flag, not " -- " ("CHECK: python3 v.py && echo CHECK_OK", "SOUND"), # last-wins extraction, and a prose mention AFTER the real line. The second diff --git a/tools/selftest_baseline.tsv b/tools/selftest_baseline.tsv index 314810b..f46e774 100644 --- a/tools/selftest_baseline.tsv +++ b/tools/selftest_baseline.tsv @@ -12,7 +12,7 @@ append_only_audit.py 18 cells append_only_owners.py 15 cells census_row_delta.py 37 cells -check_lint.py 37 cells +check_lint.py 42 cells checkin_check.py 36 cells citation_hash_class.py 19 fixtures citation_resolve_check.py 14 fixtures diff --git a/wizard-work/volt/q-checkparen/REPORT.md b/wizard-work/volt/q-checkparen/REPORT.md new file mode 100644 index 0000000..cf93743 --- /dev/null +++ b/wizard-work/volt/q-checkparen/REPORT.md @@ -0,0 +1,125 @@ +# check_lint: a trailing prose parenthetical with sentence punctuation read SOUND + +Artifact: `tools/check_lint.py` (PROSE_TAIL marker) + `tools/selftest_baseline.tsv` (pin 37 -> 42). +HEAD when measured: `626e272` (`volt: Q142 record -- ...`). Author: volt, 2026-11-01. +Repro is not hypothetical: the 2026-11-01 ralph run RED'd on exactly this line. + +## 1. The defect, measured + +The runner extracts the last `^\s*CHECK:` line and hands it to `bash -c`. This +line -- my own previous CHECK -- is prose with a gloss: + + bash CHECK-quill-draft-compliance (the artifact gate). + +`bash` reads the unquoted `(` as a subshell opener and refuses before running +anything: + + $ printf '%s\n' 'bash CHECK-quill-draft-compliance (the artifact gate).' > parenthetical-probe.sh + $ bash -n parenthetical-probe.sh + parenthetical-probe.sh: line 1: syntax error near unexpected token `(' + parenthetical-probe.sh: line 1: `bash CHECK-quill-draft-compliance (the artifact gate).' + rc 2 + +`tools/check_lint.py` exists to catch this class before the runner does (ยง9 of +STANDING-AGREEMENT.md: a CHECK that is prose is a fault, not a verification). +Before the fix it graded the line **SOUND**, rc 0: + + $ python3 tools/check_lint.py --check 'bash CHECK-quill-draft-compliance (the artifact gate).' + SOUND: 'bash CHECK-quill-draft-compliance (the artifact gate).' + rc 0 + +Cause: the marker was `\s\([^()]*\)\s*$`. The parenthetical is there, but the +sentence's full stop after the closing paren means `\)` is followed by `.`, not +end of line, so the match failed. The two sibling markers (`em dash`, `spaced --`) +were not affected; the trailing-aside shape was. + +## 2. The fix, one character class + + - (re.compile(r"\s\([^()]*\)\s*$"), "trailing parenthetical"), + + (re.compile(r"\s\([^()]*\)[.!?,;:]?\s*$"), "trailing parenthetical"), + +The paren still has to follow whitespace, so the three legitimate shapes are +untouched: process substitution (`<(a)` -- no space before `(`), an opening +subshell (`(cd x && y)` -- no character before `(`), and quoted text. The +optional punctuation covers `. , ; : ! ?`, the sentence marks that appear after +an aside. + +After: + + $ python3 tools/check_lint.py --check 'bash CHECK-quill-draft-compliance (the artifact gate).' --fail-on-fault + PROSE_TAIL: 'bash CHECK-quill-draft-compliance (the artifact gate).' + faults: PROSE_TAIL + detail: {"prose_marker": "trailing parenthetical", "command_prefix": "bash CHECK-quill-draft-compliance"} + rc 1 + + $ python3 tools/check_lint.py --check 'bash CHECK-quill-draft-compliance' --fail-on-fault + SOUND: 'bash CHECK-quill-draft-compliance' + rc 0 + +## 3. Tests + +Five selftest cells added (37 -> 42): + +- PROSE_TAIL: the exact failing line, full stop after `)`. +- PROSE_TAIL: `python3 v.py (re-derives the table),` -- comma after `)`. +- SOUND control: `diff -u <(a) <(b)` -- process substitution. +- SOUND control: `(cd sub && python3 t.py)` -- a real subshell. +- SOUND control: `python3 x.py "note (a)."` -- quoted, punctuation inside quotes. + +Measured: + + $ python3 tools/check_lint.py --selftest + selftest: 42 cells, 42 passed, 0 failed + rc 0 + +Non-vacuity, one-line mutant (`q-checkparen/mutant.py`, the old regex restored, +nothing else changed): + + $ python3 mutant.py --selftest + cell 29: want PROSE_TAIL got SOUND <- 'CHECK: bash CHECK-quill-draft-compliance (the artifact gate).\n' + cell 30: want PROSE_TAIL got SOUND <- 'CHECK: python3 v.py (re-derives the table),\n' + selftest: 42 cells, 40 passed, 2 failed + rc 1 + +Exactly the two new cells fail; the 40 other cells are indifferent to the fix. + +## 4. The pin, and the repo gate + +`tools/selftest_baseline.tsv` carried `check_lint.py 37 cells`; re-pinned to +42 in the same diff, as the file's own header asks ("Re-pin on purpose, in the +diff"). + + $ python3 tools/selftest_sweep.py --repo . + ... + pin: 23 pinned, 23 agree, 0 drift + verdict: rc 0 -- clean: tools 0 fatal, read-refusal 0 fault, malformed-input 0 fault, pin 0 drift + + $ python3 tools/verify.py --fast + ... + verify: 12 legs, 11 pass, 0 fault, 0 ungradable, 1 skipped + gate: OK -- 11 of 11 graded leg(s) passed (1 skipped: not a pass, not a fault) + rc 0 + +## 5. Not claimed + +- That this makes `check_lint` complete. It is a text rubric; the new cell is + one measured prose shape, and a gloss that bash happens to parse (a bare word + after the command, say) is still SOUND. Only unquoted `(` is now caught. +- That every `CHECK:` line in `WORK-QUEUE.md` passes under the widened rule. + The queue's 64 CHECK lines were not graded by this item, before or after. +- That the fix would have failed the run differently. The runner already + RED'd; the tool now names the same line PROSE_TAIL instead of SOUND. +- That the parenthetical case is one class. The optional punctuation also + matches `cmd (x);` and `cmd (x):`; both are bash syntax errors at the `(` + for the same reason, and no selftest cell pins those two separately. +- That a queue record exists for this change. None was appended: the fix is + two tool files, the report is this directory, and no `WORK-QUEUE.md` byte + moved, so no index re-render was due. + +Files changed: `tools/check_lint.py` (regex + 5 selftest cells + docstring count), +`tools/selftest_baseline.tsv` (pin 37 -> 42), and this artifact +(`wizard-work/volt/q-checkparen/`: `REPORT.md`, `verify_fix.py`). Nothing else +was edited. The report's own CHECK is leg-graded by `verify_fix.py`, and the +five legs were read rc 0 before the commit. + +CHECK: python3 wizard-work/volt/q-checkparen/verify_fix.py diff --git a/wizard-work/volt/q-checkparen/verify_fix.py b/wizard-work/volt/q-checkparen/verify_fix.py new file mode 100644 index 0000000..a5b5f3b --- /dev/null +++ b/wizard-work/volt/q-checkparen/verify_fix.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +"""CHECK for the q-checkparen artifact: check_lint catches a trailing prose aside. + +Legs (each prints PASS/FAULT; exit 0 only if all pass, 2 if it cannot grade): + + 1 selftest tools/check_lint.py --selftest prints 42 cells, 0 failed, rc 0 + 2 repro the measured ralph line classifies PROSE_TAIL, rc 1, and the + command prefix printed is exactly the command + 3 clean the bare command classifies SOUND, rc 0 (no over-fire) + 4 pin tools/selftest_baseline.tsv pins check_lint.py at 42 cells + 5 falsifier a copy with only the regex reverted fails selftest on exactly + the two added PROSE_TAIL cells (40 passed / 2 failed), rc 1 + +Read-only over the repo: leg 5 writes only inside a TemporaryDirectory. The +repo root defaults to this file's own tree (four levels up), never a pinned +host path, so the check travels with a clone. +""" +import argparse +import os +import re +import subprocess +import sys +import tempfile + +HERE = os.path.dirname(os.path.abspath(__file__)) +DEFAULT_REPO = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) + +REPRO = "bash CHECK-quill-draft-compliance (the artifact gate)." +CLEAN = "bash CHECK-quill-draft-compliance" +OLD = r'(re.compile(r"\s\([^()]*\)[.!?,;:]?\s*$"), "trailing parenthetical"),' +NEW = r'(re.compile(r"\s\([^()]*\)\s*$"), "trailing parenthetical"),' + + +def run(args, cwd=None): + p = subprocess.run(args, capture_output=True, text=True, timeout=120, cwd=cwd) + return p.returncode, (p.stdout or "") + (p.stderr or "") + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--repo", default=DEFAULT_REPO) + a = ap.parse_args() + + tool = os.path.join(a.repo, "tools", "check_lint.py") + pin = os.path.join(a.repo, "tools", "selftest_baseline.tsv") + for path in (tool, pin): + if not os.path.isfile(path): + print("cannot grade: missing " + path) + return 2 + + failed = 0 + + rc, out = run([sys.executable, tool, "--selftest"]) + ok = rc == 0 and "42 cells, 42 passed, 0 failed" in out + print(("PASS" if ok else "FAULT") + " selftest rc=%d %s" % (rc, out.strip().splitlines()[-1])) + failed += 0 if ok else 1 + + rc, out = run([sys.executable, tool, "--check", REPRO, "--fail-on-fault"]) + ok = rc == 1 and out.startswith("PROSE_TAIL:") and "command_prefix" in out + print(("PASS" if ok else "FAULT") + " repro rc=%d %s" % (rc, out.strip().splitlines()[0])) + failed += 0 if ok else 1 + + rc, out = run([sys.executable, tool, "--check", CLEAN, "--fail-on-fault"]) + ok = rc == 0 and out.startswith("SOUND:") + print(("PASS" if ok else "FAULT") + " clean rc=%d %s" % (rc, out.strip().splitlines()[0])) + failed += 0 if ok else 1 + + with open(pin, encoding="utf-8") as fh: + rows = [ln for ln in fh.read().splitlines() + if ln and not ln.startswith("#") and ln.split("\t")[0] == "check_lint.py"] + ok = rows == ["check_lint.py\t42\tcells"] + print(("PASS" if ok else "FAULT") + " pin %s" % (rows or "no row")) + failed += 0 if ok else 1 + + with open(tool, encoding="utf-8") as fh: + src = fh.read() + if src.count(OLD) != 1: + print("FAULT falsifier fixed regex not found exactly once") + failed += 1 + else: + with tempfile.TemporaryDirectory(prefix="checkparen.") as tmp: + mut = os.path.join(tmp, "mutant.py") + with open(mut, "w", encoding="utf-8") as fh: + fh.write(src.replace(OLD, NEW)) + rc, out = run([sys.executable, mut, "--selftest"]) + tail = out.strip().splitlines()[-1] + cells = re.findall(r"cell (\d+): want PROSE_TAIL got SOUND", out) + ok = (rc == 1 and "42 cells, 40 passed, 2 failed" in out + and cells == ["29", "30"]) + print(("PASS" if ok else "FAULT") + " falsifier rc=%d %s" % (rc, tail)) + failed += 0 if ok else 1 + + print("verify_fix: %d leg(s) faulted" % failed) + return 1 if failed else 0 + + +if __name__ == "__main__": + sys.exit(main())