A record of what your project knows — and of what it no longer believes.
Projects forget, and they forget silently. The wiki still renders. The decisions folder still has files in it. Nothing announces that a rationale expired, or that the reasoning behind a constraint now points at a decision somebody reversed.
Here is the whole idea. A page cites a decision by name:
We retry writes because ADR-012 requires at-least-once delivery.Months later that decision is superseded — one field, in one file:
status: 'Superseded — by ADR-013'Nobody edited docs/api.md. It is now wrong, and it says so:
$ luria lint
luria: 1 warning(s) — retired documents cited unacknowledged from current docs/code
ADR-012 is Superseded, cited 1× in 1 file(s) — Writes are at-least-once
$ luria reports
docs/api.md:3The old decision is not deleted; it keeps its body and its reasons. What changed is that everything resting on it became visible. Now fix the sentence, or say at the citing site why the citation is deliberate:
<!-- inactive-ok: ADR-012 — the history this section is about -->Either way the finding closes, and the reason it closed is written down.
That operation is the product. The rest is machinery around it: identities worth citing, statuses that mean something, and views generated so they cannot drift from their sources.
📖 dmarx.github.io/luria — this
repository's own record, published by luria site. Searchable, with backlinks
and a graph. Not a mirror of these docs: it is the output of one of the
features, applied to the project that ships it.
pip install luria
Python 3.11+. Two runtime dependencies (PyYAML, fire).
$ luria init --dry-run # what would it add?
$ luria init # issue_url comes from your remote
$ luria index # build the generated views
$ luria new --title "Switched the queue to at-least-once delivery"
record/devlog.d/2026/08/22/143005.md
$ luria new adr --title "Consumers must be idempotent"
record/decisions.d/ADR-tmp3kf9x.md
$ luria index && luria lint
luria: docs lint cleanEdit the two files it printed, commit, and the record has begun. The quickstart walks the same path with explanations, and ends by superseding a decision so you see the finding land.
A record is made of several kinds of material, and they are not the same kind of thing.
Referable documents — decisions, principles, RFCs, whatever a project declares. These are the ones the operation above runs on:
- A name something can cite.
ADR-012,RFC-7— in prose, in a commit, in a source comment.luria link --fixturns the bare code into a working link; the lint reports codes that resolve to nothing, so a reference is a claim that gets checked rather than a string that goes stale. - A standing, from a closed vocabulary:
Active,Proposed,Deferred,Superseded,Rejected. Retiring something is an edit to its status, never a deletion. - Rules you declare instead of hope for. Required fields, exactly one primary category, what each status means in this scheme. The conventions you would otherwise write in CONTRIBUTING become things that fail. Luria will even tell you when a status field has stopped carrying information, which is what one looks like a year after anyone maintained it.
Journals hold dated observations — true about the day they were written, never revised, so they carry no standing to change. Fragment directories hold pieces staged for a shared document, which is how a changelog stops being a file every branch has to touch. Remotes bring another project's codes, or arXiv ids, or ticket keys, into the same citation graph.
Every view of any of them is generated by luria index and never hand-edited,
so a generated page cannot drift from the sources it is built from.
Luria is also its own first user: this repository's record is scaffolded, generated and linted by the CLI it ships.
A record is kept as plain text in your repository, one entry per file, and that is a deliberate implementation choice rather than the product. It is chosen for participation: a contributor — or a coding agent — edits a file, opens a pull request, and greps the result, with no application to run, no database to migrate, and no export to negotiate when they want their history back.
Markdown is what that looks like today and will likely stay the primary shape. Nothing in the model above depends on it. Identity, standing, declared rules and generated views are claims about a record, not about a file format.
luria.toml what this record is made of (all keys have defaults)
record/ sources — one small file per entry, written by people
decisions.d/ ADR-001.md, ADR-002.md, … (a "scheme")
principles.d/ DP-001.md, … (another scheme)
devlog.d/ 2026/08/22/143005.md (a "journal")
changelog.d/ one fragment per change (a "fragment directory")
docs/ the read surface — prose plus generated views
decisions/ index + tag pages (GENERATED)
design-principles.md one page, anchored (GENERATED)
devlog/ monthly books (GENERATED)
reports/ status reports (GENERATED)
CHANGELOG.md assembled from fragments by `luria collect`
None of the names above are hard-coded. Schemes, journals, fragment
directories and remote projects are families declared in luria.toml —
a record made of RFCs, specs, and an incident log is the same engine with
different tables. See project memory and the generated
configuration reference.
ADR is not in the code. It is a table in a config file, and so is
everything else: schemes (documents with codes), journals (dated entries
that persist), fragment directories (written now, assembled later), and
remotes (someone else's namespace, cited by prefix). Name the tables and you
have a different record on the same engine.
- Project memory — decisions, principles, a changelog, a devlog. The
default, and what
luria initwrites. - A research anthology — one scheme of papers, another of the practices
drawn from them, each with its own status so a foundational paper and a
stale recommendation can disagree; arXiv identifiers linted and linked as
a
uidremote. - A standards registry — proposals browsed as an index, the interfaces they define concatenated into one page.
- An operations record — an incident journal that is never revised beside runbooks that are cited by name and go stale.
Designing a record is how to work out which of these your material is.
- Quickstart — from empty repository to linted record.
- Concepts — entries, citations, and the one operation everything else is machinery around.
- Designing a record — what belongs in one, which family fits, when two kinds of entry are two schemes, and what the schema can be made to refuse.
- Project memory — sources and views, schemes, journals, fragments, remotes, statuses, constraints, and how references work.
- CLI reference — every command and flag.
- Comment directives — acknowledging a finding where it happens instead of silencing the check.
- Adopting Luria — scaffolding an existing project, wiring up CI, publishing the site.
- Importing an existing corpus — when the material already exists as data, and what the transform will surface.
- Configuration reference — generated from the schema, every key with its default.
And the record itself, dogfooded: decisions · design principles · development log · status reports.
@software{marx_luria,
author = {Marx, David},
title = {Luria: project memory, kept honest by lint},
url = {https://github.com/dmarx/luria},
license = {MIT},
}Derived from CITATION.cff by luria index, which is also
what GitHub reads for its Cite this repository button — so the two cannot
disagree. Add version = {...} for the release you used; pip show luria
prints it. Nothing pins one here, because a version written into a file by
hand is a copy of the release tag, which is the drift DP-3 names and ADR-053
removed.
MIT.