From 2537d77902f8161a3b8f314aea283b073a0ba07f Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Wed, 16 Sep 2026 00:42:53 -0700 Subject: [PATCH] test(signals): posture matrix over the store oracle's states; runner freezes the served value before probing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runs the store oracle's eight states (plain / optimistic / derived store leaves) through the same postures as the signal states — the store's read sites (nodeValue / serveDataKey / optimisticView) are separate implementations of the same rules, so every cell is a parity check. The store states move to visibility-oracle-store.states.ts; the store runner is unchanged (72 cells, same results). Runner fix: the closure readers (untracked / latest / isPending) assign `served` inside the posture's own compute — boundary content — which re-runs after the entanglement probe resolves the state's flight. The second pass overwrote the first, so three earlier readings ("isPending false behind a fallback", "override invisible behind a fallback", "the projection's seed served as a value in boundary content") were artifacts; retracted. `served` is now frozen before probing. Findings (tests/posture-store-parity.test.ts): - S1, violation (signal side), it.fails — adopted, unflushed: inside the adopting action's body the store leaf answers latest 0 / isPending false (A28), the signal answers 1 / true — adoption stamped it and `unflushedValue` reads a stamped node with no stash as flushed-held. Spec O4. - S2, observed, both agree — a memo + render effect created inside loading-boundary content over a held value publishes it (spec O2's in-flush form). Co-authored-by: Claude via Cursor Co-authored-by: Cursor --- packages/signals/docs/RULES-INDEX.md | 725 +++++++++--------- packages/signals/docs/SPEC-ASYNC-SEMANTICS.md | 7 + .../tests/posture-store-parity.test.ts | 118 +++ .../tests/visibility-oracle-posture.test.ts | 10 +- .../tests/visibility-oracle-store.states.ts | 324 ++++++++ .../tests/visibility-oracle-store.test.ts | 325 +------- 6 files changed, 829 insertions(+), 680 deletions(-) create mode 100644 packages/signals/tests/posture-store-parity.test.ts create mode 100644 packages/signals/tests/visibility-oracle-store.states.ts diff --git a/packages/signals/docs/RULES-INDEX.md b/packages/signals/docs/RULES-INDEX.md index f934eeaa4..a4103ad2f 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 | 17 | 33 | 0 | -| V | 5 | 2 | 5 | 0 | -| B | 5 | 0 | 5 | 0 | -| C | 4 | 0 | 3 | 1 | -| INV | 11 | 11 | 5 | 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 | 33 | 17 | 33 | 0 | +| V | 5 | 2 | 5 | 0 | +| B | 5 | 0 | 5 | 0 | +| C | 4 | 0 | 3 | 1 | +| INV | 11 | 11 | 5 | 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,360 +43,371 @@ 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.test.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.test.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.test.ts×5 | [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.test.ts×1 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×4 lanes.ts×2 scheduler.ts×2 | 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 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.test.ts×5 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.test.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×9 invariants.ts×3 optimistic.ts×6 scheduler.ts×2 verdict.ts×2 signals.ts×2 optimistic.ts×1 store.ts×3 | optimistic-undefined-override.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.test.ts×12 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×6 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 spec-async-semantics.test.ts×3 flight-owned-transaction.test.ts×1 superseded-before-first-commit.test.ts×5 visibility-oracle-store.test.ts×10 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×1 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.test.ts×6 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:325` | 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:332` | — | 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.test.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.test.ts×2 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.test.ts×8 | [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` | scheduler.ts×1 | action-await-contract.test.ts×2 visibility-oracle-store.test.ts×1 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×1 core.ts×18 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 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.test.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` | core.ts×5 effect.ts×1 optimistic.ts×1 | body-end-supersession-visibility.test.ts×1 born-held.test.ts×3 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 latest-held-till-flush.test.ts×2 treeshake.test.ts×1 visibility-oracle-store.test.ts×4 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×1 effect.ts×1 scheduler.ts×3 | 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 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` | — | visibility-oracle-store.test.ts×6 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: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×1 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×4 lanes.ts×2 scheduler.ts×2 | 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 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×5 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×9 invariants.ts×3 optimistic.ts×6 scheduler.ts×2 verdict.ts×2 signals.ts×2 optimistic.ts×1 store.ts×3 | optimistic-undefined-override.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×11 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×6 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 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×1 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×5 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:332` | 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:339` | — | 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×2 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` | scheduler.ts×1 | action-await-contract.test.ts×2 visibility-oracle-store.states.ts×1 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×1 core.ts×18 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×3 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` | core.ts×5 effect.ts×1 optimistic.ts×1 | body-end-supersession-visibility.test.ts×1 born-held.test.ts×3 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 latest-held-till-flush.test.ts×2 posture-store-parity.test.ts×1 treeshake.test.ts×1 visibility-oracle-store.states.ts×3 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×1 effect.ts×1 scheduler.ts×3 | 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 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` | — | visibility-oracle-store.states.ts×5 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… | + ## V — fixed violations -| id | status | defined | cited in src | cited in tests | statement (at definition) | -|---|---|---|---|---|---| -| V1 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:395` | 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:405` | 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:411` | — | spec-async-semantics.test.ts×2 | - **V3 (violated A19) — FIXED.** After a reporter-less transition completed, | -| V4 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:418` | — | 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:430` | — | 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:402` | 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:412` | 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:418` | — | spec-async-semantics.test.ts×2 | - **V3 (violated A19) — FIXED.** After a reporter-less transition completed, | +| V4 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:425` | — | 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:437` | — | 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:361` | — | 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:371` | — | — | - [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:368` | — | 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:378` | — | — | - [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×1 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 | — | - **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×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: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×1 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 | — | - **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×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 | + ## 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.test.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.test.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.test.ts×1 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.test.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.test.ts×2 | 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.test.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.test.ts×1 | 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.test.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×1 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×2 | 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×1 | 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` | invariants.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` | invariants.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 8f38c4e38..52ac30b53 100644 --- a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md +++ b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md @@ -308,6 +308,7 @@ An error escaping every boundary permanently halts the system with `REACTIVITY_H **Status:** **recorded** 2026-09-15 — maintainer: "generally creation escapes because it isn't visible"; "I'm fine either way as long as we end up somewhere consistent"; "let's capture things as they are." Pinned as OBSERVED by `tests/posture-born-held-and-observation.test.ts`; the matrix (class B, 10 cells) records it. **Current behavior:** a memo + render effect created inside a live action's body over a value another action holds (or over the superseded / body-ended states) direct-commits (`recompute`'s `create && bornHeld === null` arm — `stagedEntry` is only recorded when `activeTransition` is null) and its render effect publishes the held value, while untracked reads keep the committed frame until both actions settle. Mainline creation over the same value is born held (A29's creation-time form, 2026-09-14). Creation inside the HOLDING action's own body is unaffected: the body's write is unflushed (A28), the creation reads the committed frame. +**In-flush form (2026-09-16, posture matrix over the store states):** a memo + render effect created inside loading-boundary CONTENT over a held value publishes it too — the content pass entered the hold, the creation direct-committed — for signals and store leaves alike (`tests/posture-store-parity.test.ts`, S2, observed). **Tension:** A29 (born held) applies to mainline creation only; the "isn't visible" premise does not hold for a render effect created in the body. A future ruling either extends A29 to every posture (`creatingPass` in `enterStagedRead`, prototyped 2026-09-15: +83 B, suite green) or states creation-escapes as the rule and re-examines the mainline form. ### O3. A render effect gated away from a never-landing flight keeps the source's write held — fixed @@ -318,6 +319,12 @@ An error escaping every boundary permanently halts the system with `REACTIVITY_H **Mechanism (2026-09-16):** the predicate was already right — `reporterBlocksSource` judged the re-run effect dead; nothing RE-JUDGED the parked transaction. `recompute`'s tail now treats a pending reporter recovering without its flight landing as the completion event it is: it wakes the transaction it reported to (`wokenTransitions`, the third site after disposal #3372 and boundary reset #3375; `wakeParked` when the reporter carries no stamp), skipped under that transaction's own flush, which judges the landing itself. 21 → 4 of 1,000 fuzzer cases. **Remaining form (open):** gate and write in ONE flush (fuzzer case 21; case 79 is the multi-step variant). The effect is notified pending by the write — registering as the reporter — and dirtied by the gate in the same flush; the verdict runs after the pure phase, before effects, so the effect still looks live, the transaction parks, and the effect's run (which would prove it dead) is stashed with the transaction. The hold keeps the run that would release it; disposal releases (#3372). Pinned `it.fails`. Belongs with the effect-phase parking question (#3407: which world an effect's run belongs to when a mainline write and a held flight dirty it in one flush). +### O4. Adopted, unflushed — the signal's verdict channels see a write no flush has carried — violation, open + +**Status:** **violation, recorded** 2026-09-16 — posture matrix over the store states beside the signal states (`staged, ambient × foreignAction`): the store leaf answers `latest` 0 / `isPending` false inside the adopting action's body; the signal answers 1 / true. Pinned `it.fails` (signal) beside the passing store twin in `tests/posture-store-parity.test.ts` (S1). +**Rule:** same-tick adoption is by design (O1); A28 (1)/(2) still govern visibility — nothing is visible before the flush that carries the write, on any channel. The store is right. +**Mechanism (current):** adoption stamps the signal with the transaction (`initTransition`'s pending-node loop); `unflushedValue` reads a stamped node with no `_flushedStaged` stash as a flushed held node and serves `_pendingValue`. The store's selection (`nodeValue` / `serveDataKey`, `flushedStaged`) does not take that path. One rule, two implementations — the fix is making "unflushed" mean the same thing at both sites (a node staged outside a flush and adopted before any flush is unflushed whatever its stamp). + ## Superseded rules (kept verbatim) Cited by tests and by A24's reasoning; the statements below are as they stood when superseded. diff --git a/packages/signals/tests/posture-store-parity.test.ts b/packages/signals/tests/posture-store-parity.test.ts new file mode 100644 index 000000000..6295b54d8 --- /dev/null +++ b/packages/signals/tests/posture-store-parity.test.ts @@ -0,0 +1,118 @@ +/** + * Posture matrix — store / signal PARITY pins (2026-09-16). Discovery source: + * visibility-oracle-posture.test.ts over the store oracle's states + * (visibility-oracle-store.states.ts) beside the signal states. + * + * S1 — VIOLATION (signal side), pinned it.fails: adopted, unflushed. Same-tick + * adoption is by design (spec O1); A28 still says nothing is visible + * before the flush. The store leaf answers latest 0 / isPending false + * inside the adopting action's body; the signal answers 1 / true. + * S2 — OBSERVED, both sides agree: a memo + render effect created inside + * loading-boundary content over a held value publishes the held value + * (the in-flush form of spec O2 — creation under a transaction escapes; + * the content pass entered the hold, the creation direct-committed). + * Mainline creation over the same value is born held (A29). + * + * (A first cut also reported the projection's seed leaking as a value inside + * boundary content, and `isPending` false / override invisible behind a + * fallback. All three were a runner artifact — the boundary content re-ran + * after the entanglement probe resolved the flight and overwrote the + * captured read. The runner now freezes the served value before probing.) + */ +import { describe, expect, it } from "vitest"; +import { + action, + createLoadingBoundary, + createMemo, + createRenderEffect, + createRoot, + createSignal, + createStore, + flush, + isPending, + latest +} from "../src/index.js"; + +const never = () => new Promise(() => {}); + +describe("S1 — adopted, unflushed: verdict channels inside the adopting action (A28 (1)/(2)) — signal vs store", () => { + // Adoption stamps the signal with the transaction, and `unflushedValue` + // reads a stamped node with no stash as "flushed, held" — so latest() + // serves the staged 1 and isPending answers true for a write no flush has + // carried. The store leaf's selection (nodeValue / serveDataKey) does not + // take that path and answers by A28. The store is right. + it("store leaf: latest / isPending inside the adopting action see nothing before the flush", () => { + const [s, setS] = createStore({ n: 0 }); + setS(d => { + d.n = 1; + }); + let seen: [number, boolean] | undefined; + action(function* () { + seen = [latest(() => s.n), isPending(() => s.n)]; + yield never(); + })(); + expect(seen).toEqual([0, false]); + }); + it.fails( + "signal: latest / isPending inside the adopting action see nothing before the flush (A28) — VIOLATION: 1 / true", + () => { + const [x, setX] = createSignal(0); + setX(1); + let seen: [number, boolean] | undefined; + action(function* () { + seen = [latest(x), isPending(x)]; + yield never(); + })(); + expect(seen).toEqual([0, false]); + } + ); +}); + +describe("S2 — creation in boundary content over a held value publishes it (OBSERVED, spec O2 in-flush form; signal and store agree)", () => { + /** Build a memo + render effect over `read` inside a loading boundary whose + * content is pending on a sibling flight (the fallback shows). */ + function behindFallback(read: () => unknown) { + const published: unknown[] = []; + createRoot(() => { + const blocker = createMemo(() => never()); + const b = createLoadingBoundary( + () => { + const m = createMemo(read); + createRenderEffect(m, v => { + published.push(v); + }); + blocker(); + return "content"; + }, + () => "fallback" + ); + createRenderEffect(b, () => {}); + }); + flush(); + return published; + } + it("signal held by a live action: the memo publishes the held 1 while x() reads 0", () => { + const [x, setX] = createSignal(0); + action(function* () { + setX(1); + yield never(); + })(); + flush(); + expect(x()).toBe(0); + expect(behindFallback(x)).toEqual([1]); + expect(x()).toBe(0); + }); + it("store leaf held by a live action: the memo publishes the held 1 while s.n reads 0", () => { + const [s, setS] = createStore({ n: 0 }); + action(function* () { + setS(d => { + d.n = 1; + }); + yield never(); + })(); + flush(); + expect(s.n).toBe(0); + expect(behindFallback(() => s.n)).toEqual([1]); + expect(s.n).toBe(0); + }); +}); diff --git a/packages/signals/tests/visibility-oracle-posture.test.ts b/packages/signals/tests/visibility-oracle-posture.test.ts index de783888d..d9035618d 100644 --- a/packages/signals/tests/visibility-oracle-posture.test.ts +++ b/packages/signals/tests/visibility-oracle-posture.test.ts @@ -60,6 +60,7 @@ import { type State } from "./visibility-oracle.harness.js"; import { STATES as ORACLE_STATES } from "./visibility-oracle.states.js"; +import { STATES as STORE_STATES } from "./visibility-oracle-store.states.js"; /** Matrix-only states (no reader-kind expectations): shapes whose question is * the READER's hold rather than the served value. */ @@ -87,7 +88,7 @@ const MATRIX_STATES: State[] = [ expect: {} as State["expect"] } ]; -const STATES = [...ORACLE_STATES, ...MATRIX_STATES]; +const STATES = [...ORACLE_STATES, ...STORE_STATES, ...MATRIX_STATES]; const POSTURES = [ "mainline", @@ -257,6 +258,11 @@ async function cell(state: State, posture: Posture, reader: Reader): Promise({ n: 0 }); + const x = () => s.n; + installStale(x); + return { x, dispose() {} }; + }, + expect: { + untracked: rule(0, "baseline"), + derivesFrom: rule(0, "baseline"), + published: rule(0, "baseline"), + preexisting: rule(HELD, "baseline"), + staleForeign: rule(0, "baseline"), + childrenForbidden: rule(0, "baseline"), + latest: rule(0, "baseline"), + isPending: rule(false, "baseline"), + authoritative: rule(0, "baseline") + } + }, + { + name: "plain store: staged, ambient (setStore before the flush)", + build(installStale) { + const [s, set] = createStore({ n: 0 }); + const x = () => s.n; + installStale(x); + set(d => { + d.n = 1; + }); + return { x, dispose() {} }; + }, + expect: { + untracked: rule( + 0, + "A28: an untracked store read serves the committed backing until the flush" + ), + derivesFrom: rule(1, "the flush carries the write"), + published: rule(1, "the flush carries the write"), + preexisting: rule(HELD, "A28: nothing is visible before the flush"), + staleForeign: rule(1, "the flush carries the write"), + childrenForbidden: rule(1, "the flush carries the write"), + latest: rule(0, "A28: latest() reads the flushed staged world"), + isPending: rule(false, "A28 (2): false for an unflushed write"), + authoritative: rule(1, "A28 (4): the predicate runs in the carrying flush and sees the write") + } + }, + { + name: "plain store: held by a live action (setStore inside action, yield forever)", + build(installStale) { + const [s, set] = createStore({ n: 0 }); + const x = () => s.n; + installStale(x); + action(function* () { + set(d => { + d.n = 1; + }); + yield never(); + })(); + flush(); + return { x, dispose() {} }; + }, + expect: { + untracked: rule(0, "A19 (i) / CS-R33: the committed backing while the write is held"), + derivesFrom: rule( + 1, + "A29: a tracked pass served the staged leaf derives from the transaction's world" + ), + published: rule(HELD, "A29 (born held)"), + preexisting: rule(HELD, "A19 (i)"), + staleForeign: rule(0, "A15 / A26: a stale reader of a parallel transaction shows committed"), + childrenForbidden: rule(0, "A32"), + latest: rule(1, "A11 / nodeValue: latest() sees the in-flight parked value (#3075)"), + isPending: rule(true, "A19 (i) / CS-R33: a write held by an action pends that property"), + authoritative: rule(1, "A17 carve-out: staged values are authoritative") + } + }, + { + name: "optimistic store: override active (edit inside a live action)", + build(installStale) { + const [s, set] = createOptimisticStore({ n: 0 }); + const x = () => s.n; + installStale(x); + action(function* () { + set(d => { + d.n = 5; + }); + yield never(); + })(); + flush(); + return { x, dispose() {} }; + }, + expect: { + untracked: rule(5, "A17 / OS: the override is the displayed value"), + derivesFrom: rule(5, "A17"), + published: observed( + 5, + "as on the signal side: whether a fresh mainline memo publishes or holds is not stated" + ), + preexisting: rule(5, "A17: no downstream async, nothing to wait for"), + staleForeign: rule(5, "A17"), + childrenForbidden: rule(5, "A32: the override is the frame"), + latest: rule(5, "A17 / OL-R11"), + isPending: rule(false, "A24 (3) / OS-R37: optimistic writes are verdict-inert"), + authoritative: rule(0, "A17 carve-out / authoritativeServe(): never the caller's optimism") + } + }, + { + name: "optimistic store: override, ambient (edit outside any action, before the flush)", + build(installStale) { + const [s, set] = createOptimisticStore({ n: 0 }); + const x = () => s.n; + installStale(x); + set(d => { + d.n = 5; + }); + return { x, dispose() {} }; + }, + expect: { + untracked: rule( + 0, + "A28 (5): an optimistic store edit is a write — visible at the flush that carries it (supersedes OS-R1, CS-R34 visibility)" + ), + derivesFrom: rule(0, "OL-R5: an ambient optimistic write reverts at the next flush"), + published: rule(0, "OL-R5"), + preexisting: rule(HELD, "A28: nothing is visible before the flush"), + staleForeign: rule(0, "OL-R5"), + childrenForbidden: rule(0, "OL-R5"), + latest: rule(0, "A28 (5)"), + isPending: rule(false, "A24 (3)"), + authoritative: rule(0, "A17 carve-out") + } + }, + { + name: "derived optimistic store: own truth landed 2 ≠ override 3, action live (A18 through store nodes)", + async build(installStale) { + const [value, setValue] = createSignal(0); + const fetches: Array<() => void> = []; + const flights: Array<() => void> = []; + let s!: N; + let set!: (fn: (d: N) => void) => void; + const dispose = createRoot(d => { + [s, set] = createOptimisticStore( + () => { + const v = value(); + return new Promise(r => fetches.push(() => r({ n: v * 2 }))); + }, + { n: -1 } + ); + const downstream = createMemo(() => { + const n = s.n; + return new Promise(r => flights.push(() => r(`${n}!`))); + }); + createRenderEffect(downstream, () => {}); + return d; + }); + holds.push(() => flights.splice(0).forEach(f => f())); + flush(); + fetches.shift()!(); // initial truth {n: 0} + await settle(); + flights.shift()!(); + await settle(); + const x = () => s.n; + installStale(x); + action(function* () { + setValue(1); + set(d => { + d.n = 3; + }); + yield never(); + })(); + flush(); + fetches.shift()!(); // own truth lands {n: 2} ≠ 3 + await settle(); + return { x, dispose }; + }, + expect: { + untracked: rule(3, "A18 (c): the overlay stays displayed until settle"), + derivesFrom: rule( + 2, + "A18 (b) through a store node: the tracked pass derives from the landed truth. (#3434's store carve-out is about the body-end SETTLE order, not landing supersession.)" + ), + published: rule( + HELD, + "A18 (c) / A29 (born held): the fresh derivation is held with the transaction; the frame keeps the overlay" + ), + preexisting: rule(HELD, "A18 (c): the frame keeps the overlay until settle"), + staleForeign: rule(3, "A18 (c): a stale reader displays the overlay"), + childrenForbidden: rule(3, "A32"), + latest: rule(2, "A18 (d) / #3075: latest() sees the landed truth beneath the overlay"), + isPending: rule(true, "A18 (d) / OS-R39: the landing differs from the override"), + authoritative: rule(2, "A17 carve-out / authoritativeServe(): the base layer") + } + }, + { + name: "derived store (projection): pending refetch on a new question, flight up", + async build(installStale) { + const [q, setQ] = createSignal(0); + const fetches: Array<() => void> = []; + let s!: N; + const dispose = createRoot(d => { + [s] = createStore( + () => { + const v = q(); + return new Promise(r => fetches.push(() => r({ n: v * 10 }))); + }, + { n: -1 } + ); + createRenderEffect( + () => s.n, + () => {} + ); + return d; + }); + flush(); + fetches.shift()!(); // {n: 0} + await settle(); + const x = () => s.n; + installStale(x); + setQ(1); // new question; the flight never lands + flush(); + return { x, dispose }; + }, + expect: { + untracked: rule( + 0, + "A19 (ii) / OS-R32: untracked reads flow the committed value during a refetch window" + ), + derivesFrom: rule( + NOT_READY, + "A15 / A9: a tracked reader of a firewall-backed leaf whose firewall is refetching suspends" + ), + published: rule(HELD, "A15"), + preexisting: rule(HELD, "A15: the reader observing the flight holds"), + staleForeign: observed( + 0, + "as on the signal side: the stale reader re-run by an unrelated write shows the pre-flight committed value — same open question (INPUTS_PUBLISHED for a flight opened by the same batch)" + ), + childrenForbidden: rule(0, "A32"), + latest: rule( + 0, + "A8 / A9: latest shows the stale value while the firewall's new-question refetch is in flight" + ), + isPending: rule( + true, + "A9: a store leaf behind a firewall reports the firewall's new-question refetch" + ), + authoritative: observed(NOT_READY, "A17 carve-out: nothing landed for the new question") + } + }, + { + name: "derived store (projection): uninitialized — the seed is a draft, never a value (A25)", + build(installStale) { + let s!: N; + const dispose = createRoot(d => { + [s] = createStore(() => never() as unknown as Promise, { n: -1 }); + createRenderEffect( + () => s.n, + () => {} + ); + return d; + }); + const x = () => s.n; + installStale(x); + flush(); + return { x, dispose }; + }, + expect: { + untracked: rule( + NOT_READY, + "A25: to every outside consumer the store is uninitialized until the first resolution lands — the seed is never visible" + ), + derivesFrom: rule(NOT_READY, "A25 / A16 carve-out"), + published: rule(HELD, "A25"), + preexisting: rule(HELD, "A25"), + staleForeign: rule(HELD, "A25 / A15: nothing committed to show"), + childrenForbidden: rule(NOT_READY, "A32: no frame yet"), + latest: rule( + NOT_READY, + "A7 (amended): latest() of an uninitialized source throws in every scope" + ), + isPending: rule(false, "A16 / A19 exception (1): loading, not pending"), + authoritative: rule(NOT_READY, "A17 carve-out: nothing landed") + } + } +]; diff --git a/packages/signals/tests/visibility-oracle-store.test.ts b/packages/signals/tests/visibility-oracle-store.test.ts index b1f22c028..cb0ed3e0a 100644 --- a/packages/signals/tests/visibility-oracle-store.test.ts +++ b/packages/signals/tests/visibility-oracle-store.test.ts @@ -1,324 +1,7 @@ /** - * Visibility oracle — STORE side. The same node states as - * visibility-oracle.test.ts, entered through store leaves (plain store, - * optimistic store, derived store / projection), read by the same nine - * reader kinds through the store's own read sites (`serveDataKey` for - * tracked reads, `nodeValue` for untracked ones, `optimisticView` for - * composed views). Store rules that differ from the signal side by ruling: - * A25 (a derived store's seed is a draft), A9/A22 (the firewall's verdict), - * A18's store corollary (#2899) and #3434's store carve-out (optimistic store - * edits keep settle-then-revert; no tracked/displayed split). + * Store visibility oracle runner — see visibility-oracle-store.states.ts for + * the states. */ -import { - action, - createMemo, - createOptimisticStore, - createRenderEffect, - createRoot, - createSignal, - createStore, - flush -} from "../src/index.js"; -import { - HELD, - NOT_READY, - holds, - never, - observed, - rule, - runOracle, - settle, - violation, - type State -} from "./visibility-oracle.harness.js"; - -type N = { n: number }; - -const STATES: State[] = [ - { - name: "plain store: committed", - build(installStale) { - const [s] = createStore({ n: 0 }); - const x = () => s.n; - installStale(x); - return { x, dispose() {} }; - }, - expect: { - untracked: rule(0, "baseline"), - derivesFrom: rule(0, "baseline"), - published: rule(0, "baseline"), - preexisting: rule(HELD, "baseline"), - staleForeign: rule(0, "baseline"), - childrenForbidden: rule(0, "baseline"), - latest: rule(0, "baseline"), - isPending: rule(false, "baseline"), - authoritative: rule(0, "baseline") - } - }, - { - name: "plain store: staged, ambient (setStore before the flush)", - build(installStale) { - const [s, set] = createStore({ n: 0 }); - const x = () => s.n; - installStale(x); - set(d => { - d.n = 1; - }); - return { x, dispose() {} }; - }, - expect: { - untracked: rule( - 0, - "A28: an untracked store read serves the committed backing until the flush" - ), - derivesFrom: rule(1, "the flush carries the write"), - published: rule(1, "the flush carries the write"), - preexisting: rule(HELD, "A28: nothing is visible before the flush"), - staleForeign: rule(1, "the flush carries the write"), - childrenForbidden: rule(1, "the flush carries the write"), - latest: rule(0, "A28: latest() reads the flushed staged world"), - isPending: rule(false, "A28 (2): false for an unflushed write"), - authoritative: rule(1, "A28 (4): the predicate runs in the carrying flush and sees the write") - } - }, - { - name: "plain store: held by a live action (setStore inside action, yield forever)", - build(installStale) { - const [s, set] = createStore({ n: 0 }); - const x = () => s.n; - installStale(x); - action(function* () { - set(d => { - d.n = 1; - }); - yield never(); - })(); - flush(); - return { x, dispose() {} }; - }, - expect: { - untracked: rule(0, "A19 (i) / CS-R33: the committed backing while the write is held"), - derivesFrom: rule( - 1, - "A29: a tracked pass served the staged leaf derives from the transaction's world" - ), - published: rule(HELD, "A29 (born held)"), - preexisting: rule(HELD, "A19 (i)"), - staleForeign: rule(0, "A15 / A26: a stale reader of a parallel transaction shows committed"), - childrenForbidden: rule(0, "A32"), - latest: rule(1, "A11 / nodeValue: latest() sees the in-flight parked value (#3075)"), - isPending: rule(true, "A19 (i) / CS-R33: a write held by an action pends that property"), - authoritative: rule(1, "A17 carve-out: staged values are authoritative") - } - }, - { - name: "optimistic store: override active (edit inside a live action)", - build(installStale) { - const [s, set] = createOptimisticStore({ n: 0 }); - const x = () => s.n; - installStale(x); - action(function* () { - set(d => { - d.n = 5; - }); - yield never(); - })(); - flush(); - return { x, dispose() {} }; - }, - expect: { - untracked: rule(5, "A17 / OS: the override is the displayed value"), - derivesFrom: rule(5, "A17"), - published: observed( - 5, - "as on the signal side: whether a fresh mainline memo publishes or holds is not stated" - ), - preexisting: rule(5, "A17: no downstream async, nothing to wait for"), - staleForeign: rule(5, "A17"), - childrenForbidden: rule(5, "A32: the override is the frame"), - latest: rule(5, "A17 / OL-R11"), - isPending: rule(false, "A24 (3) / OS-R37: optimistic writes are verdict-inert"), - authoritative: rule(0, "A17 carve-out / authoritativeServe(): never the caller's optimism") - } - }, - { - name: "optimistic store: override, ambient (edit outside any action, before the flush)", - build(installStale) { - const [s, set] = createOptimisticStore({ n: 0 }); - const x = () => s.n; - installStale(x); - set(d => { - d.n = 5; - }); - return { x, dispose() {} }; - }, - expect: { - untracked: rule( - 0, - "A28 (5): an optimistic store edit is a write — visible at the flush that carries it (supersedes OS-R1, CS-R34 visibility)" - ), - derivesFrom: rule(0, "OL-R5: an ambient optimistic write reverts at the next flush"), - published: rule(0, "OL-R5"), - preexisting: rule(HELD, "A28: nothing is visible before the flush"), - staleForeign: rule(0, "OL-R5"), - childrenForbidden: rule(0, "OL-R5"), - latest: rule(0, "A28 (5)"), - isPending: rule(false, "A24 (3)"), - authoritative: rule(0, "A17 carve-out") - } - }, - { - name: "derived optimistic store: own truth landed 2 ≠ override 3, action live (A18 through store nodes)", - async build(installStale) { - const [value, setValue] = createSignal(0); - const fetches: Array<() => void> = []; - const flights: Array<() => void> = []; - let s!: N; - let set!: (fn: (d: N) => void) => void; - const dispose = createRoot(d => { - [s, set] = createOptimisticStore( - () => { - const v = value(); - return new Promise(r => fetches.push(() => r({ n: v * 2 }))); - }, - { n: -1 } - ); - const downstream = createMemo(() => { - const n = s.n; - return new Promise(r => flights.push(() => r(`${n}!`))); - }); - createRenderEffect(downstream, () => {}); - return d; - }); - holds.push(() => flights.splice(0).forEach(f => f())); - flush(); - fetches.shift()!(); // initial truth {n: 0} - await settle(); - flights.shift()!(); - await settle(); - const x = () => s.n; - installStale(x); - action(function* () { - setValue(1); - set(d => { - d.n = 3; - }); - yield never(); - })(); - flush(); - fetches.shift()!(); // own truth lands {n: 2} ≠ 3 - await settle(); - return { x, dispose }; - }, - expect: { - untracked: rule(3, "A18 (c): the overlay stays displayed until settle"), - derivesFrom: rule( - 2, - "A18 (b) through a store node: the tracked pass derives from the landed truth. (#3434's store carve-out is about the body-end SETTLE order, not landing supersession.)" - ), - published: rule( - HELD, - "A18 (c) / A29 (born held): the fresh derivation is held with the transaction; the frame keeps the overlay" - ), - preexisting: rule(HELD, "A18 (c): the frame keeps the overlay until settle"), - staleForeign: rule(3, "A18 (c): a stale reader displays the overlay"), - childrenForbidden: rule(3, "A32"), - latest: rule(2, "A18 (d) / #3075: latest() sees the landed truth beneath the overlay"), - isPending: rule(true, "A18 (d) / OS-R39: the landing differs from the override"), - authoritative: rule(2, "A17 carve-out / authoritativeServe(): the base layer") - } - }, - { - name: "derived store (projection): pending refetch on a new question, flight up", - async build(installStale) { - const [q, setQ] = createSignal(0); - const fetches: Array<() => void> = []; - let s!: N; - const dispose = createRoot(d => { - [s] = createStore( - () => { - const v = q(); - return new Promise(r => fetches.push(() => r({ n: v * 10 }))); - }, - { n: -1 } - ); - createRenderEffect( - () => s.n, - () => {} - ); - return d; - }); - flush(); - fetches.shift()!(); // {n: 0} - await settle(); - const x = () => s.n; - installStale(x); - setQ(1); // new question; the flight never lands - flush(); - return { x, dispose }; - }, - expect: { - untracked: rule( - 0, - "A19 (ii) / OS-R32: untracked reads flow the committed value during a refetch window" - ), - derivesFrom: rule( - NOT_READY, - "A15 / A9: a tracked reader of a firewall-backed leaf whose firewall is refetching suspends" - ), - published: rule(HELD, "A15"), - preexisting: rule(HELD, "A15: the reader observing the flight holds"), - staleForeign: observed( - 0, - "as on the signal side: the stale reader re-run by an unrelated write shows the pre-flight committed value — same open question (INPUTS_PUBLISHED for a flight opened by the same batch)" - ), - childrenForbidden: rule(0, "A32"), - latest: rule( - 0, - "A8 / A9: latest shows the stale value while the firewall's new-question refetch is in flight" - ), - isPending: rule( - true, - "A9: a store leaf behind a firewall reports the firewall's new-question refetch" - ), - authoritative: observed(NOT_READY, "A17 carve-out: nothing landed for the new question") - } - }, - { - name: "derived store (projection): uninitialized — the seed is a draft, never a value (A25)", - build(installStale) { - let s!: N; - const dispose = createRoot(d => { - [s] = createStore(() => never() as unknown as Promise, { n: -1 }); - createRenderEffect( - () => s.n, - () => {} - ); - return d; - }); - const x = () => s.n; - installStale(x); - flush(); - return { x, dispose }; - }, - expect: { - untracked: rule( - NOT_READY, - "A25: to every outside consumer the store is uninitialized until the first resolution lands — the seed is never visible" - ), - derivesFrom: rule(NOT_READY, "A25 / A16 carve-out"), - published: rule(HELD, "A25"), - preexisting: rule(HELD, "A25"), - staleForeign: rule(HELD, "A25 / A15: nothing committed to show"), - childrenForbidden: rule(NOT_READY, "A32: no frame yet"), - latest: rule( - NOT_READY, - "A7 (amended): latest() of an uninitialized source throws in every scope" - ), - isPending: rule(false, "A16 / A19 exception (1): loading, not pending"), - authoritative: rule(NOT_READY, "A17 carve-out: nothing landed") - } - } -]; - +import { runOracle } from "./visibility-oracle.harness.js"; +import { STATES } from "./visibility-oracle-store.states.js"; runOracle("visibility oracle — stores (A9, A17, A18, A19, A25, A29, A32, CS/OS rules)", STATES);