ADR-071: Typed edges come from fields, and a superseded document names its successor in superseded_by:
Status Active · Filed 2026-09-03 · Issue #141
Context
The graph this package maintains has one kind of edge: a document mentions another, found by scanning prose for codes. That is the right primitive — the graph is a side effect of writing carefully (ADR-058) — and it is coarser than what the record already knows.
Three facts in the record are stronger than a mention, and each was written down long before anything read it as an edge:
| the fact | where it lives | since |
|---|---|---|
| this decision was replaced by that one | the Superseded — by … status note | ADR-003 |
| this principle was shaped by those decisions | influenced_by: | ADR-012 |
| this practice’s evidence is that paper | a field the scheme declares a reference | ADR-060 |
Nothing read them. The status report split the note off and discarded it;
the site rendered the status line verbatim; the index only rebased its
link. So a reader of ADR-035’s page was never told it replaced ADR-007, and
a downstream practice page on a site lost its source entirely, because
frontmatter renders as nothing. ADR-060 named typed backlinks as the
obvious next step and deferred them as a rendering change. The proposal in
#141 asked for the same edges and, for supersession, proposed a new field:
status: Superseded
superseded_by: ADR-035This record’s convention was status: 'Superseded — by [ADR-035](ADR-035.md)'
— the pointer in a prose note, which luria migrate --strategy supersede
wrote and nothing read as structure. The proposal’s field is the right
shape: a relation is a fact the tool should check, resolve and render,
and a field is where that happens. The note keeps its place for prose.
Decision
Typed edges come from fields. A structured field is what the tool can check, resolve and render, so a field is the preferred home for a relation, and the three the module reads today are fields; the field name is the relation:
A ──source─────────→ B any declared reference field, named for the field
A ──superseded_by──→ B the built-in `superseded_by:` field
A ──influenced_by──→ B the `influenced_by:` listProse keeps its place. A code in prose is a citation the scanner finds,
and where a record has not yet written a field, a recognised construction
in prose can seed one — luria index fills superseded_by: from an
old-form by CODE note. The field is what a document is then held to,
and what the graph reads.
Four details are load-bearing.
superseded_by: is a field every scheme has. A built-in reference,
one code or a list, into any local scheme — a decision may be superseded
by a principle — checked and resolved like a declared reference, and an
error when a Superseded document leaves it empty. The successor is
structure, and structure is what the edge, the index column and the site
read. The status note (ADR-072) stays prose for
what the field cannot say, and the old canonical by CODE note is read
once more, by luria index, as the repair that fills the field and drops
a note that said only that.
This reverses two earlier drafts, and the reason is worth stating in the reviewer’s words: the whole point is to move toward structured, explicit documentation of these relationships, not to double down on inference over free text. Fields are concrete and definable; inferring a relation from prose is strictly weaker and makes the author cooperate with a particular phrasing inside text that is supposed to be free. Given a structured interface, requiring implicature instead would countermand it.
A remote code is a citation, not an edge. A remote’s namespace is
theirs (ADR-016); the graph has no node for an edge to land
on. In superseded_by: it passes as a citation the remote machinery
verifies.
Rendered where a document has room. The site’s record line gains the
edges both ways: on the page that supersedes, Supersedes; on the
decision a principle cites, Influenced; on a practice, its Source; on
the paper, Cited as source by. The status itself reads Superseded — by [X] on the index and the site, composed from the field. Composed with
wikilinks and expanded by the resolver that owns every target in the
record (DP-4), so nothing here
spells a link.
The status field is three fields. One Status(value, note, superseded_by) on the document replaces the six places that split the
word off the note with three spellings of one regex. The word is data,
the successor is a reference, the note is prose.
Alternatives considered
- Infer the successor from the status note, which the first two
drafts of this decision did — first from every code in a Superseded
note, then from the canonical
by CODEopening only. A reviewer caught each: the first over-claimed for a note that runs on past its successor (ADR-015’s does); the second was inference in place of structure, asking authors to phrase free text so a regex would read it. Reading prose is welcome as a seed — the shape survives as the repair that fills the field — and the field is what the record is held to. - A
status_noterelation for every other code in a note, which the second draft also did: where a code was found is provenance, and naming the location as the relation dresses a location up as a meaning. Those codes are citations now that the note is a prose key. - Leave it to the site’s backlinks. Quartz already lists every page that mentions this one. Untyped: “ADR-007 mentions ADR-035” and “ADR-007 was replaced by ADR-035” render identically, and the second is the fact a reader came for.
- A
[luria.relations]table naming relations and their inverses. A named non-goal of #141, and nothing to configure yet: the field name says what the relation is, and the two built-in inverses fit in a dict. - Render the edges in the generated index and tag pages. The index is a table with one row per document and no room for a list; the site page is where a document has room. Deferred rather than rejected — the graph is read once and any view can consume it.
- Status quo. Facts written down and read by nobody, which is the shape this package objects to everywhere else.
Consequences
Twenty-six of this record’s sixty-six decision pages gain a typed edge the moment the site is next built; every succession and every principle’s lineage now reads in both directions. A downstream record with declared references gets its citations back on the site, in both directions, with no configuration change.
Three decisions in this record moved on the first luria index: the
field filled from the old note, the note dropped where it said only the
code and kept where it said more. A Superseded document with an empty
superseded_by: is now a lint error, on this record and every downstream
one; the finding names the field to write. The tombstone luria migrate --strategy supersede leaves writes the field.
The built-in field is the first of its kind, and the compiled contract carries it as such: checked like a declared field, absent from the record page’s “beyond the standard fields” list, and not counted as a declaration.