Skip to content

fix(web): a late hydration claim leaves server nodes in place (#3749) - #3760

Merged
ryansolid merged 8 commits into
nextfrom
fix/loading-lazy-claim-keeps-nodes
Oct 3, 2026
Merged

ryansolid merged 8 commits into
nextfrom
fix/loading-lazy-claim-keeps-nodes

Conversation

@ryansolid

@ryansolid ryansolid commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Fixes #3749

Root cause

This happens when a settled <Loading> wraps a lazy() component whose module (<id>_assets) is not yet in _$HY.modules when hydration starts. hydratedCreateLoadingBoundary waits on the asset load, so the boundary returns undefined in the root pass. It then claims its server nodes on a later resume (resumeBoundaryHydration).

The insert that holds the boundary's value sits outside the boundary:

  • Its hydration claim pass in insertExpression saw undefined and tracked the region as empty (current = undefined).
  • On resume, that insert is not claiming (isClaiming() only covers owners under the boundary), so it took the client path: appendNodes → insertBefore for a fragment, or appendChild for a single element.
  • That re-inserts connected, already-claimed nodes. The browser treats this as a move, which blurs a focused input and drops typing state.

The reporter's shell puts the boundary in a non-multi hole (<body>). Marker-bounded holes keep their claimed placeholder array, so they weren't affected.

This is DOM-layer only (packages/web/src/client.ts). The boundary's reactive core is unchanged.

Fix

insertExpression gets one guard, placed after its value === current early return. While hydration is in progress (sharedConfig.hydrating), when the region is untracked (current == null) and every incoming item is already a node in parent, it returns value without touching the DOM.

  • Scope: sharedConfig.hydrating is true during the root pass and during each boundary's resume window. resumeBoundaryHydration sets it around the window's set() and flush(), and that flush is where the enclosing insert re-runs.
    • A temporary probe confirmed the guard fires there with sharedConfig.hydrating === true, for both the fragment and the single-element page.
    • The reporter's app in Chrome passes with the scoped guard.
    • Once hydration completes, inserts behave exactly as on next.
  • Order: within that window, the only nodes of ours that can already be in parent are claimed server nodes, and they arrive in server order. So order isn't checked.
  • Primitives and empty arrays: a raw primitive (a failed text claim) fails the check, so it takes the normal path and still gets inserted. An empty array also fails the check and keeps its existing clear.
  • Client-only bundles: the guard is gated on the hydrationRt slot and folds away, so CSR bundles are byte-identical.

Alternative tried: keeping the claimed nodes as current in the claim pass broke spread-innerhtml.spec.tsx. Spread's children insert claims innerHTML text it doesn't own, so the claim pass can't tell the two cases apart; at landing time "already in place" is unambiguous.

Size candidates

Measured locally with a clean full pnpm build, which reproduces CI's numbers exactly. Base is next @ b57eb2e, where page base sits at exactly its cap (46,240 B / 46.24 KB) and hydrating + stores has 2 B of headroom. Each cell is minified / brotli bytes; ✗ marks a scenario over its cap. All candidates are scoped to hydration and pass the #3749 and spread-innerhtml tests.

candidate hydrating (headroom 23) hydrating + stores (headroom 2) page base (headroom 0) page live (headroom 86)
S9 (pushed): value + every + value.length !== 0 +91 / +19 +92 / −3 +92 / +9 ✗ +92 / +64
S2: S1 with the condition order changed +103 / +64 ✗ +104 / +47 ✗ +104 / −12 +104 / +47
S4: nested in the existing claim branch +107 / +60 ✗ +107 / +5 ✗ +107 / +35 ✗ +107 / +67
S6: nested, every + length guard +93 / +55 ✗ +93 / +86 ✗ +93 / +27 ✗ +93 / +16
S7: every + value.length !== 0, no value && +88 / +57 ✗ +89 / +8 ✗ +89 / −27 +89 / +26
S8: value?.length !== 0 + every +89 / +47 ✗ +90 / +9 ✗ +90 / +14 ✗ +90 / +79
S10: length guard before every +91 / +93 ✗ +92 / +13 ✗ +92 / −22 +92 / +67
S11: n && n.parentNode +93 / +31 ✗ +94 / +70 ✗ +94 / −16 +94 / +66
S12: !…some(… !==) +91 / +68 ✗ +92 / +29 ✗ +92 / +12 ✗ +92 / +72
S13: S9 placed before value === current +91 / +42 ✗ +92 / −16 +92 / −47 +92 / +39
S14: S9, current == null before the flag +91 / +63 ✗ +92 / +69 ✗ +92 / +11 ✗ +92 / +111 ✗
S15: [value].flat() +89 / +57 ✗ +90 / +9 ✗ +90 / 0 +90 / +42

Every encoding costs about 90 B minified. Brotli swings 40–90 B between equivalent text, and with zero headroom on page base none of the 12 fits all four caps. S9 comes closest, over by 9 B on page base only.

Tests

  • packages/web/test/hydration/loading-lazy-resume-3749.spec.tsx:
    • A fragment page and a single-element page. Each test focuses an input and types into it, then hydrates while the module is pending, lands the module, and asserts all of the following:
      • the server nodes are identical,
      • zero MutationObserver add/remove records,
      • focus and the input value are retained,
      • the hydrated button updates.
      • Both tests fail on next.
    • Post-hydration guard test: after hydration completes, an untracked region whose parent already holds the incoming nodes still re-inserts them at the end, exactly as on next. It passes on next and fails on the unscoped guard.
  • Reporter's Vite + rc.13 repro in Chrome 154, with the scoped guard patched into web.dev.js: 3/3 runs kept focus with no insertBefore of <main>. The no-Loading and no-lazy controls are unchanged.
  • web: client 1131 passed, server 1391 passed, hydrate 275 passed.
  • solid-js: 819 passed.
  • test-types and typecheck pass.
  • signals is untouched. attribution-waterfall-eval failed once under turbo load and passes when run alone (twice).

Public API changes

None to exports, props, or signatures.

Behavior change, scoped to while hydration is in progress: an insert whose region is untracked no longer re-inserts incoming nodes that are all already children of its parent. Before, a fragment would move to the end of the parent past any trailing nodes (for example, streamed <script>s). Now it stays where the server put it. After hydration completes, behavior is unchanged from next.

Size

The page-base cap is raised 46.24 → 46.25 KB with maintainer approval. It is the only cap raised.

Against next @ 9e85a09, measured with a clean full build that matches CI:

scenario minified brotli now / cap
page base +92 +9 46,249 / 46,250 (raised)
hydrating +91 +19 19,686 / 19,690
hydrating + stores +92 −3 30,815 / 30,820
page live +92 +64 50,428 / 50,450

CSR, signals, and server scenarios are unchanged. The ledger note is in scenarios.js.

This PR also updates the #3741 page-base ledger note from "Pending maintainer review" to accepted.

Size-Exception: page base 46.24 -> 46.25 KB: #3749 hydration in-place guard (+9 B brotli, +92 B minified); accepted by the maintainer.

A settled <Loading> whose lazy() module is still loading when hydration
starts returns nothing in the root pass and claims its server nodes on a
later resume. The insert holding the boundary's value sits outside the
boundary: its claim pass saw `undefined` and tracked the region as empty,
so on resume it took the client path (appendNodes / appendChild) and
re-inserted the connected, already-claimed nodes, moving them and
blurring a focused input.

insertExpression now leaves nodes in place when an untracked region
receives nodes already sitting in it in order. The check lives on the
hydration runtime slot, so client-only bundles carry none of it.

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

changeset-bot Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 92e79a1

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 2, 2026 •

Copy link
Copy Markdown

Size (brotli, eager entry chunk)

scenario head vs base cap lazy chunks (not counted)
signals: core floor (createSignal/Memo/Effect/Root/flush) 9.49 KB 0 B 9.51 KB ✅
signals: + createStore 16.84 KB 0 B 16.85 KB ✅
signals: + isPending/latest 12.15 KB 0 B 12.16 KB ✅
app: render + one signal (the simple-app floor) 11.98 KB 0 B 12.05 KB ✅
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 19.69 KB +19 B (+0.1%) 19.69 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 30.82 KB −3 B (−0.0%) 30.82 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 14.88 KB 0 B 14.94 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 16.42 KB 0 B 16.48 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 30.61 KB 0 B 30.71 KB ✅ lazy-page.js 0.04 KB
frames: eager client consumer (frames client + transport, lazy codec) 12.98 KB 0 B 12.98 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 46.25 KB +9 B (+0.0%) 46.25 KB ✅ decode.js 6.07 KB, lazy-page.js 0.04 KB
page: live server components (base + live/GET + action + isPending/latest) 50.43 KB +64 B (+0.1%) 50.45 KB ✅ decode.js 6.07 KB, lazy-page.js 0.04 KB
server: floor (getRequestEvent + isServer) 1.33 KB 0 B 1.34 KB ✅
server: renderToString (the server-render floor) 20.38 KB 0 B 20.38 KB ✅

Bundled with Rolldown (what Vite ships), brotli q11, decimal KB. Caps 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).

