Changelog
Assembled from changelog.d/ fragments on a cadence — never hand-edited
(ADR-002).
2026-08-31
Changed
luria migratenow carries pinned endorsements through a scheme rename (#135, ADR-066 v2): for each remote claimed viaremotes = [...], a pin is re-keyed to the new spelling with both hashes intact — the endorsement is of content, which a rename does not change, and prune-and-re-endorse would have silently vouched for unreviewed upstream drift. The claimed remote’s discovered filename map is dropped for re-discovery instead (its keys and values both spell the old world);luria remotes --refreshrebuilds it, and upstream’s own rename later surfaces as ordinaryremote-driftfor review.
Added
luria remotes --pin [CODE]endorses remote content by hash (#135): the hash of each pinned document’s bytes is committed toremotes.lock.json,--refreshrecords what upstream serves now, andluria lintreports every pinned document that changed since its endorsement — the newremote-driftwarning class, promotable viafail_on. Re-endorsing after review clears the finding; a bare--pinendorses everything cited and prunes pins nothing cites any more (ADR-066).- A
pin_urltemplate on a remote (or remote scheme) declares where its stable bytes live, so content behind a rendered page becomes pinnable —pin_url = "https://arxiv.org/e-print/{1}.{2}"pins the paper an abstract page fronts. Declared rather than guessed: only the project can vouch that a URL is content-stable. - Arbitrary URLs can be pinned too: flag one where it is cited
(
<!-- pin: https://… — why it matters -->) and runluria remotes --pin. The flag is the registration — deleting it retires the pin, so a pin that fires too often costs one removed comment. pin = trueon a remote (or one of its schemes) registers a whole code family: every cited reference is pinned by a bareluria remotes --pin, and the lint reports any not yet endorsed. A bare--pinsyncs the lockfile to what is registered — config declarations,pin:flags, existing pins — and never re-endorses drifted content: that always takes the explicit--pin CODE, so a scheduled sweep cannot quietly launder a drift finding. This repo registers its own citedLU-ADRreferences.
Fixed
luria remotes --refreshno longer writes an authoritative empty map for a remote it could not read: a private repository’s failed discovery used to flip every one of that remote’s references to “absent from the remote”. Failure now leaves the remote off the lockfile — or keeps the map it already had — so it stays on the code-only convention.- The migration sweep (
luria migrate) skipsremotes.lock.json: its JSON nests a remote’s prefix away from its tails, so the composed-span mask could not tell a foreign pin key from a local code, and a scheme rename would have rewritten another project’s namespace. Machine-derived state is re-derived after a migration, never re-spelled.
2026-08-25
Added
- A design principle: exempting a ledger from one matcher exempts it from none of the others. A mechanism that rewrites instances of a pattern records what it rewrote, in the pattern’s own spelling — so every matcher for that pattern also matches its own ledger, and the exemptions do not transfer between them.
Added
uniform_okon a scheme: the acknowledgementinert-statusnever had. Every other judgment call in luria can be answered where it is raised, but that finding is about a scheme and a scheme has no line to comment on, so a project whose uniformity was deliberate had no move. Set it — a mandatory reason — and the scheme leavesinert-statusfor a newacknowledged-uniformitysection that still reports the count and status and appends the reason. It lapses on its own when a second status appears, and it cannot be promoted to a failure.
Changed
- ADR-057 is
Activeat version 2, having absorbed the acknowledgement. The check and the reply to it are one decision restated, not two.
Added
-
A design principle: meet the project where it is. A project picks its language, platform, forge and shape for reasons that have nothing to do with keeping a record; the record arrives afterwards and should fit what it finds. Held by being explicit in what the tool writes and forgiving in what it assumes — and by asking what has been assumed and never written down, since coupling to an environment rarely arrives as a decision.
-
A decision on how principles are worded: write one as a value unless it is actually a rule. The test is whether it can be partly met. If it can, the aspirational voice keeps the state a value spends most of its life in; if the only outcomes are satisfied and violated, it is a rule and should read like one. Two of the fourteen principles here are rules and keep their voice. The question is now in the principle template.
Fixed
- Every file the package reads or writes now names UTF-8 explicitly. Nothing
did before, so each one took the platform’s preferred encoding — cp1252 on
a default Windows install, where
luria indexcrashed writing a check mark into a status report, and a tree written that way was then unreadable to the same tool underPYTHONUTF8=1. Reproducible without Windows underLC_ALL=C, which is how it is now tested. - The CLI reconfigures its output streams with
errors="replace", so a terminal that cannot encode the arrow inluria init → pathprints?rather than a traceback. The console keeps its own encoding; only files are unconditionally UTF-8.
Changed
-
The README leads with the mechanism firing. A page cites a decision, the decision is superseded, and
luria lintnames the page nobody edited — before any vocabulary is introduced. Install and the sixty-second walkthrough follow; the families and the “ADR is not in the code” reveal move after them. -
The quickstart ends by breaking something on purpose: supersede a decision, see the finding land, then close it by fixing the citation or acknowledging it.
concepts.mdhad promised this and the quickstart had never delivered it — a self-checking system whose tutorial never catches anything. -
luria init --dry-runis in the first-run path, andCLAUDE.mdgets a sentence saying what it is and that nothing depends on it. -
The live record at dmarx.github.io/luria is linked from the README and declared as
Documentationin the package metadata. -
adopting.mdopens with what makes a record worth keeping, what happens if statuses never move, what should stay ordinary prose, how much machinery adoption adds, what is GitHub-specific, and what is least settled. -
CITATION.cff, which GitHub reads for its “Cite this repository” button, and a BibTeX block at the bottom of the README derived from it byluria index— a new generated region alongside the badges, checked for staleness byluria lint. Two hand-written copies of a citation is the drift DP-3 names, and a citation is a bad thing to have two versions of: the wrong one is the one that reaches somebody’s bibliography.No version in either. The version comes from the release tag, and writing one into a file by hand is the copy ADR-053 removed.
Fixed
- The ontology said every entry has a name, a standing and declared rules.
Journals and fragments have none of those — a journal entry is identified by
when it was written and carries no status at all.
statusand citation semantics now belong to referable documents, and the other families are described as what they are. - “What people read cannot drift from what people file” claimed more than the tool delivers. Hand-written prose drifts; what cannot is a generated view from its sources. Scoped accordingly.
- Two documents disagreed about how many worked configurations ship — one said four, one said five, and the fifth had been added a day earlier. Neither states a count now.
- The README enumerated four statuses of the five in the closed vocabulary.
- The prior-art section named Doyle, de Kleer, AGM and Dung without references; they have DOIs now.
Fixed
-
tests/test_prose_frontmatter.pywrites anADR-007.mdinto a temporary project as fixture data, and the number collides with this repository’s own ADR-007, which isSuperseded— so the scanner read five lines of fixture data as citations of a retired decision. Acknowledged at file level with what they actually are.The reference-status report now reads clean in every section: nothing cited unacknowledged, every code resolving, no stale annotations.
Added
luria configwrites theluria.tomlthatluria initwould have written, and stops. The shorthand covers the two things projects usually vary; anything else — a directory name, a narrowed status vocabulary, a tag group — is an edit to the config, and making that edit after a scaffold means moving directories the first run already created. Writing the config first and scaffolding second avoids the migration.--stdoutprints instead, and works where a config already exists.
Documentation
render = "index"versusrender = "document"is explained rather than named.modeling.mdgains the choice — one question about how the set is read, with the two checks worth testing an answer against — andproject-memory.mdgains a table of what each actually produces, including thatoutputmeans a directory in one and a file in the other.
Added
luria initinfersissue_urlfrom theoriginremote when one is not given, for hosts whose issue path is known (GitHub, GitLab), and reports what it used. The value cascades —[luria.site]derives its title, Pages URL and source base from it — so a repository with a remote scaffolds a correct record with no configuration at all.
Added
-
luria init --schemesand--journals, for a project that wants the shipped defaults plus a family or two:$ luria init --schemes "RFC,SPEC:document" --journals "incidents:day"Each entry is
NAMEorNAME:kind, and the paths follow the prefix. The shorthand is an argument rather than a stored format — what lands inluria.tomlis the ordinary commented table, so nothing reads it back and the config looks like every other project’s.
Added
docs/concepts.md, between the quickstart and the modeling guide: the shortest complete account of the model — entries, citations, the status field everything hangs off, what a finding is and how one is answered, and the prior art the mechanism comes from. The page existed before the docs rewrite and was dropped; the rewrite left a gap between “do the loop” and “choose between the options”, which this fills.
Changed
- ADR-058’s rejection note no longer rests on the concepts page not existing, since it does. The reason that survives is the one that mattered: the README opens on what the record does rather than on what to call it.
Fixed
- The README carried two badge blocks. The generator updates the first, so the second had frozen at numbers three weeks stale. It was left behind when the pitch rewrite inserted a new block above it.
Changed
- The section introducing the four families is
## Kinds of recordrather than## It is not only for decisions, which argued against an impression the reader has no way to have formed.
Added
- A scheme may name where its tag vocabulary lives —
tags = "record/topics.yaml"— so two schemes can share one file instead of keeping a copy each. - A tag may declare
primary_for: [LIT, SOTA], and atag_groupsentry that lists no tags derives its membership from those keys. One vocabulary file can now give two schemes different primaries without repeating the shared part. [luria.schemes.X.references]declares that a frontmatter field holds a code from a named scheme. Whererequireschecked only that a field was truthy, a declared reference checks that it is present, is a code, belongs to that scheme, and resolves.
Changed
- A scheme’s
_template.mdis no longer scanned for code references. It is a form the tool reads, not an entry in the record, so its example codes were reported as citations — a template with a realistic example produced a finding against itself. Link targets in templates are still checked.
Fixed
- Nothing yet broken by this; all three additions are inert until declared.
Changed
- Nine proposals adopted: ADR-041, ADR-052, ADR-053, ADR-054, ADR-055,
ADR-056, ADR-060, ADR-061 and DP-010. Each describes something the tree
already does; leaving them
Proposedsaid the question was open when it had been settled in code. - ADR-058, which asked the README to call luria a truth maintenance system,
is
Rejected. The README rewrite says what the record does in its own terms, and the concepts page the decision also named no longer exists.
Documentation
- A decision recording why the skip-marker checker was dropped: it cannot fire when the marker is on a tip that stays the tip, which is the case that does harm, and does fire on commits that suppressed nothing. Required status checks in branch protection are the answer, and are not something this package can ship.
Changed
- Test fixtures that model a principles scheme now use a
VPprefix overdocs/values.mdrather than borrowingDP. A rename of the real scheme would have swept them:luria migratewalks tracked source files on the grounds that “a source file answers as truthfully as a document does”, and a fixture’s codes are not claims about this record. Six test files, and theUPremote’s document-scheme fixtures with them.
Fixed
- Five demonstration codes carried no acknowledgement, so they sat in the
unresolved-codes report indistinguishable from typos:
DP-017inmigrate.py,ADR-123inconcretize.pyand again in ADR-049’s worked collision example,DP-018in ADR-040, and the[ADR-0string literal intest_concretize.pythat the scanner reads as a code. Each file now carries anunresolved-ok-file:directive next to the reason. The report reads “every code resolves” for the first time.
Added
docs/modeling.md— designing a record: what belongs in one, which family fits which material, the rule for when two kinds of entry are two schemes, what the schema can be made to refuse, and five worked shapes.docs/importing.md— turning material that already exists as data into a record, and what that transform surfaces.examples/knowledge-base/— a record of domain content rather than project meta-documentation: two schemes citing each other with separate statuses, required fields, and a one-primary-category rule. Built and linted by CI like the others.
Changed
- The README leads with what a record is — entries with a name, a standing, declared rules and generated views — rather than with the furniture it ships with or the file format it happens to use. Markdown is demoted to an implementation note explaining why plain files are chosen (participation) and saying plainly that nothing in the model depends on them.
- The README names four shapes a record can take.
project-memory.mdgains a Constraints section.requires,tag_groups,titles_generalizeandinert-statuspreviously appeared in the prose docs only as names in the lint contract.- A register pass over
modeling.mdandproject-memory.md: headers now label their contents rather than stating verdicts or withholding them, padded triads are cut to the number of things there actually are, and a few staged constructions state their finding instead. Em-dash density is left alone — this project’s own prose runs 2.5 to 3.5 per 150 words, and scrubbing that would make the docs less like the record they document. adopting.mddocuments two CI hazards: another workflow committing to the branch defeats the generate/lint handoff from outside, and a commit message containing the skip marker suppresses its own run.
Changed
- The hand-written documentation — README, CLAUDE.md, CONTRIBUTING, and
every prose page under
docs/— was rewritten from scratch against a deliberately stripped checkout, reorganized around five pages: quickstart, project-memory, cli, directives, and adopting.
Removed
- The docs pages
api.md,schemes.md, andin-practice.md; their subject matter is folded into the rewritten set.
2026-08-24
Added
docs/record.md— what this project’s record is made of, generated byluria indexfrom the loadedluria.toml: the schemes it named and the shape of their codes, where journal entries are filed and where the books render, the fragment directories, the remotes it can cite, theluria newcommand for every kind, and the settings it moved off the defaults. The filing table comes fromnew.kinds()— the CLI’s own dispatch mapping — so it cannot advertise a kindluria newwould reject (ADR-059).
Changed
docs/configuration.mdnow renders only in Luria’s own tree. The reference is generated so it cannot drift fromluria/config.py, and that argument only holds whereconfig.pyis a file the reader can open. Downstream it was a vendored copy of Luria’s schema, stamped editluria/config.py, not this file at a reader with no such file, going stale on their next upgrade with nothing in their repository responsible for it.record.mdis what an adopting project gets instead, and links the schema rather than restating it.
Upgrading
- The first
luria indexafter this release deletes an orphaneddocs/configuration.mdand says so, guarded on the generator’s own marker — a page at that path that Luria did not write is left exactly where it is. luria lintwill then ask for arecord.mdrow in yourdocs/README.md. That is a judgement call (you describe your own page), which is why it is asked for rather than guessed at.- Any prose that linked the old page needs repointing — at
record.mdfor “how our record works”, or at https://github.com/dmarx/luria/blob/main/docs/configuration.md for the schema.
2026-08-17
Added
- An optional
statuses.yamlin a scheme’s directory, besidetags.yamland shaped like it: which of ADR-003’s five statuses the scheme uses, and what each one means there. A record whose status the scheme does not declare fails the lint, and the meanings render above the index table they explain. check_status_vocabulary: astatuses.yamlkey outside the closed five is an error. Narrowing the vocabulary per scheme is the point; extending it is what ADR-003 bought and this does not sell it back.
Documentation
docs/configuration.mdanddocs/adopting.mddescribe the file, including the part that is easy to get backwards — the words stay closed, only their meanings and their per-scheme subset are yours.
Added
broken-targets: every relative markdown link target in record prose is resolved from where that prose renders —link_base, the same authorityluria link --fixuses to write one — and reported when it does not exist. A warning by default, nameable in[luria.lint] fail_on.target-ok:acknowledges a target that deliberately resolves to nothing, such as a link into a build output that CI writes but does not commit. The first directive whose argument is a path rather than a code.
Fixed
- The scaffolded decisions stub shipped two links that are dead in every
project
luria initcreates:[_template.md](_template.md)and[design-principles.md](../design-principles.md), both written relative to the stub’s own directory rather than todocs/decisions/, where it renders. - The scaffolded decision template wrote its supersession example as a link,
[ADR-NNN](ADR-NNN.md), so a placeholder read as a citation to a file nobody has. It is a code span now, matching the placeholder on the next line.
Documentation
docs/directives.mdgains atarget-oksection, and says why this one is about the path rather than the code every other directive governs.
Added
inert-status: a scheme where every record shares one status is reported.activeis whatretired-citationsreads, so nothing is ever retired there and the citation checks cannot fire — the build is green because nothing is being judged rather than because nothing is wrong. A warning by default, nameable infail_on. Exempt below ten records, for arender = "document"scheme, and for a scheme that declares exactly one status on purpose.
Fixed
- The published version and the git tag can no longer disagree. 0.4.0 was
tagged and released against a tree whose
pyproject.tomlstill said0.3.0, sopython -m buildproduced a 0.3.0 wheel and PyPI rejected it as a duplicate — after the GitHub release was already published, and withtwine checkand the cold-install smoke test both passing, because neither validates identity.pyproject.tomlis bumped to 0.4.0, and the build job now asserts the built wheel’s version equals the release tag (vprefix tolerated) before the publish job ever runs. The version was a hand-maintained projection of a source of truth kept in two places, which is what DP-5 predicts will drift; this is its rung-2 remedy — guard the property. Rung 1, deriving the version from the tag withhatch-vcs, is the better fix and needsfetch-depth: 0on the publish checkout, so it is left as a follow-up rather than bundled into a release-unblocking change.
Fixed
-
move_doclands a document under a temporary code, not a number. “The next free number” is no more a fact inside a migration than it is on a branch: every operation plans against the tree as it is now, so two moves into one scheme both read the same highest number, and the secondgit mvsilently overwrote the first. The move now mints a temp code (ADR-049) andluria concretizeassigns the real number afterwards, at the serialization point — the same bargainluria newalready makes, rather than a second allocator with its own arithmetic. The document ends up carrying both aliases: the code it migrated from, and the provisional one it wore in between. -
luria concretizerewrites the anchor spelling too. Its sweep was a case-sensitive replace, so it upgradedADR-tmp47fjebut walked straight past#adr-tmp47fje— leaving a live link pointing at a heading that no longer existed. Generated views are re-derived and were never at risk; a hand-written or migration-written link was. -
Pairno longer returns a tail typedint | str. The padded-number spelling and the opaque temporary identity are not the same kind of value, and collapsing them pushed the ambiguity out to every call site, which then had to test the type to learn which it had. Replaced withold_parts,new_parts,new_is_provisionalandnew_anchor_tail, so the padding question and the provisional question are asked separately — they are separate questions. A test now pins that a rename mirrors each citation’s own spelling:DP-004stays padded,DP-4stays bare, the anchor stays bare.
Fixed
luria migrate’s relink pass now stops where the hyperlink lint stops (#90). It walked every tracked file and linkified what it found there, whileluria link --fixwalksdoc_files()— the fixer running wider than the linter checks, which is the disagreementdoc_refsexists to prevent. The first realmove_docmigration turned two moved documents into a 499-file working tree, 469 of them exactlyHEADplus markdown links written into Python comments, TypeScript comments and workflow YAML. The sweep still walks every tracked file, and should: “does this text spell a code that moved?” is a question a.pycomment answers as truthfully as a document does. Only the linking half was scoped wrong.- A worded citation in a source file follows the move too.
#89 caught the prose-labelled form
by the address it points at — which works in a document, where the citation
is a link, and misses it entirely in code, where
(design-principles #17)is normally unlinked: no code for the code swap, no address for the address swap. Eight of them survived the strata-g promotion, naming a document that had moved. The sweep now respells them usingfind_refs, the same recognizer that would have turned the phrase into a link in the first place, so the two cannot disagree about what counts as a reference. - A
formerly:stamp is no longer reported as a dangling reference. The reference scan is deliberately unmasked, so it read the alias the move had just written and reportedDP-017 resolves to no documentagainst the file the migration had created — one warning per moved document, every time, for the one construct whose entire purpose is to name a code that resolves to nothing.sweep_textalready excludedformerly:blocks for the mirror reason (a later migration must not rewrite an earlier one’s trail); the two exclusions now sharedoc_refs.FORMERLY_RE, because they are one exclusion.
Fixed
- Every warning class
status_sectionscan emit is now nameable in[luria.lint] fail_on, and a test asserts it over the whole vocabulary rather than one class.legacy-spellingshad been emitted since rung one landed but was missing fromFAILABLE, so a project asking to enforce it was told “which is no warning class” — the dial rejecting a notch it was already printing on, which is DP-1 inside the guard written to catch exactly that. The tuple entry itself rode in unremarked with thenarrow-titleswork; this is the test that would have caught the omission, and the changelog line it never got.
Fixed
- A generated view the project gitignores is no longer reported stale. A
project can point
[luria.paths] reportsat a build directory and publish the result as a CI artifact instead of committing it. A fresh clone then never has the file, so missing read as stale — and the remedy the failure printed, “regenerate and commit the result”, is the one thing.gitignoreforbids. Downstream that meant a docs job red on every commit for a day, on a check nothing could satisfy, which is DP-1 wearing a green hat: the tool refused and its explanation was impossible to act on.--checknow excludes gitignored outputs from all three staleness kinds. Writing is unchanged —luria indexstill renders an ignored view, because not committed is not not wanted; that report is exactly what the artifact upload publishes. luria lintandluria index --checkshare one staleness rule set. They each had their own copy of the same three rules — stale view, orphan in a view directory, drifted README badges — and the fix above landed in one of them, solintwent on rejecting the identical tree the generator had just called current. That is the fixer/linter split this package exists to prevent, reproduced inside the package.adr_index.staleness()is now the single answer both consume; only the wording stayed with the linter, because a build log and a--checkwant different sentences. A test pins the invariant from outside: whatever one command says about a tree, the other says too.
Fixed
-
luria migrate’smove_docno longer leaves links pointing at a moved document’s old address. A move always crosses schemes, and a scheme’s address is more than its code — a same-render move changes the directory, a cross-render move changes the whole shape (page.md#anchor↔dir/CODE.md). Swapping the code inside the old link fixed the label and left the target pointing at a file that does not exist, silently, with the lint clean.Citations of a moved document are now found by the ADDRESS they point at rather than by their label, replaced with the new code, and linked by the fixer from the resolver — the one place that knows how each scheme is addressed. A worded citation is rewritten too, label and all: keeping the label resurrects the problem, because the
#17left behind is itself a reference the fixer re-links to the anchor the move just vacated.
Added
requires = [...]on a scheme — frontmatter fields it demands beyond the standard set. This is what makes a cross-schemeluria migratemove safe to automate (ADR-040): a document moved into a scheme whose template asks for fields the source never had cannot have them invented, so the move succeeds and the lint fails until a human supplies them. The machinery relocates a document; only a person vouches that it belongs.
Changed
origin:is prose, likesummary:— references written there are linked byluria link --fixand checked by the lint. It was already rendered as markdown into a principle’s metadata line, so a hand-written link displayed correctly while nothing maintained it: the worst of both, and a rot with no alarm. The reference machinery now reads aPROSE_KEYSset instead of namingsummaryin four places, and the membership rule is stated — a key is prose exactly when the generator renders its value as markdown. Deliberately not configurable: a project cannot make a field prose by declaring it so.
Added
-
luria migrate— execute a migration spec fromrecord/migrations.d/, renaming a scheme or moving documents between schemes without losing the record’s memory (ADR-040, now Active). Two operations:rename_schemerewrites a whole code family, following the scheme’s view, the remotes that mirror this project, and any extra config files named in the spec.move_docrelocates one document to another scheme, auto-numbered in the target. Withstrategy = "supersede"it copies instead: the source stays where it is, tombstoned asSuperseded — by <new code>, and is deliberately left out of the rewrite mapping so existing citations keep resolving to the original. That is the shape a promotion wants — the old document is still a true record of what happened, and only its output moved.
--dry-runprints the plan and changes nothing;--commitcommits and appends the migration to.git-blame-ignore-revsso blame reads through it. The sweep is mapping-driven, never prefix-driven: only enumerated pairs are rewritten, foreign composed codes (SG-DP-4) are masked because another project’s namespace is theirs, and the spec file itself is never swept — its mapping is written in old spellings on purpose. -
luria new migrationscaffolds a numbered spec, because execution order is information: a move can depend on a rename. -
luria/aliases.py— the alias map that migrations resolve through, derived fresh fromformerly:frontmatter rather than hand-kept. Complements the concretization-flavoured alias resolution already indoc_refs: that one answers for temporary codes, this one for any renamed code.
Added
-
narrow-titles, a warning class for a title that names one of the project’s own concrete nouns in a scheme whose documents claim to transfer. A principle stated about the artifact it was first noticed on stays true, renders, and passes every other check — it simply stops being cited, and nothing could see that. Two config surfaces:[luria.lint] narrow_termsfor the project’s vocabulary, andtitles_generalize = trueper scheme for the opt-in. Luria ships no vocabulary, so an adopter who has not configured one sees nothing at all — the class is absent, not empty. A word used in another sense is acknowledged in-document withbroad-ok:, through the same directive parser asinactive-ok:, rather than by shrinking the vocabulary and stopping it protecting every other document. -
A principle carried in from strata-g, luria’s first consumer: “It’s not mine, but I’ll pick it up anyway” — fix the debt you encounter whether or not it belongs to the task you came for, bounded by repair, don’t redesign and say what you picked up. Added to this record and to the
template/starter set.
Fixed
- The
DPscheme now usesallocate = "merge", which the decisions scheme has had since ADR-049. Without it two concurrent branches each took “the next free principle number” and both got the same one — a collision that had already happened here. A scheme that renders as a document is no less prone to it than one that renders as an index.
Added
-
DP-010, “One decision, one thing.” A decision with two unrelated halves is one nobody can cite half of: the second half has no code, so nothing can point at it; superseding the first silently retires reasoning nobody meant to withdraw; and the alternatives section quietly covers whichever half the author found more interesting. The test is whether the two halves could have been decided differently.
Earned on the second re-derivation, per the rule for adding one — three splits in a single session, each made for this reason and none of them by rule.
Documentation
- Templates and the decisions stub now point at
luria new <kind>instead of telling the reader to copy_template.mdby hand. The copy instruction predates the command and had outlived it:new’s kinds are derived from config, soluria new <kind>works for a scheme the moment it is declared, and it assigns the identity — which hand-copying does not, and which is how two branches end up claiming one number. Which identity depends on the scheme’sallocatemode, so the comment names the mechanism rather than one of its two outcomes:filingtakes the next free number on the spot,mergemints a temporary code thatluria concretizenumbers where merges serialize. Fixed in both the shippedtemplate/scaffold and this project’s own record, so an adopter and a maintainer read the same instruction.
Added
- ADR-058: luria is a truth maintenance system, and should say so. Nobody could name the category, so every description reached for a new metaphor. The category exists and is from 1979. The documentation now leads with the mechanism — retract a premise, and the build names what rested on it — and gives TMS as the second sentence.
docs/concepts.md— the model and its prior art.docs/quickstart.md— fifteen minutes ending in a real finding.docs/schemes.md— designing record families beyond decisions.docs/cli.md— every command, and the CI wiring including the version-split trap.docs/api.md— the Python surface, with stability marked.docs/in-practice.md— the three existing records compared: luria itself, strata-g, and a corpus project. What varied, what drove each choice, and the short list of things all three do the same way.CONTRIBUTING.md.
Changed
- Every hand-written page rewritten from scratch:
README.md,docs/README.md,docs/adopting.md,docs/directives.md,docs/project-memory.md. The README’s four competing self-descriptions are replaced by one lead and one placement.
Documentation
- An ADR (Proposed) for the draft signal: a draft pull request carrying a
Proposeddecision 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 decisionActive, close files itRejected, and either way the record keeps the reasoning. Use it when the work exists to settle its own worth; skip it for agreed work, where a draft only slows the loop.
Changed
- A declared family replaces the shipped default (ADR-047).
schemes,fragments,journalsandremotesare now yours entirely the moment you declare them: a record of RFCs and specs has no phantom ADR scheme, and a declared scheme’s omittedoutputis genuinely unset — the view renders beside its sources, as the docs always said it would. Settings tables (paths,code,lint,site) still merge per key.
Upgrading: a config that declared part of a family while relying on
the rest from the defaults — say [luria.schemes.DP] alone, expecting ADR
to persist — now owns the family it declared. Add the missing entries
explicitly; the shipped template always declared its families in full, so
records scaffolded by luria init are unaffected.
Added
luria init --config my.toml(ADR-048): write theluria.tomlyou want and init installs it and scaffolds exactly that shape — a directory, template and view stub per scheme, templates per journal and fragment directory, and a docs index listing the views your record actually renders. A project that already has aluria.tomlnow gets its shape scaffolded rather than the template’s.--configagainst a project that already has one is a hard error, never a silent skip.
Fixed
- An index-rendered scheme with no
README.stubis titled after itself rather than# Architecture decision records— the same defect the document render had, fixed the same way. - A fresh
luria init→luria index→luria lintruns clean again: two bare references in the template (LU-ADR-048in the docs index prose,DP-1in the principles stub) became visible to the scheme-driven reference detection and would have made every new scaffold start red. The three-command adoption loop is now a CI-run test, so the class stays closed.
Changed (review round)
luria newstamps an unnamed fragment with its filing moment (20260812-021035.md), the identity the devlog already uses, instead of naming it after the git branch — which collided the first time a branch was restarted after a squash merge and refiled (ADR-036, v2).--nameremains the explicit override and still reopens rather than duplicates.- Generated views are marked
linguist-generatedin.gitattributes, so PR review collapses them by default and a contribution’s diff reads as its sources. The views stay committed; only review’s rendering changes.
Proposed
- ADR-049: schemes gain an
allocate = "merge"mode —luria newissues a temporary code (ADR-tmp47fje) that is first-class on its branch, andluria concretize, run where merges serialize, assigns real numbers in merge order and records the temporary code as a permanentformerly:alias. Filed from the review discussion on #76; implementation to follow in its own PR.
Changed
- The published version is derived from the release tag rather than a
hand-written
pyproject.tomlfield, completing the fix #93 began:hatch-vcsreadsgit describe, and the publish checkout fetches tags so there is something to describe against. #93’s assertion that the built version matches the tag stays — deriving prevents the drift, the guard makes a recurrence loud.
Fixed
- A scaffolded project no longer starts with dangling references. Three
illustrative codes in shipped templates came from the real sequence and
resolved to nothing in a fresh scaffold (
ADR-049in two_template.mdfiles,ADR-001inCLAUDE.md); they now use theFX-fixture prefix. Three more in the scaffolded workflows cited Luria’s own decisions bare, so they read as the adopting project’s decisions — they now compose asLU-. A freshinit+index+lintwent from 5 unresolved codes to none.
Fixed
- A tag page names its own scheme. Pages for a non-ADR scheme were headed
“ADRs tagged
x” and counted “N of M decisions”, regardless of what the scheme actually holds — the same wartDEFAULT_STUBalready avoids for the index. - A tag blurb keeps its capitals.
str.capitalize()lowercases everything after the first character, so any blurb running past one sentence, or naming anything capitalised, was silently downcased.
Added
[luria.schemes.X.tag_groups]— a scheme can declare which of its tags combine, andluria lintenforces it. A group takestags, an optionalrequire(any,at-most-one,exactly-one), and an optionalexcluded_bynaming tags that forbid the group. Opt-in per scheme, so a record declaring no group is unconstrained.tags.yamlhas always said what a tag means; this says which may appear together, for vocabularies that are axes rather than piles.
Added
luria indexnow rendersdocs/configuration.md, a reference for everyluria.tomlkey generated from the config dataclasses themselves — prose from their docstrings, key tables fromdataclasses.fields(). A key that exists in the schema is a documented row whether or not anyone remembered to describe it (ADR-044).
Documentation
- The docs say what Luria can be configured into, not only what it ships
as.
docs/adopting.mdgains “Shaping the record to your project” — worked examples for a second document family, a second journal, collocated views, fragment styles,uidremotes for citing things that are not Luria records (arXiv identifiers, ticket keys), and thefail_onenforcement dial. - The README and the scaffolded
CLAUDE.mdnow say plainly that the four shipped subsystems are a default rather than the machinery’s fixed parts, and point at the configuration reference. - Both documents state a limit rather than leaving it to be discovered: adding a scheme costs one table, but renaming one is still a manual pass (ADR-040).
Added (examples)
examples/holds four complete, working projects — RFCs beside specs, a collocated layout, three journals at three granularities, anduidremotes citing arXiv papers, Jira tickets and CVEs.tests/test_examples.pybuilds each one and runs the realluria indexandluria lintagainst it, so these are configurations CI defends rather than prose (ADR-045).
Fixed
- A
render = "document"scheme with noREADME.stubno longer emits the heading# Design principlesregardless of its prefix. A SPEC family rendered as a document is titled after itself. luria init’s scaffoldeddocs/README.mdnow lists the configuration reference, so a freshly initialized project passesluria linton the first run as the adoption guide promises. An existing project upgrading will see one docs-index violation naming the missing entry; adding the line clears it.- Two wrong claims in the new adoption guidance, both caught by building the
examples:
activeselects from the closed status vocabulary and cannot extend it, and omittingoutputdoes not collocate the shippedADRscheme (set it equal todir). Both are now documented accurately and pinned by tests.
Fixed (reference checking)
- Every configured scheme is now linted and linked, not just
ADR. Reference detection matched three hardcoded patterns, so a project with anRFCorSPECscheme got indexes, tag pages andluria new rfc— and no reference checking at all.RFC-7in prose was neither linked nor reported (ADR-046). - The bare
DP-6spelling is found.CLAUDE.mdand the scaffolded template both tell contributors to write the bare code and letluria link --fixspell the target; for design principles that had never been true, because only the prose spelling (design principles #6) was matched. Applying the fix linked 38 references in this repository that had accumulated unseen. - Cross-scheme references resolve in both directions — a file link into an index-rendered scheme, an anchor into a document-rendered one, each from the base where the citing text renders.
Upgrading: references your record has been carrying unchecked will become
violations in one pass. Run luria link --fix and read a sample of the diff
rather than trusting it wholesale.
Fixed
- Two acknowledgements stopped applying when the sequence reached ADR-053.
The specimen lists in
ADR-014andtests/test_adr_index.pyborrowed a code from the real sequence, and a real fifty-third decision made it resolve. This is the second time —ADR-032went the same way — soADR-014now records that trimming the list is the symptom fix and theFX-prefix is the cause fix.
Fixed
- A generated scheme’s index no longer renders stray
{and}. TheREADME.stubscaffolded for every non-ADR scheme carried{{categories}}and{{table}}— thestr.formatescaping convention — whileinit.pysubstitutes withstr.replace, so the doubled braces survived into the file and every generated index carried two literal braces. The hand-shipped decisions stub uses single braces and was always correct, which is why this only affected schemesluria initgenerated.
Changed
- This repository’s own record now allocates at merge (ADR-049,
adopted):
luria new adrmints a temporary code on the branch, and the push-to-main docs job runsluria concretize— withconcretize --checkguarding the same run. The sharedactions/generatecomposite gained aconcretizeinput, gated to non-PR events, and the scaffolded template workflow passes it the same way, so an adopter flippingallocate = "merge"gets the serialization-point wiring free.
Added
- The
legacy-spellingswarning class (ADR-040, rung 1 complete): a citation still written in a concretized code’s old temporary spelling is reported with its remedy —path:line ADR-tmpxxxxx → ADR-123— and promotable to a failure via[luria.lint] fail_on.luria link --fixupgrades the spelling to the canonical code rather than engraving the old name into a fresh link. The in-tree steady state is zero, so a row means an in-flight branch merged after a concretization pass. Theformerly:field itself is excluded — it is the alias record, not a citation.
Decided
- ADR-044 through ADR-049 — the configuration reference, executable examples, scheme-driven reference detection, family-replacement merge semantics, config-planned init, and merge allocation — are now Active.
Changed
- The decision index gains a real Title column. The middle column was one blob — the summary when present, else the title — under a header that said “Title”, so any document with a summary showed its summary mislabelled. Rows now read code | title | summary | status, and a document without a summary gets an honestly empty cell rather than its title twice.
Added
- Merge-allocated schemes (ADR-049):
allocate = "merge"makesluria newissue a temporary code (ADR-tmp47fje— a tail that can never be read as a number) instead of claiming the next number from a branch. Temporary documents are first-class: indexed, linted, citable bare or as a wikilink, cross-referencable before they have a number. luria concretize: run wherever merges serialize, it assigns real numbers in merge order (commit time, the ordering the changelog collector already trusts), renames the files, rewrites every reference — history included, journals and the collected changelog too, so exactly one spelling of each code exists in the tree afterwards — and records each temporary code in the document’sformerly:frontmatter.- Permanent aliases: a code listed under
formerly:resolves forever, in both the bare and wikilink spellings — for the citations no rewrite can reach: PR threads, commit messages, other repositories, and branches cut before concretization, which merge clean and modernize on their nextluria link --fix. luria concretize --check: the trunk’s guard — exits 1 naming any temporary code, for CI on the default branch.
The default is unchanged: schemes without allocate = "merge" number at
filing exactly as before.
2026-08-10
Added
- Wikilinks (ADR-025,
#9):
[[ADR-013]],[[SG-DP-18]],[[ARXIV-2403.05530|a label]]— typed references the author asserts, resolved against everything the machinery can construct (local scheme codes including the bareDP-3spelling, document-scheme anchors, remote and uid-remote codes, issue numbers with no cue needed).luria link --fixconsumes them into plain markdown links; an unresolvable wikilink is a lint violation with its causes named, because an explicit request deserves an explicit refusal.
Fixed
- The published front page shows its banner again
(#70):
luria siterecognised a relative target after](and inside<a href>, but not inside<img src>— the form a README reaches its logo by, since markdown isn’t parsed inside an HTML block (ADR-005). The image was neither staged nor redirected nor counted, so the run reported nothing to place while dropping one. Any project whose docs centre an image in raw HTML was losing it. - The graph view sits above the article, not below it
(#71): Quartz stacks its
sidebars under the content below 1200px, so on most windows — and on every
phone — the graph the site exists for was the last thing on the page. It
moves into the content column, directly under the title, uniformly at
every width, with its parameters retuned for a column twice a sidebar’s
width.
luria sitenow writesquartz.layout.tsas well asquartz.config.ts, so a project’s layout is Luria’s to decide rather than whatever the generator defaults to. - The landing page has a name. The README is published as
index.md, and a README that opens with a centred logo gives a site no title to read — so the front page was calledindex. It now carries the site title, and an alias so anything still pointing atREADME.mdkeeps resolving.
Added
- The published site can wear your brand
(ADR-043,
#13): four optional
[luria.site]keys —icon,logo,logo_dark, and athemetable that merges over the generator’s palette by name. An unknown colour name is refused with the known ones listed rather than dropped, and a project that sets none of them gets exactly the site it had before.- The favicon is rasterized during the build, from whatever
iconpoints at, using thesharpQuartz already depends on. Point it at the vector master: no derived PNG is committed, so none can drift (DP-3). - The logo replaces the site title in the sidebar, baked once per
theme. Artwork exposing a
--luria-inkcustom property is re-inked to each theme automatically; anything else needslogo_darkor is used as it stands.
- The favicon is rasterized during the build, from whatever
- Luria’s own record wears the brainslug kit: paper and ink from the
kit’s two colours, the horizontal lockup in the sidebar, and a new
luria_project_memory_icon.svg— the mark on a paper badge, contours thickened so the line art still reads at 16px — as the favicon.
Fixed
actions/siteno longer fails the build for a project with no favicon (#73): the icon lookup usedls … 2>/dev/null | head -1, and under the step’s ownset -euo pipefailan unmatched glob ends the step before Quartz ever runs. Silencing a command’s stderr reads as handling its failure and isn’t. It could not bite this repository, which always configures an icon; it would have bitten the first adopter who didn’t.
Added
- Another project’s decision is cited as
LU-ADR-013— a registered remote prefix composed with that project’s own code (ADR-016). One[luria.remotes.LU]entry makes it a first-class reference:luria link --fixwrites the URL,luria lintfails on a bare one, and the citation scan no longer has to guess which project a code belonged to. luria remotes— what is configured and how each foreign reference resolves;--refreshdiscovers code→filename maps from a public repository into a committedremotes.lock.json;--checkprobes reachability. A remote that follows ADR-013 needs no lockfile: the code is the filename.- Two remotes are registered, and their difference is the point.
SGis the pilot this package was extracted from — private, filenames not yet converted, so--checkreports it unverifiable.LUis Luria itself, which theluria initscaffold cites instead of pasting GitHub URLs into a new project’s templates, and which--checkverifies for real. The mechanism is exercised by the package, not only by its tests — which is how the*.stubhole below was found. - A citation may name a document before its URL resolves
(ADR-017).
SG-ADR-032404s today and will land when strata-g’s record is ported; naming the document is the durable half, and the whole set flips tookin one--checkrun when it does. version:is standard frontmatter for every scheme, not just principles. Shown in the decision index only when it isn’t 1, because a column of ones teaches nothing.
Fixed
*.stubfiles are linted. A stub is the hand-written prose of a generated view: the lint skipped it for not being markdown, and skipped the page it renders into for being generated, so a bare reference written there was invisible to both checks at once.- A code whose remote has been discovered is no longer guessed. Once a lockfile has been read from a remote, its silence about a code is authoritative and the reference stays unlinked and reported.
--checkno longer reports a private repository as a shelf of 404s. It probes the repository once and says unverifiable — an anonymous 404 is not the claim “this document was deleted”.
Changed
- Discovery reads public repositories over HTTPS only. The local-clone
option is gone: a resolution that depends on what happens to be on somebody’s
disk produces a committed lockfile nobody else can regenerate. A remote Luria
can’t read gets a
urltemplate, not a credential path (ADR-016 supersedes ADR-015).
Changed
- The repository layout now states the read/write boundary
(ADR-021,
#3):
docs/holds everything a reader browses — prose plus every generated view — andrecord/holds everything a contributor files, each container inside carrying the.dsuffix (record/decisions.d/,record/principles.d/,record/changelog.d/,record/devlog.d/). What you read atdocs/Xyou file atrecord/X.d.CHANGELOG.mdstays at the root, where convention puts it. - A scheme’s
outputis now separate from its sourcedir: the decision index and its tag pages render intodocs/decisions/while the ADR files stay inrecord/decisions.d/, with link rebasing derived from the actual paths. A scheme with nooutputkeeps the old collocated layout unchanged, so existing projects upgrade without moving anything. README.stubandtags.yamllive with the sources; a stub’s links resolve from where the index renders.- The journal’s front page now inlines the current book’s contents, newest entry first, above the shelf of older books — the newest writing is one click from the entrypoint instead of two.
luria initscaffolds the new layout; the template’sdocs/README.mdandCLAUDE.mdexplain the boundary.
Added
-
A view directory holds only what the generator wrote — anything else in one is a lint violation naming the file and the remedy. This generalizes the old orphaned-tag-page check to every view directory, and also catches a journal book stranded by a granularity change.
-
DP-9 — structure is read before text, so affordances are spent deliberately: on shaping attention, on making locations discoverable, and as smells to read when they turn inconsistent. A structural signal beats a documentary one; the read/write boundary is the worked application.
-
A new comment directive,
url-ok— a link whose label is a composed foreign code (SG-DP-18) but whose URL is hand-written rather than constructed is reported as a warning until acknowledged, because a hand URL is frozen at writing time. Same shape and scope rules as every other directive; stale acknowledgements report themselves. Foreign codes only — ADR-022 records why it does not widen to local codes or arbitrary hand-targeted links.
Fixed
- The README badges’ link target is derived from configuration instead of a
hardcoded
docs/decisions/README.md.
Added
- The record publishes as a browsable site
(ADR-042,
#13):
luria sitestages the record as an Obsidian/Quartz vault — pages at their repository paths, plus aquartz.config.tsderived fromluria.toml— and the newactions/sitecomposite action builds it onto GitHub Pages. The citations the lint already guarantees are links become a graph, backlinks, full-text search and per-tag pages, none of it maintained by hand. Luria publishes its own record with the same action adopters get, and the scaffold ships the workflow (ADR-029). One step cannot be scaffolded: set Settings → Pages → Source to “GitHub Actions”, or the deploy job fails with “Pages is not enabled” while the build stays green. [luria.site], and almost nobody needs it: the site’s title, its Pages URL, and the base a link falls back to when it points at a repository file the site does not publish all derive fromissue_urlfor a GitHub project (DP-3). Onlyexcludeis genuinely per-project.- Decisions carry a record line on the site: status, date, issue and
influenced_by, rendered under the title. Those facts live in frontmatter, which a site renders as nothing — so without it a superseded decision reads on the web as current.
Fixed
- Generated index links are normalized
(#67): a summary rebased for
the view directory emitted
../../record/decisions.d/../../docs/design-principles.md#dp-2— valid on GitHub, which collapses it, and a 404 under any generator that doesn’t. Twenty links in this repo, invisible for as long as GitHub was the only reader. Runluria indexto pick up the short form.
Added
- Published to PyPI (ADR-027,
#3):
pip install luria. Publishing runs through GitHub trusted publishing — apublish.ymlworkflow whosepypienvironment identity is the whole credential — on every GitHub release, gated by a cold-install smoke test that scaffolds a fresh project from the built wheel (init → index → journal new → lint).
Fixed
- The scaffold ships inside the package (
luria/template/in the wheel) instead of leaking a baretemplate/directory intosite-packages, where it would have collided with any other package shipping one.luria initresolves the packaged location first and falls back to the repository top level in a checkout. - A freshly scaffolded project now lints with zero warnings: the illustrative wikilinks in the template’s CLAUDE.md no longer read as dangling codes.
Added
- Design principles are fragments, and
docs/design-principles.mdis generated from them (ADR-012). One file per principle indocs/principles/, with frontmatter carrying aversion(principles are living documents — two of Luria’s eight are at v2, and now say so),influenced_bybacklinks to the decisions whose experience produced them,history:for what changed between versions, and anoriginnote. - A scheme declares how its view is rendered.
render = "index"is the browsable shape — a table plus per-tag pages;render = "document"concatenates the bodies into one page for a set that is read as a whole. This is the first exercise of ADR-006’s claim that a second scheme is a config entry and a directory: no scanner changed. docs/principles/_template.md, and principles scaffolding inluria init— a fresh project now gets five seed principles as fragments rather than one hand-maintained document.
Changed
luria indexregenerates every scheme’s view, not just the decision index, soluria lint’s staleness check covers a newly configured scheme the moment it exists.- Links to a principle use a stable
#dp-Nanchor. The generator emits<a name="dp-N">beside each heading, andluria linkprefers it over the heading slug: a principle is a living document, so a heading-derived anchor stops resolving the moment the wording moves — silently, which is the fail-stale polarity DP-3 rules out. Projects whose principles are still one hand-written file keep the heading-slug fallback.
Fixed
- Tag pages no longer credit a script that doesn’t exist here — the
generated header named
scripts/ci/build_adr_index.py, a leftover from the corpus Luria was extracted from.
Changed
ADR-018is atv2. Its rejection of the endpoint-badge alternative cited ADR-002’s per-merge bot commit, which over-applied it — that hazard depends on a file being appended to at a marker and carrying assigned numbers, and a derived badge file has neither. The decision is unchanged; the reason it gives is now the real one (a baked-in URL is correct per commit, so a reviewer sees the count move in the diff).- Contributions to this repository go through a pull request. A decision record is an interpretation of somebody’s intent, and it should be read before it becomes what the project believes.
Added
- ADR-019: a wrong reason is corrected in
place and versioned; a changed choice is superseded. Superseding over a
bad argument retires a decision still in force and points every citation at
an identical claim. “Never rewrite a body” objects to silent revision — a
versionbump with ahistory:note saying what the old version got wrong is the opposite of silent.
Documentation
- The docs no longer read as “these documents are frozen.” Project memory gains a section on what is and isn’t revisable, with a table of the four shapes — choice changed, reason wrong, value reworded, consequence falsified — and a live example of each from this repository, because a rule a project has never applied to itself is a rule nobody has tested.
- ADR-001 is at
v2. Its traffic rule said a decision is “superseded but never rewritten”, which reads as immutability and leaves no way to fix a wrong argument short of retiring a decision still in force. Narrowed to the case it governs — supersede when the choice changes — withhistory:recording the over-broad version. The rule about which layer holds what is unchanged. - The decision templates, both index stubs,
CLAUDE.mdand the adoption guide now say the same thing, and the scaffold points a new project at Luria’s worked examples by remote code rather than a pasted URL.
Added
- Per-scheme remote mappings
(ADR-023,
#6): a remote’s code families
construct independently via
[luria.remotes.X.schemes.Y]—dirfor file-per-code schemes,documentplus ananchortemplate for schemes whose documents are sections of one assembled page, or aurltemplate. The anchor defaults to the stable shape Luria’s document render emits (dp-{number}), so a remote on current conventions needs onedocumentline:SG-DP-18now constructs to…/docs/design-principles.md#dp-18instead of a URL to a file that never existed. luria remoteslabels which construction answered per code — “a document anchor, per the scheme” — alongside the existing rung labels.- uid remotes (ADR-024): a remote can
declare its references’ shape outright — a
uidregex, a configurabledelim, and aurltemplate that indexes the uid’s capture groups by position — soARXIV-2403.05530linkifies, lints andurl-oks like any foreign code. A uid is exact (never zero-padded), has exactly one resolution rung (the template; no lockfile, no convention), and an unconfigured prefix still never matches.
Changed
- The lockfile’s authority is scoped to what discovery can see: files. A document-scheme code absent from the lockfile still constructs — a section never appears in a directory listing, so its absence there is not evidence (ADR-016 unchanged for file-per-code codes).
- The remote-level
dirdefault moves fromdocs/decisionstorecord/decisions.d, following the read/write boundary (ADR-021) — defaults mirror Luria’s own conventions. Remotes with an explicitdirare unaffected. - The
url-okacknowledgingSG-DP-18narrows to its residue: the construction now reaches the right document, and the annotation excuses only strata-g’s legacy heading-derived anchor — the retirement loop ADR-022 designed, exercised in tests in both directions.
Added
- Parallel execution (ADR-026,
#7): one ordered
pmapover a thread pool, applied at three seams — render units inluria index(a scheme, a journal), per-file scans in the bare-reference lint, and per-URL probes inluria remotes --check. Results keep input order, so reports and rendered views are byte-identical at any width.LURIA_JOBS=1forces serial execution;LURIA_JOBS=Ncaps the pool. Measured:remotes --check6.6s → 2.9s on this repo’s citations; index and lint unchanged at today’s cardinality (the seams there are structure for growth, as the issue asked).
Added
- A document can opt out of reference checking
(ADR-033,
#37):
unlinted-file:exempts a whole page from the bare-reference lint, wikilink handling and the reference-status scan — the blunt tool for a fixture-heavy or vendored document where a directive per code is maintenance without information. File-scoped only (backticks are already the narrow form; a bareunlinted:is reported as misuse), and the exemption is counted: the reference report lists every opted-out file and the lint prints the count, so the report stays a complete account of what nobody is checking (ADR-007). - Fixture codes get their own prefix
(ADR-034,
#38):
FXis registered as a remote whose every code resolves to the fixture-codes note in the directives doc, so an example likeFX-ADR-032is a first-class reference that needs nounresolved-okand can never collide with the real sequence. The template scaffold ships the same entry. Mechanizes what filing the real ADR-032 taught the hard way, when five directives using that number as a specimen went stale at once.
Added
- A hand-filed journal entry heals itself (ADR-031,
#33):
luria indexpopulates an emptycreated:from the entry’s path — the path is derived from the timestamp, so it is the one witness left — and the lint error names that remedy instead of asking a human to retype what the tree already states. A field that disagrees with the path is still an error: two witnesses in conflict is a judgement, not a mechanical fix.
Changed
- The status reports are committed views, and the README badges land on
them (ADR-032,
#35):
luria indexrendersdocs/reports/pending-decisions.mdanddocs/reports/reference-status.mdwith every other view, the lint fails when they are stale, and each badge links to the report that explains its number. Everything a report names is a link — the flagged decision, every citing line, every pending code. The reports carry no clock (ages read “open since”), because a committed view that embeds today’s date goes stale at midnight on every branch at once (DP-2). The default reportspath moves frombuild/doc-reportstodocs/reports;luria reportsstill writes them standalone for the CI artifact.
Changed
- Status enforcement is a dial (ADR-035,
#40), superseding
ADR-007’s “warnings, never able to fail a
build”: the warn-first posture stays the default, and
[luria.lint] fail_onpromotes named warning classes —retired-citations,unresolved-codes,hand-written-urls,stale-directives,pending-documents,unlinted-files— to lint failures. Only unacknowledged rows ever fail, soinactive-ok:and its siblings become the way to state a deliberate exception to a rule with teeth. An unknown class name infail_onis itself a lint error naming the vocabulary. The scaffoldedluria.tomldocuments the knob.
Changed
- The CLI is a tiered eight commands instead of a flat eleven
(ADR-030): six for contributors (
lint,link,index,journal,remotes,init) and two labelled as CI’s (reports,collect) inluria --help, the README and the scaffolded CLAUDE.md. The surface had been one command per module — the package layout projected onto the interface — and three of the names claimed workflows nobody had.
Removed
luria badges,luria ref-status,luria pending. Each was already subsumed:luria indexwrites the badges andluria lintchecks them (ADR-029); both status reports print as lint warnings and land in full in theluria reportsartifact (ADR-007, corrected to v2). Removed outright, not deprecated — a name that answers is a name that still exists, and there is no workflow to migrate. The modules keep their entry points (python -m luria.ref_status --allis still the interactive dig), and theref-statusandpendingmake targets are gone.
Added
- Luria: the project-memory machinery, extracted from
strata-g as a reusable package. The four
layers (ADR-001), the
fragment convention (ADR-002),
the generated decision index
(ADR-004), the
reference-hyperlink lint
(ADR-005), the
retired-document and pending-decision reports
(ADR-007), and the
inactive-ok/unexemptdirective vocabulary (ADR-008). luriaCLI —lint,link,index,ref-status,pending,reports,collect,init.luria lintis the only one that can fail.luria initscaffolds the record into a project that has none, and never overwrites: a scaffolder that clobbers is one nobody dares re-run.- Everything project-specific is configuration
(ADR-006): paths,
issue URL, code globs, fragment directories, and reference schemes. A second
scheme (RFC, SPEC) is a
luria.tomlentry and a directory.
Documentation
- The name. The package was very nearly
chester, after Chesterton’s Fence; ADR-010 records that reasoning and ADR-011 supersedes it — Luria, after The Mind of a Mnemonist, because the name should point at the faculty rather than at one failure it prevents, and because the book’s cautionary half (a memory that never forgets and never abstracts becomes unusable) is the design brief.
Fixed
- A literal
|in a decision’ssummary:(or status note) no longer breaks its row in the generated index and tag pages — the renderer escapes cell content, and normalises an author’s hand-escaped\|rather than double-escaping it (#14).
Added
- Fragment directories can declare a collection style
(ADR-028):
append(unchanged default — narrative order, marker at the end) orchangelog— one## <date>batch per collection inserted right after the marker, newest batch first, fragments newest-first within it, and a stub-only batch emits nothing rather than an empty date heading. Luria’s own changelog now collects in the changelog style.
Added
- Journals — dated entries that persist, rendered into one generated book
per period plus an index (ADR-020). Configure one
with
[luria.journals.<name>](dir,output,granularityofyear | month | day,title,blurb); entries live at<dir>/yyyy/mm/dd/hhmmss.md, so identity is the authoring timestamp and ordering is a property of the record rather than of commit order. luria journal new "A title"files an entry at the current timestamp, stepping forward a second on collision; bareluria journalreports what is filed and which books it renders to.make journalruns the latter.- Two lint checks: a journal entry’s path must agree with its
created:and it must carry atitle:; andversion:must agree withhistory:— a bumped version with nothing saying what changed is a silent revision wearing a version number (ADR-019).
Changed
- The devlog is now a journal, not a collected view.
docs/devlog.mdis replaced bydocs/devlog/README.mdand one book per month; entries are no longer consumed, so the view is regenerated byluria indexand a hand edit to it is a lint failure. The seven existing fragments were migrated with the timestamps of the commits that added them. luria initscaffolds the journal:template/luria.tomlgains[luria.journals.devlog], anddevlog.d/_template.mddocuments the entry shape rather than a branch-slug filename.Config.is_historical()is now the one place deciding which files are dated records and therefore out of scope forluria ref-status. It covers journals, whose entries are nested and which the previouspath.parenttest could not see.
Documentation
- ADR-002 and
ADR-012 corrected in place (v2, with
history:): both cited the devlog as an example of a collected view. Neither choice changed — ADR-012’s distinction is precisely what ADR-020 applied. docs/adopting.mdgains a section on adopting into a project that already has a devlog, including how to recover fragments’ real authoring times and the two traps in doing so (committer time zones, and links written for the old collected file’s directory).
Changed
- The README’s two record badges are counts now, not adjectives
(ADR-018). “generated index” and “versioned”
were assertions that could never be false; they are replaced by needs
decision (
Proposed+Deferred) and cited but retired (retired documents still cited without an acknowledgement). Zero is green, non-zero is amber — neither number is a failure. luria pendingcovers every scheme, not just decisions. AProposedprinciple is an open question in exactly the same way, and its rows are keyed by code (ADR-012,DP-004) rather than by ADR number.
Added
luria badges, andluria indexregenerates the counts into a<!-- luria:badges -->region. The numbers are baked into static shields URLs — no endpoint to configure and no committed JSON — andluria lintfails when the region disagrees with the record. Baked in rather than served means the count is correct per commit, so a pull request shows its own numbers rather than the default branch’s.
Added
- A cited code that names no document is now reported rather than silently
dropped (ADR-014). It shows up in
luria lint,luria ref-statusand the CI artifact. A warning, never an error — a typo, another project’s decision and an illustrative code look identical to a scanner, and only a human can tell them apart. unresolved-ok:retires a deliberate one, at the same three scopes asinactive-ok:and with the validity check inverted: it is malformed when it names a code that does resolve. Both counts are printed on a clean run, so “nothing to report” can never mean “everything was silenced”.- Badges on the README: CI status, Python version, licence, and links to
the two generated views. Plus the
LICENSEfilepyproject.tomlhas been claiming all along.
Fixed
- Ten stale references to the ancestor project’s numbering, left in ported
docstrings —
ADR-187,ADR-188,ADR-123andADR-158each cited a decision that says the right thing in the wrong repo. One was a link toadr-123-adr-status-vocabulary-docs-lint.md, a file that has never existed here; the reference lint skipped it because it was already a link. All found by the new report on its first run. - A code inside a URL is no longer read as a citation. Linking out to
another project’s decision is the correct way to name a foreign document, and
the URL contains its code — without this, the
luria inittemplate failed its own scaffolded lint the moment its comments pointed at Luria’s docs.
Changed
pip install luria→pip install git+https://github.com/dmarx/luriain the README and the adoption guide. The package is not on PyPI, and a README that ships a command which 404s is the drift this repo is about.
Changed
- A document’s filename is its code and nothing else —
ADR-013.md, notadr-013-a-documents-filename-is-its-code.md(ADR-013). A slug in the filename is a third copy of the title that no tool reads and that a rename plus every inbound link is needed to correct, so it never gets corrected. - The title moves into a
title:frontmatter field, which the generated index and principles document prefer over the body’s#heading. The heading falls back in — a project mid-adoption, or one that never adds the field, still renders a title rather than a blank cell. - Filename decoding lives on
Scheme(filename(),number_of(),documents()). Five separate places had grown their own regex for it.number_of()reads legacyadr-010-a-slug.mdnames too, so adopting Luria is not a rename-everything-first proposition.
Added
luria lintreports atitle:that disagrees with its body heading, and a missingtitle:. The heading has to stay — someone opening the file alone needs one — so the two copies get a guard rather than a merge: rung 2 of DP-3, since rung 1 isn’t available.tests/test_lint.py, covering the new check in both directions and across both schemes.
Changed
- The reference-status report stops calling a Proposed document “retired” (#63): the page is titled “Reference status”, its first section — “Documents cited while not in force” — spells out the not yet vs no longer split, exclusions read as “Not listed: N citations someone has already vouched for”, per-code tallies say which sites are marked deliberate instead of “acknowledged elsewhere”, and counts pluralize as prose. The README badge follows: “cited, not in force”.
Changed
- Both CLAUDE.mds are maps now, not copies
(ADR-037, part of
#45): a short list of links to
the authoritative docs, the invitation to run
luria --helpfor the current API, and three one-line ground rules — plus the statement that when the file disagrees with the docs or the CLI, the file is the one that’s wrong. The restated command block and doctrine walkthroughs are gone; they had drifted twice in one week, exactly as DP-3 predicts for hand-maintained copies. The scaffoldedtemplate/CLAUDE.mdgets the same treatment, mapping an adopting project instead of this one.
Removed
- The Makefile (ADR-038): its “run what
CI runs is
make <target>” doctrine stopped being true when ADR-029 moved the docs jobs into composite actions, leaving onemake testline wrapping pytest and a set of targets that restated CLI one-liners and drifted twice in a week. ci.yml runs pytest directly;luria --helpis the one list of what you can run.
Added
luria initspeaks up about a kept CLAUDE.md: it never overwrote existing files, but the one file an agent reads first deserved more than a silent skip — when CLAUDE.md exists, init now prints a pointer at the scaffolded map shape (links +luria --help) and suggests asking your agent to fold it in. The recommendation goes to stdout, where permission isn’t needed; the file is never touched.
Added
luria new [kind]scaffolds an entry anywhere the record takes one (ADR-036, #42): the journal by default, any configured scheme by prefix (luria new adrcopies_template.mdto the next free number and stamps the date), any fragment directory by name (luria new changelognames the file after the branch). It computes only what a machine can know, prints the path, and leaves the content to a markdown-aware editor;--title/--status/--summary/--tagsexist for tools driving the CLI, never as requirements. Kinds derive fromluria.toml, so a new scheme scaffolds for free. This fragment and its devlog entry were created with it.
Removed
luria journal— subsumed byluria newand removed without a shim (ADR-030);python -m luria.journalremains for the interactive look at what is filed.
Changed
- The CLI is driven by Fire (ADR-039,
proposed — this ships as a draft PR): every command is a plain typed
function (
<module>.run), flags and help derive from signatures and docstrings, and the hand-rolled dispatcher plus every module’s argparse layer are deleted (~150 lines). Failure is signalled bySystemExitonly — Fire prints return values, and a CI gate’s exit code is not output. Every existing invocation spelling (--fix,--check,--commit,new adr --title …) parses identically; help output becomes Fire’s house format.fire>=0.7joins PyYAML as a runtime dependency.
Added
- ADR-040: the migrations doctrine — how schemes
get renamed and documents move between them (mapping-driven sweeps,
formerly:as identity, full rewrite including history, a rung ladder from prose relabel toluria migrate). Doctrine only; the machinery lands per the ladder, starting with rung 1. - ADR-041: the bug protocol — a defect enters the record as an issue carrying a minimal working example before any fix, the response is classified on the ADR-035 ladder, and the fix PR turns the MWE into a regression test. First live run: the journal link-frame bug.
- DP-010: defaults follow the failure mode — guards ship on and are opted out of visibly at the site; disclosures ship off and are opted into by a config line; either deviation is written down where it applies.
Documentation
- Both CLAUDE.mds (this repo’s and the template’s) rewrite the hyperlink
ground rule as “never hand-write a link target” — bare codes and
[[CODE|label]]wikilinks, with the fixer owning every target because only it knows which render frame a target must resolve in — and add a fourth ground rule: a guard that keeps catching the same mistake is a bug report about the workflow, and the fix belongs upstream of the guard. Prompted by four wrong-frame links in one day, all hand-written, all wanting a prose label the (previously undocumented) labeled-wikilink syntax already provides. - Both CLAUDE.mds now open with a read-this-first directive: load the full design-principles document into context before anything else — the principles are the one part of the record the map assumes rather than links.
Added
- Drop-in CI for the record (ADR-029):
actions/generateregenerates the views, commits and pushes them as the bot, and outputs the SHA a checking job must read (fork PRs get a warning and an un-regenerated SHA instead of a 403);actions/lintrunsluria lintand uploads the status reports. Theluria inittemplate workflow is now the full recommended shape built from those actions — it previously scaffolded a verify-only lint, handing every new adopter a gate with nothing keeping it satisfied — and luria’s ownci.ymlruns the same two actions by local path, so the scaffolded workflow is the one this repository lives on (ADR-009). luria/ci.py: luria notices when it is being read in a build. Detection is crude on purpose (CIplus the vendor variables) and only ever changes what is said — no write and no exit code depends on it.
Fixed
- The staleness remedy now names the half that matters: the output has to be
committed.
stale — run luria indexis complete advice in a working copy and half an answer in a build. Under CI the message names both legitimate routes — regenerate locally, or give CI a generation job — and warns against the specific broken shape: the generator dropped into a checking job with nothing committing its output, which discards the result and leaves a followingluria lintcomparing the generator against itself (#21, #23). - Bare
luria badgessays on stderr that it only printed. As a- run:step it looked exactly like a write and exited 0 having done nothing (DP-1). Stdout is unchanged, so redirection still works.
Documentation
docs/adopting.md’s CI section leads with the scaffolded workflow and the two actions, and keeps what stays in the caller’s hands: the fork-safe checkout ref (a fork’s head branch does not exist in the base repo — the checkout fails before any push guard can help), theneeds:+shahandoff (aGITHUB_TOKENpush does not retrigger workflows), and the warning never to write GitHub’s skip markers into a commit message you author.