From d9395e3985364d96b5f9f67919c88618f800288f Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Wed, 7 Oct 2026 00:30:43 -0700 Subject: [PATCH] =?UTF-8?q?frames:=20C2=20=E2=80=94=20live=20wire=20tier?= =?UTF-8?q?=20(lean):=20connection,=20have-list=20ledger,=20resume=20in=20?= =?UTF-8?q?a=20lazy=20chunk;=20`live`=20coverage=20closure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The live wire moves out of the eager frames client into the sixth lazy tier, `@solidjs/web/frames/wire` (frames/src/wire-tier.ts → wire.js, 1,922 min / 934 br): `connect` — the per-address connection a live() loop's response runs under (the open-frame count its end is judged by: a death with frames open vs a completion; the join of a second loop onto an address already live; `connection.ended` / `cancel`; the SSE reader selection, proxied so a drained body applies nothing further), `cancel` (supersession from `bump`), `have` / `haveOf` (the have-list ledger per mount, RFC 11 §9.5, fed by the client's `#recordHave` at its three sites) with `encodeHaveList` and the FRAME_HAVE_* literals bundled, and `resume` (the ordinal + the have-list header). The eager client keeps: `applyFrames` reading `options[LIVE_WIRE]` as its reader; `handle`'s live arm awaiting `prepareTier("wire")` before the body is read; `onLive` → `prepareTier("wire")`; `resume` and `bump`'s cancel delegating through `tierLoads.wire?.r`. Preload-at-call: `live()`'s makeIterable fires the new optional `responseHandler.onLive()` before the first fetch, so the import races only the request; a non-live call never loads the tier. Accepted degraded case: content applied before the chunk was resident kept no ledger, and `resume` answers nothing while the tier is absent — that first connect sends neither X-Frame-Have nor Last-Event-ID and gets a full snapshot (the server never read the ordinal for a frame render); pinned. Public surface: `Frame.have?()` removed from the Frame interface (the ledger is the tier's `haveOf(frame)`); `encodeHaveList` moved from frame-transport to the tier (@internal); `@solidjs/web/frames/wire` export path added; `responseHandler.onLive?()` and `ServerComponentHandler.onLive()` added. `ServerFunctionsClientConfig` gains no key. Pins: consistency/tier-wire-preload.spec (3) — the hook starts the import at the call before any fetch, the arm awaits residency, X-Frame-Tiers: wire joins the same load, a non-live call never loads the tier, reconnect through the tier with Last-Event-ID + the have-list, the degraded first connect. frames-live-resume re-pointed to the tier's haveOf / encodeHaveList; frames-live / frames-live-showing warm the tier. The coverage closure: server-functions-live-loop.spec (19, client suite) drives live()'s loop over hand-framed chunk / SSE bodies — first-connect failure, the fail-fast 4xx set and 408 / 425 / 429 / Retry-After, the cursor as Last-Event-ID, the online wake, return() mid-connect / mid-backoff / mid-seed, invoke's signal mid-stream / mid-backoff, no global EventTarget, live(GET(fn)), the handler's local answers — `live` per-function branch coverage (audit §2.4 method, client + hydrate) 26/59 sites (91/129 paths) on the base → 57/60 (129/132). Size (scripts/size, this head vs the base 55eb4c346): frames eager 33,767 / 11,193 → 32,802 / 10,888 (−965 min / −305 br; cap 11.21 → 10.90 KB); page base 112,938 / 36,071 → 112,003 / 35,722 (−349 br; cap 36.09 → 35.74); page live 124,810 / 39,747 → 123,903 / 39,412 (−335 br eager; cap 39.76 → 39.43; the page loads wire.js at its first live() call — total transfer 40,345 br, +598 against the base); non-SC scenarios 0 / 0; no cap raised. Measured before writing on edited dist copies through the harness's bundler: ceiling −1,432 / −472, lean −965 / −310, the carrier ≈ +467 / +162. Suites: web client 130 / 1,218, hydrate + consistency 89 / 452, server 159 / 1,511, solid 42 / 833, types + test-types clean; harness 500 × {3289, 91501} SC arm 0, generic arm (CONSISTENCY_IGNORE=C1,C9,C19,E) 0. Docs: savings plan §1 wire row built, §3 row C2 landed, §4 chunk table; residue doc status + §7e. Co-authored-by: Cursor --- .changeset/frames-live-wire-tier.md | 11 + documentation/plans/frames-residue-pass.md | 136 +++++- documentation/plans/frames-savings-pass.md | 80 +-- packages/web/frames/src/client.ts | 13 + packages/web/frames/src/frame-client.ts | 44 +- packages/web/frames/src/frame-transport.ts | 189 +++----- packages/web/frames/src/wire-tier.ts | 341 +++++++++++++ packages/web/package.json | 4 + packages/web/rollup.config.js | 41 +- packages/web/server-functions/src/client.ts | 11 + .../consistency/tier-wire-preload.spec.tsx | 337 +++++++++++++ packages/web/test/frames-live-resume.spec.tsx | 29 +- .../web/test/frames-live-showing.spec.tsx | 7 +- packages/web/test/frames-live.spec.tsx | 10 +- .../test/server-functions-live-loop.spec.tsx | 459 ++++++++++++++++++ packages/web/tsconfig.build.json | 3 +- packages/web/vite.config.hydrate.mjs | 1 + packages/web/vite.config.mjs | 3 +- scripts/size/floor-caps.json | 8 +- scripts/size/scenarios.js | 39 +- 20 files changed, 1533 insertions(+), 233 deletions(-) create mode 100644 .changeset/frames-live-wire-tier.md create mode 100644 packages/web/frames/src/wire-tier.ts create mode 100644 packages/web/test/consistency/tier-wire-preload.spec.tsx create mode 100644 packages/web/test/server-functions-live-loop.spec.tsx diff --git a/.changeset/frames-live-wire-tier.md b/.changeset/frames-live-wire-tier.md new file mode 100644 index 000000000..bd2742bba --- /dev/null +++ b/.changeset/frames-live-wire-tier.md @@ -0,0 +1,11 @@ +--- +"@solidjs/web": patch +--- + +frames: the live wire moves into a lazy tier (`@solidjs/web/frames/wire`, the sixth chunk). The connection a `live()` loop's response runs under — the open-frame count its end is judged by (a death with frames open vs a completion), the join of a second loop onto an address already live, the supersession cancel, `connection.ended` / `cancel` — the have-list ledger per mount with its encoder, and the resume shape (`Last-Event-ID` + `X-Frame-Have`) load with the chunk. The eager client keeps the arm that awaits the tier's residency before a live response's body is read, so a live connection without the chunk cannot happen; the chunk's import starts at the `live()` call, before its first fetch (preload-at-call, through the new optional `responseHandler.onLive()` hook the sf client fires), and a non-live call never loads it. One accepted degraded case: content applied before the chunk was resident kept no ledger, so that first connect sends no have-list (and no ordinal) and receives a full snapshot. + +Public surface: `Frame.have?()` is removed from the `Frame` interface (the ledger is the tier's `haveOf(frame)`, `@internal`); `encodeHaveList` moves from `frame-transport` into the tier (`@internal`; `decodeHaveList` and the `FRAME_HAVE_*` constants stay); the `@solidjs/web/frames/wire` export path is added; `ServerFunctionsClientConfig.responseHandler` gains the optional `onLive()`; `ServerComponentHandler` gains `onLive()`. + +Pins: `consistency/tier-wire-preload.spec` and `server-functions-live-loop.spec` — the latter drives `live()`'s reconnect loop from the consumer's side (first-connect failure, the fail-fast 4xx set and 408 / 425 / 429 / `Retry-After`, the cursor as `Last-Event-ID`, the `online` wake, `return()` and a caller's signal at every phase, `live(GET(fn))`, the handler's local answers), closing the loop's branch-coverage gap. + +Size: frames eager −965 min / −305 br (10.89 KB); pages −349 / −335 br eager; the `wire.js` chunk 1,922 / 934 (lazy; the live page loads it at its first `live()` call). Caps ratcheted to measured + 10 B. diff --git a/documentation/plans/frames-residue-pass.md b/documentation/plans/frames-residue-pass.md index f799be857..5e77aa57a 100644 --- a/documentation/plans/frames-residue-pass.md +++ b/documentation/plans/frames-residue-pass.md @@ -64,7 +64,16 @@ by the key's `data` chunk and rejected when the response ends; frames keeps the `record.pending` count — **frames eager 11,367 → 11,193 br (−505 min / −174 br)**, the decode chunk (lazy) +420 / +161; the carrier measured +374 min / +110 br over a −879 / −280 ceiling, against the row's -+40 br estimate — see §3.2 "As built" and §7d._ ++40 br estimate — see §3.2 "As built" and §7d. +**C2 landed** (`feat/frames-wire-tier`, on step 4's head, 2026-10-07): the +live wire tier in the `lean` form — the connection, the have-list ledger and +its encoder, the resume shape in a sixth lazy chunk (`wire.js` 1,922 / 934), +the eager client keeping the await-residency arm and the +`responseHandler.onLive` preload hook — **frames eager 11,193 → 10,888 br +(−965 min / −305 br)**, against §2's −292 (arithmetic from the C2 pass's own +measurement); the carrier ≈ +467 / +162 over a −1,432 / −472 ceiling. The +coverage closure the plan tied to it: `live` 26/59 → 57/60 branch sites. See +§7e._ --- @@ -1007,6 +1016,131 @@ ended without delivering a {$ref}.`). --- +## 7e. Landed — C2 (2026-10-07): the live wire tier, `lean` + +Branch `feat/frames-wire-tier` on step 4's head (`55eb4c346`, frames eager +**33,767 / 11,193**; page base 112,938 / 36,071; live 124,810 / 39,747). +§2's C2 row (−292, arithmetic) and the savings plan's §3 row C2. The +maintainer's ruling: the `lean` form, with `FRAME_HAVE_*` already off the +entry (5a), the hook on `responseHandler.onLive` (not a config key), +`Frame.have?()` leaving the public `Frame` interface (approved class), the +trace update-site wait landed apart (#3861). Re-measured before writing on +edited dist copies of the frames client and the sf client through the +harness's bundler (`.wt-logs/c2b-edit.mjs`, `c2b-measure.mjs`) — the +step 1–4 base had moved under the phase-1 edits (the sync loop, +`insert`-placed fills, the table-owned pending read), so every anchor was +re-derived; the four non-SC scenarios 0 / 0 on every row. min / br. + +| item | measured on this head | note | +| --- | ---: | --- | +| the ceiling (`del`) — the wire out whole: `connections` / `hold` / the join-or-hold arm, `resume` + `encodeHaveList`, the ledger (`#have` / `have()` / `#recordHave`), `applyFrames`' connection wiring, `isEventStream` + the reader selection | **−1,432 / −472** (pages −1,404 / −483 and −1,399 / −464) | unsound alone: a live response read as a plain stream, no reconnect | +| `lean` — the tier owns the connection, the ledger and the resume; the client keeps the await-residency arm, `onLive`, `resume` delegating, `#recordHave` forwarding through `tierLoads.wire?.r` | **−965 / −310** (pages −979 / −285 and −946 / −300) | the carrier ≈ +467 / +162 over the ceiling | +| the hook alone (`onLive` fired by `live()`'s `makeIterable`) | ≈ 20 / 15 (page live +85 br) | the config-key form measured ≈ 45 br heavier on the pages | +| the ledger's residue (`#recordHave` ×3 forwarding) | ≈ 112 / 27 | kept: the digests are known only to the client's apply | +| the field residue (the record fields the ledger reads) | ≈ 106 / 14 | kept: they feed the apply too | +| **built** | **−965 / −305 → 32,802 / 10,888**; pages **−349 / −335** → 35,722 and 39,412; **`wire.js` 1,922 / 934** (lazy, reported not counted) | +0 min / +5 br over the copy | + +**What the tier owns** (`frames/src/wire-tier.ts` → `@solidjs/web/frames/wire`, +`@internal`): `connect(response, wire, address, binding, host, bump, begin)` +— a live response's connection: the join of a second loop onto an address +whose connection is not done (one wire per address; the second loop reads +the same store), else `bump` / `begin`; the reader (`wire.open(body)` for +an event stream — the loop's `EventStreamReader`, which records each event's +`id:` as the position — else a `ChunkReader`), proxied so a drained body +applies nothing further; the open-frame count the body's end is judged by +(`start` opens, `complete` / a keyless `error` closes; a nested region's +chunks ride inside its parent's) — an end with frames open is a death the +loop reconnects from, with none a completion; `connection.ended` / `cancel` +(supersession by a later response for the address cancels the reader, +which ends the drain as a clean body end); `have(frame, key, record)` / +`haveOf(frame)` — the have-list ledger per mount (RFC 11 §9.5): the root +apply resets it to the root digest plus the holes' (`""` → `{ "": digest, +…holes }`), a hole / attr / reveal apply sets its key; `encodeHaveList` +(the `FRAME_HAVE_HEADER` / `FRAME_HAVE_BUDGET` literals bundled — they were +already off the entry); `resume(host, info, versions)` → `{ position: +String(version), headers: { [FRAME_HAVE_HEADER]: encoded } }` for an +address the host shows, else `undefined`; `cancel(host, address)`. +**What the client kept:** `applyFrames` takes `options[LIVE_WIRE]` as its +reader (one line); `handle`'s live arm — `prepareTier("wire").then(() => +tierLoads.wire.r.connect(…))` — awaits residency, never reads the body +without the tier; `onLive() { prepareTier("wire") }`; `resume(info)` +delegating; `bump` ending with `tierLoads.wire?.r?.cancel(host, address)`; +`#recordHave(key, record)` forwarding to `tierLoads.wire?.r?.have(this, +key, record)` at its three sites. **The hook:** `live()`'s `makeIterable` +fires `handler.onLive()` before the first fetch, per call — the import +races only the request, and the arm awaits it, so a live connection +without the chunk is impossible by construction; a non-live call never +loads it. + +**The degraded case, accepted.** Content applied before the chunk was +resident (`#recordHave` forwarded to nothing) kept no ledger, and `resume` +— the tier's — answers nothing while the tier is absent: that first +connect sends neither `X-Frame-Have` nor `Last-Event-ID` and gets a full +snapshot. The plan's row described it as "the ordinal with no have-list"; +the `lean` form drops the ordinal too, and the server never read it for a +frame render (`liveSource` compares `Last-Event-ID` to a value digest a +frame answer does not have; only `decodeHaveList(X-Frame-Have)` is read in +the frame sink) — the same full snapshot either way. Pinned as accepted in +`tier-wire-preload.spec` (3): the plain call's content lands with its +digests while the tier is gated, the live call's first connect carries +neither header, the tier's release lands the live root, the reconnect +after a death carries `Last-Event-ID` and the live root's ledger. + +**Pins.** `consistency/tier-wire-preload.spec` (3) — the hook starts the +import synchronously at the call, before any fetch; the arm awaits +residency (chunks sent, nothing applied, the fallback shown, no status) and +the release lands the content; `X-Frame-Tiers: wire` on a live response +joins the same load (the loader called once); a non-live call never loads +the tier and its refetch bumps nothing of the tier's; reconnect through the +tier with `Last-Event-ID` and the have-list, the morph in place, the +completion closing; the degraded first connect. `frames-live-resume` +re-pointed to the tier's `haveOf` / `encodeHaveList`; `frames-live` and +`frames-live-showing` warm the tier (`beforeAll(() => prepareTier("wire"))` +— resident cells). **The coverage closure** the savings plan tied to this +row: `server-functions-live-loop.spec` (19, client suite) drives `live()`'s +loop over hand-framed bodies — the codec's chunk stream and the same +payloads as server-sent events with `id:` lines — ending as the test says +(cleanly, dropping, hanging until the request's signal): the +non-server-function throw, a handler without `intercept`, a first-connect +failure as the call's rejection (no status, no retry), a death reconnecting +on the `online` wake with the reader's cursor as `Last-Event-ID`, the +fail-fast 4xx closing with the error, 408 / 425 / 429 retrying, `Retry-After` +naming the wait, `return()` mid-connect (the arriving stream closed, the +wire severed) / mid-backoff (the sleep cut) / mid-seed (a late local answer +not yielded), `invoke`'s signal mid-stream (a rejection, ended for good) / +mid-backoff (the sleep woken, the connect refused), a host without a global +`EventTarget`, `live(GET(fn))` at the live address over GET, the handler's +local answers (a hit at the call yielded first and adopted; a deferred hit +awaited, settling to a value or to nothing; a miss at the call but a hit at +connect — one value, no wire). `live`'s per-function branch coverage by the +audit's §2.4 method (v8, client + hydrate suites, merged): **26/59 sites +fully covered (91/129 paths) on the base → 57/60 (129/132)**; the three +left are `return()` landing between a value's arrival and its read, and +the `resume` / seed arms' stopped-while-pending elses. Web client 130 / +1,218; hydrate + consistency 89 / 452; server 159 / 1,511; solid 42 / 833; +`types` + `test-types` clean; harness 500 × {3289, 91501}: SC arm 0, +generic arm (`CONSISTENCY_IGNORE=C1,C9,C19,E`) 0. + +**Public surface.** **Removed:** `Frame.have?(): Record | +undefined` from the public `Frame` interface (the ledger is the tier's +`haveOf(frame)`; approved class). **Moved:** `encodeHaveList` from +`frame-transport` to the tier (`@internal`); `decodeHaveList` and the +`FRAME_HAVE_*` constants stay in `frame-transport` (off the client entry +since step 1). **Added:** the `@solidjs/web/frames/wire` export path (the +chunk; its exports `@internal`); `responseHandler.onLive?()` on the sf +client's `ServerFunctionsClientConfig.responseHandler` (optional; fired at +a `live()` call before its first fetch); `ServerComponentHandler.onLive()`. +`ServerFunctionsClientConfig` itself gains no key. + +**Running number: frames eager 10,888 br — 888 B above ≤ 10.0 KB.** §2's +C2 row said −292; −305 landed. The live page's total transfer is honest: +39,412 eager + 934 for the chunk it loads at its first `live()` call = +40,345 br against the base's 39,747 (+598 — the chunk carries the whole +connection, where the row's `est.` ≈ 470 br priced the dispatch alone). +What remains is C1 (≈ −440) and the claims chunk (≈ −270). + +--- + ## Appendix A — every unit on the head (273 units, 40,000 B; by whole unit) Class and group per `classes-head.mjs`; units Phase A / B / C added are diff --git a/documentation/plans/frames-savings-pass.md b/documentation/plans/frames-savings-pass.md index f72505458..6e8d8f308 100644 --- a/documentation/plans/frames-savings-pass.md +++ b/documentation/plans/frames-savings-pass.md @@ -96,13 +96,13 @@ a not-yet-completed `_hk` element, and a server component's interior has none (it renders under NoHydration) — the window an element needs is its own stamp. -| tier | the race (what arrives before what) | symptom if lost | bound | class | pin | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **holes** (E.a1) | a `hole` / `attr` chunk — or an op on the document's `sc:live` stream — arrives before `tier-holes.js` has loaded. Always after the frame's first flush (holes are later yields; the markup that carries the `lh:` markers came first). | the frame freezes at its first yield (wrong content, silent). | **buffer, no hold.** Stream face: `chunkToRecords` writes `hole:` / `attr:` into the store regardless; `#flush`'s hole pass runs only when the tier is resident, and records it cannot apply **stay pending** (store-model: any later flush retries). The tier's load triggers one `flush()` on every registered frame. Document face: the `sc:live` `ReadableStream` buffers until `pumpLiveChannel` runs (after the load); the op log replays to adopters. The announcement (§2) starts the fetch at the header / at t = 0, so in practice the chunk is resident before the first later yield. | runtime wait | **existing:** C13 control arm (one hole alone applies), C18's pump-catch-up arm (ops before the adopter replay). **New:** `tier-holes-buffer.spec` — (a) stream: a `hole` chunk delivered before the tier resolves applies after it, nothing applied twice; (b) document: ops written to `sc:live` before the pump starts land after it; (c) a frame disposed during the wait applies nothing. The 3.1 question does not arise (post-first-flush). | -| **live wire** (E.a2) | a `live()` loop's first response (SSE framing, `LIVE_WIRE` in the call context) reaches `handle` before `tier-wire.js` has loaded. | the response is read as a plain frame stream: no join/hold, no reconnect with a have-list; a reconnect lands as a full snapshot (today's post-adoption connect already does). Degraded, not wrong. | **preload-at-call.** `live(fn)`'s client decorator fires a hook the frames client installs (`configureServerFunctionsClient({ onLive })` — frames already imports `configureServerFunctionsClient`, so the dependency direction holds) that calls `prepareTier("wire")` **before** the first `fetch`; `handle`'s `LIVE_WIRE` arm awaits the same promise before reading the body, so a live connection without the chunk is impossible by construction. No buffer, no hold. | runtime wait (none in practice — the import and the request race only against each other, and the dispatch awaits the import) | **existing:** the live suite (`server-functions-live-*`, `frames-live-*`; `live` is 22/61 branches covered — the audit's hot spot, §6.4). **New:** `tier-wire-preload.spec` — the first live response is held until the tier resolves; a response that completes before the tier never reaches the plain reader; `resume` sends the have-list on the first reconnect. | -| **traces** (S1 re-based) | an adopt-time record whose literal args carry a `{ $tr, $ta }` marker — or a `data` chunk whose node tree holds the trace plugin's node — before `container-trace.js` (the materializer + the store engine) has loaded. | the fill runs with an inert marker object (`TypeError` or wrong content); on the codec face the chunk decodes a marker instead of a container. | **announce + hold.** Document: the server knows it serialized a trace (the serializer stamps it) → `modulepreload` at t = 0 + the `sc:tiers` record; the occurrence is **held** (S1's `#argsUnprepared` → the general held set) with its server interior on screen, the mount being the hydration attach; it mounts with the record it was held on (S1 commit 3). Codec: `prepareData` scans the node tree and awaits the tier before the chunk decodes (S1 as built; the header flag makes the scan a confirmation). The claim reads the snapshot; the backlog lands after done (3.6 (iii), S1 commit 3). | **3.1 participant** (document face: the hold is an adopt-time occurrence deferred — registers through `initBoundaryResume`, A2) | **existing (S1):** `frames-container-lazy-codec` (loads before decode / loads nothing), `frames-container-lazy-document` (held with the interior on screen, then mounted), `hydration/welcome-status-lazy` (claims in place, no key misses), `container-trace-hold-{id-determinism, interruption, record-retention, snapshot}`. **Re-pin:** `container-trace-hold-hydration-end` under 3.1 — _hydration waits for the load; the mount claims before done_ (rulings 3.1 "Consequences"). | -| **regions** | a `slot:` record naming a `{$frame}` ref — or a `data-fid` region element inside adopted content — before `tier-regions.js` has loaded. | the fill receives the raw `{$frame}` ref (wrong content); an occluded region cannot mount from the store. | **announce + hold.** The sink knows at render it passed server content as a prop (`{$frame}` minted) → announced. Adopt path: the occurrence is held in the same set as traces (its interior stays on screen; `#resolveArgs` runs after the load). Stream path: the record stays pending in the store until the load's flush (buffer). | **3.1 participant** on the adopt path; runtime wait on the stream path | **existing:** `frames-regions-*`, the lifecycle matrix's region rows. **New:** `tier-regions-hold.spec` — adopt-time `{$frame}` occurrence held, interior intact, mounts after the load with the held record; a stream record with a region ref applies after the load; hydration-done waits (3.1). | -| **assets** | a `reveal` for a segment whose `seg::assets` record names stylesheets (or `waitForStyles`) before `tier-assets.js` has loaded. Stream face only — the document face keeps the core's `$dfs`. | **without a bound: FOUC** — the segment reveals unstyled, then the sheet lands. With the bound: the fallback stays on screen longer. | **announce + reveal-readiness term.** The sink knows at render that a fragment is style-gated → announced in the header; `#segmentReady` gains one term: "the segment's `assets` record names styles **and** the tier is not resident → not ready". The fallback — the server's `` outcome — stays; the reveal happens at max(tier load, stylesheet load), and the stylesheet's own load dominates on every network. A segment without stylesheets never waits. Modules / preloads are not reveal-gating today and stay so. **Built (2026-10-06, C5, and its follow-up the same day):** the term is `(assets.styles | | assets.inlineStyles) && !tierReady("assets")?.gate(assets.styles | | [], this)`in`#segmentReady`— a segment whose record carries stylesheets **or inline styles** holds while the tier is absent (the inline`