diff --git a/.changeset/lane-holds-through-boundary-content.md b/.changeset/lane-holds-through-boundary-content.md new file mode 100644 index 000000000..b4f926a4f --- /dev/null +++ b/.changeset/lane-holds-through-boundary-content.md @@ -0,0 +1,5 @@ +--- +"@solidjs/signals": patch +--- + +A lane now holds through a `Loading` boundary showing content: a boundary output forwarding its tree's pending counts as a frame reader, so `latest()` no longer reveals beside the boundary's stale content (A33). An output that never committed — a mount the frame has not revealed — holds nothing. A corrected guess that never showed is void at the correction: untracked reads return the committed value (A18 amendment, 2026-10-05). diff --git a/packages/signals/docs/RULES-INDEX.md b/packages/signals/docs/RULES-INDEX.md index 2b199c54f..00bbd1f2e 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: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… | +| A1 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:311` | — | 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:319` | — | 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:327` | — | 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:335` | — | 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:343` | — | 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:351` | 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:157` | — | 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:165` | — | 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:173` | 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:181` | 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:99` | — | 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:189` | — | 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:197` | — | 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:205` | — | 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:241` | async.ts×2 constants.ts×1 core.ts×3 lanes.ts×4 scheduler.ts×14 verdict.ts×3 | async-chain-supersession.test.ts×1 boundary-output-frame-reader.test.ts×1 first-observer-stale-reader.test.ts×1 fuzz-findings-l2.test.ts×17 held-restore.test.ts×1 l2-contract.test.ts×7 lane-contract.test.ts×1 lane-hold-on-observation.test.ts×1 lane-outside-view.test.ts×1 lane-pass-stamped-effect-3662.test.ts×1 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:213` | 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… | +| 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×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:141` | 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:400` | — | 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:407` | — | 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:221` | 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:231` | — | 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:149` | 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:301` | 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:107` | — | 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:293` | — | 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:91` | 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:115` | 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:249` | 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:123` | 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:131` | 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:265` | 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:275` | 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:470` | — | spec-async-semantics.test.ts×7 | - **V1 (violated A13) — FIXED.** A _resting_ optimistic node reported | +| V2 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:480` | — | 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:486` | — | spec-async-semantics.test.ts×2 | - **V3 (violated A19) — FIXED.** After a reporter-less transition completed, | +| V4 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:493` | — | 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:505` | 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). | +| B1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:197` | — | 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:205` | — | 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:241` | — | 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). | +| B5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:213` | 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: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.** | +| C1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:141` | — | 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:436` | — | 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:446` | — | — | - [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). | ## INV — invariants diff --git a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md index 66ea0e230..e3b714992 100644 --- a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md +++ b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md @@ -46,7 +46,7 @@ The former Tier A table is these sections. Tier B/C, the fixed violations, and t **Membership in a hold is the tick's — the synchronous work one flush settles (2026-10-04, maintainer ruling; ruling 1 clarified, surfaced by #3761's review).** The frame is the tick, and a frame that derives from two futures waits on both: two mainline passes created before the same flush — no `flush()` between them — that each read a _different_ foreign hold join both, and the holds merge (`passTx` is one per tick, cleared by the flush that consumes it — `joinPassTx` merges a second transaction into it — exactly as `joinFuture` merges inside a flush). A manual `flush()` between two passes puts them in different frames: the second pass is a new tick, escapes the first's transaction, and the two holds stay independent. The tick is defined by the flush, not by a microtask, `await` or timer boundary. A29's "the entry is the pass's alone" scopes the entry away from the tick's other _writes_ (a write made after the mount is a mainline write, a render effect mounted beside it a stale reader), not from the tick's other passes. A15's "writes on fully disjoint graphs keep independent transitions" is about passes in different ticks. Pinned: `tests/tick-scoped-pass-transaction.test.ts` (`mountA(); mountB(); flush()` → the holds merge, both reveal at the second release, `isPending` true on both until then; `mountA(); flush(); mountB(); flush()` → independent, A reveals at its own release while B stays held). -**A lane's readers are placed by the screen, not by the lane's state (2026-10-05, maintainer ruling — reading A for all three; the semantic fuzzer's F8, F6 and adopted-staging cases).** A render effect off a lane that reads a lane node directly is a stale reader of the lane whether or not the lane is held at that moment, and a leaf reading the lane through a boundary that shows content is the lane's frame reader through it — and a leaf whose removal an adopted write stages (O1) still follows the lane's work while it is on screen (reading A, ruled), rather than waiting with the tick that staged its removal (reading B, rejected). **F8:** a mainline mount over a lane derivation whose re-ask nobody had observed (the lane not held) publishes its control, and a child reading the derivation directly shows the screen — the lane's revealed derivation — and re-derives at the reveal (A15 lanes corollary, #3460). Before, the child entered the lane and the control published without it, so control and content disagreed. Pre-L2 held the whole mount, which entangles more than the rule names. A derivation between the lane node and the leaf (a memo, a boundary's tree memo) is not a leaf: the mount is born held under A29's creation-time form and reveals control and content together at the landing, as #3761 rules for a mount over a foreign hold. Derivations carry the future, and that variant stays held. **F6:** a `Loading` showing content is transparent (A33, B5 — "the root holds the frame as for any reader"), so the leaf reading its output holds the lane on the derivation under it, as the same leaf reading the derivation directly does (A17; `createMemo.test.ts` A10 lane pins). The lane does not reveal `latest = 1` beside the boundary's `0`, and both show at the landing. A boundary on its fallback still holds nothing (A33). An `on`-reset boundary's one-checkpoint `[1, 0]` is a separate, older question and is not decided here. **Adopted staging:** a render effect under a mount whose withdrawal the action adopted (O1) reads a memo the lane recomputes. Its removal is staged, so it is still on screen (a zombie, live for every hold but the staging one's, #3463). Ruled (reading A): it follows the lane's work and shows `[1]`, as the same effect reading `latest()` directly already does (#3444), because wrapping a `latest` read in a memo does not change what it answers (A28 (1)). Rejected (reading B): the tick is one frame and the adopted hide makes the subtree the action's work, so it would keep the committed `[0]` until the hide commits or nets out while `latest()` read directly peers through by #3444's exception — and a sibling reader of the same memo off the withdrawn subtree shows `[1]` beside it, two on-screen readers of one memo disagreeing. Pre-L2 showed `[1]`. Pinned: `tests/fuzz-findings-l2.test.ts` "F8: …", "F8 (memo between): …", "adopted staging: …". F6 is ruled but not yet implemented: the boundary's output counting as the lane's frame reader also holds a lane the transaction dissolves before it is shown, which A18 (c)'s pins (`spec-async-semantics.test.ts` "differing arrival…", "sync wrapper…"; `store/lane-authority-twins.test.ts` #3331) expect to reveal — its pin stays `it.fails` until A18 (c) is read for that case. +**A lane's readers are placed by the screen, not by the lane's state (2026-10-05, maintainer ruling — reading A for all three; the semantic fuzzer's F8, F6 and adopted-staging cases).** A render effect off a lane that reads a lane node directly is a stale reader of the lane whether or not the lane is held at that moment, and a leaf reading the lane through a boundary that shows content is the lane's frame reader through it — and a leaf whose removal an adopted write stages (O1) still follows the lane's work while it is on screen (reading A, ruled), rather than waiting with the tick that staged its removal (reading B, rejected). **F8:** a mainline mount over a lane derivation whose re-ask nobody had observed (the lane not held) publishes its control, and a child reading the derivation directly shows the screen — the lane's revealed derivation — and re-derives at the reveal (A15 lanes corollary, #3460). Before, the child entered the lane and the control published without it, so control and content disagreed. Pre-L2 held the whole mount, which entangles more than the rule names. A derivation between the lane node and the leaf (a memo, a boundary's tree memo) is not a leaf: the mount is born held under A29's creation-time form and reveals control and content together at the landing, as #3761 rules for a mount over a foreign hold. Derivations carry the future, and that variant stays held. **F6:** a `Loading` showing content is transparent (A33, B5 — "the root holds the frame as for any reader"), so the leaf reading its output holds the lane on the derivation under it, as the same leaf reading the derivation directly does (A17; `createMemo.test.ts` A10 lane pins). The lane does not reveal `latest = 1` beside the boundary's `0`, and both show at the landing. A boundary on its fallback still holds nothing (A33). An `on`-reset boundary's one-checkpoint `[1, 0]` is a separate, older question and is not decided here. **Adopted staging:** a render effect under a mount whose withdrawal the action adopted (O1) reads a memo the lane recomputes. Its removal is staged, so it is still on screen (a zombie, live for every hold but the staging one's, #3463). Ruled (reading A): it follows the lane's work and shows `[1]`, as the same effect reading `latest()` directly already does (#3444), because wrapping a `latest` read in a memo does not change what it answers (A28 (1)). Rejected (reading B): the tick is one frame and the adopted hide makes the subtree the action's work, so it would keep the committed `[0]` until the hide commits or nets out while `latest()` read directly peers through by #3444's exception — and a sibling reader of the same memo off the withdrawn subtree shows `[1]` beside it, two on-screen readers of one memo disagreeing. Pre-L2 showed `[1]`. Pinned: `tests/fuzz-findings-l2.test.ts` "F8: …", "F8 (memo between): …", "adopted staging: …". Pinned (F6): `tests/fuzz-findings-l2.test.ts` "F6: …" — a boundary output forwarding its tree's pending is a frame reader of the lane (`blockedBy`); one that never committed holds nothing (A18, "A boundary output that never committed holds nothing", 2026-10-05); holding through the boundary leaves a corrected guess never shown, void at the correction (A18, "Correction display is what showed", 2026-10-05). **A hold never waits on work that has never committed — the direction rule (2026-10-06, maintainer ruling; amendment — the rulings quoted above and below are unchanged).** Membership in a hold has a direction. Committed work that reads a hold joins it (A15, A29): the two derive from each other and land as one, as the tick ruling above says. Work that has never committed — a node created while the hold is live, and its first load — derives from the hold one way: it lands no earlier than the hold, and the hold never waits for it. The rule names what several fixes each implement locally by checking whether a node has committed yet (`STATUS_UNINITIALIZED`); it adds no mechanism of its own: @@ -82,6 +82,10 @@ The one-way reading of both (the hold lands at its release, the frame lands afte **Statement (current).** (was B4; **refined by re-rule 2026-07-07b**) An override's lifetime is bound to **its own transition** — which, because lanes keep their transitions separate from unrelated work, contains exactly the override's own async cascade. **Supersession (re-ruled 2026-09-09, #3331): own-source arrival removes the optimism from the graph immediately; the display keeps it until the transaction commits.** Maintainer: "a new value from the source should remove the optimism immediately.. if it matches then no more work, if it doesn't match then that work gets folded into the parent transition"; "when the optimism drops we might not see it until end of transition because it folds into the parent's transition." This replaces the mechanical sentence above ("elevate to `_value` only at their transition's commit; the elevation is unobservable under the override mask") — that model let the override's own downstream flight serialize ahead of the truth's, doubling the delay the reporter saw. Now: (a) a landing that _equals_ the override confirms silently — nothing re-runs, the lane's in-flight work completes the frame; (b) a landing that _differs_ marks the node superseded: its subscribers recompute from the arrived value on the plain channel (their lane affinity is dropped, so this is held transaction work, not lane work), downstream async restarts from the truth _now_, and the override's own downstream flight is inert when it lands; (c) untracked reads and the applied screen keep the override until the transaction — holding for whatever the corrected derivations observe (A15) — commits and clears the override; (d) `latest` returns the arrived value, `isPending` reads `true` iff the arrival differs (consequence (3) unchanged in statement, now true in mechanism). **(d) before the first commit (2026-09-15, visibility oracle):** the verdict holds for a node that has never committed — its first landing held by a reveal that never landed — because the override is the observable value; A19 exception (1) ("uninitialized is loading, not pending: no observable value exists") does not apply under a displayed override. Pinned: `tests/superseded-before-first-commit.test.ts`. A later landing on the same node that equals the override un-supersedes it (the override is again the graph's value). **Scope (ruled 2026-09-10): "the source" is whatever recomputes the node** — its own async landing, or a synchronous recompute driven by an upstream change (`createOptimistic(() => userCategory())` over an async memo is the common real-world shape): "if the source recomputes it doesn't matter if it is async or not." **Ordering:** a new value from the source is one that _postdates_ the override — a source write and an override in the same batch derive nothing new (the override is written over that batch's truth knowingly and stays the graph's value until the commit reveals it). **Provenance (ruled 2026-09-10):** "a new value from the source" answers the override's _own_ question or a newer one. Two rapid actions on one node merge into one transaction, and the older action's refetch can land after the newer override; that answer is a question the user has since changed — it is staged for the commit like any landing (and reveals then iff it is still the truth) but does **not** supersede: no downstream re-derivation, no pending flip on downstream readers. "A slow source shouldn't leak back in like that." Only the override's own action, a later action, or mainline (no action — a fresh question by definition) supersedes. **Body-end corollary (2026-09-14, #3427):** the action bodies were the optimism's justification; once they have all ended, an override still in force is superseded by the truth already at hand — the staged value an equal (A17-silent) landing left, else the committed value — exactly as an arriving differing truth would be, _unless_ the transaction is still waiting on authoritative work: an override node's own source in flight (that answer confirms or supersedes on arrival), or a held flight that does not derive from an override (a plain load the action asked for; a co-written `saving` flag stays through it — the optimistic world is one, A17). Flights that derive from an override are questions about a guess that is about to revert, and nobody reads their answer: the graph re-derives from the truth as the transaction's held work and settles when _that_ lands, so the correction starts at the body's end instead of after the obsolete flight (which used to reveal the obsolete optimistic frame first, then revert and re-ask — a waterfall with a flash). Display is unchanged: the override stays on screen until the commit (c). Optimistic store edits keep the settle-then-revert order (their truth is a base layer under an overlay, with no tracked/displayed split), and companions snap at settlement. **Visibility during the body-end window (2026-09-15, visibility oracle):** identical to a landing supersession — the display and stale readers keep the override, a fresh derivation is held (A29), `latest()` answers the truth and `isPending()` reads true iff it differs — although nothing is staged (the truth at hand is the committed value). The node carries no `_transition` stamp in this window (an override written inside an action never passes the adoption loop that stamps one), so the read path and the verdict resolve the owning transaction through `_overrideOwner` (node corollary, #2912). Pinned: `tests/body-end-supersession-visibility.test.ts`. Consequences: (1) in unmerged graphs, own-source resolution IS the lane-transition's completion, so the correction reveals on arrival — the original A18 pins hold unchanged; (2) matching confirmations collapse silently (revert sees value == override, nobody re-runs); (3) when the override's transition genuinely merges with unrelated async, the correction reveals atomically with that merged completion — verdict during the window per A24 (amended 2026-07-13; was "false throughout" under the A20 mask): a held correction that _differs_ from the displayed override reads pending; a matching confirm stays quiet — corrections still _propagate_ internally on arrival (fresh readers/async drivers see the hold), so downstream refetches start immediately and no waterfalls form; only the reveal is gated. **Store corollary (2026-07-17, #2899): the optimistic layer obeys the same per-transaction lifetime.** `createOptimisticStore`'s override layer is one record per store target, but each entry is owned by the transaction that wrote it (`STORE_OPTIMISTIC_OWNERS` stamps, merge chains resolved): a settling action consumes only its own keys, so concurrent actions on disjoint keys revert independently — first-settling no longer wipes the other's live overrides. Same-key writes still entangle through the shared node (one joint settle); ambient (transaction-less) entries clear at plain flush end; a derived store's projection landing still consumes the whole layer (fresh authority supersedes every tentative write). **Node corollary (2026-07-18, #2912): ownership never travels through lanes.** Lanes are scheduling affinity — a shared subscriber (one effect reading keys touched by two actions) merges them correctly for flushing, but the merged root's `_transition` must not answer "which transaction owns this override": that let one action's settle revert another's live override, and same-key follow-up writes entangle with the wrong transaction. Every optimistic write stamps `_overrideOwner` on the node (post-merge, so entangled writers share the joint root; cleared at settle); `resolveTransition` prefers a live owner stamp over the lane, falling back to lane `_transition` for nodes without overrides (async routing) exactly as before. Pinned: `tests/spec-async-semantics.test.ts` ("#3331" describe: own-async, sync-wrapper, same-batch, provenance, simple graph; A18 entangled pin re-expected: the merged correction reveals as one frame, never the committed-behind-the-mask tear); `tests/createOptimistic.test.ts` (CategoryDisplay no-double-flicker pin, unchanged: the older action's answer never moves the graph; "second action while first still in flight" pin, resolver repaired and re-expected to the same rule). +**Correction display is what showed (2026-10-05, maintainer ruling — F6 vs A18 (c)).** A18 (c)'s "untracked reads and the applied screen keep the override until the transaction commits" keeps what the screen showed. A guess whose lane never revealed — held on its own downstream flight, including through a `Loading` showing content (A33, the lane-membership ruling) — is void at the correction (lane contract 2, lane ruling 5): untracked reads and stale readers see the committed value until the commit, as the screen does; `latest()` answers the arrived truth and `isPending()` reads `true` (A18 (d) unchanged). A lane that had shown keeps the override for both until the commit. Re-pinned: `spec-async-semantics.test.ts` "#3331 › differing arrival…", "#3331 › sync wrapper…"; `store/lane-authority-twins.test.ts` "#3331 twin". Before, those pins expected the override because the lane released past the boundary (F6) without any frame showing it. + +**A boundary output that never committed holds nothing (2026-10-05, maintainer ruling — F6's scope).** The lane holds through a `Loading` showing content because the boundary's output is on screen: it forwards its tree's pending, and its readers hold through it (A33, the lane-membership ruling). An output that never committed — a mount the frame has not revealed, such as one an action withdraws and restores in the same tick — shows nothing and holds nothing. Its readers wait with the mount's own transaction (A29's creation-time form), not with a lane reading through it. Only an output whose status is exactly pending is a frame reader: an uninitialized output, or a pending and errored one, holds nothing. Counting the never-committed output stranded the lane (the action's last write never published — fuzzer seed 91501 `latest` #138). Pinned: `tests/boundary-output-frame-reader.test.ts` (the withdraw + restore shape; withdraw without restore; an action failing after staging the withdraw, with and without the restore, leaves nothing held). + **History (superseded mechanism, kept verbatim).** The 2026-07-07b mechanical model — replaced by the 2026-09-09 supersession, which the statement now leads with; take the mechanism from the statement, not from here: Mechanically: authoritative values arriving under an active override hold in `_pendingValue` like any other transition write and **elevate to `_value` only at their transition's commit** (`_value` changes at commit points, period); the elevation is unobservable under the override mask (A17); reverting is a pure drop — there is no revert target and reverts commit nothing. This supersedes the earlier "bound to its own async source, not its transition" formulation, which was implemented by escaping the transition commit (revert-target commit at revert) and allowed a mid-flight arrival to reveal before its own transition completed. ### A28. A write becomes visible at flush — to every channel diff --git a/packages/signals/src/core/scheduler.ts b/packages/signals/src/core/scheduler.ts index caa27c767..f47e334de 100644 --- a/packages/signals/src/core/scheduler.ts +++ b/packages/signals/src/core/scheduler.ts @@ -4,6 +4,7 @@ import { CONFIG_GUESS, CONFIG_INPUTS_PUBLISHED, CONFIG_OVERRIDE, + CONFIG_REDERIVE, CONFIG_STAGED, CONFIG_VERDICT, EFFECT_RENDER, @@ -313,9 +314,16 @@ function blockedBy(nodes: Signal[], owner: Transaction, own = false): boole // `frameRead`): served committed instead of going pending, it derives // from the flight all the same and is re-derived at the landing — the // frame's observation survives its reader becoming a lane's (a guess - // over a held window, V5/A17). A probe alone does not hold. + // over a held window, V5/A17). A probe alone does not hold. A + // boundary showing content forwards the pending (its output pending, + // CONFIG_REDERIVE): transparent, its readers hold through it (A33; the + // lane-membership ruling, 2026-10-05). An output that never committed + // shows nothing — a mount the frame has not revealed — and holds + // nothing. if ( - ((r as any)._type === EFFECT_RENDER || r._flags & REACTIVE_FRAME_READ) && + ((r as any)._type === EFFECT_RENDER || + r._flags & REACTIVE_FRAME_READ || + (r._config & CONFIG_REDERIVE && r._statusFlags === STATUS_PENDING)) && (s._gen === r._depGen || r._x?._error != null) && !(r._flags & REACTIVE_DISPOSED) && onScreen(r, judge ?? owner) diff --git a/packages/signals/tests/boundary-output-frame-reader.test.ts b/packages/signals/tests/boundary-output-frame-reader.test.ts new file mode 100644 index 000000000..877879214 --- /dev/null +++ b/packages/signals/tests/boundary-output-frame-reader.test.ts @@ -0,0 +1,134 @@ +// F6 (fuzz-findings-l2.test.ts) counts a boundary output forwarding its +// tree's pending as a frame reader of the lane. Its limits: an output that +// never committed (a mount the action staged, never shown) holds nothing — +// counting it strands the lane and the action's last write never publishes +// (fuzzer seed 91501 `latest` #138); a withdrawn mount's output is a zombie +// whose removal the action stages (A15); and nothing outlives a failed action. +import { expect, it } from "vitest"; +import { + action, + createLoadingBoundary, + createMemo, + createRenderEffect, + createRoot, + createSignal, + flush, + latest, + onCleanup +} from "../src/index.js"; + +const tick = () => new Promise(r => setTimeout(r, 0)); +async function drain(n = 4) { + for (let i = 0; i < n; i++) { + await tick(); + flush(); + } +} + +function setup(fail = false) { + const [s, setS] = createSignal(0); + const [mounted, setMounted] = createSignal(true); + const view = { out: "absent" as unknown }; + let resume!: () => void; + let run!: () => Promise; + let dispose!: () => void; + createRoot(d => { + dispose = d; + const node0 = createMemo(() => Promise.resolve(latest(s))); + createRenderEffect( + () => + mounted() + ? createRoot(dd => { + const b = createLoadingBoundary( + () => node0(), + () => "loading" + ); + createRenderEffect(b, v => { + view.out = v; + }); + onCleanup(() => { + view.out = "absent"; + }); + return dd; + }) + : undefined, + dd => { + if (dd) onCleanup(dd); + } + ); + run = action(function* () { + setS(0); + yield new Promise(r => (resume = r)); + if (fail) throw new Error("cancelled"); + setS(1); + }); + }); + return { s, setS, setMounted, view, run, resume: () => resume(), dispose }; +} + +it("an action's mount withdraw + restore over a Loading forwarding latest() completes", async () => { + const t = setup(); + flush(); + await drain(); + const p = t.run(); + t.setMounted(false); + flush(); + await drain(); + t.setMounted(true); + flush(); + await drain(); + t.resume(); + await p; + await drain(8); + expect([t.view.out, t.s()]).toEqual([1, 1]); + t.dispose(); +}); + +it("an action's mount withdraw, not restored, completes with the mount gone", async () => { + const t = setup(); + flush(); + await drain(); + const p = t.run(); + t.setMounted(false); + flush(); + await drain(); + t.resume(); + await p; + await drain(8); + expect([t.view.out, t.s()]).toEqual(["absent", 1]); + t.setMounted(true); + flush(); + await drain(8); + expect([t.view.out, t.s()]).toEqual([1, 1]); + t.dispose(); +}); + +for (const restore of [false, true]) { + it(`an action failing after staging a mount withdraw${restore ? " + restore" : ""} leaves nothing held`, async () => { + const t = setup(true); + flush(); + await drain(); + const p = t.run().catch(e => e); + t.setMounted(false); + flush(); + await drain(); + if (restore) { + t.setMounted(true); + flush(); + await drain(); + } + t.resume(); + expect(await p).toBeInstanceOf(Error); + await drain(8); + t.setMounted(true); + t.setS(7); + flush(); + await drain(8); + expect([t.view.out, t.s()]).toEqual([7, 7]); + t.setMounted(false); + flush(); + await drain(8); + expect(t.view.out).toBe("absent"); + t.dispose(); + }); +} diff --git a/packages/signals/tests/fuzz-findings-l2.test.ts b/packages/signals/tests/fuzz-findings-l2.test.ts index 6c219dea4..5f7a1b53c 100644 --- a/packages/signals/tests/fuzz-findings-l2.test.ts +++ b/packages/signals/tests/fuzz-findings-l2.test.ts @@ -505,63 +505,65 @@ describe("fuzz findings on L2 — lanes", () => { // boundary's tree is the subscriber and the blocker predicate sees no frame // reader: the lane is judged unblocked, `latest=1` shows beside // `details=0`, and when `details(1)` lands it never displays — the stale - // frame stays until the action ends. - it.fails( - "F6: a retaining boundary between a lane's derivation and its reader does not unblock the lane (A17)", - async () => { - const [$id, setId] = createSignal(0); - const gates = new Map void>(); - let resume!: () => void; - let run!: () => Promise; - let L = 0; - let D: unknown = "?"; - const frames: string[] = []; - let dispose!: () => void; - createRoot(d => { - dispose = d; - const details = createMemo(() => { - const id = latest($id); - return gated(gates, `d:${id}`, id); - }); - createRenderEffect( - () => latest($id), - v => { - L = v; - frames.push(`L=${L} D=${D}`); - } - ); - const view = createLoadingBoundary(details, () => "loading" as const); - createRenderEffect(view, v => { - D = v; + // frame stays until the action ends. A33/B5 and the lane-membership ruling + // (2026-10-05, reading A): a boundary showing content is transparent — the + // lane holds through it. A boundary output forwarding its tree's pending + // (CONFIG_REDERIVE, itself pending) now counts as a frame reader in + // `blockedBy`'s one hop over the pending tree; one showing its fallback is + // not pending and still holds nothing. + it("F6: a lane holds through a Loading boundary showing content, revealing with the derivation under it (A17, A33)", async () => { + const [$id, setId] = createSignal(0); + const gates = new Map void>(); + let resume!: () => void; + let run!: () => Promise; + let L = 0; + let D: unknown = "?"; + const frames: string[] = []; + let dispose!: () => void; + createRoot(d => { + dispose = d; + const details = createMemo(() => { + const id = latest($id); + return gated(gates, `d:${id}`, id); + }); + createRenderEffect( + () => latest($id), + v => { + L = v; frames.push(`L=${L} D=${D}`); + } + ); + const view = createLoadingBoundary(details, () => "loading" as const); + createRenderEffect(view, v => { + D = v; + frames.push(`L=${L} D=${D}`); + }); + run = action(function* () { + setId(1); + yield new Promise(r => { + resume = r; }); - run = action(function* () { - setId(1); - yield new Promise(r => { - resume = r; - }); - setId(2); - }); + setId(2); }); - flush(); - gates.get("d:0")!(); - await drain(); - expect([L, D]).toEqual([0, 0]); - const p = run(); - await drain(); - // The lane is held by details' flight: no frame shows latest=1 alone. - expect([L, D]).toEqual([0, 0]); - gates.get("d:1")!(); - await drain(); - // The derivation landed: the lane reveals both. - expect([L, D]).toEqual([1, 1]); - resume(); - gates.get("d:2")?.(); - await p; - await drain(); - dispose(); - } - ); + }); + flush(); + gates.get("d:0")!(); + await drain(); + expect([L, D]).toEqual([0, 0]); + const p = run(); + await drain(); + // The lane is held by details' flight: no frame shows latest=1 alone. + expect([L, D]).toEqual([0, 0]); + gates.get("d:1")!(); + await drain(); + // The derivation landed: the lane reveals both. + expect([L, D]).toEqual([1, 1]); + resume(); + gates.get("d:2")?.(); + await p; + await drain(); + dispose(); + }); // F8. A15 lanes corollary (#3460): "a render effect OFF the lane that reads // what the lane is revealing … shows the committed value, publishes now, diff --git a/packages/signals/tests/spec-async-semantics.test.ts b/packages/signals/tests/spec-async-semantics.test.ts index d2913df83..658511bb3 100644 --- a/packages/signals/tests/spec-async-semantics.test.ts +++ b/packages/signals/tests/spec-async-semantics.test.ts @@ -1068,7 +1068,7 @@ describe("A18 (was B4): an override's lifetime is bound to its own async source, return g; } - it("differing arrival: downstream async restarts from the truth immediately; screen and untracked reads keep the override until commit", async () => { + it("differing arrival: downstream async restarts from the truth immediately; screen and untracked reads keep what showed until commit", async () => { const g = await primed(); g.setValue(1); @@ -1086,7 +1086,11 @@ describe("A18 (was B4): an override's lifetime is bound to its own async source, await settle(); expect(g.flights.map(f => f.n)).toEqual([3, 2]); expect(latest(g.double)).toBe(2); // the arrived truth - expect(g.double()).toBe(3); // untracked read: still the displayed override + // Untracked read: what the screen shows. The guess never revealed (the + // lane held on its downstream flight through the boundary), so it is + // void at the correction and the committed value stays (A18 + // amendment, 2026-10-05: correction display is what showed). + expect(g.double()).toBe(0); expect(isPending(g.double)).toBe(true); // displayed ≠ final expect(g.log).toEqual([]); @@ -1094,7 +1098,7 @@ describe("A18 (was B4): an override's lifetime is bound to its own async source, g.flights.shift()!.resolve(); await settle(); expect(g.log).toEqual([]); - expect(g.double()).toBe(3); + expect(g.double()).toBe(0); // The 2-flight lands: the transaction commits, the optimism is gone. g.flights.shift()!.resolve(); @@ -1172,7 +1176,7 @@ describe("A18 (was B4): an override's lifetime is bound to its own async source, await settle(); expect(flights.map(f => f.n)).toEqual([3, 2]); expect(latest(double)).toBe(2); - expect(double()).toBe(3); + expect(double()).toBe(0); // the never-shown guess is void (A18 amendment, 2026-10-05) expect(isPending(double)).toBe(true); expect(log).toEqual([]); diff --git a/packages/signals/tests/store/lane-authority-twins.test.ts b/packages/signals/tests/store/lane-authority-twins.test.ts index 63ea3fa44..72e0d635f 100644 --- a/packages/signals/tests/store/lane-authority-twins.test.ts +++ b/packages/signals/tests/store/lane-authority-twins.test.ts @@ -213,14 +213,16 @@ describe("store twins of the lane-authority fixes (#3335/#3334/#3330/#3331)", () await settle(); expect(flights.map(f => f.n)).toEqual([3, 2]); // graph moves to 2 NOW expect(latest(() => state.d)).toBe(2); - expect(state.d).toBe(3); + // The guess never showed (held on its downstream flight through the + // boundary): void at the correction (A18 amendment, 2026-10-05). + expect(state.d).toBe(0); expect(isPending(() => state.d)).toBe(true); expect(log).toEqual([]); flights.shift()!.resolve(); // superseded 3-flight: nothing await settle(); expect(log).toEqual([]); - expect(state.d).toBe(3); + expect(state.d).toBe(0); flights.shift()!.resolve(); await settle();