sprig and prose docs classic docs
Reference› ADRs› ADR026 ProjectionsStateTheirRules

ADR026 ProjectionsStateTheirRules

Title: Projections state their rules

  • Status: Proposed
  • Proposed: D2026-09-20
  • Related: ADR011 ExpectsBlock, ADR025 RelationshipSideBlocks
  • Affects: Ui

Context

A projection reads a manifest and decides what to show. Every one of those decisions is a rule, and right now a reader cannot see a single one of them.

The field guide is the projection that exists, so it is the evidence. It draws a sequence only when a run is at least four steps long, or three if the run closes into a cycle, and it shows at most fourteen of them. It ignores one-word relationships entirely, because a symmetric word gives it no direction to read in. It drops any component denser than a tree with one loop. It decides which way a run points by canonicalising on the manifest's forward field — which holds the alphabetically first of the two words — and then preferring whichever reading agrees with the order the author happened to declare the concepts in.

It also forms four judgments about what is unfinished, and offers them under Loose ends: concepts with no description, descriptions it considers placeholders, concepts connected to nothing, and names that look like the same thing written twice. The second of those is a regular expression:

export const PLACEHOLDER = /^(todo|tbd|tba|fixme|xxx|n\/a|none|\?+|-+|\.+)$/i;

So a universe whose author deliberately wrote none as a complete description is told it left a placeholder, on the strength of a word list nobody outside the source can read.

None of this is wrong. Every one of those calls is defensible, several are the best available answer, and sequences.js is candid in its own header that nothing in the manifest says which relationships order the things they touch, so it reads the shape of the edges instead. The problem is not the rules. It is that they are invisible.

What invisibility costs

A reader has no answer when nothing appears. An author who models a three-episode arc and sees no sequence has no way to learn that four is the threshold. The projection is silent in exactly the situation where a person needs it to speak, and the only recourse is reading the source of the tool.

The rules cannot be discussed. A threshold nobody can see cannot be argued about, so it never improves. SEQ_MIN_RUN = 4 has a comment explaining itself to whoever opens the file, and no way of explaining itself to whoever it affects.

Conventions cannot converge. 025 leaves orientation to the author's own vocabulary and says a consumer must publish which words it honours. If publishing means "whoever ships first defines the words," that is a land grab rather than a design, and the earlier advice to stake a convention quickly was wrong for that reason. What actually makes vocabularies converge is a projection saying plainly what it looks for, so an author can opt in on purpose rather than by coincidence.

Decision

A projection states the rules it applies, in the same place a reader meets their effects.

Not a manual and not a changelog. The claim is narrower: wherever a projection has made a decision a reader could otherwise mistake for the universe's own shape, it says which decision it made.

Three obligations:

Thresholds are stated where they bite. A sequence section says what counts as a run. A loose end says what it looked for. A cap says it is a cap and how many were left out. A reader who disagrees can then disagree with something specific.

Absence is explained when it is a choice. If a projection had the material to show something and declined, silence is a claim about the universe that the universe did not make. Where the material exists and a rule excluded it, the rule is what to say. Where the material was simply absent, nothing is owed — 009 already ruled that a concept with no receipts gets no empty state, and this does not reopen it.

Honoured vocabulary is published. Any word a projection treats as meaningful — an orientation, a maturity, whatever 025 leads to — is written down where an author can find it before they choose their own words. This is documentation rather than grammar, so it stays versionable and replaceable, which is the whole reason 025 keeps the ontology out of the language.

The rules stay the projection's own. This record does not move any of them into prose or into the manifest. A threshold is a rendering decision and belongs to whatever is rendering. What changes is that it stops being a secret.

Consequences

It applies to every projection, not just this one. The field guide is the first case and the one with the debt, but a diagram renderer or a document projector inherits the same obligation on its first day. That breadth is why this is a record rather than a task.

Some of the current rules will not survive being written down. Deciding direction by "whichever way agrees with the order the author typed things" is defensible as a fallback and embarrassing as a printed sentence. That is the mechanism working: a rule that cannot be stated plainly is usually a rule worth replacing, and 025 exists because this one could not be.

It is a cost on every future rule. Any new inference has to arrive with an explanation, which makes cheap heuristics slightly more expensive to add. That is intended. The alternative is what already happened — a dozen judgments accumulating in a model layer with no reader-facing trace.

It does not make the field guide chatty. The obligation is triggered by a decision that shapes what a reader sees, not by every internal step. Hue assignment by golden-angle steps changes nothing about what is present and owes nobody an explanation.

What to watch. The honest failure mode is ritual: a line of boilerplate under every section that nobody reads, satisfying the letter of this while explaining nothing. The test is whether a confused author gets unstuck, and if the stated rules are not doing that, the wording is wrong rather than the record.

Alternatives considered

Move the thresholds into prose, so a universe declares its own. Rejected. A minimum run length is not a fact about the world, it is a judgment about readability at a particular size on a particular screen, and it fails the test that sorted expects from a rendering layer: delete the UI and it means nothing. It would also make two universes render differently for reasons unrelated to what they describe.

Put them in the manifest as emitted metadata. Rejected for the same reason one layer down. The manifest is what the universe says; a renderer's cutoffs are not the universe speaking. It would also oblige the compiler to know about consumers, which is backwards.

Document them in the docs site and link to it. Rejected as insufficient on its own, though worth doing alongside. A reader who cannot see why their sequence is missing does not know they have a question, so they never go looking for the page that answers it. Explanation has to arrive where the effect is, and general documentation is where it goes afterward.

Make every rule configurable instead of explained. Rejected. It answers a different complaint, costs far more, and a configurable threshold is still invisible until someone learns it exists. Stating a rule is the prerequisite for usefully configuring it, and may well remove the want.

Do nothing, and rely on the rules being good. Rejected, and it is the status quo. The rules mostly are good. But a field guide that quietly decides what is worth showing and declines to say so is behaving like the rulebook the philosophy says it is not — and 025 is direct evidence of the cost, since the direction guess survived for as long as it did partly because nobody reading a timeline could see it was a guess.

Evidence

The thresholds, as they stand in sprig-ui-beta:

rule value where
shortest run drawn 4 steps sequences.js SEQ_MIN_RUN
shortest cycle drawn 3 steps sequences.js SEQ_MIN_CYCLE
sequences shown 14 sequences.js SEQ_MAX
one-word relationships excluded sequences.js, if (!def.inverse) continue
component density no denser than a tree with one loop sequences.js
chips per loose end 80 LooseEnds.svelte MAX_CHIPS
neighbourhood caps 12 start, 3 minimum, 30 slots neighbourhood.js

None of these numbers appears anywhere a reader can see it.

011 already named the shape of this problem without naming the problem, when it observed that expects "turns one of the four readings from a universal guess into an authored one, per kind." Four universal guesses were being made about what is unfinished; that record made one of them authorable. The other three are still guesses, and all four are still unstated.