#!/usr/bin/env python3 """checklint -- lint the CHECK line of a Wizard Troupe standup entry. The troupe's lane contract is one line: CHECK: whose own exit code IS the verdict: 0 = correct, nonzero = wrong. Two defects keep recurring in this loop, and both are statically detectable before any command runs: * the CHECK line carries a fallback arm (`cmd || true`, `cmd; exit 0`, `cmd || echo ...`) or is a constant-success builtin (`true`, `exit 0`). The exit code is then 0 whatever the command did: a gate that cannot fail. * the CHECK line is not one bare command (`a && b`, `a | b`, a redirect, a line continuation), so the harness cannot run it as written and the multi-command line resolves to whichever arm last ran. checklint reads an entry, extracts the top-level CHECK line, and reports one finding per defect. `--run` adds the only non-static leg: it executes the extracted command with a timeout and folds the command's REAL exit code into the verdict. Exit code IS the verdict: 0 the CHECK line is clean (and, with --run, the command exited 0) 1 a graded fault (one or more findings) 2 COULD NOT GRADE (entry unreadable, command could not be executed, timed out) -- every 2 names its cause, and a 2 is never a fault Usage: checklint entry.md checklint --run entry.md checklint --scan . # every file under DIR carrying a CHECK line checklint --json entry.md `--scan` is a gate over a corpus: it exits 1 when any scanned file has findings, 0 when every CHECK line in the tree is clean, 2 when the tree could not be read. It skips .git, __pycache__ and files whose first 8 KiB contain a NUL byte. Deliberately NOT checked: whether the command's assertions are strong enough. A CHECK line can be one bare command and still assert nothing (`test -f f`); a static linter cannot see that. It is documented here because a reader who assumes checklint grades the quality of the gate would be wrong. """ from __future__ import annotations import argparse import json import os import re import subprocess import sys from dataclasses import dataclass, asdict from pathlib import Path PROG = "checklint" DEFAULT_TIMEOUT = 300.0 # Shell control operators and substitution that turn a CHECK line into more # than one command, into a substitution, or into a redirected command. The # contract is one bare command whose exit code is its own, so all of these are # faults -- including `2>/dev/null`, which is still a redirect. METACHARS = "`$<>&|;(){}" # A fallback arm forces the exit code to 0 (or to the arm's own code) whatever # the command did. Reported by name, because "it contains |" does not tell a # reader why the gate cannot fail. FALLBACK_PATTERNS: tuple[tuple[str, re.Pattern[str]], ...] = ( ("cmd || true", re.compile(r"\|\|\s*(?:true|:)\s*$")), ("cmd || exit 0", re.compile(r"\|\|\s*exit\s+0\s*$")), ("cmd || echo/printf ...", re.compile(r"\|\|\s*(?:echo|printf)\b")), ("cmd ; exit 0", re.compile(r";\s*exit\s+0\s*$")), ("cmd ; true", re.compile(r";\s*(?:true|:)\s*$")), ("cmd ; echo/printf ...", re.compile(r";\s*(?:echo|printf)\b")), ) # Commands that always succeed. Exact match on the stripped command. TAUTOLOGIES = frozenset( { "true", ":", "exit 0", "echo", "echo ok", "echo pass", "echo PASS", "echo 0", "printf ok", "test 1 -eq 1", "test 1 = 1", "test -f /dev/null", "test -e /", } ) FENCE_RE = re.compile(r"^\s*(?:```|~~~)") CHECK_RE = re.compile(r"^\s*CHECK\s*:\s*(.*)$") SKIP_DIRS = {".git", "__pycache__", ".pytest_cache", ".ruff_cache"} @dataclass(frozen=True) class Finding: """One defect. kind is 'error' (a graded fault) or 'cng' (could not grade).""" code: str line: int | None message: str kind: str = "error" def as_dict(self) -> dict: return asdict(self) def iter_prose_lines(text: str): """Yield (lineno, raw_line) for lines outside fenced code blocks. A CHECK line inside a ``` fence is a replay transcript, not the contract. """ in_fence = False for lineno, raw in enumerate(text.split("\n"), start=1): if FENCE_RE.match(raw): in_fence = not in_fence continue if not in_fence: yield lineno, raw def find_check_lines(text: str) -> list[tuple[int, str]]: """Top-level CHECK lines as (lineno, command-stripped) pairs.""" hits = [] for lineno, raw in iter_prose_lines(text): m = CHECK_RE.match(raw) if m: hits.append((lineno, m.group(1).strip())) return hits def lint_command(cmd: str, lineno: int) -> list[Finding]: """Static findings for one extracted command.""" out: list[Finding] = [] if cmd.endswith("\\"): out.append( Finding("METACHAR", lineno, f"CHECK line ends a line continuation: {cmd!r}") ) return out for label, rx in FALLBACK_PATTERNS: if rx.search(cmd): out.append( Finding( "FALLBACK_ARM", lineno, f"CHECK line carries a fallback arm ({label}): {cmd!r} -- " "the exit code is forced, so the gate cannot fail", ) ) break bad = "".join(sorted({c for c in cmd if c in METACHARS})) if bad: out.append( Finding( "METACHAR", lineno, f"CHECK line contains shell metacharacters {bad!r}: {cmd!r} -- " "the contract is ONE bare command with no shell metacharacters", ) ) if cmd in TAUTOLOGIES: out.append( Finding( "TAUTOLOGY", lineno, f"CHECK line is the constant-success command {cmd!r} -- " "the gate cannot fail", ) ) return out def lint_text(text: str) -> list[Finding]: """Every finding for one entry's text.""" hits = find_check_lines(text) if not hits: return [ Finding("MISSING", None, "no top-level 'CHECK: ' line in the entry") ] if len(hits) > 1: where = ", ".join(str(n) for n, _ in hits) return [ Finding( "MULTIPLE", hits[1][0], f"{len(hits)} top-level CHECK lines (lines {where}); the contract is exactly one", ) ] lineno, cmd = hits[0] if not cmd: return [Finding("EMPTY", lineno, "CHECK line carries no command")] return lint_command(cmd, lineno) def run_check_command(cmd: str, timeout: float) -> tuple[int | None, str]: """Execute the extracted command. Returns (exit_code, detail).""" try: proc = subprocess.run( cmd, shell=True, capture_output=True, text=True, timeout=timeout, ) except subprocess.TimeoutExpired: return None, f"timed out after {timeout:g}s" except OSError as exc: return None, f"could not execute: {exc}" tail = (proc.stderr or proc.stdout or "").strip().splitlines() detail = f"exit {proc.returncode}" if tail: detail += f" | last output line: {tail[-1][:160]}" return proc.returncode, detail def grade( text: str, *, run: bool = False, timeout: float = DEFAULT_TIMEOUT ) -> tuple[list[Finding], int]: """Lint, and optionally execute. Returns (findings, verdict). verdict: 0 clean, 1 fault, 2 could-not-grade. """ findings = lint_text(text) blocking = {"MISSING", "EMPTY", "MULTIPLE", "FALLBACK_ARM", "METACHAR", "TAUTOLOGY"} if run and not any(f.code in blocking for f in findings): cmd = find_check_lines(text)[0][1] code, detail = run_check_command(cmd, timeout) if code is None: if detail.startswith("timed out"): findings.append(Finding("TIMEOUT", find_check_lines(text)[0][0], detail, "cng")) else: findings.append(Finding("UNREADABLE", find_check_lines(text)[0][0], detail, "cng")) elif code != 0: findings.append( Finding( "RUN_FAILURE", find_check_lines(text)[0][0], f"the CHECK command exited {code} ({detail})", ) ) if any(f.kind == "cng" for f in findings): return findings, 2 if findings: return findings, 1 return findings, 0 def read_text(path: Path) -> str: """Read a file as text; binary content is reported, never guessed at.""" raw = path.read_bytes() if b"\0" in raw[:8192]: raise ValueError(f"{path}: binary (NUL byte in first 8 KiB)") return raw.decode("utf-8", errors="replace") def report(path: Path, findings: list[Finding], verdict: int, as_json: bool) -> None: if as_json: print( json.dumps( {"entry": str(path), "verdict": verdict, "findings": [f.as_dict() for f in findings]}, indent=2, ) ) return if not findings: print(f"OK {path}") return print(f"{'FAULT' if verdict == 1 else 'CNG '} {path}") for f in findings: where = f"line {f.line}" if f.line else "entry" print(f" [{f.code}] ({where}) {f.message}") def scan(root: Path, *, run: bool, timeout: float, as_json: bool) -> int: """Gate over a corpus: every file under root carrying a top-level CHECK line.""" if not root.is_dir(): print(f"{PROG}: --scan: not a directory: {root}", file=sys.stderr) return 2 checked = faulty = clean = skipped = 0 results: list[dict] = [] for dirpath, dirnames, filenames in os.walk(root): dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS] for name in sorted(filenames): p = Path(dirpath) / name try: text = read_text(p) except (OSError, ValueError): skipped += 1 continue if not find_check_lines(text): continue checked += 1 findings, verdict = grade(text, run=run, timeout=timeout) if verdict == 0: clean += 1 else: faulty += 1 results.append( {"entry": str(p), "verdict": verdict, "findings": [f.as_dict() for f in findings]} ) report(p, findings, verdict, as_json) if as_json: print(json.dumps({"root": str(root), "checked": checked, "clean": clean, "faulty": faulty, "skipped": skipped, "results": results}, indent=2)) else: print( f"\nscan {root}: {checked} file(s) with a CHECK line, " f"{clean} clean, {faulty} faulty, {skipped} unreadable/binary skipped" ) if faulty: return 1 return 0 def main(argv: list[str] | None = None) -> int: ap = argparse.ArgumentParser( prog=PROG, description="Lint the CHECK line of a standup entry; its exit code is the verdict.", ) ap.add_argument("entries", nargs="*", help="entry files to lint") ap.add_argument("--run", action="store_true", help="execute the extracted command and fold its real exit code in") ap.add_argument("--timeout", type=float, default=DEFAULT_TIMEOUT, metavar="SECONDS", help=f"timeout for --run (default {DEFAULT_TIMEOUT:g}s)") ap.add_argument("--json", action="store_true", help="emit findings as JSON") ap.add_argument("--scan", metavar="DIR", help="lint every file under DIR with a CHECK line") args = ap.parse_args(argv) if args.scan: return scan(Path(args.scan), run=args.run, timeout=args.timeout, as_json=args.json) if not args.entries: ap.print_usage(sys.stderr) print(f"{PROG}: no entry given (or use --scan DIR)", file=sys.stderr) return 2 verdict = 0 for entry in args.entries: path = Path(entry) try: text = read_text(path) except OSError as exc: print(f"CNG {path}\n [UNREADABLE] {exc}", file=sys.stderr) verdict = 2 continue except ValueError as exc: print(f"CNG {path}\n [UNREADABLE] {exc}", file=sys.stderr) verdict = 2 continue findings, v = grade(text, run=args.run, timeout=args.timeout) report(path, findings, v, args.json) if v == 2: verdict = 2 elif v == 1 and verdict != 2: verdict = 1 return verdict if __name__ == "__main__": sys.exit(main())