ADR029 EdgeBlocks
Title: Edge blocks
- Status: Accepted
- Proposed: D2026-10-02
- Decided: D2026-10-03
- Follows: ADR025
RelationshipSideBlocks - Followed by: ADR030
DescribeInEdgeBlocks - Revises: ADR028
RecordFactsInTheUniverse - Related: ADR018
TemporalDimensions - Introduces: SP030
DuplicateTargetError, SP031NameInEdgeBlockError - Affects:
adr,Describe,Parser,Relates,Relationships,Ui
Context
Prose can describe a word, one side of a word, and a pair of things. It cannot describe a connection.
event GwenSpeaks {
describe {
There are only two episodes in the entire show when we hear Gwen speak,
which makes it seem like she'd be a somewhat boring character, but she is
anything but.
}
relationships {
involves { Gwen }
happensIn { S1E5 }
}
}
The sentence the author wanted to add is which two lines Gwen delivers in that episode. It is not about Gwen, or about the event, which spans two episodes, or about the episode, where plenty else happens. It is about one connection: this event, in this episode. There is nowhere to write it.
The nearest home is a relates block. relates GwenSpeaks and S1E5 with a
describe compiles, shows on both pages, and reads the same as long as exactly
one edge joins the pair. This universe has been doing that for a while, and
028 made it the rule: "a note about one
particular link, such as which part of a ruling was revised, goes in a
relates block for the pair."
It works by coincidence. A relates block describes a pair, and a pair is not an edge.
- A pair may have no edge at all. Most relates blocks are exactly that: Deedee and Alexis, Stevie and Grant, Carl and the apartment he is only implied to have lived in. Their meaning lives between two things that nothing in the universe connects.
- A pair may have several edges. A note on the pair cannot say which one it is about.
- The note lives away from the edge. A reader of
happensIn { S1E5 }does not see it, and a consumer can only join the two by assuming that a pairing with one edge is talking about that edge.
The want has now arrived four times:
- notes on lineage links in this universe, such as "the relates exception", which 028 moved into relates blocks;
- notes on affects entries, such as "spec text only";
- the per-record sentences on the syntax pages' Decided in, dropped when those pages became generated, because a sentence about how one record affects one block had nowhere to live;
- Gwen's lines, from a universe with no code in it.
The authoring guidance names the gap and turns it into advice. State or
events says an edge "cannot carry a describe, so it holds no meaning beyond
its own word," and that wanting to explain one "is the signal to give the fact
a name." Naming is right for a fact that needs to be pointed at, ordered or
connected. It is the wrong tool for a sentence. Nobody wants a concept for the
way one record affects the parser, and splitting GwenSpeaks into one event
per episode would change the model to make room for a line of dialogue.
Decision
Inside a relationships block, a target may be followed by a block that holds facts about that edge, from the end that wrote it.
event GwenSpeaks {
relationships {
involves { Gwen }
happensIn {
S1E5 {
describe {
The two lines Gwen delivers in this episode.
}
}
}
}
}
An edge block holds a describe and nothing else. A declaration inside one,
such as a title or a relationships block, is SP005. A title has no name
to stand beside (010). A relationships block would make
the edge a subject, which 024 called "a real
extension to what prose considers an entity"; that is deferred rather than
refused, under Alternatives considered. A bare name inside an edge block is
SP031 (NameInEdgeBlockError), an error of its own because it is usually
the symptom of a missing brace (see Consequences). An empty describe, or an
empty block, adds nothing to the edge, the same way an empty side describe adds
nothing to a side.
It works in every relationships block: on a concept, a dimension, a relates block, and a relationship's pair or either of its sides. There is one relationships block in the language, and it gets one rule.
The note stays where it was written. An edge is still mirrored onto its
target, so either end can find the connection, and the mirror is unchanged: it
carries no note. The describe sits only on the entry of the end that wrote it,
so where a note sits records who wrote it, and the manifest needs no field to
say so. On GwenSpeaks:
{ "edge": "happensIn",
"relationship": "SchittsCreek.event.happensIn::isWhenHappens",
"to": "SchittsCreek.S1E5",
"describe": "The two lines Gwen delivers in this episode." }
and on S1E5, the mirror, exactly as it is today:
{ "edge": "isWhenHappens",
"relationship": "SchittsCreek.event.happensIn::isWhenHappens",
"to": "SchittsCreek.GwenSpeaks" }
Each end may describe the connection from where it stands. A connection
written at both ends is already two entries, because the mirror skips an end
that wrote the fact itself. Each keeps its own note, and the two are not
expected to agree. A parent's account of isParentOf and a child's account of
isChildOf are different sentences about one connection:
character JohnnyRose {
relationships {
isParentOf {
DavidRose {
describe { Johnny's side of it. }
}
}
}
}
character DavidRose {
relationships {
isChildOf {
JohnnyRose {
describe { David's side of it. }
}
}
}
}
This is the side-block arrangement one level finer. Side blocks split a relationship by its words (025); edge blocks split a connection by its ends.
Where a note is shown is a consumer's rule. A field guide may show Johnny's note on David's page or not, and either is honest as long as it says where the note was written (026). The language records where a note was written, and nothing about where it should appear.
A target written twice in one block is refused. Today
happensIn { S1E5 S1E5 } compiles into two identical entries with no
diagnostic. With a block after either, the two would be one end saying two
things about one connection. Refusing only when a note is involved would patch
the symptom; the cause is that a duplicate target compiles at all. Targets are
compared on resolved identity, so Pond and Village.Pond are the same target.
That is SP030 (DuplicateTargetError).
A note about one link goes on the link. That changes 028's rule. What
becomes of relates, once a connection can carry its own note, is left to a
later record.
No keyword is added. The block is named by the target it follows, the way a side block is named by its word, and the describe inside it is the describe every other scope has.
Consequences
The grammar change is purely additive. An opening brace after a target is SP016 today, so no universe that compiles uses this shape, and none can change meaning. Refusing a duplicate target is the one breaking part: a universe that lists a target twice in one block stops compiling. None of the universes surveyed below does, which is the reasoning 025 used for SP027. By 021 the parser takes a minor, and the changeset says so.
Writing both ends becomes a supported pattern. It is the only way to put a note at the far end. Two sentences say otherwise and need a clause: "You only ever declare one side" on the Relationships card, and "Write only this record's side of a link" in the records' README.
Showing a note at the far end takes a lookup. The mirror carries nothing,
so a consumer that wants a note at the far end reads it from the other end's
entry. The syntax pages' Decided in reads isAffectedBy, which is the mirror,
so it would have to find each record's affects entry for its sentence. The
field guide already does this kind of lookup, for relates pairings and for side
references.
It revises 028. The README's guidance moves from a relates block to the
link itself, and the five notes in universe/adr.prose can move onto their
edges once the docs compile with a parser that reads edge blocks.
Publish before use. sprig-and-prose compiles this universe with the parser it installs from npm. Neither the universe nor a card example can use an edge block until a parser that accepts one is published. Using one sooner breaks the docs projector and check:docs, which has happened before.
It ships Experimental, as syntax EdgeBlock, the way side blocks did, so
the docs carry it as a section of the Relationships card rather than a page of
its own.
The authoring note changes its test. "Does this fact want a describe?" stops being the signal to name it. The question becomes whether the fact needs to be pointed at, ordered or connected. A fact that needs a name still gets one; a fact that needs a sentence gets an edge block.
A missing brace reads differently. Today, sitsOn { Bench followed by
isNextTo { Pond } fails with SP016 at the second opening brace. With edge
blocks, isNextTo reads as one more target of sitsOn, and { Pond } as its
edge block. Because an edge block holds nothing but a describe, the stray
Pond can be reported as exactly that, with a hint that the block before
isNextTo is missing its }. The hint is left off when the stray word opens a
block of its own, where a misspelled describe is the likelier cause. This
works only because the edge block is parsed narrowly, rather than through the
general declaration parser.
Expects has to account for it. 011 settles the
meaning of every block inside expects in one pass. An edge block there means
nothing yet. If expects ships, it has to say whether one is refused or what it
expects.
What this does not do. It does not give an edge a name or an id, and
nothing can point at one. It does not let an edge carry relationships, so a
fact about when a connection held, such as appearsIn { S2E11 } on a pairing,
or about where one occurrence happened, is still without a home. It does not
decide where a note is displayed. It does
not change what a relates block may hold, and it does not retire it. Adding
relationships to an edge block later would be additive. Taking them away later
would not, which is why they are not offered now.
What becomes of relates. Once a connection can carry its own note, much of
what relates does can be said with an ordinary relationship. A note on an edge
goes on the edge, and a pairing with no word can take a word of the author's
own, such as a relatesTo declared in their universe. That would retire relates'
separate section of the manifest, its subject inference and SP026. It would also
cost the one thing relates has that a relationship does not: a place to write a
note that belongs to neither end. Neutral meaning survives, because a one-word
relationship implies no order between its ends; neutral location does not. That
question belongs to a later record, after edge blocks reach Stable, because
retiring a Stable block for an Experimental one would be backwards.
What to watch. Edge describes will attract time. Two of the bakeoff's relates notes on a single edge already carry it: a manager who has run two teams "since the reorganisation eleven months ago", and a policy replaced "at the October meeting". On an edge, "as of S2E11" will read as a fact about the edge, and it is one, but nothing can query a sentence. If edge describes fill up with episodes and dates, that is pressure on 018's validity axis, and the answer is structure there rather than a longer describe here.
Two notes on one connection can drift apart, and nothing ties them together. That is an authoring risk rather than a fault in the language. A consumer that shows both notes, each with where it was written, puts any contradiction where a reader will see it.
A sentence on an edge is also a claim nothing checks, like every describe. One of the per-record sentences the syntax pages dropped was wrong about 002, and nothing caught it. Edge blocks bring those sentences back, and the same care has to come with them.
Alternatives considered
Keep using relates blocks. No grammar at all, and it already works when one edge joins the pair. Rejected because it works by coincidence. It cannot pick one edge of several, it keeps the note away from the edge, and a second edge added to the pair later makes the note ambiguous without anyone touching it.
Give the fact a name. The current advice. Right for a fact that needs to be pointed at, ordered or connected, and still right after this. Rejected as the only answer, because most of the evidence is sentences rather than facts. For now it is also the only answer to a connection on a connection, such as where one of Gwen's scenes happened, below.
A new keyword, such as note. Rejected. Describe already means meaning in
plain language, and a second word would have to explain how it differs.
A describe on the word's block, such as
happensIn { describe { … } S1E5 }. Rejected. It covers every edge the word
makes from this subject at once, so it cannot tell Gwen's two episodes apart.
Copy the note onto the mirror, so that both ends carry it. A consumer could then read a note from either end without a lookup. Rejected, for three reasons. A copied note looks exactly like one written there, so the manifest would stop recording who said what. It needs a conflict rule for the case where both ends write a note, which is exactly the case where the two are most likely to differ on purpose. And 025 made the same choice for side-level edges: keeping a fact where it was written leaves copying available later as an addition, while copying now could only be undone by breaking consumers.
A way to say where a note should be shown, such as a mark asking for it at both ends. Rejected. Where a note was written is a fact the manifest records; where it is shown is a projection's rule (026).
Merge a target written twice into one edge. Quieter than refusing it. Rejected because it hides what is almost always a mistake, and because a block after each copy would still need a rule for which one wins.
Relationships in an edge block now. The same block could hold a relationships block, and the want is already concrete. Gwen speaks in two episodes, and the two scenes are in different places:
happensIn {
S1E5 {
relationships { happensAt { BobsCabin } }
}
S3E10 {
relationships { happensAt { BobsHouse } }
}
}
There, an edge behaves like an occurrence of the event: Gwen speaking, in this
episode, at this place. Today the only way to say it is to name each
occurrence, which means two concepts that exist only to hold one connection
each. A validity interval, such as appearsIn { S2E11 } on a pairing, has the
same shape.
Deferred rather than refused. An edge that carries connections is a subject, so it needs an identity, its mirror needs somewhere to point, and a word on it has to be looked up from something with no dimension of its own; the natural reading is that an occurrence speaks its subject's vocabulary. Every consumer would also have to learn that a connection can point at an edge. That is the extension 024 declined to make as a side effect, and it wants its own record. This one keeps the way open: an edge block is one block that can later accept one more kind, and refusing a target written twice means each subject, word and target is written once, which is what an identity for an edge would be built from.
An edges table in the manifest, storing each edge once with an id that both ends point at. Cleaner for a consumer, and the likely shape if edges ever become subjects. Rejected as more than this needs: it would change every consumer of every manifest to serve a field most edges will never carry.
Evidence
Every universe within reach was compiled, and for each relates block the edges joining its pair were counted.
- This universe: seven relates blocks. Five sit on a pair joined by exactly
one edge, and all five are a note on that edge: two on
revises, two onaffects, one onrelated. The other two pair things nothing connects. - Schitt's Creek: three relates blocks, all on pairs with no edge. Gwen's lines are the first note this universe wanted on an edge, and the author reached for the edge rather than for a relates block.
- The bakeoff kit, written by a single author and compiled with its leftover
inclauses removed in memory: forty-two distinct relates blocks. Thirty sit on a pair with no edge, eleven on exactly one edge, and one on two. Ten of the eleven read as an explanation of their edge, such as "Reporting deliberately reads a copy that is always slightly behind". The one on two edges is the square and the cube.
So about a quarter of that author's relates blocks are notes on a single edge. One author is one habit, so it counts once. None of these universes lists a target twice in one block.
Compiled against the parser at 0.12.0:
| input | result |
|---|---|
happensIn { S1E5 { describe { … } } } |
[SP016] Expected an identifier, but found an opening "{". |
happensIn { S1E5 S1E5 } |
two happensIn entries on the subject, one isWhenHappens on the target, no diagnostic |
sitsOn { Bench then isNextTo { Pond } |
[SP016] Expected an identifier, but found an opening "{"., at the second brace |
relates A and B twice, with different describes |
one record holding the second describe, and no diagnostic |
The last row is outside this record's decision. It is recorded because it is a second describe for one pairing, answered today by silently keeping the later one.
A prototype of this record was compiled against every universe in reach and
compared with the parser at 0.13.0: 33 compiles, counting the bakeoff kit both
as written and with its in clauses removed. Diagnostics, locations and
manifests were identical, and no file writes a target twice in one block. The
33 canonical fixtures were identical too.