# build-0: checks, fixture gaps, choices

*W-build0, Opus 5.5, 2026-09-27 02:04 to about 04:00 Beirut. Brief: `briefs/W-build0.md`. Artifact of record: `build-0/index.html`. Fixtures: `contract/fixtures/*.json` (written 02:08), copied byte for byte into `build-0/fixtures/` by `scripts/build0-fixtures.py`. Checks: `scripts/build0-shots.cjs`, full log in `logs/W-build0-shots.txt`.*

## 1. Verification (run 2026-09-27, final pass after the last edit)

Command: `NODE_PATH=/home/botbox/node_modules node scripts/build0-shots.cjs`. It serves `companion-2026-09-26/` over http so every page loads under the `/build-0/` path prefix, as on Cloudflare Pages. It renders now, situation x3, hold, board, gallery and index at 1280 x 900 and 390 x 844, then reruns in two `file://` modes. Result: **ALL OK** (0 failures).

| Check (from the brief) | Result | How it was checked |
|---|---|---|
| No horizontal overflow at 390 px except the clock strip | **Pass**, 8 of 8 pages at 390 and 1280: `scrollWidth` equals the viewport | The script walks every element and flags any that extend past the viewport, unless they are inside the clock's own scroll box (`.scrollx`, `.c-strip-box`) or clipped by an ancestor. Below 480 px the clock collapses to four lanes and fits, so at 390 px the clock does not scroll either. |
| Every internal link and module import resolves from `file://` and with a `/build-0/` prefix | **Pass over http with the prefix**: 0 failed requests, 0 console errors, 0 broken links on 16 renders. **`file://`, Chromium with `--allow-file-access-from-files`**: 8 of 8 pages build, 0 failed requests, 0 broken links. **`file://`, default Chromium**: index works; the other 7 pages do not build (see the note below the table). | Playwright `requestfailed` and response status on every request; every `a[href]` is checked against the file tree. |
| No em dashes and no banned words in the output | **Pass**: 0 em dashes (U+2014) in `build-0/` (including the fixtures), `scripts/build0-*.{py,cjs}`, and this file. 0 hits, as whole words, for the banned list (the two marketing terms and the one-word marketing noun from RULES.md, the two trade verbs and the ranking noun from the brief, the two stance adjectives and the casino verb from the brand brief, and one promotional noun) over every html, js, css, json and md in `build-0/`, this file included. The word list lives only in the grep command in `logs/W-build0-shots.txt`, so this file does not spell it. | `grep -rP "\x{2014}"`, then `grep -rniE` for the word list. A's term for the clock's current-time marker (in English, "the now" plus the banned word) is renamed "now line" everywhere. |
| No literal colour or size in `components.css` or `lib/` outside the hairline exception | **Pass**: 0 hex colours, 0 `rgb()`, 0 px values other than 1 px, outside `tokens.css`. | Regex scan over `components.css` and `lib/`, excluding comments and `@media` lines. **Exception, stated:** the two breakpoints in `@media (max-width: 900px)` and `(max-width: 480px)` are literal because CSS custom properties do not work in media queries. The clock's own 480 px switch reads `--clock-compact-below` from tokens. SVG stroke widths and dash patterns are tokens (`--sw-*`, `--dash-*`), and all clock geometry is read from `--clock-*` by `lib/clock.js`. |
| `prefers-reduced-motion` makes A's three motions instant | **Pass**: under `reducedMotion: 'reduce'` the tokens resolve to `--m-land=0ms --m-state-out=0ms --m-state-in=0ms --m-underline=0ms`. With all three motions triggered (gallery motion demo), 0 elements still have a non-zero animation or transition duration. Normal motion: 600 / 120 / 200 / 400 ms. | Playwright context with `reducedMotion`; computed `animationDuration` and `transitionDuration` of every element. The now line steps and never glides in either mode. |
| Every displayed number has a C03 caption or a null reason next to it | **Pass for every number a component renders**: 53 (now), 39 / 47 / 42 (spring / agrippa / mossy), 7 (hold), 11 (board), 47 (gallery), 0 orphans. | Each component number is a `[data-num]` element inside a `[data-prov]` container. The script requires a caption (`[data-cap]`, `.c-03-fresh`) or a null reason (`[data-null]`, `.c-null`) in that container. **Scope, stated:** numbers inside fixture-authored sentences (state sentence, since-you-looked lines, why-now lines) are set in the mono voice but are not `[data-num]`. They carry their event id and clock on the same line, which is their provenance. Numbers inside the SVG clock carry their source in the lane's end column and in the tooltip (`<title>` with the event id). |
| Safety colour only at 22 px or in a filled block | **Pass**: 0 text elements in the safety colour below 22 px outside a safety-filled block, on all 16 renders. | The script compares every text element's computed colour with `--c-safety`, then checks its font size and its ancestors' backgrounds. On the Now list, *Safety changed* (QUILL) is a filled crimson block with ground-coloured text. In a room it would be the 22 px state word. |
| Nothing knowable only later appears on the SPRING room | **Pass**: 17 event ids rendered, 0 with `availableAt` past the as-of minute. 0 hits for the later facts (the 110-wallet hour after, 750 of 861 exited, the X post 10 h later). | The script reads the rendered `data-ev` ids with every drawer open and compares them with `fixtures/spring.json`. The runtime rule is in `lib/model.js` (`knowable`): an event is shown only if its `availableAt` falls inside or before the as-of minute. |
| Screenshots, JPEG under 500 KB | **Pass**: 17 shots in `build-0/shots/`. The largest are gallery desktop at 465 KB and gallery phone at 445 KB. | Gallery desktop needed JPEG quality 25 to fit under 500 KB, so it is soft; open the page itself for detail. Also shot: `situation-spring-theme-b-desktop.jpg` (the same page under the option B token block). |

