Generated by luria index from the dataclasses in luria/config.py — built, not edited. A key that exists in the schema is a row here whether or not anyone remembered to describe it.

Everything Luria does is driven by one file at the project root, luria.yaml. Every key has a default, so a project with the conventional layout needs only issue_url — and Luria runs without the file at all, on defaults alone, which is what lets luria init bootstrap into an empty repository.

The shape of the file

The tables below are the whole schema. Five of them — schemes, fragments, journals, remotes and chains — are families: you name the entries, and the name you choose becomes part of the vocabulary. schemes.RFC is how a project gets RFCs; journals.incidents is how it gets a second journal. No code path spells ADR.

That is worth stating plainly, because this package ships one instantiation of its own machinery — decisions, principles, a changelog and a devlog — and it is easy to read those four as Luria’s parts. They are its defaults. A record made of RFCs, specs and a meeting log is the same engine with different tables.

Two merge rules follow from that split (ADR-047). A settings table — paths, code, lint, site — merges per key: setting docs does not clear reports. A family table is replaced whole the moment you declare it: a project that writes schemes.RFC and nothing else has exactly one scheme, and the shipped ADR is simply absent. Declare a family and it is yours entirely; leave it undeclared and the default stands. This is also what makes omission meaningful inside a declared family — an output you leave out is genuinely unset, because there is no default entry left for it to inherit from.

tablewhat it configuresfamilies
luriaissue links, staleness horizon, nested records—
pathswhere the read and write surfaces live—
vocabularies.<name>value sets a scheme’s fields point at by name—
schemes.Xa referable document family: ADR, RFC, SPECyes, keyed by prefix
fragments.<dir>many small files assembled into one documentyes, keyed by directory
journals.Xdated entries that persist, rendered into booksyes, keyed by name
chains.Xa relation walked transitively, rendered as sequencesyes, keyed by name
remotes.Xanother project’s codes, cited from this oneyes, keyed by prefix
codewhich source files are scanned for stale references—
lintwhich warning classes fail, and which are not reported—
sitepublishing the record as a browsable site—

Top level — luria

