A relation answers a local question:
What does this record point to?A chain answers a longitudinal one:
What line of development is this record a step in?Chains walk one or more same-scheme relations transitively and render the resulting sequences as generated views (ADR-083). They can add a sibling/rival relation, carry declared fields as facets, and assert a shared invariant across a line (ADR-106, ADR-113).
This tutorial uses a small RFC lineage, but the same mechanism can represent research lineages, evolving practices, standards families, policy histories, or explanatory theories.
1. Start with a scheme that can relate to itself
Assume an RFC scheme with standing and an area vocabulary:
vocabularies:
rfc-status:
Proposed: {}
Accepted: {}
Superseded: {}
Rejected: {}
area:
runtime: {}
storage: {}
security: {}
schemes:
RFC:
dir: record/rfcs.d
output: docs/rfcs
active: Accepted
references:
extends:
scheme: RFC
required: false
many: false
corrects:
scheme: RFC
required: false
many: false
compared_against:
scheme: RFC
required: false
many: true
fields:
status:
vocabulary: rfc-status
tags:
vocabulary: area
many: true
required: true
closed: trueAll three references stay within the RFC scheme. That matters because a chain is a sequence within one scheme: a chain whose relation points at another scheme is refused when luria.yaml loads, and a cross-scheme invariant belongs on the reference itself (references.<field>.invariant, ADR-106).
Each reference says required: false on purpose. A declared reference is required unless it says otherwise, and the first step of a line has nothing to extend — without it, RFC-001 fails the lint with no `extends:` in frontmatter.
2. Scaffold and create entries normally
$ luria init
$ luria new rfc --title "Durable jobs with acknowledgements" --tags runtime
$ luria new rfc --title "Renewable leases for durable jobs" --tags runtime
$ luria new rfc --title "Lease recovery without global polling" --tags runtimeThe kind is the scheme’s prefix in lower case. --tags runtime replaces the template’s tags: [record], which the closed area vocabulary would reject; every entry starts at the template’s status: Proposed.
3. Add semantic edges
Use the relation-authoring workflow rather than inventing a second lineage document.
For example:
RFC-002 extends RFC-001
RFC-003 corrects RFC-002luria relate SOURCE FIELD TARGET writes a declared relation into the source’s frontmatter:
$ luria relate RFC-002 extends RFC-001
record/rfcs.d/RFC-002.md: extends += RFC-001
$ luria relate RFC-003 corrects RFC-002
record/rfcs.d/RFC-003.md: corrects += RFC-002The important result is local source truth:
# RFC-002
extends: RFC-001# RFC-003
corrects: RFC-002Authors state the edges. The chain will derive the line.
4. Declare the chain
Add:
chains:
durable-jobs:
scheme: RFC
relation:
- extends
- corrects
sibling: compared_against
output: docs/durable-jobs-lineage.md
title: Durable jobs lineagerelation can name one field or several. Several relations are unioned into one spine before the walk, so “builds on” and “corrects” both advance one historical line. The chain page does not say which field joined each step; that distinction stays in the source records, where extends: and corrects: remain different fields.
The sibling relation belongs to the line without advancing it.
The output is a page directly in docs/, so it must be linked from docs/README.md — otherwise luria lint fails with docs/README.md: missing index entry for durable-jobs-lineage.md. Add a line to the list there:
- [Durable jobs lineage](durable-jobs-lineage.md) — the RFC line, generated from `extends:` and `corrects:`.5. Generate the view
$ luria indexThe chain page is generated from declared edges. docs/durable-jobs-lineage.md nests each step under the one it follows:
# Durable jobs lineage
1 line, walked from `extends:` and `corrects:` on RFC documents. Each step explains itself; this page is the order they came in.
## From Durable jobs with acknowledgements
- [RFC-001](../record/rfcs.d/RFC-001.md) — Durable jobs with acknowledgements *(Proposed)*
- [RFC-002](../record/rfcs.d/RFC-002.md) — Renewable leases for durable jobs *(Proposed)*
- [RFC-003](../record/rfcs.d/RFC-003.md) — Lease recovery without global polling *(Proposed)*Do not hand-maintain that sequence elsewhere. The chain exists specifically to avoid a second prose lineage drifting from the source relations. That is DP-3’s source/projection principle applied to history.
6. Add vocabulary-backed facets
A chain is more useful when each step carries the axes needed to interpret it.
By default, each step shows its status. To show more, declare the field on the scheme first — facet_by naming an undeclared field stops luria index with facet_by names 'consensus', which RFC does not declare:
vocabularies:
# rfc-status, area ...
consensus:
emerging: {}
contested: {}
converged: {}
schemes:
RFC:
# ...
fields:
# status, tags ...
consensus:
vocabulary: consensusA declared field is optional unless it says required: true. Then name it in the chain:
chains:
durable-jobs:
scheme: RFC
relation:
- extends
- corrects
sibling: compared_against
facet_by:
- status
- consensus
output: docs/durable-jobs-lineage.md
title: Durable jobs lineageSet status: and consensus: in each RFC’s frontmatter (RFC-001 and RFC-002 Accepted, converged and contested; RFC-003 stays Proposed, emerging) and run luria index. Each step now carries both facets:
- [RFC-001](../record/rfcs.d/RFC-001.md) — Durable jobs with acknowledgements *(Accepted, converged)*
- [RFC-002](../record/rfcs.d/RFC-002.md) — Renewable leases for durable jobs *(Accepted, contested)*
- [RFC-003](../record/rfcs.d/RFC-003.md) — Lease recovery without global polling *(Proposed, emerging)*The sequence is the same graph either way, but the interpretation is not.
Vocabularies therefore serve at least three roles:
- classify records,
- constrain admissible values,
- facet higher-order views.
See Vocabularies and independent dimensions.
7. Add a chain invariant
Suppose every step in this lineage should share at least one tags value.
Declare:
chains:
durable-jobs:
scheme: RFC
relation:
- extends
- corrects
sibling: compared_against
invariant: tags
facet_by:
- status
- consensus
output: docs/durable-jobs-lineage.md
title: Durable jobs lineageNow the line itself asserts something:
These records are not merely connected; they are steps in one subject lineage.
A chain’s invariant asserts each edge it walks and the line as a whole (ADR-106). The page is also organized by it: after luria index, the line appears under a ## runtime heading, the value its members share (ADR-113). If a step shares no tag with the line, luria lint reports it.
8. Treat an invariant failure as diagnosis, not a verdict
Suppose:
RFC-001 tags: [runtime]
RFC-002 tags: [runtime]
RFC-003 tags: [security]but RFC-003 corrects RFC-002. Change RFC-003’s tag to security and run luria index and luria lint. The lint still passes — these are warnings — and reports:
luria: 1 relation(s) assert an invariant neither end holds — either the field is missing a value that is true, or the relation is wrong (`luria reports` for the table)
RFC-002 ↔ RFC-003 share no `tags` (durable-jobs; each holds: runtime / security)
luria: 1 sequence(s) share no value across every member, though each step may — the weaker signal, to read whole before acting on
RFC-001, RFC-002, RFC-003 share no `tags` across the whole line (durable-jobs)The chain page moves the line under a ## Sharing no `tags` heading.
An invariant finding can mean several different things:
- the relation is wrong,
- the metadata is wrong,
- the line genuinely crosses subjects and the invariant is too strong,
- the vocabulary lacks a concept that all three records actually share.
The last possibility is important.
A linter normally sounds like it validates instances against a fixed ontology. A chain can instead reveal pressure on the ontology itself.
If the real shared concept is job-lifecycle, adding that vocabulary term may be the correct repair.
9. Add a sibling or rival branch
Suppose another RFC proposes an incompatible approach:
$ luria new rfc --title "Durable jobs on an external broker" --tags runtime
$ luria relate RFC-004 compared_against RFC-002
$ luria indexA sibling relation allows the generated view to show that comparison without pretending RFC-004 is the next successor. It joins the line as an alongside: entry, not a nested step:
- [RFC-001](../record/rfcs.d/RFC-001.md) — Durable jobs with acknowledgements *(Accepted, converged)*
- [RFC-002](../record/rfcs.d/RFC-002.md) — Renewable leases for durable jobs *(Accepted, contested)*
- [RFC-003](../record/rfcs.d/RFC-003.md) — Lease recovery without global polling *(Proposed, emerging)*
- alongside: [RFC-004](../record/rfcs.d/RFC-004.md) — Durable jobs on an external broker *(Proposed)*This distinction keeps the graph semantically honest:
succession ≠ rivalry10. Understand the validation boundary
Luria validates chain declarations eagerly because a broken chain often renders nothing, and an empty generated page can look exactly like a correct empty result.
The configuration therefore requires:
- a declared scheme,
- declared reference fields for spine/sibling relations,
- same-scheme targets for chain-walked relations,
- declared facet fields,
- a declared invariant field,
- an output path.
This is an application of DP-15’s concern with silent non-events: “nothing happened” should not be indistinguishable from “everything worked.”
11. Publish the chain
A chain’s output is a generated projection and participates in the publishing surface like other views.
A published chain is especially useful because it answers questions that per-document pages cannot:
- Where did this line start?
- Which step followed which predecessor?
- Where did a rival branch appear?
- Which parts are active, contested, or provisional?
- What subject invariant binds the line?
When site publishing is enabled, chain pages become longitudinal navigation over the same source relations already visible locally.
What you learned
A relation stores a local semantic edge.
A chain derives a line from those edges:
local relations
↓
transitive walk
↓
faceted sequence
↓
published viewThe chain owns no independent lineage facts.
That asymmetry is the point.
Authors state the edges. Luria derives the line.