From 04790534148b8c562879dd4b3d9e74b0b06bbeca Mon Sep 17 00:00:00 2001
From: Ryan Carniato
Date: Tue, 6 Oct 2026 00:42:27 -0700
Subject: [PATCH 1/5] fix(signals): a fresh Loading over a held value shows its
fallback in a flush (#3540, L2 regression)
L2 regressed A29's boundary exemption in two ways:
- In a flush, a first pass under a loading boundary that has not shown
content joined the flush to the hold it read, so the whole tick (the
Show that mounted the boundary included) waited for the hold. A first
pass that reads a hold now asks the boundaries up its chain
(GlobalQueue._fresh); one such boundary catches it, and the pass joins
pass-scoped (passTx), as a mount outside a flush does: the boundary's,
not the tick's. A derivation outside the boundary in the same flush
still holds the tick. A flush that set passTx clears it at its end.
- A render effect born held under a fresh loading boundary (content bound
in JSX: the tree never reads the held value) was not caught, so the
boundary revealed empty content, in a flush and out of one. It is now
collected as a reader, and the output's first pass shows the fallback
whenever the boundary has collected readers.
Refs #3540
Co-authored-by: Claude
Co-authored-by: Cursor
---
.changeset/fresh-loading-fallback-in-flush.md | 5 +
packages/signals/docs/RULES-INDEX.md | 96 ++++----
packages/signals/docs/SPEC-ASYNC-SEMANTICS.md | 2 +
packages/signals/src/boundaries.ts | 30 +--
packages/signals/src/core/core.ts | 20 +-
packages/signals/src/core/scheduler.ts | 23 +-
.../loading-fallback-in-flush-3540.test.ts | 228 ++++++++++++++++++
.../loading-fallback-in-flush-3540.spec.tsx | 119 +++++++++
8 files changed, 445 insertions(+), 78 deletions(-)
create mode 100644 .changeset/fresh-loading-fallback-in-flush.md
create mode 100644 packages/signals/tests/loading-fallback-in-flush-3540.test.ts
create mode 100644 packages/web/test/loading-fallback-in-flush-3540.spec.tsx
diff --git a/.changeset/fresh-loading-fallback-in-flush.md b/.changeset/fresh-loading-fallback-in-flush.md
new file mode 100644
index 000000000..ef486c3c1
--- /dev/null
+++ b/.changeset/fresh-loading-fallback-in-flush.md
@@ -0,0 +1,5 @@
+---
+"@solidjs/signals": patch
+---
+
+A fresh `Loading` mounted over a held value shows its fallback in a flush too (#3540 under L2): a first pass a loading boundary that has not shown content catches is the boundary's, not the tick's, so the mount publishes and the content reveals at the commit. Content bound by a render effect under the boundary is collected, so the boundary no longer reveals empty content.
diff --git a/packages/signals/docs/RULES-INDEX.md b/packages/signals/docs/RULES-INDEX.md
index 2b199c54f..5c10bf7a7 100644
--- a/packages/signals/docs/RULES-INDEX.md
+++ b/packages/signals/docs/RULES-INDEX.md
@@ -45,66 +45,66 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul
| id | status | defined | cited in src | cited in tests | statement (at definition) |
|---|---|---|---|---|---|
-| A1 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:307` | — | onCleanup.test.ts×2 transitionEntanglement.test.ts×4 | [ruled 2026-07-06] Effect error interception is compute-phase only — `EffectBundle.error` intercepts compute-phase errors only; effect-phase throws escalate to the nearest error boundary (halt if none… |
-| A2 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:315` | — | onCleanup.test.ts×2 | [ruled] Unhandled compute-phase errors in user effects are logged and skipped — Compute-phase errors in _user_ effects without a handler are logged and the run is skipped; the system keeps running. |
-| A3 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:323` | — | derived-write-then-derivation-3733.test.ts×5 equals-comparator-errors.test.ts×1 | [ruled] Comparator throws are compute-phase errors — Errors thrown by a user `equals` comparator behave exactly like compute-phase errors (boundary-containable; loud halt without a boundary). |
-| A4 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:331` | — | derived-write-then-derivation-3733.test.ts×1 equals-comparator-errors.test.ts×1 | [ruled] A custom `equals` never sees `undefined` prev on first commit — A custom `equals` is never invoked with `undefined` previous value on a node's first commit. |
-| A5 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:339` | — | derived-write-then-derivation-3733.test.ts×1 errorHalt.test.ts×1 | [ruled] An error escaping every boundary halts the system — An error escaping every boundary permanently halts the system with `REACTIVITY_HALTED`; later writes log "Update ignored". |
-| A6 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:347` | boundaries.ts×2 | derived-write-then-derivation-3733.test.ts×1 enforceLoadingBoundary.test.ts×1 | [ruled] `ASYNC_OUTSIDE_LOADING_BOUNDARY` is warn-only — `ASYNC_OUTSIDE_LOADING_BOUNDARY` is a warn-only diagnostic; an `Errored` above must not swallow it and must not show its fallback for a pending. |
-| A7 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:153` | — | fuzz-findings-l2.test.ts×1 spec-async-semantics.test.ts×2 visibility-oracle-store.states.ts×1 visibility-oracle.states.ts×1 visibility-oracle.test.ts×1 | [ruled, amended in place] Resolved async never reads `[false, undefined]` — After an async memo resolves, `[isPending(x), latest(x)]` is `[false, resolvedValue]` — never `[false, undefined]`. **Amende… |
-| A8 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:161` | — | createMemo.test.ts×1 visibility-oracle-store.states.ts×1 visibility-oracle.states.ts×2 | [ruled, amended in place 2026-07-07] `isPending(() => latest(x))` follows `x`'s own async only — verdicts are per-channel — (**re-ruled 2026-07-07c** — was "tracks the transition the same as `isPendin… |
-| A9 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:169` | projection.ts×1 target.ts×1 | spec-async-semantics.test.ts×3 visibility-oracle-store.states.ts×4 visibility-oracle-store.test.ts×1 | [ruled, amended in place 2026-07-07] Store leaves behind a firewall report the firewall's new-question refetch — `isPending` on a store leaf behind a firewall reports the firewall's refetch like any a… |
-| A10 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:177` | core.ts×3 verdict.ts×2 | createMemo.test.ts×1 fuzz-findings-l2.test.ts×1 ispending-memo-unstamped-hold-3457.test.ts×2 latest-isPending-consistency.test.ts×3 verdict-contract.test.ts×2 | [ruled] `[isPending(x), x()]` is atomic within one scope — `[isPending(x), x()]` read in one scope is atomic: a reader that observed the fresh value must not see `pending === true` for it. |
-| A11 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:95` | — | latest-isPending-consistency.test.ts×1 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×1 | [ruled] Sync derivations of held sources are visible through `latest()`/`isPending()` — Sync derivations of transition-held sources are visible through `latest()`/`isPending()` (held sync recompute is… |
-| A12 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:185` | — | createOptimistic.test.ts×2 spec-async-semantics.test.ts×1 | [ruled, amended in place] Resting optimistic nodes report pending like a plain memo — A resting optimistic node reports pending via exactly the causes a plain async memo does (A19) — a reverting optim… |
-| A13 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:193` | — | spec-async-semantics.test.ts×7 | [ruled 2026-07-06 (promoted from B1)] Resting optimistic ≡ plain async memo at every checkpoint — (was B1) A resting optimistic node (no active override) is observationally identical to a plain async … |
-| A14 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:201` | — | spec-async-semantics.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B2)] Companion nodes get child lanes that do not merge with the owner — (was B2) `isPending`/`latest` companion nodes get child lanes that do not mer… |
-| A15 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:237` | async.ts×2 constants.ts×1 core.ts×3 lanes.ts×4 scheduler.ts×14 verdict.ts×3 | async-chain-supersession.test.ts×1 first-observer-stale-reader.test.ts×1 fuzz-findings-l2.test.ts×17 held-restore.test.ts×1 l2-contract.test.ts×7 lane-contract.test.ts×1 lane-hold-on-observation.test.ts×1 lane-outside-view.test.ts×1 lane-pass-stamped-effect-3662.test.ts×1 mount-over-foreign-hold-3761.test.ts×2 overlapping-flights.test.ts×3 posture-born-held-and-observation.test.ts×4 posture-store-parity.test.ts×2 reveal-carve-out.test.ts×2 shared-effect-no-entangle.test.ts×1 spec-async-semantics.test.ts×2 stale-read-uninitialized-cross-transition.test.ts×1 superseded-source-blocks-3462.test.ts×2 tick-scoped-pass-transaction.test.ts×1 treeshake.test.ts×4 visibility-oracle-store.states.ts×6 visibility-oracle.states.ts×7 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B3)] Transition entanglement is graph-driven; lanes settle as one reveal — (was B3) Transition entanglement is graph-driven: writes whose async work … |
-| A16 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:209` | scheduler.ts×1 | spec-async-semantics.test.ts×1 strict-read-pending-store.test.ts×2 uninitialized-visibility.test.ts×1 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×2 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B5)] `isPending` never throws in untracked contexts — (was B5) `isPending` never throws in untracked contexts — thunks that throw real errors or read… |
-| A17 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:67` | async.ts×3 constants.ts×1 lanes.ts×6 scheduler.ts×2 verdict.ts×1 map.ts×1 store.ts×3 | fuzz-findings-l2.test.ts×6 lane-uninitialized-landing-3648.test.ts×5 optimistic-over-held-row-3796.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×2 optimistic-undefined-override.test.ts×1 posture-store-parity.test.ts×1 refresh-await.test.ts×1 reveal-gating-contract.test.ts×3 spec-async-semantics.test.ts×10 createOptimisticStore.test.ts×2 kanban-a17-fixture.test.ts×3 optimistic-list-mutation-matrix.test.ts×1 optimistic-maparray-index-frame-f1.test.ts×1 optimistic-untracked-reads-f3-f5.test.ts×1 signal-store-twins-qd.test.ts×1 treeshake.test.ts×1 until.test.ts×1 visibility-oracle-store.states.ts×24 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×20 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from C4)] An active override is the displayed value until its transaction commits, and the graph's value until its own source answers — **Statement (curre… |
-| A18 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:77` | action.ts×1 async.ts×2 core.ts×3 lanes.ts×8 scheduler.ts×2 types.ts×1 verdict.ts×1 map.ts×1 optimistic.ts×1 projection.ts×1 | body-end-supersession-visibility.test.ts×4 createOptimistic.test.ts×3 lane-contract.test.ts×1 lane-frame-deferred-run-3662.test.ts×1 lane-outside-view.test.ts×1 lane-uninitialized-landing-3648.test.ts×5 optimistic-move-duplicate-3548.test.ts×2 optimistic-read-lane-not-transaction-3698.test.ts×4 posture-store-parity.test.ts×5 spec-async-semantics.test.ts×3 flight-owned-transaction.test.ts×1 optimistic-list-mutation-matrix.test.ts×1 optimistic-untracked-reads-f3-f5.test.ts×1 signal-store-twins-qd.test.ts×1 unchanged-presence-no-hold-3743.test.ts×2 superseded-before-first-commit.test.ts×4 visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×19 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from B4)] An override lives exactly as long as its own transaction; a newer truth from the source supersedes it in the graph immediately, on screen at com… |
-| A19 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:137` | async.ts×1 constants.ts×1 core.ts×6 scheduler.ts×2 types.ts×1 verdict.ts×4 store.ts×1 | fuzz-findings-l2.test.ts×4 lane-uninitialized-landing-3648.test.ts×2 mount-over-foreign-hold-3761.test.ts×2 spec-async-semantics.test.ts×3 derived-presence-async-3726.test.ts×2 superseded-before-first-commit.test.ts×3 uninitialized-visibility.test.ts×1 visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×14 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from C1)] `isPending(x)` ≡ the observable value is not final (three causes) — (was C1 — **partially reverses an earlier decision**) **Definition: `isPendi… |
-| A20 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:396` | — | question-scoped-pending.test.ts×2 spec-async-semantics.test.ts×3 createOptimisticStore.test.ts×1 | [superseded 2026-07-13 by A24] (superseded) Optimistic writes announce a store-wide pending — (**SUPERSEDED 2026-07-13 by A24** — the mask is deleted; optimistic writes are verdict-inert. Kept for the… |
-| A21 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:403` | — | question-scoped-pending.test.ts×3 spec-async-semantics.test.ts×3 | [superseded 2026-07-13 by A24] (superseded) The store-wide mask — (**SUPERSEDED 2026-07-13 by A24** — the store-wide mask is deleted with the mask model; nothing silences a new question. The effective… |
-| A22 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:217` | store.ts×1 | createProjection.draft-lifetime-3585.test.ts×1 spec-async-semantics.test.ts×1 visibility-oracle-store.states.ts×1 | [ruled, amended in place 2026-07-08] Pending is per-node; store-wide only for the firewall's own work — **Pending is per-node: store-wide verdicts exist only as the firewall's own in-flight work (A9) … |
-| A23 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:227` | — | spec-async-semantics.test.ts×1 | [ruled 2026-07-08] The `isPending` probe is reads-only — **The `isPending` probe is reads-only — the thunk's return value is never inspected.** `isPending(() => store)` reads nothing and reports `fals… |
-| A24 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:145` | affects.ts×2 constants.ts×1 lanes.ts×2 types.ts×1 verdict.ts×3 affects.ts×1 optimistic.ts×1 | body-end-supersession-visibility.test.ts×1 optimistic-undefined-override.test.ts×1 reveal-gating-contract.test.ts×1 spec-async-semantics.test.ts×3 kanban-a17-fixture.test.ts×1 signal-store-twins-qd.test.ts×1 visibility-oracle-store.states.ts×4 visibility-oracle.states.ts×5 visibility-oracle.test.ts×1 | [ruled 2026-07-13] Question-scoped pending: pending iff a value change is in flight or an `affects()` mark is live — (**ruled 2026-07-13** — supersedes A20/A21; the converged model from the #2844/#272… |
-| A25 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:297` | projection.ts×1 | derived-presence-async-3726.test.ts×2 uninitialized-visibility.test.ts×3 visibility-oracle-store.states.ts×7 visibility-oracle-store.test.ts×1 | [ruled 2026-07-16] A derived store's seed is a draft, never an observable value — (**ruled 2026-07-16**, #2897) **A derived store's seed is a draft, never an observable value.** The seed exists for th… |
-| A26 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:103` | — | action-await-contract.test.ts×2 fuzz-findings-l2.test.ts×1 posture-store-parity.test.ts×2 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×1 visibility-oracle.test.ts×1 | [ruled 2026-07-17] An ambient transaction window is one flush; parking is flush-driven — (**ruled 2026-07-17**, #2913; **enforcement hardened 2026-08-31**, #3141 — parking is flush-driven, and a trans… |
-| A27 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:289` | — | loading-value.test.ts×2 visibility-oracle.states.ts×18 visibility-oracle.test.ts×1 | [ruled 2026-08-10] The commit-#0 loading window is loading-class and verdict-quiet — (**ruled 2026-08-10**) **The commit-#0 loading window is loading-class and verdict-quiet.** A node born committed v… |
-| A28 | ruled, mechanism landed | `docs/SPEC-ASYNC-SEMANTICS.md:87` | constants.ts×1 core.ts×12 lanes.ts×2 scheduler.ts×3 types.ts×1 verdict.ts×2 store.ts×4 | createOptimistic.test.ts×5 fuzz-findings-l2.test.ts×6 held-derivation-not-a-proposal-3612.test.ts×1 latest-held-till-flush.test.ts×1 posture-store-parity.test.ts×5 question-scoped-pending.test.ts×3 snapshot-derived-store-rows.test.ts×1 createOptimisticStore.test.ts×10 optimistic-draft-visibility-3665.test.ts×5 optimistic-list-mutation-matrix.harness.ts×1 optimistic-list-mutation-matrix.test.ts×2 shallow.test.ts×1 woken-transaction-adopts-staged-bump.test.ts×1 treeshake.test.ts×2 verdict-contract.test.ts×1 visibility-oracle-store.states.ts×8 visibility-oracle.states.ts×8 | [ruled, mechanism landed 2026-09-15] A write becomes visible at flush — to every channel — (**ruled 2026-09-08**; supersedes the #2922 mid-tick pull) **A write becomes visible at flush — to every chan… |
-| A29 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:111` | boundaries.ts×1 action.ts×1 constants.ts×1 core.ts×9 effect.ts×1 scheduler.ts×3 signals.ts×1 store.ts×1 | adoption-unchanged-key-read-3706.test.ts×9 body-end-supersession-visibility.test.ts×1 born-held.test.ts×3 boundary-not-born-held-3540.test.ts×4 createProjection.draft-lifetime-3585.test.ts×1 direct-commit-readers-posture.test.ts×1 fuzz-findings-l2.test.ts×6 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 held-truth-lane-only.test.ts×3 l2-contract.test.ts×1 latest-held-till-flush.test.ts×2 mount-over-foreign-hold-3761.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×3 posture-born-held-and-observation.test.ts×1 posture-store-parity.test.ts×6 derived-presence-async-3726.test.ts×3 optimistic-untracked-reads-f3-f5.test.ts×1 store-unchanged-read-independent-write-3688.test.ts×1 tick-scoped-pass-transaction.test.ts×2 treeshake.test.ts×3 visibility-oracle-store.states.ts×5 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×9 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-09-13 (#3408)] A tracked read served a live transaction's staged value enters that transaction — A tracked computation served a node's staged `_pendingValue` — a value a … |
-| A30 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:245` | async.ts×3 attribution.ts×1 constants.ts×1 core.ts×1 effect.ts×1 lanes.ts×1 scheduler.ts×5 | async-landing-deps-3461.test.ts×3 fuzz-findings-l2.test.ts×3 held-conditional-effect.test.ts×1 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 ispending-in-boundary-on-3528.test.ts×1 lane-frame-deferred-run-3662.test.ts×1 lane-frame-held-lane-3662.test.ts×1 posture-born-held-and-observation.test.ts×1 treeshake.test.ts×2 write-proposals-3494.test.ts×2 zombie-rerun-after-commit-3546.test.ts×2 | [ruled 2026-09-13 (#3410)] A memo's dependencies are the committed frame's until the frame is replaced — A pass that _staged_ its value has not replaced the committed frame, so the committed value sti… |
-| A31 | live | `docs/SPEC-ASYNC-SEMANTICS.md:119` | boundaries.ts×1 core.ts×1 lanes.ts×1 verdict.ts×2 | fuzz-findings-l2.test.ts×6 ispending-combined-atomic-3442.test.ts×1 | [live 2026-09-14 (#3442)] A memo computes under its own lane posture, never its puller's — A memo's value is one shared slot every reader sees, so its pass runs under the lane posture the memo itself … |
-| A32 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:127` | core.ts×4 lanes.ts×1 store.ts×1 | visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×9 visibility-oracle.test.ts×1 | [ruled 2026-09-14] Children-forbidden readers see the frame, not the graph — `createTrackedEffect` and `onSettled` callbacks are effect-phase code that runs after the frame is decided. They read the f… |
-| A33 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:261` | boundaries.ts×1 scheduler.ts×1 | async-chain-supersession.test.ts×2 boundary-not-born-held-3540.test.ts×2 fuzz-findings-l2.test.ts×2 ispending-in-boundary-on-3528.test.ts×2 loading-reset-collects-forwarded-3459.test.ts×3 | [ruled 2026-09-12 (#3375)] A fallback-caught flight holds no transaction; a Loading reset moves the hold onto the boundary — A `` boundary showing its fallback is the display of everything un… |
-| A34 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:271` | constants.ts×1 core.ts×6 lanes.ts×2 scheduler.ts×2 store.ts×4 | a34-writes-then-derivations.test.ts×2 createMemo.test.ts×1 derived-write-then-derivation-3733.test.ts×1 finalize-reentry.test.ts×2 fuzz-findings-l2.test.ts×5 held-derivation-not-a-proposal-3612.test.ts×6 optimistic-list-mutation-matrix.test.ts×1 unchanged-presence-no-hold-3743.test.ts×3 woken-transaction-adopts-staged-bump.test.ts×1 transition-corpse-revival.test.ts×1 treeshake.test.ts×2 visibility-oracle.states.ts×2 write-proposals-3494.test.ts×5 | [ruled 2026-09-16 (#3494)] A write is a proposal: one on a held node entangles its tick; one that nets to the committed value is none — A write proposes a value for a node. **Held, both are suggestion… |
+| A1 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:309` | — | onCleanup.test.ts×2 transitionEntanglement.test.ts×4 | [ruled 2026-07-06] Effect error interception is compute-phase only — `EffectBundle.error` intercepts compute-phase errors only; effect-phase throws escalate to the nearest error boundary (halt if none… |
+| A2 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:317` | — | onCleanup.test.ts×2 | [ruled] Unhandled compute-phase errors in user effects are logged and skipped — Compute-phase errors in _user_ effects without a handler are logged and the run is skipped; the system keeps running. |
+| A3 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:325` | — | derived-write-then-derivation-3733.test.ts×5 equals-comparator-errors.test.ts×1 | [ruled] Comparator throws are compute-phase errors — Errors thrown by a user `equals` comparator behave exactly like compute-phase errors (boundary-containable; loud halt without a boundary). |
+| A4 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:333` | — | derived-write-then-derivation-3733.test.ts×1 equals-comparator-errors.test.ts×1 | [ruled] A custom `equals` never sees `undefined` prev on first commit — A custom `equals` is never invoked with `undefined` previous value on a node's first commit. |
+| A5 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:341` | — | derived-write-then-derivation-3733.test.ts×1 errorHalt.test.ts×1 | [ruled] An error escaping every boundary halts the system — An error escaping every boundary permanently halts the system with `REACTIVITY_HALTED`; later writes log "Update ignored". |
+| A6 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:349` | boundaries.ts×2 | derived-write-then-derivation-3733.test.ts×1 enforceLoadingBoundary.test.ts×1 | [ruled] `ASYNC_OUTSIDE_LOADING_BOUNDARY` is warn-only — `ASYNC_OUTSIDE_LOADING_BOUNDARY` is a warn-only diagnostic; an `Errored` above must not swallow it and must not show its fallback for a pending. |
+| A7 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:155` | — | fuzz-findings-l2.test.ts×1 spec-async-semantics.test.ts×2 visibility-oracle-store.states.ts×1 visibility-oracle.states.ts×1 visibility-oracle.test.ts×1 | [ruled, amended in place] Resolved async never reads `[false, undefined]` — After an async memo resolves, `[isPending(x), latest(x)]` is `[false, resolvedValue]` — never `[false, undefined]`. **Amende… |
+| A8 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:163` | — | createMemo.test.ts×1 visibility-oracle-store.states.ts×1 visibility-oracle.states.ts×2 | [ruled, amended in place 2026-07-07] `isPending(() => latest(x))` follows `x`'s own async only — verdicts are per-channel — (**re-ruled 2026-07-07c** — was "tracks the transition the same as `isPendin… |
+| A9 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:171` | projection.ts×1 target.ts×1 | spec-async-semantics.test.ts×3 visibility-oracle-store.states.ts×4 visibility-oracle-store.test.ts×1 | [ruled, amended in place 2026-07-07] Store leaves behind a firewall report the firewall's new-question refetch — `isPending` on a store leaf behind a firewall reports the firewall's refetch like any a… |
+| A10 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:179` | core.ts×3 verdict.ts×2 | createMemo.test.ts×1 fuzz-findings-l2.test.ts×1 ispending-memo-unstamped-hold-3457.test.ts×2 latest-isPending-consistency.test.ts×3 verdict-contract.test.ts×2 | [ruled] `[isPending(x), x()]` is atomic within one scope — `[isPending(x), x()]` read in one scope is atomic: a reader that observed the fresh value must not see `pending === true` for it. |
+| A11 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:97` | — | latest-isPending-consistency.test.ts×1 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×1 | [ruled] Sync derivations of held sources are visible through `latest()`/`isPending()` — Sync derivations of transition-held sources are visible through `latest()`/`isPending()` (held sync recompute is… |
+| A12 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:187` | — | createOptimistic.test.ts×2 spec-async-semantics.test.ts×1 | [ruled, amended in place] Resting optimistic nodes report pending like a plain memo — A resting optimistic node reports pending via exactly the causes a plain async memo does (A19) — a reverting optim… |
+| A13 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:195` | — | spec-async-semantics.test.ts×7 | [ruled 2026-07-06 (promoted from B1)] Resting optimistic ≡ plain async memo at every checkpoint — (was B1) A resting optimistic node (no active override) is observationally identical to a plain async … |
+| A14 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:203` | — | spec-async-semantics.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B2)] Companion nodes get child lanes that do not merge with the owner — (was B2) `isPending`/`latest` companion nodes get child lanes that do not mer… |
+| A15 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:239` | async.ts×2 constants.ts×1 core.ts×3 lanes.ts×4 scheduler.ts×14 verdict.ts×3 | async-chain-supersession.test.ts×1 first-observer-stale-reader.test.ts×1 fuzz-findings-l2.test.ts×17 held-restore.test.ts×1 l2-contract.test.ts×7 lane-contract.test.ts×1 lane-hold-on-observation.test.ts×1 lane-outside-view.test.ts×1 lane-pass-stamped-effect-3662.test.ts×1 mount-over-foreign-hold-3761.test.ts×2 overlapping-flights.test.ts×3 posture-born-held-and-observation.test.ts×4 posture-store-parity.test.ts×2 reveal-carve-out.test.ts×2 shared-effect-no-entangle.test.ts×1 spec-async-semantics.test.ts×2 stale-read-uninitialized-cross-transition.test.ts×1 superseded-source-blocks-3462.test.ts×2 tick-scoped-pass-transaction.test.ts×1 treeshake.test.ts×4 visibility-oracle-store.states.ts×6 visibility-oracle.states.ts×7 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B3)] Transition entanglement is graph-driven; lanes settle as one reveal — (was B3) Transition entanglement is graph-driven: writes whose async work … |
+| A16 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:211` | scheduler.ts×1 | spec-async-semantics.test.ts×1 strict-read-pending-store.test.ts×2 uninitialized-visibility.test.ts×1 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×2 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B5)] `isPending` never throws in untracked contexts — (was B5) `isPending` never throws in untracked contexts — thunks that throw real errors or read… |
+| A17 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:69` | async.ts×3 constants.ts×1 lanes.ts×6 scheduler.ts×2 verdict.ts×1 map.ts×1 store.ts×3 | fuzz-findings-l2.test.ts×6 lane-uninitialized-landing-3648.test.ts×5 optimistic-over-held-row-3796.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×2 optimistic-undefined-override.test.ts×1 posture-store-parity.test.ts×1 refresh-await.test.ts×1 reveal-gating-contract.test.ts×3 spec-async-semantics.test.ts×10 createOptimisticStore.test.ts×2 kanban-a17-fixture.test.ts×3 optimistic-list-mutation-matrix.test.ts×1 optimistic-maparray-index-frame-f1.test.ts×1 optimistic-untracked-reads-f3-f5.test.ts×1 signal-store-twins-qd.test.ts×1 treeshake.test.ts×1 until.test.ts×1 visibility-oracle-store.states.ts×24 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×20 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from C4)] An active override is the displayed value until its transaction commits, and the graph's value until its own source answers — **Statement (curre… |
+| A18 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:79` | action.ts×1 async.ts×2 core.ts×3 lanes.ts×8 scheduler.ts×2 types.ts×1 verdict.ts×1 map.ts×1 optimistic.ts×1 projection.ts×1 | body-end-supersession-visibility.test.ts×4 createOptimistic.test.ts×3 lane-contract.test.ts×1 lane-frame-deferred-run-3662.test.ts×1 lane-outside-view.test.ts×1 lane-uninitialized-landing-3648.test.ts×5 optimistic-move-duplicate-3548.test.ts×2 optimistic-read-lane-not-transaction-3698.test.ts×4 posture-store-parity.test.ts×5 spec-async-semantics.test.ts×3 flight-owned-transaction.test.ts×1 optimistic-list-mutation-matrix.test.ts×1 optimistic-untracked-reads-f3-f5.test.ts×1 signal-store-twins-qd.test.ts×1 unchanged-presence-no-hold-3743.test.ts×2 superseded-before-first-commit.test.ts×4 visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×19 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from B4)] An override lives exactly as long as its own transaction; a newer truth from the source supersedes it in the graph immediately, on screen at com… |
+| A19 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:139` | async.ts×1 constants.ts×1 core.ts×6 scheduler.ts×2 types.ts×1 verdict.ts×4 store.ts×1 | fuzz-findings-l2.test.ts×4 lane-uninitialized-landing-3648.test.ts×2 mount-over-foreign-hold-3761.test.ts×2 spec-async-semantics.test.ts×3 derived-presence-async-3726.test.ts×2 superseded-before-first-commit.test.ts×3 uninitialized-visibility.test.ts×1 visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×14 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from C1)] `isPending(x)` ≡ the observable value is not final (three causes) — (was C1 — **partially reverses an earlier decision**) **Definition: `isPendi… |
+| A20 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:398` | — | question-scoped-pending.test.ts×2 spec-async-semantics.test.ts×3 createOptimisticStore.test.ts×1 | [superseded 2026-07-13 by A24] (superseded) Optimistic writes announce a store-wide pending — (**SUPERSEDED 2026-07-13 by A24** — the mask is deleted; optimistic writes are verdict-inert. Kept for the… |
+| A21 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:405` | — | question-scoped-pending.test.ts×3 spec-async-semantics.test.ts×3 | [superseded 2026-07-13 by A24] (superseded) The store-wide mask — (**SUPERSEDED 2026-07-13 by A24** — the store-wide mask is deleted with the mask model; nothing silences a new question. The effective… |
+| A22 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:219` | store.ts×1 | createProjection.draft-lifetime-3585.test.ts×1 spec-async-semantics.test.ts×1 visibility-oracle-store.states.ts×1 | [ruled, amended in place 2026-07-08] Pending is per-node; store-wide only for the firewall's own work — **Pending is per-node: store-wide verdicts exist only as the firewall's own in-flight work (A9) … |
+| A23 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:229` | — | spec-async-semantics.test.ts×1 | [ruled 2026-07-08] The `isPending` probe is reads-only — **The `isPending` probe is reads-only — the thunk's return value is never inspected.** `isPending(() => store)` reads nothing and reports `fals… |
+| A24 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:147` | affects.ts×2 constants.ts×1 lanes.ts×2 types.ts×1 verdict.ts×3 affects.ts×1 optimistic.ts×1 | body-end-supersession-visibility.test.ts×1 optimistic-undefined-override.test.ts×1 reveal-gating-contract.test.ts×1 spec-async-semantics.test.ts×3 kanban-a17-fixture.test.ts×1 signal-store-twins-qd.test.ts×1 visibility-oracle-store.states.ts×4 visibility-oracle.states.ts×5 visibility-oracle.test.ts×1 | [ruled 2026-07-13] Question-scoped pending: pending iff a value change is in flight or an `affects()` mark is live — (**ruled 2026-07-13** — supersedes A20/A21; the converged model from the #2844/#272… |
+| A25 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:299` | projection.ts×1 | derived-presence-async-3726.test.ts×2 uninitialized-visibility.test.ts×3 visibility-oracle-store.states.ts×7 visibility-oracle-store.test.ts×1 | [ruled 2026-07-16] A derived store's seed is a draft, never an observable value — (**ruled 2026-07-16**, #2897) **A derived store's seed is a draft, never an observable value.** The seed exists for th… |
+| A26 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:105` | — | action-await-contract.test.ts×2 fuzz-findings-l2.test.ts×1 posture-store-parity.test.ts×2 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×1 visibility-oracle.test.ts×1 | [ruled 2026-07-17] An ambient transaction window is one flush; parking is flush-driven — (**ruled 2026-07-17**, #2913; **enforcement hardened 2026-08-31**, #3141 — parking is flush-driven, and a trans… |
+| A27 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:291` | — | loading-value.test.ts×2 visibility-oracle.states.ts×18 visibility-oracle.test.ts×1 | [ruled 2026-08-10] The commit-#0 loading window is loading-class and verdict-quiet — (**ruled 2026-08-10**) **The commit-#0 loading window is loading-class and verdict-quiet.** A node born committed v… |
+| A28 | ruled, mechanism landed | `docs/SPEC-ASYNC-SEMANTICS.md:89` | constants.ts×1 core.ts×12 lanes.ts×2 scheduler.ts×3 types.ts×1 verdict.ts×2 store.ts×4 | createOptimistic.test.ts×5 fuzz-findings-l2.test.ts×6 held-derivation-not-a-proposal-3612.test.ts×1 latest-held-till-flush.test.ts×1 posture-store-parity.test.ts×5 question-scoped-pending.test.ts×3 snapshot-derived-store-rows.test.ts×1 createOptimisticStore.test.ts×10 optimistic-draft-visibility-3665.test.ts×5 optimistic-list-mutation-matrix.harness.ts×1 optimistic-list-mutation-matrix.test.ts×2 shallow.test.ts×1 woken-transaction-adopts-staged-bump.test.ts×1 treeshake.test.ts×2 verdict-contract.test.ts×1 visibility-oracle-store.states.ts×8 visibility-oracle.states.ts×8 | [ruled, mechanism landed 2026-09-15] A write becomes visible at flush — to every channel — (**ruled 2026-09-08**; supersedes the #2922 mid-tick pull) **A write becomes visible at flush — to every chan… |
+| A29 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:113` | boundaries.ts×1 action.ts×1 constants.ts×1 core.ts×10 effect.ts×1 scheduler.ts×4 signals.ts×1 store.ts×1 | adoption-unchanged-key-read-3706.test.ts×9 body-end-supersession-visibility.test.ts×1 born-held.test.ts×3 boundary-not-born-held-3540.test.ts×4 createProjection.draft-lifetime-3585.test.ts×1 direct-commit-readers-posture.test.ts×1 fuzz-findings-l2.test.ts×6 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 held-truth-lane-only.test.ts×3 l2-contract.test.ts×1 latest-held-till-flush.test.ts×2 loading-fallback-in-flush-3540.test.ts×1 mount-over-foreign-hold-3761.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×3 posture-born-held-and-observation.test.ts×1 posture-store-parity.test.ts×6 derived-presence-async-3726.test.ts×3 optimistic-untracked-reads-f3-f5.test.ts×1 store-unchanged-read-independent-write-3688.test.ts×1 tick-scoped-pass-transaction.test.ts×2 treeshake.test.ts×3 visibility-oracle-store.states.ts×5 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×9 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-09-13 (#3408)] A tracked read served a live transaction's staged value enters that transaction — A tracked computation served a node's staged `_pendingValue` — a value a … |
+| A30 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:247` | async.ts×3 attribution.ts×1 constants.ts×1 core.ts×1 effect.ts×1 lanes.ts×1 scheduler.ts×5 | async-landing-deps-3461.test.ts×3 fuzz-findings-l2.test.ts×3 held-conditional-effect.test.ts×1 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 ispending-in-boundary-on-3528.test.ts×1 lane-frame-deferred-run-3662.test.ts×1 lane-frame-held-lane-3662.test.ts×1 posture-born-held-and-observation.test.ts×1 treeshake.test.ts×2 write-proposals-3494.test.ts×2 zombie-rerun-after-commit-3546.test.ts×2 | [ruled 2026-09-13 (#3410)] A memo's dependencies are the committed frame's until the frame is replaced — A pass that _staged_ its value has not replaced the committed frame, so the committed value sti… |
+| A31 | live | `docs/SPEC-ASYNC-SEMANTICS.md:121` | boundaries.ts×1 core.ts×1 lanes.ts×1 verdict.ts×2 | fuzz-findings-l2.test.ts×6 ispending-combined-atomic-3442.test.ts×1 | [live 2026-09-14 (#3442)] A memo computes under its own lane posture, never its puller's — A memo's value is one shared slot every reader sees, so its pass runs under the lane posture the memo itself … |
+| A32 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:129` | core.ts×4 lanes.ts×1 store.ts×1 | visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×9 visibility-oracle.test.ts×1 | [ruled 2026-09-14] Children-forbidden readers see the frame, not the graph — `createTrackedEffect` and `onSettled` callbacks are effect-phase code that runs after the frame is decided. They read the f… |
+| A33 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:263` | boundaries.ts×1 scheduler.ts×1 | async-chain-supersession.test.ts×2 boundary-not-born-held-3540.test.ts×2 fuzz-findings-l2.test.ts×2 ispending-in-boundary-on-3528.test.ts×2 loading-reset-collects-forwarded-3459.test.ts×3 | [ruled 2026-09-12 (#3375)] A fallback-caught flight holds no transaction; a Loading reset moves the hold onto the boundary — A `` boundary showing its fallback is the display of everything un… |
+| A34 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:273` | constants.ts×1 core.ts×6 lanes.ts×2 scheduler.ts×2 store.ts×4 | a34-writes-then-derivations.test.ts×2 createMemo.test.ts×1 derived-write-then-derivation-3733.test.ts×1 finalize-reentry.test.ts×2 fuzz-findings-l2.test.ts×5 held-derivation-not-a-proposal-3612.test.ts×6 optimistic-list-mutation-matrix.test.ts×1 unchanged-presence-no-hold-3743.test.ts×3 woken-transaction-adopts-staged-bump.test.ts×1 transition-corpse-revival.test.ts×1 treeshake.test.ts×2 visibility-oracle.states.ts×2 write-proposals-3494.test.ts×5 | [ruled 2026-09-16 (#3494)] A write is a proposal: one on a held node entangles its tick; one that nets to the committed value is none — A write proposes a value for a node. **Held, both are suggestion… |
## V — fixed violations
| id | status | defined | cited in src | cited in tests | statement (at definition) |
|---|---|---|---|---|---|
-| V1 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:466` | — | spec-async-semantics.test.ts×7 | - **V1 (violated A13) — FIXED.** A _resting_ optimistic node reported |
-| V2 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:476` | — | spec-async-semantics.test.ts×2 | - **V2 (violated A7/A13) — FIXED.** `latest()`'s verdict in the window was |
-| V3 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:482` | — | spec-async-semantics.test.ts×2 | - **V3 (violated A19) — FIXED.** After a reporter-less transition completed, |
-| V4 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:489` | — | spec-async-semantics.test.ts×5 | - **V4 (violated the old A20's three-form algebra) — FIXED, then the rule it |
-| V5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:501` | scheduler.ts×1 | spec-async-semantics.test.ts×3 | - **V5 (A17 corollary — found and fixed with the revert-target elimination, |
+| V1 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:468` | — | spec-async-semantics.test.ts×7 | - **V1 (violated A13) — FIXED.** A _resting_ optimistic node reported |
+| V2 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:478` | — | spec-async-semantics.test.ts×2 | - **V2 (violated A7/A13) — FIXED.** `latest()`'s verdict in the window was |
+| V3 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:484` | — | spec-async-semantics.test.ts×2 | - **V3 (violated A19) — FIXED.** After a reporter-less transition completed, |
+| V4 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:491` | — | spec-async-semantics.test.ts×5 | - **V4 (violated the old A20's three-form algebra) — FIXED, then the rule it |
+| V5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:503` | scheduler.ts×1 | spec-async-semantics.test.ts×3 | - **V5 (A17 corollary — found and fixed with the revert-target elimination, |
## B — tier B
| id | status | defined | cited in src | cited in tests | statement (at definition) |
|---|---|---|---|---|---|
-| B1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:193` | — | createRevealOrder.test.ts×16 derived-write-then-derivation-3733.test.ts×1 onCleanup.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A13 (A13's section carries the ruling). |
-| B2 | live | `docs/SPEC-ASYNC-SEMANTICS.md:201` | — | createRevealOrder.test.ts×16 derived-write-then-derivation-3733.test.ts×1 onCleanup.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A14 (A14's section carries the ruling). |
-| B3 | live | `docs/SPEC-ASYNC-SEMANTICS.md:237` | — | spec-async-semantics.test.ts×2 | PROMOTED → A15 (A15's section carries the ruling). |
-| B4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:77` | — | derived-write-then-derivation-3733.test.ts×1 spec-async-semantics.test.ts×2 | PROMOTED → A18 (A18's section carries the ruling). |
-| B5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:209` | boundaries.ts×1 | derived-write-then-derivation-3733.test.ts×1 fuzz-findings-l2.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A16 (A16's section carries the ruling). |
+| B1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:195` | — | createRevealOrder.test.ts×16 derived-write-then-derivation-3733.test.ts×1 onCleanup.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A13 (A13's section carries the ruling). |
+| B2 | live | `docs/SPEC-ASYNC-SEMANTICS.md:203` | — | createRevealOrder.test.ts×16 derived-write-then-derivation-3733.test.ts×1 onCleanup.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A14 (A14's section carries the ruling). |
+| B3 | live | `docs/SPEC-ASYNC-SEMANTICS.md:239` | — | spec-async-semantics.test.ts×2 | PROMOTED → A15 (A15's section carries the ruling). |
+| B4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:79` | — | derived-write-then-derivation-3733.test.ts×1 spec-async-semantics.test.ts×2 | PROMOTED → A18 (A18's section carries the ruling). |
+| B5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:211` | boundaries.ts×1 | derived-write-then-derivation-3733.test.ts×1 fuzz-findings-l2.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A16 (A16's section carries the ruling). |
## C — tier C
| id | status | defined | cited in src | cited in tests | statement (at definition) |
|---|---|---|---|---|---|
-| C1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:137` | — | onCleanup.test.ts×2 spec-async-semantics.test.ts×1 | PROMOTED → A19 (A19's section carries the ruling). |
-| C2 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:432` | — | onCleanup.test.ts×2 | - [x] **C2 — RULED (2026-07-07): reverts do not trump other live lanes.** A |
-| C3 | closed | `docs/SPEC-ASYNC-SEMANTICS.md:442` | — | — | - [x] **C3 — CLOSED by A19 (2026-07-07): early completion is by design.** |
-| C4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:67` | — | spec-async-semantics.test.ts×1 | PROMOTED → A17 (A17's section carries the ruling). |
+| C1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:139` | — | onCleanup.test.ts×2 spec-async-semantics.test.ts×1 | PROMOTED → A19 (A19's section carries the ruling). |
+| C2 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:434` | — | onCleanup.test.ts×2 | - [x] **C2 — RULED (2026-07-07): reverts do not trump other live lanes.** A |
+| C3 | closed | `docs/SPEC-ASYNC-SEMANTICS.md:444` | — | — | - [x] **C3 — CLOSED by A19 (2026-07-07): early completion is by design.** |
+| C4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:69` | — | spec-async-semantics.test.ts×1 | PROMOTED → A17 (A17's section carries the ruling). |
## INV — invariants
| id | status | defined | cited in src | cited in tests | statement (at definition) |
diff --git a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md
index 66ea0e230..ad1a31871 100644
--- a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md
+++ b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md
@@ -55,6 +55,8 @@ The former Tier A table is these sections. Tier B/C, the fixed violations, and t
- **A first load derived from a hold lands into it** (#3800): a first load born pending in a frame that read a hold lands into that hold if the hold is still live, else as its own commit; the hold commits without it. **Pending** — `fix/create-time-holds` (`91e474506`); [#3800](https://github.com/solidjs/solid/issues/3800) is open.
- **A first pass under a loading boundary that has not shown content is the boundary's, not the tick's** — in a flush as outside one (A29's boundary exemption, #3540). The out-of-flush form is on `next`; the in-flush form (a fresh `Loading` over a held value shows its fallback in a flush) is **pending** — `fix/create-time-holds` (`ca71e5d9d`).
+**The boundary scope, in a flush (2026-10-06, amendment — #3540's L2 regression; the bullet above is unchanged).** The in-flush form lands with `fix/l2-3540-fresh-loading`, standalone of `fix/create-time-holds`. A first pass (`STATUS_UNINITIALIZED`) that reads a hold asks the boundaries up its chain (`GlobalQueue._fresh`: caught as a pending status). When a loading boundary that has not shown content catches it, the pass joins pass-scoped (`passTx`), as a mount outside a flush does, instead of joining the flush: the Show that mounted the boundary publishes, and the boundary shows its fallback. A pass nothing catches joins the flush as before, so a derivation outside the boundary in the same flush still holds the tick. A render effect born held under the boundary (content bound in JSX — the tree never reads the held value) is caught the same way, so the boundary does not reveal empty content, in a flush or out of one; its output's first pass shows the fallback whenever it has collected readers. A flush that set `passTx` clears it at its end, so the boundary's hold does not merge with a later tick's. The `direction-rule-probe` pins are unchanged. **Pinned by:** `tests/loading-fallback-in-flush-3540.test.ts` (in a flush: through a memo, directly, bound, nested under a Loading that has shown content; a derivation outside the boundary still holds the tick; the boundary's hold stays its own; on a hold, from mainline: the same three content shapes); `packages/web/test/loading-fallback-in-flush-3540.spec.tsx`.
+
**Not yet one-way (recorded, not ruled).** The frame of a tick is still one transaction (the tick ruling above stands), so two shapes keep the hold waiting on first loads it never needed:
- **Same tick:** mounts created outside a flush in one tick — two mounts over the held value, or a mount and a sync reader — share the tick's frame. The frame joins the hold, and the hold waits on the mounts' first loads.
diff --git a/packages/signals/src/boundaries.ts b/packages/signals/src/boundaries.ts
index 97ca08c6f..631f60627 100644
--- a/packages/signals/src/boundaries.ts
+++ b/packages/signals/src/boundaries.ts
@@ -203,7 +203,7 @@ export function redraw(b: Boundary): void {
/** GlobalQueue._catch: status from a frame reader, nearest boundary first.
* A loading boundary on the way records a pending reader whether or not it
* catches it (its `on` may collect it later). */
-function catchStatus(node: Computed, flags: number, error: unknown): boolean {
+function catchStatus(node: Computed, flags: number, error?: unknown): boolean {
if (flags === 0) {
// A status cleared. Judged by the node's status now, against each
// boundary's own rule (`unsettled`) — the error path clears pending
@@ -596,25 +596,23 @@ function createBoundary(
}
if (isCollecting(b)) {
// A29's boundary exemption (#3540): a boundary MOUNTED over a held
- // value (its first pass; the tree born held) shows its fallback now
- // and the content at the commit — entering the transaction would
- // make the output itself born held, and nothing would show until
- // the commit. (The seam keeps a held reader until it is committed.)
- // A boundary with a committed value reads a held tree and enters:
- // the outside sees its committed value until the landing, which
- // reveals the content — a fallback staged earlier is replaced ahead
- // of the commit and never shown.
+ // value (its first pass; a first pass under it that read a hold was
+ // collected, `joinPass` — the tree's own, or a render effect's that
+ // binds the content) shows its fallback now and the content at the
+ // commit — entering the transaction would make the output itself
+ // born held, and nothing would show until the commit. The seam
+ // keeps a held reader until it is committed, then re-derives this
+ // pass. A boundary with a committed value reads a held tree and
+ // enters: the outside sees its committed value until the landing,
+ // which reveals the content — a fallback staged earlier is replaced
+ // ahead of the commit and never shown.
const self = getOwner() as Computed;
if (
- tree._config & CONFIG_HELD &&
+ b._readers.size !== 0 &&
self._statusFlags & STATUS_UNINITIALIZED &&
!(self._config & CONFIG_HELD)
- ) {
- // The seam re-derives this pass once; by then the fallback is
- // the committed value, and the next pass enters.
- collecting.add(b);
+ )
return fallback(b);
- }
// Readers under it still unready: the fallback, the tree untouched.
// The seam re-derives this pass when they settle.
if (prune(b, true) !== 0) return fallback(b);
@@ -776,6 +774,8 @@ export function createErrorBoundary(
// Installed at module evaluation — present exactly when something imports a
// boundary. An app without one pays the three null checks and nothing else.
GlobalQueue._catch = catchStatus;
+GlobalQueue._fresh = node =>
+ node._statusFlags & STATUS_UNINITIALIZED && catchStatus(node, STATUS_PENDING);
GlobalQueue._hidden = hidden;
GlobalQueue._boundarySeam = boundarySeam;
GlobalQueue._heldRun = heldRun;
diff --git a/packages/signals/src/core/core.ts b/packages/signals/src/core/core.ts
index 80f163985..b6b77681c 100644
--- a/packages/signals/src/core/core.ts
+++ b/packages/signals/src/core/core.ts
@@ -941,10 +941,18 @@ export function ext(el: { _x: NodeExtension | null }): NodeExtension {
* reader is a render effect. A render effect is the frame, not a derivation
* (rule 3): in that transaction's own flush, or born into it (uninitialized,
* A29), it reads the staged value and holds nothing of its own; otherwise it
- * reads the committed value instead (`frameRead`). */
+ * reads the committed value instead (`frameRead`). A node born into the
+ * future has no committed value: every reader joins it. A first pass is
+ * something not ready under a loading boundary that has not shown content:
+ * the boundary collects it and shows its fallback, and the pass is the
+ * boundary's, not the tick's (A29's boundary exemption, #3540). */
function joinPass(c: Computed, el: Signal | Computed): void {
c._flags |= REACTIVE_JOINED;
- if ((c as any)._type !== EFFECT_RENDER) joinPassTx(txOf(el));
+ if (
+ (el as Computed)._statusFlags & STATUS_UNINITIALIZED ||
+ (c as any)._type !== EFFECT_RENDER
+ )
+ joinPassTx(txOf(el), GlobalQueue._fresh?.(c));
}
/** A15's stale reader (shared-hole and reveal corollaries): a render effect
@@ -1651,11 +1659,9 @@ export function read(el: Signal | Computed): T {
!(el._config & CONFIG_OVERRIDE) &&
!(c._config & CONFIG_CHILDREN_FORBIDDEN)
) {
- if (owner._statusFlags & STATUS_UNINITIALIZED) {
- (c as Computed)._flags |= REACTIVE_JOINED;
- joinPassTx(txOf(el));
- } else if (frameRead(c as Computed, el)) committed = true;
- else joinPass(c as Computed, el);
+ if (owner._statusFlags & STATUS_UNINITIALIZED || !frameRead(c as Computed, el))
+ joinPass(c as Computed, el);
+ else committed = true;
}
}
// Lanes: a lane's node (after the pull — the node is current). NOT_PENDING
diff --git a/packages/signals/src/core/scheduler.ts b/packages/signals/src/core/scheduler.ts
index caa27c767..7db835628 100644
--- a/packages/signals/src/core/scheduler.ts
+++ b/packages/signals/src/core/scheduler.ts
@@ -172,11 +172,15 @@ export let flushTransaction: Transaction | null = null;
* into it, and nothing else in the tick is — a write made after the mount
* is a mainline write, a render effect mounted beside it a stale reader.
* Inside a flush, or in a tick that already has its transaction (an
- * action's body), the frame joins instead (`flushTransaction`). Cleared by
- * the flush the join schedules. */
+ * action's body), the frame joins instead (`flushTransaction`) — except a
+ * first pass a loading boundary that has not shown content catches (`own`,
+ * A29's boundary exemption, #3540): it is the boundary's, not the tick's,
+ * and a flush that has joined nothing keeps it pass-scoped as outside one.
+ * Cleared by the flush the join schedules, and at the end of a flush that
+ * set it. */
export let passTx: Transaction | null = null;
-export function joinPassTx(t: Transaction): void {
- if (globalQueue._running || flushTransaction !== null) return joinFuture(t);
+export function joinPassTx(t: Transaction, own?: unknown): void {
+ if ((globalQueue._running && !own) || flushTransaction !== null) return joinFuture(t);
if (passTx === null) passTx = resolveTx(t);
else merge(resolveTx(t), passTx);
schedule();
@@ -650,13 +654,15 @@ export class GlobalQueue implements IQueue {
// Boundaries (boundaries.ts): the display consumers between an observer
// and the root. `_catch` — status from a frame reader, nearest boundary
// first (true: caught, the root never hears of it; a clear — flags 0 —
- // settles the reader there); `_hidden` — a frame reader behind a fallback
- // is not on screen and holds nothing; `_boundarySeam` — the seam's sweep
- // (readers gone or settled without a pass reveal; an `on` re-arm
- // resolves).
+ // settles the reader there); `_fresh` — a pass that read a hold (true: a
+ // first pass a loading boundary that has not shown content caught,
+ // `joinPass`); `_hidden` — a frame reader behind a fallback is not on
+ // screen and holds nothing; `_boundarySeam` — the seam's sweep (readers
+ // gone or settled without a pass reveal; an `on` re-arm resolves).
declare static _catch:
| ((node: Computed, flags: number, error: unknown) => boolean)
| undefined;
+ declare static _fresh: ((node: Computed) => unknown) | undefined;
declare static _hidden: ((r: Computed) => boolean) | undefined;
declare static _boundarySeam: (() => void) | undefined;
// `_heldRun` — a queued run under a fallback-showing boundary waits for
@@ -702,6 +708,7 @@ export class GlobalQueue implements IQueue {
if (__DEV__) DEV.hooks.onUpdate?.();
} finally {
this._running = false;
+ passTx = null;
}
}
/** L2 — the seam: end of the pure phase. Commit this flush's staged nodes,
diff --git a/packages/signals/tests/loading-fallback-in-flush-3540.test.ts b/packages/signals/tests/loading-fallback-in-flush-3540.test.ts
new file mode 100644
index 000000000..a0a0bbf32
--- /dev/null
+++ b/packages/signals/tests/loading-fallback-in-flush-3540.test.ts
@@ -0,0 +1,228 @@
+/**
+ * #3540 under L2 — A29's boundary exemption, in a flush and on a hold.
+ *
+ * A loading boundary that has not shown content mounted while a transaction
+ * holds what it reads shows its fallback now and its content at the commit:
+ * the first pass under it is the boundary's, not the tick's. A mount inside
+ * a flush (a Show opening) publishes with it; a derivation outside the
+ * boundary in the same flush still holds the tick. Content bound by a render
+ * effect (the tree never reads the held value) is collected too, so the
+ * boundary never reveals empty content.
+ */
+import { describe, expect, it } from "vitest";
+import {
+ action,
+ createLoadingBoundary,
+ createMemo,
+ createRenderEffect,
+ createRoot,
+ createSignal,
+ flush,
+ untrack
+} from "../src/index.js";
+
+const tick = async () => {
+ for (let i = 0; i < 6; i++) await Promise.resolve();
+ flush();
+};
+
+function hold(write: () => void) {
+ let release!: () => void;
+ action(function* () {
+ write();
+ yield new Promise(resolve => (release = resolve));
+ })();
+ flush();
+ return () => release();
+}
+
+/** `{fn()}`, created untracked as
+ * createComponent does. */
+const Loading = (fn: () => T, fallback: string) =>
+ untrack(() => createLoadingBoundary(fn, () => fallback));
+
+type Content = "memo" | "direct" | "bound";
+
+/** The boundary's content over `x`, logging into `log`. */
+function content(kind: Content, x: () => number, log: string[]) {
+ return () => {
+ if (kind === "direct") return `content ${x()}`;
+ const m = createMemo(() => `content ${x()}`);
+ if (kind === "memo") return m();
+ //
{m()}
: the content's binding reads the memo, the tree does not.
+ createRenderEffect(m, v => {
+ log.push(`text ${v}`);
+ });
+ return "p";
+ };
+}
+
+function mount(kind: Content, x: () => number, log: string[]) {
+ const view = Loading(content(kind, x, log), "fallback");
+ createRenderEffect(view, v => {
+ log.push(`view ${v}`);
+ });
+}
+
+const shapes = [
+ ["memo", "through a memo"],
+ ["direct", "directly"],
+ ["bound", "bound by a render effect under the boundary"]
+] as const;
+
+/** The commit's reveal: the content's own entries, in any order. */
+const revealed = (kind: Content) =>
+ kind === "bound" ? ["text content 1", "view p"] : ["view content 1"];
+
+describe("#3540 in a flush: a fresh Loading mounted over a held value shows its fallback now", () => {
+ function setup(kind: Content, nested = false) {
+ const [x, setX] = createSignal(0);
+ const [open, setOpen] = createSignal(false);
+ const log: string[] = [];
+ createRoot(() => {
+ createRenderEffect(x, v => {
+ log.push(`x ${v}`);
+ });
+ //
+ const show = () =>
+ createRenderEffect(
+ () => {
+ if (!open()) return false;
+ mount(kind, x, log);
+ return true;
+ },
+ v => {
+ log.push(`open ${v}`);
+ }
+ );
+ if (!nested) return show();
+ // Under an outer Loading that has shown content.
+ const outer = Loading(() => (show(), "outer"), "outer fallback");
+ createRenderEffect(outer, v => {
+ log.push(`outer ${v}`);
+ });
+ });
+ flush();
+ if (nested) expect(log).toContain("outer outer");
+ const release = hold(() => setX(1));
+ log.length = 0;
+ return { setOpen, log, release };
+ }
+
+ for (const [kind, how] of shapes)
+ it(`content reads the held value ${how}: the mount publishes, the content reveals at the commit`, async () => {
+ const s = setup(kind);
+ s.setOpen(true);
+ flush();
+ expect(s.log).toEqual(["view fallback", "open true"]);
+
+ s.release();
+ await tick();
+ expect(s.log.slice(2).sort()).toEqual(["x 1", ...revealed(kind)].sort());
+ });
+
+ it("nested under an outer Loading that has shown content: the inner fallback now, the outer keeps its content", async () => {
+ const s = setup("memo", true);
+ s.setOpen(true);
+ flush();
+ expect(s.log).toEqual(["view fallback", "open true"]);
+
+ s.release();
+ await tick();
+ expect(s.log.slice(2).sort()).toEqual(["view content 1", "x 1"]);
+ });
+
+ it("a derivation outside the boundary in the same flush still holds the tick (membership is the tick's)", async () => {
+ const [x, setX] = createSignal(0);
+ const [open, setOpen] = createSignal(false);
+ const log: string[] = [];
+ createRoot(() => {
+ createRenderEffect(
+ () => {
+ if (!open()) return false;
+ const label = createMemo(() => `label ${x()}`);
+ createRenderEffect(label, v => {
+ log.push(v);
+ });
+ mount("direct", x, log);
+ return true;
+ },
+ v => {
+ log.push(`open ${v}`);
+ }
+ );
+ });
+ flush();
+ const release = hold(() => setX(1));
+ log.length = 0;
+
+ setOpen(true);
+ flush();
+ expect(log).not.toContain("open true");
+ expect(log.filter(l => l.startsWith("label"))).toEqual([]);
+
+ release();
+ await tick();
+ expect(log).toContain("open true");
+ expect(log).toContain("label 1");
+ expect(log.at(-1)).toBe("view content 1");
+ });
+
+ it("the boundary's hold stays its own: a mount over another hold after the flush reveals at that hold's release", async () => {
+ const [x, setX] = createSignal(0);
+ const [y, setY] = createSignal(0);
+ const [open, setOpen] = createSignal(false);
+ const log: string[] = [];
+ createRoot(() => {
+ createRenderEffect(
+ () => {
+ if (!open()) return false;
+ mount("direct", x, log);
+ return true;
+ },
+ v => {
+ log.push(`open ${v}`);
+ }
+ );
+ });
+ flush();
+ const releaseX = hold(() => setX(1));
+ const releaseY = hold(() => setY(1));
+ log.length = 0;
+
+ setOpen(true);
+ flush();
+ expect(log).toEqual(["view fallback", "open true"]);
+
+ // Mounted from mainline, outside a flush, over y's hold only.
+ const other: string[] = [];
+ createRoot(() => mount("direct", y, other));
+ flush();
+ expect(other).toEqual(["view fallback"]);
+
+ releaseY();
+ await tick();
+ expect(other).toEqual(["view fallback", "view content 1"]);
+ expect(log).toEqual(["view fallback", "open true"]);
+
+ releaseX();
+ await tick();
+ expect(log.at(-1)).toBe("view content 1");
+ });
+});
+
+describe("#3540 on a hold: a fresh Loading mounted from mainline over a held value", () => {
+ for (const [kind, how] of shapes)
+ it(`content reads the held value ${how}: the fallback now, the content at the commit`, async () => {
+ const [x, setX] = createSignal(0);
+ const release = hold(() => setX(1));
+ const log: string[] = [];
+ createRoot(() => mount(kind, x, log));
+ flush();
+ expect(log).toEqual(["view fallback"]);
+
+ release();
+ await tick();
+ expect(log.slice(1).sort()).toEqual(revealed(kind).sort());
+ });
+});
diff --git a/packages/web/test/loading-fallback-in-flush-3540.spec.tsx b/packages/web/test/loading-fallback-in-flush-3540.spec.tsx
new file mode 100644
index 000000000..21e199a71
--- /dev/null
+++ b/packages/web/test/loading-fallback-in-flush-3540.spec.tsx
@@ -0,0 +1,119 @@
+/**
+ * @jsxImportSource @solidjs/web
+ * @vitest-environment jsdom
+ */
+import { describe, expect, test } from "vitest";
+import { Loading, Show, action, createMemo, createSignal, flush } from "solid-js";
+import { render } from "../src/index.js";
+
+const tick = async () => {
+ for (let i = 0; i < 6; i++) await Promise.resolve();
+ flush();
+};
+
+/**
+ * #3540, in a flush: a `` that flips mainline to mount a fresh
+ * `` over a held value shows `open` and the fallback now; the
+ * content reveals with the hold's commit. A derivation outside the boundary
+ * in the same mount still makes the tick join the hold (membership is the
+ * tick's).
+ */
+describe("a fresh Loading mounted by a flush over a held value (#3540)", () => {
+ function setup(outside: boolean) {
+ const container = document.createElement("div");
+ const [x, setX] = createSignal(0);
+ const [open, setOpen] = createSignal(false);
+ function Content() {
+ const m = createMemo(() => `content ${x()}`);
+ return
"
+ );
+ s.dispose();
+ });
+});
From 4c6d570f7c6e45e03b170fff0b4abd7c813871a1 Mon Sep 17 00:00:00 2001
From: Ryan Carniato
Date: Tue, 6 Oct 2026 01:35:19 -0700
Subject: [PATCH 2/5] fix(signals): the first-pass catch asks only the nearest
loading boundary, and only one that has not shown content (#3540)
A revealed boundary (re-armed by `on` or not) neither catches nor records
a never-committed first pass, and no boundary past the nearest is asked:
which boundaries catch what is unchanged beyond A29's boundary exemption.
catchStatus is back to next's.
Refs #3540
Co-authored-by: Claude
Co-authored-by: Cursor
---
packages/signals/docs/RULES-INDEX.md | 2 +-
packages/signals/docs/SPEC-ASYNC-SEMANTICS.md | 2 +-
packages/signals/src/boundaries.ts | 19 +-
packages/signals/src/core/scheduler.ts | 6 +-
.../loading-fallback-in-flush-3540.test.ts | 345 ++++++++++++++++++
5 files changed, 366 insertions(+), 8 deletions(-)
diff --git a/packages/signals/docs/RULES-INDEX.md b/packages/signals/docs/RULES-INDEX.md
index 5c10bf7a7..cabf38466 100644
--- a/packages/signals/docs/RULES-INDEX.md
+++ b/packages/signals/docs/RULES-INDEX.md
@@ -73,7 +73,7 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul
| A26 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:105` | — | action-await-contract.test.ts×2 fuzz-findings-l2.test.ts×1 posture-store-parity.test.ts×2 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×1 visibility-oracle.test.ts×1 | [ruled 2026-07-17] An ambient transaction window is one flush; parking is flush-driven — (**ruled 2026-07-17**, #2913; **enforcement hardened 2026-08-31**, #3141 — parking is flush-driven, and a trans… |
| A27 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:291` | — | loading-value.test.ts×2 visibility-oracle.states.ts×18 visibility-oracle.test.ts×1 | [ruled 2026-08-10] The commit-#0 loading window is loading-class and verdict-quiet — (**ruled 2026-08-10**) **The commit-#0 loading window is loading-class and verdict-quiet.** A node born committed v… |
| A28 | ruled, mechanism landed | `docs/SPEC-ASYNC-SEMANTICS.md:89` | constants.ts×1 core.ts×12 lanes.ts×2 scheduler.ts×3 types.ts×1 verdict.ts×2 store.ts×4 | createOptimistic.test.ts×5 fuzz-findings-l2.test.ts×6 held-derivation-not-a-proposal-3612.test.ts×1 latest-held-till-flush.test.ts×1 posture-store-parity.test.ts×5 question-scoped-pending.test.ts×3 snapshot-derived-store-rows.test.ts×1 createOptimisticStore.test.ts×10 optimistic-draft-visibility-3665.test.ts×5 optimistic-list-mutation-matrix.harness.ts×1 optimistic-list-mutation-matrix.test.ts×2 shallow.test.ts×1 woken-transaction-adopts-staged-bump.test.ts×1 treeshake.test.ts×2 verdict-contract.test.ts×1 visibility-oracle-store.states.ts×8 visibility-oracle.states.ts×8 | [ruled, mechanism landed 2026-09-15] A write becomes visible at flush — to every channel — (**ruled 2026-09-08**; supersedes the #2922 mid-tick pull) **A write becomes visible at flush — to every chan… |
-| A29 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:113` | boundaries.ts×1 action.ts×1 constants.ts×1 core.ts×10 effect.ts×1 scheduler.ts×4 signals.ts×1 store.ts×1 | adoption-unchanged-key-read-3706.test.ts×9 body-end-supersession-visibility.test.ts×1 born-held.test.ts×3 boundary-not-born-held-3540.test.ts×4 createProjection.draft-lifetime-3585.test.ts×1 direct-commit-readers-posture.test.ts×1 fuzz-findings-l2.test.ts×6 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 held-truth-lane-only.test.ts×3 l2-contract.test.ts×1 latest-held-till-flush.test.ts×2 loading-fallback-in-flush-3540.test.ts×1 mount-over-foreign-hold-3761.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×3 posture-born-held-and-observation.test.ts×1 posture-store-parity.test.ts×6 derived-presence-async-3726.test.ts×3 optimistic-untracked-reads-f3-f5.test.ts×1 store-unchanged-read-independent-write-3688.test.ts×1 tick-scoped-pass-transaction.test.ts×2 treeshake.test.ts×3 visibility-oracle-store.states.ts×5 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×9 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-09-13 (#3408)] A tracked read served a live transaction's staged value enters that transaction — A tracked computation served a node's staged `_pendingValue` — a value a … |
+| A29 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:113` | boundaries.ts×2 action.ts×1 constants.ts×1 core.ts×10 effect.ts×1 scheduler.ts×4 signals.ts×1 store.ts×1 | adoption-unchanged-key-read-3706.test.ts×9 body-end-supersession-visibility.test.ts×1 born-held.test.ts×3 boundary-not-born-held-3540.test.ts×4 createProjection.draft-lifetime-3585.test.ts×1 direct-commit-readers-posture.test.ts×1 fuzz-findings-l2.test.ts×6 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 held-truth-lane-only.test.ts×3 l2-contract.test.ts×1 latest-held-till-flush.test.ts×2 loading-fallback-in-flush-3540.test.ts×1 mount-over-foreign-hold-3761.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×3 posture-born-held-and-observation.test.ts×1 posture-store-parity.test.ts×6 derived-presence-async-3726.test.ts×3 optimistic-untracked-reads-f3-f5.test.ts×1 store-unchanged-read-independent-write-3688.test.ts×1 tick-scoped-pass-transaction.test.ts×2 treeshake.test.ts×3 visibility-oracle-store.states.ts×5 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×9 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-09-13 (#3408)] A tracked read served a live transaction's staged value enters that transaction — A tracked computation served a node's staged `_pendingValue` — a value a … |
| A30 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:247` | async.ts×3 attribution.ts×1 constants.ts×1 core.ts×1 effect.ts×1 lanes.ts×1 scheduler.ts×5 | async-landing-deps-3461.test.ts×3 fuzz-findings-l2.test.ts×3 held-conditional-effect.test.ts×1 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 ispending-in-boundary-on-3528.test.ts×1 lane-frame-deferred-run-3662.test.ts×1 lane-frame-held-lane-3662.test.ts×1 posture-born-held-and-observation.test.ts×1 treeshake.test.ts×2 write-proposals-3494.test.ts×2 zombie-rerun-after-commit-3546.test.ts×2 | [ruled 2026-09-13 (#3410)] A memo's dependencies are the committed frame's until the frame is replaced — A pass that _staged_ its value has not replaced the committed frame, so the committed value sti… |
| A31 | live | `docs/SPEC-ASYNC-SEMANTICS.md:121` | boundaries.ts×1 core.ts×1 lanes.ts×1 verdict.ts×2 | fuzz-findings-l2.test.ts×6 ispending-combined-atomic-3442.test.ts×1 | [live 2026-09-14 (#3442)] A memo computes under its own lane posture, never its puller's — A memo's value is one shared slot every reader sees, so its pass runs under the lane posture the memo itself … |
| A32 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:129` | core.ts×4 lanes.ts×1 store.ts×1 | visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×9 visibility-oracle.test.ts×1 | [ruled 2026-09-14] Children-forbidden readers see the frame, not the graph — `createTrackedEffect` and `onSettled` callbacks are effect-phase code that runs after the frame is decided. They read the f… |
diff --git a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md
index ad1a31871..74faba282 100644
--- a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md
+++ b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md
@@ -55,7 +55,7 @@ The former Tier A table is these sections. Tier B/C, the fixed violations, and t
- **A first load derived from a hold lands into it** (#3800): a first load born pending in a frame that read a hold lands into that hold if the hold is still live, else as its own commit; the hold commits without it. **Pending** — `fix/create-time-holds` (`91e474506`); [#3800](https://github.com/solidjs/solid/issues/3800) is open.
- **A first pass under a loading boundary that has not shown content is the boundary's, not the tick's** — in a flush as outside one (A29's boundary exemption, #3540). The out-of-flush form is on `next`; the in-flush form (a fresh `Loading` over a held value shows its fallback in a flush) is **pending** — `fix/create-time-holds` (`ca71e5d9d`).
-**The boundary scope, in a flush (2026-10-06, amendment — #3540's L2 regression; the bullet above is unchanged).** The in-flush form lands with `fix/l2-3540-fresh-loading`, standalone of `fix/create-time-holds`. A first pass (`STATUS_UNINITIALIZED`) that reads a hold asks the boundaries up its chain (`GlobalQueue._fresh`: caught as a pending status). When a loading boundary that has not shown content catches it, the pass joins pass-scoped (`passTx`), as a mount outside a flush does, instead of joining the flush: the Show that mounted the boundary publishes, and the boundary shows its fallback. A pass nothing catches joins the flush as before, so a derivation outside the boundary in the same flush still holds the tick. A render effect born held under the boundary (content bound in JSX — the tree never reads the held value) is caught the same way, so the boundary does not reveal empty content, in a flush or out of one; its output's first pass shows the fallback whenever it has collected readers. A flush that set `passTx` clears it at its end, so the boundary's hold does not merge with a later tick's. The `direction-rule-probe` pins are unchanged. **Pinned by:** `tests/loading-fallback-in-flush-3540.test.ts` (in a flush: through a memo, directly, bound, nested under a Loading that has shown content; a derivation outside the boundary still holds the tick; the boundary's hold stays its own; on a hold, from mainline: the same three content shapes); `packages/web/test/loading-fallback-in-flush-3540.spec.tsx`.
+**The boundary scope, in a flush (2026-10-06, amendment — #3540's L2 regression; the bullet above is unchanged).** The in-flush form lands with `fix/l2-3540-fresh-loading`, standalone of `fix/create-time-holds`. A first pass (`STATUS_UNINITIALIZED`) that reads a hold asks its nearest loading boundary (`GlobalQueue._fresh`). Only a boundary that has not shown content catches it; a boundary that has (re-armed by `on` or not) neither catches nor records it, and no boundary past the nearest is asked, so which boundaries catch what is otherwise unchanged. When the boundary catches it, the pass joins pass-scoped (`passTx`), as a mount outside a flush does, instead of joining the flush: the Show that mounted the boundary publishes, and the boundary shows its fallback. A pass nothing catches joins the flush as before, so a derivation outside the boundary in the same flush still holds the tick. A render effect born held under the boundary (content bound in JSX — the tree never reads the held value) is caught the same way, so the boundary does not reveal empty content, in a flush or out of one; its output's first pass shows the fallback whenever it has collected readers. A flush that set `passTx` clears it at its end, so the boundary's hold does not merge with a later tick's. The `direction-rule-probe` pins are unchanged. **Pinned by:** `tests/loading-fallback-in-flush-3540.test.ts` (in a flush: through a memo, directly, bound, nested under a Loading that has shown content; a derivation outside the boundary still holds the tick; the boundary's hold stays its own; on a hold, from mainline: the same three content shapes; under an inner Loading that has not shown content, the outer revealed one records nothing, is not pending, and the commit does not wait for the first load; directly under a revealed Loading, or under a revealed one inside one that has not shown content, not caught, as before; `on` boundaries revealed (re-armed or not) as before, not yet revealed as a Loading without `on`); `packages/web/test/loading-fallback-in-flush-3540.spec.tsx`.
**Not yet one-way (recorded, not ruled).** The frame of a tick is still one transaction (the tick ruling above stands), so two shapes keep the hold waiting on first loads it never needed:
diff --git a/packages/signals/src/boundaries.ts b/packages/signals/src/boundaries.ts
index 631f60627..06f79b3e7 100644
--- a/packages/signals/src/boundaries.ts
+++ b/packages/signals/src/boundaries.ts
@@ -203,7 +203,7 @@ export function redraw(b: Boundary): void {
/** GlobalQueue._catch: status from a frame reader, nearest boundary first.
* A loading boundary on the way records a pending reader whether or not it
* catches it (its `on` may collect it later). */
-function catchStatus(node: Computed, flags: number, error?: unknown): boolean {
+function catchStatus(node: Computed, flags: number, error: unknown): boolean {
if (flags === 0) {
// A status cleared. Judged by the node's status now, against each
// boundary's own rule (`unsettled`) — the error path clears pending
@@ -236,6 +236,20 @@ function catchStatus(node: Computed, flags: number, error?: unknown): boole
return false;
}
+/** GlobalQueue._fresh: a pass that read a hold. A first pass is not pending,
+ * and only A29's boundary exemption catches it (#3540): the nearest loading
+ * boundary, if it has not shown content. One that has (re-armed or not)
+ * neither catches nor records it — it joins the hold like any reader of
+ * committed content — and no boundary past the nearest is asked. */
+function fresh(node: Computed): boolean {
+ if (!(node._statusFlags & STATUS_UNINITIALIZED)) return false;
+ let b = boundaryOf(node);
+ while (b !== undefined && b._type !== STATUS_PENDING) b = b._parent ?? undefined;
+ if (b === undefined || b._initialized) return false;
+ caught(b, node, undefined);
+ return true;
+}
+
/** The nearest boundary on `r`'s chain showing its fallback, if any. */
function hiddenBy(r: Computed): Boundary | undefined {
for (let b = boundaryOf(r); b !== undefined; b = b._parent ?? undefined)
@@ -774,8 +788,7 @@ export function createErrorBoundary(
// Installed at module evaluation — present exactly when something imports a
// boundary. An app without one pays the three null checks and nothing else.
GlobalQueue._catch = catchStatus;
-GlobalQueue._fresh = node =>
- node._statusFlags & STATUS_UNINITIALIZED && catchStatus(node, STATUS_PENDING);
+GlobalQueue._fresh = fresh;
GlobalQueue._hidden = hidden;
GlobalQueue._boundarySeam = boundarySeam;
GlobalQueue._heldRun = heldRun;
diff --git a/packages/signals/src/core/scheduler.ts b/packages/signals/src/core/scheduler.ts
index 7db835628..77711bc18 100644
--- a/packages/signals/src/core/scheduler.ts
+++ b/packages/signals/src/core/scheduler.ts
@@ -655,14 +655,14 @@ export class GlobalQueue implements IQueue {
// and the root. `_catch` — status from a frame reader, nearest boundary
// first (true: caught, the root never hears of it; a clear — flags 0 —
// settles the reader there); `_fresh` — a pass that read a hold (true: a
- // first pass a loading boundary that has not shown content caught,
- // `joinPass`); `_hidden` — a frame reader behind a fallback is not on
+ // first pass the nearest loading boundary, not having shown content,
+ // caught — `joinPass`); `_hidden` — a frame reader behind a fallback is not on
// screen and holds nothing; `_boundarySeam` — the seam's sweep (readers
// gone or settled without a pass reveal; an `on` re-arm resolves).
declare static _catch:
| ((node: Computed, flags: number, error: unknown) => boolean)
| undefined;
- declare static _fresh: ((node: Computed) => unknown) | undefined;
+ declare static _fresh: ((node: Computed) => boolean) | undefined;
declare static _hidden: ((r: Computed) => boolean) | undefined;
declare static _boundarySeam: (() => void) | undefined;
// `_heldRun` — a queued run under a fallback-showing boundary waits for
diff --git a/packages/signals/tests/loading-fallback-in-flush-3540.test.ts b/packages/signals/tests/loading-fallback-in-flush-3540.test.ts
index a0a0bbf32..b6d1c9aad 100644
--- a/packages/signals/tests/loading-fallback-in-flush-3540.test.ts
+++ b/packages/signals/tests/loading-fallback-in-flush-3540.test.ts
@@ -18,6 +18,8 @@ import {
createRoot,
createSignal,
flush,
+ getOwner,
+ isPending,
untrack
} from "../src/index.js";
@@ -226,3 +228,346 @@ describe("#3540 on a hold: a fresh Loading mounted from mainline over a held val
expect(log.slice(1).sort()).toEqual(revealed(kind).sort());
});
});
+
+/** The readers recorded on the boundary `owner` is under (internal: the
+ * catch's registrations). */
+function readersOf(owner: object): number {
+ const context = (owner as { _context: Record })._context;
+ for (const key of Object.getOwnPropertySymbols(context)) {
+ const b = context[key] as { _readers?: Set } | null;
+ if (b?._readers) return b._readers.size;
+ }
+ throw new Error("no boundary");
+}
+
+describe("#3540: the first-pass catch belongs to the boundary that has not shown content, and to no other", () => {
+ /**
+ * A revealed outer `Loading`; a transaction holding `x`; a Show under the
+ * outer one mounts, in a flush, a fresh computation over `x` with a first
+ * load of its own (`slow`) — under an inner `Loading` that has not shown
+ * content (`inner`), or directly under the revealed outer one.
+ */
+ function setup(inner: boolean) {
+ const [x, setX] = createSignal(0);
+ const [open, setOpen] = createSignal(false);
+ const log: string[] = [];
+ let resolveSlow!: () => void;
+ let outerOwner!: object;
+ let freshOwner!: object;
+ let outer!: () => unknown;
+ createRoot(() => {
+ createRenderEffect(x, v => {
+ log.push(`x ${v}`);
+ });
+ outer = Loading(() => {
+ outerOwner = getOwner()!;
+ createRenderEffect(
+ () => {
+ if (!open()) return false;
+ const body = () => {
+ freshOwner = getOwner()!;
+ const m = createMemo(async () => {
+ const v = x();
+ await new Promise(r => (resolveSlow = r));
+ return `fresh ${v}`;
+ });
+ createRenderEffect(m, v => {
+ log.push(`text ${v}`);
+ });
+ return "p";
+ };
+ if (!inner) return body();
+ const view = Loading(body, "inner fallback");
+ createRenderEffect(view, v => {
+ log.push(`inner ${v}`);
+ });
+ return true;
+ },
+ v => {
+ log.push(`open ${v}`);
+ }
+ );
+ return "outer";
+ }, "outer fallback");
+ createRenderEffect(outer, v => {
+ log.push(`outer ${v}`);
+ });
+ });
+ flush();
+ expect(log).toEqual(["x 0", "open false", "outer outer"]);
+ const release = hold(() => setX(1));
+ log.length = 0;
+ setOpen(true);
+ flush();
+ const state = () => ({
+ log: [...log],
+ outerPending: isPending(() => outer()),
+ outerReaders: readersOf(outerOwner),
+ freshReaders: readersOf(freshOwner)
+ });
+ return { release, resolve: () => resolveSlow(), state };
+ }
+
+ it("under an inner Loading that has not shown content: the inner one catches it; the outer one records nothing, is not pending, and the commit does not wait for it", async () => {
+ const s = setup(true);
+ expect(s.state()).toEqual({
+ log: ["inner inner fallback", "open true"],
+ outerPending: false,
+ outerReaders: 0,
+ freshReaders: 2
+ });
+
+ s.release();
+ await tick();
+ // The transaction commits without the fresh computation's first load.
+ expect(s.state()).toMatchObject({
+ log: ["inner inner fallback", "open true", "x 1"],
+ outerPending: false,
+ outerReaders: 0
+ });
+
+ s.resolve();
+ await tick();
+ expect(s.state()).toEqual({
+ log: ["inner inner fallback", "open true", "x 1", "text fresh 1", "inner p"],
+ outerPending: false,
+ outerReaders: 0,
+ freshReaders: 0
+ });
+ });
+
+ it("directly under the revealed Loading (none has not shown content): not caught — it joins the transaction, as on next", async () => {
+ const s = setup(false);
+ const trace: unknown[] = [s.state()];
+ s.release();
+ await tick();
+ trace.push(s.state());
+ s.resolve();
+ await tick();
+ trace.push(s.state());
+ // The Show's mount waits with the transaction, the outer content stays,
+ // and the outer boundary holds the one pending reader the frame
+ // forwarded (the memo) — never the born-held binding.
+ const held = { log: [], outerPending: false, outerReaders: 1, freshReaders: 1 };
+ expect(trace).toEqual([
+ held,
+ held,
+ {
+ log: ["text fresh 1", "x 1", "open p"],
+ outerPending: false,
+ outerReaders: 0,
+ freshReaders: 0
+ }
+ ]);
+ });
+
+ it("under a revealed Loading inside one that has not shown content: the nearest one is asked only — not caught, as on next", async () => {
+ const [x, setX] = createSignal(0);
+ const [open, setOpen] = createSignal(false);
+ let resolveGate!: () => void;
+ const log: string[] = [];
+ let outerOwner!: object;
+ let innerOwner!: object;
+ createRoot(() => {
+ const outer = Loading(() => {
+ outerOwner = getOwner()!;
+ // A revealed inner Loading whose tree mounts the fresh computation.
+ const inner = Loading(() => {
+ innerOwner = getOwner()!;
+ if (!open()) return "closed";
+ const m = createMemo(() => `fresh ${x()}`);
+ return m();
+ }, "inner fallback");
+ createRenderEffect(inner, v => {
+ log.push(`inner ${v}`);
+ });
+ // A sibling still loading: the outer one has not shown content.
+ const gate = createMemo(() => new Promise(r => (resolveGate = () => r("gate"))));
+ createRenderEffect(gate, v => {
+ log.push(`gate ${v}`);
+ });
+ return "outer";
+ }, "outer fallback");
+ createRenderEffect(outer, v => {
+ log.push(`outer ${v}`);
+ });
+ });
+ flush();
+ const release = hold(() => setX(1));
+ const state = () => ({
+ log: [...log],
+ outerReaders: readersOf(outerOwner),
+ innerReaders: readersOf(innerOwner)
+ });
+ const trace: unknown[] = [state()];
+ setOpen(true);
+ flush();
+ trace.push(state());
+ release();
+ await tick();
+ trace.push(state());
+ resolveGate();
+ await tick();
+ trace.push(state());
+ // The outer boundary only ever waits on the gate; the revealed inner one
+ // records nothing.
+ const waiting = {
+ log: ["inner closed", "outer outer fallback"],
+ outerReaders: 1,
+ innerReaders: 0
+ };
+ expect(trace).toEqual([
+ waiting,
+ waiting,
+ waiting,
+ {
+ log: ["inner closed", "outer outer fallback", "gate gate", "inner fresh 1", "outer outer"],
+ outerReaders: 0,
+ innerReaders: 0
+ }
+ ]);
+ });
+});
+
+describe("#3540 and `on` (a dependency list, #3575): the first-pass catch adds nothing to what `on` does", () => {
+ /** A revealed ``; a transaction holding `x`; a Show in
+ * its content mounts, in a flush, a fresh computation over `x`. `rearm`:
+ * the Show's own signal is in `on`, so the mounting flush re-arms it. */
+ function trace(rearm: boolean) {
+ const [x, setX] = createSignal(0);
+ const [open, setOpen] = createSignal(false);
+ const log: string[] = [];
+ let owner!: object;
+ createRoot(() => {
+ createRenderEffect(x, v => {
+ log.push(`x ${v}`);
+ });
+ const view = untrack(() =>
+ createLoadingBoundary(
+ () => {
+ owner = getOwner()!;
+ createRenderEffect(
+ () => {
+ if (!open()) return false;
+ const m = createMemo(() => `fresh ${x()}`);
+ createRenderEffect(m, v => {
+ log.push(`text ${v}`);
+ });
+ return true;
+ },
+ v => {
+ log.push(`open ${v}`);
+ }
+ );
+ return "content";
+ },
+ () => "fallback",
+ { on: () => (rearm ? open() : undefined) }
+ )
+ );
+ createRenderEffect(view, v => {
+ log.push(`view ${v}`);
+ });
+ });
+ flush();
+ const release = hold(() => setX(1));
+ log.length = 0;
+ const state = () => ({ log: [...log], readers: readersOf(owner) });
+ return { release, setOpen, state };
+ }
+
+ for (const rearm of [false, true])
+ it(`revealed, ${rearm ? "re-armed by" : "not re-armed by"} the mounting flush: not caught — the mount joins the transaction, as on next`, async () => {
+ const s = trace(rearm);
+ s.setOpen(true);
+ flush();
+ const steps: unknown[] = [s.state()];
+ s.release();
+ await tick();
+ steps.push(s.state());
+ expect(steps).toEqual([
+ { log: [], readers: 0 },
+ { log: ["text fresh 1", "x 1", "open true"], readers: 0 }
+ ]);
+ });
+
+ for (const rearm of [false, true])
+ it(`revealed, its tree mounting in the flush that ${rearm ? "re-arms" : "does not re-arm"} it: not caught, as on next`, async () => {
+ const [x, setX] = createSignal(0);
+ const [open, setOpen] = createSignal(false);
+ const log: string[] = [];
+ let owner!: object;
+ createRoot(() => {
+ createRenderEffect(x, v => {
+ log.push(`x ${v}`);
+ });
+ const view = untrack(() =>
+ createLoadingBoundary(
+ () => {
+ owner = getOwner()!;
+ if (!open()) return "closed";
+ return createMemo(() => `fresh ${x()}`)();
+ },
+ () => "fallback",
+ { on: () => (rearm ? open() : undefined) }
+ )
+ );
+ createRenderEffect(view, v => {
+ log.push(`view ${v}`);
+ });
+ });
+ flush();
+ const release = hold(() => setX(1));
+ log.length = 0;
+ setOpen(true);
+ flush();
+ const steps: unknown[] = [{ log: [...log], readers: readersOf(owner) }];
+ release();
+ await tick();
+ steps.push({ log: [...log], readers: readersOf(owner) });
+ expect(steps).toEqual([
+ { log: [], readers: 0 },
+ { log: ["x 1", "view fresh 1"], readers: 0 }
+ ]);
+ });
+
+ for (const inFlush of [true, false])
+ it(`not yet revealed, mounted ${inFlush ? "in a flush" : "from mainline"} over the held value: the fallback now, the content at the commit, as a Loading without \`on\``, async () => {
+ const [x, setX] = createSignal(0);
+ const [open, setOpen] = createSignal(!inFlush);
+ const [key] = createSignal(0);
+ const release = hold(() => setX(1));
+ const log: string[] = [];
+ createRoot(() =>
+ createRenderEffect(
+ () => {
+ if (!open()) return false;
+ const view = untrack(() =>
+ createLoadingBoundary(
+ () => `content ${x()}`,
+ () => "fallback",
+ { on: key }
+ )
+ );
+ createRenderEffect(view, v => {
+ log.push(`view ${v}`);
+ });
+ return true;
+ },
+ v => {
+ log.push(`open ${v}`);
+ }
+ )
+ );
+ flush();
+ if (inFlush) {
+ log.length = 0;
+ setOpen(true);
+ flush();
+ }
+ expect(log).toEqual(["view fallback", "open true"]);
+ release();
+ await tick();
+ expect(log).toEqual(["view fallback", "open true", "view content 1"]);
+ });
+});
From 4a9551460974e2766dc2f423670c9a1fafb13523 Mon Sep 17 00:00:00 2001
From: Ryan Carniato
Date: Tue, 6 Oct 2026 01:44:17 -0700
Subject: [PATCH 3/5] fix(signals): ask the nearest loading boundary through
_catch; no new hook (#3540)
A pass that read a hold asks GlobalQueue._catch with STATUS_UNINITIALIZED
in its flags. Only a first pass is caught, and only by the nearest loading
boundary if it has not shown content (`_initialized`, not `_armed`): a
revealed boundary, re-armed by `on` or not, neither catches nor records
it, and no boundary past the nearest is asked. Pins the revealed-boundary,
nested and `on` shapes against next.
Refs #3540
Co-authored-by: Claude
Co-authored-by: Cursor
---
packages/signals/docs/SPEC-ASYNC-SEMANTICS.md | 2 +-
packages/signals/src/boundaries.ts | 32 +++++++++----------
packages/signals/src/core/core.ts | 2 +-
packages/signals/src/core/scheduler.ts | 14 ++++----
4 files changed, 24 insertions(+), 26 deletions(-)
diff --git a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md
index 74faba282..5ce2c6ec8 100644
--- a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md
+++ b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md
@@ -55,7 +55,7 @@ The former Tier A table is these sections. Tier B/C, the fixed violations, and t
- **A first load derived from a hold lands into it** (#3800): a first load born pending in a frame that read a hold lands into that hold if the hold is still live, else as its own commit; the hold commits without it. **Pending** — `fix/create-time-holds` (`91e474506`); [#3800](https://github.com/solidjs/solid/issues/3800) is open.
- **A first pass under a loading boundary that has not shown content is the boundary's, not the tick's** — in a flush as outside one (A29's boundary exemption, #3540). The out-of-flush form is on `next`; the in-flush form (a fresh `Loading` over a held value shows its fallback in a flush) is **pending** — `fix/create-time-holds` (`ca71e5d9d`).
-**The boundary scope, in a flush (2026-10-06, amendment — #3540's L2 regression; the bullet above is unchanged).** The in-flush form lands with `fix/l2-3540-fresh-loading`, standalone of `fix/create-time-holds`. A first pass (`STATUS_UNINITIALIZED`) that reads a hold asks its nearest loading boundary (`GlobalQueue._fresh`). Only a boundary that has not shown content catches it; a boundary that has (re-armed by `on` or not) neither catches nor records it, and no boundary past the nearest is asked, so which boundaries catch what is otherwise unchanged. When the boundary catches it, the pass joins pass-scoped (`passTx`), as a mount outside a flush does, instead of joining the flush: the Show that mounted the boundary publishes, and the boundary shows its fallback. A pass nothing catches joins the flush as before, so a derivation outside the boundary in the same flush still holds the tick. A render effect born held under the boundary (content bound in JSX — the tree never reads the held value) is caught the same way, so the boundary does not reveal empty content, in a flush or out of one; its output's first pass shows the fallback whenever it has collected readers. A flush that set `passTx` clears it at its end, so the boundary's hold does not merge with a later tick's. The `direction-rule-probe` pins are unchanged. **Pinned by:** `tests/loading-fallback-in-flush-3540.test.ts` (in a flush: through a memo, directly, bound, nested under a Loading that has shown content; a derivation outside the boundary still holds the tick; the boundary's hold stays its own; on a hold, from mainline: the same three content shapes; under an inner Loading that has not shown content, the outer revealed one records nothing, is not pending, and the commit does not wait for the first load; directly under a revealed Loading, or under a revealed one inside one that has not shown content, not caught, as before; `on` boundaries revealed (re-armed or not) as before, not yet revealed as a Loading without `on`); `packages/web/test/loading-fallback-in-flush-3540.spec.tsx`.
+**The boundary scope, in a flush (2026-10-06, amendment — #3540's L2 regression; the bullet above is unchanged).** The in-flush form lands with `fix/l2-3540-fresh-loading`, standalone of `fix/create-time-holds`. A first pass (`STATUS_UNINITIALIZED`) that reads a hold asks its nearest loading boundary (`GlobalQueue._catch`, with `STATUS_UNINITIALIZED` in its flags; no new hook). Only a boundary that has not shown content catches it; a boundary that has (re-armed by `on` or not) neither catches nor records it, and no boundary past the nearest is asked, so which boundaries catch what is otherwise unchanged. When the boundary catches it, the pass joins pass-scoped (`passTx`), as a mount outside a flush does, instead of joining the flush: the Show that mounted the boundary publishes, and the boundary shows its fallback. A pass nothing catches joins the flush as before, so a derivation outside the boundary in the same flush still holds the tick. A render effect born held under the boundary (content bound in JSX — the tree never reads the held value) is caught the same way, so the boundary does not reveal empty content, in a flush or out of one; its output's first pass shows the fallback whenever it has collected readers. A flush that set `passTx` clears it at its end, so the boundary's hold does not merge with a later tick's. The `direction-rule-probe` pins are unchanged. **Pinned by:** `tests/loading-fallback-in-flush-3540.test.ts` (in a flush: through a memo, directly, bound, nested under a Loading that has shown content; a derivation outside the boundary still holds the tick; the boundary's hold stays its own; on a hold, from mainline: the same three content shapes; under an inner Loading that has not shown content, the outer revealed one records nothing, is not pending, and the commit does not wait for the first load; directly under a revealed Loading, or under a revealed one inside one that has not shown content, not caught, as before; `on` boundaries revealed (re-armed or not) as before, not yet revealed as a Loading without `on`); `packages/web/test/loading-fallback-in-flush-3540.spec.tsx`.
**Not yet one-way (recorded, not ruled).** The frame of a tick is still one transaction (the tick ruling above stands), so two shapes keep the hold waiting on first loads it never needed:
diff --git a/packages/signals/src/boundaries.ts b/packages/signals/src/boundaries.ts
index 06f79b3e7..994890a7f 100644
--- a/packages/signals/src/boundaries.ts
+++ b/packages/signals/src/boundaries.ts
@@ -202,8 +202,14 @@ export function redraw(b: Boundary): void {
/** GlobalQueue._catch: status from a frame reader, nearest boundary first.
* A loading boundary on the way records a pending reader whether or not it
- * catches it (its `on` may collect it later). */
-function catchStatus(node: Computed, flags: number, error: unknown): boolean {
+ * catches it (its `on` may collect it later). A pass that read a hold
+ * (`joinPass`: STATUS_UNINITIALIZED in `flags`) is asked too. A first pass
+ * is not pending — it has never committed — and only A29's boundary
+ * exemption catches it (#3540): the nearest loading boundary, if it has not
+ * shown content. One that has, re-armed by `on` or not, neither catches nor
+ * records it (it joins the hold like any reader of committed content), no
+ * boundary past the nearest is asked, and a committed pass is not caught. */
+function catchStatus(node: Computed, flags: number, error?: unknown): boolean {
if (flags === 0) {
// A status cleared. Judged by the node's status now, against each
// boundary's own rule (`unsettled`) — the error path clears pending
@@ -226,6 +232,13 @@ function catchStatus(node: Computed, flags: number, error: unknown): boolea
}
for (let b = boundaryOf(node); b !== undefined; b = b._parent ?? undefined) {
if (!(b._type & flags)) continue;
+ // A pass that read a hold: the nearest loading boundary only, and only
+ // a first pass before it has shown content (then it is collecting).
+ if (
+ flags & STATUS_UNINITIALIZED &&
+ (b._initialized || !(node._statusFlags & STATUS_UNINITIALIZED))
+ )
+ break;
if (isCollecting(b)) {
caught(b, node, error);
return true;
@@ -236,20 +249,6 @@ function catchStatus(node: Computed, flags: number, error: unknown): boolea
return false;
}
-/** GlobalQueue._fresh: a pass that read a hold. A first pass is not pending,
- * and only A29's boundary exemption catches it (#3540): the nearest loading
- * boundary, if it has not shown content. One that has (re-armed or not)
- * neither catches nor records it — it joins the hold like any reader of
- * committed content — and no boundary past the nearest is asked. */
-function fresh(node: Computed): boolean {
- if (!(node._statusFlags & STATUS_UNINITIALIZED)) return false;
- let b = boundaryOf(node);
- while (b !== undefined && b._type !== STATUS_PENDING) b = b._parent ?? undefined;
- if (b === undefined || b._initialized) return false;
- caught(b, node, undefined);
- return true;
-}
-
/** The nearest boundary on `r`'s chain showing its fallback, if any. */
function hiddenBy(r: Computed): Boundary | undefined {
for (let b = boundaryOf(r); b !== undefined; b = b._parent ?? undefined)
@@ -788,7 +787,6 @@ export function createErrorBoundary(
// Installed at module evaluation — present exactly when something imports a
// boundary. An app without one pays the three null checks and nothing else.
GlobalQueue._catch = catchStatus;
-GlobalQueue._fresh = fresh;
GlobalQueue._hidden = hidden;
GlobalQueue._boundarySeam = boundarySeam;
GlobalQueue._heldRun = heldRun;
diff --git a/packages/signals/src/core/core.ts b/packages/signals/src/core/core.ts
index b6b77681c..2b6a0bcbf 100644
--- a/packages/signals/src/core/core.ts
+++ b/packages/signals/src/core/core.ts
@@ -952,7 +952,7 @@ function joinPass(c: Computed, el: Signal | Computed): void {
(el as Computed)._statusFlags & STATUS_UNINITIALIZED ||
(c as any)._type !== EFFECT_RENDER
)
- joinPassTx(txOf(el), GlobalQueue._fresh?.(c));
+ joinPassTx(txOf(el), GlobalQueue._catch?.(c, STATUS_PENDING | STATUS_UNINITIALIZED));
}
/** A15's stale reader (shared-hole and reveal corollaries): a render effect
diff --git a/packages/signals/src/core/scheduler.ts b/packages/signals/src/core/scheduler.ts
index 77711bc18..ecb225cb9 100644
--- a/packages/signals/src/core/scheduler.ts
+++ b/packages/signals/src/core/scheduler.ts
@@ -654,15 +654,15 @@ export class GlobalQueue implements IQueue {
// Boundaries (boundaries.ts): the display consumers between an observer
// and the root. `_catch` — status from a frame reader, nearest boundary
// first (true: caught, the root never hears of it; a clear — flags 0 —
- // settles the reader there); `_fresh` — a pass that read a hold (true: a
- // first pass the nearest loading boundary, not having shown content,
- // caught — `joinPass`); `_hidden` — a frame reader behind a fallback is not on
- // screen and holds nothing; `_boundarySeam` — the seam's sweep (readers
- // gone or settled without a pass reveal; an `on` re-arm resolves).
+ // settles the reader there; a pass that read a hold — STATUS_UNINITIALIZED
+ // in flags, `joinPass` — is caught only as a first pass, by the nearest
+ // loading boundary if it has not shown content); `_hidden` — a frame
+ // reader behind a fallback is not on screen and holds nothing;
+ // `_boundarySeam` — the seam's sweep (readers gone or settled without a
+ // pass reveal; an `on` re-arm resolves).
declare static _catch:
- | ((node: Computed, flags: number, error: unknown) => boolean)
+ | ((node: Computed, flags: number, error?: unknown) => boolean)
| undefined;
- declare static _fresh: ((node: Computed) => boolean) | undefined;
declare static _hidden: ((r: Computed) => boolean) | undefined;
declare static _boundarySeam: (() => void) | undefined;
// `_heldRun` — a queued run under a fallback-showing boundary waits for
From a661e91f04b814eb91429ea1f39ba06923d0e388 Mon Sep 17 00:00:00 2001
From: Ryan Carniato
Date: Tue, 6 Oct 2026 02:13:46 -0700
Subject: [PATCH 4/5] =?UTF-8?q?chore(size):=20Size-Exception=20for=20#3824?=
=?UTF-8?q?=20=E2=80=94=20five=20caps=20at=20CI-measured=20+=2010=20B=20(A?=
=?UTF-8?q?29=20restore,=20#3540)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
app: hydrating + every store primitive family 28.87 -> 28.90 KB,
app: compiled CSR 25.13 -> 25.22 KB, app: compiled hydrating 30.93 ->
30.97 KB, page: base server components 44.84 -> 44.90 KB, page: live
server components 48.51 -> 48.53 KB. Approved by the maintainer.
Refs #3540
Co-authored-by: Claude
Co-authored-by: Cursor
---
scripts/size/floor-caps.json | 4 +--
scripts/size/scenarios.js | 56 ++++++++++++++++++++++++++++++++++--
2 files changed, 55 insertions(+), 5 deletions(-)
diff --git a/scripts/size/floor-caps.json b/scripts/size/floor-caps.json
index 0fb36068d..248d126c0 100644
--- a/scripts/size/floor-caps.json
+++ b/scripts/size/floor-caps.json
@@ -2,8 +2,8 @@
"signals: core floor (createSignal/Memo/Effect/Root/flush)": "7.35 KB",
"app: render + one signal (the simple-app floor)": "9.86 KB",
"app: hydrating (no stores) with Show/For/Loading/Errored/lazy": "17.71 KB",
- "page: base server components (hydrating + dynamic + frames + sf reference)": "44.84 KB",
- "page: live server components (base + live/GET + action + isPending/latest)": "48.51 KB",
+ "page: base server components (hydrating + dynamic + frames + sf reference)": "44.90 KB",
+ "page: live server components (base + live/GET + action + isPending/latest)": "48.53 KB",
"server: floor (getRequestEvent + isServer)": "1.34 KB",
"server: renderToString (the server-render floor)": "20.42 KB"
}
diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js
index 45fab4d25..1222f3164 100644
--- a/scripts/size/scenarios.js
+++ b/scripts/size/scenarios.js
@@ -2070,7 +2070,17 @@ module.exports = [
// render-effect mark in laneDirty (+4). Cap set at measured + 10 B
// rounded up to 0.01 KB. Accepted by the maintainer (2026-10-05). The
// cap is frozen again at 28.87 KB.
- limit: "28.87 KB",
+ // Size-Exception (#3824, 2026-10-06): 28.87 -> 28.90 KB,
+ // measured at 28,881 B by CI against `next` @ 7acc0390e's 28,903
+ // (-22 B; 11 B over the cap; +27 B minified) — A29's boundary
+ // exemption restored under L2 (#3540): a fresh `Loading` over a held
+ // value shows its fallback in a flush, and catches content bound under it; only the nearest loading boundary that has not shown
+ // content catches a first pass (+17 B minified in the core: joinPass,
+ // the pass-scoped join, the flush-end passTx clear; +10-11 B in
+ // boundaries: the catch's nearest-boundary stop); brotli layout.
+ // Cap set at measured + 10 B rounded up to 0.01 KB. Accepted by the
+ // maintainer (2026-10-06). The cap is frozen again at 28.90 KB.
+ limit: "28.90 KB",
alias
},
{
@@ -3024,7 +3034,17 @@ module.exports = [
// Cap at CI-measured + 10 B rounded up to 0.01 KB.
path: "fixtures/compiled/csr.jsx",
compile: { hydratable: false },
- limit: "25.13 KB",
+ // Size-Exception (#3824, 2026-10-06): 25.13 -> 25.22 KB,
+ // measured at 25,204 B by CI against `next` @ 7acc0390e's 25,073
+ // (+131 B; 74 B over the cap; +28 B minified) — A29's boundary
+ // exemption restored under L2 (#3540): a fresh `Loading` over a held
+ // value shows its fallback in a flush, and catches content bound under it; only the nearest loading boundary that has not shown
+ // content catches a first pass (+17 B minified in the core: joinPass,
+ // the pass-scoped join, the flush-end passTx clear; +10-11 B in
+ // boundaries: the catch's nearest-boundary stop); brotli layout.
+ // Cap set at measured + 10 B rounded up to 0.01 KB. Accepted by the
+ // maintainer (2026-10-06). The cap is frozen again at 25.22 KB.
+ limit: "25.22 KB",
alias
},
{
@@ -3055,7 +3075,17 @@ module.exports = [
// web=16,591 app=3,058). Cap at CI-measured + 10 B rounded up to 0.01 KB.
path: "fixtures/compiled/hydrating.jsx",
compile: { hydratable: true },
- limit: "30.93 KB",
+ // Size-Exception (#3824, 2026-10-06): 30.93 -> 30.97 KB,
+ // measured at 30,956 B by CI against `next` @ 7acc0390e's 30,962
+ // (-6 B; 26 B over the cap; +28 B minified) — A29's boundary
+ // exemption restored under L2 (#3540): a fresh `Loading` over a held
+ // value shows its fallback in a flush, and catches content bound under it; only the nearest loading boundary that has not shown
+ // content catches a first pass (+17 B minified in the core: joinPass,
+ // the pass-scoped join, the flush-end passTx clear; +10-11 B in
+ // boundaries: the catch's nearest-boundary stop); brotli layout.
+ // Cap set at measured + 10 B rounded up to 0.01 KB. Accepted by the
+ // maintainer (2026-10-06). The cap is frozen again at 30.97 KB.
+ limit: "30.97 KB",
alias
},
{
@@ -3422,6 +3452,16 @@ module.exports = [
// +16, observeFlight first-observer hold +17); brotli layout. Cap set at
// measured + 10 B rounded up to 0.01 KB. Accepted by the maintainer
// (2026-10-05). The cap is frozen again at 44.84 KB.
+ // Size-Exception (#3824, 2026-10-06): 44.84 -> 44.90 KB (floor-caps.json),
+ // measured at 44,883 B by CI against `next` @ 7acc0390e's 44,820
+ // (+63 B; 43 B over the cap; +28 B minified) — A29's boundary
+ // exemption restored under L2 (#3540): a fresh `Loading` over a held
+ // value shows its fallback in a flush, and catches content bound under it; only the nearest loading boundary that has not shown
+ // content catches a first pass (+17 B minified in the core: joinPass,
+ // the pass-scoped join, the flush-end passTx clear; +10-11 B in
+ // boundaries: the catch's nearest-boundary stop); brotli layout.
+ // Cap set at measured + 10 B rounded up to 0.01 KB. Accepted by the
+ // maintainer (2026-10-06). The cap is frozen again at 44.90 KB.
limit: floorCaps["page: base server components (hydrating + dynamic + frames + sf reference)"],
alias: pageAlias
},
@@ -3529,6 +3569,16 @@ module.exports = [
// lane re-stage +13, verdict pull +1). Cap set at measured + 10 B rounded
// up to 0.01 KB. Accepted by the maintainer (2026-10-05). The cap is
// frozen again at 48.51 KB.
+ // Size-Exception (#3824, 2026-10-06): 48.51 -> 48.53 KB (floor-caps.json),
+ // measured at 48,513 B by CI against `next` @ 7acc0390e's 48,542
+ // (-29 B; 3 B over the cap; +27 B minified) — A29's boundary
+ // exemption restored under L2 (#3540): a fresh `Loading` over a held
+ // value shows its fallback in a flush, and catches content bound under it; only the nearest loading boundary that has not shown
+ // content catches a first pass (+17 B minified in the core: joinPass,
+ // the pass-scoped join, the flush-end passTx clear; +10-11 B in
+ // boundaries: the catch's nearest-boundary stop); brotli layout.
+ // Cap set at measured + 10 B rounded up to 0.01 KB. Accepted by the
+ // maintainer (2026-10-06). The cap is frozen again at 48.53 KB.
limit: floorCaps["page: live server components (base + live/GET + action + isPending/latest)"],
alias: pageAlias
},
From 3ab71b0dc559ece8467322fb770be1a0278951be Mon Sep 17 00:00:00 2001
From: Ryan Carniato
Date: Tue, 6 Oct 2026 03:34:13 -0700
Subject: [PATCH 5/5] chore(size): reset #3824's caps from CI with the recorded
minified (#3822 format)
Size-Exception (approved): hydrating + stores 28.85 KB, CSR observe 14.48 KB,
compiled CSR 25.23 KB, compiled hydrating 30.95 KB, base page 44.86 KB,
live page 48.56 KB, each CI brotli (Size run 37450070886) + 10 B with the
minified from the same run.
Co-authored-by: Cursor
---
scripts/size/floor-caps.json | 8 +-
scripts/size/scenarios.js | 138 ++++++++++++++++++++---------------
2 files changed, 84 insertions(+), 62 deletions(-)
diff --git a/scripts/size/floor-caps.json b/scripts/size/floor-caps.json
index 0643fb10a..0baa29822 100644
--- a/scripts/size/floor-caps.json
+++ b/scripts/size/floor-caps.json
@@ -12,12 +12,12 @@
"minified": 52567
},
"page: base server components (hydrating + dynamic + frames + sf reference)": {
- "cap": "44.90 KB",
- "minified": 145607
+ "cap": "44.86 KB",
+ "minified": 145627
},
"page: live server components (base + live/GET + action + isPending/latest)": {
- "cap": "48.53 KB",
- "minified": 157569
+ "cap": "48.56 KB",
+ "minified": 157589
},
"server: floor (getRequestEvent + isServer)": {
"cap": "1.34 KB",
diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js
index a26349e30..31b19af85 100644
--- a/scripts/size/scenarios.js
+++ b/scripts/size/scenarios.js
@@ -2115,18 +2115,20 @@ module.exports = [
// unchanged at 28.87 KB, recorded minified 91,625 B (first record);
// CI-measured at 28,888 B (91,625 B minified). Lower only: cap at measured
// + 10 B rounded up to 0.01 KB; recorded minified never raised.
- // Size-Exception (#3824, 2026-10-06): 28.87 -> 28.90 KB,
- // measured at 28,881 B by CI against `next` @ 7acc0390e's 28,903
- // (-22 B; 11 B over the cap; +27 B minified) — A29's boundary
- // exemption restored under L2 (#3540): a fresh `Loading` over a held
- // value shows its fallback in a flush, and catches content bound under it; only the nearest loading boundary that has not shown
- // content catches a first pass (+17 B minified in the core: joinPass,
- // the pass-scoped join, the flush-end passTx clear; +10-11 B in
- // boundaries: the catch's nearest-boundary stop); brotli layout.
- // Cap set at measured + 10 B rounded up to 0.01 KB. Accepted by the
- // maintainer (2026-10-06). The cap is frozen again at 28.90 KB.
- limit: "28.90 KB",
- capMinified: 91632,
+ // Size-Exception (#3824, 2026-10-06): 28.87 -> 28.85 KB, recorded minified
+ // 91,625 -> 91,652 B; measured at 28,836 B (91,652 B minified) by CI (Size
+ // run 37450070886) against `next` @ b987c41f4's 28,888 (-52 B; 34 B under
+ // the cap; +27 B minified) — A29's boundary exemption restored under L2
+ // (#3540): a fresh `Loading` over a held value shows its fallback in a
+ // flush, and catches content bound under it; only the nearest loading
+ // boundary that has not shown content catches a first pass (+17 B minified
+ // in the core: joinPass, the pass-scoped join, the flush-end passTx clear;
+ // +10-11 B in boundaries: the catch's nearest-boundary stop); brotli
+ // layout. Cap set at measured + 10 B rounded up to 0.01 KB, with the
+ // minified measured in the same run. Accepted by the maintainer
+ // (2026-10-06). The cap is frozen again at 28.85 KB.
+ limit: "28.85 KB",
+ capMinified: 91652,
alias
},
{
@@ -2629,8 +2631,20 @@ module.exports = [
// unchanged at 14.46 KB, recorded minified 41,091 B (first record);
// CI-measured at 14,449 B (41,091 B minified). Lower only: cap at measured
// + 10 B rounded up to 0.01 KB; recorded minified never raised.
- limit: "14.46 KB",
- capMinified: 41091,
+ // Size-Exception (#3824, 2026-10-06): 14.46 -> 14.48 KB, recorded minified
+ // 41,091 -> 41,119 B; measured at 14,468 B (41,119 B minified) by CI (Size
+ // run 37450070886) against `next` @ b987c41f4's 14,449 (+19 B; 8 B over the
+ // cap; +28 B minified) — A29's boundary exemption restored under L2
+ // (#3540): a fresh `Loading` over a held value shows its fallback in a
+ // flush, and catches content bound under it; only the nearest loading
+ // boundary that has not shown content catches a first pass (+17 B minified
+ // in the core: joinPass, the pass-scoped join, the flush-end passTx clear;
+ // +10-11 B in boundaries: the catch's nearest-boundary stop); brotli
+ // layout. Cap set at measured + 10 B rounded up to 0.01 KB, with the
+ // minified measured in the same run. Accepted by the maintainer
+ // (2026-10-06). The cap is frozen again at 14.48 KB.
+ limit: "14.48 KB",
+ capMinified: 41119,
alias: observeAlias
},
{
@@ -3104,18 +3118,20 @@ module.exports = [
// unchanged at 25.13 KB, recorded minified 78,898 B (first record);
// CI-measured at 25,129 B (78,898 B minified). Lower only: cap at measured
// + 10 B rounded up to 0.01 KB; recorded minified never raised.
- // Size-Exception (#3824, 2026-10-06): 25.13 -> 25.22 KB,
- // measured at 25,204 B by CI against `next` @ 7acc0390e's 25,073
- // (+131 B; 74 B over the cap; +28 B minified) — A29's boundary
- // exemption restored under L2 (#3540): a fresh `Loading` over a held
- // value shows its fallback in a flush, and catches content bound under it; only the nearest loading boundary that has not shown
- // content catches a first pass (+17 B minified in the core: joinPass,
- // the pass-scoped join, the flush-end passTx clear; +10-11 B in
- // boundaries: the catch's nearest-boundary stop); brotli layout.
- // Cap set at measured + 10 B rounded up to 0.01 KB. Accepted by the
- // maintainer (2026-10-06). The cap is frozen again at 25.22 KB.
- limit: "25.22 KB",
- capMinified: 78906,
+ // Size-Exception (#3824, 2026-10-06): 25.13 -> 25.23 KB, recorded minified
+ // 78,898 -> 78,926 B; measured at 25,218 B (78,926 B minified) by CI (Size
+ // run 37450070886) against `next` @ b987c41f4's 25,129 (+89 B; 88 B over
+ // the cap; +28 B minified) — A29's boundary exemption restored under L2
+ // (#3540): a fresh `Loading` over a held value shows its fallback in a
+ // flush, and catches content bound under it; only the nearest loading
+ // boundary that has not shown content catches a first pass (+17 B minified
+ // in the core: joinPass, the pass-scoped join, the flush-end passTx clear;
+ // +10-11 B in boundaries: the catch's nearest-boundary stop); brotli
+ // layout. Cap set at measured + 10 B rounded up to 0.01 KB, with the
+ // minified measured in the same run. Accepted by the maintainer
+ // (2026-10-06). The cap is frozen again at 25.23 KB.
+ limit: "25.23 KB",
+ capMinified: 78926,
alias
},
{
@@ -3150,18 +3166,20 @@ module.exports = [
// unchanged at 30.93 KB, recorded minified 99,198 B (first record);
// CI-measured at 30,957 B (99,198 B minified). Lower only: cap at measured
// + 10 B rounded up to 0.01 KB; recorded minified never raised.
- // Size-Exception (#3824, 2026-10-06): 30.93 -> 30.97 KB,
- // measured at 30,956 B by CI against `next` @ 7acc0390e's 30,962
- // (-6 B; 26 B over the cap; +28 B minified) — A29's boundary
- // exemption restored under L2 (#3540): a fresh `Loading` over a held
- // value shows its fallback in a flush, and catches content bound under it; only the nearest loading boundary that has not shown
- // content catches a first pass (+17 B minified in the core: joinPass,
- // the pass-scoped join, the flush-end passTx clear; +10-11 B in
- // boundaries: the catch's nearest-boundary stop); brotli layout.
- // Cap set at measured + 10 B rounded up to 0.01 KB. Accepted by the
- // maintainer (2026-10-06). The cap is frozen again at 30.97 KB.
- limit: "30.97 KB",
- capMinified: 99206,
+ // Size-Exception (#3824, 2026-10-06): 30.93 -> 30.95 KB, recorded minified
+ // 99,198 -> 99,226 B; measured at 30,940 B (99,226 B minified) by CI (Size
+ // run 37450070886) against `next` @ b987c41f4's 30,957 (-17 B; 10 B over
+ // the cap; +28 B minified) — A29's boundary exemption restored under L2
+ // (#3540): a fresh `Loading` over a held value shows its fallback in a
+ // flush, and catches content bound under it; only the nearest loading
+ // boundary that has not shown content catches a first pass (+17 B minified
+ // in the core: joinPass, the pass-scoped join, the flush-end passTx clear;
+ // +10-11 B in boundaries: the catch's nearest-boundary stop); brotli
+ // layout. Cap set at measured + 10 B rounded up to 0.01 KB, with the
+ // minified measured in the same run. Accepted by the maintainer
+ // (2026-10-06). The cap is frozen again at 30.95 KB.
+ limit: "30.95 KB",
+ capMinified: 99226,
alias
},
{
@@ -3537,16 +3555,18 @@ module.exports = [
// unchanged at 44.84 KB, recorded minified 145,599 B (first record);
// CI-measured at 44,864 B (145,599 B minified). Lower only: cap at measured
// + 10 B rounded up to 0.01 KB; recorded minified never raised.
- // Size-Exception (#3824, 2026-10-06): 44.84 -> 44.90 KB (floor-caps.json),
- // measured at 44,883 B by CI against `next` @ 7acc0390e's 44,820
- // (+63 B; 43 B over the cap; +28 B minified) — A29's boundary
- // exemption restored under L2 (#3540): a fresh `Loading` over a held
- // value shows its fallback in a flush, and catches content bound under it; only the nearest loading boundary that has not shown
- // content catches a first pass (+17 B minified in the core: joinPass,
- // the pass-scoped join, the flush-end passTx clear; +10-11 B in
- // boundaries: the catch's nearest-boundary stop); brotli layout.
- // Cap set at measured + 10 B rounded up to 0.01 KB. Accepted by the
- // maintainer (2026-10-06). The cap is frozen again at 44.90 KB.
+ // Size-Exception (#3824, 2026-10-06): 44.84 -> 44.86 KB (floor-caps.json),
+ // recorded minified 145,599 -> 145,627 B; measured at 44,842 B (145,627 B
+ // minified) by CI (Size run 37450070886) against `next` @ b987c41f4's
+ // 44,864 (-22 B; 2 B over the cap; +28 B minified) — A29's boundary
+ // exemption restored under L2 (#3540): a fresh `Loading` over a held value
+ // shows its fallback in a flush, and catches content bound under it; only
+ // the nearest loading boundary that has not shown content catches a first
+ // pass (+17 B minified in the core: joinPass, the pass-scoped join, the
+ // flush-end passTx clear; +10-11 B in boundaries: the catch's
+ // nearest-boundary stop); brotli layout. Cap set at measured + 10 B rounded
+ // up to 0.01 KB, with the minified measured in the same run. Accepted by
+ // the maintainer (2026-10-06). The cap is frozen again at 44.86 KB.
limit: floorCaps["page: base server components (hydrating + dynamic + frames + sf reference)"],
capMinified:
floorMinified["page: base server components (hydrating + dynamic + frames + sf reference)"],
@@ -3660,16 +3680,18 @@ module.exports = [
// unchanged at 48.51 KB, recorded minified 157,562 B (first record);
// CI-measured at 48,527 B (157,562 B minified). Lower only: cap at measured
// + 10 B rounded up to 0.01 KB; recorded minified never raised.
- // Size-Exception (#3824, 2026-10-06): 48.51 -> 48.53 KB (floor-caps.json),
- // measured at 48,513 B by CI against `next` @ 7acc0390e's 48,542
- // (-29 B; 3 B over the cap; +27 B minified) — A29's boundary
- // exemption restored under L2 (#3540): a fresh `Loading` over a held
- // value shows its fallback in a flush, and catches content bound under it; only the nearest loading boundary that has not shown
- // content catches a first pass (+17 B minified in the core: joinPass,
- // the pass-scoped join, the flush-end passTx clear; +10-11 B in
- // boundaries: the catch's nearest-boundary stop); brotli layout.
- // Cap set at measured + 10 B rounded up to 0.01 KB. Accepted by the
- // maintainer (2026-10-06). The cap is frozen again at 48.53 KB.
+ // Size-Exception (#3824, 2026-10-06): 48.51 -> 48.56 KB (floor-caps.json),
+ // recorded minified 157,562 -> 157,589 B; measured at 48,544 B (157,589 B
+ // minified) by CI (Size run 37450070886) against `next` @ b987c41f4's
+ // 48,527 (+17 B; 34 B over the cap; +27 B minified) — A29's boundary
+ // exemption restored under L2 (#3540): a fresh `Loading` over a held value
+ // shows its fallback in a flush, and catches content bound under it; only
+ // the nearest loading boundary that has not shown content catches a first
+ // pass (+17 B minified in the core: joinPass, the pass-scoped join, the
+ // flush-end passTx clear; +10-11 B in boundaries: the catch's
+ // nearest-boundary stop); brotli layout. Cap set at measured + 10 B rounded
+ // up to 0.01 KB, with the minified measured in the same run. Accepted by
+ // the maintainer (2026-10-06). The cap is frozen again at 48.56 KB.
limit: floorCaps["page: live server components (base + live/GET + action + isPending/latest)"],
capMinified:
floorMinified["page: live server components (base + live/GET + action + isPending/latest)"],