From 2faea69f48b3c15daff5404d0cf670ab1b60b474 Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Mon, 5 Oct 2026 22:50:03 -0700 Subject: [PATCH] =?UTF-8?q?docs(signals):=20the=20direction=20rule=20?= =?UTF-8?q?=E2=80=94=20a=20hold=20never=20waits=20on=20work=20that=20has?= =?UTF-8?q?=20never=20committed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A dated SPEC amendment (2026-10-06) naming the rule the create-time fixes each implement by checking commit status: the loading-source rule, #3814 (#3802), and the pending #3800 and in-flush #3540 fixes. The same-tick and in-flush shapes are recorded as not yet one-way and pinned it.fails in tests/direction-rule-probe.test.ts; the boundary shape passes today. Cross-references in INTERNALS-ASYNC-STATE.md §0 and the carve plan. Co-authored-by: Claude Co-authored-by: Cursor --- .../plans/size-reduction-carve-step1.md | 2 + .../signals/docs/INTERNALS-ASYNC-STATE.md | 2 +- packages/signals/docs/RULES-INDEX.md | 96 +++++++------- packages/signals/docs/SPEC-ASYNC-SEMANTICS.md | 14 ++ .../tests/direction-rule-probe.test.ts | 121 ++++++++++++++++++ 5 files changed, 186 insertions(+), 49 deletions(-) create mode 100644 packages/signals/tests/direction-rule-probe.test.ts diff --git a/documentation/plans/size-reduction-carve-step1.md b/documentation/plans/size-reduction-carve-step1.md index e2fb2c4d8..01c267d1b 100644 --- a/documentation/plans/size-reduction-carve-step1.md +++ b/documentation/plans/size-reduction-carve-step1.md @@ -956,6 +956,8 @@ A34 items are the next step's. node _status_ rather than by a join: a pending uninitialized node with no observer. If a boundary step wants loading sources held for a fallback, this is the hook. + (2026-10-06: an instance of the direction rule — a hold never waits on + work that has never committed; SPEC, "The hold model — L2".) --- diff --git a/packages/signals/docs/INTERNALS-ASYNC-STATE.md b/packages/signals/docs/INTERNALS-ASYNC-STATE.md index 2b26cd324..63e988723 100644 --- a/packages/signals/docs/INTERNALS-ASYNC-STATE.md +++ b/packages/signals/docs/INTERNALS-ASYNC-STATE.md @@ -17,7 +17,7 @@ ruling. Anything marked `[ruled]` cites the decision. **State.** A node is `_value` (committed) or `_pendingValue` (staged for this flush). Cold state lives on the node's extension `_x`: `_transaction` (the `Transaction` holding it, `CONFIG_HELD`), `_lane` (a lane's value for it, `CONFIG_OVERRIDE`; `CONFIG_GUESS` for a written guess), `_q` (the question a flight answers — A18 provenance), `_reask` (a quiet re-ask's classification, A19 exc. 2), `_marks` (`affects()` count), `_pendingSources`/`_inFlight`/`_error` (the async core, unchanged). Per-pass flags on `_flags`: `REACTIVE_JOINED` (the pass read the future), `REACTIVE_FRAME_READ` (a stale reader, re-derived at the landing), `REACTIVE_STAGED_READ` (the pass read this flush's staging as the screen), `REACTIVE_LANE_READ`/`REACTIVE_SCREEN_READ` (lane work), `REACTIVE_PROBE_UNANSWERED` (a verdict probe answered before the flush had a transaction), `REACTIVE_REASK`, `REACTIVE_ZOMBIE` (a parked frame's child). -**Transaction** (`core/scheduler.ts`): `_nodes` (what it holds), `_reruns` (stale readers to re-derive at its landing), `_queues` (stashed effect runs), `_open` (actions running in it), `_lane`/`_parent`/`_verdict`/`_shown`/`_held` (lanes), `_into` (merged — `resolveTx`), `_marks`. The flush's seam (`settle`): run the heap; the lane corrections; `joinFuture` decided the flush's transaction (a pass went pending, a read of a held node joined); every staged node of the flush is held (`holdNode`) with its pass's frame (`holdFrame`) — a loading source is nobody's frame, a tick that nets to the committed value is no proposal (A34 (2)) — and the store holds the node-less stagings (`_storePark`); the lane seams; every transaction no frame waits on lands (`land`: `commitPendingNode` over `_nodes`, `reruns`, `releaseQueues`); the store folds (`_storeCommit`). `blocked(t)`: an open action, or a pending node it holds that a frame reader observes (`blockedBy`/`onScreen`), or a lane under it blocked. +**Transaction** (`core/scheduler.ts`): `_nodes` (what it holds), `_reruns` (stale readers to re-derive at its landing), `_queues` (stashed effect runs), `_open` (actions running in it), `_lane`/`_parent`/`_verdict`/`_shown`/`_held` (lanes), `_into` (merged — `resolveTx`), `_marks`. The flush's seam (`settle`): run the heap; the lane corrections; `joinFuture` decided the flush's transaction (a pass went pending, a read of a held node joined); every staged node of the flush is held (`holdNode`) with its pass's frame (`holdFrame`) — a loading source is nobody's frame, a tick that nets to the committed value is no proposal (A34 (2)) — and the store holds the node-less stagings (`_storePark`); the lane seams; every transaction no frame waits on lands (`land`: `commitPendingNode` over `_nodes`, `reruns`, `releaseQueues`); the store folds (`_storeCommit`). `blocked(t)`: an open action, or a pending node it holds that a frame reader observes (`blockedBy`/`onScreen`), or a lane under it blocked. Membership is one relation (`merge`), so the direction rule (SPEC "The hold model — L2", 2026-10-06: a hold never waits on work that has never committed) is implemented where a node's commit status is checked — the loading-source skip, the boundary scope, the born-held enqueue and landing — not by the transaction. **Read** (`core/core.ts`): the fast block for a plain signal; `frameRead` (a render effect, a verdict reader, lane work — the screen, `staleReader`); `joinPass` (a held node's staging — the pass is the transaction's); `stagedRead` (this flush's staging as the screen); a pending node served committed to a frame reader (`_observeFlight` — the verdict module's hook). `recompute` decides the pass's frame after the pass (`CONFIG_STAGED`, held children, zombies). diff --git a/packages/signals/docs/RULES-INDEX.md b/packages/signals/docs/RULES-INDEX.md index 7db08560a..3325f5743 100644 --- a/packages/signals/docs/RULES-INDEX.md +++ b/packages/signals/docs/RULES-INDEX.md @@ -45,66 +45,66 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul | id | status | defined | cited in src | cited in tests | statement (at definition) | |---|---|---|---|---|---| -| A1 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:293` | — | 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:301` | — | 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:309` | — | 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:317` | — | 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:325` | — | 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:333` | 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:139` | — | 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:147` | — | 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:155` | 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:163` | 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:81` | — | 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:171` | — | 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:179` | — | 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:187` | — | 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:223` | 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:195` | 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:53` | async.ts×3 constants.ts×1 lanes.ts×5 scheduler.ts×2 verdict.ts×1 map.ts×1 store.ts×3 | fuzz-findings-l2.test.ts×4 lane-uninitialized-landing-3648.test.ts×5 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:63` | 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:123` | 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:382` | — | 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:389` | — | 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:203` | 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:213` | — | 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:131` | 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:283` | 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:89` | — | 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:275` | — | 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:73` | 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:97` | 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:231` | async.ts×3 attribution.ts×1 constants.ts×1 core.ts×1 effect.ts×1 scheduler.ts×5 | async-landing-deps-3461.test.ts×3 fuzz-findings-l2.test.ts×2 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:105` | boundaries.ts×1 core.ts×1 verdict.ts×2 | fuzz-findings-l2.test.ts×5 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:113` | 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:247` | 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:257` | constants.ts×1 core.ts×6 lanes.ts×2 scheduler.ts×2 store.ts×4 | a34-writes-then-derivations.test.ts×2 createMemo.test.ts×1 derived-write-then-derivation-3733.test.ts×1 finalize-reentry.test.ts×2 fuzz-findings-l2.test.ts×5 held-derivation-not-a-proposal-3612.test.ts×6 optimistic-list-mutation-matrix.test.ts×1 unchanged-presence-no-hold-3743.test.ts×3 woken-transaction-adopts-staged-bump.test.ts×1 transition-corpse-revival.test.ts×1 treeshake.test.ts×2 visibility-oracle.states.ts×2 write-proposals-3494.test.ts×5 | [ruled 2026-09-16 (#3494)] A write is a proposal: one on a held node entangles its tick; one that nets to the committed value is none — A write proposes a value for a node. **Held, both are suggestion… | +| A1 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:307` | — | onCleanup.test.ts×2 transitionEntanglement.test.ts×4 | [ruled 2026-07-06] Effect error interception is compute-phase only — `EffectBundle.error` intercepts compute-phase errors only; effect-phase throws escalate to the nearest error boundary (halt if none… | +| A2 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:315` | — | onCleanup.test.ts×2 | [ruled] Unhandled compute-phase errors in user effects are logged and skipped — Compute-phase errors in _user_ effects without a handler are logged and the run is skipped; the system keeps running. | +| A3 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:323` | — | derived-write-then-derivation-3733.test.ts×5 equals-comparator-errors.test.ts×1 | [ruled] Comparator throws are compute-phase errors — Errors thrown by a user `equals` comparator behave exactly like compute-phase errors (boundary-containable; loud halt without a boundary). | +| A4 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:331` | — | derived-write-then-derivation-3733.test.ts×1 equals-comparator-errors.test.ts×1 | [ruled] A custom `equals` never sees `undefined` prev on first commit — A custom `equals` is never invoked with `undefined` previous value on a node's first commit. | +| A5 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:339` | — | derived-write-then-derivation-3733.test.ts×1 errorHalt.test.ts×1 | [ruled] An error escaping every boundary halts the system — An error escaping every boundary permanently halts the system with `REACTIVITY_HALTED`; later writes log "Update ignored". | +| A6 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:347` | boundaries.ts×2 | derived-write-then-derivation-3733.test.ts×1 enforceLoadingBoundary.test.ts×1 | [ruled] `ASYNC_OUTSIDE_LOADING_BOUNDARY` is warn-only — `ASYNC_OUTSIDE_LOADING_BOUNDARY` is a warn-only diagnostic; an `Errored` above must not swallow it and must not show its fallback for a pending. | +| A7 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:153` | — | fuzz-findings-l2.test.ts×1 spec-async-semantics.test.ts×2 visibility-oracle-store.states.ts×1 visibility-oracle.states.ts×1 visibility-oracle.test.ts×1 | [ruled, amended in place] Resolved async never reads `[false, undefined]` — After an async memo resolves, `[isPending(x), latest(x)]` is `[false, resolvedValue]` — never `[false, undefined]`. **Amende… | +| A8 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:161` | — | createMemo.test.ts×1 visibility-oracle-store.states.ts×1 visibility-oracle.states.ts×2 | [ruled, amended in place 2026-07-07] `isPending(() => latest(x))` follows `x`'s own async only — verdicts are per-channel — (**re-ruled 2026-07-07c** — was "tracks the transition the same as `isPendin… | +| A9 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:169` | projection.ts×1 target.ts×1 | spec-async-semantics.test.ts×3 visibility-oracle-store.states.ts×4 visibility-oracle-store.test.ts×1 | [ruled, amended in place 2026-07-07] Store leaves behind a firewall report the firewall's new-question refetch — `isPending` on a store leaf behind a firewall reports the firewall's refetch like any a… | +| A10 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:177` | core.ts×3 verdict.ts×2 | createMemo.test.ts×1 fuzz-findings-l2.test.ts×1 ispending-memo-unstamped-hold-3457.test.ts×2 latest-isPending-consistency.test.ts×3 verdict-contract.test.ts×2 | [ruled] `[isPending(x), x()]` is atomic within one scope — `[isPending(x), x()]` read in one scope is atomic: a reader that observed the fresh value must not see `pending === true` for it. | +| A11 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:95` | — | latest-isPending-consistency.test.ts×1 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×1 | [ruled] Sync derivations of held sources are visible through `latest()`/`isPending()` — Sync derivations of transition-held sources are visible through `latest()`/`isPending()` (held sync recompute is… | +| A12 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:185` | — | createOptimistic.test.ts×2 spec-async-semantics.test.ts×1 | [ruled, amended in place] Resting optimistic nodes report pending like a plain memo — A resting optimistic node reports pending via exactly the causes a plain async memo does (A19) — a reverting optim… | +| A13 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:193` | — | spec-async-semantics.test.ts×7 | [ruled 2026-07-06 (promoted from B1)] Resting optimistic ≡ plain async memo at every checkpoint — (was B1) A resting optimistic node (no active override) is observationally identical to a plain async … | +| A14 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:201` | — | spec-async-semantics.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B2)] Companion nodes get child lanes that do not merge with the owner — (was B2) `isPending`/`latest` companion nodes get child lanes that do not mer… | +| A15 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:237` | async.ts×2 constants.ts×1 core.ts×3 lanes.ts×4 scheduler.ts×14 verdict.ts×3 | async-chain-supersession.test.ts×1 first-observer-stale-reader.test.ts×1 fuzz-findings-l2.test.ts×17 held-restore.test.ts×1 l2-contract.test.ts×7 lane-contract.test.ts×1 lane-hold-on-observation.test.ts×1 lane-outside-view.test.ts×1 lane-pass-stamped-effect-3662.test.ts×1 mount-over-foreign-hold-3761.test.ts×2 overlapping-flights.test.ts×3 posture-born-held-and-observation.test.ts×4 posture-store-parity.test.ts×2 reveal-carve-out.test.ts×2 shared-effect-no-entangle.test.ts×1 spec-async-semantics.test.ts×2 stale-read-uninitialized-cross-transition.test.ts×1 superseded-source-blocks-3462.test.ts×2 tick-scoped-pass-transaction.test.ts×1 treeshake.test.ts×4 visibility-oracle-store.states.ts×6 visibility-oracle.states.ts×7 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B3)] Transition entanglement is graph-driven; lanes settle as one reveal — (was B3) Transition entanglement is graph-driven: writes whose async work … | +| A16 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:209` | scheduler.ts×1 | spec-async-semantics.test.ts×1 strict-read-pending-store.test.ts×2 uninitialized-visibility.test.ts×1 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×2 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B5)] `isPending` never throws in untracked contexts — (was B5) `isPending` never throws in untracked contexts — thunks that throw real errors or read… | +| A17 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:67` | async.ts×3 constants.ts×1 lanes.ts×5 scheduler.ts×2 verdict.ts×1 map.ts×1 store.ts×3 | fuzz-findings-l2.test.ts×4 lane-uninitialized-landing-3648.test.ts×5 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 scheduler.ts×5 | async-landing-deps-3461.test.ts×3 fuzz-findings-l2.test.ts×2 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 verdict.ts×2 | fuzz-findings-l2.test.ts×5 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… | ## V — fixed violations | id | status | defined | cited in src | cited in tests | statement (at definition) | |---|---|---|---|---|---| -| V1 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:452` | — | spec-async-semantics.test.ts×7 | - **V1 (violated A13) — FIXED.** A _resting_ optimistic node reported | -| V2 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:462` | — | 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:468` | — | spec-async-semantics.test.ts×2 | - **V3 (violated A19) — FIXED.** After a reporter-less transition completed, | -| V4 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:475` | — | 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:487` | 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: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, | ## B — tier B | id | status | defined | cited in src | cited in tests | statement (at definition) | |---|---|---|---|---|---| -| B1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:179` | — | 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:187` | — | 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:223` | — | spec-async-semantics.test.ts×2 | PROMOTED → A15 (A15's section carries the ruling). | -| B4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:63` | — | 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:195` | boundaries.ts×1 | derived-write-then-derivation-3733.test.ts×1 fuzz-findings-l2.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A16 (A16's section carries the ruling). | +| B1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:193` | — | createRevealOrder.test.ts×16 derived-write-then-derivation-3733.test.ts×1 onCleanup.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A13 (A13's section carries the ruling). | +| B2 | live | `docs/SPEC-ASYNC-SEMANTICS.md:201` | — | createRevealOrder.test.ts×16 derived-write-then-derivation-3733.test.ts×1 onCleanup.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A14 (A14's section carries the ruling). | +| B3 | live | `docs/SPEC-ASYNC-SEMANTICS.md:237` | — | spec-async-semantics.test.ts×2 | PROMOTED → A15 (A15's section carries the ruling). | +| B4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:77` | — | derived-write-then-derivation-3733.test.ts×1 spec-async-semantics.test.ts×2 | PROMOTED → A18 (A18's section carries the ruling). | +| B5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:209` | boundaries.ts×1 | derived-write-then-derivation-3733.test.ts×1 fuzz-findings-l2.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A16 (A16's section carries the ruling). | ## C — tier C | id | status | defined | cited in src | cited in tests | statement (at definition) | |---|---|---|---|---|---| -| C1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:123` | — | 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:418` | — | 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:428` | — | — | - [x] **C3 — CLOSED by A19 (2026-07-07): early completion is by design.** | -| C4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:53` | — | spec-async-semantics.test.ts×1 | PROMOTED → A17 (A17's section carries the ruling). | +| C1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:137` | — | onCleanup.test.ts×2 spec-async-semantics.test.ts×1 | PROMOTED → A19 (A19's section carries the ruling). | +| C2 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:432` | — | onCleanup.test.ts×2 | - [x] **C2 — RULED (2026-07-07): reverts do not trump other live lanes.** A | +| C3 | closed | `docs/SPEC-ASYNC-SEMANTICS.md:442` | — | — | - [x] **C3 — CLOSED by A19 (2026-07-07): early completion is by design.** | +| C4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:67` | — | spec-async-semantics.test.ts×1 | PROMOTED → A17 (A17's section carries the ruling). | ## INV — invariants | id | status | defined | cited in src | cited in tests | statement (at definition) | diff --git a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md index 171524bf4..66ea0e230 100644 --- a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md +++ b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md @@ -48,6 +48,20 @@ The former Tier A table is these sections. Tier B/C, the fixed violations, and t **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 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: + +- **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 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`). + +**Not yet one-way (recorded, not ruled).** The frame of a tick is still one transaction (the tick ruling above stands), so two shapes keep the hold waiting on first loads it never needed: + +- **Same tick:** mounts created outside a flush in one tick — two mounts over the held value, or a mount and a sync reader — share the tick's frame. The frame joins the hold, and the hold waits on the mounts' first loads. +- **In a flush:** a mount whose first pass reads the hold inside a flush joins the hold with that flush's frame, and the hold waits on the mount's first load. + +The one-way reading of both (the hold lands at its release, the frame lands after) is the rule's full form. It needs a transaction-level relation (the frame _follows_ the hold) that was built and measured on 2026-10-05 and not adopted: about +270 B minified over the local fixes in every size scenario, replacing only #3800's encoding. Pinned as known failures: `tests/direction-rule-probe.test.ts` — the same-tick and in-flush shapes are `it.fails`, and the boundary shape, one-way today, is a passing test. + ## Reads and visibility — what a read serves ### A17. An active override is the displayed value until its transaction commits, and the graph's value until its own source answers diff --git a/packages/signals/tests/direction-rule-probe.test.ts b/packages/signals/tests/direction-rule-probe.test.ts new file mode 100644 index 000000000..a0df1af9d --- /dev/null +++ b/packages/signals/tests/direction-rule-probe.test.ts @@ -0,0 +1,121 @@ +/** + * The direction rule (SPEC "The hold model — L2", amendment 2026-10-06): a + * hold never waits on work that has never committed. Each shape below + * mounts a first load (`page`, an async memo over `user`) while an action + * holds `user = "bob"`; the hold's own reader (`header`) must show "bob" at + * the action's release, never waiting on the pages' first loads. + * + * The same-tick and in-flush shapes are recorded by the amendment as not yet + * one-way — the frame of a tick is one transaction, so the hold waits with + * the mount — and are pinned `it.fails` until a ruling makes them one-way. + * The boundary shape is one-way today (#3540's boundary scope). + */ +import { expect, it } from "vitest"; +import { + action, + createLoadingBoundary, + createMemo, + createRenderEffect, + createRoot, + createSignal, + flush, + untrack +} from "../src/index.js"; + +const tick = async () => { + for (let i = 0; i < 20; i++) await Promise.resolve(); + flush(); +}; + +type Shape = "two-mounts-outside" | "one-mount-then-sync-reader" | "in-flush" | "in-flush-boundary"; + +/** The full log, and what showed at the action's release. */ +async function run(shape: Shape) { + const [user, setUser] = createSignal("ann"); + const [open, setOpen] = createSignal(false); + const log: string[] = []; + const gates: Array<() => void> = []; + const page = () => + createMemo(async () => { + const u = user(); + await new Promise(r => gates.push(r)); + return `page-${u}`; + }); + createRoot(() => { + createRenderEffect(user, u => void log.push(`header ${u}`)); + if (shape === "in-flush" || shape === "in-flush-boundary") + createRenderEffect( + () => { + if (!open()) return; + if (shape === "in-flush-boundary") { + const view = untrack(() => + createLoadingBoundary( + () => page()(), + () => "fallback" + ) + ); + createRenderEffect(view, x => void log.push(`view ${x}`)); + } else createRenderEffect(page(), x => void log.push(`page ${x}`)); + }, + () => {} + ); + }); + flush(); + let release!: () => void; + action(function* () { + setUser("bob"); + yield new Promise(r => (release = r)); + })(); + flush(); + if (shape === "two-mounts-outside") + for (const n of [1, 2]) + createRoot(() => createRenderEffect(page(), x => void log.push(`page${n} ${x}`))); + else if (shape === "one-mount-then-sync-reader") { + createRoot(() => createRenderEffect(page(), x => void log.push(`page ${x}`))); + createRoot(() => { + const m = createMemo(() => user().toUpperCase()); + createRenderEffect(m, x => void log.push(`sync ${x}`)); + }); + } else setOpen(true); + flush(); + const beforeRelease = log.length; + release(); + await tick(); + const atRelease = log.slice(beforeRelease); + gates.splice(0).forEach(g => g()); + await tick(); + return { log, atRelease }; +} + +it.fails( + "same tick, two mounts outside a flush: the hold lands at its release, before the pages' first loads", + async () => { + const r = await run("two-mounts-outside"); + expect(r.atRelease).toEqual(["header bob"]); + expect(r.log).toEqual(expect.arrayContaining(["page1 page-bob", "page2 page-bob"])); + } +); + +it.fails( + "same tick, a mount then a sync reader: the hold lands at its release, before the page's first load", + async () => { + const r = await run("one-mount-then-sync-reader"); + expect(r.atRelease).toEqual(["header bob"]); + expect(r.log).toEqual(expect.arrayContaining(["page page-bob", "sync BOB"])); + } +); + +it.fails( + "in a flush that joined the hold: the hold lands at its release, before the mount's first load", + async () => { + const r = await run("in-flush"); + expect(r.atRelease).toEqual(["header bob"]); + expect(r.log).toEqual(expect.arrayContaining(["page page-bob"])); + } +); + +it("in a flush, under a Loading that has not shown content: the hold lands at its release; the boundary shows its fallback meanwhile", async () => { + const r = await run("in-flush-boundary"); + expect(r.log.slice(0, 2)).toEqual(["header ann", "view fallback"]); + expect(r.atRelease).toEqual(["header bob"]); +});