(r => manual.push(() => r(v)));
+ });
+ const n2 = createMemo(() => Promise.resolve(n0()));
+ createRenderEffect(visible, v => {
+ frame.show = v;
+ });
+ createRenderEffect(
+ () => (visible() ? `outside ${s()} ${n2()}` : "hidden"),
+ v => {
+ frame.outside = v;
+ }
+ );
+ const view = Loading(() => (visible() ? `content ${n1()}` : "hidden"), "loading", s);
+ createRenderEffect(view, v => {
+ frame.view = v;
+ });
+ });
+ const turn = async (fn: () => void) => {
+ fn();
+ flush();
+ frames.push({ ...frame });
+ await drain();
+ flush();
+ frames.push({ ...frame });
+ };
+ await turn(() => {});
+ manual.shift()!();
+ await turn(() => {});
+ await turn(() => {
+ setShow(false);
+ setS(1);
+ });
+ await turn(() => setShow(true));
+ await turn(() => setS(0));
+ while (manual.length) {
+ manual.shift()!();
+ await turn(() => {});
+ }
+ expect(frames.filter(f => f.show === true && f.outside === "hidden")).toEqual([]);
+ });
+
+ it("re-armed twice, then a hide: lands hidden", async () => {
+ const w = seamWorld((s1, visible) => [
+ Loading(() => (visible() ? `content ${s1()}` : "hidden"), "loading", s1)
+ ]);
+ await w.mount();
+ await w.turn(() => w.setS1(0));
+ await w.turn(() => w.setS1(2));
+ await w.turn(() => w.setShow(false));
+ await w.settle();
+ expect(w.log).toEqual(["0:hidden"]);
+ });
});
diff --git a/packages/signals/tests/loading-on-frame-following-3540.test.ts b/packages/signals/tests/loading-on-frame-following-3540.test.ts
index 550467cc6..9d44a653c 100644
--- a/packages/signals/tests/loading-on-frame-following-3540.test.ts
+++ b/packages/signals/tests/loading-on-frame-following-3540.test.ts
@@ -445,6 +445,71 @@ describe("3b. an outside reader of a flight the `on` change did not start: the f
});
});
+// Content an earlier action holds with no flight (a write it staged): nothing
+// under the boundary is pending, but the re-armed boundary owns its content —
+// it leaves the hold, the fallback shows now, and the content reveals at the
+// action's commit. A re-arm inside the action is the action's frame: no
+// fallback, the content lands with the commit.
+describe("3c. content an earlier action holds by a staged write: the re-arm shows the fallback now", () => {
+ for (const content of ["direct", "memo", "bound"] as const)
+ for (const inside of [false, true])
+ test(`${content} content, re-armed ${inside ? "inside" : "after"} the action`, async () => {
+ const d = captureWarnings();
+ const [x, setX] = createSignal(0);
+ const [key, setKey] = createSignal(0);
+ const log: string[] = [];
+ let done!: () => void;
+ let dispose!: () => void;
+ createRoot(dispose_ => {
+ dispose = dispose_;
+ const view = untrack(() =>
+ createLoadingBoundary(
+ () => {
+ if (content === "direct") return () => `c ${x()}`;
+ const m = createMemo(() => `c ${x()}`);
+ if (content === "memo") return m;
+ createRenderEffect(m, v => {
+ log.push(`bind ${v}`);
+ });
+ return "";
+ },
+ () => "fallback",
+ { on: key }
+ )
+ );
+ createRenderEffect(view, v => {
+ log.push(`view ${v}`);
+ });
+ });
+ flush();
+ const shows = (v: number) =>
+ content === "bound" ? [`bind c ${v}`, "view
"] : [`view c ${v}`];
+ expect(log).toEqual(shows(0));
+ log.length = 0;
+ action(function* () {
+ setX(1);
+ if (inside) setKey(1);
+ yield new Promise(r => (done = r));
+ })();
+ flush();
+ expect(log).toEqual([]);
+ if (!inside) {
+ setKey(1);
+ flush();
+ expect(log).toEqual(["view fallback"]);
+ }
+ done();
+ for (let i = 0; i < 8; i++) await microtask();
+ flush();
+ expect(log).toEqual(
+ inside ? (content === "bound" ? ["bind c 1"] : shows(1)) : ["view fallback", ...shows(1)]
+ );
+ expect(d.codes()).toEqual([]);
+ d.stop();
+ dispose();
+ });
+});
+
describe("4. `on: () => latest(id)`: the display-ahead read shows the fallback now, beside the held frame", () => {
for (const write of ["plain", "action"] as Write[]) {
test(`${write} write, shell lands first: [A] → [A + spinner] → [B + spinner] → [B + comments]`, async () => {
diff --git a/packages/solid/CHEATSHEET.md b/packages/solid/CHEATSHEET.md
index ab3ff8556..97066e5d0 100644
--- a/packages/solid/CHEATSHEET.md
+++ b/packages/solid/CHEATSHEET.md
@@ -655,7 +655,7 @@ If your training data is 1.x, these are the corrections. **Read this before gene
- **Stores: setters take a draft callback** — mutate the draft in place by default. Returning a new value is shallow (array index-replace, object top-level diff); reach for it for filter/remove. Keyed reconcile is a _projection-fn_ feature, not a setter feature.
- **`undefined` is a real value in `merge`** — it overrides rather than "skip this key".
- **Async lives in computations** — return a Promise/AsyncIterable from `createMemo`/`createStore(fn)`/`createProjection`. Pending reads participate in ``.
-- **`Loading` covers unresolved branches** — once content has rendered, revalidation keeps it visible. Use `isPending(() => x())` for in-flight-change indicators or render guards; it reads `x` and participates in Loading like that read. `` is a dependency list, not a key: its value is never compared; a change to anything it reads makes the boundary stop waiting on its current content and show the fallback if something under it is pending (nothing otherwise). The fallback _follows the frame_: it lands with the change that caused it — immediately when nothing else holds that frame, together with the rest of the new page during a held navigation (never a spinner beside a page the change has not reached). `on={[a(), b()]}` for several. The re-armed boundary owns its content: data still loading from an earlier change shows the fallback now, wherever else it is read. If the data the change itself starts loading is also read outside the boundary, the frame waits on it and the fallback can never be seen — DEV warns `LOADING_ON_OUTSIDE_HOLD`; fix the structure (move the outside read under the boundary so one hold owns the data). A frame held past the content's landing by something else (the write's action, other pending data) shows no fallback either — a race, not reported; show the wait with `isPending()`. A display-ahead read in `on` (`on={latest(id)}`) shows the fallback _now_, beside the still-held frame — a capability, not the recommended shape. A fallback that names what is loading should read `latest(id)`, since `id()` is still the committed value while the new one loads.
+- **`Loading` covers unresolved branches** — once content has rendered, revalidation keeps it visible. Use `isPending(() => x())` for in-flight-change indicators or render guards; it reads `x` and participates in Loading like that read. `` is a dependency list, not a key: its value is never compared; a change to anything it reads makes the boundary stop waiting on its current content and show the fallback if something under it is pending (nothing otherwise). The fallback _follows the frame_: it lands with the change that caused it — immediately when nothing else holds that frame, together with the rest of the new page during a held navigation (never a spinner beside a page the change has not reached). `on={[a(), b()]}` for several. The re-armed boundary owns its content: data still loading, or held by an action that is still running, from an earlier change shows the fallback now, wherever else it is read. If the data the change itself starts loading is also read outside the boundary, the frame waits on it and the fallback can never be seen — DEV warns `LOADING_ON_OUTSIDE_HOLD`; fix the structure (move the outside read under the boundary so one hold owns the data). A frame held past the content's landing by something else (the write's action, other pending data) shows no fallback either — a race, not reported; show the wait with `isPending()`. A display-ahead read in `on` (`on={latest(id)}`) shows the fallback _now_, beside the still-held frame — a capability, not the recommended shape. A fallback that names what is loading should read `latest(id)`, since `id()` is still the committed value while the new one loads.
- **`isPending` ≠ 1.x `.loading`** — it fires while a value _change_ is in flight (an input changed, or `affects()` declared one), not for every fetch. A bare `refresh()`/poll re-asks the same question and is silent. For a reload that should read as pending: `affects(x); refresh(x)`. For a "saving…" affordance: a co-written optimistic flag.
- **No `Suspense.Provider` or single error path** — async errors flow to `` (or effect `error`); no inline `resource.error` branching.
- **`createRoot` is owned by parent by default** — disposed when parent disposes. To detach: `runWithOwner(null, fn)`.
From 60d2f1ded62e818811b83187efbd1a0fecdd45fa Mon Sep 17 00:00:00 2001
From: Ryan Carniato
Date: Tue, 6 Oct 2026 14:28:21 -0700
Subject: [PATCH 09/10] docs(signals): regenerate RULES-INDEX without local
probe files (#3540)
Co-authored-by: Claude via Cursor
Co-authored-by: Cursor
---
packages/signals/docs/RULES-INDEX.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/packages/signals/docs/RULES-INDEX.md b/packages/signals/docs/RULES-INDEX.md
index 26b5c5f4c..4145a33aa 100644
--- a/packages/signals/docs/RULES-INDEX.md
+++ b/packages/signals/docs/RULES-INDEX.md
@@ -39,7 +39,7 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul
**src/:** none — every citation resolves.
-**tests/:** B0, B6, §0, §15, §16, §19, §20, §21, §27, §28, §31, §39, §41 (test-only citations are informational; `--check` gates src/ only)
+**tests/:** B0, B6, §0, §16, §19, §20, §21, §27, §28, §31, §39, §41 (test-only citations are informational; `--check` gates src/ only)
## A — spec propositions
@@ -59,7 +59,7 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul
| A12 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:191` | — | 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:199` | — | 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:207` | — | 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:243` | async.ts×2 constants.ts×1 core.ts×3 lanes.ts×4 scheduler.ts×14 verdict.ts×3 | mount-cases.ts×1 async-chain-supersession.test.ts×1 boundary-output-frame-reader.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 loading-fallback-in-flush-3540.test.ts×6 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 … |
+| A15 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:243` | async.ts×2 constants.ts×1 core.ts×3 lanes.ts×4 scheduler.ts×14 verdict.ts×3 | async-chain-supersession.test.ts×1 boundary-output-frame-reader.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 loading-fallback-in-flush-3540.test.ts×6 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:215` | 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×5 flight-owned-transaction.test.ts×1 lane-authority-twins.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… |
@@ -78,7 +78,7 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul
| A31 | live | `docs/SPEC-ASYNC-SEMANTICS.md:125` | 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:133` | 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:267` | boundaries.ts×1 scheduler.ts×2 | async-chain-supersession.test.ts×2 boundary-not-born-held-3540.test.ts×2 fuzz-findings-l2.test.ts×4 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:277` | constants.ts×1 core.ts×6 lanes.ts×2 scheduler.ts×2 store.ts×4 | mount-cases.ts×1 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… |
+| A34 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:277` | 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) |
From 3e4b13beaabebbe3781615759ecc6ecfc25fe401 Mon Sep 17 00:00:00 2001
From: Ryan Carniato
Date: Tue, 6 Oct 2026 19:43:05 -0700
Subject: [PATCH 10/10] fix(signals): rebuild #3540's L2 fix on revision 1;
defer re-arm
Replaces the boundary-scope machinery (ownership hook, held-node leave,
re-arm `ownHeld`, the seam park) with revision 1's fresh-mount fix
(0479053414) on today's next: a first pass that reads a hold asks the
boundaries up its chain (GlobalQueue._fresh); when a loading boundary
that has not shown content catches it, the pass joins pass-scoped
(passTx) instead of joining the flush, so the mount publishes and the
boundary shows its fallback. A render effect born held under the
boundary is caught the same way. catchStatus is unchanged.
Re-arm under the boundary scope is deferred to a separate change,
pending its ruling: those tests are pinned it.fails, the re-arm
amendment text is back to next's wording with a dated note, and
async.ts, store.ts and flow.ts are back to next.
Refs #3540
Co-authored-by: Claude
Co-authored-by: Cursor
---
.changeset/fresh-loading-fallback-in-flush.md | 3 +-
documentation/solid-2.0/05-async-data.md | 4 +-
packages/signals/docs/RULES-INDEX.md | 6 +-
packages/signals/docs/SPEC-ASYNC-SEMANTICS.md | 2 +-
packages/signals/src/boundaries.ts | 201 ++-----
packages/signals/src/core/async.ts | 10 +-
packages/signals/src/core/core.ts | 26 +-
packages/signals/src/core/scheduler.ts | 34 +-
packages/signals/src/store/store.ts | 8 +-
.../loading-fallback-in-flush-3540.test.ts | 559 ++++++++++--------
.../loading-on-frame-following-3540.test.ts | 220 +++----
packages/solid/CHEATSHEET.md | 2 +-
packages/solid/src/client/flow.ts | 12 +-
13 files changed, 505 insertions(+), 582 deletions(-)
diff --git a/.changeset/fresh-loading-fallback-in-flush.md b/.changeset/fresh-loading-fallback-in-flush.md
index 387befbcc..ef486c3c1 100644
--- a/.changeset/fresh-loading-fallback-in-flush.md
+++ b/.changeset/fresh-loading-fallback-in-flush.md
@@ -1,6 +1,5 @@
---
"@solidjs/signals": patch
-"solid-js": patch
---
-A loading boundary that has not shown content — or that an `on` change has re-armed — owns its subtree (#3540 under L2). Content under it that reads a held value or waits on its first loads is pending: the boundary shows its fallback, no hold waits for it, and it re-derives at the hold's commit. A fresh `Loading` mounted over a held value shows its fallback in a flush too, and content bound by a render effect under it no longer reveals empty. Committed content under an `on`-re-armed boundary that reads a value held by another change now waits behind the fallback instead of holding the change, including content an earlier `action` still holds by a plain write, with nothing loading. A boundary mounted as part of a hold still appears at that hold's commit, and outside such a boundary nothing changes. DEV's `LOADING_ON_OUTSIDE_HOLD` reports only a re-arm whose fallback is actually held; the `Loading` `on` docs say which data holds the change. While such a boundary shows its fallback, `isPending()` of it is `false`, and `latest()` of content under it answers the value that content has — its committed value, or `NotReadyError` for content mounted under it — rather than the held derivation. A hold that content under a re-armed boundary read lands together with the change whose frame shows the fallback, never ahead of it.
+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/documentation/solid-2.0/05-async-data.md b/documentation/solid-2.0/05-async-data.md
index 3dc4d9be0..a4e983b46 100644
--- a/documentation/solid-2.0/05-async-data.md
+++ b/documentation/solid-2.0/05-async-data.md
@@ -47,7 +47,7 @@ Importantly, `Loading` is intended to cover **branch readiness**: it handles a s
Nested `Loading` boundaries can be used to avoid blocking large subtrees and to control where loading UI appears.
-A `Loading` that has not shown content yet owns what is under it. Content there that reads a value a still-running change holds (a write inside an `action`, data another change is loading) is not part of that change: the boundary shows its fallback now, and the content appears when the change commits. The change does not wait for it. The exception is a boundary mounted by the change itself, for example a `` where `x` is the held write: it appears with that change's commit, with no fallback. While the fallback shows, `isPending()` of the boundary is `false`, as for any first load. Content that has not rendered yet has no value for `latest()` to return either: `latest()` of a memo created under the boundary throws until the commit, as any not-yet-loaded value does.
+A `Loading` that has not shown content yet owns what is under it. Content there that reads a value a still-running change holds (a write inside an `action`, data another change is loading) is not part of that change: the boundary shows its fallback now, and the content appears when the change commits. The change does not wait for it. The exception is a boundary mounted by the change itself, for example a `` where `x` is the held write: it appears with that change's commit, with no fallback. While the fallback shows, `isPending()` of the boundary is `false`, as for any first load.
#### `Loading` `on` prop: dependencies that show the fallback again
@@ -83,7 +83,7 @@ The shell keeps showing A until `product(2)` lands; the spinner arrives with B,
One shape shows no fallback at all: when the data the boundary is waiting on is also read outside it (a sibling `` over the same `comments(id)`, an `isPending` on it in the header), or the write's `action` stays open until the data lands. The frame waits on that read, so by the time it commits the content is ready and the fallback was never needed. The first of these is structural — no ordering of the flights can show that fallback — and in development the `LOADING_ON_OUTSIDE_HOLD` diagnostic names the source; the fix is to move the outside read under the boundary so one hold owns the data. The second is a race the fallback may still win (an action that ends first shows it with the commit), and is not reported: during an `action`, show the wait with `isPending()` or an optimistic value, which is what a hold's stale content is for. The old content is on screen and valid the whole time; a `Loading` fallback says it is not.
-Both shapes are about data the change itself starts loading. The re-armed boundary owns its content, so data still loading from an earlier change (a flight another write started, or an earlier `action`'s held write) does not hold this one: the fallback shows now, even where that data is also read outside the boundary, and nothing is reported.
+_Note (2026-10-06):_ how a re-armed boundary treats data an earlier change still holds is deferred to a separate change; until then a re-arm behaves as described here.
It is possible to show the fallback beside the still-held frame anyway: a display-ahead read in `on` — `latest(id)`, `isPending()`, an optimistic signal — says the change is already on screen, so the fallback lands there too:
diff --git a/packages/signals/docs/RULES-INDEX.md b/packages/signals/docs/RULES-INDEX.md
index 4145a33aa..e7708a763 100644
--- a/packages/signals/docs/RULES-INDEX.md
+++ b/packages/signals/docs/RULES-INDEX.md
@@ -55,11 +55,11 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul
| A8 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:167` | — | 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:175` | 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:183` | 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:101` | — | 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… |
+| A11 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:101` | — | latest-isPending-consistency.test.ts×1 loading-fallback-in-flush-3540.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:191` | — | 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:199` | — | 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:207` | — | 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:243` | async.ts×2 constants.ts×1 core.ts×3 lanes.ts×4 scheduler.ts×14 verdict.ts×3 | async-chain-supersession.test.ts×1 boundary-output-frame-reader.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 loading-fallback-in-flush-3540.test.ts×6 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 … |
+| A15 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:243` | async.ts×2 constants.ts×1 core.ts×3 lanes.ts×4 scheduler.ts×14 verdict.ts×3 | async-chain-supersession.test.ts×1 boundary-output-frame-reader.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 loading-fallback-in-flush-3540.test.ts×4 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:215` | 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×5 flight-owned-transaction.test.ts×1 lane-authority-twins.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… |
@@ -73,7 +73,7 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul
| A26 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:109` | — | 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:295` | — | 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:93` | 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:117` | boundaries.ts×3 action.ts×1 async.ts×1 constants.ts×1 core.ts×10 effect.ts×1 scheduler.ts×5 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×7 loading-on-frame-following-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:117` | 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×6 loading-on-frame-following-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:251` | 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:125` | 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:133` | 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 50b6f5576..60dd7cd7a 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`).
-**A boundary that has not shown content owns its subtree — A29's boundary scope (2026-10-06, maintainer ruling; amendment — #3540's L2 regression; the bullet above and A15 are unchanged outside such a boundary).** The ruling, in substance: content under a loading boundary that has not shown content — or that an `on` change has re-armed — belongs to that boundary, not to any hold it reads, whichever computation does the reading (a creation pass, a re-running mount effect, the boundary's own render, or committed content under a re-armed boundary). While that content waits for a hold or its own first loads, it is pending: the boundary shows its fallback, and no hold waits for it. Whether the fallback is seen depends on whether the boundary's own mount is held: a boundary mounted as part of a hold (`` with `x` held, or nested in held content) appears at that hold's commit. Outside such a boundary, A15 and the direction rule are unchanged: committed work that reads a hold joins it, never-committed work waits on every hold it read, and if no boundary would catch it, it stays hidden with the hold. The `on` case is the one place the rule overrides A15 for committed content: committed content under a re-armed boundary that reads a hold waits behind the fallback rather than joining the hold. Membership is decided by the scope, not by the pass: the reading computation's nearest collecting loading boundary (the one `catchStatus` would catch at) owns the read, unless the boundary's own display is the hold's anyway — the flush has joined that hold, the boundary's output is held by it while not showing content, or the pass that is mounting the boundary has joined it. "The flush has joined that hold" is the flush's final membership, decided at the seam: a read made pending under the boundary before the flush joined the hold it read was the hold's after all — the boundary's swap parked with the hold, so its fallback is never seen — and the reader re-derives as the hold's, the content entering it to appear at its commit. And the content is behind the fallback only once the swap shows: a boundary whose swap parked with the flush's transaction (the change that re-armed it is held) keeps its content on screen until then, so a hold that content read — or left — lands with that transaction (merged at the seam), never before it. Readers that waited re-derive at the hold's commit as A15's stale readers do; no reader is registered after the commit. `isPending` of the boundary's value is false while its fallback shows (A19: none of the three causes holds — the swap is not held, and the content under it is loading, exception 1; "boundaries … never enter the definition"), as for a boundary showing its fallback over a first load. **A11 under the scope:** a derivation under an owning boundary is not the holder's work, so nothing computes it from the held world before the commit — `latest()` of it answers the value it has: its committed value while it is pending (`isPending` true), and `NotReadyError` if it has none (content mounted under the boundary, A19 exception 1). Next and pre-L2 staged it in the hold, so `latest()` served the held derivation; serving it without that pass joining the hold would need a second, unheld derivation in the hold's world, which no channel provides. Outside an owning boundary A11 is unchanged. Pre-L2 served the committed value in these shapes (a fresh boundary in a flush, and committed content under a re-armed boundary over a held write — `content 0`); under A29 that tears, and the fallback is the ruled display. **Mechanism:** `GlobalQueue._owns(c, el)` (boundaries.ts, `scopedRead`; installed with the boundary module, so a graph without one pays the hook's null checks), asked at the hold's doors: by `read()` for a held source — the reader's nearest collecting loading boundary (`owns`: `catcherOf` over `boundaryOf`; the output carries its boundary scope, `_scope`) owns the read, the reader is a stale reader of the hold and the read throws `NotReadyError(source)`, so the boundary collects it; a held computation read from the scope that owns it is content, not a hold — it leaves the transaction (`leaveHold`) and re-derives at the read — and, as `_owns(el, el)`, by `recompute`'s head before a held node's pass joins its hold and by `propagateStatus` before a pending propagated onto a held memo does: an owned held node leaves the transaction. A re-arm that finds nothing pending under the boundary owns what an earlier change holds there with no flight (a staged write, an action still running): each held computation under it that the scope owns leaves its hold and re-derives in the arming flush (`ownHeld`), so its read of the hold is pending under the boundary. The seam's decision is `GlobalQueue._boundaryPark(t)`'s, called once the flush has parked into `t` (its membership final): what the scope decided this flush — reads made pending and nodes that left a hold — is recorded with the hold and the owning boundary (`scoped`); a hold whose boundary's swap parked with `t` merges into `t`, and the reads whose hold is (now) `t` re-derive next round — the output is then held by `t` showing its fallback, which is not an owning display. Known, A15's and not the scope's: a hold the re-arm releases lands at once, and a reader outside the boundary re-derived at that landing that goes pending on the change's own flight keeps its pre-hold content beside the landed hold (pinned `it.fails`; boundaries #75's reduction shows the same tear without a boundary, on next too). Lanes and verdict readers are not scoped. Open: a boundary a verdict reader mounts (` 0}>` over an action's write) is created by verdict-lane work, and its first passes are routed into that lane (`recompute`'s first-pass lane rule), whose world is the proposal: it shows the held derivation now rather than its fallback (mount-under-hold `verdict` family; the same on pre-L2, next and #3843). The fix belongs to the first-pass lane rule, with #3835's. **Pinned by:** `tests/loading-fallback-in-flush-3540.test.ts` (fresh mounts in a flush and from mainline: memo, direct and bound content; nested under a boundary that has shown content; a mount that joined the hold appears at its commit; the boundary's hold stays its own; catchers at any depth — unrevealed, re-armed outer or nearest, revealed, plain; committed content under a re-armed or plain boundary; boundaries mounted by the hold, top-level or nested; no catcher; `isPending` and `latest` on each); a read made pending before the flush joins its hold — one boundary, two, nested, re-armed twice, with and without a later hide, and the #1078 fuzzer finding; a swap parked with another change takes the hold its content read, #476/#1674); `tests/loading-on-frame-following-3540.test.ts` (3b: a flight an earlier change started; 3c: content an earlier action holds by a staged write — memo, direct and bound, re-armed after the action and, as a control, inside it); `packages/web/test/loading-fallback-in-flush-3540.spec.tsx`.
+**A boundary that has not shown content owns its subtree — A29's boundary scope, fresh mounts (2026-10-06, maintainer ruling; amendment — #3540's L2 regression; the bullet above and A15 are unchanged outside such a boundary).** The ruling, in substance, for a boundary that has not shown content: content under it belongs to that boundary, not to any hold it reads, whichever computation does the reading (a creation pass, a re-running mount effect, the boundary's own render). While that content waits for a hold or its own first loads, it is pending: the boundary shows its fallback, and no hold waits for it. Whether the fallback is seen depends on whether the boundary's own mount is held: a boundary mounted as part of a hold (`` with `x` held, or nested in held content) appears at that hold's commit. Outside such a boundary, A15 and the direction rule are unchanged: committed work that reads a hold joins it, never-committed work waits on every hold it read, and if no boundary would catch it, it stays hidden with the hold. The in-flush form lands with `fix/l2-3540-fresh-loading`, standalone of `fix/create-time-holds`. **Mechanism:** a first pass (`STATUS_UNINITIALIZED`) that reads a hold asks the boundaries up its chain (`GlobalQueue._fresh`: `catchStatus` with a pending status — the same walk as any pending read, so revealed boundaries it passes record it as a reader until the commit). 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. `isPending` of the boundary's value is false while its fallback shows, as for a boundary showing its fallback over a first load; `latest()` of content under it is A11's, unchanged (the derivation staged in the hold). The `direction-rule-probe` pins are unchanged. **Known gaps** (pinned `it.fails`, the same on next): a boundary mounted by a pass that itself joined the hold (a memo created in the mount that reads the hold) shows its fallback now rather than appearing at the hold's commit; a boundary mounted by a flight's hold, top-level or nested, shows its fallback for a frame at the commit. **Re-arm (2026-10-06, deferred):** the ruling's `on` part — content under a boundary that an `on` change has re-armed, committed content included, belongs to that boundary — is deferred to a separate change pending its ruling; until then a re-arm behaves as on next, and its shapes are pinned `it.fails` with the ruled display. Open: a boundary a verdict reader mounts (` 0}>` over an action's write) is created by verdict-lane work, and its first passes are routed into that lane (`recompute`'s first-pass lane rule), whose world is the proposal: it shows the held derivation now rather than its fallback (mount-under-hold `verdict` family; the same on pre-L2 and next). The fix belongs to the first-pass lane rule, with #3835's. **Pinned by:** `tests/loading-fallback-in-flush-3540.test.ts` (in a flush and from mainline: 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; catchers at any depth — unrevealed, revealed, plain, and re-armed in flight; boundaries mounted by the hold; no catcher; `isPending` and `latest` on each; a read made pending before the flush joins its hold, and the #1078, #476/#1674 fuzzer findings; the re-arm shapes, pinned `it.fails`); `tests/loading-on-frame-following-3540.test.ts` (3b, 3c: re-arm over an earlier change's flight or staged write, pinned `it.fails` but for the inside-the-action control); `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 e0cac4ac2..631f60627 100644
--- a/packages/signals/src/boundaries.ts
+++ b/packages/signals/src/boundaries.ts
@@ -49,7 +49,6 @@ import {
EFFECT_USER,
NOT_PENDING,
REACTIVE_DISPOSED,
- REACTIVE_JOINED,
REACTIVE_LANE_READ,
REACTIVE_ZOMBIE,
STATUS_ERROR,
@@ -69,18 +68,16 @@ import {
globalQueue,
haltReactivity,
joinFuture,
- merge,
passLane,
passTx,
resolveTx,
schedule,
setPassLane,
- staleReader,
txOf,
type Transaction
} from "./core/scheduler.js";
import { attrHooks } from "./core/attribution-hooks.js";
-import type { Computed, Owner, Root, Signal } from "./core/types.js";
+import type { Computed, Owner, Signal } from "./core/types.js";
import { flatten } from "./flatten.js";
import { accessor, type Accessor } from "./signals.js";
@@ -125,11 +122,6 @@ interface Boundary {
_show: (b: Boundary) => unknown;
}
-/** The output is created beside the boundary, not under it; it is the
- * boundary's own render all the same (`owns`): `_scope` is the owner whose
- * context names the boundary. */
-type BoundaryOutput = Computed & { _scope?: Owner };
-
/** Context key: the nearest boundary of a node, inherited at creation. */
const BOUNDARY = Symbol(__DEV__ ? "boundary" : "");
/** Context key: the reveal controller a Loading boundary created here is a
@@ -208,122 +200,10 @@ export function redraw(b: Boundary): void {
}
}
-/** The boundary a status from `node` stops at — the nearest of its type that
- * is collecting — or none (the root hears of it). Side-effect free;
- * `catchStatus` catches there. */
-function catcherOf(node: Owner, flags: number): Boundary | undefined {
- let b = boundaryOf(node);
- while (b !== undefined && !(b._type & flags && isCollecting(b))) b = b._parent ?? undefined;
- return b;
-}
-
-/** A29's boundary scope (2026-10-06): a loading boundary that has not shown
- * content, or that `on` re-armed, owns its subtree. A pass under it reading
- * a node `t` holds does not join `t`: the content is pending under that
- * boundary — it is the boundary that would catch it — and no hold waits for
- * it. Not when the boundary's display is `t`'s anyway — then its fallback
- * is never seen, and the content enters `t` to appear with its commit: the
- * flush is `t`'s (everything it stages lands with `t`, the swap included —
- * final at the seam, `boundaryPark`); the output is held by `t` showing its
- * fallback or never committed (the boundary was mounted, or re-armed, as
- * part of the hold); or, mid-mount, the pass creating the boundary read
- * `t`. The boundary that owns `c`'s read of `t`, if one does. */
-function owner(c: Computed, t: Transaction): Boundary | undefined {
- const b = catcherOf((c as BoundaryOutput)._scope ?? c, STATUS_PENDING);
- if (b === undefined) return undefined;
- if (flushTransaction !== null && resolveTx(flushTransaction) === t) return undefined;
- const o = b._output;
- if (o !== null)
- return o._config & CONFIG_HELD &&
- (b._fallback || o._statusFlags & STATUS_UNINITIALIZED) &&
- txOf(o) === t
- ? undefined
- : b;
- const p = (b._owner as Root)._parentComputed as Computed | null;
- return p !== null && p._flags & REACTIVE_JOINED && passTx !== null && resolveTx(passTx) === t
- ? undefined
- : b;
-}
-
-/** GlobalQueue._owns — the boundary scope at the hold's doors: a read of a
- * held node (`read`, a derive's draft), and a held node about to join its
- * own hold (`c === el`: its pass's head, a pending propagated onto it).
- *
- * A held node a boundary owns now leaves its transaction (true: it goes on
- * as a plain one). A read from under an owning boundary — whichever pass
- * reads — joins nothing and serves nothing: the content is pending there,
- * the boundary catches it and shows its fallback, and it waits for the
- * landing as the hold's stale reader (`_reruns`: re-derived on the
- * committed world). A held computation the same scope owns is content, not
- * a hold: it leaves the transaction and re-derives here (true — the read
- * goes on as a plain one, pending if it is). Not lane work or a verdict
- * reader: they read the screen, not the hold (`frameRead`). */
-function scopedRead(c: Computed | null, el: Signal | Computed): boolean {
- if (c === null) return false;
- const t = txOf(el);
- if (t._lane) return false;
- if (c === el) return leaveHold(c, t, owner(c, t));
- if (passLane !== null || c._config & CONFIG_VERDICT) return false;
- const b = owner(c, t);
- if (b === undefined) return false;
- if (
- typeof (el as Computed)._fn === "function" &&
- leaveHold(el as Computed, t, owner(el as Computed, t))
- ) {
- recompute(el as Computed);
- return true;
- }
- staleReader(c, t);
- if (globalQueue._running) scoped.push([c, t, b, true]);
- throw new NotReadyError(el);
-}
-
-/** What the scope decided this flush, mid-pass: a node that left a hold,
- * or a read made pending (`read`), with the hold and the boundary that
- * owns it. Revisited when the flush parks (`boundaryPark`), dropped at the
- * seam's end. */
-const scoped: [node: Computed, hold: Transaction, owner: Boundary, read: boolean][] = [];
-
-/** GlobalQueue._boundaryPark — the flush parked into `t`: its membership is
- * final, and the reads it made pending under an owning boundary are judged
- * against it. A boundary's content shows until its swap does, so the hold
- * its content read cannot land before the swap: a swap parked with `t`
- * takes that hold into `t` (merged — they land as one, the fallback with
- * the change that caused it) — whether the content read the hold or left
- * it. A read whose hold is `t`, or is now, was
- * `t`'s after all — the flush joined it after the read: the boundary's
- * swap parked with `t` and its fallback can never be seen, so the reader
- * re-derives next round as `t`'s — the output is held by `t` showing its
- * fallback then, which is not an owning display (`owner`) — and the content
- * enters `t` to appear at its commit. */
-function boundaryPark(t: Transaction): void {
- for (const [c, u, b, read] of scoped) {
- if (c._flags & REACTIVE_DISPOSED) continue;
- const o = b._output;
- if (resolveTx(u) !== t && o !== null && o._config & CONFIG_HELD && txOf(o) === t) merge(u, t);
- if (read && resolveTx(u) === t) enqueueSub(c);
- }
-}
-
-/** A held node the loading boundary `b` owns now leaves its transaction:
- * its staging there is void, and its next pass derives on the committed
- * world. (Staged again this flush, it is held again if the flush parks
- * into `t`.) False when no boundary owns it. */
-function leaveHold(el: Computed, t: Transaction, b: Boundary | undefined): boolean {
- if (b === undefined) return false;
- el._pendingValue = NOT_PENDING;
- el._config &= ~CONFIG_HELD;
- el._x!._transaction = null;
- const i = t._nodes.indexOf(el);
- if (i >= 0) t._nodes.splice(i, 1);
- if (globalQueue._running) scoped.push([el, t, b, false]);
- return true;
-}
-
/** 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
@@ -448,10 +328,8 @@ export function ready(b: Boundary): boolean {
/** GlobalQueue._boundarySeam, end of the seam: readers that settled, died,
* or landed and committed are dropped; a fallback with none left reveals
- * next round. An arm resolves: nothing collected, nothing happened. The
- * flush's scoped reads are done with (`boundaryPark`). */
+ * next round. An arm resolves: nothing collected, nothing happened. */
function boundarySeam(): void {
- if (scoped.length !== 0) scoped.length = 0;
for (const b of collecting) {
const flags = b._output!._flags;
if (flags & REACTIVE_DISPOSED) {
@@ -461,8 +339,7 @@ function boundarySeam(): void {
// A parked frame's boundary (zombie) keeps its state for its revival.
if (flags & REACTIVE_ZOMBIE) continue;
prune(b, false);
- if (__DEV__ && b._armed && !b._initialized && !b._ahead && b._output!._config & CONFIG_HELD)
- reportOutsideHold(b);
+ if (__DEV__ && b._armed && !b._initialized && !b._ahead) reportOutsideHold(b);
b._armed = false;
if (b._readers.size === 0) {
collecting.delete(b);
@@ -472,11 +349,9 @@ function boundarySeam(): void {
}
}
-/** DEV, at the re-arm that flipped a boundary to its fallback, its swap held:
- * a source it now waits on is also read by a frame reader outside it — the
- * frame waits on the very source, and the fallback can never be seen. (A
- * swap nothing holds shows now: the boundary owns its content, A29's
- * boundary scope, whatever else reads the source.) Structural, so
+/** DEV, at the re-arm that flipped a boundary to its fallback: a source it
+ * now waits on is also read by a frame reader outside it — the frame waits
+ * on the very source, and the fallback can never be seen. Structural, so
* reported once, at the change, naming the source; a display-ahead arm
* (`latest()` in `on`) is the user's choice and not reported. */
function reportOutsideHold(b: Boundary): void {
@@ -579,25 +454,6 @@ function arm(b: Boundary): void {
return;
}
}
- if (globalQueue._running) ownHeld(b._owner);
-}
-
-/** The re-armed boundary owns its content (A29's boundary scope): what an
- * earlier change holds under it — a write staged with no flight, so nothing
- * under it is pending — leaves that hold and re-derives on the committed
- * world now, and its read of the hold is pending under the boundary. */
-function ownHeld(o: Owner): void {
- for (
- let n = o._firstChild as Computed | null;
- n !== null;
- n = n._nextSibling as Computed | null
- ) {
- if (n._config & CONFIG_HELD && !(n as any)._type) {
- const t = txOf(n);
- if (!t._lane && leaveHold(n, t, owner(n, t))) enqueueSub(n);
- }
- ownHeld(n);
- }
}
/** The error boundary's `reset`: re-run what threw (a comparator throw
@@ -738,11 +594,29 @@ function createBoundary(
// one; a loading boundary showing content forwards its pending.
throw tree._x!._error;
}
- // Readers under it still unready: the fallback, the tree untouched.
- // The seam re-derives this pass when they settle. (A tree it owns
- // that read a hold is pending, `owns`; one that is held is part of
- // the hold that mounted the boundary, and the output enters it.)
- if (isCollecting(b) && prune(b, true) !== 0) return fallback(b);
+ if (isCollecting(b)) {
+ // A29's boundary exemption (#3540): a boundary MOUNTED over a held
+ // 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 (
+ b._readers.size !== 0 &&
+ self._statusFlags & STATUS_UNINITIALIZED &&
+ !(self._config & CONFIG_HELD)
+ )
+ 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);
+ }
let value: T;
try {
value = read(tree);
@@ -785,7 +659,6 @@ function createBoundary(
__OBSERVE__ ? { name: "value", _noSnapshot: true } : { _noSnapshot: true }
);
output._config |= CONFIG_REDERIVE;
- (output as BoundaryOutput)._scope = owner;
b._output = output;
return accessor(output);
}
@@ -837,13 +710,9 @@ const ERROR_SIGNAL = { ownedWrite: true, _noSnapshot: true } as const;
* if nothing is pending, the notification is a no-op. The fallback lands
* with the same frame as the change that caused it — now, when nothing
* else holds that frame; together with the rest of the new page during a
- * held navigation, not before it. The re-armed boundary owns its content:
- * data the change did not start loading (a flight or a held write from an
- * earlier change) shows the fallback now rather than holding the change,
- * even where it is also read outside the boundary. If the change itself
- * starts the data loading and the data is also read outside the boundary,
- * the frame waits on it and the fallback can never be seen (DEV warns
- * `LOADING_ON_OUTSIDE_HOLD`); the fix is structural — move the
+ * held navigation, not before it. If the same data is also read outside
+ * the boundary, the frame waits on it and the fallback can never be seen
+ * (DEV warns `LOADING_ON_OUTSIDE_HOLD`); the fix is structural — move the
* outside read under the boundary so one hold owns the data. A frame held
* past the content's landing by something else (the write's action, other
* pending data) also shows no fallback; that is a race the fallback may
@@ -905,8 +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._owns = scopedRead;
+GlobalQueue._fresh = node =>
+ node._statusFlags & STATUS_UNINITIALIZED && catchStatus(node, STATUS_PENDING);
GlobalQueue._hidden = hidden;
GlobalQueue._boundarySeam = boundarySeam;
-GlobalQueue._boundaryPark = boundaryPark;
GlobalQueue._heldRun = heldRun;
diff --git a/packages/signals/src/core/async.ts b/packages/signals/src/core/async.ts
index 529fc7c19..e9556d66b 100644
--- a/packages/signals/src/core/async.ts
+++ b/packages/signals/src/core/async.ts
@@ -896,8 +896,7 @@ export function propagateStatus(
if (
passLane === null &&
(sub._config & (CONFIG_HELD | CONFIG_OVERRIDE)) === CONFIG_HELD &&
- globalQueue._running &&
- !GlobalQueue._owns?.(sub, sub)
+ globalQueue._running
)
joinFuture(txOf(sub));
return;
@@ -919,14 +918,11 @@ export function propagateStatus(
// recomputing it, and the propagation itself enters the memo's
// transaction — the flight flows into a memo that transaction
// holds, so the write that started it is held with it. A render
- // effect's membership is its pass's (`notify`), never sticky. A
- // memo a loading boundary owns now is that boundary's (A29's
- // boundary scope): its pending is caught there, held by nothing.
+ // effect's membership is its pass's (`notify`), never sticky.
if (
status === STATUS_PENDING &&
(sub._config & (CONFIG_HELD | CONFIG_OVERRIDE)) === CONFIG_HELD &&
- !(sub as any)._type &&
- !GlobalQueue._owns?.(sub, sub)
+ !(sub as any)._type
)
joinFuture(txOf(sub));
}
diff --git a/packages/signals/src/core/core.ts b/packages/signals/src/core/core.ts
index dd7c8fbc5..a14dfcd34 100644
--- a/packages/signals/src/core/core.ts
+++ b/packages/signals/src/core/core.ts
@@ -302,10 +302,6 @@ export function recompute(el: Computed