fix(spec-revocation-check): --tag scopes marker parsing; free-prose ignored by default (brief)
This commit is contained in:
@@ -65,6 +65,26 @@ def is_markdown_only(collapsed: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def is_free_prose_unquoted(line: str) -> bool:
|
||||
"""True if the line is a free-prose (no-quote) revocation marker.
|
||||
|
||||
Recognised by the presence of ``Revokes prior text:`` (the colon form,
|
||||
no parens) AND the absence of any ``"`` on the same physical line.
|
||||
Examples::
|
||||
|
||||
Revokes prior text: the pre-… wording
|
||||
**Revokes prior text:** the pre-… wording
|
||||
|
||||
This is an older M23-era style that uses prose instead of a verbatim
|
||||
quote. Truly malformed lines (e.g. typos like ``Revokes prior text
|
||||
(M33): no quote`` or ``Revokes prior text typo``) lack the colon form
|
||||
or contain a ``"``, so they are not classified as free-prose.
|
||||
"""
|
||||
if "Revokes prior text:" not in line:
|
||||
return False
|
||||
return '"' not in line
|
||||
|
||||
|
||||
# --- Default marker regex builders ---------------------------------------
|
||||
|
||||
# The two-branch no-tag pattern. We use named groups to disambiguate which
|
||||
@@ -207,18 +227,42 @@ def find_quotes_and_malformed(
|
||||
path: Path,
|
||||
marker_re: re.Pattern[str],
|
||||
no_tag_mode: bool,
|
||||
) -> tuple[list[tuple[int, str]], list[tuple[int, str]]]:
|
||||
"""Return (quotes, malformed).
|
||||
tag: str | None,
|
||||
strict: bool,
|
||||
) -> tuple[list[tuple[int, str]], list[tuple[int, str]], int]:
|
||||
"""Return (quotes, malformed, ignored).
|
||||
|
||||
``quotes``: [(line_no, quoted_text), ...] for every parseable marker.
|
||||
``malformed``: [(line_no, raw_line), ...] for every line containing the
|
||||
phrase ``Revokes prior text`` that did NOT parse.
|
||||
``quotes``: ``[(line_no, quoted_text), ...]`` for every parseable marker.
|
||||
``malformed``: ``[(line_no, raw_line), ...]`` for every candidate line
|
||||
that contained the marker phrase but did NOT parse.
|
||||
``ignored``: count of free-prose unquoted lines seen in no-tag mode
|
||||
when ``strict`` is False. With ``--tag``, free-prose is impossible by
|
||||
construction (only literal-tagged lines are candidates), so this is
|
||||
always 0.
|
||||
|
||||
In **tag mode** (``--tag TAG``, ``no_tag_mode == False``), only lines
|
||||
containing the literal ``Revokes prior text (TAG)`` are candidates;
|
||||
every other line is skipped before the regex runs, so a marker for a
|
||||
different tag, an untagged marker, or a free-prose marker cannot
|
||||
accidentally be classified as malformed or quoted.
|
||||
|
||||
In **no-tag mode**, every line containing ``Revokes prior text`` is a
|
||||
candidate. A candidate that fails the multi-branch regex is split:
|
||||
``is_free_prose_unquoted`` lines become ``ignored`` (or ``malformed``
|
||||
if ``strict``); everything else becomes ``malformed``.
|
||||
"""
|
||||
quotes: list[tuple[int, str]] = []
|
||||
malformed: list[tuple[int, str]] = []
|
||||
ignored = 0
|
||||
tag_literal = f"Revokes prior text ({tag})" if not no_tag_mode else None
|
||||
for n, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
|
||||
if "Revokes prior text" not in line:
|
||||
continue
|
||||
if tag_literal is not None and tag_literal not in line:
|
||||
# With --tag, lines without the literal TAG marker are entirely
|
||||
# ignored — not malformed, not quoted, not counted. Their text
|
||||
# is never used to cover deletions in this run.
|
||||
continue
|
||||
m = marker_re.search(line)
|
||||
if m:
|
||||
if no_tag_mode:
|
||||
@@ -230,8 +274,14 @@ def find_quotes_and_malformed(
|
||||
q = m.group(1)
|
||||
quotes.append((n, q or ""))
|
||||
else:
|
||||
malformed.append((n, line))
|
||||
return quotes, malformed
|
||||
if no_tag_mode and is_free_prose_unquoted(line):
|
||||
if strict:
|
||||
malformed.append((n, line))
|
||||
else:
|
||||
ignored += 1
|
||||
else:
|
||||
malformed.append((n, line))
|
||||
return quotes, malformed, ignored
|
||||
|
||||
|
||||
# --- The check itself -----------------------------------------------------
|
||||
@@ -243,6 +293,7 @@ def run_checks(
|
||||
marker_re: re.Pattern[str],
|
||||
globs: list[str],
|
||||
min_line_chars: int,
|
||||
strict: bool,
|
||||
) -> dict:
|
||||
files_at_head = iter_files(repo_root, globs)
|
||||
renames = git_diff_renames(repo_root, base_sha)
|
||||
@@ -255,6 +306,7 @@ def run_checks(
|
||||
unquoted: list[dict] = []
|
||||
fabricated: list[dict] = []
|
||||
malformed: list[dict] = []
|
||||
ignored_total = 0
|
||||
|
||||
# Pass 1: every file at HEAD (Check A + Check B).
|
||||
for path in files_at_head:
|
||||
@@ -265,7 +317,10 @@ def run_checks(
|
||||
old_rel = rename_targets[rel]
|
||||
base_text = git_show(repo_root, base_sha, old_rel)
|
||||
|
||||
quotes, mf = find_quotes_and_malformed(path, marker_re, no_tag_mode)
|
||||
quotes, mf, ignored = find_quotes_and_malformed(
|
||||
path, marker_re, no_tag_mode, tag, strict,
|
||||
)
|
||||
ignored_total += ignored
|
||||
|
||||
if base_text is None:
|
||||
# File did not exist at base (and is not a rename). Any quote is
|
||||
@@ -342,6 +397,7 @@ def run_checks(
|
||||
"repo_root": str(repo_root.resolve()),
|
||||
"base": base_sha,
|
||||
"tag": tag,
|
||||
"strict": strict,
|
||||
"marker_regex": marker_re.pattern,
|
||||
"files_checked": sorted(head_rel_set),
|
||||
"violations": {
|
||||
@@ -355,6 +411,7 @@ def run_checks(
|
||||
"fabricated": len(fabricated),
|
||||
"malformed": len(malformed),
|
||||
"deleted": len(deleted),
|
||||
"ignored": ignored_total,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -387,7 +444,7 @@ def format_human(result: dict) -> str:
|
||||
for x in v["deleted_since_base"]:
|
||||
lines.append(f" {x['path']} ({x['base']})")
|
||||
|
||||
if not any(v.values()):
|
||||
if not any(v.values()) and result["summary"]["ignored"] == 0:
|
||||
return (
|
||||
f"spec-revocation-check: clean — every {result['base']} line "
|
||||
"survives or is quoted."
|
||||
@@ -397,7 +454,8 @@ def format_human(result: dict) -> str:
|
||||
lines.append(
|
||||
f"Summary: {s['unquoted'] + s['fabricated'] + s['malformed'] + s['deleted']} "
|
||||
f"violation(s) — {s['unquoted']} unquoted, {s['fabricated']} fabricated, "
|
||||
f"{s['malformed']} malformed, {s['deleted']} deleted"
|
||||
f"{s['malformed']} malformed, {s['deleted']} deleted, "
|
||||
f"{s['ignored']} ignored"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
@@ -435,6 +493,9 @@ def main(argv: list[str]) -> int:
|
||||
help="override the marker regex; must have exactly one capture group")
|
||||
parser.add_argument("--min-line-chars", type=int, default=12,
|
||||
help="ignore base lines shorter than this after collapsing (default: 12)")
|
||||
parser.add_argument("--strict", action="store_true",
|
||||
help="without --tag, treat free-prose unquoted markers as malformed "
|
||||
"(they are otherwise counted in the summary as 'ignored')")
|
||||
parser.add_argument("--json", action="store_true",
|
||||
help="emit result as JSON on stdout")
|
||||
parser.add_argument("--dryrun", "-n", action="store_true",
|
||||
@@ -490,6 +551,7 @@ def main(argv: list[str]) -> int:
|
||||
print(f"[dryrun] repo_root: {repo_root}")
|
||||
print(f"[dryrun] base: {args.base} -> {base_sha}")
|
||||
print(f"[dryrun] tag: {args.tag if args.tag else 'None'}")
|
||||
print(f"[dryrun] strict: {args.strict}")
|
||||
print(f"[dryrun] marker: {marker_re.pattern}")
|
||||
print(f"[dryrun] min_line_chars: {args.min_line_chars}")
|
||||
print(f"[dryrun] files: {len(head_files)}")
|
||||
@@ -504,6 +566,7 @@ def main(argv: list[str]) -> int:
|
||||
marker_re=marker_re,
|
||||
globs=args.globs,
|
||||
min_line_chars=args.min_line_chars,
|
||||
strict=args.strict,
|
||||
)
|
||||
|
||||
if args.json:
|
||||
|
||||
@@ -38,7 +38,8 @@ spec-revocation-check --base <commit-ish> [--tag TAG]
|
||||
|
||||
| Argument | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `--tag TAG` | — | Restrict the marker regex to `(TAG)`. Without it, match any tag AND the untagged form `Revokes prior text: "<quote>"`. |
|
||||
| `--tag TAG` | — | Restrict the marker regex to `(TAG)`. Without it, match any tag AND the untagged form `Revokes prior text: "<quote>"`. With `--tag`, lines that do not contain the literal `Revokes prior text (TAG)` are entirely ignored — not parsed, not used as quotes, not counted. |
|
||||
| `--strict` | off | Without `--tag`, lines that look like free-prose unquoted revocation markers (e.g. `**Revokes prior text:** the pre-… wording`) are reported as malformed. Default is to count them in the summary as `ignored (unquoted-style)` without failing. No effect with `--tag` (a non-parsing `(TAG)` line is already malformed). |
|
||||
| `--marker-regex REGEX` | built-in default | Override the line-shape regex. Must have exactly one capture group (the quoted text); exit 2 if it has zero or more than one. |
|
||||
| `--min-line-chars N` | `12` | Ignore non-blank base lines whose collapsed length is shorter than this (e.g. short structural lines). After collapsing. |
|
||||
| `--repo-root DIR` | `git rev-parse --show-toplevel` of cwd | Repo root. Never inferred from the script's own path. |
|
||||
@@ -66,7 +67,12 @@ spec-revocation-check --base <commit-ish> [--tag TAG]
|
||||
|
||||
- Default, with `--tag TAG`: `Revokes prior text \(TAG\):\s*"(.*)"` (the
|
||||
match is `re.search`, so the marker can appear mid-line inside a Markdown
|
||||
table cell).
|
||||
table cell). Lines that do not contain the literal `Revokes prior text (TAG)`
|
||||
are not even candidates — they are skipped before the regex runs, so a
|
||||
tagged marker for a different tag, an untagged marker, or a free-prose
|
||||
marker cannot accidentally be classified as malformed. (Defect from
|
||||
round 1: with `--tag M33`, every M23 / untagged / free-prose marker was
|
||||
being reported as malformed.)
|
||||
- Default, without `--tag`: match either the tagged form for any single
|
||||
parenthesised token, OR the untagged form `Revokes prior text:\s*"(.*)"`.
|
||||
The untagged form is the M32-era marker; we keep it recognised so older
|
||||
@@ -82,18 +88,32 @@ containing `"`" case, which only works because of the greedy match.
|
||||
|
||||
For every file matched by a glob at HEAD:
|
||||
|
||||
- Parse all marker occurrences in the file's current text. Collect
|
||||
`(line_no, quote_text)`.
|
||||
- For each parsed quote, collapse (whitespace-collapse, strip Markdown
|
||||
- Walk the file line by line. For each line containing the phrase
|
||||
`Revokes prior text`:
|
||||
- **With `--tag TAG`**: if the line does not contain the literal
|
||||
`Revokes prior text (TAG)`, skip it entirely (not parsed, not
|
||||
reported, its text not used as a quote). Otherwise try the marker
|
||||
regex; on match, record `(line_no, quote_text)`. On miss, record
|
||||
a "malformed marker" violation.
|
||||
- **Without `--tag`**: try the multi-branch marker regex
|
||||
(tagged-any | untagged); on match, record `(line_no, quote_text)`.
|
||||
On miss, classify the line:
|
||||
- If the line is a **free-prose unquoted marker** — i.e. it
|
||||
contains `Revokes prior text:` (the colon form, no parens) AND
|
||||
has no `"` anywhere on the line — then with `--strict` record
|
||||
it as a malformed-marker violation, otherwise silently ignore
|
||||
it (count in the summary as `ignored (unquoted-style)` but do
|
||||
not contribute to the exit code).
|
||||
- Otherwise record it as a malformed-marker violation.
|
||||
- For each recorded quote, collapse (whitespace-collapse, strip Markdown
|
||||
structural prefixes — blockquote `>`, list bullet `- * 1.`, table outer
|
||||
pipes) and assert it is a substring of the SAME file at `--base` after
|
||||
the same collapse.
|
||||
- If the file did not exist at `--base`, every parsed quote is a violation
|
||||
(a "fabricated quote" — you cannot revoke text from a file that did not
|
||||
yet exist).
|
||||
- A line that contains `Revokes prior text` but does NOT match the marker
|
||||
regex is a "malformed marker" violation. (Catches a typo'd or reworded
|
||||
marker that would otherwise silently carry no enforceable quote.)
|
||||
- **No-quote markers (free-prose) are never used as quote text to cover
|
||||
deletions**, regardless of mode: they have no quote to extract.
|
||||
|
||||
### 4. Check B — completeness
|
||||
|
||||
@@ -107,7 +127,9 @@ DIFFERS from `--base`:
|
||||
`***` / `___`), skip it.
|
||||
- Otherwise, the collapsed base line MUST be present in either:
|
||||
(a) the current file's collapsed text, OR
|
||||
(b) the collapsed text of any of that file's parsed quotes.
|
||||
(b) the collapsed text of any of that file's parsed quotes (NOT free-
|
||||
prose lines — they have no quote text and so cannot cover a
|
||||
deletion).
|
||||
- If neither, it is an "unquoted deletion" violation.
|
||||
|
||||
### 5. Check D — deleted / renamed files
|
||||
@@ -142,7 +164,7 @@ malformed markers:
|
||||
deleted since base:
|
||||
spec/old.md (<base>)
|
||||
|
||||
Summary: 3 violation(s) — 1 unquoted, 1 fabricated, 1 malformed, 0 deleted
|
||||
Summary: 3 violation(s) — 1 unquoted, 1 fabricated, 1 malformed, 0 deleted, 2 ignored
|
||||
```
|
||||
|
||||
The `<text>` for `unquoted deletions` is the original base line (with
|
||||
@@ -168,7 +190,7 @@ If there are zero violations: `spec-revocation-check: clean — every
|
||||
"malformed_markers": [{"path": "spec/baz.md", "line": 3, "base": "...", "line_text": "..."}],
|
||||
"deleted_since_base": [{"path": "spec/old.md", "base": "..."}]
|
||||
},
|
||||
"summary": {"unquoted": 1, "fabricated": 1, "malformed": 1, "deleted": 0}
|
||||
"summary": {"unquoted": 1, "fabricated": 1, "malformed": 1, "deleted": 0, "ignored": 2}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -183,6 +205,7 @@ The script is read-only by nature. `--dryrun` (and `-n`) prints:
|
||||
[dryrun] repo_root: <abs-path>
|
||||
[dryrun] base: <base-as-arg> -> <resolved-sha>
|
||||
[dryrun] tag: <TAG-or-None>
|
||||
[dryrun] strict: <True-or-False>
|
||||
[dryrun] marker: <regex-source>
|
||||
[dryrun] min_line_chars: 12
|
||||
[dryrun] files: <N>
|
||||
@@ -210,7 +233,9 @@ to read the repo and report.)
|
||||
| File matched at HEAD, did not exist at base | Check A applies (any quote is a violation); Check B is skipped. |
|
||||
| File matched at base, does not exist at HEAD | Reported as `deleted since base` (Check D); unless git says renamed, in which case Check B is run on the renamed file. |
|
||||
| File matched at base, identical at HEAD | Skipped entirely. |
|
||||
| File has `Revokes prior text` but no closing `"` on the same line | Reported as `malformed marker` (Check A). |
|
||||
| File has `Revokes prior text` but no closing `"` on the same line | Reported as `malformed marker` (Check A), unless the line is a free-prose unquoted marker (see below). |
|
||||
| With `--tag X`, line has `Revokes prior text (Y): "..."` for a different tag Y | Line is entirely ignored (not parsed, not used as a quote, not counted). Y's text is NOT used to cover deletions in this run. |
|
||||
| Free-prose unquoted marker (no `--tag`): line contains `Revokes prior text:` (colon form, no parens) and has no `"` anywhere on the line | Without `--strict`: counted in the summary as `ignored (unquoted-style)`; does not contribute to exit code; never used as quote text to cover deletions. With `--strict`: reported as `malformed marker`. |
|
||||
| Quote text contains literal `"` characters | Handled by greedy match to the LAST `"` on the line. |
|
||||
| Two quote markers on one line (e.g. two revoked cells of one table row) | Both parsed separately by the regex (greedy `.*"` finds each tail `"`); each checked independently. |
|
||||
| Base file has only blank lines | Skipped (no non-blank base lines to lose). |
|
||||
@@ -257,4 +282,8 @@ spec-revocation-check --base $BASE_SHA --glob 'spec/**/*.md' --json
|
||||
| R11 | Output groups violations under `unquoted deletions`, `fabricated quotes`, `malformed markers`, `deleted since base`. Each violation is `path:line (base): <text>`. Summary line at the end. |
|
||||
| R12 | `--json` emits the same data as a single JSON object on stdout (human-readable summary still on stderr). |
|
||||
| R13 | Exit 0 clean, 1 any violation, 2 usage/environment error. |
|
||||
| R14 | `--dryrun` prints resolved repo root, base sha, tag, marker regex, file list, then runs the checks anyway. |
|
||||
| R14 | `--dryrun` prints resolved repo root, base sha, tag, marker regex, file list, then runs the checks anyway. |
|
||||
| R15 | With `--tag TAG`, lines that do not contain the literal `Revokes prior text (TAG)` are entirely ignored: not parsed, not reported, and their text is not used as a quote to cover deletions. Only lines containing that literal are candidates; candidates that fail the marker regex are `malformed markers`. |
|
||||
| R16 | Without `--tag`, a line containing `Revokes prior text` that fails the marker regex is classified as `free-prose unquoted` if it contains `Revokes prior text:` (the colon form, no parens) and has no `"` on the same line. With `--strict`, free-prose lines are `malformed markers`; otherwise they are counted in the summary as `ignored (unquoted-style)` and never contribute to the exit code. |
|
||||
| R17 | Free-prose unquoted markers are NEVER used as quote text to cover deletions, in either tag or no-tag mode. |
|
||||
| R18 | Summary line (human and JSON) gains an `ignored` count for free-prose unquoted markers seen in the current run. The human summary ends with `, I ignored` where I is the ignored count. The JSON `summary` dict gains an `"ignored"` integer key. |
|
||||
@@ -229,21 +229,36 @@ assert_exit "untagged marker no --tag exits 0" 0 "$rc" "$out" ""
|
||||
assert_contains "untagged marker no --tag says clean" "clean" "$out"
|
||||
|
||||
# ---- Test 11: --tag filters other tags ---------------------------------
|
||||
# With --tag X, lines containing markers for tag Y (or untagged or free-
|
||||
# prose) are entirely ignored: not parsed, not reported, not used as
|
||||
# quotes. The base line that the foreign marker quoted is still in the
|
||||
# current file (inside the ignored marker text), so Check B still passes.
|
||||
echo "=== --tag filters other tags ==="
|
||||
T="$SCRATCH/t11"; mk_repo "$T"
|
||||
commit_file "$T" "spec/foo.md" \
|
||||
$'# Spec\n\nA line that stays.\n\nA line that gets revoked.\n\n' >/dev/null
|
||||
BASE=$(git -C "$T" rev-parse HEAD)
|
||||
# Quote uses M33, but we run with --tag M99, so the marker is malformed
|
||||
# (no closing quote because the regex requires "M99").
|
||||
# Quote uses M33, but we run with --tag M99 — the M33 marker is ignored
|
||||
# entirely (not parsed as malformed). The base line text is still in the
|
||||
# current file (inside the ignored marker), so Check B still passes.
|
||||
write_file_via_py "$T" "spec/foo.md" \
|
||||
$'# Spec\n\nA line that stays.\n\nRevokes prior text (M33): "A line that gets revoked."\n\n'
|
||||
out=$(run_check "$T" "$BASE" --tag M99); rc=$?
|
||||
assert_exit "--tag filters other tag exits 1" 1 "$rc" "$out" ""
|
||||
# Without --tag, the marker WOULD have parsed and the line would survive.
|
||||
assert_exit "--tag M99 ignores M33 marker exits 0" 0 "$rc" "$out" ""
|
||||
assert_contains "--tag M99 ignores M33 says clean" "clean" "$out"
|
||||
# Without --tag, the marker parses and the line is covered.
|
||||
out_notag=$(run_check "$T" "$BASE"); rc_notag=$?
|
||||
assert_exit "no --tag accepts M33 marker exits 0" 0 "$rc_notag" "$out_notag" ""
|
||||
|
||||
# Now run --tag M99 with an M33 marker that quotes something OTHER than
|
||||
# a base line — the base line is then truly unquoted, and with --tag M99
|
||||
# the M33 marker doesn't cover it → unquoted deletion → violation.
|
||||
write_file_via_py "$T" "spec/foo.md" \
|
||||
$'# Spec\n\nA line that stays.\n\nRevokes prior text (M33): "Some other thing entirely."\n\n'
|
||||
out=$(run_check "$T" "$BASE" --tag M99); rc=$?
|
||||
assert_exit "--tag M99 + bogus M33 quote exits 1" 1 "$rc" "$out" ""
|
||||
assert_contains "--tag M99 + bogus M33 quote names unquoted" "unquoted deletions" "$out"
|
||||
|
||||
# ---- Test 12: bad --base (exit 2) ---------------------------------------
|
||||
echo "=== bad --base ==="
|
||||
T="$SCRATCH/t12"; mk_repo "$T"
|
||||
@@ -286,6 +301,92 @@ out=$(python3 "$SCRIPT" --help); rc=$?
|
||||
assert_exit "--help exits 0" 0 "$rc" "$out" ""
|
||||
assert_contains "--help shows usage" "spec-revocation-check" "$out"
|
||||
assert_contains "--help mentions --base" "--base" "$out"
|
||||
assert_contains "--help mentions --strict" "--strict" "$out"
|
||||
|
||||
# ---- Tests 16-19: real-case fixture (round 2) -------------------------
|
||||
# One base file, one current file with four marker styles:
|
||||
# - (M33) tagged quote (valid)
|
||||
# - Older untagged mid-line marker (M32-era, M23 amendment note)
|
||||
# - (M23) tagged quote
|
||||
# - Free-prose `**Revokes prior text:**` line
|
||||
# Each marker covers a real base line. With --tag M33 the foreign markers
|
||||
# must be ignored; the free-prose line must be ignored unless --strict.
|
||||
echo "=== --tag scopes marker parsing to the literal tag ==="
|
||||
T="$SCRATCH/t16"; mk_repo "$T"
|
||||
# Base: a stable line + one line per marker style + a short line that
|
||||
# Check B skips (under min-line-chars=12). The short line is what the
|
||||
# free-prose marker refers to (so Check B won't flag its deletion).
|
||||
commit_file "$T" "spec/foo.md" \
|
||||
$'# Spec\n\nA line that stays.\n\nLine revoked by M33.\n\nLine revoked by untagged mid-line.\n\nLine revoked by M23.\n\nshort text\n' >/dev/null
|
||||
BASE=$(git -C "$T" rev-parse HEAD)
|
||||
write_file_via_py "$T" "spec/foo.md" \
|
||||
$'# Spec\n\nA line that stays.\n\nRevokes prior text (M33): "Line revoked by M33."\n\n*(Amended M23: ... — Revokes prior text: "Line revoked by untagged mid-line.".)*\n\nRevokes prior text (M23): "Line revoked by M23."\n\n> **Revokes prior text:** the short text\n'
|
||||
|
||||
# Sub-case 1: --tag M33 — only the (M33) line is parsed; foreign markers
|
||||
# are ignored entirely. The base lines they refer to are still verbatim
|
||||
# in the current file (inside the ignored marker text), so Check B
|
||||
# passes. Clean.
|
||||
out=$(run_check "$T" "$BASE" --tag M33); rc=$?
|
||||
assert_exit "--tag M33 ignores foreign markers exits 0" 0 "$rc" "$out" ""
|
||||
assert_contains "--tag M33 ignores foreign says clean" "clean" "$out"
|
||||
|
||||
# Sub-case 2: malformed (M33) line with --tag M33 — flagged. The other
|
||||
# foreign markers must STILL be ignored (not malformed).
|
||||
write_file_via_py "$T" "spec/foo.md" \
|
||||
$'# Spec\n\nA line that stays.\n\nRevokes prior text (M33) no quote\n\n*(Amended M23: ... — Revokes prior text: "Line revoked by untagged mid-line.".)*\n\nRevokes prior text (M23): "Line revoked by M23."\n\n> **Revokes prior text:** the short text\n'
|
||||
out=$(run_check "$T" "$BASE" --tag M33); rc=$?
|
||||
assert_exit "malformed (M33) line exits 1" 1 "$rc" "$out" ""
|
||||
assert_contains "malformed (M33) under header" "malformed markers" "$out"
|
||||
assert_contains "malformed (M33) names the raw line" "no quote" "$out"
|
||||
|
||||
# Restore the valid fixture for sub-cases 3 and 4.
|
||||
write_file_via_py "$T" "spec/foo.md" \
|
||||
$'# Spec\n\nA line that stays.\n\nRevokes prior text (M33): "Line revoked by M33."\n\n*(Amended M23: ... — Revokes prior text: "Line revoked by untagged mid-line.".)*\n\nRevokes prior text (M23): "Line revoked by M23."\n\n> **Revokes prior text:** the short text\n'
|
||||
|
||||
# Sub-case 3: no --tag — foreign markers parse (untagged branch + M23
|
||||
# tagged branch); the free-prose line is silently ignored by default.
|
||||
# Output: zero violations but a summary line that counts the ignored
|
||||
# marker (NOT the early "clean — every line survives" message, since
|
||||
# ignored > 0).
|
||||
out=$(run_check "$T" "$BASE"); rc=$?
|
||||
assert_exit "no --tag, free-prose ignored, exits 0" 0 "$rc" "$out" ""
|
||||
assert_contains "no --tag, free-prose ignored, shows summary line" "Summary:" "$out"
|
||||
assert_contains "no --tag, free-prose ignored, summary shows ignored count" "1 ignored" "$out"
|
||||
|
||||
# Sub-case 4: no --tag --strict — free-prose becomes malformed.
|
||||
out=$(run_check "$T" "$BASE" --strict); rc=$?
|
||||
assert_exit "no --tag --strict, free-prose malformed exits 1" 1 "$rc" "$out" ""
|
||||
assert_contains "no --tag --strict, free-prose under header" "malformed markers" "$out"
|
||||
|
||||
# ---- Round-2 dedicated negative fixtures --------------------------------
|
||||
# The negative-test proof loop below needs fixtures that exit 1
|
||||
# consistently (not the ones above, whose state mutates between sub-
|
||||
# cases). t17 is a malformed-(M33) fixture, t19 is a free-prose line with
|
||||
# --strict; both should fail when run against the synthetic no-op.
|
||||
|
||||
# ---- Test 17 (negative): malformed (M33) line with --tag M33 ------------
|
||||
echo "=== negative: malformed (M33) line with --tag M33 ==="
|
||||
T="$SCRATCH/t17"; mk_repo "$T"
|
||||
commit_file "$T" "spec/foo.md" \
|
||||
$'# Spec\n\nA line that stays.\n\nA line that gets revoked.\n\n' >/dev/null
|
||||
BASE=$(git -C "$T" rev-parse HEAD)
|
||||
write_file_via_py "$T" "spec/foo.md" \
|
||||
$'# Spec\n\nA line that stays.\n\nRevokes prior text (M33) no quote\n\n'
|
||||
out=$(run_check "$T" "$BASE" --tag M33); rc=$?
|
||||
assert_exit "negative t17 malformed (M33) exits 1" 1 "$rc" "$out" ""
|
||||
assert_contains "negative t17 malformed (M33) under header" "malformed markers" "$out"
|
||||
|
||||
# ---- Test 19 (negative): free-prose line with no --tag --strict ---------
|
||||
echo "=== negative: free-prose with no --tag --strict ==="
|
||||
T="$SCRATCH/t19"; mk_repo "$T"
|
||||
commit_file "$T" "spec/foo.md" \
|
||||
$'# Spec\n\nA line that stays.\n\nshort text\n' >/dev/null
|
||||
BASE=$(git -C "$T" rev-parse HEAD)
|
||||
write_file_via_py "$T" "spec/foo.md" \
|
||||
$'# Spec\n\nA line that stays.\n\n> **Revokes prior text:** the short text\n'
|
||||
out=$(run_check "$T" "$BASE" --strict); rc=$?
|
||||
assert_exit "negative t19 free-prose strict exits 1" 1 "$rc" "$out" ""
|
||||
assert_contains "negative t19 free-prose strict under header" "malformed markers" "$out"
|
||||
|
||||
# ---- Negative-test verification (would each negative fail if disabled?) -
|
||||
# A "negative" test would PASS against a no-op script that always exits 0.
|
||||
@@ -317,7 +418,7 @@ sys.exit(0)
|
||||
PYEOF
|
||||
chmod +x "$SCRATCH/nop.py"
|
||||
|
||||
for case in t2 t3 t4 t6 t7; do
|
||||
for case in t2 t3 t4 t6 t7 t17 t19; do
|
||||
T="$SCRATCH/$case"
|
||||
BASE=$(git -C "$T" rev-parse HEAD 2>/dev/null || echo "HEAD")
|
||||
out_nop=$(python3 "$SCRATCH/nop.py" --base HEAD --repo-root "$T" --glob "spec/**/*.md" 2>&1); rc_nop=$?
|
||||
|
||||
Reference in New Issue
Block a user