From f2eb81489ba6c84031ae40fca23c5183167acc85 Mon Sep 17 00:00:00 2001 From: Aaron Bockelie Date: Sun, 27 Sep 2026 11:11:33 -0500 Subject: [PATCH] fix(adr): a record freezes at the merge that lands it, not its branch's first accepted commit The frozen check walked every parent of the default branch, so a record accepted on a review branch and revised there froze at the pre-review commit. Main has carried two false errors on ADR-179 and ADR-186 since #581 and #583 merged. The walk is now --first-parent (with -m for older git). make test-adr lints the repo's own records, so this class of break fails CI. --- Makefile | 1 + hooks/ways/documentation/adr/adr-tool | 5 ++++- .../adr/src/rules_v1_integrity.py | 5 ++++- tests/adr-golden-test.sh | 14 +++++++++++++ .../golden/v1-frozen-merged-branch-edited.out | 20 +++++++++++++++++++ .../adr/golden/v1-frozen-merged-branch.out | 13 ++++++++++++ 6 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/adr/golden/v1-frozen-merged-branch-edited.out create mode 100644 tests/fixtures/adr/golden/v1-frozen-merged-branch.out diff --git a/Makefile b/Makefile index 5040c169..8786fcdc 100644 --- a/Makefile +++ b/Makefile @@ -372,6 +372,7 @@ test-adr: @bash tests/adr-archive-test.sh @bash tests/adr-golden-test.sh @bash tests/adr-macro-test.sh + @docs/scripts/adr lint --check >/dev/null || { docs/scripts/adr lint; exit 1; } @echo "adr tool tests passed." test-unit: diff --git a/hooks/ways/documentation/adr/adr-tool b/hooks/ways/documentation/adr/adr-tool index 4551934d..7caf07ba 100755 --- a/hooks/ways/documentation/adr/adr-tool +++ b/hooks/ways/documentation/adr/adr-tool @@ -1397,10 +1397,13 @@ def _frozen_snapshot(adr) -> Optional[tuple]: # A record freezes where it lands: history is read from the default # branch when there is one, so a decision still in review on a feature # branch can be revised. Without a remote default, HEAD's history counts. + # --first-parent keeps to the branch's own line, so a pull request merged + # with a merge commit lands at the merge, not at the first commit on its + # branch; -m lists the merge's files against that parent on older git. ref = (_git(['rev-parse', '--abbrev-ref', 'origin/HEAD'], root) or '').strip() or 'HEAD' # --reverse drops pre-rename history under --follow, so read newest first # and reverse here. -z keeps names with spaces or non-ASCII intact. - log = _git(['log', ref, '--follow', '-z', '--format=commit:%H', '--name-only', '--', str(rel)], root) + log = _git(['log', ref, '--first-parent', '-m', '--follow', '-z', '--format=commit:%H', '--name-only', '--', str(rel)], root) if not log: return None entries, commit = [], None diff --git a/hooks/ways/documentation/adr/src/rules_v1_integrity.py b/hooks/ways/documentation/adr/src/rules_v1_integrity.py index 4f7d0e24..5f868e81 100644 --- a/hooks/ways/documentation/adr/src/rules_v1_integrity.py +++ b/hooks/ways/documentation/adr/src/rules_v1_integrity.py @@ -147,10 +147,13 @@ def _frozen_snapshot(adr) -> Optional[tuple]: # A record freezes where it lands: history is read from the default # branch when there is one, so a decision still in review on a feature # branch can be revised. Without a remote default, HEAD's history counts. + # --first-parent keeps to the branch's own line, so a pull request merged + # with a merge commit lands at the merge, not at the first commit on its + # branch; -m lists the merge's files against that parent on older git. ref = (_git(['rev-parse', '--abbrev-ref', 'origin/HEAD'], root) or '').strip() or 'HEAD' # --reverse drops pre-rename history under --follow, so read newest first # and reverse here. -z keeps names with spaces or non-ASCII intact. - log = _git(['log', ref, '--follow', '-z', '--format=commit:%H', '--name-only', '--', str(rel)], root) + log = _git(['log', ref, '--first-parent', '-m', '--follow', '-z', '--format=commit:%H', '--name-only', '--', str(rel)], root) if not log: return None entries, commit = [], None diff --git a/tests/adr-golden-test.sh b/tests/adr-golden-test.sh index 1b525f65..7c1aa220 100755 --- a/tests/adr-golden-test.sh +++ b/tests/adr-golden-test.sh @@ -307,6 +307,20 @@ fresh v1 edit docs/architecture/system/ADR-110-old-v0-record.md "s.replace('status: Accepted\n', 'contract: adr/v1\nkind: decision\nverb: add\ncapability: ingest\nstatus: accepted\nagent: {name: Claude, model: fixture-model}\nbasis:\n - evidence: migrated from v0\n').replace('# ADR-110: An unmigrated v0 record\n', '# ADR-110: An unmigrated v0 record\n\n## Summary\n\n- **Probes:** *Confident:* a. *Not confident:* b.\n- **Inversion:** c.\n')" capture v1-frozen-migrated lint docs/architecture/system/ADR-110-old-v0-record.md +# A record accepted on a review branch and revised there freezes where it +# merges, not at the branch's first accepted commit; editing it after the +# merge still fails. +fresh v1 +(cd "$WORK/repo" && git switch -q -c review) +edit docs/architecture/system/ADR-113-operator-proposed.md "s.replace('status: proposed\n', 'status: accepted\nconsidered: [{operator: developer, said: \"yes\", via: PR 13}]\n')" +commit_all accept +edit docs/architecture/system/ADR-113-operator-proposed.md "s.replace('basis:\n', 'basis:\n - evidence: a review fix\n', 1)" +commit_all "review fix" +(cd "$WORK/repo" && git switch -q - && git merge -q --no-ff -m "merge review" review) +capture v1-frozen-merged-branch lint docs/architecture/system/ADR-113-operator-proposed.md +edit docs/architecture/system/ADR-113-operator-proposed.md "s.replace(' - evidence: a review fix\n', '')" +capture v1-frozen-merged-branch-edited lint docs/architecture/system/ADR-113-operator-proposed.md + # A non-UTF-8 blob in a record's history does not stop lint. fresh v1 printf 'binary \377\376 junk\n' > "$WORK/repo/docs/architecture/system/ADR-102-ingest-spec.md" diff --git a/tests/fixtures/adr/golden/v1-frozen-merged-branch-edited.out b/tests/fixtures/adr/golden/v1-frozen-merged-branch-edited.out new file mode 100644 index 00000000..96f5cc98 --- /dev/null +++ b/tests/fixtures/adr/golden/v1-frozen-merged-branch-edited.out @@ -0,0 +1,20 @@ + +Scanned: 1 ADRs + +Status distribution: + accepted: 1 + +Contract: adr/v1 (1 v0 records remain) + +──────────────────────────────────────────────────────────── +Issues found in 1 files: +──────────────────────────────────────────────────────────── + +docs/architecture/system/ADR-113-operator-proposed.md + ❌ 'basis' changed after the decision left proposed; only concern, considered, enacted, status, superseded_by may change + +════════════════════════════════════════════════════════════ +Summary: 1 errors, 0 warnings +════════════════════════════════════════════════════════════ + +[exit 0] diff --git a/tests/fixtures/adr/golden/v1-frozen-merged-branch.out b/tests/fixtures/adr/golden/v1-frozen-merged-branch.out new file mode 100644 index 00000000..a2068ef3 --- /dev/null +++ b/tests/fixtures/adr/golden/v1-frozen-merged-branch.out @@ -0,0 +1,13 @@ + +Scanned: 1 ADRs + +Status distribution: + accepted: 1 + +Contract: adr/v1 (1 v0 records remain) + +════════════════════════════════════════════════════════════ +Summary: 0 errors, 0 warnings +════════════════════════════════════════════════════════════ + +[exit 0]