ADR023 ErrorNameCorrections
Title: Error name corrections
- Status: Accepted
- Decided: D2026-09-10
- Follows: ADR012
FileScopeUniverse - Affects:
Parser,Relates,Relationships
Context
012 removed the in clause. It dealt carefully
with SP006, which had told authors to add in UniverseName — advice that would
break a universe once file-scope declarations belonged to the universe
automatically. That code was retired rather than repointed, on the grounds that
a code which has appeared in somebody's build output should keep meaning what it
meant.
Two codes written for the same clause were not examined at the time.
SP009 (DuplicateDeclaredParentError) meant a declaration named more than
one parent. It is unreachable — the class is defined and exported, and nothing
throws it, because there is no longer any way to name a parent at all.
SP007 (DeclaredParentNotFoundError) meant the parent named in an in
clause did not exist. It is not unreachable. It is thrown from five sites in
validator.js, all inside resolveEndpoint and the path walker, and it fires
whenever a name used as a target cannot be resolved:
concept Bench {
relationships {
isNextTo { Nowhere }
}
}
[SP007] Declared parent "Nowhere" was not found. Use an existing scope or a
dot-separated identifier.
Nothing there names a parent. This is a misspelled endpoint, which is the most ordinary mistake an author can make in prose, and it is answered by an error named after a clause that no longer exists. The universe's own description of SP007 said "the parent named in an in clause does not exist," which by now described nothing the compiler does.
The two cases were found together, while modelling canCause edges from each
syntax block to the errors it can raise. Both were initially read as retirement
candidates, on the strength of the universe's wording. Reading the code
separated them.
Decision
An error's name may be corrected when it no longer describes what the error does. Its code may not be reallocated.
The two rulings are distinguished by whether the meaning moved.
SP006's meaning became wrong. What it asserted about the world stopped being true, so every previously printed occurrence of it turned into bad advice. That is why 012 retired it and allocated forward.
SP007's meaning never moved. It has always said: a name you referenced could
not be found. Every occurrence of SP007 anybody has ever seen in build output
meant that, including the ones printed when the in clause still existed —
a missing parent was one instance of a missing name, not a different fact. Only
the name and the wording lagged.
So SP007 keeps its code and is renamed:
| before | after | |
|---|---|---|
| class | DeclaredParentNotFoundError |
TargetNotFoundError |
| kind | declared-parent-not-found |
target-not-found |
| code | SP007 |
SP007 |
The message becomes Target "X" was not found. Check the spelling, or write the path from a scope you are already inside.
SP009 is retired, following 012's handling of SP006 exactly:
DeprecatedError, isDeprecatedBy { FileScopeUniverse }, and a description
that records what it used to mean. Its code is not reused.
Consequences
Two minors, by 021. An exported class name
changes shape and the printed diagnostic changes for unchanged input, so
prose-parser is a minor on both counts. The CLI prints those diagnostics, so
its output changes for unchanged input too, and it takes its own minor with its
own changeset entry rather than inheriting one. Anyone matching on the string
declared-parent-not-found has to update; that is the surprise the bump is
flagging.
The name and the universe move together or receipts notice. The class
carries a @sprig Sprig.DeclaredParentNotFoundError marker pointing at the
concept in universe/errors.prose. Rename one without the other and the
receipt goes from recognized to unrecognized on the next sprig observe. The
rename is therefore self-checking, which is a small demonstration of what
receipts are for.
SP007 now has owners. It had no canCause edge at all, meaning the most
frequently raised error in the language was attributed to no syntax at
all — and was missing from the documentation pages for relationships and
relates, where a reader would meet it. Both now name it.
One inconsistency is left standing. TargetNotFoundError sits beside
RelationshipNotFoundError (SP015), and the pair has to be read carefully: in
isNextTo { Pond }, SP015 is the word not being in reach and SP007 is the
target not being found. That contrast is the reason for the chosen name, but
it is a contrast a reader has to notice rather than one the names make obvious.
This will happen again. Any ruling that removes syntax leaves residue in the codes written for it, and 012 shows the residue is easy to miss even in a record that was thorough about the case it was looking at. The check is mechanical: after removing a construct, ask of every error that mentioned it whether anything can still raise it, and whether its name still describes what it does. The three possible answers are retire, rename, or leave alone.
Alternatives considered
Retire SP007 and allocate SP026. Rejected. This is what 012 did to SP006, and the symmetry is tempting, but it inverts 012's own reasoning. Retirement exists so that a code in somebody's old output keeps meaning what it meant. SP007 already does. Retiring it would make every past occurrence point at a retired code for no reason, burn a number, and give the same fact two codes depending on when you hit it.
Leave the name alone and fix only the description. Rejected, and it was the cheap option. The class name is exported, the kind string is printed, and both are read by people trying to understand a failure. A description that contradicts the name it explains is worse than either being wrong alone.
NameNotFoundError / name-not-found. The strongest alternative. "Name"
is the language's own central word — a bare name resolves outward through
enclosing scopes — and it covers all three cases plainly, including a relates
side, which is an endpoint rather than a target. Rejected on the grounds that
NameNotFoundError beside RelationshipNotFoundError reads as though one is a
special case of the other, when they are different failures. "Target" loses a
little accuracy on relates and buys the contrast.
Rename RelationshipNotFoundError too, so the pair reads clearly together.
Deferred. It is a real improvement and it is a larger change than this record
wants to carry, because SP015's name is not wrong — only easy to confuse. If the
confusion costs somebody time, that is the signal.
Evidence
Both codes were first read as retirement candidates, on the strength of the universe's wording. The code contradicted that for one of them, and the contradiction is the reason this record splits the two cases instead of retiring both.
DeclaredParentNotFoundError is thrown from five sites in validator.js —
lines 253, 272, 314, 387 and 405 — all within resolveEndpoint and the path
walker. None of them concern a declared parent. Three of the reachable paths,
compiled against the parser at 0.9.0:
| input | result |
|---|---|
isNextTo { Pond.DoesNotExist } |
[SP007] Declared parent "Pond.DoesNotExist" was not found. |
isNextTo { Nowhere } |
[SP007] Declared parent "Nowhere" was not found. |
relates Well and Missing { } |
[SP007] Declared parent "Missing" was not found. |
DuplicateDeclaredParentError is the opposite finding. It appears twice in the
source — the exported class declaration and its entry in the message table — and
is thrown from nowhere. That is what a genuinely retired error looks like, and it
is the difference between the two halves of this record.
The lockstep property was checked rather than assumed. Before the rename,
.sprig/receipt.json recorded 28 receipts: 25 recognized and 3 unrecognized.
The count is unchanged afterward, which is the evidence that the marker and the
universe moved together.