Skip to contentWolf-Rayet

Decision records

ADR-0114 A Scope is data-wr-layer="scope", a demand is level 4, and the first real page measures 0.023

Accepted2026-09-20Phase 5

#Context

ADR-0004 gives the emission formula, ADR-0005 makes Directed relative and says what a failure is, ADR-0001 rations the top rung. All three are implemented in packages/react/src/scope.ts and all three have only ever been run against fixtures: objects a test wrote, with lightnesses and pixel counts chosen to exercise one clause each. Eighteen checks pass and three mutations of the engine are caught by them.

Nothing had ever measured a page. The engine takes measurements rather than markup, on purpose, so the question it left open was who supplies them and from where. Two things were undecided and both blocked the same thing: what carries a Scope boundary in real markup, and how a caller knows which blocks are demands. Until both were answered the ration was a function with a test suite and no subject.

#Decision

A Scope in markup is data-wr-layer="scope", and it goes where ADR-0073's test says a Scope goes. That record decides what a Scope is: a place, identifiable by where it sits. The docs shell has three. The bar is at the top, the rail is at the left edge, and the route's column is what remains. Each declares level 1 and no other, per ADR-0073 §3, because a Scope distributes an allocation and does not hold one; each is rendered across every change of route, which is the rule ADR-0046 states. Nothing else in the shell is a place and nothing else is a Scope.

The attribute is not a new one. data-wr-layer is declared in packages/core/src/attributes.ts, written by 144 elements across 53 fixture scenes, and read by packages/budget/src/measure.js. A second spelling invented for the site would have been the exact defect that file's opening comment names: a literal in four places is four chances to disagree, and the disagreement is silent.

A demand is data-wr-level="4", derived and never listed. isDemand is declared === DEMAND_LEVEL. The alternative on the table was a set of component names, and the reason it lost is that a list is a claim about markup that markup cannot contradict. A component that stops rendering level 4 stops being a demand on the same commit, which no roster can promise.

apps/docs/tools/audit-ration.mjs is the caller. It finds the regions, composites each ground down through its ancestors until something is opaque, measures OKLab L with the constants packages/budget/src/color.js already uses, reports each block's extent, and hands the engine numbers. Nothing in the tool knows a rule and nothing in the engine touches the DOM. That is the division scope.ts states in its own header and this is the first thing on the other side of it.

#What the first run found

Six routes, both themes, 34 Scope readings, 1,220 blocks. Zero failures. Every route is calm: no element on the docs site declares level 4, so the ration is satisfied by having nothing to ration, which ADR-0001 calls the supervisor's success state rather than an absence of evidence.

Three findings, none of them a failure and all of them about the instrument.

The theme toggle is the loudest thing in the shell. div.ds__theme holds Directed in the header on every route in both themes, at 0.002884 light and 0.004560 dark, ahead of the section links at 0.002013 and 0.003065. A control for changing the theme out-emits the navigation. That is a real reading of a real page and it is the kind of thing the budget exists to say out loud.

Corrected by ADR-0115. It leads and it does not hold Directed. This reading was produced by an engine running ADR-0005's retired dominance clause, which crowns whichever element leads by more than PLATFORM_TOLERANCE. Under ADR-0036's rule, the margin is one rung of the theme's ladder across the leader's own pixels: in dark the toggle leads by 0.001577 against a requirement of 0.010986, so the header has no Directed element. The toggle is still the loudest thing in the shell. It is not a level louder than the navigation, and only the second rule can tell the difference.

A prose route measures 0.000000. Six of the 34 readings are exactly zero. The harness measures grounds, which is what Block.lightness is documented to be, and a page of text on one surface introduces no ground. The engine is not wrong and the tool is not wrong; together they under-measure text, and the quantity that would not be zero is the screenshot integral measure.js computes. Whether the engine should be fed a mean over a block's pixels rather than its ground is an amendment to ADR-0004 and is not made here, because a mean of a signed difference is not a lightness and the substitution would be silent.

Corrected by ADR-0117. It is not an amendment. ADR-0004 sums pixels in the line that defines the formula, and rejects block downsampling explicitly because a block holding pixels on both sides of the substrate lets them cancel and "actively understates". The harness was downsampling to one value per element. The objection about a signed mean was right and is why Block gained a deviation rather than a second lightness. Under the integral all six of these readings measure, and every reading on the site rose.

