Skip to content

fix(signals): L2 fuzz regressions under existing rules (groups 1, 2, part of 3) - #3853

Merged
ryansolid merged 7 commits into
nextfrom
fix/l2-fuzz-existing-rules
Oct 7, 2026
Merged

ryansolid merged 7 commits into
nextfrom
fix/l2-fuzz-existing-rules

Conversation

@ryansolid

@ryansolid ryansolid commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Fixes L2-rewrite correctness regressions that the semantic fuzzer found by comparing pre-L2 41fdf9696 with next 49a8dca84 (rev-19 oracle; seeds 3289 and 91501, 1000 cases per cohort). These are bugs under existing rules; no new rulings are used.

+ isPending/latest grows by +120 B minified. The maintainer accepted the cost with a size exception (2026-10-07); the three caps that fail the gate are raised to CI brotli + 10 B with dated ledger notes in scripts/size/scenarios.js (see Size).

Size-Exception: L2 fuzz regressions under existing rules (+120 B minified on signals: + isPending/latest, carried into app: hydrating + every store primitive family and the frozen page: live server components floor; app: CSR, observe tier + attribution engine enabled is −4 B minified but over its cap on layout with the allowance used up on next) — accepted by the maintainer 2026-10-07.

Fixes

Group 1: a superseded value left showing after completion (A15 lanes corollary)

  • Root cause. When the parent lands, dissolveLane (lanes.ts around line 414) commits a lane derivation that is still in flight. It wasn't marked CONFIG_INPUTS_PUBLISHED, meaning "committed beneath inputs already on screen". The verdict branch of read() (core.ts around line 1691) then served that committed value (A10). The result is a superseded value beside inputs that are the truth now.
  • Fix.
    • A derivation still pending at the landing is marked as committed beneath published inputs (lanes.ts around line 466).
    • observeFlight (verdict.ts around line 106) declines to serve such a flight, so the reveal observes it (A15 reveal corollary, fix(signals): hold conditional reveals on existing async work #3305).
    • A correction drops the runs that a lane's seam parked (l._held, lanes.ts around line 517). Those runs show a re-guess the seam never revealed.
  • Tests.
    • a mount over a flight the landing committed beneath its inputs observes the flight
    • a correction of a shown lane drops the runs its held re-guess parked
    • a verdict memo does not serve a flight committed beneath its inputs (Group 3 bucket reversing order of operator? #5, same mechanism)

Group 2: a mount reads a memo over a lane flight (F8: memo in between, born held)

  • Root cause. A lane pass's pending propagation (propagateStatus, async.ts around line 914) listed a mainline render effect on the lane. The effect was either born held or mid-pass, having pulled the memo itself. The seam's hold loop then skipped it as lane-owned, so the mount's transaction never held, and the control published without its child.
  • Fix. laneStage's leaf branch (lanes.ts around line 348) declines such a leaf when the propagating node has a value on screen. The propagation passes its source as the existing "no answer" argument, so no new hook is needed. The leaf's pending is queued as mainline's, and the frame holds. A node born in the lane, which has nothing on screen, still waits as the lane's (2.0.0-rc.13 async isPending derivation makes rendered memo values disagree #3766).
  • Tests.
    • a mainline mount over an existing memo of a never-shown lane flight is born held
    • a born-held mount stays held when the body's correction re-asks the lane's flight

Group 3: tear after the action body ends, no mount (partial)

All five tests are in packages/signals/tests/l2-fuzz-existing-rules.test.ts. Each fails on next and passes here.

Size

CI Size run 37599458941, head = this branch merged with next @ 2eb6e00c0 (after #3850 and #3846):

Scenario Brotli (base → head) Minified (base → head) Cap
signals: + isPending/latest 9,521 → 9,561 (+40) 26,842 → 26,962 (+120) 9.49 → 9.58 KB, recorded minified 26,828 → 26,962
app: hydrating + every store primitive family 28,886 → 28,951 (+65) 91,702 → 91,798 (+96) 28.93 → 28.97 KB, recorded minified 91,684 → 91,798
app: CSR, observe tier + attribution engine enabled 28,635 → 28,686 (+51) 86,445 → 86,441 (−4) 28.66 → 28.70 KB, recorded minified 86,419 → 86,441
signals: core floor 7,363 → 7,374 (+11) 20,148 → 20,144 (−4) unchanged (warn, within allowance)
app: render + one signal 9,858 → 9,884 (+26) 27,702 → 27,698 (−4) unchanged (warn, within allowance)
page: live server components (frozen floor) 48,803 → 48,873 (+70) 158,257 → 158,377 (+120) 48.82 → 48.89 KB (floor-caps.json), recorded minified 158,257 → 158,377

One frozen floor cap changed (page: live, above). Every other scenario is −4 B minified or unchanged.

Cost per piece

These are leave-one-out costs against the final source. Minified bytes are deterministic and add up to the total. Brotli bytes are layout-sensitive and don't add up (see scripts/size/README.md).

Piece + isPending/latest min brotli (approx.) Core four, min Serves
Pending-seat check in laneStage (Group 2) +83 ~+21 −1 Group 2 (all 5 buckets); fuzz case 416
observeFlight guard (Group 1) +21 ~+8 −3 Group 1 (#3, #9, Group 3 #5); fuzz case 827
Pending derivation marked at the landing (Group 1) +9 ~+6 0 Group 1 test 1 and Group 3 #5 test; 3 fuzz cases on seed 3289
Correction drops parked runs, l._held (Group 1, #24) +7 ~+6 0 Group 1 test 2 (no change in these fuzz cohorts)
Total +120 −4

Totals per scenario, against next (local measurement before the merge of #3824/#3850; CI's numbers are in the table above):

Scenario Minified Brotli
app: render + one signal −4 +11
signals: core floor −4 +1
signals: + createStore −4 0
signals: + isPending/latest +120 +35

The pending-seat check is about 60 B of minified code. Each clause was checked:

Moving the check into propagateStatus would make every core scenario pay for it.

Shrink history: the WIP commit was +142. Folding the hook into laneStage and reusing l._held brought it to +120, and the core scenarios went from −1 to −4.

Fuzz, rev-20 harness (failing cases out of 1000)

Cohort Seed next This PR
latest 3289 65 42
latest 91501 56 30
mount-under-hold 3289 334 334
mount-under-hold 91501 343 343

Checked case by case: no case that passes on next fails here, and no failure signature changed.

Read-order cases 416 and 827

These were bucketed as read-order, and both pass here without any change to route(). They share root causes with Groups 2 and 1; it isn't a coincidence of the shrunk cases. Knockouts confirm it: each case fails again only when its piece is removed.

  • Case 416 (latest, seed 3289; S2: "published mount control disagrees with its owned reader"). Mount reader 0 reads memo 3, which sits over memo 1, an in-flight lane flight. This is the Group 2 shape. It fails again only when the pending-seat check is removed.
  • Case 827 (latest, seed 91501; S1: torn tuple [0,3,1] then [0,1,1]). At the landing, node 0's superseded value 3 is committed beneath the new input 0 while its re-fetch 0:[0]#2 is in flight. The latest reader is served the committed 3. This is the Group 1 shape. It fails again only when the observeFlight guard is removed.
  • Case 936 still fails. Its mount reads the flight directly, with no memo in between. That's the real read-order case, so the read-order step is still needed for it.

Interactions

Public API changes

None. Only internal GlobalQueue hook signatures changed:

  • _laneStage's fourth argument may now be the propagating node.
  • _observeFlight now returns a boolean.

ryansolid and others added 2 commits October 6, 2026 15:19
…part of 3)

Over the size budget on + isPending/latest (+142 B min); shrink follows.
Read-order cases 416 and 827 pass without touching route().

Co-authored-by: Cursor <cursoragent@cursor.com>
- The pending-seat check decides in laneStage's leaf branch (the propagation
  passes its source as the no-answer argument) instead of a new
  GlobalQueue hook; the decline queues the mainline pending itself, so
  propagateStatus keeps its call shape (core -4 B).
- A correction drops the runs a lane's seam parked (l._held), the fact
  the seam already records, instead of re-judging blocked(l).
- The landing keeps its laneValueOf commit (the slot-only variant had no
  effect in the suite or the fuzzer).

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

changeset-bot Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f45bfe0

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

This PR includes changesets to release 12 packages
Name Type
@solidjs/signals Patch
test-integration Patch
@solidjs/web Patch
@solidjs/babel-plugin Patch
@solidjs/compiler Patch
@solidjs/diagnostics Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
solid-js Patch
@solidjs/universal Patch
todos-server-example 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.37 KB +11 B (+0.1%) −4 B +11 B 7.35 KB ⚠️ over by 24 B, 9 B minified headroom
signals: + createStore 14.60 KB +58 B (+0.4%) −4 B +11 B 14.56 KB ⚠️ over by 43 B, 9 B minified headroom
signals: + isPending/latest 9.56 KB +40 B (+0.4%) +120 B 0 B 9.58 KB ✅
app: render + one signal (the simple-app floor) 9.88 KB +26 B (+0.3%) −4 B +11 B 9.86 KB ⚠️ over by 24 B, 9 B minified headroom
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 17.74 KB +7 B (+0.0%) −4 B +15 B 17.73 KB ⚠️ over by 8 B, 5 B minified headroom lazy-page.js 0.04 KB
app: hydrating + every store primitive family 28.95 KB +65 B (+0.2%) +96 B 0 B 28.97 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 12.89 KB +16 B (+0.1%) −4 B +16 B 12.86 KB ⚠️ over by 30 B, 4 B minified headroom lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 14.43 KB −17 B (−0.1%) −4 B +15 B 14.46 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 28.69 KB +51 B (+0.2%) −4 B 0 B 28.70 KB ✅ lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 10.04 KB −1 B (−0.0%) −4 B +11 B 10.05 KB ✅
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 25.20 KB +55 B (+0.2%) −4 B +15 B 25.13 KB ⚠️ over by 67 B, 5 B minified headroom stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 30.96 KB −13 B (−0.0%) −4 B +15 B 31.03 KB ✅ stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 13.97 KB 0 B 0 B 0 B 13.98 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 45.06 KB −41 B (−0.1%) −4 B −4 B 45.12 KB ✅ decode.js 6.07 KB, lazy-page.js 0.04 KB
page: live server components (base + live/GET + action + isPending/latest) 48.87 KB +70 B (+0.1%) +120 B 0 B 48.89 KB ✅ 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 24 B; minified 20,144 B vs 20,133 B recorded with the cap (+11 B) — 9 B of the 20 B minified allowance left; −4 B minified over this PR's base
  • signals: + createStore: over brotli cap by 43 B; minified 44,407 B vs 44,396 B recorded with the cap (+11 B) — 9 B of the 20 B minified allowance left; −4 B minified over this PR's base
  • app: render + one signal (the simple-app floor): over brotli cap by 24 B; minified 27,698 B vs 27,687 B recorded with the cap (+11 B) — 9 B of the 20 B minified allowance left; −4 B minified over this PR's base
  • app: hydrating (no stores) with Show/For/Loading/Errored/lazy: over brotli cap by 8 B; minified 52,641 B vs 52,626 B recorded with the cap (+15 B) — 5 B of the 20 B minified allowance left; −4 B minified over this PR's base
  • app: CSR with Show/For/Loading/Errored/lazy: over brotli cap by 30 B; minified 36,584 B vs 36,568 B recorded with the cap (+16 B) — 4 B of the 20 B minified allowance left; −4 B minified over this PR's base
  • app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store): over brotli cap by 67 B; minified 78,913 B vs 78,898 B recorded with the cap (+15 B) — 5 B of the 20 B minified allowance left; −4 B minified over this PR's base

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

coveralls commented Oct 6, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 37599894907

Coverage remained the same at 75.927%

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: 1199
Covered Lines: 964
Line Coverage: 80.4%
Relevant Branches: 932
Covered Branches: 654
Branch Coverage: 70.17%
Branches in Coverage %: Yes
Coverage Strength: 28.22 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/l2-fuzz-existing-rules (f45bfe0) with next (2eb6e00)

Open in CodSpeed

…rules

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

# Conflicts:
#	packages/signals/docs/RULES-INDEX.md
@ryansolid
ryansolid marked this pull request as ready for review October 7, 2026 08:56
ryansolid and others added 3 commits October 7, 2026 01:56
Raises signals: + isPending/latest (9.49 -> 9.58 KB), app: hydrating +
every store primitive family (28.93 -> 28.97 KB) and app: CSR, observe
tier + attribution engine enabled (28.66 -> 28.70 KB) to CI brotli + 10 B
with their recorded minified, from Size run 37596666473. Accepted by the
maintainer 2026-10-07.

Co-authored-by: Cursor <cursoragent@cursor.com>
48.82 -> 48.89 KB, recorded minified 158,377 B, from Size run
37599458941 after #3846 re-based the floor. Accepted by the maintainer
2026-10-07.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ryansolid
ryansolid merged commit fd9f381 into next Oct 7, 2026
8 checks passed
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