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

{m()}

; + } + function Label() { + const m = createMemo(() => `label ${x()}`); + return {m()}; + } + const dispose = render( + () => ( +
+

{x()}

+ {String(open())} + + {outside && +
+ ), + container + ); + flush(); + let release!: () => void; + const done = action(function* () { + setX(1); + yield new Promise(r => (release = r)); + })(); + flush(); + return { container, setOpen, release: () => release(), done, dispose }; + } + + test("the mount and the fallback show now; the content reveals at the commit", async () => { + const s = setup(false); + expect(s.container.innerHTML).toBe("

0

false
"); + + s.setOpen(true); + flush(); + expect(s.container.innerHTML).toBe("

0

true

fallback

"); + + s.release(); + await s.done; + await tick(); + expect(s.container.innerHTML).toBe("

1

true

content 1

"); + s.dispose(); + }); + + test("mounted outside a flush, content bound in JSX: the fallback now, the content at the commit", async () => { + const [x, setX] = createSignal(0); + let release!: () => void; + const done = action(function* () { + setX(1); + yield new Promise(r => (release = r)); + })(); + flush(); + function Content() { + const m = createMemo(() => `content ${x()}`); + return

{m()}

; + } + const container = document.createElement("div"); + const dispose = render( + () => ( + fallback

}> + +
+ ), + container + ); + flush(); + expect(container.innerHTML).toBe("

fallback

"); + + release(); + await done; + await tick(); + expect(container.innerHTML).toBe("

content 1

"); + dispose(); + }); + + test("a derivation outside the boundary holds the mount with the tick", async () => { + const s = setup(true); + s.setOpen(true); + flush(); + expect(s.container.innerHTML).toBe("

0

false
"); + + s.release(); + await s.done; + await tick(); + expect(s.container.innerHTML).toBe( + "

1

truelabel 1

content 1

" + ); + 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)"],