ADR-020: The devlog is a journal: dated entries that persist, rendered into books

Status Active · Filed 2026-08-04 · Influenced by ADR-012

Context

The changelog and the devlog were the same mechanism: a fragment directory, collected into one file at a marker, fragments deleted. That is right for a changelog and wrong for a devlog, and the difference is the one ADR-012 already drew — whether the sources survive.

A changelog entry is a claim about a release. Once it is in CHANGELOG.md, the fragment has served its purpose. A devlog entry is a dated observation: it was true when it was written and it stays true, which is exactly the property that makes a decision record worth keeping. Consuming it throws away the only copy of something that never expires.

Collecting also bought two costs that had nothing to do with the content:

  • docs/devlog.md is a shared insertion point. Every branch appends at the same marker, so every concurrent branch conflicts there, and a careless conflict resolution silently drops somebody’s entry — DP-2.
  • The order came from the commits. Collection appended fragments in the order they landed, so a rebase could reorder the record of what happened.

And it grew without bound. The pilot’s file reached 8,281 lines across a 40-day span — a measured rate of roughly 200 lines a day — in one document that can only be appended to and can never be rebuilt.

The obvious fix, a dated file per period appended to directly, was considered during the pilot and rejected on the record (SG-ADR-157): it narrows the conflict window without closing it. Two branches in the same month still collide, which is most of them.

Decision

The devlog becomes a journal: a configured directory of entries that persist, plus a generated view.

devlog.d/2026/08/03/211926.md      created: '2026-08-03T21:19:26'
   │      └── partition ──┘└─ time
   └── the journal's dir

Identity is the authoring timestamp. A journal entry has no number to assign — unlike a decision, whose number carries the order decisions were made and has to be allocated — so the time it was written is the natural key, and it is already unique to the second. The path is derived from created: and luria lint checks the two agree; luria journal new "Title" files the entry so the agreement is the default rather than a thing to get right by hand. On a same-second collision the filer steps forward a second: the filesystem already knows, and “checked” is a better guarantee than “unlikely”.

Three properties follow from the path, and each was a defect before:

  • No shared insertion point. Two branches each add a file nobody else writes. There is nothing to conflict at.
  • Ordering is a property of the record. Sorting is a pure function of the tree, so nothing a rebase does can change what the log says happened first.
  • The view can be rebuilt. Staleness is render() != read(), which is only computable because the sources survive (ADR-012). A hand-edit to a generated book is a lint failure rather than a silent divergence.

Entries use the standard frontmatter. title: and created:, plus optional tags: — the same header every other document in the record carries, for the same reason. The title is what buys the generated table of contents at the head of each book: a listing of what happened, in the file where it happened, that cannot go stale. No slug in the filename; the title is already named once, and a second copy is a projection that drifts (DP-3, ADR-013).

One book per period, and the period is configured. granularity is year, month or day. The right book size depends on how fast a project writes, and that is a measurement rather than a guess — Luria’s own is month because the pilot’s rate makes a month 3,600–7,200 lines.

Journals are [luria.journals.<name>] in luria.toml, not a special case for one file, and luria journal reports what is filed and where it renders. The books and their index are produced by luria index alongside the decision index and the principles document — one generator, one staleness check.

Alternatives considered

  • Leave it collected. Status quo. Keeps one mechanism for changelog and devlog, at the cost of destroying entries that never expire, a conflict on every concurrent branch, and rebase-dependent ordering. The saving is one concept; the concept was already there in ADR-012.
  • A dated file per period, appended to directly (devlog/2026-08.md). Considered and rejected during the pilot: it narrows the conflict window without closing it. Same-month branches still collide, and same-month is most of them.
  • Keep flat fragments, add a date: field, sort by it. This gets the ordering right and leaves the naming wrong: devlog.d/ becomes a directory of hundreds of files whose names are branch slugs, which are neither unique nor meaningful a year later. The partition is already implied by the timestamp; a directory tree is what it looks like on disk.
  • Order by git log. Rejected for the reason the whole package exists: it makes the record depend on repository state outside the record, so a shallow clone or a rebase changes what the log says. Ordering should be readable from the files.
  • A slug in the filename (2026/08/03/211926-derived-badges.md). Reads better in ls, and ls is not how anyone reads this — the books have a contents list, and the entries are greppable. It would be a second copy of title: that no tool reads and that nobody corrects, which is precisely the copy ADR-013 removed from decision filenames.

Consequences

  • docs/devlog.md is gone, replaced by docs/devlog/README.md (an index of books) and docs/devlog/<period>.md. Inbound links to the old file break — there were none outside this repo, and the seven migrated entries had their relative links rewritten for the extra directory level.
  • The back catalogue keeps its real timestamps. The seven existing fragments were dated from the commits that added them (git log --diff-filter=A), not from the migration, so the log’s first entries say when they were actually written.
  • A journal is historical for reference-status purposes, like the changelog: an entry is true about the day it was written and is never updated to stay true, so scanning it for retired references would produce permanent, unactionable rows. Config.is_historical is now the one place that decides this, because a journal’s entries are nested and the old set-membership test on path.parent could not see them.
  • A second journal costs a config block. Nothing about the mechanism is devlog-specific; a lab notebook or an incident log is [luria.journals.<name>] with a different granularity.
  • The changelog stays collected. This is not a claim that collection is wrong — it is a claim that the two views were never the same thing.