ADR-013: A document’s filename is its code; the title lives in the frontmatter
Status Active · Filed 2026-08-03 · Influenced ADR-021 · DP-009
Context
Documents were named adr-004-generated-decision-index.md: the code, plus a
kebab-slug of the title. That slug is a copy of the title, and it is the
worst-placed of the three copies that existed.
The other two — the body’s # ADR-004: … heading, and whatever the index table
showed — were at least readable and editable. The filename copy has properties
none of the others do:
- No tool reads it. The generator took the title from the H1; the slug was decoration for a directory listing.
- Correcting it is a rename plus every inbound link. In this repo that was 157 links across 32 files. So it doesn’t get corrected — it silently becomes the oldest surviving statement of what the document is about.
- It makes the filename unpredictable from the code. Resolving a code to its
file meant globbing, and five separate places in this package had grown their
own regex to do it, each subtly different (
^adr-(\d+),adr-{n:03d}-*.md,{prefix.lower()}-*.md). That is DP-4’s latent divergence, harmless only for as long as the shape never changed.
Meanwhile the title had no field of its own. Reading it meant parsing the first
# line out of the body — a regex between a document and its own metadata,
which is precisely what
ADR-003 moved status and tags into frontmatter to avoid.
Decision
The filename is the code and nothing else — ADR-013.md, DP-002.md —
and the title is a title: frontmatter field, which the generated views
prefer over the body’s H1.
Scheme owns the translation, in one place:
scheme.filename(13) | "ADR-013.md" — what a new document is called |
scheme.number_of(path) | the number a filename carries, or None |
scheme.documents() | number → path, for the whole directory |
All five ad-hoc decoders now call documents(). That consolidation is not
incidental to this decision — it is what made changing the filename shape a
one-line change rather than a five-site sweep, and the reason to do it now was
that this ADR was about to prove the divergence was real.
number_of() deliberately accepts a trailing slug, so adr-010-old-title.md
still resolves. Luria writes the short form and reads both.
The H1 stays, and the lint guards the pair
The body keeps its # ADR-013: … heading. Someone opening the file on GitHub,
in an editor, or in a grep hit needs to see what it is without parsing YAML.
So there are still two copies, and
DP-3 says a hand-maintained projection will
drift. Rung 1 (derive it) isn’t available — the H1 is inside the body the
generator treats as opaque prose, and rewriting it on every build would put the
generator in the business of editing sources. Rung 2 is: guard the property,
not the list. luria lint reports a title: that disagrees with the H1, and
names both spellings so the reader doesn’t have to go and look up the diff.
Two copies plus a guard is a strictly better position than three copies with no guard, which is what the slug filename was.
Alternatives considered
- Keep the slug and add
title:anyway. The field would be right and the filename would still be wrong-and-uncorrectable. Adding a fourth copy of a string to a system whose central complaint is copies-that-drift is not a defensible direction. - Drop the body H1 and let the frontmatter be the only title. One copy, no guard needed, and rung 1 for free — but a document that renders as an untitled wall of prose when opened directly. The record is read by people who arrive at a raw file from a grep or a link, and making that experience worse to win a lint rule is the wrong trade.
- Derive the H1 at build time by rewriting the source file. Puts the
generator in the business of editing its own inputs, which makes every source
file a generated file and destroys the property that
luria lintchecks — you could no longer tell a stale view from a source someone edited. - Enforce the short filename in the lint. Tempting, and rejected: an
adopting project arriving with slug filenames would fail on day one for a
convention choice rather than a defect, and the lint’s contract is that it
fails only on things that are always wrong and mechanically fixable
(ADR-007). It is reported nowhere and enforced nowhere; Luria’s
own corpus and the
luria inittemplate use it, which is how a convention gets carried. - Status quo. Costs nothing today and one rename-plus-157-links the first time a title is wrong enough to matter, which is exactly why the slugs in this repo were never corrected.
Consequences
[ADR-004](ADR-004.md)is now writable by hand and checkable by eye. The reference fixer already produced the right link either way, but a human reading a diff can now tell at a glance whether a link points where it claims.- A directory listing no longer describes itself.
ls docs/decisions/used to be a table of contents; now it is a list of codes, and the index is where you browse. That is a real loss, and the mitigation is that the index is generated and always current (ADR-004) — which the directory listing never was.
- The migration itself demonstrated the reports working: an example filename in
a
config.pydocstring happened to nameADR-010, which isSuperseded, andluria ref-statusflagged it within a minute of the rename landing. title:is required by the lint, so an adopting project gets one error per document on its first run. That is one mechanical pass, and the error names the field.