Skip to contentWolf-Rayet

Decision records

ADR-0120 A grouped story names a group, and a scan that checked nothing has not passed

Accepted2026-09-21Phase 3

#Context

ADR-0119 found that check-registries.js was measuring every list against all twenty components in COMPONENT_LAYERS, and that six of its eight lists reported 0/20 because they were aimed at surfaces that render none of them. Fixing that let the budget job reach steps it had never run, and two of them failed. Both fail for the same underlying reason, and it is the reason this record exists.

The workbench used to hold one story file per component, named for it. It now holds nine grouped files, and each carries a single const meta whose title names a group:

'Forms and inputs/Button'      'Forms and inputs/Fields'
'Text and data display/Content' 'Loading/Loading and status'
'Overlays and layering/Overlays' 'Navigation/Navigation'

Two tools still describe the old tree, and each fails in its own way.

tools/check-self-description.js carries a position called "the story's title", reading apps/storybook/stories/<Component>.stories.tsx. It reported 20 problems across 0 checked — eighteen files absent, one present without a per-component title, one reporting the group title as a mismatch. That is the same zero-signature ADR-0119 named: a question asked of the wrong surface.

packages/content-lint/tools/scan-real-copy.js names thirty-eight per-component story files, thirty-six of which do not exist, and threw ENOENT before reading a single string. It has failed this way since the regrouping, which is why the docs copy has gone unscanned.

Fixing the second one exposed something larger. The scan's anchor was never the filename — it imports a component's built tone function and finds real calls to it. Pointed at every story file instead of one, it runs, and reports:

0 component(s) scanned, 38 reported without real copy, 0 real string(s) checked, 0 violation(s).

Measured across the tree: none of the thirty-eight tone functions is called with literal text by any surface. Not the nine grouped stories, not apps/docs, not apps/proof. Twenty-one are called inside packages/react/src — by the components themselves, at runtime, with props rather than with copy a scan can read.

So the corpus this scan exists to measure is not merely unreachable. It is not in the repository. ADR-0018 bootstrapped the length ceiling from that corpus, and the ceiling now rests on evidence nothing here reproduces.

#Decision

A grouped story names a group, so no story makes an identity claim about one component, and the self-description position that read one is retired.

Self-description exists to catch "a file that says it is a component it is not". A title reading 'Forms and inputs/Fields' is not a wrong claim about Checkbox; it is not a claim about Checkbox at all. The position had a subject when each file was named for its component and has none now. The remaining positions are unchanged and still cover every component: the opening comment of the component, of the contract, of the stylesheet, of the must-fail fixture, and of the fixture generator.

The real-copy scan reads every story file rather than one named for the component. The tone function is the anchor and always was. This is the same correction ADR-0119 made to the registry lists: derive from the fact, not from a filename convention that a refactor retired.

A scan that checked nothing has not passed. It fails.

This repository already refuses this shape elsewhere. ADR-0032 rejected letting an unbaselined platform measure itself and report green, on the grounds that "a first record compared to nothing is a tautology, and a tautology that reports green is worse than a gap that reports red". A corpus of zero strings yielding zero violations is the same tautology. The scan now exits 1 when no component supplied a single real string, and says why and what would fix it.

#Rejected options

Retire the real-copy scan and let content-lint go green. The smallest change, and it would turn the workflow green in one commit. Rejected because it is the tautology with an extra step: it converts "we check nothing and say so" into "we do not check", and the length ceiling ADR-0018 set from this corpus would then rest on nothing, with nothing in the repository saying so. The gap is real and it should be loud.

Leave the scan exiting 0 on an empty corpus. It already printed 0 real string(s) checked, so the information was on screen. Rejected because a green exit is what CI reads and what a reader trusts, and the printed zero is exactly the kind of detail that survives one pass and is skimmed on the next. ADR-0032 settled this shape for baselines; the argument does not change because the subject is copy.

Write the corpus now, in this record. Thirty-eight components' real copy is the work that would actually close the gap. Rejected as scope: it is authoring product copy for thirty-eight components, it wants review per component rather than per commit, and bundling it with a tooling correction would put one behind the other. It is named as the open debt instead.

Repoint self-description at the group title and check group membership. A component could be required to appear in the file whose title names its group. Rejected because it checks a different thing: membership of a grouping, not a file's claim about its own identity. It would also be checking a grouping no record establishes, so the check would be asserting a convention rather than verifying one.

#Consequences

pnpm self-description:check exits 0 — 94 identity positions across 20 components, each naming its own component and no other, plus 3 counted claims matching the set. The budget job's lint suite stops failing on it.

pnpm --filter @wolf-rayet/content-lint scan stops throwing ENOENT and now runs to completion, reads every story file, and fails honestly: no component supplied a real string, so nothing was checked. content-lint stays red, and this is the intended outcome rather than an unfinished one.

The open debt, stated plainly. No surface in this tree calls a tone function with literal copy. Until one does, the length ceiling ADR-0018 set is unreproducible from this repository. Filling it means giving a story or a fixture a real call per component; the alternative is retiring that ceiling on the record. Both are decisions, neither is made here, and the scan reports red until one of them is.

Three tools, one defect, now closed. The retired registry lists (ADR-0119), the retired self-description position and the repointed real-copy scan were the same mistake in three places: a per-component file layout encoded as a filename convention, outliving the layout. No fourth instance is known, and the shape is recorded here so the next one is recognised rather than re-diagnosed.