boma/docs/superpowers/plans/2026-06-10-adr-structure.md
sjat 6d7d27b03b docs(adr): add Proposed lifecycle state; mark ADR-011 Proposed
Revisits the lifecycle decision on the evidence of ADR-011 (a real draft
with open questions). Adds a fourth state, Proposed (YYYY-MM-DD), to ADR-023,
the template, the adr-structure check (+test), spec and plan. Sets ADR-011's
Status to Proposed and removes its now-redundant inline 'Proposed' line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 14:48:55 +02:00

25 KiB
Raw Permalink Blame History

ADR Structure & Lifecycle Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Codify how boma's ADRs are structured — a canonical section set, an Accepted/Superseded/Deprecated lifecycle, a template, a lightweight enforcement check, and a one-time Status backfill of the back-catalogue.

Architecture: Five independent units. (1) A pure-function adr-structure check added to the existing scripts/repo-scan.py (stdlib only, pytest-tested like its siblings), verifying every numbered ADR has the four mandatory sections and a parseable Status line — presence only, not order. (2) An adr-template.md scaffold. (3) ADR-023 itself, written to pass its own check. (4) Wiring into CLAUDE.md and the /review-repo command doc. (5) A mechanical backfill adding ## Status to ADRs 001018, dated from each file's first git-commit.

Tech Stack: Python 3 stdlib (scripts/repo-scan.py), pytest (.venv/bin/pytest), Markdown, git.

Spec: docs/superpowers/specs/2026-06-10-adr-structure-design.md

Branch: feat/adr-structure (already created; the design spec is the first commit).

Convention reminders (from CLAUDE.md): docs-/script-only commits skip the ansible-lint pre-commit hook and need no rbw unlock. Imperative subject ≤72 chars. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> trailer on every commit.


Decisions locked by the spec (do not re-litigate)

  • Mandatory sections, in this order: ## Status, ## Context, ## Decision, ## Consequences.
  • Optional sections: ## Related, ## Scope, ## Guardrails / ## Enforcement, ## What was ruled out, ## Verified facts (ADR-014).
  • Status lifecycle (4 states): Proposed (YYYY-MM-DD) (genuine drafts, e.g. ADR-011) → Accepted (YYYY-MM-DD) (the common starting state) → optionally Superseded by ADR-NNN (YYYY-MM-DD) or Deprecated (YYYY-MM-DD). (Proposed was added on the evidence of ADR-011, which is a real draft with open questions.)
  • No silent rewrites: material reversal = new ADR + Superseded by marker; bidirectional link.
  • Enforcement checks presence + parseable Status line, NOT section order. Order is demonstrated by the template, not machine-enforced.
  • Back-catalogue is fully restructured (no grandfathering) — ADRs 001018 are brought to all-four-section conformance. The restructure is presentational: relabel/regroup/demote existing headings, add a dated Status, assemble a Consequences section from implications the ADR already states. The substance of no decision is changed. If a faithful Consequences cannot be drawn from existing content, escalate that file rather than inventing one.

Task 1: adr-structure check in repo-scan.py

Files:

  • Modify: scripts/repo-scan.py (add module-level regexes near the other _RE definitions ~line 3844; add adr_structure_findings() next to deferred_findings() ~line 96; wire it into scan() at the findings.extend(...) site ~line 215)

  • Test: tests/test_repo_scan.py (new)

  • Step 1: Write the failing test

Create tests/test_repo_scan.py:

import importlib.util
import pathlib

_PATH = pathlib.Path(__file__).resolve().parent.parent / "scripts" / "repo-scan.py"
_spec = importlib.util.spec_from_file_location("repo_scan", _PATH)
rs = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(rs)

GOOD = [
    "# ADR-099 — Example\n", "\n",
    "## Status\n", "\n", "Accepted (2026-06-10)\n", "\n",
    "## Context\n", "\n", "Why.\n", "\n",
    "## Decision\n", "\n", "What.\n", "\n",
    "## Consequences\n", "\n", "So what.\n",
]


def _checks(findings):
    return [f for f in findings if f["check"] == "adr-structure"]