Eight of 28 resolvable readings have no Directed element. Margins of 0.000105, 0.00019, 0.000816, 0.000828 and 0.000871, all inside PLATFORM_TOLERANCE at 1e-3. Only two of the 28 have a leader below the tolerance, so the instrument is not blind; what it cannot do is separate a leader from a runner-up. A share is a block's extent over the Scope's, so shares shrink as a Scope grows, while the tolerance does not. A fixture Scope is a few blocks in a small box and its shares are large. The route column is 438 blocks in a full viewport and its leader share is 0.000125. ADR-0036 makes the dominance margin come from the ladder rather than from a constant someone picked, and this is the first evidence that the margin's *scale* is owed the same treatment. Named here, not fixed here.

Corrected by ADR-0115. The evidence is right and the conclusion is wrong. ADR-0036 had already made the margin scale with the Scope, in the sentence that states the rule, and the engine was running ADR-0005's retired clause instead. Nothing was owed a treatment; a rule was owed an implementation. Under the correct rule the same 34 readings move eight verdicts and the warning count rises rather than falls, because on this site the ladder is stricter than the constant it replaces in 27 readings of 34.

#Rejected options

data-scope, a new attribute for the site. Shorter, and it reads better in a layout file. It lost to the sentence at the top of attributes.ts: no data-wr-* name is written as a literal outside that file, and a parallel vocabulary would mean the engine and the fixtures agree only by habit. The site now writes the attribute the 53 fixture scenes write.

Declare the main column a View rather than a Scope, and find Scopes inside it. Truer to the fixtures, where main.view holds sections. It lost because the sections inside a docs route are a title and some prose, and calling each one a Scope would put a boundary where no reader learns a place. ADR-0073's test is about places and the route's column is one: the reader learns that the route's body is to the right of the rail. The cost is that a route measures as one region instead of several, and the cost is named rather than hidden.

A hand-written set of demanding components: modal, alert, status-indicator, staleness. It is the set component.config.json actually holds, so it is not a guess, and it would have worked today. It lost because those four are the components whose *legal* levels include 4, which is a different claim from the one the engine needs. An alert at level 2 is not a demand and a list cannot tell. The level a component rendered is the only evidence of what it did.

Take a screenshot and measure the pixels, as measure.js does. It would fix the zero-load routes in the same pass, and it is the method the system already committed to. It lost on scope: it changes what the engine is fed, and the quantity it would feed is not the one Block.lightness is declared to be. That is a decision about ADR-0004's formula and it deserves its own record rather than arriving inside a wiring commit.

#Consequences

The ration has a subject. pnpm --filter @wolf-rayet/docs run check:ration puts the engine in front of a dev server and returns a verdict. It exits non-zero on a failure finding and prints warnings without failing, because ADR-0005 makes an undecided Directed a thing to know: a suite that failed there would teach people to manufacture a winner to quiet the tool.

Two open questions close and two open. What a Scope is in markup, and how a demand is known, are answered above. What remains is whether the formula should read pixels instead of grounds, and whether the dominance margin should scale with the Scope. Both are amendments to accepted records and both now have a measurement behind them instead of a suspicion.

Corrected by ADR-0115. The second of those is not an open question and was not one when this was written. ADR-0036 decided it on 2026-09-02. One open question remains: pixels or grounds, against ADR-0004.

The check is not in the battery. It needs a listening server, which is a different shape from every other check in tools/run-battery.js, and wiring it in without deciding whether the battery may start a server would be the smaller half of a decision. The tool runs and is documented; the harness question is its own unit.

Still true after ADR-0115, ADR-0116 and ADR-0117. The tool now rasterises as well, so the battery question has gained a second half: whether the battery may start a server, and whether it may take screenshots.

Nothing in the ration engine changed. Eight files gained an attribute, one module names the three Scopes, one tool was written. No token, stylesheet or component moved.

#Measured

  • packages/react/lib/scope.js exports, this pass: DEMAND_LEVEL DIRECTED_LEVEL PLATFORM_TOLERANCE SCOPE_DEMAND_CAP VIEW_DEMAND_CAP auditScope auditView *(disk, this pass)*.
  • Six routes, two themes: 34 Scope readings, 1,220 blocks, 0 failures, 8 warnings, all undecided-directed.
  • Load range 0.000000 to 0.023385. Heaviest: /components/alert/examples route column, dark, at 0.023385; then /components/button/examples at 0.020692.
  • Leader share range 0.000125 to 0.004560; 2 of 28 below PLATFORM_TOLERANCE, 8 of 28 with a margin below it.
  • Six readings at exactly 0.000000, all of them prose routes with no ground introduced.
  • div.ds__theme holds Directed in shell-header on every route: 0.002884 light, 0.004560 dark.
  • Blocks per Scope: 0 to 438.
  • tsc -p apps/docs/tsconfig.json --noEmit clean *(this pass)*.