**`file://` in Chrome.** Chrome refuses ES module scripts on `file://` by default (CORS, origin `null`), and every browser refuses `fetch()` there. The brief asks for ES modules and for pages that work from `file://`, which default Chrome cannot do at the same time. What build-0 does:
(a) All paths are relative and resolve. This is verified with `--allow-file-access-from-files`, where every page builds.
(b) Fixtures fall back from `fetch` to a classic `<script src="fixtures/<name>.js">` that registers the same JSON, so data loads on `file://` wherever modules run.
(c) `index.html` is plain static HTML and works everywhere.
(d) If modules do not run, the pages show a line saying so and how to open them. They do not show a blank page.
Firefox was not tested (not installed in the Playwright cache). Over http, including the Cloudflare `/build-0/` prefix, nothing is needed.

## 2. Fixture gaps (rendered as the null-reason path; nothing invented)

| Gap | Where it shows | What build-0 does |
|---|---|---|
| **No hero field in the contract.** T7 §3.2 says the state chooses the hero, but `SituationState` has no `hero` or `heroField`. | C02 on every room | `lib/model.js` `heroFor` is a table: money first uses `holders.walletsBeforeReference` against `market.walletsIn1h`; early money leaving uses `holders.earlyCohortRetention` against the checkpoint snapshot; attention first uses `market.walletsIn1h`; developing uses lights on against lights this chain can show (derived, labelled). Proposal: add `state.hero: {field path, reference path}` to contract v3. |
| **No per-interval net flow.** `market.netFlow1h` and `volume1h` are null on SPRING and AGRIPPA ("tape flow sum not in the T5 extract"). MOSSY has one illustrative hourly value. | C07 FlowBars | Bars show wallets in per interval, derived from successive tape reads (`walletsIn` differences, height per minute), and are labelled as such. Net flow prints its null reason next to them. A true diverging C07 needs `market.flowSeries[]`. |
| **MOSSY has no relationship and no reason for having none.** `relationships: []` with no reason field. `now.json`'s MOSSY row does carry "no lag bracket: only one kind of event". | C06 on MOSSY | Prints "the fixture carries no relationship for this room". Proposal: make `relationships` nullable with a reason, or add `state.lagReason`. |
| **SPRING: a `trader_thesis` event sits on the attention lane (17:23:53Z) while the lag relationship is still open with "no attention event seen yet".** Contract §6 explains the thesis re-lights `traders`, a money light, and the relationship closes on attention lights, not on the lane. | SPRING clock | Drawn as the fixture says: a ◆ on the attention lane inside an open bracket. On the page this looks like a contradiction. Vesper should decide whether a thesis closes the bracket or moves to the money lane. |
| **SPRING as-of precision.** `meta.asOf` is 17:27:00Z. The minute-60 tape read `ev-sp-t0060` is available at 17:27:06Z, and the diffs cite it. Contract §12.2 item 4 says "asOf 17:27:30Z", but the fixture says 17:27:00Z. | knowable-later rule | build-0 treats asOf as minute-precision, so the read is shown. Either set `meta.asOf` to 17:27:30Z in the fixture, or accept the minute rule as the contract's. |
| **MOSSY price has no tape read event.** `market.price` is set (illustrative) but no `tape_read` event carries a price. | MOSSY price lane | The lane says "no tape price read in this window". The number row still shows the price with its caption. |
| **Alert switches** (v2 hold view, eight per-token switches) are not in any fixture. | hold | Not drawn. The hold page prints `board.alertsNote` and says the switches are absent. |
| **Similar situations, Record, frozen rooms** are held until Gate 1a and are not in the fixtures. | room | Not drawn. The PLUMB archived row prints its `frozenRoom` path as text marked "not in build-0". |
| **The materiality rule text** is not in the payload. Only `materialBecause` per line and `materialityVersion` are. | since you looked | Prints each line's `materialBecause` and "materiality m0-provisional, provisional". The v2 wireframe printed the whole rule under the list. Proposal: `diff.materialityRule` as a string. |
| **Components skipped because no fixture carries their data:** C10 PeerStrip (no band percentiles), C11 StanceTrack (one stance event only), C12 QuantileDots, C13 ZoneBand (gated), C19 CompositionBar (the 260 recurring is illustrative and has no three-segment split), C20 ScorecardRow, C21 Sparkline. | gallery | Not built, per the brief. |
| `contract/COMPANION-CONTRACT-v3.md` did not exist when the run started. It appeared later, and the build was then aligned with §0.6 (print `providerAt`, place by `availableAt`) and §6 (lane and glyph grammar). | event lines, clock labels | Lists and clock labels print `providerAt` with "available HH:MMZ" in the caption. The clock places events by `availableAt`. |

