sprig and prose docs classic docs
Reference› ADRs› ADR025 RelationshipSideBlocks

ADR025 RelationshipSideBlocks

Title: Relationship side blocks

  • Status: Accepted
  • Decided: D2026-09-20
  • Followed by: ADR029 EdgeBlocks
  • Related: ADR018 TemporalDimensions, ADR026 ProjectionsStateTheirRules
  • Introduces: SP027 ReservedRelationshipNameError, SP028 SideNotInRelationshipError
  • Affects: Parser, Relationship, Ui
  • Pairings:
    • ADR018 TemporalDimensions — parallel thinking over the same ground

Context

A two-word relationship has two sides, and prose can currently say something about only one thing: the pair.

relationship isLeftOf and isRightOf {
  describe {
    Places sit beside one another.
  }
}

That describe covers both directions at once. isLeftOf and isRightOf do not mean the same thing, and there is nowhere to say so. The same is true of isHeldBy and hasRole, of isProducedBy and produces — every pair whose two words carry distinct meaning gets one sentence between them.

A relationship is, in effect, half an entity. A describe inside one lands in the manifest. A relationships block inside one parses and is silently discarded — no diagnostic, and nothing appears on either side of the intended edge. So a relationship can be described but cannot be connected.

What made this visible

Drawing a diagram. An arrow has to point somewhere, so anything rendering a relationship as an arrow has to decide a direction, and nothing in the manifest tells it one. The case that looks like a counter-argument is really the same argument: a diagram may legitimately want to reverse an arrow for emphasis, and "reverse this" is only sayable if there is a canonical direction to deviate from. A declared direction is what makes a rhetorical one expressible, not its competitor.

The same gap shows up in sequences, which is why this record sits alongside 018 rather than under it. That record surveyed three temporal axes and said of the first, ordering, that "prose already does this… this axis needs nothing." The narrower truth is that nothing declares which way a sequence advances.

Both of the universes that model time do the same thing:

dimension episode {
  relationship follows and isFollowedBy { }
}

episode S1E2 { relationships { follows { S1E1 } } }

follows here points backward in time. isFollowedBy advances. To the manifest, that pair is structurally identical to isParentOf and isChildOf — oriented, but with no statement of which word moves forward. follows is a sequence and isParentOf is not, and nothing says so.

The field guide compensates by guessing, and the guess is anchored on two unrelated accidents: it canonicalizes edge direction on the manifest's forward field, which holds the alphabetically first of the two words, and then picks which way to read a run by preferring whichever direction agrees with the order the author happened to declare the concepts in. Reorder the declarations and a timeline can invert.

This record does not make a timeline renderable, and should not be read as claiming to. It provides somewhere to put the fact. What a universe puts there is its author's own vocabulary, so nothing can draw a timeline from it until some consumer publishes which words it honours and an author opts in. The guessing described above continues for every universe that does not. 018's axis 1 stays open; this record only removes the reason it could not be closed.

Decision

Inside a relationship declaration, a block named after one of the two declared words holds facts about that side.

relationship isLeftOf and isRightOf {
  describe {
    Places sit beside one another.
  }

  isRightOf {
    describe {
      The thing on the right, read as a reader would scan the row.
    }

    relationships {
      indicates { Advancing }
    }
  }
}

A side block accepts describe and relationships. Pair-level describe, title and relationships keep their existing meanings, and pair-level relationships stops being silently discarded — symmetry is a fact about the pair, advance is a fact about a side, and both levels are wanted.

The name must be one of the two words the relationship declared. A block named anything else is an error rather than a new declaration; a relationship body holds facts about that relationship and its two sides, and nothing else lives there. That is SP028.

A relationship word may not shadow a block valid inside its own body. describe, title and relationships become unavailable as relationship names. This is SP027, the same rule as SP021 and SP025 in a smaller scope: a name may not be chosen that would make an existing name unreachable.

No keyword is added for direction. Advancing above is an ordinary concept in the author's own universe, related by a word the author declared. Prose gains somewhere to put the fact and stays silent about what the fact means. Which names a consumer honours is the consumer's published convention — the same arrangement as 011, where the language records and never enforces.

Consequences

The grammar change is purely additive. A block with an unrecognised name inside a relationship body is SP016 today, so no universe that currently compiles uses this shape, and none can change meaning.

The name restriction is technically breaking. All three words are legal relationship names right now; there is no reserved-word check on them at all. The odds anyone named a relationship describe are negligible, but by 021 it is a minor with a note rather than a patch.

