## File format

```yaml
---
id: adr-3f9a2b1c7d4e            # `random` id_style; `sequential` mints adr-0007
title: Auth strategy
description: How the service authenticates API clients and refreshes tokens.
type: decision
status: accepted
tags: [auth, api]
related:                          # typed edges: bare id = relates_to
  - adr-0003
  - to: adr-0001
    kind: supersedes
created: 2026-06-15
updated: 2026-06-30
owner: platform-team             # optional: staleness steward
verified: 2026-06-30             # optional: last re-confirmation
code:                            # optional: the code this document governs
  - src/auth/**
  - tests/test_auth.py
---
```
Body: standard markdown, human-readable, diffs cleanly in git.

## Frontmatter fields

The frontmatter every document carries, grouped by what it answers. Each
field names the command that sets it; none is written by hand.

### Identity fields

| Field | Required | Set by | Description |
|---|---|---|---|
| `id` | yes | `docir add` (auto-generated) | `<type-prefix>-<suffix>`, never chosen manually. The suffix depends on the type's `id_style`: `random` (`adr-3f9a2b1c7d4e`) is what `docir init` writes by default, because two branches of one repo each have their own index and would otherwise both mint `adr-0007`; `sequential` (`adr-0007`) is opt-in via `docir init --id-style sequential` for readable numbers within a single store. `--id` adopts an existing id, for migrating a corpus whose numbers are already cited |
| `title` | yes | `docir add`, `docir update --set-title` | Canonical document title. Frontmatter-only source of truth; the CLI never enforces or generates a body heading from it |
| `description` | yes | `docir add`, `docir update --set-description` | One- or two-sentence summary of the document, written by the agent at creation and kept current on meaningful edits. Feeds search quality — indexed in FTS and included in the embedded text — and shown in `docir query`/`docir context` result listings so the agent can judge relevance without fetching the full body |

### Classification fields

| Field | Required | Set by | Description |
|---|---|---|---|
| `type` | yes | `docir add` (fixed at creation) | Document type (`decision`, `issue`, `architecture`, ...); selects the grammar that applies. That grammar is **not** only `docs-schema.yaml`: the frozen core and the named profiles are merged in from the installed package on every command, so an upgrade can change a type's rules with nothing in `git diff` — see `arch-ad342aae8293`, "Schema drift and the index build stamp" |
| `status` | yes | `docir add` (default), `docir update --status` | Type-specific enum (e.g. `decision`: proposed/accepted/rejected/superseded; `issue`: open/resolved). Transitions are validated against `docs-schema.yaml` |
| `tags` | no | `docir add --tags`, `docir update --set-tags` | List of tag keys for `docir query --tag` filtering. Each key must exist in the tag registry (Tier 0 validation) — free-form tags are rejected, preventing synonym sprawl |

### Link fields

| Field | Required | Set by | Description |
|---|---|---|---|
| `related` | no | `docir add --related`, `docir update` | List of **typed edges** to other documents (`<id>` = default `relates_to`, or `{to, kind}`); forms the relation graph used for traversal and Tier 1 graph checks. Kinds come from the schema's `relation_types` registry (unknown kind = Tier 0 error); a type may whitelist kinds/targets via `allowed_relations` |
| `code` | no | `docir add --code`, `docir update --set-code` | Repo-relative globs naming the code this document governs, so a later session can ask `docir query --code <path>` which decisions concern the files it is about to change. Only the *shape* is validated on write — absolute paths, `..` segments, backslash separators and empty entries are refused, but a pattern matching nothing today is accepted, because a decision is routinely written before the code it decides. `docir check` reports `unmatched-code` once a pattern stops matching, and only when the store sits in a repository. The index returns them sorted; the file keeps the author's order |

### Lifecycle fields

| Field | Required | Set by | Description |
|---|---|---|---|
| `created` | yes | `docir add` (auto) | Set once, never modified afterward; used for audit/sort queries |
| `updated` | yes | `docir add` / `update` / `archive` / `unarchive` | Stamped whenever one of those calls actually changes something. Deliberately **not** advanced by the mechanical rewrites — `check --fix`, the unlinking half of `delete --force`, and `tag rename` / `tag rm --force` — because staleness falls back to `updated` when there is no `verified`, so a mechanical bump would launder the review clock. `TagService` has no `Clock` for exactly this reason |
| `owner` | no | `docir add --owner`, `docir update --set-owner` | Optional steward, surfaced by the staleness check; written only when set |
| `verified` | no | `docir update --verified` | Optional date somebody last re-confirmed the doc is still correct; resets the staleness clock (staleness measures from `verified`, else `updated`) |
| `verified_code` | no | `docir update --verified` | Written alongside `verified` when the store sits in a repository: one digest per `code` glob, of the files that glob matched at that moment. `docir check` recomputes them and reports `code-changed` when they differ. Keyed by pattern, so reordering the globs cannot re-point a digest; a pattern with no entry is *unverified*, never *unchanged*. In the file rather than the index because it is the document's review state and a clone has to see it (adr-d9e6d5ccd0b4) |
| `archived` | no | `docir archive` / `docir unarchive` | Absent by default; `true` removes the document from active search (FTS, embeddings) while keeping the file and index rows |

### Field notes

`created` is set once by `docir add` and never modified afterward. `updated`
is refreshed by the CLI on every `docir update` call (metadata or body). The
distinction matters for Tier 1 checks (e.g. a recently created orphan doc
vs. a long-standing one are different signals) and for audit queries like
"decisions made last quarter", which should sort on `created` rather than
`updated`.
`title` is stored only in frontmatter — it is the canonical source used by
the index for listings, `docir query`, and `docir context` results. The CLI
does not enforce or auto-generate any heading in the body; the agent
decides what (if anything) to write there, including whether to repeat the
title as an `# H1`.
`archived` is an optional frontmatter field, absent by default and set to
`true` only by `docir archive` (removed again by `docir unarchive`) — see
`arch-0368cc754c15`, "Archiving vs. deletion" below.

## Tag registry

Tags are not free-form strings — they are registered entities, each with a
unique key and a description. The registry is the source of truth for what
tags exist, versioned in git like everything else (a `docs/tags.yaml`
mapping key → description; promotable to a full tag doc-type later if tags
need their own relations/history).

```yaml

# docs/tags.yaml
auth:    "Authentication, authorization, tokens, sessions."
api:     "Public/internal HTTP API surface and versioning."
storage: "Persistence, database schema, migrations."
```

### Referential integrity (Tier 0)

every key in a document's `tags`
must exist in the registry. An unknown tag is a hard error at
`docir add`/`docir update` time — "unknown tag, register it first" — the
same guarantee applied to `related` ids. This eliminates the main
failure mode of free-form tags: synonym sprawl (`auth`,
`authentication`, `Auth`) fragmenting the same concept.

### Descriptions feed search

a tag's description is available to
`docir context` so the agent (and the semantic layer) can reason about
what a tag means, not just match the bare key.

### CLI

`docir tag add <key> --description "..."`, `docir tag list`,
`docir tag rename <old> <new>` (rewrites the key across all referencing
documents), `docir tag rm <key>` (blocked while any document still uses
it, unless `--force`). Unlike a dangling `related` id, a `--force` tag
removal does not leave broken keys behind: since a tag is a classifier
rather than a link, the CLI strips the removed key from the `tags` list
of every referencing document (rewriting those files and reindexing
them) as part of the same operation.