def test_good_adr_has_no_findings():
    out = rs.adr_structure_findings({"docs/decisions/099-example.md": GOOD})
    assert _checks(out) == []


def test_missing_mandatory_section_is_flagged():
    lines = [ln for ln in GOOD if not ln.startswith("## Consequences")]
    out = _checks(rs.adr_structure_findings({"docs/decisions/099-example.md": lines}))
    assert len(out) == 1
    assert "Consequences" in out[0]["detail"]


def test_unparseable_status_is_flagged():
    lines = [("Designed, not built.\n" if ln == "Accepted (2026-06-10)\n" else ln)
             for ln in GOOD]
    out = _checks(rs.adr_structure_findings({"docs/decisions/099-example.md": lines}))
    assert len(out) == 1
    assert "Status not parseable" in out[0]["detail"]


def test_superseded_status_is_accepted():
    lines = [("Superseded by ADR-100 (2026-06-11)\n" if ln == "Accepted (2026-06-10)\n"
              else ln) for ln in GOOD]
    out = _checks(rs.adr_structure_findings({"docs/decisions/099-example.md": lines}))
    assert out == []


def test_non_numbered_file_is_skipped():
    bare = ["# ADR template\n", "\n", "## Status\n", "\n", "<!-- hint -->\n"]
    out = _checks(rs.adr_structure_findings({"docs/decisions/adr-template.md": bare}))
    assert out == []
  • Step 2: Run the test to verify it fails

Run: .venv/bin/pytest tests/test_repo_scan.py -q Expected: FAIL — AttributeError: module 'repo_scan' has no attribute 'adr_structure_findings'.

  • Step 3: Add the regexes

In scripts/repo-scan.py, after the RESOLVE_WORD_RE = ... line (~line 44), add:

# ADR-structure check (ADR-023): numbered ADRs must carry the four mandatory
# sections and a parseable Status line. Presence only — section ORDER is a
# template-demonstrated convention, not machine-enforced.
ADR_FILE_RE = re.compile(r"^\d{3}-.*\.md$")
ADR_REQUIRED_SECTIONS = ("Status", "Context", "Decision", "Consequences")
ADR_STATUS_LINE_RE = re.compile(
    r"^(Accepted \(\d{4}-\d{2}-\d{2}\)"
    r"|Superseded by ADR-\d{3}"
    r"|Deprecated \(\d{4}-\d{2}-\d{2}\))")
  • Step 4: Add the check function

In scripts/repo-scan.py, immediately after the deferred_findings(...) function (it ends ~line 96, just before def walk_files():), add:

def adr_structure_findings(adr_files):
    """adr_files: {rel_path: [lines]} for docs/decisions/*.md.
    Flags numbered ADRs (NNN-*.md) missing a mandatory section or whose Status
    section has no parseable lifecycle line. Non-numbered files (e.g.
    adr-template.md) are skipped. Section order is NOT checked (ADR-023)."""
    out = []
    for rpath, lines in sorted(adr_files.items()):
        if not ADR_FILE_RE.match(os.path.basename(rpath)):
            continue
        headings = {}
        for i, line in enumerate(lines):
            m = re.match(r"^##\s+(\w+)", line)
            if m:
                headings.setdefault(m.group(1), i)
        missing = [s for s in ADR_REQUIRED_SECTIONS if s not in headings]
        if missing:
            out.append({"check": "adr-structure", "severity": "medium",
                        "path": rpath, "line": 1,
                        "detail": f"missing mandatory section(s): {', '.join(missing)}"})
        if "Status" in headings:
            body = []
            for line in lines[headings["Status"] + 1:]:
                if line.startswith("## "):
                    break
                body.append(line)
            status_text = next((ln.strip() for ln in body if ln.strip()), "")
            if not ADR_STATUS_LINE_RE.match(status_text):
                out.append({"check": "adr-structure", "severity": "medium",
                            "path": rpath, "line": headings["Status"] + 1,
                            "detail": "Status not parseable (want 'Accepted (YYYY-MM-DD)', "
                                      "'Superseded by ADR-NNN', or 'Deprecated (YYYY-MM-DD)'); "
                                      f"got: {status_text[:60]!r}"})
    return out
  • Step 5: Run the test to verify it passes