## 3. Where A, the v2 hierarchy and T7 disagreed, and which won

Rule from the brief: v2 governs order, A governs appearance, T7 governs the form of a component.

| Point | A says | v2 / T7 says | Built |
|---|---|---|---|
| Desktop room layout | read on the left 8/12, since you looked in a right rail 4/12, clock full width below | state, sentence, clock, since you looked, waiting for | **Both.** The DOM order is v2's (phones and screen readers read state, hero, sentence, numbers, why, clock, since, waiting). A CSS grid places *since you looked* in A's rail and *waiting for* under the read block, as in A's render. On desktop the clock therefore sits below *since you looked* visually. |
| Phone order | state, hero, sentence, numbers, clock, since, waiting, why | the Why drawer right after the sentence | **v2**: the Why drawer (collapsed) comes right after the numbers. It costs one 44 px row. |
| Tabs | the A renders read Discover, My Board, Market, Record | Now, Situation, Board (Thomas 01:05) | **v2**: Now, Situation, Board. Record and Market are not in build-0. |
| Actions | "Watch" and "I'm in" | "Watching" / "In" | **v2 wording**: Watch, In (stubs). |
| Lights row | A shows a lights line inside Why | C18 only in the evidence drawer (Vesper P1/P2) | **Vesper / v2**: C18 is only inside Why this state, and on the hold page (entry vs now). Never on Now. |
| The Fomo trader's lane | A: the money lane | fixture: `trader_in` on money, `trader_thesis` on attention | **Fixture** (contract §6). See the gap above. |
| C15 lane | A draws no suspect print | fixture puts `suspect_print` on the safety lane; T7 says inside the price lane | **T7** (form of the component): drawn in the price lane as an open, struck marker, not connected. It is below the lane's range on AGRIPPA and says so. |
| Provider-print gap | A: hatched on the price lane | fixture: `lane: money`, source `provider:dexscreener` | **T7 C14 inside C05**: a provider-print gap is drawn on the price lane (`lib/model.js` `laneOf`). |
| Ticks | "ticks every 10 minutes" | AGRIPPA's clock spans 24.4 h | Minor ticks every 10 minutes when they fit (6 px or more apart). Labelled ticks step 10 / 20 / 30 / 60 / 120 min and up so labels never collide. The date is printed when the day changes. |
| Stale price | "number steps down one lightness step at 3x cadence", dotted underline | T7 C03: dim at over 3x the normal cadence | **Same rule.** AGRIPPA's price (26 min old against a 1 min tape cadence) renders dimmed and underlined, with "stale: over 3x the 1m cadence". |
| Three type sizes per view | A's guard | T7 §3.2 | Kept inside the read block (state 22, hero 44, body 14 or 16). Captions are 11.5 px everywhere. The page adds section labels at 11 px. |
| Safety word colour | 22 px state word, or a filled block | same | Room: 22 px crimson word. Now and board rows (small): filled block with ground text. |

## 4. What a designer will want to change first

