From 2e8f77287a7753c35c1959fbca18679b8470ab86 Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Wed, 7 Oct 2026 13:45:04 -0700 Subject: [PATCH 1/2] fix(signals): a new async memo created over a hold lands its first answer into it (#3800) A memo created while a transaction holds a value, whose first pass reads that value and returns a promise, was a loading source the seam does not hold (plan sec. 15.2), so its answer landed as mainline commit #0 and its readers showed the held future beside the committed page. The same memo with a synchronous first answer is born held (A29). The first pass now records the transaction it was born into under the born-held arm's conditions (a joined read, or a verdict lane's mount reading a staging), and its first landing is staged into that transaction while it is live. The hold never waits for the first load (the direction rule, #3820). Maintainer clarification (2026-10-07): the #3869 ruling covers new memos that don't read async; a new memo that reads the held world is held, async first answer included. Co-authored-by: Claude via Cursor Co-authored-by: Cursor --- .changeset/first-load-lands-into-hold.md | 5 + packages/signals/docs/RULES-INDEX.md | 84 +++--- packages/signals/docs/SPEC-ASYNC-SEMANTICS.md | 4 +- packages/signals/src/core/async.ts | 8 +- packages/signals/src/core/core.ts | 13 + packages/signals/src/core/scheduler.ts | 10 +- packages/signals/src/core/types.ts | 6 + .../signals/tests/issue-3800-repro.test.ts | 260 ++++++++++++++++++ .../verdict-mount-first-pass-3851.test.ts | 16 +- 9 files changed, 358 insertions(+), 48 deletions(-) create mode 100644 .changeset/first-load-lands-into-hold.md create mode 100644 packages/signals/tests/issue-3800-repro.test.ts diff --git a/.changeset/first-load-lands-into-hold.md b/.changeset/first-load-lands-into-hold.md new file mode 100644 index 000000000..66bd5da65 --- /dev/null +++ b/.changeset/first-load-lands-into-hold.md @@ -0,0 +1,5 @@ +--- +"@solidjs/signals": patch +--- + +A new async memo created over a held update now waits for it (#3800). A memo created while a transition holds a value, whose first pass reads that value and returns a promise, lands its first answer into the transition and reveals at its commit, as the same memo with a synchronous first answer already did. Before, the promise landed on its own and its readers showed the new value beside the rest of the page still on the old one. The transition never waits for the new memo's first load: if it lands after the transition commits, it is its own commit. diff --git a/packages/signals/docs/RULES-INDEX.md b/packages/signals/docs/RULES-INDEX.md index 6fcc434c6..c1d8ab1ed 100644 --- a/packages/signals/docs/RULES-INDEX.md +++ b/packages/signals/docs/RULES-INDEX.md @@ -45,65 +45,65 @@ 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:315` | — | 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:323` | — | 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:331` | — | 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:339` | — | 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:347` | — | 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:355` | 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:161` | — | 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:169` | — | 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:177` | 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:185` | 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. | +| A1 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:317` | — | 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:325` | — | 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:333` | — | 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:341` | — | 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:349` | — | 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:357` | 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:163` | — | 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:171` | — | 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:179` | 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:187` | core.ts×3 verdict.ts×2 | createMemo.test.ts×1 fuzz-findings-l2.test.ts×1 ispending-memo-unstamped-hold-3457.test.ts×2 latest-isPending-consistency.test.ts×3 verdict-contract.test.ts×2 | [ruled] `[isPending(x), x()]` is atomic within one scope — `[isPending(x), x()]` read in one scope is atomic: a reader that observed the fresh value must not see `pending === true` for it. | | A11 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:101` | — | latest-isPending-consistency.test.ts×1 loading-fallback-in-flush-3540.test.ts×1 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×1 | [ruled] Sync derivations of held sources are visible through `latest()`/`isPending()` — Sync derivations of transition-held sources are visible through `latest()`/`isPending()` (held sync recompute is… | -| A12 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:193` | — | 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:201` | — | 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:209` | — | 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:245` | async.ts×2 constants.ts×1 core.ts×3 lanes.ts×4 scheduler.ts×14 verdict.ts×4 | async-chain-supersession.test.ts×1 boundary-output-frame-reader.test.ts×1 first-observer-stale-reader.test.ts×1 fuzz-findings-l2.test.ts×17 held-restore.test.ts×1 l2-contract.test.ts×7 l2-fuzz-existing-rules.test.ts×2 lane-contract.test.ts×1 lane-hold-on-observation.test.ts×1 lane-outside-view.test.ts×1 lane-pass-stamped-effect-3662.test.ts×1 loading-fallback-in-flush-3540.test.ts×4 mount-over-foreign-hold-3761.test.ts×2 overlapping-flights.test.ts×3 posture-born-held-and-observation.test.ts×4 posture-store-parity.test.ts×2 reveal-carve-out.test.ts×2 shared-effect-no-entangle.test.ts×1 spec-async-semantics.test.ts×2 stale-read-uninitialized-cross-transition.test.ts×1 superseded-source-blocks-3462.test.ts×2 tick-scoped-pass-transaction.test.ts×1 treeshake.test.ts×4 visibility-oracle-store.states.ts×6 visibility-oracle.states.ts×7 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B3)] Transition entanglement is graph-driven; lanes settle as one reveal — (was B3) Transition entanglement is graph-driven: writes whose async work … | -| A16 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:217` | 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… | +| A12 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:195` | — | 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:203` | — | 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:211` | — | 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:247` | async.ts×2 constants.ts×1 core.ts×3 lanes.ts×4 scheduler.ts×14 verdict.ts×4 | async-chain-supersession.test.ts×1 boundary-output-frame-reader.test.ts×1 first-observer-stale-reader.test.ts×1 fuzz-findings-l2.test.ts×17 held-restore.test.ts×1 l2-contract.test.ts×7 l2-fuzz-existing-rules.test.ts×2 lane-contract.test.ts×1 lane-hold-on-observation.test.ts×1 lane-outside-view.test.ts×1 lane-pass-stamped-effect-3662.test.ts×1 loading-fallback-in-flush-3540.test.ts×4 mount-over-foreign-hold-3761.test.ts×2 overlapping-flights.test.ts×3 posture-born-held-and-observation.test.ts×4 posture-store-parity.test.ts×2 reveal-carve-out.test.ts×2 shared-effect-no-entangle.test.ts×1 spec-async-semantics.test.ts×2 stale-read-uninitialized-cross-transition.test.ts×1 superseded-source-blocks-3462.test.ts×2 tick-scoped-pass-transaction.test.ts×1 treeshake.test.ts×4 visibility-oracle-store.states.ts×6 visibility-oracle.states.ts×7 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B3)] Transition entanglement is graph-driven; lanes settle as one reveal — (was B3) Transition entanglement is graph-driven: writes whose async work … | +| A16 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:219` | 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 l2-fuzz-existing-rules.test.ts×2 lane-contract.test.ts×1 lane-frame-deferred-run-3662.test.ts×1 lane-outside-view.test.ts×1 lane-uninitialized-landing-3648.test.ts×5 optimistic-move-duplicate-3548.test.ts×2 optimistic-read-lane-not-transaction-3698.test.ts×4 posture-store-parity.test.ts×5 spec-async-semantics.test.ts×5 flight-owned-transaction.test.ts×1 lane-authority-twins.test.ts×1 optimistic-list-mutation-matrix.test.ts×1 optimistic-untracked-reads-f3-f5.test.ts×1 signal-store-twins-qd.test.ts×1 unchanged-presence-no-hold-3743.test.ts×2 superseded-before-first-commit.test.ts×4 visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×19 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from B4)] An override lives exactly as long as its own transaction; a newer truth from the source supersedes it in the graph immediately, on screen at com… | -| A19 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:145` | 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:404` | — | 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:411` | — | 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:225` | 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:235` | — | 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:153` | 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:305` | 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… | +| A19 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:147` | 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:406` | — | 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:413` | — | 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:227` | 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:237` | — | 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:155` | 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:307` | 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:109` | — | action-await-contract.test.ts×2 fuzz-findings-l2.test.ts×1 posture-store-parity.test.ts×2 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×1 visibility-oracle.test.ts×1 | [ruled 2026-07-17] An ambient transaction window is one flush; parking is flush-driven — (**ruled 2026-07-17**, #2913; **enforcement hardened 2026-08-31**, #3141 — parking is flush-driven, and a trans… | -| A27 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:297` | — | loading-value.test.ts×2 visibility-oracle.states.ts×18 visibility-oracle.test.ts×1 | [ruled 2026-08-10] The commit-#0 loading window is loading-class and verdict-quiet — (**ruled 2026-08-10**) **The commit-#0 loading window is loading-class and verdict-quiet.** A node born committed v… | -| A28 | ruled, mechanism landed | `docs/SPEC-ASYNC-SEMANTICS.md:93` | constants.ts×1 core.ts×12 lanes.ts×2 scheduler.ts×3 types.ts×1 verdict.ts×2 store.ts×4 | createOptimistic.test.ts×5 fuzz-findings-l2.test.ts×6 held-derivation-not-a-proposal-3612.test.ts×1 latest-held-till-flush.test.ts×1 posture-store-parity.test.ts×5 question-scoped-pending.test.ts×3 snapshot-derived-store-rows.test.ts×1 createOptimisticStore.test.ts×10 optimistic-draft-visibility-3665.test.ts×5 optimistic-list-mutation-matrix.harness.ts×1 optimistic-list-mutation-matrix.test.ts×2 shallow.test.ts×1 woken-transaction-adopts-staged-bump.test.ts×1 treeshake.test.ts×2 verdict-contract.test.ts×1 visibility-oracle-store.states.ts×8 visibility-oracle.states.ts×8 | [ruled, mechanism landed 2026-09-15] A write becomes visible at flush — to every channel — (**ruled 2026-09-08**; supersedes the #2922 mid-tick pull) **A write becomes visible at flush — to every chan… | -| A29 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:117` | boundaries.ts×1 action.ts×1 constants.ts×1 core.ts×12 effect.ts×1 lanes.ts×1 scheduler.ts×4 signals.ts×1 store.ts×1 | adoption-unchanged-key-read-3706.test.ts×9 body-end-supersession-visibility.test.ts×1 born-held.test.ts×3 boundary-not-born-held-3540.test.ts×4 createProjection.draft-lifetime-3585.test.ts×1 direct-commit-readers-posture.test.ts×1 fuzz-findings-l2.test.ts×6 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 held-truth-lane-only.test.ts×3 l2-contract.test.ts×1 latest-held-till-flush.test.ts×2 loading-fallback-in-flush-3540.test.ts×6 loading-on-frame-following-3540.test.ts×1 mount-over-foreign-hold-3761.test.ts×1 optimistic-mount-nested-memo-3835.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 verdict-mount-first-pass-3851.test.ts×2 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:253` | 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:127` | 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:135` | 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:269` | boundaries.ts×1 scheduler.ts×2 | async-chain-supersession.test.ts×2 boundary-not-born-held-3540.test.ts×2 fuzz-findings-l2.test.ts×4 ispending-in-boundary-on-3528.test.ts×2 loading-reset-collects-forwarded-3459.test.ts×3 | [ruled 2026-09-12 (#3375)] A fallback-caught flight holds no transaction; a Loading reset moves the hold onto the boundary — A `` boundary showing its fallback is the display of everything un… | -| A34 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:279` | 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… | +| A27 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:299` | — | loading-value.test.ts×2 visibility-oracle.states.ts×18 visibility-oracle.test.ts×1 | [ruled 2026-08-10] The commit-#0 loading window is loading-class and verdict-quiet — (**ruled 2026-08-10**) **The commit-#0 loading window is loading-class and verdict-quiet.** A node born committed v… | +| A28 | ruled, mechanism landed | `docs/SPEC-ASYNC-SEMANTICS.md:93` | constants.ts×1 core.ts×12 lanes.ts×2 scheduler.ts×3 types.ts×1 verdict.ts×2 store.ts×4 | createOptimistic.test.ts×5 fuzz-findings-l2.test.ts×6 held-derivation-not-a-proposal-3612.test.ts×1 issue-3800-repro.test.ts×1 latest-held-till-flush.test.ts×1 posture-store-parity.test.ts×5 question-scoped-pending.test.ts×3 snapshot-derived-store-rows.test.ts×1 createOptimisticStore.test.ts×10 optimistic-draft-visibility-3665.test.ts×5 optimistic-list-mutation-matrix.harness.ts×1 optimistic-list-mutation-matrix.test.ts×2 shallow.test.ts×1 woken-transaction-adopts-staged-bump.test.ts×1 treeshake.test.ts×2 verdict-contract.test.ts×1 visibility-oracle-store.states.ts×8 visibility-oracle.states.ts×8 | [ruled, mechanism landed 2026-09-15] A write becomes visible at flush — to every channel — (**ruled 2026-09-08**; supersedes the #2922 mid-tick pull) **A write becomes visible at flush — to every chan… | +| A29 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:117` | boundaries.ts×1 action.ts×1 async.ts×1 constants.ts×1 core.ts×13 effect.ts×1 lanes.ts×1 scheduler.ts×4 signals.ts×1 store.ts×1 | adoption-unchanged-key-read-3706.test.ts×9 body-end-supersession-visibility.test.ts×1 born-held.test.ts×3 boundary-not-born-held-3540.test.ts×4 createProjection.draft-lifetime-3585.test.ts×1 direct-commit-readers-posture.test.ts×1 fuzz-findings-l2.test.ts×6 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 held-truth-lane-only.test.ts×3 l2-contract.test.ts×1 latest-held-till-flush.test.ts×2 loading-fallback-in-flush-3540.test.ts×6 loading-on-frame-following-3540.test.ts×1 mount-over-foreign-hold-3761.test.ts×1 optimistic-mount-nested-memo-3835.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 verdict-mount-first-pass-3851.test.ts×2 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:255` | 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:129` | 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:137` | 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:271` | boundaries.ts×1 scheduler.ts×2 | async-chain-supersession.test.ts×2 boundary-not-born-held-3540.test.ts×2 fuzz-findings-l2.test.ts×4 ispending-in-boundary-on-3528.test.ts×2 loading-reset-collects-forwarded-3459.test.ts×3 | [ruled 2026-09-12 (#3375)] A fallback-caught flight holds no transaction; a Loading reset moves the hold onto the boundary — A `` boundary showing its fallback is the display of everything un… | +| A34 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:281` | 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:474` | — | spec-async-semantics.test.ts×7 | - **V1 (violated A13) — FIXED.** A _resting_ optimistic node reported | -| V2 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:484` | — | 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:490` | — | spec-async-semantics.test.ts×2 | - **V3 (violated A19) — FIXED.** After a reporter-less transition completed, | -| V4 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:497` | — | 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:509` | 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:476` | — | spec-async-semantics.test.ts×7 | - **V1 (violated A13) — FIXED.** A _resting_ optimistic node reported | +| V2 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:486` | — | 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:492` | — | spec-async-semantics.test.ts×2 | - **V3 (violated A19) — FIXED.** After a reporter-less transition completed, | +| V4 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:499` | — | 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:511` | 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:201` | — | 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:209` | — | 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:245` | — | spec-async-semantics.test.ts×2 | PROMOTED → A15 (A15's section carries the ruling). | +| B1 | 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 → A13 (A13's section carries the ruling). | +| B2 | live | `docs/SPEC-ASYNC-SEMANTICS.md:211` | — | 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:247` | — | 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:217` | boundaries.ts×1 | derived-write-then-derivation-3733.test.ts×1 fuzz-findings-l2.test.ts×3 spec-async-semantics.test.ts×2 | PROMOTED → A16 (A16's section carries the ruling). | +| B5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:219` | boundaries.ts×1 | derived-write-then-derivation-3733.test.ts×1 fuzz-findings-l2.test.ts×3 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:145` | — | 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:440` | — | 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:450` | — | — | - [x] **C3 — CLOSED by A19 (2026-07-07): early completion is by design.** | +| C1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:147` | — | 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:442` | — | 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:452` | — | — | - [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 diff --git a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md index a73a4f0a4..d27c79486 100644 --- a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md +++ b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md @@ -52,7 +52,7 @@ The former Tier A table is these sections. Tier B/C, the fixed violations, and t - **The first-commit frame is decided when the node is created.** A later re-ask never adopts it into a hold: a loading source created before the hold is not the hold's work (the `#2937` loading-source rule — `documentation/plans/size-reduction-carve-step1.md` §15.2, "A loading source is not held"). On `next`. - **A render effect born held owes no run before the commit that lands it** (#3802): a re-staging pass of an effect that still carries a staged value is not queued, because the commit replays it — lane-reached effects included. [#3814](https://github.com/solidjs/solid/pull/3814), merging. -- **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 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. The first pass's conditions are the born-held arm's (it read a held node, or — a verdict lane's mount — a staging of the flush, #3851); its transaction is recorded on the node (`_bornIn`) and the landing stages into it if it is still live (`handleAsync` → `liveTx`, `holdNode`). Pinned: `tests/issue-3800-repro.test.ts`, `tests/verdict-mount-first-pass-3851.test.ts` ("an async content memo"). - **A first pass under a loading boundary that has not shown content is the boundary's, not the tick's** — in a flush as outside one (A29's boundary exemption, #3540). The out-of-flush form is on `next`; the in-flush form (a fresh `Loading` over a held value shows its fallback in a flush) is **pending** — `fix/create-time-holds` (`ca71e5d9d`). **A boundary that has not shown content owns its subtree — A29's boundary scope, fresh mounts (2026-10-06, maintainer ruling; amendment — #3540's L2 regression; the bullet above and A15 are unchanged outside such a boundary).** The ruling, in substance, for a boundary that has not shown content: content under it belongs to that boundary, not to any hold it reads, whichever computation does the reading (a creation pass, a re-running mount effect, the boundary's own render). While that content waits for a hold or its own first loads, it is pending: the boundary shows its fallback, and no hold waits for it. Whether the fallback is seen depends on whether the boundary's own mount is held: a boundary mounted as part of a hold (`` with `x` held, or nested in held content) appears at that hold's commit. Outside such a boundary, A15 and the direction rule are unchanged: committed work that reads a hold joins it, never-committed work waits on every hold it read, and if no boundary would catch it, it stays hidden with the hold. The in-flush form lands with `fix/l2-3540-fresh-loading`, standalone of `fix/create-time-holds`. **Mechanism:** a first pass (`STATUS_UNINITIALIZED`) that reads a hold asks the boundaries up its chain (`GlobalQueue._fresh`: `catchStatus` with a pending status — the same walk as any pending read, so revealed boundaries it passes record it as a reader until the commit). When a loading boundary that has not shown content catches it, the pass joins pass-scoped (`passTx`), as a mount outside a flush does, instead of joining the flush: the Show that mounted the boundary publishes, and the boundary shows its fallback. A pass nothing catches joins the flush as before, so a derivation outside the boundary in the same flush still holds the tick. A render effect born held under the boundary (content bound in JSX — the tree never reads the held value) is caught the same way, so the boundary does not reveal empty content, in a flush or out of one; its output's first pass shows the fallback whenever it has collected readers. A flush that set `passTx` clears it at its end, so the boundary's hold does not merge with a later tick's. `isPending` of the boundary's value is false while its fallback shows, as for a boundary showing its fallback over a first load; `latest()` of content under it is A11's, unchanged (the derivation staged in the hold). The `direction-rule-probe` pins are unchanged. **Known gaps** (pinned `it.fails`, the same on next): a boundary mounted by a pass that itself joined the hold (a memo created in the mount that reads the hold) shows its fallback now rather than appearing at the hold's commit; a boundary mounted by a flight's hold, top-level or nested, shows its fallback for a frame at the commit. **Re-arm (2026-10-06, deferred):** the ruling's `on` part — content under a boundary that an `on` change has re-armed, committed content included, belongs to that boundary — is deferred to a separate change pending its ruling; until then a re-arm behaves as on next, and its shapes are pinned `it.fails` with the ruled display. Open: a boundary a verdict reader mounts (` 0}>` over an action's write) is created by verdict-lane work, and its first passes are routed into that lane (`recompute`'s first-pass lane rule), whose world is the proposal: it shows the held derivation now rather than its fallback (mount-under-hold `verdict` family; the same on pre-L2 and next). The fix belongs to the first-pass lane rule, with #3835's. **Pinned by:** `tests/loading-fallback-in-flush-3540.test.ts` (in a flush and from mainline: through a memo, directly, bound; nested under a Loading that has shown content; a derivation outside the boundary still holds the tick; the boundary's hold stays its own; catchers at any depth — unrevealed, revealed, plain, and re-armed in flight; boundaries mounted by the hold; no catcher; `isPending` and `latest` on each; a read made pending before the flush joins its hold, and the #1078, #476/#1674 fuzzer findings; the re-arm shapes, pinned `it.fails`); `tests/loading-on-frame-following-3540.test.ts` (3b, 3c: re-arm over an earlier change's flight or staged write, pinned `it.fails` but for the inside-the-action control); `packages/web/test/loading-fallback-in-flush-3540.spec.tsx`. @@ -124,6 +124,8 @@ A tracked computation served a node's staged `_pendingValue` — a value a live **A verdict lane's mount over a staging (amended 2026-10-07, #3851).** Maintainer ruling: "we don't want to add size for these unseen things.. that being said in general transitions can hold on any unready read.. But new memos created don't need to be part of it." A transition holds on any unready read; a memo does not join it because it is new. Inside an action, ` 0}>` opens ahead of the action (A19, display-ahead) and its condition is verdict-lane work; the `` it mounts is a mainline mount (the creator's verdict lane is not inherited). Its content's first pass runs in the action's own flush and reads `x`'s staging before the seam holds it — the same world the transaction is about to hold, so the pass is born held as if it had read the held node, and the boundary shows its fallback beside the committed `x = 0` and `content 1` at the landing (`tests/verdict-mount-first-pass-3851.test.ts`). Only a verdict lane's mount: any other first pass that read a staging of the flush publishes as before and, if the flush parks, re-derives on the committed world (the staging-read rule, L2 statement 3). In particular a boundary a held memo mounts in the same flush is not made the transaction's by its creation — its content does not join, the boundary reports no pending of its own, and it appears with the memo at the commit (`tests/loading-fallback-in-flush-3540.test.ts`, "a boundary mounted by the hold"). An action body's read of an unflushed write is served committed (A28) and never counts. +**Clarified 2026-10-07 (#3800, maintainer):** "I meant new memos that don't read async.. if you are moving from page a to page b.. and page b reads async everything is held even if it is new async (assuming the read doesn't get exclusively caught by a boundary). These are basics." A new memo whose first pass reads the held world is held with it whether its first answer is a value or a promise: an async first answer lands into the transaction while it is live (the direction rule's "a first load derived from a hold lands into it"). + ### A31. A memo computes under its own lane posture, never its puller's **Status:** **live** 2026-09-14 (#3442) — stated by the fix; the lane-side twin of A29's "a value derived from the held world is that transaction's work" diff --git a/packages/signals/src/core/async.ts b/packages/signals/src/core/async.ts index 989edba68..88a944301 100644 --- a/packages/signals/src/core/async.ts +++ b/packages/signals/src/core/async.ts @@ -29,7 +29,9 @@ import { flush, globalQueue, GlobalQueue, + holdNode, joinFuture, + liveTx, MAINLINE_QUESTION, passLane, question, @@ -464,7 +466,11 @@ export function handleAsync( } else { // CARVE 2: the override-covered landing (hold + A18 supersession) and the // lane-routed landing (derived override, lane effect queue) went with - // the optimistic engine; every landing is the plain setSignal. + // the optimistic engine; every landing is the plain setSignal. A first + // answer born into a hold still live lands into it (A29, #3800). + let t = el._x!._bornIn; + el._x!._bornIn = null; + if (wasUninitialized && t && (t = liveTx(t))) holdNode(el, t); try { setSignal(el, () => value); } catch (e) { diff --git a/packages/signals/src/core/core.ts b/packages/signals/src/core/core.ts index 87e98ae43..731cc0f6f 100644 --- a/packages/signals/src/core/core.ts +++ b/packages/signals/src/core/core.ts @@ -707,6 +707,18 @@ export function recompute(el: Computed, create: boolean = false): void { if (wasPendingSource && !(el._statusFlags & (STATUS_PENDING | STATUS_UNINITIALIZED))) settlePendingSource(el); } + // A first pass that went pending where the born-held arm would have held + // its value (it read a held node, or a verdict lane's mount read a staging, + // #3851) is born into that transaction without holding it: the hold never + // waits for a first load (the direction rule), but the answer lands into it + // while it is live (A29, #3800). + if ( + create && + lane === null && + (joined || (prevLane && el._flags & REACTIVE_STAGED_READ)) && + el._statusFlags & STATUS_PENDING + ) + ext(el)._bornIn = flushTransaction ?? passTx; // Dependencies are the committed frame's until it is replaced (A30, #3410): // a pass that staged its value leaves the previous pass's tail linked for // `commitPendingNode` to trim, so a write to a dependency the committed @@ -944,6 +956,7 @@ export function ext(el: { _x: NodeExtension | null }): NodeExtension { _pendingFirstChild: null, _pendingDisposal: null, _transaction: null, + _bornIn: null, _reask: false, _flushed: NOT_PENDING, _flushedAt: -1, diff --git a/packages/signals/src/core/scheduler.ts b/packages/signals/src/core/scheduler.ts index ff28cfcc5..3fa558f2a 100644 --- a/packages/signals/src/core/scheduler.ts +++ b/packages/signals/src/core/scheduler.ts @@ -191,6 +191,12 @@ export function joinPassTx(t: Transaction, own?: unknown): void { export function txOf(n: Signal | Computed): Transaction { return (n._x!._transaction = resolveTx(n._x!._transaction!)); } +/** `t` resolved through merges while it is still parked or open; null once + * it has landed. */ +export function liveTx(t: Transaction): Transaction | null { + t = resolveTx(t); + return transactions.indexOf(t) !== -1 ? t : null; +} export function resolveTx(t: Transaction): Transaction { while (t._into !== null) t = t._into; return t; @@ -876,8 +882,8 @@ export class GlobalQueue implements IQueue { // write; #3528: a display-ahead swap before the count it is shown // beside). A parked frame's runs wait with its transaction instead: the // effect phase applies a committed frame. - const u = t !== null ? resolveTx(t) : null; - const parked = u !== null && transactions.indexOf(u) !== -1; + const u = t && liveTx(t); + const parked = u !== null; // Attribution hook: this flush found its transaction incomplete — its // writes stay staged, its runs are stashed below. Before the effect // phase (the lanes' display-ahead runs are the visible acknowledgers); diff --git a/packages/signals/src/core/types.ts b/packages/signals/src/core/types.ts index 0d6d2b225..084a423a8 100644 --- a/packages/signals/src/core/types.ts +++ b/packages/signals/src/core/types.ts @@ -89,6 +89,12 @@ export interface NodeExtension { /** L2 — the transaction holding this node while CONFIG_HELD is set * (`holdNode`); resolved through merges by `txOf`. */ _transaction: Transaction | null; + /** L2 — the transaction a first pass that went pending was born into (it + * read a held node, or a verdict lane's mount read a staging of the + * flush — the born-held arm's conditions): its first answer lands into it + * while it is live; the hold never waits for the node's first load (the + * direction rule, #3800). Consumed at the landing. */ + _bornIn: Transaction | null; /** The node's current flight re-asks the same question (a `refresh()`, * A19 exc. 2 / A24): verdict-quiet — `isPending` reads false for it — * through its landing until the commit. */ diff --git a/packages/signals/tests/issue-3800-repro.test.ts b/packages/signals/tests/issue-3800-repro.test.ts new file mode 100644 index 000000000..341ca7176 --- /dev/null +++ b/packages/signals/tests/issue-3800-repro.test.ts @@ -0,0 +1,260 @@ +import { describe, expect, it } from "vitest"; +import { + action, + createLoadingBoundary, + createMemo, + createRenderEffect, + createRoot, + createSignal, + flush +} from "../src/index.js"; + +function hold(write: () => void) { + let release!: () => void; + action(function* () { + write(); + yield new Promise(resolve => (release = resolve)); + })(); + flush(); + return () => release(); +} + +const tick = async () => { + for (let i = 0; i < 6; i++) await Promise.resolve(); + flush(); +}; + +function setup() { + const [count, setCount] = createSignal(1); + const gates: Array<() => void> = []; + const log: string[] = []; + createRoot(() => { + const slow = createMemo(async () => { + const c = count(); + await new Promise(r => gates.push(r)); + return c; + }); + createRenderEffect( + () => [count(), slow()] as const, + ([c, s]) => void log.push(`parent ${c} ${s}`), + undefined, + { schedule: true } + ); + }); + flush(); + return { count, setCount, gates, log }; +} + +function mountChild(s: ReturnType, async: boolean) { + createRoot(() => { + const child = createMemo( + (async ? () => Promise.resolve(s.count()) : () => s.count()) as () => number + ); + createRenderEffect(child, v => void s.log.push(`child ${v}`), undefined, { schedule: true }); + }); +} + +describe("#3800", () => { + for (const async of [true, false]) { + it(`${async ? "async" : "sync"} child created after flush() over a held count waits for the hold`, async () => { + const s = setup(); + s.gates.shift()!(); + await tick(); + expect(s.log).toEqual(["parent 1 1"]); + + s.setCount(2); + flush(); + mountChild(s, async); + flush(); + await tick(); + expect(s.log).toEqual(["parent 1 1"]); + + s.gates.shift()!(); + await tick(); + expect(s.log).toEqual(["parent 1 1", "parent 2 2", "child 2"]); + }); + } + + it("control: async child, no flush() before the mount, waits for the hold", async () => { + const s = setup(); + s.gates.shift()!(); + await tick(); + s.setCount(2); + mountChild(s, true); + flush(); + await tick(); + expect(s.log).toEqual(["parent 1 1"]); + s.gates.shift()!(); + await tick(); + expect(s.log).toEqual(["parent 1 1", "parent 2 2", "child 2"]); + }); + + it("control: sync child, no flush() before the mount, reads committed 1 (A28)", async () => { + const s = setup(); + s.gates.shift()!(); + await tick(); + s.setCount(2); + mountChild(s, false); + flush(); + await tick(); + expect(s.log).toEqual(["parent 1 1", "child 1"]); + s.gates.shift()!(); + await tick(); + expect(s.log).toEqual(["parent 1 1", "child 1", "parent 2 2", "child 2"]); + }); +}); + +describe("#3800: a node created over a hold waits for it; the hold never waits for the node", () => { + // `slow` holds count = 2 while its flight is out; `parent` shows both. + function setup() { + const [count, setCount] = createSignal(1); + const slowGates: Array<() => void> = []; + const log: string[] = []; + createRoot(() => { + const slow = createMemo(async () => { + const c = count(); + await new Promise(resolve => slowGates.push(resolve)); + return c; + }); + createRenderEffect( + () => [count(), slow()] as const, + ([c, s]) => { + log.push(`parent ${c} ${s}`); + }, + undefined, + { schedule: true } + ); + }); + flush(); + return { count, setCount, slowGates, log }; + } + + async function holdCount(s: ReturnType) { + s.slowGates.shift()!(); + await tick(); + s.setCount(2); + flush(); + expect(s.log).toEqual(["parent 1 1"]); + } + + it("a first load that reads the held value lands into the hold (the report)", async () => { + const s = setup(); + await holdCount(s); + createRoot(() => { + const child = createMemo(() => Promise.resolve(s.count())); + createRenderEffect( + child, + v => { + s.log.push(`child ${v}`); + }, + undefined, + { schedule: true } + ); + }); + flush(); + await tick(); + expect(s.log).toEqual(["parent 1 1"]); + + s.slowGates.shift()!(); + await tick(); + expect(s.log).toEqual(["parent 1 1", "parent 2 2", "child 2"]); + }); + + it("under a fresh boundary: the fallback shows until the hold commits the content", async () => { + const s = setup(); + await holdCount(s); + createRoot(() => { + const child = createMemo(() => Promise.resolve(s.count())); + const view = createLoadingBoundary( + () => `content ${child()}`, + () => "fallback" + ); + createRenderEffect( + view, + v => { + s.log.push(`view ${v}`); + }, + undefined, + { schedule: true } + ); + }); + flush(); + await tick(); + expect(s.log).toEqual(["parent 1 1", "view fallback"]); + + s.slowGates.shift()!(); + await tick(); + expect(s.log).toEqual(["parent 1 1", "view fallback", "parent 2 2", "view content 2"]); + }); + + it("the hold commits without a slower first load; the load lands after as its own commit", async () => { + const s = setup(); + await holdCount(s); + const childGates: Array<() => void> = []; + createRoot(() => { + const child = createMemo(async () => { + const c = s.count(); + await new Promise(resolve => childGates.push(resolve)); + return c; + }); + createRenderEffect( + child, + v => { + s.log.push(`child ${v}`); + }, + undefined, + { schedule: true } + ); + }); + flush(); + + s.slowGates.shift()!(); + await tick(); + expect(s.log).toEqual(["parent 1 1", "parent 2 2"]); + + childGates.shift()!(); + await tick(); + expect(s.log).toEqual(["parent 1 1", "parent 2 2", "child 2"]); + }); + + it("an action's hold: the first load waits for the action, never the reverse", async () => { + const [user, setUser] = createSignal("ann"); + const log: string[] = []; + createRoot(() => { + createRenderEffect(user, u => { + log.push(`header ${u}`); + }); + }); + flush(); + const release = hold(() => setUser("bob")); + + const pageGates: Array<() => void> = []; + const mount = (name: string) => + createRoot(() => { + const page = createMemo(async () => { + const u = user(); + await new Promise(resolve => pageGates.push(resolve)); + return `${name}-${u}`; + }); + createRenderEffect(page, v => { + log.push(`page ${v}`); + }); + }); + mount("fast"); + flush(); + mount("slow"); + flush(); + + pageGates.shift()!(); + await tick(); + expect(log).toEqual(["header ann"]); + + release(); + await tick(); + expect(log).toEqual(["header ann", "header bob", "page fast-bob"]); + + pageGates.shift()!(); + await tick(); + expect(log).toEqual(["header ann", "header bob", "page fast-bob", "page slow-bob"]); + }); +}); diff --git a/packages/signals/tests/verdict-mount-first-pass-3851.test.ts b/packages/signals/tests/verdict-mount-first-pass-3851.test.ts index 02c5ef014..0078b5d35 100644 --- a/packages/signals/tests/verdict-mount-first-pass-3851.test.ts +++ b/packages/signals/tests/verdict-mount-first-pass-3851.test.ts @@ -29,7 +29,7 @@ const settle = async () => { } }; -async function mountUnderVerdict(contentInMemo: boolean) { +async function mountUnderVerdict(contentInMemo: boolean | "async") { const [x, setX] = createSignal(0); let screenX: unknown; let slot: unknown; @@ -46,7 +46,11 @@ async function mountUnderVerdict(contentInMemo: boolean) { when() ? untrack(() => createLoadingBoundary( - contentInMemo ? createMemo(() => `content ${x()}`) : () => `content ${x()}`, + contentInMemo === "async" + ? createMemo(() => Promise.resolve(`content ${x()}`)) + : contentInMemo + ? createMemo(() => `content ${x()}`) + : () => `content ${x()}`, () => "fallback" ) ) @@ -96,4 +100,12 @@ describe("#3851: a verdict reader's mount stays mainline", () => { [1, "content 1"] ]); }); + + it("same with an async content memo", async () => { + expect(await mountUnderVerdict("async")).toEqual([ + [0, "closed"], + [0, "fallback"], + [1, "content 1"] + ]); + }); }); From dfac67df6cd643501f6666388554cec0100626c4 Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Wed, 7 Oct 2026 13:53:47 -0700 Subject: [PATCH 2/2] chore(size): size exception for a new async memo's first landing into its hold (#3800) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Raises seventeen caps to CI brotli + 10 B with their recorded minified, from Size run 37684843282 against next @ dafad1db3 — the frozen core, simple-app, hydrating and live-page floors among them. Accepted by the maintainer 2026-10-07 on the condition hello world stays under 10 KB (CI: 9,914 B). Co-authored-by: Claude via Cursor Co-authored-by: Cursor --- scripts/size/floor-caps.json | 16 +-- scripts/size/scenarios.js | 209 ++++++++++++++++++++++++++++++----- 2 files changed, 191 insertions(+), 34 deletions(-) diff --git a/scripts/size/floor-caps.json b/scripts/size/floor-caps.json index 105f132d2..787d05aea 100644 --- a/scripts/size/floor-caps.json +++ b/scripts/size/floor-caps.json @@ -1,23 +1,23 @@ { "signals: core floor (createSignal/Memo/Effect/Root/flush)": { - "cap": "7.40 KB", - "minified": 20178 + "cap": "7.45 KB", + "minified": 20316 }, "app: render + one signal (the simple-app floor)": { - "cap": "9.88 KB", - "minified": 27731 + "cap": "9.93 KB", + "minified": 27872 }, "app: hydrating (no stores) with Show/For/Loading/Errored/lazy": { - "cap": "17.85 KB", - "minified": 52841 + "cap": "17.91 KB", + "minified": 52977 }, "page: base server components (hydrating + dynamic + frames + sf reference)": { "cap": "33.92 KB", "minified": 105252 }, "page: live server components (base + live/GET + action + isPending/latest)": { - "cap": "37.53 KB", - "minified": 117301 + "cap": "37.59 KB", + "minified": 117441 }, "server: floor (getRequestEvent + isServer)": { "cap": "1.34 KB", diff --git a/scripts/size/scenarios.js b/scripts/size/scenarios.js index 05da999b3..b58934052 100644 --- a/scripts/size/scenarios.js +++ b/scripts/size/scenarios.js @@ -541,6 +541,16 @@ module.exports = [ // verdict-lane stagedReaders. Cap set at measured + 10 B rounded up to // 0.01 KB. Accepted by the maintainer 2026-10-07. The cap is frozen again // at 7.40 KB. + // Size-Exception (a new async memo created over a hold lands its first + // answer into it, #3800, 2026-10-07): 7.40 KB -> 7.45 KB (floor-caps.json), + // measured at 7,432 B by CI (Size run 37684843282) against `next` @ + // dafad1db3's 7,382 (+50 B; 32 B over the cap; +139 B minified, 20,177 -> + // 20,316; recorded 20,178 -> 20,316) — the signals core: a pending first + // pass records the transaction it was born into (`_bornIn`), and its first + // landing stages into it while it is live (`liveTx`, `holdNode`). Cap set + // at measured + 10 B rounded up to 0.01 KB. Accepted by the maintainer + // 2026-10-07 on the condition hello world stays under 10 KB. The cap is + // frozen again at 7.45 KB. limit: floorCaps["signals: core floor (createSignal/Memo/Effect/Root/flush)"], capMinified: floorMinified["signals: core floor (createSignal/Memo/Effect/Root/flush)"], alias @@ -973,8 +983,17 @@ module.exports = [ // verdict lane's mount born held for a staged read, and verdict-lane // stagedReaders. Cap set at measured + 10 B rounded up to 0.01 KB. // Accepted by the maintainer 2026-10-07. - limit: "14.62 KB", - capMinified: 44439, + // Size-Exception (a new async memo created over a hold lands its first + // answer into it, #3800, 2026-10-07): 14.62 KB -> 14.68 KB, measured at + // 14,667 B by CI (Size run 37684843282) against `next` @ dafad1db3's 14,603 + // (+64 B; 47 B over the cap; +145 B minified, 44,437 -> 44,582; recorded + // 44,439 -> 44,582) — the signals core: a pending first pass records the + // transaction it was born into (`_bornIn`), and its first landing stages + // into it while it is live (`liveTx`, `holdNode`). Cap set at measured + 10 + // B rounded up to 0.01 KB. Accepted by the maintainer 2026-10-07 on the + // condition hello world stays under 10 KB. + limit: "14.68 KB", + capMinified: 44582, alias }, { @@ -1228,8 +1247,17 @@ module.exports = [ // (+21), the landing's pending-derivation mark (+9) and the correction's // drop of parked runs (+7). Cap set at measured + 10 B rounded up to // 0.01 KB. Accepted by the maintainer 2026-10-07. - limit: "9.58 KB", - capMinified: 26962, + // Size-Exception (a new async memo created over a hold lands its first + // answer into it, #3800, 2026-10-07): 9.58 KB -> 9.65 KB, measured at 9,634 + // B by CI (Size run 37684843282) against `next` @ dafad1db3's 9,570 (+64 B; + // 54 B over the cap; +135 B minified, 27,020 -> 27,155; recorded 26,962 -> + // 27,155) — the signals core: a pending first pass records the transaction + // it was born into (`_bornIn`), and its first landing stages into it while + // it is live (`liveTx`, `holdNode`). Cap set at measured + 10 B rounded up + // to 0.01 KB. Accepted by the maintainer 2026-10-07 on the condition hello + // world stays under 10 KB. + limit: "9.65 KB", + capMinified: 27155, alias }, { @@ -1433,6 +1461,16 @@ module.exports = [ // verdict-lane stagedReaders. Cap set at measured + 10 B rounded up to // 0.01 KB. Accepted by the maintainer 2026-10-07. The cap is frozen again // at 9.88 KB. + // Size-Exception (a new async memo created over a hold lands its first + // answer into it, #3800, 2026-10-07): 9.88 KB -> 9.93 KB (floor-caps.json), + // measured at 9,914 B by CI (Size run 37684843282) against `next` @ + // dafad1db3's 9,878 (+36 B; 34 B over the cap; +142 B minified, 27,730 -> + // 27,872; recorded 27,731 -> 27,872) — the signals core: a pending first + // pass records the transaction it was born into (`_bornIn`), and its first + // landing stages into it while it is live (`liveTx`, `holdNode`). Cap set + // at measured + 10 B rounded up to 0.01 KB. Accepted by the maintainer + // 2026-10-07 on the condition hello world stays under 10 KB. The cap is + // frozen again at 9.93 KB. limit: floorCaps["app: render + one signal (the simple-app floor)"], capMinified: floorMinified["app: render + one signal (the simple-app floor)"], alias @@ -1779,6 +1817,16 @@ module.exports = [ // measured + 10 B rounded up to 0.01 KB. Accepted by the maintainer // (2026-10-06, "I will follow recommendations here"; A5′ within the budget // he set). The cap is frozen again at 17.85 KB. + // Size-Exception (a new async memo created over a hold lands its first + // answer into it, #3800, 2026-10-07): 17.85 KB -> 17.91 KB + // (floor-caps.json), measured at 17,892 B by CI (Size run 37684843282) + // against `next` @ dafad1db3's 17,840 (+52 B; 42 B over the cap; +136 B + // minified, 52,841 -> 52,977; recorded 52,841 -> 52,977) — the signals + // core: a pending first pass records the transaction it was born into + // (`_bornIn`), and its first landing stages into it while it is live + // (`liveTx`, `holdNode`). Cap set at measured + 10 B rounded up to 0.01 KB. + // Accepted by the maintainer 2026-10-07 on the condition hello world stays + // under 10 KB. The cap is frozen again at 17.91 KB. limit: floorCaps["app: hydrating (no stores) with Show/For/Loading/Errored/lazy"], capMinified: floorMinified["app: hydrating (no stores) with Show/For/Loading/Errored/lazy"], alias @@ -2240,8 +2288,17 @@ module.exports = [ // (A5′, rulings 3.3) in solid-js (the hydrating floor's note). Cap set at // measured + 10 B rounded up to 0.01 KB. Accepted by the maintainer // (2026-10-06). - limit: "29.09 KB", - capMinified: 92114, + // Size-Exception (a new async memo created over a hold lands its first + // answer into it, #3800, 2026-10-07): 29.09 KB -> 29.19 KB, measured at + // 29,179 B by CI (Size run 37684843282) against `next` @ dafad1db3's 29,079 + // (+100 B; 89 B over the cap; +149 B minified, 92,114 -> 92,263; recorded + // 92,114 -> 92,263) — the signals core: a pending first pass records the + // transaction it was born into (`_bornIn`), and its first landing stages + // into it while it is live (`liveTx`, `holdNode`). Cap set at measured + 10 + // B rounded up to 0.01 KB. Accepted by the maintainer 2026-10-07 on the + // condition hello world stays under 10 KB. + limit: "29.19 KB", + capMinified: 92263, alias }, { @@ -2476,8 +2533,17 @@ module.exports = [ // verdict lane's mount born held for a staged read, and verdict-lane // stagedReaders. Cap set at measured + 10 B rounded up to 0.01 KB. // Accepted by the maintainer 2026-10-07. - limit: "12.88 KB", - capMinified: 36617, + // Size-Exception (a new async memo created over a hold lands its first + // answer into it, #3800, 2026-10-07): 12.88 KB -> 12.96 KB, measured at + // 12,945 B by CI (Size run 37684843282) against `next` @ dafad1db3's 12,863 + // (+82 B; 65 B over the cap; +139 B minified, 36,616 -> 36,755; recorded + // 36,617 -> 36,755) — the signals core: a pending first pass records the + // transaction it was born into (`_bornIn`), and its first landing stages + // into it while it is live (`liveTx`, `holdNode`). Cap set at measured + 10 + // B rounded up to 0.01 KB. Accepted by the maintainer 2026-10-07 on the + // condition hello world stays under 10 KB. + limit: "12.96 KB", + capMinified: 36755, alias }, { @@ -2753,8 +2819,17 @@ 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 (a new async memo created over a hold lands its first + // answer into it, #3800, 2026-10-07): 14.46 KB -> 14.53 KB, measured at + // 14,515 B by CI (Size run 37684843282) against `next` @ dafad1db3's 14,448 + // (+67 B; 55 B over the cap; +138 B minified, 41,146 -> 41,284; recorded + // 41,091 -> 41,284) — the signals core: a pending first pass records the + // transaction it was born into (`_bornIn`), and its first landing stages + // into it while it is live (`liveTx`, `holdNode`). Cap set at measured + 10 + // B rounded up to 0.01 KB. Accepted by the maintainer 2026-10-07 on the + // condition hello world stays under 10 KB. + limit: "14.53 KB", + capMinified: 41284, alias: observeAlias }, { @@ -3166,8 +3241,17 @@ module.exports = [ // Engine-only: every prod scenario byte-identical, the observe tier // scenario above moved -7 B. Cap set at measured + 10 B rounded up to // 0.01 KB. Accepted by the maintainer 2026-10-07. - limit: "28.82 KB", - capMinified: 86773, + // Size-Exception (a new async memo created over a hold lands its first + // answer into it, #3800, 2026-10-07): 28.82 KB -> 28.89 KB, measured at + // 28,876 B by CI (Size run 37684843282) against `next` @ dafad1db3's 28,804 + // (+72 B; 56 B over the cap; +138 B minified, 86,773 -> 86,911; recorded + // 86,773 -> 86,911) — the signals core: a pending first pass records the + // transaction it was born into (`_bornIn`), and its first landing stages + // into it while it is live (`liveTx`, `holdNode`). Cap set at measured + 10 + // B rounded up to 0.01 KB. Accepted by the maintainer 2026-10-07 on the + // condition hello world stays under 10 KB. + limit: "28.89 KB", + capMinified: 86911, alias: observeAlias }, // Compiled-template scenarios (2026-10-05): the four `app:` fixtures above @@ -3213,8 +3297,17 @@ module.exports = [ // verdict lane's mount born held for a staged read, and verdict-lane // stagedReaders. Cap set at measured + 10 B rounded up to 0.01 KB. // Accepted by the maintainer 2026-10-07. - limit: "10.07 KB", - capMinified: 28261, + // Size-Exception (a new async memo created over a hold lands its first + // answer into it, #3800, 2026-10-07): 10.07 KB -> 10.13 KB, measured at + // 10,118 B by CI (Size run 37684843282) against `next` @ dafad1db3's 10,068 + // (+50 B; 48 B over the cap; +143 B minified, 28,260 -> 28,403; recorded + // 28,261 -> 28,403) — the signals core: a pending first pass records the + // transaction it was born into (`_bornIn`), and its first landing stages + // into it while it is live (`liveTx`, `holdNode`). Cap set at measured + 10 + // B rounded up to 0.01 KB. Accepted by the maintainer 2026-10-07 on the + // condition hello world stays under 10 KB. + limit: "10.13 KB", + capMinified: 28403, alias }, { @@ -3263,8 +3356,17 @@ module.exports = [ // verdict lane's mount born held for a staged read, and verdict-lane // stagedReaders. Cap set at measured + 10 B rounded up to 0.01 KB. // Accepted by the maintainer 2026-10-07. - limit: "25.22 KB", - capMinified: 78946, + // Size-Exception (a new async memo created over a hold lands its first + // answer into it, #3800, 2026-10-07): 25.22 KB -> 25.24 KB, measured at + // 25,222 B by CI (Size run 37684843282) against `next` @ dafad1db3's 25,187 + // (+35 B; 2 B over the cap; +141 B minified, 78,944 -> 79,085; recorded + // 78,946 -> 79,085) — the signals core: a pending first pass records the + // transaction it was born into (`_bornIn`), and its first landing stages + // into it while it is live (`liveTx`, `holdNode`). Cap set at measured + 10 + // B rounded up to 0.01 KB. Accepted by the maintainer 2026-10-07 on the + // condition hello world stays under 10 KB. + limit: "25.24 KB", + capMinified: 79085, alias }, { @@ -3324,8 +3426,17 @@ module.exports = [ // (A5′, rulings 3.3) in solid-js (the hydrating floor's note). Cap set at // measured + 10 B rounded up to 0.01 KB. Accepted by the maintainer // (2026-10-06). - limit: "31.09 KB", - capMinified: 99477, + // Size-Exception (a new async memo created over a hold lands its first + // answer into it, #3800, 2026-10-07): 31.09 KB -> 31.12 KB, measured at + // 31,105 B by CI (Size run 37684843282) against `next` @ dafad1db3's 31,079 + // (+26 B; 15 B over the cap; +140 B minified, 99,477 -> 99,617; recorded + // 99,477 -> 99,617) — the signals core: a pending first pass records the + // transaction it was born into (`_bornIn`), and its first landing stages + // into it while it is live (`liveTx`, `holdNode`). Cap set at measured + 10 + // B rounded up to 0.01 KB. Accepted by the maintainer 2026-10-07 on the + // condition hello world stays under 10 KB. + limit: "31.12 KB", + capMinified: 99617, alias }, { @@ -4183,6 +4294,16 @@ module.exports = [ // branch's base land under the `dynamicComponent` note's 37,241 / // 116,485 (+816 B minified, +271 B brotli). Cap set at measured + 10 B // rounded up to 0.01 KB; recorded minified 117,301 B. + // Size-Exception (a new async memo created over a hold lands its first + // answer into it, #3800, 2026-10-07): 37.53 KB -> 37.59 KB + // (floor-caps.json), measured at 37,578 B by CI (Size run 37684843282) + // against `next` @ dafad1db3's 37,512 (+66 B; 48 B over the cap; +140 B + // minified, 117,301 -> 117,441; recorded 117,301 -> 117,441) — the signals + // core: a pending first pass records the transaction it was born into + // (`_bornIn`), and its first landing stages into it while it is live + // (`liveTx`, `holdNode`). Cap set at measured + 10 B rounded up to 0.01 KB. + // Accepted by the maintainer 2026-10-07 on the condition hello world stays + // under 10 KB. The cap is frozen again at 37.59 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)"], @@ -4259,8 +4380,17 @@ module.exports = [ // 109,300 B. path: "fixtures/compiled/sc-base.jsx", compile: { hydratable: true }, - limit: "35.09 KB", - capMinified: 109300, + // Size-Exception (a new async memo created over a hold lands its first + // answer into it, #3800, 2026-10-07): 35.09 KB -> 35.13 KB, measured at + // 35,120 B by CI (Size run 37684843282) against `next` @ dafad1db3's 35,079 + // (+41 B; 30 B over the cap; +146 B minified, 109,300 -> 109,446; recorded + // 109,300 -> 109,446) — the signals core: a pending first pass records the + // transaction it was born into (`_bornIn`), and its first landing stages + // into it while it is live (`liveTx`, `holdNode`). Cap set at measured + 10 + // B rounded up to 0.01 KB. Accepted by the maintainer 2026-10-07 on the + // condition hello world stays under 10 KB. + limit: "35.13 KB", + capMinified: 109446, alias: pageAlias }, { @@ -4301,8 +4431,17 @@ module.exports = [ // 122,779 B. path: "fixtures/compiled/sc-live.jsx", compile: { hydratable: true }, - limit: "40.61 KB", - capMinified: 122779, + // Size-Exception (a new async memo created over a hold lands its first + // answer into it, #3800, 2026-10-07): 40.61 KB -> 40.66 KB, measured at + // 40,645 B by CI (Size run 37684843282) against `next` @ dafad1db3's 40,593 + // (+52 B; 35 B over the cap; +139 B minified, 122,779 -> 122,918; recorded + // 122,779 -> 122,918) — the signals core: a pending first pass records the + // transaction it was born into (`_bornIn`), and its first landing stages + // into it while it is live (`liveTx`, `holdNode`). Cap set at measured + 10 + // B rounded up to 0.01 KB. Accepted by the maintainer 2026-10-07 on the + // condition hello world stays under 10 KB. + limit: "40.66 KB", + capMinified: 122918, alias: pageAlias }, { @@ -4342,8 +4481,17 @@ module.exports = [ // `dynamic`, but this page does not use it. Cap at measured + 10 B // rounded up to 0.01 KB; recorded minified 144,082 B (CI-confirmed). path: "sc-router-app.js", - limit: "45.95 KB", - capMinified: 144082, + // Size-Exception (a new async memo created over a hold lands its first + // answer into it, #3800, 2026-10-07): 45.95 KB -> 46.02 KB, measured at + // 46,010 B by CI (Size run 37684843282) against `next` @ dafad1db3's 45,940 + // (+70 B; 60 B over the cap; +141 B minified, 144,082 -> 144,223; recorded + // 144,082 -> 144,223) — the signals core: a pending first pass records the + // transaction it was born into (`_bornIn`), and its first landing stages + // into it while it is live (`liveTx`, `holdNode`). Cap set at measured + 10 + // B rounded up to 0.01 KB. Accepted by the maintainer 2026-10-07 on the + // condition hello world stays under 10 KB. + limit: "46.02 KB", + capMinified: 144223, alias: pageAlias }, { @@ -4362,8 +4510,17 @@ module.exports = [ // measured + 10 B rounded up to 0.01 KB; recorded minified 148,526 B // (CI-confirmed). path: "sc-live-router-app.js", - limit: "47.21 KB", - capMinified: 148526, + // Size-Exception (a new async memo created over a hold lands its first + // answer into it, #3800, 2026-10-07): 47.21 KB -> 47.29 KB, measured at + // 47,277 B by CI (Size run 37684843282) against `next` @ dafad1db3's 47,197 + // (+80 B; 67 B over the cap; +142 B minified, 148,526 -> 148,668; recorded + // 148,526 -> 148,668) — the signals core: a pending first pass records the + // transaction it was born into (`_bornIn`), and its first landing stages + // into it while it is live (`liveTx`, `holdNode`). Cap set at measured + 10 + // B rounded up to 0.01 KB. Accepted by the maintainer 2026-10-07 on the + // condition hello world stays under 10 KB. + limit: "47.29 KB", + capMinified: 148668, alias: pageAlias }, {