Compass UI fixture boot — fully offline, fixture-seeded UI
Status: Draft Ledger-impact: none (assessment at the end)
Problem / Intent
Section titled “Problem / Intent”The Bridge re-clothe visual-smoke harness (apps/ui/e2e/visual-smoke.spec.ts)
is Matt’s before/after review vehicle, but the app can only boot against a live
daemon, and the ambient dogfood daemon’s board is empty — so the board
screenshots show no issue cards and the baselines are uninformative. Matt has
chosen a fully offline UI boot mode seeded with fixtures (over seeding the
server, which would require live GitHub/Linear credentials). This record
designs how that mode works, what its hard safety wall is, and plans the
implementation slices.
Global Constraints
Section titled “Global Constraints”- Stack: SolidJS ^1.9.13 + Vite + TypeScript; pure CSS custom props. No new runtime dependency.
- Hard wall (the load-bearing safety property): the fixture boot path must be structurally incapable of shipping in a production bundle — selected by a build-time signal a production build cannot accidentally set, dead-code eliminated from every non-fixture build, and guarded by a build-scan test.
- Determinism: the fixture-booted app must render byte-stable across runs — no clock reads, no randomness, no ordering nondeterminism reaching the DOM; animations neutralized at capture time so screenshots diff cleanly.
- Reuse, don’t fork: the fixtures are the existing
STUB_ISSUES/STUB_AGENTS(apps/ui/src/stub-data.ts) andSTUB_COMMS_STATE(apps/ui/src/comms-stub.ts); the test double is the existingcompass-fake.ts. No parallel fixture set. - UI-layer only: no Go backend, daemon, or dogfood-e2e changes. The Go
go test -tags podman ./e2e/...tier stays the real-transport integration; this mode is complementary UI-layer determinism. - Live path untouched: with fixture mode off, the live-daemon boot path
(
index.tsx→envConnectionProvider→createLiveClients→ WhoAmI →createAppStore) behaves exactly as today. - No committed secret / no new env file: the mode must not require any
.env*addition; the env-secrecy gate (apps/ui/src/env-secrecy.test.ts) stays as-is.
Approach
Section titled “Approach”What actually blocks offline boot (ground truth)
Section titled “What actually blocks offline boot (ground truth)”The store already knows how to be offline. Its issue signal seeds from the
fixture and is only overwritten when a compass client is supplied —
apps/ui/src/store.ts:685:
const [issues, setIssues] = createSignal<Issue[]>(STUB_ISSUES);and the overwrite is gated — store.ts:946,959-961:
if (options.compass) { ... void runEventStream({ client, onIssues: setIssues,Same for the fleet — store.ts:847-849:
const agents = createMemo<readonly Agent[]>(() => options.comms ? joinAgents(accounts(), presence()) : STUB_AGENTS,);and the daemon banner — store.ts:945: const [daemon, setDaemon] = createSignal<DaemonInfo>(STUB_DAEMON);, only probed live behind the same
options.compass gate. Both clients are declared optional on the options
type — store.ts:573 readonly comms?: CommsClient; and store.ts:595
readonly compass?: CompassClient; — and the header at store.ts:557-559
says it outright: “optional so a unit test constructs the store with NO
network client at all: the comms surface then holds initialComms (the
fixture, in tests) and every write rejects”. Every component test in the repo
(store.test.ts:35-38, test-router.tsx:43-46, …) already builds exactly
this store with initialComms: STUB_COMMS_STATE.
What forces a daemon is solely the boot chain in apps/ui/src/index.tsx:
-
index.tsx:46-49— the mode dispatch:const bootConnectionForMode =shellMode() === "client"? () => bootNativeClient(root): () => bootConnection(root, () => envConnectionProvider().resolve()); -
apps/ui/src/live/connection.ts:46-51— resolve throws without a door URL:const baseUrl = env.VITE_COMPASS_BASE_URL?.trim();if (!baseUrl) {throw new Error("VITE_COMPASS_BASE_URL is required to reach the Compass server; " + -
index.tsx:80-87— real clients + a WhoAmI round-trip that stops boot:const clients = createLiveClients(connection);const callerId = await bootCaller(root, () => resolveCaller(clients.compass));...if (!callerId) {return;}
So the design is small by construction: add a third boot arm that builds the
already-supported clientless (offline) store from the existing fixtures and
mounts the same render tree — no fixture network client, no fake transport,
no WhoAmI. The visual-smoke harness header (visual-smoke.spec.ts:3-9) still
declares this shape — it “Navigates the HashRouter surfaces of the stub-data
app … for Matt’s before/after review”, with no daemon; the live wiring
(SEA-1729) is what invalidated it in practice.
A1 — Mode selection + the hard wall (the load-bearing choice)
Section titled “A1 — Mode selection + the hard wall (the load-bearing choice)”Three mechanisms weighed:
- Runtime env flag (
VITE_COMPASS_FIXTURE=1read at boot). Rejected: Vite inlines everyVITE_*value into the bundle, so the flag is settable from any build environment or an untracked.envfile — exactly the surface the env-secrecy gate exists to distrust (env-secrecy.test.ts:1-9). The fixture code would also ship in every production bundle unless the check happens to constant-fold. Weakest wall. - Dedicated entry point (a second
fixture.html+ entry module, multi-page Vite build). Strong wall (the prodindex.htmlnever references the entry) but heaviest: a second HTML shell to keep in sync, harness URLs move to/fixture.html#/…, andvite buildstill emits the fixture chunk unless the input list is also mode-switched — i.e. it needs the mode switch anyway. - Vite mode (
vite --mode fixture) gating a statically-replaced branch with a dynamic import in the existingindex.tsx:46-49dispatch. Recommended.
Why (3) is the wall:
import.meta.env.MODEis not settable from the environment — no env file or process variable sets it, unlike anyVITE_*var. The only other setter is themodefield ofvite.config.ts, which is in-repo and reviewable, so the mode cannot be flipped by a build’s ambient environment.- Vite statically replaces
import.meta.env.MODEat build/serve time, soif (import.meta.env.MODE === "fixture")becomesif ("production" === "fixture")in a production build: the branch is dead-code-eliminated, and because the fixture boot module is reached only via a dynamicimport()inside that branch, its chunk is never even emitted. The fixture code is absent from the artifact, not merely dormant — that is “structurally incapable of shipping”. This DCE guarantee holds only while the comparison stays inline (if (import.meta.env.MODE === "fixture")); hoisting it toconst m = import.meta.env.MODE; if (m === "fixture")defeats the fold — which is why the build-scan gate (below), not the DCE alone, is the authoritative wall. - Invariant the chunk-absence depends on:
boot-fixture.tsmust have no static importer anywhere in the productionsrcgraph — it is reached ONLY via the dynamicimport()in the fixture branch. A singleimport { … } from "./boot-fixture"in shipped code re-emits its chunk into the production bundle and the negative build-scan assertion fails. (T3’sfixture-wall.test.tsmay importFIXTURE_SENTINELfromboot-fixture.tsbecause a test is excluded from the production build — that is not a static importer in the shipped graph.) - Defense in depth, three layers — the build-scan gate is authoritative, the
other two are accident-insurance around it:
- a build-scan gate test — the authoritative layer — mirroring the
existing precedent
apps/ui/src/preview-build.test.ts(which already runsbunx vite build --outDir …and asserts on the emitted bundle text,preview-build.test.ts:79): build productiondist/, assert a unique sentinel literal defined in the fixture boot module is absent, plus a positive-control assertion that a known live-app literal is present so the scan can never pass by reading nothing (see T3); - a runtime tripwire in the fixture boot module —
if (import.meta.env.PROD) throw— which stops the realistic accident (a CI or dev job that gains--mode fixture:vite builddefaultsNODE_ENVto production, soimport.meta.env.PRODis true and the throw fires). It does not stop a deliberateNODE_ENV=development vite build --mode fixture(Vite lets an explicitNODE_ENVoverride the default, leavingPRODfalse) — that is precisely why the build-scan gate, not the tripwire, is the wall; the tripwire is insurance, and demo-hosting stays blocked by policy (§A4); - Vite’s env-file loading is mode-keyed, so
--mode fixturedoes not load.env.development—VITE_COMPASS_BASE_URLis simply absent in fixture serves, meaning even a regression that reached the live arm would throw atconnection.ts:48rather than dial anything.
- a build-scan gate test — the authoritative layer — mirroring the
existing precedent
The dispatch composes with the existing comment contract at index.tsx:23-33
(shell-injected mode → client/embedded, absent → browser env path): fixture is
a third, browser-only arm, checked before the shell modes since a shell never
launches with --mode fixture.
A component/story harness (e.g. Storybook) was not chosen: the harness subjects
are whole-shell, URL-routed compositions (full-page Bridge with both sidebars +
topbar + usage bar, visual-smoke.spec.ts:19-23), and the repo already has the
stronger primitive — test-router.tsx:37-56 mounts the full clientless shell
today. This design productionizes that exact proven construction behind a
served URL, which a component harness cannot give without a new runtime
dependency (against the stack constraint) and still no full-page routed shell.
A2 — The fixture boot module (apps/ui/src/boot-fixture.ts)
Section titled “A2 — The fixture boot module (apps/ui/src/boot-fixture.ts)”A sibling of boot-native.ts, ~40 lines, that mirrors main()
(index.tsx:76-145) minus the network:
- Build the same
QueryClient(index.tsx:97-99shape). createRoot(() => createAppStore({ queryClient, initialComms: STUB_COMMS_STATE, workspaceKey: "fixture" }))— nocomms, nocompass. By the gates quoted above this yields: board =STUB_ISSUES(never overwritten), fleet =STUB_AGENTS, banner =STUB_DAEMON, comms surfaces = the comms fixture.callerIddefaults toCALLER_ID(store.ts:74export const CALLER_ID = "acc-matt";, applied atstore.ts:681), the same identity the fixtures are authored around — no WhoAmI needed.- Mount the identical render tree. To avoid duplicating the JSX block at
index.tsx:133-144, extract it into a sharedmountShell(root, store, queryClient)helper bothmain()andbootFixture()call — the one mechanical refactor this record makes to the live path (behavior unchanged; the extraction is the whole diff). NamedmountShell, notmountApp—test-router.tsx:37already exports a test-onlymountApp(MemoryRouter,{store,container}return); a second same-named export in thesrctree is a grep/import trap.
No fixture CompassClient is constructed at all in v1. That is deliberate and
is itself part of the wall: there is no fake client object that could leak
into a production code path, because the offline arm passes no client and the
store’s existing options.compass gate does the rest.
The compass-fake extension is the explicit upgrade path, not v1. If a
Bridge shot later needs a live-update visual state (an event arriving, a
card advancing — Bridge T5 territory), the seam is
apps/ui/src/live/compass-fake.ts: createFakeCompass() already implements
the driven client subset (whoAmI at compass-fake.ts:82, getServerInfo at
:71, an abort-honoring subscribeEvents generator at :96-105) behind the
one sanctioned client as unknown as CompassClient cast
(compass-fake.ts:112). The extension shape: give createFakeCompass an
optional scripted-frame option (opts?: { events?: readonly SubscribeEventsResponse[]; board?: readonly BoardIssue[] }) whose
subscribeEvents yields the cold-start boundary frame + scripted tail and
whose listBoardIssues serves the wire snapshot — the same protocol
live/events.test.ts’s scriptedTransport already fakes at the transport
layer (live/events.test.ts:116-122). The cost it carries — maintaining a domain→wire
rendering of STUB_ISSUES — is why it is deferred until a shot actually
needs it (D2).
A3 — Harness pointing + determinism
Section titled “A3 — Harness pointing + determinism”apps/ui/playwright.config.ts:41 currently serves plain dev:
command: "bunx vite --port 5173 --strictPort",The webServer command gains --mode fixture; the specs
(visual-smoke.spec.ts) keep their URLs — same origin, same hash routes, and
.state-dot / .bridge waits now find real cards. use also gains the
standard visual-testing determinism knobs (reducedMotion: "reduce",
deviceScaleFactor: 1); see the residual-risk bullet below and T4.
Determinism inventory (verified):
- Fixture data carries only fixed literals; the sole wall-clock/random reads
in the store are the write-path request-id prefix (
store.ts:984`ui-${Date.now().toString(36)}-${Math.random()…`) which never renders, and stream reconnect jitter (events.ts:121,stream.ts:314) which never runs (no clients). Message timestamps render through fixed-input UTC formatting (ChannelView.tsx:35-39); no relative-”ago” rendering exists insrc/components(grepped: zero matches forDate.now|ago|toLocale). - Async render swap (latent, must stay latent):
MarkdownText.tsx:303debounces (HIGHLIGHT_DEBOUNCE_MS = 150) then async-tokenizes fenced code through Shiki (:337-341), swapping a plain<pre>for highlighted HTML after capture time —animations: "disabled"does not touch it. Today it never triggers: no fixture message carries a code fence (grep: zero fenced blocks /language-inputs instub-data.ts/comms-stub.ts/session-events-stub.ts). Invariant, recorded next to the fixtures: a fixture message must not carry fenced code, or T4’s capture must settle past the swap. - Query microtask (harness-side wait):
assignedIssuessettles on a microtask, not synchronously (store.test.ts:1757-1758pins the sync read as[]pre-tick); it feeds the “Assigned to me” section ofBacklogView.tsx:111and the left-rail count (LeftSidebar.tsx:427-428). The CDP screenshot round-trip almost always outlasts a microtask, but “almost” is below the byte-stable bar — so T4 waits on a row inside the “Assigned to me” section (#backlog-section-assigned-to-me .backlog-row), never a bare.backlog-rowand never the.backlog-viewcontainer. A bare.backlog-rowis unsound:BacklogViewrenders three sections, and Todo/Backlog filterstore.issues()synchronously (BacklogView.tsx:93-94) — their rows exist before theassignedIssuesquery settles, so a bare-row wait resolves early and re-opens the exact race it is meant to close. Only the “Assigned to me” section (BacklogView.tsx:109-113, fedstore.assignedIssues()) gates on the microtask; the fixture tracker seam returns non-emptySTUB_ASSIGNED_ISSUESfor the default handle (tracker.ts:99,DEFAULT_TRACKER_CONFIG.handle = "matt@sealed"), so that scoped row is guaranteed to appear once the query resolves. - Residual risk is CSS animation phase (e.g. a pulsing state dot mid-keyframe
at capture) and raster-level drift (subpixel AA, late web-font swap).
Neutralize in the harness, not the app: Playwright’s
screenshot({ animations: "disabled", scale: "css" })(+reducedMotion: "reduce"anddeviceScaleFactor: 1inuse, and anawait page.evaluate(() => document.fonts.ready)before each capture) — the app ships untouched and the rigel-motion tokens keep working for humans. These are the standard visual-testing determinism knobs (D5, D7); they make the same-box byte-identity self-test robust and are the substrate a future automated diff-gate needs.
A4 — What this is not
Section titled “A4 — What this is not”Not a demo-hosting build (the PROD tripwire deliberately blocks
vite build --mode fixture; lifting that is a future record), not a
replacement for the Go dogfood-e2e tier, not a change to .env* handling, and
not a redesign of the stub fixtures themselves.
T1 — Extract mountShell and add the fixture dispatch arm
Section titled “T1 — Extract mountShell and add the fixture dispatch arm”Extract the render block (index.tsx:133-144) into mountShell(root, store, queryClient); add the statically-gated fixture branch ahead of the existing
bootConnectionForMode chain, dynamic-importing the (new, T2) boot module.
Live behavior is byte-identical: main() calls mountShell with the same
arguments in the same order.
Interfaces:
- Produces
apps/ui/src/mount.tsx:export function mountShell(root: HTMLElement, store: AppStore, queryClient: QueryClient): void(theStoreContext.Provider+QueryClientProvider+HashRoutertree, moved verbatim; namedmountShellto avoid colliding with the test-onlymountAppattest-router.tsx:37). Also exportsexport function newAppQueryClient(): QueryClientreturningnew QueryClient({ defaultOptions: { queries: { retry: 1, staleTime: 30_000 } } })— the single source for the app’s query defaults, so the fixture boot (T2) cannot silently drift from the live client atindex.tsx:97-99. - Modifies
apps/ui/src/index.tsx: the dispatch gainsif (import.meta.env.MODE === "fixture") { void import("./boot-fixture").then((m) => m.bootFixture(root)).catch((error) => renderBootError(root, "Compass UI cannot start", …)); } else { …existing chain… }— the.catchroutes a fixture chunk-load failure to the same painter the live chain uses (index.tsx:57), so a rejected dynamic import never leaves a blank#root;main()body replaces its inlinenew QueryClient({…})(index.tsx:97-99) withnewAppQueryClient()and itsrender(...)tail withmountShell(root, store, queryClient). - Consumes:
AppStore(store.ts),QueryClient(@tanstack/solid-query),App/AppRoutes/StoreContext(moved imports).
Test cycle: existing UI suite green (moon run compass-ui:test or repo
equivalent); bunx vite (no mode) + manual load against devenv still boots
live (smoke); type-check clean.
T2 — boot-fixture.ts: the offline store boot + PROD tripwire
Section titled “T2 — boot-fixture.ts: the offline store boot + PROD tripwire”The fixture boot module: tripwire first, then the clientless store, then
mountShell. Contains the build-scan sentinel literal.
Interfaces:
- Produces
apps/ui/src/boot-fixture.ts:export function bootFixture(root: HTMLElement): void—if (import.meta.env.PROD) throw new Error(FIXTURE_SENTINEL + " must never boot in a production build");const queryClient = newAppQueryClient()(T1 — shared with the live path so query defaults cannot drift);const store = createRoot(() => createAppStore({ queryClient, initialComms: STUB_COMMS_STATE, workspaceKey: "fixture" }));mountShell(root, store, queryClient). Alsoexport const FIXTURE_SENTINEL = "COMPASS-FIXTURE-BOOT-SENTINEL-7f3a"(unique, grep-proof). - Consumes:
createAppStore(store.ts:680),STUB_COMMS_STATE(comms-stub.ts:714),mountShell+newAppQueryClient(T1).
Test cycle: a bun unit test (boot-fixture.test.ts) asserting (a) the mounted
DOM renders ≥1 board card from STUB_ISSUES under a happy-dom root, (b)
bootFixture throws when import.meta.env.PROD is stubbed true. Smoke:
bunx vite --mode fixture → board renders the designed fleet with no daemon
running and no VITE_COMPASS_BASE_URL set.
T3 — Hard-wall build gate
Section titled “T3 — Hard-wall build gate”The structural guarantee, tested the way preview-build.test.ts already
tests bundle facts.
Interfaces:
- Produces
apps/ui/src/fixture-wall.test.ts: runsbunx vite build --outDir <tmp> --emptyOutDirwith a validVITE_COMPASS_BASE_URLin env (thepreview-build.test.ts:79-82recipe), reads alldist/assets/*.js, assertsFIXTURE_SENTINELis absent AND — the positive control that keeps the scan from passing vacuously — that a known live-app literal ("missing #root element",index.tsx:20) is present, so a scan that read nothing (assets relocated out ofdist/assetsby a futurevite.configchange) fails loudly instead of green. - Consumes:
FIXTURE_SENTINEL(T2 — imported for the literal, or duplicated as a string constant with a comment pinning the pairing; prefer importing fromboot-fixture.tsso drift is impossible).
Test cycle: the gate itself red→green: first run it against a deliberately un-gated static import (red), then with the T1 dynamic-import gate (green).
T4 — Point the visual-smoke harness at fixture mode
Section titled “T4 — Point the visual-smoke harness at fixture mode”Interfaces:
- Modifies
apps/ui/playwright.config.ts:webServer.commandbecomes"bunx vite --port 5173 --strictPort --mode fixture";usegainsreducedMotion: "reduce"anddeviceScaleFactor: 1(pins DPR so subpixel anti-aliasing can’t drift between runs — the standard visual-testing knob). - Modifies
apps/ui/e2e/visual-smoke.spec.ts: everypage.screenshot(...)/locator.screenshot(...)gainsanimations: "disabled"andscale: "css"(DPR-independent raster); each capture is preceded byawait page.evaluate(() => document.fonts.ready)so a late web-font swap cannot land between the two runs. Header comment updated (the “boots fully on stub-data.ts” claim becomes true again, now guaranteed by fixture mode rather than by accident of un-wired live paths). - Modifies the backlog capture in
apps/ui/e2e/visual-smoke.spec.tsto wait on a row inside the “Assigned to me” section (#backlog-section-assigned-to-me .backlog-row) rather than a bare.backlog-rowor the.backlog-viewcontainer, so it cannot fire before theassignedIssuesquery microtask settles (§A3 — Todo/Backlog rows render synchronously, so a bare-row wait would resolve early). The other surfaces already wait on content (.state-dot,.bridge). - Consumes: T1+T2 landed.
Test cycle: run the harness twice back-to-back on the same box with no daemon
on :50051; assert (via cmp) the board PNGs are byte-identical across the two
runs and show issue cards. This byte-identity bar is a same-binary,
same-box determinism self-test — proof the harness emits a stable artifact
(no residual microtask/font/animation/DPR nondeterminism), not a
cross-environment regression oracle (Chromium raster differs across a
nix-channel or headless-shell bump; §A3, D7). It is the forcing function
that this boot path is deterministic: bridge.png now depicts the designed
board, reproducibly. Making the harness a real automated visual-regression
gate (toHaveScreenshot + a maxDiffPixelRatio threshold + baselines
committed to git + generated in a pinned CI environment) is Matt’s stated
direction and lands as its own follow-up record — this record is the
deterministic substrate that gate requires, which is why the determinism
knobs above are specified now.
T5 (deferred fast-follow — filed as its own issue) — scripted event replay
Section titled “T5 (deferred fast-follow — filed as its own issue) — scripted event replay”Extend createFakeCompass per §A2’s shape and add a fixture-boot variant that
passes the fake’s client as options.compass, for live-update / state-change
shots. Matt confirmed (D2 below) the split: static v1 ships now to unblock the
Bridge harness; replay is a named fast-follow, built when a concrete
state-change shot is specified (which is also when it can be scripted
meaningfully). Explicitly out of v1 scope; the seam is named here and the
work is tracked as its own issue, not built now.
- T1 — extract
mountShell(name avoids thetest-router.tsx:37mountAppcollision), add the inlineMODE === "fixture"dispatch arm (dynamic import +.catch→renderBootError); existing suite green, live boot unchanged. - T2 —
boot-fixture.ts(tripwire + clientless store + sentinel) with unit tests for render-from-fixtures and the PROD throw. - T3 —
fixture-wall.test.tsbuild gate:FIXTURE_SENTINELabsent from a productiondist/, plus the"missing #root element"positive control so the scan can’t pass vacuously. - T4 — harness:
--mode fixturewebServer,animations: "disabled"+scale: "css"per capture,reducedMotion: "reduce"+deviceScaleFactor: 1inuse,document.fonts.readywait per capture, backlog waits on a row inside the “Assigned to me” section (not the container), regeneratee2e/__screens__baselines; verify same-box determinism across two runs (D7 — the automated diff-gate is a follow-up record, this is its deterministic substrate). - T5 — (deferred fast-follow, own issue)
createFakeCompassscripted-snapshot extension for live-update / state-change shots.
Decisions (Matt-ruled)
Section titled “Decisions (Matt-ruled)”All forks below were surfaced to Matt and ruled before freeze (2026-08-16).
- D1 — Mode-selection mechanism / the hard wall. Vite
--mode fixturegating a statically-replacedimport.meta.env.MODEbranch with a dynamic import (§A1 option 3), plus the PROD tripwire and the build-scan gate.--modeis CLI-only (not env-settable, unlike anyVITE_*flag), the branch is dead-code-eliminated so the fixture chunk is never emitted, and it composes with the existingindex.tsx:46-49dispatch without a second HTML entry. Alternatives weighed and rejected in §A1. - D2 — Static snapshot vs scripted event replay. Static snapshot only in
v1 (the clientless store — zero new client code, maximal determinism) ships
now to unblock the Bridge harness; scripted replay (the
compass-fake.tsextension, §A2 / T5) is a named fast-follow filed as its own issue, built when a concrete state-change shot is specified. All current harness surfaces (board, sidebar, agent, backlog, done, settings —visual-smoke.spec.ts:14-63) read a settled snapshot, so nothing in the first cut needs replay. - D3 — Merged-PR fixture for the Bridge T6/G11/F6 premise. Already
satisfied, no scope here:
STUB_ISSUEScontains done issues whose PRs carryforgeState: "merged"(stub-data.ts:1143—forgeState: "merged"on PR #436 under the issue at:1131-1135, and again at:1186), and the clientless store’sissues()is all ofSTUB_ISSUESincluding those. This record does not touchSTUB_ISSUES; any further shaping (e.g. a merged PR on an in-progress issue) belongs to Bridge T6, which owns that premise. - D4 — Comms in fixture mode.
initialComms: STUB_COMMS_STATEand no comms client — the rail/channel surfaces render populated (the exact construction every component test uses, e.g.test-router.tsx:43-46) with zero stream machinery;EMPTY_COMMS_STATEwould leave the left rail and channel views empty in screenshots for no gain. Writes reject throughonCommsError(absent → undefined → silently dropped), which a screenshot-only harness never exercises. - D5 — Where to neutralize animations. Harness-side only
(
animations: "disabled"per screenshot +reducedMotion: "reduce"): the app ships no fixture-conditional styling, and rigel-motion stays intact for humans exploringvite --mode fixtureinteractively. (Matt noted a future interest in animation tests — that is its own future scope, not this record.) - D6 — Should
vite build --mode fixturebe allowed (a hosted demo)? No, blocked by the PROD tripwire for now — a hosted-demo bundle is a different artifact with its own record; leaving the tripwire out would make the wall one accidental CI flag away from shipping fixture code. - D7 — Byte-stability boundary, and whether the harness is a gate. The
harness stays a human before/after review tool in this record and T4’s
byte-identity bar is a same-binary, same-box determinism self-test (proof
the boot path emits a stable artifact — no residual microtask/font/animation/
DPR nondeterminism), captured on the box-local pinned Chromium
(
playwright.config.ts:29-31), never a cross-environment oracle (Chromium raster differs across a nix-channel or headless-shell bump). Matt’s stated direction is to make the harness a real automated visual-regression gate (toHaveScreenshot+ amaxDiffPixelRatiothreshold + baselines committed to git + generated in a pinned CI environment) — that lands as its own follow-up design record, for which this record is the required deterministic substrate. That is why the determinism knobs (D5, T4) are specified now even though the automated gate is out of this record’s scope.
Ledger assessment
Section titled “Ledger assessment”Ledger-impact: none. This is a dev-infra/testing boot-path addition: it
supersedes no frozen product decision (the board model DL-069/DL-071, the
live-wiring records SEA-1729, and the test-strategy record’s Go e2e tier are
all untouched — the fixture mode is complementary UI-layer determinism, and
the live boot path is behavior-identical when the mode is off). No
DECISIONS.md delta owed; the driver handles ledger/PR mechanics either way.