Files
small-scripts/context/briefs/spec-revocation-check.md
2026-09-24 13:39:40 +12:00

4.5 KiB

New script: spec-revocation-check — generic spec-amendment revocation gate

Rules (read first)

  • Stay in this worktree; don't push; don't work around permission prompts (stop and report).
  • Follow this repo's CLAUDE.md exactly: OpenSpec in specs/spec-revocation-check.spec.md FIRST, then scripts/spec-revocation-check, then tests/test-spec-revocation-check.sh, wired into tests/run-all.sh.
  • Python 3 stdlib only (#!/usr/bin/env python3). No third-party deps.
  • Do NOT symlink into ~/sbin (the supervisor does that after review).
  • Reference implementation (READ-ONLY copy; do not try to write there): /run/user/1000/mm-worktree-task/small-scripts/ref-spec-revocation/ — check_spec_revocations.py (the completeness half), test_spec_m33_quotes.py (the verbatim-quote half), test_check_spec_revocations.py (its temp-repo tests). They come from a private project; reuse the logic, drop every project-specific default.

Why it exists

When a spec clause is revoked or reworded, the old text must stay in the file as a verbatim quote (Revokes prior text (<TAG>): "<exact old text>"). An LLM agent fabricated such quotes twice; a second failure mode is deleting normative text with no quote at all. This script catches BOTH, for any git repo.

Required behaviour

spec-revocation-check --base <commit-ish> [--tag TAG] --glob PATTERN [--glob ...] [--repo-root DIR] [--marker-regex REGEX] [--min-line-chars N] [--json] [--dryrun|-n] [--help]

  1. Repo root defaults to git rev-parse --show-toplevel of the cwd (never the script's own location).
  2. --glob is required (repeatable, relative to repo root, ** supported). No project-specific defaults.
  3. Marker: default regex matches Revokes prior text (<TAG>): "<quote>" on one line, quote extends to the LAST " on the line (quotes may contain "). --tag optional: without it, match any tag AND the untagged form Revokes prior text: "<quote>". --marker-regex overrides with a regex that has exactly one capture group (validate this; exit 2 otherwise). Also report any line that contains Revokes prior text but does not parse (malformed marker) as a violation.
  4. Check A — verbatim quotes: every parsed quote, whitespace-collapsed (with the reference's markdown prefix stripping: blockquote >, list bullets, table outer pipes), must be a substring of the SAME file at --base (same stripping). Quote in a file that did not exist at base → violation.
  5. Check B — completeness: for each file matched at HEAD that differs from base: every non-blank base line (collapsed) must still be present in the current file or be contained in one of that file's quotes. Lines shorter than --min-line-chars (default 12, after collapsing) are ignored, and markdown-only lines (table separators like |---|---|, fences ```, horizontal rules) are ignored.
  6. Deleted / renamed files: a file that matched the globs at base but no longer exists at HEAD is a violation ("deleted since base") unless git reports it renamed (`git diff --find-renames --name-status `), in which case check the renamed file against the old one.
  7. Output: human-readable path:line (base): <text> lines grouped under "unquoted deletions", "fabricated quotes", "malformed markers", "deleted files"; --json emits the same as JSON. Summary line.
  8. Exit codes: 0 clean, 1 violations, 2 usage/environment error (bad --base, not a git repo, bad regex).
  9. --dryrun: read-only anyway — print the resolved repo root, base sha, file list and marker regex, then run the checks (they have no side effects). State this in the spec.

Tests (tests/test-spec-revocation-check.sh)

Build throwaway git repos under mktemp -d (never touch this repo or any real repo; git -c commit.gpgsign=false). Cover: clean amendment; unquoted deletion → 1; fabricated quote → 1; quote with bold dropped → 1; quote containing " → 0; malformed marker → 1; deleted file → 1; renamed file with quotes → 0; short/markdown-only line removed → 0; untagged marker with no --tag → 0; --tag filters other tags; bad --base → 2; not a git repo → 2; --json parses (python3 -m json.tool); --help → 0. Show each negative test would fail if the check were disabled (say how you proved it in your report).

Done when

./tests/test-spec-revocation-check.sh and ./tests/run-all.sh pass (report any pre-existing run-all failures separately; don't fix unrelated scripts). Report: files, test list, how negatives were proven, open questions.