ADR-0117 A block is pixels, the Scope's ground is out of the contest, and both were already accepted
#Context
ADR-0114 left one question open: whether the engine should be fed a mean over a block's pixels rather than its ground. It framed this as an amendment to ADR-0004 and declined to make it, on the grounds that a mean of a signed difference is not a lightness and the substitution would be silent.
It is not an amendment. ADR-0004's formula sums |L_block − L_substrate| · n_block, where n_block is a pixel count and a block is a region of pixels. It is a per-pixel integral in the line that defines it, and the record says so again where it rejects the optimisation that would avoid one:
Block downsampling before summing. Under the old formula it was a free optimization, identical to six decimals at block sizes 1, 4, and 8. Rejected, and the grounds strengthened under absolute deviation: a block containing pixels both lighter and darker than the substrate lets them cancel inside the block. Downsampling no longer buys nothing, it actively understates.
The harness was downsampling to the largest block size there is: one value per element, its ground. An element holding dark text on a light card is two populations, and one flat number cannot stand for both. ADR-0004 predicted the result and named the direction of the error.
This is the third clause of an accepted record that the new engine dropped, after ADR-0036's dominance margin (ADR-0115) and, found while writing this one, ADR-0005's ground exclusion:
The Scope's ground is excluded from the contest. Attribution credits an element with every governed pixel no declared child covers, so a full-Scope surface's share measures area rather than intensity. The ground is any member that encloses every other member.
The new engine had no such rule. It did not matter while blocks were grounds, because a Scope's own surface introduces no ground against itself and never appeared as a block. It matters the moment pixels are counted, because then the Scope's own prose is the largest thing in it.
#Decision
A block reports the deviation measured across its pixels, not the deviation of its ground. Block gains an optional excess: the mean |L − L_substrate| over the pixels the block owns. Omitted, it is |lightness − substrateLightness|, which is exact for a flat block and is what every fixture supplies. A caller measuring a rendered page supplies the integral instead. No fixture changes meaning and no existing check moves.
excess rather than a second lightness, because ADR-0114's objection was correct: a mean of a signed difference is not a lightness, and encoding one as a lightness would be the silent substitution that record refused. What the formula multiplies by area is a deviation, so a deviation is what the caller passes.
The Scope's ground is excluded from the Directed contest and not from the load. Block gains isGround, declared rather than inferred, for the same reason isDemand is declared: enclosure is geometry and this engine takes measurements. Both exclusions are ADR-0005's and both say the same thing in different words. A demand took attention without competing; a ground is the surface the competition happens on.
The harness rasterises. It screenshots the full page at one device pixel per CSS pixel, refuses to proceed if the image and the document disagree on their dimensions, and stamps each block's rect into an owner map in document order, so a descendant takes back what it covers. That is ADR-0005's attribution rule implemented rather than paraphrased. Whatever no block covers is credited to the Scope's ground.
Because every governed pixel has exactly one owner, the Scope's load is the mean absolute deviation over all of them, which is ADR-0004's definition with nothing in between.
#What moved, on the same 34 readings
Six readings measured exactly 0.000000 and now none do. Every one was a route column of prose: /, /components and /thesis in both themes, between 0.018535 and 0.031346.
Load rose in all 28 readings that were not zero, by between 1.58× and 25.49×. Summed across all 34, 0.161962 becomes 0.898832. The ground-based reading understated this site's emission 5.5-fold, and understated it everywhere, which is the direction ADR-0004 named.
Three Directed verdicts moved, and two of them are findings rather than bookkeeping.
On /components/button/examples in dark, button.wr-button#34 held Directed with a margin of 0.001664 against a requirement of 0.000799. Measuring only grounds had flattered it by hiding what the code samples on the same page contain: once their ink counts, div.ex__code#49 rises from 0.001192 to 0.002213 and the margin falls to 0.000713. A code sample emits about as much as a button, and the old reading could not see it.
On /components/alert/examples in both themes, the rail's current link left the contest entirely. It is not a measurement artefact: the rail's window runs y 57 to 873, and on that route the link carrying aria-current="page" sits at y 1783 with scrollTop at 0, because the rail never scrolls its current item into view. The link is not painted, so the screenshot does not contain it, so it owns no pixels. The ground-based harness had been charging the page for light no reader could see. The rail is a defect and is filed as its own unit; the measurement is correct and is the reason the defect was found.
Warnings rise from 16 to 21 and failures stay at zero. A fuller measurement makes near-ties more common, because the thing that had been hidden is the thing most elements have most of.
#Rejected options
Report a synthesized lightness, substrate + meanAbsDeviation, and leave Block alone. Merit: the arithmetic is identical, since the formula takes an absolute value and discards the sign anyway, and it needs no change to the engine's surface at all. It lost on honesty, which is ADR-0114's own objection turned on the cheaper option: the field would be named lightness and hold something no pixel is. Every reader of a receipt would have to know the convention, and the first one who did not would compare it against a token.
Mask the docs site's prose as content, per ADR-0009, before measuring. Merit: a documentation site's body text really is the payload the product carries, and ADR-0009 is explicit that the budget governs chrome and not content, so counting 8,000 words of prose against the route column's ration is arguably measuring the wrong thing. It lost because ADR-0009 also settles *how* content is exempted: through slot contracts, with no hand-maintained exclusion list anywhere in the system. The docs markup declares no slots, so there is nothing to derive a mask from, and inventing one for this site would be precisely the drift that record refuses. What is owed is slot declarations in the docs shell, and it is named in Consequences rather than smuggled in as a list.
Downsample the screenshot to keep the run cheap. Merit: the full set decodes about 30 million pixels and an 8× reduction would make it nearly free. It lost to ADR-0004's own rejected option, quoted above, for the reason given there: under absolute deviation a downsampled block lets its own pixels cancel. It is the defect this record fixes, reintroduced one level down. The full run takes 10.5 seconds.
Infer the ground geometrically inside the engine rather than declaring it. Merit: ADR-0005 defines the ground geometrically, as the member enclosing every other member, so a caller passing isGround is a caller who could pass it wrongly. It lost because the engine holds no geometry. It has areas, not rectangles, and giving it rectangles so it could rediscover a fact the caller already knows would put layout inside a function whose first sentence is that it takes measurements and not markup.
#Consequences
The engine now implements every clause of ADR-0004 and ADR-0005 that applies to it, and the three that were missing are the three that were found by running it against something real. Fixtures agreed with the retired rules because fixtures were built to agree with them.
The docs shell owes slot declarations. Until it has them, the route column's load counts the site's prose as chrome. That is the correct behaviour under ADR-0009 given a markup that declares nothing, and it is the wrong number to gate anything on. No check gates on it today.
Paid by ADR-0118, in the next unit. Every route reading fell, between 0.6% and 63.8%, in proportion to how much prose the page holds.
The tool depends on packages/budget/src/png.js, which is in one of the five packages awaiting deletion. It is a PNG decoder rather than a rule, so it was imported instead of copied. The deletion unit has to move it or replace it, and this is the second thing that unit now has to answer.
Attribution is by rectangle. A block's border radius, its overflow clipping and its transforms are not modelled, so a rounded card is credited with its corners. The retired engine derived masks from slot contracts instead. This is a simplification, it is stated rather than discovered later, and it is bounded: corners are a few hundred pixels against Scopes of two to eight million.
A block that is not painted is not measured. An element scrolled out of its own overflow container owns no pixels and drops out of the contest, which is ADR-0040's reading of a collapsed panel arriving by a different route. It is what found the rail defect.
No token, stylesheet or component moved. One engine file, one test file, one harness.
#Measured
- 34 readings, 6 routes, 2 themes. Readings at exactly 0.000000: 6 → 0. The six were
/,/componentsand/thesisroute columns in both themes, now 0.018535 to 0.031346 *(this pass)*. - Load rose in 28 of 28 nonzero readings, ratio range 1.58× to 25.49×. Sum of all loads 0.161962 → 0.898832 *(this pass)*.
- Directed verdicts moved: 3, all losing Directed.
/components/button/examplesdark route,/components/alert/examplesshell-rail in both themes. /components/button/examplesdark:div.ex__code#490.001192 → 0.002213; the leader's margin 0.001664 → 0.000713 against a requirement of 0.000799./components/alert/examples: rail window y 57 to 873, current link at y 1783 to 1817,scrollTop0. Measured through a browser at 1280x800 *(this pass)*.- Warnings 16 → 21, failures 0 → 0.
pnpm --filter @wolf-rayet/react run test: 28 ration checks passed, up from 22.- Full run wall clock: 10.5s for 12 route/theme pairs, about 30 million pixels decoded *(darwin-arm64, this pass)*.
tsc --noEmitclean on react, react tests and docs *(this pass)*.