sprig and prose docs classic docs
Reference› ADRs› ADR019 DeclaredDimensions

ADR019 DeclaredDimensions

Title: Declared dimensions

  • Status: Accepted
  • Decided: D2026-08
  • Recorded: D2026-09-07
  • Introduces: SP010 DimensionNotFoundError, SP021 DimensionAlreadyDeclaredInChainError
  • Affects: Concept, Dimension, Parser

Context

The word in front of a concept names its dimension. food Bread says Bread is a food.

The compiler used to accept any word there. If nothing named food had been declared, it minted a dimension id from the word and the scope it was written in and carried on. Nothing failed, so the mistake was invisible — and the consequence was worse than an error would have been. Two members of what the author thought was one kind, written in different folders, got two different minted ids. They looked like they belonged to different kinds, because as far as the manifest was concerned they did.

That is the failure mode a descriptive language can least afford. The universe compiled, the field guide rendered, and the graph was quietly wrong in a way that only showed up as a kind page with members missing from it.

A second problem sat next to the first. Kind lookup walks outward: a concept's dimension is found by checking the enclosing folder, then its parent, and so on to the universe. But a kind word cannot be qualified — there is no way to write Outer.food Bread to say which food you meant. So if a dimension name were declared again further down its own scope chain, every member below that point would silently change kind, and the author would have no syntax available to say otherwise.

Decision

A kind word must name a dimension that was actually declared, in the concept's scope or an enclosing one. Nothing is minted. A word that names no dimension is SP010, and the message says how to fix it in both directions: declare the dimension, or use concept if the idea has no dimension yet.

concept is the explicit escape hatch, not a special case — resolveConceptDimension treats the literal word concept as "no dimension" and returns null.

A dimension may not be declared again inside its own scope chain. The inner declaration is SP021, reported at the declaration rather than at any use, because the declaration is the thing that is wrong and the uses all look fine.

Consequences

Authoring. A typo in a kind word is now an error instead of a new kind. The cost is that a universe sketched with kind words before their dimensions exist does not compile until the dimensions are declared, or until those concepts are written as concept.

What this fixed that was not visible. Members of one kind sitting in different folders now share one dimension id. The bug this replaced did not produce a diagnostic; it produced a graph that disagreed with the author. That is the argument for refusing to mint anything: a descriptive language's whole value is that the description is true, and inventing a declaration to keep a compile alive trades that away for convenience.

Why SP021 fires at the declaration. Because there is nothing to report at the use. Every food Bread below a shadowing redeclaration is well-formed and resolves; it just resolves to a different kind than the one the author has in mind. With no way to qualify a kind word, there is also no fix available at the use site. The only actionable location is the inner declaration.

Cost. Sibling branches may still each declare cleanliness, because they are not in one another's chain and neither is fallback for the other. That is deliberate, and it means SP021 is a narrower rule than "dimension names are unique," which some authors will expect it to be.

What this does not do. It does not change where a concept sits. Where a thing lives and what kind it is remain two independent trees: food Bread inside a place is still a universe-level food.

Alternatives considered

Keep minting, and warn. Report the implicit dimension as a warning and carry on. Rejected. A warning that produces a wrong graph is worse than an error that produces none, because the graph is the artifact everything downstream trusts. The manifest would still contain two ids for one kind.

Mint one dimension per word, universe-wide. If food is undeclared, invent a single food at universe scope rather than one per writing scope. This fixes the split-kind symptom without requiring declarations. Rejected: it makes every typo a real kind, and it means the set of dimensions in a universe is whatever words happen to appear, which is not a vocabulary.

Make concept mean "infer the dimension." Rejected. concept earns its keep by meaning exactly one thing — this idea has no kind yet — and a reader needs that to be unambiguous.

Report SP021 at the use rather than the declaration. Rejected for the reason above: there is no qualified form, so the use site has no available fix. This is the same reasoning 008 later applied to relationship edge names, which is why SP025 also reports the inner declaration.

Universe-wide unique dimension names. Rejected. Two unrelated branches each wanting status is not shadowing, and forbidding it would make large universes fight over common words.

Evidence

resolveConceptDimension in packages/prose-parser/src/validator.js walks parents from the innermost scope outward and returns null only for the literal word concept; if the walk finds nothing, SP010 is raised rather than an id being constructed. The absence of a minting branch is the change.

validateDimensionChain in the same file compares each scope's dimensions against its ancestors' and raises SP021 with the inner declaration's span and the name of the enclosing scope that already holds the word.

Worth noting for a future reader, because it is not obvious and it is not recorded anywhere yet: prose now has three name-resolution axes and they do not behave the same way. A kind word walks outward (this record). A relationship edge name walks outward and then into the target's scopes (008). A concept name does not walk outward at all (002) — that walk was considered and refused. Two of the three walk, one does not, and each of SP021 and SP025 exists to contain a hazard the walking creates. That asymmetry is deliberate in each case but has never been argued as a whole.