ADR-0028 Shape carries the rule of a set, and the shape vocabulary splits in two
#Context
The checkbox and the radio button arrive together, and they are the first pair in the roster whose difference is not a difference in what they *are*. Both are Emitters, both hold levels 1 to 3 on ADR-0021 §C's reason, both are a control with a mark and a label, both stand on surface-inset, and both take the same geometry ladder. Nothing in the layer model, the intensity ladder or the theme matrix distinguishes them.
What distinguishes them is a rule about the *set* each belongs to. Several checkboxes may be picked at once; exactly one radio button may be. That rule is not a property of the control the reader is looking at — it is a property of the group — and the reader has to know it *before* deciding what to do, not after.
Every system solves this the same way: square versus circle. It works, it is universal, and until now this system had no account of why it is allowed to work here. The shape vocabulary established by the status indicator and restated by the alert, the tag and the inline loader means one thing: a triangle is "worth knowing", an octagon is "critical", a ring is "nothing to report". Every one of those is a statement about attention. A square that means "several may be picked" is not a statement about attention at all, and dropping it into the same vocabulary would make the vocabulary mean two incompatible things at once.
There is a second, narrower problem the pair exposes, and it is in the enforcement rather than the design. packages/react/test/discipline.test.js compares every polygon filed under a shape name against every other component's polygon of that name, and it also asserts that every filed name has more than one user — because a shape restated nowhere is a shape nothing is keeping in step. A checkbox's tick and a tooltip's tail are polygons drawn by exactly one component each. Filing them in that table makes the shared-vocabulary check pass on a lie; leaving them out makes the reverse half of the check — an undeclared clip-path is a failure — fire on a legitimate shape. Neither is acceptable, and the table has to say which kind of shape each polygon is.
#Decision
Shape may carry the rule of a set as well as a meaning for attention, and the two are different vocabularies. A checkbox is square because several of its set may be picked; a radio button is a circle because exactly one may be. Each component declares its rule in its own contract as setRule, and the drift check files shape names in two disjoint namespaces: shared vocabulary entries, which every component drawing them must draw identically, and a component's own shapes, which no other component may claim by that name.
The rule of the set is read before any state is read, which is why it is the outline and the state is the fill. A checkbox's states are an empty box, a box with a tick and a box with a bar; a radio button's are an empty ring and a ring with a filled centre. None of those is a colour, so all of them survive field-night untouched.
The third state is the type. indeterminate means "some of the set", which is a statement about the set rather than about this control — and it is the one state exclusivity cannot produce. CheckboxState has three members and RadioButtonState has two, and packages/react/test/radio-button-violations.tsx asserts that state="indeterminate" fails to compile there while the checkbox's fixture asserts it compiles here. The two files are otherwise the same file, and that one line is the whole difference between the components at the type level.
The vocabulary split, and the hole it would otherwise open. A private shape name that collided with a shared one would escape the cross-component comparison while looking like it had joined the vocabulary — a triangle that means something else, declared out of reach of the check that exists to stop exactly that. The two namespaces are therefore disjoint, and the discipline test asserts it: *no component means something of its own by a shared vocabulary word*. Three private shapes exist today — the tooltip's tail, the checkbox's check and bar — and each is compared against itself and named in the check's output rather than hidden by it.
**Where the square and the circle are *not* filed.** The checkbox's square is a border-radius of zero and the radio button's circle is a border-radius of 50%; neither is a clip-path, so neither appears in the polygon drift table at all. Both components declare shapes: {} there — "draws no polygon" as a claim the check verifies, the loading block's precedent — and the rule of the set is carried in the contracts' setRule and in this record instead. That is a gap worth naming: the most load-bearing shape decision in this pair is the one the polygon check cannot see, because it is a radius rather than a set of vertices.
#Rejected options
Distinguish the two controls by state names alone and let the shapes match. Two square controls, one of which happens to be exclusive, told apart by their labels and their behaviour. It is internally consistent and needs no new account of what shape means. It lost on §4. In field-night a reader has shape, position and words, and the rule of a set is precisely the thing that cannot be put into the words — a label says what the option *is*, not how many of its siblings may be chosen with it. A reader who learns the rule only by clicking has learned it from the interaction rather than the interface, which is the failure the field-first rule exists to prevent.
File the square and the circle in the shared attention vocabulary. One table, one check, no split, and the drift check keeps working unchanged. It lost because the vocabulary's entries are statements about attention — a triangle means "worth knowing" wherever it is drawn — and a square meaning "several may be picked" makes the vocabulary a bag of shapes rather than a language. The check would then be comparing polygons that were never claiming to mean the same thing, which is comparison without a claim behind it.
Add a fourth channel for structural meaning. §2 closes the channel list at three — light, language, motion — and the rule of a set is arguably none of them, so name it. It lost for the reason ADR-0021 dropped ai-label: opening the channel list to hold one property is a change to the system's thesis paid for by two components. It is also unnecessary. Shape is already how this system encodes meaning under light; what this record does is say that shape can encode a structural fact as well as an intensity fact, which is a statement about one channel's contents rather than a new channel.
Let a component file private shapes in the shared table and relax the "more than one user" check to a warning. The smallest change: one table, and a single-user shape is merely flagged. It lost because the check's whole value is in its strictness. "Every shape here is restated somewhere and kept in step" is a claim worth making; "every shape here is restated somewhere, except the ones that are not" is not a claim at all, and a warning nobody fails on is a comment in an unusual place.
#Consequences
Two components, one mechanism, and the receipts say so. 27-checkbox-calm.html and 28-radio-button-calm.html are the same scene with the control swapped, each holding a controlled pair that differs only in how many controls a row carries and a row that carries every state the component has. The pair prices the plural case ADR-0021 §C's ceiling rests on; the state row makes sure the receipt covers every glyph this record's shape argument turns on.
The drift check is stronger than it was, not weaker. It now makes two claims where it made one — the shared vocabulary is shared, and nothing private is masquerading as shared — and it prints the private shapes rather than omitting them. The tooltip's tail, filed here for the first time, is the case that motivated the split before either selection control did: a tail says "this belongs to that", which is no more a statement about attention than a square is.
A rule for the roster's remaining set controls. toggle, select, dropdown and tabs each present a set with a rule about how many members may be active. Each is now answered before it is built: the rule of the set is carried in the control's outline where an outline can carry it, declared in the contract as setRule, and never left to the label.
One thing this record does not fix. The square and the circle are radii, and the polygon drift check cannot compare radii. If a future component drew a square meaning something else, nothing would catch it. That is a real gap, it is named here rather than discovered later, and closing it would mean a second drift check over border-radius — which is not worth building for two components and is worth building for six.