Skip to content

fix(web/frames): classify an adopted occurrence only after every delivered record has drained (C18) - #3827

Closed
ryansolid wants to merge 1 commit into
nextfrom
fix/frames-c18-classify-after-drain
Closed

ryansolid wants to merge 1 commit into
nextfrom
fix/frames-c18-classify-after-drain

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Fixes C18 — the only page-halting red in the frames/hydration consistency contract (#3813; documentation/server-components/frames-consistency-contract.md §C18 / §Red R9).

The bug

One adopted boundary, two render-prop occurrences whose records are still owed when the boundary adopts (document.readyState === "loading", the #2968 defer arms). Both records' data scripts execute, then the parser finishes before the deferred setTimeout fires — the document's tail (the frame's data scripts and the end of the response) parses in one go, so this is the common order. Shrunk shape: [item#0 item#1 children] :: H R1 R0.

In the code's terms: the timer runs drainRecords(), which applies one record per host.apply, each a synchronous #flush → #syncSlots. The first apply's sync mounts item#0 and finds item#1 still recordless — its record is sitting in _$HY.r one loop iteration away — while adoptBoundary.recordsPending() (readyState === "loading" || fr.pending()) now reads false. #syncSlots classifies it direct-insert, and the render prop is evaluated as a zero-arg accessor inside the insert effect: props.text → TypeError → [REACTIVITY_HALTED]. The same window is reached by a live-hole op (applyLiveOp → host.apply) and by the live pump's catch-up read of ops logged before adoption. The existing #2968 pins (adopted-slot-late-record) never saw it: one occurrence, readyState held at "loading" through the drain.

Three ledgers, two consulted: the document's (_$HY.r), the store's (where the drain puts a record), the page's. "Recordless" read the store, "pending" read the parser, and nothing read the gap between them.

The ruling it implements

Proposed frames-rulings 3.5 (documentation/server-components/frames-rulings.md on spec/frames-rulings, pending the maintainer's confirmation):

An occurrence is classified only after every delivered record has drained. A recordless adopted occurrence is direct-insert content only once nothing the document delivered for its boundary is still waiting to be applied: the record defer's bound is the drain's end, not the parser's — a sync that runs while _$HY.r holds an unapplied record for the boundary defers every recordless occurrence it finds, whatever triggered the sync.

Maintainer's consensus wording: delivered is not drained; the parser's end classifies only the undelivered.

The fix

packages/web/frames/src/client.ts — adoptBoundary.recordsPending gains a third term: _$HY.r holds a key under the boundary's sc:slot:<id>: or sc:region:<id>. prefix that is not yet in appliedRecords. "Pending" now means not yet delivered or delivered and not yet drained. The two prefixes are hoisted out of drainRecords and shared; the drain's region branch uses the hoisted prefix instead of the two-step "sc:region:" + id + "." test (same set). The defer arm in #syncSlots needs no change: drainRecords marks a key applied before its host.apply, so on the drain's own first apply the other record is still un-applied, that sync defers the occurrence (re-arming the timer — which later fires once into a no-op), and the loop's next apply mounts it with its args. No retry, no extra timer, no drain from inside a sync.

packages/web/frames/src/frame-client.ts — comment-only: FrameOptions.recordsPending and the #syncSlots defer arm now state the drain's-end bound.

Pins

  • packages/web/test/consistency/harness/replay.spec.tsx — the three C18 replays flipped from test.fails to test, retitled to the behaviour (the first apply defers the second; a live op's sync defers the undrained occurrence; the pump's catch-up read defers), citing 3.5 and C18/R9. Control unchanged.
  • New packages/web/test/consistency/c18-classify-after-drain.spec.tsx — the consequence with a real fill (p => <li>{p.text}{tick()}</li>, the props read unguarded): (a) the record drain with two occurrences after the parser's end, (b) a live op's sync in the window; plus a control. On next both arms fail with Cannot read properties of undefined (reading 'text'); here every occurrence is invoked once with its args, the server <li> is claimed in place, no console error, and the page is still reactive after the tick bump.
  • No other test.fails flipped: the hydrate config reports exactly the 19 remaining test.fails as expected-fail (C3, C19 ×2, C2 replays; c02/c03/c04/c05/c06/c07/c12/c13/c17 pins).

Harness campaign (CONSISTENCY_FUZZ=1, 500 cases)

Per-case before/after on the same seeds (same generator, same scenarios; the dump was a temporary spec, not committed):

seed law before after
3289 C18 classify-after-drain 49 0
3289 C3 done-counts-holds 280 280
3289 C19 claim-shows-oracle 71 81
3289 C2 every-range-live / -reactive 11 / 16 10 / 15
91501 C18 classify-after-drain 55 0
91501 C3 done-counts-holds 268 268
91501 C19 claim-shows-oracle 68 76
91501 C2 every-range-live / -reactive 25 / 26 22 / 23

Nothing new. No law appears after that did not appear before; the only cases whose finding set changed are exactly the 49 / 55 former C18 cases. Of those, 10 / 8 now surface the pre-existing C19 (a trace patch delivered before the claim — R10): the occurrence now really claims instead of being replaced by the oracle's inert stand-in, so the claim-shows-oracle law can see it. The small C2 drop is the same effect from the other side (the inert stand-in used to register as a dead range). C3 is untouched (seam 3.1/3.2, not this PR).

Suites (packages/web, on the rebased tree)

config result
vitest run (dom) 1142 passed, 1 expected fail
--config vite.config.server.mjs 1473 passed, 3 expected fail, 2 skipped
--config vite.config.hydrate.mjs 346 passed, 19 expected fail, 1 skipped
pnpm run test-types (tsc over tests) clean
root pnpm typecheck clean
test/harness/__artifacts__ byte-identical (0 changed after the server run)

packages/solid/src and packages/signals/src are untouched.

Size (local, scripts/size, head vs origin/next base measured in the same run)

scenario minified brotli local gate
frames: eager client consumer 43,310 → 43,377 (+67 B) 13,770 → 13,776 (+6 B) ok (under cap)
page: base server components 145,599 → 145,666 (+67 B) +6 B over cap locally fail under #3821's gate
page: live server components 157,562 → 157,629 (+67 B) +90 B over cap locally fail under #3821's gate
app: compiled hydrating 99,198 → 99,198 (0) unchanged ok

The growth is the predicate (the ruling's own estimate was ≈ +50 B min; the sc:region: prefix term is most of the difference). Under the gate that landed in #3821/#3822, an over-cap scenario passes only within a 20 B minified allowance, so page: base / page: live fail on real growth, not noise — a region-less predicate would still be ≈ +45 B and fail the same way. No cap or recorded minified is raised here; that is the maintainer's call (cap + capMinified bump with a reason, or a Size-Exception:). CI's numbers are authoritative for the brotli side; local brotli on this machine runs a few tens of bytes above CI's on several untouched scenarios.

Public API changes

None. recordsPending is an internal, spread-cast option on the adopt path; no export, prop, option, signature, default or diagnostic changes.


Changeset: .changeset/frames-c18-classify-after-drain.md ("@solidjs/web": patch).

…vered record has drained (C18)

Frames-rulings 3.5 (proposed): "an occurrence is classified only after
every delivered record has drained" — pending is the drain's state, not
the parser's. `adoptBoundary.recordsPending` gains a third term: `_$HY.r`
holds a slot/region record for the boundary not yet in `appliedRecords`.

Before: two records drained after the parser finished classified each
other — `drainRecords` applies one record per `host.apply`, each a
synchronous `#flush` → `#syncSlots`; the first apply's sync found the
second occurrence recordless with `readyState` no longer "loading",
classified it direct-insert and evaluated its render prop argless:
`TypeError` → REACTIVITY_HALTED. Same window via a live-hole op and the
live pump's catch-up read (contract C18 / R9).

Pins: harness/replay C18 ×3 flipped from test.fails to test;
c18-classify-after-drain.spec.tsx pins the consequence with a real fill
(record drain; live op in the window; control).

Co-authored-by: Claude via Cursor <noreply@cursor.com>
@changeset-bot

changeset-bot Bot commented Oct 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: af55363

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
Name Type
@solidjs/web Patch
@solidjs/babel-plugin Patch
@solidjs/diagnostics Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
test-integration Patch
todos-server-example Patch
@solidjs/compiler Patch
@solidjs/signals Patch
solid-js Patch
@solidjs/universal Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

Size (brotli, eager entry chunk)

scenario head vs base minified vs base minified vs recorded cap lazy chunks (not counted)
signals: core floor (createSignal/Memo/Effect/Root/flush) 7.36 KB 0 B 0 B 0 B 7.35 KB ⚠️ over by 11 B, 20 B minified headroom
signals: + createStore 14.54 KB 0 B 0 B 0 B 14.56 KB ✅
signals: + isPending/latest 9.51 KB 0 B 0 B 0 B 9.49 KB ⚠️ over by 23 B, 20 B minified headroom
app: render + one signal (the simple-app floor) 9.86 KB 0 B 0 B 0 B 9.86 KB ✅
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 17.67 KB 0 B 0 B 0 B 17.71 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 28.89 KB 0 B 0 B 0 B 28.87 KB ⚠️ over by 18 B, 20 B minified headroom lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 12.90 KB 0 B 0 B 0 B 12.86 KB ⚠️ over by 45 B, 20 B minified headroom lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 14.45 KB 0 B 0 B 0 B 14.46 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 28.64 KB 0 B 0 B 0 B 28.66 KB ✅ lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 10.03 KB 0 B 0 B 0 B 10.05 KB ✅
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 25.13 KB 0 B 0 B 0 B 25.13 KB ✅ stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 30.96 KB 0 B 0 B 0 B 30.93 KB ⚠️ over by 27 B, 20 B minified headroom stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 13.78 KB +6 B (+0.0%) +67 B +67 B 13.78 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 44.85 KB −18 B (−0.0%) +67 B +67 B 44.84 KB ❌ over by 6 B decode.js 6.07 KB, lazy-page.js 0.04 KB
page: live server components (base + live/GET + action + isPending/latest) 48.60 KB +73 B (+0.2%) +67 B +67 B 48.51 KB ❌ over by 90 B decode.js 6.07 KB, lazy-page.js 0.04 KB
server: floor (getRequestEvent + isServer) 1.33 KB 0 B 0 B 0 B 1.34 KB ✅
server: renderToString (the server-render floor) 20.41 KB 0 B 0 B 0 B 20.42 KB ✅

⚠️ Over the brotli cap within the minified allowance (passes)

  • signals: core floor (createSignal/Memo/Effect/Root/flush): over brotli cap by 11 B; minified 20,133 B vs 20,133 B recorded with the cap (+0 B) — 20 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • signals: + isPending/latest: over brotli cap by 23 B; minified 26,828 B vs 26,828 B recorded with the cap (+0 B) — 20 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • app: hydrating + every store primitive family: over brotli cap by 18 B; minified 91,625 B vs 91,625 B recorded with the cap (+0 B) — 20 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • app: CSR with Show/For/Loading/Errored/lazy: over brotli cap by 45 B; minified 36,568 B vs 36,568 B recorded with the cap (+0 B) — 20 B of the 20 B minified allowance left; +0 B minified over this PR's base
  • app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable): over brotli cap by 27 B; minified 99,198 B vs 99,198 B recorded with the cap (+0 B) — 20 B of the 20 B minified allowance left; +0 B minified over this PR's base

❌ Fails the gate

  • page: base server components (hydrating + dynamic + frames + sf reference): over brotli cap by 6 B; minified 145,666 B vs 145,599 B recorded with the cap (+67 B) — 47 B past the 20 B minified allowance; +67 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)
  • page: live server components (base + live/GET + action + isPending/latest): over brotli cap by 90 B; minified 157,629 B vs 157,562 B recorded with the cap (+67 B) — 47 B past the 20 B minified allowance; +67 B minified over this PR's base. real growth: reduce it, or raise the cap and its recorded minified in this PR with a reason (frozen floors need Size-Exception:)

Bundled with Rolldown (what Vite ships), brotli q11, decimal KB. A scenario fails only when it is over its brotli cap and its minified size is more than 20 B over the minified recorded with the cap; over the cap within that allowance is brotli layout noise and passes with a warning. Caps and their recorded minified in scripts/size/scenarios.js; the floor and page caps in floor-caps.json are frozen (lower only, or Size-Exception: in the PR body). npm run ratchet lowers caps per RC; it never raises one (scripts/size/README.md).

@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 37447904115

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage remained the same at 76.058%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1196
Covered Lines: 963
Line Coverage: 80.52%
Relevant Branches: 930
Covered Branches: 654
Branch Coverage: 70.32%
Branches in Coverage %: Yes
Coverage Strength: 28.27 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 6, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 188 untouched benchmarks


Comparing fix/frames-c18-classify-after-drain (af55363) with next (b987c41)

Open in CodSpeed

@ryansolid

ryansolid commented Oct 6, 2026 •

Copy link
Copy Markdown
Member Author

Closing as superseded, per the maintainer.

The S-flush seam (now part of the frames correctness-pass PR against next, #3837) makes C18 unrepresentable rather than guarded: the producer mints every called occurrence as prop#n and emits its record at the call, so on every sync the occurrence name alone decides waiting-vs-direct — a prop#n found without its record waits for it, a bare prop is direct-insert content. There is no window in which a recordless called occurrence can be classified direct, so the third recordsPending term this PR adds has nothing left to guard. The C18 ×3 harness pins and the C18 replay pins flip to green on that line; this PR's own c18-classify-after-drain consequence (the real fill with the unguarded props.text read) is covered by the same mechanism.

The diagnosis here — three ledgers, two consulted; delivered is not drained — stands and is recorded in documentation/server-components/frames-rulings.md (3.5 and the as-landed table). The branch is kept.

— Claude via Cursor

@ryansolid ryansolid closed this Oct 6, 2026
ryansolid added a commit that referenced this pull request Oct 6, 2026
… decisions: the defaults accepted, the C3 hold cost accepted (caps raised), #3827 closed as superseded by S-flush

Co-authored-by: Cursor <cursoragent@cursor.com>
ryansolid added a commit that referenced this pull request Oct 6, 2026
…port, rulings (#3837)

* fix(web/frames): the address is an async source — one landing per bound address, one response per store (S-flush)

A0 (SCs are rendered data): a frame is one async value outward. The mount's
covering <Loading> now pends on the bound address's FIRST FLUSH — content or
error — through `FrameHost.landing(address)`, read per bound address through a
fresh node (frames-rulings 1.5, 1.6 (i)): a switch is a new question on the
source, the superseded address's late writes release nothing, an unrevealed
boundary stays on its fallback and a revealed one holds. The two hand-rolled
shell gates (`boundaryComponent`'s arm/release/settle/setGate and
`adoptBoundary`'s twin), `followAddress`'s re-arm and frameless waiter, and
`onApply`-as-release go. An unstaged response announces itself to the host at
its header (`start`), so the address reads "in flight" from the header to the
landing.

The store is one response's (1.4 full; 2.1/2.2): a version bump or rebind
replaces the store wholesale — root, segments, slot records, the error — so a
byte-identical root under a new version still applies as the new version's
(C7 c), a held record leaves with the response that carried it (C6 a1, b2),
and `argsEquivalent`/`clearStreamRecords` delete; the mount's `#slotArgs`
value compare is what preserves occurrence state. The host keeps the latest
landed version as `shown` so a mount opened mid-flight seeds the committed
value (holds-latest) and the landing fans out as the version's whole set.

The occurrence's name decides its class: a called occurrence (`prop#n`) found
without its record waits for it — never evaluated argless (C18 ×3 flip; the
#2968 poll stays as the document face's re-sync trigger, scoped to called
occurrences) — and a bare occurrence is direct-insert. A reveal is an apply
(2.3, interim): the document face's reveal cascade syncs the adopting frame,
so a direct-insert range the fragment carried mounts (C2 b) and a record
drained before its range was shown takes effect at the reveal (C4 d).

Pins flipped to `test`: C2 (b), C4 (d), C6 (a1, b2), C7 (c), C17 (a — re-pinned:
B's first flush, not its `start`, releases), C17 (c), harness C2 ×1, C18 ×3.
Re-pinned: frames-binding-slots "orphan record … waits" (was "still mounts"),
frames-hn-client zero-data occurrence is the bare prop.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web/frames): hydration-done counts the frame's holds — a waiting adopted occurrence is a pending boundary (C3)

Frames-rulings 3.1 (ruled): hydration-done follows non-SC Solid 2 — done is
what `checkHydrationComplete` says (the root pass over, `_pendingBoundaries`
zero), and the frames client participates through that mechanism, with no
accounting of its own. 3.2, the carrier: ONE registration per frame while a
sync leaves an adopted occurrence waiting to mount (for its record, for a
`{$ref}`'s data), released by the first sync that leaves none or by disposal.

solid-js: `sharedConfig.holdBoundary(id)` (internal) — `initBoundaryResume`'s
registration for a holder with nothing to resume: the count, the owner's `_hp`
mark, the disposal release; returns the release, which checks completion.
One assignment in `enableHydration`.

web/frames: `FrameOptions.hold()` (adopt path; wired by `adoptBoundary` under
the component's owner, only while `isHydrationInProgress()`, keyed `sc:<fid>`
so no fragment's bookkeeping is touched); `#syncSlots` tracks whether a full
sync left an unmounted occurrence waiting and registers/releases at its end;
`dispose` releases.

Harness: the C3 law exempts a done that fired at or after the mount's
disposal — a disposed holder owes no claim (its release is what lets done
fire, as a disposed <Loading>'s is). Verified to hide nothing on `next`
(C3 stays 280/500 there).

Pins flipped to `test`: C3 (a), harness C3 ×1. Campaign (500 cases): C3
280 → 0 (seed 3289), 268 → 0 (seed 91501); only C19 remains.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web/frames): a data chunk of a superseded response lands nowhere (C5)

Frames-rulings 1.2: a `data` chunk lands in its own response's table or
nowhere. The integration rotates the address's table at the header and
creates it at first use; the transport restamps every chunk with its
response's version; but `createFrameHost.apply` routed `data` straight to
the data hook with no version read — so a superseded response's late chunk
was the first use and filled the table that was now the current response's
(R4). The data path is now under the store's version guard like every other
chunk: with the response announced to the store at its header (S-flush), a
`data` chunk whose version is below the store's is dropped.

Pins flipped to `test`: C5 (a), (b), (e).

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web/frames): report a rejected server <Loading> fragment inside an adopted frame in dev; the client shows the server's outcome (C12 c, client half)

Frames-rulings 3.3 under A0's corollary 4 (inward): a server `<Loading>`
inside a server component is the SERVER's boundary; its outcome arrives as
markup and the client shows whatever the server rendered for it — never a
blank it invents, never a client error fallback for a server state. A
rejected one has no client twin to surface its `<key>_fr` rejection
(`hydratedCreateLoadingBoundary`'s `s === 2` arm runs only for a boundary
registered against it), so the adoption that claims its placeholder
observes the rejection and names it in dev (0 B prod).

The blank the position shows today is the server half's gap: `server.ts`'s
error path hands `sink.fragment` a `" "` template (the client twin, when
there is one, renders over it; a server component's boundary has none).

Pins: C12 (c) re-pinned per 3.3 — (c1) the rejection is reported in dev,
the client invents no error state (test); (c2) the position shows the
server's rendered outcome, never a blank (test.fails, the server half).

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* web/frames: trim the hold's bytes — one mounted read per occurrence in #syncSlots, the guard reads isHydrationInProgress alone

−48 B min on the frames client (43,411 → 43,363); the zombie check now
precedes the {$ref} wait, so a zombie held on a ref is unmounted at once
rather than when the ref resolves.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* web/frames: the data guard needs no undefined check (n < undefined is false)

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(server-components): frames rulings — the three seams the consistency reds and the size audit's duplicates share (proposed)

Draft, marked "proposed — maintainer ruling pending"; no engine changes.

The frames/hydration consistency contract (spec/frames-consistency-contract)
found thirteen reds across C2, C3, C4, C5, C6, C7, C12 and C17; the SC size
audit (size/sc-audit §4) listed the layer's duplicated seams. This document
tests the hypothesis that they are the same seams seen from two sides, and
drafts the rulings — numbered one-sentence statements in the L2 section's
voice, each with the mechanism that carries it today, the state it lives in
twice, and the reds it decides — grouped by the three questions the reds
cluster on:

- Response identity (C5, C6, C17): a response owns what it delivered; a
  `data` chunk lands in its own response's table or nowhere; a record
  resolves its refs through its own response's table wherever the frame is
  bound; a bump drops what the previous version never applied; the shell
  gate is the bound address's; a switch keeps on screen what was on screen.
- Applied state per version (C7, C2, C4): the store is the truth and the
  applied state is a cache of it keyed by version; a bump re-applies a
  byte-identical root; a reveal is an apply; applied means shown.
- Hydration-done accounting (C3, C12, S1's C3b and `.fails`): done counts
  every occurrence the document delivered; every frames hold is counted once
  per frame; a claim owns the fragment's outcome; the client consumes the
  ids the server consumed.

For each: the fix shape as one collapsed carrier (response-owned data
cells, records carrying their resolver, one shell gate, one applied record,
one hold counter), a byte estimate (− collapse / + carrier) read off the
audit's function table, the pins that flip, and what touches public
surface or the server half. Two-readings items for the maintainer: 1.4
(narrow/full), 1.6 (per-address gate value vs holds-latest), 3.1 (done
counts the page vs the root pass — S1's pin and the contract's contradict).
What the rulings do not decide (plain bugs, product questions, the wire)
and an order of work gated by the contract's pins and scripts/size.

Co-authored-by: Claude via Cursor <noreply@cursor.com>

* docs(server-components): frames rulings — C13 confirmed, C18 first, C19 under seam 3; gate is #3813

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(server-components): frames rulings — principle: SCs are rendered data; a frame is one async value outward, its inner boundaries are the server's; 3.1 ruled

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(server-components): A0 — equivalence: SCs are rendered data, the hold model governs; axioms annotated with the Solid 2 rules they restate

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(server-components): frames rulings — prior art comparison

Per-reading comparison (1.3, 1.4, 1.6, 2.3, 3.1, 3.3, 3.5, 3.6) and per-
product-question comparison (C12 (c), settles-once projections, live
containers, slot-trails-html) against RSC/Flight+Fizz, Next.js, React
Router/Remix, Turbo/htmx, Phoenix LiveView, Qwik, Astro server islands,
Marko, Vue and Solid Router. Adds an addressing section (content-keyed vs
tree-shaped vs DOM-addressed vs connection-tree) for the maintainer's A3/A4
model, a summary table, and a list of mechanisms the references have that
we don't. Primary-source citations; verified vs inferred marked.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(plans): bring the frames A0 re-attribution and the savings-pass plan onto the rulings branch (from size/frames-a0-reattribution @ 1e0be39)

The plans tonight's correctness pass implements (frames-savings-pass.md
Phase A: A0–A6), carried with the code per the maintainer's preference.
Squashed from the branch's nine commits; content unchanged.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(server-components): frames rulings — the overnight pass's defaults, the server-half drafts, the as-landed table

What the rulings do NOT decide: the nine readings the 2026-10-06 pass took
by default (first flush not start; warm switch-back shows the committed
value; the name decides the class on every sync; 2.3's interim with
S-flush; 1.2 by the version guard; 3.2 via initBoundaryResume keyed
sc:<fid> while in progress; 3.3's dev report; S-adopted documented; the
harness's C3 dispose exemption), each with its alternative. The server
half: C13's delimiter with the client's one-write status, the
plain-response streaming bound (complete.bound), C12 (c)'s error template
located in finalizeError/the done closure with the two decisions the
server PR must make. Order of work: the as-landed table (#3830–#3833),
what each step left undone. Public surface: landing, hold, holdBoundary,
the behaviour notes.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(server-components): frames rulings — the maintainer's 2026-10-06 decisions: the defaults accepted, the C3 hold cost accepted (caps raised), #3827 closed as superseded by S-flush

Co-authored-by: Cursor <cursoragent@cursor.com>

* size: raise frames/hydration caps for C3 hold (maintainer Size-Exception)

The C3 hold (frames-rulings 3.1, ruled) costs +59 B min on every hydrating page (sharedConfig.holdBoundary in solid-js) and the pass nets +104 B min on the frames client (S-flush −187, the hold and C5's guard +291). Six scenarios are over their cap past the 20 B minified allowance; the maintainer accepted the cost 2026-10-06 ("pay the cost for correctness"). Each cap is set at the 0.01 KB step at or below measured + 10 B, with its recorded minified, from a local measurement that matches CI on this machine: hydrating (no stores) 17.71 -> 17.73 KB, page base 44.84 -> 44.89 KB, page live 48.51 -> 48.60 KB (floor-caps.json, under the PR's Size-Exception:); hydrating + stores 28.87 -> 28.93 KB, compiled hydrating 30.93 -> 31.03 KB, frames eager 13.78 -> 13.79 KB (scenarios.js). The three pre-existing over-cap-within-allowance scenarios (signals floor, isPending, CSR) are at +0 B minified and are not raised.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(server-components): frames rulings — the pass lands as one PR, #3837

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: record 2026-10-06 rulings (3.6 (iii), wire order, §6 decisions, holes eager) and the measured Phase D floor

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants