From 6e9474b3d181cfd0c6769f1fada2d31b8af334af Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Mon, 14 Sep 2026 10:05:05 -0700 Subject: [PATCH 1/7] =?UTF-8?q?docs(signals):=20rules=20index=20=E2=80=94?= =?UTF-8?q?=20every=20cited=20rule=20ID=20resolves;=20recover=20the=20stag?= =?UTF-8?q?e-3=20node-shape=20sections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Source comments in @solidjs/signals cite rule IDs from six vocabularies: A (spec), V (fixed violations), INV- (invariants), RUL- (store rulings), R (rules-mining — five files that EACH number from R1, so a bare R is ambiguous five ways) and § (design sections). Six citations resolved to nothing: §11b, §12, §12b, §12c, §12d, §12e were entries in the Stage-3 increment log of DESIGN-PATCH-CHANNEL.md — the patch-channel journal that was carried into the monorepo only from §17 and deleted with the feature (d6011192) — and INV-8 was a retired invariant whose definition had gone with its mechanism. - docs/NODE-SHAPE.md: the stage-3 node-shape decisions restored under their original numbers — §11b presence bits, §12 cold-field extension, §12b zombie pair + plain-commit fast drain, §12c what stays in the core literal (recovered from packages/solid-signals/DESIGN-PATCH-CHANNEL.md at 3a505c7a); §12d staged-rewrite fast path and §12e signal-literal diet reconstructed from the code comments that cite them, marked as such. - INTERNALS-ASYNC-STATE.md: INV-8 entry (RETIRED 2026-07-07b) recording what the hold-provenance invariant proved before revert targets were eliminated, so invariants.ts's citation resolves. - docs/RULES-INDEX.md (generated): 305 rules across the six vocabularies with status (live / ruled / amended / superseded / retired / fixed / promoted), definition site, and src/test citation counts; R-ids namespaced CS/OL/OS/PJ/RS without renumbering (comments qualify with core/opt/proj/snap; a bare R resolves to the citing module's file, else core-store); tier ids promoted into A-rules ("(was B1)") resolve to the row they became. - scripts/rules-index.mjs generates it; --check fails on any src/ citation without a definition. tests/rules-index.test.ts runs the check. - SPEC / both INTERNALS headers point at the index. IDs are never renumbered. Co-authored-by: Cursor --- .../signals/docs/INTERNALS-ASYNC-STATE.md | 13 + .../signals/docs/INTERNALS-STORE-STATE.md | 2 + packages/signals/docs/NODE-SHAPE.md | 159 +++++++ packages/signals/docs/RULES-INDEX.md | 396 ++++++++++++++++++ packages/signals/docs/SPEC-ASYNC-SEMANTICS.md | 2 + packages/signals/scripts/rules-index.mjs | 269 ++++++++++++ packages/signals/tests/rules-index.test.ts | 21 + 7 files changed, 862 insertions(+) create mode 100644 packages/signals/docs/NODE-SHAPE.md create mode 100644 packages/signals/docs/RULES-INDEX.md create mode 100644 packages/signals/scripts/rules-index.mjs create mode 100644 packages/signals/tests/rules-index.test.ts diff --git a/packages/signals/docs/INTERNALS-ASYNC-STATE.md b/packages/signals/docs/INTERNALS-ASYNC-STATE.md index fc98ea78f..fd8011571 100644 --- a/packages/signals/docs/INTERNALS-ASYNC-STATE.md +++ b/packages/signals/docs/INTERNALS-ASYNC-STATE.md @@ -1,5 +1,7 @@ # Async/Transition/Lane State Model — Working Notes +> **Index:** every rule ID cited from `src/` or `tests/` — this file's A/B/C/V ids, the INV/RUL/R/§ vocabularies of the internals and rules-mining docs — is listed with status, definition and citations in [`RULES-INDEX.md`](./RULES-INDEX.md) (generated; `node scripts/rules-index.mjs`). IDs are never renumbered. + Living document for the pending/transition/optimistic-lane machinery in `src/core/`. Captures the state model, the invariants we believe hold (with confidence levels), and the assumptions/decisions made while reviewing. The @@ -164,6 +166,17 @@ Confidence: **high** = implementation self-consistency, assert now. - **INV-7 (medium)** `_pendingValue !== NOT_PENDING` on a non-optimistic node implies the node is queued (`_pendingNode`/`_pendingNodes`) or held by a transition — a pending value with no committer is a leak (the #2827 class). +- **INV-8 (RETIRED 2026-07-07b, §5e)** Hold-provenance: a `_pendingValue` on an + optimistic node was tracked as either a *revert target* (the pre-override + value stashed for the revert) or a *held authoritative value*, and the + invariant asserted a resting node never carried a revert target. That + provenance proved a held value on a resting node is always a refetch / + transition hold — pending like a plain memo (V1, §5d) — and then the A18 + re-rule eliminated revert targets altogether (§5e), leaving `_pendingValue` + one meaning and nothing to distinguish. The tracker was deleted with them. + The ID stays: `invariants.ts` and §5d/§5e/§7 cite it for the proof it gave. + Its lane-scoped successor ("live lane members are only released by their own + lane's resolution", C2) is queued, not asserted. - **INV-9 (high)** An `isPending` companion of a DISPOSED owner reads `false` at quiescence — a stale `true` outliving its source would hold a spinner forever (the #2845 disposal edge). Enforced by the disposal guard in diff --git a/packages/signals/docs/INTERNALS-STORE-STATE.md b/packages/signals/docs/INTERNALS-STORE-STATE.md index a5a50c913..e9dade590 100644 --- a/packages/signals/docs/INTERNALS-STORE-STATE.md +++ b/packages/signals/docs/INTERNALS-STORE-STATE.md @@ -1,5 +1,7 @@ # Store Storage Model Rewrite — Semantic Rules & Working Notes +> **Index:** every rule ID cited from `src/` or `tests/` — this file's §-sections and RUL-ids, the rules-mining R-ids (namespaced per file: CS/OL/OS/PJ/RS), the spec's A-ids — is listed with status, definition and citations in [`RULES-INDEX.md`](./RULES-INDEX.md) (generated; `node scripts/rules-index.mjs`). IDs are never renumbered. + Companion to `INTERNALS-ASYNC-STATE.md`, same method: pin the semantic contract as rules first, wire the checkable ones as `__TEST__` assertions, then let the implementation land against them. Nothing below is final until it survives the diff --git a/packages/signals/docs/NODE-SHAPE.md b/packages/signals/docs/NODE-SHAPE.md new file mode 100644 index 000000000..fb5c6426c --- /dev/null +++ b/packages/signals/docs/NODE-SHAPE.md @@ -0,0 +1,159 @@ +# Node Shape and Hot-Path Layout — Stage-3 design record (§11–§12) + +**Why this file exists.** Source comments in `core.ts`, `scheduler.ts`, +`graph.ts`, `types.ts`, `constants.ts` and `optimistic.ts` cite `§11b`, `§12`, +`§12b`, `§12c`, `§12d` and `§12e`. Those were entries in the _Stage-3 +increment log_ (§11c) of `DESIGN-PATCH-CHANNEL.md`, the patch-channel design +journal that lived in the pre-absorb signals repo. That journal was carried +into the monorepo only from §17 on, and deleted with the patch channel +(`d6011192`); the node-shape decisions it recorded outlived the feature they +were recorded next to. This file restores them under their original numbers +so the citations resolve. **Do not renumber**: the numbers are referenced +from code. + +Provenance: + +- §11, §11b, §11c, §12, §12b, §12c — recovered verbatim (lightly trimmed of + benchmark chatter) from `packages/solid-signals/DESIGN-PATCH-CHANNEL.md` at + `3a505c7a` (2026-08-21). +- §12d, §12e — **reconstructed 2026-09-14 from the code comments that cite + them**; no committed version of the journal contained these entries. The + statements below are what the code implements; the original log text is + lost. + +Related: `INTERNALS-ASYNC-STATE.md` §1 (node state fields), +`BITWISE_OPERATIONS.md` (the `_config` presence bits), `write-coalescing.md`. + +--- + +## 11. Stage 3 opener: the core tax map (2026-08-21) + +Stage 2 closed the store/list story. The remaining bottleneck was the +REACTIVE CORE's fixed costs, isolated against the r3 heap engine +(js-reactivity-benchmark, `solid-2.0-benchmarks` branch): overall +solid-next / r3 = 2.20x; / r3-solid-target = 1.67x (so ~1.3x is the cost of +Solid's target semantics; ~1.67x our implementation of them). Engine choice +CONFIRMED: r3's heap still wins the diamond/avoidable-propagation shapes it +was chosen for; alien-signals is not the answer. + +The tax map (worst first): + +1. CREATION — create0to1 12.9x, create2to1 6.2x, create4to1 4.1x. The + constructor path does eager work r3 defers (owner wiring, id inheritance, + queue/context, flag init). Fix shape: field-layout + lazy-init discipline. +2. WRITE FAST PATH — a write nobody observes pays 4x; update1to1 3.4x. Fix + shape: one armed-check branch on the core write path; lane/transition/ + status machinery consulted only when armed. +3. PROPAGATION CONSTANTS — deep/broad 2.3–2.7x per-hop overhead; expected to + largely fall out of (2). + +Size ruling context: this is GATING/TRIMMING existing paths, not a second +engine — expected size-neutral or negative. + +### §11b. Presence bits — hot-path monomorphism + +Profiles named the taxes (2026-08-21, side-by-side vs r3): creation was an +ALLOCATION problem before it was a code problem (GC 66% of the create0to1 +profile), and the write/notify loops were paying for _missing-property +reads_ — optional per-node slots (`_overrideValue`, `_pendingSignal` / +`_latestValueComputed`, `_snapshotValue`, `_optimisticLane`) were not part of +every node's hidden class, and reading a missing property defeats V8's inline +caches on the hottest loops. + +**Rule.** Optional-slot _presence_ is recorded as bits on the always-present +`_config` (`CONFIG_OPTIMISTIC`, `CONFIG_HAS_COMPANIONS`, `CONFIG_HAS_SNAPSHOT`, +`CONFIG_HAS_LANE`, later `CONFIG_FW_CHILDREN`, …). Hot paths — `setSignal`, +`insertSubs`, commit, `recompute`, `markNode` — pay one monomorphic masked read +and touch the optional field only when its bit is set. Bits are sticky: once +set, the field read stays authoritative. Optimistic constructors no longer +fork hidden classes. Measured ~7–10% on write-path benches; core-floor ceiling +consciously +~120 B. See `constants.ts` (presence bits block). + +### §11c. Stage-3 increment log + +The log entries that became standing design are broken out below as §12–§12e. +Two entries recorded as _ruled out_ still govern: + +- **Quiet-world memo direct-commit — RULED OUT.** #3009's `latest()`/plain- + write purity family failed immediately: mid-batch pulls must see fresh + values while plain reads stay committed until flush; the pending round-trip + IS that separation. The update-path commit cost is semantic price. +- **Measurement discipline.** Daytime load inflates µs benches ~10–15% (the + r3 control moves identically). Verify by profile shape or idle runs. + +## §12. Cold-field extension (`_x`, `ext()`) + +**Rule.** The optional-machinery fields — `_inFlight`, `_error`, `_blocked`, +`_pendingSources`, `_notifyStatus`, `_reask`, `_child`, `_unobserved`, +`_optimisticLane`, `_overrideValue`, the verdict/lane companion slots, and +later additions — live off the node literal in ONE lazily-allocated extension +object (`_x`, allocated by `ext()` in `core.ts`). The core literal MUST NOT +grow a field that most nodes never use. The recompute status gate collapses to +`_statusFlags !== 0 || _x !== null`; presence bits (§11b) remain the hot +gates. + +Measured at landing: computed literal 39 → 29 fields, signal 13 → 12; memo +553 B → 429 B (−22%), create 330 → 280 ns; interleaved A/B create1to1 −19%, +update1to1 −12%, writeNoSubs −9%. The V8 in-object cliff (~39 fields) did NOT +drive the create bench; the wins were the memory diet. Byte cost +~740 B on +the core floor (the `_x?.` chains + the `ext` initializer). + +Hazards recorded for posterity (all bit us once): `this`-sensitive callbacks +stored in `_x` must be `.call(node, …)`-dispatched; `any`-typed reads of moved +fields slip `tsc` silently (the #2951 firewall-transition entanglement died +reading a dead raw slot); `?.` flips `null` defaults to `undefined` in +comparisons; never allocate `_x` just to store a field's default. + +### §12b. Zombie pair in the extension; plain-commit fast drain + +`_pendingDisposal` / `_pendingFirstChild` moved into `_x` (computed literal +29 → 27; roots swap the pair for `_x: null`). Staged disposal only fires for +owners that HAVE children/disposal, so childless memos never pay it, and the +per-recompute commit gate collapses to one `_x` null check. With it: a +plain-commit fast drain in `GlobalQueue.flush` (prod-only; dev keeps the full +spine for invariant checks; semantically identical when its preconditions +hold — see `canUseSimpleSyncFlush`). Interleaved A/B: update1to1 −12% on top +of §12, create1to1 ~−10%. Per-write direct commit stays ruled out (#3009 +purity). Byte cost +~300 B. + +### §12c. What stays IN the core literal + +Recorded as "§12c candidates" at the time; the settled rule is the inverse of +§12: a field consulted on EVERY write or on recompute scheduling stays on the +node — the per-write extension chase measurably taxes propagation chains. +`_transition` is the canonical example (`setSignal`'s transition-init check +reads it on every write; see `types.ts`). Owner-tree slimming (~10 fields of +ownership machinery per node) was identified as the next real creation lever +and left as rewrite-scale. + +### §12d. Staged-rewrite fast path (notify epoch) — _reconstructed_ + +A re-write to a node whose subscribers were already walked, and where NOTHING +has recomputed or linked since, re-stages the value and skips the walk. The +walk is idempotent (subs marked, heap entries flag-guarded, effects queued +once), so skipping it loses nothing _as long as no subscriber has been cleaned +in between_ — a mid-batch pull can clean a marked subscriber, and a skipped +re-write would then leave it stale. + +**Mechanism.** A global `notifyEpoch` (scheduler.ts) bumps on every recompute +(`recompute`, core.ts) and on every new subscriber edge (`link`, graph.ts). +`insertSubs` stamps `node._notifiedAt = notifyEpoch` before walking. +`setSignal` skips the walk iff `wasStaged && el._notifiedAt === notifyEpoch` +— and never under an optimistic lane or an armed re-ask, because those change +what a walk MEANS. The `_notifiedAt` slot is in the core literal (it is read +on every write; §12c). + +Note: #3337 (A28, writes visible at flush) proposes replacing this epoch with +an unflushed-write list; see that PR for the trade. + +### §12e. Signal-literal diet: `_time`, `_fn`, `_statusFlags` are computed-only — _reconstructed_ + +Signals carry NO `_time` / `_fn` / `_statusFlags` slots. Stores materialize +one signal per touched leaf, so signal bytes are store bytes. `_time` is +write-only on signals (every read site is computed-typed error-retry gating), +and `_fn` / `_statusFlags` read falsy-identically as missing properties on the +shared paths (`undefined` masks to 0). + +**Rule.** Any path that writes `_time` must guard it with the computed check — +`if (el._fn !== undefined) el._time = clock` — writing it on a signal would +fork the lean shape (`setSignal`, core.ts; `optimisticWrite`, optimistic.ts). diff --git a/packages/signals/docs/RULES-INDEX.md b/packages/signals/docs/RULES-INDEX.md new file mode 100644 index 000000000..1f7b853cf --- /dev/null +++ b/packages/signals/docs/RULES-INDEX.md @@ -0,0 +1,396 @@ +# Rules Index — every rule ID the code and tests cite, and where it lives + +**Generated by `scripts/rules-index.mjs`; do not edit by hand.** Regenerate with `node scripts/rules-index.mjs`; `--check` fails when a `src/` citation resolves to nothing (run in `pnpm test` via `tests/rules-index.test.ts`). + +IDs are never renumbered or deleted — source comments cite them. A superseded, retired or fixed rule keeps its row with its status; the statement column shows how it reads at its definition (first ~200 chars). + +## 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) | + +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 | 27 | 9 | 18 | 8 | +| 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 | 9 | 36 | +| 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 + +**src/:** none — every citation resolves. + +**tests/:** A28 (test-only citations are informational; `--check` gates src/ only) + +## A — spec propositions + +| id | status | defined | cited in src | cited in tests | statement (at definition) | +|---|---|---|---|---|---| +| A1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:22` | — | onCleanup.test.ts×2 transitionEntanglement.test.ts×4 | `EffectBundle.error` intercepts compute-phase errors only; effect-phase throws escalate to the nearest error boundary (halt if none). \| #2839 ruling (2026-07-06) \| `tests/effect-error-phases.test.ts` … | +| A2 | live | `docs/SPEC-ASYNC-SEMANTICS.md:23` | — | onCleanup.test.ts×2 | Compute-phase errors in _user_ effects without a handler are logged and the run is skipped; the system keeps running. \| #2839 ruling \| `tests/effect-error-phases.test.ts` \| | +| A3 | live | `docs/SPEC-ASYNC-SEMANTICS.md:24` | — | — | Errors thrown by a user `equals` comparator behave exactly like compute-phase errors (boundary-containable; loud halt without a boundary). \| #2837 \| `tests/equals-comparator-errors.test.ts` \| | +| A4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:25` | — | — | A custom `equals` is never invoked with `undefined` previous value on a node's first commit. \| #2837 follow-on \| `tests/equals-comparator-errors.test.ts` (async case) \| | +| A5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:26` | — | — | An error escaping every boundary permanently halts the system with `REACTIVITY_HALTED`; later writes log "Update ignored". \| #2761/#2762 \| `tests/createErrorBoundary.test.ts`, `tests/errorHalt.test.ts… | +| A6 | live | `docs/SPEC-ASYNC-SEMANTICS.md:27` | — | — | `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. \| #2822 \| `tests/enforceLoadingBoundary.test.ts`, solid… | +| A7 | live | `docs/SPEC-ASYNC-SEMANTICS.md:28` | — | spec-async-semantics.test.ts×2 | After an async memo resolves, `[isPending(x), latest(x)]` is `[false, resolvedValue]` — never `[false, undefined]`. \| #2829 (the `[false, undefined]` pins were a regression) \| `tests/latest-async.test… | +| A8 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:29` | — | — | (**re-ruled 2026-07-07c** — was "tracks the transition the same as `isPending(x)`"; **amended 2026-07-13**: "own async in flight" is further filtered to _non-quiet_ flight — a re-ask of the same quest… | +| A9 | live | `docs/SPEC-ASYNC-SEMANTICS.md:30` | — | spec-async-semantics.test.ts×3 | `isPending` on a store leaf behind a firewall reports the firewall's refetch like any async memo — in **both** forms (the latest-form filter of the old A20 is gone; an in-flight refetch supersedes bot… | +| A10 | live | `docs/SPEC-ASYNC-SEMANTICS.md:31` | invariants.ts×1 | — | `[isPending(x), x()]` read in one scope is atomic: a reader that observed the fresh value must not see `pending === true` for it. \| #2831 finding 2 \| `tests/latest-isPending-consistency.test.ts` \| | +| A11 | live | `docs/SPEC-ASYNC-SEMANTICS.md:32` | — | — | Sync derivations of transition-held sources are visible through `latest()`/`isPending()` (held sync recompute is a write path like any other). \| #2831 finding 3 \| `tests/latest-isPending-consistency.t… | +| A12 | live | `docs/SPEC-ASYNC-SEMANTICS.md:33` | — | — | A resting optimistic node reports pending via exactly the causes a plain async memo does (A19) — a reverting optimistic write is not among them (a revert is not a refetch). (Phrasing updated with V1: … | +| A13 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:34` | async.ts×1 | spec-async-semantics.test.ts×7 | (was B1) A resting optimistic node (no active override) is observationally identical to a plain async memo for `read`/`latest`/`isPending` at every checkpoint of a refetch cycle — both before any over… | +| A14 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:35` | — | spec-async-semantics.test.ts×2 | (was B2) `isPending`/`latest` companion nodes get child lanes that do not merge with the owner's lane: an `isPending` effect (spinner) fires while the owner's async is still in flight. (Re-scoped 2026… | +| A15 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:36` | core.ts×2 lanes.ts×1 scheduler.ts×1 | lane-hold-on-observation.test.ts×1 reveal-carve-out.test.ts×2 spec-async-semantics.test.ts×2 stale-read-uninitialized-cross-transition.test.ts×1 treeshake.test.ts×3 | (was B3) Transition entanglement is graph-driven: writes whose async work is observed by a shared reader settle as one unit (no tearing — nothing commits until all entangled async resolves); writes on… | +| A16 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:37` | verdict.ts×1 | spec-async-semantics.test.ts×1 strict-read-pending-store.test.ts×2 uninitialized-visibility.test.ts×1 | (was B5) `isPending` never throws in untracked contexts — thunks that throw real errors or read uninitialized async sources yield `false`. Carve-out (B5a, pinned as current behavior): in _tracked_ con… | +| A17 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:38` | async.ts×3 constants.ts×2 core.ts×7 invariants.ts×3 optimistic.ts×3 scheduler.ts×2 verdict.ts×1 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 | (was C4) An _active_ optimistic override is THE value for every **read** — ambient/untracked and tracked alike — regardless of transition entanglement. "It is the optimistic future value... it is both… | +| A18 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:39` | async.ts×2 constants.ts×1 core.ts×4 optimistic.ts×3 scheduler.ts×2 types.ts×2 optimistic.ts×1 | createOptimistic.test.ts×1 spec-async-semantics.test.ts×3 flight-owned-transaction.test.ts×1 | (was B4; **refined by re-rule 2026-07-07b**) An override's lifetime is bound to **its own transition** — which, because lanes keep their transitions separate from unrelated work, contains exactly the … | +| A19 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:40` | async.ts×1 optimistic.ts×1 | spec-async-semantics.test.ts×3 uninitialized-visibility.test.ts×1 | (was C1 — **partially reverses an earlier decision**) **Definition: `isPending(x)` ≡ the value you can currently observe for `x` is not the final one.** Three causes of non-finality, each ending on it… | +| A20 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:41` | 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** — the mask is deleted; optimistic writes are verdict-inert. Kept for the reasoning record. The surviving pieces: verdicts are per-channel (§2, now in A8), and action … | +| A21 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:42` | — | question-scoped-pending.test.ts×3 spec-async-semantics.test.ts×3 | (**SUPERSEDED 2026-07-13 by A24** — the store-wide mask is deleted with the mask model; nothing silences a new question. The effective-write gate (consequence 4) survives as transaction-entanglement h… | +| A22 | live | `docs/SPEC-ASYNC-SEMANTICS.md:43` | — | spec-async-semantics.test.ts×1 | **Pending is per-node: store-wide verdicts exist only as the firewall's own in-flight work (A9) and the decree that silences it (A21).** Every other A19 cause lives on the individual node — a transiti… | +| A23 | live | `docs/SPEC-ASYNC-SEMANTICS.md:44` | — | spec-async-semantics.test.ts×1 | **The `isPending` probe is reads-only — the thunk's return value is never inspected.** `isPending(() => store)` reads nothing and reports `false` by design: keying any behavior off the returned value … | +| A24 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:45` | — | optimistic-undefined-override.test.ts×1 reveal-gating-contract.test.ts×1 spec-async-semantics.test.ts×2 | (**ruled 2026-07-13** — supersedes A20/A21; the converged model from the #2844/#2728 threads) **Question-scoped pending: a read is pending iff a value change is in flight for it that has not yet revea… | +| A25 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:46` | — | uninitialized-visibility.test.ts×3 | (**ruled 2026-07-16**, #2897) **A derived store's seed is a draft, never an observable value.** The seed exists for the derive function — self reads while it works its draft are the point — but to eve… | +| A26 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:48` | scheduler.ts×1 | action-await-contract.test.ts×2 | (**ruled 2026-07-17**, #2913; **enforcement hardened 2026-08-31**, #3141 — parking is flush-driven, and a transaction opened with no writes before its first suspension scheduled no flush, so `activeTr… | +| A27 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:49` | — | — | (**ruled 2026-08-10**) **The commit-#0 loading window is loading-class and verdict-quiet.** A node born committed via `loadingValue` (memos: `createMemo` / `createSignal(fn)` / `createOptimistic(fn)`)… | +## V — fixed violations + +| id | status | defined | cited in src | cited in tests | statement (at definition) | +|---|---|---|---|---|---| +| V1 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:105` | 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:115` | 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:121` | — | spec-async-semantics.test.ts×2 | - **V3 (violated A19) — FIXED.** After a reporter-less transition completed, | +| V4 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:128` | — | 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:140` | — | 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:34` | — | createRevealOrder.test.ts×16 onCleanup.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A13 (A13's row carries the ruling). | +| B2 | live | `docs/SPEC-ASYNC-SEMANTICS.md:35` | — | createRevealOrder.test.ts×16 onCleanup.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A14 (A14's row carries the ruling). | +| B3 | live | `docs/SPEC-ASYNC-SEMANTICS.md:36` | — | spec-async-semantics.test.ts×2 | PROMOTED → A15 (A15's row carries the ruling). | +| B4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:39` | — | spec-async-semantics.test.ts×2 | PROMOTED → A18 (A18's row carries the ruling). | +| B5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:37` | — | spec-async-semantics.test.ts×2 | PROMOTED → A16 (A16's row 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:40` | — | onCleanup.test.ts×2 spec-async-semantics.test.ts×1 | PROMOTED → A19 (A19's row carries the ruling). | +| C2 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:71` | — | 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:81` | — | — | - [x] **C3 — CLOSED by A19 (2026-07-07): early completion is by design.** | +| C4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:38` | — | spec-async-semantics.test.ts×1 | PROMOTED → A17 (A17's row 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:140` | invariants.ts×2 | — | - **INV-1 (high)** `pendingProbe` is non-null only inside an `isPending()` call | +| INV-2 | live | `docs/INTERNALS-ASYNC-STATE.md:142` | invariants.ts×2 | — | - **INV-2 (high)** A node with an _active_ override (`hasActiveOverride`) is | +| INV-3 | live | `docs/INTERNALS-ASYNC-STATE.md:146` | core.ts×1 invariants.ts×2 lanes.ts×1 scheduler.ts×2 | lane-hold-on-observation.test.ts×1 | - **INV-3 (high)** `_asyncReporters` gains entries only inside | +| INV-4 | live | `docs/INTERNALS-ASYNC-STATE.md:153` | 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:157` | invariants.ts×2 lanes.ts×1 | — | - **INV-5 (medium)** A lane in `activeLanes` has `_mergedInto === null` | +| INV-6 | live | `docs/INTERNALS-ASYNC-STATE.md:163` | 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:166` | 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:169` | 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:180` | 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:185` | 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:190` | core.ts×1 optimistic.ts×1 | 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- | +## 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:7` | — | flatten-async-iterable.test.ts×5 syncThenable.test.ts×14 | 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:10` | — | syncThenable.test.ts×12 | 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:14` | — | — | 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:17` | — | — | 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:20` | — | — | Upstream writes propagate downstream through the chain without re-running structural machinery.** | +| CS-R6 | live | `docs/rules-mining/core-store.md:23` | — | — | 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:26` | — | — | Circular references wrap without infinite recursion; cycle consistent through proxy (`state.b.a === state.a`).** | +| CS-R8 | live | `docs/rules-mining/core-store.md:29` | — | — | `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:32` | 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:37` | — | 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:40` | — | — | 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:43` | 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:46` | 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:49` | — | — | `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:52` | 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:55` | — | — | `length` independently trackable; index write extending the array notifies length subscribers.** | +| CS-R17 | live | `docs/rules-mining/core-store.md:58` | — | — | 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:61` | — | — | `snapshot` is non-tracking.** Aligned with read table. | +| CS-R19 | live | `docs/rules-mining/core-store.md:64` | — | — | `untrack` scopes only the wrapped read; property access on the escaped value afterwards tracks normally.** | +| CS-R20 | live | `docs/rules-mining/core-store.md:67` | 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:70` | 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:74` | — | — | 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:79` | 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:82` | — | 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:86` | — | 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:90` | — | — | 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:93` | 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:96` | — | — | `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:99` | 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:103` | 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:106` | 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:110` | store.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:113` | — | — | 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:116` | store.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.** | +| CS-R35 | live | `docs/rules-mining/core-store.md:120` | — | — | 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:123` | — | — | 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:127` | — | — | Setting store state from effect callbacks and promise resolutions works, applying next flush.** | +| CS-R38 | live | `docs/rules-mining/core-store.md:132` | — | — | 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:135` | — | — | 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:138` | — | — | 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:141` | 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:145` | 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:148` | — | — | 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:151` | 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:155` | — | — | 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:158` | — | — | 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:163` | — | — | 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:167` | — | — | 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:170` | — | — | Null-prototype objects wrap and track; function-valued props callable through the proxy.** | +| CS-R50 | live | `docs/rules-mining/core-store.md:173` | — | — | Frozen sources fully supported (read/snapshot; getters returning frozen don't throw).** | +| CS-R51 | live | `docs/rules-mining/core-store.md:177` | 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:180` | — | — | 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:183` | — | — | 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:186` | — | — | 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:188` | — | — | Functions stored as values served raw, replaceable, slot-tracked.** | +| CS-R56 | live | `docs/rules-mining/core-store.md:192` | — | — | 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:195` | — | — | Effect ordering: parent effects before child effects created inside them, incl. shared deps through memos.** | +| CS-R58 | live | `docs/rules-mining/core-store.md:198` | — | — | 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:9` | — | — | `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 | live | `docs/rules-mining/optimistic-lanes.md:12` | — | — | An optimistic write is synchronously visible to direct reads before any flush — inside the action body, outside it, and outside any reactive context. | +| OL-R3 | live | `docs/rules-mining/optimistic-lanes.md:15` | — | — | 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:18` | — | — | 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:21` | — | — | 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:25` | — | — | 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:28` | — | — | 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:31` | — | — | 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:34` | — | — | 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:37` | — | — | `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:40` | — | — | 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:43` | — | — | 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:46` | — | — | 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:52` | — | — | 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:55` | — | — | 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:59` | — | — | 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:63` | — | — | 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:67` | — | — | 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:70` | — | — | 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:73` | — | — | 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:78` | — | — | 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:82` | — | — | 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:85` | — | — | 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:91` | — | — | 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:94` | — | — | 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:97` | — | — | 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:100` | — | — | 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:105` | — | — | 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:109` | — | — | 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:112` | — | — | 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:115` | — | — | 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:118` | — | — | `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:121` | — | — | 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:124` | — | — | `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:129` | — | — | `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:132` | — | — | 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:136` | — | — | 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 | live | `docs/rules-mining/optimistic-store.md:7` | — | — | Synchronous universal visibility.** An optimistic write is visible to every reader immediately at write time, before any flush: tracked, untracked, inside the action body, outside any reactive context… | +| OS-R2 | live | `docs/rules-mining/optimistic-store.md:10` | — | — | 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:14` | — | — | 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:17` | — | — | 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:20` | — | — | 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:24` | — | — | 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:28` | — | — | Propagation through derived graphs** (memo chains, mapArray) like committed values. | +| OS-R8 | live | `docs/rules-mining/optimistic-store.md:30` | — | — | `latest()` returns the optimistic value** during a pending refetch window. | +| OS-R9 | live | `docs/rules-mining/optimistic-store.md:32` | — | — | 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:37` | — | — | Settle reverts to base with one notification** (`[0,1,0]`). | +| OS-R11 | live | `docs/rules-mining/optimistic-store.md:39` | — | — | 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:41` | — | — | 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:45` | — | — | 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:47` | — | — | 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:51` | — | — | Unaffected subscribers do not rerun on another action's settle.** | +| OS-R16 | live | `docs/rules-mining/optimistic-store.md:53` | — | — | Cycles are independent** (no residue between sequential write/settle cycles). | +| OS-R17 | live | `docs/rules-mining/optimistic-store.md:55` | — | — | 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:59` | — | — | 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:61` | — | — | Disjoint-key concurrent actions revert independently** (incl. different rows, deletes) (#2899 ×3). | +| OS-R20 | live | `docs/rules-mining/optimistic-store.md:64` | — | — | 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:68` | — | — | 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:71` | — | — | 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:74` | — | — | 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:76` | — | — | 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:80` | — | — | 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:82` | — | — | 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:85` | — | — | 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:87` | — | — | 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:90` | — | — | 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:95` | 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:98` | — | — | 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:100` | — | — | Post-init untracked reads flow committed values,** including during a later refetch window. | +| OS-R33 | live | `docs/rules-mining/optimistic-store.md:102` | — | — | Refetch window keeps the dev safeguard** (committed value untracked; component-body read still dev-throws). | +| OS-R34 | live | `docs/rules-mining/optimistic-store.md:104` | — | — | 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:106` | — | — | Plain stores unaffected** (read normally in every context incl. component bodies). | +| OS-R36 | live | `docs/rules-mining/optimistic-store.md:110` | — | — | Dependency-driven refetch pends the leaf and holds the committed view** until the fetch lands. | +| OS-R37 | live | `docs/rules-mining/optimistic-store.md:112` | — | — | 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:114` | 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:117` | — | — | 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:119` | — | — | 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:121` | — | — | 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:123` | — | — | 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:126` | — | — | 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:128` | — | — | 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:130` | — | — | 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:133` | — | — | 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:7` | — | — | 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:10` | 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:13` | — | — | 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:17` | — | — | 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:20` | 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:23` | 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:26` | 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:30` | — | — | `{ key: null }` merges positionally** (proxy identity preserved regardless of key-field changes). | +| PJ-R9 | live | `docs/rules-mining/projections.md:32` | — | — | 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:35` | 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:39` | — | — | `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:44` | — | — | 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:47` | — | — | 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:51` | — | — | Every subscriber of a changed property is notified exactly once per change.** | +| PJ-R15 | live | `docs/rules-mining/projections.md:53` | — | — | 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:55` | — | — | `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:60` | — | — | 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:64` | — | — | 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:66` | — | — | 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:68` | — | — | Chained backing works for array roots** (structural + row-level edits flow). | +| PJ-R21 | live | `docs/rules-mining/projections.md:70` | — | — | `createStore(fn, seed)` is the same projection mechanism and chains identically.** | +| PJ-R22 | live | `docs/rules-mining/projections.md:72` | — | — | `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:78` | 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:81` | — | — | 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:83` | — | — | 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:85` | — | — | 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:87` | — | — | Async recompute does not coarsen granularity**: after settle, only changed-path subscribers rerun. | +| PJ-R28 | live | `docs/rules-mining/projections.md:89` | — | — | `refresh(proj)` forces a new derive run; bare refresh is quiet** (no pending published; silent reveal). | +| PJ-R29 | live | `docs/rules-mining/projections.md:91` | — | — | `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:93` | — | — | With no effect subscribed, async work creates no transition** (isPending false throughout initial load). | +| PJ-R31 | live | `docs/rules-mining/projections.md:95` | — | — | 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:97` | — | — | 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:99` | — | — | 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:101` | — | — | 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:104` | — | — | 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:108` | — | — | 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:7` | — | — | 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:10` | — | — | Reconcile applies to any nested proxy, not just the root**, with identical semantics. | +| RS-R3 | live | `docs/rules-mining/reconcile-snapshot.md:13` | — | — | 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:16` | — | — | `key: null` / `key: ""` disables key matching**: positional merge, no root identity check. | +| RS-R5 | live | `docs/rules-mining/reconcile-snapshot.md:19` | — | — | Key modes: string key, key function, none.** KeyFn's call set is observable (see R17). | +| RS-R6 | live | `docs/rules-mining/reconcile-snapshot.md:22` | — | — | Key-matched items preserve logical (proxy) identity across reorder, insert, delete.** | +| RS-R7 | live | `docs/rules-mining/reconcile-snapshot.md:25` | — | — | 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:28` | 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:31` | 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:34` | 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:37` | reconcile.ts×2 | — | Null entries and primitives are legal keyed-array members** (#2772). | +| RS-R12 | live | `docs/rules-mining/reconcile-snapshot.md:40` | — | — | 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:43` | — | — | 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:46` | — | — | Symbol keys have full parity with string keys under reconcile** (update/remove/add/nested/mixed). | +| RS-R15 | live | `docs/rules-mining/reconcile-snapshot.md:49` | — | — | 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:52` | 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:55` | 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:58` | 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:61` | — | — | `deep()` observes a reconcile as a single notification carrying the final plain data.** | +| RS-R20 | live | `docs/rules-mining/reconcile-snapshot.md:64` | — | — | (type-level) — `reconcile(next)` requires the complete store type.** | +| RS-R21 | live | `docs/rules-mining/reconcile-snapshot.md:68` | — | — | 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:71` | — | 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:76` | — | — | `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:79` | — | — | 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:82` | — | — | 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:85` | — | — | 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:88` | — | — | 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:91` | 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:94` | 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:97` | — | — | 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:100` | — | — | 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:103` | — | — | 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:108` | — | — | `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:111` | — | — | `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:113` | — | — | `merge` over signal-of-object source is reactive with minimal notifications.** | +| RS-R36 | live | `docs/rules-mining/reconcile-snapshot.md:115` | — | — | `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:117` | — | 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:119` | — | — | 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_ | diff --git a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md index ab9cd1462..8b63119bb 100644 --- a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md +++ b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md @@ -1,5 +1,7 @@ # Async/Pending/Transition Semantics — Spec Propositions +> **Index:** every rule ID cited from `src/` or `tests/` — this file's A/B/C/V ids, the INV/RUL/R/§ vocabularies of the internals and rules-mining docs — is listed with status, definition and citations in [`RULES-INDEX.md`](./RULES-INDEX.md) (generated; `node scripts/rules-index.mjs`). IDs are never renumbered. + Companion to `INTERNALS-ASYNC-STATE.md`. Each proposition is a testable, user-observable behavior statement about `isPending` / `latest` / transitions / optimistic lanes. diff --git a/packages/signals/scripts/rules-index.mjs b/packages/signals/scripts/rules-index.mjs new file mode 100644 index 000000000..70fe86d4d --- /dev/null +++ b/packages/signals/scripts/rules-index.mjs @@ -0,0 +1,269 @@ +// Generates docs/RULES-INDEX.md — one row per rule ID across every vocabulary +// the code and tests cite — and resolves every citation in src/ and tests/. +// +// node scripts/rules-index.mjs # write docs/RULES-INDEX.md +// node scripts/rules-index.mjs --check # exit 1 if a src/ citation does not resolve +// +// Vocabularies (IDs are NEVER renumbered; this file only indexes them): +// A SPEC-ASYNC-SEMANTICS.md Tier-A propositions (table rows) +// B/C SPEC tier B/C entries +// V SPEC "Known violations" (fixed) +// INV- INTERNALS-ASYNC-STATE.md §5 invariants +// RUL- INTERNALS-STORE-STATE.md §8b rulings (+ rules-mining/FINDINGS.md) +// -R rules-mining/*.md mined 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. Source comments qualify with the words +// "core", "opt", "proj", "snap"; a bare R in a module comment +// refers to that module's file (store/target → CS, reconcile → RS, +// projection → PJ, optimistic → OS, core/* → OL). +// § INTERNALS-STORE-STATE.md numbered sections, and NODE-SHAPE.md's +// recovered stage-3 sections (§11b, §12–§12e). +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const DOCS = path.join(ROOT, "docs"); +const read = p => fs.readFileSync(p, "utf8"); +const rel = p => path.relative(ROOT, p); +const walk = (dir, out = []) => { + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + const p = path.join(dir, e.name); + if (e.isDirectory()) walk(p, out); + else if (/\.(ts|tsx|md)$/.test(e.name)) out.push(p); + } + return out; +}; + +// ── definitions ──────────────────────────────────────────────────────────── +const rules = new Map(); // key -> { key, id, vocab, ns, def, status, text } +function status(text) { + const head = text.slice(0, 160); + if (/SUPERSEDED/.test(text)) return "superseded"; + if (/RETIRED/.test(head)) return "retired"; + if (/RULED OUT/.test(head)) return "ruled out"; + if (/— FIXED|FIXED\.\*\*/.test(head)) return "fixed"; + if (/RESOLVED/.test(head)) return "resolved"; + if (/CLOSED by/.test(head)) return "closed"; + if ( + /re-ruled|Amended|amended|refined by re-rule|Re-scoped|Phrasing updated|was [BC]\d/i.test(head) + ) + return "amended"; + if (/RULED|ruled 20/.test(head)) return "ruled"; + return "live"; +} +function add(key, id, vocab, ns, file, line, text) { + if (rules.has(key)) return; + rules.set(key, { + key, + id, + vocab, + ns, + def: `${rel(file)}:${line}`, + status: status(text), + text: text.replace(/\s+/g, " ").trim() + }); +} +function scan(file, re, mk) { + if (!fs.existsSync(file)) return; + read(file) + .split("\n") + .forEach((l, i) => { + const m = re.exec(l); + if (m) mk(m, i + 1, l); + }); +} +const SPEC = path.join(DOCS, "SPEC-ASYNC-SEMANTICS.md"); +scan(SPEC, /^\| (A\d{1,2}) +\|(.*)$/, (m, n) => add(m[1], m[1], "A", "", SPEC, n, m[2])); +scan(SPEC, /^- \*\*(V\d)\b(.*)$/, (m, n, l) => add(m[1], m[1], "V", "", SPEC, n, l)); +// Tier B/C ids promoted into A-rules ("(was B1)") keep resolving to the row they became. +scan(SPEC, /^\| (A\d{1,2}) +\|.*\(was ([BC]\d)\b/, (m, n) => + add(m[2], m[2], m[2][0], "", SPEC, n, `PROMOTED → ${m[1]} (${m[1]}'s row carries the ruling).`) +); +scan(SPEC, /^- \[[ x]\] \*\*([BC]\d)\b(.*)$/, (m, n, l) => + add(m[1], m[1], m[1][0], "", SPEC, n, l) +); +const IAS = path.join(DOCS, "INTERNALS-ASYNC-STATE.md"); +scan(IAS, /^- \*\*(INV-\d+)\b(.*)$/, (m, n, l) => add(m[1], m[1], "INV", "", IAS, n, l)); +const ISS = path.join(DOCS, "INTERNALS-STORE-STATE.md"); +for (const f of [ISS, path.join(DOCS, "rules-mining/FINDINGS.md")]) + scan(f, /^- \*\*(RUL-\d+)\b(.*)$/, (m, n, l) => add(m[1], m[1], "RUL", "", f, n, l)); +scan(ISS, /^## (\d+[a-z]?)\. (.*)$/, (m, n) => add("§" + m[1], "§" + m[1], "§", "", ISS, n, m[2])); +scan(ISS, /^Corollary (R\d+[a-z])\b(.*)$/, (m, n, l) => + add("CS-" + m[1], m[1], "R", "CS", ISS, n, l) +); +const NS = { + "core-store": "CS", + "optimistic-lanes": "OL", + "optimistic-store": "OS", + projections: "PJ", + "reconcile-snapshot": "RS" +}; +for (const [file, ns] of Object.entries(NS)) { + const f = path.join(DOCS, "rules-mining", file + ".md"); + scan(f, /^\*\*(R\d{1,2}[a-z]?)[.\s*—-]+(.*)$/, (m, n) => + add(ns + "-" + m[1], m[1], "R", ns, f, n, m[2]) + ); +} +const SHAPE = path.join(DOCS, "NODE-SHAPE.md"); +scan(SHAPE, /^#{2,3} §?(\d+[a-z]?)\. (.*)$/, (m, n) => + add("§" + m[1], "§" + m[1], "§", "", SHAPE, n, m[2]) +); + +// ── citations ────────────────────────────────────────────────────────────── +const MODULE_NS = [ + [/store\/next\/reconcile\.ts$/, "RS"], + [/store\/next\/projection\.ts$/, "PJ"], + [/store\/next\/optimistic\.ts$/, "OS"], + [/store\/next\/(store|target)\.ts$/, "CS"], + [/store\//, "CS"], + [/core\//, "OL"] +]; +const QUAL = { core: "CS", opt: "OS", proj: "PJ", snap: "RS", lanes: "OL", lane: "OL" }; +function nsFor(file, qualifier) { + if (qualifier && QUAL[qualifier]) return QUAL[qualifier]; + for (const [re, ns] of MODULE_NS) if (re.test(file)) return ns; + return "CS"; +} +const CITE = + /\b(A\d{1,2}|V[1-5]|INV-\d+|RUL-\d+|[BC]\d)\b(?![.\d])|(?:\b(core|opt|proj|snap|lanes?)\s+)?\bR(\d{1,2}[a-z]?)\b|(§\d+[a-z]?)/g; +function citationsIn(file) { + const out = []; + const c = read(file); + for (const m of c.matchAll(CITE)) { + if (m[1]) out.push(m[1]); + else if (m[3]) { + const ns = nsFor(file, m[2]); + // A bare R the citing module's own file does not define is a + // core-store rule (the base vocabulary every store module builds on). + out.push(rules.has(ns + "-R" + m[3]) || m[2] ? ns + "-R" + m[3] : "CS-R" + m[3]); + } else if (m[4]) out.push(m[4]); + } + return out; +} +const srcFiles = walk(path.join(ROOT, "src")).filter(f => /\.tsx?$/.test(f)); +const testFiles = walk(path.join(ROOT, "tests")).filter(f => /\.tsx?$/.test(f)); +const cited = { src: new Map(), tests: new Map() }; +for (const [kind, files] of [ + ["src", srcFiles], + ["tests", testFiles] +]) + for (const f of files) + for (const id of citationsIn(f)) { + const m = cited[kind].get(id) ?? new Map(); + m.set(rel(f), (m.get(rel(f)) ?? 0) + 1); + cited[kind].set(id, m); + } +const fmtCites = m => (m ? [...m].map(([f, n]) => `${path.basename(f)}×${n}`).join(" ") : "—"); +const unresolved = kind => [...cited[kind].keys()].filter(id => !rules.has(id)).sort(); + +// ── output ───────────────────────────────────────────────────────────────── +if (process.argv.includes("--check")) { + const bad = unresolved("src"); + if (bad.length) { + console.error("rules-index: citations in src/ that resolve to no definition:", bad.join(" ")); + process.exit(1); + } + console.log("rules-index: every src/ citation resolves (" + cited.src.size + " ids)"); + process.exit(0); +} +const order = { A: 0, V: 1, B: 2, C: 3, INV: 4, RUL: 5, R: 6, "§": 7 }; +const rows = [...rules.values()].sort( + (a, b) => + order[a.vocab] - order[b.vocab] || + (a.ns || "").localeCompare(b.ns || "") || + parseInt(a.id.replace(/\D/g, "")) - parseInt(b.id.replace(/\D/g, "")) || + a.id.localeCompare(b.id) +); +const esc = s => s.replace(/\|/g, "\\|"); +const L = []; +L.push("# Rules Index — every rule ID the code and tests cite, and where it lives", ""); +L.push( + "**Generated by `scripts/rules-index.mjs`; do not edit by hand.** Regenerate with `node scripts/rules-index.mjs`; `--check` fails when a `src/` citation resolves to nothing (run in `pnpm test` via `tests/rules-index.test.ts`).", + "" +); +L.push( + "IDs are never renumbered or deleted — source comments cite them. A superseded, retired or fixed rule keeps its row with its status; the statement column shows how it reads at its definition (first ~200 chars).", + "" +); +L.push( + "## 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) |", + "" +); +L.push( + "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.", + "" +); +const summary = {}; +for (const r of rows) { + const k = r.vocab === "R" ? `R (${r.ns})` : r.vocab; + summary[k] ??= { n: 0, src: 0, tests: 0, uncited: 0 }; + summary[k].n++; + if (cited.src.has(r.key)) summary[k].src++; + if (cited.tests.has(r.key)) summary[k].tests++; + if (!cited.src.has(r.key) && !cited.tests.has(r.key)) summary[k].uncited++; +} +L.push( + "## Summary", + "", + "| vocabulary | rules | cited in src | cited in tests | cited nowhere |", + "|---|---|---|---|---|" +); +for (const [k, v] of Object.entries(summary)) + L.push(`| ${k} | ${v.n} | ${v.src} | ${v.tests} | ${v.uncited} |`); +L.push(""); +const un = unresolved("src"), + unT = unresolved("tests"); +L.push( + "## Unresolved citations", + "", + un.length ? `**src/:** ${un.join(", ")}` : "**src/:** none — every citation resolves.", + "", + unT.length + ? `**tests/:** ${unT.join(", ")} (test-only citations are informational; \`--check\` gates src/ only)` + : "**tests/:** none.", + "" +); +let cur = ""; +for (const r of rows) { + const section = + r.vocab === "R" + ? `R — ${Object.keys(NS).find(k => NS[k] === r.ns)} (\`${r.ns}-R\`)` + : { + A: "A — spec propositions", + V: "V — fixed violations", + B: "B — tier B", + C: "C — tier C", + INV: "INV — invariants", + RUL: "RUL — store rulings", + "§": "§ — design sections" + }[r.vocab]; + if (section !== cur) { + cur = section; + L.push( + `## ${section}`, + "", + "| id | status | defined | cited in src | cited in tests | statement (at definition) |", + "|---|---|---|---|---|---|" + ); + } + L.push( + `| ${r.key} | ${r.status} | \`${r.def}\` | ${esc(fmtCites(cited.src.get(r.key)))} | ${esc(fmtCites(cited.tests.get(r.key)))} | ${esc(r.text.slice(0, 200))}${r.text.length > 200 ? "…" : ""} |` + ); +} +fs.writeFileSync(path.join(DOCS, "RULES-INDEX.md"), L.join("\n") + "\n"); +console.log( + `RULES-INDEX.md: ${rows.length} rules; src citations ${cited.src.size} ids (${un.length} unresolved); tests ${cited.tests.size} ids (${unT.length} unresolved)` +); +if (un.length) console.log("unresolved in src:", un.join(" ")); diff --git a/packages/signals/tests/rules-index.test.ts b/packages/signals/tests/rules-index.test.ts new file mode 100644 index 000000000..edea32dbe --- /dev/null +++ b/packages/signals/tests/rules-index.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +// Every rule ID a source comment cites (A, V, INV-, RUL-, R, +// §) must resolve to a definition in docs/ — see docs/RULES-INDEX.md and +// scripts/rules-index.mjs. IDs are never renumbered; a section that moves or +// a rule that retires keeps its ID as an anchor. This caught six dangling +// citations (§11b, §12–§12e, INV-8) whose definitions had left with the +// pre-absorb DESIGN-PATCH-CHANNEL.md. +describe("rules index", () => { + it("every rule ID cited in src/ resolves to a documented definition", () => { + const script = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../scripts/rules-index.mjs" + ); + const out = execFileSync(process.execPath, [script, "--check"], { encoding: "utf8" }); + expect(out).toContain("every src/ citation resolves"); + }); +}); From a1359d40085477ed315b6d07eeb855fa748045ea Mon Sep 17 00:00:00 2001 From: Ryan Carniato Date: Mon, 14 Sep 2026 10:11:14 -0700 Subject: [PATCH 2/7] docs(signals): reorganize the async spec by topic; dated findings to History MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SPEC-ASYNC-SEMANTICS.md: the Tier-A table (27 rows, 130 KB of cell padding) becomes topic sections — Reads and visibility, Verdicts, Transactions and holds, Loading window and seeds, Errors — each rule as "### A. " with an explicit **Status** line (ruled / ruled, amended in place / superseded by), its **Pinned by** tests, and a **Mechanism** cross-reference (the node fields and functions the rule requires — the column a single visibility resolver would be built from; added by the index, not part of the ruling). Every proposition, ruling and pin is carried over verbatim (checked by the transform); A20/A21 stay at their IDs under "Superseded rules"; Tier B/C, the fixed violations and the three re-ruling logs move unchanged under History. 200 KB → 70 KB, no ID renumbered. INTERNALS-ASYNC-STATE.md: the dated findings §5a–§5h move verbatim under "History — dated findings" with a note in their place saying which still govern (§5d/§5e/§5g/§5h live; §5a–§5c historical; §5f superseded). scripts/rules-index.mjs reads the new section format; every src/ citation still resolves (77 ids), tests/rules-index.test.ts green. Co-authored-by: Cursor <cursoragent@cursor.com> --- .../signals/docs/INTERNALS-ASYNC-STATE.md | 448 +++++++++--------- packages/signals/docs/RULES-INDEX.md | 82 ++-- packages/signals/docs/SPEC-ASYNC-SEMANTICS.md | 298 ++++++++++-- packages/signals/scripts/rules-index.mjs | 41 +- 4 files changed, 562 insertions(+), 307 deletions(-) diff --git a/packages/signals/docs/INTERNALS-ASYNC-STATE.md b/packages/signals/docs/INTERNALS-ASYNC-STATE.md index fd8011571..999878314 100644 --- a/packages/signals/docs/INTERNALS-ASYNC-STATE.md +++ b/packages/signals/docs/INTERNALS-ASYNC-STATE.md @@ -208,7 +208,231 @@ Rejected for assertion (state space too dynamic, would need semantic rulings): whether `_optimisticLane` must always resolve to a live lane (stale lanes are legal and lazily cleared by `resolveLane`). -## 5a. Findings from the first assertion run (2026-07-06) +### Dated findings — where they went + +The findings that used to follow here (§5a–§5h, 2026-07-06 → 2026-07-16) are kept verbatim under **History — dated findings** at the end of this file, with their numbers (other docs cite them). What still governs the current model: + +- **§5d — the #2838 redesign as landed** (2026-07-07): the companion/oracle structure in §1–§4 IS this redesign. Live. +- **§5e — revert-target elimination** (A18 re-rule): `_pendingValue` has one meaning; INV-8 retired. Live. +- **§5g — question-scoped pending** (A24, supersedes the mask): `_reask`, `_affectsCount`, the sentinel rails. Live. +- **§5h — seed invisibility on derived stores** (A25). Live. +- **§5a–§5c** — the first assertion runs and the companion-vs-oracle census: how the redesign was arrived at. Historical. +- **§5f — the mask model** (A20/A21): SUPERSEDED by §5g six days later; kept for the reasoning record only. + +## 6. Assumptions / open questions (feed into tier B/C propositions) + +- `[RULED 2026-07-07 → A19/V3]` When async is in flight on a node whose + transition already completed: `isPending` must report `true` — the + observable value is not final. See decision log and SPEC A19. + +- `[assumed]` A resting optimistic node (`_overrideValue === NOT_PENDING`) is + semantically identical to a plain node for every read/pending computation + (#2799/#2806 lean this way; not stated as a rule). +- `[assumed]` Parent/child lane independence (companion lanes don't merge with + owner lanes) is a design decision, not an accident — the `assignOrMergeLane` + carve-out encodes it. (The other encoder, `updatePendingSignal`'s late + merge at override-clear, was removed as dead under the mask model — §5f.) +- `[open]` When two transitions merge, should `isPending` observers of a source + in transition A report pending for async that only transition B is waiting + on? (Current behavior: yes, merged transitions are one unit.) +- `[RULED 2026-07-07 → C2]` A revert may only clear lane assignments that + resolve to the reverting node's own lane — reverts do not trump other live + lanes. `insertSubs`'s blanket reversion clear is wrong in principle + (unobservable today); fix + assertion queued for the #2838 redesign. +- `[RULED 2026-07-07 → C3, closed by A19]` Early transition completion after + reporter pruning is by design — transitions coordinate rendered commits + only. Verdict correctness in the leftover window is A19's job (V3). + +## 7. Decision log + +- 2026-07-17: **A18 store corollary — per-transaction optimistic layer** + (#2899). `createOptimisticStore`'s override layer is one record per store + target, but a settling action must consume only its own entries: layer + writes stamp their transaction in `STORE_OPTIMISTIC_OWNERS` (per key, + `null` = ambient), and `clearOptimisticOverride` scopes the settle-path + clear to keys whose resolved owner (merge chains via `currentTransition`, + dead owners never strand) is the completing transition. Node-level + overrides already had this granularity via `_optimisticNodes`; this is the + layer's half. Same-key writes entangle through the shared node as before; + projection landings still clear everything. Also fixed with #2898 in the + same window: literal-`undefined` optimistic writes land as + `OVERRIDE_UNDEFINED` so they can't erase the `_overrideValue` brand. +- 2026-07-16: **A25 ruled — seed invisibility on derived stores** (#2897). + The seed is a draft for the derive function; outside consumers get + NotReady (dev strictRead scopes: `PENDING_ASYNC_UNTRACKED_READ` first) + from every untracked trap path — get, has, ownKeys — until the first + resolution / first yield lands. Self reads and write-path (reconcile) + reads exempt. Returning the seed leaked an unobservable value; returning + `undefined` breaks non-nullable types. Implementation in §5h; six + createProjection.async pins updated from the old seed-visible behavior. +- 2026-07-13: **A20/A21 re-ruled — question-scoped pending** (supersedes the + 2026-07-07c mask; converged from the #2844/#2728 threads with GabbeV and + brenelz after cause-scoped pending, per-path masking + UNCHANGED vouching, + `background()`, and lane-bounded vouches were each rejected). The verdict + definition is now "a value change in flight that has not yet revealed, or a + live `affects()` mark": same-question re-asks (refresh/poll/confirm) are + silent; a new question pends monotonically and nothing silences it; + optimistic writes are verdict-inert (display without decree — honest mixed + state over an in-flight question); `affects(target, key?)` is the sole, + additive declaration verb (single optional key since 2026-07-14 — the + variadic form read as a 1.x path and was dropped). Vouching, `UNCHANGED`, + and the store-wide mask are deleted concepts. `isPending` keeps its name (isStale was weighed — + semantics now match "stale" but the argument-taking form already reads as + data-scoped; revisit only with docs-team pressure). Implementation in §5g; + scenario matrix pinned in `tests/question-scoped-pending.test.ts`. +- 2026-07-08: two open items carried out of the retired issue-triage log: + (1) **queued cleanup from #2838** — the `_parentSource !== el` read-ternary + exemption and the `NotReadyError` catch in `read()`'s latest branch + survive the redesign; the suite passes without the former (probed, + reverted) — take both in the next code-reduction pass with proper + analysis. (2) **watch-item from #2850** — `unwrapStoreValue` (set-trap + value extraction) deliberately consults only `STORE_OVERRIDE`, not the + optimistic overlay: writing another store's optimistic _guesses_ into a + target store's base data is a different semantic question than reading + (the guess would outlive its revert). Flag if it comes up. +- 2026-07-07c: **A20 re-ruled — the mask** (supersedes the previous day's + "overrides are unsettled" entry below; GabbeV model adopted after the + #2844/#2728 discussions and the todos-example precedent). **(SUPERSEDED + 2026-07-13 by the question-scoped pending re-rule above; kept for the + reasoning record.)** An active + override reads `isPending === false` for its whole lifetime, on every node + kind, in both forms — action affordances belong in the data (co-written + flags / separate `createOptimistic(false)`), never in verdicts. **A21** + added: for derived optimistic stores the mask is store-wide (any live + optimistic write silences the whole store — "the store is the boundary"; + writes to the same store entangle). **A8** re-ruled: `latest` is an + override the system writes for itself as soon as a held value exists, so + the latest form follows the source's own async only (never pending on + signals/sync computeds; false the instant the fetch resolves even if the + commit is held by merged async). **A9** re-ruled: both forms report a + firewall refetch on resting leaves (old latest-form filter gone; A21 is + the only silencer). Implementation in §5f; INV-10 enforces; the + repo agent rules (`.cursor/rules/async-registration-invariants.mdc`) carry + the mask rules so future changes can't regress them silently. +- 2026-07-07c: disposal latch fixed (INV-9, the #2845 edge): + `computePendingState` returns `false` for disposed nodes and + `disposeChildren` snaps companions, so no verdict outlives its source. +- 2026-07-07: #2838 core redesign landed — V1–V4 fixed (see §5d). The + carve-out removal reverses the #2799 _mechanism_ while preserving its + intent (the original #2799 symptom — pending muted during refresh — is + covered by the A13 spec tests; the fix's over-broad skip was V1's cause). +- 2026-07-07: considered and REJECTED — mode-conditional NotReady from + `isPending` (throw only during SSR/hydration, return a value in CSR). + Rationale for rejection: (1) the tracked-uninitialized read is the ONLY + throwing case — everything post-initialization is already safe outside + boundaries in every mode, so the proposal only legalizes hand-rolled + initial-load boundaries (`isPending(data) ? spinner : data()`); (2) + boundaries are structural (SSR streaming and hydration reveal need + delimited regions), verdicts are informational — a safe-everywhere client + primitive becomes the easiest way to write loading UI that SSR cannot + stream, creating a CSR→SSR migration cliff in code authors consider + finished; (3) it forks a primitive's semantics by execution mode right + after V1–V3 established that verdicts must not depend on context + accidents; (4) the one-rule model "isPending performs the read you give + it" (the probe is not a shield) stays true in every environment today. + Keep A16/B5a as ruled. +- 2026-07-07: A20 — overrides are unsettled; pending scope is a property of + the read. **(SUPERSEDED next day by the 2026-07-07c mask re-rule above; + kept for the reasoning record.)** (1) An active override reads `isPending === true` uniformly (every + node kind): overrides mask stale _content_ (A17), not _settlement_ — the + community no-extra-boolean idioms depend on it. Non-derived optimistic + signals/stores are transaction-scoped values, not predictions (no source can + confirm them; reversion is certain), so they are pending for the override's + whole lifetime. (2) An optimistic write pends exactly the leaves it touched + (known change set); a refetch pends every read of the store (unbounded + change set) — broadness is what unbounded uncertainty looks like, not a + store rule. (3) Three-form algebra: `latest` strips _coordination_ + (transition holds, broad firewall inheritance) and nothing strips + _confirmation_ (own async in flight, active override) — so the latest form + is the "unconfirmed edit?" discriminator on store leaves, and is identical + to the plain form on standalone self-async nodes (A8): `latest` + discriminates _whose_ unsettledness you read, never _why_. An alternative + ("override = settled latest view → latest form as pure reload detector") + was considered and set aside: it makes the unconfirmed-edit question + inexpressible in any form and breaks the published community idioms, while + its reload-only question is already answered by scoping reads at + triggers/sources. NOTE the no-contradiction result that settled the ruling: + the "optimism guards against pending" pattern (News/Finance) is downstream + value-shielding — the override stops _invalidation_ from cascading, so a + downstream memo stays clean and its own verdict stays false; pending + propagates through async status flags, not through cached values, so the + optimistic node's own `pending === true` never "reads through" a clean + memo. Both halves are pinned by the News/Finance action tests. + Latest-form leaf filtering is currently broken (fires during pure refresh, + then the companion is STUCK true at settle — INV-4 catches the stuck + state): pinned as V4 for the #2838 redesign. +- 2026-07-07: C2 ruled — reverts do not trump other live lanes; a revert + releases only members of the reverting node's own lane. Fix + INV-8-style + assertion ("live lane members are only released by their own lane's + resolution") queued for the #2838 redesign. +- 2026-07-07: C3 closed by A19 — early transition completion in reporter-less + graphs is legal; verdicts must not depend on it (V3 pins the symptom). +- 2026-07-07: C1 → A19 — `isPending(x)` ≡ "the observable value of x is not + final". Three causes with independent lifetimes: (i) transition-held write + (ends at commit), (ii) own async in flight (ends at resolution), (iii) + fresh value held uncommitted by an entangled transition (ends at commit). + Uninitialized async is loading, not pending; its initial NotReady plays to + boundaries for SSR/hydration (A16). Partially reverses the earlier + boundary-scoped framing, which was cause (i) wrongly generalized to + (ii)/(iii). Verdicts must not depend on reporters/graph topology. Causes + (ii)/(iii) land with the #2838 redesign — pinned as V3/V1 expected + failures until then. +- 2026-07-07: B4 → A18 — an override's lifetime is bound to its own async + source, not its transition. Own-source resolution clears/corrects the + override immediately (fresh value, never the pre-write value); unrelated + async in a merged transition must not delay the correction or the async it + triggers. Together with A17: the override holds while its own fetch is in + flight (transition can't complete and drop it), and yields the moment the + authoritative value arrives. **(Superseded by the 2026-07-07b re-rule + below.)** +- 2026-07-07b: A18 re-ruled during the revert-target elimination (§5e) — + an override's lifetime is bound to **its own transition**, and `_value` + changes only at commit points. In unmerged graphs own-source resolution IS + the lane-transition's completion, so behavior coincides with the original + ruling (all original A18 pins unchanged). In genuinely merged transitions, + corrections reveal atomically with the merged completion — pending true + throughout (A20) — instead of escaping early via the revert-commit. The + "unrelated async must not delay" concern was re-examined: matching + confirmations collapse silently either way; corrections propagate + internally on arrival (no waterfalls); only the reveal is gated, honestly + dimmed. Maintainer: "the non-blocking aspect… this only gates the reveal"; + "`_value` elevation should only happen at the end of the transition." + +- 2026-07-06: C4 → A17 — an active optimistic override is THE value for every + reader (ambient and tracked), regardless of entanglement, until its owning + transition completes. `transitionComplete` no longer excludes self-sourced + pending optimistic nodes from blocking completion. +- 2026-07-07: A17 clarification — no-tearing is enforced at the EFFECT level, + not the read level. When async derived from the optimistic value is in + flight, the lane holds its render effects (`runLaneEffects` skips lanes with + `_pendingAsync`), so the rendered view updates as one unit; but direct reads + (ambient or in-graph) still return the override immediately. An attempted + read-level gate (return committed value from ownerless reads while the lane + holds) broke 19 real-world pinned tests (CategoryDisplay/News-Finance in + `createOptimistic.test.ts` — "direct read shows optimistic, effect waits") + and was reverted. Render effects read close to ambient semantics (stale + reads), so a read-level gate cannot distinguish them cleanly anyway. +- 2026-07-06: #2837 — comparator errors are node errors (boundary-containable); + `setSignal` checks uninitialized before comparator. +- 2026-07-06: #2839 — `EffectBundle.error` is compute-phase only; effect-phase + throws escalate to boundary/halt. Compute-phase errors in _user_ effects + without a handler: log + skip run, system stays alive. +- 2026-05..07: #2829/#2831 — `latest()`/`isPending()` fixes; `syncCompanions` + is the single companion-update chokepoint; `[false, undefined]` test pins + were a regression, not design. +- #2822 — `ASYNC_OUTSIDE_LOADING_BOUNDARY` is warn-only; the old hard-error + path was vestigial. +- #2761/#2762 — uncaught errors halt the system loudly (`REACTIVITY_HALTED`); + recovery belongs to error boundaries. +- #2838 (tracked) — `latest()` shadow should become write-driven post-release; + the probe-based design is acknowledged overcomplication. + + +## History — dated findings (§5a–§5h) + +Verbatim, in original order. See "Dated findings — where they went" under §5 for which still govern. + +### 5a. Findings from the first assertion run (2026-07-06) Enabling the assertions against the existing green suite immediately produced two findings — one real defect, one wrong assumption of mine: @@ -229,7 +453,7 @@ two findings — one real defect, one wrong assumption of mine: status, no held value, no override). What the companion should read in that in-between window is a **semantic** question, not a consistency one — see §6. -## 5b. Findings from the second assertion pass (2026-07-06, INV-2 + window probes) +### 5b. Findings from the second assertion pass (2026-07-06, INV-2 + window probes) - **INV-2 implemented and green.** Active override ⇒ `_pendingValue` revert target + registration in a `_optimisticNodes` list, checked at the end of @@ -256,7 +480,7 @@ two findings — one real defect, one wrong assumption of mine: an optimistic node with an active override and _any_ in-flight async (its own fetch included) is not complete. Ruled as A17. -## 5c. Companion-vs-oracle census (2026-07-07, #2838 pre-work) +### 5c. Companion-vs-oracle census (2026-07-07, #2838 pre-work) A non-asserting diff logger (`devCensusCompanions`, enabled via the `COMPANION_CENSUS` env var) compared every live companion against a fresh @@ -297,7 +521,7 @@ must be updated at exactly four transition points — status-flag transitions and (for shadows) initialization from the committed value + override mirroring. Those four cover all nine fingerprints plus V1–V4. -## 5d. The redesign as landed (2026-07-07, closes #2838's core) +### 5d. The redesign as landed (2026-07-07, closes #2838's core) Companions stayed lazy and probe-created; what changed is that every oracle input now flows through to them, and settlement re-derives them: @@ -345,7 +569,7 @@ Cost: +253 B gzip on `dist/prod.js` (+1.0%); core reactivity benchmarks unchanged within noise. C2's `insertSubs` blanket lane-clear on reversion remains queued (still unobservable; needs dead-lane plumbing). -## 5e. Revert-target elimination (2026-07-07b — A18 re-rule) +### 5e. Revert-target elimination (2026-07-07b — A18 re-rule) The `_pendingValue` slot used to mean three things: a plain write awaiting flush commit, a transition-held value awaiting transition commit, and the @@ -387,7 +611,7 @@ window: under the 2026-07-13 model a held _correction_ — differing from the displayed override — reads pending; a matching confirm stays quiet. The 2026-07-07c mask read `false` throughout; §5g.) -## 5f. The mask model (2026-07-07c — A20/A21 re-rule, #2844/#2728) — SUPERSEDED +### 5f. The mask model (2026-07-07c — A20/A21 re-rule, #2844/#2728) — SUPERSEDED > **SUPERSEDED 2026-07-13 by the question-scoped pending model (§5g).** The > mask (`_optimisticMask`/`STORE_MASKED`/`maskStoreTarget`) is deleted; @@ -451,7 +675,7 @@ Dead machinery removed with the model (verified by suite + census): Cost: net −27 B raw / +8 B gzip on minified `dist/prod.js`; core reactivity and store benchmarks flat within noise (best-of-3 isolated runs). -## 5g. Question-scoped pending (2026-07-13 — supersedes the mask, #2844/#2728) +### 5g. Question-scoped pending (2026-07-13 — supersedes the mask, #2844/#2728) The verdict was re-derived from one definition: **a read is pending iff a value change is in flight for it that has not yet revealed, or it carries a @@ -609,7 +833,7 @@ and list over-lighting both fall out (an optimistic increment can't silence an unrelated in-flight navigation; an optimistic list add doesn't pend sibling rows because the confirm refresh is a quiet re-ask). -## 5h. Seed invisibility on derived stores (2026-07-16 — A25, #2897) +### 5h. Seed invisibility on derived stores (2026-07-16 — A25, #2897) A derived store's seed is a draft for the derive function, never a value an outside reader may observe. Memos already enforced this (untracked reads of @@ -640,211 +864,3 @@ keeps running. Supersession keeps the window open (a discarded stale yield lands nothing). Rejected alternatives: returning the seed (leaks a value the reader can never observe updating), returning `undefined` (breaks non-nullable types). - -## 6. Assumptions / open questions (feed into tier B/C propositions) - -- `[RULED 2026-07-07 → A19/V3]` When async is in flight on a node whose - transition already completed: `isPending` must report `true` — the - observable value is not final. See decision log and SPEC A19. - -- `[assumed]` A resting optimistic node (`_overrideValue === NOT_PENDING`) is - semantically identical to a plain node for every read/pending computation - (#2799/#2806 lean this way; not stated as a rule). -- `[assumed]` Parent/child lane independence (companion lanes don't merge with - owner lanes) is a design decision, not an accident — the `assignOrMergeLane` - carve-out encodes it. (The other encoder, `updatePendingSignal`'s late - merge at override-clear, was removed as dead under the mask model — §5f.) -- `[open]` When two transitions merge, should `isPending` observers of a source - in transition A report pending for async that only transition B is waiting - on? (Current behavior: yes, merged transitions are one unit.) -- `[RULED 2026-07-07 → C2]` A revert may only clear lane assignments that - resolve to the reverting node's own lane — reverts do not trump other live - lanes. `insertSubs`'s blanket reversion clear is wrong in principle - (unobservable today); fix + assertion queued for the #2838 redesign. -- `[RULED 2026-07-07 → C3, closed by A19]` Early transition completion after - reporter pruning is by design — transitions coordinate rendered commits - only. Verdict correctness in the leftover window is A19's job (V3). - -## 7. Decision log - -- 2026-07-17: **A18 store corollary — per-transaction optimistic layer** - (#2899). `createOptimisticStore`'s override layer is one record per store - target, but a settling action must consume only its own entries: layer - writes stamp their transaction in `STORE_OPTIMISTIC_OWNERS` (per key, - `null` = ambient), and `clearOptimisticOverride` scopes the settle-path - clear to keys whose resolved owner (merge chains via `currentTransition`, - dead owners never strand) is the completing transition. Node-level - overrides already had this granularity via `_optimisticNodes`; this is the - layer's half. Same-key writes entangle through the shared node as before; - projection landings still clear everything. Also fixed with #2898 in the - same window: literal-`undefined` optimistic writes land as - `OVERRIDE_UNDEFINED` so they can't erase the `_overrideValue` brand. -- 2026-07-16: **A25 ruled — seed invisibility on derived stores** (#2897). - The seed is a draft for the derive function; outside consumers get - NotReady (dev strictRead scopes: `PENDING_ASYNC_UNTRACKED_READ` first) - from every untracked trap path — get, has, ownKeys — until the first - resolution / first yield lands. Self reads and write-path (reconcile) - reads exempt. Returning the seed leaked an unobservable value; returning - `undefined` breaks non-nullable types. Implementation in §5h; six - createProjection.async pins updated from the old seed-visible behavior. -- 2026-07-13: **A20/A21 re-ruled — question-scoped pending** (supersedes the - 2026-07-07c mask; converged from the #2844/#2728 threads with GabbeV and - brenelz after cause-scoped pending, per-path masking + UNCHANGED vouching, - `background()`, and lane-bounded vouches were each rejected). The verdict - definition is now "a value change in flight that has not yet revealed, or a - live `affects()` mark": same-question re-asks (refresh/poll/confirm) are - silent; a new question pends monotonically and nothing silences it; - optimistic writes are verdict-inert (display without decree — honest mixed - state over an in-flight question); `affects(target, key?)` is the sole, - additive declaration verb (single optional key since 2026-07-14 — the - variadic form read as a 1.x path and was dropped). Vouching, `UNCHANGED`, - and the store-wide mask are deleted concepts. `isPending` keeps its name (isStale was weighed — - semantics now match "stale" but the argument-taking form already reads as - data-scoped; revisit only with docs-team pressure). Implementation in §5g; - scenario matrix pinned in `tests/question-scoped-pending.test.ts`. -- 2026-07-08: two open items carried out of the retired issue-triage log: - (1) **queued cleanup from #2838** — the `_parentSource !== el` read-ternary - exemption and the `NotReadyError` catch in `read()`'s latest branch - survive the redesign; the suite passes without the former (probed, - reverted) — take both in the next code-reduction pass with proper - analysis. (2) **watch-item from #2850** — `unwrapStoreValue` (set-trap - value extraction) deliberately consults only `STORE_OVERRIDE`, not the - optimistic overlay: writing another store's optimistic _guesses_ into a - target store's base data is a different semantic question than reading - (the guess would outlive its revert). Flag if it comes up. -- 2026-07-07c: **A20 re-ruled — the mask** (supersedes the previous day's - "overrides are unsettled" entry below; GabbeV model adopted after the - #2844/#2728 discussions and the todos-example precedent). **(SUPERSEDED - 2026-07-13 by the question-scoped pending re-rule above; kept for the - reasoning record.)** An active - override reads `isPending === false` for its whole lifetime, on every node - kind, in both forms — action affordances belong in the data (co-written - flags / separate `createOptimistic(false)`), never in verdicts. **A21** - added: for derived optimistic stores the mask is store-wide (any live - optimistic write silences the whole store — "the store is the boundary"; - writes to the same store entangle). **A8** re-ruled: `latest` is an - override the system writes for itself as soon as a held value exists, so - the latest form follows the source's own async only (never pending on - signals/sync computeds; false the instant the fetch resolves even if the - commit is held by merged async). **A9** re-ruled: both forms report a - firewall refetch on resting leaves (old latest-form filter gone; A21 is - the only silencer). Implementation in §5f; INV-10 enforces; the - repo agent rules (`.cursor/rules/async-registration-invariants.mdc`) carry - the mask rules so future changes can't regress them silently. -- 2026-07-07c: disposal latch fixed (INV-9, the #2845 edge): - `computePendingState` returns `false` for disposed nodes and - `disposeChildren` snaps companions, so no verdict outlives its source. -- 2026-07-07: #2838 core redesign landed — V1–V4 fixed (see §5d). The - carve-out removal reverses the #2799 _mechanism_ while preserving its - intent (the original #2799 symptom — pending muted during refresh — is - covered by the A13 spec tests; the fix's over-broad skip was V1's cause). -- 2026-07-07: considered and REJECTED — mode-conditional NotReady from - `isPending` (throw only during SSR/hydration, return a value in CSR). - Rationale for rejection: (1) the tracked-uninitialized read is the ONLY - throwing case — everything post-initialization is already safe outside - boundaries in every mode, so the proposal only legalizes hand-rolled - initial-load boundaries (`isPending(data) ? spinner : data()`); (2) - boundaries are structural (SSR streaming and hydration reveal need - delimited regions), verdicts are informational — a safe-everywhere client - primitive becomes the easiest way to write loading UI that SSR cannot - stream, creating a CSR→SSR migration cliff in code authors consider - finished; (3) it forks a primitive's semantics by execution mode right - after V1–V3 established that verdicts must not depend on context - accidents; (4) the one-rule model "isPending performs the read you give - it" (the probe is not a shield) stays true in every environment today. - Keep A16/B5a as ruled. -- 2026-07-07: A20 — overrides are unsettled; pending scope is a property of - the read. **(SUPERSEDED next day by the 2026-07-07c mask re-rule above; - kept for the reasoning record.)** (1) An active override reads `isPending === true` uniformly (every - node kind): overrides mask stale _content_ (A17), not _settlement_ — the - community no-extra-boolean idioms depend on it. Non-derived optimistic - signals/stores are transaction-scoped values, not predictions (no source can - confirm them; reversion is certain), so they are pending for the override's - whole lifetime. (2) An optimistic write pends exactly the leaves it touched - (known change set); a refetch pends every read of the store (unbounded - change set) — broadness is what unbounded uncertainty looks like, not a - store rule. (3) Three-form algebra: `latest` strips _coordination_ - (transition holds, broad firewall inheritance) and nothing strips - _confirmation_ (own async in flight, active override) — so the latest form - is the "unconfirmed edit?" discriminator on store leaves, and is identical - to the plain form on standalone self-async nodes (A8): `latest` - discriminates _whose_ unsettledness you read, never _why_. An alternative - ("override = settled latest view → latest form as pure reload detector") - was considered and set aside: it makes the unconfirmed-edit question - inexpressible in any form and breaks the published community idioms, while - its reload-only question is already answered by scoping reads at - triggers/sources. NOTE the no-contradiction result that settled the ruling: - the "optimism guards against pending" pattern (News/Finance) is downstream - value-shielding — the override stops _invalidation_ from cascading, so a - downstream memo stays clean and its own verdict stays false; pending - propagates through async status flags, not through cached values, so the - optimistic node's own `pending === true` never "reads through" a clean - memo. Both halves are pinned by the News/Finance action tests. - Latest-form leaf filtering is currently broken (fires during pure refresh, - then the companion is STUCK true at settle — INV-4 catches the stuck - state): pinned as V4 for the #2838 redesign. -- 2026-07-07: C2 ruled — reverts do not trump other live lanes; a revert - releases only members of the reverting node's own lane. Fix + INV-8-style - assertion ("live lane members are only released by their own lane's - resolution") queued for the #2838 redesign. -- 2026-07-07: C3 closed by A19 — early transition completion in reporter-less - graphs is legal; verdicts must not depend on it (V3 pins the symptom). -- 2026-07-07: C1 → A19 — `isPending(x)` ≡ "the observable value of x is not - final". Three causes with independent lifetimes: (i) transition-held write - (ends at commit), (ii) own async in flight (ends at resolution), (iii) - fresh value held uncommitted by an entangled transition (ends at commit). - Uninitialized async is loading, not pending; its initial NotReady plays to - boundaries for SSR/hydration (A16). Partially reverses the earlier - boundary-scoped framing, which was cause (i) wrongly generalized to - (ii)/(iii). Verdicts must not depend on reporters/graph topology. Causes - (ii)/(iii) land with the #2838 redesign — pinned as V3/V1 expected - failures until then. -- 2026-07-07: B4 → A18 — an override's lifetime is bound to its own async - source, not its transition. Own-source resolution clears/corrects the - override immediately (fresh value, never the pre-write value); unrelated - async in a merged transition must not delay the correction or the async it - triggers. Together with A17: the override holds while its own fetch is in - flight (transition can't complete and drop it), and yields the moment the - authoritative value arrives. **(Superseded by the 2026-07-07b re-rule - below.)** -- 2026-07-07b: A18 re-ruled during the revert-target elimination (§5e) — - an override's lifetime is bound to **its own transition**, and `_value` - changes only at commit points. In unmerged graphs own-source resolution IS - the lane-transition's completion, so behavior coincides with the original - ruling (all original A18 pins unchanged). In genuinely merged transitions, - corrections reveal atomically with the merged completion — pending true - throughout (A20) — instead of escaping early via the revert-commit. The - "unrelated async must not delay" concern was re-examined: matching - confirmations collapse silently either way; corrections propagate - internally on arrival (no waterfalls); only the reveal is gated, honestly - dimmed. Maintainer: "the non-blocking aspect… this only gates the reveal"; - "`_value` elevation should only happen at the end of the transition." - -- 2026-07-06: C4 → A17 — an active optimistic override is THE value for every - reader (ambient and tracked), regardless of entanglement, until its owning - transition completes. `transitionComplete` no longer excludes self-sourced - pending optimistic nodes from blocking completion. -- 2026-07-07: A17 clarification — no-tearing is enforced at the EFFECT level, - not the read level. When async derived from the optimistic value is in - flight, the lane holds its render effects (`runLaneEffects` skips lanes with - `_pendingAsync`), so the rendered view updates as one unit; but direct reads - (ambient or in-graph) still return the override immediately. An attempted - read-level gate (return committed value from ownerless reads while the lane - holds) broke 19 real-world pinned tests (CategoryDisplay/News-Finance in - `createOptimistic.test.ts` — "direct read shows optimistic, effect waits") - and was reverted. Render effects read close to ambient semantics (stale - reads), so a read-level gate cannot distinguish them cleanly anyway. -- 2026-07-06: #2837 — comparator errors are node errors (boundary-containable); - `setSignal` checks uninitialized before comparator. -- 2026-07-06: #2839 — `EffectBundle.error` is compute-phase only; effect-phase - throws escalate to boundary/halt. Compute-phase errors in _user_ effects - without a handler: log + skip run, system stays alive. -- 2026-05..07: #2829/#2831 — `latest()`/`isPending()` fixes; `syncCompanions` - is the single companion-update chokepoint; `[false, undefined]` test pins - were a regression, not design. -- #2822 — `ASYNC_OUTSIDE_LOADING_BOUNDARY` is warn-only; the old hard-error - path was vestigial. -- #2761/#2762 — uncaught errors halt the system loudly (`REACTIVITY_HALTED`); - recovery belongs to error boundaries. -- #2838 (tracked) — `latest()` shadow should become write-driven post-release; - the probe-based design is acknowledged overcomplication. diff --git a/packages/signals/docs/RULES-INDEX.md b/packages/signals/docs/RULES-INDEX.md index 1f7b853cf..4ef1ffc51 100644 --- a/packages/signals/docs/RULES-INDEX.md +++ b/packages/signals/docs/RULES-INDEX.md @@ -45,59 +45,59 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul | id | status | defined | cited in src | cited in tests | statement (at definition) | |---|---|---|---|---|---| -| A1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:22` | — | onCleanup.test.ts×2 transitionEntanglement.test.ts×4 | `EffectBundle.error` intercepts compute-phase errors only; effect-phase throws escalate to the nearest error boundary (halt if none). \| #2839 ruling (2026-07-06) \| `tests/effect-error-phases.test.ts` … | -| A2 | live | `docs/SPEC-ASYNC-SEMANTICS.md:23` | — | onCleanup.test.ts×2 | Compute-phase errors in _user_ effects without a handler are logged and the run is skipped; the system keeps running. \| #2839 ruling \| `tests/effect-error-phases.test.ts` \| | -| A3 | live | `docs/SPEC-ASYNC-SEMANTICS.md:24` | — | — | Errors thrown by a user `equals` comparator behave exactly like compute-phase errors (boundary-containable; loud halt without a boundary). \| #2837 \| `tests/equals-comparator-errors.test.ts` \| | -| A4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:25` | — | — | A custom `equals` is never invoked with `undefined` previous value on a node's first commit. \| #2837 follow-on \| `tests/equals-comparator-errors.test.ts` (async case) \| | -| A5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:26` | — | — | An error escaping every boundary permanently halts the system with `REACTIVITY_HALTED`; later writes log "Update ignored". \| #2761/#2762 \| `tests/createErrorBoundary.test.ts`, `tests/errorHalt.test.ts… | -| A6 | live | `docs/SPEC-ASYNC-SEMANTICS.md:27` | — | — | `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. \| #2822 \| `tests/enforceLoadingBoundary.test.ts`, solid… | -| A7 | live | `docs/SPEC-ASYNC-SEMANTICS.md:28` | — | spec-async-semantics.test.ts×2 | After an async memo resolves, `[isPending(x), latest(x)]` is `[false, resolvedValue]` — never `[false, undefined]`. \| #2829 (the `[false, undefined]` pins were a regression) \| `tests/latest-async.test… | -| A8 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:29` | — | — | (**re-ruled 2026-07-07c** — was "tracks the transition the same as `isPending(x)`"; **amended 2026-07-13**: "own async in flight" is further filtered to _non-quiet_ flight — a re-ask of the same quest… | -| A9 | live | `docs/SPEC-ASYNC-SEMANTICS.md:30` | — | spec-async-semantics.test.ts×3 | `isPending` on a store leaf behind a firewall reports the firewall's refetch like any async memo — in **both** forms (the latest-form filter of the old A20 is gone; an in-flight refetch supersedes bot… | -| A10 | live | `docs/SPEC-ASYNC-SEMANTICS.md:31` | invariants.ts×1 | — | `[isPending(x), x()]` read in one scope is atomic: a reader that observed the fresh value must not see `pending === true` for it. \| #2831 finding 2 \| `tests/latest-isPending-consistency.test.ts` \| | -| A11 | live | `docs/SPEC-ASYNC-SEMANTICS.md:32` | — | — | Sync derivations of transition-held sources are visible through `latest()`/`isPending()` (held sync recompute is a write path like any other). \| #2831 finding 3 \| `tests/latest-isPending-consistency.t… | -| A12 | live | `docs/SPEC-ASYNC-SEMANTICS.md:33` | — | — | A resting optimistic node reports pending via exactly the causes a plain async memo does (A19) — a reverting optimistic write is not among them (a revert is not a refetch). (Phrasing updated with V1: … | -| A13 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:34` | async.ts×1 | spec-async-semantics.test.ts×7 | (was B1) A resting optimistic node (no active override) is observationally identical to a plain async memo for `read`/`latest`/`isPending` at every checkpoint of a refetch cycle — both before any over… | -| A14 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:35` | — | spec-async-semantics.test.ts×2 | (was B2) `isPending`/`latest` companion nodes get child lanes that do not merge with the owner's lane: an `isPending` effect (spinner) fires while the owner's async is still in flight. (Re-scoped 2026… | -| A15 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:36` | core.ts×2 lanes.ts×1 scheduler.ts×1 | lane-hold-on-observation.test.ts×1 reveal-carve-out.test.ts×2 spec-async-semantics.test.ts×2 stale-read-uninitialized-cross-transition.test.ts×1 treeshake.test.ts×3 | (was B3) Transition entanglement is graph-driven: writes whose async work is observed by a shared reader settle as one unit (no tearing — nothing commits until all entangled async resolves); writes on… | -| A16 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:37` | verdict.ts×1 | spec-async-semantics.test.ts×1 strict-read-pending-store.test.ts×2 uninitialized-visibility.test.ts×1 | (was B5) `isPending` never throws in untracked contexts — thunks that throw real errors or read uninitialized async sources yield `false`. Carve-out (B5a, pinned as current behavior): in _tracked_ con… | -| A17 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:38` | async.ts×3 constants.ts×2 core.ts×7 invariants.ts×3 optimistic.ts×3 scheduler.ts×2 verdict.ts×1 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 | (was C4) An _active_ optimistic override is THE value for every **read** — ambient/untracked and tracked alike — regardless of transition entanglement. "It is the optimistic future value... it is both… | -| A18 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:39` | async.ts×2 constants.ts×1 core.ts×4 optimistic.ts×3 scheduler.ts×2 types.ts×2 optimistic.ts×1 | createOptimistic.test.ts×1 spec-async-semantics.test.ts×3 flight-owned-transaction.test.ts×1 | (was B4; **refined by re-rule 2026-07-07b**) An override's lifetime is bound to **its own transition** — which, because lanes keep their transitions separate from unrelated work, contains exactly the … | -| A19 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:40` | async.ts×1 optimistic.ts×1 | spec-async-semantics.test.ts×3 uninitialized-visibility.test.ts×1 | (was C1 — **partially reverses an earlier decision**) **Definition: `isPending(x)` ≡ the value you can currently observe for `x` is not the final one.** Three causes of non-finality, each ending on it… | -| A20 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:41` | 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** — the mask is deleted; optimistic writes are verdict-inert. Kept for the reasoning record. The surviving pieces: verdicts are per-channel (§2, now in A8), and action … | -| A21 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:42` | — | question-scoped-pending.test.ts×3 spec-async-semantics.test.ts×3 | (**SUPERSEDED 2026-07-13 by A24** — the store-wide mask is deleted with the mask model; nothing silences a new question. The effective-write gate (consequence 4) survives as transaction-entanglement h… | -| A22 | live | `docs/SPEC-ASYNC-SEMANTICS.md:43` | — | spec-async-semantics.test.ts×1 | **Pending is per-node: store-wide verdicts exist only as the firewall's own in-flight work (A9) and the decree that silences it (A21).** Every other A19 cause lives on the individual node — a transiti… | -| A23 | live | `docs/SPEC-ASYNC-SEMANTICS.md:44` | — | spec-async-semantics.test.ts×1 | **The `isPending` probe is reads-only — the thunk's return value is never inspected.** `isPending(() => store)` reads nothing and reports `false` by design: keying any behavior off the returned value … | -| A24 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:45` | — | optimistic-undefined-override.test.ts×1 reveal-gating-contract.test.ts×1 spec-async-semantics.test.ts×2 | (**ruled 2026-07-13** — supersedes A20/A21; the converged model from the #2844/#2728 threads) **Question-scoped pending: a read is pending iff a value change is in flight for it that has not yet revea… | -| A25 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:46` | — | uninitialized-visibility.test.ts×3 | (**ruled 2026-07-16**, #2897) **A derived store's seed is a draft, never an observable value.** The seed exists for the derive function — self reads while it works its draft are the point — but to eve… | -| A26 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:48` | scheduler.ts×1 | action-await-contract.test.ts×2 | (**ruled 2026-07-17**, #2913; **enforcement hardened 2026-08-31**, #3141 — parking is flush-driven, and a transaction opened with no writes before its first suspension scheduled no flush, so `activeTr… | -| A27 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:49` | — | — | (**ruled 2026-08-10**) **The commit-#0 loading window is loading-class and verdict-quiet.** A node born committed via `loadingValue` (memos: `createMemo` / `createSignal(fn)` / `createOptimistic(fn)`)… | +| A1 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:191` | — | 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:199` | — | 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:207` | — | — | [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:215` | — | — | [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:223` | — | — | [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:231` | — | — | [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 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:81` | — | spec-async-semantics.test.ts×2 | [ruled] Resolved async never reads `[false, undefined]` — After an async memo resolves, `[isPending(x), latest(x)]` is `[false, resolvedValue]` — never `[false, undefined]`. | +| A8 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:89` | — | — | [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:97` | — | spec-async-semantics.test.ts×3 | [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:105` | invariants.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:47` | — | — | [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:113` | — | — | [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:121` | 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:129` | — | 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 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:163` | core.ts×2 lanes.ts×1 scheduler.ts×1 | lane-hold-on-observation.test.ts×1 reveal-carve-out.test.ts×2 spec-async-semantics.test.ts×2 stale-read-uninitialized-cross-transition.test.ts×1 treeshake.test.ts×3 | [ruled 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 is observed by a s… | +| A16 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:137` | verdict.ts×1 | spec-async-semantics.test.ts×1 strict-read-pending-store.test.ts×2 uninitialized-visibility.test.ts×1 | [ruled 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 uninitialized asy… | +| A17 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:31` | async.ts×3 constants.ts×2 core.ts×7 invariants.ts×3 optimistic.ts×3 scheduler.ts×2 verdict.ts×1 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 | [ruled 2026-07-06 (promoted from C4)] An active override is THE value for every read — (was C4) An _active_ optimistic override is THE value for every **read** — ambient/untracked and tracked alike — … | +| A18 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:39` | async.ts×2 constants.ts×1 core.ts×4 optimistic.ts×3 scheduler.ts×2 types.ts×2 optimistic.ts×1 | createOptimistic.test.ts×1 spec-async-semantics.test.ts×3 flight-owned-transaction.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from B4)] An override's lifetime is bound to its own transition; arriving truth holds until commit — (was B4; **refined by re-rule 2026-07-07b**) An overr… | +| A19 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:65` | async.ts×1 optimistic.ts×1 | spec-async-semantics.test.ts×3 uninitialized-visibility.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:243` | 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:250` | — | 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:145` | — | spec-async-semantics.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:153` | — | 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:73` | — | optimistic-undefined-override.test.ts×1 reveal-gating-contract.test.ts×1 spec-async-semantics.test.ts×2 | [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:181` | — | uninitialized-visibility.test.ts×3 | [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:55` | scheduler.ts×1 | action-await-contract.test.ts×2 | [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:173` | — | — | [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… | ## V — fixed violations | id | status | defined | cited in src | cited in tests | statement (at definition) | |---|---|---|---|---|---| -| V1 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:105` | 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:115` | 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:121` | — | spec-async-semantics.test.ts×2 | - **V3 (violated A19) — FIXED.** After a reporter-less transition completed, | -| V4 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:128` | — | 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:140` | — | spec-async-semantics.test.ts×3 | - **V5 (A17 corollary — found and fixed with the revert-target elimination, | +| V1 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:313` | 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:323` | 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:329` | — | spec-async-semantics.test.ts×2 | - **V3 (violated A19) — FIXED.** After a reporter-less transition completed, | +| V4 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:336` | — | 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:348` | — | 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:34` | — | createRevealOrder.test.ts×16 onCleanup.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A13 (A13's row carries the ruling). | -| B2 | live | `docs/SPEC-ASYNC-SEMANTICS.md:35` | — | createRevealOrder.test.ts×16 onCleanup.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A14 (A14's row carries the ruling). | -| B3 | live | `docs/SPEC-ASYNC-SEMANTICS.md:36` | — | spec-async-semantics.test.ts×2 | PROMOTED → A15 (A15's row carries the ruling). | -| B4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:39` | — | spec-async-semantics.test.ts×2 | PROMOTED → A18 (A18's row carries the ruling). | -| B5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:37` | — | spec-async-semantics.test.ts×2 | PROMOTED → A16 (A16's row carries the ruling). | +| B1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:121` | — | 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:129` | — | 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:163` | — | spec-async-semantics.test.ts×2 | PROMOTED → A15 (A15's section carries the ruling). | +| B4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:39` | — | spec-async-semantics.test.ts×2 | PROMOTED → A18 (A18's section carries the ruling). | +| B5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:137` | — | 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:40` | — | onCleanup.test.ts×2 spec-async-semantics.test.ts×1 | PROMOTED → A19 (A19's row carries the ruling). | -| C2 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:71` | — | 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:81` | — | — | - [x] **C3 — CLOSED by A19 (2026-07-07): early completion is by design.** | -| C4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:38` | — | spec-async-semantics.test.ts×1 | PROMOTED → A17 (A17's row carries the ruling). | +| C1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:65` | — | 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:279` | — | 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:289` | — | — | - [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) | diff --git a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md index 8b63119bb..1d1a77f9a 100644 --- a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md +++ b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md @@ -9,46 +9,254 @@ optimistic lanes. - **Tier A** — has a citable ruling or design decision. Pinned by tests that should be treated as _spec_: changing them requires a design decision, not a code fix. Do not "update expectations" here to make an implementation change - pass. -- **Tier B** — believed correct but _inferred_ from code/issues. Needs a - maintainer verdict (keep / change) before promotion to Tier A. -- **Tier C** — genuinely open. Current behavior documented; a decision is - needed either way. - -## Tier A (ruled — pinned) - -| # | Proposition | Ruling | Pinned by | -| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| A1 | `EffectBundle.error` intercepts compute-phase errors only; effect-phase throws escalate to the nearest error boundary (halt if none). | #2839 ruling (2026-07-06) | `tests/effect-error-phases.test.ts` | -| A2 | Compute-phase errors in _user_ effects without a handler are logged and the run is skipped; the system keeps running. | #2839 ruling | `tests/effect-error-phases.test.ts` | -| A3 | Errors thrown by a user `equals` comparator behave exactly like compute-phase errors (boundary-containable; loud halt without a boundary). | #2837 | `tests/equals-comparator-errors.test.ts` | -| A4 | A custom `equals` is never invoked with `undefined` previous value on a node's first commit. | #2837 follow-on | `tests/equals-comparator-errors.test.ts` (async case) | -| A5 | An error escaping every boundary permanently halts the system with `REACTIVITY_HALTED`; later writes log "Update ignored". | #2761/#2762 | `tests/createErrorBoundary.test.ts`, `tests/errorHalt.test.ts` | -| A6 | `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. | #2822 | `tests/enforceLoadingBoundary.test.ts`, solid-web `test/dev-warning.spec.tsx` | -| A7 | After an async memo resolves, `[isPending(x), latest(x)]` is `[false, resolvedValue]` — never `[false, undefined]`. | #2829 (the `[false, undefined]` pins were a regression) | `tests/latest-async.test.ts`, `tests/createMemo.test.ts` | -| A8 | (**re-ruled 2026-07-07c** — was "tracks the transition the same as `isPending(x)`"; **amended 2026-07-13**: "own async in flight" is further filtered to _non-quiet_ flight — a re-ask of the same question is silent in the latest form too, and a live `affects()` mark on the owner pends it) **`isPending(() => latest(x))` follows `x`'s own async only — verdicts are per-channel.** `latest` is an override the system writes for itself the moment a held value exists ("it is like an optimistic that sets itself to the value as soon as it is available to do so"). So the latest form reads `true` only while `x`'s own fetch for a _new question_ is in flight (showing the stale value) and turns `false` the instant that fetch resolves — even if the same update has other async still running and the commit is held. On a signal or sync computed the held value exists from the instant of the write, so their latest form is _never_ pending. The plain form keeps watching the committed channel (holds included). Pairing falls out: `[isPending(() => latest(x)), latest(x)]` never pairs `true` with the fresh value. | GabbeV/maintainer re-rule, 2026-07-07c; quiet-re-ask filter 2026-07-13 | `tests/createMemo.test.ts`, solid-web `test/latest-async.spec.tsx` | -| A9 | `isPending` on a store leaf behind a firewall reports the firewall's refetch like any async memo — in **both** forms (the latest-form filter of the old A20 is gone; an in-flight refetch supersedes both channels). (**Amended 2026-07-13**: "refetch" means a _new-question_ refetch — an input value change in flight. A quiet re-ask of the same question (`refresh(store)` with value-stable inputs) is silent in both forms; the declared reload `affects(store); refresh(store)` pends. The old exception — the store-wide mask (A21) silencing it — is deleted with A21.) | #2831 finding 1; both-forms re-ruled 2026-07-07c; question scoping 2026-07-13 | `tests/latest-isPending-consistency.test.ts`, V4 pin in `tests/spec-async-semantics.test.ts`, `tests/question-scoped-pending.test.ts` | -| A10 | `[isPending(x), x()]` read in one scope is atomic: a reader that observed the fresh value must not see `pending === true` for it. | #2831 finding 2 | `tests/latest-isPending-consistency.test.ts` | -| A11 | Sync derivations of transition-held sources are visible through `latest()`/`isPending()` (held sync recompute is a write path like any other). | #2831 finding 3 | `tests/latest-isPending-consistency.test.ts` | -| A12 | A resting optimistic node reports pending via exactly the causes a plain async memo does (A19) — a reverting optimistic write is not among them (a revert is not a refetch). (Phrasing updated with V1: "only the async-in-flight check" predated held-value pends, which resting nodes now report like any memo.) | #2799, #2806 | `tests/createOptimistic.test.ts` (#2806 cases) | -| A13 | (was B1) A resting optimistic node (no active override) is observationally identical to a plain async memo for `read`/`latest`/`isPending` at every checkpoint of a refetch cycle — both before any override was written and after a full override cycle reverted. (Pin re-checked 2026-07-13: both sides of the equivalence now read `false` through a bare `refresh` — the quiet re-ask, A24 — which preserves the identity.) | maintainer keep, 2026-07-06 | `tests/spec-async-semantics.test.ts` | -| A14 | (was B2) `isPending`/`latest` companion nodes get child lanes that do not merge with the owner's lane: an `isPending` effect (spinner) fires while the owner's async is still in flight. (Re-scoped 2026-07-13: the pin drives the spinner with a _question change_ — `setId` — which pends the slot even while an optimistic override displays over it; the override is verdict-inert, A24. The 2026-07-07c mask scoping is superseded.) | maintainer keep, 2026-07-06; re-scoped 2026-07-07c and 2026-07-13 | `tests/spec-async-semantics.test.ts` | -| A15 | (was B3) Transition entanglement is graph-driven: writes whose async work is observed by a shared reader settle as one unit (no tearing — nothing commits until all entangled async resolves); writes on fully disjoint graphs keep independent transitions and settle independently. **Lanes corollary (clarified 2026-09-09, #3335):** for optimistic writes the unit that settles is the _reveal_ — lanes merge through the shared reader (their effect queues become one) while transaction ownership stays put (A18 node corollary, #2912). The merged reveal is held while **any** member's observed async is in flight: a hold is a property of the async node — observed pending by a render reader in whichever live transaction recorded it (INV-3) — never of the root lane's transaction, which after a cross-transaction merge knows only one member's observations. Pinned: `tests/lane-hold-on-observation.test.ts` (#3335). **Reveal corollary (clarified 2026-09-09, re-ruled 2026-09-10; #3305, #3334):** a reveal that _discovers_ an async already in flight — a write that makes a render reader read a pending node for the first time — is that shared-reader observation: the reveal holds and joins the transition the flight blocks, settling as one unit with it, **whenever the flight's inputs are already visible** — committed by a batch that left the flight in the air with no observer (#3305), or revealed through an optimistic / `latest` lane (#3334). Showing the node's pre-flight (committed) value beside those inputs would tear the frame, and which transaction stamped the node says nothing about it. When the flight's inputs are themselves still held (unpublished, in some _other_ transaction), the reveal is a stale reader of a parallel transaction and follows the effects rule: it shows the node's committed value — coherent with the frame, whose inputs are also committed — does **not** entangle the two transactions, and re-derives at that transaction's commit (the reader is recorded for the commit replay). Corollary of the A18 node corollary: when the flight is lane-routed, the reveal waits on the _flight_, not on the transaction that owns the lane — an in-flight action holding that lane open does not hold the reveal once the flight lands. Pinned: `tests/spec-async-semantics.test.ts` (#3334, optimistic and `latest` sources; #3305 second reveal), `tests/stale-read-uninitialized-cross-transition.test.ts` (unpublished inputs: show committed, no entanglement), `tests/reveal-carve-out.test.ts`. | maintainer keep, 2026-07-06 | `tests/spec-async-semantics.test.ts` | -| A16 | (was B5) `isPending` never throws in untracked contexts — thunks that throw real errors or read uninitialized async sources yield `false`. Carve-out (B5a, pinned as current behavior): in _tracked_ contexts the `NotReadyError` of an uninitialized source propagates so the reader participates in loading boundaries. | maintainer keep, 2026-07-06 | `tests/spec-async-semantics.test.ts` | -| A17 | (was C4) An _active_ optimistic override is THE value for every **read** — ambient/untracked and tracked alike — regardless of transition entanglement. "It is the optimistic future value... it is both immediate and is the future until we know otherwise." "Knowing otherwise" is its own async source resolving (see A18); a transition whose optimistic node is still pending on its own fetch is not complete, so the override cannot be dropped early. **No-tearing is an effect-level concern, not a read-level one**: when async _derived from_ the optimistic value is in flight, the lane holds its render effects (the rendered view keeps the committed state as a unit) — but direct reads still return the override ("direct read shows optimistic, effect waits"). Do NOT mask the override from any read path to prevent tearing; that breaks the real-world optimistic-UI contract. **Amended 2026-09-09 (#3331):** "until we know otherwise" is the landing of the node's own async, and knowing otherwise splits the readers: from that landing on, the override is the value for the _display_ — untracked/ambient reads and the applied frame — until the transaction commits, while tracked derivations (memos, async drivers, lane recomputes) see the arrived truth (A18 supersession). `latest(x)` returns the arrived value; `isPending(x)` is `true` iff it differs from the override. | maintainer ruling, 2026-07-06/07 | `tests/spec-async-semantics.test.ts`; downstream-async lane holding: `tests/createOptimistic.test.ts` (CategoryDisplay/News-Finance real-world sections) | -| A18 | (was B4; **refined by re-rule 2026-07-07b**) An override's lifetime is bound to **its own transition** — which, because lanes keep their transitions separate from unrelated work, contains exactly the override's own async cascade. Mechanically: authoritative values arriving under an active override hold in `_pendingValue` like any other transition write and **elevate to `_value` only at their transition's commit** (`_value` changes at commit points, period); the elevation is unobservable under the override mask (A17); reverting is a pure drop — there is no revert target and reverts commit nothing. Consequences: (1) in unmerged graphs, own-source resolution IS the lane-transition's completion, so the correction reveals on arrival — the original A18 pins hold unchanged; (2) matching confirmations collapse silently (revert sees value == override, nobody re-runs); (3) when the override's transition genuinely merges with unrelated async, the correction reveals atomically with that merged completion — verdict during the window per A24 (amended 2026-07-13; was "false throughout" under the A20 mask): a held correction that _differs_ from the displayed override reads pending; a matching confirm stays quiet — corrections still _propagate_ internally on arrival (fresh readers/async drivers see the hold), so downstream refetches start immediately and no waterfalls form; only the reveal is gated. This supersedes the earlier "bound to its own async source, not its transition" formulation, which was implemented by escaping the transition commit (revert-target commit at revert) and allowed a mid-flight arrival to reveal before its own transition completed. **Store corollary (2026-07-17, #2899): the optimistic layer obeys the same per-transaction lifetime.** `createOptimisticStore`'s override layer is one record per store target, but each entry is owned by the transaction that wrote it (`STORE_OPTIMISTIC_OWNERS` stamps, merge chains resolved): a settling action consumes only its own keys, so concurrent actions on disjoint keys revert independently — first-settling no longer wipes the other's live overrides. Same-key writes still entangle through the shared node (one joint settle); ambient (transaction-less) entries clear at plain flush end; a derived store's projection landing still consumes the whole layer (fresh authority supersedes every tentative write). **Node corollary (2026-07-18, #2912): ownership never travels through lanes.** Lanes are scheduling affinity — a shared subscriber (one effect reading keys touched by two actions) merges them correctly for flushing, but the merged root's `_transition` must not answer "which transaction owns this override": that let one action's settle revert another's live override, and same-key follow-up writes entangle with the wrong transaction. Every optimistic write stamps `_overrideOwner` on the node (post-merge, so entangled writers share the joint root; cleared at settle); `resolveTransition` prefers a live owner stamp over the lane, falling back to lane `_transition` for nodes without overrides (async routing) exactly as before. **Supersession (re-ruled 2026-09-09, #3331): own-source arrival removes the optimism from the graph immediately; the display keeps it until the transaction commits.** Maintainer: "a new value from the source should remove the optimism immediately.. if it matches then no more work, if it doesn't match then that work gets folded into the parent transition"; "when the optimism drops we might not see it until end of transition because it folds into the parent's transition." This replaces the mechanical sentence above ("elevate to `_value` only at their transition's commit; the elevation is unobservable under the override mask") — that model let the override's own downstream flight serialize ahead of the truth's, doubling the delay the reporter saw. Now: (a) a landing that _equals_ the override confirms silently — nothing re-runs, the lane's in-flight work completes the frame; (b) a landing that _differs_ marks the node superseded: its subscribers recompute from the arrived value on the plain channel (their lane affinity is dropped, so this is held transaction work, not lane work), downstream async restarts from the truth _now_, and the override's own downstream flight is inert when it lands; (c) untracked reads and the applied screen keep the override until the transaction — holding for whatever the corrected derivations observe (A15) — commits and clears the override; (d) `latest` returns the arrived value, `isPending` reads `true` iff the arrival differs (consequence (3) unchanged in statement, now true in mechanism). A later landing on the same node that equals the override un-supersedes it (the override is again the graph's value). **Scope (ruled 2026-09-10): "the source" is whatever recomputes the node** — its own async landing, or a synchronous recompute driven by an upstream change (`createOptimistic(() => userCategory())` over an async memo is the common real-world shape): "if the source recomputes it doesn't matter if it is async or not." **Ordering:** a new value from the source is one that _postdates_ the override — a source write and an override in the same batch derive nothing new (the override is written over that batch's truth knowingly and stays the graph's value until the commit reveals it). **Provenance (ruled 2026-09-10):** "a new value from the source" answers the override's _own_ question or a newer one. Two rapid actions on one node merge into one transaction, and the older action's refetch can land after the newer override; that answer is a question the user has since changed — it is staged for the commit like any landing (and reveals then iff it is still the truth) but does **not** supersede: no downstream re-derivation, no pending flip on downstream readers. "A slow source shouldn't leak back in like that." Only the override's own action, a later action, or mainline (no action — a fresh question by definition) supersedes. Pinned: `tests/spec-async-semantics.test.ts` ("#3331" describe: own-async, sync-wrapper, same-batch, provenance, simple graph; A18 entangled pin re-expected: the merged correction reveals as one frame, never the committed-behind-the-mask tear); `tests/createOptimistic.test.ts` (CategoryDisplay no-double-flicker pin, unchanged: the older action's answer never moves the graph; "second action while first still in flight" pin, resolver repaired and re-expected to the same rule). | maintainer rulings, 2026-07-07 (original) and 2026-07-07b (re-rule: "the non-blocking aspect… only gates the reveal"; `_value` elevation at commit points) | `tests/spec-async-semantics.test.ts` (same pins — behavior coincides in unmerged graphs); `tests/optimistic-store-layer-scope.test.ts` (store corollary: disjoint-key independence, nested rows, same-key entanglement, delete survival, ambient flush-end); `tests/optimistic-lane-transaction-ownership.test.ts` (node corollary: shared-subscriber lane merge with swapped write order, three-action signal hijack) | -| A19 | (was C1 — **partially reverses an earlier decision**) **Definition: `isPending(x)` ≡ the value you can currently observe for `x` is not the final one.** Three causes of non-finality, each ending on its own terms: (i) a write held by a live transition — ends at commit; (ii) the node's own async in flight — ends at resolution; (iii) a fresh value that arrived but is held uncommitted by a transition it's entangled with — ends at that commit. A node is pending while _any_ cause holds it and final the moment none does — cascading async falls out of the definition rather than needing a rule ("once it can show its landed value it is no longer pending"). **Two exceptions.** (1) The initial NotReady: an uninitialized source is _loading_, not pending (A16/A12) — no observable value exists to be non-final — and its thrown `NotReadyError` must propagate to loading boundaries (A16/B5a) because SSR streaming and hydration reveal are driven by boundaries. (2) (re-amended 2026-07-13; was the 2026-07-07c decree) Question scoping: causes (i)–(iii) count only when the in-flight work answers a _new question_ (an input value change not yet revealed) — a re-ask of the same question (refresh/poll/confirm with value-stable inputs) is quiet, because the shown value still answers the question being asked (A24). An active optimistic override is the displayed value on its own slot (verdict-inert — pending only for a held correction that differs from it) but never exempts anything else; the old mask exemption is deleted. Everywhere else, boundaries and reporters never enter the definition: they decide what renders and what a transition waits for, not verdicts. The rejected earlier framing ("if it isn't read somewhere that reports to the transition, it isn't actually pending") was a proxy for cause (i) wrongly applied to causes (ii)/(iii), tying data verdicts to graph-topology accidents. Causes (ii)/(iii) were implemented by the #2838 shadow/companion redesign (2026-07-07) — see V3/V1 under Known violations (fixed). (A27 extends the question scoping to the commit-#0 loading window: a node born committed via `loadingValue` answers its first question by declaration, so its first flight is quiet.) | maintainer ruling, 2026-07-07 | cause (i) + boundary interplay: `tests/spec-async-semantics.test.ts`; causes (ii)/(iii): same file, "V1–V5" describe | -| A20 | (**SUPERSEDED 2026-07-13 by A24** — the mask is deleted; optimistic writes are verdict-inert. Kept for the reasoning record. The surviving pieces: verdicts are per-channel (§2, now in A8), and action affordances belong in the data (co-written flags), not derived from verdicts.) (**re-ruled 2026-07-07c** — supersedes the 2026-07-07 "overrides are unsettled" ruling, which held for one day) **The mask: an optimistic override is certainty by decree; `isPending` follows the channel you read.** (1) An _active_ override reads `isPending === false` — uniformly, on every node kind, in **both** forms, for the override's whole lifetime (until its own source confirms it, A18, or the transition reverts it). Writing optimistically _declares_ the shown value the outcome; a decree cannot be superseded by work already in motion, because the writer just asserted it won't be. `isPending` is reserved for data being updated by machinery the reader did _not_ decree — refetches, transition-held commits — never for the provisional nature of an override ("isPending is about the data being in the process of being updated, not about an action being in progress"). Action-scoped affordances ("Saving…", per-row spinners) therefore belong **in the data**: a co-written flag (`todo.pending = true` — the repo's todos example) or a separate `createOptimistic(false)`. You are already writing the optimistic update; the flag rides along. The old no-extra-boolean idiom (`isPending(() => books.length)` as the "Adding…" label) is rejected — it derived an action's progress from a data verdict. (2) Verdicts are per-channel: the plain form watches the _committed_ channel — pending while its own fetch is in flight and while a resolved value is held uncommitted by a transition; the latest form watches the _fresh_ channel — `latest` is an override the system writes for itself the moment a held value exists (A8), and that self-override masks holds like any user override, leaving only actually-in-flight async as its pending cause. Pairing falls out for both forms: neither ever pairs `true` with the value that made it false. (3) Scope: the mask covers the primitive that was written — node-scoped for signals and computeds; store-scoped for derived optimistic stores (A21). (4) Non-derived optimistic signals/stores are never pending _from themselves_ — there is no source to confirm or refetch, the write is an instantly-visible decree — they pend only via a transition hold on the trigger like any plain signal. (5) No tension with A17/A18: the override is THE value (A17), its lifetime is transition-bound (A18), and the mask simply says the verdict agrees with the decree for exactly that lifetime — mask on at write, off at revert/confirm, in the same atomic settle. | GabbeV model adopted, maintainer re-rule 2026-07-07c (#2844/#2728 discussions) | A20 describe in `tests/spec-async-semantics.test.ts`; `tests/createOptimistic.test.ts` (mask + source-still-pends contrast); latest-channel: `tests/createMemo.test.ts`, solid-web `test/latest-async.spec.tsx`; INV-10 enforces the mask in dev | -| A21 | (**SUPERSEDED 2026-07-13 by A24** — the store-wide mask is deleted with the mask model; nothing silences a new question. The effective-write gate (consequence 4) survives as transaction-entanglement hygiene: no-op optimistic writes still neither entangle nor decree anything.) **The store-wide mask: for a derived optimistic store, the store is the primitive — any active optimistic write masks `isPending` for the _entire_ store.** Written leaves, untouched siblings, structural reads (`length`, iteration), and the firewall's own refetch all read `false` while any override on the store is live, in both forms; the mask lifts when the store's optimistic state fully clears (same lane lifetime as A20). Rationale: a refetch pends the whole store because the authority's change set is unbounded (A9) — the decree that silences it must speak for the same unbounded scope, or `isPending(() => store.items.length)` would flip on a refresh the writer already declared the outcome of ("If I do `setOptimisticFormOptions(x => x.cities.push("London"))` then I expect the select to consider it settled" — same for `x.cities[i] = "London"`). Once you write optimistically you own the store's pending affordances (A20 §1: flags in the data). Consequences: (1) optimistic writes to the same store entangle — not just writes to the same property; (2) plain (non-derived) optimistic stores get this for free — with no source they were never pending from themselves (A20 §4); (3) A9 is the unmasked rule: with **no** active override, every leaf reports the firewall's refetch in both forms — the store-wide mask is an override-lifetime exception, not a repeal; (4) (added 2026-07-08) only **effective** writes arm the mask and entangle — the decree is about data actually asserted, so trap fires that change nothing (`s => s`, `s => ({ ...s })` replaying equal values, same-value property writes, deletes of absent properties) are no-ops with no decree, matching the signal path where an equal-value first optimistic write short-circuits before any override exists. A deliberate "silence this refresh" affordance is future explicit API (#2844 family), not an emergent no-op write. | GabbeV/maintainer, 2026-07-07c ("any optimistic write turns off isPending for the whole store"; "the store is the boundary"); effective-write gate ruled 2026-07-08 (brenelz/GabbeV probing `setOptStore(s => s)`) | "store-wide mask" pin in the A20 describe, `tests/spec-async-semantics.test.ts`; `tests/store/createOptimisticStore.test.ts` (refresh-pends → write-masks → lift contrasts); INV-10 store-mask arm | -| A22 | **Pending is per-node: store-wide verdicts exist only as the firewall's own in-flight work (A9) and the decree that silences it (A21).** Every other A19 cause lives on the individual node — a transition-held write to a plain store pends exactly the touched leaves (untouched siblings and proxy-level reads stay settled: the writer's change set is known, unlike a refetching authority's unbounded one), manual projection writes pend the written leaf only, holds outliving a settled firewall stay leaf-local. Direction of flow: a node's verdict never inherits its _consumers'_ in-flight state — once a fetch commits, leaves show the landed value and read settled immediately, even while downstream async still holds the effect-level reveal (the commit is immediate at the data level; only the visual is lane-held, and `isPending` companions probe from their own lane, A14, so they are not fooled by the hold). | GabbeV plain-store demo + maintainer, 2026-07-08 ("probably not.. it's non optimistic and it isn't derived from an async source"; "this makes me want to keep things per property even more") | A22 describe in `tests/spec-async-semantics.test.ts` | -| A23 | **The `isPending` probe is reads-only — the thunk's return value is never inspected.** `isPending(() => store)` reads nothing and reports `false` by design: keying any behavior off the returned value is inconsistent under the probe's expression semantics (`() => store && other()` doesn't return the store; a child component reading `props.options` never has the store to return — the props issue). Whole-store questions are asked through reads: any leaf reports the firewall's refetch (A9), spread/iteration reads report structure. The accepted ergonomic complement is the **direct-argument form** `isPending(store)`, mirroring `refresh(store)` — _argument_ inspection (a controlled API taking the store identity, no expression semantics), consulting the firewall: projections report their shared computation's refetch (question-scoped per A24; the original "A21-mask-aware" note died with the mask), plain stores read `false` (no firewall — consistent with A22 and with `refresh`, which is also only meaningful for derived stores). Accepted 2026-07-08; implementation post-2.0. | maintainer, 2026-07-08 (GabbeV ergonomics ask; "This isn't about returns.. the whole props issue again") | A23 describe in `tests/spec-async-semantics.test.ts` (reads-only half; direct form pinned when implemented) | -| A24 | (**ruled 2026-07-13** — supersedes A20/A21; the converged model from the #2844/#2728 threads) **Question-scoped pending: a read is pending iff a value change is in flight for it that has not yet revealed, or it carries a live `affects()` mark.** (1) **Same-question motion is silent.** Async whose tracked inputs are value-stable — `refresh()`, polling, an action's confirm refetch — is a _re-ask of the same question_: the shown value still answers it, so `isPending` stays `false` and the fresh value reveals silently. (2) **A new question pends monotonically.** Any tracked input value change in flight (a `setSignal`, an upstream memo's new value, an optimistic write feeding downstream async) pends every read under the source until its answer reveals; **nothing can silence it** — pendingness is additive-only. (3) **Optimistic writes are verdict-inert.** An active override is the displayed value on its own slot: not pending from itself (only a held authoritative _correction_ differing from the override re-opens the verdict; a matching confirm reveals nothing), and it masks nothing — an override displaying over an in-flight new question is the honest mixed state `{ value: guess, pending: true }`. To _downstream_ async the write is a real input change and pends those slots normally. Action affordances still belong in the data (co-written flags — the A20 §1 half that survives). (4) **`affects(target, key?)` is the sole declaration verb** (single optional key since 2026-07-14; the variadic form read as a 1.x path and was dropped). Additive pending on exactly the marked data (store record → every record reachable from it at declaration time, captured proxies included per #2882, siblings untouched; key → that leaf slot; accessor → that source); **store marks — keyed and keyless — cover by raw identity, not by proxy family** (amended 2026-07-17, #2904): a keyed mark registers an identity scope (owning record's raw, narrowed to the key), so reads through any other proxy sharing that backing record — e.g. a derived optimistic store whose projection landed the source store's value — witness the mark and inherit it on nodes born during the window, exactly as keyless scopes do **and on everything derived from it** (re-ruled 2026-07-14: a mark is a synthetic in-flight change on the normal status rails — `isPending(() => derived())` reads `true` during a mark window on `derived`'s inputs, exactly as it would over real in-flight async — while the marked values themselves stay readable; **mark-only pending is value-transparent through derivation too** (amended 2026-07-14, #2886): a read whose owner's pending sources are all mark sentinels never suspends — pendingness reaches readers only through verdicts, so optimistic writes under a whole-store mark keep rendering in live tracked readers; a mark's channel is never a re-ask, so a declared reload's own `refresh()` cannot silence it, and a mark never blocks its own transaction's settlement), live from declaration until its surrounding transaction settles or reverts (ambient marks release at flush end). Four corollaries pinned by the #2893 audit (2026-07-16): **(a)** derivation coverage is transitive and probe-stable — tracked reads of mark-pended owners re-establish the mark on the reader after any mid-window recompute (including the recompute an `isPending()` probe itself triggers), at every derivation depth, for graphs built before or during the window; **(b)** mark propagation is transaction-inert — pended subscribers are not queued as pending nodes, so plain writes to marked data (value-transparency) and to unmarked data sharing a downstream memo commit and render immediately, and concurrent actions don't merge into the marker's transaction through the pend; **(c)** a real error outranks a mark — a node holding `STATUS_ERROR` neither takes a sentinel on propagation nor re-applies collected marks after its recompute, so the user's error is never clobbered by a sentinel `NotReadyError`; **(d)** the pending-source container survives any number of overlapping sources (the singular→Set migration bug stranded mark sentinels forever on the third source — exactly the keyless-store-mark-over-`mapArray` shape). The declared-reload idiom `affects(x); refresh(x)` is how process knowledge enters the verdict when the graph can't see the change yet. Trade accepted knowingly: a re-ask that happens to return different data is silent until it reveals — honest silence over blanket alarm; whoever knows declares. | maintainer ruling 2026-07-13 (#2844/#2728 convergence; cause-scoped pending, per-path masking + UNCHANGED vouching, `background()`, and lane-bounded vouches each rejected on the way) | `tests/question-scoped-pending.test.ts` (scenario matrix: foos bug, list over-lighting, navigation-over-override, poll, reload, iMessage posture; cross-family raw sharing #2904); `tests/affects-propagation.test.ts` (marks through derivation: bare-mark windows on memos, late-mark wake, mid-mark landing hold, settle release, no settlement deadlock, store record/keyed marks reaching derived readers); `tests/affects-audit-2893.test.ts` (audit corollaries: container survival under 3+ sources, transaction-inert propagation, transitive/probe-stable re-establishment, error precedence); re-pinned A13/A14/A20-block in `tests/spec-async-semantics.test.ts`; `tests/createOptimistic.test.ts`, `tests/store/createOptimisticStore.test.ts`, `tests/store/createProjection.async.test.ts`, `tests/latest-isPending-consistency.test.ts`, `tests/createMemo.test.ts`, `tests/createLoadingBoundary.test.ts` (quiet-refresh + declared-reload re-pins); INV-10 (affects-count balance) | -| A25 | (**ruled 2026-07-16**, #2897) **A derived store's seed is a draft, never an observable value.** The seed exists for the derive function — self reads while it works its draft are the point — but to every outside consumer the store is _uninitialized_ (A19 exception 1: loading, not pending) until the first resolution lands; for async-iterator derives, until the **first yield** lands (uninitialized only until then — each later yield is a revealed snapshot, readable between yields even while the generator is still running). During that window every outside consumer path throws: tracked reads suspend into loading boundaries via their node's `NotReadyError` as always, and the untracked fall-throughs — property reads, `in` checks, enumeration/spread — throw the same `NotReadyError` from the firewall (in dev strictRead scopes, i.e. component bodies, the more descriptive `PENDING_ASYNC_UNTRACKED_READ` error wins, matching async memos and preventing infinite loops). Returning the seed leaked a value the reader could never observe updating; returning `undefined` would break non-nullable types. Write-path reads (reconcile enumerating during the first landing) are exempt — they _are_ the initialization. This is safeguard parity: memos already behaved this way; store proxies bypassed `read()` and with it every guard. **Write-visibility corollary (ruled 2026-07-17, #2910 follow-up): the seed IS visible to write-path consumers.** A setter's function-form argument — the store setter's draft, `prev` in `set(prev => …)` — reads the raw current state: the seed for an uninitialized derived store, `undefined` for an uninitialized optimistic computed (it has no seed argument), the displayed value once initialized. Same exemption as the derive body: writes need a base, and because every read channel throws during the window, no consumer can _rely_ on the seed — visibility on the write path leaks nothing observable. Absolute writes were never gated. | maintainer rulings 2026-07-16 ("a seed… should never be visible under any case"; "we throw NotReady except in top-level component scope where we throw that other error"; "self reads are fine though — that's the point of seed, but outside isn't") and 2026-07-17 ("if the seed is visible in compute body it probably should be visible on write.. it throws on read so no consumer can rely on it") | `tests/strict-read-pending-store.test.ts` (untracked dev/prod matrix); `tests/store/createProjection.async.test.ts` (seed hidden until first resolution/first yield, supersession keeps it hidden, enumeration throws); `tests/uninitialized-visibility.test.ts` (write-path seed visibility, loading-vs-pending probe #2910) | - -| A26 | (**ruled 2026-07-17**, #2913; **enforcement hardened 2026-08-31**, #3141 — parking is flush-driven, and a transaction opened with no writes before its first suspension scheduled no flush, so `activeTransition` stayed ambient across the await window and captured exactly the unrelated work this ruling rejects: an optimistic store's authoritative landing on a still-live lane was adopted and held until the stranger action settled. `initTransition` now guarantees a flush, so the ambient window closes one flush later regardless of whether the transaction wrote anything) **`yield` is an action's only transaction-safe suspension point; internal `await` continuations run outside the transaction by platform necessity.** The driver regains control exclusively at yield boundaries (`it.next()` resumes the body synchronously, so `restoreTransition` wraps the segment); a post-`await` continuation is a bare promise job the runtime cannot hook — JavaScript has no ambient async context (TC39 AsyncContext, unshipped), and holding `activeTransition` open across the await window was rejected as strictly worse: unrelated ambient writes interleaving during `await fetch()` (a user click, a timer) would be captured into the action's transaction and held until it settles. Consequences, all accepted: (1) a write to a **fresh** signal between an `await` and the next `yield` escapes and commits ambiently; (2) a signal **already written under the transaction** rejoins it even after an `await` (its `_transition` stamp routes the write back) — containment is write-history-dependent by design, not by accident; (3) the supported idiom is `await` for typed results, then a **bare `yield` before any writes** — re-entry is what `yield` is for, and TypeScript ergonomics are exactly why `await` stays welcome (yield results are untyped; awaited results are typed); (4) calling public `flush()` inside an action body is out of contract — it drains and stashes the transaction mid-step, stranding later same-segment writes; follow the idiom and there is nothing to flush for. The doc block on `action()` teaches the idiom. | maintainer ruling 2026-07-17 ("2913 is not addressable… it is a known thing and it isn't detectable, otherwise we'd have a different solution"; "one reason to not yield the promise is TypeScript — we are set up so you can await typed results and then yield nothing the next line"; flush-in-body ruled out of contract: "if they follow that they shouldn't be flushing there") | `tests/action-await-contract.test.ts` (documented escape, the await-then-bare-yield idiom, yield-the-promise alternative, pre-await stamp rejoin); `tests/store/optimistic-ambient-capture.test.ts` (#3141 — the ambient window closes in one flush even for a writeless transaction) | -| A27 | (**ruled 2026-08-10**) **The commit-#0 loading window is loading-class and verdict-quiet.** A node born committed via `loadingValue` (memos: `createMemo` / `createSignal(fn)` / `createOptimistic(fn)`) or `seedLoadingValue` (projections: `createProjection` / `createStore(fn)` / `createOptimisticStore(fn)`) starts with the loading value as commit #0 of its lineage instead of `STATUS_UNINITIALIZED`. While its first real answer is in flight, the window is loading-class on every axis: (1) **reads** — every consumer path serves commit #0 (no `NotReadyError`, no Loading-boundary suspension; `latest()` and `resolve()` return it; it is the compute's first `prev`); (2) **transitions** — the window never initiates or extends one (matching boundary-fallback semantics): ambient writes concurrent with the window commit immediately rather than being held, and a loading node mounted inside a live transition does not add to what that transition waits for; (3) **verdict** — `isPending` reads false at the source, upstream, and downstream, in both forms. The quiet ruling is not an exception to A19 but its question scoping applied: commit #0 answers the question **by declaration**, so the first flight is re-ask-shaped — the shown answer still answers the question (A24 family). The alternative was rejected as structurally unavailable: genuine pending is chain-shaped (the shadow of a held commit — held write upstream, in-flight async at the node, propagated non-finality downstream), and the window has no held commit to shadow, so a true verdict could only exist as a point anomaly at the probed node while upstream and downstream read false — and making it propagate would reintroduce exactly the status machinery the window exists to silence (and re-open the server/client split: `isPending` is always false on the server). First-load affordances are therefore the **value channel**'s job — the author encodes provenance (`null`, a `skeleton` flag) into the loading value itself — and `data.skeleton || isPending(data)` covers the two disjoint states. The window closes at the first real landing on any path (sync return, sync-resolved thenable, first iterator yield, async settle); a real error answers reads but does not close it — a retry serves commit #0 again. After close, A19 applies unchanged: refetches are pending-class forever. A25 is unchanged for plain seeds: without `seedLoadingValue` a derived store's seed remains an unobservable draft; `seedLoadingValue` is precisely the author promoting the seed to commit #0 — observable by declaration. | maintainer ruling 2026-08-10 ("the reason I had skeleton or isPending is because isPending would be false in my mind"; "it was false both upstream and downstream") | `tests/loading-value.test.ts` | - -## Tier B (inferred — needs verdict) + pass. These are the topic sections below. +- **Tier B** — believed correct but _inferred_ from code/issues; needed a + maintainer verdict before promotion to Tier A. Every B item has since been + ruled (promoted or rejected); the list is under History. +- **Tier C** — genuinely open items needing a decision. Every C item has since + been ruled or closed; the list is under History. + +## How to read this file + +Rules are grouped by **topic**, not by number; IDs are stable and never renumbered (source comments cite them — `docs/RULES-INDEX.md` resolves every citation). Each rule carries: + +- **Status** — `ruled` (a citable ruling), `ruled, amended in place` (re-ruled or re-scoped since; the statement text records how and when), `superseded … by A<n>` (replaced; kept verbatim for the reasoning record). +- **Pinned by** — the tests that are this rule's spec. Changing them is a design decision, not a code fix. +- **Mechanism** — added by the 2026-09-14 index: the node fields / functions the rule requires. This is the column a single visibility resolver would be built from; it is a cross-reference, not part of the ruling. + +The former Tier A table is these sections. Tier B/C, the fixed violations, and the dated re-ruling logs are unchanged below under History. + +## Reads and visibility — what a read serves + +### A17. An active override is THE value for every read + +**Status:** **ruled** 2026-07-06 (promoted from C4) — maintainer ruling, 2026-07-06/07 +**Pinned by:** `tests/spec-async-semantics.test.ts`; downstream-async lane holding: `tests/createOptimistic.test.ts` (CategoryDisplay/News-Finance real-world sections) +**Mechanism (index, 2026-09-14):** `_overrideValue` + `hasActiveOverride`; value selection in `read` / `readNodeFast` / store `serveDataKey`; authoritative-view carve-out for `until()` (`CONFIG_AUTHORITATIVE_READ`); held-truth mask `CONFIG_HELD_TRUTH` (#3164). + +(was C4) An _active_ optimistic override is THE value for every **read** — ambient/untracked and tracked alike — regardless of transition entanglement. "It is the optimistic future value... it is both immediate and is the future until we know otherwise." "Knowing otherwise" is its own async source resolving (see A18); a transition whose optimistic node is still pending on its own fetch is not complete, so the override cannot be dropped early. **No-tearing is an effect-level concern, not a read-level one**: when async _derived from_ the optimistic value is in flight, the lane holds its render effects (the rendered view keeps the committed state as a unit) — but direct reads still return the override ("direct read shows optimistic, effect waits"). Do NOT mask the override from any read path to prevent tearing; that breaks the real-world optimistic-UI contract. **Amended 2026-09-09 (#3331):** "until we know otherwise" is the landing of the node's own async, and knowing otherwise splits the readers: from that landing on, the override is the value for the _display_ — untracked/ambient reads and the applied frame — until the transaction commits, while tracked derivations (memos, async drivers, lane recomputes) see the arrived truth (A18 supersession). `latest(x)` returns the arrived value; `isPending(x)` is `true` iff it differs from the override. + +### A18. An override's lifetime is bound to its own transition; arriving truth holds until commit + +**Status:** **ruled, amended in place** 2026-07-07 (promoted from B4) — maintainer rulings, 2026-07-07 (original) and 2026-07-07b (re-rule: "the non-blocking aspect… only gates the reveal"; `_value` elevation at commit points) +**Pinned by:** `tests/spec-async-semantics.test.ts` (same pins — behavior coincides in unmerged graphs); `tests/optimistic-store-layer-scope.test.ts` (store corollary: disjoint-key independence, nested rows, same-key entanglement, delete survival, ambient flush-end); `tests/optimistic-lane-transaction-ownership.test.ts` (node corollary: shared-subscriber lane merge with swapped write order, three-action signal hijack) +**Mechanism (index, 2026-09-14):** `_pendingValue` hold under an override, elevation in `commitPendingNode`; `CONFIG_OVERRIDE_SUPERSEDED` + `supersedeOverride` / `supersededRead` (#3331); `_overrideTime`, `_overrideStamp` provenance. + +(was B4; **refined by re-rule 2026-07-07b**) An override's lifetime is bound to **its own transition** — which, because lanes keep their transitions separate from unrelated work, contains exactly the override's own async cascade. Mechanically: authoritative values arriving under an active override hold in `_pendingValue` like any other transition write and **elevate to `_value` only at their transition's commit** (`_value` changes at commit points, period); the elevation is unobservable under the override mask (A17); reverting is a pure drop — there is no revert target and reverts commit nothing. Consequences: (1) in unmerged graphs, own-source resolution IS the lane-transition's completion, so the correction reveals on arrival — the original A18 pins hold unchanged; (2) matching confirmations collapse silently (revert sees value == override, nobody re-runs); (3) when the override's transition genuinely merges with unrelated async, the correction reveals atomically with that merged completion — verdict during the window per A24 (amended 2026-07-13; was "false throughout" under the A20 mask): a held correction that _differs_ from the displayed override reads pending; a matching confirm stays quiet — corrections still _propagate_ internally on arrival (fresh readers/async drivers see the hold), so downstream refetches start immediately and no waterfalls form; only the reveal is gated. This supersedes the earlier "bound to its own async source, not its transition" formulation, which was implemented by escaping the transition commit (revert-target commit at revert) and allowed a mid-flight arrival to reveal before its own transition completed. **Store corollary (2026-07-17, #2899): the optimistic layer obeys the same per-transaction lifetime.** `createOptimisticStore`'s override layer is one record per store target, but each entry is owned by the transaction that wrote it (`STORE_OPTIMISTIC_OWNERS` stamps, merge chains resolved): a settling action consumes only its own keys, so concurrent actions on disjoint keys revert independently — first-settling no longer wipes the other's live overrides. Same-key writes still entangle through the shared node (one joint settle); ambient (transaction-less) entries clear at plain flush end; a derived store's projection landing still consumes the whole layer (fresh authority supersedes every tentative write). **Node corollary (2026-07-18, #2912): ownership never travels through lanes.** Lanes are scheduling affinity — a shared subscriber (one effect reading keys touched by two actions) merges them correctly for flushing, but the merged root's `_transition` must not answer "which transaction owns this override": that let one action's settle revert another's live override, and same-key follow-up writes entangle with the wrong transaction. Every optimistic write stamps `_overrideOwner` on the node (post-merge, so entangled writers share the joint root; cleared at settle); `resolveTransition` prefers a live owner stamp over the lane, falling back to lane `_transition` for nodes without overrides (async routing) exactly as before. **Supersession (re-ruled 2026-09-09, #3331): own-source arrival removes the optimism from the graph immediately; the display keeps it until the transaction commits.** Maintainer: "a new value from the source should remove the optimism immediately.. if it matches then no more work, if it doesn't match then that work gets folded into the parent transition"; "when the optimism drops we might not see it until end of transition because it folds into the parent's transition." This replaces the mechanical sentence above ("elevate to `_value` only at their transition's commit; the elevation is unobservable under the override mask") — that model let the override's own downstream flight serialize ahead of the truth's, doubling the delay the reporter saw. Now: (a) a landing that _equals_ the override confirms silently — nothing re-runs, the lane's in-flight work completes the frame; (b) a landing that _differs_ marks the node superseded: its subscribers recompute from the arrived value on the plain channel (their lane affinity is dropped, so this is held transaction work, not lane work), downstream async restarts from the truth _now_, and the override's own downstream flight is inert when it lands; (c) untracked reads and the applied screen keep the override until the transaction — holding for whatever the corrected derivations observe (A15) — commits and clears the override; (d) `latest` returns the arrived value, `isPending` reads `true` iff the arrival differs (consequence (3) unchanged in statement, now true in mechanism). A later landing on the same node that equals the override un-supersedes it (the override is again the graph's value). **Scope (ruled 2026-09-10): "the source" is whatever recomputes the node** — its own async landing, or a synchronous recompute driven by an upstream change (`createOptimistic(() => userCategory())` over an async memo is the common real-world shape): "if the source recomputes it doesn't matter if it is async or not." **Ordering:** a new value from the source is one that _postdates_ the override — a source write and an override in the same batch derive nothing new (the override is written over that batch's truth knowingly and stays the graph's value until the commit reveals it). **Provenance (ruled 2026-09-10):** "a new value from the source" answers the override's _own_ question or a newer one. Two rapid actions on one node merge into one transaction, and the older action's refetch can land after the newer override; that answer is a question the user has since changed — it is staged for the commit like any landing (and reveals then iff it is still the truth) but does **not** supersede: no downstream re-derivation, no pending flip on downstream readers. "A slow source shouldn't leak back in like that." Only the override's own action, a later action, or mainline (no action — a fresh question by definition) supersedes. Pinned: `tests/spec-async-semantics.test.ts` ("#3331" describe: own-async, sync-wrapper, same-batch, provenance, simple graph; A18 entangled pin re-expected: the merged correction reveals as one frame, never the committed-behind-the-mask tear); `tests/createOptimistic.test.ts` (CategoryDisplay no-double-flicker pin, unchanged: the older action's answer never moves the graph; "second action while first still in flight" pin, resolver repaired and re-expected to the same rule). + +### A11. Sync derivations of held sources are visible through `latest()`/`isPending()` + +**Status:** **ruled** — #2831 finding 3 +**Pinned by:** `tests/latest-isPending-consistency.test.ts` +**Mechanism (index, 2026-09-14):** `recompute`'s transition-held branch stages into `_pendingValue` and syncs companions like a write (§4 write path 3). + +Sync derivations of transition-held sources are visible through `latest()`/`isPending()` (held sync recompute is a write path like any other). + +### A26. An ambient transaction window is one flush; parking is flush-driven + +**Status:** **ruled** 2026-07-17 — maintainer ruling 2026-07-17 ("2913 is not addressable… it is a known thing and it isn't detectable, otherwise we'd have a different solution"; "one reason to not yield the promise is TypeScript — we are set up so you can await typed results and then yield nothing the next line"; flush-in-body ruled out of contract: "if they follow that they shouldn't be flushing there") +**Pinned by:** `tests/action-await-contract.test.ts` (documented escape, the await-then-bare-yield idiom, yield-the-promise alternative, pre-await stamp rejoin); `tests/store/optimistic-ambient-capture.test.ts` (#3141 — the ambient window closes in one flush even for a writeless transaction) +**Mechanism (index, 2026-09-14):** `initTransition` schedules a flush; `stashQueues` parks incomplete transactions (#2913, hardened #3141). + +(**ruled 2026-07-17**, #2913; **enforcement hardened 2026-08-31**, #3141 — parking is flush-driven, and a transaction opened with no writes before its first suspension scheduled no flush, so `activeTransition` stayed ambient across the await window and captured exactly the unrelated work this ruling rejects: an optimistic store's authoritative landing on a still-live lane was adopted and held until the stranger action settled. `initTransition` now guarantees a flush, so the ambient window closes one flush later regardless of whether the transaction wrote anything) **`yield` is an action's only transaction-safe suspension point; internal `await` continuations run outside the transaction by platform necessity.** The driver regains control exclusively at yield boundaries (`it.next()` resumes the body synchronously, so `restoreTransition` wraps the segment); a post-`await` continuation is a bare promise job the runtime cannot hook — JavaScript has no ambient async context (TC39 AsyncContext, unshipped), and holding `activeTransition` open across the await window was rejected as strictly worse: unrelated ambient writes interleaving during `await fetch()` (a user click, a timer) would be captured into the action's transaction and held until it settles. Consequences, all accepted: (1) a write to a **fresh** signal between an `await` and the next `yield` escapes and commits ambiently; (2) a signal **already written under the transaction** rejoins it even after an `await` (its `_transition` stamp routes the write back) — containment is write-history-dependent by design, not by accident; (3) the supported idiom is `await` for typed results, then a **bare `yield` before any writes** — re-entry is what `yield` is for, and TypeScript ergonomics are exactly why `await` stays welcome (yield results are untyped; awaited results are typed); (4) calling public `flush()` inside an action body is out of contract — it drains and stashes the transaction mid-step, stranding later same-segment writes; follow the idiom and there is nothing to flush for. The doc block on `action()` teaches the idiom. + +## Verdicts — `isPending()` and `latest()` + +### A19. `isPending(x)` ≡ the observable value is not final (three causes) + +**Status:** **ruled** 2026-07-07 (promoted from C1) — maintainer ruling, 2026-07-07 +**Pinned by:** cause (i) + boundary interplay: `tests/spec-async-semantics.test.ts`; causes (ii)/(iii): same file, "V1–V5" describe +**Mechanism (index, 2026-09-14):** `computePendingState` (verdict.ts) over `_pendingValue`, `STATUS_PENDING`, transition membership; `_pendingSources` rails. + +(was C1 — **partially reverses an earlier decision**) **Definition: `isPending(x)` ≡ the value you can currently observe for `x` is not the final one.** Three causes of non-finality, each ending on its own terms: (i) a write held by a live transition — ends at commit; (ii) the node's own async in flight — ends at resolution; (iii) a fresh value that arrived but is held uncommitted by a transition it's entangled with — ends at that commit. A node is pending while _any_ cause holds it and final the moment none does — cascading async falls out of the definition rather than needing a rule ("once it can show its landed value it is no longer pending"). **Two exceptions.** (1) The initial NotReady: an uninitialized source is _loading_, not pending (A16/A12) — no observable value exists to be non-final — and its thrown `NotReadyError` must propagate to loading boundaries (A16/B5a) because SSR streaming and hydration reveal are driven by boundaries. (2) (re-amended 2026-07-13; was the 2026-07-07c decree) Question scoping: causes (i)–(iii) count only when the in-flight work answers a _new question_ (an input value change not yet revealed) — a re-ask of the same question (refresh/poll/confirm with value-stable inputs) is quiet, because the shown value still answers the question being asked (A24). An active optimistic override is the displayed value on its own slot (verdict-inert — pending only for a held correction that differs from it) but never exempts anything else; the old mask exemption is deleted. Everywhere else, boundaries and reporters never enter the definition: they decide what renders and what a transition waits for, not verdicts. The rejected earlier framing ("if it isn't read somewhere that reports to the transition, it isn't actually pending") was a proxy for cause (i) wrongly applied to causes (ii)/(iii), tying data verdicts to graph-topology accidents. Causes (ii)/(iii) were implemented by the #2838 shadow/companion redesign (2026-07-07) — see V3/V1 under Known violations (fixed). (A27 extends the question scoping to the commit-#0 loading window: a node born committed via `loadingValue` answers its first question by declaration, so its first flight is quiet.) + +### A24. Question-scoped pending: pending iff a value change is in flight or an `affects()` mark is live + +**Status:** **ruled** 2026-07-13 — maintainer ruling 2026-07-13 (#2844/#2728 convergence; cause-scoped pending, per-path masking + UNCHANGED vouching, `background()`, and lane-bounded vouches each rejected on the way) +**Pinned by:** `tests/question-scoped-pending.test.ts` (scenario matrix: foos bug, list over-lighting, navigation-over-override, poll, reload, iMessage posture; cross-family raw sharing #2904); `tests/affects-propagation.test.ts` (marks through derivation: bare-mark windows on memos, late-mark wake, mid-mark landing hold, settle release, no settlement deadlock, store record/keyed marks reaching derived readers); `tests/affects-audit-2893.test.ts` (audit corollaries: container survival under 3+ sources, transaction-inert propagation, transitive/probe-stable re-establishment, error precedence); re-pinned A13/A14/A20-block in `tests/spec-async-semantics.test.ts`; `tests/createOptimistic.test.ts`, `tests/store/createOptimisticStore.test.ts`, `tests/store/createProjection.async.test.ts`, `tests/latest-isPending-consistency.test.ts`, `tests/createMemo.test.ts`, `tests/createLoadingBoundary.test.ts` (quiet-refresh + declared-reload re-pins); INV-10 (affects-count balance) +**Mechanism (index, 2026-09-14):** `_reask` (quiet re-ask), `_affectsCount` / `_affectsSentinel`, `witnessAffects` (verdict.ts). + +(**ruled 2026-07-13** — supersedes A20/A21; the converged model from the #2844/#2728 threads) **Question-scoped pending: a read is pending iff a value change is in flight for it that has not yet revealed, or it carries a live `affects()` mark.** (1) **Same-question motion is silent.** Async whose tracked inputs are value-stable — `refresh()`, polling, an action's confirm refetch — is a _re-ask of the same question_: the shown value still answers it, so `isPending` stays `false` and the fresh value reveals silently. (2) **A new question pends monotonically.** Any tracked input value change in flight (a `setSignal`, an upstream memo's new value, an optimistic write feeding downstream async) pends every read under the source until its answer reveals; **nothing can silence it** — pendingness is additive-only. (3) **Optimistic writes are verdict-inert.** An active override is the displayed value on its own slot: not pending from itself (only a held authoritative _correction_ differing from the override re-opens the verdict; a matching confirm reveals nothing), and it masks nothing — an override displaying over an in-flight new question is the honest mixed state `{ value: guess, pending: true }`. To _downstream_ async the write is a real input change and pends those slots normally. Action affordances still belong in the data (co-written flags — the A20 §1 half that survives). (4) **`affects(target, key?)` is the sole declaration verb** (single optional key since 2026-07-14; the variadic form read as a 1.x path and was dropped). Additive pending on exactly the marked data (store record → every record reachable from it at declaration time, captured proxies included per #2882, siblings untouched; key → that leaf slot; accessor → that source); **store marks — keyed and keyless — cover by raw identity, not by proxy family** (amended 2026-07-17, #2904): a keyed mark registers an identity scope (owning record's raw, narrowed to the key), so reads through any other proxy sharing that backing record — e.g. a derived optimistic store whose projection landed the source store's value — witness the mark and inherit it on nodes born during the window, exactly as keyless scopes do **and on everything derived from it** (re-ruled 2026-07-14: a mark is a synthetic in-flight change on the normal status rails — `isPending(() => derived())` reads `true` during a mark window on `derived`'s inputs, exactly as it would over real in-flight async — while the marked values themselves stay readable; **mark-only pending is value-transparent through derivation too** (amended 2026-07-14, #2886): a read whose owner's pending sources are all mark sentinels never suspends — pendingness reaches readers only through verdicts, so optimistic writes under a whole-store mark keep rendering in live tracked readers; a mark's channel is never a re-ask, so a declared reload's own `refresh()` cannot silence it, and a mark never blocks its own transaction's settlement), live from declaration until its surrounding transaction settles or reverts (ambient marks release at flush end). Four corollaries pinned by the #2893 audit (2026-07-16): **(a)** derivation coverage is transitive and probe-stable — tracked reads of mark-pended owners re-establish the mark on the reader after any mid-window recompute (including the recompute an `isPending()` probe itself triggers), at every derivation depth, for graphs built before or during the window; **(b)** mark propagation is transaction-inert — pended subscribers are not queued as pending nodes, so plain writes to marked data (value-transparency) and to unmarked data sharing a downstream memo commit and render immediately, and concurrent actions don't merge into the marker's transaction through the pend; **(c)** a real error outranks a mark — a node holding `STATUS_ERROR` neither takes a sentinel on propagation nor re-applies collected marks after its recompute, so the user's error is never clobbered by a sentinel `NotReadyError`; **(d)** the pending-source container survives any number of overlapping sources (the singular→Set migration bug stranded mark sentinels forever on the third source — exactly the keyless-store-mark-over-`mapArray` shape). The declared-reload idiom `affects(x); refresh(x)` is how process knowledge enters the verdict when the graph can't see the change yet. Trade accepted knowingly: a re-ask that happens to return different data is silent until it reveals — honest silence over blanket alarm; whoever knows declares. + +### A7. Resolved async never reads `[false, undefined]` + +**Status:** **ruled** — #2829 (the `[false, undefined]` pins were a regression) +**Pinned by:** `tests/latest-async.test.ts`, `tests/createMemo.test.ts` +**Mechanism (index, 2026-09-14):** `latest()` shadow (`_latestValueComputed`) backfilled from the landed value (`backfillCompanion`, verdict.ts). + +After an async memo resolves, `[isPending(x), latest(x)]` is `[false, resolvedValue]` — never `[false, undefined]`. + +### A8. `isPending(() => latest(x))` follows `x`'s own async only — verdicts are per-channel + +**Status:** **ruled, amended in place** 2026-07-07 — GabbeV/maintainer re-rule, 2026-07-07c; quiet-re-ask filter 2026-07-13 +**Pinned by:** `tests/createMemo.test.ts`, solid-web `test/latest-async.spec.tsx` +**Mechanism (index, 2026-09-14):** Companion nodes per channel (`_pendingSignal`, `_latestValueComputed`, `_parentSource` backlink); quiet re-ask filter via `_reask`. + +(**re-ruled 2026-07-07c** — was "tracks the transition the same as `isPending(x)`"; **amended 2026-07-13**: "own async in flight" is further filtered to _non-quiet_ flight — a re-ask of the same question is silent in the latest form too, and a live `affects()` mark on the owner pends it) **`isPending(() => latest(x))` follows `x`'s own async only — verdicts are per-channel.** `latest` is an override the system writes for itself the moment a held value exists ("it is like an optimistic that sets itself to the value as soon as it is available to do so"). So the latest form reads `true` only while `x`'s own fetch for a _new question_ is in flight (showing the stale value) and turns `false` the instant that fetch resolves — even if the same update has other async still running and the commit is held. On a signal or sync computed the held value exists from the instant of the write, so their latest form is _never_ pending. The plain form keeps watching the committed channel (holds included). Pairing falls out: `[isPending(() => latest(x)), latest(x)]` never pairs `true` with the fresh value. + +### A9. Store leaves behind a firewall report the firewall's new-question refetch + +**Status:** **ruled, amended in place** 2026-07-07 — #2831 finding 1; both-forms re-ruled 2026-07-07c; question scoping 2026-07-13 +**Pinned by:** `tests/latest-isPending-consistency.test.ts`, V4 pin in `tests/spec-async-semantics.test.ts`, `tests/question-scoped-pending.test.ts` +**Mechanism (index, 2026-09-14):** `_parentSource` chain store leaf → firewall; `computePendingState` follows it; A24 filters same-question re-asks. + +`isPending` on a store leaf behind a firewall reports the firewall's refetch like any async memo — in **both** forms (the latest-form filter of the old A20 is gone; an in-flight refetch supersedes both channels). (**Amended 2026-07-13**: "refetch" means a _new-question_ refetch — an input value change in flight. A quiet re-ask of the same question (`refresh(store)` with value-stable inputs) is silent in both forms; the declared reload `affects(store); refresh(store)` pends. The old exception — the store-wide mask (A21) silencing it — is deleted with A21.) + +### A10. `[isPending(x), x()]` is atomic within one scope + +**Status:** **ruled** — #2831 finding 2 +**Pinned by:** `tests/latest-isPending-consistency.test.ts` +**Mechanism (index, 2026-09-14):** `_recordFresh` (#2831): a probe that observed the fresh value cannot pair it with `pending`. + +`[isPending(x), x()]` read in one scope is atomic: a reader that observed the fresh value must not see `pending === true` for it. + +### A12. Resting optimistic nodes report pending like a plain memo + +**Status:** **ruled, amended in place** — #2799, #2806 +**Pinned by:** `tests/createOptimistic.test.ts` (#2806 cases) +**Mechanism (index, 2026-09-14):** `(NOT_PENDING, NOT_PENDING)` posture; verdict has no optimistic carve-out (V1 removed it). + +A resting optimistic node reports pending via exactly the causes a plain async memo does (A19) — a reverting optimistic write is not among them (a revert is not a refetch). (Phrasing updated with V1: "only the async-in-flight check" predated held-value pends, which resting nodes now report like any memo.) + +### A13. Resting optimistic ≡ plain async memo at every checkpoint + +**Status:** **ruled** 2026-07-06 (promoted from B1) — maintainer keep, 2026-07-06 +**Pinned by:** `tests/spec-async-semantics.test.ts` +**Mechanism (index, 2026-09-14):** Same as A12; pinned equivalence matrix in `spec-async-semantics.test.ts`. + +(was B1) A resting optimistic node (no active override) is observationally identical to a plain async memo for `read`/`latest`/`isPending` at every checkpoint of a refetch cycle — both before any override was written and after a full override cycle reverted. (Pin re-checked 2026-07-13: both sides of the equivalence now read `false` through a bare `refresh` — the quiet re-ask, A24 — which preserves the identity.) + +### A14. Companion nodes get child lanes that do not merge with the owner + +**Status:** **ruled, amended in place** 2026-07-06 (promoted from B2) — maintainer keep, 2026-07-06; re-scoped 2026-07-07c and 2026-07-13 +**Pinned by:** `tests/spec-async-semantics.test.ts` +**Mechanism (index, 2026-09-14):** `_parentLane` carve-out in `assignOrMergeLane` (lanes.ts). + +(was B2) `isPending`/`latest` companion nodes get child lanes that do not merge with the owner's lane: an `isPending` effect (spinner) fires while the owner's async is still in flight. (Re-scoped 2026-07-13: the pin drives the spinner with a _question change_ — `setId` — which pends the slot even while an optimistic override displays over it; the override is verdict-inert, A24. The 2026-07-07c mask scoping is superseded.) + +### A16. `isPending` never throws in untracked contexts + +**Status:** **ruled** 2026-07-06 (promoted from B5) — maintainer keep, 2026-07-06 +**Pinned by:** `tests/spec-async-semantics.test.ts` +**Mechanism (index, 2026-09-14):** `pendingCheckRead` swallows `NotReadyError` / errors when `getObserver() === null`; tracked carve-out B5a. + +(was B5) `isPending` never throws in untracked contexts — thunks that throw real errors or read uninitialized async sources yield `false`. Carve-out (B5a, pinned as current behavior): in _tracked_ contexts the `NotReadyError` of an uninitialized source propagates so the reader participates in loading boundaries. + +### A22. Pending is per-node; store-wide only for the firewall's own work + +**Status:** **ruled** 2026-07-08 — GabbeV plain-store demo + maintainer, 2026-07-08 ("probably not.. it's non optimistic and it isn't derived from an async source"; "this makes me want to keep things per property even more") +**Pinned by:** A22 describe in `tests/spec-async-semantics.test.ts` +**Mechanism (index, 2026-09-14):** Per-leaf `_pendingSignal`s; firewall `_parentSource`; no store-wide mask (A21 superseded). + +**Pending is per-node: store-wide verdicts exist only as the firewall's own in-flight work (A9) and the decree that silences it (A21).** Every other A19 cause lives on the individual node — a transition-held write to a plain store pends exactly the touched leaves (untouched siblings and proxy-level reads stay settled: the writer's change set is known, unlike a refetching authority's unbounded one), manual projection writes pend the written leaf only, holds outliving a settled firewall stay leaf-local. Direction of flow: a node's verdict never inherits its _consumers'_ in-flight state — once a fetch commits, leaves show the landed value and read settled immediately, even while downstream async still holds the effect-level reveal (the commit is immediate at the data level; only the visual is lane-held, and `isPending` companions probe from their own lane, A14, so they are not fooled by the hold). + +### A23. The `isPending` probe is reads-only + +**Status:** **ruled** 2026-07-08 — maintainer, 2026-07-08 (GabbeV ergonomics ask; "This isn't about returns.. the whole props issue again") +**Pinned by:** A23 describe in `tests/spec-async-semantics.test.ts` (reads-only half; direct form pinned when implemented) +**Mechanism (index, 2026-09-14):** `pendingCheckActive` / `pendingProbe` collect reads; the thunk's return value is never inspected. + +**The `isPending` probe is reads-only — the thunk's return value is never inspected.** `isPending(() => store)` reads nothing and reports `false` by design: keying any behavior off the returned value is inconsistent under the probe's expression semantics (`() => store && other()` doesn't return the store; a child component reading `props.options` never has the store to return — the props issue). Whole-store questions are asked through reads: any leaf reports the firewall's refetch (A9), spread/iteration reads report structure. The accepted ergonomic complement is the **direct-argument form** `isPending(store)`, mirroring `refresh(store)` — _argument_ inspection (a controlled API taking the store identity, no expression semantics), consulting the firewall: projections report their shared computation's refetch (question-scoped per A24; the original "A21-mask-aware" note died with the mask), plain stores read `false` (no firewall — consistent with A22 and with `refresh`, which is also only meaningful for derived stores). Accepted 2026-07-08; implementation post-2.0. + +## Transactions and holds + +### A15. Transition entanglement is graph-driven; lanes settle as one reveal + +**Status:** **ruled** 2026-07-06 (promoted from B3) — maintainer keep, 2026-07-06 +**Pinned by:** `tests/spec-async-semantics.test.ts` +**Mechanism (index, 2026-09-14):** `_asyncReporters`, `mergeTransitionState`, `laneHeld` / `waitingTransition` (#3335). + +(was B3) Transition entanglement is graph-driven: writes whose async work is observed by a shared reader settle as one unit (no tearing — nothing commits until all entangled async resolves); writes on fully disjoint graphs keep independent transitions and settle independently. **Lanes corollary (clarified 2026-09-09, #3335):** for optimistic writes the unit that settles is the _reveal_ — lanes merge through the shared reader (their effect queues become one) while transaction ownership stays put (A18 node corollary, #2912). The merged reveal is held while **any** member's observed async is in flight: a hold is a property of the async node — observed pending by a render reader in whichever live transaction recorded it (INV-3) — never of the root lane's transaction, which after a cross-transaction merge knows only one member's observations. Pinned: `tests/lane-hold-on-observation.test.ts` (#3335). **Reveal corollary (clarified 2026-09-09, re-ruled 2026-09-10; #3305, #3334):** a reveal that _discovers_ an async already in flight — a write that makes a render reader read a pending node for the first time — is that shared-reader observation: the reveal holds and joins the transition the flight blocks, settling as one unit with it, **whenever the flight's inputs are already visible** — committed by a batch that left the flight in the air with no observer (#3305), or revealed through an optimistic / `latest` lane (#3334). Showing the node's pre-flight (committed) value beside those inputs would tear the frame, and which transaction stamped the node says nothing about it. When the flight's inputs are themselves still held (unpublished, in some _other_ transaction), the reveal is a stale reader of a parallel transaction and follows the effects rule: it shows the node's committed value — coherent with the frame, whose inputs are also committed — does **not** entangle the two transactions, and re-derives at that transaction's commit (the reader is recorded for the commit replay). Corollary of the A18 node corollary: when the flight is lane-routed, the reveal waits on the _flight_, not on the transaction that owns the lane — an in-flight action holding that lane open does not hold the reveal once the flight lands. Pinned: `tests/spec-async-semantics.test.ts` (#3334, optimistic and `latest` sources; #3305 second reveal), `tests/stale-read-uninitialized-cross-transition.test.ts` (unpublished inputs: show committed, no entanglement), `tests/reveal-carve-out.test.ts`. + +## Loading window and seeds + +### A27. The commit-#0 loading window is loading-class and verdict-quiet + +**Status:** **ruled** 2026-08-10 — maintainer ruling 2026-08-10 ("the reason I had skeleton or isPending is because isPending would be false in my mind"; "it was false both upstream and downstream") +**Pinned by:** `tests/loading-value.test.ts` +**Mechanism (index, 2026-09-14):** `_loading`; `handleAsync` serves `_value` instead of `NotReadyError` while set; `parkLoadingWindow`; window closes on the first OBSERVABLE landing (#2990). + +(**ruled 2026-08-10**) **The commit-#0 loading window is loading-class and verdict-quiet.** A node born committed via `loadingValue` (memos: `createMemo` / `createSignal(fn)` / `createOptimistic(fn)`) or `seedLoadingValue` (projections: `createProjection` / `createStore(fn)` / `createOptimisticStore(fn)`) starts with the loading value as commit #0 of its lineage instead of `STATUS_UNINITIALIZED`. While its first real answer is in flight, the window is loading-class on every axis: (1) **reads** — every consumer path serves commit #0 (no `NotReadyError`, no Loading-boundary suspension; `latest()` and `resolve()` return it; it is the compute's first `prev`); (2) **transitions** — the window never initiates or extends one (matching boundary-fallback semantics): ambient writes concurrent with the window commit immediately rather than being held, and a loading node mounted inside a live transition does not add to what that transition waits for; (3) **verdict** — `isPending` reads false at the source, upstream, and downstream, in both forms. The quiet ruling is not an exception to A19 but its question scoping applied: commit #0 answers the question **by declaration**, so the first flight is re-ask-shaped — the shown answer still answers the question (A24 family). The alternative was rejected as structurally unavailable: genuine pending is chain-shaped (the shadow of a held commit — held write upstream, in-flight async at the node, propagated non-finality downstream), and the window has no held commit to shadow, so a true verdict could only exist as a point anomaly at the probed node while upstream and downstream read false — and making it propagate would reintroduce exactly the status machinery the window exists to silence (and re-open the server/client split: `isPending` is always false on the server). First-load affordances are therefore the **value channel**'s job — the author encodes provenance (`null`, a `skeleton` flag) into the loading value itself — and `data.skeleton || isPending(data)` covers the two disjoint states. The window closes at the first real landing on any path (sync return, sync-resolved thenable, first iterator yield, async settle); a real error answers reads but does not close it — a retry serves commit #0 again. After close, A19 applies unchanged: refetches are pending-class forever. A25 is unchanged for plain seeds: without `seedLoadingValue` a derived store's seed remains an unobservable draft; `seedLoadingValue` is precisely the author promoting the seed to commit #0 — observable by declaration. + +### A25. A derived store's seed is a draft, never an observable value + +**Status:** **ruled** 2026-07-16 — maintainer rulings 2026-07-16 ("a seed… should never be visible under any case"; "we throw NotReady except in top-level component scope where we throw that other error"; "self reads are fine though — that's the point of seed, but outside isn't") and 2026-07-17 ("if the seed is visible in compute body it probably should be visible on write.. it throws on read so no consumer can rely on it") +**Pinned by:** `tests/strict-read-pending-store.test.ts` (untracked dev/prod matrix); `tests/store/createProjection.async.test.ts` (seed hidden until first resolution/first yield, supersession keeps it hidden, enumeration throws); `tests/uninitialized-visibility.test.ts` (write-path seed visibility, loading-vs-pending probe #2910) +**Mechanism (index, 2026-09-14):** Derived store `STATUS_UNINITIALIZED` until first landing / first yield; `createShadowDraft`; strict-read matrix. + +(**ruled 2026-07-16**, #2897) **A derived store's seed is a draft, never an observable value.** The seed exists for the derive function — self reads while it works its draft are the point — but to every outside consumer the store is _uninitialized_ (A19 exception 1: loading, not pending) until the first resolution lands; for async-iterator derives, until the **first yield** lands (uninitialized only until then — each later yield is a revealed snapshot, readable between yields even while the generator is still running). During that window every outside consumer path throws: tracked reads suspend into loading boundaries via their node's `NotReadyError` as always, and the untracked fall-throughs — property reads, `in` checks, enumeration/spread — throw the same `NotReadyError` from the firewall (in dev strictRead scopes, i.e. component bodies, the more descriptive `PENDING_ASYNC_UNTRACKED_READ` error wins, matching async memos and preventing infinite loops). Returning the seed leaked a value the reader could never observe updating; returning `undefined` would break non-nullable types. Write-path reads (reconcile enumerating during the first landing) are exempt — they _are_ the initialization. This is safeguard parity: memos already behaved this way; store proxies bypassed `read()` and with it every guard. **Write-visibility corollary (ruled 2026-07-17, #2910 follow-up): the seed IS visible to write-path consumers.** A setter's function-form argument — the store setter's draft, `prev` in `set(prev => …)` — reads the raw current state: the seed for an uninitialized derived store, `undefined` for an uninitialized optimistic computed (it has no seed argument), the displayed value once initialized. Same exemption as the derive body: writes need a base, and because every read channel throws during the window, no consumer can _rely_ on the seed — visibility on the write path leaks nothing observable. Absolute writes were never gated. + +## Errors + +### A1. Effect error interception is compute-phase only + +**Status:** **ruled** 2026-07-06 — #2839 ruling (2026-07-06) +**Pinned by:** `tests/effect-error-phases.test.ts` +**Mechanism (index, 2026-09-14):** `EffectBundle.error` handler wraps the compute half of `createEffect`; effect-phase throws route through `handleError` → nearest `createErrorBoundary` → `REACTIVITY_HALTED`. + +`EffectBundle.error` intercepts compute-phase errors only; effect-phase throws escalate to the nearest error boundary (halt if none). + +### A2. Unhandled compute-phase errors in user effects are logged and skipped + +**Status:** **ruled** — #2839 ruling +**Pinned by:** `tests/effect-error-phases.test.ts` +**Mechanism (index, 2026-09-14):** `runEffect` catch for `EFFECT_USER`; no boundary participation. + +Compute-phase errors in _user_ effects without a handler are logged and the run is skipped; the system keeps running. + +### A3. Comparator throws are compute-phase errors + +**Status:** **ruled** — #2837 +**Pinned by:** `tests/equals-comparator-errors.test.ts` +**Mechanism (index, 2026-09-14):** `setSignal` / `asyncWrite` / `recompute` route a throwing `_equals` through `notifyStatus(STATUS_ERROR)` (#2837). + +Errors thrown by a user `equals` comparator behave exactly like compute-phase errors (boundary-containable; loud halt without a boundary). + +### A4. A custom `equals` never sees `undefined` prev on first commit + +**Status:** **ruled** — #2837 follow-on +**Pinned by:** `tests/equals-comparator-errors.test.ts` (async case) +**Mechanism (index, 2026-09-14):** `STATUS_UNINITIALIZED` is checked before `_equals` runs in `setSignal` and `recompute`. + +A custom `equals` is never invoked with `undefined` previous value on a node's first commit. + +### A5. An error escaping every boundary halts the system + +**Status:** **ruled** — #2761/#2762 +**Pinned by:** `tests/createErrorBoundary.test.ts`, `tests/errorHalt.test.ts` +**Mechanism (index, 2026-09-14):** `REACTIVITY_HALTED` latch in `scheduler.ts`; later writes log 'Update ignored'. + +An error escaping every boundary permanently halts the system with `REACTIVITY_HALTED`; later writes log "Update ignored". + +### A6. `ASYNC_OUTSIDE_LOADING_BOUNDARY` is warn-only + +**Status:** **ruled** — #2822 +**Pinned by:** `tests/enforceLoadingBoundary.test.ts`, solid-web `test/dev-warning.spec.tsx` +**Mechanism (index, 2026-09-14):** Diagnostic emission only; `createErrorBoundary` must not catch a pending (`NotReadyError` is not `StatusError`). + +`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. + +## Superseded rules (kept verbatim) + +Cited by tests and by A24's reasoning; the statements below are as they stood when superseded. + +### A20. (superseded) Optimistic writes announce a store-wide pending + +**Status:** **superseded** 2026-07-13 by A24 — kept for the reasoning record +**Pinned by:** A20 describe in `tests/spec-async-semantics.test.ts`; `tests/createOptimistic.test.ts` (mask + source-still-pends contrast); latest-channel: `tests/createMemo.test.ts`, solid-web `test/latest-async.spec.tsx`; INV-10 enforces the mask in dev + +(**SUPERSEDED 2026-07-13 by A24** — the mask is deleted; optimistic writes are verdict-inert. Kept for the reasoning record. The surviving pieces: verdicts are per-channel (§2, now in A8), and action affordances belong in the data (co-written flags), not derived from verdicts.) (**re-ruled 2026-07-07c** — supersedes the 2026-07-07 "overrides are unsettled" ruling, which held for one day) **The mask: an optimistic override is certainty by decree; `isPending` follows the channel you read.** (1) An _active_ override reads `isPending === false` — uniformly, on every node kind, in **both** forms, for the override's whole lifetime (until its own source confirms it, A18, or the transition reverts it). Writing optimistically _declares_ the shown value the outcome; a decree cannot be superseded by work already in motion, because the writer just asserted it won't be. `isPending` is reserved for data being updated by machinery the reader did _not_ decree — refetches, transition-held commits — never for the provisional nature of an override ("isPending is about the data being in the process of being updated, not about an action being in progress"). Action-scoped affordances ("Saving…", per-row spinners) therefore belong **in the data**: a co-written flag (`todo.pending = true` — the repo's todos example) or a separate `createOptimistic(false)`. You are already writing the optimistic update; the flag rides along. The old no-extra-boolean idiom (`isPending(() => books.length)` as the "Adding…" label) is rejected — it derived an action's progress from a data verdict. (2) Verdicts are per-channel: the plain form watches the _committed_ channel — pending while its own fetch is in flight and while a resolved value is held uncommitted by a transition; the latest form watches the _fresh_ channel — `latest` is an override the system writes for itself the moment a held value exists (A8), and that self-override masks holds like any user override, leaving only actually-in-flight async as its pending cause. Pairing falls out for both forms: neither ever pairs `true` with the value that made it false. (3) Scope: the mask covers the primitive that was written — node-scoped for signals and computeds; store-scoped for derived optimistic stores (A21). (4) Non-derived optimistic signals/stores are never pending _from themselves_ — there is no source to confirm or refetch, the write is an instantly-visible decree — they pend only via a transition hold on the trigger like any plain signal. (5) No tension with A17/A18: the override is THE value (A17), its lifetime is transition-bound (A18), and the mask simply says the verdict agrees with the decree for exactly that lifetime — mask on at write, off at revert/confirm, in the same atomic settle. + +### A21. (superseded) The store-wide mask + +**Status:** **superseded** 2026-07-13 by A24 — kept for the reasoning record +**Pinned by:** "store-wide mask" pin in the A20 describe, `tests/spec-async-semantics.test.ts`; `tests/store/createOptimisticStore.test.ts` (refresh-pends → write-masks → lift contrasts); INV-10 store-mask arm + +(**SUPERSEDED 2026-07-13 by A24** — the store-wide mask is deleted with the mask model; nothing silences a new question. The effective-write gate (consequence 4) survives as transaction-entanglement hygiene: no-op optimistic writes still neither entangle nor decree anything.) **The store-wide mask: for a derived optimistic store, the store is the primitive — any active optimistic write masks `isPending` for the _entire_ store.** Written leaves, untouched siblings, structural reads (`length`, iteration), and the firewall's own refetch all read `false` while any override on the store is live, in both forms; the mask lifts when the store's optimistic state fully clears (same lane lifetime as A20). Rationale: a refetch pends the whole store because the authority's change set is unbounded (A9) — the decree that silences it must speak for the same unbounded scope, or `isPending(() => store.items.length)` would flip on a refresh the writer already declared the outcome of ("If I do `setOptimisticFormOptions(x => x.cities.push("London"))` then I expect the select to consider it settled" — same for `x.cities[i] = "London"`). Once you write optimistically you own the store's pending affordances (A20 §1: flags in the data). Consequences: (1) optimistic writes to the same store entangle — not just writes to the same property; (2) plain (non-derived) optimistic stores get this for free — with no source they were never pending from themselves (A20 §4); (3) A9 is the unmasked rule: with **no** active override, every leaf reports the firewall's refetch in both forms — the store-wide mask is an override-lifetime exception, not a repeal; (4) (added 2026-07-08) only **effective** writes arm the mask and entangle — the decree is about data actually asserted, so trap fires that change nothing (`s => s`, `s => ({ ...s })` replaying equal values, same-value property writes, deletes of absent properties) are no-ops with no decree, matching the signal path where an equal-value first optimistic write short-circuits before any override exists. A deliberate "silence this refresh" affordance is future explicit API (#2844 family), not an emergent no-op write. + +## History + +### Tier B (inferred — needs verdict) Mark each **keep** or **change**; on _keep_ it gets a spec test and moves to Tier A. @@ -61,7 +269,7 @@ Tier A. `_overrideSinceLane` flag that also guarded this was removed 2026-07-07 — the re-ruled A18 hold model made its correction path unreachable.) -## Tier C (open — needs decision) +### Tier C (open — needs decision) - [x] **C1 — RULED, promoted to A19 (2026-07-07).** `isPending` is about data, not boundaries: `true` during any in-flight refetch with a stale @@ -92,7 +300,7 @@ Tier A. entangled (merged) transition completed on the first flush and silently dropped the override. Fixed by removing the self-source exclusion. -## Known violations — ALL FIXED by the #2838 redesign (2026-07-07) +### Known violations — ALL FIXED by the #2838 redesign (2026-07-07) Four ruled Tier A propositions were violated, mostly in the **blocked-merged window** (a node's own fetch resolved, but a shared reader entangles it with @@ -150,7 +358,7 @@ fixed it: The companion-vs-oracle census (`COMPANION_CENSUS=1`) reports **zero divergence fingerprints** across the suite post-redesign. -## Re-ruling log — 2026-09-09: lane authority (#3335, #3334, #3331, #3330) +### Re-ruling log — 2026-09-09: lane authority (#3335, #3334, #3331, #3330) Four GitHub reports against `2.0.0-rc` lanes, all pre-existing (not regressions from the held-till-flush change, #3337). Common thread: places @@ -239,7 +447,7 @@ question. supersession, the `runEffect` owner-gate exception for lane-less lane runners. See INTERNALS-ASYNC-STATE.md §1–§3. -## Re-ruling log — 2026-07-13: question-scoped pending (#2844/#2728, supersedes the mask) +### Re-ruling log — 2026-07-13: question-scoped pending (#2844/#2728, supersedes the mask) The mask model held for six days. The #2844 thread kept producing cases where a decree silenced ground truth it could not know (the foos bug: an optimistic @@ -297,7 +505,7 @@ of this record): if `reconcile` replaces the underlying object mid-flight (new object, new identity — accepted as correct). -## Re-ruling log — 2026-07-07c: the mask model (#2844/#2728) — superseded 2026-07-13 +### Re-ruling log — 2026-07-07c: the mask model (#2844/#2728) — superseded 2026-07-13 The "overrides are unsettled" algebra ruled on 2026-07-07 was reversed the next day after the #2844 background-refresh discussion converged with GabbeV's diff --git a/packages/signals/scripts/rules-index.mjs b/packages/signals/scripts/rules-index.mjs index 70fe86d4d..9db90447a 100644 --- a/packages/signals/scripts/rules-index.mjs +++ b/packages/signals/scripts/rules-index.mjs @@ -40,6 +40,12 @@ const walk = (dir, out = []) => { const rules = new Map(); // key -> { key, id, vocab, ns, def, status, text } function status(text) { const head = text.slice(0, 160); + const tag = /^\[([^\]]+)\]/.exec(text); + if (tag) + return tag[1] + .replace("ruled, amended in place", "amended") + .replace(/ \d{4}-\d{2}-\d{2}.*$/, "") + .replace(/ \(promoted.*$/, ""); if (/SUPERSEDED/.test(text)) return "superseded"; if (/RETIRED/.test(head)) return "retired"; if (/RULED OUT/.test(head)) return "ruled out"; @@ -75,12 +81,37 @@ function scan(file, re, mk) { }); } const SPEC = path.join(DOCS, "SPEC-ASYNC-SEMANTICS.md"); -scan(SPEC, /^\| (A\d{1,2}) +\|(.*)$/, (m, n) => add(m[1], m[1], "A", "", SPEC, n, m[2])); +{ + // A-rules: "### A<n>. <title>" sections. Status comes from the **Status:** + // line (its first clause), the statement is the section body. Tier B/C ids + // promoted into an A-rule ("(was B1)" in the statement) resolve to that rule. + const L = read(SPEC).split("\n"); + for (let i = 0; i < L.length; i++) { + const m = /^### (A\d{1,2})\. (.*)$/.exec(L[i]); + if (!m) continue; + let j = i + 1; + const body = []; + while (j < L.length && !/^#{2,3} /.test(L[j])) body.push(L[j++]); + const st = (body.find(l => l.startsWith("**Status:**")) || "") + .replace(/\*\*Status:\*\*\s*/, "") + .replace(/\*\*/g, "") + .split(" — ")[0]; + const prop = body.filter(l => l && !/^\*\*(Status|Pinned by|Mechanism)/.test(l)).join(" "); + add(m[1], m[1], "A", "", SPEC, i + 1, `[${st}] ${m[2]} — ${prop}`); + const was = /\(was ([BC]\d)\b/.exec(prop); + if (was) + add( + was[1], + was[1], + was[1][0], + "", + SPEC, + i + 1, + `PROMOTED → ${m[1]} (${m[1]}'s section carries the ruling).` + ); + } +} scan(SPEC, /^- \*\*(V\d)\b(.*)$/, (m, n, l) => add(m[1], m[1], "V", "", SPEC, n, l)); -// Tier B/C ids promoted into A-rules ("(was B1)") keep resolving to the row they became. -scan(SPEC, /^\| (A\d{1,2}) +\|.*\(was ([BC]\d)\b/, (m, n) => - add(m[2], m[2], m[2][0], "", SPEC, n, `PROMOTED → ${m[1]} (${m[1]}'s row carries the ruling).`) -); scan(SPEC, /^- \[[ x]\] \*\*([BC]\d)\b(.*)$/, (m, n, l) => add(m[1], m[1], m[1][0], "", SPEC, n, l) ); From 51035323b053b0607a15ad6cd1f95f8254b65f59 Mon Sep 17 00:00:00 2001 From: Ryan Carniato <ryansolid@gmail.com> Date: Mon, 14 Sep 2026 10:12:08 -0700 Subject: [PATCH 3/7] docs(signals): name the five rules-mining namespaces at the source Each rules-mining file numbers from R1 (58 / 37 / 46 / 36 / 38 rules), so a bare R-id is ambiguous five ways. Each file now opens with its namespace (CS / OL / OS / PJ / RS), how source comments cite it (core / lanes / opt / proj / snap R<n>) and which module's bare citations resolve to it; rules-mining/README.md carries the table. Matches scripts/rules-index.mjs's resolution rules. No ID renumbered. Co-authored-by: Cursor <cursoragent@cursor.com> --- packages/signals/docs/RULES-INDEX.md | 430 +++++++++--------- packages/signals/docs/rules-mining/README.md | 21 + .../signals/docs/rules-mining/core-store.md | 2 + .../docs/rules-mining/optimistic-lanes.md | 2 + .../docs/rules-mining/optimistic-store.md | 2 + .../signals/docs/rules-mining/projections.md | 2 + .../docs/rules-mining/reconcile-snapshot.md | 2 + 7 files changed, 246 insertions(+), 215 deletions(-) create mode 100644 packages/signals/docs/rules-mining/README.md diff --git a/packages/signals/docs/RULES-INDEX.md b/packages/signals/docs/RULES-INDEX.md index 4ef1ffc51..6cdbd4042 100644 --- a/packages/signals/docs/RULES-INDEX.md +++ b/packages/signals/docs/RULES-INDEX.md @@ -134,238 +134,238 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul | id | status | defined | cited in src | cited in tests | statement (at definition) | |---|---|---|---|---|---| -| CS-R1 | live | `docs/rules-mining/core-store.md:7` | — | flatten-async-iterable.test.ts×5 syncThenable.test.ts×14 | 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:10` | — | syncThenable.test.ts×12 | Raw→proxy resolution is global and deduplicating: wrapping the same raw through two different stores yields the same proxy (`outer.list === inner`).** | +| CS-R1 | live | `docs/rules-mining/core-store.md:9` | — | flatten-async-iterable.test.ts×5 syncThenable.test.ts×14 | 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:12` | — | syncThenable.test.ts×12 | 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:14` | — | — | 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:17` | — | — | 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:20` | — | — | Upstream writes propagate downstream through the chain without re-running structural machinery.** | -| CS-R6 | live | `docs/rules-mining/core-store.md:23` | — | — | 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:26` | — | — | Circular references wrap without infinite recursion; cycle consistent through proxy (`state.b.a === state.a`).** | -| CS-R8 | live | `docs/rules-mining/core-store.md:29` | — | — | `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:32` | 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:37` | — | 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:40` | — | — | 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:43` | 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:46` | 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:49` | — | — | `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:52` | 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:55` | — | — | `length` independently trackable; index write extending the array notifies length subscribers.** | -| CS-R17 | live | `docs/rules-mining/core-store.md:58` | — | — | 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:61` | — | — | `snapshot` is non-tracking.** Aligned with read table. | -| CS-R19 | live | `docs/rules-mining/core-store.md:64` | — | — | `untrack` scopes only the wrapped read; property access on the escaped value afterwards tracks normally.** | -| CS-R20 | live | `docs/rules-mining/core-store.md:67` | 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:70` | 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:74` | — | — | 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:79` | 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:82` | — | 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:86` | — | 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:90` | — | — | 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:93` | 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:96` | — | — | `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:99` | 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:103` | 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:106` | 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:110` | store.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:113` | — | — | 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:116` | store.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.** | -| CS-R35 | live | `docs/rules-mining/core-store.md:120` | — | — | 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:123` | — | — | 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:127` | — | — | Setting store state from effect callbacks and promise resolutions works, applying next flush.** | -| CS-R38 | live | `docs/rules-mining/core-store.md:132` | — | — | 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:135` | — | — | 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:138` | — | — | 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:141` | 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:145` | 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:148` | — | — | 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:151` | 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:155` | — | — | 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:158` | — | — | 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:163` | — | — | 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:167` | — | — | 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:170` | — | — | Null-prototype objects wrap and track; function-valued props callable through the proxy.** | -| CS-R50 | live | `docs/rules-mining/core-store.md:173` | — | — | Frozen sources fully supported (read/snapshot; getters returning frozen don't throw).** | -| CS-R51 | live | `docs/rules-mining/core-store.md:177` | 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:180` | — | — | 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:183` | — | — | 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:186` | — | — | 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:188` | — | — | Functions stored as values served raw, replaceable, slot-tracked.** | -| CS-R56 | live | `docs/rules-mining/core-store.md:192` | — | — | 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:195` | — | — | Effect ordering: parent effects before child effects created inside them, incl. shared deps through memos.** | -| CS-R58 | live | `docs/rules-mining/core-store.md:198` | — | — | 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… | +| CS-R3 | live | `docs/rules-mining/core-store.md:16` | — | — | 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:19` | — | — | 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:22` | — | — | Upstream writes propagate downstream through the chain without re-running structural machinery.** | +| CS-R6 | live | `docs/rules-mining/core-store.md:25` | — | — | 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:28` | — | — | Circular references wrap without infinite recursion; cycle consistent through proxy (`state.b.a === state.a`).** | +| CS-R8 | live | `docs/rules-mining/core-store.md:31` | — | — | `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:34` | 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:39` | — | 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:42` | — | — | 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:45` | 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:48` | 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:51` | — | — | `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:54` | 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:57` | — | — | `length` independently trackable; index write extending the array notifies length subscribers.** | +| CS-R17 | live | `docs/rules-mining/core-store.md:60` | — | — | 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:63` | — | — | `snapshot` is non-tracking.** Aligned with read table. | +| CS-R19 | live | `docs/rules-mining/core-store.md:66` | — | — | `untrack` scopes only the wrapped read; property access on the escaped value afterwards tracks normally.** | +| CS-R20 | live | `docs/rules-mining/core-store.md:69` | 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:72` | 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:76` | — | — | 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:81` | 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:84` | — | 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:88` | — | 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:92` | — | — | 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:95` | 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:98` | — | — | `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:101` | 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:105` | 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:108` | 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:112` | store.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:115` | — | — | 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:118` | store.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.** | +| CS-R35 | live | `docs/rules-mining/core-store.md:122` | — | — | 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:125` | — | — | 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:129` | — | — | Setting store state from effect callbacks and promise resolutions works, applying next flush.** | +| CS-R38 | live | `docs/rules-mining/core-store.md:134` | — | — | 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:137` | — | — | 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:140` | — | — | 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:143` | 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:147` | 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:150` | — | — | 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:153` | 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:157` | — | — | 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:160` | — | — | 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:165` | — | — | 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:169` | — | — | 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:172` | — | — | Null-prototype objects wrap and track; function-valued props callable through the proxy.** | +| CS-R50 | live | `docs/rules-mining/core-store.md:175` | — | — | Frozen sources fully supported (read/snapshot; getters returning frozen don't throw).** | +| CS-R51 | live | `docs/rules-mining/core-store.md:179` | 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:182` | — | — | 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:185` | — | — | 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:188` | — | — | 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:190` | — | — | Functions stored as values served raw, replaceable, slot-tracked.** | +| CS-R56 | live | `docs/rules-mining/core-store.md:194` | — | — | 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:197` | — | — | Effect ordering: parent effects before child effects created inside them, incl. shared deps through memos.** | +| CS-R58 | live | `docs/rules-mining/core-store.md:200` | — | — | 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<n>`) | id | status | defined | cited in src | cited in tests | statement (at definition) | |---|---|---|---|---|---| -| OL-R1 | live | `docs/rules-mining/optimistic-lanes.md:9` | — | — | `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 | live | `docs/rules-mining/optimistic-lanes.md:12` | — | — | An optimistic write is synchronously visible to direct reads before any flush — inside the action body, outside it, and outside any reactive context. | -| OL-R3 | live | `docs/rules-mining/optimistic-lanes.md:15` | — | — | 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:18` | — | — | 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:21` | — | — | 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:25` | — | — | 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:28` | — | — | 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:31` | — | — | 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:34` | — | — | 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:37` | — | — | `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:40` | — | — | 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:43` | — | — | 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:46` | — | — | 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:52` | — | — | 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:55` | — | — | 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:59` | — | — | 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:63` | — | — | 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:67` | — | — | 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:70` | — | — | 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:73` | — | — | 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:78` | — | — | 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:82` | — | — | 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:85` | — | — | 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:91` | — | — | 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:94` | — | — | 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:97` | — | — | 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:100` | — | — | 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:105` | — | — | 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:109` | — | — | 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:112` | — | — | 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:115` | — | — | 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:118` | — | — | `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:121` | — | — | 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:124` | — | — | `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:129` | — | — | `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:132` | — | — | 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:136` | — | — | Store optimism is per-key: different keys written by different actions settle independently — same ownership rules as signals (R15/R16) at store-key granularity. | +| 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 | live | `docs/rules-mining/optimistic-lanes.md:14` | — | — | An optimistic write is synchronously visible to direct reads before any flush — inside the action body, outside it, and outside any reactive context. | +| OL-R3 | live | `docs/rules-mining/optimistic-lanes.md:17` | — | — | 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:20` | — | — | 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:23` | — | — | 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:27` | — | — | 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:30` | — | — | 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:33` | — | — | 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:36` | — | — | 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:39` | — | — | `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:42` | — | — | 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:45` | — | — | 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:48` | — | — | 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:54` | — | — | 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:57` | — | — | 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:61` | — | — | 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:65` | — | — | 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:69` | — | — | 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:72` | — | — | 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:75` | — | — | 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:80` | — | — | 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:84` | — | — | 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:87` | — | — | 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:93` | — | — | 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:96` | — | — | 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:99` | — | — | 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:102` | — | — | 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:107` | — | — | 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:111` | — | — | 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:114` | — | — | 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:117` | — | — | 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:120` | — | — | `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:123` | — | — | 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:126` | — | — | `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:131` | — | — | `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:134` | — | — | 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:138` | — | — | 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<n>`) | id | status | defined | cited in src | cited in tests | statement (at definition) | |---|---|---|---|---|---| -| OS-R1 | live | `docs/rules-mining/optimistic-store.md:7` | — | — | Synchronous universal visibility.** An optimistic write is visible to every reader immediately at write time, before any flush: tracked, untracked, inside the action body, outside any reactive context… | -| OS-R2 | live | `docs/rules-mining/optimistic-store.md:10` | — | — | 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:14` | — | — | 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:17` | — | — | 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:20` | — | — | 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:24` | — | — | 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:28` | — | — | Propagation through derived graphs** (memo chains, mapArray) like committed values. | -| OS-R8 | live | `docs/rules-mining/optimistic-store.md:30` | — | — | `latest()` returns the optimistic value** during a pending refetch window. | -| OS-R9 | live | `docs/rules-mining/optimistic-store.md:32` | — | — | 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:37` | — | — | Settle reverts to base with one notification** (`[0,1,0]`). | -| OS-R11 | live | `docs/rules-mining/optimistic-store.md:39` | — | — | 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:41` | — | — | 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:45` | — | — | 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:47` | — | — | 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:51` | — | — | Unaffected subscribers do not rerun on another action's settle.** | -| OS-R16 | live | `docs/rules-mining/optimistic-store.md:53` | — | — | Cycles are independent** (no residue between sequential write/settle cycles). | -| OS-R17 | live | `docs/rules-mining/optimistic-store.md:55` | — | — | 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:59` | — | — | 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:61` | — | — | Disjoint-key concurrent actions revert independently** (incl. different rows, deletes) (#2899 ×3). | -| OS-R20 | live | `docs/rules-mining/optimistic-store.md:64` | — | — | 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:68` | — | — | 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:71` | — | — | 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:74` | — | — | 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:76` | — | — | 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:80` | — | — | 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:82` | — | — | 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:85` | — | — | 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:87` | — | — | 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:90` | — | — | 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:95` | 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:98` | — | — | 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:100` | — | — | Post-init untracked reads flow committed values,** including during a later refetch window. | -| OS-R33 | live | `docs/rules-mining/optimistic-store.md:102` | — | — | Refetch window keeps the dev safeguard** (committed value untracked; component-body read still dev-throws). | -| OS-R34 | live | `docs/rules-mining/optimistic-store.md:104` | — | — | 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:106` | — | — | Plain stores unaffected** (read normally in every context incl. component bodies). | -| OS-R36 | live | `docs/rules-mining/optimistic-store.md:110` | — | — | Dependency-driven refetch pends the leaf and holds the committed view** until the fetch lands. | -| OS-R37 | live | `docs/rules-mining/optimistic-store.md:112` | — | — | 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:114` | 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:117` | — | — | 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:119` | — | — | 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:121` | — | — | 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:123` | — | — | 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:126` | — | — | 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:128` | — | — | 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:130` | — | — | 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:133` | — | — | Refetch persistence across multi-action windows:** overlay survives arbitrary interleaved refresh landings while any overlapping action is pending. | +| OS-R1 | live | `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: tracked, untracked, inside the action body, outside any reactive context… | +| OS-R2 | live | `docs/rules-mining/optimistic-store.md:12` | — | — | 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:16` | — | — | 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:19` | — | — | 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:22` | — | — | 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:26` | — | — | 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:30` | — | — | Propagation through derived graphs** (memo chains, mapArray) like committed values. | +| OS-R8 | live | `docs/rules-mining/optimistic-store.md:32` | — | — | `latest()` returns the optimistic value** during a pending refetch window. | +| OS-R9 | live | `docs/rules-mining/optimistic-store.md:34` | — | — | 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:39` | — | — | Settle reverts to base with one notification** (`[0,1,0]`). | +| OS-R11 | live | `docs/rules-mining/optimistic-store.md:41` | — | — | 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:43` | — | — | 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:47` | — | — | 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:49` | — | — | 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:53` | — | — | Unaffected subscribers do not rerun on another action's settle.** | +| OS-R16 | live | `docs/rules-mining/optimistic-store.md:55` | — | — | Cycles are independent** (no residue between sequential write/settle cycles). | +| OS-R17 | live | `docs/rules-mining/optimistic-store.md:57` | — | — | 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:61` | — | — | 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:63` | — | — | Disjoint-key concurrent actions revert independently** (incl. different rows, deletes) (#2899 ×3). | +| OS-R20 | live | `docs/rules-mining/optimistic-store.md:66` | — | — | 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:70` | — | — | 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:73` | — | — | 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:76` | — | — | 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:78` | — | — | 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:82` | — | — | 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:84` | — | — | 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:87` | — | — | 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:89` | — | — | 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:92` | — | — | 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:97` | 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:100` | — | — | 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:102` | — | — | Post-init untracked reads flow committed values,** including during a later refetch window. | +| OS-R33 | live | `docs/rules-mining/optimistic-store.md:104` | — | — | Refetch window keeps the dev safeguard** (committed value untracked; component-body read still dev-throws). | +| OS-R34 | live | `docs/rules-mining/optimistic-store.md:106` | — | — | 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:108` | — | — | Plain stores unaffected** (read normally in every context incl. component bodies). | +| OS-R36 | live | `docs/rules-mining/optimistic-store.md:112` | — | — | Dependency-driven refetch pends the leaf and holds the committed view** until the fetch lands. | +| OS-R37 | live | `docs/rules-mining/optimistic-store.md:114` | — | — | 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:116` | 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:119` | — | — | 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:121` | — | — | 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:123` | — | — | 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:125` | — | — | 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:128` | — | — | 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:130` | — | — | 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:132` | — | — | 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:135` | — | — | Refetch persistence across multi-action windows:** overlay survives arbitrary interleaved refresh landings while any overlapping action is pending. | ## R — projections (`PJ-R<n>`) | id | status | defined | cited in src | cited in tests | statement (at definition) | |---|---|---|---|---|---| -| PJ-R1 | live | `docs/rules-mining/projections.md:7` | — | — | 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:10` | 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:13` | — | — | 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:17` | — | — | 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:20` | 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:23` | 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:26` | 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:30` | — | — | `{ key: null }` merges positionally** (proxy identity preserved regardless of key-field changes). | -| PJ-R9 | live | `docs/rules-mining/projections.md:32` | — | — | 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:35` | 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:39` | — | — | `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:44` | — | — | 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:47` | — | — | 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:51` | — | — | Every subscriber of a changed property is notified exactly once per change.** | -| PJ-R15 | live | `docs/rules-mining/projections.md:53` | — | — | 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:55` | — | — | `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:60` | — | — | 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:64` | — | — | 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:66` | — | — | 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:68` | — | — | Chained backing works for array roots** (structural + row-level edits flow). | -| PJ-R21 | live | `docs/rules-mining/projections.md:70` | — | — | `createStore(fn, seed)` is the same projection mechanism and chains identically.** | -| PJ-R22 | live | `docs/rules-mining/projections.md:72` | — | — | `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:78` | 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:81` | — | — | 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:83` | — | — | 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:85` | — | — | 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:87` | — | — | Async recompute does not coarsen granularity**: after settle, only changed-path subscribers rerun. | -| PJ-R28 | live | `docs/rules-mining/projections.md:89` | — | — | `refresh(proj)` forces a new derive run; bare refresh is quiet** (no pending published; silent reveal). | -| PJ-R29 | live | `docs/rules-mining/projections.md:91` | — | — | `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:93` | — | — | With no effect subscribed, async work creates no transition** (isPending false throughout initial load). | -| PJ-R31 | live | `docs/rules-mining/projections.md:95` | — | — | 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:97` | — | — | 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:99` | — | — | 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:101` | — | — | 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:104` | — | — | 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:108` | — | — | Disposing the owning root stops the projection** (no recomputes, no notifications afterward). | +| 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<n>`) | id | status | defined | cited in src | cited in tests | statement (at definition) | |---|---|---|---|---|---| -| RS-R1 | live | `docs/rules-mining/reconcile-snapshot.md:7` | — | — | 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:10` | — | — | Reconcile applies to any nested proxy, not just the root**, with identical semantics. | -| RS-R3 | live | `docs/rules-mining/reconcile-snapshot.md:13` | — | — | 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:16` | — | — | `key: null` / `key: ""` disables key matching**: positional merge, no root identity check. | -| RS-R5 | live | `docs/rules-mining/reconcile-snapshot.md:19` | — | — | Key modes: string key, key function, none.** KeyFn's call set is observable (see R17). | -| RS-R6 | live | `docs/rules-mining/reconcile-snapshot.md:22` | — | — | Key-matched items preserve logical (proxy) identity across reorder, insert, delete.** | -| RS-R7 | live | `docs/rules-mining/reconcile-snapshot.md:25` | — | — | 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:28` | 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:31` | 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:34` | 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:37` | reconcile.ts×2 | — | Null entries and primitives are legal keyed-array members** (#2772). | -| RS-R12 | live | `docs/rules-mining/reconcile-snapshot.md:40` | — | — | 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:43` | — | — | 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:46` | — | — | Symbol keys have full parity with string keys under reconcile** (update/remove/add/nested/mixed). | -| RS-R15 | live | `docs/rules-mining/reconcile-snapshot.md:49` | — | — | 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:52` | 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:55` | 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:58` | 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:61` | — | — | `deep()` observes a reconcile as a single notification carrying the final plain data.** | -| RS-R20 | live | `docs/rules-mining/reconcile-snapshot.md:64` | — | — | (type-level) — `reconcile(next)` requires the complete store type.** | -| RS-R21 | live | `docs/rules-mining/reconcile-snapshot.md:68` | — | — | 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:71` | — | 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:76` | — | — | `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:79` | — | — | 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:82` | — | — | 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:85` | — | — | 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:88` | — | — | 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:91` | 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:94` | 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:97` | — | — | 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:100` | — | — | 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:103` | — | — | 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:108` | — | — | `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:111` | — | — | `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:113` | — | — | `merge` over signal-of-object source is reactive with minimal notifications.** | -| RS-R36 | live | `docs/rules-mining/reconcile-snapshot.md:115` | — | — | `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:117` | — | 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:119` | — | — | 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. … | +| 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) | diff --git a/packages/signals/docs/rules-mining/README.md b/packages/signals/docs/rules-mining/README.md new file mode 100644 index 000000000..e21ef4ea4 --- /dev/null +++ b/packages/signals/docs/rules-mining/README.md @@ -0,0 +1,21 @@ +# Mined rules — five files, five namespaces + +Each file below was mined from one test suite family and numbers its rules from +**R1**. An R-id therefore only means something together with its file. The +index (`../RULES-INDEX.md`, generated by `scripts/rules-index.mjs`) gives each +file a two-letter namespace and lists every rule with its status and citations. + +| file | namespace | how source comments cite it | bare `R<n>` resolves here from | +|---|---|---|---| +| `core-store.md` | `CS` | `core R<n>` | `store/next/store.ts`, `target.ts`, any store module whose own file lacks the id | +| `optimistic-lanes.md` | `OL` | `lanes R<n>` | `core/*` | +| `optimistic-store.md` | `OS` | `opt R<n>` | `store/next/optimistic.ts` | +| `projections.md` | `PJ` | `proj R<n>` | `store/next/projection.ts` | +| `reconcile-snapshot.md` | `RS` | `snap R<n>` | `store/next/reconcile.ts` | + +`FINDINGS.md` is the log of rule assertions run against the shipped store; it +defines no R-ids (its `RUL-<n>` rulings are indexed with +`INTERNALS-STORE-STATE.md` §8b). + +IDs are never renumbered or deleted. A rule that is superseded or retired keeps +its number and says so in place. diff --git a/packages/signals/docs/rules-mining/core-store.md b/packages/signals/docs/rules-mining/core-store.md index 883820c48..2e866912f 100644 --- a/packages/signals/docs/rules-mining/core-store.md +++ b/packages/signals/docs/rules-mining/core-store.md @@ -1,5 +1,7 @@ # Mined rules: core store suites +> **Namespace `CS`.** Every rules-mining file numbers its rules from R1, so an R-id is only meaningful with its file: this one is `CS-R<n>` in [`../RULES-INDEX.md`](../RULES-INDEX.md), the base store vocabulary — a bare `R<n>` in any store module comment resolves here when the module's own file has no such rule. IDs are never renumbered. + Files: **CS** = `tests/store/createStore.test.ts`, **SP** = `tests/store/storePath.test.ts`, **SIS** = `tests/store/store-in-store-tracking.test.ts`, **SH** = `tests/store/shallow.test.ts`, **SPC** = `tests/shallow-store-proxy-children.test.ts`, **RE** = `tests/store/recursive-effects.test.ts`, **NC** = `tests/store/native-collections.test.ts`, **MA** = `tests/maparray-store-nonkeyed.test.ts`. ## A. Value residency & identity diff --git a/packages/signals/docs/rules-mining/optimistic-lanes.md b/packages/signals/docs/rules-mining/optimistic-lanes.md index 5e7a123d6..4d72b3ba4 100644 --- a/packages/signals/docs/rules-mining/optimistic-lanes.md +++ b/packages/signals/docs/rules-mining/optimistic-lanes.md @@ -1,5 +1,7 @@ # Mined rules: optimistic lanes (createOptimistic, undefined-override, lane-transaction-ownership) +> **Namespace `OL`.** Every rules-mining file numbers its rules from R1, so an R-id is only meaningful with its file: this one is `OL-R<n>` in [`../RULES-INDEX.md`](../RULES-INDEX.md), cited as `lanes R<n>`; a bare `R<n>` in `core/*` comments resolves here. IDs are never renumbered. + Source suites: `tests/createOptimistic.test.ts` (CO), `tests/optimistic-undefined-override.test.ts` (UO), `tests/optimistic-lane-transaction-ownership.test.ts` (LTO). Scope note: CO contains **no store-form tests** — it is entirely the signal/computed form. Store-form coverage in this set exists only in UO (tests 3–5) and LTO (repro 1). Nested paths, deep writes, and per-property-vs-whole-store optimism beyond those have **no coverage in this set** — a gap the rewrite's rule-derived tests must fill. diff --git a/packages/signals/docs/rules-mining/optimistic-store.md b/packages/signals/docs/rules-mining/optimistic-store.md index 4dae0b59a..6ef3abb97 100644 --- a/packages/signals/docs/rules-mining/optimistic-store.md +++ b/packages/signals/docs/rules-mining/optimistic-store.md @@ -1,5 +1,7 @@ # Mined rules: optimistic store suites +> **Namespace `OS`.** Every rules-mining file numbers its rules from R1, so an R-id is only meaningful with its file: this one is `OS-R<n>` in [`../RULES-INDEX.md`](../RULES-INDEX.md), cited as `opt R<n>`; a bare `R<n>` in `store/next/optimistic.ts` resolves here. IDs are never renumbered. + Source suites: `tests/store/createOptimisticStore.test.ts`, `tests/optimistic-store-refetch-hold.test.ts`, `tests/optimistic-store-layer-scope.test.ts`, `tests/strict-read-pending-store.test.ts`. ## A. Visibility diff --git a/packages/signals/docs/rules-mining/projections.md b/packages/signals/docs/rules-mining/projections.md index b24ae76db..07488c389 100644 --- a/packages/signals/docs/rules-mining/projections.md +++ b/packages/signals/docs/rules-mining/projections.md @@ -1,5 +1,7 @@ # Mined rules: projections +> **Namespace `PJ`.** Every rules-mining file numbers its rules from R1, so an R-id is only meaningful with its file: this one is `PJ-R<n>` in [`../RULES-INDEX.md`](../RULES-INDEX.md), cited as `proj R<n>`; a bare `R<n>` in `store/next/projection.ts` resolves here. IDs are never renumbered. + Source suites: `sync` = `tests/store/createProjection.test.ts`, `async` = `tests/store/createProjection.async.test.ts`, `jsdom` = `tests/store/createProjection.jsdom.test.ts`. ## Recompute semantics diff --git a/packages/signals/docs/rules-mining/reconcile-snapshot.md b/packages/signals/docs/rules-mining/reconcile-snapshot.md index 76e5f9546..474b77ede 100644 --- a/packages/signals/docs/rules-mining/reconcile-snapshot.md +++ b/packages/signals/docs/rules-mining/reconcile-snapshot.md @@ -1,5 +1,7 @@ # Mined rules: reconcile, snapshot, utilities +> **Namespace `RS`.** Every rules-mining file numbers its rules from R1, so an R-id is only meaningful with its file: this one is `RS-R<n>` in [`../RULES-INDEX.md`](../RULES-INDEX.md), cited as `snap R<n>`; a bare `R<n>` in `store/next/reconcile.ts` resolves here. IDs are never renumbered. + Source suites: `tests/store/reconcile.test.ts`, `tests/store/reconcile-captured-proxies.test.ts`, `tests/snapshot.test.ts`, `tests/snapshot-derived-store-rows.test.ts`, `tests/store/utilities.test.ts`. ## A. Reconcile contract From f6d251a00a6138a91edac97516f5d735081bcb21 Mon Sep 17 00:00:00 2001 From: Ryan Carniato <ryansolid@gmail.com> Date: Mon, 14 Sep 2026 10:19:06 -0700 Subject: [PATCH 4/7] docs(signals): correct three Mechanism identifiers to names that exist in src MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit backfillCompanion → the _pendingValue backfill at companion creation (#3041); _affectsSentinel → _affectsNodes (on the transaction); createShadowDraft → the projection-trap status gate (#2988, proj R23). Found by checking every identifier named in a Mechanism line against src/. Co-authored-by: Cursor <cursoragent@cursor.com> --- packages/signals/docs/SPEC-ASYNC-SEMANTICS.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md index 1d1a77f9a..94f59481d 100644 --- a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md +++ b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md @@ -74,7 +74,7 @@ Sync derivations of transition-held sources are visible through `latest()`/`isPe **Status:** **ruled** 2026-07-13 — maintainer ruling 2026-07-13 (#2844/#2728 convergence; cause-scoped pending, per-path masking + UNCHANGED vouching, `background()`, and lane-bounded vouches each rejected on the way) **Pinned by:** `tests/question-scoped-pending.test.ts` (scenario matrix: foos bug, list over-lighting, navigation-over-override, poll, reload, iMessage posture; cross-family raw sharing #2904); `tests/affects-propagation.test.ts` (marks through derivation: bare-mark windows on memos, late-mark wake, mid-mark landing hold, settle release, no settlement deadlock, store record/keyed marks reaching derived readers); `tests/affects-audit-2893.test.ts` (audit corollaries: container survival under 3+ sources, transaction-inert propagation, transitive/probe-stable re-establishment, error precedence); re-pinned A13/A14/A20-block in `tests/spec-async-semantics.test.ts`; `tests/createOptimistic.test.ts`, `tests/store/createOptimisticStore.test.ts`, `tests/store/createProjection.async.test.ts`, `tests/latest-isPending-consistency.test.ts`, `tests/createMemo.test.ts`, `tests/createLoadingBoundary.test.ts` (quiet-refresh + declared-reload re-pins); INV-10 (affects-count balance) -**Mechanism (index, 2026-09-14):** `_reask` (quiet re-ask), `_affectsCount` / `_affectsSentinel`, `witnessAffects` (verdict.ts). +**Mechanism (index, 2026-09-14):** `_reask` (quiet re-ask), `_affectsCount` on the node / `_affectsNodes` on the transaction, `witnessAffects` (verdict.ts). (**ruled 2026-07-13** — supersedes A20/A21; the converged model from the #2844/#2728 threads) **Question-scoped pending: a read is pending iff a value change is in flight for it that has not yet revealed, or it carries a live `affects()` mark.** (1) **Same-question motion is silent.** Async whose tracked inputs are value-stable — `refresh()`, polling, an action's confirm refetch — is a _re-ask of the same question_: the shown value still answers it, so `isPending` stays `false` and the fresh value reveals silently. (2) **A new question pends monotonically.** Any tracked input value change in flight (a `setSignal`, an upstream memo's new value, an optimistic write feeding downstream async) pends every read under the source until its answer reveals; **nothing can silence it** — pendingness is additive-only. (3) **Optimistic writes are verdict-inert.** An active override is the displayed value on its own slot: not pending from itself (only a held authoritative _correction_ differing from the override re-opens the verdict; a matching confirm reveals nothing), and it masks nothing — an override displaying over an in-flight new question is the honest mixed state `{ value: guess, pending: true }`. To _downstream_ async the write is a real input change and pends those slots normally. Action affordances still belong in the data (co-written flags — the A20 §1 half that survives). (4) **`affects(target, key?)` is the sole declaration verb** (single optional key since 2026-07-14; the variadic form read as a 1.x path and was dropped). Additive pending on exactly the marked data (store record → every record reachable from it at declaration time, captured proxies included per #2882, siblings untouched; key → that leaf slot; accessor → that source); **store marks — keyed and keyless — cover by raw identity, not by proxy family** (amended 2026-07-17, #2904): a keyed mark registers an identity scope (owning record's raw, narrowed to the key), so reads through any other proxy sharing that backing record — e.g. a derived optimistic store whose projection landed the source store's value — witness the mark and inherit it on nodes born during the window, exactly as keyless scopes do **and on everything derived from it** (re-ruled 2026-07-14: a mark is a synthetic in-flight change on the normal status rails — `isPending(() => derived())` reads `true` during a mark window on `derived`'s inputs, exactly as it would over real in-flight async — while the marked values themselves stay readable; **mark-only pending is value-transparent through derivation too** (amended 2026-07-14, #2886): a read whose owner's pending sources are all mark sentinels never suspends — pendingness reaches readers only through verdicts, so optimistic writes under a whole-store mark keep rendering in live tracked readers; a mark's channel is never a re-ask, so a declared reload's own `refresh()` cannot silence it, and a mark never blocks its own transaction's settlement), live from declaration until its surrounding transaction settles or reverts (ambient marks release at flush end). Four corollaries pinned by the #2893 audit (2026-07-16): **(a)** derivation coverage is transitive and probe-stable — tracked reads of mark-pended owners re-establish the mark on the reader after any mid-window recompute (including the recompute an `isPending()` probe itself triggers), at every derivation depth, for graphs built before or during the window; **(b)** mark propagation is transaction-inert — pended subscribers are not queued as pending nodes, so plain writes to marked data (value-transparency) and to unmarked data sharing a downstream memo commit and render immediately, and concurrent actions don't merge into the marker's transaction through the pend; **(c)** a real error outranks a mark — a node holding `STATUS_ERROR` neither takes a sentinel on propagation nor re-applies collected marks after its recompute, so the user's error is never clobbered by a sentinel `NotReadyError`; **(d)** the pending-source container survives any number of overlapping sources (the singular→Set migration bug stranded mark sentinels forever on the third source — exactly the keyless-store-mark-over-`mapArray` shape). The declared-reload idiom `affects(x); refresh(x)` is how process knowledge enters the verdict when the graph can't see the change yet. Trade accepted knowingly: a re-ask that happens to return different data is silent until it reveals — honest silence over blanket alarm; whoever knows declares. @@ -82,7 +82,7 @@ Sync derivations of transition-held sources are visible through `latest()`/`isPe **Status:** **ruled** — #2829 (the `[false, undefined]` pins were a regression) **Pinned by:** `tests/latest-async.test.ts`, `tests/createMemo.test.ts` -**Mechanism (index, 2026-09-14):** `latest()` shadow (`_latestValueComputed`) backfilled from the landed value (`backfillCompanion`, verdict.ts). +**Mechanism (index, 2026-09-14):** `latest()` shadow (`_latestValueComputed`) is backfilled from `_pendingValue` when created after the write (verdict.ts, #3041). After an async memo resolves, `[isPending(x), latest(x)]` is `[false, resolvedValue]` — never `[false, undefined]`. @@ -182,7 +182,7 @@ A resting optimistic node reports pending via exactly the causes a plain async m **Status:** **ruled** 2026-07-16 — maintainer rulings 2026-07-16 ("a seed… should never be visible under any case"; "we throw NotReady except in top-level component scope where we throw that other error"; "self reads are fine though — that's the point of seed, but outside isn't") and 2026-07-17 ("if the seed is visible in compute body it probably should be visible on write.. it throws on read so no consumer can rely on it") **Pinned by:** `tests/strict-read-pending-store.test.ts` (untracked dev/prod matrix); `tests/store/createProjection.async.test.ts` (seed hidden until first resolution/first yield, supersession keeps it hidden, enumeration throws); `tests/uninitialized-visibility.test.ts` (write-path seed visibility, loading-vs-pending probe #2910) -**Mechanism (index, 2026-09-14):** Derived store `STATUS_UNINITIALIZED` until first landing / first yield; `createShadowDraft`; strict-read matrix. +**Mechanism (index, 2026-09-14):** Derived store stays `STATUS_UNINITIALIZED` until first landing / first yield; the status gate in the projection traps hides the seed (projection.ts, #2988; proj R23); strict-read matrix. (**ruled 2026-07-16**, #2897) **A derived store's seed is a draft, never an observable value.** The seed exists for the derive function — self reads while it works its draft are the point — but to every outside consumer the store is _uninitialized_ (A19 exception 1: loading, not pending) until the first resolution lands; for async-iterator derives, until the **first yield** lands (uninitialized only until then — each later yield is a revealed snapshot, readable between yields even while the generator is still running). During that window every outside consumer path throws: tracked reads suspend into loading boundaries via their node's `NotReadyError` as always, and the untracked fall-throughs — property reads, `in` checks, enumeration/spread — throw the same `NotReadyError` from the firewall (in dev strictRead scopes, i.e. component bodies, the more descriptive `PENDING_ASYNC_UNTRACKED_READ` error wins, matching async memos and preventing infinite loops). Returning the seed leaked a value the reader could never observe updating; returning `undefined` would break non-nullable types. Write-path reads (reconcile enumerating during the first landing) are exempt — they _are_ the initialization. This is safeguard parity: memos already behaved this way; store proxies bypassed `read()` and with it every guard. **Write-visibility corollary (ruled 2026-07-17, #2910 follow-up): the seed IS visible to write-path consumers.** A setter's function-form argument — the store setter's draft, `prev` in `set(prev => …)` — reads the raw current state: the seed for an uninitialized derived store, `undefined` for an uninitialized optimistic computed (it has no seed argument), the displayed value once initialized. Same exemption as the derive body: writes need a base, and because every read channel throws during the window, no consumer can _rely_ on the seed — visibility on the write path leaks nothing observable. Absolute writes were never gated. From c38f15338e76b5346b2778be2796ef0a4987677a Mon Sep 17 00:00:00 2001 From: Ryan Carniato <ryansolid@gmail.com> Date: Mon, 14 Sep 2026 10:38:23 -0700 Subject: [PATCH 5/7] =?UTF-8?q?docs(signals):=20A18=20=E2=80=94=20retitle?= =?UTF-8?q?=20to=20the=20live=20rule;=20split=20statement=20from=20superse?= =?UTF-8?q?ded=20mechanism?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The title "arriving truth holds until commit" restated the 2026-07-07b mechanical model (hold in _pendingValue, elevate at commit, unobservable under the mask) — the sentence the 2026-09-09 re-rule (#3331) explicitly replaced. Live rule: a newer truth from the source removes the optimism from the graph immediately; only the display keeps the override until the transaction commits. The section now leads with **Statement (current)** — lifetime bound to the own transaction, the #3331 supersession, 09-10 scope and provenance, consequences, store/node ownership corollaries, pins — and keeps the 07-07b mechanical sentence and the formulation it replaced under **History (superseded mechanism, kept verbatim)**. Every token of the original proposition is present (asserted); nothing reworded. Status and Mechanism lines updated to lead with the supersession machinery. Co-authored-by: Cursor <cursoragent@cursor.com> --- packages/signals/docs/RULES-INDEX.md | 76 +++++++++---------- packages/signals/docs/SPEC-ASYNC-SEMANTICS.md | 10 ++- 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/packages/signals/docs/RULES-INDEX.md b/packages/signals/docs/RULES-INDEX.md index 6cdbd4042..95b5a7e8a 100644 --- a/packages/signals/docs/RULES-INDEX.md +++ b/packages/signals/docs/RULES-INDEX.md @@ -45,58 +45,58 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul | id | status | defined | cited in src | cited in tests | statement (at definition) | |---|---|---|---|---|---| -| A1 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:191` | — | 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:199` | — | 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:207` | — | — | [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:215` | — | — | [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:223` | — | — | [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:231` | — | — | [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 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:81` | — | spec-async-semantics.test.ts×2 | [ruled] Resolved async never reads `[false, undefined]` — After an async memo resolves, `[isPending(x), latest(x)]` is `[false, resolvedValue]` — never `[false, undefined]`. | -| A8 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:89` | — | — | [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:97` | — | spec-async-semantics.test.ts×3 | [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:105` | invariants.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:47` | — | — | [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:113` | — | — | [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:121` | 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:129` | — | 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 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:163` | core.ts×2 lanes.ts×1 scheduler.ts×1 | lane-hold-on-observation.test.ts×1 reveal-carve-out.test.ts×2 spec-async-semantics.test.ts×2 stale-read-uninitialized-cross-transition.test.ts×1 treeshake.test.ts×3 | [ruled 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 is observed by a s… | -| A16 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:137` | verdict.ts×1 | spec-async-semantics.test.ts×1 strict-read-pending-store.test.ts×2 uninitialized-visibility.test.ts×1 | [ruled 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 uninitialized asy… | +| A1 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:193` | — | 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:201` | — | 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:209` | — | — | [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:217` | — | — | [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:225` | — | — | [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:233` | — | — | [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 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:83` | — | spec-async-semantics.test.ts×2 | [ruled] Resolved async never reads `[false, undefined]` — After an async memo resolves, `[isPending(x), latest(x)]` is `[false, resolvedValue]` — never `[false, undefined]`. | +| A8 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:91` | — | — | [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:99` | — | spec-async-semantics.test.ts×3 | [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:107` | invariants.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:49` | — | — | [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:115` | — | — | [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:123` | 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:131` | — | 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 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:165` | core.ts×2 lanes.ts×1 scheduler.ts×1 | lane-hold-on-observation.test.ts×1 reveal-carve-out.test.ts×2 spec-async-semantics.test.ts×2 stale-read-uninitialized-cross-transition.test.ts×1 treeshake.test.ts×3 | [ruled 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 is observed by a s… | +| A16 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:139` | verdict.ts×1 | spec-async-semantics.test.ts×1 strict-read-pending-store.test.ts×2 uninitialized-visibility.test.ts×1 | [ruled 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 uninitialized asy… | | A17 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:31` | async.ts×3 constants.ts×2 core.ts×7 invariants.ts×3 optimistic.ts×3 scheduler.ts×2 verdict.ts×1 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 | [ruled 2026-07-06 (promoted from C4)] An active override is THE value for every read — (was C4) An _active_ optimistic override is THE value for every **read** — ambient/untracked and tracked alike — … | -| A18 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:39` | async.ts×2 constants.ts×1 core.ts×4 optimistic.ts×3 scheduler.ts×2 types.ts×2 optimistic.ts×1 | createOptimistic.test.ts×1 spec-async-semantics.test.ts×3 flight-owned-transaction.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from B4)] An override's lifetime is bound to its own transition; arriving truth holds until commit — (was B4; **refined by re-rule 2026-07-07b**) An overr… | -| A19 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:65` | async.ts×1 optimistic.ts×1 | spec-async-semantics.test.ts×3 uninitialized-visibility.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:243` | 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:250` | — | 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:145` | — | spec-async-semantics.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:153` | — | 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:73` | — | optimistic-undefined-override.test.ts×1 reveal-gating-contract.test.ts×1 spec-async-semantics.test.ts×2 | [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:181` | — | uninitialized-visibility.test.ts×3 | [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:55` | scheduler.ts×1 | action-await-contract.test.ts×2 | [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:173` | — | — | [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… | +| A18 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:39` | async.ts×2 constants.ts×1 core.ts×4 optimistic.ts×3 scheduler.ts×2 types.ts×2 optimistic.ts×1 | createOptimistic.test.ts×1 spec-async-semantics.test.ts×3 flight-owned-transaction.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:67` | async.ts×1 optimistic.ts×1 | spec-async-semantics.test.ts×3 uninitialized-visibility.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:245` | 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:252` | — | 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:147` | — | spec-async-semantics.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:155` | — | 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:75` | — | optimistic-undefined-override.test.ts×1 reveal-gating-contract.test.ts×1 spec-async-semantics.test.ts×2 | [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:183` | — | uninitialized-visibility.test.ts×3 | [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:57` | scheduler.ts×1 | action-await-contract.test.ts×2 | [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:175` | — | — | [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… | ## V — fixed violations | id | status | defined | cited in src | cited in tests | statement (at definition) | |---|---|---|---|---|---| -| V1 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:313` | 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:323` | 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:329` | — | spec-async-semantics.test.ts×2 | - **V3 (violated A19) — FIXED.** After a reporter-less transition completed, | -| V4 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:336` | — | 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:348` | — | spec-async-semantics.test.ts×3 | - **V5 (A17 corollary — found and fixed with the revert-target elimination, | +| V1 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:315` | 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:325` | 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:331` | — | spec-async-semantics.test.ts×2 | - **V3 (violated A19) — FIXED.** After a reporter-less transition completed, | +| V4 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:338` | — | 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:350` | — | 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:121` | — | 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:129` | — | 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:163` | — | spec-async-semantics.test.ts×2 | PROMOTED → A15 (A15's section carries the ruling). | +| B1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:123` | — | 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:131` | — | 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:165` | — | spec-async-semantics.test.ts×2 | PROMOTED → A15 (A15's section carries the ruling). | | B4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:39` | — | spec-async-semantics.test.ts×2 | PROMOTED → A18 (A18's section carries the ruling). | -| B5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:137` | — | spec-async-semantics.test.ts×2 | PROMOTED → A16 (A16's section carries the ruling). | +| B5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:139` | — | 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:65` | — | 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:279` | — | 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:289` | — | — | - [x] **C3 — CLOSED by A19 (2026-07-07): early completion is by design.** | +| C1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:67` | — | 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:281` | — | 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:291` | — | — | - [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 diff --git a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md index 94f59481d..5326eb12b 100644 --- a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md +++ b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md @@ -36,13 +36,15 @@ The former Tier A table is these sections. Tier B/C, the fixed violations, and t (was C4) An _active_ optimistic override is THE value for every **read** — ambient/untracked and tracked alike — regardless of transition entanglement. "It is the optimistic future value... it is both immediate and is the future until we know otherwise." "Knowing otherwise" is its own async source resolving (see A18); a transition whose optimistic node is still pending on its own fetch is not complete, so the override cannot be dropped early. **No-tearing is an effect-level concern, not a read-level one**: when async _derived from_ the optimistic value is in flight, the lane holds its render effects (the rendered view keeps the committed state as a unit) — but direct reads still return the override ("direct read shows optimistic, effect waits"). Do NOT mask the override from any read path to prevent tearing; that breaks the real-world optimistic-UI contract. **Amended 2026-09-09 (#3331):** "until we know otherwise" is the landing of the node's own async, and knowing otherwise splits the readers: from that landing on, the override is the value for the _display_ — untracked/ambient reads and the applied frame — until the transaction commits, while tracked derivations (memos, async drivers, lane recomputes) see the arrived truth (A18 supersession). `latest(x)` returns the arrived value; `isPending(x)` is `true` iff it differs from the override. -### A18. An override's lifetime is bound to its own transition; arriving truth holds until commit +### A18. 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 commit -**Status:** **ruled, amended in place** 2026-07-07 (promoted from B4) — maintainer rulings, 2026-07-07 (original) and 2026-07-07b (re-rule: "the non-blocking aspect… only gates the reveal"; `_value` elevation at commit points) +**Status:** **ruled, amended in place** 2026-07-07 (promoted from B4) — maintainer rulings, 2026-07-07 (original) and 2026-07-07b (re-rule: "the non-blocking aspect… only gates the reveal"); mechanism re-ruled 2026-09-09 (#3331, supersession: "a new value from the source should remove the optimism immediately") with scope and provenance ruled 2026-09-10; store/node ownership corollaries 2026-07-17/18 (#2899, #2912) **Pinned by:** `tests/spec-async-semantics.test.ts` (same pins — behavior coincides in unmerged graphs); `tests/optimistic-store-layer-scope.test.ts` (store corollary: disjoint-key independence, nested rows, same-key entanglement, delete survival, ambient flush-end); `tests/optimistic-lane-transaction-ownership.test.ts` (node corollary: shared-subscriber lane merge with swapped write order, three-action signal hijack) -**Mechanism (index, 2026-09-14):** `_pendingValue` hold under an override, elevation in `commitPendingNode`; `CONFIG_OVERRIDE_SUPERSEDED` + `supersedeOverride` / `supersededRead` (#3331); `_overrideTime`, `_overrideStamp` provenance. +**Mechanism (index, 2026-09-14):** `CONFIG_OVERRIDE_SUPERSEDED` set by `supersedeOverride` on a differing, postdating source landing; `supersededRead` serves the arrived value to tracked readers while untracked reads keep `_overrideValue` until commit (#3331); `_overrideTime` / `_overrideStamp` decide "postdates" and provenance (2026-09-10); `_overrideOwner` answers ownership instead of the lane (#2912); the arrival itself is staged in `_pendingValue` and elevated by `commitPendingNode` like any transition write. -(was B4; **refined by re-rule 2026-07-07b**) An override's lifetime is bound to **its own transition** — which, because lanes keep their transitions separate from unrelated work, contains exactly the override's own async cascade. Mechanically: authoritative values arriving under an active override hold in `_pendingValue` like any other transition write and **elevate to `_value` only at their transition's commit** (`_value` changes at commit points, period); the elevation is unobservable under the override mask (A17); reverting is a pure drop — there is no revert target and reverts commit nothing. Consequences: (1) in unmerged graphs, own-source resolution IS the lane-transition's completion, so the correction reveals on arrival — the original A18 pins hold unchanged; (2) matching confirmations collapse silently (revert sees value == override, nobody re-runs); (3) when the override's transition genuinely merges with unrelated async, the correction reveals atomically with that merged completion — verdict during the window per A24 (amended 2026-07-13; was "false throughout" under the A20 mask): a held correction that _differs_ from the displayed override reads pending; a matching confirm stays quiet — corrections still _propagate_ internally on arrival (fresh readers/async drivers see the hold), so downstream refetches start immediately and no waterfalls form; only the reveal is gated. This supersedes the earlier "bound to its own async source, not its transition" formulation, which was implemented by escaping the transition commit (revert-target commit at revert) and allowed a mid-flight arrival to reveal before its own transition completed. **Store corollary (2026-07-17, #2899): the optimistic layer obeys the same per-transaction lifetime.** `createOptimisticStore`'s override layer is one record per store target, but each entry is owned by the transaction that wrote it (`STORE_OPTIMISTIC_OWNERS` stamps, merge chains resolved): a settling action consumes only its own keys, so concurrent actions on disjoint keys revert independently — first-settling no longer wipes the other's live overrides. Same-key writes still entangle through the shared node (one joint settle); ambient (transaction-less) entries clear at plain flush end; a derived store's projection landing still consumes the whole layer (fresh authority supersedes every tentative write). **Node corollary (2026-07-18, #2912): ownership never travels through lanes.** Lanes are scheduling affinity — a shared subscriber (one effect reading keys touched by two actions) merges them correctly for flushing, but the merged root's `_transition` must not answer "which transaction owns this override": that let one action's settle revert another's live override, and same-key follow-up writes entangle with the wrong transaction. Every optimistic write stamps `_overrideOwner` on the node (post-merge, so entangled writers share the joint root; cleared at settle); `resolveTransition` prefers a live owner stamp over the lane, falling back to lane `_transition` for nodes without overrides (async routing) exactly as before. **Supersession (re-ruled 2026-09-09, #3331): own-source arrival removes the optimism from the graph immediately; the display keeps it until the transaction commits.** Maintainer: "a new value from the source should remove the optimism immediately.. if it matches then no more work, if it doesn't match then that work gets folded into the parent transition"; "when the optimism drops we might not see it until end of transition because it folds into the parent's transition." This replaces the mechanical sentence above ("elevate to `_value` only at their transition's commit; the elevation is unobservable under the override mask") — that model let the override's own downstream flight serialize ahead of the truth's, doubling the delay the reporter saw. Now: (a) a landing that _equals_ the override confirms silently — nothing re-runs, the lane's in-flight work completes the frame; (b) a landing that _differs_ marks the node superseded: its subscribers recompute from the arrived value on the plain channel (their lane affinity is dropped, so this is held transaction work, not lane work), downstream async restarts from the truth _now_, and the override's own downstream flight is inert when it lands; (c) untracked reads and the applied screen keep the override until the transaction — holding for whatever the corrected derivations observe (A15) — commits and clears the override; (d) `latest` returns the arrived value, `isPending` reads `true` iff the arrival differs (consequence (3) unchanged in statement, now true in mechanism). A later landing on the same node that equals the override un-supersedes it (the override is again the graph's value). **Scope (ruled 2026-09-10): "the source" is whatever recomputes the node** — its own async landing, or a synchronous recompute driven by an upstream change (`createOptimistic(() => userCategory())` over an async memo is the common real-world shape): "if the source recomputes it doesn't matter if it is async or not." **Ordering:** a new value from the source is one that _postdates_ the override — a source write and an override in the same batch derive nothing new (the override is written over that batch's truth knowingly and stays the graph's value until the commit reveals it). **Provenance (ruled 2026-09-10):** "a new value from the source" answers the override's _own_ question or a newer one. Two rapid actions on one node merge into one transaction, and the older action's refetch can land after the newer override; that answer is a question the user has since changed — it is staged for the commit like any landing (and reveals then iff it is still the truth) but does **not** supersede: no downstream re-derivation, no pending flip on downstream readers. "A slow source shouldn't leak back in like that." Only the override's own action, a later action, or mainline (no action — a fresh question by definition) supersedes. Pinned: `tests/spec-async-semantics.test.ts` ("#3331" describe: own-async, sync-wrapper, same-batch, provenance, simple graph; A18 entangled pin re-expected: the merged correction reveals as one frame, never the committed-behind-the-mask tear); `tests/createOptimistic.test.ts` (CategoryDisplay no-double-flicker pin, unchanged: the older action's answer never moves the graph; "second action while first still in flight" pin, resolver repaired and re-expected to the same rule). +**Statement (current).** (was B4; **refined by re-rule 2026-07-07b**) An override's lifetime is bound to **its own transition** — which, because lanes keep their transitions separate from unrelated work, contains exactly the override's own async cascade. **Supersession (re-ruled 2026-09-09, #3331): own-source arrival removes the optimism from the graph immediately; the display keeps it until the transaction commits.** Maintainer: "a new value from the source should remove the optimism immediately.. if it matches then no more work, if it doesn't match then that work gets folded into the parent transition"; "when the optimism drops we might not see it until end of transition because it folds into the parent's transition." This replaces the mechanical sentence above ("elevate to `_value` only at their transition's commit; the elevation is unobservable under the override mask") — that model let the override's own downstream flight serialize ahead of the truth's, doubling the delay the reporter saw. Now: (a) a landing that _equals_ the override confirms silently — nothing re-runs, the lane's in-flight work completes the frame; (b) a landing that _differs_ marks the node superseded: its subscribers recompute from the arrived value on the plain channel (their lane affinity is dropped, so this is held transaction work, not lane work), downstream async restarts from the truth _now_, and the override's own downstream flight is inert when it lands; (c) untracked reads and the applied screen keep the override until the transaction — holding for whatever the corrected derivations observe (A15) — commits and clears the override; (d) `latest` returns the arrived value, `isPending` reads `true` iff the arrival differs (consequence (3) unchanged in statement, now true in mechanism). A later landing on the same node that equals the override un-supersedes it (the override is again the graph's value). **Scope (ruled 2026-09-10): "the source" is whatever recomputes the node** — its own async landing, or a synchronous recompute driven by an upstream change (`createOptimistic(() => userCategory())` over an async memo is the common real-world shape): "if the source recomputes it doesn't matter if it is async or not." **Ordering:** a new value from the source is one that _postdates_ the override — a source write and an override in the same batch derive nothing new (the override is written over that batch's truth knowingly and stays the graph's value until the commit reveals it). **Provenance (ruled 2026-09-10):** "a new value from the source" answers the override's _own_ question or a newer one. Two rapid actions on one node merge into one transaction, and the older action's refetch can land after the newer override; that answer is a question the user has since changed — it is staged for the commit like any landing (and reveals then iff it is still the truth) but does **not** supersede: no downstream re-derivation, no pending flip on downstream readers. "A slow source shouldn't leak back in like that." Only the override's own action, a later action, or mainline (no action — a fresh question by definition) supersedes. Consequences: (1) in unmerged graphs, own-source resolution IS the lane-transition's completion, so the correction reveals on arrival — the original A18 pins hold unchanged; (2) matching confirmations collapse silently (revert sees value == override, nobody re-runs); (3) when the override's transition genuinely merges with unrelated async, the correction reveals atomically with that merged completion — verdict during the window per A24 (amended 2026-07-13; was "false throughout" under the A20 mask): a held correction that _differs_ from the displayed override reads pending; a matching confirm stays quiet — corrections still _propagate_ internally on arrival (fresh readers/async drivers see the hold), so downstream refetches start immediately and no waterfalls form; only the reveal is gated. **Store corollary (2026-07-17, #2899): the optimistic layer obeys the same per-transaction lifetime.** `createOptimisticStore`'s override layer is one record per store target, but each entry is owned by the transaction that wrote it (`STORE_OPTIMISTIC_OWNERS` stamps, merge chains resolved): a settling action consumes only its own keys, so concurrent actions on disjoint keys revert independently — first-settling no longer wipes the other's live overrides. Same-key writes still entangle through the shared node (one joint settle); ambient (transaction-less) entries clear at plain flush end; a derived store's projection landing still consumes the whole layer (fresh authority supersedes every tentative write). **Node corollary (2026-07-18, #2912): ownership never travels through lanes.** Lanes are scheduling affinity — a shared subscriber (one effect reading keys touched by two actions) merges them correctly for flushing, but the merged root's `_transition` must not answer "which transaction owns this override": that let one action's settle revert another's live override, and same-key follow-up writes entangle with the wrong transaction. Every optimistic write stamps `_overrideOwner` on the node (post-merge, so entangled writers share the joint root; cleared at settle); `resolveTransition` prefers a live owner stamp over the lane, falling back to lane `_transition` for nodes without overrides (async routing) exactly as before. Pinned: `tests/spec-async-semantics.test.ts` ("#3331" describe: own-async, sync-wrapper, same-batch, provenance, simple graph; A18 entangled pin re-expected: the merged correction reveals as one frame, never the committed-behind-the-mask tear); `tests/createOptimistic.test.ts` (CategoryDisplay no-double-flicker pin, unchanged: the older action's answer never moves the graph; "second action while first still in flight" pin, resolver repaired and re-expected to the same rule). + +**History (superseded mechanism, kept verbatim).** The 2026-07-07b mechanical model — replaced by the 2026-09-09 supersession, which the statement now leads with; take the mechanism from the statement, not from here: Mechanically: authoritative values arriving under an active override hold in `_pendingValue` like any other transition write and **elevate to `_value` only at their transition's commit** (`_value` changes at commit points, period); the elevation is unobservable under the override mask (A17); reverting is a pure drop — there is no revert target and reverts commit nothing. This supersedes the earlier "bound to its own async source, not its transition" formulation, which was implemented by escaping the transition commit (revert-target commit at revert) and allowed a mid-flight arrival to reveal before its own transition completed. ### A11. Sync derivations of held sources are visible through `latest()`/`isPending()` From f06d1f5ebcb86451836bfea6558f2123df6d6b09 Mon Sep 17 00:00:00 2001 From: Ryan Carniato <ryansolid@gmail.com> Date: Mon, 14 Sep 2026 10:45:01 -0700 Subject: [PATCH 6/7] =?UTF-8?q?docs(signals):=20A17=20=E2=80=94=20lead=20w?= =?UTF-8?q?ith=20the=20post-#3331=20split;=20A22=20=E2=80=94=20mark=20the?= =?UTF-8?q?=20dead=20A21=20clause?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A17's body opened with the 2026-07-06 wording ("THE value for every read — ambient/untracked and tracked alike"; "do NOT mask the override from any read path") and appended the 2026-09-09 amendment (#3331) that contradicts the tracked half: after the node's own source lands, tracked derivations see the arrived truth and only the display keeps the override. Same failure as A18's title. The section now leads with Statement (current) — the #3331 split, then the still-true 07-06 reasoning (the future until we know otherwise; direct read shows optimistic, effect waits) — and keeps the two replaced sentences verbatim under History. Retitled from "THE value for every read" to the display/graph split. Status and Mechanism updated. Every token preserved (asserted). A22 cited "the decree that silences it (A21)" as live; A21 is superseded by A24 and nothing silences a new question. Annotated in place so the sentence's A9 half reads as the only live part. Co-authored-by: Cursor <cursoragent@cursor.com> --- packages/signals/docs/RULES-INDEX.md | 80 +++++++++---------- packages/signals/docs/SPEC-ASYNC-SEMANTICS.md | 12 +-- 2 files changed, 47 insertions(+), 45 deletions(-) diff --git a/packages/signals/docs/RULES-INDEX.md b/packages/signals/docs/RULES-INDEX.md index 95b5a7e8a..7319c8c93 100644 --- a/packages/signals/docs/RULES-INDEX.md +++ b/packages/signals/docs/RULES-INDEX.md @@ -45,58 +45,58 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul | id | status | defined | cited in src | cited in tests | statement (at definition) | |---|---|---|---|---|---| -| A1 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:193` | — | 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:201` | — | 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:209` | — | — | [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:217` | — | — | [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:225` | — | — | [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:233` | — | — | [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 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:83` | — | spec-async-semantics.test.ts×2 | [ruled] Resolved async never reads `[false, undefined]` — After an async memo resolves, `[isPending(x), latest(x)]` is `[false, resolvedValue]` — never `[false, undefined]`. | -| A8 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:91` | — | — | [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:99` | — | spec-async-semantics.test.ts×3 | [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:107` | invariants.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:49` | — | — | [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:115` | — | — | [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:123` | 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:131` | — | 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 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:165` | core.ts×2 lanes.ts×1 scheduler.ts×1 | lane-hold-on-observation.test.ts×1 reveal-carve-out.test.ts×2 spec-async-semantics.test.ts×2 stale-read-uninitialized-cross-transition.test.ts×1 treeshake.test.ts×3 | [ruled 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 is observed by a s… | -| A16 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:139` | verdict.ts×1 | spec-async-semantics.test.ts×1 strict-read-pending-store.test.ts×2 uninitialized-visibility.test.ts×1 | [ruled 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 uninitialized asy… | -| A17 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:31` | async.ts×3 constants.ts×2 core.ts×7 invariants.ts×3 optimistic.ts×3 scheduler.ts×2 verdict.ts×1 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 | [ruled 2026-07-06 (promoted from C4)] An active override is THE value for every read — (was C4) An _active_ optimistic override is THE value for every **read** — ambient/untracked and tracked alike — … | -| A18 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:39` | async.ts×2 constants.ts×1 core.ts×4 optimistic.ts×3 scheduler.ts×2 types.ts×2 optimistic.ts×1 | createOptimistic.test.ts×1 spec-async-semantics.test.ts×3 flight-owned-transaction.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:67` | async.ts×1 optimistic.ts×1 | spec-async-semantics.test.ts×3 uninitialized-visibility.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:245` | 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:252` | — | 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:147` | — | spec-async-semantics.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:155` | — | 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:75` | — | optimistic-undefined-override.test.ts×1 reveal-gating-contract.test.ts×1 spec-async-semantics.test.ts×2 | [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:183` | — | uninitialized-visibility.test.ts×3 | [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:57` | scheduler.ts×1 | action-await-contract.test.ts×2 | [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:175` | — | — | [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… | +| A1 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:195` | — | 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:203` | — | 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:211` | — | — | [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:219` | — | — | [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:227` | — | — | [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:235` | — | — | [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 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:85` | — | spec-async-semantics.test.ts×2 | [ruled] Resolved async never reads `[false, undefined]` — After an async memo resolves, `[isPending(x), latest(x)]` is `[false, resolvedValue]` — never `[false, undefined]`. | +| A8 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:93` | — | — | [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:101` | — | spec-async-semantics.test.ts×3 | [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:109` | invariants.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:51` | — | — | [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:117` | — | — | [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:125` | 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:133` | — | 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 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:167` | core.ts×2 lanes.ts×1 scheduler.ts×1 | lane-hold-on-observation.test.ts×1 reveal-carve-out.test.ts×2 spec-async-semantics.test.ts×2 stale-read-uninitialized-cross-transition.test.ts×1 treeshake.test.ts×3 | [ruled 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 is observed by a s… | +| A16 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:141` | verdict.ts×1 | spec-async-semantics.test.ts×1 strict-read-pending-store.test.ts×2 uninitialized-visibility.test.ts×1 | [ruled 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 uninitialized asy… | +| A17 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:31` | async.ts×3 constants.ts×2 core.ts×7 invariants.ts×3 optimistic.ts×3 scheduler.ts×2 verdict.ts×1 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 | [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×2 constants.ts×1 core.ts×4 optimistic.ts×3 scheduler.ts×2 types.ts×2 optimistic.ts×1 | createOptimistic.test.ts×1 spec-async-semantics.test.ts×3 flight-owned-transaction.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:69` | async.ts×1 optimistic.ts×1 | spec-async-semantics.test.ts×3 uninitialized-visibility.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:247` | 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:254` | — | 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:149` | — | spec-async-semantics.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:157` | — | 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:77` | — | optimistic-undefined-override.test.ts×1 reveal-gating-contract.test.ts×1 spec-async-semantics.test.ts×2 | [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:185` | — | uninitialized-visibility.test.ts×3 | [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:59` | scheduler.ts×1 | action-await-contract.test.ts×2 | [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:177` | — | — | [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… | ## V — fixed violations | id | status | defined | cited in src | cited in tests | statement (at definition) | |---|---|---|---|---|---| -| V1 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:315` | 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:325` | 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:331` | — | spec-async-semantics.test.ts×2 | - **V3 (violated A19) — FIXED.** After a reporter-less transition completed, | -| V4 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:338` | — | 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:350` | — | spec-async-semantics.test.ts×3 | - **V5 (A17 corollary — found and fixed with the revert-target elimination, | +| V1 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:317` | 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:327` | 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:333` | — | spec-async-semantics.test.ts×2 | - **V3 (violated A19) — FIXED.** After a reporter-less transition completed, | +| V4 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:340` | — | 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:352` | — | 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:123` | — | 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:131` | — | 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:165` | — | spec-async-semantics.test.ts×2 | PROMOTED → A15 (A15's section carries the ruling). | -| B4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:39` | — | spec-async-semantics.test.ts×2 | PROMOTED → A18 (A18's section carries the ruling). | -| B5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:139` | — | spec-async-semantics.test.ts×2 | PROMOTED → A16 (A16's section carries the ruling). | +| B1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:125` | — | 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:133` | — | 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:167` | — | 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:141` | — | 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:67` | — | 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:281` | — | 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:291` | — | — | - [x] **C3 — CLOSED by A19 (2026-07-07): early completion is by design.** | +| C1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:69` | — | 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:283` | — | 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:293` | — | — | - [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 diff --git a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md index 5326eb12b..836615448 100644 --- a/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md +++ b/packages/signals/docs/SPEC-ASYNC-SEMANTICS.md @@ -28,13 +28,15 @@ The former Tier A table is these sections. Tier B/C, the fixed violations, and t ## Reads and visibility — what a read serves -### A17. An active override is THE value for every read +### A17. An active override is the displayed value until its transaction commits, and the graph's value until its own source answers -**Status:** **ruled** 2026-07-06 (promoted from C4) — maintainer ruling, 2026-07-06/07 +**Status:** **ruled, amended in place** 2026-07-06 (promoted from C4) — maintainer ruling, 2026-07-06/07; amended 2026-09-09 (#3331: "knowing otherwise" splits display from tracked derivations — see A18 supersession) **Pinned by:** `tests/spec-async-semantics.test.ts`; downstream-async lane holding: `tests/createOptimistic.test.ts` (CategoryDisplay/News-Finance real-world sections) -**Mechanism (index, 2026-09-14):** `_overrideValue` + `hasActiveOverride`; value selection in `read` / `readNodeFast` / store `serveDataKey`; authoritative-view carve-out for `until()` (`CONFIG_AUTHORITATIVE_READ`); held-truth mask `CONFIG_HELD_TRUTH` (#3164). +**Mechanism (index, 2026-09-14):** `_overrideValue` + `hasActiveOverride`; value selection in `read` / `readNodeFast` / store `serveDataKey`; authoritative-view carve-out for `until()` (`CONFIG_AUTHORITATIVE_READ`); held-truth mask `CONFIG_HELD_TRUTH` (#3164); after the own-source landing `supersededRead` routes tracked readers to the arrived value while untracked reads keep `_overrideValue` (`CONFIG_OVERRIDE_SUPERSEDED`, #3331). -(was C4) An _active_ optimistic override is THE value for every **read** — ambient/untracked and tracked alike — regardless of transition entanglement. "It is the optimistic future value... it is both immediate and is the future until we know otherwise." "Knowing otherwise" is its own async source resolving (see A18); a transition whose optimistic node is still pending on its own fetch is not complete, so the override cannot be dropped early. **No-tearing is an effect-level concern, not a read-level one**: when async _derived from_ the optimistic value is in flight, the lane holds its render effects (the rendered view keeps the committed state as a unit) — but direct reads still return the override ("direct read shows optimistic, effect waits"). Do NOT mask the override from any read path to prevent tearing; that breaks the real-world optimistic-UI contract. **Amended 2026-09-09 (#3331):** "until we know otherwise" is the landing of the node's own async, and knowing otherwise splits the readers: from that landing on, the override is the value for the _display_ — untracked/ambient reads and the applied frame — until the transaction commits, while tracked derivations (memos, async drivers, lane recomputes) see the arrived truth (A18 supersession). `latest(x)` returns the arrived value; `isPending(x)` is `true` iff it differs from the override. +**Statement (current).** (was C4) **Amended 2026-09-09 (#3331):** "until we know otherwise" is the landing of the node's own async, and knowing otherwise splits the readers: from that landing on, the override is the value for the _display_ — untracked/ambient reads and the applied frame — until the transaction commits, while tracked derivations (memos, async drivers, lane recomputes) see the arrived truth (A18 supersession). `latest(x)` returns the arrived value; `isPending(x)` is `true` iff it differs from the override. "It is the optimistic future value... it is both immediate and is the future until we know otherwise." "Knowing otherwise" is its own async source resolving (see A18); a transition whose optimistic node is still pending on its own fetch is not complete, so the override cannot be dropped early. **No-tearing is an effect-level concern, not a read-level one**: when async _derived from_ the optimistic value is in flight, the lane holds its render effects (the rendered view keeps the committed state as a unit) — but direct reads still return the override ("direct read shows optimistic, effect waits"). + +**History (superseded formulation, kept verbatim).** The 2026-07-06 wording — its "tracked alike" and "any read path" clauses are replaced by the 2026-09-09 amendment the statement now leads with; the display half of both sentences still holds: An _active_ optimistic override is THE value for every **read** — ambient/untracked and tracked alike — regardless of transition entanglement. Do NOT mask the override from any read path to prevent tearing; that breaks the real-world optimistic-UI contract. ### A18. 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 commit @@ -150,7 +152,7 @@ A resting optimistic node reports pending via exactly the causes a plain async m **Pinned by:** A22 describe in `tests/spec-async-semantics.test.ts` **Mechanism (index, 2026-09-14):** Per-leaf `_pendingSignal`s; firewall `_parentSource`; no store-wide mask (A21 superseded). -**Pending is per-node: store-wide verdicts exist only as the firewall's own in-flight work (A9) and the decree that silences it (A21).** Every other A19 cause lives on the individual node — a transition-held write to a plain store pends exactly the touched leaves (untouched siblings and proxy-level reads stay settled: the writer's change set is known, unlike a refetching authority's unbounded one), manual projection writes pend the written leaf only, holds outliving a settled firewall stay leaf-local. Direction of flow: a node's verdict never inherits its _consumers'_ in-flight state — once a fetch commits, leaves show the landed value and read settled immediately, even while downstream async still holds the effect-level reveal (the commit is immediate at the data level; only the visual is lane-held, and `isPending` companions probe from their own lane, A14, so they are not fooled by the hold). +**Pending is per-node: store-wide verdicts exist only as the firewall's own in-flight work (A9) and the decree that silences it (A21 — **superseded 2026-07-13 by A24**; no decree survives, so of this sentence only the A9 half is live).** Every other A19 cause lives on the individual node — a transition-held write to a plain store pends exactly the touched leaves (untouched siblings and proxy-level reads stay settled: the writer's change set is known, unlike a refetching authority's unbounded one), manual projection writes pend the written leaf only, holds outliving a settled firewall stay leaf-local. Direction of flow: a node's verdict never inherits its _consumers'_ in-flight state — once a fetch commits, leaves show the landed value and read settled immediately, even while downstream async still holds the effect-level reveal (the commit is immediate at the data level; only the visual is lane-held, and `isPending` companions probe from their own lane, A14, so they are not fooled by the hold). ### A23. The `isPending` probe is reads-only From 6ba9bc2fe8edff4469cad6b1d4820ffaf019abca Mon Sep 17 00:00:00 2001 From: Ryan Carniato <ryansolid@gmail.com> Date: Mon, 14 Sep 2026 11:05:44 -0700 Subject: [PATCH 7/7] test(signals): every live A-rule is cited by a test; pin A4 directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second gate in scripts/rules-index.mjs --check (run by tests/rules-index.test.ts): every A-rule not superseded must be cited by ID from at least one test file. The spec's "Pinned by" column was prose — a renamed or deleted pin was caught by nothing. Nine A-rules had pins that never carried the ID (A3 A4 A5 A6 A8 A10 A11 A12 A27); the describe/it names of those pins now do. A10 was cited in src/ and by no test, which the earlier "uncited anywhere" list missed. A4 (a custom equals is never invoked with undefined prev on first commit) was only covered implicitly — every comparator in equals-comparator-errors dereferences prev unguarded and the first commit succeeds. It now has a direct pin that records the comparator's calls: none on first commit, one with the committed prev on the next write. Co-authored-by: Cursor <cursoragent@cursor.com> --- packages/signals/docs/RULES-INDEX.md | 20 +++++++------- packages/signals/scripts/rules-index.mjs | 16 +++++++++++- packages/signals/tests/createMemo.test.ts | 4 +-- .../signals/tests/createOptimistic.test.ts | 4 +-- .../tests/enforceLoadingBoundary.test.ts | 2 +- .../tests/equals-comparator-errors.test.ts | 26 ++++++++++++++++++- packages/signals/tests/errorHalt.test.ts | 2 +- .../latest-isPending-consistency.test.ts | 4 +-- packages/signals/tests/loading-value.test.ts | 4 +-- packages/signals/tests/rules-index.test.ts | 9 +++++++ .../tests/spec-async-semantics.test.ts | 2 +- 11 files changed, 70 insertions(+), 23 deletions(-) diff --git a/packages/signals/docs/RULES-INDEX.md b/packages/signals/docs/RULES-INDEX.md index 7319c8c93..816056e74 100644 --- a/packages/signals/docs/RULES-INDEX.md +++ b/packages/signals/docs/RULES-INDEX.md @@ -22,7 +22,7 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul | vocabulary | rules | cited in src | cited in tests | cited nowhere | |---|---|---|---|---| -| A | 27 | 9 | 18 | 8 | +| A | 27 | 9 | 27 | 0 | | V | 5 | 2 | 5 | 0 | | B | 5 | 0 | 5 | 0 | | C | 4 | 0 | 3 | 1 | @@ -47,16 +47,16 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul |---|---|---|---|---|---| | A1 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:195` | — | 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:203` | — | 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:211` | — | — | [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:219` | — | — | [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:227` | — | — | [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:235` | — | — | [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. | +| A3 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:211` | — | 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:219` | — | 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:227` | — | 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:235` | — | 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 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:85` | — | spec-async-semantics.test.ts×2 | [ruled] Resolved async never reads `[false, undefined]` — After an async memo resolves, `[isPending(x), latest(x)]` is `[false, resolvedValue]` — never `[false, undefined]`. | -| A8 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:93` | — | — | [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… | +| A8 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:93` | — | createMemo.test.ts×1 | [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:101` | — | spec-async-semantics.test.ts×3 | [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:109` | invariants.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:51` | — | — | [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:117` | — | — | [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… | +| A10 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:109` | invariants.ts×1 | createMemo.test.ts×1 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:51` | — | latest-isPending-consistency.test.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:117` | — | 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:125` | 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:133` | — | 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 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:167` | core.ts×2 lanes.ts×1 scheduler.ts×1 | lane-hold-on-observation.test.ts×1 reveal-carve-out.test.ts×2 spec-async-semantics.test.ts×2 stale-read-uninitialized-cross-transition.test.ts×1 treeshake.test.ts×3 | [ruled 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 is observed by a s… | @@ -71,7 +71,7 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul | A24 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:77` | — | optimistic-undefined-override.test.ts×1 reveal-gating-contract.test.ts×1 spec-async-semantics.test.ts×2 | [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:185` | — | uninitialized-visibility.test.ts×3 | [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:59` | scheduler.ts×1 | action-await-contract.test.ts×2 | [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:177` | — | — | [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… | +| A27 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:177` | — | loading-value.test.ts×2 | [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… | ## V — fixed violations | id | status | defined | cited in src | cited in tests | statement (at definition) | diff --git a/packages/signals/scripts/rules-index.mjs b/packages/signals/scripts/rules-index.mjs index 9db90447a..b26205891 100644 --- a/packages/signals/scripts/rules-index.mjs +++ b/packages/signals/scripts/rules-index.mjs @@ -196,7 +196,21 @@ if (process.argv.includes("--check")) { console.error("rules-index: citations in src/ that resolve to no definition:", bad.join(" ")); process.exit(1); } - console.log("rules-index: every src/ citation resolves (" + cited.src.size + " ids)"); + // Every live A-rule must be pinned: cited by ID from at least one test. The + // spec's "Pinned by" column is prose until a test actually carries the ID; a + // renamed or deleted pin is only caught here. + const unpinned = [...rules.values()] + .filter(r => r.vocab === "A" && !/^\[superseded/.test(r.text) && !cited.tests.has(r.key)) + .map(r => r.key); + if (unpinned.length) { + console.error("rules-index: live A-rules with no test citing them:", unpinned.join(" ")); + process.exit(1); + } + console.log( + "rules-index: every src/ citation resolves (" + + cited.src.size + + " ids); every live A-rule is cited by a test" + ); process.exit(0); } const order = { A: 0, V: 1, B: 2, C: 3, INV: 4, RUL: 5, R: 6, "§": 7 }; diff --git a/packages/signals/tests/createMemo.test.ts b/packages/signals/tests/createMemo.test.ts index 3246c68a4..84c6f488a 100644 --- a/packages/signals/tests/createMemo.test.ts +++ b/packages/signals/tests/createMemo.test.ts @@ -1921,7 +1921,7 @@ describe("async compute", () => { }); }); -describe("isPending and latest with async upstream and downstream", () => { +describe("isPending and latest with async upstream and downstream (A8: verdicts are per-channel)", () => { afterEach(() => flush()); // Diagnostic: latest(x) alone in a render effect - same setup as Test 1 @@ -2169,7 +2169,7 @@ describe("isPending and latest with async upstream and downstream", () => { }); // Test 3: Single async - [isPending(x), x()] pairs update atomically - it("single async - [isPending(x), x()] pairs update atomically", async () => { + it("A10: single async - [isPending(x), x()] pairs update atomically", async () => { const [$x, setX] = createSignal(1); let asyncMemo: () => number; let resolveAsync: (() => void) | null = null; diff --git a/packages/signals/tests/createOptimistic.test.ts b/packages/signals/tests/createOptimistic.test.ts index 0cf01ad4a..01cf9f14d 100644 --- a/packages/signals/tests/createOptimistic.test.ts +++ b/packages/signals/tests/createOptimistic.test.ts @@ -912,7 +912,7 @@ describe("createOptimistic", () => { expect(isPending($data!)).toBe(false); }); - it("refresh() of an async optimistic accessor is a quiet re-ask — not pending (#2799, re-ruled 2026-07-13)", async () => { + it("refresh() of an async optimistic accessor is a quiet re-ask — not pending (#2799, A12, re-ruled 2026-07-13)", async () => { let resolveFetch: ((v: number[]) => void) | null = null; const makeFetch = () => new Promise<number[]>(r => (resolveFetch = r)); @@ -947,7 +947,7 @@ describe("createOptimistic", () => { expect(isPending(() => $data())).toBe(false); }); - it("a declared reload (affects + refresh) fires isPending when it is the only consumer (#2806, re-ruled 2026-07-13)", async () => { + it("a declared reload (affects + refresh) fires isPending when it is the only consumer (#2806, A12, re-ruled 2026-07-13)", async () => { // The #2799 test above keeps the node alive with a value-observer // (`createRenderEffect(data, () => {})`). When the only consumer is a // reactive `isPending(() => data())` (the real JSX diff --git a/packages/signals/tests/enforceLoadingBoundary.test.ts b/packages/signals/tests/enforceLoadingBoundary.test.ts index 69c70edd9..81c80a4ee 100644 --- a/packages/signals/tests/enforceLoadingBoundary.test.ts +++ b/packages/signals/tests/enforceLoadingBoundary.test.ts @@ -8,7 +8,7 @@ import { flush } from "../src/index.js"; -describe("enforceLoadingBoundary", () => { +describe("enforceLoadingBoundary (A6: ASYNC_OUTSIDE_LOADING_BOUNDARY is warn-only)", () => { let warnSpy!: ReturnType<typeof vi.spyOn>; beforeEach(() => { diff --git a/packages/signals/tests/equals-comparator-errors.test.ts b/packages/signals/tests/equals-comparator-errors.test.ts index 1d97a3fd2..6d1fd3885 100644 --- a/packages/signals/tests/equals-comparator-errors.test.ts +++ b/packages/signals/tests/equals-comparator-errors.test.ts @@ -20,7 +20,7 @@ import { type User = { id: number }; -describe("equals comparator errors (#2837)", () => { +describe("equals comparator errors (#2837, A3: comparator throws are compute-phase errors)", () => { let errorSpy!: ReturnType<typeof vi.spyOn>; beforeEach(() => { @@ -71,6 +71,30 @@ describe("equals comparator errors (#2837)", () => { expect(log).toEqual(["beat=1"]); }); + it("A4: the first commit never invokes a custom equals with an undefined previous value", () => { + const calls: [unknown, unknown][] = []; + const [users, setUsers] = createSignal<User[]>([{ id: 1 }]); + let value: User | undefined; + createRoot(() => { + const selected = createMemo(() => users().find(u => u.id === 1), { + equals: (prev, next) => { + calls.push([prev, next]); + return prev!.id === next!.id; // would throw on an undefined prev + } + }); + createRenderEffect(selected, v => { + value = v; + }); + }); + flush(); + expect(value).toEqual({ id: 1 }); + expect(calls).toEqual([]); + + setUsers([{ id: 1 }]); + flush(); + expect(calls).toEqual([[{ id: 1 }, { id: 1 }]]); + }); + it("user effect on the errored memo does not fire with a bogus value", () => { const values: unknown[] = []; const [users, setUsers] = createSignal<User[]>([{ id: 1 }]); diff --git a/packages/signals/tests/errorHalt.test.ts b/packages/signals/tests/errorHalt.test.ts index 6df86b216..accffa77a 100644 --- a/packages/signals/tests/errorHalt.test.ts +++ b/packages/signals/tests/errorHalt.test.ts @@ -14,7 +14,7 @@ import { // An error that escapes every boundary permanently halts the reactive system // (#2761/#2762): app state is undefined after an uncaught error, so instead of // limping along with half-applied updates the scheduler stops accepting work. -describe("uncaught effect errors halt the reactive system", () => { +describe("uncaught effect errors halt the reactive system (A5)", () => { afterEach(() => { resetErrorHalt(); vi.restoreAllMocks(); diff --git a/packages/signals/tests/latest-isPending-consistency.test.ts b/packages/signals/tests/latest-isPending-consistency.test.ts index d5e299f9c..8fc27247d 100644 --- a/packages/signals/tests/latest-isPending-consistency.test.ts +++ b/packages/signals/tests/latest-isPending-consistency.test.ts @@ -83,7 +83,7 @@ describe("latest/isPending consistency (#2831)", () => { expect(log[log.length - 1]).toBe("pRead=false pLatest=false latest=v2"); }); - it("[isPending(x), x()] never pairs pending with the fresh value when async resolves inside an open action", async () => { + it("A10: [isPending(x), x()] never pairs pending with the fresh value when async resolves inside an open action", async () => { const renderLog: string[] = []; const userLog: string[] = []; let setX!: (v: number) => void; @@ -156,7 +156,7 @@ describe("latest/isPending consistency (#2831)", () => { expect(userLog).toEqual(["[false, data-2]"]); }); - it("sync memo over a transition-held signal is visible to latest() and isPending()", async () => { + it("A11: sync memo over a transition-held signal is visible to latest() and isPending()", async () => { const log: string[] = []; let setX!: (v: number) => void; const gate = deferred<void>(); diff --git a/packages/signals/tests/loading-value.test.ts b/packages/signals/tests/loading-value.test.ts index c08b0542a..c72c728ef 100644 --- a/packages/signals/tests/loading-value.test.ts +++ b/packages/signals/tests/loading-value.test.ts @@ -99,7 +99,7 @@ describe("createMemo with loadingValue", () => { expect(result).toBe(42); }); - it("keeps the loading window verdict-quiet: isPending stays false through the first flight", async () => { + it("A27: keeps the loading window verdict-quiet: isPending stays false through the first flight", async () => { const d = deferred<string>(); let user!: () => string; createRoot(() => { @@ -718,7 +718,7 @@ describe("projections with seedLoadingValue", () => { }); }); -describe("loading window and transitions", () => { +describe("loading window and transitions (A27)", () => { it("is loading-class: writes concurrent with the window commit ambiently; after close the same writes are held", async () => { const defs: Record<number, ReturnType<typeof deferred<string>>> = { 1: deferred<string>(), diff --git a/packages/signals/tests/rules-index.test.ts b/packages/signals/tests/rules-index.test.ts index edea32dbe..f50e563bf 100644 --- a/packages/signals/tests/rules-index.test.ts +++ b/packages/signals/tests/rules-index.test.ts @@ -18,4 +18,13 @@ describe("rules index", () => { const out = execFileSync(process.execPath, [script, "--check"], { encoding: "utf8" }); expect(out).toContain("every src/ citation resolves"); }); + + it("every live A-rule is cited by ID from at least one test", () => { + const script = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "../scripts/rules-index.mjs" + ); + const out = execFileSync(process.execPath, [script, "--check"], { encoding: "utf8" }); + expect(out).toContain("every live A-rule is cited by a test"); + }); }); diff --git a/packages/signals/tests/spec-async-semantics.test.ts b/packages/signals/tests/spec-async-semantics.test.ts index 0e4c18ac9..372d6195a 100644 --- a/packages/signals/tests/spec-async-semantics.test.ts +++ b/packages/signals/tests/spec-async-semantics.test.ts @@ -47,7 +47,7 @@ const settle = async () => { flush(); }; -describe("A13 (was B1): resting optimistic node ≡ plain async memo", () => { +describe("A13 (was B1) / A12: resting optimistic node ≡ plain async memo", () => { // A createOptimistic node with no active override must be observationally // identical to a plain async memo: same values, same isPending/latest // verdicts, at every checkpoint of a refetch cycle — both before any