Skip to contentWolf-Rayet

Decision records

ADR-0024 A pending state carries no motion; the block form is a Pending message

Accepted2026-09-01Phase 4, reopened by ADR-0021

#Context

ADR-0021 put loading and inline-loading on the roster at levels 1 to 3, with the reason stated in one line: *a pending state reports and does not demand.* That line was written about intensity. It turns out to decide something else, and the something else is larger.

§2 grants sustained motion at level 4 and at no other level. A spinner is sustained motion — continuous, unprompted, running for as long as the wait does. So a component capped at level 3 cannot spin, and the cap was already set, by a record that was reasoning about how loud a wait may be rather than about whether it may move.

This is not a gap discovered late. It is the ladder doing exactly what §2 says it does: motion is one of three rationed channels, the rung is the ration, and a component does not get to spend a channel its level does not grant it because the convention elsewhere is to spend it. What is new is that the roster now contains the two components where that ration bites hardest, because a rotating arc is what every other system in the industry puts here.

Something has to be decided, because the two components cannot be built without deciding it. A spinner that is not permitted is not a smaller spinner. The question is what a wait looks like in a system whose motion channel is closed to it — and, since ADR-0021 committed to building thirty-eight more components against a mechanism that is already green, whether the mechanism bends the first time a convention pushes on it.

There is a second thing worth naming before the decision, because it is what makes the decision easy rather than merely forced. A spinner communicates one bit: *something is happening.* The reader who is looking at it pressed the button; they already have that bit. What they do not have is how long, what happens when it ends, and whether they can walk away — and a rotation has nowhere to put any of those. §6 has had the schema for this message since it was written: Pending — what's running, what happens next, whether they can leave. No component had claimed it.

#Decision

Neither pending component declares motion beyond the ambient grant, and the block form renders §6's Pending pattern in full.

loading and inline-loading are capped at level 3 by ADR-0021 and therefore have no representable sustained motion: MotionDeclaration<DemandLevel> has no level in either component's Level union to be declared at, so the absence is a compile-time property rather than a stylesheet that happens to contain no @keyframes. Each declares one transition at the ambient level — enter and exit, plus the mark change when an operation resolves — which is the grant every level carries. packages/react/test/discipline.test.js checks the absence as an absence: no @keyframes, no animation-name, no selector naming a level the component cannot hold.

loading claims the pending message pattern and renders all three of its roles as required props. whether they can leave is a required string, so a product cannot ship a block wait without saying whether the reader has to stay for it. inline-loading claims no pattern: it is one line beside one control and has room for one string, and a component claiming a pattern it cannot render would give the linter a schema two thirds unfillable. That division is now what separates the two components, in place of a size preference.

#Rejected options

Raise the two components to level 4 so a spinner becomes legal. The direct fix, and it needs no amendment to anything: level 4 grants sustained motion, so declare the components at level 4 and the problem disappears. It lost on the arithmetic of the ration. Level 4 is capped at one per Scope and one per View (ADR-0010), and a pending state is the single most common thing on a working screen — every fetch, every save, every table refresh. A system whose demand cap is spent by whichever request happened to be in flight has no demand cap; the alert, which carries an actual imperative with an actual consequence, would find the rung occupied by a spinner every time it needed it. It also contradicts ADR-0021 in terms, three weeks after that record set the ceiling and said why.

Open a sustained-motion exception below level 4 for pending states. Textually the smallest change in the set: one clause in §2, and everything else stands. It had the best argument of the rejected options, too — a spinner is genuinely a different kind of motion from a demand's pulse, in that it reports rather than escalates, and a ladder that cannot express that distinction is arguably the coarser instrument. It lost on where the exception would have to live. The level-4 sustained grant is the only hard motion boundary the ladder has, and it is the boundary the generated pairing matrix is keyed on — 194 cells over levels, motion classes and easings, and not one of them over a component name. An exception for "pending states" is an exception keyed on what a component *is*, which is exactly the thing ADR-0021's own consequences forbid growing: "the enforcement surface does not grow with the roster … never on component names." The first component-shaped hole in the matrix would be the last time the matrix meant anything.