The field guide has to publish the names it honours. That is the cost of keeping the ontology out of the grammar: a documented convention rather than a keyword. It is versionable and replaceable, which a keyword is not.

forward and inverse in the manifest want renaming. They hold the alphabetically first and second word, which is not what either name suggests, and the confusion gets worse once a field exists that means actual direction. 023 already wrote the rule for this: a name may be corrected when it no longer describes what it holds, while identity may not be reallocated. Something lexical — first and second — frees the word.

Per-side description is a gain on its own terms. Orientation is one use of this facility, not its justification. The narrower reading of this record is that a relationship has two sides and prose could only describe one of them.

The inverse of a pair-level or side-level edge is not mirrored onto the target concept. indicates { Advancing } lands on the relationship node and Advancing gains nothing. Mirroring would put relationship ids into a slot that currently holds only concept and dimension ids. Adding the mirror later is additive; removing it later would be breaking.

What to watch. If several projections invent several orientation vocabularies, the one that wins will be whichever shipped first rather than whichever was right. The answer is not to stake a vocabulary quickly, which is a land grab wearing a design's clothes. It is for a projection to say plainly what words it looks for, so an author can opt in on purpose rather than by coincidence. 026 carries that obligation.

Alternatives considered

A keyword for direction, such as ordering follows and isFollowedBy { }. Cheapest for consumers, because no convention has to be agreed. Rejected: it puts an ontology back into the grammar, which is what the removed arc layer was. Every further property anyone wanted — transitivity, acyclicity — would cost another keyword, and the survey below found only one property clearly needed.

Treat the first-declared word as primary. Rejected twice over. It makes the reverse unspeakable, because declaring relationship isRightOf and isLeftOf { } to say something about the other side collides with the original as SP012. And the manifest does not preserve declaration order at all — the pair is sorted alphabetically — so the information the rule depends on is already gone.

or instead of and for a bidirectional pair, so that relationship isLeftOf or isRightOf { } marks two equal sides while and marks a primary and an opposite. Rejected. It is the keyword alternative wearing a smaller costume: the distinction lives in the grammar rather than in the universe. or also reads as a choice between two things when both are true simultaneously from different ends, and nothing about the pair is disjunctive. And it smuggles first-is-primary back in through and, which the paragraph above rejects on independent grounds.

Make relationship words globally addressable, so a fact about a side could be authored from either end, as it can for concepts. Rejected on namespace grounds. Relationship words and dimensions do not collide today — a universe may hold both dimension neighbors and relationship neighbors — and putting words into the shared namespace creates a third kind of collision needing its own code. It also gives one relationship three names: a pair identity and two side names.

Author the fact from the concept side, as orientation Advancing { relationships { isIndicatedBy { isRightOf } } }. Rejected even setting the namespace question aside. It scatters facts about a relationship across whichever concepts happen to be their values, so a reader looking at isRightOf would have to search the universe to learn what was said about it.

Declare the head of a sequence instead of the direction. Attractive because it needs no grammar at all: the head is the member with no incoming edge, and from there the single edge present is the advancing one. Rejected as insufficient. It fails on a closed run, where every member has edges both ways and the head says nothing about which neighbour is next. timeOfDay closes, so the first universe to need this is also the counterexample.

Evidence

The guess in sequences.js is explicit in the source, and honest about itself — its header notes that nothing in the manifest says which relationships order the things they touch, so it reads the shape of the edges instead. The direction choice is this:

// Which way round? Compile order is very often the order the author
// wrote the run in, so prefer whichever direction agrees with it.
const forwardIsReading = rising(fwd) >= rising(rev);

Compiled against the parser at 0.9.0:

input result
relationship isDating and dates { } id=V.dates::isDating forward=dates inverse=isDating
relationship isLeftOf and isRightOf { } then the reverse [SP012] Relationship edge "isLeftOf" already exists
dimension neighbors { } and relationship neighbors { } 0 errors, both in the manifest

The first row is the one that settles the primary-declared-first question: isDating was written first and forward came back as dates.

Inside a relationship body today, describe, title and relationships are all accepted, and any other block name is SP016. Of those three, relationships parses and then vanishes, which is the silent case this record also closes.

A survey of what else a relationship might declare found one gap. Bidirectional versus oriented is already encoded by whether the author gave one word or two — a one-word relationship mirrors the same word onto both ends, a two-word one gives each end its own. Cardinality was declined by 011. Transitivity is derivable by walking, and nesting already covers containment. Acyclicity is a validation nicety and timeOfDay cycles deliberately. Only advance direction is both a fact about the world and load-bearing with no renderer present.