ADR-0097 The proof product is five screens, and each is a captured scene
#Context
Phase 7's criterion is "the supervisor tool built entirely from the system, added to the fixture set". What existed was one screen: a static queue of six items, composed from three components, with no shell and no navigation. Scene 21 captured it, but only from a dev server someone had to start by hand, so fixtures:all skipped it by name (ADR-0094) and build:check never reran it.
The playbook does not list the tool's screens. The maintainer chose all four proposed beyond the queue: incident detail, alarm flood, handover and team.
#Decision
Capture is a build step. tools/make-proof-fixture.js makes a production build of apps/proof, serves it with next start on a free port, captures every screen and stops the server. WR_PROOF_URL still points it at an outside server. The exclusion in make-all-fixtures.js is gone, so build:check reruns the capture like every other generator, and the core and tokens workflows now install Chromium to do it.
Five routes share one shell. A View holds a ShellHeader, a ShellLeftPanel whose NavigationItems carry icons from @wolf-rayet/icons, the route, and on two screens a ShellRightPanel. The shell takes the current route as a prop, so each screen's markup is fixed by its own file. Layout is the app's own stylesheet, on the system grid (ADR-0096); it sets no ground, border, colour or type on anything a component draws.
| Route | Scene | Demand | What it proves |
|---|---|---|---|
/ | 21 | the sixth row's Alert | a demand belongs to the Emitter, not the Arbiter ranking it |
/incident | 67 | the page header's summary | the demand moves to the one route surface built to hold it |
/flood | 68 | the page header's Alert | thirty-four alarms, one demand: the cause |
/handover | 69 | none | an acknowledged failure renders one rung down |
/team | 70 | none | load is Marked, not Demanded |
The capture takes the View, not <main>. The shell is part of what the product ships and part of what the View's demand cap counts. The capture also copies the <html> attributes, because the app puts its density there so the grid's gutters, spelled in spacing tokens, resolve at :root. That is the placement apps/docs already uses.
_scene-proof.css holds the ground and the metric font and nothing else. The layout it carried for scene 21 moved into the app.
The engine corrected the product once. The first team screen declared "Rebalance the queue" at level 1. It was the brightest thing in its page header, and the scene failed on salience — share as under-declared. It is level 3 now, which is what the screen always meant.
The product found a component gap too. NavigationItem coloured its label and not its anchor, so an icon beside the label drew in the browser's link blue. The anchor now takes the label's colour. No scene without children moved, themed or unthemed.
The phone width found four more. At 375px the shell header's one-line identity set the width of the whole product, its action truncated before its context did, the left panel's column filled the first screen, the page header's controls shrank to ellipses, and a table cut every cell to one letter. The app now bounds its own grid tracks. Below the grid's md breakpoint, ShellHeader keeps its controls whole, ShellLeftPanel runs its entries in one scrolling row, PageHeader wraps its controls, and DataTable keeps 8rem a column and scrolls sideways.
A breakpoint is a token, not a media query in a component. The first attempt put @media (max-width: 42rem) in four component stylesheets, and the React discipline test refused it: a component stylesheet holds no literal size, and a media query cannot read a custom property. So the switch lives where the breakpoints already live. generator.config.json gains grid.layout, five properties with a narrow and a wide value (bar-direction, bar-wrap, bar-overflow, bar-shrink, cell-floor); grid.css declares the narrow values at :root and the wide values from md on; and the component tier gains a grid source, aliasing them with the wide value as the fallback, so a page without grid.css, every fixture among them, renders as it did before. verify-output.js checks the new aliases. The budget measures at 1280px, where every property takes its wide value, and no scene moved on either set.
One screen list, read by the tool and the site. The screens live in packages/budget/proof-screens.json: key, route, scene, where the demand sits and what the screen proves. The capture tool writes from it, and r136.dev's /proof page renders it: a table of the five screens with each scene's verdict, then one section per screen with the components it imports (read from its source), its receipt, and the captured scene framed at half size through the same route the demo uses, which now serves exactly the screens the list names.
The site fonts gain Latin-1, declared ahead of the content. The team screen's accented name failed verify-output.js: ADR-0035 subsets the faces to the codepoints the repository writes, and no file had written one. familySubset now declares U+00A0-00FF whole. That widens ADR-0035's "no wider" on purpose: a product names real people, and a subset that cannot set their names fails the first one it meets.
The phone entries wrap rather than scroll. A scrolling row hid the current entry whenever it sat past the edge, so ShellLeftPanel reads bar-wrap instead of bar-overflow below md, and every entry stays in view.
The handover form works, and read-only is drawn. A form control given no onChange rendered read-only, or disabled for a choice, and nothing showed it (standing item 15), so the first handover screen was inert while looking editable. The form is now a client component with every control wired. InputField and NumberInput gain an explicit readOnly: no field ground, a dashed outline, and aria-readonly on a choice. Only the explicit prop is drawn, so no static specimen moved; the handover's locked outgoing supervisor uses it, and so does the whole form after sign-off.
The budget is read after the supervisor acts, too. A screen list entry may carry steps, a click or a choice in a select, and the capture replays them before it reads the View. Five such scenes exist, one per screen: the queue after its payment failure is acknowledged (71), the flood after its cause is acknowledged (72), the handover after sign-off (73), the incident after a new card is requested (74), and the team after rebalancing (75). In 71, 72 and 74 the acknowledged demand renders one rung down and the View holds none; in 73 every field is drawn read-only; in 75 the load marks are gone. All five pass. /proof lists every captured state with its steps in words.
#Consequences
- Scene 21's values moved, since its screen gained a shell and a page header.
Scenes 67 to 70 are new. All five pass on darwin-arm64; linux-x64 values come from the budget workflow's receipts.
- All five are excluded from the themed set by name, for the reason scene 21
already gives: a themed sibling would be a recomposition.
build:checknow needs a browser and a Next build, which costs about a
minute in each workflow that runs it.
#Rejected
- A static export (
output: 'export').apps/proofis deployed, and the
capture should not change how.
- Building the shell in the root layout. A layout cannot know the current
route without becoming a client component, and a client shell makes the captured markup depend on hydration.
- Capturing only
<main>. It would leave the shell out of the scene, and
the shell is where a persistent surface could try to spend the View's demand.