---
stability: stable
---

# ADR Layout — Per-area Directories

> Status: accepted · 2026-05-16 · Roadmap: `step-11-ruflo-parity` Phase 4

## Scope

Two ADR surfaces coexist in this repo. **Both are canonical** — neither supersedes the other.

| Surface | Path | Use for |
|---|---|---|
| **Flat (legacy)** | `docs/decisions/ADR-NNN-<slug>.md` | Cross-cutting governance decisions: kernel composition, rule taxonomy, package-wide architecture. Numbering is global, sequential, gap-free. |
| **Per-area** | `docs/adrs/<area>/NNNN-<slug>.md` | Sub-area decisions whose blast radius is one plugin / one subsystem. Numbering is per-area, starts at `0001`, padded to 4 digits. |

Choice rule — does the decision constrain code **inside one area folder** (one runtime module, one contract group, one CLI surface)? → per-area. Does it constrain **the package's contract with consumers**? → flat. In doubt → per-area (cheaper to surface, easier to relocate).

## Per-area layout

```
docs/adrs/
  <area>/
    README.md          # one-paragraph area scope + table of all ADRs in this area
    0001-<slug>.md     # first ADR, retrospective or prospective
    0002-<slug>.md
    ...
```

`<area>` is a kebab-case stem matching one of:

- An entry in the canonical area inventory (see [`scripts/audit_adr_coverage.py`](../../src/scripts/audit_adr_coverage.py) `AREAS`).
- A new area added to that inventory in the same PR.

Reserved areas (bootstrap pass — step-11 Phase 4 Step 3):

| Area | Scope | Owner contract |
|---|---|---|
| `cost` | Budget ladder, hard-stop hook, cost reporting | [`cost-enforcement.md`](cost-enforcement.md) |
| `telegraph` | Telegraph-speak condensation, decondensation, reversibility | [`condensation-default-kill-criterion.md`](condensation-default-kill-criterion.md) |
| `schema` | Frontmatter schemas, v2 rigor, lint behaviour | [`schema-versioning.md`](schema-versioning.md) (when published) |
| `router` | `router.json` shape, tier semantics, dispatch precedence | [`rule-router.md`](rule-router.md) |
| `smoke` | Per-tier smoke contracts, baseline locks | [`smoke-contracts.md`](smoke-contracts.md) |
| `memory` | Memory MCP, propose / promote / poison flow | [`agent-memory-contract.md`](agent-memory-contract.md) |

## Frontmatter

Identical across both surfaces:

```yaml
---
adr: NNN              # zero-padded; per-area uses 4-digit (0001), flat uses 3-digit (010)
area: <area> | flat   # 'flat' for docs/decisions/, otherwise the area slug
status: proposed | accepted | superseded | deprecated
date: YYYY-MM-DD
decision: <slug>
supersedes: — | ADR-<area>-NNNN | ADR-MMM
superseded_by: — | ADR-<area>-NNNN | ADR-MMM
phase: <roadmap-stem> · <phase-id>     # optional but recommended
type: retrospective | prospective
---
```

Supersession links cross surfaces: a per-area ADR may supersede a flat ADR and vice versa. The numeric prefix in `supersedes:` makes the target unambiguous (`ADR-007` = flat, `ADR-cost-0001` = per-area).

## Per-area README contract

Every `<area>/` directory carries a `README.md` with:

1. One-paragraph area scope (≤ 4 sentences).
2. Single contract pointer — the `docs/contracts/<X>.md` this area implements (or "no published contract" if pre-Phase 5).
3. Numbered table of ADRs in the area: `| # | Title | Status | Date | Supersedes |`. Generated by `scripts/audit_adr_coverage.py --regen-area-readme <area>`.

## Coverage gate

`scripts/audit_adr_coverage.py --check` (wired to `task lint-adr-coverage`):

- Warns when a `docs/contracts/<X>.md` exists without a matching `docs/adrs/<X>/0001-*.md`.
- Hard-fails on number gaps within an area (e.g. `0001`, `0003` without `0002`).
- Hard-fails on missing `README.md` in any non-empty area directory.
- Warns on dangling `supersedes:` or `superseded_by:` references.

Default mode is **warn** at the consumer surface; **fail** under `task ci`. Rationale: a new contract dropped without an ADR is a documentation gap, not a bug. CI enforces it for this package; consumer projects opt in by adding the task to their own pipeline.

## Numbering & gaps

- Per-area: 4-digit, gap-free, starts at `0001`. Re-use of numbers is a hard failure in the index regenerator.
- Flat: 3-digit, gap-free, starts at `001`. Existing ADRs in `docs/decisions/` set the precedent.
- A deleted ADR is **never** removed from history — supersede it. The lint surfaces broken supersession chains.

## Relationship to `adr-create` skill

[`adr-create`](../../.agent-src.uncondensed/skills/adr-create/SKILL.md) accepts an optional `<area>` argument (added in step-11 Phase 4 Step 4):

- No `<area>` → flat surface, `docs/decisions/`.
- `<area>` matches inventory → per-area surface, `docs/adrs/<area>/`.
- `<area>` does **not** match inventory → skill refuses with a hint to update the inventory first.

The skill's template, numbering logic, and validation hooks are identical for both surfaces; only the target directory and number padding differ.

## References

- [`docs/adrs/cost/0001-hard-stop-hook.md`](../adrs/cost/0001-hard-stop-hook.md) — first per-area ADR (bootstrap).
- [`docs/decisions/INDEX.md`](../decisions/INDEX.md) — flat surface index.
- [`scripts/audit_adr_coverage.py`](../../src/scripts/audit_adr_coverage.py) — coverage gate.
- [`scripts/adr/regenerate_index.py`](../../src/scripts/adr/regenerate_index.py) — index regenerator (works on both surfaces; pass `--dir`).
- `step-11-ruflo-parity` Phase 4 — origin.
