fix(609): arm the decisions body-diff with a git trailer, not a bare substring
The rationale-edit exemption was a substring test over the whole commit range, so any message containing the literal marker armed it -- including prose explaining why no marker was needed, which is how it fired live in PR #605: a green --base/--head run that was vacuous on the body-diff dimension, in the one PR that hand-resolved a merge conflict inside the corpus the guard exists to police. Now read as an affirmative `Decisions-Edit:` git trailer. Git parses trailers only in the final paragraph, so a quoted example message cannot arm it -- which matters because this commit and its decision record both quote one. A non-affirmative value (`no`) does not arm it either; the retired substring arms nothing and gets a ::warning:: nudge. Tests: negative controls (prose mention, quoted mid-body trailer, `no` value, retired substring) plus positive controls (trailer, uppercase, alongside Co-Authored-By). All four negative controls verified red against the old matcher. fixes #609 Decisions-Edit: yes Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# ersatztv#521 — the line-level append-only mechanic is retired. Decision integrity is now enforced by
|
||||
# the lifecycle validator. `[decisions-edit]` survives ONLY for rationale-prose edits (validator
|
||||
# the lifecycle validator. A `Decisions-Edit: yes` trailer survives ONLY for rationale-prose edits (validator
|
||||
# body-diff, CI). This shim runs the structural validator over the working tree; the body-diff/no-
|
||||
# vanish checks run in CI where a base/head is available. Fail-open on any tooling trouble.
|
||||
set -uo pipefail
|
||||
|
||||
@@ -160,7 +160,8 @@ jobs:
|
||||
|
||||
# BLOCKING (ersatztv#521, supersedes the ersatztv#303 H9 append-only mechanic): validates decision-
|
||||
# record lifecycle invariants (metadata schema, one active record per key, reciprocal
|
||||
# supersedes/superseded-by links, no rationale-prose rewrite without [decisions-edit], no record
|
||||
# supersedes/superseded-by links, no rationale-prose rewrite without a Decisions-Edit: yes git
|
||||
# trailer (ersatztv#609 — never a bare substring, which prose about the marker could arm), no record
|
||||
# vanishing from the active set without an archive copy) and that the generated active catalog
|
||||
# (docs/decisions/README.md) is in sync. Same validator the Husky pre-commit hook shim calls, so
|
||||
# local and CI enforcement can't drift. Seconds-long git diff + parse -> keep it off the build runners.
|
||||
|
||||
+4
-4
@@ -60,8 +60,8 @@ step is now continuous. The release boundary is instead where you:
|
||||
not a release gate.
|
||||
5. Report the remaining `legacy-unmigrated` count (the validator prints it as a `::notice::`) so the
|
||||
backlog is visible, even though it isn't required to hit zero before a release.
|
||||
A genuine rationale-prose rewrite still needs `[decisions-edit]` in the commit message (see the
|
||||
`decisions.md` header) — routine lifecycle metadata writes above do not.
|
||||
A genuine rationale-prose rewrite still needs a `Decisions-Edit: yes` git trailer on a commit in the
|
||||
range (see the `decisions.md` header) — routine lifecycle metadata writes above do not.
|
||||
|
||||
**Cutting a release:** keep build and promotion as two explicit phases (#335):
|
||||
|
||||
@@ -578,7 +578,7 @@ blocking `api-docs` job, and `docs/decisions.md` by the blocking `decisions-guar
|
||||
Enforces decision-record lifecycle invariants (ersatztv#521, supersedes the ersatztv#303 H9
|
||||
append-only mechanic): well-formed 5-field metadata, exactly one `active` record per `key`,
|
||||
reciprocal `supersedes`/`superseded-by` links, no record vanishing from the active set without an
|
||||
archive copy, no rationale-prose rewrite without the `[decisions-edit]` token in the commit range,
|
||||
archive copy, no rationale-prose rewrite without a `Decisions-Edit: yes` trailer in the commit range,
|
||||
and the generated active catalog (`docs/decisions/README.md`) in sync with source. Two steps:
|
||||
`scripts/decisions_validate.py --base origin/<base> --head HEAD` (the merge-base diff checks, which
|
||||
need a base/head range — CI-only) and `scripts/build_decisions_catalog.py --check` (catalog drift).
|
||||
@@ -1062,7 +1062,7 @@ entirely in `web/`), the wiring is:
|
||||
3. **`commit-msg`** — enforces the CLAUDE.md protocol: the message must carry a
|
||||
`Co-Authored-By:` trailer, else the commit is rejected (merge commits are exempt, detected
|
||||
via `git rev-parse --verify MERGE_HEAD`). The decision-lifecycle check lives in `pre-commit`
|
||||
(above), not here — `[decisions-edit]` is read from the commit message, but only by the CI
|
||||
(above), not here — the `Decisions-Edit:` trailer is read from the commit message, but only by the CI
|
||||
`decisions lifecycle` job's body-diff step (`range` mode over the PR's merge-base diff), which is
|
||||
the only place a base/head range exists to diff against.
|
||||
|
||||
|
||||
+38
-5
@@ -55,7 +55,9 @@ budget (replaces the old 1800-line
|
||||
floor on this single file). The Husky `pre-commit` hook runs the structural checks over the working
|
||||
tree; the CI `decisions lifecycle` job additionally runs the body-diff check with `--base`/`--head`.
|
||||
|
||||
**`[decisions-edit]`** is now narrow: it is required ONLY when a commit changes the **rationale
|
||||
**`Decisions-Edit: yes`** (a git **trailer** — the message's final paragraph, alongside
|
||||
`Co-Authored-By:`; the pre-#609 `[decisions-edit]` substring is retired and no longer exempts
|
||||
anything) is narrow: it is required ONLY when a commit changes the **rationale
|
||||
prose** of a surviving or archived record — a factual correction to already-written history. Routine
|
||||
lifecycle writes (adding a new active record, relocating a superseded/retired record to the archive,
|
||||
updating metadata fields, regenerating the catalog) are token-free; the validator proves they're
|
||||
@@ -3080,7 +3082,8 @@ an `active` record MUST NOT live under `archive/`; every `supersedes`/`supersede
|
||||
resolve to a record that actually exists (active or archived); and — replacing the append-only
|
||||
guard's line-delete detection — a record's rationale prose (the schema strips only the contiguous
|
||||
metadata block before comparing) must not change between a PR's base and head unless the commit
|
||||
carries `[decisions-edit]`, and a record that disappears from the active set without a matching
|
||||
carries the rationale-edit marker (`Decisions-Edit: yes`, a git trailer since #609 — see
|
||||
`ci.decisions-edit-trailer`), and a record that disappears from the active set without a matching
|
||||
archive copy is flagged as an unlogged removal. `scripts/build_decisions_catalog.py` regenerates
|
||||
`docs/decisions/README.md`, a compact router listing only `active` records by key, so an agent
|
||||
doesn't have to read the full chronological log (or the archive) to find the current rule.
|
||||
@@ -3092,9 +3095,9 @@ validator that checks reciprocal keys, archive placement, and rationale-prose st
|
||||
by construction, and additionally catches classes of mistake the old guard could not (a supersession
|
||||
banner added without moving the entry out of the active read-path; a `superseded-by` reference to a
|
||||
key that doesn't exist; two `active` records quietly claiming the same key after a copy-paste).
|
||||
`[decisions-edit]` is kept, narrowed to its one remaining legitimate use — an actual rationale-prose
|
||||
edit (fixing a factual error) — since routine lifecycle moves (a status flip, an archive relocation)
|
||||
are now token-free and machine-checked instead.
|
||||
The rationale-edit marker is kept, narrowed to its one remaining legitimate use — an actual
|
||||
rationale-prose edit (fixing a factual error) — since routine lifecycle moves (a status flip, an
|
||||
archive relocation) are now marker-free and machine-checked instead.
|
||||
|
||||
**This PR is the first to exercise the new regime, on itself.** `docs.append-only-guard` (the H9 half
|
||||
of #303's combined H9/H3 entry; H3's root-screenshot guard is untouched and stays active) is marked
|
||||
@@ -3918,3 +3921,33 @@ owner's sweep **destroyed** the row the other library still served. This is the
|
||||
rather than *destroys* that shared row. The unrecoverable data loss is gone; the shared-row limitation is a
|
||||
whole-app property, not a music-video one, and is tracked separately as **#606**. #496's Done-when was
|
||||
amended to this parity wording rather than ticked as literally written.
|
||||
|
||||
## 2026-07-25 — The rationale-edit marker is a git trailer, not a substring anywhere in the commit range (#609)
|
||||
`key: ci.decisions-edit-trailer` · `status: active` · `since: 2026-07-25` · `supersedes: none` · `superseded-by: none`
|
||||
**Rule:** The body-diff exemption is armed by an affirmative `Decisions-Edit:` **git trailer** (`yes`/`true`/`1`, case-insensitive) on some commit in the PR's merge-base range — never by a substring search over the message text. A non-affirmative value (`no`) does not arm it, the retired `[decisions-edit]` substring arms nothing (the validator emits a `::warning::` nudge when it sees one without a trailer), and a git error leaves the guard ON.
|
||||
**Signals:** decisions-edit, Decisions-Edit trailer, rationale-prose edit, body-diff guard, decisions_validate, vacuous gate, green no-op · paths: `scripts/decisions_validate.py`, `scripts/tests/test_decisions_validate.py` · issues: #609, #603, #521
|
||||
**Mechanics:** `git log --format='%(trailers:key=Decisions-Edit,valueonly)' <mb>..<head>`; `dv._edit_trailer_armed`
|
||||
|
||||
**The guard could disable itself by being described.** The original check was
|
||||
`EDIT_TOKEN.lower() in git log --format=%B mb..head`, so *any* commit message containing the literal
|
||||
string anywhere set `token = True` and suppressed all three rewrite comparisons (active survivors,
|
||||
active→archive laundering, archive survivors). It fired live in PR #605: a commit message explaining
|
||||
*why no token was needed* contained the bracketed token, and armed it. The result was a **green
|
||||
`--base/--head` run that was vacuous on the body-diff dimension**, in the one PR that hand-resolved a
|
||||
merge conflict inside the corpus the guard exists to police. No content was damaged, but the gate
|
||||
reported success while checking nothing — the "a gate can merge green yet be a no-op" class.
|
||||
|
||||
**Why a trailer and not an own-line match.** Requiring the token alone on its own line fixes the
|
||||
observed instance, but not the class: this repo's docs commits routinely *quote example commit
|
||||
messages*, and the commit introducing this very record does so. An own-line matcher would arm on the
|
||||
example. Git recognises a trailer only in the message's final paragraph, so a `Decisions-Edit: yes`
|
||||
line quoted mid-body followed by more prose parses as nothing at all (verified against git 2.55) —
|
||||
which makes documenting the mechanism structurally incapable of disabling it. Requiring an
|
||||
*affirmative value* closes the same trap one level up: `Decisions-Edit: no`, the natural way to record
|
||||
a deliberate non-edit, must not read as consent.
|
||||
|
||||
**The marker's scope is unchanged** — still narrow, still only for a rationale-prose edit to a
|
||||
surviving or archived record (`docs.decision-lifecycle`); only its form changed. It coexists with the
|
||||
`Co-Authored-By:` trailer the `commit-msg` hook already mandates, so armed commits carry a
|
||||
multi-trailer block. The retired form is left detectable on purpose: a contributor with the old habit
|
||||
gets a warning naming the form change, not a bare "prose changed" failure.
|
||||
|
||||
@@ -36,6 +36,7 @@ the link for rationale. Superseded/retired history lives in `archive/`. Regenera
|
||||
| `ci.batch-pushes-no-cancel-route` | Hold review fixes, doc corrections and format fixes locally and push **once** — a superseded run cannot be cancelled from the agent side and holds a runner slot until it finishes. | 2026-07-21 | [link](workflow-process.md#2026-07-21--batch-your-pushes-there-is-no-agent-side-cancel-route-on-gitea-1254-542) |
|
||||
| `ci.build-once-rejected` | CI build-once (a shared compile artifact across jobs) was implemented, measured, and rejected for a 40-85% wall-clock regression; keep the #420 cross-run tree-identity skip instead. | 2026-07-18 | [link](../decisions.md#2026-07-18--ci-build-once-was-measured-and-rejected-keep-the-420-tree-skip) |
|
||||
| `ci.cancelled-is-not-a-verdict` | Treat a `cancelled` conclusion as "no verdict" — never as pass or fail — and report FAILED and CANCELLED counts separately in any CI monitor. | 2026-07-21 | [link](workflow-process.md#2026-07-21--cancelled-is-not-failure-a-cancelled-run-is-no-verdict-542) |
|
||||
| `ci.decisions-edit-trailer` | The body-diff exemption is armed by an affirmative `Decisions-Edit:` **git trailer** (`yes`/`true`/`1`, case-insensitive) on some commit in the PR's merge-base range — never by a substring search over the message text. A non-affirmative value (`no`) does not arm it, the retired `[decisions-edit]` substring arms nothing (the validator emits a `::warning::` nudge when it sees one without a trailer), and a git error leaves the guard ON. | 2026-07-25 | [link](../decisions.md#2026-07-25--the-rationale-edit-marker-is-a-git-trailer-not-a-substring-anywhere-in-the-commit-range-609) |
|
||||
| `ci.decisions-lifecycle-flake` | When `decisions lifecycle` is the **only** red job, do not investigate and do not create a new run to clear it — no rebase, no `--amend`, no no-op push; the operator reruns that single job from the Gitea UI. | 2026-07-21 | [link](workflow-process.md#2026-07-21--a-lone-decisions-lifecycle-red-is-a-known-infra-flake-do-nothing-542) |
|
||||
| `ci.docs-only-detect-shallow-safe` | The docs-only detect script must diff against `FETCH_HEAD` (always resolves after `git fetch`, even shallow) using a two-dot tree diff — not `origin/<base>` with three-dot — because a `fetch-depth: 1` shallow clone has no remote-tracking ref and no merge-base, which silently fails the original detect into `docs_only=false` (full matrix, no functional error). A CI-behavior change must be verified by measuring the effect (job durations), not just a green check. | 2026-07-17 | [link](../decisions.md#2026-07-17--docs-only-detect-must-be-shallow-checkout-safe-fetch_head--two-dot-not-originmain--three-dot-416-follow-up) |
|
||||
| `ci.docs-only-skip-steps` | A docs-only change must still run every required job (`test`, `migrations`) so their commit-status contexts always report; each heavy job runs `scripts/ci-detect-docs-only.sh` first and gates its real STEPS on `if: steps.detect.outputs.docs_only != 'true'`, never `if:`-skips the whole job (an `if:`-skipped job reports `skipped`, not `success`, which branch protection may never unblock on). Detection biases toward running more on any doubt. | 2026-07-17 | [link](../decisions.md#2026-07-17--docs-only-ci-skip-gates-steps-in-always-running-required-jobs-never-if-skips-them-416) |
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
Replaces the line-level append-only guard (ersatztv#303 H9) with lifecycle checks that preserve its
|
||||
spirit — rationale is never silently rewritten or deleted; every history touch is deliberate and
|
||||
reviewable. `[decisions-edit]` is kept ONLY for rationale-prose edits (see body-diff below); routine
|
||||
reviewable. A `Decisions-Edit: yes` commit trailer is kept ONLY for rationale-prose edits; routine
|
||||
lifecycle metadata writes are token-free. Fail-open on tooling trouble (missing refs, git errors,
|
||||
parse issues), matching the old guard.
|
||||
"""
|
||||
@@ -25,7 +25,15 @@ SKIP_HEADINGS = {"Index", "Active catalog", "Contents"}
|
||||
# weak recall metadata and produces confident false-negatives for the "MemPalace to find, file to
|
||||
# confirm" workflow (ersatztv#545).
|
||||
REQUIRED_META = ("key", "status", "since", "supersedes", "superseded_by", "signals")
|
||||
EDIT_TOKEN = "[decisions-edit]" # noqa: S105 (a commit-message marker, not a credential)
|
||||
# The rationale-edit marker is a git TRAILER, not a bare substring anywhere in the commit range.
|
||||
# ersatztv#609: the original `[decisions-edit]` substring test armed on ANY commit message containing
|
||||
# the literal string — including prose *about* the mechanism, and including a message explaining why
|
||||
# no token was needed — which silently disabled all three body-diff comparisons while the job still
|
||||
# reported green. Git recognises a trailer only in a message's final trailer block, so prose can't
|
||||
# arm it, and neither can a quoted example commit message (which docs commits here routinely carry).
|
||||
EDIT_TRAILER = "Decisions-Edit"
|
||||
_EDIT_TRAILER_AFFIRMATIVE = frozenset({"yes", "true", "1"})
|
||||
_LEGACY_EDIT_TOKEN = "[decisions-edit]" # noqa: S105 (a commit-message marker, not a credential)
|
||||
|
||||
|
||||
# `date.fromisoformat` is NOT a YYYY-MM-DD validator: on Python >= 3.11 it also accepts ISO basic
|
||||
@@ -162,7 +170,7 @@ def validate(
|
||||
for h in removed:
|
||||
errs.append(f"record removed from the active set without an archive copy: {h!r}")
|
||||
for h in rewritten:
|
||||
errs.append(f"rationale prose of {h!r} changed without the {EDIT_TOKEN} token")
|
||||
errs.append(f"rationale prose of {h!r} changed without a '{EDIT_TRAILER}: yes' commit trailer")
|
||||
for h in demoted:
|
||||
errs.append(f"{h}: migrated record demoted to legacy-unmigrated (metadata block removed)")
|
||||
|
||||
@@ -252,6 +260,32 @@ def _rationale(rec) -> str:
|
||||
return "\n".join(s for s in (ln.strip() for ln in rest) if s)
|
||||
|
||||
|
||||
def _edit_trailer_armed(mb: str, head: str) -> bool:
|
||||
"""True when some commit in `mb..head` carries an affirmative `Decisions-Edit:` git trailer.
|
||||
|
||||
A non-affirmative value (`no`, `not-needed`, …) deliberately does NOT arm the exemption: the
|
||||
marker must be an explicit yes, so a trailer written to *record that no edit was intended* can
|
||||
never disable the guard — the ersatztv#609 failure mode, one abstraction level up.
|
||||
|
||||
Git errors return False (the guard stays ON), matching the rest of the fail-safe posture here:
|
||||
a lost marker costs a contributor one clear error message, a lost guard costs silent history
|
||||
rewrites in the one PR that most needed policing.
|
||||
"""
|
||||
out = _run(["git", "log", f"--format=%(trailers:key={EDIT_TRAILER},valueonly)", f"{mb}..{head}"])
|
||||
if out is not None and any(ln.strip().lower() in _EDIT_TRAILER_AFFIRMATIVE for ln in out.splitlines()):
|
||||
return True
|
||||
# Retired-form nudge: without it, a contributor using the pre-#609 habit gets a bare "prose
|
||||
# changed" failure and no hint that the marker's FORM (not their intent) is what changed.
|
||||
if _LEGACY_EDIT_TOKEN in (_run(["git", "log", "--format=%B", f"{mb}..{head}"]) or "").lower():
|
||||
print(
|
||||
f"::warning::decisions-validate: the commit range carries the retired "
|
||||
f"{_LEGACY_EDIT_TOKEN} marker but no '{EDIT_TRAILER}: yes' trailer — since ersatztv#609 "
|
||||
"the rationale-edit marker is a git trailer, and the substring no longer exempts anything",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def _diff_findings(base: str, head: str) -> tuple[list[str], list[str], list[str]]:
|
||||
"""(removed, rewritten, demoted) between merge-base(base,head) and head. Fail-open → ([], [], [])."""
|
||||
mb = _merge_base(base, head)
|
||||
@@ -261,10 +295,7 @@ def _diff_findings(base: str, head: str) -> tuple[list[str], list[str], list[str
|
||||
file=sys.stderr,
|
||||
)
|
||||
return [], [], []
|
||||
if EDIT_TOKEN.lower() in (_run(["git", "log", "--format=%B", f"{mb}..{head}"]) or "").lower():
|
||||
token = True
|
||||
else:
|
||||
token = False
|
||||
token = _edit_trailer_armed(mb, head)
|
||||
|
||||
base_active = _records_at(mb, _active_paths_at(mb))
|
||||
head_active = _records_at(head, _active_paths_at(head))
|
||||
|
||||
@@ -97,7 +97,7 @@ def test_removed_active_not_in_archive_fails():
|
||||
|
||||
def test_rewritten_rationale_without_token_fails():
|
||||
assert any(
|
||||
"rationale" in e and "decisions-edit" in e
|
||||
"rationale" in e and "Decisions-Edit" in e
|
||||
for e in _v([_rec(key="a.b")], rewritten=["2026-01-01 — Reworded (#9)"])
|
||||
)
|
||||
|
||||
@@ -249,7 +249,12 @@ def test_diff_engine_detects_rationale_rewrite_without_token(tmp_path, monkeypat
|
||||
assert "2026-01-01 — Some decision (#1)" in rewritten
|
||||
|
||||
|
||||
def test_diff_engine_allows_rewrite_with_token(tmp_path, monkeypatch):
|
||||
def _rewrite_repo(tmp_path: Path, monkeypatch, *commit_msg_paragraphs: str) -> tuple[list[str], list[str]]:
|
||||
"""Seed a repo, rewrite the record's rationale under `commit_msg_paragraphs`, return (removed, rewritten).
|
||||
|
||||
Each paragraph is a separate `-m`, which is how git composes a message with a trailer block: the
|
||||
LAST paragraph is the only one git parses for trailers (verified against git 2.55).
|
||||
"""
|
||||
repo = tmp_path / "repo"
|
||||
(repo / "docs").mkdir(parents=True)
|
||||
_git(tmp_path, "init", str(repo))
|
||||
@@ -264,13 +269,82 @@ def test_diff_engine_allows_rewrite_with_token(tmp_path, monkeypatch):
|
||||
|
||||
_write_decisions(repo / "docs" / "decisions.md", status="active", since="2026-01-01", rationale="Reworded prose.")
|
||||
_git(repo, "add", "-A")
|
||||
_git(repo, "commit", "-m", "rewrite rationale [decisions-edit]")
|
||||
_git(repo, "commit", *[a for p in commit_msg_paragraphs for a in ("-m", p)])
|
||||
|
||||
removed, rewritten, *_rest = dv._diff_findings("HEAD~1", "HEAD")
|
||||
return removed, rewritten
|
||||
|
||||
|
||||
_RECORD = "2026-01-01 — Some decision (#1)"
|
||||
|
||||
|
||||
def test_diff_engine_allows_rewrite_with_trailer(tmp_path, monkeypatch):
|
||||
removed, rewritten = _rewrite_repo(tmp_path, monkeypatch, "rewrite rationale", "Decisions-Edit: yes")
|
||||
assert removed == []
|
||||
assert rewritten == []
|
||||
|
||||
|
||||
def test_diff_engine_trailer_is_case_insensitive(tmp_path, monkeypatch):
|
||||
_removed, rewritten = _rewrite_repo(tmp_path, monkeypatch, "rewrite rationale", "Decisions-Edit: YES")
|
||||
assert rewritten == []
|
||||
|
||||
|
||||
def test_diff_engine_trailer_coexists_with_coauthor(tmp_path, monkeypatch):
|
||||
# the repo's `commit-msg` hook mandates a Co-Authored-By trailer, so the real-world shape of an
|
||||
# armed commit is a multi-trailer block — the marker must survive sharing it.
|
||||
_removed, rewritten = _rewrite_repo(
|
||||
tmp_path,
|
||||
monkeypatch,
|
||||
"rewrite rationale",
|
||||
"Decisions-Edit: yes\nCo-Authored-By: Someone <s@example.com>",
|
||||
)
|
||||
assert rewritten == []
|
||||
|
||||
|
||||
def test_diff_engine_prose_mention_of_retired_token_does_not_arm(tmp_path, monkeypatch):
|
||||
"""ersatztv#609's live failure: a message *describing* the marker disarmed the whole body-diff.
|
||||
|
||||
This is the negative control the bare-substring matcher could not pass — under it, `token` went
|
||||
True and all three rewrite comparisons were skipped while CI still reported green.
|
||||
"""
|
||||
_removed, rewritten = _rewrite_repo(
|
||||
tmp_path,
|
||||
monkeypatch,
|
||||
"docs: explain the guard",
|
||||
"No [decisions-edit] marker is needed here, because this commit only moves a record.",
|
||||
)
|
||||
assert _RECORD in rewritten
|
||||
|
||||
|
||||
def test_diff_engine_quoted_trailer_mid_body_does_not_arm(tmp_path, monkeypatch):
|
||||
"""The same class one level up: a commit that QUOTES an armed message as an example.
|
||||
|
||||
Git parses trailers only in the final paragraph, so a `Decisions-Edit: yes` line followed by more
|
||||
prose is documentation, not a marker. Docs commits in this repo routinely carry such examples.
|
||||
"""
|
||||
_removed, rewritten = _rewrite_repo(
|
||||
tmp_path,
|
||||
monkeypatch,
|
||||
"docs: document how to arm the guard",
|
||||
"Contributors write:",
|
||||
"Decisions-Edit: yes",
|
||||
"...as the final paragraph of the message, alongside Co-Authored-By.",
|
||||
)
|
||||
assert _RECORD in rewritten
|
||||
|
||||
|
||||
def test_diff_engine_negative_trailer_value_does_not_arm(tmp_path, monkeypatch):
|
||||
# `Decisions-Edit: no` records a deliberate NON-edit; reading it as consent would recreate #609.
|
||||
_removed, rewritten = _rewrite_repo(tmp_path, monkeypatch, "rewrite rationale", "Decisions-Edit: no")
|
||||
assert _RECORD in rewritten
|
||||
|
||||
|
||||
def test_diff_engine_retired_bracket_token_no_longer_arms(tmp_path, monkeypatch):
|
||||
# the pre-#609 habit (token appended to the subject) is retired, not silently honored.
|
||||
_removed, rewritten = _rewrite_repo(tmp_path, monkeypatch, "rewrite rationale [decisions-edit]")
|
||||
assert _RECORD in rewritten
|
||||
|
||||
|
||||
def test_diff_engine_detects_appended_smuggled_rationale(tmp_path, monkeypatch):
|
||||
repo = tmp_path / "repo"
|
||||
(repo / "docs").mkdir(parents=True)
|
||||
|
||||
Reference in New Issue
Block a user