Run: .venv/bin/pytest tests/test_repo_scan.py -q Expected: PASS — 5 passed.

  • Step 6: Wire the check into scan()

In scripts/repo-scan.py, find (~line 215):

    findings.extend(deferred_findings(adr_files, defer_refs))
    return findings

Replace with:

    findings.extend(deferred_findings(adr_files, defer_refs))
    findings.extend(adr_structure_findings(adr_files))
    return findings
  • Step 7: Confirm the check fires on the real (not-yet-backfilled) repo

Run: python3 scripts/repo-scan.py 2>/dev/null | python3 -c "import json,sys; print(sorted({f['path'] for f in json.load(sys.stdin)['findings'] if f['check']=='adr-structure'}))" Expected: a list including docs/decisions/001-architecture.md … through 018-logging.md (001015 missing Status; 016018 unparseable Status). 019022 and 023 must NOT appear. This proves the check works and previews Task 5's worklist.

  • Step 8: Commit
git add scripts/repo-scan.py tests/test_repo_scan.py
git commit -m "feat(review): add adr-structure check to repo-scan

Flags numbered ADRs missing a mandatory section (Status/Context/Decision/
Consequences) or with an unparseable Status line. Presence only, not order.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"

Task 2: ADR template

Files:

  • Create: docs/decisions/adr-template.md

  • Step 1: Write the template

Create docs/decisions/adr-template.md with exactly:

# ADR-NNN — <Title>: <optional clarifying subtitle>

<!-- Filename: NNN-kebab-title.md (zero-padded, monotonic, never reused).
     Register a row in CLAUDE.md "Further reading" when this ADR is created.
     Sections below in order. Mandatory: Status, Context, Decision, Consequences.
     Delete this comment and any optional section you don't use. -->

## Status

Accepted (YYYY-MM-DD)
<!-- Lifecycle: "Accepted (YYYY-MM-DD)" → later "Superseded by ADR-NNN (YYYY-MM-DD)"
     or "Deprecated (YYYY-MM-DD)" + one-line why. Optional trailing note OK, e.g.
     "Accepted (2026-06-10). Doctrine ADR — pins policy, builds nothing yet." -->

## Context

<!-- The forces, the problem, what exists today, why now. -->

## Decision

