docs: add docs format specification
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
37b0516fd2
commit
b4fab41135
1 file changed
+236
@@ -0,0 +1,236 @@
|
|||||||
|
# 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](https://adr.github.io/madr/). The frontmatter, naming and cross-reference rules are specific to this format.
|
||||||
|
|
||||||
|
## 1. Files and directories
|
||||||
|
|
||||||
|
- ADRs live in `docs/adr/`, plans in `docs/plans/`, relative to the repository root. Both are flat: subdirectories are not scanned and are errors.
|
||||||
|
- A missing directory is treated as empty.
|
||||||
|
- `README.md` in 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 `.md` file 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-9` words 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/adr` and `docs/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. Requires `implementation: not-applicable`.
|
||||||
|
- `deprecated`: once accepted, no longer recommended, with no replacement.
|
||||||
|
- `superseded`: replaced by another ADR. Requires non-empty `superseded_by`.
|
||||||
|
|
||||||
|
ADR `implementation`:
|
||||||
|
|
||||||
|
- `not-started`, `in-progress`, `deferred`: delivery state.
|
||||||
|
- `implemented`: requires non-empty `implemented_by`.
|
||||||
|
- `not-applicable`: nothing to deliver (required for `rejected`; 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 in `superseded_by`, and the reverse. Both records exist and are ADRs.
|
||||||
|
- Supersession MUST NOT form a cycle.
|
||||||
|
- The superseding record (A, the one whose `supersedes` lists the other) MUST have status `accepted`, or `superseded` or `deprecated` (it was accepted once and has since moved on). It MUST NOT be `proposed` or `rejected`.
|
||||||
|
- A superseded record keeps its original `date` and filename; only its `status` and `superseded_by` change.
|
||||||
|
|
||||||
|
## 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 `title` exactly. 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`.
|
||||||
|
- `Outcome` is required when `status` is `completed` or `cancelled` and 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 `#fragment` suffix 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 `http` or `https` with a host, for example `https://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; `date` not 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`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
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`:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
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.
|
||||||
|
```
|
||||||
Reference in new issue
Block a user