One binary, luria, dispatching to plain functions. Every command takes
--help. Flags are shown GNU-style; the CLI (python-fire) also accepts
--flag=value and positional forms.
| command | one line |
|---|---|
luria init | scaffold a record into a repository |
luria config | write a starting luria.yaml, without scaffolding |
luria new | file a new entry of any configured kind |
luria relate | write a relation into an existing document |
luria repair | write every mechanical source repair |
luria index | render every generated view |
luria link | turn bare codes and wikilinks into links |
luria lint | enforce the record’s invariants |
luria reports | write the status reports |
luria collect | assemble fragments into their target |
luria concretize | give temporary codes real numbers |
luria remotes | inspect and verify foreign references |
luria migrate | execute a rename/move spec |
luria site | stage the record as a publishable site |
luria export | write the record as a SQLite database, for querying |
luria upgrade | carry a record across a version boundary |
luria init
luria init [INTO] [--issue-url URL] [--schemes S] [--journals J]
[--config FILE] [--dry-run]
Scaffolds the default record — templates, stubs, tag vocabulary, principle
seeds, a docs index, a CLAUDE.md, and CI workflows — into INTO
(default: the project root, found via luria.yaml, then .git).
-
Existing files are always skipped, never overwritten; each is reported. Re-running on a grown project is safe.
-
--issue-urlseedsissue_urlin the scaffoldedluria.yaml; append{n}yourself or let init place it. Left out, it is inferred from theoriginremote —git@github.com:acme/widgets.gitbecomeshttps://github.com/acme/widgets/issues/{n}, and init says so as it goes. That one value also givessiteits title, its Pages URL and the base a link falls back to, so a repository with a remote needs no configuration at all. GitHub and GitLab are recognised; any other host infers nothing, because a wrong issue URL renders a broken link on every entry that carries an issue. -
--schemesand--journalsadd families to the shipped shape, for a project that wants the defaults plus a little:$ luria init --schemes "RFC,SPEC:document" --journals "incidents:day"Each entry is
NAMEorNAME:kind—indexordocumentfor a scheme (which one?),year,monthordayfor a journal. Paths follow the prefix, soRFCgivesrecord/rfcs.drendered intodocs/rfcs; rename them afterwards if the family is better called something other than what its codes spell.The shorthand is an argument, not a stored format: what it writes is the ordinary commented table, so nothing reads it back and the config looks like every other project’s. It is additive — the template’s own ADR and DP tables stay, which is what keeps them alive given that a declared family replaces the shipped one whole. Removing a default means deleting a table.
-
--config FILEscaffolds from your ownluria.yamlinstead of the shipped one — this is how you init a record with no ADR scheme at all, rather than one with an extra family. Refused if aluria.yamlalready exists (merge by hand instead), as are--schemes/--journals: where a config exists the shape is declared, and a flag should not edit it. -
--dry-runprints the plan and writes nothing.
luria config
luria config [INTO] [--schemes S] [--journals J] [--issue-url URL] [--stdout]
Writes the luria.yaml that luria init would have written, and stops.
The shorthand covers the two things projects usually vary. A project that also wants a different directory, a narrowed status vocabulary or a tag group has to edit the config — and editing it after a scaffold means moving directories the first run already created. This is the order that avoids that:
$ luria config --schemes "RFC,SPEC:document"
luria.yaml
Edit it, then `luria init` to scaffold the shape it declares.
$ $EDITOR luria.yaml
$ luria init- Takes the same
--schemes,--journalsand--issue-urlasinit, and infers the issue URL from theoriginremote the same way. - Refuses to overwrite. A config that exists has already started.
--stdoutprints instead of writing, which also works where a config exists — looking is not writing.
luria new
luria new [KIND] [--title T] [--status S] [--summary S] [--tags a,b]
[--influenced_by ADR-1,ADR-2] [--body TEXT] [--name N]
luria new [KIND] --draft FILE
Files one new entry and prints its path. KIND is any name the project’s
luria.yaml gives the machinery, lower-cased:
- a scheme prefix (
adr,rfc, …) — scaffolds the next document from the scheme’s_template.md, with the number allocated (or a temporary code minted, if the scheme allocates on merge) and today’s date stamped; - a journal name (
devlog,incidents, …) — createsdir/yyyy/mm/dd/hhmmss.mdstamped with the current time; - a fragment directory’s short name (
changelogforrecord/changelog.d) — creates a timestamped fragment, or a named one with--name; migration— scaffolds a numbered migration spec inrecord/migrations.d/.
With no KIND and exactly one journal configured, the journal is the
default — luria new --title "…" is the cheapest possible filing. The
field flags (--title, --status, --summary, --tags,
--influenced_by) pre-fill the scaffolded frontmatter; --influenced_by is
written as the list of codes the index reads, and a scheme’s own declared
reference fields are accepted by name the same way. --body TEXT hands over
the prose: it replaces the template’s body below the # CODE: title heading
of a scheme document, the placeholder paragraph of a journal entry, or the
whole of a fragment. The heading stays luria’s — it is derived from the
title — so a body that opens with one has it dropped rather than doubled.
--draft FILE files what a tool wrote instead of flags: one draft object
({"scheme": "ADR", "title": …, "tags": […], "summary": …, "influenced_by": […], "body": …}) or a luria-drafts document holding several
({"format": "luria-drafts", "drafts": […]}), which is what strata-g’s
Record — luria drafts (JSON) export writes for the entries drafted on its
canvas. Each draft files into the kind its scheme names, with the same
validation the flags get — a key the scheme has no opinion about is refused
by name — and one path is printed per draft. The generated view of your own config lists every
kind your project accepts (see the record for this one).
luria relate
luria relate SOURCE FIELD TARGET
luria relate --draft FILE
Writes one relation into a document that already exists: TARGET joins
FIELD of the document SOURCE names, and the path is printed. The edit
is one line in the frontmatter; the form’s comments and every other field
stay as they were.
FIELDis a relation the source’s scheme reads:influenced_by, the successor field (superseded_byunless the scheme renames it), or any field declared under the scheme’sreferences:. A prose mention is not a field, socitesis refused by name.TARGEThas to resolve — a local code, or a remote one where the field allows any scheme — and has to be of the scheme a typed field names. An unresolvable code is refused, not written.- A relation already present is reported, never duplicated. A scalar field already set to something else is refused rather than replaced.
- Naming the successor on a document still in force says so: the status is yours to set, and the lint will ask for it.
- A declared converse is not written here;
luria repaircompletes it, as for every other one-sided relation, and the command says so.
--draft FILE reads the relations list of a luria-drafts document
({"source": …, "field": …, "target": …} by code) — the shape a canvas
exports for a relation drawn between two filed documents — and files each
entry in turn.
luria repair
luria repair
Writes every mechanical repair to the sources — each one a state the lint
reports with this command as its remedy: bare codes in prose become links
(what luria link --fix does, over every file), a journal entry filed
without created: gets the timestamp its path already implies, and a
configuration reference Luria no longer renders here is removed. Prints
what changed. Idempotent: a second run changes nothing.
Repairs are a command apart from the views because they land in a different place. A repair touches only the files a branch itself authored, so the generate action commits it onto the branch, where the review reads it; a view is a shared file every branch would rewrite, so it is committed on the default branch only.
luria index
luria index [--check]
Renders every generated view from the sources: scheme indexes and tag
pages (or the single concatenated page for render = "document" schemes),
journal books, the status reports, the record and configuration reference
pages, and the README badge region. Also deletes orphaned files from view
directories. Writes views only; the sources are luria repair’s.
--check writes nothing and exits non-zero if any committed view differs
from what would be generated, a file sits in a view directory the generator
never wrote, or the README’s generated region has drifted — the one
staleness check there is, run by the generation job on the default branch
right after it regenerates. luria lint asks no staleness question, so it
runs on a branch as it is. (Views listed in .gitignore are exempt: an
uncommitted view cannot be stale.)
luria link
luria link [PATHS…] [--fix] [--links-only]
Spells out what the record left implicit, in two places.
Links. Finds every linkable reference in the given files (default:
every non-generated markdown file the record knows about): bare codes
(ADR-012, DP-3), temporary codes, remote codes (LU-ADR-013),
issue numbers, and [[wikilinks]]. Prints per-file counts; with --fix,
rewrites them into links whose relative targets are computed for the
directory where each piece of prose ultimately renders — which is why
hand-writing targets is the one thing the workflow forbids.
References inside backticks, fences, existing links, HTML comments, and frontmatter (except designated prose fields) are left alone.
Back-references. A reference field may declare its converse — the
field holding the same relation read backwards:
schemes:
LIT:
references:
extends:
scheme: LIT
many: true
converse: extended_by
extended_by:
scheme: LIT
many: true
converse: extends
compared_against:
scheme: LIT
many: true
converse: compared_againstA relation naming itself is what symmetry is, so there is one rule and
not two. --fix writes the side that is missing, in either direction, so
the author states the relation once on whichever document they were
holding. One side holding it alone is the one-sided-relations finding.
A relation with no declared converse is left entirely alone — nothing written, nothing reported. Its reverse edge would be a guess, and a guess in the record is worse than an absence, because an absence looks like one.
Adding and removing are both propagated. A one-sided pair means one of
two opposite things — somebody wrote the relation and the other side has
not caught up, or somebody deleted it and the other side is stale — and
the working tree holds neither answer. Which side changed since the last
commit does, so --fix reads it from git:
| since HEAD | --fix does |
|---|---|
| a side gained the relation | writes it to the other side |
| a side lost it | removes it from the other side |
| one side gained while the other lost | nothing — reports the conflict |
| nothing changed | writes the missing side |
The last row is what a corpus predating the fixer needs. It can read a
deletion wrong when that deletion was committed before --fix ran, and it
is self-correcting: delete it once more and the deletion is a change.
With no repository or no commit yet, every relation reads as added, which
is right for a document git has never seen.
--fix never makes luria lint worse. The fixer edits frontmatter and
frontmatter is what the contract judges, so a repair can move a document
from satisfying its scheme to violating it — a back-reference added into a
field group that permits only one of two fields, or a stale one removed out
of a field the document’s status requires. Any repair that would introduce a
new violation is not applied; the pair stays one-sided and the finding
says which two rules disagree. A document already in breach elsewhere still
gets its back-references, or one unrelated mistake would freeze every
relation it stands in.
The one thing --fix will not touch is a contradiction: a document naming
another in both directions of one pair, two documents each claiming to
come first, or a relation withdrawn on one side and asserted on the other
in the same working tree. Nothing is missing there — two incompatible
things are present, and the data does not say which was meant.
Completion reads every document of a scheme, because that is what
“missing” is defined against, so PATHS narrows the linking only.
--links-only skips it entirely — the behaviour --fix had before
completion existed, for a run that must touch nothing but link text.
luria lint
luria lint
The contract, in two halves.
Violations (exit 1), each with the file and line:
- a scheme document without frontmatter,
status:,title:, ortags:; a status outside the vocabulary (Active,Proposed,Deferred,Superseded,Rejected; a note still riding instatus:rather than instatus_note:;Supersededwith nosuperseded_by:) or undeclared in the scheme’s vocabulary; a missing field the schemerequires, or one itsrequired_whendemands at the status the document is at; atitle:disagreeing with the body heading - a constraint the scheme declares, broken: tag-group rules
(
exactly-one,at-most-one,excluded_by), a field group’sat-least-one, a value outside a closed vocabulary, a reference field holding something that is not a resolvable code of the scheme it names, a relation’s declaredinvariantdisagreeing across the edge, or a field markeduniquewhose value appears on a second document of the same scheme - a
version:above 1 with nohistory:, or history that ends on a different version than the document claims - a journal entry with no derivable
created:, or filed at a path its timestamp says is wrong - a number collision inside a scheme, an alias two documents both claim,
or a code on the reserved
tmpprefix - a stray hand-written file in a view directory (whether a committed view
is current is
luria index --check’s question, asked in the generation job on the default branch; a branch carries no view of its own) - a bare code or unexpanded wikilink that
luria link --fixwould rewrite, or a wikilink that resolves to nothing - a docs page missing from the docs index (
docs/README.md) luria.yamlnaming a warning class that does not exist infail_onormute, or the same class in both
Warnings, printed but passing — each is a judgement call, surfaced with its acknowledgement route (see directives) and listed in full in the reports:
retired-citations · unresolved-codes · unresolved-citations ·
hand-written-urls · broken-targets · remote-drift ·
source-mismatch · source-unchecked · inert-status ·
legacy-spellings · narrow-titles · stale-directives ·
template-drift · broken-chains · one-sided-relations ·
spent-upgrades · pending-documents · unlinted-files ·
workflow-temp-codes · unlinked-site
Any of those class names listed in lint.fail_on fails the build
instead. Only unacknowledged findings ever reach a class, so
acknowledgements keep working under enforcement.
lint.mute is the other direction: a class named there is not reported at
all. Where fail_on changes what a finding costs, mute decides whether
it is heard — the blunter instrument, for a check a project has decided is
not useful to it, where an acknowledgement directive is the right shape when
the finding is about one document and carries its reason at the citing site.
Every failable class is mutable, plus acknowledged-uniformity, which is
mutable only: a project cannot promote its own acknowledgement to a failure.
Naming a class in both is refused rather than resolved — a record cannot
both enforce a check and refuse to hear it, and guessing which was meant
would make one of the two settings a lie. luria reports renders the full
accounting either way, so muting changes what a run prints and not what the
record says.
lint.network promotes one class on its own: under network = "require",
source-unchecked fails without being named in fail_on, because the
setting already said that a green run means the references were verified.
luria reports
luria reports [--out DIR]
Writes the two status reports (default: the configured reports path,
docs/reports/): pending-decisions — every Proposed/Deferred
document with age and citation counts — and reference-status — retired
documents cited unacknowledged, codes resolving to nothing, files opted
out of checking, and directives that no longer apply. luria index writes
these too; the standalone command exists for CI jobs that want only the
reports.
luria collect
luria collect [DIR] [--commit]
For each configured fragment directory (or just DIR): read every
fragment in the order it entered git history, insert the non-empty ones at
the target’s <!-- luria-insert-here --> marker (the changelog style
adds a dated heading; the default appends), and delete the fragments.
--commit stages and commits the result with [skip ci] — the shape a
scheduled CI job wants.
luria concretize
luria concretize [--check]
For schemes with allocate = "merge": assign each temporary code
(ADR-tmp3kf9x) the next real number, rename the file, rewrite every
reference in docs and scanned code, and record the old spelling under
formerly: so stale spellings keep resolving. Run it where merges
serialize — the push-to-main CI job — and never on a PR branch, which
would re-create the collision the temporary codes exist to avoid.
--check exits non-zero while any temporary code is pending.
luria remotes
luria remotes [--refresh] [--check] [--pin [CODE]] [--resolve [CODE]]
Prints every foreign code the record cites, per remote, with the URL each
resolves to and the evidence behind it (explicit template, discovered
filename, or bare convention). --refresh re-discovers each remote’s
actual filenames via the GitHub API and writes remotes.lock.json —
committed, so CI and offline checkouts resolve identically. --check
HEAD-probes every cited URL and reports what a reader would find: broken,
absent from the remote, or unverifiable because the repository is not
readable anonymously.
--pin endorses remote content: it fetches each document, hashes it,
and stores the hash in the same lockfile. A remote document has no status
this project can read, but a change in its bytes is knowable — --refresh
re-observes every pinned document, and luria lint reports each one whose
content moved on since its endorsement (the remote-drift warning class,
promotable via fail_on). Review the change, then
luria remotes --pin CODE endorses it again.
Every pin has a registration — the thing that says it should exist, and
whose removal retires it. pin = true on a remote (or one of its
schemes) registers the whole code family: each cited reference is pinned
automatically, and the lint reports any the lockfile has not endorsed
yet. A pin: comment directive registers one arbitrary URL where it is
cited. An explicit --pin CODE registers one ad-hoc pin, whose lockfile
entry is its own registration. A bare --pin syncs the lockfile to the
registrations — endorsing what is newly registered, re-observing what
exists, dropping what nothing cites or flags — and it never re-endorses
drifted content: that always takes the explicit command, so a scheduled
sweep cannot quietly launder a drift finding.
--resolve asks what each identifier actually is. Where --pin watches
for a document changing, this watches for a citation that was never right:
it fetches the title behind every arxiv:/doi: field — any frontmatter
key named after a configured remote — and records it in the lockfile, which
luria lint compares against the title the document claims. A disagreement
is the source-mismatch warning class; source-ok: acknowledges a
deliberate one, which the legitimate cases need — a nickname the project
prefers (AdamW: Decoupled Weight Decay Regularization), a subtitle
trimmed, a v1 title that changed between versions.
The lockfile is a cache with an endorsement in it, not the boundary of what
may be known, and lint.network says how far the lint may go:
auto (default) | ask about identifiers the lockfile has no answer for — normally the one citation a contribution just added — and write what comes back. Falls back to source-unchecked when the network is not there. |
never | answer only from the lockfile. The hermetic build. |
require | not being able to ask is a finding, promoted to a failure without needing fail_on. A green CI run then means the references were verified rather than remembered. |
An identifier nothing has verified is source-unchecked rather than silent,
because a citation is likeliest wrong in the minutes after it is typed —
exactly when nothing has resolved it yet.
Failure kinds are told apart by status: 404/410 is upstream saying the
identifier names nothing (an answer, recorded and not retried), while
429/503 is retried with backoff, honouring Retry-After, and reported as
unchecked if it persists rather than written down as an absence.
A remote declares how to ask, for the same reason pin_url is declared
rather than derived — only the project can vouch that a URL serves metadata
worth trusting:
remotes:
ARXIV:
uris:
title: https://export.arxiv.org/api/query?id_list={1}.{2}
title_re: <entry>.*?<title>(.*?)</title>
DOI:
uris:
title: https://api.crossref.org/works/{uid}
title_re: '"title":\s*\[\s*"(.*?)"'title_re’s first capture group is the title, matched with re.DOTALL. A
remote that declares neither is skipped entirely: most remotes are records,
not metadata APIs, and with no way to ask the check has no opinion.
What gets hashed is the construction’s stable bytes, not the page a
reader lands on. A GitHub file construction qualifies on its own; any
other remote declares where its stable bytes live with a pin_url
template — arXiv’s immutable e-print archive behind its abstract page, a
forge’s own raw scheme — because a rendered page’s markup churns under
identical content, and a hash of it would cry wolf. Without either, the
command says so rather than storing a hash that would drift on its own.
Under the hood these are two entries in one table: a code relates to a
set of named URIs (read, bytes, and any name a project declares in
remotes.X.uris), each a template over one vocabulary — see the
configuration reference.
An arbitrary URL — a spec, a dataset card, a post the design leans on —
is pinned by flagging it where it is cited
(<!-- pin: https://… — why it matters -->, see
comment directives) and running the same --pin.
Deleting the flag retires the pin.
luria migrate
luria migrate SPEC [--dry-run] [--commit]
Executes a migration spec from record/migrations.d/ (SPEC can be a
path, a filename, or a unique prefix like 0001). Two operations:
rename_scheme— rename a prefix wholesale: files move (git mv), every reference and anchor in the repository is swept to the new spelling, config tables are renamed, and each moved document is stampedformerly:with its old code.move_doc— move one document into another scheme, where it gets a temporary code for the next concretize.
Either operation takes strategy = "supersede" to copy instead of move,
leaving a tombstone (status: Superseded, superseded_by: …) at the
old code. --dry-run
prints the full plan. --commit commits the sweep and appends the commit
to .git-blame-ignore-revs, so blame reads through the rename. The spec
itself is never swept: its mapping is the durable memory of the old names.
luria site
luria site [--out build/site]
Stages the record as a Quartz vault: every
publishable page at its repository path (so all relative links keep
working), every page titled (CODE: title for a scheme document), the
record’s facts (status · version · issue · influenced by · the typed edges
in and out of it) written into the staged frontmatter as properties Quartz
renders, codes registered as aliases,
README as the landing page, links to unpublished files redirected to the
repository, and the theme/branding from site rendered into
Quartz config. The published site gets search, backlinks, and a local
graph per page. The actions/site composite action builds the staged
vault with a pinned Quartz for GitHub Pages — see adopting.
luria export
luria export [--out build/record.sqlite]
Writes the record as one SQLite file: a row per scheme document (code,
scheme, number, title, status, version, date, path, the whole frontmatter
as JSON, and the body), a row per field value with list fields exploded
into positioned rows, the typed edges (superseded_by:, influenced_by:,
every declared reference field), every citation the lint’s scanner counts
— where it sits, whether it resolves, whether a directive excuses it — and
every journal entry with its tags. Rebuilt from scratch on each run, so a
row never outlives its source; refuses to overwrite a file it did not
write. It is a generated view for asking the record questions with
sqlite3, Datasette or pandas — never a source, and not committed
(ADR-116).
luria upgrade
luria upgrade [NAME] [--dry-run] [--root DIR]
Writes what a new version of Luria requires into a record that predates it.
With no NAME, lists the upgrades that exist and what each is waiting on:
$ luria upgrade
temporary — each is deleted once every record has run it:
luria upgrade yaml
convert `luria.toml` and the per-scheme vocabulary files into one
`luria.yaml`
remove when: 1.0.0, or when no record on TOML remains.yaml— foldsluria.tomland the per-scheme vocabulary files into a singleluria.yaml, carrying the comments across. This is the only way over that boundary: the current version does not read TOML at all.statuses— declaresstatus:as the controlled vocabulary it always was, for a record filed before it became one.
--dry-run prints what would be written. --root points the run at a tree
that is not the working directory, for upgrading a record from outside it.
Every command here is temporary by construction. An upgrade exists to
move records that predate a change onto it; once they have moved it is dead
code that still has to be read, tested and explained. Each one carries the
condition for its own deletion, and luria lint reports an upgrade this
record no longer needs as spent-upgrades — so the question of removing it
comes up on its own rather than waiting to be remembered. Nothing here goes
through the config loader: the config an upgrade repairs is the config the
new version refuses to load, so a command that needed it would be unrunnable
in exactly the situation it exists for.
Environment
LURIA_ROOT— overrides project-root discovery; how CI and the test suite point a run at a tree that is not the working directory.LURIA_JOBS— caps the thread pool used for rendering, scanning, and URL probing.LURIA_JOBS=1is the deterministic escape hatch.
Every module is also runnable standalone (python -m luria.ref_status --all) for projects that vendor a file rather than installing the
package.