diff --git a/.changeset/write-proposals-3494.md b/.changeset/write-proposals-3494.md new file mode 100644 index 000000000..df405ad7b --- /dev/null +++ b/.changeset/write-proposals-3494.md @@ -0,0 +1,5 @@ +--- +"@solidjs/signals": patch +--- + +A write is a proposal (A34, #3494). A mainline write to a node a transaction holds — the same value again or another — is a second proposal for the same slot: the writer's tick joins the hold at the next flush's start and reveals with it (`setA(1); setB(1)` with `b=1` held holds A with B again; #3473 had dropped the entry, and with it the grouping, while fixing its `activeTransition` leak — the entry is now deferred instead). A tick whose writes net to the committed value proposed nothing: the node is not staged, not stamped, and not pending (`setShow(false); setShow(true)` beside a held `setCount(1)` no longer reads `isPending(show)` true nor captures the later `setShow(false)` into the hold — the hide that used to be lost). Fixes the torn `[1, 0, 1]` effect input from #3473's review. Also: a stale reader served a held memo's committed value counts as observing the memo's flight (a reveal in the flush that retires the flight's last reader holds the transaction — `Count: 1` no longer publishes beside a visible `Copy: 0`), and a `latest()` shadow backfilled under a transaction never blocks its settle (removing the last reader releases the held write at once). From differential fuzzing of the ruling and review: a source going pending behind a memo's kept dependency tail now re-derives the memo instead of marking it pending (A30 amendment) — a reader that stopped reading a memo is no longer registered on the memo's next flight (which held an action's truth on a fetch nobody displayed), and a memo whose committed frame still derives from the source no longer publishes stale beside the source's new inputs (`1 0` with `selected` derived from `remote(0)`); the no-proposal drop applies to unstamped nodes only — a held proposal rewritten to the committed value stays its transaction's, so the authoritative value that follows commits instead of being skipped as another transaction's — and covers writable memos (`createSignal(fn)`) as it covers signals; a same-value write to a held node schedules its own flush, so the join never leaks into a later, unrelated tick; and the join drain runs inside the flush's guard, so a throwing comparator cannot wedge the scheduler. diff --git a/packages/signals/docs/INTERNALS-ASYNC-STATE.md b/packages/signals/docs/INTERNALS-ASYNC-STATE.md index bf09f21b0..6ade22625 100644 --- a/packages/signals/docs/INTERNALS-ASYNC-STATE.md +++ b/packages/signals/docs/INTERNALS-ASYNC-STATE.md @@ -108,6 +108,9 @@ Semantics of the `(_pendingValue, _overrideValue)` pair for an optimistic node - `transitionComplete`: prunes dead reporters (`reporterBlocksSource`), transition is done when no live reporter still blocks a source that is **still pending** (a non-empty `_pendingSources`) and no active-override node is blocked on someone else's async. Judged by the set, not `_error.source` (#3375: a later-pending input overwrites it on propagation while the flight is still in the air) and not the self entry alone (#3462: an upstream re-ask that supersedes the source's own flight retires that entry and leaves the source pending on the re-ask — its reader still cannot render, and the landing folds the transaction in; judged complete instead, a re-entry before the landing, such as a repeated write to the held signal, committed the held writes beside the reader's stale frame). **Zombies (#3463):** a reporter with `REACTIVE_ZOMBIE` — its owner's pass replaced it, its disposal staged in a live transaction (held children, #3404) — is still on screen and is live for every hold but one: `reporterBlocksSource(reporter, source, verdict)` walks the zombie's `_parent` chain to the first non-zombie owner and resolves the transaction staging its removal (the owner's `_transition`, or `activeTransition` for a `CONFIG_HELD_CHILDREN` owner); the zombie is moot only when that transaction is the one being judged (`verdict`) — done, and the commit disposes it; not done, and it stays parked regardless — or is already done. `sourceObserved(transition, source, verdict?)` passes `verdict ? transition : null`, and keeps (rather than prunes) a zombie the verdict passed over: moot for this verdict, it still holds a lane's reveal while the transaction stays parked on something else. Before, a zombie counted as disposed everywhere, and the lane revealed `Value: 1` beside the zombie's `Details: 0`. Pinned: `tests/lane-outside-view.test.ts` (#3463). - Fallback-caught async holds nothing — in both orders (A33; ruled 2026-09-12, #3375). A collecting boundary consumes the notification, so a reader under a fallback never registers. A reader registered while its boundary showed content (forwarded) stays registered when the boundary's `on` changes and it flips to the fallback; `reporterBlocksSource` therefore walks the reporter's `_queue._parent` chain and treats a reporter behind a collecting pending-type boundary (`_collectionType & STATUS_PENDING && !_initialized`) as not live. If nothing outside the boundary consumes the flight, the hold is over; a reader outside it still holds. The reset itself calls `wakeParked()` so the re-judgement happens in the same drain. The hold moves onto the boundary, not off the screen (#3459): the reset also collects, from every live transaction's `_asyncReporters` (INV-3, the one record of a forwarded reader), the sources of each reporter it routes — `_holds`: under this queue with no collecting pending-type boundary between — plus that reporter's `_pendingSources`, and flips to the fallback if it found any. A forwarded reader already pending never re-notifies (status propagation dedupes on its `_pendingSources`), so without this a sibling reader's fresh flight was the only source collected, and its landing revealed the still-flying one stale (`B: 1 | Fast: 1 | Slow: 0`). Pinned: `tests/loading-reset-collects-forwarded-3459.test.ts`. - Wake of parked transactions (`wokenTransitions`): the flush judges only the _active_ transaction; a parked one is re-entered by a stamped node's landing (`settleTransition`), a stamped recompute, or an action resuming. A reporter that stops counting for another reason — its boundary reset (above), or its disposal by ambient work (#3372: `disposeChildren(self)` on a node with `_transition` and `STATUS_PENDING`; a pending reader is always queued as a pending node, so the stamp is reliable) — is none of those: `reporterBlocksSource` would prune it at the next check, but no check comes, and the writes held with it stay staged. Such sites record the transaction (deduped) and schedule; the flush re-enters a woken transaction from the `finally` of a full pass — reached from the park exit and the normal exit alike — and only when idle: no `activeTransition` and `!scheduled`, which at that point means an empty dirty heap, no write since the heap ran (every write re-arms it) and, the finalize having reverted them, no optimistic ambient nodes. Entering adopts the ambient batch, and ambient work present at that instant would be held behind flights it never read; a wake in a pass with work just falls to the next. Entries are popped in a loop until one enters: a wake whose transaction completed by other means is a bare return (`initTransition` on `_done`) and must not strand the ones behind it. The fast drain defers to the full path while a wake is outstanding so such dead entries are still consumed. A wake with other live reporters re-parks; the idle pass is its only cost. Known shape: the ambient write that triggered the reset commits in its own pass and the released hold in the idle pass after it — two effect runs in one synchronous drain (`Sum: 1`, `Sum: 2` at the same clock time in the #3375 pin), never a visible tear. +- A write proposes (A34, #3494). Two mechanisms. **Join at flush (`batchJoins`, scheduler.ts):** `setSignal` on a node stamped by a transaction that is not active enters it at once inside a flush; from mainline it pushes the transaction to `batchJoins` (dupes are harmless: `initTransition` on the active transaction returns) and calls `schedule()` — the write may be a repeat that leaves through the equality gate without scheduling, and a join left for a later flush adopted that flush's unrelated tick (#3519 review); `flush()` enters each at its start — after the companion re-sync, inside the `try` (the adoption runs user comparators; a throw must not leave `_running` set), before the heap — adopting the tick's ambient batch; the fast sync path (`canUseSimpleSyncFlush`) is bypassed while a join waits, so the whole tick reveals with the hold ("both are suggesting a value"). Recorded before `setSignal`'s equality gate: a repeat of the held value is a proposal too, and had no other route in (a differing value re-asks the flight, which the adoption already held). #3473 had removed the entry from mainline because it set `activeTransition` for the rest of the caller's block — a memo created after the write was born the transaction's (A29) — and thereby dropped the grouping; deferring the entry keeps mainline code between the write and the flush mainline. **No proposal (adoption loop of `initTransition`):** an unstamped (`_transition === null`) signal (`!_fn`), or an initialized writable memo under `REACTIVE_MANUAL_WRITE` (`createSignal(fn)`'s setter routes through `setMemo → setSignal`; parity, #3519 review — a computed's other stagings are its pass's result and may equal an uninitialized `undefined`), whose staged value equals its committed one (`_equals`) is unstaged through `commitPendingNode` with `_pendingValue` cleared first (the commit's own cleanup: companions snapped, the manual-write flag dropped) and neither stamped nor pushed to the transaction — its subscribers were walked at the write and re-derive the same value; its companions need no snap, the flush-start re-sync (A28) already read the equality through `computePendingState`. `computePendingState` gates its "staged, therefore pending" arm on the same inequality (a staged value equal to the committed one is final). Before, `setShow(false); setShow(true); setCount(1)` stamped `show` into `count`'s hold with nothing to reveal: `isPending(show)` read true for the hold's life, and the later `setShow(false)` — the tick above routes it to the stamp — was held and then lost (gabbev's coalesced-toggle case). Pinned: `tests/write-proposals-3494.test.ts`. The unstamped guard (fuzzer latest-2 #1470, S3): a parked transaction's batch folds into a merge through this same loop, and its nodes carry FLUSHED proposals — an action's `1` then `0` on a committed `0` is a held node, not a fresh tick's no-op. Dropped, the node kept its dead stamp and was pushed nowhere; the authoritative `1` that followed queued under the merged transaction as another transaction's node, and `commitPendingNodes` skipped it — readers revealed `1` beside a source anchor stuck at `0`. +- A pending mark over a kept-tail link re-derives its subscriber (A30 amendment, #3494 review; fuzzer latest-1 #2141, O2; #3519 review; fuzzer branches-1 #1105): `notifyStatus`'s `forEachDependent` callback, for `STATUS_PENDING` when `link._gen !== sub._depGen` — the link was not (re)validated by the subscriber's current pass (`link()` stamps the pass generation on every link in the `[deps.._depsTail]` prefix), i.e. it lies in the tail A30 keeps (a staged or unchanged pass) or, mid-pass, has not been re-read yet — calls `enqueueSub(sub); schedule()` and returns: no `_pendingSources` entry, no `initTransition(sub._transition)` (the A15 held-memo entanglement arm), no downstream `notifyStatus`. O(1). Clears and errors (`STATUS_NONE`/`STATUS_ERROR`) still ride every link. The recompute decides: reading the pending dep registers the node through its own read; reading a held input enters that transaction (A29); reading neither leaves nothing. Why not skip (the first amendment): a mainline flight on a dep the committed frame derives from left that frame on screen beside its new inputs — `selected` (held pass: constant `0`, tail to `remote` kept) published `1 0` with `query=1`. Why not mark (base): the mark's registration and entanglement bind the node's holder to a flight the held frame never reads — a hide joined to a parked action (A34) held the action's truth on the memo's orphaned re-ask; a branch memo held in a transaction, its kept-tail dep a manual flight, entangled the transaction with a flight nobody resolved and kept a gated reader hidden (#1105, S2/L1). Gating `reporterBlocksSource` instead was wrong (first attempt, S1 flood in the fuzzer): it runs mid-pass from `heldFromStale` → `waitingTransition`, when the reporter's tail sits at the dep just read and the pending dep it is about to throw on is still past it — the registration was pruned and never re-added. +- Reporter liveness through a memo (#3494): `reporterBlocksSource`'s deps scan, for each dependency of this pass, also asks `dep._x._pendingSources.has(source)` — a stale reader served a held memo's committed value never turned pending itself, so its only trace of the flight is the memo between them; `_pendingSources` is transitive, one hop covers any depth. Beside a release in the same flush (a gate closes on the flight's last reader as a new reader reveals the memo), the verdict judged the new reader dead and released `count=1` beside the `Copy: 0` it displayed. And `transitionBlocked` (optimistic.ts) skips companions (`_parentSource`): a `latest()` shadow created from mainline over a pending memo is backfilled under the owner's transaction (A28 (3)) with an active override and a `NotReadyError` — the exact shape of an authoritative blocker — and held a released write until the orphaned request landed. - Staged reads enter (A29, #3408): `read()` calls `enterStagedRead` on every selection that returns `_pendingValue` — the fast paths and the slow path — and it enters `el._transition` unless that is null (ambient batch), already active, or the read is a probe (`pendingCheckActive`). The third entry beside `setSignal` on a stamped node and `recompute` of a stamped node; rule text and the `Panel: 1` beside `Count: 0` shape live in the spec. A stale (render) reader never reaches it: the carve-out below serves it the committed value. - Dependencies are the committed frame's (A30, #3410): `recompute`'s tail trims the previous pass's dependency tail only for a pass that published or changed nothing (`_pendingValue === NOT_PENDING` and no `_error`); a staged pass leaves it linked and `commitPendingNode` trims after a clean pass (`_error == null` — a set `_error` means the last pass threw, kept its full list, and `_depsTail` marks where it stopped); an effect pass that direct-committed but still owes a run (`_modified`, #3438 — the flush may stash that run into a transaction it opens later) leaves it for `runEffect` to trim once the run applies. `__OBSERVE__` fan-in counting walks the validated prefix only, so a held tail does not inflate distinct-source counts. Why commit-time and not the pass, and the `Selected: 0` beside `Count: 1` shape: the spec. **Unchanged pass (#3469, `heldTrims`, scheduler.ts):** a pass that changed nothing replaced nothing either, and cannot know at its own tail whether the flush that ran it will park with its inputs held — `b() ? b() : a()` computed `1` from the held `b=1`, equal to the `1` it had from `a`, trimmed `a`, and the mainline `a=2` never reached it. `recompute`'s tail now trims at once only for a creation pass, an OPT-dirty pass, or a tracked effect (its frame is replaceable like a direct commit — it runs after the commit and a spurious run is user-visible); any other unchanged pass with a stale tail is pushed to `heldTrims`, drained by `commitPendingNodes` (the flush committed: trim) and cleared when the flush parks (the tail stays linked until a committing pass trims it — one spurious recompute at most). Pinned: `tests/held-frame-dependencies.test.ts`. - Reveal-hold and its carve-out (#3305, #3334, re-ruled 2026-09-10): a reader landing on a node with `STATUS_PENDING` throws — the throw reaches `GlobalQueue.notify`, which opens a transaction for the reveal if none is active (#3305) and records the source as its reporter (INV-3); the reveal completes when the flight lands. One carve-out, the staged-value rule's twin for flights: a **stale** (render) reader of a node pending in some **other** transaction shows the node's committed value, does not entangle (its own writes stay outside that transaction), is recorded for that transaction's commit replay (`heldFromStale`), and joins the transaction's reporters for the node when it has an entry (#3374) — the reader displays the pre-flight value, so the transaction cannot commit the flight's inputs ahead of its answer just because the reader that opened the entry was disposed (a keyed remount). It is refused — the reader holds — when the committed value would tear against the frame: the node carries `CONFIG_INPUTS_PUBLISHED` (a batch or transaction committed with the node still pending, `commitPendingNode`'s computed branch: the flight's inputs are on screen; cleared when the node next enters pending from a settled state, `notifyStatus`), or the node is routed through a live lane (`GlobalQueue._laneLive` → `resolveLane`, exact rather than sticky: lane-revealed inputs, optimistic or `latest`), or the node is uninitialized (nothing committed to show). The stamp itself is pending-node bookkeeping and decides nothing. Replay hygiene: an effect recorded in `_gatedSubs` that later recomputes _under_ the transaction sees its staged view and is applied by the commit (ownership) — `recompute` drops the stale recording at its start (`activeTransition._gatedSubs.delete`), and a lane's committed-view read re-records during the run, so the lane replay (`laneReadsCommitted`) is untouched. diff --git a/packages/signals/docs/RULES-INDEX.md b/packages/signals/docs/RULES-INDEX.md index ea25086d9..28a08dc29 100644 --- a/packages/signals/docs/RULES-INDEX.md +++ b/packages/signals/docs/RULES-INDEX.md @@ -6,34 +6,34 @@ IDs are never renumbered or deleted — source comments cite them. A superseded, ## Vocabularies -| prefix | defined in | meaning | -| ------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `A` | `SPEC-ASYNC-SEMANTICS.md` Tier A | ruled, test-pinned propositions about isPending / latest / transitions / optimistic lanes | -| `B` `C` | `SPEC-ASYNC-SEMANTICS.md` Tier B/C | inferred / open items, all since ruled, closed or promoted into an A-rule (`(was B1)` — the alias row points at it) | -| `V` | `SPEC-ASYNC-SEMANTICS.md` Known violations | violations of A-rules found and fixed by the #2838 redesign; pinned in `spec-async-semantics.test.ts` | -| `INV-` | `INTERNALS-ASYNC-STATE.md` §5 | `__TEST__` invariants (asserted in `invariants.ts`) | -| `RUL-` | `INTERNALS-STORE-STATE.md` §8b, `rules-mining/FINDINGS.md` | store rulings mined from the suites (2026-08-16) | -| `-R` | `rules-mining/.md` | mined behavioral rules. **Each file numbers from R1**, so an R-id is only meaningful with its namespace: `CS` core-store · `OL` optimistic-lanes · `OS` optimistic-store · `PJ` projections · `RS` reconcile-snapshot. Comments qualify with `core`/`opt`/`proj`/`snap`; a bare `R` refers to the citing module's own file. | -| `§` | `INTERNALS-STORE-STATE.md` sections; `NODE-SHAPE.md` §11b, §12–§12e | design sections cited as rules. §11–§12 are the stage-3 node-shape decisions recovered from the deleted `DESIGN-PATCH-CHANNEL.md` (see NODE-SHAPE.md for provenance) | +| prefix | defined in | meaning | +|---|---|---| +| `A` | `SPEC-ASYNC-SEMANTICS.md` Tier A | ruled, test-pinned propositions about isPending / latest / transitions / optimistic lanes | +| `B` `C` | `SPEC-ASYNC-SEMANTICS.md` Tier B/C | inferred / open items, all since ruled, closed or promoted into an A-rule (`(was B1)` — the alias row points at it) | +| `V` | `SPEC-ASYNC-SEMANTICS.md` Known violations | violations of A-rules found and fixed by the #2838 redesign; pinned in `spec-async-semantics.test.ts` | +| `INV-` | `INTERNALS-ASYNC-STATE.md` §5 | `__TEST__` invariants (asserted in `invariants.ts`) | +| `RUL-` | `INTERNALS-STORE-STATE.md` §8b, `rules-mining/FINDINGS.md` | store rulings mined from the suites (2026-08-16) | +| `-R` | `rules-mining/.md` | mined behavioral rules. **Each file numbers from R1**, so an R-id is only meaningful with its namespace: `CS` core-store · `OL` optimistic-lanes · `OS` optimistic-store · `PJ` projections · `RS` reconcile-snapshot. Comments qualify with `core`/`opt`/`proj`/`snap`; a bare `R` refers to the citing module's own file. | +| `§` | `INTERNALS-STORE-STATE.md` sections; `NODE-SHAPE.md` §11b, §12–§12e | design sections cited as rules. §11–§12 are the stage-3 node-shape decisions recovered from the deleted `DESIGN-PATCH-CHANNEL.md` (see NODE-SHAPE.md for provenance) | Status legend: **live** stated and standing · **ruled** carries an explicit ruling date · **amended** re-ruled or re-scoped in place (the row text says how) · **superseded** replaced by a later rule (the text names it) · **retired** mechanism removed, ID kept for citations · **fixed / resolved / closed** a violation or open item with its outcome · **ruled out** a design that was tried and rejected. ## Summary | vocabulary | rules | cited in src | cited in tests | cited nowhere | -| ---------- | ----- | ------------ | -------------- | ------------- | -| A | 33 | 18 | 33 | 0 | -| V | 5 | 2 | 5 | 0 | -| B | 5 | 0 | 5 | 0 | -| C | 4 | 0 | 3 | 1 | -| INV | 11 | 11 | 7 | 0 | -| RUL | 13 | 6 | 6 | 5 | -| R (CS) | 59 | 18 | 16 | 31 | -| R (OL) | 37 | 0 | 0 | 37 | -| R (OS) | 46 | 2 | 0 | 44 | -| R (PJ) | 36 | 6 | 1 | 30 | -| R (RS) | 38 | 9 | 2 | 27 | -| § | 24 | 14 | 8 | 8 | +|---|---|---|---|---| +| A | 34 | 19 | 34 | 0 | +| V | 5 | 2 | 5 | 0 | +| B | 5 | 0 | 5 | 0 | +| C | 4 | 0 | 3 | 1 | +| INV | 11 | 11 | 7 | 0 | +| RUL | 13 | 6 | 6 | 5 | +| R (CS) | 59 | 18 | 16 | 31 | +| R (OL) | 37 | 0 | 0 | 37 | +| R (OS) | 46 | 2 | 0 | 44 | +| R (PJ) | 36 | 6 | 1 | 30 | +| R (RS) | 38 | 9 | 2 | 27 | +| § | 24 | 14 | 8 | 8 | ## Unresolved citations @@ -43,371 +43,361 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul ## A — spec propositions -| id | status | defined | cited in src | cited in tests | statement (at definition) | -| --- | ----------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A1 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:251` | — | 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:259` | — | 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:267` | — | 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:275` | — | 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:283` | — | 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:291` | — | 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:117` | verdict.ts×2 | 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:125` | core.ts×1 verdict.ts×2 | 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:133` | — | 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:141` | invariants.ts×1 verdict.ts×1 | createMemo.test.ts×1 ispending-memo-unstamped-hold-3457.test.ts×2 latest-isPending-consistency.test.ts×1 | [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:59` | — | 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:149` | — | 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:157` | async.ts×1 | 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:165` | — | spec-async-semantics.test.ts×2 | [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:199` | async.ts×3 core.ts×5 lanes.ts×2 scheduler.ts×2 store.ts×1 | async-chain-supersession.test.ts×1 first-observer-stale-reader.test.ts×1 lane-hold-on-observation.test.ts×1 lane-outside-view.test.ts×1 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 treeshake.test.ts×4 visibility-oracle-store.states.ts×6 visibility-oracle.states.ts×6 visibility-oracle.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:173` | verdict.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:31` | async.ts×4 constants.ts×2 core.ts×10 invariants.ts×3 optimistic.ts×6 scheduler.ts×2 verdict.ts×2 signals.ts×2 optimistic.ts×1 store.ts×1 | 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×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×25 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:41` | async.ts×3 constants.ts×1 core.ts×7 optimistic.ts×6 scheduler.ts×3 types.ts×2 verdict.ts×2 optimistic.ts×1 | body-end-supersession-visibility.test.ts×4 createOptimistic.test.ts×1 lane-outside-view.test.ts×1 posture-store-parity.test.ts×5 spec-async-semantics.test.ts×3 flight-owned-transaction.test.ts×1 superseded-before-first-commit.test.ts×5 visibility-oracle-store.states.ts×9 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×24 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 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:101` | async.ts×1 core.ts×2 optimistic.ts×1 verdict.ts×1 | spec-async-semantics.test.ts×3 superseded-before-first-commit.test.ts×1 uninitialized-visibility.test.ts×1 visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×11 visibility-oracle.test.ts×1 | [ruled 2026-07-07 (promoted from C1)] `isPending(x)` ≡ the observable value is not final (three causes) — (was C1 — **partially reverses an earlier decision**) \*\*Definition: `isPending(x)` ≡ the value… | -| A20 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:340` | invariants.ts×1 | 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:347` | — | 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 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:181` | — | spec-async-semantics.test.ts×1 visibility-oracle-store.states.ts×1 | [ruled 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) and the decree tha… | -| A23 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:189` | — | 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:109` | — | optimistic-undefined-override.test.ts×1 reveal-gating-contract.test.ts×1 spec-async-semantics.test.ts×2 visibility-oracle-store.states.ts×4 visibility-oracle.states.ts×3 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:241` | verdict.ts×1 | 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:67` | core.ts×1 scheduler.ts×1 store.ts×1 | action-await-contract.test.ts×2 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:233` | — | 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:51` | constants.ts×2 core.ts×20 optimistic.ts×1 scheduler.ts×3 types.ts×1 verdict.ts×6 optimistic.ts×3 store.ts×1 | createOptimistic.test.ts×5 latest-held-till-flush.test.ts×1 optimistic-store-layer-scope.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 shallow.test.ts×1 treeshake.test.ts×2 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:75` | action.ts×1 core.ts×7 effect.ts×1 optimistic.ts×1 signals.ts×1 store.ts×3 | body-end-supersession-visibility.test.ts×1 born-held.test.ts×3 direct-commit-readers-posture.test.ts×1 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 latest-held-till-flush.test.ts×2 posture-store-parity.test.ts×4 treeshake.test.ts×1 visibility-oracle-store.states.ts×5 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×5 visibility-oracle.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:207` | async.ts×1 attribution.ts×1 core.ts×2 effect.ts×1 scheduler.ts×4 | async-landing-deps-3461.test.ts×3 held-conditional-effect.test.ts×1 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 posture-born-held-and-observation.test.ts×1 treeshake.test.ts×1 | [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:83` | core.ts×2 | 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:91` | core.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:221` | boundaries.ts×2 scheduler.ts×1 | async-chain-supersession.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… | - +| id | status | defined | cited in src | cited in tests | statement (at definition) | +|---|---|---|---|---|---| +| A1 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:261` | — | 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:269` | — | 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:277` | — | 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:285` | — | 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:293` | — | 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:301` | — | 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:117` | verdict.ts×2 | 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:125` | core.ts×1 verdict.ts×2 | 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:133` | — | 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:141` | invariants.ts×1 verdict.ts×1 | createMemo.test.ts×1 ispending-memo-unstamped-hold-3457.test.ts×2 latest-isPending-consistency.test.ts×1 | [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:59` | — | 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:149` | — | 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:157` | async.ts×1 | 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:165` | — | spec-async-semantics.test.ts×2 | [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:199` | async.ts×4 core.ts×5 lanes.ts×2 scheduler.ts×2 store.ts×1 | async-chain-supersession.test.ts×1 first-observer-stale-reader.test.ts×1 lane-hold-on-observation.test.ts×1 lane-outside-view.test.ts×1 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 treeshake.test.ts×4 visibility-oracle-store.states.ts×6 visibility-oracle.states.ts×6 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:173` | verdict.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:31` | async.ts×4 constants.ts×2 core.ts×10 invariants.ts×3 optimistic.ts×6 scheduler.ts×2 verdict.ts×2 signals.ts×2 optimistic.ts×1 store.ts×1 | 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×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×25 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:41` | async.ts×3 constants.ts×1 core.ts×7 optimistic.ts×6 scheduler.ts×3 types.ts×2 verdict.ts×2 optimistic.ts×1 | body-end-supersession-visibility.test.ts×4 createOptimistic.test.ts×1 lane-outside-view.test.ts×1 posture-store-parity.test.ts×5 spec-async-semantics.test.ts×3 flight-owned-transaction.test.ts×1 superseded-before-first-commit.test.ts×5 visibility-oracle-store.states.ts×9 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×24 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 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:101` | async.ts×1 core.ts×2 optimistic.ts×1 verdict.ts×2 | spec-async-semantics.test.ts×3 superseded-before-first-commit.test.ts×1 uninitialized-visibility.test.ts×1 visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×11 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled 2026-07-07 (promoted from C1)] `isPending(x)` ≡ the observable value is not final (three causes) — (was C1 — **partially reverses an earlier decision**) **Definition: `isPending(x)` ≡ the value… | +| A20 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:350` | invariants.ts×1 | 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:357` | — | 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 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:181` | — | spec-async-semantics.test.ts×1 visibility-oracle-store.states.ts×1 | [ruled 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) and the decree tha… | +| A23 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:189` | — | 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:109` | — | optimistic-undefined-override.test.ts×1 reveal-gating-contract.test.ts×1 spec-async-semantics.test.ts×2 visibility-oracle-store.states.ts×4 visibility-oracle.states.ts×3 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:251` | verdict.ts×1 | 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:67` | core.ts×1 scheduler.ts×1 store.ts×1 | action-await-contract.test.ts×2 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:243` | — | 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:51` | constants.ts×2 core.ts×20 optimistic.ts×2 scheduler.ts×3 types.ts×1 verdict.ts×6 optimistic.ts×3 store.ts×1 | createOptimistic.test.ts×5 latest-held-till-flush.test.ts×1 optimistic-store-layer-scope.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 shallow.test.ts×1 treeshake.test.ts×2 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:75` | action.ts×1 async.ts×1 core.ts×7 effect.ts×1 optimistic.ts×1 scheduler.ts×1 signals.ts×1 store.ts×3 | body-end-supersession-visibility.test.ts×1 born-held.test.ts×3 direct-commit-readers-posture.test.ts×1 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 latest-held-till-flush.test.ts×2 posture-store-parity.test.ts×4 treeshake.test.ts×1 visibility-oracle-store.states.ts×5 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×5 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:207` | async.ts×3 attribution.ts×1 core.ts×2 effect.ts×1 scheduler.ts×4 | async-landing-deps-3461.test.ts×3 held-conditional-effect.test.ts×1 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 posture-born-held-and-observation.test.ts×1 treeshake.test.ts×2 write-proposals-3494.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:83` | core.ts×2 | 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:91` | core.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:223` | boundaries.ts×2 scheduler.ts×1 | async-chain-supersession.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:233` | async.ts×1 core.ts×1 scheduler.ts×2 verdict.ts×1 | treeshake.test.ts×1 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:410` | async.ts×1 | spec-async-semantics.test.ts×7 | - **V1 (violated A13) — FIXED.** A _resting_ optimistic node reported | -| V2 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:420` | async.ts×1 | 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:426` | — | spec-async-semantics.test.ts×2 | - **V3 (violated A19) — FIXED.** After a reporter-less transition completed, | -| V4 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:433` | — | 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:445` | — | spec-async-semantics.test.ts×3 | - \*\*V5 (A17 corollary — found and fixed with the revert-target elimination, | - +| id | status | defined | cited in src | cited in tests | statement (at definition) | +|---|---|---|---|---|---| +| V1 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:420` | async.ts×1 | spec-async-semantics.test.ts×7 | - **V1 (violated A13) — FIXED.** A _resting_ optimistic node reported | +| V2 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:430` | async.ts×1 | 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:436` | — | spec-async-semantics.test.ts×2 | - **V3 (violated A19) — FIXED.** After a reporter-less transition completed, | +| V4 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:443` | — | 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:455` | — | 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:157` | — | createRevealOrder.test.ts×16 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:165` | — | createRevealOrder.test.ts×16 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:199` | — | spec-async-semantics.test.ts×2 | PROMOTED → A15 (A15's section carries the ruling). | -| B4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:41` | — | spec-async-semantics.test.ts×2 | PROMOTED → A18 (A18's section carries the ruling). | -| B5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:173` | — | spec-async-semantics.test.ts×2 | PROMOTED → A16 (A16's section carries the ruling). | - +| id | status | defined | cited in src | cited in tests | statement (at definition) | +|---|---|---|---|---|---| +| B1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:157` | — | createRevealOrder.test.ts×16 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:165` | — | createRevealOrder.test.ts×16 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:199` | — | spec-async-semantics.test.ts×2 | PROMOTED → A15 (A15's section carries the ruling). | +| B4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:41` | — | spec-async-semantics.test.ts×2 | PROMOTED → A18 (A18's section carries the ruling). | +| B5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:173` | — | 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:101` | — | 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:376` | — | 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:386` | — | — | - [x] **C3 — CLOSED by A19 (2026-07-07): early completion is by design.** | -| C4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:31` | — | spec-async-semantics.test.ts×1 | PROMOTED → A17 (A17's section carries the ruling). | - +| id | status | defined | cited in src | cited in tests | statement (at definition) | +|---|---|---|---|---|---| +| C1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:101` | — | 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:386` | — | 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:396` | — | — | - [x] **C3 — CLOSED by A19 (2026-07-07): early completion is by design.** | +| C4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:31` | — | 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) | -| ------ | ------- | ----------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | -| INV-1 | live | `docs/INTERNALS-ASYNC-STATE.md:147` | invariants.ts×2 | — | - **INV-1 (high)** `pendingProbe` is non-null only inside an `isPending()` call | -| INV-2 | live | `docs/INTERNALS-ASYNC-STATE.md:149` | invariants.ts×2 | — | - **INV-2 (high)** A node with an _active_ override (`hasActiveOverride`) is | -| INV-3 | live | `docs/INTERNALS-ASYNC-STATE.md:153` | boundaries.ts×1 core.ts×2 invariants.ts×2 lanes.ts×1 scheduler.ts×2 | first-observer-stale-reader.test.ts×1 lane-hold-on-observation.test.ts×1 loading-reset-collects-forwarded-3459.test.ts×1 | - **INV-3 (high)** `_asyncReporters` gains entries only inside | -| INV-4 | live | `docs/INTERNALS-ASYNC-STATE.md:160` | invariants.ts×3 verdict.ts×1 | inv4-projection-dispose-shadow.test.ts×3 posture-store-parity.test.ts×1 | - **INV-4 (medium)** After any of the three write paths completes for node `el` | -| INV-5 | live | `docs/INTERNALS-ASYNC-STATE.md:164` | invariants.ts×2 lanes.ts×1 | — | - **INV-5 (medium)** A lane in `activeLanes` has `_mergedInto === null` | -| INV-6 | live | `docs/INTERNALS-ASYNC-STATE.md:170` | invariants.ts×2 | — | - **INV-6 (medium)** At the end of a completing-transition flush: every node in | -| INV-7 | live | `docs/INTERNALS-ASYNC-STATE.md:173` | core.ts×1 invariants.ts×2 | action-completion-race.test.ts×2 | - **INV-7 (medium)** `_pendingValue !== NOT_PENDING` on a non-optimistic node | -| INV-8 | retired | `docs/INTERNALS-ASYNC-STATE.md:176` | invariants.ts×1 | rules-index.test.ts×1 | - **INV-8 (RETIRED 2026-07-07b, §5e)** Hold-provenance: a `_pendingValue` on an | -| INV-9 | live | `docs/INTERNALS-ASYNC-STATE.md:187` | invariants.ts×1 owner.ts×2 | inv4-projection-dispose-shadow.test.ts×1 | - **INV-9 (high)** An `isPending` companion of a DISPOSED owner reads `false` | -| INV-10 | live | `docs/INTERNALS-ASYNC-STATE.md:192` | invariants.ts×2 | action-done-window.test.ts×1 | - **INV-10 (high)** Affects-count balance (question-scoped model, 2026-07-13; | -| INV-11 | live | `docs/INTERNALS-ASYNC-STATE.md:197` | core.ts×1 optimistic.ts×2 | spec-async-semantics.test.ts×1 treeshake.test.ts×1 | - **INV-11 (high, structural — pinned, not asserted)** A recompute's equality | - +| id | status | defined | cited in src | cited in tests | statement (at definition) | +|---|---|---|---|---|---| +| INV-1 | live | `docs/INTERNALS-ASYNC-STATE.md:150` | invariants.ts×2 | — | - **INV-1 (high)** `pendingProbe` is non-null only inside an `isPending()` call | +| INV-2 | live | `docs/INTERNALS-ASYNC-STATE.md:152` | invariants.ts×2 | — | - **INV-2 (high)** A node with an _active_ override (`hasActiveOverride`) is | +| INV-3 | live | `docs/INTERNALS-ASYNC-STATE.md:156` | boundaries.ts×1 core.ts×2 invariants.ts×2 lanes.ts×1 scheduler.ts×2 | first-observer-stale-reader.test.ts×1 lane-hold-on-observation.test.ts×1 loading-reset-collects-forwarded-3459.test.ts×1 | - **INV-3 (high)** `_asyncReporters` gains entries only inside | +| INV-4 | live | `docs/INTERNALS-ASYNC-STATE.md:163` | invariants.ts×3 verdict.ts×1 | inv4-projection-dispose-shadow.test.ts×3 posture-store-parity.test.ts×1 | - **INV-4 (medium)** After any of the three write paths completes for node `el` | +| INV-5 | live | `docs/INTERNALS-ASYNC-STATE.md:167` | invariants.ts×2 lanes.ts×1 | — | - **INV-5 (medium)** A lane in `activeLanes` has `_mergedInto === null` | +| INV-6 | live | `docs/INTERNALS-ASYNC-STATE.md:173` | invariants.ts×2 | — | - **INV-6 (medium)** At the end of a completing-transition flush: every node in | +| INV-7 | live | `docs/INTERNALS-ASYNC-STATE.md:176` | core.ts×1 invariants.ts×2 | action-completion-race.test.ts×2 | - **INV-7 (medium)** `_pendingValue !== NOT_PENDING` on a non-optimistic node | +| INV-8 | retired | `docs/INTERNALS-ASYNC-STATE.md:179` | invariants.ts×1 | rules-index.test.ts×1 | - **INV-8 (RETIRED 2026-07-07b, §5e)** Hold-provenance: a `_pendingValue` on an | +| INV-9 | live | `docs/INTERNALS-ASYNC-STATE.md:190` | invariants.ts×1 owner.ts×2 | inv4-projection-dispose-shadow.test.ts×1 | - **INV-9 (high)** An `isPending` companion of a DISPOSED owner reads `false` | +| INV-10 | live | `docs/INTERNALS-ASYNC-STATE.md:195` | invariants.ts×2 | action-done-window.test.ts×1 | - **INV-10 (high)** Affects-count balance (question-scoped model, 2026-07-13; | +| INV-11 | live | `docs/INTERNALS-ASYNC-STATE.md:200` | core.ts×1 optimistic.ts×2 | spec-async-semantics.test.ts×1 treeshake.test.ts×1 | - **INV-11 (high, structural — pinned, not asserted)** A recompute's equality | ## RUL — store rulings -| id | status | defined | cited in src | cited in tests | statement (at definition) | -| ------ | -------- | ----------------------------------- | ----------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -| RUL-1 | resolved | `docs/INTERNALS-STORE-STATE.md:512` | store.ts×2 target.ts×1 | next-smoke.test.ts×1 | - \*\*RUL-1 — RESOLVED (2026-08-16, verified per "mirror signals if verified" — | -| RUL-2 | ruled | `docs/INTERNALS-STORE-STATE.md:521` | optimistic.ts×1 target.ts×1 | createOptimisticStore.test.ts×1 | - \*\*RUL-2 — RULED (Ryan, 2026-08-17): no landing matrix. Two orthogonal | -| RUL-3 | resolved | `docs/INTERNALS-STORE-STATE.md:620` | optimistic.ts×1 target.ts×1 | — | - **RUL-3 — RESOLVED (verified 2026-08-16).** Ownership already lives | -| RUL-4 | resolved | `docs/INTERNALS-STORE-STATE.md:627` | — | optimistic-signal-refetch-hold.test.ts×1 | - **RUL-4 — RESOLVED (2026-08-17, signal parity — verified empirically).** | -| RUL-5 | live | `docs/INTERNALS-STORE-STATE.md:640` | reconcile.ts×1 | adoption-lane-rollback.test.ts×1 | - **RUL-5 — SPEC'D (2026-08-17): §6b.** Lane backing + lane-view/committed- | -| RUL-6 | live | `docs/INTERNALS-STORE-STATE.md:644` | — | — | - **RUL-6 — reclassified: SPEC WORK, not a ruling.** Live chaining (#2941 — | -| RUL-7 | live | `docs/INTERNALS-STORE-STATE.md:653` | — | — | - **RUL-7 — SPEC'D (2026-08-17): §6c.** One status field on the root target, | -| RUL-8 | live | `docs/INTERNALS-STORE-STATE.md:656` | — | adoption-lane-rollback.test.ts×1 | - **RUL-8 — SPEC'D (2026-08-17): §6 rewrite, resolves O2.** Per-transaction | -| RUL-9 | resolved | `docs/INTERNALS-STORE-STATE.md:659` | — | — | - **RUL-9 — RESOLVED (2026-08-17, parity by construction).** Every piece of | -| RUL-10 | live | `docs/INTERNALS-STORE-STATE.md:664` | optimistic.ts×1 | — | - **RUL-10 — The equality trio.** One precise rule needed spanning: no-op | -| RUL-11 | live | `docs/INTERNALS-STORE-STATE.md:669` | — | — | - **RUL-11 — SPEC'D (2026-08-17): §6d.** Sticky descendants flag ported from | -| RUL-12 | live | `docs/INTERNALS-STORE-STATE.md:671` | optimistic.ts×1 reconcile.ts×1 store.ts×2 | createProjection.async.test.ts×1 shared-child-multiparent.test.ts×1 | - **RUL-12 — Smaller rulings, each with a proposed default** (proceeding on | -| RUL-13 | resolved | `docs/INTERNALS-STORE-STATE.md:717` | — | — | - **RUL-13 — RESOLVED (verified 2026-08-16)**: `optimistic-lane-transaction- | - +| id | status | defined | cited in src | cited in tests | statement (at definition) | +|---|---|---|---|---|---| +| RUL-1 | resolved | `docs/INTERNALS-STORE-STATE.md:512` | store.ts×2 target.ts×1 | next-smoke.test.ts×1 | - **RUL-1 — RESOLVED (2026-08-16, verified per "mirror signals if verified" — | +| RUL-2 | ruled | `docs/INTERNALS-STORE-STATE.md:521` | optimistic.ts×1 target.ts×1 | createOptimisticStore.test.ts×1 | - **RUL-2 — RULED (Ryan, 2026-08-17): no landing matrix. Two orthogonal | +| RUL-3 | resolved | `docs/INTERNALS-STORE-STATE.md:620` | optimistic.ts×1 target.ts×1 | — | - **RUL-3 — RESOLVED (verified 2026-08-16).** Ownership already lives | +| RUL-4 | resolved | `docs/INTERNALS-STORE-STATE.md:627` | — | optimistic-signal-refetch-hold.test.ts×1 | - **RUL-4 — RESOLVED (2026-08-17, signal parity — verified empirically).** | +| RUL-5 | live | `docs/INTERNALS-STORE-STATE.md:640` | reconcile.ts×1 | adoption-lane-rollback.test.ts×1 | - **RUL-5 — SPEC'D (2026-08-17): §6b.** Lane backing + lane-view/committed- | +| RUL-6 | live | `docs/INTERNALS-STORE-STATE.md:644` | — | — | - **RUL-6 — reclassified: SPEC WORK, not a ruling.** Live chaining (#2941 — | +| RUL-7 | live | `docs/INTERNALS-STORE-STATE.md:653` | — | — | - **RUL-7 — SPEC'D (2026-08-17): §6c.** One status field on the root target, | +| RUL-8 | live | `docs/INTERNALS-STORE-STATE.md:656` | — | adoption-lane-rollback.test.ts×1 | - **RUL-8 — SPEC'D (2026-08-17): §6 rewrite, resolves O2.** Per-transaction | +| RUL-9 | resolved | `docs/INTERNALS-STORE-STATE.md:659` | — | — | - **RUL-9 — RESOLVED (2026-08-17, parity by construction).** Every piece of | +| RUL-10 | live | `docs/INTERNALS-STORE-STATE.md:664` | optimistic.ts×1 | — | - **RUL-10 — The equality trio.** One precise rule needed spanning: no-op | +| RUL-11 | live | `docs/INTERNALS-STORE-STATE.md:669` | — | — | - **RUL-11 — SPEC'D (2026-08-17): §6d.** Sticky descendants flag ported from | +| RUL-12 | live | `docs/INTERNALS-STORE-STATE.md:671` | optimistic.ts×1 reconcile.ts×1 store.ts×2 | createProjection.async.test.ts×1 shared-child-multiparent.test.ts×1 | - **RUL-12 — Smaller rulings, each with a proposed default** (proceeding on | +| RUL-13 | resolved | `docs/INTERNALS-STORE-STATE.md:717` | — | — | - **RUL-13 — RESOLVED (verified 2026-08-16)**: `optimistic-lane-transaction- | ## R — core-store (`CS-R`) -| id | status | defined | cited in src | cited in tests | statement (at definition) | -| ------ | ------ | ------------------------------------- | ------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| CS-R1 | live | `docs/rules-mining/core-store.md:9` | — | flatten-async-iterable.test.ts×5 syncThenable.test.ts×14 visibility-oracle-store.states.ts×1 | Wrappable values are wrapped: reading a plain object/array child never returns the raw source (`state.data !== data`).\*\* | -| CS-R2 | live | `docs/rules-mining/core-store.md:13` | — | syncThenable.test.ts×12 visibility-oracle.states.ts×1 | Raw→proxy resolution is global and deduplicating: wrapping the same raw through two different stores yields the same proxy (`outer.list === inner`).\*\* | -| CS-R2a | live | `docs/INTERNALS-STORE-STATE.md:107` | reconcile.ts×1 store.ts×1 | — | Corollary R2a (Ryan, 2026-08-17): \*\*take no responsibility for mutation | -| CS-R3 | live | `docs/rules-mining/core-store.md:18` | — | — | A store proxy ingested into another store (deep or shallow) is re-wrapped in the ingesting store's own proxy family — never identity-passed, never raw-marked.\*\* | -| CS-R4 | live | `docs/rules-mining/core-store.md:22` | — | — | Write isolation across a store chain: writing through the last store in a derived chain is visible only there; upstream stores and base objects untouched (shallow or deep middle).\*\* | -| CS-R5 | live | `docs/rules-mining/core-store.md:26` | — | visibility-oracle-store.states.ts×4 visibility-oracle.states.ts×4 | Upstream writes propagate downstream through the chain without re-running structural machinery.\*\* | -| CS-R6 | live | `docs/rules-mining/core-store.md:30` | — | — | No store write path ever mutates a user-provided source object.\*\* Aligned with 2026-08-16b. | -| CS-R7 | live | `docs/rules-mining/core-store.md:34` | — | — | Circular references wrap without infinite recursion; cycle consistent through proxy (`state.b.a === state.a`).\*\* | -| CS-R8 | live | `docs/rules-mining/core-store.md:38` | — | — | `snapshot` returns fully unwrapped values (no proxy anywhere, `$TARGET` undefined), incl. frozen objects/arrays; reflects committed written values incl. writes over inherited prototype props.\*\* | -| CS-R9 | live | `docs/rules-mining/core-store.md:42` | store.ts×2 target.ts×1 | — | Proxy identity per logical slot is stable across writes and reconciles\*\* (mapArray keyed flows reuse rows across refetch/reconcile). | -| CS-R10 | live | `docs/rules-mining/core-store.md:48` | — | next-smoke.test.ts×1 | Per-property tracking; same-value writes (direct or functional path setter returning prev) do not re-trigger.\*\* | -| CS-R11 | live | `docs/rules-mining/core-store.md:52` | — | visibility-oracle-store.states.ts×3 visibility-oracle.states.ts×2 | Per-path tracking: reading `state.user.firstName` subscribes to that leaf; reading the reference `store[0]` does not subscribe to `store[0].i`.\*\* | -| CS-R12 | live | `docs/rules-mining/core-store.md:56` | store.ts×1 | — | Reading an absent key subscribes to that key: other-key changes don't trigger; defining it later (assignment or defineProperty) does.\*\* | -| CS-R13 | live | `docs/rules-mining/core-store.md:60` | target.ts×1 | — | `in` tracks presence, not value: undefined-write doesn't retrigger; delete does; adding absent key does. `in`/`has` never invokes source getters.\*\* | -| CS-R14 | live | `docs/rules-mining/core-store.md:64` | — | — | `Object.keys` / `for…in` subscribe to key-set membership (root and nested) — distinct from property nodes.\*\* Aligned: key-set node. | -| CS-R15 | live | `docs/rules-mining/core-store.md:68` | store.ts×1 | — | Array structural tracking is uniform across idioms: indexed length loop, `for…of`, mapArray ($TRACK) all re-run exactly once per flush on add/update/removal.\*\* | -| CS-R16 | live | `docs/rules-mining/core-store.md:72` | — | — | `length` independently trackable; index write extending the array notifies length subscribers.\*\* | -| CS-R17 | live | `docs/rules-mining/core-store.md:76` | — | — | Truncating via `length = N` notifies tracked index reads of removed slots (re-run, observe undefined) and clears has/index/keys for removed indices.\*\* | -| CS-R18 | live | `docs/rules-mining/core-store.md:80` | — | — | `snapshot` is non-tracking.\*\* Aligned with read table. | -| CS-R19 | live | `docs/rules-mining/core-store.md:84` | — | — | `untrack` scopes only the wrapped read; property access on the escaped value afterwards tracks normally.\*\* | -| CS-R20 | live | `docs/rules-mining/core-store.md:88` | store.ts×1 | — | Source getters (own, prototype, merge-installed) execute with the proxy as receiver, so their internal reads track — incl. through projections.\*\* | -| CS-R21 | live | `docs/rules-mining/core-store.md:92` | store.ts×3 | deep-chained-view.test.ts×1 | Structural subscriptions through a wrapper view (store-in-store) chain to the wrapped source: $TRACK/mapArray, ownKeys, snapshot/trackSelf through an outer derived store re-run when the inner store re… | -| CS-R22 | live | `docs/rules-mining/core-store.md:97` | — | — | Slots holding non-wrappable values (markRaw, Map/Date, function) track by reference: reassignment notifies; internal mutation doesn't.\*\* | -| CS-R23 | live | `docs/rules-mining/core-store.md:103` | store.ts×1 | — | The proxy is immutable from outside the setter: direct assignment and delete are silently ignored (no change, no notify, no TypeError — traps report success while discarding).\*\* | -| CS-R24 | live | `docs/rules-mining/core-store.md:107` | — | next-smoke.test.ts×1 | Writes batch like signals: inside the setter draft, reads are read-your-writes (values, length, `in` sync); outside the setter, ALL reads — value, `in`, length — return pre-write state until flush(). … | -| CS-R25 | live | `docs/rules-mining/core-store.md:112` | — | next-smoke.test.ts×1 | Writes to properties with ZERO observers still batch (no effects anywhere; pre-write value visible between setState and flush).\*\* | -| CS-R26 | live | `docs/rules-mining/core-store.md:117` | — | — | Setting a key to undefined is not deletion: key stays present (`in` true, no key-set notify); only delete / storePath.DELETE removes.\*\* | -| CS-R27 | live | `docs/rules-mining/core-store.md:121` | store.ts×1 | — | The setter may return a replacement value that swaps the root wholesale; symbol keys on the replacement preserved.\*\* | -| CS-R28 | live | `docs/rules-mining/core-store.md:125` | — | — | `storePath` addressing: string keys, numeric indices, index arrays, predicate filters ((value, index)), ranges, trailing functional setters address and update intended paths + trigger per-path subscri… | -| CS-R29 | live | `docs/rules-mining/core-store.md:129` | store.ts×2 | overlay.test.ts×1 | Merge/replacement preserve accessor descriptors and keep getters LIVE (re-evaluated per read, reactive reads track), for pre-existing and new keys.\*\* | -| CS-R30 | live | `docs/rules-mining/core-store.md:134` | store.ts×2 | — | Prototype pollution fully guarded: `__proto__` assignment inert; reading `constructor` on the draft returns undefined; storePath refuses `__proto__`/`constructor`/`prototype` segments; skips unsafe ow… | -| CS-R31 | live | `docs/rules-mining/core-store.md:138` | projection.ts×1 | reconcile-resend-identity.test.ts×1 | Derived-store manual writes win over the recompute for the tick: manual setStore beats a queued recompute in the same flush; a SAME-VALUE manual write still holds against the recompute for that tick; … | -| CS-R32 | live | `docs/rules-mining/core-store.md:143` | store.ts×1 | visibility-oracle-store.states.ts×1 | A setter-staged replacement followed by reconcile lands the reconciled value — staged writes fold into the diff.\*\* Aligned: O7's resolution (a test already exists). | -| CS-R33 | live | `docs/rules-mining/core-store.md:147` | — | visibility-oracle-store.states.ts×4 | Action/async lane semantics on store properties: a write held by an action makes isPending true for that property (per-property, not whole-store) while showing the committed value; applies on settle.\*… | -| CS-R34 | live | `docs/rules-mining/core-store.md:151` | store.ts×1 | visibility-oracle-store.states.ts×1 | ~~Optimistic writes visible immediately at write time (before flush)~~, never touch base raw; ambient (non-action) optimistic writes auto-revert at flush end.\*\* \_Visibility superseded 2026-09-10 by A2… | -| CS-R35 | live | `docs/rules-mining/core-store.md:156` | — | — | Mid-refetch optimistic overlays are consumed when data lands — identical via direct reads, mapArray, wrapper views, Object.keys, snapshot.\*\* | -| CS-R36 | live | `docs/rules-mining/core-store.md:160` | — | — | An active optimistic hold on a wrapper view masks inner-store changes for the view's subscribers: mid-hold inner refresh landing causes ZERO re-runs of the view's structural subscribers; the reveal re… | -| CS-R37 | live | `docs/rules-mining/core-store.md:165` | — | visibility-oracle-store.states.ts×3 | Setting store state from effect callbacks and promise resolutions works, applying next flush.\*\* | -| CS-R38 | live | `docs/rules-mining/core-store.md:171` | — | — | Shallow stores: root keys reactive (per-key nodes, membership, length), values served raw by identity at every depth, arrays and objects.\*\* | -| CS-R39 | live | `docs/rules-mining/core-store.md:175` | — | visibility-oracle-store.states.ts×1 | Shallow setter-scope reads serve raws; in-place mutation of a served raw is reactively inert — records replaced, never edited.\*\* | -| CS-R40 | live | `docs/rules-mining/core-store.md:179` | — | — | Shallow reconcile is positional: per-index effects only where the reference changed; reference-identical rows skip entirely; length propagates; `key` option moot.\*\* Aligned: unowned-reference skip rul… | -| CS-R41 | live | `docs/rules-mining/core-store.md:183` | reconcile.ts×1 store.ts×2 | — | A plain record replaced into a shallow store is STICKY raw-marked: presents raw in this store AND in any deep store that later ingests it.\*\* | -| CS-R42 | live | `docs/rules-mining/core-store.md:188` | reconcile.ts×1 store.ts×1 | — | markRaw values never wrap through ANY store (deep included); leaves for reconcile (reference replacement, no recursion).\*\* | -| CS-R43 | live | `docs/rules-mining/core-store.md:192` | — | — | Store proxies are exempt from shallow raw treatment: shallow store ingesting another store's proxy passes it through unmarked and serves a live wrapped view (upstream visible, downstream isolated), se… | -| CS-R44 | live | `docs/rules-mining/core-store.md:196` | store.ts×1 | — | Ingesting an already-deep-tracked raw into a shallow store throws in dev.\*\* | -| CS-R45 | live | `docs/rules-mining/core-store.md:201` | — | — | A shallow store nested in a deep store participates in the parent's reconcile (raw replacement, per-index notify).\*\* | -| CS-R46 | live | `docs/rules-mining/core-store.md:205` | — | — | Shallow projections work end-to-end (derive re-runs, output reconciles at boundary, rows stay raw).\*\* | -| CS-R47 | live | `docs/rules-mining/core-store.md:211` | — | — | Platform objects (Map, Set, Date, Node instances, subclasses) are structurally non-wrappable: served raw by identity; internal-slot methods work on read and draft paths; draft mutations land on the ra… | -| CS-R48 | live | `docs/rules-mining/core-store.md:216` | — | — | User class instances (custom prototypes) DO wrap: prototype getters track; methods on the draft receive the proxy as `this` (reactive writes).\*\* | -| CS-R49 | live | `docs/rules-mining/core-store.md:220` | — | — | Null-prototype objects wrap and track; function-valued props callable through the proxy.\*\* | -| CS-R50 | live | `docs/rules-mining/core-store.md:224` | — | — | Frozen sources fully supported (read/snapshot; getters returning frozen don't throw).\*\* | -| CS-R51 | live | `docs/rules-mining/core-store.md:229` | store.ts×3 | overlay.test.ts×1 write-floor.test.ts×2 | Proxy-invariant compliance via target indirection: keys/spread/descriptor reads never throw regardless of source rigidity; source-non-configurable prop readable, writable through the store, reported `… | -| CS-R52 | live | `docs/rules-mining/core-store.md:233` | — | — | Symbol-keyed properties first-class: read/write/descriptors/preserved through root replacement + storePath root merge; on arrays symbol writes are metadata (never affect length).\*\* | -| CS-R53 | live | `docs/rules-mining/core-store.md:237` | — | — | Array key hygiene: non-index string keys never affect length; `s[len] = undefined` grows length AND creates a present key.\*\* | -| CS-R54 | live | `docs/rules-mining/core-store.md:241` | — | — | Array natives work through the proxy on read (filter/reduce/map/iterate) and draft (push/pop/shift) paths.\*\* | -| CS-R55 | live | `docs/rules-mining/core-store.md:243` | — | — | Functions stored as values served raw, replaceable, slot-tracked.\*\* | -| CS-R56 | live | `docs/rules-mining/core-store.md:247` | — | — | Multiple setter calls before one flush coalesce: even a deep-reading (structural clone) effect re-runs exactly once per flush.\*\* | -| CS-R57 | live | `docs/rules-mining/core-store.md:251` | — | — | Effect ordering: parent effects before child effects created inside them, incl. shared deps through memos.\*\* | -| CS-R58 | live | `docs/rules-mining/core-store.md:255` | — | — | Mid-flush read coherence: untracked store reads inside internal machinery running WITHIN a flush (mapArray keyed:false under a Root owner) must observe the value being written in that flush, not stale… | - +| id | status | defined | cited in src | cited in tests | statement (at definition) | +|---|---|---|---|---|---| +| CS-R1 | live | `docs/rules-mining/core-store.md:9` | — | flatten-async-iterable.test.ts×5 syncThenable.test.ts×14 visibility-oracle-store.states.ts×1 | Wrappable values are wrapped: reading a plain object/array child never returns the raw source (`state.data !== data`).** | +| CS-R2 | live | `docs/rules-mining/core-store.md:13` | — | syncThenable.test.ts×12 visibility-oracle.states.ts×1 | Raw→proxy resolution is global and deduplicating: wrapping the same raw through two different stores yields the same proxy (`outer.list === inner`).** | +| CS-R2a | live | `docs/INTERNALS-STORE-STATE.md:107` | reconcile.ts×1 store.ts×1 | — | Corollary R2a (Ryan, 2026-08-17): **take no responsibility for mutation | +| CS-R3 | live | `docs/rules-mining/core-store.md:18` | — | — | A store proxy ingested into another store (deep or shallow) is re-wrapped in the ingesting store's own proxy family — never identity-passed, never raw-marked.** | +| CS-R4 | live | `docs/rules-mining/core-store.md:22` | — | — | Write isolation across a store chain: writing through the last store in a derived chain is visible only there; upstream stores and base objects untouched (shallow or deep middle).** | +| CS-R5 | live | `docs/rules-mining/core-store.md:26` | — | visibility-oracle-store.states.ts×4 visibility-oracle.states.ts×4 | Upstream writes propagate downstream through the chain without re-running structural machinery.** | +| CS-R6 | live | `docs/rules-mining/core-store.md:30` | — | — | No store write path ever mutates a user-provided source object.** Aligned with 2026-08-16b. | +| CS-R7 | live | `docs/rules-mining/core-store.md:34` | — | — | Circular references wrap without infinite recursion; cycle consistent through proxy (`state.b.a === state.a`).** | +| CS-R8 | live | `docs/rules-mining/core-store.md:38` | — | — | `snapshot` returns fully unwrapped values (no proxy anywhere, `$TARGET` undefined), incl. frozen objects/arrays; reflects committed written values incl. writes over inherited prototype props.** | +| CS-R9 | live | `docs/rules-mining/core-store.md:42` | store.ts×2 target.ts×1 | — | Proxy identity per logical slot is stable across writes and reconciles** (mapArray keyed flows reuse rows across refetch/reconcile). | +| CS-R10 | live | `docs/rules-mining/core-store.md:48` | — | next-smoke.test.ts×1 | Per-property tracking; same-value writes (direct or functional path setter returning prev) do not re-trigger.** | +| CS-R11 | live | `docs/rules-mining/core-store.md:52` | — | visibility-oracle-store.states.ts×3 visibility-oracle.states.ts×2 | Per-path tracking: reading `state.user.firstName` subscribes to that leaf; reading the reference `store[0]` does not subscribe to `store[0].i`.** | +| CS-R12 | live | `docs/rules-mining/core-store.md:56` | store.ts×1 | — | Reading an absent key subscribes to that key: other-key changes don't trigger; defining it later (assignment or defineProperty) does.** | +| CS-R13 | live | `docs/rules-mining/core-store.md:60` | target.ts×1 | — | `in` tracks presence, not value: undefined-write doesn't retrigger; delete does; adding absent key does. `in`/`has` never invokes source getters.** | +| CS-R14 | live | `docs/rules-mining/core-store.md:64` | — | — | `Object.keys` / `for…in` subscribe to key-set membership (root and nested) — distinct from property nodes.** Aligned: key-set node. | +| CS-R15 | live | `docs/rules-mining/core-store.md:68` | store.ts×1 | — | Array structural tracking is uniform across idioms: indexed length loop, `for…of`, mapArray ($TRACK) all re-run exactly once per flush on add/update/removal.** | +| CS-R16 | live | `docs/rules-mining/core-store.md:72` | — | — | `length` independently trackable; index write extending the array notifies length subscribers.** | +| CS-R17 | live | `docs/rules-mining/core-store.md:76` | — | — | Truncating via `length = N` notifies tracked index reads of removed slots (re-run, observe undefined) and clears has/index/keys for removed indices.** | +| CS-R18 | live | `docs/rules-mining/core-store.md:80` | — | — | `snapshot` is non-tracking.** Aligned with read table. | +| CS-R19 | live | `docs/rules-mining/core-store.md:84` | — | — | `untrack` scopes only the wrapped read; property access on the escaped value afterwards tracks normally.** | +| CS-R20 | live | `docs/rules-mining/core-store.md:88` | store.ts×1 | — | Source getters (own, prototype, merge-installed) execute with the proxy as receiver, so their internal reads track — incl. through projections.** | +| CS-R21 | live | `docs/rules-mining/core-store.md:92` | store.ts×3 | deep-chained-view.test.ts×1 | Structural subscriptions through a wrapper view (store-in-store) chain to the wrapped source: $TRACK/mapArray, ownKeys, snapshot/trackSelf through an outer derived store re-run when the inner store re… | +| CS-R22 | live | `docs/rules-mining/core-store.md:97` | — | — | Slots holding non-wrappable values (markRaw, Map/Date, function) track by reference: reassignment notifies; internal mutation doesn't.** | +| CS-R23 | live | `docs/rules-mining/core-store.md:103` | store.ts×1 | — | The proxy is immutable from outside the setter: direct assignment and delete are silently ignored (no change, no notify, no TypeError — traps report success while discarding).** | +| CS-R24 | live | `docs/rules-mining/core-store.md:107` | — | next-smoke.test.ts×1 | Writes batch like signals: inside the setter draft, reads are read-your-writes (values, length, `in` sync); outside the setter, ALL reads — value, `in`, length — return pre-write state until flush(). … | +| CS-R25 | live | `docs/rules-mining/core-store.md:112` | — | next-smoke.test.ts×1 | Writes to properties with ZERO observers still batch (no effects anywhere; pre-write value visible between setState and flush).** | +| CS-R26 | live | `docs/rules-mining/core-store.md:117` | — | — | Setting a key to undefined is not deletion: key stays present (`in` true, no key-set notify); only delete / storePath.DELETE removes.** | +| CS-R27 | live | `docs/rules-mining/core-store.md:121` | store.ts×1 | — | The setter may return a replacement value that swaps the root wholesale; symbol keys on the replacement preserved.** | +| CS-R28 | live | `docs/rules-mining/core-store.md:125` | — | — | `storePath` addressing: string keys, numeric indices, index arrays, predicate filters ((value, index)), ranges, trailing functional setters address and update intended paths + trigger per-path subscri… | +| CS-R29 | live | `docs/rules-mining/core-store.md:129` | store.ts×2 | overlay.test.ts×1 | Merge/replacement preserve accessor descriptors and keep getters LIVE (re-evaluated per read, reactive reads track), for pre-existing and new keys.** | +| CS-R30 | live | `docs/rules-mining/core-store.md:134` | store.ts×2 | — | Prototype pollution fully guarded: `__proto__` assignment inert; reading `constructor` on the draft returns undefined; storePath refuses `__proto__`/`constructor`/`prototype` segments; skips unsafe ow… | +| CS-R31 | live | `docs/rules-mining/core-store.md:138` | projection.ts×1 | reconcile-resend-identity.test.ts×1 | Derived-store manual writes win over the recompute for the tick: manual setStore beats a queued recompute in the same flush; a SAME-VALUE manual write still holds against the recompute for that tick; … | +| CS-R32 | live | `docs/rules-mining/core-store.md:143` | store.ts×1 | visibility-oracle-store.states.ts×1 | A setter-staged replacement followed by reconcile lands the reconciled value — staged writes fold into the diff.** Aligned: O7's resolution (a test already exists). | +| CS-R33 | live | `docs/rules-mining/core-store.md:147` | — | visibility-oracle-store.states.ts×4 | Action/async lane semantics on store properties: a write held by an action makes isPending true for that property (per-property, not whole-store) while showing the committed value; applies on settle.*… | +| CS-R34 | live | `docs/rules-mining/core-store.md:151` | store.ts×1 | visibility-oracle-store.states.ts×1 | ~~Optimistic writes visible immediately at write time (before flush)~~, never touch base raw; ambient (non-action) optimistic writes auto-revert at flush end.** _Visibility superseded 2026-09-10 by A2… | +| CS-R35 | live | `docs/rules-mining/core-store.md:156` | — | — | Mid-refetch optimistic overlays are consumed when data lands — identical via direct reads, mapArray, wrapper views, Object.keys, snapshot.** | +| CS-R36 | live | `docs/rules-mining/core-store.md:160` | — | — | An active optimistic hold on a wrapper view masks inner-store changes for the view's subscribers: mid-hold inner refresh landing causes ZERO re-runs of the view's structural subscribers; the reveal re… | +| CS-R37 | live | `docs/rules-mining/core-store.md:165` | — | visibility-oracle-store.states.ts×3 | Setting store state from effect callbacks and promise resolutions works, applying next flush.** | +| CS-R38 | live | `docs/rules-mining/core-store.md:171` | — | — | Shallow stores: root keys reactive (per-key nodes, membership, length), values served raw by identity at every depth, arrays and objects.** | +| CS-R39 | live | `docs/rules-mining/core-store.md:175` | — | visibility-oracle-store.states.ts×1 | Shallow setter-scope reads serve raws; in-place mutation of a served raw is reactively inert — records replaced, never edited.** | +| CS-R40 | live | `docs/rules-mining/core-store.md:179` | — | — | Shallow reconcile is positional: per-index effects only where the reference changed; reference-identical rows skip entirely; length propagates; `key` option moot.** Aligned: unowned-reference skip rul… | +| CS-R41 | live | `docs/rules-mining/core-store.md:183` | reconcile.ts×1 store.ts×2 | — | A plain record replaced into a shallow store is STICKY raw-marked: presents raw in this store AND in any deep store that later ingests it.** | +| CS-R42 | live | `docs/rules-mining/core-store.md:188` | reconcile.ts×1 store.ts×1 | — | markRaw values never wrap through ANY store (deep included); leaves for reconcile (reference replacement, no recursion).** | +| CS-R43 | live | `docs/rules-mining/core-store.md:192` | — | — | Store proxies are exempt from shallow raw treatment: shallow store ingesting another store's proxy passes it through unmarked and serves a live wrapped view (upstream visible, downstream isolated), se… | +| CS-R44 | live | `docs/rules-mining/core-store.md:196` | store.ts×1 | — | Ingesting an already-deep-tracked raw into a shallow store throws in dev.** | +| CS-R45 | live | `docs/rules-mining/core-store.md:201` | — | — | A shallow store nested in a deep store participates in the parent's reconcile (raw replacement, per-index notify).** | +| CS-R46 | live | `docs/rules-mining/core-store.md:205` | — | — | Shallow projections work end-to-end (derive re-runs, output reconciles at boundary, rows stay raw).** | +| CS-R47 | live | `docs/rules-mining/core-store.md:211` | — | — | Platform objects (Map, Set, Date, Node instances, subclasses) are structurally non-wrappable: served raw by identity; internal-slot methods work on read and draft paths; draft mutations land on the ra… | +| CS-R48 | live | `docs/rules-mining/core-store.md:216` | — | — | User class instances (custom prototypes) DO wrap: prototype getters track; methods on the draft receive the proxy as `this` (reactive writes).** | +| CS-R49 | live | `docs/rules-mining/core-store.md:220` | — | — | Null-prototype objects wrap and track; function-valued props callable through the proxy.** | +| CS-R50 | live | `docs/rules-mining/core-store.md:224` | — | — | Frozen sources fully supported (read/snapshot; getters returning frozen don't throw).** | +| CS-R51 | live | `docs/rules-mining/core-store.md:229` | store.ts×3 | overlay.test.ts×1 write-floor.test.ts×2 | Proxy-invariant compliance via target indirection: keys/spread/descriptor reads never throw regardless of source rigidity; source-non-configurable prop readable, writable through the store, reported `… | +| CS-R52 | live | `docs/rules-mining/core-store.md:233` | — | — | Symbol-keyed properties first-class: read/write/descriptors/preserved through root replacement + storePath root merge; on arrays symbol writes are metadata (never affect length).** | +| CS-R53 | live | `docs/rules-mining/core-store.md:237` | — | — | Array key hygiene: non-index string keys never affect length; `s[len] = undefined` grows length AND creates a present key.** | +| CS-R54 | live | `docs/rules-mining/core-store.md:241` | — | — | Array natives work through the proxy on read (filter/reduce/map/iterate) and draft (push/pop/shift) paths.** | +| CS-R55 | live | `docs/rules-mining/core-store.md:243` | — | — | Functions stored as values served raw, replaceable, slot-tracked.** | +| CS-R56 | live | `docs/rules-mining/core-store.md:247` | — | — | Multiple setter calls before one flush coalesce: even a deep-reading (structural clone) effect re-runs exactly once per flush.** | +| CS-R57 | live | `docs/rules-mining/core-store.md:251` | — | — | Effect ordering: parent effects before child effects created inside them, incl. shared deps through memos.** | +| CS-R58 | live | `docs/rules-mining/core-store.md:255` | — | — | Mid-flush read coherence: untracked store reads inside internal machinery running WITHIN a flush (mapArray keyed:false under a Root owner) must observe the value being written in that flush, not stale… | ## R — optimistic-lanes (`OL-R`) -| id | status | defined | cited in src | cited in tests | statement (at definition) | -| ------ | ---------- | ------------------------------------------- | ------------ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| OL-R1 | live | `docs/rules-mining/optimistic-lanes.md:11` | — | — | `createOptimistic(value \| fn)` returns `[accessor, setter]`; the accessor returns the initial or computed value; the setter accepts a value or an updater function. | -| OL-R2 | superseded | `docs/rules-mining/optimistic-lanes.md:15` | — | — | ~~An optimistic write is synchronously visible to direct reads before any flush — inside the action body, outside it, and outside any reactive context.~~ **Superseded 2026-09-10 by A28(5)** (SPEC-ASYN… | -| OL-R3 | live | `docs/rules-mining/optimistic-lanes.md:19` | — | — | The setter's updater receives the current _visible_ (optimistic-if-overridden) value, never the committed value; a plain setter on the underlying source during a transition composes on the transition'… | -| OL-R4 | live | `docs/rules-mining/optimistic-lanes.md:23` | — | — | Multiple optimistic writes before settle compose sequentially (each updater sees the prior override; last write wins). | -| OL-R5 | live | `docs/rules-mining/optimistic-lanes.md:27` | — | — | An optimistic write **outside any action** reverts at the next flush; subscribers observe the optimistic value and then the reverted value within that single flush (effect log `[1, 2, 1]` after one `f… | -| OL-R6 | live | `docs/rules-mining/optimistic-lanes.md:32` | — | — | An optimistic write inside an `action` holds for the entire action window and reverts when the action's transition completes; each intermediate write during a multi-yield action is observable in order… | -| OL-R7 | live | `docs/rules-mining/optimistic-lanes.md:36` | — | — | Computed-form `createOptimistic(fn)` with no overrides is a transparent passthrough of its (possibly async) source: promise resolutions, re-fired promises, and async-iterable yields all propagate; ove… | -| OL-R8 | live | `docs/rules-mining/optimistic-lanes.md:40` | — | — | Reset-on-settle targets the source's **newly computed value at settle time**, not the pre-write value: a wrong optimistic guess is auto-corrected to the real result; a correct guess settles silently (… | -| OL-R9 | live | `docs/rules-mining/optimistic-lanes.md:44` | — | — | Regular signals written in the same action are held (transition semantics) while optimistic writes display immediately; downstream memos and chained optimistic computeds see optimistic values and reve… | -| OL-R10 | live | `docs/rules-mining/optimistic-lanes.md:48` | — | — | `refresh()` of an optimistic accessor inside an action clears the override when the refetch settles; calling `refresh()` while the upstream source is still pending must not throw. | -| OL-R11 | live | `docs/rules-mining/optimistic-lanes.md:52` | — | — | Verdict channels: an optimistic override **is the value** on every channel — plain read and `latest()` both return it (including literal `undefined`); the override itself is **verdict-inert** — it nev… | -| OL-R12 | live | `docs/rules-mining/optimistic-lanes.md:56` | — | — | A bare `refresh()` is a quiet re-ask — never pending; a **declared** reload (`affects(x)` + `refresh(x)` inside an action) pends the slot for the whole reload window, even when the sole consumer is a … | -| OL-R13 | live | `docs/rules-mining/optimistic-lanes.md:60` | — | — | During the pending window, a source recompute that reveals a value **different** from the current override corrects the override in place (before the action settles), triggering downstream refetch; a … | -| OL-R14 | live | `docs/rules-mining/optimistic-lanes.md:67` | — | — | Independent optimistic writes to unrelated signals form independent lanes: notifications scoped to each signal's own subscribers; each action's overrides revert when _that_ action settles, regardless … | -| OL-R15 | live | `docs/rules-mining/optimistic-lanes.md:71` | — | — | A shared subscriber reading multiple optimistic sources merges lanes **for scheduling only**; it must not transfer transaction ownership of overrides. Disjoint-key work settles with its owning action … | -| OL-R16 | live | `docs/rules-mining/optimistic-lanes.md:76` | — | — | Same-key writes from multiple actions **entangle** those actions: the override (and transitively every override of the entangled actions) reverts only when the **last** entangled action settles. | -| OL-R17 | live | `docs/rules-mining/optimistic-lanes.md:81` | — | — | An equal-value write still registers ownership and still performs lane bookkeeping: a second action writing the same value keeps the override alive after the first settles; an override write whose val… | -| OL-R18 | live | `docs/rules-mining/optimistic-lanes.md:86` | — | — | All optimistic writes in one action share one transaction and revert together atomically; lanes/transactions clean up fully between cycles — the Nth cycle behaves exactly like the first, including aft… | -| OL-R19 | live | `docs/rules-mining/optimistic-lanes.md:90` | — | — | A shared **upstream** async resolving must not merge distinct downstream optimistic lanes — independent paths keep updating independently; genuine merge happens only at convergence points (a memo read… | -| OL-R20 | live | `docs/rules-mining/optimistic-lanes.md:94` | — | — | A later action's override wins over an earlier action's background settle: when action 1's refresh resolves _under_ action 2's live override, the visible value is unchanged, downstream must not recomp… | -| OL-R21 | live | `docs/rules-mining/optimistic-lanes.md:100` | — | — | An optimistic write of literal `undefined` is a full-fledged override: visible on plain read and `latest()`, verdict-inert on `isPending`, and it reverts at settle exactly like any other value. | -| OL-R22 | live | `docs/rules-mining/optimistic-lanes.md:105` | — | — | A follow-up optimistic write after an `undefined` override still rides the optimistic path and reverts at settle — `undefined` in the slot must never erase the node's optimistic identity or route late… | -| OL-R23 | live | `docs/rules-mining/optimistic-lanes.md:109` | — | — | Store form distinguishes "override to undefined" from "delete": optimistic set-to-undefined reads `undefined` with the key still present; optimistic `delete` reads `undefined` **and** `"key" in store … | -| OL-R24 | live | `docs/rules-mining/optimistic-lanes.md:116` | — | — | A transition completes only when **all** reachable asyncs (upstream source and downstream lane asyncs) resolve; held source values must never leak to subscribers before completion, even when the upstr… | -| OL-R25 | live | `docs/rules-mining/optimistic-lanes.md:120` | — | — | Lane readiness gating: subscribers reached _through a downstream async memo_ fire with optimistic values only once that async resolves; direct reads show the override immediately. The lane may flush \*… | -| OL-R26 | live | `docs/rules-mining/optimistic-lanes.md:124` | — | — | At settle, the commit of held transition writes and the revert of optimistic overrides are delivered **atomically**: one subscriber run observing both, never a torn intermediate. | -| OL-R27 | live | `docs/rules-mining/optimistic-lanes.md:128` | — | — | Rapid successive user writes replay correctly: the latest override wins; earlier lane flushes deliver the values current at their readiness time; final settled state reflects the last action's confirm… | -| OL-R28 | live | `docs/rules-mining/optimistic-lanes.md:134` | — | — | No-op settles are silent: if the optimistic write equals the current value, neither the write nor the revert notifies; if the settle-time computed value equals the override, no extra notification fire… | -| OL-R29 | live | `docs/rules-mining/optimistic-lanes.md:139` | — | — | Pre-flush writes coalesce: subscribers see only the latest override per flush (`[0, 2, 0]`, never intermediate `1`). | -| OL-R30 | live | `docs/rules-mining/optimistic-lanes.md:143` | — | — | Render-tier and user-tier effects must observe **identical value sequences** at every flush, including the mid-transition moment where an action finished but async reporters are still in flight. | -| OL-R31 | live | `docs/rules-mining/optimistic-lanes.md:147` | — | — | Optimistic lane notifications run even while an unrelated transition is stashed/pending; pending async in one lane never blocks another lane's write/revert notifications. | -| OL-R32 | live | `docs/rules-mining/optimistic-lanes.md:151` | — | — | `isPending` granularity: each async path's pending slot clears when its **own** async resolves; merged downstream nodes stay pending — emitting **no intermediate half-state values** — until all inputs… | -| OL-R33 | live | `docs/rules-mining/optimistic-lanes.md:155` | — | — | No pending flicker when the visible value is unchanged: background refresh phases with an unchanged visible override must not re-pend downstream; a genuinely new in-flight question must fire `isPendin… | -| OL-R34 | live | `docs/rules-mining/optimistic-lanes.md:159` | — | — | `latest()` readers opt into progressive per-path display while plain readers of merged memos wait for full resolution. | -| OL-R35 | live | `docs/rules-mining/optimistic-lanes.md:165` | — | — | `createOptimisticStore` returns `[proxy, setter]`; draft-style mutations inside an action are optimistic: immediately visible through the proxy, wholly reverted at settle. | -| OL-R36 | live | `docs/rules-mining/optimistic-lanes.md:169` | — | — | Array structural edits (e.g. filter-removal) are visible during the window through `length`, index reads, and iteration, and fully revert at settle. | -| OL-R37 | live | `docs/rules-mining/optimistic-lanes.md:174` | — | — | Store optimism is per-key: different keys written by different actions settle independently — same ownership rules as signals (R15/R16) at store-key granularity. | - +| id | status | defined | cited in src | cited in tests | statement (at definition) | +|---|---|---|---|---|---| +| OL-R1 | live | `docs/rules-mining/optimistic-lanes.md:11` | — | — | `createOptimistic(value \| fn)` returns `[accessor, setter]`; the accessor returns the initial or computed value; the setter accepts a value or an updater function. | +| OL-R2 | superseded | `docs/rules-mining/optimistic-lanes.md:15` | — | — | ~~An optimistic write is synchronously visible to direct reads before any flush — inside the action body, outside it, and outside any reactive context.~~ **Superseded 2026-09-10 by A28(5)** (SPEC-ASYN… | +| OL-R3 | live | `docs/rules-mining/optimistic-lanes.md:19` | — | — | The setter's updater receives the current _visible_ (optimistic-if-overridden) value, never the committed value; a plain setter on the underlying source during a transition composes on the transition'… | +| OL-R4 | live | `docs/rules-mining/optimistic-lanes.md:23` | — | — | Multiple optimistic writes before settle compose sequentially (each updater sees the prior override; last write wins). | +| OL-R5 | live | `docs/rules-mining/optimistic-lanes.md:27` | — | — | An optimistic write **outside any action** reverts at the next flush; subscribers observe the optimistic value and then the reverted value within that single flush (effect log `[1, 2, 1]` after one `f… | +| OL-R6 | live | `docs/rules-mining/optimistic-lanes.md:32` | — | — | An optimistic write inside an `action` holds for the entire action window and reverts when the action's transition completes; each intermediate write during a multi-yield action is observable in order… | +| OL-R7 | live | `docs/rules-mining/optimistic-lanes.md:36` | — | — | Computed-form `createOptimistic(fn)` with no overrides is a transparent passthrough of its (possibly async) source: promise resolutions, re-fired promises, and async-iterable yields all propagate; ove… | +| OL-R8 | live | `docs/rules-mining/optimistic-lanes.md:40` | — | — | Reset-on-settle targets the source's **newly computed value at settle time**, not the pre-write value: a wrong optimistic guess is auto-corrected to the real result; a correct guess settles silently (… | +| OL-R9 | live | `docs/rules-mining/optimistic-lanes.md:44` | — | — | Regular signals written in the same action are held (transition semantics) while optimistic writes display immediately; downstream memos and chained optimistic computeds see optimistic values and reve… | +| OL-R10 | live | `docs/rules-mining/optimistic-lanes.md:48` | — | — | `refresh()` of an optimistic accessor inside an action clears the override when the refetch settles; calling `refresh()` while the upstream source is still pending must not throw. | +| OL-R11 | live | `docs/rules-mining/optimistic-lanes.md:52` | — | — | Verdict channels: an optimistic override **is the value** on every channel — plain read and `latest()` both return it (including literal `undefined`); the override itself is **verdict-inert** — it nev… | +| OL-R12 | live | `docs/rules-mining/optimistic-lanes.md:56` | — | — | A bare `refresh()` is a quiet re-ask — never pending; a **declared** reload (`affects(x)` + `refresh(x)` inside an action) pends the slot for the whole reload window, even when the sole consumer is a … | +| OL-R13 | live | `docs/rules-mining/optimistic-lanes.md:60` | — | — | During the pending window, a source recompute that reveals a value **different** from the current override corrects the override in place (before the action settles), triggering downstream refetch; a … | +| OL-R14 | live | `docs/rules-mining/optimistic-lanes.md:67` | — | — | Independent optimistic writes to unrelated signals form independent lanes: notifications scoped to each signal's own subscribers; each action's overrides revert when _that_ action settles, regardless … | +| OL-R15 | live | `docs/rules-mining/optimistic-lanes.md:71` | — | — | A shared subscriber reading multiple optimistic sources merges lanes **for scheduling only**; it must not transfer transaction ownership of overrides. Disjoint-key work settles with its owning action … | +| OL-R16 | live | `docs/rules-mining/optimistic-lanes.md:76` | — | — | Same-key writes from multiple actions **entangle** those actions: the override (and transitively every override of the entangled actions) reverts only when the **last** entangled action settles. | +| OL-R17 | live | `docs/rules-mining/optimistic-lanes.md:81` | — | — | An equal-value write still registers ownership and still performs lane bookkeeping: a second action writing the same value keeps the override alive after the first settles; an override write whose val… | +| OL-R18 | live | `docs/rules-mining/optimistic-lanes.md:86` | — | — | All optimistic writes in one action share one transaction and revert together atomically; lanes/transactions clean up fully between cycles — the Nth cycle behaves exactly like the first, including aft… | +| OL-R19 | live | `docs/rules-mining/optimistic-lanes.md:90` | — | — | A shared **upstream** async resolving must not merge distinct downstream optimistic lanes — independent paths keep updating independently; genuine merge happens only at convergence points (a memo read… | +| OL-R20 | live | `docs/rules-mining/optimistic-lanes.md:94` | — | — | A later action's override wins over an earlier action's background settle: when action 1's refresh resolves _under_ action 2's live override, the visible value is unchanged, downstream must not recomp… | +| OL-R21 | live | `docs/rules-mining/optimistic-lanes.md:100` | — | — | An optimistic write of literal `undefined` is a full-fledged override: visible on plain read and `latest()`, verdict-inert on `isPending`, and it reverts at settle exactly like any other value. | +| OL-R22 | live | `docs/rules-mining/optimistic-lanes.md:105` | — | — | A follow-up optimistic write after an `undefined` override still rides the optimistic path and reverts at settle — `undefined` in the slot must never erase the node's optimistic identity or route late… | +| OL-R23 | live | `docs/rules-mining/optimistic-lanes.md:109` | — | — | Store form distinguishes "override to undefined" from "delete": optimistic set-to-undefined reads `undefined` with the key still present; optimistic `delete` reads `undefined` **and** `"key" in store … | +| OL-R24 | live | `docs/rules-mining/optimistic-lanes.md:116` | — | — | A transition completes only when **all** reachable asyncs (upstream source and downstream lane asyncs) resolve; held source values must never leak to subscribers before completion, even when the upstr… | +| OL-R25 | live | `docs/rules-mining/optimistic-lanes.md:120` | — | — | Lane readiness gating: subscribers reached _through a downstream async memo_ fire with optimistic values only once that async resolves; direct reads show the override immediately. The lane may flush *… | +| OL-R26 | live | `docs/rules-mining/optimistic-lanes.md:124` | — | — | At settle, the commit of held transition writes and the revert of optimistic overrides are delivered **atomically**: one subscriber run observing both, never a torn intermediate. | +| OL-R27 | live | `docs/rules-mining/optimistic-lanes.md:128` | — | — | Rapid successive user writes replay correctly: the latest override wins; earlier lane flushes deliver the values current at their readiness time; final settled state reflects the last action's confirm… | +| OL-R28 | live | `docs/rules-mining/optimistic-lanes.md:134` | — | — | No-op settles are silent: if the optimistic write equals the current value, neither the write nor the revert notifies; if the settle-time computed value equals the override, no extra notification fire… | +| OL-R29 | live | `docs/rules-mining/optimistic-lanes.md:139` | — | — | Pre-flush writes coalesce: subscribers see only the latest override per flush (`[0, 2, 0]`, never intermediate `1`). | +| OL-R30 | live | `docs/rules-mining/optimistic-lanes.md:143` | — | — | Render-tier and user-tier effects must observe **identical value sequences** at every flush, including the mid-transition moment where an action finished but async reporters are still in flight. | +| OL-R31 | live | `docs/rules-mining/optimistic-lanes.md:147` | — | — | Optimistic lane notifications run even while an unrelated transition is stashed/pending; pending async in one lane never blocks another lane's write/revert notifications. | +| OL-R32 | live | `docs/rules-mining/optimistic-lanes.md:151` | — | — | `isPending` granularity: each async path's pending slot clears when its **own** async resolves; merged downstream nodes stay pending — emitting **no intermediate half-state values** — until all inputs… | +| OL-R33 | live | `docs/rules-mining/optimistic-lanes.md:155` | — | — | No pending flicker when the visible value is unchanged: background refresh phases with an unchanged visible override must not re-pend downstream; a genuinely new in-flight question must fire `isPendin… | +| OL-R34 | live | `docs/rules-mining/optimistic-lanes.md:159` | — | — | `latest()` readers opt into progressive per-path display while plain readers of merged memos wait for full resolution. | +| OL-R35 | live | `docs/rules-mining/optimistic-lanes.md:165` | — | — | `createOptimisticStore` returns `[proxy, setter]`; draft-style mutations inside an action are optimistic: immediately visible through the proxy, wholly reverted at settle. | +| OL-R36 | live | `docs/rules-mining/optimistic-lanes.md:169` | — | — | Array structural edits (e.g. filter-removal) are visible during the window through `length`, index reads, and iteration, and fully revert at settle. | +| OL-R37 | live | `docs/rules-mining/optimistic-lanes.md:174` | — | — | Store optimism is per-key: different keys written by different actions settle independently — same ownership rules as signals (R15/R16) at store-key granularity. | ## R — optimistic-store (`OS-R`) -| id | status | defined | cited in src | cited in tests | statement (at definition) | -| ------ | ---------- | ------------------------------------------- | --------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| OS-R1 | superseded | `docs/rules-mining/optimistic-store.md:9` | — | — | ~~Synchronous universal visibility.~~** ~~An optimistic write is visible to every reader immediately at write time, before any flush.~~ **Superseded 2026-09-10 by A28(5)\*\*: visible at the flush that c… | -| OS-R2 | live | `docs/rules-mining/optimistic-store.md:13` | — | — | Drafts compose on the live optimistic view.\*\* Each setter draft reads through all prior optimistic state (same tick, across ticks, across separate actions/refetches). | -| OS-R3 | live | `docs/rules-mining/optimistic-store.md:18` | — | — | Per-change notification.\*\* One notification per distinct optimistic value change; sequences like `[0, 1, 2, 0]` are contract. | -| OS-R4 | live | `docs/rules-mining/optimistic-store.md:22` | — | — | Equality cut.\*\* An optimistic write equal to current committed value: no notification on write or settle. | -| OS-R5 | live | `docs/rules-mining/optimistic-store.md:26` | — | — | Snapshot/deep read the optimistic view (resolves O1).\*\* snapshot()/deep() agree with every other reader: overlays, nested writes, optimistic deletes (key absent), array mutations; after settle show co… | -| OS-R6 | live | `docs/rules-mining/optimistic-store.md:31` | — | — | Snapshot allocates fresh objects while an overlay is live\*\* (not identity-stable across calls); settled returns raw identity. | -| OS-R7 | live | `docs/rules-mining/optimistic-store.md:36` | — | — | Propagation through derived graphs\*\* (memo chains, mapArray) like committed values. | -| OS-R8 | live | `docs/rules-mining/optimistic-store.md:38` | — | — | `latest()` returns the optimistic value\*\* during a pending refetch window. | -| OS-R9 | live | `docs/rules-mining/optimistic-store.md:40` | — | — | Cross-lane atomic flip.\*\* Regular store written in the same action holds old value while optimistic store shows overlay; at settle both land in ONE notification pass (mixed intermediates never observe… | -| OS-R10 | live | `docs/rules-mining/optimistic-store.md:46` | — | — | Settle reverts to base with one notification\*\* (`[0,1,0]`). | -| OS-R11 | live | `docs/rules-mining/optimistic-store.md:48` | — | — | Deep-state restoration.\*\* Revert restores complete pre-overlay state at every depth: nested writes, wholesale replacement, array length/indices/order, deletions (value + key membership). | -| OS-R12 | live | `docs/rules-mining/optimistic-store.md:50` | — | — | Revert target is the CURRENT derived base, not a stale snapshot\*\* (dependency changed mid-overlay → revert to recomputed value). | -| OS-R13 | live | `docs/rules-mining/optimistic-store.md:55` | — | — | Base data is not overlay data.\*\* Async-fetched/derived data commits to base and persists; only setter-originated optimistic state discards. | -| OS-R14 | live | `docs/rules-mining/optimistic-store.md:57` | — | — | No-flicker across the settle/refresh seam.\*\* From action-body return until refresh fetch lands, subscribers never observe the previously-committed value of an overridden property. | -| OS-R15 | live | `docs/rules-mining/optimistic-store.md:62` | — | — | Unaffected subscribers do not rerun on another action's settle.\*\* | -| OS-R16 | live | `docs/rules-mining/optimistic-store.md:64` | — | — | Cycles are independent\*\* (no residue between sequential write/settle cycles). | -| OS-R17 | live | `docs/rules-mining/optimistic-store.md:66` | — | — | Optimistic writes never pend.\*\* A plain optimistic store is never pending; an optimistic write alone never makes isPending true on any read (shallow, deep(), root or nested proxy, value or length, sam… | -| OS-R18 | live | `docs/rules-mining/optimistic-store.md:70` | — | — | Overlay lifetime is transaction-bound, per key\*\* (never a timer, never a mere flush boundary — under an action). | -| OS-R19 | live | `docs/rules-mining/optimistic-store.md:72` | — | — | Disjoint-key concurrent actions revert independently\*\* (incl. different rows, deletes) (#2899 ×3). | -| OS-R20 | live | `docs/rules-mining/optimistic-store.md:76` | — | — | Same-key writes entangle whole transactions:\*\* latest write displays; NOTHING in the merged transaction settles until the last member completes — including keys written by only one of them. | -| OS-R21 | live | `docs/rules-mining/optimistic-store.md:81` | — | — | Optimistic delete is per-transaction scoped\*\* (a concurrent action's settle must not resurrect another action's delete). | -| OS-R22 | live | `docs/rules-mining/optimistic-store.md:85` | — | — | Ambient (transaction-less) writes flash:\*\* visible until end of flush, then revert — without touching in-flight actions' keys. | -| OS-R23 | live | `docs/rules-mining/optimistic-store.md:89` | — | — | Actions scope globally (a transaction, not a store handle):\*\* writes made under action A belong to A regardless of which store; separate stores under separate actions settle independently. | -| OS-R24 | live | `docs/rules-mining/optimistic-store.md:91` | — | — | Re-override of a still-overridden key notifies and wins;\*\* the earlier action's completion never resurfaces its value. | -| OS-R25 | live | `docs/rules-mining/optimistic-store.md:95` | — | — | Array mutation overlays:\*\* push, splice, whole-array replacement, top-level array stores — length, index reads, holes, spread/iteration, .map all coherent mid-pending and restore exactly on revert. | -| OS-R26 | live | `docs/rules-mining/optimistic-store.md:97` | — | — | Length reactively consistent with contents;\*\* a consumer reading length then indices in one computation never observes a torn state. | -| OS-R27 | live | `docs/rules-mining/optimistic-store.md:101` | — | — | Key enumeration and `has` are lane-reactive\*\* (Object.keys / `in` reflect optimistic adds/deletes, notify, revert). | -| OS-R28 | live | `docs/rules-mining/optimistic-store.md:103` | — | — | Proxy identity survives truth adoption of optimistic rows:\*\* server data key-matching an optimistically pushed row recycles the proxy (identity preserved) and adopts server values. Single and multiple… | -| OS-R29 | live | `docs/rules-mining/optimistic-store.md:107` | — | — | Entity-swap key probes read committed base, not overlay\*\* (an optimistic `s.id = 99` must not confuse the swap); `key: null` → positional identity. | -| OS-R30 | live | `docs/rules-mining/optimistic-store.md:113` | store.ts×1 | — | Seed invisibility.\*\* Derived store's seed is a draft, never observable: before first resolution every read — get, `in`, keys, spread — throws NotReadyError untracked. Applies to createStore(fn, seed) … | -| OS-R31 | live | `docs/rules-mining/optimistic-store.md:117` | — | — | Dev strictRead scopes escalate:\*\* uninitialized read in a component body throws the `[PENDING_ASYNC_UNTRACKED_READ]` dev error (exact tag is contract), precedence over plain NotReadyError. | -| OS-R32 | live | `docs/rules-mining/optimistic-store.md:119` | — | — | Post-init untracked reads flow committed values,\*\* including during a later refetch window. | -| OS-R33 | live | `docs/rules-mining/optimistic-store.md:121` | — | — | Refetch window keeps the dev safeguard\*\* (committed value untracked; component-body read still dev-throws). | -| OS-R34 | live | `docs/rules-mining/optimistic-store.md:123` | — | — | isPending probes take the prod path in both builds:\*\* dev safeguard must not fire inside a probe; uninitialized + surrounding context ⇒ NotReadyError propagates out of isPending identically dev/prod; … | -| OS-R35 | live | `docs/rules-mining/optimistic-store.md:125` | — | — | Plain stores unaffected\*\* (read normally in every context incl. component bodies). | -| OS-R36 | live | `docs/rules-mining/optimistic-store.md:129` | — | — | Dependency-driven refetch pends the leaf and holds the committed view\*\* until the fetch lands. | -| OS-R37 | live | `docs/rules-mining/optimistic-store.md:131` | — | — | Optimistic writes are verdict-inert:\*\* a mid-refetch write displays but neither clears nor causes pending; the honest mixed state {value: 999, pending: true} is observable. (Re-ruled 2026-07-13, super… | -| OS-R38 | live | `docs/rules-mining/optimistic-store.md:133` | optimistic.ts×1 | — | No-op setters are fully inert:\*\* trap-firing no-ops (s => s, s => ({...s}), same-value write, delete of absent prop) mid-refetch display nothing, don't silence pending, don't entangle with the surroun… | -| OS-R39 | live | `docs/rules-mining/optimistic-store.md:137` | — | — | Landing truth wins over the override:\*\* fetch resolves → server/computed value displays, override consumed, pending clears — even if written mid-flight. | -| OS-R40 | live | `docs/rules-mining/optimistic-store.md:139` | — | — | Bare refresh is a quiet re-ask; affects + refresh is a declared reload.\*\* refresh(store) alone never pends reads; affects(store) + refresh pends them, clearing when data lands. Sync-back refresh insid… | -| OS-R41 | live | `docs/rules-mining/optimistic-store.md:141` | — | — | Streaming continuations are not pending windows.\*\* A generator-based derive (or wrapped createProjection) that yielded once reads settled while awaiting its next chunk, incl. with an override displaye… | -| OS-R42 | live | `docs/rules-mining/optimistic-store.md:143` | — | — | Bare writes ride an in-flight refetch (#2951).\*\* A transaction-less optimistic write while the store's own truth is in flight does NOT revert at flush end; holds until truth lands. Order-independent w… | -| OS-R43 | live | `docs/rules-mining/optimistic-store.md:147` | — | — | Refresh-in-action landings preserve still-pending overlays\*\* (same key ⇒ merged transaction: landing does not consume the pending action's optimistic value). | -| OS-R44 | live | `docs/rules-mining/optimistic-store.md:149` | — | — | Bare-refresh landings consume key-matched overlay content\*\* (optimistic "Optimistic" → server "Saved"); the action's later settle does not revert it. | -| OS-R45 | live | `docs/rules-mining/optimistic-store.md:151` | — | — | Separate-transition landings clear foreign optimistic rows (#2719):\*\* a different source transition resolving fresh data clears optimistic rows of a still-pending unrelated action immediately; later s… | -| OS-R46 | live | `docs/rules-mining/optimistic-store.md:155` | — | — | Refetch persistence across multi-action windows:\*\* overlay survives arbitrary interleaved refresh landings while any overlapping action is pending. | - +| id | status | defined | cited in src | cited in tests | statement (at definition) | +|---|---|---|---|---|---| +| OS-R1 | superseded | `docs/rules-mining/optimistic-store.md:9` | — | — | ~~Synchronous universal visibility.~~** ~~An optimistic write is visible to every reader immediately at write time, before any flush.~~ **Superseded 2026-09-10 by A28(5)**: visible at the flush that c… | +| OS-R2 | live | `docs/rules-mining/optimistic-store.md:13` | — | — | Drafts compose on the live optimistic view.** Each setter draft reads through all prior optimistic state (same tick, across ticks, across separate actions/refetches). | +| OS-R3 | live | `docs/rules-mining/optimistic-store.md:18` | — | — | Per-change notification.** One notification per distinct optimistic value change; sequences like `[0, 1, 2, 0]` are contract. | +| OS-R4 | live | `docs/rules-mining/optimistic-store.md:22` | — | — | Equality cut.** An optimistic write equal to current committed value: no notification on write or settle. | +| OS-R5 | live | `docs/rules-mining/optimistic-store.md:26` | — | — | Snapshot/deep read the optimistic view (resolves O1).** snapshot()/deep() agree with every other reader: overlays, nested writes, optimistic deletes (key absent), array mutations; after settle show co… | +| OS-R6 | live | `docs/rules-mining/optimistic-store.md:31` | — | — | Snapshot allocates fresh objects while an overlay is live** (not identity-stable across calls); settled returns raw identity. | +| OS-R7 | live | `docs/rules-mining/optimistic-store.md:36` | — | — | Propagation through derived graphs** (memo chains, mapArray) like committed values. | +| OS-R8 | live | `docs/rules-mining/optimistic-store.md:38` | — | — | `latest()` returns the optimistic value** during a pending refetch window. | +| OS-R9 | live | `docs/rules-mining/optimistic-store.md:40` | — | — | Cross-lane atomic flip.** Regular store written in the same action holds old value while optimistic store shows overlay; at settle both land in ONE notification pass (mixed intermediates never observe… | +| OS-R10 | live | `docs/rules-mining/optimistic-store.md:46` | — | — | Settle reverts to base with one notification** (`[0,1,0]`). | +| OS-R11 | live | `docs/rules-mining/optimistic-store.md:48` | — | — | Deep-state restoration.** Revert restores complete pre-overlay state at every depth: nested writes, wholesale replacement, array length/indices/order, deletions (value + key membership). | +| OS-R12 | live | `docs/rules-mining/optimistic-store.md:50` | — | — | Revert target is the CURRENT derived base, not a stale snapshot** (dependency changed mid-overlay → revert to recomputed value). | +| OS-R13 | live | `docs/rules-mining/optimistic-store.md:55` | — | — | Base data is not overlay data.** Async-fetched/derived data commits to base and persists; only setter-originated optimistic state discards. | +| OS-R14 | live | `docs/rules-mining/optimistic-store.md:57` | — | — | No-flicker across the settle/refresh seam.** From action-body return until refresh fetch lands, subscribers never observe the previously-committed value of an overridden property. | +| OS-R15 | live | `docs/rules-mining/optimistic-store.md:62` | — | — | Unaffected subscribers do not rerun on another action's settle.** | +| OS-R16 | live | `docs/rules-mining/optimistic-store.md:64` | — | — | Cycles are independent** (no residue between sequential write/settle cycles). | +| OS-R17 | live | `docs/rules-mining/optimistic-store.md:66` | — | — | Optimistic writes never pend.** A plain optimistic store is never pending; an optimistic write alone never makes isPending true on any read (shallow, deep(), root or nested proxy, value or length, sam… | +| OS-R18 | live | `docs/rules-mining/optimistic-store.md:70` | — | — | Overlay lifetime is transaction-bound, per key** (never a timer, never a mere flush boundary — under an action). | +| OS-R19 | live | `docs/rules-mining/optimistic-store.md:72` | — | — | Disjoint-key concurrent actions revert independently** (incl. different rows, deletes) (#2899 ×3). | +| OS-R20 | live | `docs/rules-mining/optimistic-store.md:76` | — | — | Same-key writes entangle whole transactions:** latest write displays; NOTHING in the merged transaction settles until the last member completes — including keys written by only one of them. | +| OS-R21 | live | `docs/rules-mining/optimistic-store.md:81` | — | — | Optimistic delete is per-transaction scoped** (a concurrent action's settle must not resurrect another action's delete). | +| OS-R22 | live | `docs/rules-mining/optimistic-store.md:85` | — | — | Ambient (transaction-less) writes flash:** visible until end of flush, then revert — without touching in-flight actions' keys. | +| OS-R23 | live | `docs/rules-mining/optimistic-store.md:89` | — | — | Actions scope globally (a transaction, not a store handle):** writes made under action A belong to A regardless of which store; separate stores under separate actions settle independently. | +| OS-R24 | live | `docs/rules-mining/optimistic-store.md:91` | — | — | Re-override of a still-overridden key notifies and wins;** the earlier action's completion never resurfaces its value. | +| OS-R25 | live | `docs/rules-mining/optimistic-store.md:95` | — | — | Array mutation overlays:** push, splice, whole-array replacement, top-level array stores — length, index reads, holes, spread/iteration, .map all coherent mid-pending and restore exactly on revert. | +| OS-R26 | live | `docs/rules-mining/optimistic-store.md:97` | — | — | Length reactively consistent with contents;** a consumer reading length then indices in one computation never observes a torn state. | +| OS-R27 | live | `docs/rules-mining/optimistic-store.md:101` | — | — | Key enumeration and `has` are lane-reactive** (Object.keys / `in` reflect optimistic adds/deletes, notify, revert). | +| OS-R28 | live | `docs/rules-mining/optimistic-store.md:103` | — | — | Proxy identity survives truth adoption of optimistic rows:** server data key-matching an optimistically pushed row recycles the proxy (identity preserved) and adopts server values. Single and multiple… | +| OS-R29 | live | `docs/rules-mining/optimistic-store.md:107` | — | — | Entity-swap key probes read committed base, not overlay** (an optimistic `s.id = 99` must not confuse the swap); `key: null` → positional identity. | +| OS-R30 | live | `docs/rules-mining/optimistic-store.md:113` | store.ts×1 | — | Seed invisibility.** Derived store's seed is a draft, never observable: before first resolution every read — get, `in`, keys, spread — throws NotReadyError untracked. Applies to createStore(fn, seed) … | +| OS-R31 | live | `docs/rules-mining/optimistic-store.md:117` | — | — | Dev strictRead scopes escalate:** uninitialized read in a component body throws the `[PENDING_ASYNC_UNTRACKED_READ]` dev error (exact tag is contract), precedence over plain NotReadyError. | +| OS-R32 | live | `docs/rules-mining/optimistic-store.md:119` | — | — | Post-init untracked reads flow committed values,** including during a later refetch window. | +| OS-R33 | live | `docs/rules-mining/optimistic-store.md:121` | — | — | Refetch window keeps the dev safeguard** (committed value untracked; component-body read still dev-throws). | +| OS-R34 | live | `docs/rules-mining/optimistic-store.md:123` | — | — | isPending probes take the prod path in both builds:** dev safeguard must not fire inside a probe; uninitialized + surrounding context ⇒ NotReadyError propagates out of isPending identically dev/prod; … | +| OS-R35 | live | `docs/rules-mining/optimistic-store.md:125` | — | — | Plain stores unaffected** (read normally in every context incl. component bodies). | +| OS-R36 | live | `docs/rules-mining/optimistic-store.md:129` | — | — | Dependency-driven refetch pends the leaf and holds the committed view** until the fetch lands. | +| OS-R37 | live | `docs/rules-mining/optimistic-store.md:131` | — | — | Optimistic writes are verdict-inert:** a mid-refetch write displays but neither clears nor causes pending; the honest mixed state {value: 999, pending: true} is observable. (Re-ruled 2026-07-13, super… | +| OS-R38 | live | `docs/rules-mining/optimistic-store.md:133` | optimistic.ts×1 | — | No-op setters are fully inert:** trap-firing no-ops (s => s, s => ({...s}), same-value write, delete of absent prop) mid-refetch display nothing, don't silence pending, don't entangle with the surroun… | +| OS-R39 | live | `docs/rules-mining/optimistic-store.md:137` | — | — | Landing truth wins over the override:** fetch resolves → server/computed value displays, override consumed, pending clears — even if written mid-flight. | +| OS-R40 | live | `docs/rules-mining/optimistic-store.md:139` | — | — | Bare refresh is a quiet re-ask; affects + refresh is a declared reload.** refresh(store) alone never pends reads; affects(store) + refresh pends them, clearing when data lands. Sync-back refresh insid… | +| OS-R41 | live | `docs/rules-mining/optimistic-store.md:141` | — | — | Streaming continuations are not pending windows.** A generator-based derive (or wrapped createProjection) that yielded once reads settled while awaiting its next chunk, incl. with an override displaye… | +| OS-R42 | live | `docs/rules-mining/optimistic-store.md:143` | — | — | Bare writes ride an in-flight refetch (#2951).** A transaction-less optimistic write while the store's own truth is in flight does NOT revert at flush end; holds until truth lands. Order-independent w… | +| OS-R43 | live | `docs/rules-mining/optimistic-store.md:147` | — | — | Refresh-in-action landings preserve still-pending overlays** (same key ⇒ merged transaction: landing does not consume the pending action's optimistic value). | +| OS-R44 | live | `docs/rules-mining/optimistic-store.md:149` | — | — | Bare-refresh landings consume key-matched overlay content** (optimistic "Optimistic" → server "Saved"); the action's later settle does not revert it. | +| OS-R45 | live | `docs/rules-mining/optimistic-store.md:151` | — | — | Separate-transition landings clear foreign optimistic rows (#2719):** a different source transition resolving fresh data clears optimistic rows of a still-pending unrelated action immediately; later s… | +| OS-R46 | live | `docs/rules-mining/optimistic-store.md:155` | — | — | Refetch persistence across multi-action windows:** overlay survives arbitrary interleaved refresh landings while any overlapping action is pending. | ## R — projections (`PJ-R`) -| id | status | defined | cited in src | cited in tests | statement (at definition) | -| ------ | ------ | -------------------------------------- | -------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| PJ-R1 | live | `docs/rules-mining/projections.md:9` | — | — | The derive receives the projection's current state as a mutable draft that persists across runs\*\*; prior runs' writes are visible and editable later. | -| PJ-R2 | live | `docs/rules-mining/projections.md:12` | store.ts×1 | projection-absent-key-tracking.test.ts×1 | Draft reads inside the derive never register dependencies (no self-tracking)\*\*, including `has`/index probes from array methods (`findIndex`, `splice`) and inspection traps from `console.log`. | -| PJ-R3 | live | `docs/rules-mining/projections.md:15` | — | — | The derive runs eagerly at creation (before any read/subscriber) and re-runs on flush when tracked sources change, even with zero subscribers.\*\* | -| PJ-R4 | live | `docs/rules-mining/projections.md:19` | — | — | A returned value merges reconcile-style\*\*: changed paths notify, absent keys delete, unchanged paths keep value and identity. | -| PJ-R5 | live | `docs/rules-mining/projections.md:22` | reconcile.ts×2 | — | The projection's root proxy identity is stable for its lifetime\*\*: across entity swaps, shape changes, and root key mismatches (root key change merges in place, no throw). | -| PJ-R6 | live | `docs/rules-mining/projections.md:25` | reconcile.ts×1 | — | Keyed diff (default key `"id"`)\*\*: key-matched subtrees merge in place preserving child proxy identity, skipping notification for unchanged slots; key mismatch replaces the subtree with a fresh proxy. | -| PJ-R7 | live | `docs/rules-mining/projections.md:28` | reconcile.ts×2 | — | Key matching is hierarchically scoped\*\*: when the root entity's key changes, children are NOT merged across the entity change even if their own keys match. | -| PJ-R8 | live | `docs/rules-mining/projections.md:32` | — | — | `{ key: null }` merges positionally\*\* (proxy identity preserved regardless of key-field changes). | -| PJ-R9 | live | `docs/rules-mining/projections.md:34` | — | — | A proxy detached by an entity swap remains a coherent read view of its own (old) data\*\* — never dead, never reflecting the new entity. | -| PJ-R10 | live | `docs/rules-mining/projections.md:37` | reconcile.ts×1 | — | After a root swap, the outgoing raw stops resolving to the projection root\*\*; re-handed as nested data it wraps as a distinct proxy with its own values. | -| PJ-R11 | live | `docs/rules-mining/projections.md:41` | — | — | `reconcile()` on a plain store still throws on root key mismatch\*\*; the projection root's merge-in-place (R5) is a projection-specific relaxation. | -| PJ-R12 | live | `docs/rules-mining/projections.md:46` | — | — | Only subscribers of actually-changed properties rerun\*\*; equal-value rewrites and writes to unobserved keys notify nobody. | -| PJ-R13 | live | `docs/rules-mining/projections.md:49` | — | — | Deleting a key notifies its subscribers; subscribers of absent keys track and are notified on later creation.\*\* | -| PJ-R14 | live | `docs/rules-mining/projections.md:53` | — | — | Every subscriber of a changed property is notified exactly once per change.\*\* | -| PJ-R15 | live | `docs/rules-mining/projections.md:55` | — | — | Projections compose\*\* (projection reading another projection; downstream effects run once per upstream change with correct previous values). | -| PJ-R16 | live | `docs/rules-mining/projections.md:57` | — | — | `Object.keys` of a projection is tracked and notifies on key-set changes, including through a chained store backing.\*\* | -| PJ-R17 | live | `docs/rules-mining/projections.md:62` | — | — | A derive returning a live store proxy adopts it live\*\*: subsequent source-store writes flow through the projection without re-running the derive. | -| PJ-R18 | live | `docs/rules-mining/projections.md:66` | — | — | Fine-grained isolation preserved through the chain\*\*: a nested source-store write notifies only the projection subscribers of that nested path. | -| PJ-R19 | live | `docs/rules-mining/projections.md:68` | — | — | When the derive's return switches (store → plain → other store), subscribers see each new value and the previous chain is fully severed.\*\* | -| PJ-R20 | live | `docs/rules-mining/projections.md:70` | — | — | Chained backing works for array roots\*\* (structural + row-level edits flow). | -| PJ-R21 | live | `docs/rules-mining/projections.md:72` | — | — | `createStore(fn, seed)` is the same projection mechanism and chains identically.\*\* | -| PJ-R22 | live | `docs/rules-mining/projections.md:74` | — | — | `snapshot()` of a chained projection returns plain data equal to the current view and detached from future source writes.\*\* | -| PJ-R23 | live | `docs/rules-mining/projections.md:80` | store.ts×2 | — | The seed is a draft for the derive, never observable (#2897)\*\*: until first settle/yield, every read — tracked, untracked, enumeration/spread — throws NotReadyError. | -| PJ-R24 | live | `docs/rules-mining/projections.md:83` | — | — | Draft writes during an in-flight async run are invisible until that run settles\*\* (per-run atomic visibility). | -| PJ-R25 | live | `docs/rules-mining/projections.md:85` | — | — | Async generators publish one snapshot per yield\*\*: bare `yield` publishes accumulated draft mutations; `yield value` replaces the entire state (no merge); each yield transforms again. | -| PJ-R26 | live | `docs/rules-mining/projections.md:87` | — | — | Latest-run-wins supersession\*\*: superseded runs' later yields and pending draft writes are discarded entirely; if no run ever landed, stays NotReady. | -| PJ-R27 | live | `docs/rules-mining/projections.md:89` | — | — | Async recompute does not coarsen granularity\*\*: after settle, only changed-path subscribers rerun. | -| PJ-R28 | live | `docs/rules-mining/projections.md:91` | — | — | `refresh(proj)` forces a new derive run; bare refresh is quiet\*\* (no pending published; silent reveal). | -| PJ-R29 | live | `docs/rules-mining/projections.md:93` | — | — | `affects(proj)` + `refresh(proj)` is a declared reload\*\*: subscribed effects see isPending true + stale value for the window, then settle. | -| PJ-R30 | live | `docs/rules-mining/projections.md:95` | — | — | With no effect subscribed, async work creates no transition\*\* (isPending false throughout initial load). | -| PJ-R31 | live | `docs/rules-mining/projections.md:97` | — | — | With a subscribed effect, source-triggered async reruns are transitions\*\* (pending true + stale during window); initial no-stale-data load is never pending. | -| PJ-R32 | live | `docs/rules-mining/projections.md:99` | — | — | Reading a pending async source inside the derive propagates NotReady to consumers\*\* (Loading boundaries fall back); settle fires downstream effects exactly once with the settled value, never the seed … | -| PJ-R33 | live | `docs/rules-mining/projections.md:101` | — | — | Settlement is a status change, not a value diff\*\*: boundaries and blocked effects release even when the settled value equals the seed. | -| PJ-R34 | live | `docs/rules-mining/projections.md:103` | — | — | Errored derives follow async memo rules\*\*: after rejection ALL readers (settle-time, late tracked, untracked) throw the error (StatusError-wrapped; boundaries unwrap). Seed never served uninitialized;… | -| PJ-R35 | live | `docs/rules-mining/projections.md:106` | — | — | A genuine tracked read on a later cycle retries an errored derive\*\* (memo parity: never untracked, never inside isPending probe, at most once per cycle); successful retry serves fresh value. | -| PJ-R36 | live | `docs/rules-mining/projections.md:110` | — | — | Disposing the owning root stops the projection\*\* (no recomputes, no notifications afterward). | - +| id | status | defined | cited in src | cited in tests | statement (at definition) | +|---|---|---|---|---|---| +| PJ-R1 | live | `docs/rules-mining/projections.md:9` | — | — | The derive receives the projection's current state as a mutable draft that persists across runs**; prior runs' writes are visible and editable later. | +| PJ-R2 | live | `docs/rules-mining/projections.md:12` | store.ts×1 | projection-absent-key-tracking.test.ts×1 | Draft reads inside the derive never register dependencies (no self-tracking)**, including `has`/index probes from array methods (`findIndex`, `splice`) and inspection traps from `console.log`. | +| PJ-R3 | live | `docs/rules-mining/projections.md:15` | — | — | The derive runs eagerly at creation (before any read/subscriber) and re-runs on flush when tracked sources change, even with zero subscribers.** | +| PJ-R4 | live | `docs/rules-mining/projections.md:19` | — | — | A returned value merges reconcile-style**: changed paths notify, absent keys delete, unchanged paths keep value and identity. | +| PJ-R5 | live | `docs/rules-mining/projections.md:22` | reconcile.ts×2 | — | The projection's root proxy identity is stable for its lifetime**: across entity swaps, shape changes, and root key mismatches (root key change merges in place, no throw). | +| PJ-R6 | live | `docs/rules-mining/projections.md:25` | reconcile.ts×1 | — | Keyed diff (default key `"id"`)**: key-matched subtrees merge in place preserving child proxy identity, skipping notification for unchanged slots; key mismatch replaces the subtree with a fresh proxy. | +| PJ-R7 | live | `docs/rules-mining/projections.md:28` | reconcile.ts×2 | — | Key matching is hierarchically scoped**: when the root entity's key changes, children are NOT merged across the entity change even if their own keys match. | +| PJ-R8 | live | `docs/rules-mining/projections.md:32` | — | — | `{ key: null }` merges positionally** (proxy identity preserved regardless of key-field changes). | +| PJ-R9 | live | `docs/rules-mining/projections.md:34` | — | — | A proxy detached by an entity swap remains a coherent read view of its own (old) data** — never dead, never reflecting the new entity. | +| PJ-R10 | live | `docs/rules-mining/projections.md:37` | reconcile.ts×1 | — | After a root swap, the outgoing raw stops resolving to the projection root**; re-handed as nested data it wraps as a distinct proxy with its own values. | +| PJ-R11 | live | `docs/rules-mining/projections.md:41` | — | — | `reconcile()` on a plain store still throws on root key mismatch**; the projection root's merge-in-place (R5) is a projection-specific relaxation. | +| PJ-R12 | live | `docs/rules-mining/projections.md:46` | — | — | Only subscribers of actually-changed properties rerun**; equal-value rewrites and writes to unobserved keys notify nobody. | +| PJ-R13 | live | `docs/rules-mining/projections.md:49` | — | — | Deleting a key notifies its subscribers; subscribers of absent keys track and are notified on later creation.** | +| PJ-R14 | live | `docs/rules-mining/projections.md:53` | — | — | Every subscriber of a changed property is notified exactly once per change.** | +| PJ-R15 | live | `docs/rules-mining/projections.md:55` | — | — | Projections compose** (projection reading another projection; downstream effects run once per upstream change with correct previous values). | +| PJ-R16 | live | `docs/rules-mining/projections.md:57` | — | — | `Object.keys` of a projection is tracked and notifies on key-set changes, including through a chained store backing.** | +| PJ-R17 | live | `docs/rules-mining/projections.md:62` | — | — | A derive returning a live store proxy adopts it live**: subsequent source-store writes flow through the projection without re-running the derive. | +| PJ-R18 | live | `docs/rules-mining/projections.md:66` | — | — | Fine-grained isolation preserved through the chain**: a nested source-store write notifies only the projection subscribers of that nested path. | +| PJ-R19 | live | `docs/rules-mining/projections.md:68` | — | — | When the derive's return switches (store → plain → other store), subscribers see each new value and the previous chain is fully severed.** | +| PJ-R20 | live | `docs/rules-mining/projections.md:70` | — | — | Chained backing works for array roots** (structural + row-level edits flow). | +| PJ-R21 | live | `docs/rules-mining/projections.md:72` | — | — | `createStore(fn, seed)` is the same projection mechanism and chains identically.** | +| PJ-R22 | live | `docs/rules-mining/projections.md:74` | — | — | `snapshot()` of a chained projection returns plain data equal to the current view and detached from future source writes.** | +| PJ-R23 | live | `docs/rules-mining/projections.md:80` | store.ts×2 | — | The seed is a draft for the derive, never observable (#2897)**: until first settle/yield, every read — tracked, untracked, enumeration/spread — throws NotReadyError. | +| PJ-R24 | live | `docs/rules-mining/projections.md:83` | — | — | Draft writes during an in-flight async run are invisible until that run settles** (per-run atomic visibility). | +| PJ-R25 | live | `docs/rules-mining/projections.md:85` | — | — | Async generators publish one snapshot per yield**: bare `yield` publishes accumulated draft mutations; `yield value` replaces the entire state (no merge); each yield transforms again. | +| PJ-R26 | live | `docs/rules-mining/projections.md:87` | — | — | Latest-run-wins supersession**: superseded runs' later yields and pending draft writes are discarded entirely; if no run ever landed, stays NotReady. | +| PJ-R27 | live | `docs/rules-mining/projections.md:89` | — | — | Async recompute does not coarsen granularity**: after settle, only changed-path subscribers rerun. | +| PJ-R28 | live | `docs/rules-mining/projections.md:91` | — | — | `refresh(proj)` forces a new derive run; bare refresh is quiet** (no pending published; silent reveal). | +| PJ-R29 | live | `docs/rules-mining/projections.md:93` | — | — | `affects(proj)` + `refresh(proj)` is a declared reload**: subscribed effects see isPending true + stale value for the window, then settle. | +| PJ-R30 | live | `docs/rules-mining/projections.md:95` | — | — | With no effect subscribed, async work creates no transition** (isPending false throughout initial load). | +| PJ-R31 | live | `docs/rules-mining/projections.md:97` | — | — | With a subscribed effect, source-triggered async reruns are transitions** (pending true + stale during window); initial no-stale-data load is never pending. | +| PJ-R32 | live | `docs/rules-mining/projections.md:99` | — | — | Reading a pending async source inside the derive propagates NotReady to consumers** (Loading boundaries fall back); settle fires downstream effects exactly once with the settled value, never the seed … | +| PJ-R33 | live | `docs/rules-mining/projections.md:101` | — | — | Settlement is a status change, not a value diff**: boundaries and blocked effects release even when the settled value equals the seed. | +| PJ-R34 | live | `docs/rules-mining/projections.md:103` | — | — | Errored derives follow async memo rules**: after rejection ALL readers (settle-time, late tracked, untracked) throw the error (StatusError-wrapped; boundaries unwrap). Seed never served uninitialized;… | +| PJ-R35 | live | `docs/rules-mining/projections.md:106` | — | — | A genuine tracked read on a later cycle retries an errored derive** (memo parity: never untracked, never inside isPending probe, at most once per cycle); successful retry serves fresh value. | +| PJ-R36 | live | `docs/rules-mining/projections.md:110` | — | — | Disposing the owning root stops the projection** (no recomputes, no notifications afterward). | ## R — reconcile-snapshot (`RS-R`) -| id | status | defined | cited in src | cited in tests | statement (at definition) | -| ------ | ------ | --------------------------------------------- | -------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| RS-R1 | live | `docs/rules-mining/reconcile-snapshot.md:9` | — | — | Keyed object merge deletes absent keys.\*\* Properties present in `next` update; properties absent from `next` are deleted (read `undefined`, removed from `in`/keys). | -| RS-R2 | live | `docs/rules-mining/reconcile-snapshot.md:12` | — | — | Reconcile applies to any nested proxy, not just the root\*\*, with identical semantics. | -| RS-R3 | live | `docs/rules-mining/reconcile-snapshot.md:15` | — | — | Keyed identity mismatch at the target throws\*\* (key differs, or key present on target but missing from `next`). Post-throw state is deliberately unasserted (original expectations commented out) — the … | -| RS-R4 | live | `docs/rules-mining/reconcile-snapshot.md:18` | — | — | `key: null` / `key: ""` disables key matching\*\*: positional merge, no root identity check. | -| RS-R5 | live | `docs/rules-mining/reconcile-snapshot.md:21` | — | — | Key modes: string key, key function, none.\*\* KeyFn's call set is observable (see R17). | -| RS-R6 | live | `docs/rules-mining/reconcile-snapshot.md:24` | — | — | Key-matched items preserve logical (proxy) identity across reorder, insert, delete.\*\* | -| RS-R7 | live | `docs/rules-mining/reconcile-snapshot.md:27` | — | — | Re-sent identical objects preserve raw identity: `snapshot(state.arr[i])` is `Object.is`-equal to the original.\*\* CONFLICT (benign, verify): adoption satisfies this since raw becomes the incoming obje… | -| RS-R8 | live | `docs/rules-mining/reconcile-snapshot.md:30` | reconcile.ts×1 | — | Positional merge preserves slot proxy identity even when identifying fields change\*\* (fixed-shape dashboard pattern). | -| RS-R9 | live | `docs/rules-mining/reconcile-snapshot.md:33` | reconcile.ts×2 | — | Only changed leaves notify\*\* (changed `a` reruns its subscriber exactly once; `b` subscriber zero times). | -| RS-R10 | live | `docs/rules-mining/reconcile-snapshot.md:36` | reconcile.ts×2 | — | Kind changes (object↔array) at any position replace wholesale, never merge, and notify the property node.\*\* | -| RS-R11 | live | `docs/rules-mining/reconcile-snapshot.md:39` | reconcile.ts×2 | — | Null entries and primitives are legal keyed-array members\*\* (#2772). | -| RS-R12 | live | `docs/rules-mining/reconcile-snapshot.md:42` | — | — | Array resize notification matrix.\*\* Shrink: tracked removed indices notify `undefined`; tracked `in` flips false; untracked reads agree with new length (no stale node values). Growth: tracked missing … | -| RS-R13 | live | `docs/rules-mining/reconcile-snapshot.md:45` | — | — | Numeric-coercible non-index string props on arrays (`"1e3"`, `"1.5"`) survive resize\*\*; node sync must be membership-based, not length-range-based. | -| RS-R14 | live | `docs/rules-mining/reconcile-snapshot.md:48` | — | — | Symbol keys have full parity with string keys under reconcile\*\* (update/remove/add/nested/mixed). | -| RS-R15 | live | `docs/rules-mining/reconcile-snapshot.md:51` | — | — | Reconcile can assign, swap, and reorder values that are other stores' proxies.\*\* CONFLICT (attention): adoption must handle `next` values that are live proxies of other stores — `storeLookup` resoluti… | -| RS-R16 | live | `docs/rules-mining/reconcile-snapshot.md:54` | reconcile.ts×1 | — | Captured proxies with a live subscriber anywhere below are diffed in place through never-tracked intermediate levels.\*\* CONFLICT (design obligation): a node exists deep below an un-noded path; adoptio… | -| RS-R17 | live | `docs/rules-mining/reconcile-snapshot.md:57` | reconcile.ts×1 | — | Never-subscribed subtrees are pruned: the diff does not walk below their top-level pair\*\* (observable via keyFn call set). | -| RS-R18 | live | `docs/rules-mining/reconcile-snapshot.md:60` | reconcile.ts×3 | — | Captured-but-unobserved proxies may detach and go stale after reconcile\*\* (pinned pruning contract); a key mismatch detaches even an observed captured proxy. | -| RS-R19 | live | `docs/rules-mining/reconcile-snapshot.md:63` | — | — | `deep()` observes a reconcile as a single notification carrying the final plain data.\*\* | -| RS-R20 | live | `docs/rules-mining/reconcile-snapshot.md:66` | — | — | (type-level) — `reconcile(next)` requires the complete store type.\*\* | -| RS-R21 | live | `docs/rules-mining/reconcile-snapshot.md:70` | — | — | A reconcile in the same batch after an unflushed setter write behaves identically to a clean reconcile.\*\* CONFLICT (framing only): tests motivate via `STORE_OVERRIDE`/`applyStateSlow` routing (deleted… | -| RS-R22 | live | `docs/rules-mining/reconcile-snapshot.md:73` | — | adoption-lane-rollback.test.ts×1 | Reconcile inside an optimistic action window is tentatively visible; captured-proxy readers see exactly what tracked readers see, during and after settle.\*\* CONFLICT (load-bearing): adoption must ride… | -| RS-R23 | live | `docs/rules-mining/reconcile-snapshot.md:78` | — | — | `snapshot()`/`deep()` always return plain non-proxy data\*\* — including rows through derived stores, nested objects in them, chained views. | -| RS-R24 | live | `docs/rules-mining/reconcile-snapshot.md:81` | — | — | CoW identity preservation:\*\* never-written store snapshots as the original source object (`===`); after a write, changed object + ancestors are new copies, unchanged siblings keep prior snapshot ident… | -| RS-R25 | live | `docs/rules-mining/reconcile-snapshot.md:84` | — | — | Snapshot through a derived-store view returns the same raw object as through the base store\*\* when nothing overridden. CONFLICT (attention): requires unwrapping chained proxy backings to base raw; the… | -| RS-R26 | live | `docs/rules-mining/reconcile-snapshot.md:87` | — | — | Snapshot reflects in-flight optimistic overrides while the base stays untouched.\*\* Confirms O1's "snapshot = current view, lane values included". | -| RS-R27 | live | `docs/rules-mining/reconcile-snapshot.md:90` | — | — | Snapshot sees pending (unflushed) setter writes synchronously, while untracked proxy reads return the previous value until flush.\*\* CONFLICT (MAJOR): §3's "urgent writes are synchronous commits" would… | -| RS-R28 | live | `docs/rules-mining/reconcile-snapshot.md:93` | reconcile.ts×1 | — | Array holes and length survive snapshot/deep\*\* (trailing delete keeps length; holes stay holes; explicit length truncation round-trips; overridden length 0 snapshots as `[]`). | -| RS-R29 | live | `docs/rules-mining/reconcile-snapshot.md:96` | store.ts×1 | — | Symbol-keyed data round-trips through snapshot\*\*: enumerable symbols preserved in copies; writes inside symbol subtrees captured; added-after-snapshot appear; deleted dropped; NON-enumerable symbols e… | -| RS-R30 | live | `docs/rules-mining/reconcile-snapshot.md:99` | — | — | Snapshot-scope machinery (setSnapshotCapture / markSnapshotScope / releaseSnapshotScope / clearSnapshots):\*\* signals/memos created during capture freeze creation-time value for scoped readers; writes … | -| RS-R31 | live | `docs/rules-mining/reconcile-snapshot.md:102` | — | — | Store properties written during capture preserve pre-write value for scoped readers; unwritten use current.\*\* CONFLICT: current mechanism (`STORE_SNAPSHOT_PROPS` in set trap) is layer-adjacent; the wr… | -| RS-R32 | live | `docs/rules-mining/reconcile-snapshot.md:105` | — | — | A pending async projection suppresses snapshot capture\*\*; after resolve + release, readers see resolved value. CONFLICT (mild): guard must move to lane-scoped adoption writes. | -| RS-R33 | live | `docs/rules-mining/reconcile-snapshot.md:110` | — | — | `merge` core contract:\*\* lazy getters (`this` = source); later sources win incl. explicit `undefined`; key union via `in`/keys; value props copied by value; non-enumerable → enumerable on result; firs… | -| RS-R34 | live | `docs/rules-mining/reconcile-snapshot.md:113` | — | — | `merge` reference-return optimization:\*\* same reference for single arg, trailing falsy args, and when last source's own keys cover the union; new proxy otherwise; holds for store proxies. | -| RS-R35 | live | `docs/rules-mining/reconcile-snapshot.md:115` | — | — | `merge` over signal-of-object source is reactive with minimal notifications.\*\* | -| RS-R36 | live | `docs/rules-mining/reconcile-snapshot.md:117` | — | — | `omit` contract:\*\* removed keys disappear from get/`in`/keys incl. store-proxy sources; kept value props copied; descriptors cloned faithfully; pollution-safe; composes with merge. | -| RS-R37 | live | `docs/rules-mining/reconcile-snapshot.md:119` | — | shared-child-multiparent.test.ts×1 | `deep()` contract:\*\* plain data; tracks entire reachable tree (leaf writes, push, branch replacement, symbol subtree writes, symbol add/delete, shared-object writes through other paths); one notificat… | -| RS-R38 | live | `docs/rules-mining/reconcile-snapshot.md:121` | — | — | Untracked read-through (via merge clone) shows pre-write values until flush.\*\* CONFLICT (same as R27, MAJOR): contradicts write-through-immediately unless plain setter writes stay staged until flush. … | - +| id | status | defined | cited in src | cited in tests | statement (at definition) | +|---|---|---|---|---|---| +| RS-R1 | live | `docs/rules-mining/reconcile-snapshot.md:9` | — | — | Keyed object merge deletes absent keys.** Properties present in `next` update; properties absent from `next` are deleted (read `undefined`, removed from `in`/keys). | +| RS-R2 | live | `docs/rules-mining/reconcile-snapshot.md:12` | — | — | Reconcile applies to any nested proxy, not just the root**, with identical semantics. | +| RS-R3 | live | `docs/rules-mining/reconcile-snapshot.md:15` | — | — | Keyed identity mismatch at the target throws** (key differs, or key present on target but missing from `next`). Post-throw state is deliberately unasserted (original expectations commented out) — the … | +| RS-R4 | live | `docs/rules-mining/reconcile-snapshot.md:18` | — | — | `key: null` / `key: ""` disables key matching**: positional merge, no root identity check. | +| RS-R5 | live | `docs/rules-mining/reconcile-snapshot.md:21` | — | — | Key modes: string key, key function, none.** KeyFn's call set is observable (see R17). | +| RS-R6 | live | `docs/rules-mining/reconcile-snapshot.md:24` | — | — | Key-matched items preserve logical (proxy) identity across reorder, insert, delete.** | +| RS-R7 | live | `docs/rules-mining/reconcile-snapshot.md:27` | — | — | Re-sent identical objects preserve raw identity: `snapshot(state.arr[i])` is `Object.is`-equal to the original.** CONFLICT (benign, verify): adoption satisfies this since raw becomes the incoming obje… | +| RS-R8 | live | `docs/rules-mining/reconcile-snapshot.md:30` | reconcile.ts×1 | — | Positional merge preserves slot proxy identity even when identifying fields change** (fixed-shape dashboard pattern). | +| RS-R9 | live | `docs/rules-mining/reconcile-snapshot.md:33` | reconcile.ts×2 | — | Only changed leaves notify** (changed `a` reruns its subscriber exactly once; `b` subscriber zero times). | +| RS-R10 | live | `docs/rules-mining/reconcile-snapshot.md:36` | reconcile.ts×2 | — | Kind changes (object↔array) at any position replace wholesale, never merge, and notify the property node.** | +| RS-R11 | live | `docs/rules-mining/reconcile-snapshot.md:39` | reconcile.ts×2 | — | Null entries and primitives are legal keyed-array members** (#2772). | +| RS-R12 | live | `docs/rules-mining/reconcile-snapshot.md:42` | — | — | Array resize notification matrix.** Shrink: tracked removed indices notify `undefined`; tracked `in` flips false; untracked reads agree with new length (no stale node values). Growth: tracked missing … | +| RS-R13 | live | `docs/rules-mining/reconcile-snapshot.md:45` | — | — | Numeric-coercible non-index string props on arrays (`"1e3"`, `"1.5"`) survive resize**; node sync must be membership-based, not length-range-based. | +| RS-R14 | live | `docs/rules-mining/reconcile-snapshot.md:48` | — | — | Symbol keys have full parity with string keys under reconcile** (update/remove/add/nested/mixed). | +| RS-R15 | live | `docs/rules-mining/reconcile-snapshot.md:51` | — | — | Reconcile can assign, swap, and reorder values that are other stores' proxies.** CONFLICT (attention): adoption must handle `next` values that are live proxies of other stores — `storeLookup` resoluti… | +| RS-R16 | live | `docs/rules-mining/reconcile-snapshot.md:54` | reconcile.ts×1 | — | Captured proxies with a live subscriber anywhere below are diffed in place through never-tracked intermediate levels.** CONFLICT (design obligation): a node exists deep below an un-noded path; adoptio… | +| RS-R17 | live | `docs/rules-mining/reconcile-snapshot.md:57` | reconcile.ts×1 | — | Never-subscribed subtrees are pruned: the diff does not walk below their top-level pair** (observable via keyFn call set). | +| RS-R18 | live | `docs/rules-mining/reconcile-snapshot.md:60` | reconcile.ts×3 | — | Captured-but-unobserved proxies may detach and go stale after reconcile** (pinned pruning contract); a key mismatch detaches even an observed captured proxy. | +| RS-R19 | live | `docs/rules-mining/reconcile-snapshot.md:63` | — | — | `deep()` observes a reconcile as a single notification carrying the final plain data.** | +| RS-R20 | live | `docs/rules-mining/reconcile-snapshot.md:66` | — | — | (type-level) — `reconcile(next)` requires the complete store type.** | +| RS-R21 | live | `docs/rules-mining/reconcile-snapshot.md:70` | — | — | A reconcile in the same batch after an unflushed setter write behaves identically to a clean reconcile.** CONFLICT (framing only): tests motivate via `STORE_OVERRIDE`/`applyStateSlow` routing (deleted… | +| RS-R22 | live | `docs/rules-mining/reconcile-snapshot.md:73` | — | adoption-lane-rollback.test.ts×1 | Reconcile inside an optimistic action window is tentatively visible; captured-proxy readers see exactly what tracked readers see, during and after settle.** CONFLICT (load-bearing): adoption must ride… | +| RS-R23 | live | `docs/rules-mining/reconcile-snapshot.md:78` | — | — | `snapshot()`/`deep()` always return plain non-proxy data** — including rows through derived stores, nested objects in them, chained views. | +| RS-R24 | live | `docs/rules-mining/reconcile-snapshot.md:81` | — | — | CoW identity preservation:** never-written store snapshots as the original source object (`===`); after a write, changed object + ancestors are new copies, unchanged siblings keep prior snapshot ident… | +| RS-R25 | live | `docs/rules-mining/reconcile-snapshot.md:84` | — | — | Snapshot through a derived-store view returns the same raw object as through the base store** when nothing overridden. CONFLICT (attention): requires unwrapping chained proxy backings to base raw; the… | +| RS-R26 | live | `docs/rules-mining/reconcile-snapshot.md:87` | — | — | Snapshot reflects in-flight optimistic overrides while the base stays untouched.** Confirms O1's "snapshot = current view, lane values included". | +| RS-R27 | live | `docs/rules-mining/reconcile-snapshot.md:90` | — | — | Snapshot sees pending (unflushed) setter writes synchronously, while untracked proxy reads return the previous value until flush.** CONFLICT (MAJOR): §3's "urgent writes are synchronous commits" would… | +| RS-R28 | live | `docs/rules-mining/reconcile-snapshot.md:93` | reconcile.ts×1 | — | Array holes and length survive snapshot/deep** (trailing delete keeps length; holes stay holes; explicit length truncation round-trips; overridden length 0 snapshots as `[]`). | +| RS-R29 | live | `docs/rules-mining/reconcile-snapshot.md:96` | store.ts×1 | — | Symbol-keyed data round-trips through snapshot**: enumerable symbols preserved in copies; writes inside symbol subtrees captured; added-after-snapshot appear; deleted dropped; NON-enumerable symbols e… | +| RS-R30 | live | `docs/rules-mining/reconcile-snapshot.md:99` | — | — | Snapshot-scope machinery (setSnapshotCapture / markSnapshotScope / releaseSnapshotScope / clearSnapshots):** signals/memos created during capture freeze creation-time value for scoped readers; writes … | +| RS-R31 | live | `docs/rules-mining/reconcile-snapshot.md:102` | — | — | Store properties written during capture preserve pre-write value for scoped readers; unwritten use current.** CONFLICT: current mechanism (`STORE_SNAPSHOT_PROPS` in set trap) is layer-adjacent; the wr… | +| RS-R32 | live | `docs/rules-mining/reconcile-snapshot.md:105` | — | — | A pending async projection suppresses snapshot capture**; after resolve + release, readers see resolved value. CONFLICT (mild): guard must move to lane-scoped adoption writes. | +| RS-R33 | live | `docs/rules-mining/reconcile-snapshot.md:110` | — | — | `merge` core contract:** lazy getters (`this` = source); later sources win incl. explicit `undefined`; key union via `in`/keys; value props copied by value; non-enumerable → enumerable on result; firs… | +| RS-R34 | live | `docs/rules-mining/reconcile-snapshot.md:113` | — | — | `merge` reference-return optimization:** same reference for single arg, trailing falsy args, and when last source's own keys cover the union; new proxy otherwise; holds for store proxies. | +| RS-R35 | live | `docs/rules-mining/reconcile-snapshot.md:115` | — | — | `merge` over signal-of-object source is reactive with minimal notifications.** | +| RS-R36 | live | `docs/rules-mining/reconcile-snapshot.md:117` | — | — | `omit` contract:** removed keys disappear from get/`in`/keys incl. store-proxy sources; kept value props copied; descriptors cloned faithfully; pollution-safe; composes with merge. | +| RS-R37 | live | `docs/rules-mining/reconcile-snapshot.md:119` | — | shared-child-multiparent.test.ts×1 | `deep()` contract:** plain data; tracks entire reachable tree (leaf writes, push, branch replacement, symbol subtree writes, symbol add/delete, shared-object writes through other paths); one notificat… | +| RS-R38 | live | `docs/rules-mining/reconcile-snapshot.md:121` | — | — | Untracked read-through (via merge clone) shows pre-write values until flush.** CONFLICT (same as R27, MAJOR): contradicts write-through-immediately unless plain setter writes stay staged until flush. … | ## § — design sections -| id | status | defined | cited in src | cited in tests | statement (at definition) | -| ---- | ------ | ----------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | -| §1 | live | `docs/INTERNALS-STORE-STATE.md:27` | target.ts×1 | reveal-gating-contract.test.ts×1 | Storage model (the single-home rule) | -| §2 | live | `docs/INTERNALS-STORE-STATE.md:81` | — | — | Read paths | -| §3 | live | `docs/INTERNALS-STORE-STATE.md:116` | scheduler.ts×1 optimistic.ts×1 reconcile.ts×1 store.ts×1 target.ts×1 | — | Write paths (all must stay equivalent) | -| §4 | live | `docs/INTERNALS-STORE-STATE.md:178` | — | — | Identity rules | -| §5 | live | `docs/INTERNALS-STORE-STATE.md:190` | — | — | Laziness invariants (candidates for `__TEST__` assertions) | -| §5b | live | `docs/INTERNALS-STORE-STATE.md:208` | target.ts×2 | — | Creation budget (phase-1 fitness) | -| §5c | live | `docs/INTERNALS-STORE-STATE.md:227` | — | — | Comparison method (shipped vs rewrite) | -| §6 | live | `docs/INTERNALS-STORE-STATE.md:278` | core.ts×1 invariants.ts×1 lanes.ts×1 optimistic.ts×1 store.ts×2 target.ts×1 | — | Structural edits — the key-set node (resolves O2, RUL-8) | -| §6b | live | `docs/INTERNALS-STORE-STATE.md:306` | reconcile.ts×2 | adoption-lane-rollback.test.ts×1 | Lane-aware adoption (RUL-5) | -| §6c | live | `docs/INTERNALS-STORE-STATE.md:325` | projection.ts×2 store.ts×1 | createProjection.async.test.ts×1 flight-owned-transaction.test.ts×1 | Store-wide status gating (RUL-7) | -| §6d | live | `docs/INTERNALS-STORE-STATE.md:338` | reconcile.ts×1 target.ts×2 | — | Diff reachability (RUL-11) | -| §7 | live | `docs/INTERNALS-STORE-STATE.md:350` | optimistic.ts×1 projection.ts×1 | — | Projections & optimism layering | -| §7b | live | `docs/INTERNALS-STORE-STATE.md:360` | projection.ts×1 reconcile.ts×1 store.ts×11 target.ts×3 store.ts×1 | — | Chained backing (cross-store) — spec | -| §8 | live | `docs/INTERNALS-STORE-STATE.md:431` | — | reconcile-resend-identity.test.ts×1 | Assumptions / open questions | -| §8b | live | `docs/INTERNALS-STORE-STATE.md:487` | — | — | Suite-mined rules (2026-08-16) — index & rulings needed | -| §9 | live | `docs/INTERNALS-STORE-STATE.md:722` | — | — | Decision log | -| §11 | live | `docs/NODE-SHAPE.md:29` | — | — | Stage 3 opener: the core tax map (2026-08-21) | -| §11b | live | `docs/NODE-SHAPE.md:53` | constants.ts×1 | rules-index.test.ts×1 treeshake.test.ts×1 | Presence bits — hot-path monomorphism | -| §11c | live | `docs/NODE-SHAPE.md:72` | — | — | Stage-3 increment log | -| §12 | live | `docs/NODE-SHAPE.md:84` | constants.ts×1 core.ts×1 types.ts×1 | dist-artifacts.test.ts×1 rules-index.test.ts×1 treeshake.test.ts×2 | Cold-field extension (`_x`, `ext()`) | -| §12b | live | `docs/NODE-SHAPE.md:107` | — | treeshake.test.ts×1 | Zombie pair in the extension; plain-commit fast drain | -| §12c | live | `docs/NODE-SHAPE.md:119` | types.ts×1 | — | What stays IN the core literal | -| §12d | live | `docs/NODE-SHAPE.md:129` | core.ts×2 graph.ts×1 scheduler.ts×2 types.ts×1 | — | Staged-rewrite fast path (notify epoch) — _reconstructed_ | -| §12e | live | `docs/NODE-SHAPE.md:149` | core.ts×2 optimistic.ts×1 | rules-index.test.ts×1 | Signal-literal diet: `_time`, `_fn`, `_statusFlags` are computed-only — _reconstructed_ | +| id | status | defined | cited in src | cited in tests | statement (at definition) | +|---|---|---|---|---|---| +| §1 | live | `docs/INTERNALS-STORE-STATE.md:27` | target.ts×1 | reveal-gating-contract.test.ts×1 | Storage model (the single-home rule) | +| §2 | live | `docs/INTERNALS-STORE-STATE.md:81` | — | — | Read paths | +| §3 | live | `docs/INTERNALS-STORE-STATE.md:116` | scheduler.ts×1 optimistic.ts×1 reconcile.ts×1 store.ts×1 target.ts×1 | — | Write paths (all must stay equivalent) | +| §4 | live | `docs/INTERNALS-STORE-STATE.md:178` | — | — | Identity rules | +| §5 | live | `docs/INTERNALS-STORE-STATE.md:190` | — | — | Laziness invariants (candidates for `__TEST__` assertions) | +| §5b | live | `docs/INTERNALS-STORE-STATE.md:208` | target.ts×2 | — | Creation budget (phase-1 fitness) | +| §5c | live | `docs/INTERNALS-STORE-STATE.md:227` | — | — | Comparison method (shipped vs rewrite) | +| §6 | live | `docs/INTERNALS-STORE-STATE.md:278` | core.ts×1 invariants.ts×1 lanes.ts×1 optimistic.ts×1 store.ts×2 target.ts×1 | — | Structural edits — the key-set node (resolves O2, RUL-8) | +| §6b | live | `docs/INTERNALS-STORE-STATE.md:306` | reconcile.ts×2 | adoption-lane-rollback.test.ts×1 | Lane-aware adoption (RUL-5) | +| §6c | live | `docs/INTERNALS-STORE-STATE.md:325` | projection.ts×2 store.ts×1 | createProjection.async.test.ts×1 flight-owned-transaction.test.ts×1 | Store-wide status gating (RUL-7) | +| §6d | live | `docs/INTERNALS-STORE-STATE.md:338` | reconcile.ts×1 target.ts×2 | — | Diff reachability (RUL-11) | +| §7 | live | `docs/INTERNALS-STORE-STATE.md:350` | optimistic.ts×1 projection.ts×1 | — | Projections & optimism layering | +| §7b | live | `docs/INTERNALS-STORE-STATE.md:360` | projection.ts×1 reconcile.ts×1 store.ts×11 target.ts×3 store.ts×1 | — | Chained backing (cross-store) — spec | +| §8 | live | `docs/INTERNALS-STORE-STATE.md:431` | — | reconcile-resend-identity.test.ts×1 | Assumptions / open questions | +| §8b | live | `docs/INTERNALS-STORE-STATE.md:487` | — | — | Suite-mined rules (2026-08-16) — index & rulings needed | +| §9 | live | `docs/INTERNALS-STORE-STATE.md:722` | — | — | Decision log | +| §11 | live | `docs/NODE-SHAPE.md:29` | — | — | Stage 3 opener: the core tax map (2026-08-21) | +| §11b | live | `docs/NODE-SHAPE.md:53` | constants.ts×1 | rules-index.test.ts×1 treeshake.test.ts×1 | Presence bits — hot-path monomorphism | +| §11c | live | `docs/NODE-SHAPE.md:72` | — | — | Stage-3 increment log | +| §12 | live | `docs/NODE-SHAPE.md:84` | constants.ts×1 core.ts×1 types.ts×1 | dist-artifacts.test.ts×1 rules-index.test.ts×1 treeshake.test.ts×2 | Cold-field extension (`_x`, `ext()`) | +| §12b | live | `docs/NODE-SHAPE.md:107` | — | treeshake.test.ts×1 | Zombie pair in the extension; plain-commit fast drain | +| §12c | live | `docs/NODE-SHAPE.md:119` | types.ts×1 | — | What stays IN the core literal | +| §12d | live | `docs/NODE-SHAPE.md:129` | core.ts×2 graph.ts×1 scheduler.ts×2 types.ts×1 | — | Staged-rewrite fast path (notify epoch) — _reconstructed_ | +| §12e | live | `docs/NODE-SHAPE.md:149` | core.ts×2 optimistic.ts×1 | rules-index.test.ts×1 | Signal-literal diet: `_time`, `_fn`, `_statusFlags` are computed-only — _reconstructed_ | diff --git a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md index 38ab3826f..ebce46b7e 100644 --- a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md +++ b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md @@ -52,7 +52,7 @@ The former Tier A table is these sections. Tier B/C, the fixed violations, and t **Status:** **ruled, mechanism landed 2026-09-15** — maintainer ruling 2026-09-08 ("since we can't derive downstream before flush happens I do like latest being invisible until flush"; "nothing should be 30 pre flush… because double count can't be 60"; "latest opts into the tearing but really only after a flush"); optimistic writes 2026-09-10 ("My gut is to match. I'm gathering React does."; "yeah lets match"). Landed as a READ-SIDE rule (see Mechanism), superseding #3337's deferred-walk mechanism; the runtime's pre-flush cells in both oracles read the rule. **Pinned by:** `tests/latest-held-till-flush.test.ts` (A28 (1)–(4): held and ambient writes, companions created after the write, #3336 born-holding store keys and companions); `tests/visibility-oracle.test.ts` and `tests/visibility-oracle-store.test.ts` (the pre-flush states — every cell cites A28); `tests/latest-repeated-writes.test.ts` (#2922 re-pinned); `tests/latest-unobserved-memo.test.ts`; `tests/latest-probe-order-independence.test.ts`; `tests/isPending-memo-consistency.test.ts`; `tests/latest-plain-write-purity.test.ts`; `tests/store/projection-transition-isolation.test.ts`; optimistic (5): `tests/createOptimistic.test.ts`, `tests/store/createOptimisticStore.test.ts`, `tests/question-scoped-pending.test.ts` (same-tick `affects(parent)` covers the pushed row), `tests/optimistic-store-layer-scope.test.ts`, `tests/store/shallow.test.ts`. -**Mechanism (index, 2026-09-15 — as landed):** read-side, no marker on the plain write path. "Unflushed" is structural: outside a flush (`!globalQueue._running`), a node with an ambient staged value (`_pendingValue` set, `_transition` null) was written since the last flush — ambient staging commits at flush end, so nothing else leaves a node in that state (`unflushedValue`, core.ts). Exempt: engine companions (`_parentSource` — written at the source's write to mirror it, installing eagerly, A8) and writes issued inside a creation-time recompute (`CONFIG_PROMOTED`, set only when `!_running && context !== null` — A28 (4): promoted at that recompute's end). A rewrite of a HELD node stashes the staged value the last flush left (`_x._flushedStaged`, `unflushedRewrites`) and `latest()`/verdicts answer with it until the next flush. Optimistic writes (5): `_overrideTime === clock` outside a flush is the unflushed override (`unflushedOverride`); readers skip it (`read()`'s override arm, store `nodeValue`/`visibleOverride`/`optimisticView(…, draft=false)`), the writer's channels — the draft, the `affects()` declaration walk — compose on it. Readers served the flushed value because of an unflushed write are latched for the carrying flush (`REACTIVE_MISSED_WAKE`, `markLateLinker`) — the late-linker case #3337 deferred the walk for. Companion-bearing nodes written outside a flush are re-synced as the flush begins (`unflushedCompanions`, `resyncUnflushedCompanions` inside the running window); a companion created lazily backfills as the owner's transaction's write (`backfillCompanion`, #3336) and, when the source is unflushed at its creation, joins that re-sync (`joinUnflushedResync`) — brought current by the optimistic write like a companion that existed at the write, so a derivation over `latest()` direct-commits as the optimistic view it is instead of being staged under whatever hold the round entered. The read sites test one module flag (`unflushedStaged`, set when a node is staged or a held node rewritten outside a flush, cleared at flush start) rather than `_running`; the write-path arms (`stashHeldRewrite`, `notePromotedWrite`) are cold helpers gated on loads the write already pays. No mid-tick `latest()` pull (#2922 superseded). A write to a held node from mainline no longer enters the transaction eagerly (the node is already its; entering captured the caller's block). Store: a key first read under a held FOLD is born holding (`heldFoldTransition`, `stageHeldKey`), stale/owner-less readers of a transaction-held backing see committed through every channel (`heldFromReader`, `foreignHold`) — #3336's store half, lifted from #3337. +**Mechanism (index, 2026-09-15 — as landed):** read-side, no marker on the plain write path. "Unflushed" is structural: outside a flush (`!globalQueue._running`), a node with an ambient staged value (`_pendingValue` set, `_transition` null) was written since the last flush — ambient staging commits at flush end, so nothing else leaves a node in that state (`unflushedValue`, core.ts). Exempt: engine companions (`_parentSource` — written at the source's write to mirror it, installing eagerly, A8) and writes issued inside a creation-time recompute (`CONFIG_PROMOTED`, set only when `!_running && context !== null` — A28 (4): promoted at that recompute's end). A rewrite of a HELD node stashes the staged value the last flush left (`_x._flushedStaged`, `unflushedRewrites`) and `latest()`/verdicts answer with it until the next flush. Optimistic writes (5): `_overrideTime === clock` outside a flush is the unflushed override (`unflushedOverride`); readers skip it (`read()`'s override arm, store `nodeValue`/`visibleOverride`/`optimisticView(…, draft=false)`), the writer's channels — the draft, the `affects()` declaration walk — compose on it. Readers served the flushed value because of an unflushed write are latched for the carrying flush (`REACTIVE_MISSED_WAKE`, `markLateLinker`) — the late-linker case #3337 deferred the walk for. Companion-bearing nodes written outside a flush are re-synced as the flush begins (`unflushedCompanions`, `resyncUnflushedCompanions` inside the running window); a companion created lazily backfills as the owner's transaction's write (`backfillCompanion`, #3336) and, when the source is unflushed at its creation, joins that re-sync (`joinUnflushedResync`) — brought current by the optimistic write like a companion that existed at the write, so a derivation over `latest()` direct-commits as the optimistic view it is instead of being staged under whatever hold the round entered. The read sites test one module flag (`unflushedStaged`, set when a node is staged or a held node rewritten outside a flush, cleared at flush start) rather than `_running`; the write-path arms (`stashHeldRewrite`, `notePromotedWrite`) are cold helpers gated on loads the write already pays. No mid-tick `latest()` pull (#2922 superseded). A write to a held node from mainline does not enter the transaction at the write (entering captured the caller's block) — it records the transaction for the next flush's start, which adopts the tick's batch into it (A34, #3494; #3473 had dropped the entry outright, and with it the tick's grouping). Store: a key first read under a held FOLD is born holding (`heldFoldTransition`, `stageHeldKey`), stale/owner-less readers of a transaction-held backing see committed through every channel (`heldFromReader`, `foreignHold`) — #3336's store half, lifted from #3337. (**ruled 2026-09-08**; supersedes the #2922 mid-tick pull) **A write becomes visible at flush — to every channel.** Between `set(x)` and the next `flush()` the write is _unflushed_: it is not the committed value, not the staged value `latest()`/`isPending()` serve, and not an input to any recompute. `latest(x)` reads the **flushed staged world** — the newest value a flush has processed, held or not (a transaction's hold governs what _effects_ publish, not what `latest` answers; `latest` opts into that tearing, but only once a flush has carried the write) — so `setCount(30); latest(count)` is the pre-write answer until the flush that carries the write, after which `latest(count)` is 30 and `latest(doubled)` is 60 in the same instant (the derivation flushed with it). Nothing is ever 30 while its derivations are still 20-shaped: there is no "read your own write" channel that bypasses the flush, because no channel can show downstream of an unflushed write, and a channel that shows the write alone tears against every derivation. Consequences: (1) `latest` is one rule regardless of reader — event handler, memo, prop getter — so wrapping a `latest` read in a memo does not change what it answers (the visibility mismatch that motivated a separate `readStaged` does not arise); (2) `isPending(x)` is false for an unflushed write (nothing is observable yet to be pending _from_); (3) a companion created lazily after several flushes needs no retained history — the flushed staged value is the only answer it could ever have given — and it answers **as if it had always existed** (#3336): its backfill is the write of the transaction _holding_ that value, not of the ambient window the reader happens to run in, so it lives and reverts with the hold instead of reverting at the reader's flush end; the same one level down for a store key first read under a hold — the node is born with the committed value and the held write staged as the holding transaction's, so plain reads and `latest()` answer for an unobserved key exactly as they do for one some other reader had materialized before the hold; (4) `flush()` is the boundary, so the imperative idiom is `set(x); flush(); latest(x)`, and within a recompute or a flush every write issued is promoted at the end of that recompute/round so the graph never runs a round against a value it wrote but cannot see; (5) (**extended 2026-09-10**, "let's match" — React's `useOptimistic` shows the optimistic value on the next render, never synchronously) **an optimistic write is a write**: `setOptimistic(x)` becomes the _active_ override (A17) at the flush that carries it, and until then no reader — plain, `snapshot()`, `isPending()` — sees it. An ambient one (no action in flight) is installed at the flush's start and reverted at its end, so effects are the channel that shows it (`[1, 2, 1]`); one an action holds stays readable after the flush for the action's lifetime. The writer's own composition channels see the parked write exactly as they see a plain write's staged value: a functional updater, and a store draft (`setState(s => …)` composes on the tick's earlier setters — two `count++` are +2, a push after a push lands in the next slot, a toggle toggled back diffs against the first and emits the write that cancels it). The `affects()` declaration walk is a writer channel too: tagging a parent covers the whole record as the writer sees it, so a same-tick `affects(state)` after an optimistic push covers the pushed row (the walk composes the tick's parked writes, as it already walks a plain store's pending backing). Only the slot form on a row born this tick needs the draft — `state.rows[2]` is not readable before the flush, so it is named as `affects(s.rows[2], key)` inside the setter (the same target the flush will serve). Engine companions (the `latest()` shadow, the `isPending()` verdict signal — `_parentSource` set) are the system's own overrides, written inside the flush after its promotions to mirror flushed state (A8), and install eagerly. @@ -198,17 +198,17 @@ A resting optimistic node reports pending via exactly the causes a plain async m ### A15. Transition entanglement is graph-driven; lanes settle as one reveal -**Status:** **ruled, amended in place** 2026-07-06 (promoted from B3) — maintainer keep, 2026-07-06; amended 2026-09-14 (#3407: a shared render effect entangles nothing by itself — see the shared-hole corollary); mechanism completed 2026-09-14 (#3443: a held memo made pending by another flight entangles at the propagation, not at its next pass); lanes corollary extended 2026-09-15 (#3460, maintainer: "a held lane is basically a micro transition from the outside… we wouldn't hold a sync write on a transition. Lanes are the same"); reveal corollary's stale-reader term completed 2026-09-15 (#3458 first observer; #3463 a zombie reader is live for the hold) -**Pinned by:** `tests/spec-async-semantics.test.ts`; `tests/shared-effect-no-entangle.test.ts` (#3407); `tests/overlapping-flights.test.ts` (#3443: two flights through one memo reveal once; the effect arm stays parallel; the second flight's own write is held with the first); `tests/lane-outside-view.test.ts` (#3460: a `latest()` / `createOptimistic` reader mounted or re-run by a sync write mid-hold shows the committed value and reveals with the lane; #3463: a reader whose removal is staged holds the lane while it is visible, a plain removal still releases at once); `tests/first-observer-stale-reader.test.ts` (#3458: a stale reader that is a flight's first observer holds the transaction on it); `tests/posture-born-held-and-observation.test.ts` (posture matrix, ruled 2026-09-15: `latest(x)` evaluated inside another live action ENTANGLES — "optimistic lanes are transition-bound, so it does need to entangle; that doesn't mean optimism for both can't poke through in the meanwhile" — the held value is served at once, the two transactions reveal as one) -**Mechanism (index, 2026-09-14):** `_asyncReporters`, `mergeTransitionState`, `laneHeld` / `waitingTransition` (#3335); `sourceObserved` (the live-reporter test, shared by the verdict, the lane hold and the landing, #3426); `recompute`'s stamp re-entry is memo-only and `settleTransition` → `enterWaiting` folds every waiter in at the landing (#3407); `notifyStatus`'s pending propagation onto a memo another live transaction holds (stamped, and pending or staged) enters it (`initTransition(sub._transition)`, #3443). `readsHeldCommitted` (lanes.ts; from `overrideRead` — `read()`'s override arm's single engine hook `GlobalQueue._overrideRead`, which also carries the A18 supersession selection — and `latestRead`) serves a render effect off a held lane the committed value and queues its re-run on the lane's render queue (#3460); `heldFromStale` notifies a first observer's pending status up its queue chain under the transaction (#3458); `reporterBlocksSource` / `sourceObserved` take the judged transaction (`verdict`) and count a `REACTIVE_ZOMBIE` reporter as live unless the verdict is the commit that disposes it (#3463). +**Status:** **ruled, amended in place** 2026-07-06 (promoted from B3) — maintainer keep, 2026-07-06; amended 2026-09-14 (#3407: a shared render effect entangles nothing by itself — see the shared-hole corollary); mechanism completed 2026-09-14 (#3443: a held memo made pending by another flight entangles at the propagation, not at its next pass); lanes corollary extended 2026-09-15 (#3460, maintainer: "a held lane is basically a micro transition from the outside… we wouldn't hold a sync write on a transition. Lanes are the same"); reveal corollary's stale-reader term completed 2026-09-15 (#3458 first observer; #3463 a zombie reader is live for the hold); stale-reader liveness through a memo and beside a same-flush release, companions never block the settle, 2026-09-16 (#3494) +**Pinned by:** `tests/spec-async-semantics.test.ts`; `tests/shared-effect-no-entangle.test.ts` (#3407); `tests/overlapping-flights.test.ts` (#3443: two flights through one memo reveal once; the effect arm stays parallel; the second flight's own write is held with the first); `tests/lane-outside-view.test.ts` (#3460: a `latest()` / `createOptimistic` reader mounted or re-run by a sync write mid-hold shows the committed value and reveals with the lane; #3463: a reader whose removal is staged holds the lane while it is visible, a plain removal still releases at once); `tests/first-observer-stale-reader.test.ts` (#3458: a stale reader that is a flight's first observer holds the transaction on it); `tests/write-proposals-3494.test.ts` (#3494: a reveal in the flush that retires the last reader holds; removing the last reader releases despite a mainline `latest()`); `tests/posture-born-held-and-observation.test.ts` (posture matrix, ruled 2026-09-15: `latest(x)` evaluated inside another live action ENTANGLES — "optimistic lanes are transition-bound, so it does need to entangle; that doesn't mean optimism for both can't poke through in the meanwhile" — the held value is served at once, the two transactions reveal as one) +**Mechanism (index, 2026-09-14):** `_asyncReporters`, `mergeTransitionState`, `laneHeld` / `waitingTransition` (#3335); `sourceObserved` (the live-reporter test, shared by the verdict, the lane hold and the landing, #3426); `recompute`'s stamp re-entry is memo-only and `settleTransition` → `enterWaiting` folds every waiter in at the landing (#3407); `notifyStatus`'s pending propagation onto a memo another live transaction holds (stamped, and pending or staged) enters it (`initTransition(sub._transition)`, #3443). `readsHeldCommitted` (lanes.ts; from `overrideRead` — `read()`'s override arm's single engine hook `GlobalQueue._overrideRead`, which also carries the A18 supersession selection — and `latestRead`) serves a render effect off a held lane the committed value and queues its re-run on the lane's render queue (#3460); `heldFromStale` notifies a first observer's pending status up its queue chain under the transaction (#3458); `reporterBlocksSource` / `sourceObserved` take the judged transaction (`verdict`) and count a `REACTIVE_ZOMBIE` reporter as live unless the verdict is the commit that disposes it (#3463); `reporterBlocksSource`'s deps scan follows a dep's `_pendingSources` one hop, so a stale reader served a held memo's committed value counts as observing the memo's flight (#3494); `transitionBlocked` (optimistic.ts) skips companions (`_parentSource`) — a `latest()` shadow backfilled under the transaction observes the flight and never holds the settle (#3494). -(was B3) Transition entanglement is graph-driven: writes whose async work is observed by a shared reader settle as one unit (no tearing — nothing commits until all entangled async resolves); writes on fully disjoint graphs keep independent transitions and settle independently. **Shared-hole corollary (amended 2026-09-14, #3407):** "observed by a shared reader" is a reader's _pass_ observing the flight pending — not the reader's mere existence. A render effect groups whatever bindings the compiler put in one hole, and a pass belongs to whoever dirtied it: a stamped effect (it observed one transaction's flight) dirtied by another transaction's write — a sync `action`, or a second flight's landing — runs that writer's pass, reads the held flight as a stale reader (its committed value, coherent with the flight's inputs which are also committed) and publishes with the writer; the two transactions stay parallel. Only a pass that _observes_ a pending flight — the reveal carve-out refused, next paragraph — joins that flight's transaction, and every transaction waiting on a flight completes at its landing. Maintainer: "we do want unrelated sync updates to pass through render effects… it makes no sense to the end user that separate bindings would hold"; "splitting a render effect per [binding] is a non-starter… the grouping cannot change." Consequence: `{b()}:{detailsA()}` publishes `1:0` when `b` is written (plainly or in an action) and `1:1` when `detailsA` lands; two independent flights read in one hole land at their own times. Memos keep the stamped re-entry (a memo's value _is_ its transaction's work), so entanglement through a user derivation of both stands — and it stands from the moment the second flight reaches the memo (#3443): pending _propagates_ onto a held memo without recomputing it (its inputs' values are unchanged), so the propagation itself enters the memo's transaction — when the memo is genuinely _held_ (pending on that transaction's work, or staged by it); a stamp alone decides nothing (#3334), so a second write that supersedes the first through a shared output memo does not drag the superseded flight into the live reveal; waiting for the memo's next pass let the first flight land, reveal its inputs (`A: 1`) beside the memo's committed value (`Sum: 0`), and left `Sum: 2` to arrive with `B: 1`. Consequence: the write that started the second flight is held with the first when its async work flows into a memo the first holds (`page=1` waits with `count=1` while `details` re-asks), even where a plain binding of the same write would have passed through — the async work, not the binding, is what is shared. **Lanes corollary (clarified 2026-09-09, #3335):** for optimistic writes the unit that settles is the _reveal_ — lanes merge through the shared reader (their effect queues become one) while transaction ownership stays put (A18 node corollary, #2912). The merged reveal is held while **any** member's observed async is in flight: a hold is a property of the async node — observed pending by a render reader in whichever live transaction recorded it (INV-3) — never of the root lane's transaction, which after a cross-transaction merge knows only one member's observations. Pinned: `tests/lane-hold-on-observation.test.ts` (#3335). **Reveal corollary (clarified 2026-09-09, re-ruled 2026-09-10; #3305, #3334):** a reveal that _discovers_ an async already in flight — a write that makes a render reader read a pending node for the first time — is that shared-reader observation: the reveal holds and joins the transition the flight blocks, settling as one unit with it, **whenever the flight's inputs are already visible** — committed by a batch that left the flight in the air with no observer (#3305), or revealed through an optimistic / `latest` lane (#3334). Showing the node's pre-flight (committed) value beside those inputs would tear the frame, and which transaction stamped the node says nothing about it. When the flight's inputs are themselves still held (unpublished, in some _other_ transaction), the reveal is a stale reader of a parallel transaction and follows the effects rule: it shows the node's committed value — coherent with the frame, whose inputs are also committed — does **not** entangle the two transactions, and re-derives at that transaction's commit (the reader is recorded for the commit replay). Corollary of the A18 node corollary: when the flight is lane-routed, the reveal waits on the _flight_, not on the transaction that owns the lane — an in-flight action holding that lane open does not hold the reveal once the flight lands. Pinned: `tests/spec-async-semantics.test.ts` (#3334, optimistic and `latest` sources; #3305 second reveal), `tests/stale-read-uninitialized-cross-transition.test.ts` (unpublished inputs: show committed, no entanglement), `tests/reveal-carve-out.test.ts`. **First observer (2026-09-15, #3458):** the stale reader's "joins the transaction's reporters when it has an entry" (#3374) has no gap: when the transaction has NO entry for the flight — it was in flight but nothing displayed it, and this reveal is its first observer — the observation registers it (the reader's status notification runs under the transaction), and the transaction waits on the flight it now shows a reader of. Before, `show → true` revealed `B: {b()}` as a stale reader of the `count=1` transaction (correctly served the committed `0`), the transaction was judged complete on `a` alone, and `Count: 1 | A: 1` published beside `B: 0`. Now one reveal, when both have landed. **Zombie readers (2026-09-15, #3463):** a reader whose removal is staged in a live transaction is still on screen and is live for every hold until the commit that disposes it — its say is moot only for the verdict of the transaction staging its removal (done, and the commit disposes it; not done, and it stays parked regardless). Before, it counted as dead everywhere, and a lane it alone held revealed `Value: 1` beside its `Details: 0`. A removal nothing else holds still commits at once and releases (the #3426 line). **Lanes mirror transitions (2026-09-15, #3460):** a held lane is a transaction seen from the outside. A render effect OFF the lane that reads what the lane is revealing — an override, a `latest()` shadow — is a stale reader of it: it shows the committed value (which is what is on screen: the lane defers its own readers' runs), publishes now, entangles nothing — "we wouldn't hold a sync write on a transition. Lanes are the same" — and re-derives at the release. Inside, a reader ON the lane computes the reveal and sees the lane's values and the parent transaction's landings, as before. `latest()` is not a special case: the same rule for a `createOptimistic` source. Before, only a reader under ANOTHER lane got the committed shadow; a mainline reader mounted mid-hold, or re-run by an unrelated sync write, showed the speculative `1` beside the lane's deferred `Value: 0`. Now `Late: 0` at the mount and `Details: 1 | Late: 1 | Value: 1` at the release; a sibling sync write re-runs `Both` to `0 y` at once and the release brings `1 y`. +(was B3) Transition entanglement is graph-driven: writes whose async work is observed by a shared reader settle as one unit (no tearing — nothing commits until all entangled async resolves); writes on fully disjoint graphs keep independent transitions and settle independently. **Shared-hole corollary (amended 2026-09-14, #3407):** "observed by a shared reader" is a reader's _pass_ observing the flight pending — not the reader's mere existence. A render effect groups whatever bindings the compiler put in one hole, and a pass belongs to whoever dirtied it: a stamped effect (it observed one transaction's flight) dirtied by another transaction's write — a sync `action`, or a second flight's landing — runs that writer's pass, reads the held flight as a stale reader (its committed value, coherent with the flight's inputs which are also committed) and publishes with the writer; the two transactions stay parallel. Only a pass that _observes_ a pending flight — the reveal carve-out refused, next paragraph — joins that flight's transaction, and every transaction waiting on a flight completes at its landing. Maintainer: "we do want unrelated sync updates to pass through render effects… it makes no sense to the end user that separate bindings would hold"; "splitting a render effect per [binding] is a non-starter… the grouping cannot change." Consequence: `{b()}:{detailsA()}` publishes `1:0` when `b` is written (plainly or in an action) and `1:1` when `detailsA` lands; two independent flights read in one hole land at their own times. Memos keep the stamped re-entry (a memo's value _is_ its transaction's work), so entanglement through a user derivation of both stands — and it stands from the moment the second flight reaches the memo (#3443): pending _propagates_ onto a held memo without recomputing it (its inputs' values are unchanged), so the propagation itself enters the memo's transaction — when the memo is genuinely _held_ (pending on that transaction's work, or staged by it); a stamp alone decides nothing (#3334), so a second write that supersedes the first through a shared output memo does not drag the superseded flight into the live reveal; waiting for the memo's next pass let the first flight land, reveal its inputs (`A: 1`) beside the memo's committed value (`Sum: 0`), and left `Sum: 2` to arrive with `B: 1`. Consequence: the write that started the second flight is held with the first when its async work flows into a memo the first holds (`page=1` waits with `count=1` while `details` re-asks), even where a plain binding of the same write would have passed through — the async work, not the binding, is what is shared. **Lanes corollary (clarified 2026-09-09, #3335):** for optimistic writes the unit that settles is the _reveal_ — lanes merge through the shared reader (their effect queues become one) while transaction ownership stays put (A18 node corollary, #2912). The merged reveal is held while **any** member's observed async is in flight: a hold is a property of the async node — observed pending by a render reader in whichever live transaction recorded it (INV-3) — never of the root lane's transaction, which after a cross-transaction merge knows only one member's observations. Pinned: `tests/lane-hold-on-observation.test.ts` (#3335). **Reveal corollary (clarified 2026-09-09, re-ruled 2026-09-10; #3305, #3334):** a reveal that _discovers_ an async already in flight — a write that makes a render reader read a pending node for the first time — is that shared-reader observation: the reveal holds and joins the transition the flight blocks, settling as one unit with it, **whenever the flight's inputs are already visible** — committed by a batch that left the flight in the air with no observer (#3305), or revealed through an optimistic / `latest` lane (#3334). Showing the node's pre-flight (committed) value beside those inputs would tear the frame, and which transaction stamped the node says nothing about it. When the flight's inputs are themselves still held (unpublished, in some _other_ transaction), the reveal is a stale reader of a parallel transaction and follows the effects rule: it shows the node's committed value — coherent with the frame, whose inputs are also committed — does **not** entangle the two transactions, and re-derives at that transaction's commit (the reader is recorded for the commit replay). Corollary of the A18 node corollary: when the flight is lane-routed, the reveal waits on the _flight_, not on the transaction that owns the lane — an in-flight action holding that lane open does not hold the reveal once the flight lands. Pinned: `tests/spec-async-semantics.test.ts` (#3334, optimistic and `latest` sources; #3305 second reveal), `tests/stale-read-uninitialized-cross-transition.test.ts` (unpublished inputs: show committed, no entanglement), `tests/reveal-carve-out.test.ts`. **First observer (2026-09-15, #3458):** the stale reader's "joins the transaction's reporters when it has an entry" (#3374) has no gap: when the transaction has NO entry for the flight — it was in flight but nothing displayed it, and this reveal is its first observer — the observation registers it (the reader's status notification runs under the transaction), and the transaction waits on the flight it now shows a reader of. Before, `show → true` revealed `B: {b()}` as a stale reader of the `count=1` transaction (correctly served the committed `0`), the transaction was judged complete on `a` alone, and `Count: 1 | A: 1` published beside `B: 0`. Now one reveal, when both have landed. **Through a memo, and beside a release (2026-09-16, #3494):** the stale reader's liveness is judged by what it derives from, not by whether it turned pending — served the committed value, it never did. Read through a memo of the flight (`copy = createMemo(details)`), its only trace of the flight is the memo between them, and the liveness scan follows a dependency's own pending sources. This matters most when the reveal and a release share one flush: `setShow(true); setDirect(false)` retires the flight's last reader (O3) and reveals a new one in the same pass — the reveal wins, the transaction waits, and `Count: 1` reveals with `Copy: 1` instead of beside a visible `Copy: 0` still waiting on `count=1`'s answer. And the last reader's removal releases the write even when a mainline `latest(details)` follows it: the shadow it creates is backfilled under the transaction (A28 (3)) and is an observation, never a blocker of the settle (A23) — before, it held `count=1` until the orphaned request landed. Pinned: `tests/write-proposals-3494.test.ts`. **Zombie readers (2026-09-15, #3463):** a reader whose removal is staged in a live transaction is still on screen and is live for every hold until the commit that disposes it — its say is moot only for the verdict of the transaction staging its removal (done, and the commit disposes it; not done, and it stays parked regardless). Before, it counted as dead everywhere, and a lane it alone held revealed `Value: 1` beside its `Details: 0`. A removal nothing else holds still commits at once and releases (the #3426 line). **Lanes mirror transitions (2026-09-15, #3460):** a held lane is a transaction seen from the outside. A render effect OFF the lane that reads what the lane is revealing — an override, a `latest()` shadow — is a stale reader of it: it shows the committed value (which is what is on screen: the lane defers its own readers' runs), publishes now, entangles nothing — "we wouldn't hold a sync write on a transition. Lanes are the same" — and re-derives at the release. Inside, a reader ON the lane computes the reveal and sees the lane's values and the parent transaction's landings, as before. `latest()` is not a special case: the same rule for a `createOptimistic` source. Before, only a reader under ANOTHER lane got the committed shadow; a mainline reader mounted mid-hold, or re-run by an unrelated sync write, showed the speculative `1` beside the lane's deferred `Value: 0`. Now `Late: 0` at the mount and `Details: 1 | Late: 1 | Value: 1` at the release; a sibling sync write re-runs `Both` to `0 y` at once and the release brings `1 y`. ### A30. A memo's dependencies are the committed frame's until the frame is replaced -**Status:** **ruled** 2026-09-13 (#3410) — maintainer ruling, Cluster 4 triage; the dependency twin of held children (#3404); amended 2026-09-15 (#3469: an unchanged pass waits on the flush's verdict) -**Pinned by:** `tests/held-conditional-memo.test.ts` (#3410: a memo whose held pass stopped reading an input still follows a mainline write to it); `tests/held-conditional-effect.test.ts` (#3438: an effect whose held pass stopped reading an input, its run stashed, still follows a mainline write to it); `tests/async-landing-deps-3461.test.ts` (#3461: an async memo whose held flight stopped reading an input, its landing staged, still follows a mainline write to it); `tests/held-frame-dependencies.test.ts` (#3469: a memo whose held pass computed the same value still follows its committed inputs; the render-effect arm follows them at once) -**Mechanism (index, 2026-09-15):** `recompute` trims the previous pass's dependency tail (`trimStaleDeps`) only when the pass published or changed nothing (`_pendingValue === NOT_PENDING`, no `_error`) and, for an effect, owes no run (`_modified` clear); a pass that staged its value leaves the tail linked and `commitPendingNode` trims it after a clean pass (`_error == null`); an effect pass that direct-committed and owes a run leaves it for `runEffect` to trim once the run applies (again after a clean pass). `__OBSERVE__` fan-in counting and the attribution engine's subscription diff walk the validated prefix only. An async landing (`asyncWrite`) follows the same gate (#3461): it trims only when it published (`_pendingValue === NOT_PENDING` after the write, an equal-value or lane landing); a transition-held landing leaves the tail for `commitPendingNode`. An unchanged pass (#3469) trims at its tail only when created, OPT-dirty, or a tracked effect; otherwise its stale tail goes to `heldTrims`, trimmed by `commitPendingNodes` when the flush commits and dropped (tail kept) when it parks. +**Status:** **ruled** 2026-09-13 (#3410) — maintainer ruling, Cluster 4 triage; the dependency twin of held children (#3404); amended 2026-09-15 (#3469: an unchanged pass waits on the flush's verdict); amended 2026-09-16 (#3494 review, fuzzer: a pending mark over the kept tail re-derives the node); refined 2026-09-17 (#3519 review: re-derive, not skip — skipped, the committed frame published stale beside its new inputs) +**Pinned by:** `tests/held-conditional-memo.test.ts` (#3410: a memo whose held pass stopped reading an input still follows a mainline write to it); `tests/held-conditional-effect.test.ts` (#3438: an effect whose held pass stopped reading an input, its run stashed, still follows a mainline write to it); `tests/async-landing-deps-3461.test.ts` (#3461: an async memo whose held flight stopped reading an input, its landing staged, still follows a mainline write to it); `tests/held-frame-dependencies.test.ts` (#3469: a memo whose held pass computed the same value still follows its committed inputs; the render-effect arm follows them at once); `tests/write-proposals-3494.test.ts` (fuzzer latest-1 #2141: a reader whose held pass stopped reading a memo is not registered on the memo's next flight through its kept tail; #3519 review: a kept-tail dep going pending under another transaction holds the committed frame — `query=1` never publishes beside a `selected` derived from `remote(0)`) +**Mechanism (index, 2026-09-15; 2026-09-17):** `notifyStatus`'s dependent walk, for a `STATUS_PENDING` mark whose link lies past the subscriber's `_depsTail` (`link._gen !== sub._depGen`, async.ts — the pass generation `link()` stamps on the validated prefix, O(1)), enqueues the subscriber for a recompute and stops — no `_pendingSources` entry, no reporter registration, no holder entanglement, no downstream propagation; clears and errors ride every link. `recompute` trims the previous pass's dependency tail (`trimStaleDeps`) only when the pass published or changed nothing (`_pendingValue === NOT_PENDING`, no `_error`) and, for an effect, owes no run (`_modified` clear); a pass that staged its value leaves the tail linked and `commitPendingNode` trims it after a clean pass (`_error == null`); an effect pass that direct-committed and owes a run leaves it for `runEffect` to trim once the run applies (again after a clean pass). `__OBSERVE__` fan-in counting and the attribution engine's subscription diff walk the validated prefix only. An async landing (`asyncWrite`) follows the same gate (#3461): it trims only when it published (`_pendingValue === NOT_PENDING` after the write, an equal-value or lane landing); a transition-held landing leaves the tail for `commitPendingNode`. An unchanged pass (#3469) trims at its tail only when created, OPT-dirty, or a tracked effect; otherwise its stale tail goes to `heldTrims`, trimmed by `commitPendingNodes` when the flush commits and dropped (tail kept) when it parks. A pass that _staged_ its value has not replaced the committed frame, so the committed value still derives from the previous pass's dependencies and a write to one of them must reach the node — and, through the node's `_transition` stamp, join its hold — exactly as an unconditional read would. Before: `selected = fixed() ? 2 : count()` held on `fixed → true` dropped `count` at its held pass, and a mainline `count` write then published `Count: 1` beside the committed `Selected: 0` / `Fixed: false`. Decided at commit rather than at the pass because a plain flush knows nothing at recompute time: the transaction that ends up holding the pass may open later in the same flush (an async memo downstream pends and the batch is adopted). An errored pass (a throw, NotReady included, a comparator throw) keeps its full list as before, and the commit skips the trim by the same `_error`. Cost on the plain path: none — a pass that publishes trims at its tail as before; only a staged pass moves the trim to the same flush's commit. @@ -218,6 +218,8 @@ An async memo's frame is replaced by its landing, not by the pass that registere A pass that changed nothing replaced nothing either (#3469). "Published or changed nothing" is not one case: a pass that changed nothing under a hold still left the committed frame deriving from the previous pass's dependencies, and it cannot know at its own tail whether the flush that ran it will park with its inputs held. Before: `selected = b() ? b() : a()` held on `b → 1` computed `1`, equal to the `1` it had from `a`, and trimmed `a`; the flush parked (b's flight was observed), and the mainline `a=2` never reached it — `A: 2 | B: 0 | Selected: 1`. Now the trim waits on the flush's verdict: trimmed when it commits, kept when it parks (the tail stays linked until a committing pass trims it — one spurious recompute at most). The consequence is the memo rule's: the `a=2` pass re-derives `selected`, is served the staged `b` and enters the hold (A29), so `A: 2` reveals with `B: 1` — the same held outcome as `sum = a() + b()` has always had, and one of the two outcomes the report accepts. The render-effect arm follows its inputs at once instead (`Selected: 2` on the write, `1` at the commit): a stale reader is served the committed `b`, and a sync write is never held by a transaction. A creation pass, an OPT-dirty pass and a tracked effect trim at their tails as before — their frames are replaceable like a direct commit, and a tracked effect's spurious run would be user-visible. +A pending mark over the kept tail re-derives the node; it neither marks it nor skips it (#3494 review; fuzzer latest-1 #2141; #3519 review). The tail is kept so that a _write_ to a dependency the committed frame still derives from reaches the node, whose pass then decides what it reads. A source going _pending_ behind that tail is a question for that same pass, not a fact about the node's current one. Marked (the A15 arm: `_pendingSources`, reporter registration, the holder entangled with the flight), the node claimed a flight its held frame never reads: `show` written in the same tick as an action's proposal, then hidden from mainline — the hide rides with the action (A34) and its pass under the parked transaction stops reading the memo, unchanged, tail kept; the action's truth re-asks the memo, and the truth waited for a fetch nobody displays. Also a gated reader held hidden forever on a manual flight nobody awaited, its memo's held pass having switched branches (fuzzer branches-1 #1105). Skipped (the first amendment), the committed frame went stale beside its new inputs: a held pass switches `selected` to the constant `0` (unchanged, tail to `remote` kept, parked with its action); a mainline `query=1` re-asks `remote`, and `1 0` publishes — `selected` derived from `remote(0)` beside the new query. Re-derived, the pass decides: it reads the dep and registers through its own read (the read links before it throws; mid-pass, a dependency the pass has yet to reach registers the same way — the heap refuses a recomputing node); or it reads a held input and enters that transaction (A29 — `selected` reads the held `mode`, and the mainline tick joins the action's hold: `1 0` reveals at the action's end, and `isPending(selected)` reads false meanwhile, its value `0` final either way, A19); or it reads neither and is done. Clears and errors are unchanged: they ride every link, so a reader whose kept tail still points at a landing flight learns of the landing. + ### A33. A fallback-caught flight holds no transaction; a Loading reset moves the hold onto the boundary **Status:** **ruled** 2026-09-12 (#3375) — maintainer ruling; extended 2026-09-15 (#3459, maintainer confirmed: the hold transfers to the boundary rather than ending) @@ -228,6 +230,14 @@ A `` boundary showing its fallback is the display of everything under i The hold does not vanish; it moves onto the boundary (#3459). The readers behind the fallback are still pending, and a revealed `` shows nothing pending, so the boundary must wait for what they wait on. A reader already pending never re-notifies (status propagation dedupes on its `_pendingSources`), so after the reset the boundary's own `_sources` held only the fresh flights its readers started — a sibling's fast landing then revealed the boundary with the forwarded reader still in the air, `B: 1 | Fast: 1 | Slow: 0`. Now the reset collects the forwarded readers' sources from their transaction registrations (the one record of a forwarded reader) and stays on the fallback until the whole chain lands: `B: 1 | Loading` until `Slow: 1`, one coherent reveal. The writes the transaction released still commit at once (`B: 1` beside the fallback) — the transaction's hold and the boundary's are different things, and only the second survives the reset. Cost: one walk of the live transactions' reporter registrations per `on` change. +### A34. A write is a proposal: one on a held node entangles its tick; one that nets to the committed value is none + +**Status:** **ruled** 2026-09-16 (#3494) — maintainer: "a write to the same signal already set to that value would entangle I think. Unless it's committed, both are suggesting a value. If one finished before the other that would be odd." Reverses a mechanism choice #3473 landed unstated (a mainline write to a held node stopped entering the transaction — the entry leaked `activeTransition` into the caller's block, A29); the leak is fixed differently, the grouping restored. +**Pinned by:** `tests/write-proposals-3494.test.ts` (the contract question: repeating a held value holds the tick with it, as a differing write does; a coalesced toggle stamps nothing and pends nothing; the torn `[1, 0, 1]` effect input; the lost hide after a coalesced toggle; #3519 review: a lone same-value write to a held node does not capture the next unrelated tick; a writable memo written back to its committed value proposes nothing) +**Mechanism (index, 2026-09-16; 2026-09-17):** `setSignal`, on a node stamped by a transaction that is not active, enters it at once inside a flush (as before) and from mainline records it in `batchJoins` (scheduler.ts) and schedules — before the equality gate, so a repeat of the held value proposes too, and the repeat's own flush drains it (left for the next flush, the join adopted an unrelated tick — #3519 review); `flush()` enters each recorded transaction at its start (`initTransition`, adopting the tick's ambient batch; inside the flush's `try`, so a throwing comparator in the drop below cannot leave `_running` set; the fast sync path defers while a join waits), and mainline code between the write and the flush never sees `activeTransition` set. `initTransition`'s adoption loop drops an _unstamped_ node (`_transition === null`) — a signal, or a writable memo under `REACTIVE_MANUAL_WRITE` (`createSignal(fn)`'s setter; a computed's other stagings are its pass's result) — whose staged value equals its committed one (`_equals`; unstaged through `commitPendingNode`, which also snaps companions and clears the manual-write flag; not stamped, not pushed) — a node already a transaction's, folded in as a parked batch merges, carries a flushed proposal a later rewrite brought back to the committed value and is held, not proposal-free (fuzzer latest-2 #1470: dropped, its dead stamp made the commit skip the authoritative value that followed); `computePendingState` (verdict.ts) reads a staged value equal to the committed one as not pending. + +A write proposes a value for a node. **Held, both are suggestions (1):** while a transaction holds a proposal for a node (staged, uncommitted), a further write to that node — the same value again or another — is a second suggestion for the same slot, and the two cannot finish at different times: the writer's tick joins the hold and reveals with it. `setB(1)` held on an async reader of `b`, then `setA(1); setB(1)` in a later tick: A reveals with B when the flight lands, never alone (before #3473 it did, through the eager entry; after, `A: 1` published at once beside the held `B: 0` — gabbev's "verified grouping difference"). Same for `setA(1); setB(2)`, which the flight route already held. Inside an action body the same write merges the action's transaction with the holder's, as it always did. **Committed, a repeat is nothing (2):** a tick whose writes net to the node's committed value made no proposal — `setShow(false); setShow(true)` on a committed `true`. The node is not staged, not stamped into whatever transaction the tick opens, and pends nothing: `isPending(show)` stays false (A19: the observable value is final), and a later `setShow(false)` is a plain mainline write — the effect's pass is the writer's (A15 shared-hole), it publishes "hidden" at once, and dropping its read of the flight retires it as a reporter (O3) so the hold on `count` releases in the same drain. Before, the coalesced toggle left `show` staged at its own value, the hold's adoption stamped it, the verdict read it pending, and the hide was captured by the stamp — and then lost. Consequence for A26 (2): "a signal already written under the transaction rejoins it" is now literal — only a signal the transaction actually holds a value for is its. Consequence for A28's mechanism: the write-side entry is deferred, not removed. + ## Loading window and seeds ### A27. The commit-#0 loading window is loading-class and verdict-quiet diff --git a/packages/signals/src/core/async.ts b/packages/signals/src/core/async.ts index 1e1155af6..f68b87628 100644 --- a/packages/signals/src/core/async.ts +++ b/packages/signals/src/core/async.ts @@ -1004,6 +1004,30 @@ export function notifyStatus( } forEachDependent(el, (sub, link) => { sub._time = clock; + // A pending mark on a kept-tail link re-derives the subscriber instead of + // marking it (A30, #3494 review; fuzzer latest-1 #2141; #3519 review). + // Past `_depsTail` lie the committed frame's deps, kept by A30 because + // that frame still derives from them while the pass that dropped them is + // held (staged, or unchanged and parked). A source going pending there is + // a question for the node's NEXT pass, not a fact about its current one: + // marked, the node was registered as the flight's reporter and its holder + // entangled with the flight (the A15 arm below) on a dep the held frame + // never reads — an orphaned fetch held the truth (a hide joined to a + // parked action, A34), and a manual flight nobody awaited held a gated + // reader hidden forever (fuzzer branches-1 #1105). Skipped, the committed + // frame published stale beside its new inputs (`query=1` beside a + // `selected` derived from `remote(0)`). Re-derived, the pass decides: it + // reads the dep and registers through its own read, or reads a held input + // and enters that transaction (A29), or reads neither and is done. Clears + // and errors still ride every link. A link inside the prefix carries the + // pass's generation (`link()`), so the test is O(1); mid-pass the prefix + // is what the pass has read so far, and the heap refuses a recomputing + // node — a dep it has yet to reach registers through its own read. + if (status === STATUS_PENDING && link._gen !== sub._depGen) { + enqueueSub(sub); + schedule(); + return; + } if ( (status === STATUS_PENDING && pendingSource && diff --git a/packages/signals/src/core/core.ts b/packages/signals/src/core/core.ts index 765fae69b..c0ba32f22 100644 --- a/packages/signals/src/core/core.ts +++ b/packages/signals/src/core/core.ts @@ -97,6 +97,7 @@ import { globalQueue, GlobalQueue, insertSubs, + batchJoins, projectionWriteActive, queuePendingNode, heldTrims, @@ -2253,14 +2254,22 @@ export function setSignal(el: Signal | Computed, v: T | ((prev: T) => T throw new Error(ownedScopeWriteMessage(context)); } - // A write to a held node inside a flush joins the hold (the round's other - // work is the transaction's). From mainline it does not: the node is - // already the transaction's (stamped, in its pending list) and the rewrite - // commits with it; entering here left activeTransition set for the rest of - // the caller's block, so a memo created after the write — and the whole - // next flush — became the transaction's instead of mainline's (A28, A29). - if (el._transition && activeTransition !== el._transition && globalQueue._running) - globalQueue.initTransition(el._transition); + // A write to a held node is a second proposal on a contested node (A34, #3494): + // the writer's tick reveals with the hold, the same value or another. Inside + // a flush the round enters now; from mainline the entry waits for the next + // flush's start (batchJoins) — entering here left activeTransition set for + // the rest of the caller's block, so a memo created after the write became + // the transaction's instead of mainline's (A28, A29). Before the equality + // gate below: repeating the held value proposes it too — and that repeat + // leaves through the gate, so the join schedules its own flush here; left + // for the next flush to find, it adopted an unrelated tick (#3519 review). + if (el._transition && activeTransition !== el._transition) { + if (globalQueue._running) globalQueue.initTransition(el._transition); + else { + batchJoins.push(el._transition); // dupes: a bare return in initTransition + schedule(); + } + } // The optimistic write path lives with the engine: only optimisticSignal / // optimisticComputed callers and optimistic store nodes carry an diff --git a/packages/signals/src/core/optimistic.ts b/packages/signals/src/core/optimistic.ts index 750bc91ee..0c3d21bad 100644 --- a/packages/signals/src/core/optimistic.ts +++ b/packages/signals/src/core/optimistic.ts @@ -193,12 +193,17 @@ function laneOverride(el: Computed, value: unknown, lane: OptimisticLane): * while one of its optimistic nodes holds an active override that is still * pending on real (non-affects-sentinel) async. A derived override's flight is * the lane's own work, never authoritative — it does not hold the settle. + * Neither is a companion's (#3494): the `latest()` shadow backfilled under the + * owner's transaction (A28 (3)) is an observation of the flight, and a + * mainline `latest(details)` after the flight's last reader unmounted held the + * released write until the orphaned request landed. */ function transitionBlocked(transition: Transition): boolean { for (let i = 0; i < transition._optimisticNodes.length; i++) { const node = transition._optimisticNodes[i]; if ( !(node._config & CONFIG_DERIVED_OVERRIDE) && + node._x?._parentSource === undefined && hasActiveOverride(node) && "_statusFlags" in node && (node as Computed)._statusFlags & STATUS_PENDING && diff --git a/packages/signals/src/core/scheduler.ts b/packages/signals/src/core/scheduler.ts index 1262b9572..1ba3c20d2 100644 --- a/packages/signals/src/core/scheduler.ts +++ b/packages/signals/src/core/scheduler.ts @@ -430,6 +430,14 @@ export function wakeParked(): void { for (const t of transitions) wokenTransitions.includes(t) || wokenTransitions.push(t); schedule(); } +/** Transactions a mainline tick has PROPOSED against (A34, #3494): a write to a + * node one of them holds — the same value or another — is a second proposal + * on a contested node, and the tick reveals with the hold ("both are + * suggesting a value; if one finished before the other that would be odd"). + * Entered at the next flush's start, where the ambient batch is adopted; + * never from the write itself, which left `activeTransition` set across the + * caller's block and made creation after the write the transaction's (A29). */ +export const batchJoins: Transition[] = []; /** * Permanently halts the reactive system. Called when a user error escapes @@ -748,6 +756,7 @@ export class GlobalQueue extends Queue { this._queues[1].length === 0 && this._children.length === 0 && !wokenTransitions.length && + !batchJoins.length && // a join must drain in its own tick (#3519 review) canUseSimpleSyncFlush(this) ) { this._running = true; @@ -775,6 +784,10 @@ export class GlobalQueue extends Queue { this._running = true; resyncUnflushedCompanions(); // A28, see above try { + // The tick proposed against a hold (#3494): adopt its batch into it. + // Inside the try: the adoption runs user comparators (the no-proposal + // drop), and a throw there must not leave `_running` set. + while (batchJoins.length) this.initTransition(batchJoins.pop()); if (__DEV__) devCheckFlushStart(); // Before runHeap for the same reason as the fast drain above; late // subscribers (an effect reading a swept memo this flush) revive it, @@ -997,6 +1010,38 @@ export class GlobalQueue extends Queue { const adopted = this._running ? 0 : CONFIG_ADOPTED_UNFLUSHED; for (let i = 0; i < batch._pendingNodes.length; i++) { const node = batch._pendingNodes[i]; + // A tick that nets to the committed value proposed nothing (A34, #3494): + // `setShow(false); setShow(true)` beside a write that opens a hold + // left `show` staged at its own value, stamped, pending to the + // verdict, and its next mainline write held by a flight it never + // derived from. Unstage it here — its subscribers were walked at the + // write and re-derive the same value. Writes only: a signal's staging + // is always one, a computed's only under REACTIVE_MANUAL_WRITE + // (`createSignal(fn)`'s setter, #3519 review) — otherwise it is its + // pass's result, which may equal an uninitialized `undefined` (a + // born-held first pass). `_equals: false` opts out. The unstaging is + // the commit's own path (commitPendingNode with nothing staged): the + // manual-write flag, companions and the rest are cleaned up as a + // commit would, and the node is stamped nowhere. Unstamped + // only: a node already a transaction's — arriving here as a parked + // batch folds into a merge — carries a FLUSHED proposal a later + // rewrite brought back to the committed value; it is held, not + // proposal-free. Dropped, it kept the dead stamp, and the next write + // to it queued under the merged transaction a value the commit then + // skipped as another's (fuzzer latest-2 #1470, S3). + if ( + node._transition === null && + node._pendingValue !== NOT_PENDING && + (!(node as Computed)._fn || + ((node as Computed)._flags & REACTIVE_MANUAL_WRITE && + !((node as Computed)._statusFlags & STATUS_UNINITIALIZED))) && + node._equals && + node._equals(node._value, node._pendingValue) + ) { + node._pendingValue = NOT_PENDING; + commitPendingNode(node); + continue; + } node._transition = activeTransition; node._config |= adopted; activeTransition._pendingNodes.push(node); @@ -1550,7 +1595,6 @@ function reporterBlocksSource( // boundary consumes the flight, the hold is over (A33, ruled 2026-09-12, #3375). for (let q: IQueue | null = reporter._queue; q; q = q._parent) if (q._collectionType! & STATUS_PENDING && !q._initialized) return false; - if (reporter._x?._pendingSources?.has(source)) return true; // "Still derives from the source" is a question about THIS pass's reads: // the deps up to `_depsTail`. Past it lie the committed frame's — kept // linked by A30 until the commit trims them (a staged pass, an errored @@ -1560,6 +1604,10 @@ function reporterBlocksSource( // kept it (spec O3, same-flush form; fuzzer #3446 P1 cases 21/79). A // trimmed list ends at `_depsTail`, so the bound is free there; a pass // that read nothing has a null tail and derives from nothing. + // The registration is trusted: a pending mark rides only this pass's links + // (notifyStatus skips the kept tail), so a registered reporter read the + // source, or threw on it. + if (reporter._x?._pendingSources?.has(source)) return true; const tail = reporter._depsTail; for ( let dep = tail === null ? null : reporter._deps; @@ -1568,7 +1616,18 @@ function reporterBlocksSource( ) { let current = dep._dep as Signal | Computed | undefined; while (current) { - if (current === source || (current as any)._firewall === source) return true; + // Or through a memo pending on the flight (#3494): a stale reader + // served a held memo's committed value never turned pending itself, so + // its only trace of the flight is the memo between them — `copy` of + // `details`. Judged dead, its transaction released `count=1` beside + // the `Copy: 0` it displays. `_pendingSources` is transitive, so one + // hop covers any depth. + if ( + current === source || + (current as any)._firewall === source || + current._x?._pendingSources?.has(source) + ) + return true; current = current._x?._parentSource; } } diff --git a/packages/signals/src/core/verdict.ts b/packages/signals/src/core/verdict.ts index 62e39d2fe..95d44dae2 100644 --- a/packages/signals/src/core/verdict.ts +++ b/packages/signals/src/core/verdict.ts @@ -311,7 +311,16 @@ function computePendingState(el: Signal | Computed): boolean { // classification survives the landing (asyncWrite) and dies with the // commit (commitPendingNode) — verdict-quiet through the reveal, like // the loading window above (#3178). - if (!(comp._statusFlags & STATUS_UNINITIALIZED) && !comp._x?._reask) return true; + // A staged value equal to the committed one is no proposal (A34, #3494): the + // observable value IS final (A19). The coalesced `setShow(false); + // setShow(true)` read pending through the flush that carried it — and, + // stamped into a hold that flush opened, until the hold settled. + if ( + !(comp._statusFlags & STATUS_UNINITIALIZED) && + !comp._x?._reask && + (!el._equals || !el._equals(el._value as any, staged as any)) + ) + return true; } return newQuestionInFlight(comp); } diff --git a/packages/signals/tests/treeshake.test.ts b/packages/signals/tests/treeshake.test.ts index 29aaf6ecc..3bd4afad6 100644 --- a/packages/signals/tests/treeshake.test.ts +++ b/packages/signals/tests/treeshake.test.ts @@ -358,7 +358,15 @@ describe("pay-for-use tree-shaking (#2883)", () => { // 25,338 -> 25,426. The additive half; the twins it makes deletable // (overrideRead's wrapper, nodeValue, the verdict re-derivations) are the // deletion half — see docs/DESIGN-CONSOLIDATION.md §0. - expect(minifiedBytes).toBeLessThan(25_500); + // A write is a proposal (A34, #3494 / #3519 review, 2026-09-17): +234 B + // core-retained (25,426 -> 25,660 over move 3b) — `batchJoins` (setSignal records a + // held node's join and schedules; drained inside flush's try; the fast + // sync path defers while one waits), initTransition's no-proposal drop + // for an unstamped signal or writable memo staged at its committed value + // (through commitPendingNode), notifyStatus re-deriving a subscriber + // instead of marking it over a kept-tail link (`_gen`, A30), and + // reporterBlocksSource following a dep's `_pendingSources` one hop. + expect(minifiedBytes).toBeLessThan(25_750); }); it("plain stores shed the verdict layer, affects, boundaries, and map", async () => { diff --git a/packages/signals/tests/write-proposals-3494.test.ts b/packages/signals/tests/write-proposals-3494.test.ts new file mode 100644 index 000000000..262a7738b --- /dev/null +++ b/packages/signals/tests/write-proposals-3494.test.ts @@ -0,0 +1,584 @@ +import { describe, expect, it } from "vitest"; +import { + action, + createMemo, + createRenderEffect, + createRoot, + createSignal, + flush, + isPending, + latest +} from "../src/index.js"; + +let now = 0; +let timers: { at: number; run: () => void }[] = []; +function delay(ms: number, value?: T): Promise { + return new Promise(r => timers.push({ at: now + ms, run: () => r(value as T) })); +} +async function settle() { + for (let r = 0; r < 3; r++) { + for (let i = 0; i < 10; i++) await Promise.resolve(); + flush(); + } +} +async function advanceTo(t: number) { + while (true) { + const due = timers.filter(x => x.at <= t).sort((a, b) => a.at - b.at); + if (!due.length) break; + const next = due[0]; + timers = timers.filter(x => x !== next); + now = next.at; + next.run(); + await settle(); + } + now = t; + await settle(); +} +function reset() { + now = 0; + timers = []; +} +function text(fn: () => string, log: string[], when: number[]) { + createRenderEffect(fn, v => { + log.push(v); + when.push(now); + }); +} +function frames(log: string[], when: number[]) { + const m = new Map(); + log.forEach((l, i) => { + (m.get(when[i]) ?? m.set(when[i], []).get(when[i])!).push(l); + }); + return [...m].map(([t, ls]) => `${t}: ${ls.sort().join(" | ")}`); +} +/** Explicit flights (gabbev's standalone shape): each pass parks a resolver. */ +function requestQueue() { + const requests: (() => void)[] = []; + return { + ask(value: T) { + return new Promise(r => requests.push(() => r(value))); + }, + async land(n = Infinity) { + while (n-- > 0 && requests.length) requests.shift()!(); + await settle(); + }, + /** Resolve without yielding — the landing job runs after the caller's tick. */ + resolveNext() { + requests.shift()!(); + }, + /** The newest flight lands first; older ones stay up. */ + resolveLast() { + requests.pop()!(); + }, + get pending() { + return requests.length; + } + }; +} + +// A34 (#3494, gabbev, after #3473). A write PROPOSES a value. Ruled 2026-09-16: +// - a proposal on a node that already carries an uncommitted one — held by a +// transaction — entangles the proposer's tick with that hold, same value or +// not ("both are suggesting a value; if one finished before the other that +// would be odd"); +// - a tick whose proposal nets to the COMMITTED value made none: the node is +// not staged, not stamped, and pends nothing. +describe("A34 — a write is a proposal (#3494)", () => { + it("contract: repeating a held value entangles the tick, as a differing write does", async () => { + reset(); + const log: string[] = []; + const when: number[] = []; + let setA!: (v: number) => void; + let setB!: (v: number) => void; + createRoot(() => { + const [a, sa] = createSignal(0); + const [b, sb] = createSignal(0); + setA = sa; + setB = sb; + const obs = createMemo(() => delay(1000, b())); + text(() => `A: ${a()}`, log, when); + text(() => `B: ${b()}`, log, when); + text(() => `Obs: ${obs()}`, log, when); + }); + flush(); + await advanceTo(1000); + setB(1); + await settle(); + await advanceTo(1500); + // The repeat is a second proposal for `b`'s held `1`: A rides with it. + setA(1); + setB(1); + await settle(); + await advanceTo(3000); + expect(frames(log, when)).toEqual([ + "0: A: 0 | B: 0", + "1000: Obs: 0", + "2000: A: 1 | B: 1 | Obs: 1" + ]); + }); + + it("a differing mainline write to a held node holds its tick-mates with it", async () => { + reset(); + const log: string[] = []; + const when: number[] = []; + let setA!: (v: number) => void; + let setB!: (v: number) => void; + createRoot(() => { + const [a, sa] = createSignal(0); + const [b, sb] = createSignal(0); + setA = sa; + setB = sb; + const obs = createMemo(() => delay(1000, b())); + text(() => `A: ${a()}`, log, when); + text(() => `B: ${b()}`, log, when); + text(() => `Obs: ${obs()}`, log, when); + }); + flush(); + await advanceTo(1000); + setB(1); + await settle(); + await advanceTo(1500); + setA(1); + setB(2); + await settle(); + await advanceTo(4000); + // `b=2` re-asks obs (lands 2500); the whole tick reveals with it. + expect(frames(log, when)).toEqual([ + "0: A: 0 | B: 0", + "1000: Obs: 0", + "2500: A: 1 | B: 2 | Obs: 2" + ]); + }); + + it("a tick that nets to the committed value proposes nothing: no stamp, no verdict", async () => { + reset(); + const log: string[] = []; + const when: number[] = []; + let setCount!: (v: number) => void; + let setShow!: (v: boolean) => void; + let show!: () => boolean; + createRoot(() => { + const [count, sc] = createSignal(0); + const [sh, ss] = createSignal(true); + setCount = sc; + setShow = ss; + show = sh; + const data = createMemo(() => delay(2000, count())); + text(() => `Data: ${data()}`, log, when); + text(() => `Show: ${show()}`, log, when); + }); + flush(); + await advanceTo(2000); + setShow(false); + setShow(true); + setCount(1); + await settle(); + // `count=1` is held on data's flight; `show` was never written as far as + // the graph is concerned. + expect(isPending(show)).toBe(false); + expect(show()).toBe(true); + await advanceTo(2500); + setShow(false); + await settle(); + expect(show()).toBe(false); + await advanceTo(6000); + expect(frames(log, when)).toEqual([ + "0: Show: true", + "2000: Data: 0", + "2500: Show: false", + "4000: Data: 1" + ]); + }); + + // #3494 "torn effect input" (original comment A on #3473): a signal and its + // synchronous identity memo disagree inside one effect run, `[1, 0, 1]`. + it("an effect never receives a signal beside a stale identity memo of it", async () => { + reset(); + const q = requestQueue(); + const log: string[] = []; + let setCount!: (v: number) => void; + let setShow!: (v: boolean) => void; + createRoot(() => { + const [count, sc] = createSignal(0); + const [show, ss] = createSignal(false); + setCount = sc; + setShow = ss; + const details = createMemo(() => q.ask(count())); + const copy = createMemo(count); + createRenderEffect( + () => (show() ? `${count()} ${copy()} ${details()}` : null), + v => { + if (v) log.push(v); + } + ); + createRenderEffect(details, () => {}); + }); + flush(); + await q.land(); + setCount(1); + setCount(0); + await settle(); + setShow(true); + setCount(1); + await settle(); + await q.land(); + expect(log).toEqual(["1 1 1"]); + }); + + // #3494 "lost visibility update": coalesced show writes beside a held count; + // the later hide never publishes although every request has resolved. + it("a hide after a coalesced toggle publishes, and at once", async () => { + reset(); + const log: string[] = []; + const when: number[] = []; + let setCount!: (v: number) => void; + let setShow!: (v: boolean) => void; + createRoot(() => { + const [count, sc] = createSignal(0); + const [show, ss] = createSignal(true); + setCount = sc; + setShow = ss; + const data = createMemo(() => delay(2000, count())); + const panel = () => (show() ? data() : "hidden"); + text(() => `Data: ${panel()}`, log, when); + }); + flush(); + await advanceTo(2000); + setShow(false); + setShow(true); + setCount(1); + await settle(); + await advanceTo(2500); + setShow(false); + await settle(); + await advanceTo(6000); + // `show` proposed nothing at 2000, so the hide is a plain mainline write: + // the effect's pass is the writer's (A15 shared-hole), it stops reading + // `data`, and the hold on `count` — its last reader gone — releases (O3). + expect(frames(log, when)).toEqual(["2000: Data: 0", "2500: Data: hidden"]); + }); + + // #3494 "stale visible derivation": the flight's last reader leaves and a new + // reader reveals it in the same flush; the release won and `Count: 1` + // published beside a visible `Copy: 0` still waiting on `count=1`'s flight. + it("a reveal in the flush that retires the last reader holds the transaction", async () => { + reset(); + const log: string[] = []; + const when: number[] = []; + let setCount!: (v: number) => void; + let setShow!: (v: boolean) => void; + let setDirect!: (v: boolean) => void; + let copy!: () => number; + createRoot(() => { + const [count, sc] = createSignal(0); + const [show, ss] = createSignal(true); + const [direct, sd] = createSignal(true); + setCount = sc; + setShow = ss; + setDirect = sd; + const details = createMemo(() => delay(1500, count())); + copy = createMemo(details); + text(() => `Count: ${count()}`, log, when); + text(() => `Direct: ${direct() ? details() : "gone"}`, log, when); + text(() => `Copy: ${show() ? copy() : "hidden"}`, log, when); + }); + flush(); + await advanceTo(2000); + setShow(false); + await settle(); + await advanceTo(2500); + setCount(1); + latest(copy); + await settle(); + await advanceTo(3000); + setShow(true); + setDirect(false); + await settle(); + await advanceTo(6000); + expect(frames(log, when)).toEqual([ + "0: Count: 0", + "1500: Copy: 0 | Direct: 0", + "2000: Copy: hidden", + "3000: Copy: 0 | Direct: gone", + "4000: Copy: 1 | Count: 1" + ]); + }); + + // #3494 "delayed release": the only reader of the flight is removed and + // `latest()` is read untracked from mainline; the held write waited on the + // orphaned request anyway. + it("removing the last reader releases the write; a mainline latest() holds nothing", async () => { + reset(); + const q = requestQueue(); + const seen: number[] = []; + let count!: () => number; + let setCount!: (v: number) => void; + let details!: () => number; + let removeReader!: () => void; + createRoot(() => { + const [c, sc] = createSignal(0); + count = c; + setCount = sc; + details = createMemo(() => q.ask(count())); + createRenderEffect(count, v => { + seen.push(v); + }); + removeReader = createRoot(remove => { + createRenderEffect(details, () => {}); + return remove; + }); + }); + flush(); + await q.land(); + setCount(1); + await settle(); + // `1`'s answer lands in the tick that rewrites the held node (the landing + // job runs after the two writes, before the flush). + q.resolveNext(); + setCount(0); + setCount(1); + await settle(); + removeReader(); + latest(details); + await settle(); + expect(count()).toBe(1); + expect(seen).toEqual([0, 1]); + await q.land(); + expect(count()).toBe(1); + }); + + // Differential fuzzing of the ruling (gabbev's #3446 fuzzer, latest-2 #1470, + // S3). The action proposes 1, then 0 — the committed value — and its + // transaction folds into the one a mainline `show` opened. The no-proposal + // drop took the folded node for a fresh tick's, unstaged it and left its + // dead stamp; the authoritative 1 then queued under the merged transaction + // as another's and the commit skipped it — readers revealed 1 beside a + // source anchor stuck at 0. A node already a transaction's is held, not + // proposal-free. + it("a held proposal rewritten to the committed value stays the transaction's; its truth commits", async () => { + reset(); + const q = requestQueue(); + const log: string[] = []; + const when: number[] = []; + const gates: (() => void)[] = []; + let setShow!: (v: boolean) => void; + let run!: () => unknown; + createRoot(() => { + const [source, setSource] = createSignal(0); + const [show, ss] = createSignal(false); + setShow = ss; + const value = () => latest(source); + const node = createMemo(() => q.ask(value())); + run = action(function* () { + setSource(1); + yield new Promise(r => gates.push(r)); + setSource(0); + yield new Promise(r => gates.push(r)); + setSource(1); + }); + text(() => `Source: ${source()}`, log, when); + text(() => `Always: ${node()}`, log, when); + text(() => `Gated: ${show() ? node() : "hidden"}`, log, when); + }); + flush(); + await q.land(); + run(); + await settle(); + gates.shift()!(); + await settle(); + // The gated reader mounts on the flight for 0, which lands first. + setShow(true); + q.resolveLast(); + await settle(); + gates.shift()!(); + await settle(); + await q.land(); + const shown = (prefix: string) => log.filter(l => l.startsWith(prefix)); + expect(shown("Source").at(-1)).toBe("Source: 1"); + expect(shown("Always").at(-1)).toBe("Always: 1"); + expect(shown("Gated").at(-1)).toBe("Gated: 1"); + }); + + // Fuzzer latest-1 #2141 (O2). `show` and the action's proposal share a tick, + // so the hide is a second proposal on a held node and rides with the action + // (A34). Its pass under the parked transaction stopped reading the memo but + // changed nothing, so its tail stayed linked (A30); the action's truth + // re-asked the memo, and the pending mark rode that kept link back to the + // reader — registered as a reporter of a fetch it no longer asked for, it + // held the truth until the orphaned flight landed. A pending mark rides + // only the links a pass made. + it("a reader that stopped reading a memo is not held by the memo's next flight", async () => { + reset(); + const q = requestQueue(); + const log: string[] = []; + const when: number[] = []; + const gates: (() => void)[] = []; + let setShow!: (v: boolean) => void; + let run!: () => unknown; + createRoot(() => { + const [source, setSource] = createSignal(0); + const [show, ss] = createSignal(false); + setShow = ss; + const value = () => latest(source); + const node = createMemo(() => q.ask(value())); + run = action(function* () { + setSource(1); + yield new Promise(r => gates.push(r)); + setSource(0); + }); + text(() => `Latest: ${value()}`, log, when); + text(() => `Reader: ${show() ? node() : "hidden"}`, log, when); + }); + flush(); + await q.land(); + run(); + setShow(true); + await settle(); + setShow(false); + await settle(); + gates.shift()!(); + await settle(); + // The truth is on screen before the memo's orphaned flights land. + const shown = () => log.filter(l => l.startsWith("Latest")); + expect(q.pending).toBe(2); + expect(shown().at(-1)).toBe("Latest: 0"); + await q.land(); + expect(shown()).toEqual(["Latest: 0", "Latest: 1", "Latest: 0"]); + }); +}); + +// #3519 review. Three holes in the first cut, each pinned before its fix. +describe("A34 — review (#3519)", () => { + // The kept tail IS the committed frame's dependency list (A30). A flight on + // one of those deps makes the committed frame non-final — the frame that a + // mainline (or any OTHER transaction's) commit would publish beside its + // new inputs. The pending mark may be skipped only when the mark's + // transaction is the one holding the subscriber's replacement frame: that + // frame does not read the dep, and it is what that commit publishes. + it("a kept-tail dep going pending under another transaction holds the committed frame", async () => { + reset(); + const log: string[] = []; + const when: number[] = []; + let setQuery!: (v: number) => void; + let switchMode!: () => unknown; + let selected!: () => number; + createRoot(() => { + const [query, sq] = createSignal(0); + const [mode, setMode] = createSignal(false); + setQuery = sq; + const remote = createMemo(() => delay(1000, query())); + selected = createMemo(() => (mode() ? 0 : remote())); + switchMode = action(function* () { + setMode(true); + yield delay(10_000); + }); + text(() => `${query()} ${selected()}`, log, when); + }); + flush(); + await advanceTo(1000); + // A held pass switches `selected` to the constant 0 — unchanged, so its + // tail (remote) stays linked — and parks with the action. + switchMode(); + await settle(); + await advanceTo(1500); + // Mainline: a new flight on `remote`. The committed `selected` derives + // from remote=0; publishing `1 0` would be a torn frame. + setQuery(1); + await settle(); + await advanceTo(2000); + expect(frames(log, when)).toEqual(["1000: 0 0"]); + // The mark rode the kept link, `selected` re-derived, read the held + // `mode` and entered the action's transaction (A29): the tick is held. + // `selected` itself is not pending (A19): its observable value is 0 and + // stays 0 — the held frame is the constant, and the committed one only + // ever re-derives under a commit that publishes the held frame instead. + expect(isPending(selected)).toBe(false); + await advanceTo(3000); + // The flight landed; the tick is still held (the action runs to 11000). + expect(frames(log, when)).toEqual(["1000: 0 0"]); + await advanceTo(12_000); + expect(frames(log, when)).toEqual(["1000: 0 0", "11000: 1 0"]); + }); + + // A same-value write to a held node records the join, then leaves through + // setSignal's equality gate. Without a schedule the join outlived its tick + // and the next unrelated flush drained it — adopting that tick's work into + // a hold it never touched. + it("a lone same-value write to a held node does not capture the next unrelated tick", async () => { + reset(); + const log: string[] = []; + const when: number[] = []; + let setB!: (v: number) => void; + let setC!: (v: number) => void; + createRoot(() => { + const [b, sb] = createSignal(0); + const [c, sc] = createSignal(0); + setB = sb; + setC = sc; + const obs = createMemo(() => delay(1000, b())); + text(() => `Obs: ${obs()}`, log, when); + text(() => `C: ${c()}`, log, when); + }); + flush(); + await advanceTo(1000); + setB(1); + await settle(); + await advanceTo(1500); + setB(1); // the repeat: a proposal, and the tick's only write + await settle(); + await advanceTo(1600); + setC(1); // an unrelated tick + await settle(); + expect(frames(log, when)).toEqual(["0: C: 0", "1000: Obs: 0", "1600: C: 1"]); + await advanceTo(3000); + expect(frames(log, when)).toEqual(["0: C: 0", "1000: Obs: 0", "1600: C: 1", "2000: Obs: 1"]); + }); + + // A34 (2) for the memo form: `createSignal(fn)`'s setter stages through + // setSignal too, and a manual write back to the committed value is as much + // "no proposal" as a signal's. + it("a writable memo written back to its committed value proposes nothing", async () => { + reset(); + const log: string[] = []; + const when: number[] = []; + let setCount!: (v: number) => void; + let setShow!: (v: boolean) => void; + let show!: () => boolean; + createRoot(() => { + const [count, sc] = createSignal(0); + const [base] = createSignal(true); + const [sh, ss] = createSignal(() => base()); + setCount = sc; + setShow = ss; + show = sh; + const data = createMemo(() => delay(2000, count())); + text(() => `Data: ${data()}`, log, when); + text(() => `Show: ${show()}`, log, when); + }); + flush(); + await advanceTo(2000); + setShow(false); + setShow(true); + setCount(1); + await settle(); + expect(isPending(show)).toBe(false); + expect(show()).toBe(true); + await advanceTo(2500); + setShow(false); + await settle(); + expect(show()).toBe(false); + await advanceTo(6000); + // The hide is mainline (the point of the drop) — and the `Show` effect had + // computed under `count`'s born-held transaction at 2000 (it sits above + // the memo, so it ran after `data` pended), so it is a contested effect + // (#3322): the reveal at 4000 re-derives it against the committed world. + // Same value, one redundant run; the signal form above computes below the + // transaction's birth and is never contested. Not this rule's business. + expect(frames(log, when)).toEqual([ + "0: Show: true", + "2000: Data: 0", + "2500: Show: false", + "4000: Data: 1 | Show: false" + ]); + }); +}); diff --git a/scripts/size/.size-limit.js b/scripts/size/.size-limit.js index 03b221f51..bbd55746a 100644 --- a/scripts/size/.size-limit.js +++ b/scripts/size/.size-limit.js @@ -294,7 +294,14 @@ module.exports = [ // Five store/signal divergences fixed (posture-store-parity S4, S5, S7, S8; // S6 ruled and deferred); every paired matrix state row-identical. // Measured at 9,489 B (+39 over the rebased cap). - limit: "9.50 KB", + // A write is a proposal (A34, #3494 / #3519 review, 2026-09-17): 9,543 B against + // `next`'s 9,489 (+54 B). Core-retained: `batchJoins` (a held node's + // mainline write records the join and schedules; drained inside flush's + // try, the fast sync path defers to it), the adoption loop's no-proposal + // drop (signals and writable memos, through commitPendingNode), a + // kept-tail pending mark re-deriving its subscriber (A30), and + // reporterBlocksSource following `_pendingSources` one hop. + limit: "9.60 KB", modifyEsbuildConfig }, { @@ -566,7 +573,14 @@ module.exports = [ // Five store/signal divergences fixed (posture-store-parity S4, S5, S7, S8; // S6 ruled and deferred); every paired matrix state row-identical. // Measured at 16,610 B (+60 over the rebased cap). - limit: "16.65 KB", + // A write is a proposal (A34, #3494 / #3519 review, 2026-09-17): 16,645 B against + // `next`'s 16,574 (+71 B). Core-retained: `batchJoins` (a held node's + // mainline write records the join and schedules; drained inside flush's + // try, the fast sync path defers to it), the adoption loop's no-proposal + // drop (signals and writable memos, through commitPendingNode), a + // kept-tail pending mark re-deriving its subscriber (A30), and + // reporterBlocksSource following `_pendingSources` one hop. + limit: "16.70 KB", modifyEsbuildConfig }, { @@ -718,7 +732,14 @@ module.exports = [ // Five store/signal divergences fixed (posture-store-parity S4, S5, S7, S8; // S6 ruled and deferred); every paired matrix state row-identical. // Measured at 12,188 B (+38 over the rebased cap). - limit: "12.20 KB", + // A write is a proposal (A34, #3494 / #3519 review, 2026-09-17): 12,285 B against + // `next`'s 12,188 (+97 B). Core-retained: `batchJoins` (a held node's + // mainline write records the join and schedules; drained inside flush's + // try, the fast sync path defers to it), the adoption loop's no-proposal + // drop (signals and writable memos, through commitPendingNode), a + // kept-tail pending mark re-deriving its subscriber (A30), and + // reporterBlocksSource following `_pendingSources` one hop. + limit: "12.35 KB", modifyEsbuildConfig }, { @@ -828,7 +849,14 @@ module.exports = [ // Five store/signal divergences fixed (posture-store-parity S4, S5, S7, S8; // S6 ruled and deferred); every paired matrix state row-identical. // Measured at 12,274 B (+24 over the rebased cap). - limit: "12.30 KB", + // A write is a proposal (A34, #3494 / #3519 review, 2026-09-17): 12,351 B against + // `next`'s 12,274 (+77 B). Core-retained: `batchJoins` (a held node's + // mainline write records the join and schedules; drained inside flush's + // try, the fast sync path defers to it), the adoption loop's no-proposal + // drop (signals and writable memos, through commitPendingNode), a + // kept-tail pending mark re-deriving its subscriber (A30), and + // reporterBlocksSource following `_pendingSources` one hop. + limit: "12.40 KB", modifyEsbuildConfig }, { @@ -992,7 +1020,14 @@ module.exports = [ // Five store/signal divergences fixed (posture-store-parity S4, S5, S7, S8; // S6 ruled and deferred); every paired matrix state row-identical. // Measured at 20,148 B (+48 over the rebased cap). - limit: "20.20 KB", + // A write is a proposal (A34, #3494 / #3519 review, 2026-09-17): 20,208 B against + // `next`'s 20,148 (+60 B). Core-retained: `batchJoins` (a held node's + // mainline write records the join and schedules; drained inside flush's + // try, the fast sync path defers to it), the adoption loop's no-proposal + // drop (signals and writable memos, through commitPendingNode), a + // kept-tail pending mark re-deriving its subscriber (A30), and + // reporterBlocksSource following `_pendingSources` one hop. + limit: "20.25 KB", modifyEsbuildConfig }, { @@ -1200,6 +1235,13 @@ module.exports = [ // Five store/signal divergences fixed (posture-store-parity S4, S5, S7, S8; // S6 ruled and deferred); every paired matrix state row-identical. // Measured at 30,296 B (+46 over the rebased cap). + // A write is a proposal (A34, #3494 / #3519 review, 2026-09-17): 30,303 B against + // `next`'s 30,219 (+84 B). Core-retained: `batchJoins` (a held node's + // mainline write records the join and schedules; drained inside flush's + // try, the fast sync path defers to it), the adoption loop's no-proposal + // drop (signals and writable memos, through commitPendingNode), a + // kept-tail pending mark re-deriving its subscriber (A30), and + // reporterBlocksSource following `_pendingSources` one hop. limit: "30.35 KB", modifyEsbuildConfig }, @@ -1325,7 +1367,14 @@ module.exports = [ // Five store/signal divergences fixed (posture-store-parity S4, S5, S7, S8; // S6 ruled and deferred); every paired matrix state row-identical. // Measured at 15,342 B (+42 over the rebased cap). - limit: "15.40 KB", + // A write is a proposal (A34, #3494 / #3519 review, 2026-09-17): 15,461 B against + // `next`'s 15,342 (+119 B). Core-retained: `batchJoins` (a held node's + // mainline write records the join and schedules; drained inside flush's + // try, the fast sync path defers to it), the adoption loop's no-proposal + // drop (signals and writable memos, through commitPendingNode), a + // kept-tail pending mark re-deriving its subscriber (A30), and + // reporterBlocksSource following `_pendingSources` one hop. + limit: "15.50 KB", modifyEsbuildConfig }, { @@ -1438,7 +1487,14 @@ module.exports = [ // flush; +56 B minified in the signals floor (25,193 -> 25,249). // Error hook thrower/boundary paths (2026-09-17): 16.80 -> 16.85 KB, // measured at 16,804 B rebased over #3515, against `next`'s 16,752 (+52 B). - limit: "16.85 KB", + // A write is a proposal (A34, #3494 / #3519 review, 2026-09-17): 16,937 B against + // `next`'s 16,846 (+91 B). Core-retained: `batchJoins` (a held node's + // mainline write records the join and schedules; drained inside flush's + // try, the fast sync path defers to it), the adoption loop's no-proposal + // drop (signals and writable memos, through commitPendingNode), a + // kept-tail pending mark re-deriving its subscriber (A30), and + // reporterBlocksSource following `_pendingSources` one hop. + limit: "17.00 KB", modifyEsbuildConfig: observeEsbuildConfig }, { @@ -1586,7 +1642,14 @@ module.exports = [ // Five store/signal divergences fixed (posture-store-parity S4, S5, S7, S8; // S6 ruled and deferred); every paired matrix state row-identical. // Measured at 27,335 B (+35 over the rebased cap). - limit: "27.40 KB", + // A write is a proposal (A34, #3494 / #3519 review, 2026-09-17): 27,426 B against + // `next`'s 27,335 (+91 B). Core-retained: `batchJoins` (a held node's + // mainline write records the join and schedules; drained inside flush's + // try, the fast sync path defers to it), the adoption loop's no-proposal + // drop (signals and writable memos, through commitPendingNode), a + // kept-tail pending mark re-deriving its subscriber (A30), and + // reporterBlocksSource following `_pendingSources` one hop. + limit: "27.50 KB", modifyEsbuildConfig: observeEsbuildConfig }, {