Skip to contentWolf-Rayet

Decision records

ADR-0054 An Arbiter grants, and this is the first one that does

Accepted2026-09-05Phase 4, reopened

#Context

§3 gives each layer one sentence, and the Arbiter's is: *"Resolves competing requests among its children."* Four Arbiters are built — queue-item, table-row, navigation-item, accordion — and not one of them assigns a level to anything. They resolve by *position*: a queue item's rank, a table row's order, an accordion's disclosure. Each is a real resolution and none of them is a grant.

That left the layer's distinguishing power unexercised, and it is the power the model is built around. §3 says an Emitter *"can hold an allocation, cannot grant one"*, and the whole layer split turns on the second clause. A tier whose defining capability nothing uses is a tier that has not been tested.

Three places in this repository had already said which component would use it, each of them while explaining something else:

  • Button.tsx, first paragraph: *"the decision about which of a toolbar's buttons matters belongs to the Arbiter the toolbar is."*
  • button-contract.ts, arguing that a button is an Emitter: *"a toolbar of six buttons has not decided which of them matters, and the component that did decide is the Arbiter the toolbar is."*
  • ADR-0050, on what performs an acknowledgement: the control is a Button, and an alarm's controls arrive as a set — acknowledge, escalate, silence — of which exactly one may be the local focus.

The first two are the same sentence written twice, which is a fair measure of how long it had been true. The third is a live gap: scene 49 draws a demand an operator has acknowledged and cannot draw the acknowledging.

#Decision

button-group is an Arbiter that grants a level to each action it holds. The primary action takes the group's own rung; every other action takes the rung immediately beneath it, floored at the bottom of the ladder.

#It takes the actions as data, and that is the whole component

Every other Arbiter takes children and ranks what it is given. This one takes ButtonGroupAction records and renders the buttons itself.

That is not ergonomics. A component that accepted <Button level={3}/> as a child could not grant anything — the caller would already have decided, and the Arbiter would be reduced to asking nicely and drawing a row. Owning the render is what makes the grant a grant rather than a convention, and the missing field on ButtonGroupAction is the point of the type: an action carries a label, a variant and its interaction props, and no level.

#"Exactly one" is unrepresentable, not validated

The props are primary: ButtonGroupAction and secondary?: readonly ButtonGroupAction[]. There is one primary field and it takes one action, so a caller cannot write two.

This is the move ADR-0048 made for a withdrawn demand and ADR-0050 for an acknowledged one, arriving at the props boundary instead of at a computed level: the illegal state has nowhere to be written. An actions array with a primary flag on one member would have been the obvious shape and it admits two flags, which is the rule stated as a comment.

#Order first, light second

The primary is first in the DOM and first on the screen, so reading order and ranking order are one order and a screen reader gets the answer a reader does.

That is forced rather than chosen, and the forcing is at the bottom of the ladder. A group declared at Ambient grants Ambient to everything: there is no emitting rung beneath it, so the two granted levels are the same number and light says nothing at all. If order were not carrying the ranking there, the component would have no encoding at its own floor — and §4 designs in field-night first precisely so that a component's encoding is the one that survives when the channels are taken away. Order is the encoding; light is the reinforcement.

The stylesheet contains no order property and no reversal of any kind, and that absence is the commitment.

#The stylesheet draws almost nothing

A row, a gap, a label and one rule. No perimeter around the set, no ground under it, no fill behind the primary.

An Arbiter's output is an allocation, not a box. The granted levels are drawn by button.css on the buttons themselves, which is where they belong — a level is a claim the element making it has to carry. A group that also drew the ranking would be making the same statement twice in a channel the budget prices separately, and the two could then disagree while each was right about itself.

The one rule it does draw is under the label, it climbs with the group's own rung, and it says *the set is a set*. It does not say which member wins.

#Rejected options

A children slot, with the group re-writing its children's levels. Possible in React and rejected on sight: a component that reaches into what it was handed and changes it is doing invisibly what this one does in the open, and a caller reading <Button level={3}/> at the call site would be reading a number that is not what renders.

An actions array with primary: true on one member. The shape every design system uses, and it makes the central rule a comment. Two members can carry the flag; nothing stops them; the component then has to decide at runtime which flag wins, and whatever it decides is a rule living in an implementation.

A segmented perimeter binding the set into one control. It looks like arbitration and is not: a segmented control is a set of *mutually exclusive* options, which is content-switcher's job and is on the roster. A button group is a set of things the operator may do, in an order; drawing it as one control would say they were alternatives.

Primary last, as the platform conventions have it. Rejected on §4's terms rather than on taste. Trailing-primary is a convention learned from dialogs, where the set is always two and the reader already knows the shape. This system's sets are alarm responses on a dense screen and its first theme has no hue to spend; the reader has to be able to find the primary by *scanning*, and scanning starts at the beginning.

No component at all — let the product lay out its own buttons. What the system did until now, and the reason three files had to explain that a button is an Emitter *and* that something else should be deciding. A rule stated three times in three files and enforced nowhere is a rule that is about to be broken.

#Consequences

Nothing in the engine changes. No new check, no new attribute, no new failure. The grant appears in the frame as the level each button declares, which is exactly where the engine already reads it — and a second attribute saying *"and I assigned those"* would be a claim about the same fact in a place nothing checks it against.

Scene 52 has no must-fail twin, and that is not an omission. This component's illegal states are unrepresentable: a set at Demanded is a compile error, and two primary actions cannot be written. A hand-written scene with two level-4 buttons in one Scope would fail salience — scope, which is scene 10 and is committed. There is no new rule here for a new scene to break.

ADR-0050's loop closes. That record found the acknowledgement control to be a Button and named the set it arrives in; scene 52 is the first time the set is on a screen this system measures.

The Arbiter tier is tested for the first time. ADR-0021 reopened the roster to test §3's claim that the authority question *"has exactly one answer per component and no ambiguous cases"* against an adversarial sample. This is a narrower and sharper test: the layer's defining capability now has one user, and the next Arbiter built has something to be compared against.

#Measured

  • The twenty-seventh component and the fifth Arbiter. Roster 27 built of 57.
  • 15 component tier tokens, every alias resolving in 4 themes and 2 densities; 24 non-text APCA checks, all met, worst |Lc| 16.23 against a floor of 15 in interior-light.
  • The grant, at each of the three rungs: Directed → (3, 2), Marked → (2, 1), Ambient → (1, 1). The last pair is the case the encoding has to survive and does.
  • Scene 52: 1 demand against a cap of 1, three Scopes, every one under the field-night ceiling of 0.128641, zero warnings. Engine roster 48 → 49 unthemed.
  • Cascade audit 2948 → 2961 scene-state pairs across 183 → 184 scenes.
  • Eight registries at 27 of 27; 155 identity positions across 27 components.