From d5bf1792964d854778a2ba1030782a790b06091c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 01:41:59 +0900 Subject: [PATCH 1/5] docs(adr): define monotonic proposal reservations --- docs/adr/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/adr/README.md b/docs/adr/README.md index d95e78b5b..ce824f20a 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -13,6 +13,16 @@ Template for new material decisions: `docs/adr/0000-template.md`. The template i A conversation, issue, PR body, design note, or paper summary is not an Accepted decision by itself. Accepted ADRs must match current code/policy or explicitly describe an accepted invariant whose implementation is tracked. +## Number allocation and active reservations + +ADR numbers are monotonic decision identities, not disposable branch-local labels. Before publishing a new numbered ADR, the writer must inventory both protected `docs/adr/` and every still-valid active PR reservation. Looking only at the highest number on protected main is insufficient because Draft and Proposed branches reserve identities before integration. + +When concurrent, unprotected proposals collide on one number, preserve the earliest-created still-valid proposal's reservation and assign later colliders the next free numbers in PR creation order. The free-number search includes protected ADRs and all live reservations. A stacked proposal keeps its reservation until it is protected-integrated or explicitly retired with no surviving decision delta. + +Renumbering is an ordinary-forward repair. Preserve the decision content, tests, citations, review history, and maturity state; update the ADR filename, in-file identity, index, traceability/doctoring/changelog references, tests, and PR authority that encode the number. Do not force-rewrite history, close valid work solely because of a number collision, recycle an identity silently, or mark a branch-only proposal Accepted merely because its number was repaired. + +The protected tree itself must contain at most one material ADR per four-digit number. The repository contract test enforces that local invariant; active-reservation coordination remains a live PR/issue governance responsibility because concurrent proposals do not coexist in one checkout before integration. + ## Decision index | ADR | Status | Decision | From 5c8f1ee2e23e0160652b45df611f8f7fdc1d7631 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 01:42:12 +0900 Subject: [PATCH 2/5] test(adr): reject duplicate protected decision numbers --- tests/test_adr_number_allocation_contract.py | 42 ++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 tests/test_adr_number_allocation_contract.py diff --git a/tests/test_adr_number_allocation_contract.py b/tests/test_adr_number_allocation_contract.py new file mode 100644 index 000000000..d41ebe117 --- /dev/null +++ b/tests/test_adr_number_allocation_contract.py @@ -0,0 +1,42 @@ +"""Contracts for monotonic Architecture Decision Record identities.""" + +from collections import defaultdict +from pathlib import Path +import re + + +ROOT = Path(__file__).resolve().parents[1] +ADR_DIR = ROOT / "docs" / "adr" +ADR_INDEX = ADR_DIR / "README.md" +NUMBERED_ADR_RE = re.compile(r"^(\d{4})-.+\.md$") + + +def test_protected_tree_has_one_material_adr_per_number() -> None: + """A four-digit ADR identity cannot name two material decisions in one tree.""" + by_number: dict[str, list[str]] = defaultdict(list) + for path in ADR_DIR.glob("*.md"): + match = NUMBERED_ADR_RE.fullmatch(path.name) + if match is None or match.group(1) == "0000": + continue + by_number[match.group(1)].append(path.name) + + duplicates = { + number: sorted(paths) + for number, paths in by_number.items() + if len(paths) > 1 + } + assert duplicates == {} + + +def test_adr_index_requires_live_reservation_inventory_before_allocation() -> None: + """Keep the concurrent-proposal reservation rule discoverable and fail-closed.""" + index = ADR_INDEX.read_text(encoding="utf-8") + for requirement in ( + "active PR reservation", + "Looking only at the highest number on protected main is insufficient", + "earliest-created still-valid proposal", + "ordinary-forward repair", + "Do not force-rewrite history", + "branch-only proposal Accepted", + ): + assert requirement in index From 5ad6445a92a03ba3315a23e2b3ed184537c9bfbb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 02:02:45 +0900 Subject: [PATCH 3/5] test(adr): pin collision allocation semantics --- tests/test_adr_number_allocation_contract.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_adr_number_allocation_contract.py b/tests/test_adr_number_allocation_contract.py index d41ebe117..5eef6efe1 100644 --- a/tests/test_adr_number_allocation_contract.py +++ b/tests/test_adr_number_allocation_contract.py @@ -35,6 +35,8 @@ def test_adr_index_requires_live_reservation_inventory_before_allocation() -> No "active PR reservation", "Looking only at the highest number on protected main is insufficient", "earliest-created still-valid proposal", + "later colliders the next free numbers in PR creation order", + "The free-number search includes protected ADRs and all live reservations", "ordinary-forward repair", "Do not force-rewrite history", "branch-only proposal Accepted", From 674af72b76759437dff308c964caa0cddf576282 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 02:04:41 +0900 Subject: [PATCH 4/5] docs(adr): ground reservation policy without overclaiming sources --- docs/adr/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/adr/README.md b/docs/adr/README.md index ce824f20a..cba0ae6b3 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -23,6 +23,17 @@ Renumbering is an ordinary-forward repair. Preserve the decision content, tests, The protected tree itself must contain at most one material ADR per four-digit number. The repository contract test enforces that local invariant; active-reservation coordination remains a live PR/issue governance responsibility because concurrent proposals do not coexist in one checkout before integration. +### Standards basis and local-policy scope + +ISO/IEC/IEEE 42010:2022 requires architecture descriptions to record architecture decisions considered essential to the architecture and recommends that organizations/projects establish a decision-recording and sharing strategy with rationale. Nygard's original ADR proposal uses sequentially numbered records with explicit status and preserves superseded decisions instead of rewriting their history. These sources support keeping stable decision identities, maturity, rationale, and non-destructive lineage. They do **not** prescribe GitHub PR reservation semantics or a collision winner. + +The rule above that preserves the earliest-created still-valid reservation and assigns later colliders the next free numbers in PR creation order is therefore a repository-local concurrency policy. It is chosen because active proposal branches cannot share one filesystem namespace before integration, while deterministic reservation order prevents two valid decisions from silently reusing one identity. Do not describe this allocation rule as mandated by ISO/IEC/IEEE 42010 or by Nygard. + +References: + +- International Organization for Standardization, International Electrotechnical Commission, & Institute of Electrical and Electronics Engineers. (2022). *ISO/IEC/IEEE 42010:2022, Software, systems and enterprise—Architecture description* (2nd ed.). https://www.iso.org/standard/74393.html +- Nygard, M. (2011, November 15). *Documenting architecture decisions*. Cognitect. https://www.cognitect.com/blog/2011/11/15/documenting-architecture-decisions + ## Decision index | ADR | Status | Decision | From 50be41e202269931e091c7e8e0c036998ef64a6e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 12 Sep 2026 02:13:01 +0900 Subject: [PATCH 5/5] test(adr): reserve 0000 for template only --- tests/test_adr_number_allocation_contract.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_adr_number_allocation_contract.py b/tests/test_adr_number_allocation_contract.py index 5eef6efe1..9a7494ac8 100644 --- a/tests/test_adr_number_allocation_contract.py +++ b/tests/test_adr_number_allocation_contract.py @@ -8,6 +8,7 @@ ROOT = Path(__file__).resolve().parents[1] ADR_DIR = ROOT / "docs" / "adr" ADR_INDEX = ADR_DIR / "README.md" +ADR_TEMPLATE = "0000-template.md" NUMBERED_ADR_RE = re.compile(r"^(\d{4})-.+\.md$") @@ -15,11 +16,14 @@ def test_protected_tree_has_one_material_adr_per_number() -> None: """A four-digit ADR identity cannot name two material decisions in one tree.""" by_number: dict[str, list[str]] = defaultdict(list) for path in ADR_DIR.glob("*.md"): + if path.name == ADR_TEMPLATE: + continue match = NUMBERED_ADR_RE.fullmatch(path.name) - if match is None or match.group(1) == "0000": + if match is None: continue by_number[match.group(1)].append(path.name) + assert "0000" not in by_number, "ADR 0000 is reserved for the non-live template" duplicates = { number: sorted(paths) for number, paths in by_number.items()