Skip to contentWolf-Rayet

Decision records

ADR-0080 The roster's last Scope, and the two things its depth changed that nothing predicted

Accepted2026-09-06Phase 4, reopened

#Context

popover is the last Scope on ADR-0021's roster and the last row of section E. Three records were written about it without building it: ADR-0076 removed toggletip beside it, ADR-0077 found where it renders and named the engine rule it needed, ADR-0078 wrote that rule, and ADR-0079 removed the second obstacle and closed with *"popover is buildable. What remains on that row is the component."*

This is the component. Two of its properties were decided before the build and held; two things the build found were not predicted by any of the four records, and both are consequences of the same fact — this is the first component in the system at §5's second Scope depth.

#The identity test puts it against region, and D5 is what separates them

§12's eight grounds, run against the eight built Scopes: on the six tier axes popover is region, page-header and data-table exactly — scope, [1], non-interactive, no vouching, no acknowledgement, no persistence. Three of those four rows are already mutually identical there and are separated on D4.

What separates this one is D5, and it is the first Scope in the system that has it. A popover removes itself from the frame. ADR-0059 made disclosure a dimension when tabs became the second component to disclose, and every component that had it until now was an Emitter or an Arbiter.

Its D4 answer is its own as well, and the argument is ADR-0073's read backwards. That record found the ground the identity test cannot carry: a Scope is a place, and shell-left-panel and shell-right-panel are identical on all eight because position is deliberately not a tier axis. Every Scope in this system is named by where it is — the shell surfaces are at the edges, the page header is at the top, a region is where the composer put it. A popover has no place. It borrows its anchor's, and the anchor is a control that may be an icon with no words on it, so the one thing it is obliged to say is its subject *in words*. page-header's title can be read off the route; this cannot be read off anything.

#The first thing the depth changed: core's nesting table is the wrong thing to ask

Every Scope contract in this set derives its accepted layers from AcceptedBy<'scope'>, which reads core's Accepts table: scope: 'scope' | 'arbiter' | 'emitter'.

That table is keyed on layer, and depth is not a layer. §5's nesting bound is carried in ScopeNode's third type parameter, *because a union cannot count* — core's own comment says so. So the table answers "what does a Scope accept" and has no way to answer "what does a Scope at depth 2 accept". ScopeChildren answers the second, and the two answers differ: at depth 1 it admits a ScopeNode<..., 2>, and at depth 2 it does not.

A popover reading the table would have declared that it accepts Scopes while the model refused them — and nothing would have caught it, because accepts is a list a contract writes and children is a type core computes. The two have never been able to disagree because no component was ever deep enough for them to.

So this contract derives its accepted set from ScopeChildren at its own depth, and then from the layer field of the element type. Two layers, not three, and the difference is not a decision this file made.

#The second thing the depth changed: it needs no subtree-permission attribute

ADR-0079 specified an attribute for the question ADR-0053 tabulated and ADR-0075 left declared and unenforced — whether a Scope's subtree may hold the View's demand — and named popover as *"the third component to have the gap."*

It is the third component to have the question, and the answer arrives from somewhere else. live-feed and page-header need an attribute because their subtrees are always rendered: nothing a frame carries distinguishes a demand that is allowed to be there from one that is not, so the permission has to be said out loud. A popover's subtree is not always rendered. Its resting state is absence from the frame, and a demand inside a closed popover is the exact node ADR-0040's oldest salience check already collects — !n.rendered && n.level === DEMAND_LEVEL, no component name anywhere in it.

That is ADR-0079's own test applied one row later. The permission is not a fact the frame has to be told; it is a consequence of a property the frame already carries. The attribute is still owed by the other two and is unaffected.

The enforcement is narrower than the declaration and that is stated rather than glossed. An *open* popover holding a demand is visible and is caught by nothing. It becomes the closed case the moment the reader clicks away, and the closed case is the resting state — so every fixture of this component at rest is a fixture of the rule holding.

#Decision

popover is built: a raised Scope at §5's second depth, rendered inside its anchor's Scope and never portalled, one rung, one variant, one governed part, non-interactive, disclosing. The thirty-seventh component and the ninth Scope.

Its accepted set is derived from ScopeChildren at its own depth rather than from AcceptedBy. Every Scope contract before it may keep the layer table, because every one of them is at depth 1 where the two agree.

Its subtree demand rule is enforced by ADR-0040 and gains no attribute. ADR-0079's specification stands for live-feed and page-header.

Its perimeter takes border-strong where region and live-feed take border-subtle. This tier has three surfaces — page, raised, inset — and no fourth for a nested Scope to climb to, so a popover anchored inside a Region paints the value its parent paints. ADR-0013 decided long ago what tells a raised context from its ground: a full perimeter, never a shadow. Here that stops being one signal among several and becomes the only one. The rung does not move and the mark stays at the Ambient role — what changed is the ground the line is read against, and the APCA floors are run against both.

#Rejected options

Declare a rung above Ambient, because a popover is what the reader is looking at. Tempting, and refused twice over. §3 gives the first refusal: a Scope distributes an allocation and does not hold one, which is why every Scope but modal says [1] — and modal's floor is occlusion's consequence, which a popover does not have. ADR-0040 gives the second, and it is the one with an enforcer: a component whose resting state is present-and-not-rendered may not declare a rung the frame would then be unable to show. What is *in* a popover may be Marked or Directed; that is the content's allocation, not the frame's.

