Skip to contentWolf-Rayet

Decision records

ADR-0077 The roster's last Scope comes due on §5's second depth, and the engine has no word for it

Accepted2026-09-06Phase 4, reopened

#Context

popover is the last Scope on ADR-0021's roster and the last row of section E. ADR-0026 left an instruction for it — *"when each is built it answers the hosting question rather than re-litigating this one"* — and ADR-0076 applied that instruction to remove toggletip beside it, leaving one row.

The hosting question is easy and it is not the one that stops this component. A popover hosts arbitrary content anchored to something else on the screen, so it is a Scope. What stops it is the question underneath: where does it render, and who pays for its pixels?

Three answers are available and this record refuses two of them from the repository's own machinery.

#The first answer, refused by the measurement: overlay

ADR-0021's section E says popover declares the overlay stacking context and grounds on the scrim composite, and that sentence has stood unexamined since the roster reopened.

measure.js refuses it. That file computes two grounds and compares them:

floorPrediction is what the declared context predicts the Scope sits on: the theme substrate for base and raised, the scrim composite for overlay ... floorRendered is what the Scope demonstrably sits on ... The check compares the two. A mis-declaration fails because the pixels will not match the prediction.

A popover draws no scrim. Declaring overlay predicts a ground of substrate-composited-with-scrim, and the pixels behind an anchored popover are whatever it is anchored over. The two do not match, and the check exists to fail exactly that.

And the escape is worse than the refusal. A popover *could* draw a scrim and satisfy the prediction — at which point it dims the screen, and ADR-0014 says what that means: *"occlusion is a statement about interactivity."* A surface that dims the screen and blocks what is under it is modal, and the only thing left separating them would be how much of the view the sheet covers. ADR-0057 removed three roster rows for exactly that: *"this system's tier has no quantity in it ... two components separated only by *how much* would be told apart by their names."*

So a popover is not an overlay, and section E's sentence about it was wrong in a way nothing had reason to catch, because no overlay but modal had ever been measured.

#The second answer, refused by ADR-0026: raised, portalled

The ordinary implementation. Render the popover to the document body, position it against its anchor, give it raised.

ADR-0026 refused this for tooltip and its reason does not depend on the layer:

A portalled tooltip would emit into a region whose budget it does not belong to while the region it visually occupies paid nothing for it — the substrate laundering ADR-0009 refuses at the content boundary, arriving through the stacking context instead.

A portalled popover is that with children in it. It is the same defect at a larger size, and the size is the only difference.

#The third answer is right, and the type already allows it: §5's second Scope depth

A popover renders inside its anchor's Scope, which is ADR-0026's answer for the tooltip arriving at a component that hosts. It is a Scope inside a Scope, and §5 permits exactly one such nesting: *"Scopes do not nest more than two deep."*

Core has permitted it since the model was written. ScopeChildren at depth 1 admits ScopeNode<StackingContext, Dens, 2>, and ADR-0058 recorded that the rung was unoccupied: *"All seven Scopes are ScopeNode<..., 1>, and ScopeChildren admits a ScopeNode<..., 2> that no component is."* Eight Scopes later it is still unoccupied, and this is the component it was left open for.

The engine has no word for it, and that is the conflict. engine.js attributes a pixel to a Scope by that Scope's own rect, and removes a pixel from a Scope's governed set in exactly one case — when another Scope grounding on scrim-composite covers it (ADR-0038). It has no notion of containment at all. Two Scopes that both contain a pixel are both charged for it, so a popover nested in a region would be double-counted: once by the popover, once by the region it sits inside.

That has never mattered, because nothing has ever nested.

#Decision

popover is a raised Scope at §5's second depth, rendered inside its anchor's Scope and never portalled. It is not built in this pass, because the engine rule it needs is an engine rule and not a component's.

The rule it needs is ADR-0038's, generalised from covering to containing. That record says a pixel covered by an occluding Scope *"belongs to that Scope's budget and to no other"*, and the same sentence answers this: a pixel inside a nested Scope belongs to the innermost Scope that contains it. Both are the same principle — one pixel, one Scope, no double charge — and the second is the case the first was written next to without needing.

Naming it and not writing it is the decision, not a deferral of one. The engine is the artifact every other claim in this repository is measured against (ADR-0020), and a change to how it attributes pixels moves every scene's numbers or none of them — which is a thing to find out deliberately, in a pass whose subject is that, with both baselines re-recorded on purpose. Landing it inside a component build would put the engine change and the component's own first measurement in one commit, where neither could be read against the other.

#Rejected options

Build it as an overlay and let the ground check tell us. Cheap, and the check would have caught it — which is the argument for it and against it. The refusal is derivable from measure.js without rendering anything, and building a component in order to be refused by a file that could be read is the ceremony this repository's own discipline refuses.

Build it portalled and accept the laundering as a known limit. Rejected on ADR-0026, which refused it for a component carrying one string. Accepting it for a component carrying a subtree would be relaxing a rule at the point it matters most.

Change the engine in this pass and build the component in the same commit. The efficient option and the reason this record exists instead. The engine change is not small in *consequence* even if it is small in lines: every scene with a Scope inside a Scope changes, and today that is none, so the change is invisible until the component lands — which is precisely why they must land separately, with the engine's own pass proving the number moved for nothing before a component asks it to move for something.

Drop popover and let a composer put content in a Region positioned by a scene stylesheet. Genuinely available, and it is what every scene in this repository already does for layout. Rejected because a popover is not a placed region: it is anchored to a control, it appears and disappears with an interaction, and the thing that makes it a component here is that the budget must follow it into someone else's Scope. A Region placed absolutely would be the portalled case wearing a different name.

#Consequences

**Roster 48, built 36, unchanged. popover stays *to build* with a named conflict**, which is the state ADR-0061 gave tree-view and ADR-0068 resolved four records later. Twelve rows remain.

Section E's sentence is corrected in ADR-0021: popover is not an overlay, and modal is the only Scope that grounds on the scrim composite.

§5's second Scope depth has a first occupant waiting for it, and ADR-0058's owed note is answered in one direction: the type was never the obstacle, the engine is.

Two records are joined that were written apart. ADR-0038 is about covering and this is about containing, and they are one rule about who owns a pixel. Whichever pass writes the engine change should state them as one.

Nothing built, no token, scene or baseline moved. A record and a corrected sentence.

#Measured

  • measure.js, this pass: floorPrediction is the scrim composite for overlay and the theme substrate for base and raised; floorRendered is the backdrop plus any full-bleed layer; *"the check compares the two"* — *disk, this pass*.
  • engine.js, this pass: a pixel leaves a Scope's governed set only when another Scope whose ground is scrim-composite covers it. No containment test exists — grep for nesting in that file returns nothing — *disk, this pass*.
  • model.ts, this pass: ScopeChildren at depth 1 admits ScopeNode<StackingContext, Dens, 2>; all eight built Scopes are depth 1 — *disk, this pass*.
  • Roster 48, built 36, section E 1 row remaining. pnpm battery — the verdict this pass is measured by.