Architecture decision records

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 (34) — 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

Mechanism (58) — 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

Process (25) — 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

Ci (1): 069

Chronological

#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 journalnew). 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 v2The 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 allRFC-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