<!-- What we are doing. Use numbered sub-decisions (### 1. ...) for multi-part ADRs. -->

## Consequences

<!-- Results, trade-offs explicitly accepted, follow-on work. -->

<!-- Optional sections — uncomment any that genuinely apply; never pad:

## Scope — explicit in / out-of-scope boundaries.

## Guardrails — how the decision is mechanically enforced (lint, CI, hooks).

## What was ruled out — rejected alternatives, each with its reason.

## Verified facts (ADR-014) — verified: <subject> · <tool> <version> · <source> · <YYYY-MM-DD>

## Related — links to other ADRs by number; bidirectional for Supersedes/Superseded-by.
-->

(HTML comments do not nest — optional sections use one flat comment block with inline em-dash descriptions, not commented sub-hints inside an outer comment.)

  • Step 2: Confirm the template is skipped by the check

Run: python3 scripts/repo-scan.py 2>/dev/null | python3 -c "import json,sys; print([f for f in json.load(sys.stdin)['findings'] if f['check']=='adr-structure' and 'adr-template' in f['path']])" Expected: [] (non-numbered filename → skipped).

  • Step 3: Commit
git add docs/decisions/adr-template.md
git commit -m "docs(adr): add adr-template.md scaffold (ADR-023)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"

Task 3: ADR-023 itself

Files:

  • Create: docs/decisions/023-adr-structure.md

  • Step 1: Write ADR-023

Create docs/decisions/023-adr-structure.md. It must pass its own check (Status/Context/Decision/Consequences present; parseable Status line). Use this content:

# ADR-023 — ADR structure & lifecycle

## Status

Accepted (2026-06-10). Meta/doctrine ADR — pins how ADRs are written; the
`adr-structure` check (`scripts/repo-scan.py`) and `docs/decisions/adr-template.md`
ship with it, and ADRs 001018 were retroactively restructured to conform. Resolves
the FRICTION signal (2026-05-31) about ADR-writing policy being unsettled.

## Context

boma records architectural decisions as numbered ADRs in `docs/decisions/`, and
CLAUDE.md treats them as load-bearing. Yet no ADR said how an ADR is written. The
newest ADRs (019022) converged on a clean shape — Status → Context → Decision →
Consequences → Related — but only by imitation. ADRs 001018 predate it and drifted
widely: most lacked a `## Status` section entirely (016018 carried only a trailing
build-state note), and many lacked an explicit `## Decision` or `## Consequences`
heading, their decisions spread across ad-hoc topical sections. The result was
structural drift and no uniform way to tell an active decision from a superseded or
deprecated one.

## Decision

### 1. Title & filename

Title line: `# ADR-NNN — <Title>: <optional clarifying subtitle>` (em-dash). Filename:
`NNN-kebab-title.md`, zero-padded 3-digit, monotonic, never reused — a superseded ADR
keeps its number and file. A new ADR is registered as a row in the CLAUDE.md
"Further reading" table.

### 2. Mandatory sections, in this order

- `## Status` — a lifecycle line, usually `Accepted (YYYY-MM-DD)` (see §4), plus an
  optional one-line note.
- `## Context` — the forces, the problem, what exists today, why now.
- `## Decision` — what we are doing; numbered sub-decisions for multi-part ADRs.
- `## Consequences` — results, trade-offs explicitly accepted, follow-on work.

### 3. Optional sections (use only where they genuinely apply)

`## Related`, `## Scope`, `## Guardrails` / `## Enforcement`, `## What was ruled out`,
`## Verified facts (ADR-014)`.

### 4. Status lifecycle

Four states. Because boma is single-contributor and trunk-based with no review gate,
most ADRs are **born `Accepted (YYYY-MM-DD)`** — committed-to on writing. A
**`Proposed`** state exists for a genuine draft whose core direction is recorded but
whose specifics are still open for discussion (e.g. ADR-011); it is promoted to
`Accepted` once settled.

- **`Proposed (YYYY-MM-DD)`** — drafted, under discussion, not yet committed-to. May
  carry open questions. Promoted to `Accepted (YYYY-MM-DD)` when decided.
- **`Accepted (YYYY-MM-DD)`** — committed-to. The common starting state.
- Replaced → old ADR's Status becomes **`Superseded by ADR-NNN (YYYY-MM-DD)`**; the new
  ADR records `Supersedes ADR-MMM` in its Status and `## Related`. The link is
  **bidirectional**.
- Retired with no replacement → **`Deprecated (YYYY-MM-DD)`** + a one-line reason.

**No silent rewrites.** An Accepted ADR is not edited to reverse its decision. Typo and
clarity fixes are fine; a material reversal requires a new ADR and a `Superseded by`
marker on the old one.

### 5. Template & enforcement

`docs/decisions/adr-template.md` is the scaffold for new ADRs. The `/review-repo`
command's pre-scan (`scripts/repo-scan.py`) emits an `adr-structure` finding for any
numbered ADR missing a mandatory section or with an unparseable Status line. It checks
**presence and Status, not section order** — order is a convention the template carries,
deliberately not gated, to keep enforcement lightweight (consistent with boma's other
doctrine ADRs adding no CI gate).

### 6. Retroactive conformance of the back-catalogue

ADRs 001018 are restructured to satisfy this standard rather than grandfathered. The
restructure is **presentational** — existing headings are relabelled, regrouped, or
demoted under a `## Decision` umbrella; a dated `## Status` is added; a `## Consequences`
section is assembled from implications the ADR already states. **The substance of no
decision is changed.** This keeps the check uniform (no number threshold) and the corpus
a consistent, legible decision history.

## Consequences

- New ADRs have one obvious shape and a scaffold; structural drift stops.
- Every ADR declares its lifecycle state uniformly, and reversals are traceable.
- The whole corpus conforms; the check needs no grandfathering and stays simple.
- One-time restructure churn across ADRs 001018 (heading reorganization + a Status and
  a Consequences section per file; no decision substance changed).
- `/review-repo` grows one deterministic check; no new CI machinery.
- This ADR is the first conformant example and is held to its own check.

## What was ruled out

- **A `make lint` / CI gate for ADR structure** — heavier than the risk warrants;
  the `/review-repo` check and the template suffice.
- **Machine-enforcing section order** — brittle for marginal value; left as a
  template-demonstrated convention.
- **Grandfathering 001018 from the check** — rejected in favour of restructuring the
  whole corpus to conform, so the standard applies uniformly with no exceptions.

## Related

- ADR-014 — knowledge sourcing (the `Verified facts` optional section).
- ADR-019/020/021/022 — the emergent structure this ADR codifies.
- `docs/decisions/adr-template.md` — the scaffold.
- `scripts/repo-scan.py` — the `adr-structure` enforcement check.
  • Step 2: Confirm ADR-023 passes its own check

Run: python3 scripts/repo-scan.py 2>/dev/null | python3 -c "import json,sys; print([f for f in json.load(sys.stdin)['findings'] if f['check']=='adr-structure' and '023-' in f['path']])" Expected: [].

  • Step 3: Commit
git add docs/decisions/023-adr-structure.md
git commit -m "docs(adr): ADR-023 — ADR structure & lifecycle

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"

Task 4: Wire into CLAUDE.md and the review-repo command doc

Files:

  • Modify: CLAUDE.md ("Further reading" table)

  • Modify: .claude/commands/review-repo.md (the deterministic-findings description, ~line 2628)

  • Step 1: Add the CLAUDE.md "Further reading" row

In CLAUDE.md, in the "Further reading" table, after the Backup & disaster recovery row, add:

| ADR structure & lifecycle | `docs/decisions/023-adr-structure.md`     |
  • Step 2: Mention the new check in review-repo.md

In .claude/commands/review-repo.md, find (~line 2728):

(roles, ADRs, runbooks, playbooks, scripts — your shard list) and **exact findings**
(markers, broken refs, unencrypted vaults). Fold these into the report verbatim.

Replace the parenthetical with:

(roles, ADRs, runbooks, playbooks, scripts — your shard list) and **exact findings**
(markers, broken refs, unencrypted vaults, ADR-structure violations). Fold these into
the report verbatim.
  • Step 3: Verify the CLAUDE.md link resolves

Run: test -f docs/decisions/023-adr-structure.md && echo OK Expected: OK.

  • Step 4: Commit
git add CLAUDE.md .claude/commands/review-repo.md
git commit -m "docs(adr): register ADR-023 and note adr-structure check

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"

Task 5: Retroactively restructure ADRs 001018 to full conformance

Goal: every ADR in 001018 ends with all four mandatory sections present and a parseable Status line, so the adr-structure check reports zero findings — without changing the substance of any decision.

Files (current findings — the exact worklist):

  • Missing Status + Consequences: 001-architecture.md, 002-security.md, 004-docker-model.md, 005-bootstrapping.md, 014-knowledge-sourcing.md
  • Missing Status + Decision + Consequences: 006-terraform.md, 007-network.md, 008-testing.md, 009-provisioning-handoff.md, 010-forgejo-ci.md, 011-update-management.md
  • Missing all four: 003-toolchain.md
  • Missing Status + Decision: 013-heritage-v4.md
  • Missing Status only: 012-hardware-capacity.md, 015-control-host.md
  • Have unparseable Status + missing Consequences: 016-mesh-vpn.md, 017-service-ui-verification.md, 018-logging.md

(010/011 use ## Decisions (plural) → relabel to ## Decision. The "missing Decision" cases generally have the decision spread across topical ## headings.)

THE FAITHFULNESS RULE (non-negotiable): This is a presentational restructure. You MAY: add a ## Status section; relabel a heading (## Decisions## Decision); introduce a ## Decision umbrella heading and demote existing topical ## headings to ### beneath it; add a ## Consequences section. You MUST NOT alter any existing sentence of decision prose, reword arguments, or add new policy. A ## Consequences section is assembled only from implications the ADR already states (its trade-offs, "what was ruled out", "open questions", named follow-on work). If an ADR states nothing that can be faithfully cast as a consequence, STOP and report it as DONE_WITH_CONCERNS / escalate — do not invent consequences.

Per-file date source: the file's first git-commit (add) date — git log --diff-filter=A --format=%as -- <path> | tail -1 (yields YYYY-MM-DD).

  • Step 1: Add a dated ## Status section to each ADR

For 001015 (no Status today): insert, between the title line and the first ## heading, a Status section:

## Status

Accepted (<d>)

where <d> is the file's first-git-commit date. For 016/017/018 (unparseable Status today): prepend a parseable Accepted (<d>). clause to the first line of their existing ## Status section so the build-state note becomes its tail, e.g. Accepted (2026-06-05). Designed. **Authorable now:** ....

  • Step 2: Ensure a ## Decision section exists

For ADRs flagged "missing Decision" (003, 006, 007, 008, 009, 010, 011, 013): relabel a plural/synonym heading where one exists (## Decisions## Decision in 010/011), or introduce a ## Decision umbrella immediately after ## Context and demote the existing topical ## body headings (e.g. in 003: "Execution engine", "Python environment", …) to ###. Do not move or rewrite the prose under them.

  • Step 3: Ensure a ## Consequences section exists

For every ADR flagged "missing Consequences" (001, 002, 003, 004, 005, 006, 007, 008, 009, 010, 011, 014, 016, 017, 018): add a ## Consequences section near the end, assembled strictly from implications the ADR already states. Where an ADR has a trailing section that is consequences under another name (e.g. "What was ruled out", "Open questions", "Trade-offs"), you may keep that section and add a short ## Consequences that references/summarizes the already-stated trade-offs — without introducing new claims. Honour the faithfulness rule; escalate any ADR where no faithful Consequences can be drawn.

  • Step 4: Verify the whole corpus passes the check

Run: python3 scripts/repo-scan.py 2>/dev/null | python3 -c "import json,sys; v=[f for f in json.load(sys.stdin)['findings'] if f['check']=='adr-structure']; print('adr-structure findings:', len(v)); [print(' ', f['path'], '—', f['detail']) for f in v]" Expected: adr-structure findings: 0.

  • Step 5: Verify faithfulness via diff

Run: git diff --stat and spot-check git diff docs/decisions/003-toolchain.md. Expected: changes are heading additions/relabels/level-demotions, a new Status section, and a new Consequences section — no edits to existing decision sentences.

  • Step 6: Run the repo-scan test suite

Run: .venv/bin/pytest tests/test_repo_scan.py -q Expected: PASS — 5 passed.

  • Step 7: Commit
git add docs/decisions/0*.md docs/decisions/1*.md
git commit -m "docs(adr): restructure ADRs 001-018 to ADR-023 conformance

Presentational only: add a dated Status section, relabel/regroup headings
under Decision, and add a Consequences section assembled from each ADR's
already-stated implications. No decision substance changed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"

Final verification (after all tasks)

  • Lint: make lint — Expected: passes (docs + a stdlib script touched; ansible content unchanged).
  • Full deterministic scan clean for our check: python3 scripts/repo-scan.py 2>/dev/null | python3 -c "import json,sys; print('adr-structure:', sum(1 for f in json.load(sys.stdin)['findings'] if f['check']=='adr-structure'))"adr-structure: 0.
  • Tests green: .venv/bin/pytest tests/ -q → all pass.
  • Branch ready: invoke superpowers:finishing-a-development-branch to merge feat/adr-structure to main (trunk-based, no PR) and delete the branch.

Self-review notes

  • Spec coverage: §1 title/filename → Task 3 + template; §2 sections → Tasks 2/3 + check; §3 lifecycle → Task 3; §4 cross-refs → Task 3 ## Related; §5 template → Task 2; §6 retroactive restructure → Task 5; §7 enforcement → Task 1 + Task 4. All covered.
  • Order nuance: spec says sections come "in this order"; the check enforces presence + Status only. This is intentional and stated in both the spec's enforcement wording ("the four mandatory sections and a parseable Status line") and ADR-023's Decision §5 / "What was ruled out". Not a gap.
  • Type/name consistency: adr_structure_findings and the "adr-structure" check key are used identically in the function, the scan() wiring, the tests, and both verification one-liners.