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.mdFIRST, thenscripts/spec-revocation-check, thentests/test-spec-revocation-check.sh, wired intotests/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]
- Repo root defaults to
git rev-parse --show-toplevelof the cwd (never the script's own location). - --glob is required (repeatable, relative to repo root,
**supported). No project-specific defaults. - Marker: default regex matches
Revokes prior text (<TAG>): "<quote>"on one line, quote extends to the LAST"on the line (quotes may contain").--tagoptional: without it, match any tag AND the untagged formRevokes prior text: "<quote>".--marker-regexoverrides with a regex that has exactly one capture group (validate this; exit 2 otherwise). Also report any line that containsRevokes prior textbut does not parse (malformed marker) as a violation. - 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. - 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. - 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.
- Output: human-readable
path:line (base): <text>lines grouped under "unquoted deletions", "fabricated quotes", "malformed markers", "deleted files";--jsonemits the same as JSON. Summary line. - Exit codes: 0 clean, 1 violations, 2 usage/environment error (bad --base, not a git repo, bad regex).
--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.