sprig and prose docs classic docs
Reference› ADRs› ADR023 ErrorNameCorrections

ADR023 ErrorNameCorrections

Title: Error name corrections

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.