1. **Caption weight on the Now list.** Every cap and change on a row carries source, "1 source", "provider" and "illus." tags. With composite fixtures that is up to four tags per row, and the ledger reads busy. Suggestion: on list rows print only source and age, and move the provider and illustrative tags into a row-level marker.
2. **Label collisions inside the clock** at 1280: first-value labels against the state-change rule, and the holder-step label over the holder line. There are halos and simple collision skips, but no real layout pass. The clock needs a label-placement rule, which is a design decision (leader lines as in B, or a label row under each lane).
3. **Empty space in the left column** when *since you looked* is long (AGRIPPA has five items plus the rule). Options: cap the rail at three lines on desktop, or let the clock start beside the rail.
4. **The now line and the last tick label** share the axis row; the tick label is dropped within about 8 clock-font widths of now. The designer may prefer a fixed "now" column.
5. **Mini clock on Now rows.** It is drawn at full cell width with glyphs only. A may want the lag bracket labelled in the row, and the phone may want it at 48 px (it is 56).
6. **The ice blue.** It reads as "positive market" on the Now list when every row's state dot is blue (money first, crowd arriving, attention first). OPTIONS §2 already names this risk. A token-only fix is to move `--c-evidence` toward neutral for state dots.
7. **Wordmark.** A's mono CAVERIO plus the reading square is in. The 09-16 mark is in `assets/` untouched and unused.
8. **Theme B** is complete in `tokens.css` (`?theme=b`). B's wordmark tittle (the ochre dot over the i) is approximated by recolouring the reading square as a round ochre dot. The real tittle needs an SVG wordmark.

## 5. What I did not do

- No test in Firefox or Safari. Chromium only, via Playwright.
- No CVD simulation render. The palette values are OPTIONS §2's, whose CIEDE2000 table was computed there.
- No accessibility audit beyond the checks above: focus-visible styles exist; SVG strips carry `aria-label` and per-glyph `<title>`; the drawers are native `<details>`.
- No live data, no search, no Watch or In behaviour, no alerts.
- I did not touch `contract/`, `wireframes-v1/`, `wireframes-v2/`, `design-options-2026-09-27/` or `site/`. Nothing was deployed.

## 6. Files

- `index.html`: the walk and the illustrative list. The list is generated from the fixtures' `illustrative` flags by `scripts/build0-fixtures.py`: 175 fields.
- `now.html`, `situation.html?f=spring|agrippa|mossy`, `hold.html`, `board.html`, `gallery.html`.
- `tokens.css` (A, plus a complete `[data-theme="b"]` block), `components.css`.
- `lib/render.js` (fixture loader and page builder), `lib/frame.js`, `lib/clock.js` (the shared x scale, ticks, lanes, glyphs, now line), `lib/strip.js`, `lib/model.js`, `lib/format.js`, `lib/dom.js`, `lib/components/c01 ... c23`, `lib/pages/*.js`.
- `fixtures/*.json` (copies) and `fixtures/*.js` (the `file://` fallback).
- `assets/caverio-mark.svg`, `assets/caverio-favicon.svg` (09-16 mark, untouched).
- `shots/`: 17 JPEGs.

Status: DONE

## v3.1 fixtures (2026-09-27, W-contract-v31)

The contract moved to v3.1 (`contract/CHANGES-v3.1.md`). `python3 scripts/build0-fixtures.py` copied the five fixtures into `build-0/fixtures/` byte for byte (`cmp` identical for all five) and regenerated the `.js` twins and the illustrative list between the markers in `index.html` (182 fields). No page, component, `lib/` or CSS file was changed.

Rerun: `NODE_PATH=/home/botbox/node_modules node scripts/build0-shots.cjs`, exit 0, **ALL OK**. Same results as §1: 16 http renders at 1280 and 390 with `scrollWidth` equal to the viewport, 0 orphan numbers, 0 small safety text, fonts loaded; reduced motion 0 ms on all four tokens with 0 elements still moving; SPRING knowable-later 17 event ids rendered, 0 past asOf, no later facts; `file://` with `--allow-file-access-from-files` 8 of 8 build; default Chromium `file://` as before (index builds, module pages refused, as §1 explains). The 17 shots in `build-0/shots/` were rewritten.

What the §2 gaps became in v3.1:

| §2 gap | v3.1 |
|---|---|
| No hero field | `state.hero {field, reference}` in every situation, default table = `heroFor` (contract §3.6). The page still uses its own table; switching it to read `state.hero` is a page change, not done here. |
| MOSSY relationship without a reason | `state.lagReason`: the relationship id on SPRING and AGRIPPA, null with `no lag bracket: only one kind of event` on MOSSY. |
| SPRING thesis on the attention lane | `trader_thesis` is a money-lane event (contract §6). The shot shows the diamond on the money lane at about 17:24Z, and the bracket stays open. SPRING's mini clock in `now.json` carries it on the money lane too. |
| SPRING as-of precision | The fixture is 17:27:30Z (it already was); the rule is written down in contract §0.12: to the second, never rounded forward. |
| Materiality rule text | `diff.materialityRule` on every diff. The page still prints `materialityVersion` only. |

Also visible in the payload but not drawn by any page: situation ids carry `~k` (sha256 of the opening event's `sourceKey`), `pools[]` rows carry `window`, `coveredBlock`, `sourceHead`, `lagSec` from those two, `lastTradeAt`, `seekCursor`, `missingSpans`, and the IDL-named `bondingCurve`. The page header prints `Contract companion-v3.1, projection p1-provisional`.
