Skip to contentWolf-Rayet

Decision records

ADR-0126 The discipline test governs the twenty, and is run

Accepted2026-09-24Phase 7

Repairs the enforcement ADR-0107 rests on. Builds on ADR-0120 and ADR-0125.

#Context

ADR-0107 states that the React discipline test asserts the absence of sustained motion for every component. It has not run since the grouped-modules rewrite.

Three failures stacked on each other, and each one hid the next.

The file dies on its fourth entry. node packages/react/test/discipline.test.js exits on ENOENT opening packages/react/src/meter.css. 16 of its 37 roster entries name stylesheets the rewrite deleted, and the same entries name modules and contracts that went with them. 37 assertions ran before the crash; the 300 after it did not. A check that dies is worse than a check that fails, because it reads as a crash rather than as a result and takes everything downstream with it.

Nothing runs it. pnpm react:test maps to node test/scope.test.js. test:discipline was a script no workflow invoked.

The log named a check nobody was running. The step in .github/workflows/budget.yml that runs pnpm react:test carried a comment describing discipline.test.js and what it proves. The comment and the command had disagreed for long enough that the CI receipt asserted a check that was not executing.

The consequence is ADR-0125's subject, seen from the other side. Three infinite animations live in feedback.css on spinner, skeleton and progress bar. ADR-0107's rule forbids exactly that, its test is the enforcement, and the enforcement was dead. The cascade check landed in ADR-0125 caught them from the stylesheet; nothing was going to catch them from here.

The table also disagreed with itself. Its first check compares the table against COMPONENT_LAYERS in both directions, and reported all 17 grouped entries as in the table and not in the package. That check was correct and failing, underneath a crash nobody was reading.

#Decision

#The table is exactly COMPONENT_LAYERS

The 17 grouped entries are retired: meter, tag, tooltip, button, checkbox, radio-button, modal, accordion, empty-state, button-group, page-header, tabs, progress-indicator, pagination, presence, link, code-snippet.

They could not be repointed. Every per-component check here reads a contract, a token record, a module and a stylesheet that belong to one component, and the grouped set has none of those in that shape: there is no meter-contract.ts to point at, and three of the 17 have no export left at all. They described components in a shape the rewrite replaced, and the honest repair for a description of something that no longer exists is to remove it.

The table now holds the operational twenty, which is what its own completeness check always compared against, and that check passes in both directions for the first time.

#Four checks are about a mechanism, so they keep their subjects

ADR-0040's disclosure rule and ADR-0041's readonly rule are rules about a technique rather than about a component, and both of their subjects moved into the grouped set: the tabs panel is removed by the hidden attribute in Navigation.tsx, the checking-type inputs are in Form.tsx. Retiring those sections with the entries that used to carry them would have retired the rules and not the roster.

GROUPED_SUBJECTS carries two entries and four fields each, name, block, module and stylesheet, and nothing else. A grouped component has no contract, no token record and no must-fail fixture, so it cannot be asked for what COMPONENTS is asked for, and putting one in that table would make the completeness check lie about the package.

cssOf narrows a grouped stylesheet to the rules whose selector names the block. Without that, a check anchored on a block prefix reads its neighbours' rules and a question about what a component may not do is answered by a component that is not it.

#The exempt content slot retires with its subject

Nothing in packages/react/src names CONTENT_SLOT or EXEMPT_SLOTS. The component that declared one was code-snippet, which the rewrite replaced, so the section checking that a component keeps its hands off a region it has exempted was watching an empty set.

The other half is live and stays checked: the budget engine refuses a declared exempt slot that nothing between the element and its Scope actually exempts, and core still exports EXEMPT_SLOTS. The rule is enforced where a violation can occur, which is in a scene.

#Every path the tables name is read against the disk, first

This is the guard for what actually went wrong. The completeness check compares two lists of names, and both lists were right; what rotted was the files the names point at. So before any component is opened, every css, tsx, sources and contract path either table names is checked for existence, and a stale one is a line in the report rather than a stack trace.

#It is run

pnpm react:discipline, a step in the budget workflow, an entry in tools/battery.js. The comment on the react:test step is corrected to describe the file that step actually runs.

#Consequences

324 checks run where 37 ran. Three defects surfaced on the first complete pass, none of which any suite in the repository could see:

  • ADR-0105's variant-alone lookup has two roles, not one. The per-instance check recognised ${variant}-bold and not ${variant}-subtle, and the alert's panel selects the second exactly as its ground selects the first. The check was right to fail on a shape it did not know; the fix is to teach it the sibling deliberately rather than loosen the pattern.
  • Toggle wrote readOnly on a type="checkbox". The platform does not apply readonly to a checking-type control, so the markup promised a protection nothing supplied and a click still moved it.
  • Toggle wrote checked with no handler and no defaultChecked, which asks React to treat as controlled an input nothing is controlling. It now takes the mechanism this package already chose: defaultChecked with no handler, checked with one, and no no-op onChange, because nothing here declares use client. aria-checked went with the readOnly: a native checkbox exposes its own checked state, and an explicit copy goes stale the moment the control is uncontrolled.

Coverage lost knowingly, recorded the way ADR-0123 recorded its own: the component half of the exempt content slot, and the 17 grouped components, which are checked by tokens:verify for their token records and by react:cascade for their motion and by nothing for the properties this file checks. That gap is real and it is smaller than it was yesterday, because the two mechanisms worth keeping were kept.

#Rejected

Repoint the 17 at their grouped files. The first plan. It fails on the contracts: there are none to point at, the type-level enforcement layer does not cover the grouped set, and three of the 17 no longer export. Repointing would have produced 17 entries whose checks were mostly skipped, which is a roster that looks complete and asserts little.

Leave the file out of CI and keep it as a local tool. It is what the file already was, and it is how a rule stays written down while going unenforced for a whole rewrite.

Delete it. It holds 324 assertions about the twenty, including the one ADR-0107 depends on. The problem was never the checks.

Loosen the per-instance shape pattern to one permissive regex so a fourth lookup shape could never fail it again. The comment already in that check argues against it, and this pass is the argument's evidence: the shape it did not know was a real one worth adding, and a permissive pattern would have absorbed it silently along with the next one that is not.