@coveralls

coveralls commented Oct 2, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 37097955920

Coverage remained the same at 75.991%

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: 1195
Covered Lines: 962
Line Coverage: 80.5%
Relevant Branches: 925
Covered Branches: 649
Branch Coverage: 70.16%
Branches in Coverage %: Yes
Coverage Strength: 27.76 hits per line

💛 - Coveralls

@codspeed

codspeed Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 185 untouched benchmarks
⏩ 3 skipped benchmarks1


Comparing fix/loading-lazy-claim-keeps-nodes (92e79a1) with next (9e85a09)

Open in CodSpeed

Footnotes

  1. 3 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

ryansolid and others added 7 commits October 2, 2026 14:14
…m-keeps-nodes

# Conflicts:
#	scripts/size/floor-caps.json
#	scripts/size/scenarios.js
One guard at the top of insertExpression replaces the hydration-runtime
helper and its two call sites: an untracked region whose incoming nodes
are all already children of the parent returns without touching the DOM.
The first node rejects a fresh render without allocating. +90 B minified
on hydrating bundles instead of +180, and under every existing cap.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The guard now runs only while `sharedConfig.hydrating` is set: the root
pass and each boundary's resume window, where the #3749 landing happens.
Then claimed server nodes are the only nodes of ours already in the
parent, so order needs no check. After hydration completes, inserts
behave exactly as on `next`; a new test pins that.

Encoded without the first-node fast path (the allocation is now
hydration-only): +91 B minified on hydrating bundles, under every cap.

Also marks the #3741 page-base ledger note as accepted.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The hydration in-place guard measures 46,249 B on the base
server-components page against `next` @ 9e85a09's 46,240 (+9 B brotli;
+92 B minified). Ledger note in scenarios.js.

