ADR024 RelatesTargetsInPair
Title: Relates targets must be in the pair
- Status: Accepted
- Decided: D2026-09-11
- Revises: ADR007
RelatesEndpointDimensionPairs - Introduces: SP026
TargetNotInPairError - Affects:
Parser,Relates - Pairings:
- ADR007
RelatesEndpointDimensionPairs— the neither-endpoint fallback
- ADR007
Context
007 settled how a connection written inside a relates block binds. Its ruling is two sentences long and it is the right one:
The endpoint that is not
tois the subject.tois the target.
That is what makes relates A and B and relates B and A mean the same thing.
The block never names a subject, and it does not need to, because naming either
endpoint as the target determines the other by elimination. The same record says
so plainly: clause order does not pick the subject.
It then added a third case:
A
tothat names neither endpoint treats left as subject so the target dimension is still consulted.
That clause was reached for a resolution reason. If the target is a stranger, something still has to be the subject or the edge word cannot be looked up, and left was available. It reads as a decision about keeping the resolver total.
Its effect is that a stranger compiles. Found while modelling a television show:
relates Carl and ApartmentForRent {
describe {
It is strongly implied in the show that Carl lived in the apartment that
was for rent as of season 2, episode 11.
}
relationships {
appearsIn { S2E11 }
isHeldBy { Mayor }
}
}
Neither S2E11 nor Mayor is Carl or the apartment. It compiles with no
diagnostics, and the manifest records this:
{ "edge": "isHeldBy",
"relationship": "SchittsCreek.role.hasRole::isHeldBy",
"to": "SchittsCreek.Mayor" }
There is no subject on it. For a well-formed relates none is needed — a reader
derives it as the endpoint that is not to. For a stranger it is not derivable
at all. 007 says the subject is Carl; nothing in the artifact says Carl, and no
consumer of the manifest can find out. Read back as a sentence it is
Carl-and-ApartmentForRent isHeldBy Mayor, which is about nothing.
Three problems, in increasing order of seriousness.
The clause contradicts the bullet two lines above it. Order does not pick the subject when the target is an endpoint, and does pick it when the target is a stranger. One record, two rules, and the difference is invisible at the call site.
The manifest cannot carry the result. The language permits writing a fact the artifact has no field for. Whatever the resolver decided is lost on the way out.
The better home already exists. A fact about one of the two belongs in that thing's own relationships block, where the subject is the node containing it and is therefore structurally present. Nothing is lost by refusing it here.
Decision
Every target inside a relates block must resolve to one of the two paired
endpoints. A target that names neither is SP026
(TargetNotInPairError, kind target-not-in-pair).
The comparison is on resolved identity, not on spelling, so the village's
produces { ClockworkVillage.Egg } inside relates Chicken and Egg continues
to bind exactly as before.
007's ruling is otherwise untouched. The endpoint that is not the target is
still the subject, clause order still does not pick it, endpoints may still be
dimensions, and conflicting inverses are still SP023. Only the third case is
removed, and with it the last branch of relatesSubjectEndpoint that returned
left for want of anything better.
A relates edge now records its subject. Each entry in a relates
relationships array carries a subject alongside edge, relationship and
to. With strangers refused the subject is always derivable, so this is
redundant on purpose: it means a consumer reads the direction rather than
reimplementing a rule from an ADR.
Consequences
A minor for prose-parser, and one for the CLI, by
021. An error appears where none did, which is
squarely the surprise test, and the manifest gains a field, which is output
changing for unchanged input. The CLI prints the diagnostic and ships the
manifest, so it takes its own entry.
Some universes stop compiling, and that is the point. Every one of them was recording an edge whose direction the manifest never captured. 021 anticipates exactly this shape of change and asks that the changeset body say so.
One legitimate want is refused along with the nonsense. The two lines in the
example are not the same mistake. isHeldBy { Mayor } is a fact about Carl,
misfiled, and it has somewhere better to go. appearsIn { S2E11 } is a fact
about the pairing — this connection was true as of that episode — and it has
nowhere to go at all. That is axis 2 of 018,
which names it precisely: "a concept or an edge is true across part of an
ordering."
So the accidental fallback was the only way to write a thing the language has an open proposal about. Refusing it is still right, because what it recorded was uninterpretable. But the refusal should not be read as deciding against the want, and SP026's description says as much.
A small piece of evidence for 018. The want surfaced unprompted, in a universe with no code in it, from somebody watching television and writing down what they saw. 018 already cites a Schitt's Creek modelling exercise as one of the two cases that reframed the temporal question; this is a third instance of the same pressure, arriving without anyone looking for it.
One degenerate case is not addressed. If both endpoints resolve to the same
identity, relates A and A, then "the endpoint that is not the target" is
undefined and this record does not say what happens. It is left alone because it
has not come up, and because it is a question about whether that form should
exist at all rather than about which subject it picks.
Alternatives considered
Keep the fallback and record left as the subject explicitly. The smallest change, and it fixes the unreadable-artifact problem without refusing anything. Rejected because it ratifies order-picks-the-subject, which 007 rules against in the case it actually thought about. It would leave the language with two directional rules and no way to tell at a glance which applies.
Let a relates edge mean a fact about the pairing, with the pairing as the
subject. Genuinely tempting, because it is what appearsIn { S2E11 } wants,
and the relates node has an identity of its own that could serve as a subject.
Rejected as premature. It would make a pairing a thing that can be a subject,
which is a real extension to what prose considers an entity, and 018 is the
record where that argument belongs. Deciding it here, as a side effect of fixing
a fallback, is how a language acquires a feature nobody designed.
Require the subject to be named in the block. Rejected. It is more syntax for something elimination already answers, and it would make the common two-endpoint case wordier to serve a case being removed.
Warn rather than error. Rejected. The output is uninterpretable, not merely questionable, and a warning would leave the manifest carrying edges with no recoverable direction.
Evidence
The fallback is one line. relatesSubjectEndpoint in validator.js:
if (toScopeId === left.scopeId) return right;
if (toScopeId === right.scopeId) return left;
return left;
The third return is the whole of the behaviour this record removes.
That it resolved something is what kept it quiet. isHeldBy is declared on
dimension role; Carl is a character. Treating left as subject found nothing
on character, fell through to the target's dimension, and bound against
role — which is exactly what 007's clause intended. The mechanism worked as
designed. What it produced was an edge with no author.