From 6b7213d64cd33c172327abe5f2a2735354bbb73b Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Tue, 6 Oct 2026 19:24:16 -0700 Subject: [PATCH 1/4] =?UTF-8?q?frames:=20trace=20tier=20=E2=80=94=20update?= =?UTF-8?q?-site=20wait=20for=20a=20mounted=20occurrence's=20new=20`{=20$t?= =?UTF-8?q?r=20}`=20record?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `#syncSlots`' mounted branch guarded the update site against the regions tier only (`needsRegions`); the trace tier's wait covered the fresh mount alone. A MOUNTED occurrence's new record whose literal args carry a `{ $tr }` marker while the trace tier is absent — a live slot op minting the page's first trace after the shell, a refetch adding a projection arg — was pushed into the live binding raw, the marker read as the value. Now that record stays pending in the store: the live binding keeps the args it shows, `needsTrace` starts the load (nothing announced it), and the install's flush re-syncs with the tier in place and applies the record as an args change into the same mount with the marker materialized. C4's `regions` update-site wait, mirrored. Pin: consistency/tier-trace-hold.spec "update site" (document face: a live slot op mints the page's first trace post-shell with the tier absent → no raw read, args kept, one load, applied on the install's flush into the same `
  • `). Fails on the base at the first assertion (the loader never called). Measured (exact A/B of the built dist through scripts/size's bundler, base numbers reproduce the Phase 1 record byte-for-byte): frames: eager client consumer 40,012 min / 13,086 br +12 / +3 page: base server components 119,056 min / 37,747 br +12 / -25 page: live server components 130,927 min / 41,396 br +12 / -31 Non-SC scenarios 0 / 0. No cap changed. Web client suite 1187, hydrate suite 439 green; harness seeds 3289 / 91501 ×500: SC arm 0 findings, generic arm (IGNORE=C1,C9,C19,E) 0 findings. Co-authored-by: Cursor --- .changeset/frames-trace-update-site-wait.md | 5 ++ documentation/plans/frames-savings-pass.md | 2 +- packages/web/frames/src/frame-client.ts | 8 ++- .../test/consistency/tier-trace-hold.spec.tsx | 71 +++++++++++++++++++ 4 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 .changeset/frames-trace-update-site-wait.md diff --git a/.changeset/frames-trace-update-site-wait.md b/.changeset/frames-trace-update-site-wait.md new file mode 100644 index 000000000..144db25cb --- /dev/null +++ b/.changeset/frames-trace-update-site-wait.md @@ -0,0 +1,5 @@ +--- +"@solidjs/web": patch +--- + +frames: the trace tier's update-site wait. A MOUNTED occurrence's new slot record whose args carry a `{ $tr }` container-trace marker while the trace tier is absent (a live slot op minting the page's first trace after the shell, a refetch adding a projection arg) now stays pending in the store: the live binding keeps the args it shows, the tier load starts, and the install's flush applies the record as an args change with the marker materialized. Before, `#syncSlots` guarded only the fresh mount (`needsTrace`) and the regions tier at the update site (`needsRegions`), so such a record was pushed into the live binding raw — the marker read as the value. Mirrors the regions tier's update-site wait. diff --git a/documentation/plans/frames-savings-pass.md b/documentation/plans/frames-savings-pass.md index 6faf90633..52648a6ce 100644 --- a/documentation/plans/frames-savings-pass.md +++ b/documentation/plans/frames-savings-pass.md @@ -361,7 +361,7 @@ cap on the same terms; no frames / page cap moved. | **B** | **The tier mechanism** (§2): `sink.needs(tier)` at the five mint sites; `X-Frame-Tiers` at first flush; `sc:tiers` record + `modulepreload` links on the document face; `prepareTier(name)` + `installTier`; the **held set is A2's registered set** — a tier's adopt-path hold is one more reason an occurrence is deferred, so it registers under 3.1 from day one. No tier is cut yet — this step is the seam alone, measured. S1's `prepareData` / `prepareArgs` are not in the tree (S1 has not merged); the general seam is built directly and S1 re-bases onto it at C3. **Landed** (2026-10-06, `feat/frames-tier-mechanism`, measured before written — §2 "Landed"): frames eager **+473 min / +145 br** (≤ the +150 budget; est. ≈ +100, ×2.5 pre-estimate ≈ +250), pages +478 min, non-SC scenarios 0; frames server dist +793 min. `installServerComponents` gained `{ tiers }` (the client's loader map); the in-band form is `FrameChunk.tiers` on the next chunk out; the document face writes `sc:tiers` cumulative and a `modulepreload` only when `frameTransformDirectResult` is given `tierUrls` (decision 3 sub-item). Pins `tier-announce.spec` (14) / `tier-prepare.spec` (8); 5 of 150 artifacts re-recorded. | **≈ +100 / ≈ +100 / ≈ +100 / 0** gross (`est.`); server ≈ +300–450 min. S1's two faces (+543 min / +134 br) are never shipped — the ≈ −35 net the earlier draft credited here appears at C3 instead, as "S1 re-based costs less than S1 as built". **Measured: +145 / +177 / +158 / 0** br (+473 / +478 / +478 / 0 min); server +793 min. | none new | `tier-announce.spec`, `tier-prepare.spec` (new); artifacts re-recorded once. Size: frames eager ≤ 12.45. **Landed:** both specs green; 5 artifacts; frames eager 13,949 br (the 12.45 gate assumed Phase A savings that did not materialize — the cap is the maintainer's). | **the Phase A gate**; A2 (the holds register), A1 (the `landing` node is what an installed tier's `flush()` wakes) | **wire (additive):** `X-Frame-Tiers`, `_$HY.r["sc:tiers"]`, the links. Decision 3. **Landed:** also `installServerComponents(host?, { tiers })`, `frameTransformDirectResult`'s `tierUrls`, `FRAME_TIERS_HEADER` (server entry), `FrameChunk.tiers` (`TierAnnouncement`). | | **C1** | **Holes tier** (E.a1; cheapest, buffer-only). `tier-holes.js` = `#applyHole`, `#applyAttrs` (less its owned-position arms, which are bind's), `findLiveTarget`, the hole pass, `pumpLiveChannel` + the op log + `applyLiveOp`. The eager client keeps `chunkToRecords`' `hole` / `attr` cases (records must land in the store before the tier is resident) and a one-line dispatch in `#flush`. Under the **8.0 reading** this step is skipped and holes stay eager (§6 decision 1). | **−546 / −508 / −508 / 0** (measured: `T+holes` → `L8`; page `T+holes` → `L8`; live page the same cut) | `tier-holes.js` ≈ 1,900 min / **≈ 620 br** (`est.`: the 2,116-min cut as its own module + the install glue) | `tier-holes-buffer.spec` (new, §1); C13 control + C18 catch-up arms unchanged; `frames-live-holes-*`, `document-live-*` green through the tier. Size: frames eager ≤ 11.9. | B | none (the record shapes and `sc:live` are unchanged; the hole appliers were never exported) | | **C2** | **Live wire tier** (E.a2; preload-at-call). `tier-wire.js` = `connections` / `hold` / the join-or-hold arm of `handle`, `resume` + `encodeHaveList` / `FRAME_HAVE_*`, the have-list ledger (`#have` / `have()` / `#recordHave` and the record fields that feed it), `applyFrames`' connection wiring + `connection.cancel`, `isEventStream` + the SSE reader selection, `deserializeStream`'s live arm. The eager client keeps a one-line `LIVE_WIRE` dispatch in `handle` and `bump`'s cancel hook (a no-op without the tier). `live()`'s decorator fires the `onLive` hook (set by frames through `configureServerFunctionsClient`) that calls `prepareTier("wire")` before its first fetch; the arm awaits it. | **−433 / −355 / −355 / 0** (measured: `T+wire` → `L8`; the live page keeps the chunk lazy — its eager measurement drops the same bytes) | `tier-wire.js` ≈ 1,300 min / **≈ 470 br** (`est.`) | `tier-wire-preload.spec` (new); the live suite green; the audit's `live` branch gap (22/61) closed to ≥ 45/61 in the same PR (the tier's own tests). Size: frames eager ≤ 11.5; live page unchanged ±50 (the chunk is reported, not counted). | B; independent of C1 | `ServerFunctionsClientConfig.onLive` (new, internal hook on `configureServerFunctionsClient`); `FRAME_HAVE_HEADER` / `FRAME_HAVE_BUDGET` are exported constants today and move to the tier's module — **re-export from the eager entry** to keep the surface, or flag the move | -| **C3** | **Traces tier = S1 re-based** (§5; S1 merges **here**, not first). The materializer entry (`solid-js/internal/container-trace`) and `loadContainers` as S1 built them; S1's `prepareData` / `prepareArgs` / `#argsUnprepared` become B's `prepareTier("trace")` + A2's registered held set; the codec-face node scan stays as the un-announced fallback behind the header flag; the eager half of F.trace (`reviveContainerTraces` / `materialize` / `isContainerTraceMarker` / `isMaterializedContainer` / `setContainerTraceMaterializer` / `getFrameHost.revive`) moves into `container-trace.js`'s `installTier`, leaving a ≈ 150-min trigger. S1's commit 3 re-bases onto A2's park: the `claiming` hint (`revive(value, claiming?)`) and the held-record mount land here, the detached root and the backlog are already on `next`. **`container-trace-hold-hydration-end` re-pins under 3.1 at merge** (A2 is in). The +134 B frames exception S1 as built would have needed **never needs granting**: B's seam is already paid and the tier cut is a saving. **Landed (2026-10-06, `feat/frames-traces-tier`, measured before written on edited dist copies per re-attribution §7).** The chunk is `@solidjs/web/frames/trace` (frames/src/trace-tier.ts — a NEW `@solidjs/web` export path: the tier module imports solid's `solid-js/internal/container-trace` entry and the plugin's client half, and its `install()` sets the materializer on the plugin's shared state and the shared host's `revive`); `installTier` is B's `install()`, unchanged in shape. The eager client keeps the trigger alone: the loader entry, the container probe read off the plugin's registered state, the held-set predicate (`needsTrace`: the `{ $tr }` marker walk, run only while the tier is not resident — a resident tier means a decoded arg may be a live container whose traps throw), the `claiming` thread and the held-record mount (`#heldRecords`, S1 commit 3's, generalized to every adopt-path hold). S1's codec-face node scan (`prepareData(chunk)`) **dropped**: B's in-band `tiers` rides the very `data` chunk that carries the node, so the scan was unreachable behind it (keeping it measured +79 min / +25 br); the un-announced codec face (a producer predating the tier) decodes the inert marker — a skew one package never ships. The park is keyed on the claim again (3.6 "Landed" closed): `revive(value, claiming)` from the adopt-time mount; a fresh mount pays no beat. | **≈ −250 / ≈ −6,640 / ≈ −6,600 / 0** (S1's measured page savings −6,390 / −6,352 plus F.trace's eager half: 843 attributed, −289 measured as `T+trace` → `L8`, less the trigger ≈ −250; S1's +134 on frames does not recur — its two faces are B's seam. **Measured: −83 / −6,612 / −6,726 / 0** br (−484 / −24,069 / −24,161 / 0 min) against B's head; the deletion alone −239 / −8,016 / −8,031 br on an edited copy (F.trace's eager half −951 min; the engine, the materializer and the shared symbols out of the pages), the trigger +467 min / +156 br on frames (loader entry + probe +160 / +45, the held-set predicate +126 / +67, `claiming` +6 / −9, the held-record mount +175 / +53) — ≈ ×3 the ≈ 150-min estimate, which counted the loader alone; the predicate and the held-record mount were S1's and never budgeted. Page brotli lands ≈ 1.2 KB above the edited-copy figure because the real chunk pins its shared imports (store symbols, `withStoreHydration`'s adapters) in the eager graph.) | `container-trace.js` 24.3 KB / **7.86 KB br measured** (S1) + the eager half (≈ +700 min / +200 br → ≈ 8.1 KB br) **Measured: `trace.js` 25,409 min / 8,170 br** (page base; 8,158 on live) | S1's seven surviving pins green through the general seam (`frames-container-lazy-{codec,document}`, `hydration/welcome-status-lazy`, `container-trace-hold-{id-determinism, interruption, record-retention, snapshot}`); **re-pin** `container-trace-hold-hydration-end` (_hydration waits for the load; the mount claims before done_); **flip** S1's C3 (b); the `.fails` id-drift pin → 3.4 (3c). Size: frames eager ≤ 11.25; page base ≤ 36.0, live ≤ 39.8 (S1's caps 38.45 / 42.12 are superseded by these at landing). **Landed:** the seven pins green through the general seam (the hold re-armed per test by dropping the tier's load — `tierLoads`, the runtime's test seam); hydration-end re-pinned; C3 (b) was never `.fails` on `next` (the eager materializer made it pass) — it now asserts the hold and the 3.1 order; the id-drift pin stays `.fails` under 3.4 (unfixed on `next`); new `tier-trace-hold.spec` (§1's pin: un-announced hold + held-record mount, announced start-at-install, the codec wait). Harness 500 × 2 seeds: SC 0, generic (`C1,C9,C19,E` ignored) 0. Caps lowered (the ratchet): page base 44.89 → **38.61 KB**, live 48.60 → **42.16 KB**; frames eager's cap stays 13.79 (still 76 B over by Phase A's and B's bytes; its recorded minified lowered to 42,968, so the gate passes by the minified rule). | B, A2 (the hold registers; the park is on `next`), A3 | S1's: `revive(value, claiming?)`, `setContainerTraceMaterializer(…, claiming?)`, the entry, `withStoreHydration` / `applyPatches` / `forwardIteratorReturn` `@internal` on the main entry; **not shipped:** S1's `prepareData` / `prepareArgs` (replaced by `prepareTier` before they exist on `next`). `reviveContainerTraces` / `setContainerTraceMaterializer` move behind the tier — **flag**: re-export lazily-resolving wrappers or accept the move. **Landed:** the move accepted (both were `@internal` module exports, never on the public entry); new export path `@solidjs/web/frames/trace` (`install()`); `FrameHost.revive` documented as assignable after creation (the tier's install sets it); `solid-js` loses `materializeContainerTrace` from `solid-js/internal` and the main entry (the new subpath carries it); `tierLoads` exported from the internal runtime module for the specs. | +| **C3** | **Traces tier = S1 re-based** (§5; S1 merges **here**, not first). The materializer entry (`solid-js/internal/container-trace`) and `loadContainers` as S1 built them; S1's `prepareData` / `prepareArgs` / `#argsUnprepared` become B's `prepareTier("trace")` + A2's registered held set; the codec-face node scan stays as the un-announced fallback behind the header flag; the eager half of F.trace (`reviveContainerTraces` / `materialize` / `isContainerTraceMarker` / `isMaterializedContainer` / `setContainerTraceMaterializer` / `getFrameHost.revive`) moves into `container-trace.js`'s `installTier`, leaving a ≈ 150-min trigger. S1's commit 3 re-bases onto A2's park: the `claiming` hint (`revive(value, claiming?)`) and the held-record mount land here, the detached root and the backlog are already on `next`. **`container-trace-hold-hydration-end` re-pins under 3.1 at merge** (A2 is in). The +134 B frames exception S1 as built would have needed **never needs granting**: B's seam is already paid and the tier cut is a saving. **Landed (2026-10-06, `feat/frames-traces-tier`, measured before written on edited dist copies per re-attribution §7).** The chunk is `@solidjs/web/frames/trace` (frames/src/trace-tier.ts — a NEW `@solidjs/web` export path: the tier module imports solid's `solid-js/internal/container-trace` entry and the plugin's client half, and its `install()` sets the materializer on the plugin's shared state and the shared host's `revive`); `installTier` is B's `install()`, unchanged in shape. The eager client keeps the trigger alone: the loader entry, the container probe read off the plugin's registered state, the held-set predicate (`needsTrace`: the `{ $tr }` marker walk, run only while the tier is not resident — a resident tier means a decoded arg may be a live container whose traps throw), the `claiming` thread and the held-record mount (`#heldRecords`, S1 commit 3's, generalized to every adopt-path hold). S1's codec-face node scan (`prepareData(chunk)`) **dropped**: B's in-band `tiers` rides the very `data` chunk that carries the node, so the scan was unreachable behind it (keeping it measured +79 min / +25 br); the un-announced codec face (a producer predating the tier) decodes the inert marker — a skew one package never ships. The park is keyed on the claim again (3.6 "Landed" closed): `revive(value, claiming)` from the adopt-time mount; a fresh mount pays no beat. **Update-site wait landed (fix PR):** `#syncSlots` holds a MOUNTED occurrence's new `{ $tr }` record pending while the tier is absent (the live binding keeps its args, the load starts, the install's flush applies it) — the fresh-mount hold's twin, mirroring C4's `needsRegions` update-site wait; +12 min / +3 br frames eager. | **≈ −250 / ≈ −6,640 / ≈ −6,600 / 0** (S1's measured page savings −6,390 / −6,352 plus F.trace's eager half: 843 attributed, −289 measured as `T+trace` → `L8`, less the trigger ≈ −250; S1's +134 on frames does not recur — its two faces are B's seam. **Measured: −83 / −6,612 / −6,726 / 0** br (−484 / −24,069 / −24,161 / 0 min) against B's head; the deletion alone −239 / −8,016 / −8,031 br on an edited copy (F.trace's eager half −951 min; the engine, the materializer and the shared symbols out of the pages), the trigger +467 min / +156 br on frames (loader entry + probe +160 / +45, the held-set predicate +126 / +67, `claiming` +6 / −9, the held-record mount +175 / +53) — ≈ ×3 the ≈ 150-min estimate, which counted the loader alone; the predicate and the held-record mount were S1's and never budgeted. Page brotli lands ≈ 1.2 KB above the edited-copy figure because the real chunk pins its shared imports (store symbols, `withStoreHydration`'s adapters) in the eager graph.) | `container-trace.js` 24.3 KB / **7.86 KB br measured** (S1) + the eager half (≈ +700 min / +200 br → ≈ 8.1 KB br) **Measured: `trace.js` 25,409 min / 8,170 br** (page base; 8,158 on live) | S1's seven surviving pins green through the general seam (`frames-container-lazy-{codec,document}`, `hydration/welcome-status-lazy`, `container-trace-hold-{id-determinism, interruption, record-retention, snapshot}`); **re-pin** `container-trace-hold-hydration-end` (_hydration waits for the load; the mount claims before done_); **flip** S1's C3 (b); the `.fails` id-drift pin → 3.4 (3c). Size: frames eager ≤ 11.25; page base ≤ 36.0, live ≤ 39.8 (S1's caps 38.45 / 42.12 are superseded by these at landing). **Landed:** the seven pins green through the general seam (the hold re-armed per test by dropping the tier's load — `tierLoads`, the runtime's test seam); hydration-end re-pinned; C3 (b) was never `.fails` on `next` (the eager materializer made it pass) — it now asserts the hold and the 3.1 order; the id-drift pin stays `.fails` under 3.4 (unfixed on `next`); new `tier-trace-hold.spec` (§1's pin: un-announced hold + held-record mount, announced start-at-install, the codec wait). Harness 500 × 2 seeds: SC 0, generic (`C1,C9,C19,E` ignored) 0. Caps lowered (the ratchet): page base 44.89 → **38.61 KB**, live 48.60 → **42.16 KB**; frames eager's cap stays 13.79 (still 76 B over by Phase A's and B's bytes; its recorded minified lowered to 42,968, so the gate passes by the minified rule). | B, A2 (the hold registers; the park is on `next`), A3 | S1's: `revive(value, claiming?)`, `setContainerTraceMaterializer(…, claiming?)`, the entry, `withStoreHydration` / `applyPatches` / `forwardIteratorReturn` `@internal` on the main entry; **not shipped:** S1's `prepareData` / `prepareArgs` (replaced by `prepareTier` before they exist on `next`). `reviveContainerTraces` / `setContainerTraceMaterializer` move behind the tier — **flag**: re-export lazily-resolving wrappers or accept the move. **Landed:** the move accepted (both were `@internal` module exports, never on the public entry); new export path `@solidjs/web/frames/trace` (`install()`); `FrameHost.revive` documented as assignable after creation (the tier's install sets it); `solid-js` loses `materializeContainerTrace` from `solid-js/internal` and the main entry (the new subpath carries it); `tierLoads` exported from the internal runtime module for the specs. | | **C4** | **Regions tier.** `tier-regions.js` = `#bindRegions` / `#regionsFor` / `#discoverRegions` / `collectRegionElements` / `disposeRegions` / `makeFrameElement` / `isFrameRef`, the `{$frame}` arm of `#resolveArgs`, the `resolveSlot` / `resolveSlotRecord` / `removeSlotRecord` thread-up, `tableFor`'s prefix walk, `drainRecords`' `sc:region:` arm. The eager client keeps the `{$frame}` detection in `#resolveArgs` (one `isFrameRef` test → hold, registered). The rename machinery (`renameRegion` / `#reconcileRegions`, D) deletes outright — it is not moved. **Landed (2026-10-06, `feat/frames-regions-tier`, measured before written on edited dist copies per re-attribution §7).** The chunk is `@solidjs/web/frames/regions` (frames/src/regions-tier.ts — a NEW `@solidjs/web` export path): the per-frame region cache (a `WeakMap` the tier keeps, not a frame field), discovery in an adopted interior (`collectRegionElements`, the range walk), the `{$frame}` arm of arg resolution (`resolve`: mint / reuse / rename), the bind of a frame over each region element (`bind`: `createFrame` from the eager entry with the parent linked as `options.parent`, so the slot-resolution thread-up is private access across instances — no closure per region), disposal, and the staged preview's two region reads (`changed` / `frames`). The module's exports ARE its appliers: `prepareTier` records the installed module in a dispatch table (`tierModules`) and the frame calls through it; no `install()`. The eager client keeps: `needsRegions` (one test on the host's `record.regions` note → the fresh-mount hold under 3.1, AND the same wait at the update site — a mounted occurrence's new record naming a region while the tier is absent stays pending in the store, the live binding keeps its args; the check starts the load), the `sc:region:` drain arm in client.ts (an occluded region's html lands in the store regardless; the frame the tier binds seeds from it), the `tierModules` table, the loader entry, an `options` getter on the frame (`@internal`). Already gone on the base: `isFrameRef` (the host's inline `$frame` note is the detection), `tableFor`'s prefix walk and `#reconcileRegions` (A1b). **The rename machinery was NOT deleted — moved into the tier (`rename`, the rename arm of `resolve`), flagged:** it is live, not dead — a single-flight response renders a boundary's regions under the call's address while a direct response and the document render them under the function id, so a flight refresh of a shown boundary renames every region (`#regionsChange`/`preview` anticipates exactly this); two pins assert the rebind (`lifecycle-matrix/call-driven-args` › regions, `frames-optimistic-hold` › the renamed region). Deleting it would leave the refreshed region's content stale; it deletes with S7's normalization (principles §5.3), which is not this step. Eager cost of keeping it: 0 (it rides the chunk). | **−489 / ≈ −480 / ≈ −480 / 0** (measured on frames: `T+regions` → `L8`; pages `est.` at the same cut) **Measured: −237 / −281 / −261 / 0** br (−1,078 / −1,083 / −1,083 / 0 min) against C3's head; the deletion alone −423 / −484 / −346 br (−1,596 min) on an edited copy — less than the −489 because the `sc:region:` drain arm and B's predicate stay and A1b had already taken the prefix walk and `#reconcileRegions`; the glue +518 min / +186 br on frames (the edited-copy cut measured +498 / +173 and landed at −250 br; TypeScript's TS18030 — no private name in an optional chain — forced the longer `#outer && #outer.#x()` thread-up form, +33 min). Glue ≈ ×3.3 the ≈ 150-min line, reported; it is 44% of the deletion (the brief's "not more than half"). Vs `next` 9d89df731: −158 / −6,565 / −6,709 br. | `tier-regions.js` ≈ 1,900 min / **≈ 540 br** (`est.`) **Measured: `regions.js` 1,872 min / 805 br** (page base; 802 on live) — min on the estimate, brotli higher: a 1.9 KB standalone chunk has little to share against. It bundles its own copy of frame-client's pure DOM helpers (`eachInRange`, `makeFrameElement`, `isFrameElement`, ≈ 300 min) and carries a bare `import "solid-js"` (frame-client's one import, kept by Rollup as an external side effect; the app has it loaded). | `tier-regions-hold.spec` (new); `frames-regions-*`, lifecycle matrix region rows green; principles §4 row 19 (the rename compensations) deleted with D. Size: frames eager ≤ 10.75. **Landed:** new `consistency/tier-regions-hold.spec` (4: un-announced `{$frame}` in an adopted record → held under 3.1, interior and region content on screen, mounts with the held record, the ADOPTED region element discovered and bound (a later chunk to its wire id morphs it), no TypeError, hydration-done after; announced → the import starts at install; an occluded region's `sc:region:` record drained before the tier seeds the frame the tier binds — shows the html when placed, nothing re-delivered; stream face, a MOUNTED occurrence's region-naming record waits and applies at the install into the live binding). `tier-prepare`'s regions test releases the real module (the fake `{ install }` cannot resolve a region). Resident cells warm with `prepareTier("regions")`: `lifecycle-matrix/call-driven-args`, `frames-hn-client`, `frames-occlusion-client`, `frames-optimistic-hold`, `frames-used-region-client`; `test/server/frame-hn` (the runtime without the entry) registers the loader itself. Harness 500 × 2 seeds: SC 0, generic (`C1,C9,C19,E` ignored) 0. Caps lowered (the ratchet): frames eager 13.79 → **13.64 KB** (first time under the cap since Phase A), page base 38.61 → **38.33 KB**, live 42.16 → **41.90 KB**. Principles §4 row 19 NOT removed — re-dispositioned (above). | B, A2 (the adopt-path hold registers — no 3.1 gap to flag) | none (`createFrameElement` stays eager — it is `@experimental` public API, re-attribution §5.3 item 5) **Landed:** new export path `@solidjs/web/frames/regions` (`@experimental`; exports `resolve` / `bind` / `unmount` / `changed` / `frames` — the appliers; no `install`); `InstallOptions.tiers`' module type widened to the new exported `TierModule` (`{ install?(): void; [applier: string]: unknown }` — the old `{ install?(): void }` is a TypeScript weak type a real tier module cannot satisfy); `FrameImpl` gains an `@internal` `options` getter (not on `Frame`); frame-client's `eachInRange` / `makeFrameElement` / `isFrameElement` exported `@internal` for the tier (not on the public entry); the internal `resolveSlot` / `resolveSlotRecord` / `removeSlotRecord` options (never on `FrameOptions`) replaced by `parent`. `createFrameElement` unchanged. Wire unchanged; 0 of 150 artifacts changed. | | **C5** | **Assets tier.** `tier-assets.js` = `ensureStylesheet` / `ensurePreload` / `ensureModulePreload` / `applyInlineStyles` / `qualifierValue` / `findHeadElement` / `PRELOAD_QUALIFIERS` / `#processedAssets` / `#styleFlush` / the assets pass; the eager client keeps `chunkToRecords`' `assets` case, `host.write`'s `seg::assets` accumulate, and the `#segmentReady` term. **Pin the two untested functions first** (`ensureStylesheet`, `applyInlineStyles` — the audit's 0-coverage gap) in the same PR. Alternative under decision 2: S10's route-through-`web` instead of a tier. **Landed (2026-10-06, `feat/frames-assets-tier`, measured before written on edited dist copies per re-attribution §7; the two coverage pins committed first, against the code as it was).** The chunk is `@solidjs/web/frames/assets` (frames/src/assets-tier.ts — a NEW `@solidjs/web` export path): the whole head mirror (`ensureStylesheet` / `ensurePreload` / `ensureModulePreload` / `applyInlineStyles` / `qualifierValue` / `findHeadElement` / `PRELOAD_QUALIFIERS`), the per-frame flush a pending sheet wakes (`#styleFlush` → a `WeakMap` in the tier), and the assets pass over one record — modules, typed preloads, **and inline styles** (moved off the reveal: they land at the record's arrival when the tier is resident, before the segment's content as the document face orders them; at the install when it was absent). The tier is import-free and has no `install()`: its exports **are** the dispatch — `prepareTier` stamps the load's `r` with the module (B's `true` → the module; truthy either way), and the eager client calls `gate(styles, frame)` from `#segmentReady` and `apply(record)` from `#flush`'s walk off that stamp. The eager client keeps `chunkToRecords`' `assets` case, `host.write`'s accumulate, the walk over `:assets` records (deduped through the applied map — `#processedAssets` deleted), and the **reveal-readiness term**: `assets.styles && !tierReady("assets")?.gate(assets.styles, this) → not ready` — a style-gated segment is not ready while the tier is absent (`tierReady` starts the load if nothing announced it; the server's fallback stays) and, once resident, until every sheet has settled; the reveal is at max(tier load, stylesheet load). A segment without stylesheets never consults the tier; a stream with no asset records never loads it; an un-announced record met by the walk starts the load and stays pending until the install's flush. | **−684 / ≈ −665 / ≈ −665 / 0** (measured on frames: `T+assets` → `L8`; the `noassets` full-client cut −665) **Measured: −534 / −628 / −496 / 0** br (−1,913 / −1,926 / −1,926 / 0 min) against C3's head `89954fa1b`; vs `next` @ `9d89df731` −455 / −6,912 / −6,944 / 0. The deletion alone −666 / −681 / −627 br (−2,419 min: the whole F.assets group, record cases included) on an edited copy; what stays eager costs +508 min / +131 br on frames — the kept record cases +208 / +64 (the plan's own list), the readiness term + loader entry + `r = module` + the walk's dispatch +300 / +67 — over the ≈ +150-min line, reported in the PR. A folded variant (the walk riding the hole/attr loop, content before its inline style within one flush) measured −562 br and was not taken: the separate walk before the segments keeps a segment's `