A decision record is a choice among alternatives at a point in time. Write one when you rejected an alternative, chose a constraint, or made something a future edit could silently violate. Run luria new adr; it assigns the identity and scaffolds from _template.md.

Values that decisions cite live in design-principles.md instead. The split, and why these are separate files, is ADR-003.

A decision whose choice changes is superseded by adding a decision and flipping the old one’s status — not by rewriting its body. A record you can quietly rewrite can’t be trusted about what you used to think.

That is a rule about silence, not about editing. A decision whose choice stands but whose reasoning was wrong is corrected in place, with a version bump and a history: entry saying what the previous version claimed (ADR-019). Nothing here is frozen; it is only un-silently revisable, and this record has worked examples of both (project memory §2).

By tag

The record (60) — what the four layers hold, and the rules between them: 001 · 002 · 003 · 009 · 012 · 013 · 014 · 015 · 016 · 017 · 018 · 019 · 020 · 021 · 022 · 023 · 024 · 025 · 032 · 040 · 041 · 045 · 050 · 051 · 052 · 055 · 056 · 057 · 058 · 059 · 065 · 070 · 072 · 076 · 083 · 084 · 085 · 086 · 087 · 088 · 089 · 090 · 092 · 093 · 094 · 095 · 098 · 099 · 100 · 102 · 104 · 105 · 106 · 114 · 115 · 116 · 117 · 118 · 119 · 120

Mechanism (81) — collectors, generators, the lint, the directive vocabulary: 002 · 003 · 004 · 005 · 006 · 007 · 008 · 012 · 013 · 014 · 015 · 016 · 018 · 020 · 021 · 023 · 024 · 025 · 026 · 027 · 028 · 029 · 030 · 031 · 032 · 033 · 034 · 035 · 036 · 039 · 040 · 042 · 043 · 044 · 045 · 046 · 047 · 048 · 049 · 054 · 055 · 056 · 057 · 060 · 061 · 062 · 063 · 064 · 066 · 067 · 068 · 069 · 071 · 072 · 073 · 074 · 075 · 076 · 079 · 080 · 081 · 083 · 086 · 092 · 093 · 096 · 097 · 098 · 099 · 100 · 101 · 102 · 103 · 104 · 105 · 108 · 112 · 113 · 116 · 117 · 121

Process (28) — how the machinery is adopted, run, and reported on: 007 · 009 · 010 · 011 · 017 · 019 · 022 · 027 · 029 · 030 · 034 · 035 · 036 · 037 · 038 · 042 · 048 · 049 · 052 · 053 · 058 · 062 · 065 · 068 · 070 · 091 · 101 · 112

Documented (17) — cited by the documentation a reader browses — a hand-written page under docs/, or prose that renders into one — so retiring this decision changes what the docs claim and the page has to change with it: 001 · 002 · 003 · 006 · 012 · 016 · 020 · 023 · 024 · 028 · 042 · 047 · 093 · 095 · 105 · 109 · 113

Load-bearing (20) — fixes something an adopting record must write: a frontmatter field, a path on disk, the spelling of a code, or directive syntax in prose. Reversing it does not just change luria’s behaviour, it invalidates records that have already been filed: 003 · 005 · 008 · 013 · 016 · 019 · 020 · 021 · 024 · 025 · 049 · 051 · 071 · 072 · 087 · 093 · 095 · 098 · 104 · 105

architecture (4): 077 · 078 · 094 · 095

ci (4): 069 · 118 · 119 · 120

config (5): 082 · 107 · 109 · 110 · 111

contract (5): 082 · 107 · 109 · 110 · 111

By status: Active (113) · Proposed (4) · Deferred (0) · Superseded (3) · Rejected (1)

Chronological

What the status column means in this scheme — the words are luria’s, the meanings are this project’s.

