Context
ADR-084 made a converse pair one fact written in two places, and
luria link --fix kept the two in step, working out against HEAD whether a
one-sided pair was a new write or a stale deletion. It weighed making one
side authoritative and rejected it:
it gives up the symmetric case, where there is no canonical side, and it gives up declaring the relation from whichever document you happen to be holding — which is most of the value.
Its own Consequences section already saw the cost: “With a directed pair there is a canonical side, and the converse is derivable, so storing it buys one thing: the fact is legible when reading that document alone.”
That cost has been measured since. Storing both sides is a hand-maintained
projection of one fact (DP-003), and two writers keep it, the author and
whoever runs the fixer (DP-002). It drifted at DP-003’s rate. Nucleation
declares 63 converse fields and carried 812 one-sided pairs, because
completion lived in a command its CI never ran (ADR-136). Every reader
already worked around the gap: relations.edges() unions a relation with
its converse, so chains, invariants, patterns and reliance read a one-sided
pair correctly. The stored second side was serving only someone reading the
raw frontmatter of the derived document.
Decision
A directed pair declares which side is derived, and only the primary side is stored.
references:
extends: { scheme: LIT, many: true, converse: extended_by }
extended_by: { scheme: LIT, many: true, converse: extends, derived: true }
Every reader gets the derived side the way it already did, through
relations.edges() (held/4 in luria/logic/relations.lp).
The derived field can still be written. This answers ADR-084’s second
objection. An author holding the earlier paper can write
extended_by: LIT-007 on it. luria repair moves that entry as
extends: LIT-001 to LIT-007 and removes it from where it was written. If
LIT-007 already says so, the written copy is just dropped. Either way the
author states the relation from whichever document they hold, and the record
stores it once.
Normalization is a repair (ADR-136). It reads the sources and the
configuration’s marks and nothing else: no HEAD, because nothing ever writes
the derived side back. CI applies it on every pull request, and a second run
changes nothing. It is decided by luria/logic/normalize.lp.
What it does not write, it reports, under derived-relations, which
names luria repair as the remedy for the rest:
- a move that would make a directed relation run both ways within one scheme (A extends B, and B is written as extending A). That is ADR-084’s contradiction rule, unchanged: two things are present, nothing is missing;
- a move or drop that would put either document in breach of its contract. This is ADR-084’s “a repair never breaks the document it lands in”, applied to the move as one unit, so the far side’s refusal never loses the edge by removing it from the near side;
- a derived entry naming no document the field may hold. There is nowhere to move it. The contract reports the code as well.
One side, exactly. A pair with both sides marked derived is refused when
the configuration loads, as is derived on a field with no converse. A
self-converse relation is refused too, because marking it marks both of its
sides.
Why derived: true and not primary: true
The mark goes on the side whose behaviour changes. A derived field is never
stored, and repair moves what is written in it. That is a fact about that
field, and a reader of the configuration sees it where they would look to
learn what the field holds. The primary side behaves exactly as an ordinary
reference does. Nucleation’s configuration already describes its derived
fields this way, each blurb reading “the converse of extends, written by
luria link --fix”, so the key sits beside the sentence it replaces. One key
on one side also keeps the declaration from repeating itself (DP-003, the
argument ADR-097 used against converse_scheme). A rule requiring
primary: true on one side and derived: true on the other could disagree
with itself.
What keeps ADR-084’s rule
Symmetric relations. compared_against, rivals and complements have no
derived side. Each document is an equally natural home for the fact. They
stay stored on both documents and completed by luria link --fix against
HEAD, and they still appear in one-sided-relations until it runs. Storing
each edge once on a canonical document, such as the lower code, would extend
this decision to them. That choice is left open for the owner rather than
made here: the canonical home would be arbitrary, a temporary code changes
its order when luria concretize numbers it, so edges would move at merge,
and a symmetric relation would need its own opt-in key to avoid changing
every existing record silently.
A pair declaring no direction. It keeps working exactly as before, with
both sides stored and completion in link --fix. The lint reports it as
undirected-relations, once per pair, naming the key to add. Refusing to
load would break every record the moment it upgraded, over a choice only its
owner can make (DP-014). Which side is primary is a judgement about the
relation and cannot be inferred, so luria upgrade cannot write it either.
The report is the disclosure, and lint.fail_on can promote it for a record
that wants the migration enforced.
one-sided-relations therefore covers only symmetric and undirected pairs. A
directed pair is never one-sided, because its derived side is computed.
Alternatives considered
- Keep both sides stored (ADR-084). This is what produced the 812 pending pairs. Every one-sided pair was ambiguous between a write and a deletion, and only HEAD could tell them apart, which only works where an author runs the fixer before committing.
primary: trueon the forward side. It reads naturally, but it marks the side whose behaviour does not change and leaves the unusual one unmarked. See above.- Refuse a pair that declares no direction. This is cleaner, and it is hostile to every record that predates the key. See DP-014 above.
- Infer the direction, by declaration order or by which field holds more entries. Rejected for the reason ADR-084 rejected inferring the converse: that would be a guess written into the record.
- Make normalization a
link --fixstep. It would run where completion ran and pile up the same way. ADR-136 puts it inrepair. - Forbid writing the derived field. This gives up ADR-084’s real objection, stating a relation from the document you hold, for nothing: moving the entry is mechanical.
Consequences
Deletion means deleting the primary entry. Removing a derived entry while
the primary holds the edge changes nothing anyone reads, because the
relation is still derived. That is the intended reading: there is one place
the fact lives. One transitional edge follows from this. In a record that
stored both sides, an author who deletes only the primary entry before
repair has dropped the derived copy will see the copy moved back. The
first repair on the default branch closes that window, since no derived
copies remain after it.
ADR-084’s HEAD baseline is no longer consulted for a directed pair. Open PR #387, which makes that baseline read aliases, is moot for directed pairs. It still matters for symmetric and undirected ones.
luria export’s edge table holds a directed relation under its primary
field only. A query for the reverse reads the primary edge backwards. The
site and the chains already render both directions from relations.edges().
Fired on Nucleation (a git archive of its default branch, with
derived: true added to 28 reference fields): luria repair dropped 2,626
derived entries the primary side already held and moved 62 that it did not.
It changed 1,256 files (4,228 lines out, 105 in), held nothing back, found
no clash and no dangling entry, and the second run changed nothing.
luria lint stayed clean, one-sided-relations fell from 812 rows to 79
(all symmetric), and held/4, the relation every reader sees, was identical
before and after at 10,815 edges.