Size-Exception: page base 46.24 -> 46.25 KB: #3749 hydration in-place guard (+9 B brotli, +92 B minified); accepted by the maintainer.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid
ryansolid merged commit e44b2e4 into next Oct 3, 2026
7 checks passed
ryansolid added a commit that referenced this pull request Oct 4, 2026
…oor, −18% hello world, 0 regressions (#3774)

* size(signals): L2 hold model, carve, lanes/verdicts/boundaries/reveal/actions — state at §27.2

Checkpoint of the measurement branch before the §28 replay of the lane
layer. The reactive engine rebuilt on the L2 hold model (transactions as
the one hold relationship), with lanes, verdicts, boundaries, reveal and
actions as modules behind GlobalQueue hooks; stores, affects() and the
attribution engine carved out for later steps. Rulings and per-step
measurements in documentation/plans/size-reduction-carve-step1.md.

Signals 4759: 2125 passed, 2536 carved, 97 failed (66 pre-existing, 15
lane-family, the rest store/affects); web 888, solid 678. Floor 7202 br.

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

* size(signals): carve the lane layer (lanes.ts, verdict.ts) for the §28 replay

Two imports stubbed (createOptimistic's write; isPending/latest), the two
files removed; the core's lane touch points stay as dead, gated code until
S0 rewrites them. Measured: + isPending/latest 9025 → 7250 br (−1775),
page live 39519 → 37710 (−1809), floor 7202 → 7193. Suite 1612 passed
(303 pins carved, 210 in the halt cascade behind them). Numbers and method
in the plan doc §28.15.

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

* size(signals): §28 S0 — the lane layer rebuilt on the slot (lanes.ts, verdict.ts)

Three places per node, each meaning one thing: _value the committed truth,
_pendingValue a transaction's staging, _x._lane the lane's value; the lane
carries shown/held. Seat from carriage; leaves read the screen; a fresh lane
is judged from its first round; body-end corrections at the seam with one
more pure round (#3409); a dissolving lane drops its runs and retires its
flights; lane work reads unparked staging through and a park repairs the
leak. Removes CONFIG_LANE_HELD, CONFIG_HELD_TRUTH, REACTIVE_VERDICT_RERUN
and the seam swap. Two core fixes surfaced: merge resolves both ends
(a resolveTx cycle), and updateIfNecessary keeps the pass-verdict flags on
a pull that recomputed nothing.

Signals 2128 passed (pre-replay 2125), 0 passed→not-passed vs the carved
and the pre-replay baselines; web 888, solid 678. Floor 7222 br (+20),
+ isPending/latest 9213 (+188) — the consolidation is S2 (verdict
unification). Design amendments and numbers in the plan doc §28.15.

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

* size(signals): §28 S1 — holds and seams (A15 LANE, #3463, #3648, #3540 latest(dep), V5)

A leaf reading a lane flight that has never shown waits on it in its own
frame and reads the landed answer at the landing — no new edge, as 28.6
predicted. blocked() records the judged transaction: a zombie is moot for
the commit that disposes it alone; a landed lane's held runs release at
the parent's landing; a lane-dirty zombie runs now. A derivation committed
at a dissolve that re-derives publishes its inputs. flip() lists the
boundary's output in the arming lane at once. blockedBy counts every stale
reader of a flight, lane work included (V5, red since §20). Three re-pins
to the lane rulings (body-end visibility, #3479 boundary display-ahead,
carve-out gates).

Signals 2138 passed (S0 2128; the §28.12 target), 0 passed→not-passed vs
S0 and vs the carved core; web 888, solid 678. Floor 7212 br (−10 vs S0),
+ isPending/latest 9269 (+56). Details in the plan doc §28.15.

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

* size(signals): §28 S2 — verdicts and consolidation (watchers → stagedReaders, staleReader, dissolveLane by outcome)

The verdict watchers merge into the lane layer's stagedReaders (a verdict
reader before the frame's verdict is the same reader as lane work that read
the frame's staging); the four stale-reader registrations become one
staleReader(c, t); observeFlight is verdictRead with a forced rerun; the
verdict HELD arm reads the lane's seam verdict (_held) instead of walking
blocked(); the OVERRIDE arm's tail is display(). dissolveLane restructured
by outcome, with every guess of a dissolving lane re-homed with its truth
(the body-end pre-staging loop was the general rule). verdict.js 2477 →
2091 min; lanes.js 4212 (the §28 rules). The rest of verdictValue stays:
its arms are value semantics, not membership.

Signals 2138 / web 888 / solid 678, 0 passed→not-passed vs S1 and the
carved core. Floor 7208 br (+6 vs pre-replay), + isPending/latest 9212
(+187 — the rulings' rules, each pinned; recorded in §28.15).

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

* size(signals): affects() on L2 — transaction-inert, as ruled (A24 (4)); GlobalQueue hooks declare-only

A mark is a count on the node (_x._marks) listed with its scope (the
transaction's _marks, released at land; or the ambient list, released at
the seam); coverage of derivations is pull-derived at probe time (the walk
over the probed node's current deps, error outranks); verdict readers
re-derive at registration and at the last release. A marked probe in a
transaction's flush is its verdict lane's work (display-ahead, not parked).
Store targets return with the stores. The 23 hook statics on GlobalQueue
emitted as 'static X;' under esnext — now declare-only.

Signals 2156 passed (+18, every signal-half affects pin), 0 regressions vs
S2 and the carved core; web 888, solid 678. Floor 7189 br (−19 vs S2; below
pre-replay and carved), hello world 9688, CSR 12684. Plan doc §29.

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

* size: attribution on L2 — feed the observe engine from the lane/verdict layers

The attribution engine stayed intact through the carve; its feed did not.
This re-feeds it from the L2 hold model:

- holds: holdStart at the parking seam, holdEnd after the next un-parked
  flush's effects, transitionSettled in land, transitionMerged in merge
- hold census: nodes = t._nodes, action = t._open > 0, blockers =
  blockersOf(t) (pending non-effect nodes blockedBy still finds; the
  reporter effect is not a blocker); acknowledgements from _laneGuesses(t)
  (optimistic), t._marks (affects), and CONFIG_VERDICT readers' observe-only
  _devWindows bits (isPending / latest)
- optimistic reverts: supersede / dissolveLane fire for a SHOWN guess
  replaced by a differing truth, "superseded" (a landing) vs "reverted"
  (correction found nothing beneath); the label is observe-only state, not
  a parameter (a parameter leaked ~46 min B into lanes.js)
- fallbacks: boundaryFallback on the swap to / from the fallback with the
  transaction the swap lands with; a mount's synchronous first show
  schedules the drain whose flushEnd is its display instant
- run posture: recomputeEnd's optimistic = lane work, transition = ran
  under a hold

Dev: the SETTLE_WALK_UNINITIALIZED_SOURCE tripwire excludes a lane
derivation's first landing (sits in the lane slot until the lane shows).

Re-pins (shape, not expectation): attribution optimistic-phase (guess in an
action's hold), settle-walk fake node (CONFIG_OVERRIDE + _x._lane),
untracked-read-after-await latest naming (no shadow node: names the source),
treeshake optimistic.ts -> lanes.ts, createMemo resolveAsync and
async-chain-supersession #3374 x3 (PRIMITIVE_IN_EFFECT_CALLBACK shapes:
resolve from the owning scope, compiled <Show keyed> remount),
scheduler-livelock (one heap).

Signals 2219 passed (+63), 0 passed->not-passed vs the affects commit and
the carved core; web 891 (+3: performance-tracks hold/fallback pins);
solid 678. Prod sizes unchanged except +isPending/latest +1 min B; observe
tiers carry the feed (CSR observe+attribution 28470 br vs next's 30654).
Plan doc section 30.

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

* docs(plan): §31 stores on L2 — T0 inventory, design candidate, perf contracts, phases, Q-list

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

* docs(plan): §31.7 stores on L2 — rulings Q-A…Q-G, benchmark references and order

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

* size(signals): §31 S1 — plain stores on L2 (container node, slot literals)

A store is a tree of L2 nodes and nothing else carries reactive state:
one-literal slot nodes (slotSignal) for leaves, presence, the deep witness
and the container node — the $TRACK node given a value, whose _value is the
committed backing and whose _pendingValue is the pending backing, so the
scheduler owns the backing's lifetime (park/commit/revert on later steps)
and the store keeps no fold ledger. A write-created container nobody
subscribes to is released at the fold (transient staging home, INTERNALS §5).

Core seams (the two §31.7 named): CONFIG_SLOT_NODE dispatch at last-one-out
to the store's shared release hook (a leaf has no _x by design — jsfb's
sliding selection keys depend on the release); GlobalQueue._storeCommit
after commitPendingNodes. stagedRead/ownedScopeWriteMessage exported.
mapArray reads $TRACK again. _devWindows is now an observe-literal slot on
computed/effect (the attribution step had it as a write after construction;
found by the dist-artifacts literal pin, re-pinned).

Rules settled by pins: staging visibility is the pass's (`context`, not the
owner — onSettled/handlers see committed); adoption notifies at adoption
time against the view the nodes were last told (#3296; `ab` gone); the
enumerator check is per pass (_gen === _depGen).

Signals 2821 passed (+602), 0 passed->not-passed vs §30 and the carved core;
web 924 (+33); solid 691 (+13). Floor 7215 br (+26); `+ createStore` 11134
= floor +3919 br (gate <= +4000; next +7340). Remaining non-carved: holds
(S2), affects' store half (S5), rules-index, refresh-await.

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

* size(signals): §31 S2 — plain stores under holds; read()'s held arm for untracked pass reads

Every read of a key with a leaf goes through core read(), tracked or not,
so the hold rules (frameRead / joinPass / A28) are core's and the store
restates none of them. Nodes are born from the two frames (bornStaged):
committed from the committed backing, a staging from the pending one when
they differ, held by the container's transaction when it is held — A29
born-held and #3706's per-key hold with no ledger. Structural reads take
the container node's frame (read(k): the node's value is the backing);
a lone descriptor read is gated by its presence node. Untracked reads of
un-noded keys by a pass ask the container without linking; a verdict
window with no reader judges the container only for a changed key (A22).
Adoption is staged (the container's _pendingValue), not eager; snapshot
sees the batch's own staging, not a held one. Fold hook after landings.

Core: read()'s held arm applies to untracked reads by a pass (fast block
and slow path) — pinned by posture-store-parity S4/S5's signal twins,
which had never run on L2. +2 min B.

Re-pins (flagged): posture-store-parity S6 flipped as its text instructs
(store follows signal); visibility-oracle-store structure rows' `latest`
violation re-pinned to the rule (structure rides nodes latest() tunnels).

Signals 2849 passed (+28), 0 passed->not-passed vs S1 and the carved core;
web 924; solid 691. Floor 7213 br; `+ createStore` 11269 (+135).

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

* size(signals): §31 S3a — projections, derived stores, reconcile on L2

The firewall is projection-only and lives in the store module: every read
through a family target pulls the derive first — core read() of it,
untracked when settled (the barrier: no subscription), tracked while a
flight is up or errored (readers observe it as a memo's readers would;
the next settled pass pulls without linking and the stale link trims).
A render effect outside the flight's own flush is the frame, not a stale
reader of the derive. Pending propagates to leaf readers by notifyStatus's
own dependent rule (wakeFamily over the family's live index) and settles
at the landing from each leaf (settleFamily). A derive's continuation
writes join its hold (holdWithDerive). The creation run commits directly
(runDirect); later runs stage like a memo's recompute; a projection's
unheld staging is ahead for a handler. Adoption is eager (INTERNALS §3)
with the container node keeping the committed frame. Chained backings.

Core (flagged): recompute's self-registered-flight probe restored (a
projection's undefined return is not a sync answer — carved as store-only
in 1b, which it is); verdictValue's uninitialized-pending throw links an
untracked reader as read() does.

Re-pins (flagged): createStore.test #2692 trio -> A34 rule B store twins;
projection-slot-release counts the family's live index.

Signals 3185 passed (+336), 0 passed->not-passed vs S2 and the carved
core; web 931 (+7); solid 789 (+98). 19 projection reds open (S3b).
Floor 7257 br (+44); `+ createStore` 13302 (projections + reconcile
coupled; next 16848).

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

* size(signals): §31 S3b — holds on derived stores (#3706, #3688, #3612 store twins)

A pass reading a key the held batch left unchanged reads committed and
holds no one; a changed key reads the container's frame by core's rules
(read(k) untracked). keyChanged covers presence, enumerability,
accessor-ness; a swapped/non-plain prototype or a chained backing changes
every key. A mainline setter above a held adoption is a layer above the
held staging (the container node keeps the adoption; the draft is a
clone): keys the hold left unchanged read the ambient view. A user write
to a held derivation re-derives the fold under the hold (setMemo's twin);
CONFIG_MANUAL_WRITE marks a user proposal so a write made under the hold
keeps last-write-wins. holdWithDerive keyed on the setter's author.

Signals 3188 passed, 0 passed->not-passed vs S3a and the carved core.

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

* size(signals): §35 S4 — optimistic stores on lanes (createOptimisticStore out of carved)

A user write to an optimistic store is a guess on the written key's node,
with presence and container-arrangement guesses (LaneView, judged by row
identity); landings confirm or correct beneath it, settle dissolves it; the
committed backing is never touched. Reads compose per key through the
nodes; a family's guesses stand in for its in-flight derive (A17); chained
views keep their links on the inner's truth (#3672); a user's reconcile
writes the draft. Late-bound (OptHooks): plain stores shed lanes.ts.

Core seams: inFlight/_slotFlight; untracked reads inside lane work derive
from the lane; a joined pass entering a lane is a staged reader;
linkBlocked mirrors blocked(k); supersede resolves the merged parent;
guessedValueOf/pendingGuessOf.

Signals 4707 passed (+1513), 0 passed→not-passed vs S3b and the carved
core; web 998 (+67), solid 812 (+23). 15 no-parent pins re-pinned to lane
contract 2 (three action twins added); S8/oracle supersede → the lane
rules; matrix F7 unpinned. Open: the overlapping-actions arrangement rows
(Q-D's replay evidence), see §35.

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

* size(signals): §35.1 store size pass — attribution, seam trim (−29 br), finding

writeOverride as a live export binding (the get-trap hot path no longer
calls an accessor); the optimistic draft + set-aside staging move into
optimistic.ts (OptHooks.draft; writes returns the staging); optRead()
for the five lane-view sites. + createStore 13968 → 13939, every store
family 27800 → 27783. 0 regressions (signals 4707, web 998).

Per-function attribution of store.js recorded in §35.1: the remaining
store bytes are feature surface (overlay draft, accessor keys, per-key
holds, projection+reconcile by #2883), not seams — the every-store gate
needs a ruling, not a trim.

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

* size(signals): §36 S-U — utils.ts back (merge/omit, views); the carve is closed

next's utils.ts verbatim with three import edits (symbols from
store/types, SUPPORTS_PROXY from constants, dead pendingCheckActive
import dropped). carved.ts has no stubs left.

Signals 4783 (+76, 0 regressions); web 1109 (+111, 0 carved); solid 813.
Page base/live 43402/46969 (next 46193/50442).

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

* size(signals): §37 S5 — affects() store half; StoreNode back; declarations green

store/affects.ts (installed with the stores): affects(store) marks the
record's $AFFECTS carrier and every live node under it, affects(record,
key) the slot's leaf; coverage by raw identity (#2882/#2904) — nodes born
in the window inherit (noteNode seam), untracked probes are witnessed
(verdict.ts _witnessMark, before the family pull — #2910), chained
backings followed to the base.

Signal half (flagged): ambient marks whose flush parks join its
transaction; a mark over a node whose own flight is up (or a slot node
whose family derive is — _slotFlight, now store.ts) stays to the landing
('nothing async below ⇒ no window', and its converse). settle passes the
parked transaction to _releaseAmbientMarks.

StoreNode type exported again as the L2 target (public API; flagged).

Signals 4808 (+25, 0 regressions); web 1109; solid 819/819 (declarations
regenerated). Floor +15 br, + createStore +49, every store family −38.

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

* docs(plan): §38 Q-D decision memo — overlapping actions on one array, the first landing (evidence + options, no pick)

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

* size(signals): §39 Q-D — provenance-gated landings on optimistic stores (option 2′)

A landing is judged against the guess of the question it answers (#3331's
rule for derivations, now for store landings): the derive's writes carry
the flight's question; laneWrite holds an older question's truth beneath
the guess instead of correcting it; an arrangement guess on a keyed list
is re-based over the held truth by key (LaneView.removed keeps deliberate
deletes removed). Value optimism, uniform; replay deferred (§38/§39).

Core (flagged): a corrected SHOWN lane keeps this round's runs (the
body-end seam judged before the lane seam released them — web For over a
chained view lost a landing's row); a chained link's covered value is the
inner's live commit (_slotCovered). Verdict gate built then reverted —
#3331 pins isPending true over a differing stale truth (A24).

Store: an optimistic setter's first read births the draft over a staging
too (#2951: C's +1 composed on the truth, not the display); the get hot
path precedes the backing read and serves a chained guess node (A17).

New pin: tests/store/kanban-a17-fixture.test.ts (Gabriel's kanban).
Matrix 904/904, #2951 compose half, web F1 ×5, 3672 re-run pin.
Signals 4815 (0 regressions vs S5), web 1114, solid 819. Every store
family +670 br (the keyed re-base).

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

* size(signals): §39.1 parity — signal/store twins for Q-D and the kanban shape; one stale() for both landing paths

laneOutcome and laneWrite share stale(el, q): answered(el) (the newest
stamped source changed this round) for a derivation's pass, question for
a write. Behaviour unchanged. New pins run each shape on createOptimistic
nodes and on an optimistic store and assert the same frames.

Signals 4819 (+4), 0 regressions.

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

* size(signals): §40 S3c — projection verdict windows, family pending via core propagation, promise-delivery readers by posture

Signals 22 → 3 reds (rules-index, PR-time); web 2 → 1; solid 0; 0 regressions vs qd8.

Engine: an unanswered probe (answered before the flush had a transaction) re-runs at the seam even when routed into the verdict lane (REACTIVE_PROBE_UNANSWERED); a render effect already observing a derive's flight keeps pulling it (linkedTo) — the frame's hold no longer drops with its flight up; the post-pass family wake requires a flight this pass registered (#3181); handleAsync onError seam + errorFamily (derive rejection reaches leaf readers); wakeFamily stamps leaves as pending-source carriers and propagates via core's propagateStatus (split from notifyStatus); refresh() does not launder a new question; quiet landings stamp their leaves' _reask; resolve/until/refresh waiter deliver own-transaction frames and defer over foreign holds (#3482/#3490); descriptor trap reads the frame's backing (#3706).

Re-pins (plan §40.2, maintainer to veto): A22-3, #3662 step 1, #3585 never-resolving case, #3038 companion walk.

Size: floor 7309 br (+46), +createStore 14169 (+94), every-store 28415 (±0), page live 47348 (+72).

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

* docs(plan): §40.3 — the remaining web red diagnosed (frames rebind held by its own gate under L2)

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

* perf(signals): §41 — family settle walk only after a wake; value reads skip descriptor probes under a staging

Found by the fork-vs-carve benchmark (plan §41). settleFamily ran on every derive commit, flight or not — O(all leaf nodes) per sync re-derive (a 1k-row derived store's no-op tick: 130 µs vs the fork's 9 µs); gated on fam.woke. keyChanged ran __lookupGetter__/__lookupSetter__/propertyIsEnumerable twice per key for every state[i] value read in the flush that staged a reconcile; a value read needs value + presence only (readSource's shape flag — the descriptor trap, bornStaged and the hold check keep the full test; a container with accessors seen keeps it everywhere). 10%-changed keyed reconcile of 1k rows: 996 → 400 µs (fork 270).

Signals 4838/3, web 1115/1, 0 pins moved. Size: +createStore +79 br.

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

* perf(signals): §41.4 — the per-key staging question once per pass; no O(rows) slot re-resolve at the fold

From the jfb-deep two-arm A/B (plan §41.2/§41.4). readSource serves an unheld container staging directly to a plain pass already marked REACTIVE_STAGED_READ (not a verdict reader, not children-forbidden): an unchanged key is the same value in both frames, a changed one is what the mark already stands for — after a shift every index key had 'changed' and each read paid a closure + read() to set a mark the first had set. parentSlotKey short-circuits when the parent slot already holds this target's backing (a parent adopted whole carries its children's new raws before they fold; the indexOf(oldRaw) was a guaranteed O(rows) miss per changed row).

jfb-deep store ops at fork parity in node and interleaved in-browser; 100%-changed keyed reconcile 0.99 → 0.81 ms (fork 0.94). Signals 4838/3, web 1115/1, 0 pins moved; +createStore +16 br.

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

* test(signals): re-point next-smoke and owner-stamp at the rebuilt store (14/14 pins hold)

The two suites imported the removed src/store/next/*; createStore / storeLookup / isOwned / $OWNER from the carve's store answer the same pins.

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

* perf(signals): §41.5 — Q-A escape hatch: container node only when a hold is live or a subscriber exists; allocation-free fold queue

stageOn: a staging gets its container node only when something can read the frame through it (a structural subscriber, lane work, a creation-time pass, a held derive); otherwise the backing swap and the fold queue are the whole staging. GlobalQueue._storePark materializes and holds the node for every node-less staging when the flush parks (pinned: a never-read key of a held container reads committed until the landing). getContainerNode born under a staging takes it as its own; committed()/readSource/bornStaged handle the node-less case. The fold queue is a reusable list (membership = pb !== null) plus a WeakMap of pre-batch backings written in place — the per-batch Map cost ~540 KB/tick of table allocation on dbmon's ~7000 containers and most of its GC.

dbmon-deep tick: node parity; browser 1.17× → 1.08–1.135× (harness method); remaining allocation is the per-adopted-raw lookup registration (§41.5). Signals 4853/3 (+1 pin), web 1115/1, solid 819/0; +createStore +124 br.

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

* fix(web): frames — re-arm the switch gate and rebind in the pass, not the effect run (mid-flight second switch deadlocked under the hold model)

The follow effect's run was stashed with the frame the previous gate held, behind the gate the rebind would release (plan §40.3 diagnosis). Web 1116/0.

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

* fix(html): exit raw text mode after a self-closing raw text element (#3729) (#3730)

<textarea /> put the tokenizer in raw text mode because only the token
before the last was checked for a slash, so the rest of the template was
read as text. Check the last token as well; closing tags still have
their slash second to last.

Fixes #3729

(cherry picked from commit 6be8486)

* fix(html): component props named on/only/once get getters like any dynamic prop (#3728) (#3751)

The getter-wrapping exemption for `on*` names applied to components as well
as native elements, so `<${Loading} on=${() => key()}>` passed a function
that never changed. Components now exempt only camelCase `onXxx` handlers
(and `ref`); native elements are unchanged.

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit a5d0e76)

* fix(compilers,web): compiled SSR style objects no longer glue a computed key onto the previous entry (#3737)

* fix(compilers,web): compiled SSR style objects no longer glue a computed key onto the previous entry

The compilers chained `ssrStyleProperty` calls with the `;` baked into each
later literal name; a computed key's name got none, so
`style={{ color: "red", [k]: v }}` rendered `color:redtop:1px`.

* test(web): run ssrStyleProperties spec under the server vitest config

The server config only includes test/server/**/*.spec.tsx, so the .spec.ts
file never ran in CI.

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

---------

Co-authored-by: Ryan Carniato <ryansolid@gmail.com>
Co-authored-by: Claude via Cursor <noreply@cursor.com>
(cherry picked from commit 98d35b9)

* fix(compiler): load the WASI fallback where / is not accessible (Android) (#3758)

The @solidjs/compiler-wasm32-wasi loader is generated by `napi build`, and
@napi-rs/cli 3.8.6 emitted a WASI preopen of the host filesystem root. On
Android/Termux `/` cannot be opened, so `new WASI(...)` failed with
UVWASI_EACCES before the compiler ran. napi-rs fixed the template upstream
(napi-rs/napi-rs#3485, in 3.9.1+): on Android the guest root maps to the
working directory, and the main thread passes that root to its workers.
Bump @napi-rs/cli to 3.10.6 so the published loader carries it.

The WASI test run gains a regression test that loads the binding with
`node:wasi` rejecting host-root preopens, as Termux does: it fails with the
3.8.6 loader and passes with the regenerated one.

Fixes #3748

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 41be120)

* fix(web): spread applies attributes before children, matching ssrElement (#3741) (#3757)

A dynamic() / <Dynamic> element whose prop getter mints a hydration id (a
ternary over a signal compiles to a condition memo inside the getter) and
whose children include an element hydrated with a key miss: the client's
runtime spread() inserted `children` before its attribute effect read the
getters, while the server's ssrElement read attributes first. Both draw on
the enclosing component's id counter, so the memo and the child element
swapped ids and the server's child was never claimed.

spread() now creates its attribute effect before the children insert -- the
order a compiled element already uses (its spread/attribute effects precede
its children inserts). ssrElement defers reading a `children` prop until
after every attribute (and the trailing compiled attribute thunk), so a
`children` key listed before an id-minting attribute agrees too.

Side effect on the client: a ref on a dynamic() element now runs before its
children are inserted, as on a compiled element.

Size: page: base +59 B brotli (+1 B minified), layout only; frozen cap
raised 46.20 -> 46.24 KB with a Size-Exception note.

Fixes #3741

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 365f37d)

* fix(web): stylesheet-gated fragments reveal under a nonce CSP and when a sheet loads early (#3755)

* fix(web): stylesheet-gated fragments reveal under a nonce CSP and when a sheet loads early

A fragment whose boundary streamed stylesheet links waited on inline
`onload`/`onerror` handlers. Those can't carry a nonce, so under a strict
`script-src` CSP the gate never released and the boundary stayed on its
fallback. The links now carry `data-dfc`, and the fragment helpers install
one capture-phase `load`/`error` listener, from the nonce-carrying task
script, that counts each link down once.

The links were also written ahead of the content template. A sheet that
loads before the parser reaches the template (a cached sheet, or a chunk
boundary between the two) ran `$dfc` while the template was missing, and
`$dfr` treats a missing template as a swap that already ran, so the reveal
was dropped. The template now goes first.

Fixes #3747

* test(web): remove the stylesheet gate spec's document listeners between tests

Each test evaluates the stream helpers, which install their capture-phase
listener on the shared jsdom document. Record and remove them so a test only
sees listeners its own scripts added, which is what the count-once test means
to exercise.

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

---------

Co-authored-by: Ryan Carniato <ryansolid@gmail.com>
Co-authored-by: Claude via Cursor <noreply@cursor.com>
(cherry picked from commit b57eb2e)

* fix!: lowercase on* names are attributes, not event handlers (#3753)

* fix!: lowercase on* names are attributes, not event handlers

Only `on` followed by an uppercase letter (`/^on[A-Z]/`) is an event
handler. Lowercase names such as `onclick` are plain attributes everywhere:
both compilers (DOM, hydratable, dynamic and SSR), the web runtime's
spread/assign and server spread walk, useHead, and the html/h element
paths. A leftover `on:click` lowering (to a `:click` listener) is gone;
the name is now an ordinary namespaced attribute.

Dev builds warn once per name with LOWERCASE_EVENT_ATTRIBUTE when a
function reaches setAttribute under an `on*` name. ssrAttribute now
escapes function values instead of interpolating their source raw.

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

* fix(html)!: component props follow h's getter rule, no onXxx exemption

On components every zero-argument function prop, `onXxx` handlers and
`ref` included, becomes a getter, matching @solidjs/h and 1.x
hyper-dom-expressions: a component handler declares its event argument.
This reverts #3751's unreleased component exemption. Elements keep the
`ref` / `onXxx` exemption; lowercase `on*` stays an attribute.

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

* perf(web): keep the per-key event check off the regex engine

The server spread walk and the client's assignProp test every key of a
spread for `onXxx`. A `/^on[A-Z]/` test there regressed the
spread-static-tail renderToString benches by 5-7% against the
`startsWith("on")` it replaced. Gate on `startsWith("on")` and compare one
char code, so a key not starting with `on` costs what it did before.
`ssrAttribute` takes strings first, ahead of the new function branch, and
h tests a prop's type before its name. Behavior is unchanged.

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

---------

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 4f67697)

* fix(h): materialize elements without mutating the caller's props; correct the render root form (#3756)

* fix(h): materialize elements without mutating the caller's props

An h() element is a thunk that materializes each time its consumer reads
it, but materializing wrote to the caller's props object (children assigned,
zero-arity props turned into getters, class rewritten), so a second
materialization threw — e.g. a Show re-showing a child whose children are
h() elements. Props are now built copy-on-write.

A thunk returned from an accessor child reached insert's inner pass and was
re-created whenever its output changed (a Loading over async children never
settled). It is now materialized in the insert's tracking pass, where JSX
would create the component.

The README's render(() => h(App), el) / render(App, el) root forms hit the
same re-creation through render's insert; it now prescribes render(h(App), el),
and h's call type no longer includes the array passthrough so that form
type-checks.

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

* docs(h): lead with the render(h(App), el) rule

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

---------

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit 9e85a09)

* fix(web): a late hydration claim leaves server nodes in place (#3749) (#3760)

* fix(web): a late hydration claim leaves server nodes in place (#3749)

A settled <Loading> whose lazy() module is still loading when hydration
starts returns nothing in the root pass and claims its server nodes on a
later resume. The insert holding the boundary's value sits outside the
boundary: its claim pass saw `undefined` and tracked the region as empty,
so on resume it took the client path (appendNodes / appendChild) and
re-inserted the connected, already-claimed nodes, moving them and
blurring a focused input.

insertExpression now leaves nodes in place when an untracked region
receives nodes already sitting in it in order. The check lives on the
hydration runtime slot, so client-only bundles carry none of it.

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

* fix(web): shrink the late-claim in-place check (#3749)

One guard at the top of insertExpression replaces the hydration-runtime
helper and its two call sites: an untracked region whose incoming nodes
are all already children of the parent returns without touching the DOM.
The first node rejects a fresh render without allocating. +90 B minified
on hydrating bundles instead of +180, and under every existing cap.

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

* fix(web): scope the late-claim in-place guard to hydration (#3749)

The guard now runs only while `sharedConfig.hydrating` is set: the root
pass and each boundary's resume window, where the #3749 landing happens.
Then claimed server nodes are the only nodes of ours already in the
parent, so order needs no check. After hydration completes, inserts
behave exactly as on `next`; a new test pins that.

Encoded without the first-node fast path (the allocation is now
hydration-only): +91 B minified on hydrating bundles, under every cap.

Also marks the #3741 page-base ledger note as accepted.

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

* chore(size): raise the page-base cap to 46.25 KB for #3749

The hydration in-place guard measures 46,249 B on the base
server-components page against `next` @ 9e85a09's 46,240 (+9 B brotli;
+92 B minified). Ledger note in scenarios.js.

Size-Exception: page base 46.24 -> 46.25 KB: #3749 hydration in-place guard (+9 B brotli, +92 B minified); accepted by the maintainer.

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

* ci: re-run the size gate against the PR body's Size-Exception line

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

---------

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
(cherry picked from commit e44b2e4)

* chore(size): keep the size comparison green on fork PRs (#3765)

A fork PR's token is read-only, so posting the report comment answered 403
and the compare job showed as failed on every external PR. The report now
also goes to the job summary, and the comment only runs on PRs from this
repository.

(cherry picked from commit 4738c6d)

* fix(signals): derived writes apply first, then derivations re-run (#3742)

A manual write to createSignal(fn) / createStore(fn) lands at once and never
re-runs the derivation on its own; a source change, in the same flush or
later, inside or outside an action, across a hold, re-runs it with the write
as `prev` (the store's draft). Reverses #2692's beta.11 same-tick precedence
(maintainer-approved): within a flush call order isn't observable, and for
stores a partial write dropped the whole fold. Supersedes #3740's
frame-scoped mask: the heap refusal, `_manualWriteTime` and the setter's
deleteFromHeap / DIRTY-CHECK clear are removed; REACTIVE_MANUAL_WRITE keeps
only its A34 proposal role. Refs #3733.

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

(cherry picked from commit 9a213bbb514ca313bdefc5bfe31d072e94409c65; the carve's core already implements rule B — its pins (createMemo R31 ×7, 3612 ×19, 3733 ×n) pass unchanged — so core.ts/heap.ts/types.ts keep the carve's text; next's docs, tests, changeset, signals.ts doc comment and solid hydration change are taken; src/store/next/* stays deleted)
Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(signals): a derive reading a held key through its draft joins the holder (#3733 action-hold pin)

readSource's draft/write-override arm: for the derive (write override, not a user write), a held leaf — or a held container whose held staging changed the key — is the future; the pass joins it (joinPassTx), so the re-derived result reveals with the action. Brought over with #3742's tests: 4890/3, 0 moved.

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

* docs(signals): the spec pass — "The hold model — L2" in SPEC-ASYNC-SEMANTICS, amendments on A18/A19/A22/A29/A34, INTERNALS §0, RULES-INDEX regenerated

rules-index ×3 green (57 src ids resolving; every live A-rule cited by a test). src/ comments cite plan sections as 'plan sec. N' (the §N grammar is INTERNALS-STORE-STATE's). Plan §42 records the finish-out: signals 4894/0, web 1132/0, solid 819/0, compiler 5957, babel-plugin 271, h 70, html 206.

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

* docs(signals): every A-rule's Mechanism paragraph rewritten for L2; the pre-L2 index moved to History verbatim

32 rules now carry 'Mechanism (L2, 2026-10-04)' against the running code (scheduler/core/lanes/verdict/boundaries/store by function); the 2026-09-14/15 paragraphs are under History as 'Pre-L2 mechanism index'. RULES-INDEX regenerated; rules-index 3/3. Plan §42 records the 2026-10-04 rulings (store gates: none; deep tick: accept; Q-D unkeyed: leave; fate: one PR against next).

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

* docs(signals): regenerate RULES-INDEX after merging origin/next (citation census moved with #3742's treeshake note)

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

* size: frames eager client 12.98 -> 13.00 KB for the switch-gate fix (+21 B)

Size-Exception: the frames client's address switch re-arms the gate and rebinds in the pass, not the effect run (#3774, e133516) — under the hold model the run was stashed behind the gate it would release; the two `bound` locals and the `ownedWrite` option are the bytes. Measured 12,997 B against next @ 41fdf96's 12,976. Pending maintainer review.
Co-authored-by: Cursor <cursoragent@cursor.com>

* size: frames eager client ledger note — CI measurements (+20 B br, +75 B min), accepted by the maintainer

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

* perf(signals): record a pre-batch backing only for adoptions — fresh one-key stores 2.1 → 1.27 ms per 2000 (next 1.13)

CodSpeed flagged 'fresh stores: create + first write + first commit' −13%; on the built dist it was 2×: every fresh draft recorded a weak-map entry for a backing that never moves until the fold. Adoptions keep the weak map (written in place for reused containers — dbmon's steady state is unchanged); drafts record nothing; a mid-batch privatization or a draft over an adopted raw is listed in a small per-batch map (preBatch). Signals suite green, web green.

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

* size: lower every cap to the L2 measurements + 10 B (the ratchet)

floor 9.51 -> 7.33 KB, render+signal 12.05 -> 9.81, hydrating 19.69 -> 17.64, page base 46.25 -> 44.03, page live 50.45 -> 47.67 (floor-caps.json); +createStore 16.85 -> 14.53, +isPending 12.16 -> 9.45, every-store 30.82 -> 28.78, CSR 14.94 -> 12.82, observe 16.48 -> 14.39, attribution 30.71 -> 28.61 (inline). Measured by CI at 22c3d3e; frames (13.00) and the server caps unchanged. Lowering is always allowed; raises need a Size-Exception.

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

---------

Co-authored-by: Claude via Cursor <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Younes Beriane <paranoyouz@gmail.com>
Co-authored-by: Artem Samofalov <dex157@gmail.com>
Co-authored-by: Éverton Toffanetto <evertondgn@hotmail.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