Declare state-change at the level-2 grant, so the resolve moment animates at its own rung. Not a spinner, and defensible: state-change is granted from level 2, the resolution of an operation is precisely a state change, and declaring it would make inline-loading the first component to exercise the grant table's class gating rather than only its durations. It lost twice. It buys nothing the ambient grant does not already cover — the mark's colour and geometry already transition, at a cap the motion tier collapses correctly under reduced motion — and it would give the component motion that exists at two of its three levels, so the same component would behave differently at Ambient than at Marked for no reason a reader could infer. It would also require a MARKED_LEVEL constant in @wolf-rayet/core, because a component may not write a level number, and adding a name to the ladder's vocabulary to buy a transition already available is the wrong trade at both ends.

Drop both components from the roster under the clause ADR-0021 used for ai-label: a Carbon component whose premise this system has no position on is dropped by name with its reason. It is available, it is cheap, and it would have been honest if true. It lost because the premise is one this system has a strong position on. A pending state is a report; §2 ranks reports; §6 has had a schema for this exact message since it was written. Dropping the two most common components in any product to avoid discovering what a closed channel implies would have been the ration dictating the roster, which is the reverse of ADR-0021's whole direction.

Use a determinate progress bar instead. The meter is built, its encoding is a length, and a length says more about a wait than a rotation ever did. It lost because it answers a different question. A meter reports a fraction, and the case these two components exist for is the case where there is no fraction — a request in flight, an index rebuilding, a job queued behind others. Rendering an invented fraction to get an encoding is the laundering ADR-0009 refuses in a different channel: a number the system made up, drawn as though the product supplied it. Where a real fraction exists, the meter is already the right component and this record changes nothing about that.

#Consequences

Two components ship with no @keyframes and no animation-name, and the absence is checked. packages/react/test/discipline.test.js already distinguished "we did not look" from "there is none"; these are the first two components where the second claim is the point rather than a side effect. A future edit that adds an animation to either fails the discipline test before it reaches the lint.

The engine's two-render exactness check acquires a meaning it did not have. Every fixture is rendered twice and the two loads must agree exactly. A spinning component could not pass that without the harness freezing its animation, because a rotating arc paints different pixels on different frames — so the exactness reported on scenes 23 and 24 is a property of the components rather than of the harness. That was true of the seven before them by accident; it is true of these two by construction.

§6 acquires its second pattern in a shipped component. The alert claimed Error one phase ago; loading claims Pending. Three of the five — Empty, Blocked, Confirmation — remain unclaimed, and the components that will carry them are on the roster. The mechanism that enforces this is core's PatternPartOf, unchanged: a role dropped from the map fails to compile, and packages/react/test/loading-violations.tsx asserts the failure.

The product cannot ship a wait that does not say whether to wait. departure is a required prop. That is the largest user-facing change this record makes, and it is the one worth defending: the string a spinner never carried is now the one a developer cannot omit.

The enforcement surface does not grow. No new check kind, no new rule, no cell added to the pairing matrix — regenerated this unit at 194 cells across 10 dimensions, unchanged, exactly as ADR-0021 predicted the roster would leave it.

Every later component meets the same wall, and this record is the answer. Thirty-five components remain. Any of them wanting continuous motion below level 4 faces the identical ration, and the general rule is stated here rather than rediscovered per component: the channel a level does not grant is not a channel the component may spend, and the design work is finding what the open channels can say instead.

What is harder. A reader trained on twenty years of spinners will look for one and not find it, and for the first second that reads as a page that has not started. The mitigation is in the copy rather than in the light: the Pending message's first role is what is running, present tense, and the specimens set it at the body size at the top of the block. Whether that mitigation is sufficient is a question for the proof product rather than for a record, and it is the thing to watch when this component reaches a real screen.