Use a typed reference when a relationship matters semantically.
1. Declare the reference
Example (a fragment: the IMPLEMENTATION table also needs its dir and the rest of a scheme’s keys, and DECISION must be a declared scheme):
schemes:
IMPLEMENTATION:
references:
decision:
scheme: DECISION
required: true
many: true
label: Implements decisionNow decision is not an arbitrary string field. It is a declared relation to DECISION (ADR-060): the lint checks that every value is a code of that scheme that resolves.
A reference field is required unless it says required: false, so spell that out for an optional edge — required: true above only restates the default.
A value may be the target’s code or, where the target scheme declares an alias template, its derived alias (AREA-runtime for AREA-001, ADR-088). The two are the same relation everywhere it is read: the lint, the views, chains, invariants and luria relate. luria link --fix leaves an alias as written, and writes a missing converse in the alias spelling.
If the field is a taxonomy — the documents it points at are categories a
reader browses by, as a promoted vocabulary’s terms are — add group: true.
Each target then gets a page listing the documents that cite it, the way a
vocabulary value does; naming the field as the scheme’s axis does the same
and heads the index with it. Most relations (extends, superseded_by) are
not taxonomies and stay ungrouped.
2. Decide cardinality
Use:
many: falsefor a scalar relation, or:
many: truefor a list. many: false is the default. A field that takes part in a converse pair (below) must be many: true on both sides, because several documents can stand in one relation to the same target.
3. Add a converse when the reverse edge is semantic
A converse is the same relation read backwards: if A extends B, then B is extended_by A. Declare it as a pair — the reverse field is declared on the scheme whose codes the first field holds, and each side names the other (ADR-084, ADR-097):
schemes:
RFC:
references:
extends:
scheme: RFC
required: false
many: true
converse: extended_by
extended_by:
scheme: RFC
required: false
many: true
converse: extendsFor a relation that crosses schemes, the converse field lives on the other scheme and points back: SOTA.introduced_by holding LIT codes pairs with LIT.introduces holding SOTA codes.
Only declare a converse when the reverse relation is actually known. Without one, Luria does not guess, and nothing writes a reverse edge.
A symmetric relation is its own converse:
compared_against:
scheme: RFC
required: false
many: true
converse: compared_againstA half-declared pair — a converse that is not a declared field of the target scheme, does not name the original back, or is not many: true — is refused when luria.yaml loads.
4. Use conditional requirements for state-dependent fields
Every scheme already has a successor relation: its successor field defaults
to superseded_by. An entry carrying the scheme’s retires_on status
(default Superseded) must fill it (ADR-071), and an entry carrying its
active status may not, because it cannot be in force and replaced at once
(ADR-125). Do not redeclare superseded_by merely to make those
rules conditional.
For your own field or relation, required_when expresses the same general
shape. For example, a proposed or deferred recommendation can be required to
say what would settle it:
fields:
promote_when:
required_when:
status:
- Proposed
- DeferredThe field is required only in the named states: an entry at status: Proposed
with no promote_when: fails the lint, and an Active one does not.
forbidden_when takes the same table in the opposite sense: the field may
not be written in the named states. Adding
forbidden_when:
status:
- Activemakes an Active entry that still carries promote_when: a violation, since
nothing remains to settle. A value can be in one of the two lists, not both,
and a field with a default cannot be forbidden.
5. Add an invariant when the edge implies shared structure
Example:
extends:
scheme: RFC
invariant: tagsNow the edge claims the endpoints share some declared subject vocabulary. The invariant belongs to the relation, so it holds whether or not a chain walks the field, and it may cross schemes (ADR-106).
6. Write the relation
luria relate SOURCE FIELD TARGET writes a declared relation into an existing document’s frontmatter. SOURCE and TARGET are codes, not paths; the target must resolve.
$ luria relate RFC-003 compared_against RFC-002
record/rfcs.d/RFC-003.md: compared_against += RFC-002It writes the one side you named. Until the other side exists, luria lint reports the pair under one-sided-relations; luria link --fix writes the converse:
$ luria link --fix
linked 0 reference(s) in 19 file(s)
wrote 1 back-reference(s) in 1 file(s)luria relate --draft FILE reads relations instead of taking them as arguments: FILE is a JSON drafts file whose relations list holds {"source", "field", "target"} entries, each written as above.
7. State the relation where you explain it
A relation is often clearest in the sentence that justifies it. State it
there, with the same comment-directive grammar every acknowledgement uses:
the ref:: namespace, then the field that holds the relation.
The recovery path is RFC-007's, generalised. <!-- ref::extends: RFC-007 -->
<!-- ref::extended_by-block: RFC-012, RFC-015 — both carry the retry loop on -->
Two later designs picked this up...The name after ref:: is a reference field of this document, either one
your scheme declares or the built-in superseded_by. There is no direction to
choose, because a relation read the other way has its own name, its converse.
The namespace keeps these names apart from the fixed directive vocabulary, so
a field may be called anything but a name ending in -block or -file, which
would read as a scope. It also means a misspelt field is reported rather
than ignored.
Everything a directive has comes with it: line, -block and
-file scope, a — reason, and until <date>. An example in a code span
or fence states nothing, and a statement in frontmatter comments is ignored
(ADR-122).
[[extends::RFC-7]] is shorthand for a citation plus its statement.
luria link --fix expands it to [RFC-7](RFC-007.md)<!-- ref::extends: RFC-007 -->.
A shorthand naming a field this document doesn’t have is left unexpanded and
fails the lint, and a ref:: statement naming one is bad-annotations.
Pushing up. A stated relation missing from frontmatter is
unrecorded-relations, and luria link --fix writes it into this document’s
field. The converse completion in the same run writes the far side.
Pushing down. A reference declared explain: asks for the reverse: every
code the field holds accounted for in the body. There are two strengths.
references:
extends:
scheme: RFC
required: false
many: true
converse: extended_by
explain: cited # or: stated (stricter); `true` means citedexplain: cited(ortrue, which means the same) asks that the body cite each code somewhere, and takes the citation as serving the relation. A code the body never cites isunexplained-relations, a report rather than a fix, since the explanation is prose only you can write. This is the useful default: it finds the relation nothing in the prose mentions, and leaves alone the citations whose sentence already says what the relation is.explain: statedalso asks that each citation carry a statement of the relation, aref::statement governing it. A citation with no statement isunannotated-relations, and the fixer writes the statement after it. Use it when the relation’s name adds something the prose would not.
At either strength, a statement’s — reason counts as the explanation, so a
-file statement with a reason is how you say the relation needs no more
prose than that.
At either strength, a citation inside a reference entry does not count.
A line like Kingma et al. (2014), LIT-001 — ARXIV-1412.6980. names the work
and says nothing about it. A code cited only there is unexplained-relations,
and the row gives the entry’s line. Entries are found by their shape, not by
the heading they sit under: an author/year lead or a list of initials and
surnames, identifiers, a title and venue, and almost no prose left over. An
entry followed by a sentence about the work, an annotated entry, is prose and
does count (ADR-123).
What to write. Say what the relation is for this pair: what the source
measured or showed and which part of the claim it supports, what an
extension took from its parent and what it changed, who compared what with
what and what came out. A sentence that only names the work (as shown in LIT-012) passes the check and explains nothing.
When there is nothing to write. Clearing these findings means reading the cited work, and sometimes the reading finds that the relation is wrong: the source never makes the claim, the comparison was never run, or the origin is an earlier paper the cited one credits. Then fix the relation (repoint it, or remove it on both sides) instead of writing prose for it. The finding clears either way. Only the second leaves the record true.
A statement the record can’t hold as written is bad-annotations. That
covers an argument that isn’t a code, a code naming no document here, a code
in a scheme the field doesn’t hold, and a single-valued field that already
holds another code.
8. Lint the result
$ luria lintCheck for:
- unresolved targets,
- wrong target scheme,
- converse inconsistencies,
- invariant failures,
- relations stated in prose and missing from frontmatter, or the reverse,
- relations declared
explain:that the body never explains, - standing-related findings.
9. If the relation forms a longitudinal sequence
Do not maintain a prose lineage by hand.