context: brief for spec-revocation-check
This commit is contained in:
54
context/briefs/spec-revocation-check.md
Normal file
54
context/briefs/spec-revocation-check.md
Normal file
@@ -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 (<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
|
||||||
|
<base>`), 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.
|
||||||
Reference in New Issue
Block a user