diff --git a/context/briefs/spec-revocation-check.md b/context/briefs/spec-revocation-check.md new file mode 100644 index 0000000..e9a9a06 --- /dev/null +++ b/context/briefs/spec-revocation-check.md @@ -0,0 +1,54 @@ +# 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 (): ""`). 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 [--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 (): ""` 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: ""`. `--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): ` 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.