#!/usr/bin/env python3 """ersatztv#784 — ADVISORY nudge for `docs.no-session-narrative`. A doc records the END STATE; the path to it belongs in the commit message, not the artifact. THIS NEVER FAILS. Every path returns exit 0 — including a bad argument, an unresolvable base ref, an unreadable file and an unhandled exception. That is a design constraint, not an oversight: a narrative detector is a string predicate over prose, and `docs/defect-shapes-773.md` §4 plus `testing.guard-derives-population-from-source` both argue that class must not be load-bearing (the withdrawn `test_review_verdict_vocabulary_parity.py` — six review rounds, then deleted — is the empirical case). Do not convert this into a gate; the decision record says no in as many words. WHY PYTHON AND NOT SHELL. The first implementation hand-parsed `git diff -U0` output in bash by matching line prefixes, and cold review demonstrated four separate defects in that one parser: the `\\ No newline at end of file` marker was counted as content, an added line whose own text began `++ ` was eaten by the `+++ ` header arm, `core.quotePath` hid non-ASCII paths, and `read` dropped a final unterminated line. Those are four instances of one mistake — deciding what a diff line IS from its prefix alone, with no hunk state. Patching them one at a time is the shape this repo has recorded as never converging, so the mechanism was replaced rather than the sites. check-doc-narrative.py --diff scan lines ADDED against (the CI mode) check-doc-narrative.py --all scan the whole tracked corpus (deliberate sweep) Scanning only ADDED lines in CI is what keeps the existing corpus of legitimate history out of the output; `--all` deliberately reports all of it, for a human to apply the who-benefits test to. """ from __future__ import annotations import os import re import subprocess import sys # The never-fails invariant must not depend on the ambient locale. Both the summary line and the # last-resort handler below carry non-ASCII text, so under ascii/latin-1 stdio the very code meant to # guarantee exit 0 is what raises. Degrade unencodable characters instead of failing on them. try: sys.stdout.reconfigure(errors="replace") # type: ignore[union-attr] sys.stderr.reconfigure(errors="replace") # type: ignore[union-attr] except Exception: # noqa: S110 — a stdout that cannot be reconfigured is not a reason to fail pass # deliberate: this is the never-fails invariant's own setup, so it cannot itself raise # `docs/decisions/**` is exempt WHOLESALE — a decision record narrating how a rule was got wrong is # carrying the rationale it exists to carry, so a detector that flagged it would fight the # convention it serves. EXEMPT_PREFIXES = ("docs/decisions/",) # Session-narrative phrasings. Deliberately narrow: each is first person or names a revision of THIS # artifact. Broad words that also appear in legitimate dated history ("previously", "was wrong") are # absent on purpose — a false positive on a carved-out case is what makes an advisory check stop # being read. PATTERNS = re.compile( r"an earlier draft" r"|earlier drafts" r"|the (?:first|previous|original) (?:version|draft) of (?:this|the)" r"|my first attempt" r"|I (?:initially|first|originally|then) (?:thought|assumed|tried|wrote|found)" r"|we (?:then|initially) (?:found|thought|realis|realiz)" r"|it turned out that" r"|earlier today" r"|as of just now" r"|currently investigating", re.IGNORECASE, ) WARNING = ( "{path}:{line} reads as session narrative — a reader coming cold never saw the earlier draft. " "Answer the review finding in the COMMIT MESSAGE and let only the corrected claim enter the " "doc (docs.no-session-narrative). Keep it only if a reader would ACT differently knowing it " "(dated measurement, stated snapshot boundary, tested-and-rejected result, a trap and its " "consequence). Line: {text}" ) def is_scanned_path(path: str) -> bool: """The population: `docs/**/*.md` minus `docs/decisions/**`, plus root-level `*.md`. Stated positively and in one place so the record's `mechanics:` can quote it exactly. Skills, `web/`, and other nested markdown outside `docs/` are deliberately NOT in scope. """ if not path.endswith(".md"): return False if any(path.startswith(p) for p in EXEMPT_PREFIXES): return False return path.startswith("docs/") or "/" not in path # Git's OUTPUT FORMAT is configurable, and this script reads paths and line numbers out of that # format. Three separate knobs were each demonstrated turning a real hit into `scanned 0 file(s)` — # `core.quotePath` hiding non-ASCII paths, `diff.dstPrefix` rewriting the header, `color.diff=always` # injecting ANSI escapes. Pinning them one at a time is refuting variants, not clearing the channel, # so the channel is closed at both ends: the user's and the system's config files are taken out of # the picture entirely (which also covers knobs nobody has thought of yet), and the handful that a # REPO-local config could still set are pinned explicitly on the command line, where they win. GIT_ENV_OVERRIDES = { "GIT_CONFIG_GLOBAL": os.devnull, "GIT_CONFIG_SYSTEM": os.devnull, "GIT_CONFIG_NOSYSTEM": "1", } # `core.quotePath=false` is witnessed by a test. The env overrides above are NOT, and cannot be: they # exist for the knob nobody has named yet, which is exactly what three rounds of naming one knob at a # time argued for. Every knob that IS named is pinned on the command line, where it also beats a # repo-local config, and has a row in FORMAT_KNOBS in the test file. GIT_CONFIG_PINS = ("-c", "core.quotePath=false") def git(*args: str) -> tuple[int, str]: """Run git with its output format pinned. Returns (returncode, stdout) and never raises.""" try: p = subprocess.run( ["git", *GIT_CONFIG_PINS, *args], capture_output=True, text=True, errors="replace", env={**os.environ, **GIT_ENV_OVERRIDES}, ) except OSError as exc: # git missing, or not a repo we can exec in return 1, f"{exc}" if p.returncode != 0: sys.stderr.write(p.stderr) return p.returncode, p.stdout def scan_line(path: str, lineno: int, text: str, out: list[str]) -> None: if PATTERNS.search(text): detail = WARNING.format(path=path, line=lineno, text=text[:160]) out.append(f"::warning file={path}::{detail}") def added_lines(diff: str): """Yield (path, lineno, text) for every ADDED line in a unified diff. A line's meaning comes from HUNK STATE, not from its prefix: `+++ ` is a header only before the first `@@` of a file, and inside a hunk it is content whose own text starts `++ `. That distinction is the whole reason this is not a prefix match. """ path = None in_hunk = False lineno = 0 for raw in diff.split("\n"): if raw.startswith("diff --git "): path, in_hunk = None, False elif raw.startswith("@@"): m = re.match(r"@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@", raw) if not m: in_hunk = False continue in_hunk = True lineno = int(m.group(1)) - 1 elif not in_hunk: if raw.startswith("+++ "): p = raw[4:] # `/dev/null` on the new side means the file was DELETED. Stated honestly: this arm # is DEFENSIVE, not load-bearing — a deletion contributes no `+` lines, so nothing is # yielded for it either way, and removing this arm reddens no test. It is kept because # `path` should never name a file the added lines do not belong to. A # `--diff-filter=d` on the git call was removed rather than kept beside it: a second # mechanism nobody can witness failing is how a duplicate guard hides its twin. path = None if p == "/dev/null" else (p[2:] if p.startswith("b/") else p) elif raw.startswith("+"): lineno += 1 if path is not None: yield path, lineno, raw[1:] elif raw.startswith("-") or raw.startswith("\\"): pass # a removed line, or the no-trailing-newline marker: neither advances the new file else: lineno += 1 # context (absent at -U0, but harmless and correct if -U grows) def run_diff(base: str, out: list[str]) -> int: rc, diff = git( "diff", "-U0", # Pinned, not decorative: `diff.renames=false` in a developer's gitconfig turns a `git mv` # into a whole-file add and re-flags every pre-existing line. Same channel as the prefixes. "--find-renames", # Pin the header shape the path is parsed out of. `diff.noprefix`, `diff.srcPrefix` and # `diff.dstPrefix` each rewrite it from a developer's gitconfig, and `diff.external` replaces # the output entirely — a configured prefix silently produced a scanned-0-files clean run. "--src-prefix=a/", "--dst-prefix=b/", "--no-ext-diff", "--no-color", f"{base}...HEAD", ) if rc != 0: print( f"doc-narrative: could not diff against '{base}' — SCANNED NOTHING. " "This is reported rather than swallowed: a silent zero-file scan is indistinguishable " "from a clean one, which is the failure `ci.required-job-step-execution-markers` exists for." ) return -1 scanned = set() for path, lineno, text in added_lines(diff): if not is_scanned_path(path): continue scanned.add(path) scan_line(path, lineno, text, out) return len(scanned) def run_all(out: list[str]) -> int: # Population from `git ls-files`, never a filesystem walk — an untracked scratch file is not # part of the corpus (#778). rc, listing = git("ls-files", "-z", "--", "*.md") if rc != 0: print("doc-narrative: could not list tracked files — SCANNED NOTHING.") return -1 scanned = 0 for path in listing.split("\0"): if not path or not is_scanned_path(path): continue try: with open(path, encoding="utf-8", errors="replace") as fh: text = fh.read() except OSError as exc: # A tracked-but-deleted doc is an ordinary working state, not a reason to fail. print(f"doc-narrative: skipped {path} ({exc.strerror}).") continue scanned += 1 # splitlines() keeps a final unterminated line, which `read`-per-line dropped. for i, line in enumerate(text.splitlines(), start=1): scan_line(path, i, line, out) return scanned def main(argv: list[str]) -> int: mode = argv[1] if len(argv) > 1 else "--all" out: list[str] = [] if mode == "--diff": if len(argv) < 3 or not argv[2]: print("doc-narrative: --diff needs a base ref — SCANNED NOTHING. (advisory; not a failure)") return 0 scanned = run_diff(argv[2], out) elif mode == "--all": scanned = run_all(out) else: print(f"doc-narrative: unknown mode '{mode}'. usage: {argv[0]} [--all | --diff ]") return 0 for line in out: print(line) if scanned < 0: return 0 print( f"doc-narrative: scanned {scanned} file(s); {len(out)} advisory warning(s). " "NON-BLOCKING — this check never fails a run." ) return 0 if __name__ == "__main__": try: sys.exit(main(sys.argv)) # The never-fails constraint outranks a clean traceback, so this catch is deliberately blind. except Exception as exc: print(f"doc-narrative: internal error ({exc!r}) — SCANNED NOTHING. Advisory; not a failure.") sys.exit(0)