keydefaultwhat it does
issue_urlunsetA template for issue links, with {n} for the number. Writing it also tells Luria which GitHub repository this is, which is where every site default comes from. Unset, issue numbers stay bare rather than linking somewhere wrong.
stale_days90How long a document may sit undecided before the pending-decisions report calls it out.
user_agent"luria/0.1.dev1"What Luria announces when it opens a socket — the identifier check and remote discovery both send it. The default is luria/<version>: honest about the software, the way curl/8.0 is, and carrying no contact, because a URL or mailbox in a shipped default would route every user’s traffic to whoever maintains Luria. Add your own contact here if you want the treatment some hosts reserve for callers who identify themselves — CrossRef’s polite pool wants a real address — since that address belongs to your record rather than to Luria.
include_recordsunsetGlobs naming further records inside this repository — examples/* — each read with its own luria.yaml and linted in the same run. What makes a worked example a thing CI exercises rather than a directory of prose nobody runs.

Paths — paths

The read/write boundary: docs/ is where a reader browses, record/ is where a contributor files.

keydefaultwhat it does
docs"docs"The read surface — prose plus every generated view.
decisions"record/decisions.d"The default scheme’s source directory. Kept as its own key for projects that predate schemes.X.
design_principles"docs/design-principles.md"Where the principles document renders.
reports"docs/reports"Where the status reports land. Committed views rather than build artifacts, so a README badge has a real page to point at.

Code scanning — code

Which files outside the record are checked for references to retired documents. A decision number in a code comment is the strongest form of the claim being checked — it is the stated reason the code is shaped that way.

keydefaultwhat it does
globsunsetSource files to scan, as glob patterns.
historical['CHANGELOG.md']Files that are true about the day they were written and never updated — scanning them produces permanent, unactionable noise. Journals are covered automatically.

Enforcement — lint

The dial between reported and enforced. Status findings are warnings by default; a class named here fails luria lint instead. Acknowledgement directives keep working under enforcement, because only unacknowledged rows ever reach a class.

keydefaultwhat it does
fail_onunsetWarning classes promoted to failures.
muteunsetWarning classes not reported at all. Where fail_on changes a finding’s consequence, this removes it from the run — 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. Naming a class in both is refused rather than resolved. luria reports renders the full accounting either way.
baselineunsetA standing count per warning class, for a record whose residue is real and not shrinking. A class at or below its number reports as usual; above it, the excess is a violation naming both figures, and the rows still print because which ones are new is not knowable from a count. Below it, and when the class has cleared entirely, the run says so — a baseline nobody lowers is a ratchet pointing the wrong way. This is what fail_on cannot say (it fails at the first row) and what mute cannot say (it hides the new rows with the old). A count of 0 is fail_on in this dial’s dialect; naming a class in both, or in this and mute, is refused rather than resolved. An unknown class or a negative count is reported and then ignored for gating.
narrow_termsunsetThis project’s own concrete nouns. A title in a scheme marked titles_generalize that names one is reported as narrow-titles. Luria ships none — the words are yours, and empty means the class never fires.
network"auto"How far luria lint may reach to check what a cited identifier actually is. auto asks only about identifiers the lockfile has no answer for and falls back to reporting them unchecked; never answers from the lockfile alone, for the hermetic build; require makes an unreachable remote a failure, so a green run means the references were verified rather than remembered.

Schemes — schemes.X

One entry per referable document family. The key is the prefix codes carry, so schemes.RFC makes RFC-7 a first-class reference: luria link --fix writes its link, luria lint demands one, and luria new rfc scaffolds the next free number — none of which needs anything beyond the table.

A family of referable documents — ADR-012, RFC-7, SPEC-3.

Luria ships with one, and knowing that one is not built in is the point: the annotation vocabulary says inactive-ok, not adr-ok, and a code carries its prefix, so a second scheme is an entry here (ADR-006).

title and blurb say what the family IS (ADR-109, #279) — Journal already carries exactly that pair and a scheme is the same shape of thing, a stream of documents with a rendered index. The pairing is deliberate and is the rule this repository now follows: a thing that renders its own page takes title + blurb, and a thing named INSIDE a scheme — a vocabulary, a group, a field, a relation — takes label + blurb. Without it, docs/record.md could say a scheme’s codes look like LIT-001 and not what a LIT is.

keytypedefault
prefixstrthe table’s own name
dirPathrequired
titlestrunset
blurbstrunset
activestr"Active"
successorstr"superseded_by"
retires_onstr"Superseded"
aliasstrunset
renderstr"index"
outputPath | Noneunset
citestr"view"
allocatestr"filing"
titles_generalizeboolFalse
requirestuple[str, ...]unset
tag_groupstuple[TagGroup, ...]unset
statuses_vocabstrunset
axisstrunset
vocab_valuesdict[str, dict]unset
field_groupstuple[FieldGroup, ...]unset
referencestuple[Reference, ...]unset
vocabulariestuple[Vocabulary, ...]unset
plain_fieldstuple[PlainField, ...]unset
derivedtupleunset
uniform_okstr | Noneunset
uniform_sharefloat1.0

Vocabulary fields — schemes.X.fields.<field>

A field whose values come from a named set the project declares. status and tags are two instances of this shape rather than two special cases beside it, which is why a scheme can narrow either one.

A frontmatter field backed by a scheme-local controlled vocabulary (ADR-076):

schemes:
  SCENE:
    fields:
      worlds:
        vocabulary: worlds
        many: true
        default:            # the effective value when absent
        - B

fields is the table a field’s shape and type are declared in; today vocabulary is the one type it takes, and requires and references remain the spellings for the other two kinds until they consolidate here. The values file is shaped like tags.yaml. Closed: a value outside the file is a finding. required (default false) and default are exclusive — a field with a default is never absent, so required would say nothing.

keytypedefault
fieldstrrequired
namestrrequired
labelstrunset
blurbstrunset
values_by_namedict[str, dict]unset
manyboolFalse
requiredboolFalse
defaulttuple[str, ...] | Noneunset
closedboolTrue
alertstrunset
required_whenRequiredWhen | Noneunset

Plain fields — schemes.X.fields.<field>

The same table, for a field that carries no vocabulary: its type is “any truthy value” and what it adds over a requires entry is when the requirement applies.

A field declared in the fields table that carries no vocabulary — its type is “any truthy value”, the same as a requires entry, and what it adds is when the requirement applies.

keytypedefault
fieldstrrequired
requiredboolFalse
manyboolFalse
required_whenRequiredWhen | Noneunset
uniqueboolFalse
labelstrunset
blurbstrunset

Conditional requirement — …fields.<field>.required_when

A field demanded only while another field says one of a set of values — the shape a question that is only open while a document is unsettled actually has.

A field demanded only while another field says one of these things:

schemes:
  SOTA:
    fields:
      promote_when:
        required_when:
          status:
          - Proposed
          - Deferred

requires says a field must always be there, which is right for identity — a title, a source. It is wrong for a field that is about a state: a practice at a provisional status should say what would settle it, and one already in force has nothing to be waiting for. Demanding the field of everything makes most documents carry a key with nothing to put in it, and demanding it of nothing is what a project has today (#170).

One field against a set of literal values, and no more than that. Not negation, not conjunction, not an expression: a config that can state arbitrary predicates is a config nobody reads at a glance, and the whole value of this one is that a reader sees the rule in the line.

Pure data. Deciding whether it holds of a document needs the field’s effective value — a status carrying a note is still that status, a vocabulary field with a default is never absent — and only the compiled contract knows how to resolve that. Contract.demands does it, which also keeps this module from reaching up into ones that depend on it.

keytypedefault
onstrrequired
valuestuple[str, ...]required

Reference fields — schemes.X.references.<field>

A frontmatter field that holds a code from a named scheme. Declaring the relationship rather than merely requiring the field is what turns one check into four — present, shaped like a code, resolving to a real document, and in force — and what lets luria link --fix complete a declared converse.

A frontmatter field that holds a code from a named scheme.

schemes:
  SOTA:
    references:
      source:
        scheme: LIT
        required: true

requires = ["source"] already says the field must be there. What it cannot say is what the field MEANS, and the gap is wider than it looks: a required field is satisfied by any truthy value, so a practice citing a decision as its evidence passes, and so does one citing the string “a paper I read once”. The rule the project was relying on — every practice names the paper behind it — was enforced only in the sense that the field was not blank.

Declaring the relationship instead makes four checks out of one: present, shaped like a code, belonging to that scheme, resolving to a document (ADR-060).

converse names the field holding the same relation read backwards — extends and extended_by, or compared_against naming itself, which is what symmetry is. Declaring it is what licenses luria link --fix to write one side from the other, and what makes a one-sided pair a finding; a relation with no declared converse is left entirely alone, because its reverse edge would be a guess (#178).

many says the field holds a list of codes rather than one. Without it a list was stringified and its first code checked, the rest ignored — structured input coerced to prose and half-read, with no finding. A scalar field given a list is now a finding; a plural field checks and resolves every element, and each becomes an edge (#141).

invariant names the field both ends of the relation must share a value in — the assertion A extends B makes and never spells out. It lives here, on the relation, rather than only on a chain, because the assertion is a property of the relation itself: a chain adds transitivity and a rendered page, neither of which a relation needs in order to mean something. Declaring it here is also the only way to assert it over a relation that crosses schemes, since a chain may not (ADR-106, #272).

keytypedefault
fieldstrrequired
schemestrrequired
requiredboolTrue
manyboolFalse
conversestrunset
invariantstrunset
labelstrunset
blurbstrunset
required_whenRequiredWhen | Noneunset

Tag groups — schemes.X.fields.<field>.groups.<name>

Which of a field’s values may appear together. A vocabulary says what a value means; a group says whether the field is an axis or a pile.

A set of a scheme’s tags that combine under a rule.

tags.yaml declares what a tag means; this declares which of them may appear together, because some vocabularies are axes rather than piles. An argument is sound or overreaching or invalid — exactly one — and saying so in prose leaves it to be checked by nobody, which is how a rule becomes a comment.

Opt-in per scheme: a scheme declaring no group is unconstrained, which is every scheme that exists today.

keytypedefault
namestrrequired
tagsfrozenset[str]required
fieldstr"tags"
requirestr"any"
excluded_byfrozenset[str]frozenset()
alertstrunset
labelstrunset
blurbstrunset
derivedboolFalse

Field groups — schemes.X.field_groups.<name>

Several fields of which an entry must carry some, named for what they have in common — the shape of a source, where requires can only spell one of the fields that would be one.

Several fields of which an entry must carry some — a requirement that is satisfied by any of them, named for what they have in common:

schemes:
  LIT:
    field_groups:
      source:
        fields:
        - arxiv
        - doi
        - url
        require: at-least-one   # or exactly-one, at-most-one

requires demands every field it names; a paper that was never posted to arXiv but has a DOI, or only a URL, has a source all the same, and demanding arxiv of it is demanding the wrong thing. The group says what is actually required — a source — and which fields count as one. Opt-in per scheme, like a tag group (ADR-054).

keytypedefault
namestrrequired
fieldstuple[str, ...]required
requirestr"at-least-one"
uniqueboolFalse
labelstrunset
blurbstrunset

Vocabularies — vocabularies.<name>

The central table a scheme’s fields point at by name, so a set of values and their blurbs are declared once however many fields use them. A value may be a bare blurb or this nested table.

The nested shape of one entry in the central vocabularies: table.

This exists so the set of keys a nested table may carry is a consequence of the declaration rather than a second list beside it. alert arrived on a vocabulary (#273) and the nested form arrived separately (#279); the first record to use both was refused, because the discriminator spelled the keys inline and nobody updated them when the dataclass grew a fourth (ADR-110, #281). A list that has to be edited in step with a dataclass is one that will not be — so there is no list, and KEYS below is read off the fields.

It is a schema, not a carrier: _vocabulary_tables builds one and reads its attributes, so a field added here reaches both the discriminator and the metadata in the same edit.

keytypedefault
labelstrunset
blurbstrunset
alertstrunset
termsdict[str, dict]unset

Fragment directories — fragments.<dir>

One entry per directory whose files are assembled into a single document and then consumed. The changelog is the shipped instance; the mechanism is not changelog-shaped.

One fragment directory: where its pieces assemble to, and in what shape.

fragments:
  record/changelog.d: CHANGELOG.md         # the append style
  record/changelog.d:                      # or, spelled as a mapping:
    file: CHANGELOG.md
    style: changelog

append is the narrative shape: bodies oldest-first, inserted before the marker, so the marker stays at the end and the log reads top-down. changelog is the release shape: each collection is one dated batch, inserted right after the marker so the newest batch reads first, fragments newest-first within it. The shape is configuration because the fragment convention is the contract, not the collector (ADR-028) — the same directory-of-fragments serves either reading order.

keytypedefault
targetPathrequired
stylestr"append"

Journals — journals.X

One entry per stream of dated entries that persist. This is a family like any other — a project can run a devlog, a meeting log and an incident log side by side, each with its own granularity and output.

Dated entries that persist, rendered into books (ADR-020).

journals:
  devlog:
    dir: record/devlog.d      # entries, partitioned yyyy/mm/dd/
    output: docs/devlog       # a directory of books plus an index
    granularity: month        # year | month | day
    title: Development log
    blurb: "…"                # optional prose for the index

The difference from a fragment directory is that nothing is consumed: an entry was true when written and stays true, so the view is generated from sources that persist rather than collected from sources that are deleted.

keytypedefault
namestrthe table’s own name
dirPathrequired
outputPathrequired
granularitystr"month"
titlestr"Journal"
blurbstrunset

Chains — chains.X

One entry per relation walked transitively and rendered as sequences. Where a reference field gives a document its neighbours, a chain answers the question no single document holds: what line of work is this a step in?

A relation walked transitively and rendered as sequences (#171).

chains:
  lineage:
    scheme: LIT
    relation: extends
    sibling: compared_against
    output: docs/lineage.md
    title: Lines of work

relation takes one field or several — ["extends", "corrects"] — and several are walked as one spine (#211). That is not a convenience: a record can carry succession with a sign, where “builds on the parent” and “exists because the parent is broken” are both steps in one line and one relation renders them identically. Two relations state the difference where a per-entry attribute would have to qualify a reference, which is the shape the consumer record’s own ADR-011 refuses. The sign is which field the code sits in.

edges.py already reads a reference field as a typed relation, and the site already renders each page’s neighbours. What no view answered is what sequence is this document a step in — and that is the question the documents themselves were answering, one prose paragraph each, until two of them went stale on the same fact and had to be corrected in two places.

Both fields must be declared references on the scheme; a chain over a field nothing declares would render nothing, and nothing looks exactly like current (DP-15).

keytypedefault
namestrrequired
schemestrrequired
relationtuple[str, ...]required
outputPathrequired
siblingstrunset
titlestrunset
facet_bytuple[str, ...]('status',)
blurbstrunset
invariantstrunset

Remotes — remotes.X

One entry per foreign project whose codes this record cites. The uid key is the general case: with a regex and a URL template, a remote need not hold a Luria-shaped record — or any record — at all, which is how arXiv identifiers, ticket keys and CVE numbers become linted references.

Another project’s record, cited from this one.

A reference to it composes the remote’s prefix with the foreign scheme’s own code — LU-ADR-013 — so the namespace is explicit at the point of use and nothing has to guess which project an unprefixed code meant (ADR-016).

remotes:
  LU:
    name: luria
    repo: dmarx/luria           # GitHub owner/name
    ref: main                   # branch or tag the links point at
    dir: record/decisions.d     # where its decisions live
    url: "https://…/{code}.md"  # optional: overrides construction

Everything but repo (or url) has a default, because the defaults are Luria’s own conventions — a remote that uses them needs one line. A code family with a different shape gets a schemes entry (RemoteScheme), which wins over these remote-level settings for its own prefix.

A remote need not hold a Luria-shaped record at all (ADR-024). Give it a uid pattern and its references are the prefix, the delimiter and whatever the pattern matches — an arxiv id, a ticket key — constructed through the url template, which can index the uid’s capture groups by position:

remotes:
  ARXIV:
    uid: '(\d{4})[.:](\d{4,5})'
    url: https://arxiv.org/abs/{1}.{2}   # {0}/{uid} is the whole tail

A pin_url template names where the remote’s stable bytes live, which is what lets luria remotes --pin endorse content that has no GitHub file behind it (#135) — arXiv’s e-print archive is the paper where its abstract page is a rendering:

remotes:
  ARXIV:
    pin_url: "https://arxiv.org/e-print/{1}.{2}"

Both are the short names of a general table: a code relates to a SET of named URIs, each through a template over one vocabulary — {code}, {number}, {prefix}, {repo}, {ref}, {dir}, {document}, {anchor}, and {filename}, which the discovered lockfile map fills. url is uris.read, pin_url is uris.bytes, and a relation Luria does not ship yet is one more name:

remotes:
  LU:
    uris:
      bytes: "https://gitlab.example/{repo}/-/raw/{ref}/{dir}/{filename}"
      history: "https://github.com/{repo}/commits/{ref}/{dir}/{filename}"

GitHub’s blob/raw pair is simply the shipped default pair of read and bytes templates for a remote with a repo — a different forge is a different pair of lines, not a different subsystem.

keytypedefault
prefixstrthe table’s own name
repostrunset
issue_urlstrunset
refstr"main"
dirstr"record/decisions.d"
namestrunset
urlstrunset
delimstr"-"
uidstrunset
pin_urlstrunset
title_restrunset
pinboolFalse
urisdict[str, str]unset
schemesdict[str, RemoteScheme]unset

Per-scheme remote construction — remotes.X.schemes.Y

Optional, and only for a remote whose code families do not all construct the same way — a file per code in one, sections of a single page in another.

How one of a remote’s code families constructs (ADR-023).

A remote is not one directory of files — it is a project, and different schemes in it have different shapes. Each entry names one construction:

remotes:
  SG:
    schemes:
      ADR:
        dir: docs/decisions       # file per code
      DP:
        document: docs/design-principles.md  # sections of one file…
        anchor: "dp-{number}"                # …at stable anchors

anchor defaults to the prefix lowercased plus the number — dp-18 — which is the anchor shape Luria’s own document render emits, so a remote on current conventions needs only the document line. A url template overrides both, and a uris table names further relations for this family alone — both with the full template vocabulary (see Remote).

keytypedefault
prefixstrthe table’s own name
dirstrunset
documentstrunset
anchorstrunset
urlstrunset
pin_urlstrunset
pinboolFalse
urisdict[str, str]unset

The site — site

Publishing the record as a browsable vault. Every key derives from issue_url for a GitHub project, so the conventional case needs no table at all.

How the record publishes as a browsable site (ADR-042).

site:
  title: Luria
  base_url: dmarx.github.io/luria
  source_url: https://github.com/dmarx/luria/blob/HEAD
  exclude:
  - template/**

Only exclude is genuinely per-project: the rest default off issue_url, because a project that told Luria where its issues live has already told it which GitHub repository it is (DP-3 — derive the projection).

source_url is where a link lands when its target is a repository file the site does not publish: a workflow, a template, the licence. Empty means “leave those links alone”, and luria site says how many it left.

The branding keys are the project’s own artwork, cited by path:

site:
  icon: assets/brand/icon.svg     # favicon, any square image
  logo: assets/brand/lockup.svg   # shown in place of the title
  logo_dark: assets/brand/lockup-inverted.svg   # optional
site:
  theme:
    light:
      light: "#f4f1e8"          # any of Quartz's colour names

logo_dark is only needed when the artwork can’t invert itself. A logo whose SVG exposes a --luria-ink custom property — the convention this project’s own kit uses — is re-inked to the theme automatically, and one that doesn’t is used as it is in both modes.

keytypedefault
titlestrderived from issue_url
base_urlstrderived from issue_url
source_urlstrderived from issue_url
excludetuple[str, ...]unset
publishboolTrue
iconPath | Noneunset
logoPath | Noneunset
logo_darkPath | Noneunset
themedictunset

Environment variables

Two, both escape hatches rather than configuration — they change how a run behaves, never what the record is, which is why they are not in the file.

variableeffect
LURIA_ROOTthe project root, overriding the search for luria.yaml / .git. What lets the test suite run against fixture trees, and what a CI job uses when it checks out a project somewhere other than the working directory.
LURIA_JOBScaps the thread pool that renders views, scans files and probes remote URLs. LURIA_JOBS=1 is the serial escape hatch — the one to reach for when a failure needs a deterministic ordering to reproduce.

Running one module without the package

Every command is a plain function, and the modules stay runnable standalone:

python -m luria.ref_status --all

That is for a project that vendors a single file rather than installing Luria. The CLI is the supported surface; this is the door left unlocked.

What is not configurable

Said plainly, because a reference that only lists dials reads as though everything is one:

  • The lint checks themselves. They are functions, not a registry — a project cannot add a check or switch one off. What it can do is promote warning classes to failures with lint.fail_on, and acknowledge individual findings with the comment directives. A check earns its place by being always wrong and mechanically fixable; anything else is a report.
  • The frontmatter vocabulary. status:, title:, tags:, date:, version:, history: are fixed field names. Which statuses count as in force is per-scheme (active), but the field they live in is not.
  • The record’s shape, from the command line. Adding schemes.RFC costs one table, and that table is the only way to add it: there is no luria add-scheme. Changing a shape that already has documents under it is luria migrate, which executes a spec committed to record/migrations.d/ — renaming a prefix or moving a document between schemes, swept across the repository in one pass. The spec is a file because the rename is a thing the record should remember, not an argument typed once.

Two earlier entries in this list are gone because they stopped being true (ADR-047): the shipped ADR scheme can be removed — declare the schemes family without it — and a declared scheme’s output is unset by omission, because a declared family no longer inherits from the defaults. Both are demonstrated and pinned by examples/ in the Luria repository, which CI runs.