Make the disclosure states two variants, as accordion does. Rejected on ADR-0028's own reading: the variant axis is the rule of a component's own *set*, and accordion's collapsed/expanded qualify because a stack of sections is a set with a rule about how many may be open. A popover is one surface and has no set, so its open state is a state and not a rule — which is live-feed's reading of its own three states, arriving at a component whose second state is absence.

Give it a close control and an onDismiss. Rejected on ADR-0076, which removed toggletip for naming a trigger this system puts outside the component, and on ADR-0050's split, now applied for the fourth time. It is also what keeps interactive: false true, and that declaration is checked in both directions against the component source.

Unmount the children when closed, which is what every popover implementation does. Rejected, and it is the one rejection that would have silently disarmed a check. A subtree that was never mounted produces no node, and Salience — hidden reads nodes — scene 56's finding, arriving at a component whose whole surface disappears rather than one panel of it. A clipped form is worse: it leaves the hidden children's border boxes at their positions for the engine to attribute pixels to.

Position it in the component's own stylesheet. Rejected because layout lives in scene stylesheets in this system (ADR-0073), and a component that placed itself would need an opinion about a screen it cannot see. It is also how the portal creeps back in: position: fixed in a component stylesheet is a portal without the API.

#Consequences

Roster 48, built 37. Section E is complete and eleven rows remain. popover was the last Scope on the roster; every Scope this system declared it would build is built.

The set has its first component at §5's second Scope depth. ADR-0058 recorded the rung unoccupied at eight Scopes and ADR-0077 found which row it had been left open for. ScopeChildren's depth-2 branch has a subject for the first time, and test/popover-violations.tsx is the first file in the repository where it refuses anything.

Two scenes, and 65 is the artifact the row's finding rests on. 64 is the first nested Scope in the repository that is a component: #scope-outer nested 49,560, #scope-popover governed 49,560, and the popover's ground predicted 0.099587 against 0.099587 rendered — ADR-0079's re-basing meeting a component rather than a probe. 65 is a demand inside a closed popover, declared must-fail, and it is not a copy of 41 or 56: those hide a demand inside a panel of a Scope that is fully on the screen, and this one hides it inside a Scope that is itself not rendered. It is the first scene where Salience — hidden names a node whose owning Scope governs nothing.

A defect was found by the machinery and not by a reader. .wr-popover { display: flex } outranks the UA's [hidden] { display: none }, so the attribute was written and did nothing. react:test reported it in those words before any scene ran — the check ADR-0040 left behind after watching a must-fail scene pass, catching the same defect in a component written five years of records later.

The interactive count moved and twelve files said the old one. popover is the twenty-seventh component that does not accept input, and self-description:check failed on twelve $interactiveNote sentences still saying twenty-six. Corrected rather than left, which is what that check exists for.

The identity test is unchanged and gained its first D5 separation among Scopes. No axis was added, no dimension was recovered, and the eight grounds stand as ADR-0074 left them.

#Measured

  • pnpm tokens:verify — VERIFY OK; popover: 12 token(s), 8 non-text APCA checks, worst anchored/1 on surface-raised in field-night |Lc| 20.00 / floor 15 *(darwin-arm64, this pass)*.
  • pnpm react:test — REACT OK; 37 component(s) in the layer map, Popover=scope; popover: the hidden attribute actually removes .wr-popover — .wr-popover[hidden] { display: none } *(this pass)*.
  • The same check before the rule existed: FAIL popover: the hidden attribute actually removes .wr-popover — .wr-popover sets display on a class selector, which outranks the UA's [hidden] { display: none } — the attribute is written and does nothing *(this pass)*.
  • POPOVER_ACCEPTS with scope added: error TS2353: 'scope' does not exist in type 'Record<"arbiter" | "emitter", true>'; restored, clean *(this pass)*.
  • Scene 64 receipt: #scope-outer nested 49,560; #scope-popover governed 49,560; #scope-popover predicted 0.099587, rendered 0.099587, delta 0, suspicious false *(darwin-arm64, this pass)*.
  • Scene 65 receipt: #scope-popover governed 0, nested 0, load 0; hidden.demands = ["#pop-alarm"], inScope "#scope-popover", rect 0×0, pass false *(darwin-arm64, this pass)*.
  • pnpm budget — 62 comparisons, every one identical to six decimals, 0 first records after the fill *(darwin-arm64, this pass)*.
  • pnpm themed — 194 comparisons; 64's siblings field-day 0.08105 / 0.14320, interior-dark 0.10289 / 0.15292, interior-light 0.07979 / 0.15134 *(darwin-arm64, this pass)*.
  • pnpm cascade — 3,840 scene-state pairs across 251 scenes, zero findings *(this pass)*.
  • pnpm roster:check — ROSTER OK, 48 rows, 37 built, joined both ways *(this pass)*.
  • pnpm self-description:check — SELF-DESCRIPTION OK, 215 identity positions across 37 components, 5 counted claims matching *(this pass)*.
  • linux-x64 is owed for scenes 64 and 65 and their six themed siblings. Standing item 12 opens with this commit.