adr_gate · git:20260915.459047e · 2026-09-15 · sha256 52a2e1c66f6ae053
adr_gate git:20260915.459047eA
Immutable. This exact content is served forever at /api/v1/blob/52a2e1c66f6ae053.
---
name: adr_gate
description: ADR Gate — evaluates after non-trivial tasks whether an ADR is warranted; writes to Corvin-ADR repo when yes, names the skip reason when no, requires three-level analysis (conceptual/structural/implementation), emits ADR-0264 decision-graph frontmatter (depends_on/related/paths/docs — the three-layer Code/Docs/ADR graph) so scripts/adr_graph.py can traverse it, and generates E2E tests for every new structural invariant so LDD has a loss signal.
---
# ADR Gate — Architectural Decision Record discipline
Apply this gate once at the END of any non-trivial implementation task, immediately before declaring "done." Most tasks will not produce an ADR — that is the correct and expected outcome.
## The bar is HIGH
The default answer is: **no ADR needed.** An ADR is only warranted when a future contributor would be genuinely confused about *why* the system works a certain way — and that confusion would cost them real time or lead them to inadvertently break a structural invariant.
**Do NOT write an ADR for:**
- Bug fixes with no design decision (one obvious fix, no real alternative)
- Pure refactors where behaviour is unchanged
- Config value or threshold changes
- Test additions or fixes
- Documentation, comment, or copy changes
- UI tweaks, renaming, small cleanups
- Anything trivially reversible without data migration or cross-repo coordination
## Write an ADR only when BOTH conditions hold
**Condition A — A real design choice was made:**
You chose A over B, and that choice non-trivially constrains future code. There was a genuine alternative.
**Condition B — At least one structural trigger applies:**
- New or changed protocol, wire format, audit schema field, MCP tool contract, or CLI flag shape
- New security or compliance mechanism (anything touching L10, L16, L34, L35, GDPR, EU AI Act)
- Irreversible or hard-to-reverse default: fail-open/closed, deny/allow, opt-in/opt-out
- Decision binds two or more repos (e.g. Corvin + Adapter + Cloud)
- New layer-level mechanism, plugin contract, or cross-layer invariant
**Gut-check:** *"Would a capable new contributor, six months from now, spend hours puzzling over why this works this way — or worse, accidentally undo it?"*
- Likely yes: write the ADR
- Probably not: skip it
## Three-level analysis (mandatory)
Before deciding whether to write an ADR — and when writing one — think through all three levels explicitly. A decision described at only one level is an incomplete record; the missing levels will drift independently until someone breaks something.
**Conceptual level** — What is the core principle or idea being decided? What mental model does this create for future contributors? What rule of thumb or invariant follows from this choice that a reader should carry in their head?
**Structural level** — How does this decision constrain the architecture? Which layers, interfaces, protocols, contracts, or cross-repo boundaries does it shape? What cannot be changed without revisiting this decision?
**Implementation level** — What specific code, config, runtime behaviour, file layout, or schema shape is locked in? What does a developer need to know concretely to implement something that respects this decision?
All three levels must appear explicitly in the ADR's **Context** and **Decision** sections. If one level has nothing to say (rare), name that explicitly — e.g. *"Implementation level: no specific shape is locked in beyond the structural constraint above."*
## How to write the ADR
**Step 1** — Find the next number:
```bash
ls ../Corvin-ADR/decisions/ | grep -E '^[0-9]{4}' | sort | tail -1
```
Add 1, zero-pad to 4 digits (e.g. `0068`).
**Step 2** — Write to `../Corvin-ADR/decisions/XXXX-short-kebab-title.md`. Every ADR
carries **both** the human-facing prose header **and** a machine-readable frontmatter
block (ADR-0264 — "ADR Decision Graph"): the frontmatter is what makes an ADR a node a
coding agent can traverse to instead of a document it must have already read.
```markdown
---
id: ADR-XXXX
status: proposed # proposed | accepted | superseded | frozen
supersedes: [] # ADR ids this one fully replaces
depends_on: [] # structural prerequisites — read these first
related: [] # associative, non-blocking cross-references
commits: [] # git SHAs implementing this, filled in at merge
paths: # CODE globs this ADR structurally constrains
- "path/or/glob/**"
docs: # DOCUMENTATION globs describing this decision
- "docs/claude-ref/some-file.md"
---
# ADR-XXXX — [Title]
**Status:** Accepted
**Date:** YYYY-MM-DD
**Deciders:** shumway
## Context
[What situation, constraint, or requirement forced a decision here?]
## Decision
[What was decided. Direct statement.]
## Consequences
### Positive
-
### Negative / Trade-offs
-
### Alternatives considered
[What else was evaluated and why it was rejected.]
```
Do NOT hand-fill `superseded_by` — leave it absent. It is derived automatically (by
`scripts/adr_graph.py`, see ADR-0264) from every OTHER ADR's `supersedes` field, so the
node being superseded never needs editing after the fact.
Populate `depends_on`/`related` with real ADR ids only when a real relationship exists;
an empty list is the correct default for a freestanding decision. `paths` and `docs` are
two SEPARATE surfaces, not one merged list: `paths` is the code this decision constrains
(matched by `scripts/adr_graph.py adrs_for_path()`), `docs` is the documentation that
explains it to a reader — exactly the artifacts the `docs-as-definition-of-done` skill
keeps in sync (matched by `adrs_for_doc()`). An empty or inaccurate list in either makes
this ADR invisible to that specific traversal, not merely undocumented.
**Step 3** — Commit in the Corvin-ADR repo:
```bash
cd ../Corvin-ADR
git add decisions/XXXX-short-kebab-title.md
git commit -m "adr: add ADR-XXXX — [title]"
```
**Step 4** — If the ADR documents a load-bearing mechanism in Corvin, add a one-line reference at the end of the relevant CLAUDE.md section:
```
→ ADR: `Corvin-ADR: decisions/XXXX-short-title.md`
```
This prose pointer stays — it is what a human skimming CLAUDE.md sees — but it is no
longer the only way to find this ADR from the code. `paths:` in Step 2's frontmatter is
the machine-readable equivalent: run `python3 scripts/adr_graph.py <path>` from a
CorvinOS checkout to confirm the new ADR is actually discoverable from the code it
constrains before moving on. If the ADR also has a documented surface (a
`docs/claude-ref/*.md` page, a CLAUDE.md section) beyond this one pointer line, add it to
`docs:` too and check it with `--doc <path>` — this is the same query
`docs-as-definition-of-done` runs before editing a doc, so an ADR with an accurate `docs:`
list gets found from that direction as well, not just from code.
A generated (not hand-authored) ADR — e.g. Plugin-Builder's per-plugin classification
ADR (`core/plugins/plugin_builder/generators/adr.py`) — carries this same frontmatter
schema too, with a plugin-scoped `id` (`{plugin_id}-ADR-0001`, never a bare `ADR-NNNN`,
which is reserved for this repo's own sequence) and `related` pointing into the real
Corvin-ADR corpus. Any new document-generator that produces an ADR-shaped artifact
should follow this same convention rather than emitting unstructured prose.
**Step 5 — E2E tests for the ADR's mechanism (LDD loss signal)**
Immediately after writing the ADR, ask: *"Can I write one or more E2E tests that would FAIL if this mechanism broke or was accidentally reverted?"*
If yes, write the test(s) — this is the LDD loss signal for the new invariant. Rules:
- **Real subprocess, real filesystem, real bwrap** — no mocks for anything touching security, forge, policy, or audit (per `per_subtask_e2e` layer). Mocks are only acceptable for pure protocol-shape tests where the real runtime is unavailable.
- **Name to the ADR:** prefix the test file with the ADR number, e.g. `test_adr_0068_egress_lockdown.py`. This makes regressions immediately traceable.
- **One test per structural invariant** stated in the ADR's "Must NOT do" list. Each invariant is a distinct test case.
- **Register in the suite:** add the new file to `operator/bridges/run-all-tests.sh` (or the relevant sub-suite) so it runs on every `git push`.
- **Commit together** with the implementation, not as a follow-up — a structural decision without a sensor is an untested invariant.
If an E2E test is genuinely infeasible (external system unavailable at test time, hardware dependency, etc.), name the reason explicitly — e.g. *"No E2E test — requires live TSA endpoint; covered by contract test in `test_adr_0073_tsa_contract.py`."* Never skip silently.
**Why this matters for LDD:** The outer loop of LDD (`loop-driven-engineering`) drives refinement via measurable loss. An ADR that ships without a test provides no loss signal — drift goes undetected until a human notices. E2E tests convert structural guarantees into machine-checkable facts.
## When you skip the ADR
Write one sentence — e.g. *"No ADR — bug fix, no design decision."*
Do not leave the gate result implicit. A named skip is as valid as a written ADR.
If the ADR is skipped but the task introduced observable new behavior, still evaluate Step 5 independently: E2E tests are warranted for significant behavior changes even without a formal ADR.