StatusMeans
ActiveActiveIn force — the current answer, and what a citation should normally point at
ProposedProposedNot in force yet — an open question, so citing it as settled is what the reference report catches
DeferredDeferredNot in force and not being worked on; the question is real and the answer waits on something
SupersededSupersededNo longer in force because something replaced it; the successor is named in the field, not in the prose
RejectedRejectedNo longer in force and nothing replaced it — kept because a rejection is worth being able to point at
#TitleSummaryStatus
ADR-001 v2Four layers of record, each with a test for what belongs in itFour layers, each with a one-line test for what belongs in it: design principles hold standing values, decisions hold a choice among alternatives at a point in time, changelog fragments hold what an operator would notice, devlog fragments hold how it went — including the wrong theories, which are the reusable part. Separate files rather than one document because they have different lifecycles: a principle is revised, a decision whose choice changes is superseded rather than rewritten, a fragment is collected and deleted. Rejected: one CHANGELOG holding all four (the layers’ different write patterns collide, and the one that gets skipped is always the narrative), and inferring the narrative from git history (commit messages are written to a different audience, and the failed approaches — the expensive part — never appear in them).Active
ADR-002 v2Contributions write fragments; shared documents are generated viewsEvery contribution writes a fragment nobody else touches (changelog.d/<slug>.md, a journal entry, one decision file); the shared documents are VIEWS assembled on a cadence, never hand-edited. A file every contribution appends to is a lock (DP-2) — the conflicts carry no information and each hand-resolution can silently drop someone’s work. Collection is deliberately NOT per-merge: the bot commit races in-flight rebases, so it runs weekly or on demand. A stub fragment (only an HTML comment) keeps “every contribution files one” enforceable when the honest answer is “nothing a reader would notice”. Rejected: per-merge collection (the race), and asking contributors to hand-merge carefully (contention, not carelessness).Active
ADR-003 v2Status is a closed vocabulary in frontmatter, enforced by lintA decision’s status comes from a closed vocabulary (Active | Proposed | Deferred | Superseded | Rejected, plus an optional em-dash note) and lives in YAML frontmatter alongside tags, date and issue — with the lint enforcing both. The vocabulary is closed because an open one drifts into synonyms: a strata-g audit found ~30 distinct status forms, with Accepted and Active split 44/46 and meaning the same thing. Frontmatter rather than a prose header because the index is generated from these fields (ADR-004), and parsing prose to build it puts a regex between a decision and its own metadata. Deferred earns its place: postponement stated is better than postponement faked as Proposed. Rejected: free-text status (drifted), and keeping the bold **Status:** header alongside frontmatter (two copies to drift, DP-4).Active
ADR-004The decision index is generated from frontmatterThe decision index and its per-tag pages are GENERATED from each decision’s frontmatter; luria lint fails on a stale one. Hand-maintaining the index made it both a lock (DP-2) and a drifting copy (DP-3) — in strata-g, 45 of 155 rows disagreed with their own decision’s status, and two were filed under a category their header didn’t claim. Prose lives in a README.stub with {categories}/{table} placeholders so humans still edit prose in markdown. Adding a decision is one new file with no shared edit; adding a TAG needs no code change at all. Every rendered field is rebased for the directory it lands in, so a link in a summary works from both the index and the one-level-deeper tag pages. Rejected: a fragment directory like changelog.d (the data is derivable, so generation beats collection — no step to forget).Active
ADR-005References in prose are hyperlinks, enforced by lintEvery document code, design principle and issue number cited in prose is a hyperlink, and luria lint fails on one that isn’t; luria link --fix writes exactly the links the lint demands, from the same scanner, so the linter can never demand a rewrite the fixer wouldn’t make (DP-4). In the corpus this was extracted from, 2,246 references across 160 files were bare — and the split between linked and bare was random, which is worse than uniformly bare because a reader can’t learn which references are worth clicking. Non-obvious rules, each found by a wrong rewrite: markdown isn’t parsed inside a raw HTML block (those get an <a href>); backtick pairing is per-paragraph or one stray tick inverts code-vs-prose for a whole file; a low #N is ambiguous and stays bare without an explicit cue; fragment links resolve from the file the collector puts them in. Rejected: absolute URLs everywhere (breaks offline and non-GitHub reading), and a prose rule with no lint (the surface that drifted).Active
ADR-006Reference schemes and paths are configuration, not constantsA referable document family — ADR, RFC, SPEC — is a [luria.schemes.X] entry naming a directory and an in-force status, not a hardcoded prefix. The annotation vocabulary follows: the verb is inactive-ok, not adr-ok, and a code carries its prefix (ADR-012, never 012), with a bare number reported as an error rather than assumed. ADRs are the only scheme this package ships, so the short forms were available and would have baked one knowledge-management system into the vocabulary — the cost of the general form is one prefix per annotation; the cost of the specific one is a rename across every annotation the day a second scheme appears. Also configured: issue URL, code globs, fragment directories, and which files are dated records. Rejected: arguments threaded through every entry point (the second caller forgets one and the linter and fixer diverge — DP-4).Active
ADR-007 v2Document status is reported, not enforcedTwo standing reports — reference status (references to retired documents) and pending decisions (undecided, by age AND citation count) — are WARNINGS that never fail a build, with one summary line each from luria lint. Citing a Rejected decision is often exactly right, and a decision can be legitimately open for months, so only a human can judge a row; a guard that’s wrong most of the time gets suppressed, and then the times it’s right go unread. A deliberate reference is silenced by an inactive-ok: comment (line, -block or -file scoped), counted in the report rather than hidden, and reported when it stops applying. luria reports writes both in full for a CI artifact, because a warning with nowhere to be read is a warning nobody reads. Rejected: a lint error (fails CI on correct citations), a bot that auto-updates statuses (invents decisions nobody made), and a ratchet on a checked-in baseline (a file every contribution touches — DP-2).Superseded — by ADR-035
ADR-008One directive vocabulary, one scope ruleComment directives share one parser, one shape — <name>[-block|-file]: <args> — <reason> — and one scope rule with no per-directive defaults: bare is the line and the line below, -block the run of non-blank lines it sits in, -file the document. inactive-ok acknowledges a deliberate reference to a retired document; unexempt is the inverse, putting a region the linter skips (a code block) back under it. The directive must OPEN its comment, # noqa style, and directive-SHAPED text is never a citation — both rules exist because documenting the syntax kept invoking it, four separate times. A first pass gave unexempt an implicit block scope so that a plausible example would parse; that bought one example and cost the rule its predictability, and was removed. Rejected: per-directive default scopes, and a wildcard that would mute a whole file including references added later.Active
ADR-009Extracted with provenance, not importedLuria’s decisions are renumbered from 001 and rewritten as decisions about Luria, each naming its strata-g ancestor as provenance rather than importing the original number. Porting the eight ancestor decisions verbatim would have carried a numbering with gaps that mean nothing here, bodies arguing about a graph tool, and cross-references to decisions that were never extracted. The evidence is kept — every principle and decision names the incident that earned it, because a rule whose evidence is missing reads as taste and gets re-litigated. Luria also runs its own machinery on its own record, so the first consumer to hit a bug is this repo. Rejected: importing numbers verbatim (dangling cross-references), and starting clean with no provenance (throws away the only thing that makes the rules persuasive).Active
ADR-010Name the project chester, after Chesterton’s FenceName the package chester, after Chesterton’s Fence — the parable that a fence should not be removed until you know why it was put there, which is what a decision record is for. Superseded the same day by ADR-011: the allusion names only the narrowest slice of what the record does (defending existing constraints), it survives being shortened to “chester” only for readers who already know the parable, and it frames the record defensively — as an argument against change — when its actual job is to make change cheap. Kept for the record, and as the corpus’s worked example of supersession.Superseded — by ADR-011
ADR-011Name the project Luria, after The Mind of a MnemonistName the package Luria, after Alexander Luria and his case study The Mind of a Mnemonist — a man who could not forget. The name points at the faculty the package supplies (memory that survives the session) rather than at one failure it prevents, and the book carries its own cautionary half: Shereshevsky’s total recall was a burden, because a record that never forgets and never abstracts becomes unusable. That tension is the design brief, not a flaw in the allusion. Supersedes ADR-010 (chester, after Chesterton’s Fence), whose allusion named only the narrowest slice of the job, survived shortening only for readers who already knew the parable, and framed the record defensively when its actual job is to make change cheap.Active
ADR-012 v2Principles are fragments too, rendered as a documentDesign principles are decomposed into one fragment each, with frontmatter carrying a version, the decisions that shaped them (influenced_by), and an origin note; docs/design-principles.md becomes a generated view. This is the same move as the decision index, not a third mechanism — a scheme gains a render setting, index (a table plus tag pages) or document (bodies concatenated). The distinction that matters is not frontmatter but whether the sources survive: a collected view (the changelog) consumes its fragments and can only be appended to; generated views are a pure function of sources that persist, which is the only reason luria lint can detect a stale one. Rejected: collecting principles like a changelog (the fragments would be deleted, taking the version history with them, and staleness would become undetectable), and leaving the document hand-maintained (a lock and a drifting projection, DP-2 and DP-3).Active
ADR-013A document’s filename is its code; the title lives in the frontmatterA scheme’s documents are named for their code alone — ADR-013.md, not adr-013-a-documents-filename-is-its-code.md — and the title moves into a title: frontmatter field, which the generated views prefer over the body’s H1. A slug in the filename is a third copy of the title that no tool reads, that a rename plus every inbound link is required to correct, and that therefore never gets corrected. The body H1 stays, because someone opening the file alone needs a heading, so luria lint guards that the two agree — rung 2 of DP-3, since rung 1 isn’t available. Filename decoding, which had accumulated five separate copies, is now Scheme.documents() alone, and it reads legacy slug filenames so adoption isn’t a rename-everything-first proposition. Rejected: dropping the H1 (unreadable on its own), and enforcing the short filename on adopters (a convention choice, not a defect).Active
ADR-014A code that resolves to nothing is reported, not droppedA reference whose code names no document here was silently skipped: the fixer can’t link it, so the lint — which never demands a rewrite the fixer wouldn’t make — said nothing. That silence hid ten stale strata-g numbers left in ported docstrings, one of them a link to a file that does not exist. Such codes are now counted in luria ref-status, luria lint and the CI report, as a WARNING: a typo, a foreign project’s decision and an illustrative example look identical to a scanner and only a human can tell them apart. unresolved-ok: retires a deliberate one, at the same three scopes as inactive-ok: and with the validity check inverted — it must name a code that doesn’t resolve. Codes inside URLs are masked, because linking out to another project’s ADR-013 is the correct way to cite a foreign document. Rejected: a lint error (a foreign citation is often right), and inferring a URL for unresolvable codes (silently wrong on a typo).Active
ADR-015A foreign record gets a prefix; the config turns it into a URLAnother project’s decision is cited as SG-ADR-032 — a registered remote prefix composed with that project’s own code — and a [luria.remotes.SG] entry teaches Luria to build the URL, so luria link --fix writes it and luria lint demands it exactly as for a local code. This is the alternative ADR-014 rejected, and what it was missing is a lockfile: a remote whose filenames carry title slugs can’t be resolved by any template, but it can be discovered — from a local clone, or the GitHub contents API, reading the remote’s own luria.toml for where its documents live — and the code→filename map committed so CI and offline checkouts resolve identically. luria remotes --check probes reachability; it is a separate command, never part of luria lint, because a check that opens a socket fails on a train. Rejected: a live lookup (flaky, and useless for a private remote), and a flat second scheme (loses which project a code belongs to).Superseded — by ADR-016; drops the local-clone discovery path and makes Luria its own worked example
ADR-016Remotes are public URLs, Luria is its own worked example, and every scheme versionsSupersedes ADR-015, keeping its core — a foreign record is cited as LU-ADR-013, a registered prefix composed with that project’s own code, and the config builds the URL — and dropping the part that bent the tool around one repository. Discovery reads public HTTPS only; the local-clone option is gone, because a resolution that depends on what happens to be on somebody’s disk is not reproducible and quietly made the ancestor’s pre-convention shape Luria’s problem. Luria registers itself as remote LU, which the luria init scaffold cites instead of pasting URLs, so the mechanism is exercised by the package rather than only by its tests. Also: version: becomes standard frontmatter for every scheme, not just principles, shown in the index only when it isn’t 1; and *.stub files are linted, closing a hole where hand-written prose rendered into a page the lint skipped for being generated. Rejected: keeping the clone path behind a flag (the reproducibility problem is the same), and a credentialed fetch.Active
ADR-017A reference may name a document before its URL resolvesSG-ADR-032 stays in the prose even though that URL 404s today: strata-g’s filenames still carry title slugs, and the code-only convention Luria builds against is what strata-g will use once its record is ported. The reference is therefore correct and merely early. Naming a foreign document is the durable half — which project, which decision — and it survives whatever the URL does; dropping the citations to avoid a broken link would throw away the meaning to protect the plumbing. Two remotes are registered, SG and LU, and their difference is the point: LU is public and already on the convention, so luria remotes --check verifies it for real, while SG reports as unverifiable and stays that way until the port. Rejected: a hand-authored lockfile (a drifting projection with no guard, since the repo is private), and de-prefixing the citations back to prose (unresolvable and unnameable).Active
ADR-018 v2The README’s badges are counts derived from the recordThe two decorative badges (“generated index”, “versioned”) said nothing a reader couldn’t guess and could never be wrong, which makes them furniture. They are replaced by the two numbers the record actually has to answer for — needs decision (Proposed + Deferred, across every scheme) and cited but retired (retired documents still cited without an acknowledgement) — baked into static shields URLs that luria index rewrites and luria lint checks for staleness. Derived, not hand-written: rung 1 of DP-3. luria pending was generalized to every scheme in the process, since a Proposed principle is an open question exactly as a decision is. Rejected: a shields endpoint reading a committed JSON file (it always reports the default branch, so the count can’t move in a reviewer’s diff), and a live query (nothing outside this repo can compute either number).Active
ADR-019A wrong reason is corrected in place and versioned; a changed choice is supersededTwo ways a decision record can be wrong, and only one of them is a supersession. If the choice changes, add a new decision and retire the old one — the rule ADR-001 has always stated. If the choice stands but a reason for it turns out to be wrong, correct the body in place, bump version, and say what changed in history:. Superseding over a bad argument is theatre: it retires a decision still in force, and every citation of it has to be repointed at an identical claim. Leaving the argument standing is worse, because a wrong reason is exactly what gets quoted at the next person. The version field makes the correction visible, which is the whole reason “never rewrite a body” exists — the objection is to silent revision, not to being wrong out loud. Rejected: superseding for any change (churn, and it makes the status vocabulary lie), and correcting silently (indistinguishable from rewriting history).Active
ADR-020The devlog is a journal: dated entries that persist, rendered into booksThe devlog stops being a collected view and becomes a journal: entries live at devlog.d/yyyy/mm/dd/hhmmss.md, persist, and are rendered into one generated book per period plus an index. Identity is the authoring timestamp, so there is no number to assign and no name to collide on; ordering is a pure function of the tree rather than of commit order, which a rebase can change. Entries carry the standard frontmatter — title: is what each book’s contents list shows — and luria lint checks the path agrees with created:. Rejected: a dated file per period appended to directly (two branches in the same month still conflict, which is most of them), a single growing docs/devlog.md (8,281 lines in 40 days at the pilot’s rate), and slugs in the filename (a second name for the thing title: already names, DP-3).Active
ADR-021The read/write boundary: views in docs/, sources in record/The repository layout is split along what a person is doing: docs/ holds everything a reader browses — prose and every generated view — and record/ holds everything a contributor files, each container inside carrying the .d suffix as the visual affordance that you have crossed into the write domain. A scheme gains a separate output so its index renders into docs/ while its sources live in record/; the stub and tags.yaml move to the source side, so a view directory holds only what the generator wrote — which turns “don’t hand-edit” from a comment into a lint. Rejected: marking sources with .d at the top level (four write locations, no single answer to “where do I file”), segregating by project into a meta/ directory (the wrong axis — it split ours-vs-theirs when every repo’s actual confusion is read-vs-write), and burying the write root as a dotfile (contributors work there daily).Active
ADR-022url-ok acknowledges foreign codes onlyThe url-ok directive — and the warning it acknowledges — applies only to links whose label is a composed foreign code (SG-DP-18), never to local codes or arbitrary hand-targeted links. Three reasons: a warning needs a well-defined wrong, and only the remote namespace has one (a foreign code has exactly one constructed URL, so “differs” is a meaningful binary; a local code has a family of legitimate targets, so the same check would flag correct links until acknowledgement became reflex); the risk profiles differ (a local link fails loudly when its target moves, a remote hand URL fails silently, and the directive exists to compensate for the silent case); and the local want has a better answer the repo already models — register yourself as a remote, the way Luria registers LU. The underlying asymmetry: a local hand URL restates a fact the machinery already owns, so the remedy is mechanical, while a foreign hand URL carries a fact the construction has no home for. That reframes the directive’s role: each url-ok reason describes a gap in what remote config can express, and a recurring shape is the cue to grow the config — per-scheme remote mappings are the known first candidate — not to keep annotating.Active
ADR-023A remote constructs per scheme: dir, document + anchor, or urlA remote gains per-scheme construction: [luria.remotes.X.schemes.Y] maps one code family to dir (file per code), document plus an anchor template (sections of one assembled page), or a url template. The remote model had assumed one directory of code-named files, so a document-rendered scheme’s codes constructed confident URLs to files that never existed — the gap ADR-022 predicted url-ok reasons would accumulate. The anchor template defaults to dp-{number}-shaped stable anchors, which is what Luria’s own document render emits, so a remote on current conventions needs one document line. The lockfile’s authority stays scoped to what discovery can see — files — so an anchor construction never consults it. Rejected: zero-config prefix magic (a default is a guess about the remote, and a legacy remote would get confidently wrong anchors; explicit config is a claim by the user), and anchor discovery by fetching the document (network in the resolution path, useless for private repos). The residue — legacy heading-derived anchors no template can compute — remains url-ok’s jurisdiction, now excusing only the anchor.Active
ADR-024A remote reference is a prefix, a delimiter and a uid — numbers are the special caseA remote need not hold a Luria-shaped record. Give it a uid regex and its references are the prefix, a configurable delim and whatever the pattern matches — an arxiv id, a ticket key — constructed through the url template, which can address the uid’s capture groups by position ({1}.{2}) or take the whole tail as {0}/{uid}. A uid is exact and is never normalised: zero-padding an arxiv id would quietly cite a different paper. One rung only — no lockfile, no code-only convention, and a uid remote without a template constructs nothing rather than guessing. The refactor this forced was overdue anyway: one combined regex with a hardcoded hyphen became per-remote patterns behind references() and parse_code(), so the delimiter and tail shape are spelled in one place. Rejected: overloading schemes for uid families (a scheme implies documents with statuses and an index; an arxiv paper has neither), and auto-linkifying unregistered shapes (an unconfigured prefix must never match).Active
ADR-025Wikilinks: [[CODE]] is a typed reference, and typing it changes the rules[[ADR-013]], [[SG-DP-18]], [[ARXIV-2403.05530|the report]] — wikilink brackets are the author asserting “this is a reference, link it”, and the assertion changes the rules on both sides. Inside the brackets no prose heuristics apply: a bare DP-3 (no #) resolves, a low [[#10]] needs no cue, any registered remote shape works. And an unresolvable wikilink is a lint violation the fixer cannot clear — the one deliberate exception to “the lint never demands what --fix won’t do”, because an explicit request deserves an explicit refusal rather than a silent skip. Wikilinks are consumed: luria link --fix rewrites them to ordinary markdown links (an <a href> inside raw-HTML blocks), because GitHub renders [[…]] as literal brackets and source files must read as plain markdown wherever views aren’t generated. Rejected: expanding at view-generation time (fails everywhere sources are read directly), and heuristic-free auto-linking without brackets (already rejected in ADR-024 — this is the sanctioned way to opt a single reference out of the heuristics).Active
ADR-026One ordered pmap over threads; width is an environment variableParallelism lands as one primitive — parallel.pmap(fn, items), a thread pool that returns results in input order — applied at three seams: render units in outputs() (a scheme, a journal), per-file scans in the bare-reference lint, and per-URL probes in remotes --check. Measured honestly: the probes are the real win (6.6s → 2.9s, round-trips overlapped); renders and scans are a wash at today’s cardinality, because that work is regex-CPU under the GIL — those seams are structure bought now, cheap, against the growth the issue anticipates. Threads not processes (the winning workloads are I/O; processes add pickling and spawn cost for no measured gain), ordered results not as-completed (the staleness diff and the lint report must read identically at any width), and LURIA_JOBS=1 as the serial escape hatch. If render units ever measure in seconds, the pmap seam is where a process pool swaps in.Active
ADR-027Published to PyPI via trusted publishing; the scaffold ships inside the packageLuria publishes to PyPI through GitHub’s trusted publishing: a publish.yml workflow whose pypi environment’s OIDC identity is the whole credential — no token exists to leak, rotate or forget. Build and publish are separate jobs; the artifact that ships is the artifact that passed a cold-install smoke test (pip install dist/*.whl, then init → index → journal new → lint in an empty directory), which is the guard against shipping a luria init that only works from a checkout. The scaffold ships inside the package (luria/template/ in the wheel, via hatchling force-include) while staying top-level in the repository where a visitor browses it (ADR-021) — the setuptools parent-relative package-data it replaces leaked a bare template/ into site-packages, functional only by collision-prone accident. Publishing fires on a GitHub release (plus manual dispatch); the sdist carries build inputs, not the record — the record’s browsing surface is the repository.Active
ADR-028Collection styles are configuration; the changelog shape is one of themA fragment directory now declares how it assembles: append (the narrative shape — bodies oldest-first, inserted before the marker, log reads top-down) or changelog (the release shape — one ## <date> batch per collection, inserted right after the marker so the newest batch reads first, fragments newest-first within it, and a batch of only stubs emits nothing). Until now the collector had only the narrative shape, and the position on changelogs was “point that directory at scriv” — which strata-g did, and retiring scriv there left its changelog with no collector whose output reads in release order. The style is one config key on the fragment mapping (ADR-002); what it deliberately does not add is scriv’s category merging — a fragment’s ### Added/### Fixed sections stay in its body, kept per-contribution within the batch. Rejected: making the marker’s position imply the order (silent, and wrong the day a file’s header moves), and reproducing scriv inside luria (category merging earns its complexity in a versioned-release changelog, which a per-merge project log is not).Active
ADR-029 v2A generated view must be committed by something; the remedy says so where it is readGenerated views are committed artifacts, and who commits them is open: the author can regenerate and commit, or a generation job can run the generator and push — the latter is the better default, since a view a human rebuilds by hand is still a hand-maintained projection. What breaks is the shape in between, the generator dropped into a checking job committing nothing: the output dies with the runner and a following luria lint compares the generator against itself, so it can no longer fail. The generation route ships as machinery — actions/generate and actions/lint composite actions holding the one authoritative implementation, the luria init template workflow built from them (it previously scaffolded verify-only, handing every adopter the incident-shaped setup), and this repo’s own ci.yml running the same actions by local path. The staleness remedy is context-aware (names both routes, warns against the broken shape) and bare luria badges says on stderr that it only printed. Rejected: banning writes from CI (the first draft — it outlaws generation jobs, including this repo’s own), an always-on write-in-CI warning (noise on every correct generation-job run), and folding README.md into outputs() (ADR-018’s reason still holds).Active
ADR-030The CLI surface is the workflows, not the module layoutTrim the CLI from eleven commands to a tiered eight: six contributor commands (lint, link, index, journal, remotes, init) and two CI commands (reports, collect), named as such in the help text. badges, ref-status and pending are removed outright — each was already subsumed (luria index writes the badges and luria lint checks them per ADR-029; the two reports print as lint warnings per ADR-007 and land in full via luria reports) — and a removed name is an ordinary unknown command: the library is days old with one user, so a deprecation shim would be an affordance for a workflow nobody has. The modules keep their main()s, so python -m luria.ref_status --all still exists for an interactive dig. Rejected: keeping one command per module (mirrors the package, not any workflow), a RETIRED table answering each old name with its successor (built in the first draft, removed in review as legacy-preservation bias), folding link into lint --fix (the check/fix split is deliberate, ADR-005), and deleting the modules’ entry points along with the commands (breaks vendoring and the tests for no surface gain).Active
ADR-031A fact the tree already states is populated, not demandedA journal entry filed without created: gets the field populated by luria index from its path — the path is derived from the timestamp (ADR-020), so when the field is empty the path is the one witness left, and populating from it writes down what the tree already asserts. The lint error now names that remedy when the path parses, and says the author must answer when it doesn’t. An entry whose field and path disagree is never touched: two witnesses in conflict is a judgement, not a mechanical fix. Rejected: having luria lint write the field (the lint never writes — check and fix are separate surfaces, ADR-005), a new fix command (grows the surface ADR-030 just trimmed), and leaving it an error (demands a human retype a timestamp the machine is already holding).Active
ADR-032The status reports are committed views, and the badges land on themThe two status reports move from a CI artifact (build/doc-reports) to committed generated views (docs/reports by default), rendered by luria index with everything else and checked for staleness by the lint. Each README badge now links to the report that explains its number, and everything a report names — the flagged document, every citation site, every pending decision — is a link. Two load-bearing details: a report is a pure function of the record (no generation date, ages stated as “open since ” — anything clock-derived goes stale at midnight and fails the staleness check with no record change behind it), and the reports directory is excluded from reference scanning (a page that lists retired codes would otherwise cite them, and the view could never converge). Rejected: artifact-only reports (unread — the reason for the issue), date-stamped committed reports (daily churn on every branch, the DP-2 lock), and badges pointing at the decision index (a number with no explanation behind it).Active
ADR-033A document can opt out of reference checking, and the report counts itA fifth directive, unlinted-file:, exempts a whole document from the reference machinery — bare-reference lint, wikilinks, and the reference-status scan — for the fixture-heavy or vendored page where a directive per code is maintenance without information. Two constraints make it safe: it is file-scoped only (a bare unlinted: or unlinted-block: governs nothing and is reported, because backticks already provide the narrow form), and opted-out files are counted and listed in the reference report per ADR-007 — the blanket exemption is the one suppression the reports cannot converge past, so it must never be invisible. Rejected: a config file-list (the excuse belongs where the exemption is, like every other directive), narrower scopes (duplicate quoting), and exempting the non-reference checks too (frontmatter and staleness still apply).Active
ADR-034Fixture codes come from a registered prefix, not from the sequenceA reserved remote-style prefix, FX, is registered in luria.toml with a constant url pointing at the fixture-codes note in the directives doc — so FX-ADR-032 is resolvable by construction: the fixer links it, nothing reports it as dangling, no directive is needed, and it can never collide with the real sequence because it is not in it. Mechanizes the convention the template already used informally (ADR-777), after filing the real ADR-032 made every specimen that borrowed that number resolve at once and five unresolved-ok directives went stale together. Existing bare-code specimens that quote history (ADR-014/015/017) keep their directives — the prefix is for everything written from now on. Rejected: a dedicated local scheme (a scheme’s documents must exist), reserved number ranges (a convention with no teeth), and leaving it to unresolved-ok (per-site maintenance that detonates when the sequence arrives).Active
ADR-035Status enforcement is a dial — reported by default, failable by configurationSupersedes ADR-007’s “warnings, never able to fail a build”: the warn-first posture stays the default, but [luria.lint] fail_on promotes named warning classes — retired-citations, unresolved-codes, hand-written-urls, stale-directives, pending-documents, unlinted-files — to lint failures. The accounting is unchanged: only unacknowledged rows ever reach a class, so inactive-ok: and its siblings remain the way to state a deliberate exception even under enforcement, and the dial changes the consequence, not the bookkeeping. An unknown class name in fail_on is itself a lint error naming the vocabulary. Rejected: keeping “never” (a soft gate against the DP-5 mechanism→guarantee promotion, review’s finding on #32), per-invocation CLI flags (CI and a laptop would enforce different policy), and a single boolean strict mode (the classes fail for different reasons on different projects).Active
ADR-036 v2One scaffold for every entry kind — luria new, driven by the configluria new [kind] scaffolds an entry anywhere the record takes one — the journal by default, any scheme by prefix (adr, dp: next free number from _template.md, date stamped), any fragment directory by name (changelog: filename stamped from the filing moment, like a journal entry; --name overrides). It computes only what a machine can know — filename, number, timestamp, date: — prints the path, and leaves every other field a template placeholder for a markdown-aware editor; --title/--status/--summary/--tags exist for tools driving the CLI, never as requirements. Kinds derive from luria.toml, so a new scheme scaffolds for free. Subsumes and removes luria journal (no shim, ADR-030). Rejected: interrogating for fields on the command line (humans author fragments in editors), hardcoding the three known kinds (the config already knows them), and keeping journal beside new (two spellings of one scaffold).Active
ADR-037The agent file is a map, not a copyCLAUDE.md — this repo’s and the scaffolded template’s — is truncated to a short list of links plus the invitation to run luria --help: the doctrine lives in docs/ and the current command surface lives in the CLI, so an agent file that restates either is a hand-maintained copy that drifts (it did, twice in one week — the command block had to be chased in ADR-030 and again in ADR-036). The map keeps only what the links can’t carry: three one-line ground rules and the statement that when this file disagrees with the docs or the CLI, this file is the one that’s wrong. Rejected: restating the command list (a fourth hand copy of COMMANDS), restating the doctrine (a worse copy of project-memory.md that costs agent context), and generating CLAUDE.md from the record (machinery for a page that is now ten stable lines).Active
ADR-038The CLI is the interface; the Makefile retiresThe Makefile is deleted. Its stated purpose — “run what CI runs is always make <target>” — stopped being true when ADR-029 moved the docs jobs into composite actions that invoke luria directly: CI’s entire remaining Makefile usage was one make test line wrapping python -m pytest tests -q, and every other target restated a CLI one-liner, a fifth hand-maintained copy of the command surface that had to be chased twice in one week (ADR-030’s removals, ADR-036’s journal → new). ci.yml runs pytest directly, and luria --help is the one list of what you can run. Rejected: keeping it as a convenience alias layer (aliases drift and their drift caught nothing the CLI’s own help doesn’t catch), and a task runner swap (same restatement, new syntax).Active
ADR-039Drive the CLI with Fire: typed functions, derived flagsReplace the hand-rolled dispatcher and every module’s argparse layer with Google’s Fire: each command is a plain typed function (<module>.run), fire.Fire(COMMANDS) derives flags and help from signatures and docstrings, and failure is signalled by raising SystemExit — never by return value, because Fire prints return values and a CI gate’s exit code is not output. Proposed rather than Active on purpose: this ships as a draft PR with a worth-it writeup, and the decision is the merge verdict. What it buys: ~150 lines of argparse deleted, one interface convention, flags that cannot drift from the functions they call. What it costs: the first runtime dependency beyond PyYAML, Fire’s house help format in place of the tiered usage text, and exit-code discipline that every future command must know about.Active
ADR-040Migrations: renaming schemes and moving documents without losing the record’s memoryA record’s abstraction ladder can grow rungs after documents already sit on the wrong one, so the machinery gains migrations: mapping-driven code rewrites (never prefix pattern-matching, which would eat fixtures and other projects’ namespaces), a formerly: frontmatter field as persistent identity across renames, and a full rewrite of every reference — historical journals included, because a code that matches no configured scheme is invisible to the linter and git already guards the true history. A derived alias map plus a legacy-spellings warning class keeps in-flight branches safe. Rejected: the supersede-and-copy shuffle as the default (kept only as an explicit low-rent mode), a config-level migrations ledger (config describes the present; documents carry their pasts), and preserving old spellings in historical files (a half-measure that leaves history unwatched).Active
ADR-041Bugs enter the record characterized: the minimal working example protocolA bug joins the record as an issue carrying a minimal working example — a replayable transcript showing the behavior and the expectation it violates — before any fix is attempted, because a fix without a characterization is a guess wearing a diff. The response is then classified on the ADR-035 ladder (lint check, report, doc fix, or expectation-corrected wontfix), the fix PR turns the MWE into a regression test, and any new guard is fired once per DP-6. Rejected: fix-first-explain-later (the diff becomes the spec), demanding a full root-cause analysis up front (heavy enough to deter filing), and leaving reporting ad hoc (the status quo that let a broken-link class ship green through lint).Active
ADR-042 v3The record publishes as a Quartz vault: paths preserved, sources withheld, frontmatter surfacedluria site stages the record as an Obsidian/Quartz vault and a composite action builds it onto GitHub Pages, giving the citations a graph and backlinks nobody maintains. Three rules do the work. Paths are preserved, so the relative targets luria link --fix already wrote keep resolving and no second link resolver exists to drift from the first (DP-4). A source that renders into a view is withheld — derived from link_base, not listed: a fragment, a journal entry and a document-scheme source all spell their links for the page they land in, so publishing them in place would break every one and duplicate the view besides. A link leaving the published set goes to the repository, because the record cites workflows and templates that are files, not pages. Plus one addition: each decision gets a rendered record line — status, date, issue, influenced_by — since a site renders frontmatter as nothing, and a retired decision that reads as current is worse than one nobody can find. Rejected: publishing docs/ alone (the decisions it links to live in record/), rewriting links for a flattened vault (a second resolver), Quartz v5 (its plugin installer crashes on this Node), and expanding wikilinks at build time (the fixer consumes them at source, so the vault never sees one — which is why the interpretation gap the issue worried about never arrives).Active
ADR-043The site wears the project’s brand: artwork by path, favicon rasterized at build time[luria.site] gains icon, logo, logo_dark and a theme table, so a published record looks like the project rather than like the generator. Two constraints carry the weight. The favicon is rasterized during the build, never committed: a project points at the vector master it maintains, and actions/site renders it with the sharp Quartz already depends on, so no derived PNG exists in the repository to drift (DP-3). The logo is baked once per theme, because whether artwork inverts itself is a browser question — a browser that carries the page’s color-scheme into an embedded SVG resolves its dark-mode rules against the site’s toggle, one that doesn’t resolves them against the operating system. The palette merges over Quartz’s defaults by name, and an unknown name is refused rather than dropped (DP-1). Rejected: shipping Luria’s own brand as everyone’s default, committing a rendered icon, and asking the package to compose an icon out of a wordmark — a package that designs is a package with taste to argue about.Active
ADR-044The configuration reference is generated from the config schemaluria index renders docs/configuration.md from the dataclasses in luria/config.py — prose from each class’s docstring, and the key tables from dataclasses.fields() — so the one page that documents Luria’s whole capability surface cannot fall behind the schema it describes. The gap it closes is not that the generality was undocumented but that it was misfiled: the configurable surface lived in Python docstrings, in template/luria.toml’s comments, and in the decision record, which is archaeology rather than reference, so a reader met the shipped ADR / DP / changelog / devlog record and reasonably concluded those four were Luria’s parts rather than its defaults. The load-bearing detail is that the key tables are read from the schema, never listed in the generator: a field added to Site is a documented row on the next build, and the safe direction to be wrong in is a row that appears without prose, not prose that outlives its key. Rejected: writing the page by hand (the second copy of a moving schema — the exact projection DP-3 says will drift, and the reason this page did not already exist); leaving the prose in docstrings (not browsable, not linkable, absent from the published site); a per-project README.stub for the prose (a scheme’s stub exists because a project’s index needs its own preamble, whereas this text is Luria describing its own schema and is identical everywhere — a file every adopter carries and none edits); and a general autodoc dependency, which renders module layout when ADR-030 says the surface is the workflows.Active
ADR-045Worked configurations are executable examples, not proseexamples/ holds four complete projects — RFCs beside specs, a collocated layout, three journals at three granularities, and uid remotes citing arXiv papers, Jira tickets and CVEs — and tests/test_examples.py builds each into a temp tree, runs the real luria index and luria lint, and asserts the capability it advertises. The motivation is this project’s own founding audit: every surface with an executable guard held, every surface governed by prose alone had drifted. A luria.toml block in a guide is a claim nobody runs, and writing four of them immediately proved the point — the first pass through these examples falsified two claims that had just been written in docs/adopting.md and found a third defect, all three invisible to review. Generated views are deliberately not committed under examples/: the tests build in tmp_path, because a committed view nobody regenerates is the stale projection the examples exist to argue against. Rejected: prose examples alone (the status quo, now known to have been wrong in two places); fixtures inside tests/ only (a reader cannot browse or copy them, and adopters are the audience); and doctest-style snippets embedded in the guide (they test the snippet, not a project that renders and lints end to end).Active
ADR-046Reference detection is scheme-driven, not three hardcoded patternsdoc_refs.find_refs now iterates the configured schemes and matches each by its own Scheme.pattern, and resolve dispatches on the scheme’s render mode — a file link for index, an anchor in the assembled page for document. It previously knew three hardcoded patterns: ADR-N, the # spelling of a design principle, and issue numbers. So a project that configured RFC got an index, tag pages and luria new rfc and no reference checking at all — RFC-7 in prose was neither linked nor reported — and the documented shorthand DP-6 was invisible for the same reason, since DP_RE matched only design principles #6. ADR-006 made schemes configuration rather than constants; that generality reached rendering and scaffolding and stopped one layer short of the linter, which is the layer the promise was about. Found by building examples/ (ADR-045), not by review. Rejected: adding RFC_RE and friends beside ADR_RE (the same mistake with more constants); leaving Ref.kind as one kind per scheme (ADR becomes special again, which is the vocabulary ADR-006 rejected when it chose inactive-ok over adr-ok); and treating it as documentation-only by deleting the claim from the guide, which would trade a working feature for an accurate sentence.Active
ADR-047A declared family replaces the default; settings still mergeluria.toml now merges under two rules split by what a table is. A settings table — paths, code, lint, site — merges per key, so setting docs does not clear reports. A family table — schemes, fragments, journals, remotes — is replaced whole the moment the project declares it, and left on the shipped default when it doesn’t. Under the old single rule (everything merges), two limits followed that could only be documented, never obeyed: the shipped ADR scheme could not be removed, and a key it set could not be unset by omission — a project declaring [luria.schemes.ADR] dir = "decisions" inherited output = "docs/decisions" from the default entry and had its index silently relocated, on the documented adoption path. The distinction that carries the decision: a family’s entries are named by the project (ADR-006), so “you get the ones you wrote” is the only reading under which a family can shrink, while a settings table’s keys are named by Luria, where partial override is the point. Rejected: a removal sentinel (ADR = false — a second vocabulary for un-saying what merge said); replacing on any subtable write (makes [luria.site.theme.light] clear the site table); and the status quo, whose cost was two permanent warning labels in the configuration reference and a workaround line in the collocated example.Active
ADR-048The scaffold is planned from configuration, not copied from a treeluria init plans its scaffold from a config — one named with --config, the project’s own luria.toml if it already has one, or the shipped template’s — and writes the shape that config declares: a directory, template and view stub per scheme, a template per journal and fragment directory, and a docs index generated to list exactly the views this record renders. Config-first adoption follows: write the luria.toml you want, run luria init --config, and the record you declared is the record you get — RFCs and an incident log, with no decision directory you never asked for. ADR and a document-rendered DP keep the shipped rich templates (the decision doctrine and the seed principles are the content this package has to offer); any other scheme gets a neutral template using the {PREFIX}-NNN placeholder luria new already substitutes (ADR-036), and a stub titled after itself. The never-overwrite rule stands, with one hard refusal: --config against a project that already has a luria.toml errors instead of skipping, because scaffolding one config’s shape while another governs the record builds directories the project’s machinery doesn’t know about. Rejected: keeping the fixed-tree copy (it scaffolded decision directories for records whose configs declared none — the wart that prompted the question); a --scheme flag vocabulary (re-states in flags what the config already says); and templating the whole tree per scheme (Luria has no doctrine about what an RFC should say, and a neutral template is honest about that).Active
ADR-049Temporary codes at filing; concretized and aliased at the serialization pointA scheme can allocate its numbers at the merge point instead of at filing. With allocate = "merge", luria new adr issues a temporary code — ADR-tmp47fje, visibly provisional and never readable as a number — and the document is first-class on its branch: indexed, linted, citable as [[ADR-tmp47fje]], linked by the fixer like any neighbour. luria concretize, run wherever merges serialize (a merge queue, the merge-to-main job), assigns the next free sequential numbers in merge order, renames the files, rewrites every reference in the tree, and records each temporary code in the document’s formerly: frontmatter — which the resolver honours forever, so a temp code cited in a PR thread, a commit message, or another repository’s LU- reference is never a dead link, only an old name. luria concretize --check is the guard for the trunk: a temporary code on main is always wrong and mechanically fixable, so it fails (ADR-035’s admission rule). The motivating failure: filing-time numbering is a distributed claim on a global counter, so two branches both mint ADR-123 and collide at merge — rarely for one human, structurally for N concurrent agent branches. Rejected: filing-time numbering (the status quo, which lies about order under concurrency); timestamp codes forever (no collision, but trades away the small citable numbers the record’s culture runs on); reserving numbers against main at filing (a coordination round-trip per document, and racy without the serialization it was trying to avoid); and coupling this to an intermediate branch (concretization needs a serialization point, not a branch — a queue or the merge job serializes fine, and the batching question is separable).Active
ADR-050Titles in a transferable scheme are checked against the project’s own nounsA principle stated about the artifact it was first noticed on is one nobody applies to the next artifact, and nothing catches it: the document stays true, renders, and passes every check while quietly never being cited. The narrow-titles class reports a title that names one of the project’s own concrete nouns, in a scheme that opted in with titles_generalize = true. Luria ships no vocabulary — [luria.lint] narrow_terms is the project’s list, and empty means the class never fires, so a project that has not thought about this is told nothing rather than told it is clean. Titles only: body-linting was measured at 5 of 6 caught against 8 false alarms in 15, and a check wrong more often than right gets switched off. Fail-open by choice, and another sense is acknowledged with broad-ok: rather than by shrinking the vocabulary, which would stop the word working everywhere else. Rejected: shipping a default noun list (someone else’s vocabulary with the authority of a default), inferring narrowness from abstraction (fires on exactly the phrasings worth keeping), and requiring two worked instances before a principle may claim breadth (wants the citation graph, not a title scan).Active
ADR-051Prose frontmatter is what the generator renders, not what the author preferssummary: was the single frontmatter key treated as prose — scanned for bare references, linked by the fixer, checked by the lint — while everything else was data read by value. origin: broke that split: it is rendered as markdown into a principle’s metadata line, so a reference written there should be followable, but the machinery silently left it as literal text, which is the worst of both (it renders as a link if hand-written, and rots unchecked forever). Decision: a PROSE_KEYS set, currently summary and origin, and the membership rule is stated — a key is prose exactly when the generator renders its value as markdown somewhere. Rejected: making the set configurable, because a project cannot make a field prose by declaring it so; the rendering is what makes it true, and a config knob would let a project ask for links in a field that will display them as raw text.Active
ADR-052The draft signal: a contribution that asks for a verdict, not a reviewA draft pull request carrying a Proposed decision means the contribution itself is the question: the choice could only be weighed from the finished diff, the writeup argues the trade in both directions, and rejection is a live, cheap outcome — merge flips the decision Active, close files it Rejected, and either way the record keeps the reasoning. Use the signal when the work exists to settle its own worth (a worth-it experiment, the first live run of new machinery); skip it for agreed work, where a draft only slows the loop. Rejected alternatives: no signal at all (every PR presumes merge, making “no” socially expensive), and deciding before building (some trades are only visible in the diff).Active
ADR-053The published version is derived from the release tagRung 1 of the fix #93 started. pyproject.toml declares dynamic = ["version"] and hatch-vcs reads git describe, so there is one source for the version rather than two that can disagree. #93’s assertion stays as rung 2 — deriving prevents the drift, the guard makes a recurrence loud. Needs fetch-depth: 0 on the publish checkout, without which the version silently becomes a dev fallback. Rejected: keeping the field with a release checklist, and a bot commit that writes it before tagging.Active
ADR-054A scheme can constrain which of its tags combine[luria.schemes.X.tag_groups] declares sets of a scheme’s tags that combine under a rule — exactly-one, at-most-one, or a group forbidden by some other tag — and luria lint enforces it. tags.yaml has always said what a tag means and nothing has said which may appear together, so a vocabulary that is an axis rather than a pile left its rule to prose. Opt-in per scheme, so every existing record is unconstrained. Rejected: putting the constraint in tags.yaml (presentation, not policy), a closed flag alone (catches typos, not cardinality), and leaving it to each project’s tests.Active
ADR-055A link target is checked from where the prose rendersEvery reference check asks about a code — does ADR-035 name a document, what is its status, how is it spelled. None asks whether the path wrapped around it goes anywhere, so a hand-written target could be dead through ninety-nine links and eleven clean lints. broken-targets resolves each relative target from link_base — where the prose renders, not where the file sits — and reports what does not exist. A report rather than a lint error, because unlike a bare code a wrong path is not mechanically fixable: the fixer owns codes, and an arbitrary path is a typo only its author can resolve. Rejected: checking anchors too (a different and much noisier question), and resolving from the source directory (the frame a reader never uses).Active
ADR-056A scheme declares which statuses it uses and what they meanADR-003 closed the status words and lint-enforced them, on an audit finding that prose-governed surfaces drift and checked ones hold. What it left in prose is the layer above — what each word MEANS in a given scheme — and downstream that layer drifted twice. An optional statuses.yaml beside tags.yaml lets a scheme narrow the five and say what each one means, with the meaning rendered above the index table it explains. Rejected: adding words to the vocabulary, which is the thing ADR-003 bought; and a stub placeholder, which makes the legend a second edit nobody makes.Active
ADR-057 v2A scheme whose status never varies is reportedactive is what retired-citations reads, so a scheme where nothing is ever retired has an enforcement mechanism that cannot fire — and its build is green because nothing is being judged rather than because nothing is wrong. Downstream that state cost thirteen green builds over a scheme with fifty-one records at one status, twenty-three of which its own bodies refuted. Reported, not failed: there is no correct proportion. Exempt below ten records, for a document-rendered scheme, and for a project that has declared exactly one status on purpose. Version 2 gives the finding the acknowledgement every other one has: uniform_ok on the scheme, carrying a mandatory reason, moves it from a finding to a reported fact.Active
ADR-058Luria is a truth maintenance system, and should say soNobody could name the category, so every description reached for a new metaphor and none of them stuck. The category exists and is from 1979: a truth maintenance system maintains beliefs plus the justifications linking them, and retracting one propagates to everything that rested on it. That is the engine, exactly. What is ours is narrow and worth stating narrowly — the nodes are human prose, propagation halts at a finding instead of resolving itself, and acknowledgement is a first-class move. Rejected: claiming a new category, which costs us the prior art.Rejected — the README opens on what the record does, not on what to call it; the prior art belongs in the concepts page instead
ADR-059The configuration reference renders where its schema lives; every project gets its own record descriptiondocs/configuration.md — the generated reference for every luria.toml key — stops rendering into adopting projects and renders only in the tree that contains luria/config.py. The page is generated so it cannot drift from the schema, and that argument only holds where the schema is a file the reader can open: downstream it is a vendored copy of somebody else’s file, stamped edit luria/config.py, not this file at a reader who has no such file, going stale on their next upgrade with nothing in their repository responsible for it. In its place every project — Luria included — gets docs/record.md, generated from the loaded config rather than the schema: the families it named, where entries are filed, where views render, and the luria new command for each kind, taken from the CLI’s own dispatch table. The two answer different questions and only one of them is about the reader’s repository. Rejected: a config flag (a dial nobody would find, defaulting to the wrong answer for everyone who never looks); rendering both everywhere (keeps the vendored copy); deleting the reference outright (Luria’s own readers need it). luria index removes an orphaned copy on the first run after upgrade, guarded on the generator’s marker so a project whose own prose lives at that path keeps it.Active
ADR-060Schemes declare their own shape — where the vocabulary lives, and what a field meansA record with two content schemes could express the schemes but not the relationship between them or the vocabulary they shared, so both were restated by hand: one twelve-term vocabulary written in four places, and a citation rule that turned out to check only that a field was not blank. A scheme may now name its tags.yaml and declare what its reference fields hold. Rejected: scheme inheritance, which reduces the same lines without saying anything.Active
ADR-061A scheme’s template is a form, not a document_template.md is scaffolding the tool itself reads, so its example codes are illustrative by definition — yet they were scanned as citations, and a template using a realistic example reported a finding against itself. Templates are now exempt from the code machinery and still checked for link targets. Rejected: acknowledging it per project, which is a directive every scheme has to carry forever.Active
ADR-062A suppressed build is branch protection’s problem, not the lint’sA commit message describing the CI skip marker contains it, and so stops its own build. A checker was written for this and measured backwards: it cannot fire in the case that does harm, and does fire on commits that caused none. Required status checks answer the question that matters — does the commit being merged have a green check of its own — and no mechanism here can. Rejected: shipping the checker, and a commit-msg hook.Active
ADR-063Init takes shorthand and writes ordinary tablesA project that wants the defaults plus one scheme had to write the whole config, and three of a scheme table’s four lines follow from its prefix. --schemes/--journals accept NAME:kind and expand into the ordinary commented tables, and issue_url is inferred from the origin remote where the host is one whose issue path we know. Rejected: a compact form stored in luria.toml, which would be a second grammar every reader has to learn.Active
ADR-064Files are UTF-8; the console is the platform’sNothing in the package named an encoding, so every read and write took the platform’s — cp1252 on a default Windows install. A scaffold crashed writing a check mark, and a tree written that way was then unreadable to the same tool under UTF-8. Files are now UTF-8 unconditionally, because a record that only opens on the machine that wrote it is not a record. The console keeps its own encoding and stops raising. Rejected: UTF-8 output at a cp1252 console, which trades a crash for mojibake.Active
ADR-065A principle is written as a value, unless it is actually a ruleA principle drafted as “a record must not constrain the project it records” could only be satisfied or violated, which loses the state a value spends most of its life in — partly met, and moving. Rewritten as “meet the project where it is” it kept the same content and gained that reading. Principles are written in the aspirational voice by default; the constraining voice is reserved for the ones that genuinely are rules, which this record has two of. Rejected: requiring the positive voice everywhere, and adding a lint.Active
ADR-066 v2Remote content endorsed by hash, drift compared offlineA remote document has no status this project can read, so the citation checks that keep the local record honest stop at the project boundary. What is knowable is whether the cited bytes changed: luria remotes --pin stores a hash of the content a human endorses, --refresh records what upstream serves now, and the lint compares the two committed hashes offline — the remote-drift warning class, cleared by re-endorsing after review. A pin is registered per source (pin = true on a remote or scheme), per URL (a pin: flag where it is cited), or per code (an explicit --pin), and removing the registration retires it. Rejected: fetching in the lint (a check that fails on a train), mirroring upstream status (a projection of somebody else’s record), and hashing rendered pages (their markup churns under identical content — a remote with stable bytes elsewhere declares the location with pin_url, and a bare URL is pinned by a pin: flag where it is cited).Active
ADR-067A code relates to named URIs through one template vocabularyA foreign code had accumulated one subsystem per URI it related to: url templates for the reader’s link, a hardcoded GitHub blob construction, the document/anchor shape, pin_url templates for stable bytes, and a regex that parsed the rendered blob URL back apart to derive the raw one. Unified: a code relates to a SET of named URIs (read, bytes, and any name a project declares), each rendered through one template vocabulary in which the discovered filename is an ordinary variable carrying the lockfile’s authority. url and pin_url survive as sugar; the rebase regex is deleted in favor of shipped default templates. Rejected: a field and method per relation (the status quo), per-forge builtins, and deriving bytes by parsing whatever read rendered.Active
ADR-068 v2Source repairs are committed on the branch that authored them; generated views on the default branch onlyThe generation job ran on pull requests too, committing regenerated views onto every branch — so any two concurrent branches that added a decision or a devlog entry diverged on the decision index, its tag pages and the devlog book, and the second to merge conflicted on files nobody wrote. Six times across one stack, resolved the same mechanical way each time. The two kinds of write the job makes now land in two places: a source repair (luria repair — a bare code linked, a missing created: filled) touches only the files a branch authored and is committed onto the branch, where the review reads it; a generated view is a shared file every branch would rewrite and is committed on the default branch only. A pull request pushes its repairs, regenerates the views in the working tree, lints the result in the same job, and commits no view. Amends ADR-029, whose choice stands — a view is committed by something — while the somewhere moves. Rejected: a merge driver (the merge button does not run one), a documented routine of regenerate-on-conflict, and the first draft of this decision, which discarded the repairs on a pull request along with the views.Active
ADR-069A workflow file cites decisions in a form the generation job’s token can commit: by number, or in proseThe generation job pushes with the workflow’s own token, and that token cannot write under .github/workflows/; when luria concretize numbered ADR-068 it rewrote the temporary code in a ci.yml comment too, and the bot’s whole commit was refused. The job keeps the workflow token — no setup, fork-safe — and a workflow file cites a decision by its number or in prose; a temporary code there is the workflow-temp-codes warning class, enforced here and in the scaffold through fail_on. A project that gives its job a token with workflow write leaves the class alone and the bot rewrites the file. A person’s luria concretize was never constrained: the limit is the token, not the file. Rejected: a lint error regardless of token (the first draft, #150), a concretizer that skips workflow files, and dropping them from the code globs. alternatives that lost. Written to be read in a table row.Active
ADR-070A decision is stated for what it chooses; a prohibition is reserved for a constraint that has been verifiedThree decision drafts in one day carried a “never” the mechanism did not enforce — a title saying edges are never inferred from prose, a rule that a workflow file never cites a temporary code — and each was sent back in review. A decision is stated for what it chooses: the title and the decision paragraph say what is preferred and what is done, and a rejected alternative is named as such. A prohibition (“never”, “must not”) appears only where the constraint has been verified against the thing that enforces it, and the text names that enforcer. The “X, not Y” title names a rejected alternative and is not a prohibition. Rejected: a lint for the words (a rejected-alternative list legitimately says “never”), and leaving it to review, which is where it kept being caught. alternatives that lost. Written to be read in a table row.Active
ADR-071Typed edges come from fields, and a superseded document names its successor in superseded_by:The citation graph had one kind of edge, “A mentions B”, while facts the record states in structure went unread: a declared reference field, an influenced_by: list, and the successor a superseded document names. They are now typed edges, the field name being the relation, and the site renders each page’s edges both ways. superseded_by: is a built-in reference field on every scheme — the successor written as structure and read as structure, checked, resolved, an edge — where two drafts of this decision inferred it from the by CODE shape of a status note and were corrected on review: a field is concrete and checkable, and a relation inferred from free text makes the author’s prose conform to a shape the tool happens to recognise. Rejected: that inference; leaving it to Quartz’s untyped backlinks; a relations DSL.Active
ADR-072The status note is its own fieldstatus: Superseded — by FX-ADR-032 was one scalar carrying two types: a word from a closed vocabulary, and a prose note that was rendered, rebased for links, and split off the word in six places. The note is now its own field, status_note:, and a prose key like summary: — a code in it is a citation the fixer links. The successor a superseded document names is a reference field, superseded_by: (ADR-071); the note is for what the field cannot say. A note still riding in status: is a lint finding that luria index repairs, the way it fills created: from a path (ADR-031). Amends ADR-003, which placed the note after an em-dash. Rejected: leaving the scalar and parsing it forever, which keeps the field’s type a lie; and scanning status: as prose without splitting it.Active
ADR-073A finding names the key that declared its obligation; the record page lists the contract; no explain verbA lint finding said “(luria.toml)” and no more, which is one file today and the wrong table the day a second authoring surface exists. Findings now cite the key that declared the obligation, the generated record page lists each scheme’s whole contract from the same renderer, and there is no luria explain verb: provenance belongs where a surprised author meets it, and ADR-030 retired the last standalone report commands nobody ran. Rejected: the verb, a verbose lint mode, and a separate explanation renderer that would drift from the findings.Active
ADR-074A scheme can require one of several fieldsrequires = ["arxiv"] on a paper demanded the wrong thing: a report never posted to arXiv but carrying a DOI, or only a URL, has a source all the same. A field group names the need and the fields that satisfy it — [field_groups.source] over arxiv, doi, url, require = "at-least-one" — and the lint asks for one, naming all of them when none is there. Opt-in per scheme, shaped after tag groups (ADR-054). Rejected: keeping arxiv required; a list-valued entry inside requires; and typing each field as a source, which is the field-typing decision and not this one.Active
ADR-075A reference field declares whether it holds one code or many; a list where one was declared is a findingA reference field given a YAML list was stringified, its first code checked and the rest ignored, with no finding — structured input coerced to prose and half-read. A downstream world-building record has nine intentionally plural fields and found it. A reference now declares many = true to hold a list; every element is checked, resolved and becomes an edge, and a list where one code was declared is a finding. Rejected: accepting scalar-or-list on every reference (throws away the shape the author knows), min/max cardinalities (no measured need), and refusing a single value in a plural field (it is unambiguous).Active
ADR-076A frontmatter field can be backed by a scheme-local controlled vocabularyA downstream world-building record carries worlds: [A, C] on 37 of 75 entries, drawn from six values, absent meaning B, with a view per value wanted — a field that is not a reference (the values are not codes), not a tag (it is a second axis, not the browsing pile) and not a status (closed and single). Decided: a scheme declares the field under [luria.schemes.X.fields.NAME] with vocabulary, many, required and default, the values live in NAME.yaml beside the records shaped like tags.yaml, the lint holds the field to the vocabulary, the default is an effective value that never rewrites the source, and luria index renders a page per value. statuses.yaml and tags.yaml become the first two instances of the shape rather than special cases. Considered and priced: tags plus a tag group, a scheme of six documents cited by a plural reference, inline values in TOML, and implicit *.yaml wiring.Active
ADR-077Publish a nested record as a section of its parent site, staged by its own configsite.include_records mounts a whole record inside another’s vault. Each is staged by its own config, because the source-versus-view test reads from the reading config’s schemes — so a parent publishing a child’s files directly emits both a fragment and the view it renders into.Active
ADR-078A nested record’s views are generated and committed with the parent’sluria index regenerates every nested record’s views from the root, and --check fails on a stale one, so they are committed like this project’s own. include_records moves out of [luria.site] in the same change: it says a project contains other projects, which generation needs as much as publishing does.Active
ADR-079Read a note’s frontmatter comments as directive commentsA reference field is a citation site — superseded_by: naming a document that was itself later retired is reported at its line — but the markdown scan read only HTML comments, so the finding could be acknowledged only file-wide. Whole-line # comments inside the frontmatter are now directive comments, and in frontmatter a line-scoped directive reaches the whole YAML entry below it. Rejected: exempting reference fields from the retired-citation report, which would hide the one edge the report exists to question.Active
ADR-080A remote declares how to ask what an identifier is, and the lint may ask53 of 139 arXiv identifiers in one record resolved to real papers on unrelated subjects and stayed green for two years, because every check asked whether a reference points somewhere and none asked whether it points where it says. uris.title plus title_re say how to ask, and the lint asks about what the lockfile cannot answer — a citation is likeliest wrong in the minutes after it is typed, which is exactly when nothing has resolved it. Rejected: making the lockfile the boundary of what may be known, guessing the metadata API per host, and fuzzy title matching.Active
ADR-081inert-status measures a distribution, and its threshold is the project’s to setThe check fired only when every record shared one status, on the argument that one retirement proves the judgement is live. A 144-document registry disproved it: eleven exceptions silenced the check while a quarter of its entries went unexamined. uniform_share makes the threshold configurable and defaults to 1.0, so nothing starts reporting. Rejected: lowering the default, and inferring the threshold from scheme size.Active
ADR-082A field can be required by another field’s valueA record can state what it believes and had no way to state what would change its mind, so provisional documents accumulate with nothing saying what they are provisional pending. required_when demands a field only while another field holds one of a listed set of values — one field, one set, no expression language, because the value of the rule is that it is legible in the line that declares it.Active
ADR-083A relation is walked transitively and rendered as sequences, on one pageTyped edges gave every page its neighbours and no view answered “what sequence is this document a step in”, so the sequences stayed as prose, re-described once per participant, and went stale in two places at once. [luria.chains] walks a declared relation transitively and renders the lines on one page; a cycle and a one-sided comparison become findings.Active
ADR-084A relation declares its converse; symmetry is the self-converse caseA reference field may declare its converse, the field holding the same relation read backwards; a pair is mutual, same-scheme and plural on both sides, and a symmetric relation is its own converse. The declaration is what licenses luria link --fix to complete the missing side, and it completes by what changed since HEAD rather than by which side is empty, so a deleted relation removes its back-reference instead of returning. A document naming another in both directions, or two each claiming to come first, is a contradiction reported and never written, and a repair that would break the document it lands in is not applied. The check leaves broken-chains for its own class, one-sided-relations, because a pair is one-sided whether or not a chain walks it. Rejected: deleting the symmetry check, completing only symmetric relations (what #178 shipped), inferring the converse without a declaration, declaring it on the chain, a separate --prune flag, and making one side authoritative.Active
ADR-085Status is an ordinary controlled vocabulary; a built-in is a declaration nobody wroteWhere Luria’s code states a rule about how a record works, the rule moves to the project’s configuration and the code keeps only the role it fills. status: is declared like any vocabulary-backed field ([luria.schemes.ADR.fields.status] vocabulary = "statuses"), statuses.yaml becomes an ordinary vocabulary file, and the bespoke reader, the bespoke validation and the second spelling of the five words are deleted. A missing declaration is an error naming its remedy, not a silent fallback: a default inherited from code cannot be read, so nobody learns it is theirs to change. luria upgrade statuses writes the declaration into an existing project and carries a marker so it is removed once the version boundary is crossed. tags: stays out of scope until an open vocabulary and groups over a subset of values are each decided. Rejected: leaving status built in, defaulting the declaration instead of requiring it, and migrating tags at the same time.Active
ADR-086A marked region is how the README carries a derived factluria site has always known where a record is published, and nothing wrote it on the front page — luria’s own README hand-maintained the link one line below the region luria index rewrites. Adds a luria:site region rendered from Site, extracts the region machinery the third copy of which prompted this, and pairs the opt-in disclosure with an unlinked-site finding, since a marker nobody knows about is a feature nobody has.Active
ADR-087Identity is a field; the filename is a projection of itA scheme document carries number:, and its filename is derived from it — the model journals have always used for created:, applied to schemes. Identity in the filesystem was the obstacle to expressive filenames and aliases, because anything the name encoded became load-bearing. A counter rather than a UUID: the sequence carries the order documents were filed, and merge-safety is already solved by temporary codes.Active
ADR-088A derived alias is kept; a former spelling is rewrittenA scheme can render a second spelling from each document’s frontmatter, and luria link --fix leaves it written — the opposite of what it does with a formerly: entry, which it rewrites away. Two kinds in one map, each carrying its kind, because the fixer’s instruction differs and shape cannot tell them apart.Active
ADR-089 v2A derived field is read-only, unlike a derived URIA field declared with derive is computed from another field on every read and is never written down; writing one is a finding. ADR-066 settled the opposite for a remote’s URIs, where an explicit template beats the construction — the difference is that a URI template is a second source for something luria cannot otherwise know, while a written primary_topic: is the same fact as tags[0], stored twice and free to drift.Active
ADR-090The block below a directive is a syntactic unit, read from a grammar when one is installed-block scope guessed at a run of non-blank lines, which is wrong in every language that lets a construct hold one. An optional tree-sitter extra reads the real unit instead — one rule for every grammar, no per-language table — and is stated as a union with the blank-line block so it can only widen what a directive already governed, never narrow it.Active
ADR-091A stack of pull requests lands from the tipThis project squash-merges, which rewrites a branch’s commits into a new one. Merging a stack from the bottom therefore guarantees a conflict on the branch above it, between two commits holding the same text. Merge the tip: it already contains the whole stack, it lands in one operation, and the lower pull request is closed by hand as included.Active
ADR-092A derivation may follow a reference: one hop, against written frontmatterderive gains from, naming a reference to render the template against, so a fact one scheme owns can be read by another instead of copied into it. Rejected: a ref. namespace inside the template, which would have made the template language grow a second grammar; and resolving the target’s own derivations first, which buys chaining at the cost of cycle detection and an evaluation order.Active
ADR-093Fixture schemes come from a reserved namespaceFX is reserved for fixture codes that RESOLVE (ADR-034); the names under it — FXL, and FXM where a migration fixture needs a second — are reserved for a fixture project’s own SCHEMES, the codes that must resolve to nothing and be claimed by nobody. A leading-prefix reservation, checked by luria lint at the config rather than at each citation, so a project cannot quietly take the guarantee away from itself. Retires the three unlinted-file: opt-outs that stood in for it, which blanked whole suites to excuse a handful of specimen codes and are the one suppression the reports cannot converge past (ADR-033). Rejected: masking string literals in the tree-sitter grammar (narrows what a directive governs, against the stated invariant, and CI has no grammar installed, so it would be inert exactly where the codes are), one reserved name rather than a namespace (a migration fixture needs two prefixes, and the second would be invisible only by luck), a per-site unresolved-ok: (93 acknowledgement directives already live in 21 test files), and refusing the prefix at config load (fixtures declare it deliberately, so the refusal would break the mechanism it protects).Active
ADR-094 v2A cited document gets a page, and where a citation points is configurableA render = "document" scheme’s sources are published as pages, and a new cite key chooses whether a citation of its codes resolves to that page or to an anchor in the assembled view. This record sets “page”; unset keeps today’s behaviour. Rejected: emitting anchors that survive each publisher, and shipping the key without the pages, which sends citations off-site.Active
ADR-095An acknowledgement can carry its own deadlineA directive may be written until <YYYY-MM-DD>; after that date find stops returning it, so every check behaves as if it were never written, and luria lint reports what expired. Rejected: a relative duration, which needs an anchor the file does not carry; and a version milestone, which needs a project-version concept luria does not have.Active
ADR-096A rate limit is an answer, not a reason to ask againA 429 was retried three times with 3s→6s backoff, so under a sustained throttle every identifier paid 9 seconds to reach the throttled it already had after the first request — and Retry-After, the one piece of scheduling information a throttle carries, was parsed and spent on a message string. Fails fast instead, honours a short Retry-After, and stops asking a remote that has refused. Rejects a longer backoff, a global rate limiter, and persisting the refusal.Active
ADR-097A converse belongs to the scheme whose codes the field holdsconverse was resolved against the declaring scheme’s own fields, so a relation crossing a scheme boundary was sayable from one end and unreachable from the other — luria.toml said so out loud on NOTE.paper. The converse now lives on the scheme whose codes the field holds, with same-scheme as the case where those coincide. Rejects a converse_scheme key and a fully-qualified converse name.Active
ADR-098One config file, one format, vocabularies declared onceConfig was TOML, vocabularies were YAML files beside each scheme’s records, and the lockfile was JSON — three formats for one system, with no reason any of them could state. Worse, a vocabulary two schemes share had to be two files: in the corpus that motivated this, ten of thirteen entries had silently drifted. Moves to one luria.yaml with a central vocabularies: table, omegaconf underneath the existing validation rather than in place of it. Rejects keeping vocabularies local, and rejects letting structured configs replace the semantic checks.Active
ADR-099An anchor is an id, and a fragment link is checked against oneThe generator anchored assembled documents and journal entries with <a name="x">. That is reachable on a real navigation and nowhere else, so every one of those links worked in the repository and on GitHub and landed at the top of the page on the published site — 89 of the 100 fragment links in this repository. Emits id instead, and adds a check with a --fix, so the next hand-written anchor cannot put it back. Corrects ADR-094, whose measurement was right and whose stated cause was not.Active
ADR-100A generated link uses the anchor the page offers, and luria owns the slugA journal entry had two addresses: luria’s durable <a id="{timestamp}">, which its contents list linked, and the heading’s own slug, which is what the published page puts on the ¶ anchor and in Quartz’s sidebar. Both resolve; only one is what a reader copies. The generated links switch to the heading, which means luria now has to compute github-slugger’s slug itself — validated against 288 headings the site actually published, and guarded by a check that every generated fragment resolves. Rejected: putting the timestamp on the heading, which breaks Quartz’s sidebar entirely.Active
ADR-101The site builds on Quartz 5ADR-042 rejected Quartz 5 because v5.0.0 could not build at all, and said to revisit. The blocker is gone, and the upgrade pays for itself twice: the popover bug that sent a reader to the wrong section is fixed upstream, and v5 positions components from each plugin’s own entry, so the generated quartz.layout.ts — 90 lines of TSX luria wrote to move one component — disappears. Pins a commit rather than a tag, because v5.0.0 is still the newest tag and is the release that does not build.Active
ADR-102The TOML crossing carries comments, because a comment is not a valueluria upgrade yaml parsed a TOML config with tomllib and wrote the values, which is right — a regex in a uid does not survive a byte copy. But a config is documented in its comments, and tomllib never sees them: the crossing dropped 273 comment lines in one record and 85 in another, and reported only what it had folded. Comments are now carried to the key they documented, the vocabulary files are round-tripped rather than re-parsed, and the handful whose key does not exist on the far side are printed in full. Rejected: leaving them in git history, which is not where anyone reads a config.Active
ADR-103One reader for a document, and the listing it comes fromread_document caches a document’s parse and expires it when the file’s mtime moves — the bargain that lets repair write mid-run and read back. Eleven call sites did not take it, opening and parsing documents for themselves, so forget_documents() cleared a cache half the readers were not using. All eleven now ask read_document, and a test over the sources keeps it that way. Separately, Scheme.documents() re-globbed and re-sorted its directory on every one of 24,960 calls per lint; it and temp_documents() now share one walk cached on the directory’s mtime. Anthology lint 127.6s to 20.4s, output byte-identical. Rejected: a --only flag, which can report clean because it did not look.Active
ADR-104A directive’s argument list is syntax; its reason is proseActive
ADR-105A mention is not a citation, and says nothing about statusActive
ADR-106An invariant is a property of the relation, not of the chain that walks itinvariant becomes a key on a declared reference, so the assertion that two related documents share a field holds without a chain to walk it and may cross schemes. Chains stay within one scheme and are refused at load when they do not, in place of the KeyError they used to raise. Rejected: teaching the chain walker to carry far-scheme nodes, and making a chain’s output optional so an invariant could be declared with no view.Active
ADR-107A whole-field derivation holds what its source holdsderive = "{tags}" off a plural source derives a plural field. many says so and is checked against the source’s scheme rather than inferred, because a followed derivation reads a scheme the loader has not finished assembling. Both directions of disagreement are refused eagerly — the one that was possible before was silent, emptying every tag page of the scheme with nothing failing. Rejected: inferring many from the template, and leaving the blanket refusal in place.Active
ADR-108A rule carries its own alert, on the thing the rule belongs toA closed vocabulary’s finding says what is allowed. Whether the list is finished or merely short is a fact only the record knows, and it had nowhere to say it — so the message reads as “pick one of these”, which is how a vocabulary stops growing. Vocabulary and TagGroup each gain a alert:, printed as a continuation of their own violation. Rejected: the per-rule keys the issue proposed (closed_alert, required_alert), which turn out to be unnecessary once the alert rides the carrier rather than the field.Active
ADR-109A config object says what it is, and where it says it depends on what it islabel + blurb for a thing named inside a scheme — a vocabulary, a tag group, a plain field, a field group — matching what a relation already had; title + blurb for a thing that renders its own page, which is a scheme, a journal and a chain. A vocabulary’s pair lives in the central table, not on the field that invokes it, so a set two schemes share is described once. Rejected: putting it on the field, which is where the first implementation put it and where it would have drifted.Active
ADR-110 v2A vocabulary’s alert belongs to the set, like its blurbalert on a vocabulary moves from the field that names it to the central table, beside label and blurb. The rule it explains is a fact about the set, and a record whose three schemes name one vocabulary was otherwise writing the same sentence three times. The nested form’s key list is read off a dataclass shaped like the table, because the version that was spelled inline did not know about alert and refused the first record to use both features together; the key holding the values is terms, not values.Active
ADR-111A field the project declares unique, checked across the schemeunique: true on a plain field, or once on a field group to say it of each field in it, and a lint violation when two documents hold one value. Every other check asks whether a pointer resolves; this asks the converse, whether two documents resolve to the same place, and nothing did. A duplicate retired naming its survivor is the resolution and not the finding, so the check clears on the pointer rather than on a status. Rejected: a bespoke duplicate-source finding, a warning class, pooling a group’s values across its fields, and waiting on an alternative backend.Active
ADR-112 v2The lockfile is written where merges serialize, and the lint only reads itluria lint asked upstream about identifiers the lockfile could not answer and wrote what it learned. Caching the answer is right; doing it from a check that runs on every branch made remotes.lock.json a file every contribution rewrites. The write moves to luria remotes --resolve at the serialization point, via a resolve: input on the generate action; the ask stays, so a wrong citation is still caught on the pull request that adds it. Rejected: sharding the lockfile, a merge driver, and scoping the finding to the trunk.Active
ADR-113A chain’s page is organized by the invariant the chain declaresA chain that declares invariant already says what its members hold in common; the page now uses it, giving each value a section and listing the lines that share it underneath. Lines sharing nothing get a section of their own, because that is a finding about a line and this is where lines are read. Opt-in through the same key that opts into the check, so a chain without one renders exactly as before. Rejected: choosing one value per line, and a separate grouping key.Active
ADR-114A 406 from a metadata remote is a throttle, not an outagearXiv returns 406 and 429 interchangeably for the same identifier seconds apart when it is shedding load. _once classified 406 as unreachable, which skips the retry and — the part that cost something — never trips the circuit breaker, so a sustained refusal bought one socket per unverified identifier every run. 406 now classifies as throttled. Rejected: a fourth status, and leaving the RFC reading in place.Active
ADR-115Every request names the software and no contact, and the project adds the restLuria sent Python-urllib/3.11 — the language and nothing else — from three call sites that had already drifted apart. All three now go through fetch.request(), which carries the project’s user_agent. The default is luria/<version>: honest about the software, and carrying no contact, because a shipped contact routes every user’s traffic to whoever maintains luria. Rejected: a browser string, and a hardcoded project URL.Active
ADR-116Alternative backends are generated views; the sources stay filesluria export writes the record as a SQLite database — every document, field value, typed edge, citation and journal entry — rebuilt from scratch on each run and never written back to. That is what “store it in a database” buys: asking the record arbitrary questions. The sources stay one markdown file per entry in git, because that is the contribution model, not a storage detail. Rejected: a pluggable source store, a data-model refactor ahead of a second implementation, committing the database as a view, and a query command in place of the file.Active
ADR-117--body hands the prose to the caller, but never the headingluria new --body TEXT (and a draft’s body key) hands over a document’s prose, so a tool holding a finished document can file it through the CLI instead of writing markdown itself. The # CODE: title heading stays derived from title: — a body that opens with one has it dropped rather than doubled — because the lint holds the two equal. Rejected: letting callers write the file and validating afterwards, which makes every tool re-derive the numbering, the heading and the frontmatter shape.Active
ADR-118A temporary code nothing here mints is its own findingforeign-temp-codes reports a temporary code cited in this tree that no document in it mints — the branch-side guard, where concretize --check guards the trunk and legacy-spellings reports the aftermath. The rows are partitioned out of unresolved-codes rather than added to it, so each code is counted once under the reading that carries its remedy. Rejected: a directive of its own, since an illustrative code is unresolved for the ordinary reason and unresolved-ok: already covers it; and comparing against origin/main, which needs a network in the lint path and still misses an unmerged branch.Proposed
ADR-119luria ack: the code comes from the scan, the reason from a personluria ack writes an acknowledgement directive at every unacknowledged citing site the scan reports, with the code, the directive name and the location read from Scan and only the reason supplied by a person. It refuses over a document in force, a code nothing cites, and a citation already covered. Rejected: a --fix that writes them in bulk, since a generated reason vouches for nothing; putting it in luria repair, whose contract a judgement cannot satisfy; and an interactive prompt, which no CI or agent caller can answer.Proposed
ADR-120Unbound relations reach the lint, so an invariant can be declared over a residueunbound-relations and unbound-lines are lint classes, so lint.baseline can hold a declared invariant to a residue a project has read and accepted. This removes the all-or-nothing adoption cost without giving any individual row a third reading — ADR-049’s two-readings rule is untouched. Rejected: an unbound-ok: directive, which would retire a row and is the move ADR-049 refused; and closing the issue, which leaves the cost standing.Proposed
ADR-121The site delegates frontmatter to Quartz: luria composes it, the note-properties plugin renders itQuartz 5 parses frontmatter only in its note-properties plugin, and ADR-101’s config left it out — so every published page was untitled (blank search results, graph nodes labelled with paths) and its YAML rendered as a paragraph above the page. The plugin is now configured, and the record table luria drew into each body to make up for frontmatter rendering as nothing is gone: luria composes the staged frontmatter — Quartz’s own keys, a title carrying the code, the record’s facts as labelled properties with root-relative wikilinks — and Quartz renders it.Proposed