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 elseADR-013.md, DP-002.mdand 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 lint checks — 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 init template 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.py docstring happened to name ADR-010, which is Superseded, and luria ref-status flagged 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.