diff --git a/docs/docs-format.md b/docs/docs-format.md new file mode 100644 index 0000000..d43465c --- /dev/null +++ b/docs/docs-format.md @@ -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/.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. +```