10 KiB
Docs format
This is the complete specification for architecture decision records (ADRs) and implementation plans checked by docs check. It is standalone: a file that follows this document is valid without running the tool. The words MUST, MUST NOT and MAY are used in their RFC 2119 sense.
The ADR body follows the core sections of MADR 4. The frontmatter, naming and cross-reference rules are specific to this format.
1. Files and directories
- ADRs live in
docs/adr/, plans indocs/plans/, relative to the repository root. Both are flat: subdirectories are not scanned and are errors. - A missing directory is treated as empty.
README.mdin either directory is skipped. Every other entry MUST be a regular file whose name matches section 2 and ends in.md. Any other file, directory or name is an error. There are no other exceptions: no templates and no companion files; every.mdfile is a record.- Files are UTF-8 without a byte order mark. LF and CRLF line endings are both accepted.
2. Names and IDs
A record file is named YYYYMMDDTHHMMSSmmmZ-slug.md, for example 20261023T204512345Z-use-example-queue.md.
- The timestamp is UTC to the millisecond and MUST be a real date and time.
- The slug is lowercase
a-z0-9words joined by single hyphens: no leading, trailing or doubled hyphen, 1 to 80 characters. - The filename without
.md(the stem) is the record's permanent ID. Never rename a record, even to fix its slug; change the title instead. - IDs MUST be unique across
docs/adranddocs/plans(compared by the timestamp-and-slug stem). - Tools create files exclusively (
O_EXCL). If a file with the same millisecond exists, the creator advances by 1 ms until it is free.
3. Frontmatter
Every record starts with a YAML frontmatter block: the first line is exactly ---, and the block ends at the next line that is exactly ---. The block MUST be a mapping containing exactly the keys listed for the record type, no more, no fewer, each once. Key order is free. Empty lists are written []. A key with no value (null) is an error.
3.1 ADR keys
| Key | Type | Rule |
|---|---|---|
title |
string | Non-empty, single line. |
date |
YYYY-MM-DD |
Valid date. Equals the date in the filename (UTC). |
status |
enum | proposed, accepted, rejected, deprecated, superseded. |
implementation |
enum | not-started, in-progress, implemented, deferred, not-applicable. |
summary |
string | Non-empty, single line. |
tags |
list of strings | Non-empty list. Each tag is lowercase a-z0-9 words joined by single hyphens; no duplicates. |
supersedes |
list of references | May be empty. |
superseded_by |
list of references | May be empty. |
related |
list of references | May be empty. |
tracking |
list of references | May be empty. |
implemented_by |
list of references | May be empty. |
3.2 Plan keys
| Key | Type | Rule |
|---|---|---|
status |
enum | draft, active, completed, cancelled. |
tracking |
list of references | May be empty unless active. |
adrs |
list of references | May be empty. |
completion |
list of references | May be empty unless completed or cancelled. |
A plan has no title key; its title is its H1.
3.3 References in frontmatter
Every list item is a string. No list may contain duplicates. Which kinds of reference each key accepts:
| Key | Accepts |
|---|---|
supersedes, superseded_by, plan adrs |
Repo-root paths to ADRs (/docs/adr/<id>.md). |
related |
Repo-root paths to any record (/docs/adr/... or /docs/plans/...). |
tracking (ADR and plan) |
http or https URLs. |
implemented_by, completion |
http or https URLs (pull request, commit, release), or repo-root paths to existing files. |
See section 7 for what a valid path or URL is. A record MUST NOT reference itself.
Metadata values MUST be durable: a bare issue number such as #123 and the placeholders "this PR", "this pull request" or "this commit" (any case) are rejected. Use a full URL.
4. Status
status is the decision; implementation is delivery of that decision. They are independent except where stated.
ADR status:
proposed: under discussion; not binding.accepted: the decision stands. Accepting is the owner's act.rejected: considered and declined. Requiresimplementation: not-applicable.deprecated: once accepted, no longer recommended, with no replacement.superseded: replaced by another ADR. Requires non-emptysuperseded_by.
ADR implementation:
not-started,in-progress,deferred: delivery state.implemented: requires non-emptyimplemented_by.not-applicable: nothing to deliver (required forrejected; allowed for any other status).
Per-status requirements, summarized:
| Condition | Requirement |
|---|---|
status: superseded |
superseded_by non-empty. |
superseded_by non-empty |
status: superseded. |
status: rejected |
implementation: not-applicable. |
implementation: implemented |
implemented_by non-empty. |
Plan status:
| Status | Meaning | tracking |
completion |
|---|---|---|---|
draft |
Being written. | any | MUST be empty |
active |
Work under way. | MUST be non-empty | MUST be empty |
completed |
All steps done. | any | MUST be non-empty |
cancelled |
Abandoned. | any | MUST be non-empty |
5. Supersession
- If ADR A lists B in
supersedes, then B MUST list A insuperseded_by, and the reverse. Both records exist and are ADRs. - Supersession MUST NOT form a cycle.
- The superseding record (A, the one whose
supersedeslists the other) MUST have statusaccepted, orsupersededordeprecated(it was accepted once and has since moved on). It MUST NOT beproposedorrejected. - A superseded record keeps its original
dateand filename; only itsstatusandsuperseded_bychange.
6. Body
The body is Markdown (CommonMark) after the frontmatter. Headings inside fenced code blocks or HTML comments do not count. Required section headings are matched case-sensitively and exactly, at level 2 (##). A required section MUST appear exactly once and contain non-empty prose: at least one paragraph, list item or table cell of text. HTML comments and code (fenced, indented or inline) do not count as prose. Extra sections are allowed anywhere.
6.1 ADR
- The first heading is an H1 whose text equals
titleexactly. There is one H1. - Required H2 sections:
Context and Problem Statement,Considered Options,Decision Outcome,Consequences. - Optional MADR sections (for example
Decision Drivers,Pros and Cons of the Options,More Information) MAY be added.
6.2 Plan
- The first heading is a non-empty H1: the plan title. There is one H1.
- Required H2 sections:
Scope,Steps,Validation. Outcomeis required whenstatusiscompletedorcancelledand MUST NOT appear otherwise.
6.3 Placeholders
A record is invalid if it still contains unfinished template text: {{ or }} anywhere outside code, or the whole words TODO or TBD in prose outside code.
7. References and links
A reference is either a repo-root path or a URL. This applies to frontmatter reference lists and to every Markdown link and image destination in the body (inline, reference-style and autolinks; bare URLs in text are not checked).
Repo-root path:
- Starts with a single
/and is relative to the repository root, for example/docs/adr/20261023T204512345Z-use-example-queue.md. - The target MUST exist as a regular file inside the repository. Symlinks that resolve outside the repository are rejected.
- In body links only, a
#fragmentsuffix is allowed; the fragment is not verified. In frontmatter, a fragment is an error. - Rejected: relative paths (
foo.md,./foo.md), any..segment, backslashes, and machine paths (/Users/...,/home/...,C:\...,file:URLs).
URL:
- Scheme
httporhttpswith a host, for examplehttps://example.com/org/repo/pull/42. Credentials in the URL (user:pass@) are rejected. URLs are not fetched.
Always allowed in body prose only: in-page anchors (#section) and mailto: links. Any other scheme is rejected.
8. What docs check reports
docs check [--root DIR] checks every record under DIR (default: the working directory) and prints one line per problem:
path:line: message
path is relative to the root using / separators. line is the best location of the problem (the key, heading or link), or 1 for file-level problems. Output is sorted by path, then line, then message. The exit status is 1 if any problem is reported, 2 for usage errors, and 0 otherwise, in which case a one-line summary of the records checked is printed.
It reports violations of everything above:
- unexpected files, directories or names; bad timestamps or slugs; duplicate IDs;
- missing, malformed or unterminated frontmatter; unknown, missing, duplicate or null keys; wrong types or values;
datenot matching the filename; - status/implementation/field combinations from section 4;
- supersession problems from section 5, and self-references;
- missing, repeated or empty required sections; H1 problems; placeholders;
- invalid, unresolvable or non-durable references and links.
It does not judge whether a decision is good, whether a record should have been written, or whether a link's fragment exists.
9. Minimal valid examples
docs/adr/20261023T204512345Z-use-example-queue.md:
---
title: Use Example Queue for background jobs
date: 2026-10-23
status: proposed
implementation: not-started
summary: Run background jobs on Example Queue.
tags:
- jobs
supersedes: []
superseded_by: []
related: []
tracking: []
implemented_by: []
---
# Use Example Queue for background jobs
## Context and Problem Statement
Jobs currently run inline and block requests.
## Considered Options
* Example Queue
* Keep running jobs inline
## Decision Outcome
Chosen option: Example Queue, because it decouples jobs from requests.
## Consequences
* We operate one more service.
docs/plans/20261023T210000000Z-adopt-example-queue.md:
---
status: draft
tracking: []
adrs:
- /docs/adr/20261023T204512345Z-use-example-queue.md
completion: []
---
# Adopt Example Queue
## Scope
Move the email job to Example Queue.
## Steps
1. Add the queue client.
2. Move the email job.
## Validation
The email job runs from the queue in staging.