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.mdis 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 inls, andlsis not how anyone reads this — the books have a contents list, and the entries are greppable. It would be a second copy oftitle:that no tool reads and that nobody corrects, which is precisely the copy ADR-013 removed from decision filenames.
Consequences
docs/devlog.mdis gone, replaced bydocs/devlog/README.md(an index of books) anddocs/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_historicalis now the one place that decides this, because a journal’s entries are nested and the old set-membership test onpath.parentcould 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 differentgranularity. - 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.