diff --git a/documentation/plans/optimism-seam-spike-brief.md b/documentation/plans/optimism-seam-spike-brief.md new file mode 100644 index 0000000000..ff1c49f8dc --- /dev/null +++ b/documentation/plans/optimism-seam-spike-brief.md @@ -0,0 +1,420 @@ +# Optimism: the seam / reveal / hold layer spike (2026-11-05) + +Date: 2026-11-05. `next` @ `4fe75321d`. Branch `tembo/optimism-seam-spike-brief`. +Companion record: `reader-seat-rebuild.md` §8 (the S1 result this brief starts +from) and the analysis report of the same date ("Solid 2 stores and optimism: +three assessments, the shape to move to, and the rules scan"). Rules live in +`packages/signals/docs/`; this brief cites them and does not restate them. + +## 0. Why this spike, and why not S1 again + +S1 (`seat/s1-swap`, #4430, 2026-10-29) replaced the read and seating deciders +with the posture table and passed every suite, and the gate still failed: ++294 B brotli on the core floor, +1,061 B on `+ isPending/latest`, 25 of 150k +generator cases moved, two of them final screens. §8.4's diagnosis is the +premise here: **the bytes and the moved screens live in the seam / reveal / +hold layer of `lanes.ts`** (`laneSeam`, `laneSeams`, `endLanes`, +`dissolveLane`, `supersede`, `applyGuesses`, the Transaction state +`_shown`/`_held`/`_links`/`_verdict`/`over`/`folded`, `revealing`, +`adoptParked`, `heldBy`, the `stagedReaders` repair), which the table did not +touch. That layer is still growing: the three commits since the analysis pin +(#4687, #4691, #4693) added 81 lines to `lanes.ts`, among them a `marked` +set that holds two kinds of member, a `waiting` set, and a `walk` over the +landing's subscribers. + +The table's own evidence has drifted too. The seat shadow landed at zero +mismatch classes on 2026-10-29 (#4438). On today's `next`, over the scalar +pins, it reads **80 classes** (126,390 reads compared, 274 files); the +largest are Vp1 and V2 "expected C, got P" — verdict readers served a +staging — which match the D-25 amendments of 2026-11-02 (#4544). The shadow +is not gated in CI. Whichever side is right, the rule table and the code +disagree in eighty places a week after they agreed in none. + +So this spike does not rebuild the deciders. It asks whether the layer under +them can be made ordinary. + +## 1. The question + +**H1 — the value model.** Today a node has three value homes: `_value` +(committed), `_pendingValue` (a transaction's staging), `_x._lane` (the +lane's value: a written guess or a lane pass's derivation). Every seam, +reveal, dissolve and supersede step is a shuffle between the three +(`laneSeam` promotes `_pendingValue` → `_x._lane`; `dissolveLane` has four +outcomes per node; `supersede` moves the guess into `_value` "so the parent's +re-derivation compares against what is seen"; `display`, `laneValueOf`, +`truthRead` and `laneRead`'s seven exits choose among them per read). + +A17 as amended says the override is the displayed value **and** the graph's +value until its own source answers. Between the guess and its landing nothing +but `until` needs a value other than the guess. The hypothesis: a guess is +the node's `_value`; the value it covered lives in a revert log on the +transaction (one entry per guessed node); a landing beneath a shown guess is +an ordinary write staged under the parent hold (`_pendingValue`); a lane +derivation is ordinary staging under the lane transaction, revealed by the +lane's commit. `_x._lane`, `display`, `laneValueOf`, the promote step, and +the lane arms of `read` go. `until` reads the log. + +This is a relocation, not a reduction, of what the system knows: guess, +staged truth and covered value still exist. What it removes is the third +read path and the lane-specific promote / dissolve / supersede code, which +is where §8.4 says the bytes are. + +**H2 — verdict readers by posture, no lane record.** C1 (below) says posture +is a property of a read. A display-ahead read registers its reader for +re-derivation at the hold's landing — the mechanism frame readers already +use (`_reruns`, `staleReader`) — instead of listing it in a verdict lane. +`verdictLane`, `t._verdict`, and the 25 `isVerdict` discriminators go. S1 +kept `t._verdict` "as the reveal record"; this spike tests whether, once H1 +has made lane values ordinary staging, the record is needed at all. + +The spike's output is a yes or no for each, with numbers, and — if yes — the +scalar interface the store work targets: `optimisticWrite(node, guess)`, the +outcome on a guessed node, the comparator hook. + +## 2. Rules first + +Two rule texts are written before any mechanism, as proposals in +`RULES-DERIVED.md` (not rulings; the maintainer rules). They must reproduce +the ruled screens; where they would not, that is a finding. + +- **C1.** A read is one of three kinds. A plain read sees the screen, and a + plain read served a held staging makes its pass that hold's work. A + display-ahead read (`latest`, `isPending`) sees the hold's world, registers + its reader for re-derivation at that hold's landing, and never joins. An + authoritative read (`until`) sees the truth. A pass has reads; it has no + posture. (Absorbs A10, A31, D-25's "either order", "posture outlives the + pass", "inherited posture is not a probe"; D-26's parity clauses; D-28; + D-27's verdict-reader mount.) +- **C2.** A node has an answer once any landing has reached it, held or + committed, or a first paint was declared at creation. `latest(x)` is the + newest answer and throws only when there is none. `isPending(x)` is true + iff `x` has an answer and a new question about `x` is unanswered on screen. + A re-ask of the same question is not a new question; an `affects()` mark is + a declared one. (Absorbs A19 exc. 1, A27, D-22/D-23's same-flush and + pending-by-propagation amendments.) + +The seat table (`core/seat.ts`) is then re-derived to C1 and C2 — rows, not +mechanism — and the shadow re-baselined. Each of today's 80 classes is +triaged per `reader-seat-rebuild.md` §4.1: a row error (the code is right +under the rule), or a bug on `next` (the row is right), filed and +allow-listed, never fixed in the spike. + +## 3. Scope + +Scalar only. The store is out of scope and stays compiled; its hooks are +reached only by store tests, which the spike skips (`tests/store/**` and the +202 files that use `createStore` / `createOptimisticStore` / +`createProjection`). The store work is the analysis report's §4 and starts +after this spike's answer. Boundaries are consumers of the seat, not +deciders; no boundary change. The in-flush mount split (report §7 C4) is a +ruling, not a spike target: a mismatch there is a finding. + +## 4. Baselines (recorded 2026-11-05 on `4fe75321d`) + +| suite / measure | result | +| ------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| `@solidjs/signals` full | 481 files, 6,744 passed, 14 expected fail, 2 skipped | +| scalar pin set (275 files) | 3,834 passed, 12 expected fail, 7 skipped | +| `solid-js` | 55 files, 926 passed | +| `@solidjs/web` | 2,006 passed, 3 expected fail; one failure on the first run that did not reproduce (see §4.1) | +| seat shadow, scalar pins | 80 mismatch classes; top: Vp1 C→P ×66/×38/×36, V2 C→P ×29, V6 ×28, D6 C→P ×26 | +| generator 2k × {3289, 91501, 7} | 0 findings; recordings `/tmp/gen--2k.json` | +| generator 50k × 3, pairs E1,E2,E2e,E3,E6,E7,E8,E9 | recording (`/tmp/gen--50k.json`) | + +Size (brotli / minified, `scripts/size` on the fresh build; caps in `scenarios.js`): + +| scenario | br | min | cap | +| ----------------------------------- | -----: | ------: | -----: | +| signals: core floor | 7,421 | 19,825 | 7,430 | +| signals: + isPending/latest | 12,239 | 33,615 | 12,250 | +| signals: + createStore | 15,089 | 43,868 | 15,100 | +| signals: + until/refresh | 8,044 | 21,641 | 8,060 | +| app: render + one signal | 9,800 | 26,597 | 9,800 | +| app: hydrating + every store family | 33,398 | 100,547 | 33,410 | +| page: live server components | 40,289 | 121,153 | 40,300 | + +Three scenarios sit within 11 B of their caps; the spike's size gate is "not +above base", measured, not "under cap". + +### 4.1 Environment notes + +`dist-artifacts.test.ts` needs `pnpm build` first. The compiled size +scenarios need the compiler's native build (`pnpm --filter @solidjs/compiler +run build`, Rust toolchain). The generator's pairs flag is a comma list of +`PAIR_DEFS` keys, not `all`. The web suite's single first-run failure did not +reproduce on the second run; it is treated as flaky until it recurs, and the +test is named in the spike log when it does. + +## 5. Milestones and gates + +Each milestone is one branch off the previous, green on the scalar pins, and +measured before the next starts. Nothing lands on `next` from the spike; the +spike's deliverable is §1's answer. + +- **M0 — rules and table.** C1, C2 as proposals; seat rows re-derived; the 80 + classes triaged. No mechanism change; dist byte-identical (§5.1 of the + rebuild doc). Gate: shadow at 0 classes against the re-derived rows, with + every allow-list entry naming an issue. +- **M1 — H1, the value model.** Guess into `_value`; revert log on the + transaction; `until` reads the log; lane derivations as lane-transaction + staging; `_x._lane`, `display`, `laneValueOf`, `truthRead`'s arm selection, + `laneRead`'s value arms removed. Gate: scalar pins green; shadow 0; size + not above base on the four floors and `+ isPending/latest`. +- **M2 — the lifecycle.** `laneSeam`'s promote step and `LEFT_SHOWN` go + (reveal = the lane transaction's commit); `dissolveLane`'s four outcomes + become commit or revert-log replay; `supersede` becomes a write plus a log + drop; `marked`, `revealing`, `adoptParked`, `heldBy` each cut and judged + by the differential (the rebuild's §8.2 method: a candidate nothing + exercises is "undecidable", not "deleted"). Gate: as M1, plus 2k × 3 + against the recordings at 0. +- **M3 — H2.** Verdict readers by posture; `verdictLane`, `_verdict`, + `isVerdict`, `route`'s seating go; display-ahead readers register through + `_reruns`. Gate: as M2. +- **M4 — the full gate.** 50k × 3 with every pair against the recordings; + every differing case replayed on M1, M2, M3 to attribute it; `scripts/size` + table from the scratch build; the full signals, web and solid suites. The + answer is written into this file's §6 with the numbers. + +What stops the spike: a floor above base after M2; a final-screen move at +50k that C1 or C2 does not explain; a milestone that needs a new flag or +hook on a core node to pass its gate. + +## 6. Result + +(To be written at M4.) + +## 7. Commands + +```sh +# scalar pins (packages/signals) +pnpm exec vitest run $(cat /tmp/scalar-pins.txt | tr '\n' ' ') --reporter=dot +# seat shadow over the scalar pins, then the summary +SEAT_SHADOW_REPORT=/tmp/seat.jsonl pnpm exec vitest run $(cat /tmp/scalar-pins.txt | tr '\n' ' ') --reporter=dot +node scripts/seat-shadow-summary.mjs /tmp/seat.jsonl +# generator differential (packages/web), one seed +ASYNC_GEN=1 ASYNC_GEN_SEED=3289 ASYNC_GEN_CASES=50000 ASYNC_GEN_PAIRS=E1,E2,E2e,E3,E6,E7,E8,E9 \ + ASYNC_GEN_MODE=survey ASYNC_GEN_DIFF=/tmp/gen-3289-50k.json pnpm exec vitest run test/async-gen/campaign.spec.ts +# size (scripts/size, after pnpm build at the root) +node size.mjs --json /tmp/size-head.json --no-gate +``` + +## 8. Decisions, walked through with the maintainer + +### Decision 1 — posture is per read (settled 2026-11-05) + +_Rows: S-21, S-48 (standing), S-49 and S-50 (ruled, not built) — executable in `packages/web/test/async-gen/rulings`._ + +Asked first as an A/B on a memo re-run by a sync write. That shape has no +pin: the rebuild doc says the general claim "rests on `core.ts`" (the verdict +bit shares REACTIVE_PROBED's bit, is copied from the probe at the pass's +tail, and is read by the next pass) "and has no dedicated pin yet". The +ruled screen is the boundary's (F-vlane, `24adaff59`, 2026-10-28): a +`` shows its fallback now, beside the held shell, +pinned in `loading-on-frame-following-3540` §4–5, `loading-on-rearm-3540`, +`loading-on-latest-fallback-4337`. + +`on` is a dependency list (its value is never compared), so the re-arm's +timing is the ordinary rule for the `on` pass: a plain read of a held +staging makes the pass the hold's work and its re-arm is held (D-1); a +display-ahead read publishes now and re-arms now. No posture has to outlive +a pass or be handed between nodes. + +```tsx +}>… // follows the frame (#3540) +}>… // the spinner now (D-17) +}>… // waits while filter is held +``` + +Settled: + +- **C1 stands, refined 2026-11-06.** A pass's posture is decided by its own reads in that pass: never inherited (#4544), never carried into the next run. A plain read of a node the pass also read ahead is the screen's (A10: `[x(), isPending(x)]` in a memo shows `[committed, true]` now, S-27's memo case — the R1 ruling of 2026-10-28 stands); a plain read of any other held value makes the pass the hold's work (S-49, S-50). The first draft of C1 ("a pass has reads, not a posture") would have held the probing memo until the commit; the S-27 memo row caught it. +- **D-25's paragraph "A verdict posture outlives the pass that asked (ruled + 2026-10-28)" is withdrawn.** D-17 restated in one sentence: a re-arm is + ahead iff every held value the `on` expression read, it read ahead. + Mechanism: the swap belongs to the frame of the `on` pass that armed it — + one pointer recorded at arm time (`_armLane` minus the lane). +- **A memo that probed once and is later re-run by a sync write** reads the + held value as a plain read, joins the hold, and the tick holds with it. + Today (run on `4fe75321d`): with a prior probe the sync write publishes + beside the held value (`m=0 k=1`, twice); with no probe the tick holds. + The second is the rule. +- **Change against today, flagged:** the mixed `on={[latest(id), filter()]}` + with `filter` held re-arms ahead today (any probe makes the pass a verdict + reader); under this it waits for the frame. + +M0 checks: the three loading-on pins under the re-derived rows; two new pins +— the memo-and-sync-write shape holds the tick; the mixed `on` form waits. + +### Decision 2 — "answer" is defined as today (settled 2026-11-05) + +_Rows: S-26, S-27, S-45 (the leaf and memo cases ruled, not built; the plain case standing)._ + +C2 is written with reading (ii): a node has an **answer** once it has a +committed value, or its own async has landed into a hold, or a first paint +was declared at creation. A sync memo born held has **produced** a value +(`latest` returns it, D-22) but has no answer: no verdict, its owned reader +is held with it and runs at the commit (D-2, D-23 exception 1, F-9's +hydration parity). No screen changes. Exception 1 becomes the definition; +#4459, #4569 and A27 are instances. The spike implements one predicate where +`core.ts`, `verdict.ts` and `seat.ts` each spell the uninitialized / +first-landing / netted test today. + +### Decision 3 — `latest` of a slot displaying a guess shows the displayed value (settled 2026-11-05) + +_Rows: S-33 (standing), S-44 (ruled, not built: `latest(comments)` at the new question's landing)._ + +Asked as "is a guess an answer or an edit". The answer kept provenance as it +is and moved the question to what `latest` means. + +- **Scenario 1 (S-44 row 1, #4336) is unchanged.** A guess is an edit over + the answers to the question it was written over; an answer to a new + question is not its truth, and the frame that changed the input is held + with the edit's hold. The displaced-guess rows stay. +- **Provenance is unchanged** (D-35, A24): a same-question landing from + outside the hold still supersedes in the graph — plain-read derivations + re-derive from it at once as the hold's work, so #3331's downstream + refetch still starts early — and `isPending` reads true while the held + truth differs. +- **What changes is `latest`.** Since U-10 (#4050, 2026-10-14) `latest` + inside a memo decides membership: the memo's result shows now instead of + being held. A17's older clause also handed it the graph's value, so a + `latest`-built count showed the landed truth beside rows showing the guess + (#4689, found by ReactiveScript's checker, whose `latest x` means only + "don't wait"). Ruled: `latest` of a slot that is displaying a guess answers + the displayed value until the hold commits. `until` keeps the truth. +- **Re-ruled:** D-22's "the arrived truth under a superseded override"; + A17's #3331 amendment narrows to plain-read derivations; D-34's + "`latest()` answers the truth" under a shown guess. S-44's `latest` column + at step 2 becomes the displayed guess (a view built only from `latest` + reads shows `#1: dana | mine (sending)`; today's `#1: ` tears count + against rows the same way #4689 did). Approved unscoped. +- **#4689 is a bug under this.** Fix: delete the `laneValueOf` pick at + `verdict.ts:521` and make `latestView` the displayed view; re-pin + `optimistic-key-guess-reask-4329.test.ts:382`; re-check the 17 files that + read `latest` over an optimistic primitive around a re-ask (35 read it at + all). A deletion in the read decider, not the seam layer: can land ahead of + the spike. #4685 (D-23 cause (iii) under lane entanglement) stays a + separate bug in the spike's layer. +- **Chains built on `latest` of a guessed slot** correct at the commit + instead of the landing; plain-read chains are unchanged. + +### Decision 4 — a first pass that reads this flush's staging is born held when the flush parks (settled 2026-11-05) + +_Rows: S-3, S-6, S-31 (standing), S-46 (the plain row ruled, not built: today the branch attaches with its hole empty)._ + +Run on `4fe75321d`, plain write and action alike: +`primed: a=0 A closed B closed → flushed: A panel=1 (published into a closed +gate) · B open (no panel inside) → landed: a=1 B panel=1 A open`. + +- A mount behind a plain gate written in the same tick never shows early: + the gate's write is held with the tick (D-4). D-2's in-flush clause + ("publishes with the flush; if the flush parks it re-derives on the + committed values") only produced invisible work — the content's first pass + published the staged value into a branch nobody could see. +- A mount behind a display-ahead gate (` 0}>`) opens + now; its content, reading `x()` plainly, is born held: fallback now if a + `` is inside (S-31), nothing visible until the landing if not + (S-46, the deferred insert of #4574), content at the landing. +- **Ruled (b):** a first pass that reads this flush's staging, when the + flush parks, is born held — D-2's main clause, with the in-flush exception + removed. No screen changes. Reading (a) (re-derive on committed) was + rejected: it would show the old content inside a gate opened by the new + value and re-rule S-31's fallback row and S-46. +- **Mechanism:** the first-pass half of the `stagedReaders` repair goes; + `bornInto` and `CONFIG_HELD_FRAME` stay as the rule rather than the + exception. Pins to add: mount A's content held with its gate; mount B's + content born held under both write kinds. + +### What M0 now consists of + +1. C1 and C2 (Decision 2's definition of "answer") written into + `RULES-DERIVED.md` as proposals; D-25's carry-over paragraph withdrawn; + D-17 restated per Decision 1; D-2's in-flush exception removed per + Decision 4; D-22/D-34 amended per Decision 3. +2. Seat rows re-derived to them; the 80 shadow classes triaged (row error, + or bug on `next` with an issue and an allow-list entry). +3. New pins: memo-and-sync-write holds the tick; mixed `on` waits; mount A + held with its gate; mount B born held. The #4689 fix (a deletion in + `verdict.ts` and `latestView`) may land ahead of the spike with its + re-pins. + +## 9. Executable rulings (2026-11-06) + +The rows above are executable: `packages/web/test/async-gen/rulings/S-.ts` +(the scenario in the generator's grammar, the expected screen per step, the +✗ screens), run by `rulings.spec.ts` in `pnpm test`, with the `SCENARIOS.md` +blocks generated from the same objects (`RULINGS_WRITE=1 pnpm exec vitest run +test/async-gen/rulings` in `packages/web`; the spec fails when a block is +stale). The seven rules D-2, D-17, D-22, D-23, D-25, D-27 and D-34 are one or +two sentences naming their rows; their former text is archived in +`INTERNALS-ASYNC-STATE.md` §7. Six cases are `ruled-not-built` and record +today's screen beside the ruled one: S-44 (the `latest` clause and, found by +the run, an owned `isPending` probe reading false after the outside change +where the imperative probe reads true), S-45 leaf and memo, S-46 plain, S-49, +S-50. The grammar grew `on`, rich actions with guesses and refreshes, +optimistic sources, gated and `isPending` inputs, `isPending(() => latest(x))` +reads, and branch-owned memos; the random generator does not emit them. + +## 10. M0 triage of the seat shadow's 80 classes — first pass (2026-11-06) + +Run: `SEAT_SHADOW_REPORT` over the 275 scalar pin files on `4fe75321d`; 126,390 reads compared, 80 mismatch classes. Dispositions under the rules as rewritten (§8 and `RULES-DERIVED.md`), by reading the rows in `core/seat.ts` against them. Nothing in `seat.ts` is changed yet; each line is a proposed row edit or a drop, to confirm before the table is re-derived. + +| family | classes | hits | state | disposition | +| ------------------------------------------------------------------------------- | ------: | ---: | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Vp1 `expected C, got P`, node `s u` (staged, uninitialized) | ~20 | ~190 | a probe of a node born staged, reader often a first pass (`cU`) | **row error.** An uninitialized node has no screen; D-22/D-23: no answer, no verdict — the probe's read does what a read does and is served the staging. `windowRow`'s `staged && !pending` branch returns `SLOT_C` for `isPending`; for `uninit` it should be `stagedSlot` with `RAISE_H` when owned, as `Vp2` already does for a held node. | +| Vp1 `expected C, got P`, node `s` (staged this flush, not held), `w0 flushTx` | ~10 | ~30 | a plain read of this flush's staging inside an `isPending` window | **decision needed.** A probe is reads-only (A23): the value its thunk is served never reaches a screen; what matters is the side effect (join, AH, W). The table asserts the slot. Proposal: Vp rows assert side effects only. If the slot must be asserted, the same-node clause says the probed node's own value is the screen's, and these would be code deviations to trace. | +| Vp1 / V1 `expected C, got P`, node `s h`, reader `cH cSameTx w2` | ~10 | ~40 | the hold's own work (a pass held by the same hold) probing or plainly reading its hold's staging | **row error.** D-25: the hold's own work reads plainly whatever it asked (#3884, #4341). `windowRow`/`heldRow` exclude only `seatBornInto`; add "held by the same hold" (`cSameTx`). | +| V2 `expected C, got P`, V posture from the sticky bit | 4 | 61 | a pass whose V posture is carried (`CONFIG_VERDICT` from a previous run) or inherited reading a staging | **row error, the carry-over.** `postureOf` returns V from the sticky `CONFIG_VERDICT`; under D-25 as rewritten posture is this pass's own probes. The seat records the nodes this pass probed (`seat.probed`); V applies to a plain read of a probed node only (same-node), D/L otherwise. Resolves these with the V1 sticky cases. | +| V6 / V2 / B2 "verdict seat after the read" and "at the pass's end" | ~16 | ~70 | slot agrees; the verdict-lane membership bookkeeping differs | **drop.** The verdict lane is mechanism (membership), not a rule; D-25 as rewritten has no "verdict seat". The table stops asserting `vseat`; H2 removes the record. | +| D6 `expected C, got P`, D posture, `s h · inLane`, `flushIsElTx` | 6 | ~50 | lane work (a derivation) reading the parent hold's staging in the hold's own flush | **row error.** D-26 (clarified 2026-10-28), the computing half: a lane derivation that reads a value the parent holds joins that hold and computes from the staging now; it shows at the commit. `D6` predates that clarification. → `P` with `RAISE_H`. | +| D6 `expected C, got P`, L posture (`t1`), `s h · inLane` | 3 | ~12 | a render effect in a lane reading the parent hold's staging | **trace.** D-26's displaying half says a plain read in lane work of a node the parent holds is the screen's; the code serves the staging. Either the leaf is the hold's own work in its own flush (`flushIsElTx`, L2) or this is a deviation. | +| D6 / L6 / L0 / D0 / L4 / D4 "world after the read" | ~9 | ~60 | slot agrees; the seat's world bookkeeping (S/Fl/H) differs after a stale or lane read | **drop or adjust.** A stale read (`MARK_ST`) does not raise the world under D-1; the table's world ledger is seat mechanism, not a screen. Keep only where a world change predicts a later slot. | +| L7' `expected P, got Dv/Lv/NR`, `s d lane shown lheld`, reader `cH cOtherTx t1` | 6 | 23 | a render effect held by another hold reading a shown lane's derivation whose staging is the lane's next reveal | **row error.** #4666 (2026-11-03): "a shown lane's `_pendingValue` is the lane's staging for its next reveal, not the frame's" — the leaf reads the screen (`Dv`) and re-derives at the reveal. `L7'` was written for the pre-#4666 reading. | +| B2 "seatLane at the pass's end (rows L7')" | 1 | 8 | follows L7' | **drop with L7'.** | + +Net: every class but the three-class D6 leaf family and the ten Vp1 slot-in-a-window classes is a row error or a bookkeeping assertion the rewritten rules do not make. No class is yet a bug on `next`; the one candidate from the rulings run is S-44's owned probe (false where the imperative probe reads true), which is not in the shadow's scalar pins. + +Before the table is re-derived: (1) the same-node clause in D-25 — **confirmed 1a, 2026-11-06**; (2) `isPending`-window rows assert side effects only and (3) verdict-seat and world-ledger comparisons leave the table — **taken as consequences of "the rows are the rules", 2026-11-06** (a probe's served value and a lane membership are not screens); the join comparison stays, since D-1 names it. Then M0.2 edits the rows, re-runs to zero, and files any class that survives. + +## 11. M0.2 — the seat table re-derived (2026-11-06) + +`core/seat.ts` edited beside the deciders (test-only; dist unchanged). Shadow over the 275 scalar pin files, 126,390 reads: **80 classes → 6 classes (8 reads)**, with 538 reads under three named deviations of today's code from the rewritten rules. Suites green. + +What changed in the table: + +- **Posture is this pass's own reads.** `postureOf` no longer returns V from the sticky `CONFIG_VERDICT`. The seat records the nodes the pass read ahead (`probed`, seeded from the node's previous pass — today's sticky bit is how a plain-first memo stays order-independent across runs); a plain read of one of them takes the V rows (the same-node clause, 1a); any other plain read of a held value takes the plain rows (joins). +- **`isPending`-window reads**: slot not compared, side effects compared (decision 2). +- **Comparisons dropped** (decision 3): the `Fl` rank (`REACTIVE_STAGED_READ`, D-2's in-flush re-derive, withdrawn by decision 4) and the verdict seat, at reads, at B1 and at B2. The join (`REACTIVE_JOINED`, D-1) is still compared, for derivations; a render effect's membership is the tick's (D-4). The verdict seat stays as an internal selector for a lane node's read (a verdict memo's own value versus its display is screen-visible). +- **Rows corrected:** a born-staged node under `isPending` has no screen (Vp2, the staging); the hold's own work — a pass the same hold holds — reads its hold's staging whatever it asked (V1''/Vp1''/Vl1'', #3884/#4341) and joins unless born into the hold; a shown lane's staging is the lane's next reveal, not the frame's (L7', #4666); lane work in a lane re-based over the hold's answer reads the hold's staging, the hold's flight as the screen (D6', `Transaction.over`, #4536 — the store seam the report's §4 removes). +- **Two attempts reverted, recorded so they are not retried:** "lane work in the hold's own flush reads the staging and joins" (D6'') cascaded into ~2,500 spurious join mismatches — lane work reads the screen (D-37) except through `over`; and the own-work rows must not raise a join for a probe's inner read or for a pass born into the hold. + +The three deviations, allow-listed by predicate (`known` in `seat.ts`). They are not issues to file: each is the mechanism M1–M3 replace (the sticky verdict bit, the verdict lane, the carry-over), ruled in §8 and pinned by S-49 and S-50 as `it.fails`; the allow-list counts go to zero when M3 lands, and the two rows flip. + +| label | reads | what today does | what the rule says | +| ----------------------------------------------------- | ----: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- | +| D-25 pass-wide posture (S-49, S-50) | 368 | a probe makes the pass a verdict reader for every held read in it (the sticky bit, or the verdict-lane seat): the screen, no join; on a staged error, the proposed error | a plain read of a node the pass did not read ahead joins (D-1); the same-node clause only | +| D-25 verdict-lane membership over the hold's own work | 159 | a pass the hold holds, seated in the hold's verdict lane by membership, reads the screen and does not join | the hold's own work reads its staging and is the hold's | +| D-25 carry-over (S-50) | 11 | a node probed last pass, read plainly this pass without a probe, is the screen's | the plain read joins | + +The six residual classes (8 reads; `fallback-fn-latest-runaway-4467` ×4, `held-branch-verdict-reader-4010`, `verdict-probe-committed-4648`): a V1 read served the staging where the table says the screen, a V4 read thrown NotReady where the table says the screen, two join disagreements on own-work rows for a pass still uninitialized (`cU`), and two bookkeeping reports (B1 inherited world, B2 born held) under a creator that joined per the code. Each needs a trace, not a row change. + +M0 is done to the point the spike needs: the table encodes the rewritten rules, and what it disagrees with is named. The spike's M1 starts from here. + +## 12. The derived rules collapsed (2026-11-06) + +With the glossary holding the definitions and the rows holding the screens, sixteen derived rules were restatements of other rules or of the fundamentals. They are stubs now — the ID stays, the stub names where the rule went — and their rows and pins moved to the absorber; the text as it stood is archived in `INTERNALS-ASYNC-STATE.md` §7 (the 2026-11-06b entry). + +| absorber | absorbed | why | +| -------- | ---------------- | -------------------------------------------------------------------------------------------------------------------- | +| D-1 | D-2 | the same sentence once _the hold's work_ and _born held_ are glossary terms | +| D-9 | D-11 | D-9's last clause and F-3's statement | +| D-12 | D-13, D-14, D-21 | the held half of "the fallback is seen when the frame that armed it is"; F-3/F-4/F-5 restated; the lane form of D-12 | +| D-17 | D-15, D-16 | D-17's first sentence; a consequence of it (the outside-hold clause and the diagnostic kept) | +| D-23 | D-24 | one sentence of the isPending rule | +| D-25 | D-26, D-27, D-28 | the same-node clause; D-1 after decision 4 plus the glossary's _Lane_; per-read posture stated for throws | +| D-33 | D-32 | F-10 restated plus the glossary's void-guess clause | +| D-34 | D-35 | D-34's second paragraph | +| D-41 | D-39 | contained in D-41 as "recovery is the mirror" | +| — | D-36, D-37 | consequences of the glossary's _Guess_ and _Lane_ plus D-25; D-36 stays as a pin | + +D-30 shortened to its one distinct sentence. `RULES-DERIVED.md`: 40 rules, 14,335 words on 2026-11-05 → 24 live rules, about 7,600 words. Checks: every row a folded rule listed is listed by its absorber; the rulings spec and the rules index pass; the boundary rules D-18 and D-19 and the hold rules D-3, D-4, D-6, D-7, D-8, D-10 stand as they were. diff --git a/packages/signals/docs/INTERNALS-ASYNC-STATE.md b/packages/signals/docs/INTERNALS-ASYNC-STATE.md index 8d4b97159f..2e7a3822f9 100644 --- a/packages/signals/docs/INTERNALS-ASYNC-STATE.md +++ b/packages/signals/docs/INTERNALS-ASYNC-STATE.md @@ -507,6 +507,380 @@ The findings that used to follow here (§5a–§5h, 2026-07-06 → 2026-07-16) a - #2838 (tracked) — `latest()` shadow should become write-driven post-release; the probe-based design is acknowledged overcomplication. +### 2026-11-06 — the seven spike rules rewritten in screen vocabulary; their mechanism text and amendment history moved here verbatim + +`RULES-DERIVED.md` D-2, D-17, D-22, D-23, D-25, D-27 and D-34 are now one or two sentences naming their scenario rows (executable: `packages/web/test/async-gen/rulings`, generated into `SCENARIOS.md`). What each rule said before — mechanism, dated amendments, pins, deviations — is kept below unchanged, as the archive the short form was collapsed from. A sentence here is not a rule; the rows are. + +#### D-2 as of 2026-11-05 (archive) + +##### D-2. A new memo joins a hold by reading it, not by being created + +**From:** F-2, F-1, F-7. **Scenarios:** S-3, S-6, S-31. + +A memo or effect created while a hold is live is not the hold's because it is new. It becomes the hold's if its first pass reads a held value, sync or async (D-1): it is then _born held_ — it has no committed value until the hold's commit, an untracked read of it throws `NotReadyError` meanwhile, and a render effect created with it first runs at that commit. A render effect that reads it throws, so it holds the change that mounted it (F-2): the branch does not attach until the commit — never an empty slot — unless a fresh boundary catches the read (F-4). A new memo that reads nothing held and nothing pending publishes now. A fresh render effect that reads the held value _directly_ shows the committed value (D-3). The entry is the pass's alone: a write made after the mount is a mainline write, not the action's. **Inside a flush**, a first pass that reads a value this same flush staged (not yet held) is treated as the frame's: it publishes with the flush, and if the flush parks it re-derives on the committed values; only a mount made under a display-ahead reader whose first pass read such a staging is born held (D-27). A boundary a held memo mounts in the same flush is not the hold's by creation: its content does not join, and it appears with the memo at the commit (D-13), on its fallback if its content is still pending then. + +A sync memo that has read a held value has produced one, so `latest` of it returns that pending value. It has no committed value, so it has no verdict: inside an owner `isPending` of it holds its reader as its read does — the reader runs at the commit, where the answer is final — and unowned it is `false` (F-9, D-23, D-24). An async memo has produced nothing until its answer lands, so `latest` of it throws. That answer is the hold's while a render effect outside a boundary consumes it, so the hold waits for it (F-2). If no such render effect reads it, the answer is the memo's own first commit once the hold has landed. A lane held on async downstream of the read does not paint, and does not hide a value `latest` can already return. + +"In general transitions can hold on any unready read. But new memos created don't need to be part of it" (2026-10-07): a transition holds on any unready read (F-2); creation alone joins nothing. The word in that sentence was async; what was meant was a held value. + +**Collapses:** [A29](./SPEC-ASYNC-SEMANTICS.md#a29-a-tracked-read-served-a-live-transactions-staged-value-enters-that-transaction) creation-time form ("born held", 2026-09-14/15, #3451) and its 2026-10-07 amendment "A verdict lane's mount over a staging" (#3851/#3869); L2 statement 3 as amended 2026-10-07; [O2](./SPEC-ASYNC-SEMANTICS.md#o2-creation-under-a-transaction-escapes-the-hold--recorded-not-ruled) (its text is stale — see Flagged); N1 (reverted 2026-10-02); #3761 (ruled against 2026-10-05); recovery 3.1/3.3; #3800 (the async first answer; drafted as D-5 and withdrawn as its own rule 2026-10-09) and the direction rule's value bullets (#3820). **Status:** standing, ruled 2026-10-09 (`tests/born-held.test.ts`, `tests/verdict-mount-first-pass-3851.test.ts`). The async first answer is ruled; built in #3977 (the memo is held into the hold it read, and the hold waits for it unless a boundary that has not shown content catches it; `issue-3800-repro.test.ts`, `direction-rule-probe.test.ts`) (S-6, U-1). Fixed 2026-11-02 (#4417, #4574): a park re-derive that gives back the staging it superseded keeps its held readers and their children (one key, one child), and an insert of a DOM node waits while its subtree holds a child born into the open hold. + +#### D-17 as of 2026-11-05 (archive) + +##### D-17. A display-ahead read in `on` shows the fallback now, beside the held frame + +**From:** F-6, F-5. **Scenarios:** S-21. + +`on` re-arms from whatever it reads, and the fallback appears when that read's frame has landed (D-15). `on={latest(id)}`, an `isPending()` or an optimistic signal reads a value whose frame has already landed, so the fallback shows now, beside the page that is still held: `[A] → [A + spinner] → [B + spinner] → [B + comments]`. The hold still owns every read of the held value. Content under the boundary stays off screen until it is ready; content under it that reads nothing held reveals as soon as it lands. No diagnostic. The `[A + spinner]` frame is the new id's loading state inside the old page, the same shape as a header showing the new id early. A memo between `isPending` and `on` behaves as the direct read does. What the _fallback_ reads is not `on`: a fallback reading `latest(id)` shows the new id once it is on screen (F-6), but the swap that `on={id()}` asks for still lands with its frame (D-15), never beside the held value (#4337). + +**Collapses:** #3575, B-AHEAD, recovery 6.1, §27 7a (#3528), `tests/loading-on-frame-following-3540.test.ts` §4, `tests/loading-on-rearm-reveal-3540.test.ts` ("on: () => latest(dep)"). **Status:** standing, ruled 2026-10-09 as the frame rule, not a break through the hold. + +#### D-22 as of 2026-11-05 (archive) + +##### D-22. `latest(x)` reads the newest value a flush has carried; it throws before a first value + +**From:** F-6, F-10, F-9. **Scenarios:** S-25, S-45. + +`latest(x)` answers the held write (the newest value a flush has processed, held or not — for a memo nobody has read, the value the flushes it missed would have given it), the optimistic value where one displays, the arrived truth under a superseded override, and the committed value of a node whose own flight is up (below the async nothing newer exists). A sync derivation of a held source is visible through it (the held recompute is a write path like any other). It throws `NotReadyError` only when no value has been produced — before a first landing, including an async memo with no resolution — in every scope. It returns `T`, never `undefined`. A lane held on async downstream of the read does not paint, and does not hide a value `latest` can already return. A memo behind a fallback, or in a branch not yet attached, has still produced its value, so `latest` returns it — a memo born held included: its plain read throws, and `latest` returns its pending value (F-9). Resolved async never pairs `[false, undefined]`. **A first landing a hold carries is a produced value (clarified 2026-10-28, amended 2026-10-30):** an async memo's first answer that lands in the same tick as a write to its input that a hold carries — an action's, or a plain write re-asking it — is held with it (D-4), and `latest` of it returns that answer while the plain read still throws (F-9); a `Loading` whose only read is `latest` of it shows the answer now (S-40). The frame is whole: the screen's committed input beside the answer to it. **A memo nobody has read has still produced what the flushes it missed would have given it (ruled 2026-10-30, #4474):** `latest` of a memo on the observation lifecycle (unowned, or `lazy: true`) whose sources have committed values is that value, computed on the committed world (laziness is never observable — #3078, #4366, #4474). An identity memo over an async memo, read for the first time in the flush that re-asks the async memo, shows the async memo's landed answer and is pending on the re-ask, the same screen as with a prior reader of it; a plain read of it in that mount is not display-ahead (D-27): its reader is born held (D-2) and shows the re-ask's answer. **Inside a flush, `latest()` of a staging not yet held is the frame's read like any other (clarified 2026-10-28):** it serves the staging, opens no hold, and leaves a plain read beside it in the same pass reading the staging too (F-10: one frame for every channel); if the flush parks, the pass re-derives once as a verdict reader (D-2, A10). A `latest()` read that manufactured a hold for the frame so its pass could run once made the plain reads after it see the screen in a flush that then committed (#4365). + +**Collapses:** [A28](./SPEC-ASYNC-SEMANTICS.md#a28-a-write-becomes-visible-at-flush--to-every-channel) (`latest` reads the flushed staged world), [A11](./SPEC-ASYNC-SEMANTICS.md#a11-sync-derivations-of-held-sources-are-visible-through-latestispending), [A7](./SPEC-ASYNC-SEMANTICS.md#a7-resolved-async-never-reads-false-undefined), [A17](./SPEC-ASYNC-SEMANTICS.md#a17-an-active-override-is-the-displayed-value-until-its-transaction-commits-and-the-graphs-value-until-its-own-source-answers) (`latest` returns the arrived value). **Status:** standing. **Fixed 2026-10-31 at the release (#4474).** The memo was not lazy but unowned — a memo on the observation lifecycle (unowned, or `lazy: true`) computes eagerly and, blocked on an uninitialized async source, is re-enqueued by that source's landing like any dependent; the settle walk queued its retry and then, finding it unobserved, released it (`async.ts`: `releaseIfSettledUnobserved` → `unobserved` pulled the retry back out of the heap), so the node never computed the landing and woke later with no value. Now the release waits for the pass the walk queued (REACTIVE_RELEASE_AFTER): the retry runs — once, as the owned twin's does — and `recompute`'s tail releases the node if it is still unobserved and settled (#2934's no-perpetual-recompute contract kept; #3078's read-revives and pre-heap reclaim untouched). Both read orders read as ruled through the existing paths (`tests/latest-unread-identity-memo-4474.test.ts`, `web/test/latest-unread-identity-memo-4474.spec.tsx`, `tests/released-memo-retry-runs-4474.test.ts`). A fix at the wake was built first and parked on 2026-10-31 (reference branch `fix/core-batch-4474-4417`, commit `ed85ca2a3`: +186 B brotli on the core floor and a generator regression, 91501 #15992) — history, not the mechanism. Consequence worth knowing: a settle's one retry is a compute — an unobserved async memo over a landing source starts the one flight its owned twin starts, then is released at that flight's landing. + +#### D-23 as of 2026-11-05 (archive) + +##### D-23. `isPending(x)`: three causes, two exceptions, question-scoped, atomic with the value + +**From:** F-9, F-1. **Scenarios:** S-26, S-27. + +`isPending(x)` is `true` while any of three causes holds: (i) a write held by a live hold; (ii) the node's own async in flight for a new question; (iii) a fresh value landed but held uncommitted by a hold it is entangled with — and `false` the moment none does. Exception 1: a node with no committed value — a first load, or a memo born held over a held value (D-2) — is loading, not pending. A node answered and then re-asked in the same flush — an async first answer that lands in the tick a write re-asks it — is not a first load (amended 2026-10-30): its answer is a produced value, so `latest` of it shows the answer and `isPending` of it is `true`, the re-ask being a new question; so is a derivation of it with no value of its own, at any depth (F-9, D-22). It has no verdict: inside an owner `isPending` of it does what its read does — a first load's `NotReadyError` goes to a boundary or holds the change (F-2); a reader of a memo born held is held with it, or caught by a boundary that has not shown content — and the reader runs at the commit, where the answer is final; unowned it is `false` (D-24). A node with a committed value is judged wherever its readers are: on screen, behind a fallback, or in a branch not yet attached. **A node initialized while pending by propagation is committed and pending (ruled 2026-10-31, #4569):** an answer that lands on a node with no committed value while an upstream re-ask has marked it pending by propagation — its own flight still current, nothing superseded it (#3373) — initializes the node: a node's question changes only when the node itself re-runs, so the answer is current, and the node is committed and pending on the upstream flight. Content that reads only its verdict reveals with `true` (S-40: a fresh boundary reading only a display-ahead channel shows what it has); content that reads the node holds until the chain settles — a first reveal has no screen to keep (F-2). That asymmetry is the rule, not a bug; a reading that kept the node loading would need a supersession by propagation no rule has, a new resting state, and would treat an uninitialized node more strictly than a committed one. A verdict on content that may have no committed value is read inside the boundary that owns it — `

{m()}

{isPending(m) ? "updating" : ""}
` — where its reader is caught with the content; read outside, it holds the change that mounted it. Either way no answer is shown before the commit: `` is never seen enabled over an unjudged value (`packages/web/test/ispending-guard-born-held.spec.tsx`). No verdict is the honest answer: `true` would say "updating" for something never shown, and `false` would read as settled to a guard like `if (!isPending(x)) save(x())`. Exception 2: a re-ask of the same question is quiet; an `affects()` mark is a declared new question and pends exactly the marked data and what derives from it, released when the declaring hold settles (or at flush end, ambient). `[isPending(x), x()]` read in one scope never pairs `true` with the fresh value: a verdict reader sees the screen — in either read order — unless it is a node a hold's fetch is waiting on (D-25). `isPending(() => latest(x))` follows `x`'s own non-quiet async only — on a signal or sync memo the latest form is never pending — so `[isPending(() => latest(x)), latest(x)]` never pairs `true` with the fresh value either. A spinner driven by `isPending` fires while the owner's async is still in flight (it does not wait for the hold). + +**Collapses:** [A19](./SPEC-ASYNC-SEMANTICS.md#a19-ispendingx--the-observable-value-is-not-final-three-causes), [A24](./SPEC-ASYNC-SEMANTICS.md#a24-question-scoped-pending-pending-iff-a-value-change-is-in-flight-or-an-affects-mark-is-live), [A10](./SPEC-ASYNC-SEMANTICS.md#a10-ispendingx-x-is-atomic-within-one-scope), [A8](./SPEC-ASYNC-SEMANTICS.md#a8-ispending--latestx-follows-xs-own-async-only--verdicts-are-per-channel), [A14](./SPEC-ASYNC-SEMANTICS.md#a14-companion-nodes-get-child-lanes-that-do-not-merge-with-the-owner), [A20](./SPEC-ASYNC-SEMANTICS.md#a20-superseded-optimistic-writes-announce-a-store-wide-pending)/[A21](./SPEC-ASYNC-SEMANTICS.md#a21-superseded-the-store-wide-mask) (superseded — the mask is gone), recovery 6.3. **Status:** standing. A probe alone holds nothing; a plain read beside it holds (`optimistic-settle-verdicts.test.ts` #3409, `question-scoped-pending.test.ts` 3.4; #3977). `isPending` of a memo born held, read inside and outside the boundary: `tests/fresh-boundary-born-held-reader.test.ts` (#3977; the engine joins the reader to the hold as the read does, which is the throw's observable effect). A first load answered while pending by propagation from an upstream re-ask, under a boundary (the issue's verdict-only content, a 3-deep chain, the plain-read controls that hold) and in a root: `tests/first-load-superseded-answer-4569.test.ts`, `packages/web/test/first-load-superseded-answer-4569.spec.tsx` (#4569, ruled 2026-10-31). **Known deviation (#4469, 2026-10-30):** a verdict reader is re-run at every landing of a hold it observed, even when the landing changes nothing it read; the gate that would skip it needs a deps walk. Measured 2026-10-30: two cheaper gates broke F-9/D-23 (verdict-memo-flight-no-reask) and #4189 (errored-store-probe-settled-3962) pins and failed the frozen floors by +31/+36 B. A plain reader of the same recovered hold is not in this: it shows the committed value for the whole hold and runs once, at the commit, since a recovery to a value equal to the last good one is still a change of answer (D-41, D-9). Pinned in `tests/hold-recovers-landing-reruns-4469.test.ts` (the verdict rows `it.fails`). A verdict reader of a signal whose writes net to its committed value in the tick an action opens a hold is not in this either (#4485, fixed 2026-10-30): the writes made no proposal (D-8), so the reader is not the hold's stale reader and `isPending(s)` stays `false` (`tests/netted-verdict-reader-4485.test.ts`). + +#### D-25 as of 2026-11-05 (archive) + +##### D-25. A display-ahead reader's own async is held; a memo computes under its own posture + +**From:** F-6, F-1. **Scenarios:** S-29. + +What is rendered from a display-ahead read reveals together with it: an async memo over `latest(x)` holds the `latest` view's reveal until it lands; a combined `isPending(() => [fast(), copy()])` over two async memos, one wrapped in a sync memo, stays `true` until both land, because a memo's value is one shared slot — it computes under its own hold posture, never under the posture of whichever reader pulled it. A node whose fetch a hold is waiting on is that hold's work, not a verdict reader: a pass that re-runs it while its fetch is in flight reads plainly — following the change it is fetching for — even if an earlier pass of it called `isPending` or `latest` (#3884). A memo's posture is what it reads: one that reads a verdict reader's value is a derivation of a display-ahead read, and a verdict reader itself — `createMemo(() => [held(), V()])` with `V = createMemo(() => isPending(x) && latest(e))` shows `V`'s verdict now, beside the screen's `held()`, as the inline probe and a render effect reading both do (#4050: a Provider's children memo over a router shell). **The probe decides the pass's posture in either read order (clarified 2026-10-28):** a pass that probes is a verdict reader whether the probe came before or after a plain read of the same held node — `[x(), isPending(x)]` and `[isPending(x), x()]` both show `[committed, true]` now and re-derive at the landing (A10) — unless the pass is the hold's own work: born in its world (D-27, #4341) or re-run by the write it holds (#3884 above). Those two read plainly; a probe does not rescue them. A pass that read two holds' display-ahead values is one reveal unit of both (A15 lanes corollary, #3335): it waits for either lane's flight, never showing one hold's `latest()` beside a half-computed derivation of the other's. + +**A verdict posture outlives the pass that asked (ruled 2026-10-28):** a pass that asked (`isPending`, `latest`) is the hold's verdict reader on every later run of its node until one completes having asked nothing — that run still reads as a verdict reader; the next is an ordinary derivation. On each such run its plain read of a node the hold holds is the screen's (a frame reader, A10; re-derived at the landing as A15's stale reader) and it joins nothing, whoever dirtied it — a sync write, a boundary redraw, a landing. The hold's own work is not in this: a node whose fetch the hold waits on, or a pass born in its world, reads plainly whatever it asked before (the #3884 clause and D-27 #4341 above, unchanged). A boundary's display-ahead swap is this posture handed on: an `on` pass that read `latest`/`isPending` is a verdict reader, and the fallback swap it asks for runs as that reader's work — the output's pass reads nothing of the held frame (a pending tree is linked, not read) and its answer, the fallback, is shown now beside the held frame and stays the screen until the landing, instead of being staged with the write's frame as a plain `on`'s swap is (D-15): `[A] → [A + spinner] → [B + spinner] → [B + comments]`, never `[A] → [B + spinner]`. Only the `on` pass's asking counts: `latest()` inside the fallback is the fallback's own display-ahead, not the swap's — a plain `on` over an action-held input lands the swap with the frame whatever the fallback reads (#4337). Today two memories carry this — `CONFIG_VERDICT` left on the node by the pass that probed, and the node's listing in the hold's verdict lane (`fallback`'s `b._lane`, `recompute`'s head) — and the rule is the posture, not the membership. Pins: `tests/loading-on-rearm-3540.test.ts`, `tests/loading-on-frame-following-3540.test.ts` §4–5, `tests/loading-on-latest-fallback-4337.test.ts`. **An inherited posture is not a probe (2026-11-02, #4544):** a pass whose verdict posture is only inherited — it read a verdict memo, and neither it nor its node's last pass opened a window of its own — is the hold's work where it reads the hold's own write (a sync write the hold holds, not a flight's answer): it is served that staging and joins the hold, as a plain derivation does. One flush or several make no difference (F-10): before the flush parks, the pass reads the flush's own write as that staging, leaving the verdict lane an earlier read of the verdict memo seated it in, as the other read order keeps it out. Not a staging a window asked of under the hold that holds it (or in this flush, before one): `[latest(x), x()]` through a memo stays atomic (A10, D-26: `tests/latest-memo-inline-parity.test.ts`). That mark is the hold's — it expires when the hold lands or dissolves, and a later hold reads the node as never asked. A pass that probes in its own body stays the hold's verdict reader and reads the screen (F-6 over a staging it holds). When a frame born into the hold (a render effect never committed) reads such a reader's settled value and goes pending on what that display-ahead value asks for, the pending does not hold the hold (F-6: display-ahead readers never hold the change they read ahead of): the hold lands, the reader re-derives at the landing (A15) and the frame with it. A lane's reveal still waits on it (S-29). A store key is served the same with or without a node (R2, signal parity): a node-less staging is this flush's own write with no node a window could have asked through, so the posture alone decides (2026-11-04, #4681). Pins: `tests/verdict-memo-held-branch-4544.test.ts`, `tests/posture-store-parity.test.ts` S9. + +**Collapses:** [A31](./SPEC-ASYNC-SEMANTICS.md#a31-a-memo-computes-under-its-own-lane-posture-never-its-pullers) (#3442), §20.1 question 3 ("Yeah definitely it is held. Consistent with any optimistic update"), OL-R24/R25/R34, recovery 6.2. **Status:** standing. A memo over a verdict memo: `tests/verdict-memo-under-held-memo-4050.test.ts`, `packages/web/test/held-shell-pending-feedback-4050.spec.tsx` (U-10). **Fixed 2026-11-01:** the verdict shortcut in `propagateStatus` (a probed source settling re-derives its verdict reader) now clears the settled source from the reader's pending sources — left in place, an errored probe target stranded a reader re-parked on a second source at that source's landing; `tests/verdict-shortcut-pending-source-liveness.test.ts`. + +#### D-27 as of 2026-11-05 (archive) + +##### D-27. A verdict reader's mount is a mainline mount + +**From:** F-6, F-4, F-1. **Scenarios:** S-31, S-46. + +A ` 0}>` opens at once (display-ahead). The `` it mounts is a mainline mount: it does not inherit the display-ahead world, so its content reads the screen's `x`. Inside an action's own flush that content's first pass reads the staging the action is about to hold, so it is born held (D-2): the boundary shows its fallback beside the committed `x = 0`, and `content 1` appears at the landing. Never `content 1` beside `x = 0`. A first pass created by an _optimistic_ lane, by contrast, is that lane's work (a binding an optimistic mount creates lands with its element). + +**Why the two differ, and what the mount's content may show (clarified 2026-10-28).** A plain read is never display-ahead; only a read that asks (`latest`, `isPending`) or a slot that is guessed (an optimistic value, F-11) is. A guess is _slot-level_: it is the value of its slot for the whole screen, so every plain read of it agrees and an optimistic mount's plain reads show it without tearing. `latest(x)` is _read-level_: only the reads that say `latest` see the held value. A mount's children are _gated_ by the display-ahead read, not _derived_ from it (F-6's "what is rendered from a display-ahead read" is the derivation — `fetch(latest(x))` — not the branch it opens); their plain `x()` is an independent read, and showing it the held value would make a plain read's answer depend on which `Show` wraps it — two plain reads of one signal disagreeing on one screen. So the content reads the screen, and the fallback it shows is `Loading` declining to tear over content that is, by the author's choice of a plain read, not yet the committed value. To show the held value inside the branch, read it there as `latest(x)`: `content {latest(x)}` shows `content 1` now, with no fallback. **Whoever opened the hold (ruled 2026-10-30):** an action is a way to extend a hold, not a different kind of one — a plain write held by an async memo on screen is the same case: the branch a display-ahead `Show` opens is born held and keeps the fallback until the landing, `latest(x)` inside shows ahead, a `Loading` inside shows its fallback (S-46, `tests/display-ahead-plain-write-content.test.ts`). + +**A joined pass's children are the hold's (promoted from A29, 2026-10-28):** a pass — a render effect included — that read a node with no screen to fall back to (a node born into the hold, never committed: D-2; D-4's tick-join) joins the hold: it has no committed value to show, so it is not a leaf reading the screen (D-3) but a reader that waits (A29 ruling A, 2026-10-01: "every reader derives from it — a render effect included — and joins"). A node it creates during that pass is born held: its value commits with the frame, and a mainline reader of it waits rather than showing it ahead of the frame (F-7) — the subtree-disagreement tear this rule guards against, a child's value on screen beside a parent frame still held. Shape: a lazy outlet's render effect reads a router query memo that has no committed value and creates a memo during that pass; the memo is the navigation's, even where its own first pass reads nothing held. This is the third mount beside the two above — a verdict reader's is mainline, an optimistic lane's is the lane's, a joined pass's is the hold's — and A29's "a held pass's children compute but do not effect and die with the commit that replaces their frame" is its archive form. Pins: `tests/lane-uninitialized-landing-3648.test.ts`, `tests/boundary-output-frame-reader.test.ts`, `tests/loading-fallback-in-flush-3540.test.ts`, `tests/fuzz-findings-l2.test.ts`. + +**Collapses:** #3851/#3869 (2026-10-07), [A29](./SPEC-ASYNC-SEMANTICS.md#a29-a-tracked-read-served-a-live-transactions-staged-value-enters-that-transaction)'s 2026-10-07 amendment, L2 statement 3 as amended, #3835. **Status:** standing (`tests/verdict-mount-first-pass-3851.test.ts`, `tests/optimistic-mount-binding-3835.test.ts`). + +**Parked (2026-10-31, #4417; issue open).** The ruling stands: the branch a display-ahead `Show` opens under a plain write's hold is born held and attaches once, at the landing, with its content (S-46 — today it attaches with its text hole empty, F-2's empty slot; and the `[1,2,2]` mount-count pins of `tests/boundary-not-born-held-3540.test.ts` / `web/test/loading-on-keyed-boundary-3540.spec.tsx` see a third mount). A fix was attempted on 2026-10-30 beside #4474's ("a pass whose mount is the hold's is the hold's": a render effect born held under a verdict-lane mount marks its creator pass joined, and `laneStage` leaves the verdict lane for the hold; reference branch `fix/4417-display-ahead-attach-parked`, commit `04c218537`) and dropped before #4474's reference branch was cut: it looped forever on the generator's seed 91501 #32043 (`boundarySeam` → `redraw` re-enqueueing a held boundary's output every round) and opened four new findings (seed 7 #1691 E6, #17379 E1, #38770 E6, #46918 E1). Rulings reaffirmed 2026-10-31 after a rules review: S-46 reading A (born held, attaches at the landing — entailed by D-27 and D-2's in-flush carve-out; attaching with the committed value instead would re-rule S-31's `[fallback]` row) and the `[1,2,2]` mount count. No mechanism inside the size budget is known for either half: adoption's frame half measures ≥100 B minified on the live-page floor; the display-ahead attach attempt looped. Parked; issue open; pins stay `test.fails` at the ruled screens: the S-46 plain row in `web/test/display-ahead-plain-write-content-s46.spec.tsx`, the four `[1,2,2]` rows. Fixed 2026-11-02 (#4417, #4574): a park re-derive that gives back the staging it superseded keeps its held readers and their children (one key, one child), and an insert of a DOM node waits while its subtree holds a child born into the open hold. + +#### D-34 as of 2026-11-05 (archive) + +##### D-34. A different truth corrects the graph now and the screen at the commit; what the screen never showed is void + +**From:** F-11, F-1, F-6. **Scenarios:** S-33, S-44. + +When an override's own source answers (its own landing, or a sync recompute from an upstream change): an equal answer confirms silently — nothing re-runs; a different answer corrects — derivations re-derive from the truth now as the hold's work (downstream refetches restart at once, no waterfall), the screen and untracked reads keep what the screen showed until the commit, `latest()` answers the truth and `isPending()` reads `true` while they differ, and the correction reveals with the hold. "Keep the override" means keep what _showed_: a guess the screen never displayed — its reveal held on its own downstream flight, including through a shown `Loading` — is void at the correction, and untracked reads see the committed value, as the screen does. A later landing equal to the override restores it as the graph's value. **A write before the first landing is no guess (ruled 2026-11-03, maintainer, #4656):** "if it hadn't landed it isn't optimistic, its loading" (09:22); "Write before load in the non optimistic case gets squashed.. optimistic is odd one because its visible. Idd be ok with it erroring I think"; "I guess it could be in a second list but then it wouldn't have this problem" (09:27); "That or chat input is disabled until it loads" (09:29). An optimistic write to a source whose first answer has not landed — a `createOptimistic` signal's, an optimistic store's, keyed or `key: null`, seed or not — throws `OPTIMISTIC_WRITE_BEFORE_LOAD` in DEV and guesses nothing; the remedies are a separate list (an outbox) for the pending items, or the action disabled until the source has loaded. Production behaviour is unchanged (not specified). So nothing in this rule, or in D-35's re-base, meets a first load. + +A keyed store's arrangement guess (rows by key) that the screen showed is not voided by a correction: the truth folds beneath it (one the screen never showed is void, as above). Derivations outside the lane re-derive from the truth now and join the hold, `until`/`resolve` keep their postures over it (an `until` outside the lane reads the truth and joins the hold, D-31), and `latest()` shows the hold's world whole. Lane passes, untracked reads and the screen keep the guess until the commit, and at the commit rows match by key. So a row the truth returns keeps its identity, a row another tab added shows at the commit, and the first of two overlapping actions confirming does not void the second's guess (2026-10-21). A folded lane's list pass inside a frame that already has a transaction reads that frame's stagings as the screen, as a verdict reader does, so it never pairs the truth's length with the guess's rows. + +A new question is not the guess's own source answering. When a guess's node re-asks because an input changed outside the guess's own action (a plain `setSel(1)` while a post that guessed over `sel() = 3` is out), the guess cannot stand beside the new input — that frame would show the new input over the old question's answer and the guess (F-1) — so the frame that changed the input is held with the guess's action (2026-10-28, #4336). The screen keeps the old input and the guess until that action commits, then shows the new input with its answer as one frame, however the reads are grouped into render effects (#4284). Meanwhile `isPending` reads `true` while the new question is in flight or held (F-9), `latest()` answers the newest truth (F-6), and untracked reads see the screen. A `refresh()` re-asks the same question and holds nothing new; the guess's own action changing the input is the correction above. A store's derive is one node asking one question for every key, so the hold is decided where the guess is read: a frame still reading a guess when the flush ends is held with it, and a read of a key with no guess is an ordinary pending read (F-2) — `comments[1]` waits for issue 1's answer, not for a post that guessed on `comments[3]`. An input set through its own optimistic write asks the new question in its own lane: the other action's guess does not answer it, so the lane waits for its own answer (F-6) and then shows it, with the guessed node (for a store, every guessed slot of the target its derive writes) as the lane's derivation from there (F-11); the guess's readers wait with the lane, however the reads are grouped into render effects. The other action's guess was only displaced: when the input's action ends without writing the truth, its lane's world is void, the revert re-asks the old question as the revert's work, and the displaced guess comes back in the revert's frame over that answer — `#1: ` then `#3: dana | mine (sending)`, nothing between — and lives until its own action settles (D-33). If the input's action commits, the displaced guess is gone, as for any correction. + +**Collapses:** [A18](./SPEC-ASYNC-SEMANTICS.md#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) supersession (#3331, 2026-09-09/10) and (d) before the first commit, "Correction display is what showed" (2026-10-05), lane contract 2, lane ruling 5, [A17](./SPEC-ASYNC-SEMANTICS.md#a17-an-active-override-is-the-displayed-value-until-its-transaction-commits-and-the-graphs-value-until-its-own-source-answers) (2026-09-09), OS-R39. **Status:** standing. Fixed 2026-11-05 (#4690): a shown guess a `refresh()` from outside its action corrected keeps the action's hold open while its own source is in flight, as an uncorrected guess does (`lanes.ts` `lanesBlocked`). With nothing rendering the source, the hold landed with the action's write, so effects and untracked reads showed the re-ask's answer until the action's own `refresh()` landed while rendered readers kept the guess — a tear between effect-only and rendered readers, not a flicker on screen; `tests/optimistic-outside-reask-4690.test.ts`. + +### 2026-11-06b — the derived rules collapsed 40 → 24; the folded and rewritten rules as they stood + +Sixteen rules folded into nine absorbers (D-1, D-9, D-12, D-17, D-23, D-25, D-33, D-34, D-41), D-30 shortened; each stub in `RULES-DERIVED.md` names where it went. The text as of the collapse, verbatim: + +#### D-2 as of 2026-11-06 (archive) + +##### D-2. A new memo joins a hold by reading it, not by being created + +**From:** F-2, F-1, F-7. **Rows:** S-3, S-6, S-31. **Pins also:** `tests/born-held.test.ts`, `tests/issue-3800-repro.test.ts`, `tests/verdict-mount-first-pass-3851.test.ts`. + +A memo or render effect is the hold's when a pass of it reads a value the hold holds — its first pass included — never because it was created while the hold was open. Born held, it has no answer until the commit: a plain read of it throws, `latest` of it shows what it produced, and a render effect that reads it holds the change that mounted it, so the branch attaches whole at the commit, never around an empty slot — unless a fresh boundary catches the read (F-4). A first pass that reads this flush's staging is born held when the flush parks, whoever opened the branch (ruled 2026-11-05, decision 4 in `documentation/plans/optimism-seam-spike-brief.md` §8). + +**Status:** standing. Mechanism and history: `INTERNALS-ASYNC-STATE.md` §7, the 2026-11-06 entry. + +#### D-11 as of 2026-11-06 (archive) + +##### D-11. A hold ends when its last on-screen reader leaves + +**From:** F-3. **Scenarios:** S-13. + +A hold lasts while an on-screen reader still observes a flight it waits on. When the last such reader leaves — disposed, re-run without the read (a gate closing), reset behind a fallback — the writes the hold kept for those readers alone commit at once; a hold is never kept open by a reader that no longer derives from the flight, nor by a display-ahead reader. A `Loading` reset moves the wait onto the boundary instead (D-14). + +**Collapses:** [O3](./SPEC-ASYNC-SEMANTICS.md#o3-a-render-effect-gated-away-from-a-never-landing-flight-keeps-the-sources-write-held--fixed), #3426, #3372, #3375, A23 (a probe is never a blocker). **Status:** standing. + +--- + +#### D-13 as of 2026-11-06 (archive) + +##### D-13. A boundary whose own mount is held appears at that hold's commit + +**From:** F-4, F-7. **Scenarios:** S-15. + +Whether a fallback is seen depends on whether the boundary is on screen. A fresh boundary opened by `` with `x` held, or nested in content a hold has not revealed, is not on screen until that hold commits; it appears then, showing its fallback if its content is still loading, its content otherwise. A boundary mounted by a pass that itself joined a hold is the same case. + +**Collapses:** the 2026-10-06 boundary-scope ruling ("a boundary mounted as part of a hold appears at that hold's commit"), #3869's "a boundary a held memo mounts in the same flush … appears with the memo at the commit". **Status:** standing. Built (U-3, 2026-10-14): a born-held first pass is not caught when the flush has joined the hold and the pass is its own, so the boundary the mount creates reads its content with the hold and appears at its commit — `tests/loading-fallback-in-flush-3540.test.ts` "a boundary mounted by a pass that joined the hold appears at its commit, no fallback", and the `flight` forms of "a boundary mounted by the hold" (a `` over a landing, flat and nested). Still order-dependent: a boundary created in the mount before anything in it joins the hold prepares its fallback first (the synchronous first render into the unattached branch) and swaps at the commit, never painted. The same gap shows when a render effect outside the boundary holds the mount (D-12): the boundary prepares its fallback with the held change and swaps to its content in the commit's own flush — never painted, but run (`tests/fresh-boundary-born-held-reader.test.ts`, the outside cases). + +#### D-14 as of 2026-11-06 (archive) + +##### D-14. A fallback-caught flight holds nothing; an `on` reset moves the wait onto the boundary + +**From:** F-3, F-5. **Scenarios:** S-16, S-17. + +A flight whose only observers are render effects behind a fallback holds no change — a memo under the boundary that no render effect reads was never an observer at all, and one rendered by an effect outside the boundary is that effect's read (F-3). In both orders: a render effect created under a fallback never counts, and a reader that counted while the boundary showed content stops counting the moment `on` sends it back to the fallback. Writes the hold kept for those readers alone commit at once (`Boundary: Loading... | Sum: 2` publishes as one frame); a reader outside the boundary still holds (`Sum` waits until `details` lands). The wait does not vanish; it moves onto the boundary: after the reset the fallback stays until every reader under it has settled — including readers whose flights started before the reset — then one coherent reveal. + +**Collapses:** [A33](./SPEC-ASYNC-SEMANTICS.md#a33-a-fallback-caught-flight-holds-no-transaction-a-loading-reset-moves-the-hold-onto-the-boundary) (#3375, #3459), §23.1 item 1, recovery 2.2. **Status:** standing. The nested form — an outer `on`-reset boundary not waiting for a flight its nested `on`-reset boundary catches — is built (U-4, 2026-10-13: a reader a nested collecting boundary holds is that boundary's alone; `tests/fuzz-findings-l2.test.ts`, fuzzer finding F1). The re-arm over a hold is built (U-9, 2026-10-14): content an earlier hold staged under the boundary, or that reads a hold after the re-arm, is the boundary's — the fallback shows now and the content reveals at that hold's commit, which does not wait for it; no redraw loop (`tests/loading-fallback-in-flush-3540.test.ts`, the re-armed shapes; `tests/loading-on-frame-following-3540.test.ts` §3b, §3c). A fallback staged into the hold its readers land in folds back at any seam, not only the re-arm's own: readers still pending at the re-arm land into that hold a flush later, and the content is ready when it commits (#4379, `tests/loading-on-held-filter-refresh-4379.test.ts`). + +#### D-21 as of 2026-11-06 (archive) + +##### D-21. An armed boundary lets a lane reveal: the async under the fallback does not hold it + +**From:** F-4, F-3, F-6. **Scenarios:** S-24. + +An armed boundary catches a lane the same way it catches a transition (D-12, D-14). A fallback lets a transition end early because the reads under it hold nothing; a fallback lets a lane end early for the same reason. A display-ahead value whose own async sits under that fallback is not held by it, so it reveals immediately, beside the fallback: header `1 | loading`, then `1 | details 1` when the async lands. A reader of that same async outside the boundary still holds the lane, as an outside reader still holds a transition. A disarmed boundary is not this rule: it forwards, and the lane holds on the async under it until that lands (F-5). + +**Collapses:** the fuzzer-finding F6 ruling's fallback half (2026-10-05, "a boundary on its fallback still holds nothing", A33, B5). The showing-content half of that ruling is F-5, pinned by `tests/fuzz-findings-l2.test.ts` "F6: a lane holds through a Loading boundary showing content". "A boundary output that never committed holds nothing" (2026-10-05) is F-3. **Status:** standing, as the lane form of D-12 and D-14. A header `latest(id)` beside an `on={id()}` re-arm is D-16. + +--- + +#### D-15 as of 2026-11-06 (archive) + +##### D-15. `on` is a dependency list; the swap lands with the frame of the change that caused it + +**From:** F-4, F-5, F-7. **Scenarios:** S-18. + +`on` is a tracked expression whose value is irrelevant; a write to anything it reads re-arms the boundary. Optimistic writes and a source going pending notify like any write; an expression that reads nothing reactive never does; a zero-argument function is a tracked accessor, not a callback. The fallback follows the frame of that read: it lands with the change that caused it — now, when nothing else on the page waits on that change (`[A] → [B + spinner] → [B + comments]`); together with the rest of the new page during a held navigation (the shell reads `product(id)` outside: `[A] → [B + spinner] → [B + comments]` with the spinner arriving with B, not beside A). While that frame is held, the boundary has entered the fallback and the screen keeps the old content; the fallback is seen only if the hold lifts before the new content is ready. If the content is ready when the frame commits, no fallback is ever shown. The children are not re-created. + +**Collapses:** #3575 (GabbeV's design, ruled 2026-09-21), B-ON, B-FRAME, recovery 4.1/4.2/5.1/5.2, the `createLoadingBoundary` JSDoc, `05-async-data.md`. **Status:** standing (`tests/loading-on-frame-following-3540.test.ts` §1–§2). + +#### D-16 as of 2026-11-06 (archive) + +##### D-16. A re-arm whose data is also read outside the boundary never shows its fallback; a race lost is not reported + +**From:** F-5, F-7, F-2. **Scenarios:** S-19, S-20. + +When the frame of an `on` change waits on the very source the boundary waits on — because a live reader _outside_ the boundary also reads it (a sibling `` over the same data, a header that renders it) — the re-arm has entered its fallback, and that fallback is not shown. Only a reader whose read throws holds; an `isPending()` or `latest()` of the data in the header does not (F-2, F-6) — unless the same render effect also reads the data plainly: that read holds (F-6). The hold lifts when the data lands, the content is ready in the same frame, and the fallback is never seen: `[A] → [B + comments]`. DEV reports `LOADING_ON_OUTSIDE_HOLD` once, at the change; the fix is structural (one boundary owns the read). A fresh mount by a separate change is not this rule: the held change is not its frame, so its fallback shows now (D-12). The diagnostic is re-arms only. This is only the flight the `on` change itself started: an outside reader of a flight the change did not start, or content an earlier action holds by a staged write, does not keep the re-arm's fallback from showing now (§3b, §3c of the frame-following pins). A frame held past the content's landing by something _else_ — the write's `action` staying open, other data the shell waits on — is a race the fallback may still win, a legitimate outcome, and nothing is reported (S-20). + +A header that reads `latest(id)` beside `on={id()}` shows the new id beside the boundary's old content for the whole hold. That is `latest` (F-6). The boundary's `on` still reads the held value, so its fallback stays off screen; the header does not pull it on screen. + +**Collapses:** #3575, #3584 (the after-the-fact report removed), B-SAME-SOURCE, B-RACE, recovery 5.3/5.4, `08-dev-diagnostics.md` `LOADING_ON_OUTSIDE_HOLD`. The 2026-10-06 sentence "committed content included, belongs to that boundary" (deferred in #3824) is this rule's off-screen half. Read as a visible spinner beside the outside reader's stale data, it is withdrawn (2026-10-09). **Status:** standing, ruled 2026-10-09 (`loading-on-frame-following-3540.test.ts` §3, §5). + +#### D-24 as of 2026-11-06 (archive) + +##### D-24. `isPending` never throws unowned; the probe is reads-only + +**From:** F-9, F-12. **Scenarios:** S-28. + +`isPending(fn)` returns `false` from an unowned caller (an event handler, imperative code) whose thunk throws a real error or reads a node with no committed value (a first load, or a memo born held — D-23); inside an owner — a component body, a computation, `untrack()` within either — the reader participates in loading boundaries: a first load's `NotReadyError` propagates, and a reader of a memo born held is held with it (D-23). In DEV, a plain `untrack()` in a component body that reads a pending memo the same body created throws `PENDING_ASYNC_UNTRACKED_READ` instead: its `NotReadyError` would re-run the body onto a fresh pending memo forever. Untracked inside a computation, `isPending`/`latest` is a snapshot, as any untracked read is: it does not make the computation a verdict reader, so the computation is not re-derived when its own dependencies go pending — a throw still reaches it through `untrack`. Event handlers are the supported untracked use (#3946: a router's link claim probing inside the computation that creates the anchor). `latest()` shares the mechanism but throws in every scope (D-22). The probe's thunk return value is never inspected: `isPending(() => store)` reads nothing and reports `false`; whole-store questions are asked through reads. (The direct-argument form `isPending(store)` is accepted API for post-2.0.) + +**Collapses:** [A16](./SPEC-ASYNC-SEMANTICS.md#a16-ispending-never-throws-in-untracked-contexts) (B5/B5a; wording corrected 2026-09-14), [A23](./SPEC-ASYNC-SEMANTICS.md#a23-the-ispending-probe-is-reads-only). **Status:** standing. The untracked snapshot: `tests/untracked-verdict-rederive.test.ts`, `packages/solid/test/show-untracked-verdict.spec.ts` (#3946). + +#### D-26 as of 2026-11-06 (archive) + +##### D-26. A `latest()`/optimistic reader mounted or re-run mid-hold shows the committed value and reveals with the lane + +**From:** F-6, F-1, F-3. **Scenarios:** S-30. + +A held display-ahead world ("a lane") is a hold seen from the outside. A render effect _off_ it that reads what it is revealing — mounted mid-hold, or re-run by an unrelated sync write — shows the committed value (what is on screen), publishes now, entangles nothing, and re-derives at the release: `Late: 0` at the mount, `Details: 1 | Late: 1 | Value: 1` at the release. `latest()` is not special here: a `createOptimistic` source behaves the same. A reader whose removal is staged in a live hold is still on screen and keeps holding the lane while visible; `latest()` read in such a branch peers through to the proposal. A direct leaf reading a lane's derivation shows the lane's revealed value and re-derives at the reveal (control and content never disagree); a derivation between (a memo, a boundary's tree) carries the future and is born held with its mount (D-2). The same holds for a render effect that reads an optimistic value before its lane is judged: if the lane holds on a derivation's flight, the effect shows the committed value and `latest()` of the guess shows the guess beside it, whether the two reads share one render effect or not — `1 x1 / 5`, then `5 x5 / 5` at the landing (#4284). **Inside the lane, "sees the parent's landings" is about computing, not showing (clarified 2026-10-28):** a lane derivation that reads a value the parent holds joins that hold (D-1) — it computes now, so async downstream of it starts without waiting for the commit, and its result shows at the commit as the hold's work. What lane work _displays_ is the screen plus the lane's own values: its guesses, or what its `latest()` reads asked for. A plain read in lane work of a node the parent holds and the lane does not show ahead is the screen's value, in a verdict lane as in an optimistic one (F-6: "a plain read beside it is a read"); `[s(), L()]` with `L = createMemo(() => latest(s))` shows `v:0 l:1`, as the inline `[s(), latest(s)]` does. A15 #3460's sentence is the computing half. + +**Collapses:** [A15](./SPEC-ASYNC-SEMANTICS.md#a15-transition-entanglement-is-graph-driven-lanes-settle-as-one-reveal) lanes corollary (#3460, #3463, #3479, #3662, #3698), #3444, the fuzzer-finding F8 ruling (2026-10-05, #3806), the adopted-staging ruling. **Status:** standing; fuzzer finding F5b ("a stale reader of a held re-guess shows the revealed guess") pinned `it.fails`. Grouping: `tests/lane-render-effect-grouping-4284.test.ts`. Fixed 2026-11-02 (#4629, #4633, #4637): a second action's guess into a slot whose earlier action's `refresh()` is in flight shows now, beside the screen's value of the re-asked node (the plain read in lane work above), whatever order the pass reads them in (D-28); `tests/projection-second-guess-4629.test.ts`, `tests/optimistic-second-guess-reask-4629.test.ts`. Fixed 2026-11-03 (#4666): the same with the second guess written into a different `createOptimistic` log — a reader held by another action's hold shows the guess at once when every flight it waits on is a `refresh()` re-ask (D-34 ¶3: it holds nothing new), and a reader pending on that hold's new question stays held (#4577); also into a log whose lane has already shown, where a held leaf reads that lane's staging as the screen and re-derives at its reveal, not as its frame's staging; `tests/optimistic-second-log-guess-4666.test.ts`. + +#### D-27 as of 2026-11-06 (archive) + +##### D-27. The branch a display-ahead `Show` opens is a mainline mount; its plain reads are the screen's + +**From:** F-6, F-4, F-1. **Rows:** S-31, S-46. **Pins also:** `tests/verdict-mount-first-pass-3851.test.ts`, `tests/display-ahead-plain-write-content.test.ts`, `web/test/display-ahead-plain-write-content-s46.spec.tsx`. + +` 0}>` opens now; what it mounts is gated by the read, not derived from it, so its plain reads mean what they mean on the rest of the screen. A first pass under it that reads the hold's staging plainly is born held (D-2) and the branch attaches at the landing — on its fallback inside a `` (S-31), as nothing until then without one (S-46); `latest(x)` inside shows ahead at its own site. Whoever opened the hold, an action or a plain write an async memo holds, makes no difference. + +**Status:** standing; S-46's plain row ruled, not built — today the branch attaches with its hole empty. Mechanism and history: `INTERNALS-ASYNC-STATE.md` §7, the 2026-11-06 entry. + +#### D-28 as of 2026-11-06 (archive) + +##### D-28. A read throws on its own; an earlier read does not disarm a later throw + +**From:** F-1, F-2, F-4. **Scenarios:** S-39. + +A read that has nothing to show throws, and a throw ends the pass, so the pass is held. Whether it throws is a property of that read. An earlier read in the same pass does not change it: an earlier `latest(s)` does not stop a later `n0()` — a lane derivation whose value has never shown — from throwing. `[n0(), latest(s)]` throws on `n0()` and `latest(s)` never runs. `[latest(s), n0()]` reads `latest(s)`, then `n0()` throws anyway. Both orders hold the mount: control stays off until `n0(1)` lands, then control and `[1, 1]` together. Under a fresh `` the same throw is caught (F-4): control on, the fallback, then `[1, 1]`. + +Order is not irrelevant. A throw means the rest of the pass does not run, so the two orders are different computations. What must not depend on order is the throw. The empty slot — control on, child absent — is that throw going missing: `latest(s)` put the reader in the lane and `n0()` waited as lane work instead of throwing. + +**Collapses:** `read-order-ruling-brief.md` case 827 (the brief's "order never changes what a mount shows" was the wrong statement of this), `rewrite-bugs-rulings.md` item 1. **Status:** standing, ruled 2026-10-09. The `latest`-first order still suppresses the throw today. A fresh `` that reads only `latest(s)` beside a sibling reading `n0()` never reads the throwing value, so it is not this rule (D-12, F-6, S-40). + +#### D-32 as of 2026-11-06 (archive) + +##### D-32. Nothing shows a write before its flush; a guess with nothing to be optimistic over is void + +**From:** F-10, F-11. **Scenarios:** S-25, S-32. + +`set(count, 30); latest(count)` is the pre-write answer until the flush that carries it, after which `latest(count)` is 30 and `latest(doubled)` is 60 in the same instant. `isPending(x)` is `false` for an unflushed write. A `latest()`/`isPending()` reader created after several flushes answers as if it had always existed (its answer is the holding change's, and lives and reverts with the hold). An optimistic write is visible from the flush that carries it, as a guess over that frame's hold: one an action holds stays for the action's lifetime; one made in a frame that something else holds (a plain write waiting on async) rides that hold. A guess written in a frame that commits with nothing held is as if it never happened — nothing is applied, no effect sees it. A28 (5) and OL-R5 installed that guess for the flush and reverted it (`[1, 2, 1]`): the narrowest width that could be built then. Lane contract 2 (2026-10-01) closes that gap. The `createOptimistic` JSDoc states the tightened rule. + +**Collapses:** [A28](./SPEC-ASYNC-SEMANTICS.md#a28-a-write-becomes-visible-at-flush--to-every-channel) (1)–(5), [O4](./SPEC-ASYNC-SEMANTICS.md#o4-adopted-unflushed--the-signals-verdict-channels-see-a-write-no-flush-has-carried--fixed), lane contract 2, OL-R5, OS-R22. **Status:** standing. + +#### D-35 as of 2026-11-06 (archive) + +##### D-35. Provenance: an older question's answer never corrects a newer guess; the body's end corrects what nothing authoritative is answering + +**From:** F-11. **Scenarios:** S-34, S-35. + +Two rapid actions on one slot: the older action's refetch landing after the newer override is a question the user has since changed — it is staged for the commit but does not supersede (no downstream re-derivation, no pending flip; `latest` keeps the override; `isPending` is `true` because the held value differs). Only the override's own action, a later action, or mainline supersedes. A keyed store landing beneath an arrangement guess is re-based over the older truth by row key. When every action body has returned and nothing authoritative is in flight — not the override's own source, not a plain load the action asked for — each override still in force is corrected by the truth at hand (the staged equal landing, else the committed value), for a store guess as for a signal's; display is unchanged until the commit. + +**Collapses:** [A18](./SPEC-ASYNC-SEMANTICS.md#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) provenance (2026-09-10; Q-D 2026-10-03), body-end corollary (#3427), §27 question 1, L2 statement 5, OL-R20, OS-R24/R43/R45. **Status:** standing. + +#### D-39 as of 2026-11-06 (archive) + +##### D-39. An error fallback is the display; it stays until a new answer replaces the error + +**From:** F-12, F-3, F-1. **Scenarios:** S-38. + +An `` flipping to its fallback is a boundary going back to its fallback: the nodes behind it stop holding (unless read elsewhere, where they error too), and non-errored nodes that lose their only visible consumer that way stop holding as well. The error stays on screen until a new answer replaces the errored one — a pending is not a value: over `Errored > Loading(never shown)`, a write publishes beside the old error (`0 | Fetch error for 0` → `1 | Fetch error for 0` → `1 | Fetch error for 1`); the inner fallback is hidden behind the error. While the fallback shows, nothing under it is on screen, so its recovery re-arms the whole subtree (ruled 2026-11-02, #4627, re-ruling async-gen 91501 #1831): every `` under it has not shown content — whether or not it showed content before the error — and once the errored source has its new answer, each catches what is still pending in the replacement frame and shows its fallback beside the recovered content, as on a first load (F-4, D-12): `[Crash][finding related]`. After the recovery the ordinary rules apply: the first content such a `` shows makes it a shown boundary, and a later re-ask keeps that content (F-5). The error stays up while something no inner `` catches still waits: the errored source's own re-ask (the write above), a pending read directly under the `` with no `` between (a pending is not a value; #4592), or a recovery that is part of a held write, action or transition, which reveals with that hold (D-41). `Errored` builds its function fallback in its own scope, like `Show`, once per show: a re-derive of the boundary while the fallback shows — a second reader caught, the same error propagated again, a retry pending after `reset()`, a later error — keeps the built fallback, its DOM and focus with it; a later error reaches it through the error accessor; the fallback is disposed once the content that replaces it is on screen — at the commit, when the swap is held (#3991). A fallback that no live reader holds heals: the boundary waits on what threw only while a reader that caught the error is alive, so when the last one is disposed (a `` closing over `m()` as the action that made `m` throw settles) the content shows again — what a fresh render with the same inputs shows, since nothing on it reads the error (ruled 2026-10-28, #4373). + +**Collapses:** §23.1 items 2–3, §27 7b (2026-10-02; the top-level `isPending(count)` affordance was lost by this ruling, knowingly), recovery 9.1–9.3. **Status:** standing. The recovery re-arm (#4627): `tests/errored-recovery-fresh-loading-4627.test.ts`, `tests/latest-memo-inline-parity.test.ts` (#1831 rows), `packages/web/test/errored-recovery-nested-loading-4425.spec.tsx`, `tests/derived-store-recovery-wake.test.ts` (the store twin, plain and held in an action), the "inside Loading" rows of `tests/errored-recovery-stale-memo-4592.test.ts` and `packages/web/test/errored-recovery-stale-memo-4592.spec.tsx` (their no-boundary rows keep the error: a pending is not a value). A recovery written in an action's tick is the action's and keeps the error up until its hold reveals the inner fallback: the last row of `tests/errored-recovery-fresh-loading-4627.test.ts`, `packages/web/test/async-gen/e1-3289-122.spec.ts` (async-gen 3289 #122). Once per show: `tests/createErrorBoundary.test.ts` ("instantiates the fallback once per show"), `packages/web/test/errored-show-retry-3991.spec.tsx`; the held swap: `tests/boundary-fallback-held-swap-3991.test.ts`; healing when the last reader is disposed: `tests/errored-heal-disposed-reader-4373.test.ts`, `packages/web/test/errored-heal-disposed-reader-4373.spec.tsx`. + +--- + +#### D-36 as of 2026-11-06 (archive) + +##### D-36. A resting optimistic node is a plain memo + +**From:** F-9, F-11. + +With no active override, `createOptimistic(fn)` is observationally a `createMemo(fn)` for `read`/`latest`/`isPending` at every checkpoint of a refetch cycle — before any override and after a full cycle reverted; a revert is not a refetch and never pends. + +**Collapses:** [A12](./SPEC-ASYNC-SEMANTICS.md#a12-resting-optimistic-nodes-report-pending-like-a-plain-memo), [A13](./SPEC-ASYNC-SEMANTICS.md#a13-resting-optimistic--plain-async-memo-at-every-checkpoint), V1–V3/V5. **Status:** standing. + +#### D-37 as of 2026-11-06 (archive) + +##### D-37. A lane sees the screen plus its own guesses and never holds a sync write + +**From:** F-6, F-11, F-1. + +The optimistic world is the screen plus the action's guesses: derivations of a guess publish into that world (a memo over an optimistic value shows the speculative result to the lane's readers and to direct reads, the committed one to readers off the lane — a whole frame either way). Lane work never makes its node the action's: an unrelated sync write re-running a `` over an optimistic value publishes at once and the action stays open; the frame a lane pass replaces leaves the screen when the lane's run applies, after a held lane's reveal, never before. + +**Collapses:** L2 statement 4 and §19 (2026-10-01), [A17](./SPEC-ASYNC-SEMANTICS.md#a17-an-active-override-is-the-displayed-value-until-its-transaction-commits-and-the-graphs-value-until-its-own-source-answers) "lanes stage" (#3479), [A15](./SPEC-ASYNC-SEMANTICS.md#a15-transition-entanglement-is-graph-driven-lanes-settle-as-one-reveal) lane work vs transaction work (#3662, #3698), OL-R9/R31. **Status:** standing. + +--- + +#### D-33 as of 2026-11-06 (archive) + +##### D-33. An optimistic value lives exactly as long as the hold it was made over; same-slot writes entangle, disjoint slots do not + +**From:** F-11. **Scenarios:** S-32. + +An override's lifetime is that of the hold it was made over — in the usual shape, its own action's: the hold committing reverts (or lands the truth beneath) exactly its own writes; concurrent actions on disjoint slots — different signals, different store keys — settle independently; same-slot writes from two actions entangle, and the slot reverts only when the last of them settles. Ownership never travels through a shared reader: a render effect reading keys two actions wrote merges their reveals for scheduling, not their ownership. Regular writes in the same action hold (F-2); at the end, revert and commit are one frame. + +**Collapses:** [A18](./SPEC-ASYNC-SEMANTICS.md#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) lifetime, store corollary (#2899), node corollary (#2912); OL-R6/R14–R18/R26; OS-R18–R23. **Status:** standing. + +#### D-12 as of 2026-11-06 (archive) + +##### D-12. An armed `` whose mount is not held shows its fallback now; its content appears when ready + +**From:** F-4, F-7, F-6. **Scenarios:** S-14, S-40. + +An armed `` catches the render effects under it that are pending (F-4) — membership is the render effect's position, not where a memo was created — whether the read is a first load, a held value, or a memo born held over one; what it catches holds nothing outside it. When the frame that mounts it is not held — a click with nothing else holding, a mainline flush, a root `render`, a mount nested under a disarmed boundary, a separate change made while another hold is up — that frame is shown now, so the fallback is seen now, and the content appears when it is ready: with that hold's commit for content that derives from a hold, at the landing for its own first load. A reader elsewhere on screen that holds the same value does not hold that frame: `save` holds `count = 1` with `Count 0 | Slow 0` on screen, and a separate `setOpen(true)` opens `{m()}` with `m` a memo of `count` — the screen is `Count 0 | Slow 0 | open true | [fallback]`, then `Count 1 | Slow 1 | open true | count 1` at `save`'s commit (S-14). The fallback is not seen when the mounting frame is itself held: the boundary is part of the held change (D-13), or a render effect in the same branch, outside the boundary, holds it (F-2). Content under it that is bound by a render effect (`

{m()}

`) is caught the same way, so the boundary never reveals empty content. Content under it that is only loading (its own first fetch) appears at the landing, and the hold does not wait for it (F-4). A derivation _outside_ the boundary in the same flush still holds the tick (D-4). A fresh boundary whose content reads only `latest` of a held value, and nothing pending, shows that value now. A sibling that reads the unresolved async stays on the old content. The boundary does not wait for the sibling (F-6, S-40). `isPending` of the boundary's value is false: the fallback is not a value of the source. `isPending` and `latest` of the content behind it are that content's own: `latest` of a memo born held is its pending value, and `isPending` of it holds its reader as its read does — read inside the boundary, the reader is caught with the content (F-9, D-22, D-23). + +**Collapses:** [A29](./SPEC-ASYNC-SEMANTICS.md#a29-a-tracked-read-served-a-live-transactions-staged-value-enters-that-transaction) boundary exemption (#3540), "A boundary that has not shown content owns its subtree" (2026-10-06) and #3824, direction rule bullet 4, B-FRESH, B-ROOT, recovery 1.1–1.4, 8.1; the dropped A35 / fuzzer MH8 (Flagged). **Status:** standing (`tests/boundary-not-born-held-3540.test.ts`; the reader's position, a memo created outside the boundary: `tests/fresh-boundary-born-held-reader.test.ts`, #3977; a memo created inside the boundary: `issue-3800-repro.test.ts`, built 2026-10-14 (U-1) — the boundary catches it and its answer is staged with the hold, which does not wait for it). Ruled 2026-09-18 (#3540: "A `Loading` mounted while a write is held shows its fallback; the hold stays with readers that have content to keep") and reaffirmed 2026-10-09. A draft of 2026-10-09 that kept the fallback off screen while any outside reader held the same value is withdrawn; so is the 2026-10-06 "fresh mount stays closed beside an outside reader" draft (withdrawn 2026-10-07). + +#### D-34 as of 2026-11-06 (archive) + +##### D-34. A different truth corrects the graph now and the screen at the commit; what the screen never showed is void + +**From:** F-11, F-1, F-6. **Rows:** S-33, S-44. **Pins also:** `tests/spec-async-semantics.test.ts` "#3331", `tests/optimistic-question-change-4336.test.ts`, `tests/optimistic-key-guess-reask-4329.test.ts`. + +When a guess's own source answers: an equal answer confirms silently; a different one corrects — derivations re-derive from the truth now as the hold's work, so downstream refetches restart at once — while the screen, untracked reads and `latest` of the displayed slot keep the guess until the commit, and a guess the screen never showed is void at the correction (S-33). A guess is an edit over the answers to the question it was written over: an answer to a new question is not its truth, and the frame that changed the input is held with the guess's hold until it commits (S-44); a `refresh()` re-asks the same question and holds nothing new. A write before a source's first answer is no guess (DEV `OPTIMISTIC_WRITE_BEFORE_LOAD`, #4656). + +**Status:** standing; the `latest` clause ruled, not built (decision 3, #4689). Mechanism and history: `INTERNALS-ASYNC-STATE.md` §7, the 2026-11-06 entry. + +#### D-1 as of 2026-11-06 (archive) + +##### D-1. Reading a held value joins its hold + +**From:** F-2, F-7, F-1. **Scenarios:** S-1, S-2. + +A derivation that reads a value a hold has not yet revealed — a memo whose branch flips onto a held signal, a derived store's fold reading a held key through its draft — derives from the held world, so its result belongs to that hold and reveals with it. Tracked or untracked makes no difference (`untrack` is about dependencies, not about which world a pass derives from). A render effect is not a derivation: it reads the screen (D-3). + +**Collapses:** [A29](./SPEC-ASYNC-SEMANTICS.md#a29-a-tracked-read-served-a-live-transactions-staged-value-enters-that-transaction) (read form, #3408; generalized 2026-10-01 "ruling A"), [A11](./SPEC-ASYNC-SEMANTICS.md#a11-sync-derivations-of-held-sources-are-visible-through-latestispending) (the sync derivation is visible through `latest`), L2 statement 2 "Reading the future joins it", CS-R31/#3733 (a derive reading a held key joins). **Status:** standing. + +#### D-17 as of 2026-11-06 (archive) + +##### D-17. A re-arm shows its fallback with the frame of the `on` pass + +**From:** F-6, F-5, F-4. **Rows:** S-21, S-48, S-49. **Pins also:** `tests/loading-on-frame-following-3540.test.ts` §4–5, `tests/loading-on-rearm-3540.test.ts`, `tests/loading-on-latest-fallback-4337.test.ts`. + +`on` is a dependency list: a change to anything it reads arms the boundary, and what it returns is never compared. The pass that reads it is a pass like any other. One whose every read of a held value was ahead (`latest`, `isPending`, a guess) has a frame that has landed, so the fallback shows now beside the held screen (S-21); one that read a held value plainly — a value it did not also read ahead (D-25) — is that hold's work, and the swap lands with the hold's frame (S-48; the mixed `on`, S-49, ruled 2026-11-05). What the fallback itself reads is its own display-ahead, not the swap's. + +**Status:** standing; S-49 ruled, not built. Mechanism and history: `INTERNALS-ASYNC-STATE.md` §7, the 2026-11-06 entry. + +#### D-9 as of 2026-11-06 (archive) + +##### D-9. Entanglement is graph-driven; what shares an on-screen async derivation settles as one + +**From:** F-1, F-2, F-3. **Scenarios:** S-11. + +Two changes whose async work is observed by a shared on-screen derivation settle as one unit; changes on fully disjoint graphs settle independently. "Observed by a shared reader" is a pass observing the flight pending — a render effect's mere existence entangles nothing (D-3). A second flight reaching a memo another hold already holds enters that hold at the propagation, not at the memo's next pass. A reveal that _discovers_ an async already in flight — a `Show` opening onto a pending node whose inputs are already visible — joins the hold that flight blocks, as its first observer if nothing displayed it before; when the flight's inputs are themselves still held elsewhere, the reveal is a stale reader of that other hold (shows committed, entangles nothing, re-derives at its commit). A reader whose removal a hold has _staged_ is still on screen and holds for every other change until the commit removes it; a removal nothing holds releases at once. Removing a flight's last on-screen reader releases the write it held, even when a `latest()` of the flight follows. + +**Collapses:** [A15](./SPEC-ASYNC-SEMANTICS.md#a15-transition-entanglement-is-graph-driven-lanes-settle-as-one-reveal) and its corollaries (#3407, #3443, #3305/#3334 reveal, #3458 first observer, #3463 zombies, #3494 through-a-memo and beside-a-release), [O3](./SPEC-ASYNC-SEMANTICS.md#o3-a-render-effect-gated-away-from-a-never-landing-flight-keeps-the-sources-write-held--fixed). **Status:** standing; fuzzer finding F2 ("a landing does not reveal a gate its stale reader re-derives onto a new flight") pinned in `tests/fuzz-findings-l2.test.ts` (built, #4071). + +#### D-25 as of 2026-11-06 (archive) + +##### D-25. A display-ahead read never holds the change it reads ahead of; its reader's own async holds the reader + +**From:** F-6, F-1. **Rows:** S-27 (the memo case), S-29, S-50. **Pins also:** `tests/ispending-combined-atomic-3442.test.ts`, `tests/verdict-memo-held-branch-4544.test.ts`, `tests/latest-memo-inline-parity.test.ts`. + +A pass's posture is decided by its own reads in that pass, never inherited from a creator or a memo it reads (#4544) and never carried into its next run. Each `latest` or `isPending` read sees the hold's world and joins nothing. A plain read of a node this pass also read ahead is the screen's: the two channels of one node agree within a scope, so `[x(), isPending(x)]` shows `[committed, true]` now in either order (A10, S-27). A plain read of any other held value makes the pass the hold's work (D-1): a mixed `on` waits (S-49), and a memo that probed on its last run and probes nothing on this one holds the tick (S-50, both ruled 2026-11-05). What is rendered from a display-ahead read waits for that read's own async, as one reveal (S-29). + +**Status:** standing; S-49 and S-50 ruled, not built. The 2026-10-28 paragraph "a verdict posture outlives the pass that asked" is withdrawn: its ruled screen is D-17's, and its general clause had no pin. Mechanism and history: `INTERNALS-ASYNC-STATE.md` §7, the 2026-11-06 entry. + +#### D-41 as of 2026-11-06 (archive) + +##### D-41. An error is an answer: it ends the wait like a value, and it shows when a value would + +**From:** F-2, F-1, F-12, D-39. **Scenarios:** S-42, S-43. **Ruled 2026-10-15** (Q-6 B, from #3886; first drafted as D-40 in #3995). + +A rejection, a compute-phase throw or a comparator throw is the answer to the question its node asked. As an answer it _settles_ the async: a hold never waits on an errored flight, and nothing here changes what a hold waits for. But settled is not shown. A value that lands on a held node is staged until the frame reveals (F-2); an error that lands on a held node is staged the same way, because the error fallback is content (F-4) and showing it beside the previous frame's other content is the torn screen F-1 forbids. Until the reveal: the screen keeps what it showed, an ordinary outside read serves the committed value (it reads the screen, D-29, D-34), `createEffect`'s error arm does not run, `` does not flip, `resolve`/`until`/`refresh` do not reject (D-31), `isPending` of it is `true`, and `latest` of it throws the proposed error — the one way to see the answer early (F-6). At the reveal the error and everything that waited with it show as one frame: the arm runs, the boundary shows its fallback, the outside read throws, `isPending` is `false`. + +Recovery is the mirror (D-39): a shown error stays the display until the frame that carries its replacement reveals — through a pending retry, and through a held recovery, success or a different failure. A recovery to a value equal to the last good one is still a change of answer: payload equality never silences it, held or not. + +A first load that rejects has no previous content to keep: born held (D-2), it stays loading until its frame commits, as a first load's value does; under an armed boundary it is the boundary's and shows with the boundary's frame (F-4). A derivation that reads a staged error throws it, as it would read a staged value: its own pass errors and stages with the same frame. + +**What it does not say.** It does not say an error keeps a hold open. It does not say an unheld error waits: with nothing held, an error publishes at the flush that found it, like a value (S-38). It does not say `latest` hides the error. It does not change what a boundary catches (F-12), the effect arms (D-38), or the halt. + +**Built 2026-10-15.** An error a held node's flight or pass finds is staged in `_pendingValue` (CONFIG_STAGED_ERROR) and committed with the frame, which publishes it through the ordinary error path; a held recovery keeps the shown error (`_shownError`) until its own commit, and a recovery equal to the last good value is a change of answer, held or not. `watch` (`resolve`, `until`, `refresh`) rejects at the landing. The duplicated immediate-publish path is gone. Pins: `tests/error-is-an-answer.test.ts`, `tests/watch-error-outcomes.test.ts` (GabbeV, #3886). + +**Derived stores (closed 2026-10-30, #4489).** A derived store's (`createStore(fn)`, projections, optimistic stores) held rejection is staged like a memo's: an untracked read of its property reads the screen until the frame reveals. Pinned in `tests/error-is-an-answer.test.ts` and `tests/optimistic-held-error-untracked-read-4489.test.ts`. The standalone cases from #3886's audit (a write to a failed writable memo, error-wrapper formatting, comparator release, snapshot capture) are not this rule. **Status:** standing. + +#### D-23 as of 2026-11-06 (archive) + +##### D-23. `isPending(x)` is true while a new question about `x` has no committed answer + +**From:** F-9, F-1. **Rows:** S-26, S-27. **Pins also:** `tests/question-scoped-pending.test.ts`, `tests/latest-isPending-consistency.test.ts`, `tests/ispending-combined-atomic-3442.test.ts`. + +True while an input change is held, while `x`'s own flight for a new question is up, or while a landed answer waits in a hold; false the moment the shown value is final. A re-ask of the same question is quiet; an `affects()` mark is a declared new question; a guess is verdict-inert. A node with no answer (D-22) has no verdict: owned, the probe does what a read does — it suspends or holds; unowned, it is false (D-24). `[isPending(x), x()]` read in one scope never pairs `true` with the fresh value, in either order; the `latest` channel is judged on its own (A8, S-27). + +**Status:** standing. Mechanism and history: `INTERNALS-ASYNC-STATE.md` §7, the 2026-11-06 entry. + +#### D-30 as of 2026-11-06 (archive) + +##### D-30. Stores: pending is per key; a derived store's seed is never visible; a declared first paint is loading + +**From:** F-9, F-1. **Scenarios:** S-36, S-37. + +Pending is per node — per store key: a held write to a plain store pends exactly the touched keys; a derived store's leaves report the derive's new-question refetch like any async memo, in both forms, and a quiet re-ask (`refresh(store)`) is quiet on them too. So do the leaves of a derive that is a verdict reader only through what it reads (D-25) and was served a flight's committed value: a tracked reader of them reads the flight as the memo's reader would — one that mounts mid-flight is held (F-2) and shows the flight's answer, never the old question's — and the derive computes that landing plainly, as the hold's work (D-25) (#4491, `tests/unread-derive-midflight-mount-4491.test.ts`). A derived store's landing resumes the frame that asked it: when a consumer of its leaves goes async, the landed leaves are held with that frame and read stale and pending until it reveals (signal parity); an untouched sibling key is final. A derived store's seed is a draft for the derive, never an observable value: until the first resolution (first yield for a generator) every outside read — tracked, untracked, `in`, enumeration — throws `NotReadyError` (DEV in a component body: `PENDING_ASYNC_UNTRACKED_READ`); write-path reads see it. A seedless derived store (`createStore(fn)` with a derive that takes no draft) starts from an empty record, the same unobservable draft. `loadingValue` / `seedLoadingValue` promote a first value to commit #0: reads serve it, no boundary trips, no hold starts or extends, `isPending` is `false` on every axis until the first real landing — a hydration invariant (the server always answers `false`) — after which the node is an ordinary memo. + +**Collapses:** [A9](./SPEC-ASYNC-SEMANTICS.md#a9-store-leaves-behind-a-firewall-report-the-firewalls-new-question-refetch), [A22](./SPEC-ASYNC-SEMANTICS.md#a22-pending-is-per-node-store-wide-only-for-the-firewalls-own-work) (amended 2026-10-03), [A25](./SPEC-ASYNC-SEMANTICS.md#a25-a-derived-stores-seed-is-a-draft-never-an-observable-value), [A27](./SPEC-ASYNC-SEMANTICS.md#a27-the-commit-0-loading-window-is-loading-class-and-verdict-quiet), the L2 stores ruling (statement 7), OS-R30–R36, PJ-R23/R30/R31. **Status:** standing; one adoption shape pinned `it.fails` (`adoption-unchanged-key-read-3706.test.ts`, a row first read after the adoption). + ## History — dated findings (§5a–§5h) Verbatim, in original order. See "Dated findings — where they went" under §5 for which still govern. diff --git a/packages/signals/docs/RULES-DERIVED.md b/packages/signals/docs/RULES-DERIVED.md index a6f3386ffa..d126cd50b8 100644 --- a/packages/signals/docs/RULES-DERIVED.md +++ b/packages/signals/docs/RULES-DERIVED.md @@ -12,23 +12,15 @@ Archive links: `SPEC` = `./SPEC-ASYNC-SEMANTICS.md`. The L2 hold-model section i ### D-1. Reading a held value joins its hold -**From:** F-2, F-7, F-1. **Scenarios:** S-1, S-2. +**From:** F-2, F-7, F-1. **Rows:** S-1, S-2, S-3, S-6, S-31. **Pins also:** `tests/born-held.test.ts`, `tests/issue-3800-repro.test.ts`, `tests/verdict-mount-first-pass-3851.test.ts`. -A derivation that reads a value a hold has not yet revealed — a memo whose branch flips onto a held signal, a derived store's fold reading a held key through its draft — derives from the held world, so its result belongs to that hold and reveals with it. Tracked or untracked makes no difference (`untrack` is about dependencies, not about which world a pass derives from). A render effect is not a derivation: it reads the screen (D-3). +A derivation whose pass reads a value a hold has not yet revealed — tracked or untracked, its first pass included — is that hold's work: its result belongs to the hold and reveals with it. Nothing joins by being created while a hold is open; a pass joins by what it reads. Born held, a node has no answer until the commit: a plain read of it throws, `latest` of it shows what it produced, and a render effect that reads it holds the change that mounted it, so the branch attaches whole at the commit, never around an empty slot — unless a fresh boundary catches the read (F-4). A first pass that reads this flush's staging is born held when the flush parks, whoever opened the branch (decision 4, 2026-11-05). A render effect is not a derivation: it reads the screen (D-3). -**Collapses:** [A29](./SPEC-ASYNC-SEMANTICS.md#a29-a-tracked-read-served-a-live-transactions-staged-value-enters-that-transaction) (read form, #3408; generalized 2026-10-01 "ruling A"), [A11](./SPEC-ASYNC-SEMANTICS.md#a11-sync-derivations-of-held-sources-are-visible-through-latestispending) (the sync derivation is visible through `latest`), L2 statement 2 "Reading the future joins it", CS-R31/#3733 (a derive reading a held key joins). **Status:** standing. +**Status:** standing. Absorbs D-2 (2026-11-06). Mechanism and history: `INTERNALS-ASYNC-STATE.md` §7. ### D-2. A new memo joins a hold by reading it, not by being created -**From:** F-2, F-1, F-7. **Scenarios:** S-3, S-6, S-31. - -A memo or effect created while a hold is live is not the hold's because it is new. It becomes the hold's if its first pass reads a held value, sync or async (D-1): it is then _born held_ — it has no committed value until the hold's commit, an untracked read of it throws `NotReadyError` meanwhile, and a render effect created with it first runs at that commit. A render effect that reads it throws, so it holds the change that mounted it (F-2): the branch does not attach until the commit — never an empty slot — unless a fresh boundary catches the read (F-4). A new memo that reads nothing held and nothing pending publishes now. A fresh render effect that reads the held value _directly_ shows the committed value (D-3). The entry is the pass's alone: a write made after the mount is a mainline write, not the action's. **Inside a flush**, a first pass that reads a value this same flush staged (not yet held) is treated as the frame's: it publishes with the flush, and if the flush parks it re-derives on the committed values; only a mount made under a display-ahead reader whose first pass read such a staging is born held (D-27). A boundary a held memo mounts in the same flush is not the hold's by creation: its content does not join, and it appears with the memo at the commit (D-13), on its fallback if its content is still pending then. - -A sync memo that has read a held value has produced one, so `latest` of it returns that pending value. It has no committed value, so it has no verdict: inside an owner `isPending` of it holds its reader as its read does — the reader runs at the commit, where the answer is final — and unowned it is `false` (F-9, D-23, D-24). An async memo has produced nothing until its answer lands, so `latest` of it throws. That answer is the hold's while a render effect outside a boundary consumes it, so the hold waits for it (F-2). If no such render effect reads it, the answer is the memo's own first commit once the hold has landed. A lane held on async downstream of the read does not paint, and does not hide a value `latest` can already return. - -"In general transitions can hold on any unready read. But new memos created don't need to be part of it" (2026-10-07): a transition holds on any unready read (F-2); creation alone joins nothing. The word in that sentence was async; what was meant was a held value. - -**Collapses:** [A29](./SPEC-ASYNC-SEMANTICS.md#a29-a-tracked-read-served-a-live-transactions-staged-value-enters-that-transaction) creation-time form ("born held", 2026-09-14/15, #3451) and its 2026-10-07 amendment "A verdict lane's mount over a staging" (#3851/#3869); L2 statement 3 as amended 2026-10-07; [O2](./SPEC-ASYNC-SEMANTICS.md#o2-creation-under-a-transaction-escapes-the-hold--recorded-not-ruled) (its text is stale — see Flagged); N1 (reverted 2026-10-02); #3761 (ruled against 2026-10-05); recovery 3.1/3.3; #3800 (the async first answer; drafted as D-5 and withdrawn as its own rule 2026-10-09) and the direction rule's value bullets (#3820). **Status:** standing, ruled 2026-10-09 (`tests/born-held.test.ts`, `tests/verdict-mount-first-pass-3851.test.ts`). The async first answer is ruled; built in #3977 (the memo is held into the hold it read, and the hold waits for it unless a boundary that has not shown content catches it; `issue-3800-repro.test.ts`, `direction-rule-probe.test.ts`) (S-6, U-1). Fixed 2026-11-02 (#4417, #4574): a park re-derive that gives back the staging it superseded keeps its held readers and their children (one key, one child), and an insert of a DOM node waits while its subtree holds a child born into the open hold. +**Folded into D-1 (2026-11-06).** the same sentence as D-1 once _the hold's work_ and _born held_ are glossary terms. Rows and pins moved to D-1; the text is archived in `INTERNALS-ASYNC-STATE.md` §7. ### D-3. A render effect is a leaf: it shows the screen and re-derives at the commit @@ -70,13 +62,13 @@ While a hold has a proposal for a slot (a staged, unrevealed value), a further w **Collapses:** [A34](./SPEC-ASYNC-SEMANTICS.md#a34-a-write-is-a-proposal-one-on-a-held-node-entangles-its-tick-one-that-nets-to-the-committed-value-is-none) (1)–(3), rule B (#3733), #3743 (a store fold's notifications are diffs). **Status:** standing. The store shape — a later mainline setter that does not write the held key publishes on the mainline tick — is built (U-8, 2026-10-13: a setter's written keys are its own set, the batch's earlier setters' chained behind it, so the notify visits this setter's keys and the fold keeps the batch's union; `store/unchanged-presence-no-hold-3743.test.ts`). -### D-9. Entanglement is graph-driven; what shares an on-screen async derivation settles as one +### D-9. Entanglement is graph-driven; what shares an on-screen async derivation settles as one, and a hold ends when its last on-screen reader leaves -**From:** F-1, F-2, F-3. **Scenarios:** S-11. +**From:** F-1, F-2, F-3. **Rows:** S-11, S-13. **Pins also:** `tests/fuzz-findings-l2.test.ts`. -Two changes whose async work is observed by a shared on-screen derivation settle as one unit; changes on fully disjoint graphs settle independently. "Observed by a shared reader" is a pass observing the flight pending — a render effect's mere existence entangles nothing (D-3). A second flight reaching a memo another hold already holds enters that hold at the propagation, not at the memo's next pass. A reveal that _discovers_ an async already in flight — a `Show` opening onto a pending node whose inputs are already visible — joins the hold that flight blocks, as its first observer if nothing displayed it before; when the flight's inputs are themselves still held elsewhere, the reveal is a stale reader of that other hold (shows committed, entangles nothing, re-derives at its commit). A reader whose removal a hold has _staged_ is still on screen and holds for every other change until the commit removes it; a removal nothing holds releases at once. Removing a flight's last on-screen reader releases the write it held, even when a `latest()` of the flight follows. +Two changes whose async work is observed by a shared on-screen derivation settle as one unit; changes on fully disjoint graphs settle independently. "Observed by a shared reader" is a pass observing the flight pending — a render effect's mere existence entangles nothing (D-3). A second flight reaching a memo another hold already holds enters that hold at the propagation. A reveal that discovers an async already in flight joins the hold that flight blocks, as its first observer if nothing displayed it before; when the flight's inputs are themselves still held elsewhere, the reveal is a stale reader of that other hold. A reader whose removal a hold has staged is still on screen and holds for every other change until the commit removes it. A hold lasts while an on-screen reader still observes a flight it waits on; when the last such reader leaves — disposed, re-run without the read, reset behind a fallback — the writes it kept for those readers alone commit at once; a display-ahead reader never keeps it open, and a `` reset moves the wait onto the boundary (D-12). -**Collapses:** [A15](./SPEC-ASYNC-SEMANTICS.md#a15-transition-entanglement-is-graph-driven-lanes-settle-as-one-reveal) and its corollaries (#3407, #3443, #3305/#3334 reveal, #3458 first observer, #3463 zombies, #3494 through-a-memo and beside-a-release), [O3](./SPEC-ASYNC-SEMANTICS.md#o3-a-render-effect-gated-away-from-a-never-landing-flight-keeps-the-sources-write-held--fixed). **Status:** standing; fuzzer finding F2 ("a landing does not reveal a gate its stale reader re-derives onto a new flight") pinned in `tests/fuzz-findings-l2.test.ts` (built, #4071). +**Status:** standing. Absorbs D-11 (2026-11-06). Mechanism and history: `INTERNALS-ASYNC-STATE.md` §7. ### D-10. The committed frame keeps its inputs until it is replaced @@ -88,65 +80,41 @@ A held pass has not replaced the committed frame, so the committed value still d ### D-11. A hold ends when its last on-screen reader leaves -**From:** F-3. **Scenarios:** S-13. - -A hold lasts while an on-screen reader still observes a flight it waits on. When the last such reader leaves — disposed, re-run without the read (a gate closing), reset behind a fallback — the writes the hold kept for those readers alone commit at once; a hold is never kept open by a reader that no longer derives from the flight, nor by a display-ahead reader. A `Loading` reset moves the wait onto the boundary instead (D-14). - -**Collapses:** [O3](./SPEC-ASYNC-SEMANTICS.md#o3-a-render-effect-gated-away-from-a-never-landing-flight-keeps-the-sources-write-held--fixed), #3426, #3372, #3375, A23 (a probe is never a blocker). **Status:** standing. - ---- +**Folded into D-9 (2026-11-06).** D-9's last clause and F-3's statement. Rows and pins moved to D-9; the text is archived in `INTERNALS-ASYNC-STATE.md` §7. ## B. Boundaries (F-3, F-4, F-5) -### D-12. An armed `` whose mount is not held shows its fallback now; its content appears when ready +### D-12. An armed boundary catches what is pending under it; its fallback is seen when the frame that armed it is -**From:** F-4, F-7, F-6. **Scenarios:** S-14, S-40. +**From:** F-4, F-3, F-5, F-7, F-6. **Rows:** S-14, S-15, S-16, S-17, S-24, S-40. **Pins also:** `tests/boundary-not-born-held-3540.test.ts`, `tests/fresh-boundary-born-held-reader.test.ts`, `tests/loading-fallback-in-flush-3540.test.ts`, `tests/fuzz-findings-l2.test.ts`. -An armed `` catches the render effects under it that are pending (F-4) — membership is the render effect's position, not where a memo was created — whether the read is a first load, a held value, or a memo born held over one; what it catches holds nothing outside it. When the frame that mounts it is not held — a click with nothing else holding, a mainline flush, a root `render`, a mount nested under a disarmed boundary, a separate change made while another hold is up — that frame is shown now, so the fallback is seen now, and the content appears when it is ready: with that hold's commit for content that derives from a hold, at the landing for its own first load. A reader elsewhere on screen that holds the same value does not hold that frame: `save` holds `count = 1` with `Count 0 | Slow 0` on screen, and a separate `setOpen(true)` opens `{m()}` with `m` a memo of `count` — the screen is `Count 0 | Slow 0 | open true | [fallback]`, then `Count 1 | Slow 1 | open true | count 1` at `save`'s commit (S-14). The fallback is not seen when the mounting frame is itself held: the boundary is part of the held change (D-13), or a render effect in the same branch, outside the boundary, holds it (F-2). Content under it that is bound by a render effect (`

{m()}

`) is caught the same way, so the boundary never reveals empty content. Content under it that is only loading (its own first fetch) appears at the landing, and the hold does not wait for it (F-4). A derivation _outside_ the boundary in the same flush still holds the tick (D-4). A fresh boundary whose content reads only `latest` of a held value, and nothing pending, shows that value now. A sibling that reads the unresolved async stays on the old content. The boundary does not wait for the sibling (F-6, S-40). `isPending` of the boundary's value is false: the fallback is not a value of the source. `isPending` and `latest` of the content behind it are that content's own: `latest` of a memo born held is its pending value, and `isPending` of it holds its reader as its read does — read inside the boundary, the reader is caught with the content (F-9, D-22, D-23). +An armed `` catches the render effects under it that are pending — a first load, a held value, a memo born held over one, a lane's own async — by their position under it, wherever the memo was created; what it catches holds nothing outside it, so a transition or a lane whose only pending readers sit behind the fallback reveals at once, while a reader of the same async outside the boundary still holds. Whether the fallback is seen is the frame's: when the frame that mounts the boundary is not held, the fallback shows now and the content appears when ready — with the hold's commit for content that derives from a hold, at the landing for its own first load; when the mount is itself held — a `` over a held value, content a hold has not revealed, a pass that joined the hold — the boundary appears at that hold's commit, on its fallback if its content is still loading. An `on` reset moves the wait onto the boundary: the readers it sends back to the fallback stop holding the change, and the fallback stays until everything under it has settled, flights started before the reset included, then one reveal. A disarmed boundary forwards instead (F-5). -**Collapses:** [A29](./SPEC-ASYNC-SEMANTICS.md#a29-a-tracked-read-served-a-live-transactions-staged-value-enters-that-transaction) boundary exemption (#3540), "A boundary that has not shown content owns its subtree" (2026-10-06) and #3824, direction rule bullet 4, B-FRESH, B-ROOT, recovery 1.1–1.4, 8.1; the dropped A35 / fuzzer MH8 (Flagged). **Status:** standing (`tests/boundary-not-born-held-3540.test.ts`; the reader's position, a memo created outside the boundary: `tests/fresh-boundary-born-held-reader.test.ts`, #3977; a memo created inside the boundary: `issue-3800-repro.test.ts`, built 2026-10-14 (U-1) — the boundary catches it and its answer is staged with the hold, which does not wait for it). Ruled 2026-09-18 (#3540: "A `Loading` mounted while a write is held shows its fallback; the hold stays with readers that have content to keep") and reaffirmed 2026-10-09. A draft of 2026-10-09 that kept the fallback off screen while any outside reader held the same value is withdrawn; so is the 2026-10-06 "fresh mount stays closed beside an outside reader" draft (withdrawn 2026-10-07). +**Status:** standing. Absorbs D-13, D-14, D-21 (2026-11-06). Mechanism and history: `INTERNALS-ASYNC-STATE.md` §7. ### D-13. A boundary whose own mount is held appears at that hold's commit -**From:** F-4, F-7. **Scenarios:** S-15. - -Whether a fallback is seen depends on whether the boundary is on screen. A fresh boundary opened by `` with `x` held, or nested in content a hold has not revealed, is not on screen until that hold commits; it appears then, showing its fallback if its content is still loading, its content otherwise. A boundary mounted by a pass that itself joined a hold is the same case. - -**Collapses:** the 2026-10-06 boundary-scope ruling ("a boundary mounted as part of a hold appears at that hold's commit"), #3869's "a boundary a held memo mounts in the same flush … appears with the memo at the commit". **Status:** standing. Built (U-3, 2026-10-14): a born-held first pass is not caught when the flush has joined the hold and the pass is its own, so the boundary the mount creates reads its content with the hold and appears at its commit — `tests/loading-fallback-in-flush-3540.test.ts` "a boundary mounted by a pass that joined the hold appears at its commit, no fallback", and the `flight` forms of "a boundary mounted by the hold" (a `` over a landing, flat and nested). Still order-dependent: a boundary created in the mount before anything in it joins the hold prepares its fallback first (the synchronous first render into the unattached branch) and swaps at the commit, never painted. The same gap shows when a render effect outside the boundary holds the mount (D-12): the boundary prepares its fallback with the held change and swaps to its content in the commit's own flush — never painted, but run (`tests/fresh-boundary-born-held-reader.test.ts`, the outside cases). +**Folded into D-12 (2026-11-06).** the held half of "the fallback is seen when the frame that armed the boundary is". Rows and pins moved to D-12; the text is archived in `INTERNALS-ASYNC-STATE.md` §7. ### D-14. A fallback-caught flight holds nothing; an `on` reset moves the wait onto the boundary -**From:** F-3, F-5. **Scenarios:** S-16, S-17. - -A flight whose only observers are render effects behind a fallback holds no change — a memo under the boundary that no render effect reads was never an observer at all, and one rendered by an effect outside the boundary is that effect's read (F-3). In both orders: a render effect created under a fallback never counts, and a reader that counted while the boundary showed content stops counting the moment `on` sends it back to the fallback. Writes the hold kept for those readers alone commit at once (`Boundary: Loading... | Sum: 2` publishes as one frame); a reader outside the boundary still holds (`Sum` waits until `details` lands). The wait does not vanish; it moves onto the boundary: after the reset the fallback stays until every reader under it has settled — including readers whose flights started before the reset — then one coherent reveal. - -**Collapses:** [A33](./SPEC-ASYNC-SEMANTICS.md#a33-a-fallback-caught-flight-holds-no-transaction-a-loading-reset-moves-the-hold-onto-the-boundary) (#3375, #3459), §23.1 item 1, recovery 2.2. **Status:** standing. The nested form — an outer `on`-reset boundary not waiting for a flight its nested `on`-reset boundary catches — is built (U-4, 2026-10-13: a reader a nested collecting boundary holds is that boundary's alone; `tests/fuzz-findings-l2.test.ts`, fuzzer finding F1). The re-arm over a hold is built (U-9, 2026-10-14): content an earlier hold staged under the boundary, or that reads a hold after the re-arm, is the boundary's — the fallback shows now and the content reveals at that hold's commit, which does not wait for it; no redraw loop (`tests/loading-fallback-in-flush-3540.test.ts`, the re-armed shapes; `tests/loading-on-frame-following-3540.test.ts` §3b, §3c). A fallback staged into the hold its readers land in folds back at any seam, not only the re-arm's own: readers still pending at the re-arm land into that hold a flush later, and the content is ready when it commits (#4379, `tests/loading-on-held-filter-refresh-4379.test.ts`). +**Folded into D-12 (2026-11-06).** F-3, F-4 and F-5 restated; the reset clause is now D-12's last sentence. Rows and pins moved to D-12; the text is archived in `INTERNALS-ASYNC-STATE.md` §7. ### D-15. `on` is a dependency list; the swap lands with the frame of the change that caused it -**From:** F-4, F-5, F-7. **Scenarios:** S-18. - -`on` is a tracked expression whose value is irrelevant; a write to anything it reads re-arms the boundary. Optimistic writes and a source going pending notify like any write; an expression that reads nothing reactive never does; a zero-argument function is a tracked accessor, not a callback. The fallback follows the frame of that read: it lands with the change that caused it — now, when nothing else on the page waits on that change (`[A] → [B + spinner] → [B + comments]`); together with the rest of the new page during a held navigation (the shell reads `product(id)` outside: `[A] → [B + spinner] → [B + comments]` with the spinner arriving with B, not beside A). While that frame is held, the boundary has entered the fallback and the screen keeps the old content; the fallback is seen only if the hold lifts before the new content is ready. If the content is ready when the frame commits, no fallback is ever shown. The children are not re-created. - -**Collapses:** #3575 (GabbeV's design, ruled 2026-09-21), B-ON, B-FRAME, recovery 4.1/4.2/5.1/5.2, the `createLoadingBoundary` JSDoc, `05-async-data.md`. **Status:** standing (`tests/loading-on-frame-following-3540.test.ts` §1–§2). +**Folded into D-17 (2026-11-06).** D-17's first sentence. Rows and pins moved to D-17; the text is archived in `INTERNALS-ASYNC-STATE.md` §7. ### D-16. A re-arm whose data is also read outside the boundary never shows its fallback; a race lost is not reported -**From:** F-5, F-7, F-2. **Scenarios:** S-19, S-20. - -When the frame of an `on` change waits on the very source the boundary waits on — because a live reader _outside_ the boundary also reads it (a sibling `` over the same data, a header that renders it) — the re-arm has entered its fallback, and that fallback is not shown. Only a reader whose read throws holds; an `isPending()` or `latest()` of the data in the header does not (F-2, F-6) — unless the same render effect also reads the data plainly: that read holds (F-6). The hold lifts when the data lands, the content is ready in the same frame, and the fallback is never seen: `[A] → [B + comments]`. DEV reports `LOADING_ON_OUTSIDE_HOLD` once, at the change; the fix is structural (one boundary owns the read). A fresh mount by a separate change is not this rule: the held change is not its frame, so its fallback shows now (D-12). The diagnostic is re-arms only. This is only the flight the `on` change itself started: an outside reader of a flight the change did not start, or content an earlier action holds by a staged write, does not keep the re-arm's fallback from showing now (§3b, §3c of the frame-following pins). A frame held past the content's landing by something _else_ — the write's `action` staying open, other data the shell waits on — is a race the fallback may still win, a legitimate outcome, and nothing is reported (S-20). - -A header that reads `latest(id)` beside `on={id()}` shows the new id beside the boundary's old content for the whole hold. That is `latest` (F-6). The boundary's `on` still reads the held value, so its fallback stays off screen; the header does not pull it on screen. - -**Collapses:** #3575, #3584 (the after-the-fact report removed), B-SAME-SOURCE, B-RACE, recovery 5.3/5.4, `08-dev-diagnostics.md` `LOADING_ON_OUTSIDE_HOLD`. The 2026-10-06 sentence "committed content included, belongs to that boundary" (deferred in #3824) is this rule's off-screen half. Read as a visible spinner beside the outside reader's stale data, it is withdrawn (2026-10-09). **Status:** standing, ruled 2026-10-09 (`loading-on-frame-following-3540.test.ts` §3, §5). +**Folded into D-17 (2026-11-06).** a consequence of D-17: the `on` read of a held value is the hold's work, so the swap lands with the frame; the outside-hold clause and the diagnostic are in D-17. Rows and pins moved to D-17; the text is archived in `INTERNALS-ASYNC-STATE.md` §7. -### D-17. A display-ahead read in `on` shows the fallback now, beside the held frame +### D-17. A re-arm shows its fallback with the frame of the `on` pass -**From:** F-6, F-5. **Scenarios:** S-21. +**From:** F-6, F-5, F-4, F-7. **Rows:** S-18, S-19, S-20, S-21, S-48, S-49. **Pins also:** `tests/loading-on-frame-following-3540.test.ts` §1–5, `tests/loading-on-rearm-3540.test.ts`, `tests/loading-on-latest-fallback-4337.test.ts`. -`on` re-arms from whatever it reads, and the fallback appears when that read's frame has landed (D-15). `on={latest(id)}`, an `isPending()` or an optimistic signal reads a value whose frame has already landed, so the fallback shows now, beside the page that is still held: `[A] → [A + spinner] → [B + spinner] → [B + comments]`. The hold still owns every read of the held value. Content under the boundary stays off screen until it is ready; content under it that reads nothing held reveals as soon as it lands. No diagnostic. The `[A + spinner]` frame is the new id's loading state inside the old page, the same shape as a header showing the new id early. A memo between `isPending` and `on` behaves as the direct read does. What the _fallback_ reads is not `on`: a fallback reading `latest(id)` shows the new id once it is on screen (F-6), but the swap that `on={id()}` asks for still lands with its frame (D-15), never beside the held value (#4337). +`on` is a dependency list: a change to anything it reads arms the boundary, and what it returns is never compared — optimistic writes and a source going pending notify like any write, an expression that reads nothing reactive never does. The pass that reads it is a pass like any other. One whose every read of a held value was ahead (`latest`, `isPending`, a guess) has a frame that has landed, so the fallback shows now beside the held screen (S-21); one that read a held value plainly — a value it did not also read ahead (D-25) — is that hold's work, and the swap lands with the hold's frame (S-18, S-48; the mixed `on`, S-49, ruled 2026-11-05). While that frame is held the boundary has entered its fallback and the screen keeps the old content; the fallback is seen only if the hold lifts before the new content is ready. When the frame waits on the very data the boundary waits on — a reader outside the boundary of the same source — the content is ready in the same frame and the fallback is never seen; DEV reports `LOADING_ON_OUTSIDE_HOLD` once, at the change, for re-arms only, and a race lost the other way is not reported (S-19, S-20). What the fallback itself reads is its own display-ahead, not the swap's. -**Collapses:** #3575, B-AHEAD, recovery 6.1, §27 7a (#3528), `tests/loading-on-frame-following-3540.test.ts` §4, `tests/loading-on-rearm-reveal-3540.test.ts` ("on: () => latest(dep)"). **Status:** standing, ruled 2026-10-09 as the frame rule, not a break through the hold. +**Status:** standing; S-49 ruled, not built. Absorbs D-15, D-16 (2026-11-06). Mechanism and history: `INTERNALS-ASYNC-STATE.md` §7. ### D-18. A fallback that itself reads pending data @@ -166,81 +134,49 @@ Every _queued_ run behind a fallback — render-effect updates and user effects, ### D-21. An armed boundary lets a lane reveal: the async under the fallback does not hold it -**From:** F-4, F-3, F-6. **Scenarios:** S-24. - -An armed boundary catches a lane the same way it catches a transition (D-12, D-14). A fallback lets a transition end early because the reads under it hold nothing; a fallback lets a lane end early for the same reason. A display-ahead value whose own async sits under that fallback is not held by it, so it reveals immediately, beside the fallback: header `1 | loading`, then `1 | details 1` when the async lands. A reader of that same async outside the boundary still holds the lane, as an outside reader still holds a transition. A disarmed boundary is not this rule: it forwards, and the lane holds on the async under it until that lands (F-5). - -**Collapses:** the fuzzer-finding F6 ruling's fallback half (2026-10-05, "a boundary on its fallback still holds nothing", A33, B5). The showing-content half of that ruling is F-5, pinned by `tests/fuzz-findings-l2.test.ts` "F6: a lane holds through a Loading boundary showing content". "A boundary output that never committed holds nothing" (2026-10-05) is F-3. **Status:** standing, as the lane form of D-12 and D-14. A header `latest(id)` beside an `on={id()}` re-arm is D-16. - ---- +**Folded into D-12 (2026-11-06).** the lane form of D-12: an armed boundary catches a lane as it catches a transition. Rows and pins moved to D-12; the text is archived in `INTERNALS-ASYNC-STATE.md` §7. ## C. Display-ahead readers and verdicts (F-6, F-9) -### D-22. `latest(x)` reads the newest value a flush has carried; it throws before a first value +### D-22. `latest(x)` shows the newest answer `x` has produced; before any, it throws -**From:** F-6, F-10, F-9. **Scenarios:** S-25, S-45. +**From:** F-6, F-10, F-9. **Rows:** S-25, S-44, S-45. **Pins also:** `tests/latest-held-till-flush.test.ts`, `tests/latest-repeated-writes.test.ts`, `tests/latest-unread-identity-memo-4474.test.ts`. -`latest(x)` answers the held write (the newest value a flush has processed, held or not — for a memo nobody has read, the value the flushes it missed would have given it), the optimistic value where one displays, the arrived truth under a superseded override, and the committed value of a node whose own flight is up (below the async nothing newer exists). A sync derivation of a held source is visible through it (the held recompute is a write path like any other). It throws `NotReadyError` only when no value has been produced — before a first landing, including an async memo with no resolution — in every scope. It returns `T`, never `undefined`. A lane held on async downstream of the read does not paint, and does not hide a value `latest` can already return. A memo behind a fallback, or in a branch not yet attached, has still produced its value, so `latest` returns it — a memo born held included: its plain read throws, and `latest` returns its pending value (F-9). Resolved async never pairs `[false, undefined]`. **A first landing a hold carries is a produced value (clarified 2026-10-28, amended 2026-10-30):** an async memo's first answer that lands in the same tick as a write to its input that a hold carries — an action's, or a plain write re-asking it — is held with it (D-4), and `latest` of it returns that answer while the plain read still throws (F-9); a `Loading` whose only read is `latest` of it shows the answer now (S-40). The frame is whole: the screen's committed input beside the answer to it. **A memo nobody has read has still produced what the flushes it missed would have given it (ruled 2026-10-30, #4474):** `latest` of a memo on the observation lifecycle (unowned, or `lazy: true`) whose sources have committed values is that value, computed on the committed world (laziness is never observable — #3078, #4366, #4474). An identity memo over an async memo, read for the first time in the flush that re-asks the async memo, shows the async memo's landed answer and is pending on the re-ask, the same screen as with a prior reader of it; a plain read of it in that mount is not display-ahead (D-27): its reader is born held (D-2) and shows the re-ask's answer. **Inside a flush, `latest()` of a staging not yet held is the frame's read like any other (clarified 2026-10-28):** it serves the staging, opens no hold, and leaves a plain read beside it in the same pass reading the staging too (F-10: one frame for every channel); if the flush parks, the pass re-derives once as a verdict reader (D-2, A10). A `latest()` read that manufactured a hold for the frame so its pass could run once made the plain reads after it see the screen in a flush that then committed (#4365). +An answer is a committed value, `x`'s own async landed — into a hold or not — or a declared first paint; a sync derivation of a held value has produced one too. `latest(x)` is the newest of these and never `undefined`; before a first one it throws `NotReadyError` in every scope. A slot that is displaying a guess has the guess as its newest value: `latest` of it shows the guess until the hold commits (ruled 2026-11-05, decision 3; S-44). A write is visible to `latest` from the flush that carries it, not before (S-25). Laziness is never observable: a memo nobody has read has produced what the flushes it missed would have given it (#4474). -**Collapses:** [A28](./SPEC-ASYNC-SEMANTICS.md#a28-a-write-becomes-visible-at-flush--to-every-channel) (`latest` reads the flushed staged world), [A11](./SPEC-ASYNC-SEMANTICS.md#a11-sync-derivations-of-held-sources-are-visible-through-latestispending), [A7](./SPEC-ASYNC-SEMANTICS.md#a7-resolved-async-never-reads-false-undefined), [A17](./SPEC-ASYNC-SEMANTICS.md#a17-an-active-override-is-the-displayed-value-until-its-transaction-commits-and-the-graphs-value-until-its-own-source-answers) (`latest` returns the arrived value). **Status:** standing. **Fixed 2026-10-31 at the release (#4474).** The memo was not lazy but unowned — a memo on the observation lifecycle (unowned, or `lazy: true`) computes eagerly and, blocked on an uninitialized async source, is re-enqueued by that source's landing like any dependent; the settle walk queued its retry and then, finding it unobserved, released it (`async.ts`: `releaseIfSettledUnobserved` → `unobserved` pulled the retry back out of the heap), so the node never computed the landing and woke later with no value. Now the release waits for the pass the walk queued (REACTIVE_RELEASE_AFTER): the retry runs — once, as the owned twin's does — and `recompute`'s tail releases the node if it is still unobserved and settled (#2934's no-perpetual-recompute contract kept; #3078's read-revives and pre-heap reclaim untouched). Both read orders read as ruled through the existing paths (`tests/latest-unread-identity-memo-4474.test.ts`, `web/test/latest-unread-identity-memo-4474.spec.tsx`, `tests/released-memo-retry-runs-4474.test.ts`). A fix at the wake was built first and parked on 2026-10-31 (reference branch `fix/core-batch-4474-4417`, commit `ed85ca2a3`: +186 B brotli on the core floor and a generator regression, 91501 #15992) — history, not the mechanism. Consequence worth knowing: a settle's one retry is a compute — an unobserved async memo over a landing source starts the one flight its owned twin starts, then is released at that flight's landing. +**Status:** standing; the guess clause ruled, not built (#4689). Mechanism and history: `INTERNALS-ASYNC-STATE.md` §7, the 2026-11-06 entry. -### D-23. `isPending(x)`: three causes, two exceptions, question-scoped, atomic with the value +### D-23. `isPending(x)` is true while a new question about `x` has no committed answer -**From:** F-9, F-1. **Scenarios:** S-26, S-27. +**From:** F-9, F-1, F-12. **Rows:** S-26, S-27, S-28. **Pins also:** `tests/question-scoped-pending.test.ts`, `tests/latest-isPending-consistency.test.ts`, `tests/ispending-combined-atomic-3442.test.ts`, `tests/untracked-verdict-rederive.test.ts`. -`isPending(x)` is `true` while any of three causes holds: (i) a write held by a live hold; (ii) the node's own async in flight for a new question; (iii) a fresh value landed but held uncommitted by a hold it is entangled with — and `false` the moment none does. Exception 1: a node with no committed value — a first load, or a memo born held over a held value (D-2) — is loading, not pending. A node answered and then re-asked in the same flush — an async first answer that lands in the tick a write re-asks it — is not a first load (amended 2026-10-30): its answer is a produced value, so `latest` of it shows the answer and `isPending` of it is `true`, the re-ask being a new question; so is a derivation of it with no value of its own, at any depth (F-9, D-22). It has no verdict: inside an owner `isPending` of it does what its read does — a first load's `NotReadyError` goes to a boundary or holds the change (F-2); a reader of a memo born held is held with it, or caught by a boundary that has not shown content — and the reader runs at the commit, where the answer is final; unowned it is `false` (D-24). A node with a committed value is judged wherever its readers are: on screen, behind a fallback, or in a branch not yet attached. **A node initialized while pending by propagation is committed and pending (ruled 2026-10-31, #4569):** an answer that lands on a node with no committed value while an upstream re-ask has marked it pending by propagation — its own flight still current, nothing superseded it (#3373) — initializes the node: a node's question changes only when the node itself re-runs, so the answer is current, and the node is committed and pending on the upstream flight. Content that reads only its verdict reveals with `true` (S-40: a fresh boundary reading only a display-ahead channel shows what it has); content that reads the node holds until the chain settles — a first reveal has no screen to keep (F-2). That asymmetry is the rule, not a bug; a reading that kept the node loading would need a supersession by propagation no rule has, a new resting state, and would treat an uninitialized node more strictly than a committed one. A verdict on content that may have no committed value is read inside the boundary that owns it — `

{m()}

{isPending(m) ? "updating" : ""}
` — where its reader is caught with the content; read outside, it holds the change that mounted it. Either way no answer is shown before the commit: `` is never seen enabled over an unjudged value (`packages/web/test/ispending-guard-born-held.spec.tsx`). No verdict is the honest answer: `true` would say "updating" for something never shown, and `false` would read as settled to a guard like `if (!isPending(x)) save(x())`. Exception 2: a re-ask of the same question is quiet; an `affects()` mark is a declared new question and pends exactly the marked data and what derives from it, released when the declaring hold settles (or at flush end, ambient). `[isPending(x), x()]` read in one scope never pairs `true` with the fresh value: a verdict reader sees the screen — in either read order — unless it is a node a hold's fetch is waiting on (D-25). `isPending(() => latest(x))` follows `x`'s own non-quiet async only — on a signal or sync memo the latest form is never pending — so `[isPending(() => latest(x)), latest(x)]` never pairs `true` with the fresh value either. A spinner driven by `isPending` fires while the owner's async is still in flight (it does not wait for the hold). +True while an input change is held, while `x`'s own flight for a new question is up, or while a landed answer waits in a hold; false the moment the shown value is final. A re-ask of the same question is quiet; an `affects()` mark is a declared new question; a guess is verdict-inert. A node with no answer (D-22) has no verdict: owned, the probe does what a read does — it suspends or holds; unowned, it never throws and reads `false`, as it does for a thunk that throws a real error. Untracked inside a computation the probe is a snapshot. Its thunk's return value is never inspected: the probe is reads-only. `[isPending(x), x()]` read in one scope never pairs `true` with the fresh value, in either order; the `latest` channel is judged on its own (A8, S-27). -**Collapses:** [A19](./SPEC-ASYNC-SEMANTICS.md#a19-ispendingx--the-observable-value-is-not-final-three-causes), [A24](./SPEC-ASYNC-SEMANTICS.md#a24-question-scoped-pending-pending-iff-a-value-change-is-in-flight-or-an-affects-mark-is-live), [A10](./SPEC-ASYNC-SEMANTICS.md#a10-ispendingx-x-is-atomic-within-one-scope), [A8](./SPEC-ASYNC-SEMANTICS.md#a8-ispending--latestx-follows-xs-own-async-only--verdicts-are-per-channel), [A14](./SPEC-ASYNC-SEMANTICS.md#a14-companion-nodes-get-child-lanes-that-do-not-merge-with-the-owner), [A20](./SPEC-ASYNC-SEMANTICS.md#a20-superseded-optimistic-writes-announce-a-store-wide-pending)/[A21](./SPEC-ASYNC-SEMANTICS.md#a21-superseded-the-store-wide-mask) (superseded — the mask is gone), recovery 6.3. **Status:** standing. A probe alone holds nothing; a plain read beside it holds (`optimistic-settle-verdicts.test.ts` #3409, `question-scoped-pending.test.ts` 3.4; #3977). `isPending` of a memo born held, read inside and outside the boundary: `tests/fresh-boundary-born-held-reader.test.ts` (#3977; the engine joins the reader to the hold as the read does, which is the throw's observable effect). A first load answered while pending by propagation from an upstream re-ask, under a boundary (the issue's verdict-only content, a 3-deep chain, the plain-read controls that hold) and in a root: `tests/first-load-superseded-answer-4569.test.ts`, `packages/web/test/first-load-superseded-answer-4569.spec.tsx` (#4569, ruled 2026-10-31). **Known deviation (#4469, 2026-10-30):** a verdict reader is re-run at every landing of a hold it observed, even when the landing changes nothing it read; the gate that would skip it needs a deps walk. Measured 2026-10-30: two cheaper gates broke F-9/D-23 (verdict-memo-flight-no-reask) and #4189 (errored-store-probe-settled-3962) pins and failed the frozen floors by +31/+36 B. A plain reader of the same recovered hold is not in this: it shows the committed value for the whole hold and runs once, at the commit, since a recovery to a value equal to the last good one is still a change of answer (D-41, D-9). Pinned in `tests/hold-recovers-landing-reruns-4469.test.ts` (the verdict rows `it.fails`). A verdict reader of a signal whose writes net to its committed value in the tick an action opens a hold is not in this either (#4485, fixed 2026-10-30): the writes made no proposal (D-8), so the reader is not the hold's stale reader and `isPending(s)` stays `false` (`tests/netted-verdict-reader-4485.test.ts`). +**Status:** standing. Absorbs D-24 (2026-11-06). Mechanism and history: `INTERNALS-ASYNC-STATE.md` §7. ### D-24. `isPending` never throws unowned; the probe is reads-only -**From:** F-9, F-12. **Scenarios:** S-28. - -`isPending(fn)` returns `false` from an unowned caller (an event handler, imperative code) whose thunk throws a real error or reads a node with no committed value (a first load, or a memo born held — D-23); inside an owner — a component body, a computation, `untrack()` within either — the reader participates in loading boundaries: a first load's `NotReadyError` propagates, and a reader of a memo born held is held with it (D-23). In DEV, a plain `untrack()` in a component body that reads a pending memo the same body created throws `PENDING_ASYNC_UNTRACKED_READ` instead: its `NotReadyError` would re-run the body onto a fresh pending memo forever. Untracked inside a computation, `isPending`/`latest` is a snapshot, as any untracked read is: it does not make the computation a verdict reader, so the computation is not re-derived when its own dependencies go pending — a throw still reaches it through `untrack`. Event handlers are the supported untracked use (#3946: a router's link claim probing inside the computation that creates the anchor). `latest()` shares the mechanism but throws in every scope (D-22). The probe's thunk return value is never inspected: `isPending(() => store)` reads nothing and reports `false`; whole-store questions are asked through reads. (The direct-argument form `isPending(store)` is accepted API for post-2.0.) - -**Collapses:** [A16](./SPEC-ASYNC-SEMANTICS.md#a16-ispending-never-throws-in-untracked-contexts) (B5/B5a; wording corrected 2026-09-14), [A23](./SPEC-ASYNC-SEMANTICS.md#a23-the-ispending-probe-is-reads-only). **Status:** standing. The untracked snapshot: `tests/untracked-verdict-rederive.test.ts`, `packages/solid/test/show-untracked-verdict.spec.ts` (#3946). +**Folded into D-23 (2026-11-06).** one sentence of D-23. Rows and pins moved to D-23; the text is archived in `INTERNALS-ASYNC-STATE.md` §7. -### D-25. A display-ahead reader's own async is held; a memo computes under its own posture +### D-25. A display-ahead read never holds the change it reads ahead of; its reader's own async holds the reader -**From:** F-6, F-1. **Scenarios:** S-29. +**From:** F-6, F-1, F-3. **Rows:** S-27, S-29, S-30, S-31, S-39, S-40, S-46, S-49, S-50. **Pins also:** `tests/ispending-combined-atomic-3442.test.ts`, `tests/verdict-memo-held-branch-4544.test.ts`, `tests/latest-memo-inline-parity.test.ts`, `tests/lane-render-effect-grouping-4284.test.ts`, `tests/display-ahead-plain-write-content.test.ts`. -What is rendered from a display-ahead read reveals together with it: an async memo over `latest(x)` holds the `latest` view's reveal until it lands; a combined `isPending(() => [fast(), copy()])` over two async memos, one wrapped in a sync memo, stays `true` until both land, because a memo's value is one shared slot — it computes under its own hold posture, never under the posture of whichever reader pulled it. A node whose fetch a hold is waiting on is that hold's work, not a verdict reader: a pass that re-runs it while its fetch is in flight reads plainly — following the change it is fetching for — even if an earlier pass of it called `isPending` or `latest` (#3884). A memo's posture is what it reads: one that reads a verdict reader's value is a derivation of a display-ahead read, and a verdict reader itself — `createMemo(() => [held(), V()])` with `V = createMemo(() => isPending(x) && latest(e))` shows `V`'s verdict now, beside the screen's `held()`, as the inline probe and a render effect reading both do (#4050: a Provider's children memo over a router shell). **The probe decides the pass's posture in either read order (clarified 2026-10-28):** a pass that probes is a verdict reader whether the probe came before or after a plain read of the same held node — `[x(), isPending(x)]` and `[isPending(x), x()]` both show `[committed, true]` now and re-derive at the landing (A10) — unless the pass is the hold's own work: born in its world (D-27, #4341) or re-run by the write it holds (#3884 above). Those two read plainly; a probe does not rescue them. A pass that read two holds' display-ahead values is one reveal unit of both (A15 lanes corollary, #3335): it waits for either lane's flight, never showing one hold's `latest()` beside a half-computed derivation of the other's. +A pass's posture is decided by its own reads in that pass, never inherited from a creator or a memo it reads (#4544) and never carried into its next run. Each `latest` or `isPending` read sees the hold's world and joins nothing. A plain read of a node this pass also read ahead is the screen's: the two channels of one node agree within a scope, so `[x(), isPending(x)]` shows `[committed, true]` now in either order (A10, S-27). A plain read of any other held value makes the pass the hold's work (D-1): a mixed `on` waits (S-49), and a memo that probed on its last run and probes nothing on this one holds the tick (S-50, both ruled 2026-11-05). Whether a read throws is that read's own: an earlier display-ahead read does not disarm a later plain read's throw (S-39). A branch a display-ahead read gates is not derived from it: a mount under ` 0}>` is a mainline mount whose plain reads are the screen's — `latest(x)` inside shows ahead at its own site — and a first pass under it that reads the hold's staging is born held (D-1), attaching at the landing, on its fallback inside a `` (S-31, S-46). A render effect off a lane that reads what the lane is revealing — mounted mid-hold, or re-run by an unrelated write — shows the committed value and re-derives at the reveal (S-30). What is rendered from a display-ahead read waits for that read's own async, as one reveal (S-29). -**A verdict posture outlives the pass that asked (ruled 2026-10-28):** a pass that asked (`isPending`, `latest`) is the hold's verdict reader on every later run of its node until one completes having asked nothing — that run still reads as a verdict reader; the next is an ordinary derivation. On each such run its plain read of a node the hold holds is the screen's (a frame reader, A10; re-derived at the landing as A15's stale reader) and it joins nothing, whoever dirtied it — a sync write, a boundary redraw, a landing. The hold's own work is not in this: a node whose fetch the hold waits on, or a pass born in its world, reads plainly whatever it asked before (the #3884 clause and D-27 #4341 above, unchanged). A boundary's display-ahead swap is this posture handed on: an `on` pass that read `latest`/`isPending` is a verdict reader, and the fallback swap it asks for runs as that reader's work — the output's pass reads nothing of the held frame (a pending tree is linked, not read) and its answer, the fallback, is shown now beside the held frame and stays the screen until the landing, instead of being staged with the write's frame as a plain `on`'s swap is (D-15): `[A] → [A + spinner] → [B + spinner] → [B + comments]`, never `[A] → [B + spinner]`. Only the `on` pass's asking counts: `latest()` inside the fallback is the fallback's own display-ahead, not the swap's — a plain `on` over an action-held input lands the swap with the frame whatever the fallback reads (#4337). Today two memories carry this — `CONFIG_VERDICT` left on the node by the pass that probed, and the node's listing in the hold's verdict lane (`fallback`'s `b._lane`, `recompute`'s head) — and the rule is the posture, not the membership. Pins: `tests/loading-on-rearm-3540.test.ts`, `tests/loading-on-frame-following-3540.test.ts` §4–5, `tests/loading-on-latest-fallback-4337.test.ts`. **An inherited posture is not a probe (2026-11-02, #4544):** a pass whose verdict posture is only inherited — it read a verdict memo, and neither it nor its node's last pass opened a window of its own — is the hold's work where it reads the hold's own write (a sync write the hold holds, not a flight's answer): it is served that staging and joins the hold, as a plain derivation does. One flush or several make no difference (F-10): before the flush parks, the pass reads the flush's own write as that staging, leaving the verdict lane an earlier read of the verdict memo seated it in, as the other read order keeps it out. Not a staging a window asked of under the hold that holds it (or in this flush, before one): `[latest(x), x()]` through a memo stays atomic (A10, D-26: `tests/latest-memo-inline-parity.test.ts`). That mark is the hold's — it expires when the hold lands or dissolves, and a later hold reads the node as never asked. A pass that probes in its own body stays the hold's verdict reader and reads the screen (F-6 over a staging it holds). When a frame born into the hold (a render effect never committed) reads such a reader's settled value and goes pending on what that display-ahead value asks for, the pending does not hold the hold (F-6: display-ahead readers never hold the change they read ahead of): the hold lands, the reader re-derives at the landing (A15) and the frame with it. A lane's reveal still waits on it (S-29). A store key is served the same with or without a node (R2, signal parity): a node-less staging is this flush's own write with no node a window could have asked through, so the posture alone decides (2026-11-04, #4681). Pins: `tests/verdict-memo-held-branch-4544.test.ts`, `tests/posture-store-parity.test.ts` S9. - -**Collapses:** [A31](./SPEC-ASYNC-SEMANTICS.md#a31-a-memo-computes-under-its-own-lane-posture-never-its-pullers) (#3442), §20.1 question 3 ("Yeah definitely it is held. Consistent with any optimistic update"), OL-R24/R25/R34, recovery 6.2. **Status:** standing. A memo over a verdict memo: `tests/verdict-memo-under-held-memo-4050.test.ts`, `packages/web/test/held-shell-pending-feedback-4050.spec.tsx` (U-10). **Fixed 2026-11-01:** the verdict shortcut in `propagateStatus` (a probed source settling re-derives its verdict reader) now clears the settled source from the reader's pending sources — left in place, an errored probe target stranded a reader re-parked on a second source at that source's landing; `tests/verdict-shortcut-pending-source-liveness.test.ts`. +**Status:** standing; S-49 and S-50 ruled, not built; S-46's plain row ruled, not built. Absorbs D-26, D-27, D-28 (2026-11-06). The 2026-10-28 paragraph "a verdict posture outlives the pass that asked" is withdrawn. Mechanism and history: `INTERNALS-ASYNC-STATE.md` §7. ### D-26. A `latest()`/optimistic reader mounted or re-run mid-hold shows the committed value and reveals with the lane -**From:** F-6, F-1, F-3. **Scenarios:** S-30. - -A held display-ahead world ("a lane") is a hold seen from the outside. A render effect _off_ it that reads what it is revealing — mounted mid-hold, or re-run by an unrelated sync write — shows the committed value (what is on screen), publishes now, entangles nothing, and re-derives at the release: `Late: 0` at the mount, `Details: 1 | Late: 1 | Value: 1` at the release. `latest()` is not special here: a `createOptimistic` source behaves the same. A reader whose removal is staged in a live hold is still on screen and keeps holding the lane while visible; `latest()` read in such a branch peers through to the proposal. A direct leaf reading a lane's derivation shows the lane's revealed value and re-derives at the reveal (control and content never disagree); a derivation between (a memo, a boundary's tree) carries the future and is born held with its mount (D-2). The same holds for a render effect that reads an optimistic value before its lane is judged: if the lane holds on a derivation's flight, the effect shows the committed value and `latest()` of the guess shows the guess beside it, whether the two reads share one render effect or not — `1 x1 / 5`, then `5 x5 / 5` at the landing (#4284). **Inside the lane, "sees the parent's landings" is about computing, not showing (clarified 2026-10-28):** a lane derivation that reads a value the parent holds joins that hold (D-1) — it computes now, so async downstream of it starts without waiting for the commit, and its result shows at the commit as the hold's work. What lane work _displays_ is the screen plus the lane's own values: its guesses, or what its `latest()` reads asked for. A plain read in lane work of a node the parent holds and the lane does not show ahead is the screen's value, in a verdict lane as in an optimistic one (F-6: "a plain read beside it is a read"); `[s(), L()]` with `L = createMemo(() => latest(s))` shows `v:0 l:1`, as the inline `[s(), latest(s)]` does. A15 #3460's sentence is the computing half. - -**Collapses:** [A15](./SPEC-ASYNC-SEMANTICS.md#a15-transition-entanglement-is-graph-driven-lanes-settle-as-one-reveal) lanes corollary (#3460, #3463, #3479, #3662, #3698), #3444, the fuzzer-finding F8 ruling (2026-10-05, #3806), the adopted-staging ruling. **Status:** standing; fuzzer finding F5b ("a stale reader of a held re-guess shows the revealed guess") pinned `it.fails`. Grouping: `tests/lane-render-effect-grouping-4284.test.ts`. Fixed 2026-11-02 (#4629, #4633, #4637): a second action's guess into a slot whose earlier action's `refresh()` is in flight shows now, beside the screen's value of the re-asked node (the plain read in lane work above), whatever order the pass reads them in (D-28); `tests/projection-second-guess-4629.test.ts`, `tests/optimistic-second-guess-reask-4629.test.ts`. Fixed 2026-11-03 (#4666): the same with the second guess written into a different `createOptimistic` log — a reader held by another action's hold shows the guess at once when every flight it waits on is a `refresh()` re-ask (D-34 ¶3: it holds nothing new), and a reader pending on that hold's new question stays held (#4577); also into a log whose lane has already shown, where a held leaf reads that lane's staging as the screen and re-derives at its reveal, not as its frame's staging; `tests/optimistic-second-log-guess-4666.test.ts`. +**Folded into D-25 (2026-11-06).** the same-node clause of D-25 (the `[s(), L()]` parity) and D-3 (a leaf shows the screen). Rows and pins moved to D-25; the text is archived in `INTERNALS-ASYNC-STATE.md` §7. -### D-27. A verdict reader's mount is a mainline mount +### D-27. The branch a display-ahead `Show` opens is a mainline mount; its plain reads are the screen's -**From:** F-6, F-4, F-1. **Scenarios:** S-31, S-46. - -A ` 0}>` opens at once (display-ahead). The `` it mounts is a mainline mount: it does not inherit the display-ahead world, so its content reads the screen's `x`. Inside an action's own flush that content's first pass reads the staging the action is about to hold, so it is born held (D-2): the boundary shows its fallback beside the committed `x = 0`, and `content 1` appears at the landing. Never `content 1` beside `x = 0`. A first pass created by an _optimistic_ lane, by contrast, is that lane's work (a binding an optimistic mount creates lands with its element). - -**Why the two differ, and what the mount's content may show (clarified 2026-10-28).** A plain read is never display-ahead; only a read that asks (`latest`, `isPending`) or a slot that is guessed (an optimistic value, F-11) is. A guess is _slot-level_: it is the value of its slot for the whole screen, so every plain read of it agrees and an optimistic mount's plain reads show it without tearing. `latest(x)` is _read-level_: only the reads that say `latest` see the held value. A mount's children are _gated_ by the display-ahead read, not _derived_ from it (F-6's "what is rendered from a display-ahead read" is the derivation — `fetch(latest(x))` — not the branch it opens); their plain `x()` is an independent read, and showing it the held value would make a plain read's answer depend on which `Show` wraps it — two plain reads of one signal disagreeing on one screen. So the content reads the screen, and the fallback it shows is `Loading` declining to tear over content that is, by the author's choice of a plain read, not yet the committed value. To show the held value inside the branch, read it there as `latest(x)`: `content {latest(x)}` shows `content 1` now, with no fallback. **Whoever opened the hold (ruled 2026-10-30):** an action is a way to extend a hold, not a different kind of one — a plain write held by an async memo on screen is the same case: the branch a display-ahead `Show` opens is born held and keeps the fallback until the landing, `latest(x)` inside shows ahead, a `Loading` inside shows its fallback (S-46, `tests/display-ahead-plain-write-content.test.ts`). - -**A joined pass's children are the hold's (promoted from A29, 2026-10-28):** a pass — a render effect included — that read a node with no screen to fall back to (a node born into the hold, never committed: D-2; D-4's tick-join) joins the hold: it has no committed value to show, so it is not a leaf reading the screen (D-3) but a reader that waits (A29 ruling A, 2026-10-01: "every reader derives from it — a render effect included — and joins"). A node it creates during that pass is born held: its value commits with the frame, and a mainline reader of it waits rather than showing it ahead of the frame (F-7) — the subtree-disagreement tear this rule guards against, a child's value on screen beside a parent frame still held. Shape: a lazy outlet's render effect reads a router query memo that has no committed value and creates a memo during that pass; the memo is the navigation's, even where its own first pass reads nothing held. This is the third mount beside the two above — a verdict reader's is mainline, an optimistic lane's is the lane's, a joined pass's is the hold's — and A29's "a held pass's children compute but do not effect and die with the commit that replaces their frame" is its archive form. Pins: `tests/lane-uninitialized-landing-3648.test.ts`, `tests/boundary-output-frame-reader.test.ts`, `tests/loading-fallback-in-flush-3540.test.ts`, `tests/fuzz-findings-l2.test.ts`. - -**Collapses:** #3851/#3869 (2026-10-07), [A29](./SPEC-ASYNC-SEMANTICS.md#a29-a-tracked-read-served-a-live-transactions-staged-value-enters-that-transaction)'s 2026-10-07 amendment, L2 statement 3 as amended, #3835. **Status:** standing (`tests/verdict-mount-first-pass-3851.test.ts`, `tests/optimistic-mount-binding-3835.test.ts`). - -**Parked (2026-10-31, #4417; issue open).** The ruling stands: the branch a display-ahead `Show` opens under a plain write's hold is born held and attaches once, at the landing, with its content (S-46 — today it attaches with its text hole empty, F-2's empty slot; and the `[1,2,2]` mount-count pins of `tests/boundary-not-born-held-3540.test.ts` / `web/test/loading-on-keyed-boundary-3540.spec.tsx` see a third mount). A fix was attempted on 2026-10-30 beside #4474's ("a pass whose mount is the hold's is the hold's": a render effect born held under a verdict-lane mount marks its creator pass joined, and `laneStage` leaves the verdict lane for the hold; reference branch `fix/4417-display-ahead-attach-parked`, commit `04c218537`) and dropped before #4474's reference branch was cut: it looped forever on the generator's seed 91501 #32043 (`boundarySeam` → `redraw` re-enqueueing a held boundary's output every round) and opened four new findings (seed 7 #1691 E6, #17379 E1, #38770 E6, #46918 E1). Rulings reaffirmed 2026-10-31 after a rules review: S-46 reading A (born held, attaches at the landing — entailed by D-27 and D-2's in-flush carve-out; attaching with the committed value instead would re-rule S-31's `[fallback]` row) and the `[1,2,2]` mount count. No mechanism inside the size budget is known for either half: adoption's frame half measures ≥100 B minified on the live-page floor; the display-ahead attach attempt looped. Parked; issue open; pins stay `test.fails` at the ruled screens: the S-46 plain row in `web/test/display-ahead-plain-write-content-s46.spec.tsx`, the four `[1,2,2]` rows. Fixed 2026-11-02 (#4417, #4574): a park re-derive that gives back the staging it superseded keeps its held readers and their children (one key, one child), and an insert of a DOM node waits while its subtree holds a child born into the open hold. +**Folded into D-25 (2026-11-06).** after decision 4 its content is D-1's; its one distinct idea — a branch a display-ahead read gates is not derived from it — is the glossary's _Lane_ and D-25's third sentence. Rows and pins moved to D-25; the text is archived in `INTERNALS-ASYNC-STATE.md` §7. ### D-28. A read throws on its own; an earlier read does not disarm a later throw -**From:** F-1, F-2, F-4. **Scenarios:** S-39. - -A read that has nothing to show throws, and a throw ends the pass, so the pass is held. Whether it throws is a property of that read. An earlier read in the same pass does not change it: an earlier `latest(s)` does not stop a later `n0()` — a lane derivation whose value has never shown — from throwing. `[n0(), latest(s)]` throws on `n0()` and `latest(s)` never runs. `[latest(s), n0()]` reads `latest(s)`, then `n0()` throws anyway. Both orders hold the mount: control stays off until `n0(1)` lands, then control and `[1, 1]` together. Under a fresh `` the same throw is caught (F-4): control on, the fallback, then `[1, 1]`. - -Order is not irrelevant. A throw means the rest of the pass does not run, so the two orders are different computations. What must not depend on order is the throw. The empty slot — control on, child absent — is that throw going missing: `latest(s)` put the reader in the lane and `n0()` waited as lane work instead of throwing. - -**Collapses:** `read-order-ruling-brief.md` case 827 (the brief's "order never changes what a mount shows" was the wrong statement of this), `rewrite-bugs-rulings.md` item 1. **Status:** standing, ruled 2026-10-09. The `latest`-first order still suppresses the throw today. A fresh `` that reads only `latest(s)` beside a sibling reading `n0()` never reads the throwing value, so it is not this rule (D-12, F-6, S-40). +**Folded into D-25 (2026-11-06).** per-read posture (D-25) stated for throws. Rows and pins moved to D-25; the text is archived in `INTERNALS-ASYNC-STATE.md` §7. ### D-29. `createTrackedEffect` and `onSettled` callbacks see the screen @@ -250,13 +186,13 @@ Effect-phase code that runs after the frame is decided reads the frame as it sta **Collapses:** [A32](./SPEC-ASYNC-SEMANTICS.md#a32-children-forbidden-readers-see-the-frame-not-the-graph). **Status:** standing. -### D-30. Stores: pending is per key; a derived store's seed is never visible; a declared first paint is loading +### D-30. A derived store's seed is a draft, never an observable value -**From:** F-9, F-1. **Scenarios:** S-36, S-37. +**From:** F-9, F-1. **Rows:** S-36, S-37. **Pins also:** `tests/store/createProjection.async.test.ts`, `tests/strict-read-pending-store.test.ts`. -Pending is per node — per store key: a held write to a plain store pends exactly the touched keys; a derived store's leaves report the derive's new-question refetch like any async memo, in both forms, and a quiet re-ask (`refresh(store)`) is quiet on them too. So do the leaves of a derive that is a verdict reader only through what it reads (D-25) and was served a flight's committed value: a tracked reader of them reads the flight as the memo's reader would — one that mounts mid-flight is held (F-2) and shows the flight's answer, never the old question's — and the derive computes that landing plainly, as the hold's work (D-25) (#4491, `tests/unread-derive-midflight-mount-4491.test.ts`). A derived store's landing resumes the frame that asked it: when a consumer of its leaves goes async, the landed leaves are held with that frame and read stale and pending until it reveals (signal parity); an untouched sibling key is final. A derived store's seed is a draft for the derive, never an observable value: until the first resolution (first yield for a generator) every outside read — tracked, untracked, `in`, enumeration — throws `NotReadyError` (DEV in a component body: `PENDING_ASYNC_UNTRACKED_READ`); write-path reads see it. A seedless derived store (`createStore(fn)` with a derive that takes no draft) starts from an empty record, the same unobservable draft. `loadingValue` / `seedLoadingValue` promote a first value to commit #0: reads serve it, no boundary trips, no hold starts or extends, `isPending` is `false` on every axis until the first real landing — a hydration invariant (the server always answers `false`) — after which the node is an ordinary memo. +Until a derived store's first landing (the first yield for a generator), every outside read of it — tracked, untracked, `in`, enumeration — throws `NotReadyError`; write-path reads see the draft. A seedless derived store starts from an empty record, the same unobservable draft. Pending is per key (F-9); a declared first paint is an answer (D-22); a derived store's leaves report their derive's new question as an async memo's readers would, and a quiet re-ask is quiet on them too. -**Collapses:** [A9](./SPEC-ASYNC-SEMANTICS.md#a9-store-leaves-behind-a-firewall-report-the-firewalls-new-question-refetch), [A22](./SPEC-ASYNC-SEMANTICS.md#a22-pending-is-per-node-store-wide-only-for-the-firewalls-own-work) (amended 2026-10-03), [A25](./SPEC-ASYNC-SEMANTICS.md#a25-a-derived-stores-seed-is-a-draft-never-an-observable-value), [A27](./SPEC-ASYNC-SEMANTICS.md#a27-the-commit-0-loading-window-is-loading-class-and-verdict-quiet), the L2 stores ruling (statement 7), OS-R30–R36, PJ-R23/R30/R31. **Status:** standing; one adoption shape pinned `it.fails` (`adoption-unchanged-key-read-3706.test.ts`, a row first read after the adoption). +**Status:** standing. Shortened 2026-11-06 (the per-key and first-paint clauses are F-9's and D-22's). Mechanism and history: `INTERNALS-ASYNC-STATE.md` §7. ### D-31. `resolve`, `until` and `refresh` deliver by the hold they are called in @@ -274,57 +210,35 @@ An action is not special here. It is only the usual way a hold is open when they ### D-32. Nothing shows a write before its flush; a guess with nothing to be optimistic over is void -**From:** F-10, F-11. **Scenarios:** S-25, S-32. - -`set(count, 30); latest(count)` is the pre-write answer until the flush that carries it, after which `latest(count)` is 30 and `latest(doubled)` is 60 in the same instant. `isPending(x)` is `false` for an unflushed write. A `latest()`/`isPending()` reader created after several flushes answers as if it had always existed (its answer is the holding change's, and lives and reverts with the hold). An optimistic write is visible from the flush that carries it, as a guess over that frame's hold: one an action holds stays for the action's lifetime; one made in a frame that something else holds (a plain write waiting on async) rides that hold. A guess written in a frame that commits with nothing held is as if it never happened — nothing is applied, no effect sees it. A28 (5) and OL-R5 installed that guess for the flush and reverted it (`[1, 2, 1]`): the narrowest width that could be built then. Lane contract 2 (2026-10-01) closes that gap. The `createOptimistic` JSDoc states the tightened rule. - -**Collapses:** [A28](./SPEC-ASYNC-SEMANTICS.md#a28-a-write-becomes-visible-at-flush--to-every-channel) (1)–(5), [O4](./SPEC-ASYNC-SEMANTICS.md#o4-adopted-unflushed--the-signals-verdict-channels-see-a-write-no-flush-has-carried--fixed), lane contract 2, OL-R5, OS-R22. **Status:** standing. +**Folded into D-33 (2026-11-06).** F-10 restated, plus the glossary's "a guess with nothing held under it is void" (now D-33's last sentence). Rows and pins moved to D-33; the text is archived in `INTERNALS-ASYNC-STATE.md` §7. ### D-33. An optimistic value lives exactly as long as the hold it was made over; same-slot writes entangle, disjoint slots do not -**From:** F-11. **Scenarios:** S-32. +**From:** F-11, F-10. **Rows:** S-25, S-32. -An override's lifetime is that of the hold it was made over — in the usual shape, its own action's: the hold committing reverts (or lands the truth beneath) exactly its own writes; concurrent actions on disjoint slots — different signals, different store keys — settle independently; same-slot writes from two actions entangle, and the slot reverts only when the last of them settles. Ownership never travels through a shared reader: a render effect reading keys two actions wrote merges their reveals for scheduling, not their ownership. Regular writes in the same action hold (F-2); at the end, revert and commit are one frame. +An override's lifetime is that of the hold it was made over — in the usual shape, its own action's: the hold committing reverts, or lands the truth beneath, exactly its own writes; concurrent holds on disjoint slots — different signals, different store keys — settle independently; same-slot writes from two holds entangle, and the slot reverts only when the last of them settles. Ownership never travels through a shared reader. A guess is visible from the flush that carries it, as a guess over that frame's hold; one written in a frame that commits with nothing held is as if it never happened — nothing is applied, no effect sees it (S-32). -**Collapses:** [A18](./SPEC-ASYNC-SEMANTICS.md#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) lifetime, store corollary (#2899), node corollary (#2912); OL-R6/R14–R18/R26; OS-R18–R23. **Status:** standing. +**Status:** standing. Absorbs D-32's guess clause (2026-11-06). Mechanism and history: `INTERNALS-ASYNC-STATE.md` §7. ### D-34. A different truth corrects the graph now and the screen at the commit; what the screen never showed is void -**From:** F-11, F-1, F-6. **Scenarios:** S-33, S-44. +**From:** F-11, F-1, F-6. **Rows:** S-33, S-34, S-35, S-44. **Pins also:** `tests/spec-async-semantics.test.ts` "#3331", `tests/optimistic-question-change-4336.test.ts`, `tests/optimistic-key-guess-reask-4329.test.ts`. -When an override's own source answers (its own landing, or a sync recompute from an upstream change): an equal answer confirms silently — nothing re-runs; a different answer corrects — derivations re-derive from the truth now as the hold's work (downstream refetches restart at once, no waterfall), the screen and untracked reads keep what the screen showed until the commit, `latest()` answers the truth and `isPending()` reads `true` while they differ, and the correction reveals with the hold. "Keep the override" means keep what _showed_: a guess the screen never displayed — its reveal held on its own downstream flight, including through a shown `Loading` — is void at the correction, and untracked reads see the committed value, as the screen does. A later landing equal to the override restores it as the graph's value. **A write before the first landing is no guess (ruled 2026-11-03, maintainer, #4656):** "if it hadn't landed it isn't optimistic, its loading" (09:22); "Write before load in the non optimistic case gets squashed.. optimistic is odd one because its visible. Idd be ok with it erroring I think"; "I guess it could be in a second list but then it wouldn't have this problem" (09:27); "That or chat input is disabled until it loads" (09:29). An optimistic write to a source whose first answer has not landed — a `createOptimistic` signal's, an optimistic store's, keyed or `key: null`, seed or not — throws `OPTIMISTIC_WRITE_BEFORE_LOAD` in DEV and guesses nothing; the remedies are a separate list (an outbox) for the pending items, or the action disabled until the source has loaded. Production behaviour is unchanged (not specified). So nothing in this rule, or in D-35's re-base, meets a first load. +When a guess's own source answers: an equal answer confirms silently; a different one corrects — derivations re-derive from the truth now as the hold's work, so downstream refetches restart at once — while the screen, untracked reads and `latest` of the displayed slot keep the guess until the commit, and a guess the screen never showed is void at the correction (S-33). A guess is an edit over the answers to the question it was written over: an answer to a new question is not its truth, and the frame that changed the input is held with the guess's hold until it commits (S-44); a `refresh()` re-asks the same question and holds nothing new. Provenance: two rapid holds on one slot — the older's answer landing after the newer guess is staged for the commit but does not supersede (`latest` keeps the guess, `isPending` is true); only the guess's own hold, a later one, or mainline supersedes (S-34). When every body has returned and nothing authoritative is in flight, each guess still in force is corrected by the truth at hand, the display unchanged until the commit (S-35). A write before a source's first answer is no guess (DEV `OPTIMISTIC_WRITE_BEFORE_LOAD`, #4656). -A keyed store's arrangement guess (rows by key) that the screen showed is not voided by a correction: the truth folds beneath it (one the screen never showed is void, as above). Derivations outside the lane re-derive from the truth now and join the hold, `until`/`resolve` keep their postures over it (an `until` outside the lane reads the truth and joins the hold, D-31), and `latest()` shows the hold's world whole. Lane passes, untracked reads and the screen keep the guess until the commit, and at the commit rows match by key. So a row the truth returns keeps its identity, a row another tab added shows at the commit, and the first of two overlapping actions confirming does not void the second's guess (2026-10-21). A folded lane's list pass inside a frame that already has a transaction reads that frame's stagings as the screen, as a verdict reader does, so it never pairs the truth's length with the guess's rows. - -A new question is not the guess's own source answering. When a guess's node re-asks because an input changed outside the guess's own action (a plain `setSel(1)` while a post that guessed over `sel() = 3` is out), the guess cannot stand beside the new input — that frame would show the new input over the old question's answer and the guess (F-1) — so the frame that changed the input is held with the guess's action (2026-10-28, #4336). The screen keeps the old input and the guess until that action commits, then shows the new input with its answer as one frame, however the reads are grouped into render effects (#4284). Meanwhile `isPending` reads `true` while the new question is in flight or held (F-9), `latest()` answers the newest truth (F-6), and untracked reads see the screen. A `refresh()` re-asks the same question and holds nothing new; the guess's own action changing the input is the correction above. A store's derive is one node asking one question for every key, so the hold is decided where the guess is read: a frame still reading a guess when the flush ends is held with it, and a read of a key with no guess is an ordinary pending read (F-2) — `comments[1]` waits for issue 1's answer, not for a post that guessed on `comments[3]`. An input set through its own optimistic write asks the new question in its own lane: the other action's guess does not answer it, so the lane waits for its own answer (F-6) and then shows it, with the guessed node (for a store, every guessed slot of the target its derive writes) as the lane's derivation from there (F-11); the guess's readers wait with the lane, however the reads are grouped into render effects. The other action's guess was only displaced: when the input's action ends without writing the truth, its lane's world is void, the revert re-asks the old question as the revert's work, and the displaced guess comes back in the revert's frame over that answer — `#1: ` then `#3: dana | mine (sending)`, nothing between — and lives until its own action settles (D-33). If the input's action commits, the displaced guess is gone, as for any correction. - -**Collapses:** [A18](./SPEC-ASYNC-SEMANTICS.md#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) supersession (#3331, 2026-09-09/10) and (d) before the first commit, "Correction display is what showed" (2026-10-05), lane contract 2, lane ruling 5, [A17](./SPEC-ASYNC-SEMANTICS.md#a17-an-active-override-is-the-displayed-value-until-its-transaction-commits-and-the-graphs-value-until-its-own-source-answers) (2026-09-09), OS-R39. **Status:** standing. Fixed 2026-11-05 (#4690): a shown guess a `refresh()` from outside its action corrected keeps the action's hold open while its own source is in flight, as an uncorrected guess does (`lanes.ts` `lanesBlocked`). With nothing rendering the source, the hold landed with the action's write, so effects and untracked reads showed the re-ask's answer until the action's own `refresh()` landed while rendered readers kept the guess — a tear between effect-only and rendered readers, not a flicker on screen; `tests/optimistic-outside-reask-4690.test.ts`. +**Status:** standing; the `latest` clause ruled, not built (decision 3, #4689). Absorbs D-35 (2026-11-06). Mechanism and history: `INTERNALS-ASYNC-STATE.md` §7. ### D-35. Provenance: an older question's answer never corrects a newer guess; the body's end corrects what nothing authoritative is answering -**From:** F-11. **Scenarios:** S-34, S-35. - -Two rapid actions on one slot: the older action's refetch landing after the newer override is a question the user has since changed — it is staged for the commit but does not supersede (no downstream re-derivation, no pending flip; `latest` keeps the override; `isPending` is `true` because the held value differs). Only the override's own action, a later action, or mainline supersedes. A keyed store landing beneath an arrangement guess is re-based over the older truth by row key. When every action body has returned and nothing authoritative is in flight — not the override's own source, not a plain load the action asked for — each override still in force is corrected by the truth at hand (the staged equal landing, else the committed value), for a store guess as for a signal's; display is unchanged until the commit. - -**Collapses:** [A18](./SPEC-ASYNC-SEMANTICS.md#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) provenance (2026-09-10; Q-D 2026-10-03), body-end corollary (#3427), §27 question 1, L2 statement 5, OL-R20, OS-R24/R43/R45. **Status:** standing. +**Folded into D-34 (2026-11-06).** D-34's second paragraph. Rows and pins moved to D-34; the text is archived in `INTERNALS-ASYNC-STATE.md` §7. ### D-36. A resting optimistic node is a plain memo -**From:** F-9, F-11. - -With no active override, `createOptimistic(fn)` is observationally a `createMemo(fn)` for `read`/`latest`/`isPending` at every checkpoint of a refetch cycle — before any override and after a full cycle reverted; a revert is not a refetch and never pends. - -**Collapses:** [A12](./SPEC-ASYNC-SEMANTICS.md#a12-resting-optimistic-nodes-report-pending-like-a-plain-memo), [A13](./SPEC-ASYNC-SEMANTICS.md#a13-resting-optimistic--plain-async-memo-at-every-checkpoint), V1–V3/V5. **Status:** standing. +**Retired as a rule (2026-11-06).** a consequence of the glossary's _Guess_: a slot with no guess is its source. Kept as a pin (`tests/spec-async-semantics.test.ts`, A12/A13), not a rule. The text is archived in `INTERNALS-ASYNC-STATE.md` §7. ### D-37. A lane sees the screen plus its own guesses and never holds a sync write -**From:** F-6, F-11, F-1. - -The optimistic world is the screen plus the action's guesses: derivations of a guess publish into that world (a memo over an optimistic value shows the speculative result to the lane's readers and to direct reads, the committed one to readers off the lane — a whole frame either way). Lane work never makes its node the action's: an unrelated sync write re-running a `` over an optimistic value publishes at once and the action stays open; the frame a lane pass replaces leaves the screen when the lane's run applies, after a held lane's reveal, never before. - -**Collapses:** L2 statement 4 and §19 (2026-10-01), [A17](./SPEC-ASYNC-SEMANTICS.md#a17-an-active-override-is-the-displayed-value-until-its-transaction-commits-and-the-graphs-value-until-its-own-source-answers) "lanes stage" (#3479), [A15](./SPEC-ASYNC-SEMANTICS.md#a15-transition-entanglement-is-graph-driven-lanes-settle-as-one-reveal) lane work vs transaction work (#3662, #3698), OL-R9/R31. **Status:** standing. - ---- +**Retired as a rule (2026-11-06).** the glossary's _Lane_ plus D-25. The text is archived in `INTERNALS-ASYNC-STATE.md` §7. ## E. Errors (F-12) @@ -338,21 +252,15 @@ The optimistic world is the screen plus the action's guesses: derivations of a g ### D-39. An error fallback is the display; it stays until a new answer replaces the error -**From:** F-12, F-3, F-1. **Scenarios:** S-38. - -An `` flipping to its fallback is a boundary going back to its fallback: the nodes behind it stop holding (unless read elsewhere, where they error too), and non-errored nodes that lose their only visible consumer that way stop holding as well. The error stays on screen until a new answer replaces the errored one — a pending is not a value: over `Errored > Loading(never shown)`, a write publishes beside the old error (`0 | Fetch error for 0` → `1 | Fetch error for 0` → `1 | Fetch error for 1`); the inner fallback is hidden behind the error. While the fallback shows, nothing under it is on screen, so its recovery re-arms the whole subtree (ruled 2026-11-02, #4627, re-ruling async-gen 91501 #1831): every `` under it has not shown content — whether or not it showed content before the error — and once the errored source has its new answer, each catches what is still pending in the replacement frame and shows its fallback beside the recovered content, as on a first load (F-4, D-12): `[Crash][finding related]`. After the recovery the ordinary rules apply: the first content such a `` shows makes it a shown boundary, and a later re-ask keeps that content (F-5). The error stays up while something no inner `` catches still waits: the errored source's own re-ask (the write above), a pending read directly under the `` with no `` between (a pending is not a value; #4592), or a recovery that is part of a held write, action or transition, which reveals with that hold (D-41). `Errored` builds its function fallback in its own scope, like `Show`, once per show: a re-derive of the boundary while the fallback shows — a second reader caught, the same error propagated again, a retry pending after `reset()`, a later error — keeps the built fallback, its DOM and focus with it; a later error reaches it through the error accessor; the fallback is disposed once the content that replaces it is on screen — at the commit, when the swap is held (#3991). A fallback that no live reader holds heals: the boundary waits on what threw only while a reader that caught the error is alive, so when the last one is disposed (a `` closing over `m()` as the action that made `m` throw settles) the content shows again — what a fresh render with the same inputs shows, since nothing on it reads the error (ruled 2026-10-28, #4373). - -**Collapses:** §23.1 items 2–3, §27 7b (2026-10-02; the top-level `isPending(count)` affordance was lost by this ruling, knowingly), recovery 9.1–9.3. **Status:** standing. The recovery re-arm (#4627): `tests/errored-recovery-fresh-loading-4627.test.ts`, `tests/latest-memo-inline-parity.test.ts` (#1831 rows), `packages/web/test/errored-recovery-nested-loading-4425.spec.tsx`, `tests/derived-store-recovery-wake.test.ts` (the store twin, plain and held in an action), the "inside Loading" rows of `tests/errored-recovery-stale-memo-4592.test.ts` and `packages/web/test/errored-recovery-stale-memo-4592.spec.tsx` (their no-boundary rows keep the error: a pending is not a value). A recovery written in an action's tick is the action's and keeps the error up until its hold reveals the inner fallback: the last row of `tests/errored-recovery-fresh-loading-4627.test.ts`, `packages/web/test/async-gen/e1-3289-122.spec.ts` (async-gen 3289 #122). Once per show: `tests/createErrorBoundary.test.ts` ("instantiates the fallback once per show"), `packages/web/test/errored-show-retry-3991.spec.tsx`; the held swap: `tests/boundary-fallback-held-swap-3991.test.ts`; healing when the last reader is disposed: `tests/errored-heal-disposed-reader-4373.test.ts`, `packages/web/test/errored-heal-disposed-reader-4373.spec.tsx`. - ---- +**Folded into D-41 (2026-11-06).** contained in D-41 as "recovery is the mirror". Rows and pins moved to D-41; the text is archived in `INTERNALS-ASYNC-STATE.md` §7. ### D-41. An error is an answer: it ends the wait like a value, and it shows when a value would -**From:** F-2, F-1, F-12, D-39. **Scenarios:** S-42, S-43. **Ruled 2026-10-15** (Q-6 B, from #3886; first drafted as D-40 in #3995). +**From:** F-2, F-1, F-12. **Rows:** S-38, S-42, S-43. **Pins also:** `tests/error-is-an-answer.test.ts`, `tests/watch-error-outcomes.test.ts`, `tests/errored-recovery-wedge-4403-4404-4405.test.ts`. **Ruled 2026-10-15** (Q-6 B, from #3886; first drafted as D-40 in #3995). A rejection, a compute-phase throw or a comparator throw is the answer to the question its node asked. As an answer it _settles_ the async: a hold never waits on an errored flight, and nothing here changes what a hold waits for. But settled is not shown. A value that lands on a held node is staged until the frame reveals (F-2); an error that lands on a held node is staged the same way, because the error fallback is content (F-4) and showing it beside the previous frame's other content is the torn screen F-1 forbids. Until the reveal: the screen keeps what it showed, an ordinary outside read serves the committed value (it reads the screen, D-29, D-34), `createEffect`'s error arm does not run, `` does not flip, `resolve`/`until`/`refresh` do not reject (D-31), `isPending` of it is `true`, and `latest` of it throws the proposed error — the one way to see the answer early (F-6). At the reveal the error and everything that waited with it show as one frame: the arm runs, the boundary shows its fallback, the outside read throws, `isPending` is `false`. -Recovery is the mirror (D-39): a shown error stays the display until the frame that carries its replacement reveals — through a pending retry, and through a held recovery, success or a different failure. A recovery to a value equal to the last good one is still a change of answer: payload equality never silences it, held or not. +Recovery is the mirror: a shown error stays the display until the frame that carries its replacement reveals — through a pending retry, and through a held recovery, success or a different failure. A recovery to a value equal to the last good one is still a change of answer: payload equality never silences it, held or not. A first load that rejects has no previous content to keep: born held (D-2), it stays loading until its frame commits, as a first load's value does; under an armed boundary it is the boundary's and shows with the boundary's frame (F-4). A derivation that reads a staged error throws it, as it would read a staged value: its own pass errors and stages with the same frame. @@ -362,6 +270,8 @@ A first load that rejects has no previous content to keep: born held (D-2), it s **Derived stores (closed 2026-10-30, #4489).** A derived store's (`createStore(fn)`, projections, optimistic stores) held rejection is staged like a memo's: an untracked read of its property reads the screen until the frame reveals. Pinned in `tests/error-is-an-answer.test.ts` and `tests/optimistic-held-error-untracked-read-4489.test.ts`. The standalone cases from #3886's audit (a write to a failed writable memo, error-wrapper formatting, comparator release, snapshot capture) are not this rule. **Status:** standing. +Absorbs D-39 (2026-11-06): an error fallback is the display, and it stays until the frame that carries a new answer reveals. + ### D-42. A reactive read is not an `await`: `try/catch` around it is not a channel **From:** F-12, F-2, F-4, F-9. **Scenarios:** S-47. **Ruled 2026-10-30.** diff --git a/packages/signals/docs/RULES-FUNDAMENTALS.md b/packages/signals/docs/RULES-FUNDAMENTALS.md index 540e952c9b..9194234905 100644 --- a/packages/signals/docs/RULES-FUNDAMENTALS.md +++ b/packages/signals/docs/RULES-FUNDAMENTALS.md @@ -29,9 +29,13 @@ These are the only words the fundamentals use. Internal names (transaction, stag - **Action.** `action(fn)`: the explicit hold — its writes are held until the body has returned and everything they put in flight has landed. - **Boundary.** `` (`createLoadingBoundary`). A boundary is **armed** until it has shown content (a _fresh_ boundary), and armed again by `on`; a boundary showing content is **disarmed** (a _shown_ boundary). Armed, it catches what is pending under it; disarmed, it forwards — it holds like any reader. `` is the error boundary. - **Fallback.** What a boundary displays instead of its content. Content behind a fallback is kept alive but is not on screen. An armed boundary with something pending under it _enters_ its fallback with the frame that armed it — the mount, or the `on` change — and the fallback is on screen when that frame is (F-4). -- **First load.** The wait before a source has its first answer. A first load is _loading_, not a change: there is no previous content to keep. A memo created during a hold that reads the held change (_born held_, D-2) is loading in the same sense: it has no value the screen can show until that hold commits. A read of something loading suspends — to a boundary, or into the hold (F-2, F-4) — and `isPending` of it gives no verdict, because there is nothing shown to be stale (F-9). +- **First load.** The wait before a source has its first answer. A first load is _loading_, not a change: there is no previous content to keep. A memo created during a hold that reads the held change (_born held_, D-2) — including a first pass that reads this flush's staging when the flush then parks — is loading in the same sense: it has no value the screen can show until that hold commits. A read of something loading suspends — to a boundary, or into the hold (F-2, F-4) — and `isPending` of it gives no verdict, because there is nothing shown to be stale (F-9). - **Question / re-ask.** A source's inputs are its question. An input changing asks a new question; `refresh()`, polling, an action's confirming refetch re-ask the same one. -- **Display-ahead reader.** `latest(x)`, `isPending(x)`, an optimistic value: a reader that shows a held change (or the verdict about it) before the hold reveals it. +- **Display-ahead read.** A single read — `latest(x)`, `isPending(x)`, or a read of a slot showing a guess — that shows the held change, or the verdict about it, now, without joining the hold. It is a read, not a kind of reader: a plain read beside it in the same pass is still a plain read, and a pass has reads, not a posture (D-25). +- **Answer.** A value a source has for its question: its committed value, its own async landed — into a hold or not — or a declared first paint (`loadingValue`). `latest` shows the newest answer; `isPending` judges whether a newer question has none yet (D-22, D-23). +- **Produced.** Said of a node that has an answer, and of a derivation that has computed a value from held inputs (born held): what `latest` can return. A first load has produced nothing. +- **The hold's work.** Every pass that read a value the hold holds plainly, and what those passes created: it computes now and shows at the commit, never before (D-1, D-2). +- **Guess.** An optimistic write: an edit over the answers to the question it was written over, and the value of its slot for the whole screen from its flush until the hold it was made over commits or its own source answers differently (F-11, D-34). A guess with nothing held under it is void. - **Declared lag.** `createDeferred(fn)`: a source whose author has declared that its previous answer is an acceptable answer while the next one loads. After its first answer, its own re-asks hold nothing and its readers keep the answer they show until the new one lands (D-40). It is the counterpart of a display-ahead reader: that one shows a held change early, this one shows a change's consequence late, and neither is a hold. - **Lane.** The world a display-ahead read opens: the screen plus what it shows ahead. What is rendered from a display-ahead read — derived from its value — lives in its lane, and the lane holds _itself_ on its own async — a hold of its own, separate from the hold it reads ahead of (F-6). A branch a display-ahead read merely _gates_ is not derived from it: it is a mainline mount whose plain reads are the screen's (D-27). - **Flush.** The unit of visibility. Writes become visible at the flush that carries them; a manual `flush()` ends a tick. diff --git a/packages/signals/docs/RULES-INDEX.md b/packages/signals/docs/RULES-INDEX.md index 76956ee17b..80538da36c 100644 --- a/packages/signals/docs/RULES-INDEX.md +++ b/packages/signals/docs/RULES-INDEX.md @@ -6,34 +6,40 @@ IDs are never renumbered or deleted — source comments cite them. A superseded, ## Vocabularies -| prefix | defined in | meaning | -|---|---|---| -| `A` | `SPEC-ASYNC-SEMANTICS.md` Tier A | ruled, test-pinned propositions about isPending / latest / transitions / optimistic lanes | -| `B` `C` | `SPEC-ASYNC-SEMANTICS.md` Tier B/C | inferred / open items, all since ruled, closed or promoted into an A-rule (`(was B1)` — the alias row points at it) | -| `V` | `SPEC-ASYNC-SEMANTICS.md` Known violations | violations of A-rules found and fixed by the #2838 redesign; pinned in `spec-async-semantics.test.ts` | -| `INV-` | `INTERNALS-ASYNC-STATE.md` §5 | `__TEST__` invariants (asserted in `invariants.ts`) | -| `RUL-` | `INTERNALS-STORE-STATE.md` §8b, `rules-mining/FINDINGS.md` | store rulings mined from the suites (2026-08-16) | -| `-R` | `rules-mining/.md` | mined behavioral rules. **Each file numbers from R1**, so an R-id is only meaningful with its namespace: `CS` core-store · `OL` optimistic-lanes · `OS` optimistic-store · `PJ` projections · `RS` reconcile-snapshot. Comments qualify with `core`/`opt`/`proj`/`snap`; a bare `R` refers to the citing module's own file. | -| `§` | `INTERNALS-STORE-STATE.md` sections; `NODE-SHAPE.md` §11b, §12–§12e | design sections cited as rules. §11–§12 are the stage-3 node-shape decisions recovered from the deleted `DESIGN-PATCH-CHANNEL.md` (see NODE-SHAPE.md for provenance) | +| prefix | defined in | meaning | +| ------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `F-` | `RULES-FUNDAMENTALS.md` | the fundamentals: immutable statements of visible behavior | +| `D-` | `RULES-DERIVED.md` | derived rules: a fundamental narrowed to a shape, naming its scenarios | +| `S-` | `SCENARIOS.md` | scenarios — the rulings. `executable` rows are generated from `packages/web/test/async-gen/rulings/S-.ts` and run in `pnpm test`; `prose` rows are hand-written | +| `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 | 34 | 21 | 34 | 0 | -| V | 5 | 5 | 5 | 0 | -| B | 5 | 3 | 5 | 0 | -| C | 4 | 0 | 3 | 1 | -| INV | 11 | 4 | 7 | 4 | -| RUL | 13 | 3 | 6 | 6 | -| R (CS) | 59 | 15 | 20 | 30 | -| R (OL) | 37 | 5 | 1 | 31 | -| R (OS) | 46 | 0 | 0 | 46 | -| R (PJ) | 37 | 2 | 5 | 32 | -| R (RS) | 38 | 0 | 2 | 36 | -| § | 24 | 13 | 11 | 7 | +| ---------- | ----- | ------------ | -------------- | ------------- | +| F | 11 | 10 | 11 | 0 | +| D | 40 | 35 | 36 | 2 | +| A | 34 | 22 | 34 | 0 | +| V | 5 | 5 | 5 | 0 | +| B | 5 | 3 | 5 | 0 | +| C | 4 | 0 | 3 | 1 | +| INV | 11 | 4 | 7 | 4 | +| RUL | 13 | 3 | 6 | 6 | +| R (CS) | 59 | 15 | 20 | 30 | +| R (OL) | 37 | 5 | 1 | 31 | +| R (OS) | 46 | 0 | 0 | 46 | +| R (PJ) | 37 | 2 | 5 | 32 | +| R (RS) | 38 | 0 | 2 | 36 | +| § | 24 | 14 | 11 | 7 | +| S | 50 | 9 | 13 | 33 | ## Unresolved citations @@ -41,364 +47,491 @@ Status legend: **live** stated and standing · **ruled** carries an explicit rul **tests/:** B0, B6, §0, §16, §19, §20, §21, §27, §28, §31, §39, §41 (test-only citations are informational; `--check` gates src/ only) +## F — fundamentals + +| id | status | defined | cited in src | cited in tests | statement (at definition) | +| ---- | ------ | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| F-1 | live | `docs/RULES-FUNDAMENTALS.md:45` | boundaries.ts×3 async.ts×1 core.ts×1 effect.ts×1 lanes.ts×4 scheduler.ts×1 seat.ts×2 verdict.ts×3 optimistic.ts×4 | deep-snapshot-after-action.test.ts×7 errored-held-mount-staged-error-4480.test.ts×1 errored-list-recovery-held-4526.test.ts×1 errored-recovery-fresh-loading-4627.test.ts×2 errored-recovery-stale-memo-4592.test.ts×1 ispending-async-derivation-4677.test.ts×3 latest-beside-staged-read-4365.test.ts×1 latest-derived-held-error-guess-lane-4106.test.ts×1 latest-snapshot-scope-4570.test.ts×1 loading-on-latest-fallback-4337.test.ts×1 loading-reveal-held-outside-4593.test.ts×1 midflight-memo-over-reask-4504.test.ts×1 optimistic-guess-entangled-async-4577.test.ts×2 optimistic-guess-memo-binding-4561.test.ts×4 optimistic-guess-parked-question-4559.test.ts×1 optimistic-guess-second-async-memo-4577.test.ts×1 optimistic-guess-split-bindings-4536.test.ts×1 optimistic-held-error-untracked-read-4489.test.ts×1 optimistic-lane-seam-loop-4643.test.ts×2 optimistic-list-push-filter-push-4642.test.ts×2 optimistic-outside-reask-4690.test.ts×2 optimistic-question-change-4336.test.ts×3 optimistic-refresh-late-shared-memo-4576.test.ts×1 optimistic-shared-effect-question-4536.test.ts×1 repark-drops-unreached-source-4541.test.ts×1 tracked-effect-staged-dep-4606-4607.test.ts×1 verdict-memo-flight-no-reask.test.ts×1 verdict-memo-under-held-memo-4050.test.ts×1 verdict-probe-committed-4648.test.ts×2 verdict-probe-landing-4648.test.ts×1 verdict-seat-merged-lane-4427.test.ts×1 | The screen is whole | +| F-2 | live | `docs/RULES-FUNDAMENTALS.md:53` | async.ts×1 core.ts×1 lanes.ts×1 seat.ts×3 optimistic.ts×1 projection.ts×1 store.ts×1 | direction-rule-probe.test.ts×1 error-is-an-answer.test.ts×1 errored-held-mount-staged-error-4480.test.ts×2 first-load-superseded-answer-4569.test.ts×2 lane-unshown-leaf-4338.test.ts×1 loading-on-latest-fallback-4337.test.ts×2 loading-reveal-held-outside-4593.test.ts×1 midflight-memo-over-reask-4504.test.ts×2 optimistic-held-write-leak.test.ts×1 optimistic-nested-action-refresh-4576.test.ts×1 optimistic-question-change-4336.test.ts×1 optimistic-refresh-late-shared-memo-4576.test.ts×1 settle-diamond-revisit.test.ts×1 try-catch-reactive-read.test.ts×1 unread-derive-midflight-mount-4491.test.ts×2 verdict-probe-committed-4648.test.ts×1 | A change that needs async to be shown is held; the screen keeps what it showed | +| F-3 | live | `docs/RULES-FUNDAMENTALS.md:61` | boundaries.ts×1 lanes.ts×1 verdict.ts×2 | errored-heal-disposed-reader-4373.test.ts×1 errored-heal-memo-reader-4644.test.ts×1 errored-memo-over-output-held-reject-4640.test.ts×1 lane-dissolve-disposed-member-4613.test.ts×2 loading-on-errorboundary-through-4639.test.ts×1 | A reader holds only while it is the display | +| F-4 | live | `docs/RULES-FUNDAMENTALS.md:69` | boundaries.ts×3 core.ts×1 | derived-store-recovery-wake.test.ts×1 error-is-an-answer.test.ts×1 errored-memo-over-output-held-reject-4640.test.ts×1 errored-recovery-fresh-loading-4627.test.ts×3 errored-recovery-stale-memo-4592.test.ts×1 issue-3800-repro.test.ts×2 latest-memo-inline-parity.test.ts×1 loading-on-errorboundary-sibling-4638.test.ts×1 loading-on-errorboundary-through-4639.test.ts×1 loading-on-latest-fallback-4337.test.ts×1 | An armed boundary catches: what is pending under it holds nothing outside it | +| F-5 | live | `docs/RULES-FUNDAMENTALS.md:77` | — | loading-on-errorboundary-sibling-4638.test.ts×1 loading-on-held-change-4605.test.ts×2 loading-reveal-held-outside-4593.test.ts×1 | A disarmed boundary forwards: it keeps its content and holds like any reader, until `on` arms it again | +| F-6 | live | `docs/RULES-FUNDAMENTALS.md:85` | boundaries.ts×1 constants.ts×2 core.ts×2 effect.ts×1 lanes.ts×10 scheduler.ts×3 seat.ts×2 verdict.ts×4 optimistic.ts×4 store.ts×3 | derived-store-lane-derive.test.ts×1 error-is-an-answer.test.ts×1 gc.test.ts×1 ispending-async-derivation-4677.test.ts×3 ispending-memo-parked-seam-4439.test.ts×1 latest-beside-staged-read-4365.test.ts×1 latest-derived-held-error-4106.test.ts×1 latest-derived-sync-error-4377.test.ts×1 latest-first-one-pass-4402.test.ts×1 latest-memo-inline-parity.test.ts×3 latest-stale-branch-hold-4661.test.ts×2 latest-two-readers-one-ask-4393.test.ts×5 loading-on-latest-fallback-4337.test.ts×1 optimistic-held-write-leak.test.ts×1 optimistic-layered-filter-4336.test.ts×1 optimistic-outside-reask-4690.test.ts×2 optimistic-question-change-4336.test.ts×6 optimistic-second-guess-reask-4629.test.ts×1 optimistic-second-log-guess-4666.test.ts×1 posture-store-parity.test.ts×1 projection-lane-length-4631.test.ts×1 projection-second-guess-4629.test.ts×1 rejection-beside-ispending-3957.test.ts×1 derived-latest-confirmed-key.test.ts×1 derived-store-heal-4451.test.ts×1 unread-derive-midflight-mount-4491.test.ts×1 verdict-memo-after-staged-read.test.ts×1 verdict-memo-flight-no-reask.test.ts×7 verdict-memo-held-branch-4544.test.ts×3 verdict-memo-under-held-memo-4050.test.ts×1 verdict-read-order.test.ts×1 | Display-ahead readers show now and never hold the change they read ahead of; their own async holds them, in their own lane | +| F-7 | live | `docs/RULES-FUNDAMENTALS.md:93` | boundaries.ts×1 core.ts×1 effect.ts×1 lanes.ts×1 | error-staged-recovery.test.ts×1 ispending-async-derivation-4677.test.ts×1 loading-reveal-held-outside-4593.test.ts×1 optimistic-held-write-leak.test.ts×1 optimistic-lane-seam-loop-4643.test.ts×4 verdict-memo-after-staged-read.test.ts×2 | Nothing else reveals ahead of the hold it belongs to | +| F-9 | live | `docs/RULES-FUNDAMENTALS.md:101` | boundaries.ts×1 async.ts×4 core.ts×2 lanes.ts×3 scheduler.ts×1 seat.ts×3 types.ts×2 verdict.ts×8 deferred.ts×2 projection.ts×1 store.ts×1 | action-refresh-ispending-loop-4349.test.ts×1 answered-then-reasked-released-reader.test.ts×1 answered-then-reasked-same-tick.test.ts×2 derived-store-derive-settle.test.ts×2 derived-store-first-landing-verdict.test.ts×1 first-load-superseded-answer-4569.test.ts×1 latest-first-one-pass-4402.test.ts×2 latest-ispending-refetch-loop.test.ts×1 loading-on-held-filter-refresh-4379.test.ts×1 midflight-memo-over-reask-4504.test.ts×1 optimistic-question-change-4336.test.ts×2 settle-diamond-revisit.test.ts×1 staged-error-netted-write.test.ts×1 staged-error-through-async-memo-4640.test.ts×1 two-sources-answered-reasked-4495.test.ts×2 verdict-memo-flight-no-reask.test.ts×8 verdict-probe-committed-4648.test.ts×2 | `isPending` is about a new question; a re-ask is quiet; a first load is loading, not pending | +| F-10 | live | `docs/RULES-FUNDAMENTALS.md:109` | async.ts×1 core.ts×1 seat.ts×3 verdict.ts×1 | latest-memo-inline-parity.test.ts×1 optimistic-lane-seam-loop-4643.test.ts×1 proto-key-removal-4672.test.ts×1 two-sources-answered-reasked-4495.test.ts×2 verdict-memo-held-branch-4544.test.ts×1 verdict-probe-committed-4648.test.ts×2 | A write becomes visible at flush — to every channel | +| F-11 | live | `docs/RULES-FUNDAMENTALS.md:117` | lanes.ts×3 scheduler.ts×1 optimistic.ts×2 | optimistic-guess-over-held-slot.test.ts×2 optimistic-held-error-untracked-read-4489.test.ts×1 optimistic-held-write-leak.test.ts×1 optimistic-lane-seam-loop-4643.test.ts×1 optimistic-layered-filter-4336.test.ts×1 optimistic-list-push-filter-push-4642.test.ts×5 optimistic-live-echo-3898.test.ts×1 optimistic-outside-reask-4690.test.ts×7 optimistic-question-change-4336.test.ts×3 optimistic-second-log-guess-4666.test.ts×4 | An optimistic value is a guess over a hold: the displayed value until that hold commits or its own source answers differently | +| F-12 | live | `docs/RULES-FUNDAMENTALS.md:125` | optimistic.ts×1 projection.ts×1 | derived-store-derive-settle.test.ts×1 derived-store-recovery-wake.test.ts×1 errored-heal-disposed-reader-4373.test.ts×1 errored-heal-memo-reader-4644.test.ts×1 errored-held-mount-staged-error-4480.test.ts×1 errored-memo-over-output-held-reject-4640.test.ts×1 errored-reader-4486.test.ts×1 errored-staged-source-4364.test.ts×1 loading-reveal-held-outside-4593.test.ts×1 optimistic-derive-memo-error-4437.test.ts×1 try-catch-reactive-read.test.ts×2 | An error is a boundary returning to its fallback; pending is not an error | + +## D — derived rules + +| id | status | defined | cited in src | cited in tests | statement (at definition) | +| ---- | ------ | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | +| D-1 | live | `docs/RULES-DERIVED.md:13` | seat.ts×5 | fallback-born-creator-4428.test.ts×1 | Reading a held value joins its hold | +| D-2 | live | `docs/RULES-DERIVED.md:21` | core.ts×1 seat.ts×6 | adoption-lazy-child-pairing-3712.test.ts×1 boundary-not-born-held-3540.test.ts×1 fallback-fn-latest-runaway-4467.test.ts×1 latest-unread-identity-memo-4474.test.ts×1 midflight-memo-over-reask-4504.test.ts×2 | A new memo joins a hold by reading it, not by being created | +| D-3 | live | `docs/RULES-DERIVED.md:25` | constants.ts×1 scheduler.ts×1 seat.ts×2 | boundary-mounts-separate-flushes-4283.test.ts×1 derived-store-latest-seed-4355.test.ts×1 errored-recovery-wedge-4403-4404-4405.test.ts×2 | A render effect is a leaf: it shows the screen and re-derives at the commit | +| D-4 | live | `docs/RULES-DERIVED.md:33` | constants.ts×1 seat.ts×3 | boundary-mounts-separate-flushes-4283.test.ts×1 verdict-seat-merged-lane-4427.test.ts×1 | Membership in a hold is the tick's | +| D-6 | live | `docs/RULES-DERIVED.md:41` | boundaries.ts×1 core.ts×1 | direction-rule-probe.test.ts×1 errored-memo-over-output-held-reject-4640.test.ts×1 first-load-superseded-answer-4569.test.ts×2 | A first load starts no hold of its own; uncaught, it holds the change that mounted it, and the root attach | +| D-7 | live | `docs/RULES-DERIVED.md:49` | lanes.ts×1 | optimistic-held-write-leak.test.ts×1 optimistic-outside-reask-4690.test.ts×3 | Actions are holds; `yield` is the suspension point; the same tick's earlier writes are adopted | +| D-8 | live | `docs/RULES-DERIVED.md:57` | core.ts×4 seat.ts×2 verdict.ts×1 store.ts×2 target.ts×1 | action-outside-write-ispending-4367.test.ts×3 errored-recovery-wedge-4403-4404-4405.test.ts×2 lane-mount-effects-4557.test.ts×1 netted-equals-4483.test.ts×2 netted-verdict-reader-4485.test.ts×1 staged-error-netted-write.test.ts×2 | A write is a proposal | +| D-9 | live | `docs/RULES-DERIVED.md:65` | scheduler.ts×1 seat.ts×4 | hold-recovers-landing-reruns-4469.test.ts×2 ispending-async-derivation-4677.test.ts×1 optimistic-guess-entangled-async-4577.test.ts×2 optimistic-refresh-late-shared-memo-4576.test.ts×1 | Entanglement is graph-driven; what shares an on-screen async derivation settles as one, and a hold ends when its last on-screen reader leaves | +| D-10 | live | `docs/RULES-DERIVED.md:73` | seat.ts×1 | — | The committed frame keeps its inputs until it is replaced | +| D-11 | live | `docs/RULES-DERIVED.md:81` | lanes.ts×2 verdict.ts×1 | lane-dissolve-disposed-member-4613.test.ts×2 optimistic-refresh-late-shared-memo-4576.test.ts×1 | A hold ends when its last on-screen reader leaves | +| D-12 | live | `docs/RULES-DERIVED.md:87` | boundaries.ts×1 core.ts×2 | errored-recovery-fresh-loading-4627.test.ts×1 issue-3800-repro.test.ts×1 loading-fallback-in-flush-3540.test.ts×2 | An armed boundary catches what is pending under it; its fallback is seen when the frame that armed it is | +| D-13 | live | `docs/RULES-DERIVED.md:95` | boundaries.ts×2 core.ts×2 effect.ts×1 | fallback-fn-latest-runaway-4467.test.ts×1 fresh-boundary-born-held-reader.test.ts×1 loading-fallback-in-flush-3540.test.ts×4 | A boundary whose own mount is held appears at that hold's commit | +| D-14 | live | `docs/RULES-DERIVED.md:99` | boundaries.ts×2 core.ts×1 | loading-fallback-in-flush-3540.test.ts×1 loading-on-errorboundary-sibling-4638.test.ts×1 loading-on-errorboundary-through-4639.test.ts×1 optimistic-guess-memo-binding-4561.test.ts×1 optimistic-guess-split-bindings-4536.test.ts×1 | A fallback-caught flight holds nothing; an `on` reset moves the wait onto the boundary | +| D-15 | live | `docs/RULES-DERIVED.md:103` | boundaries.ts×1 | latest-stale-branch-hold-4661.test.ts×1 loading-on-errorboundary-sibling-4638.test.ts×1 loading-on-held-change-4605.test.ts×2 loading-on-held-filter-refresh-4379.test.ts×2 | `on` is a dependency list; the swap lands with the frame of the change that caused it | +| D-16 | live | `docs/RULES-DERIVED.md:107` | boundaries.ts×3 | errored-held-mount-staged-error-4480.test.ts×1 loading-fallback-in-flush-3540.test.ts×1 loading-on-errorboundary-sibling-4638.test.ts×3 loading-on-errorboundary-through-4639.test.ts×4 loading-on-held-filter-refresh-4379.test.ts×1 | A re-arm whose data is also read outside the boundary never shows its fallback; a race lost is not reported | +| D-17 | live | `docs/RULES-DERIVED.md:111` | — | action-outside-write-ispending-4367.test.ts×1 loading-on-latest-two-flights-stuck-fallback.test.ts×1 | A re-arm shows its fallback with the frame of the `on` pass | +| D-18 | live | `docs/RULES-DERIVED.md:119` | — | loading-on-rearm-fallback-effect-4268.test.ts×1 | A fallback that itself reads pending data | +| D-19 | live | `docs/RULES-DERIVED.md:127` | effect.ts×1 | render-effect-under-async-gate-4343.test.ts×1 | Queued runs behind a fallback wait; the synchronous first render goes through | +| D-21 | live | `docs/RULES-DERIVED.md:135` | — | — | An armed boundary lets a lane reveal: the async under the fallback does not hold it | +| D-22 | live | `docs/RULES-DERIVED.md:141` | async.ts×3 constants.ts×1 core.ts×6 lanes.ts×4 scheduler.ts×1 seat.ts×7 types.ts×1 verdict.ts×5 | answered-then-reasked-released-reader.test.ts×1 answered-then-reasked-same-tick.test.ts×3 attribution-waterfall-eval.test.ts×1 autodispose-pending-release.test.ts×1 derived-store-latest-seed-4355.test.ts×1 latest-derived-held-error-4106.test.ts×1 latest-first-one-pass-4402.test.ts×2 latest-ispending-refetch-loop.test.ts×1 latest-memo-inline-parity.test.ts×4 latest-two-readers-one-ask-4393.test.ts×1 latest-unread-identity-memo-4474.test.ts×2 midflight-memo-over-reask-4504.test.ts×3 probe-pulled-pass-4429.test.ts×1 released-memo-retry-runs-4474.test.ts×2 staged-error-derivation.test.ts×1 staged-error-through-async-memo-4640.test.ts×1 two-sources-answered-reasked-4495.test.ts×7 verdict-memo-flight-no-reask.test.ts×1 verdict-probe-committed-4648.test.ts×1 | `latest(x)` shows the newest answer `x` has produced; before any, it throws | +| D-23 | live | `docs/RULES-DERIVED.md:149` | core.ts×1 lanes.ts×2 scheduler.ts×1 seat.ts×4 verdict.ts×6 | action-refresh-ispending-loop-4349.test.ts×1 first-load-superseded-answer-4569.test.ts×4 fresh-boundary-born-held-reader.test.ts×1 gc.test.ts×1 hold-recovers-landing-reruns-4469.test.ts×1 ispending-memo-parked-seam-4439.test.ts×1 latest-ispending-refetch-loop.test.ts×1 latest-pulled-pass-ispending.test.ts×1 latest-two-readers-one-ask-4393.test.ts×2 midflight-memo-over-reask-4504.test.ts×1 netted-verdict-reader-4485.test.ts×1 released-memo-retry-runs-4474.test.ts×1 staged-error-derivation.test.ts×1 staged-error-through-async-memo-4640.test.ts×1 strict-read-pending-nested-untrack.test.ts×2 verdict-probe-committed-4648.test.ts×5 verdict-probe-landing-4648.test.ts×2 verdict-read-order.test.ts×1 | `isPending(x)` is true while a new question about `x` has no committed answer | +| D-24 | live | `docs/RULES-DERIVED.md:157` | seat.ts×1 verdict.ts×1 | verdict-probe-committed-4648.test.ts×2 | `isPending` never throws unowned; the probe is reads-only | +| D-25 | live | `docs/RULES-DERIVED.md:161` | async.ts×2 constants.ts×1 core.ts×8 lanes.ts×6 scheduler.ts×1 seat.ts×16 verdict.ts×6 projection.ts×1 store.ts×5 | derived-store-lane-derive.test.ts×1 error-staged-recovery.test.ts×1 errored-recovery-wedge-4403-4404-4405.test.ts×1 fallback-fn-latest-runaway-4467.test.ts×2 held-frame-verdict-parity.test.ts×1 ispending-async-derivation-4677.test.ts×1 ispending-memo-parked-seam-4439.test.ts×1 latest-first-one-pass-4402.test.ts×2 latest-memo-inline-parity.test.ts×1 latest-stale-branch-hold-4661.test.ts×1 latest-two-readers-one-ask-4393.test.ts×1 midflight-memo-over-reask-4504.test.ts×4 posture-store-parity.test.ts×2 projection-lane-length-4631.test.ts×1 derived-store-heal-4451.test.ts×1 unread-derive-midflight-mount-4491.test.ts×2 verdict-born-into-other-flush-4426.test.ts×1 verdict-memo-after-staged-read.test.ts×1 verdict-memo-flight-no-reask.test.ts×12 verdict-memo-held-branch-4544.test.ts×3 verdict-probe-landing-4648.test.ts×1 verdict-seat-merged-lane-4427.test.ts×1 verdict-shortcut-pending-source-liveness.test.ts×1 visibility-oracle-posture.test.ts×1 | A display-ahead read never holds the change it reads ahead of; its reader's own async holds the reader | +| D-26 | live | `docs/RULES-DERIVED.md:169` | core.ts×1 lanes.ts×4 scheduler.ts×2 seat.ts×6 verdict.ts×2 | latest-first-one-pass-4402.test.ts×1 latest-memo-inline-parity.test.ts×2 latest-two-readers-one-ask-4393.test.ts×1 midflight-memo-over-reask-4504.test.ts×1 optimistic-second-guess-reask-4629.test.ts×2 optimistic-second-log-guess-4666.test.ts×3 posture-store-parity.test.ts×2 projection-second-guess-4629.test.ts×2 verdict-memo-after-staged-read.test.ts×3 verdict-memo-flight-no-reask.test.ts×2 | A `latest()`/optimistic reader mounted or re-run mid-hold shows the committed value and reveals with the lane | +| D-27 | live | `docs/RULES-DERIVED.md:173` | boundaries.ts×1 core.ts×2 invariants.ts×1 lanes.ts×1 seat.ts×9 verdict.ts×1 | display-ahead-plain-write-content.test.ts×2 fallback-born-creator-4428.test.ts×2 fallback-fn-latest-runaway-4467.test.ts×1 held-frame-verdict-parity.test.ts×1 latest-unread-identity-memo-4474.test.ts×1 midflight-memo-over-reask-4504.test.ts×1 probe-pulled-pass-4429.test.ts×1 verdict-born-into-other-flush-4426.test.ts×1 verdict-seat-merged-lane-4427.test.ts×2 | The branch a display-ahead `Show` opens is a mainline mount; its plain reads are the screen's | +| D-28 | live | `docs/RULES-DERIVED.md:177` | lanes.ts×1 scheduler.ts×1 seat.ts×7 | latest-first-later-throw-4275.test.ts×1 optimistic-second-guess-reask-4629.test.ts×1 optimistic-second-log-guess-4666.test.ts×1 projection-second-guess-4629.test.ts×1 verdict-seat-merged-lane-4427.test.ts×2 | A read throws on its own; an earlier read does not disarm a later throw | +| D-29 | live | `docs/RULES-DERIVED.md:181` | effect.ts×2 seat.ts×2 | held-sync-throw-4109.test.ts×1 optimistic-held-error-untracked-read-4489.test.ts×1 tracked-effect-staged-dep-4606-4607.test.ts×3 | `createTrackedEffect` and `onSettled` callbacks see the screen | +| D-30 | live | `docs/RULES-DERIVED.md:189` | projection.ts×1 store.ts×1 | derived-store-first-landing-verdict.test.ts×1 derived-store-ispending-boundary-4269.test.ts×1 derived-store-latest-seed-4355.test.ts×1 unread-derive-midflight-mount-4491.test.ts×3 | A derived store's seed is a draft, never an observable value | +| D-31 | live | `docs/RULES-DERIVED.md:197` | lanes.ts×1 seat.ts×1 | optimistic-keyed-fold.test.ts×1 optimistic-live-echo-3898.test.ts×1 optimistic-outside-reask-4690.test.ts×1 optimistic-second-guess-reask-4629.test.ts×1 until-memo-confirmation-3943.test.ts×1 | `resolve`, `until` and `refresh` deliver by the hold they are called in | +| D-32 | live | `docs/RULES-DERIVED.md:211` | seat.ts×2 | — | Nothing shows a write before its flush; a guess with nothing to be optimistic over is void | +| D-33 | live | `docs/RULES-DERIVED.md:215` | lanes.ts×2 scheduler.ts×1 | optimistic-key-guess-reask-4329.test.ts×4 optimistic-lane-seam-loop-4643.test.ts×2 optimistic-layered-filter-4336.test.ts×3 optimistic-list-push-filter-push-4642.test.ts×2 optimistic-nested-action-refresh-4576.test.ts×1 optimistic-outside-reask-4690.test.ts×6 optimistic-question-change-4336.test.ts×3 optimistic-second-guess-reask-4629.test.ts×2 optimistic-second-log-guess-4666.test.ts×3 | An optimistic value lives exactly as long as the hold it was made over; same-slot writes entangle, disjoint slots do not | +| D-34 | live | `docs/RULES-DERIVED.md:223` | async.ts×1 core.ts×4 lanes.ts×7 scheduler.ts×3 seat.ts×7 verdict.ts×1 hooks.ts×1 optimistic.ts×10 reconcile.ts×1 store.ts×2 | errored-list-recovery-held-4526.test.ts×1 lane-dissolve-disposed-member-4613.test.ts×1 loading-on-held-change-4605.test.ts×1 loading-on-held-filter-refresh-4379.test.ts×1 loading-on-latest-fallback-4337.test.ts×1 optimistic-guess-memo-binding-4561.test.ts×3 optimistic-guess-parked-question-4559.test.ts×1 optimistic-guess-split-bindings-4536.test.ts×1 optimistic-held-error-untracked-read-4489.test.ts×2 optimistic-key-guess-reask-4329.test.ts×3 optimistic-keyed-fold.test.ts×3 optimistic-lane-seam-loop-4643.test.ts×2 optimistic-list-push-filter-push-4642.test.ts×4 optimistic-outside-reask-4690.test.ts×3 optimistic-question-change-4336.test.ts×7 optimistic-second-guess-reask-4629.test.ts×1 optimistic-second-log-guess-4666.test.ts×1 projection-lane-length-4631.test.ts×1 derived-latest-confirmed-key.test.ts×3 superseded-before-first-commit.test.ts×1 visibility-oracle.states.ts×3 | A different truth corrects the graph now and the screen at the commit; what the screen never showed is void | +| D-35 | live | `docs/RULES-DERIVED.md:231` | lanes.ts×1 optimistic.ts×1 | optimistic-guess-split-bindings-4536.test.ts×1 optimistic-layered-filter-4336.test.ts×2 optimistic-list-push-filter-push-4642.test.ts×6 optimistic-outside-reask-4690.test.ts×1 optimistic-second-guess-reask-4629.test.ts×1 optimistic-second-log-guess-4666.test.ts×1 | Provenance: an older question's answer never corrects a newer guess; the body's end corrects what nothing authoritative is answering | +| D-36 | live | `docs/RULES-DERIVED.md:235` | — | — | A resting optimistic node is a plain memo | +| D-37 | live | `docs/RULES-DERIVED.md:239` | async.ts×1 core.ts×3 lanes.ts×1 scheduler.ts×2 seat.ts×9 optimistic.ts×1 | display-ahead-plain-write-content.test.ts×1 lane-mount-effects-4557.test.ts×1 optimistic-held-write-leak.test.ts×1 optimistic-lane-seam-loop-4643.test.ts×5 optimistic-list-push-filter-push-4642.test.ts×1 | A lane sees the screen plus its own guesses and never holds a sync write | +| D-38 | live | `docs/RULES-DERIVED.md:245` | — | errored-reader-4486.test.ts×1 | Effect error phases; a comparator throw is a compute error; an escaping error halts | +| D-39 | live | `docs/RULES-DERIVED.md:253` | boundaries.ts×9 core.ts×1 lanes.ts×1 scheduler.ts×3 types.ts×1 store.ts×1 | derived-store-recovery-wake.test.ts×1 error-boundary-superseded-staged-error-4527.test.ts×1 error-is-an-answer.test.ts×2 error-staged-recovery.test.ts×1 errored-heal-memo-reader-4644.test.ts×2 errored-list-recovery-held-4526.test.ts×1 errored-memo-over-output-held-reject-4640.test.ts×1 errored-over-rearmed-loading-4384.test.ts×1 errored-reader-4486.test.ts×3 errored-recovery-fresh-loading-4627.test.ts×2 errored-recovery-stale-memo-4592.test.ts×2 errored-recovery-wedge-4403-4404-4405.test.ts×5 latest-memo-inline-parity.test.ts×2 latest-uninitialized-over-latest-4362.test.ts×1 createProjection.async.test.ts×1 derived-store-heal-4451.test.ts×1 | An error fallback is the display; it stays until a new answer replaces the error | +| D-40 | live | `docs/RULES-DERIVED.md:293` | constants.ts×1 core.ts×1 deferred.ts×2 signals.ts×1 | create-deferred-3710.test.ts×1 create-deferred.test.ts×1 | A deferred source lags its readers and never leads them | +| D-41 | live | `docs/RULES-DERIVED.md:257` | boundaries.ts×5 async.ts×5 constants.ts×1 core.ts×11 scheduler.ts×3 seat.ts×10 types.ts×1 verdict.ts×4 signals.ts×1 projection.ts×3 store.ts×2 | derived-store-error-delivered-once.test.ts×2 derived-store-recovery-wake.test.ts×1 error-boundary-superseded-staged-error-4527.test.ts×3 error-is-an-answer.test.ts×3 error-staged-recovery.test.ts×1 errored-held-mount-staged-error-4480.test.ts×3 errored-latest-obsolete-flight-4568.test.ts×1 errored-list-recovery-held-4526.test.ts×2 errored-memo-over-output-held-reject-4640.test.ts×1 errored-reader-4486.test.ts×1 errored-recovery-fresh-loading-4627.test.ts×4 errored-recovery-wedge-4403-4404-4405.test.ts×6 errored-staged-source-4364.test.ts×1 held-sync-throw-4109.test.ts×2 hold-recovers-landing-reruns-4469.test.ts×3 latest-beside-staged-read-4365.test.ts×1 latest-derived-held-error-4106.test.ts×1 latest-derived-held-error-guess-lane-4106.test.ts×1 latest-derived-sync-error-4377.test.ts×1 loading-reveal-held-outside-4593.test.ts×1 optimistic-held-error-untracked-read-4489.test.ts×3 settle-diamond-revisit.test.ts×1 staged-error-derivation.test.ts×1 staged-error-rerun-4109.test.ts×1 staged-error-through-async-memo-4640.test.ts×1 writable-memo-write-clears-error-4110.test.ts×1 | An error is an answer: it ends the wait like a value, and it shows when a value would | +| D-42 | live | `docs/RULES-DERIVED.md:275` | boundaries.ts×2 async.ts×3 core.ts×5 dev.ts×1 effect.ts×2 owner.ts×1 verdict.ts×1 index.ts×1 signals.ts×1 | caught-reactive-read-diagnostic.test.ts×1 errored-reader-4486.test.ts×2 try-catch-reactive-read.test.ts×2 | A reactive read is not an `await`: `try/catch` around it is not a channel | + ## A — spec propositions -| id | status | defined | cited in src | cited in tests | statement (at definition) | -|---|---|---|---|---|---| -| A1 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:321` | seat.ts×3 | 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:329` | — | 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:337` | async.ts×1 core.ts×2 | derived-write-then-derivation-3733.test.ts×5 equals-comparator-errors.test.ts×1 | [ruled] Comparator throws are compute-phase errors — Errors thrown by a user `equals` comparator behave exactly like compute-phase errors (boundary-containable; loud halt without a boundary). | -| A4 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:345` | — | derived-write-then-derivation-3733.test.ts×1 equals-comparator-errors.test.ts×1 | [ruled] A custom `equals` never sees `undefined` prev on first commit — A custom `equals` is never invoked with `undefined` previous value on a node's first commit. | -| A5 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:353` | — | derived-write-then-derivation-3733.test.ts×1 errorHalt.test.ts×1 | [ruled] An error escaping every boundary halts the system — An error escaping every boundary permanently halts the system with `REACTIVITY_HALTED`; later writes log "Update ignored". | -| A6 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:361` | boundaries.ts×4 | derived-write-then-derivation-3733.test.ts×1 enforceLoadingBoundary.test.ts×1 loading-on-errorboundary-through-4639.test.ts×1 | [ruled] `ASYNC_OUTSIDE_LOADING_BOUNDARY` is warn-only — `ASYNC_OUTSIDE_LOADING_BOUNDARY` is a warn-only diagnostic; an `Errored` above must not swallow it and must not show its fallback for a pending. | -| A7 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:167` | seat.ts×1 truth.ts×1 verdict.ts×1 | fuzz-findings-l2.test.ts×1 latest-uninitialized-over-latest-4362.test.ts×2 spec-async-semantics.test.ts×2 visibility-oracle-store.states.ts×1 visibility-oracle.states.ts×1 visibility-oracle.test.ts×1 | [ruled, amended in place] Resolved async never reads `[false, undefined]` — After an async memo resolves, `[isPending(x), latest(x)]` is `[false, resolvedValue]` — never `[false, undefined]`. **Amende… | -| A8 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:175` | — | create-deferred-3710.test.ts×1 createMemo.test.ts×1 visibility-oracle-store.states.ts×1 visibility-oracle.states.ts×2 | [ruled, amended in place 2026-07-07] `isPending(() => latest(x))` follows `x`'s own async only — verdicts are per-channel — (**re-ruled 2026-07-07c** — was "tracks the transition the same as `isPendin… | -| A9 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:183` | async.ts×1 projection.ts×1 target.ts×1 | spec-async-semantics.test.ts×3 visibility-oracle-store.states.ts×4 visibility-oracle-store.test.ts×1 | [ruled, amended in place 2026-07-07] Store leaves behind a firewall report the firewall's new-question refetch — `isPending` on a store leaf behind a firewall reports the firewall's refetch like any a… | -| A10 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:191` | async.ts×1 core.ts×4 lanes.ts×2 scheduler.ts×1 seat.ts×1 verdict.ts×5 | boundary-not-born-held-3540.test.ts×1 createMemo.test.ts×1 error-staged-recovery.test.ts×1 fuzz-findings-l2.test.ts×1 ispending-memo-unstamped-hold-3457.test.ts×2 latest-isPending-consistency.test.ts×3 latest-stale-branch-hold-4661.test.ts×3 latest-uninitialized-over-latest-4362.test.ts×1 verdict-contract.test.ts×2 verdict-memo-flight-no-reask.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:103` | — | latest-isPending-consistency.test.ts×1 loading-fallback-in-flush-3540.test.ts×2 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×1 | [ruled] Sync derivations of held sources are visible through `latest()`/`isPending()` — Sync derivations of transition-held sources are visible through `latest()`/`isPending()` (held sync recompute is… | -| A12 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:199` | — | 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:207` | — | 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:215` | — | spec-async-semantics.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B2)] Companion nodes get child lanes that do not merge with the owner — (was B2) `isPending`/`latest` companion nodes get child lanes that do not mer… | -| A15 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:251` | async.ts×2 constants.ts×1 core.ts×4 effect.ts×1 lanes.ts×7 scheduler.ts×18 seat.ts×1 verdict.ts×5 store.ts×1 | async-chain-supersession.test.ts×1 boundary-output-frame-reader.test.ts×1 create-deferred-3710.test.ts×2 first-observer-stale-reader.test.ts×1 fuzz-findings-l2.test.ts×17 held-restore.test.ts×1 hold-recovers-landing-reruns-4469.test.ts×1 l2-contract.test.ts×7 l2-fuzz-existing-rules.test.ts×2 lane-contract.test.ts×1 lane-hold-on-observation.test.ts×1 lane-outside-view.test.ts×1 lane-pass-stamped-effect-3662.test.ts×1 latest-memo-inline-parity.test.ts×1 latest-stale-branch-hold-4661.test.ts×1 latest-uninitialized-over-latest-4362.test.ts×1 loading-fallback-in-flush-3540.test.ts×4 mount-over-foreign-hold-3761.test.ts×2 optimistic-lane-seam-loop-4643.test.ts×1 overlapping-flights.test.ts×3 posture-born-held-and-observation.test.ts×4 posture-store-parity.test.ts×2 reveal-carve-out.test.ts×2 shared-effect-no-entangle.test.ts×1 show-stale-child-in-action-4137.test.ts×1 spec-async-semantics.test.ts×2 stale-read-uninitialized-cross-transition.test.ts×1 superseded-source-blocks-3462.test.ts×2 tick-scoped-pass-transaction.test.ts×1 tracked-effect-staged-dep-4606-4607.test.ts×1 treeshake.test.ts×4 unread-derive-midflight-mount-4491.test.ts×1 verdict-memo-flight-no-reask.test.ts×1 verdict-memo-held-branch-4544.test.ts×1 verdict-probe-committed-4648.test.ts×2 verdict-shortcut-pending-source-liveness.test.ts×1 visibility-oracle-store.states.ts×6 visibility-oracle.states.ts×7 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B3)] Transition entanglement is graph-driven; lanes settle as one reveal — (was B3) Transition entanglement is graph-driven: writes whose async work … | -| A16 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:223` | scheduler.ts×1 | caught-reactive-read-diagnostic.test.ts×1 latest-uninitialized-over-latest-4362.test.ts×2 spec-async-semantics.test.ts×1 strict-read-pending-store.test.ts×2 uninitialized-visibility.test.ts×1 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×2 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B5)] `isPending` never throws in untracked contexts — (was B5) `isPending` never throws in untracked contexts — thunks that throw real errors or read… | -| A17 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:71` | async.ts×3 constants.ts×1 lanes.ts×8 scheduler.ts×2 seat.ts×1 verdict.ts×1 map.ts×1 optimistic.ts×2 store.ts×3 | create-deferred-3710.test.ts×1 fuzz-findings-l2.test.ts×6 lane-uninitialized-landing-3648.test.ts×5 optimistic-outside-reask-4690.test.ts×1 optimistic-over-held-row-3796.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×2 optimistic-undefined-override.test.ts×1 posture-store-parity.test.ts×1 refresh-await.test.ts×1 reveal-gating-contract.test.ts×3 spec-async-semantics.test.ts×10 createOptimisticStore.test.ts×2 kanban-a17-fixture.test.ts×3 optimistic-list-mutation-matrix.test.ts×1 optimistic-maparray-index-frame-f1.test.ts×1 optimistic-untracked-reads-f3-f5.test.ts×1 signal-store-twins-qd.test.ts×1 treeshake.test.ts×1 until.test.ts×1 visibility-oracle-store.states.ts×24 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×20 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from C4)] An active override is the displayed value until its transaction commits, and the graph's value until its own source answers — **Statement (curre… | -| A18 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:81` | action.ts×1 async.ts×2 core.ts×3 lanes.ts×8 scheduler.ts×2 types.ts×1 verdict.ts×1 map.ts×1 optimistic.ts×1 projection.ts×1 | body-end-supersession-visibility.test.ts×4 createOptimistic.test.ts×3 l2-fuzz-existing-rules.test.ts×2 lane-contract.test.ts×1 lane-frame-deferred-run-3662.test.ts×1 lane-outside-view.test.ts×1 lane-uninitialized-landing-3648.test.ts×5 optimistic-move-duplicate-3548.test.ts×2 optimistic-read-lane-not-transaction-3698.test.ts×4 posture-store-parity.test.ts×5 spec-async-semantics.test.ts×5 flight-owned-transaction.test.ts×1 lane-authority-twins.test.ts×1 optimistic-list-mutation-matrix.test.ts×1 optimistic-untracked-reads-f3-f5.test.ts×1 signal-store-twins-qd.test.ts×1 unchanged-presence-no-hold-3743.test.ts×2 superseded-before-first-commit.test.ts×4 visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×17 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from B4)] An override lives exactly as long as its own transaction; a newer truth from the source supersedes it in the graph immediately, on screen at com… | -| A19 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:151` | boundaries.ts×1 async.ts×1 constants.ts×1 core.ts×6 lanes.ts×1 scheduler.ts×2 seat.ts×1 types.ts×1 verdict.ts×6 store.ts×2 | create-deferred-3710.test.ts×2 derived-store-first-landing-verdict.test.ts×1 errored-latest-obsolete-flight-4568.test.ts×1 fuzz-findings-l2.test.ts×4 lane-uninitialized-landing-3648.test.ts×2 latest-memo-inline-parity.test.ts×1 latest-uninitialized-over-latest-4362.test.ts×2 mount-over-foreign-hold-3761.test.ts×2 spec-async-semantics.test.ts×3 derived-presence-async-3726.test.ts×2 superseded-before-first-commit.test.ts×3 uninitialized-visibility.test.ts×1 verdict-memo-flight-no-reask.test.ts×2 visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×14 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from C1)] `isPending(x)` ≡ the observable value is not final (three causes) — (was C1 — **partially reverses an earlier decision**) **Definition: `isPendi… | -| A20 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:410` | — | 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:417` | — | question-scoped-pending.test.ts×3 spec-async-semantics.test.ts×3 | [superseded 2026-07-13 by A24] (superseded) The store-wide mask — (**SUPERSEDED 2026-07-13 by A24** — the store-wide mask is deleted with the mask model; nothing silences a new question. The effective… | -| A22 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:231` | store.ts×1 | createProjection.draft-lifetime-3585.test.ts×1 spec-async-semantics.test.ts×1 visibility-oracle-store.states.ts×1 | [ruled, amended in place 2026-07-08] Pending is per-node; store-wide only for the firewall's own work — **Pending is per-node: store-wide verdicts exist only as the firewall's own in-flight work (A9) … | -| A23 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:241` | — | 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:159` | affects-marks.ts×1 affects.ts×1 constants.ts×1 lanes.ts×2 types.ts×1 verdict.ts×3 affects.ts×1 optimistic.ts×1 | body-end-supersession-visibility.test.ts×1 create-deferred-3710.test.ts×2 optimistic-undefined-override.test.ts×1 reveal-gating-contract.test.ts×1 spec-async-semantics.test.ts×3 kanban-a17-fixture.test.ts×1 signal-store-twins-qd.test.ts×1 visibility-oracle-store.states.ts×4 visibility-oracle.states.ts×5 visibility-oracle.test.ts×1 | [ruled 2026-07-13] Question-scoped pending: pending iff a value change is in flight or an `affects()` mark is live — (**ruled 2026-07-13** — supersedes A20/A21; the converged model from the #2844/#272… | -| A25 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:311` | projection.ts×2 store.ts×1 | derived-presence-async-3726.test.ts×2 seedless-derived-store.test.ts×1 uninitialized-visibility.test.ts×3 visibility-oracle-store.states.ts×7 visibility-oracle-store.test.ts×1 | [ruled 2026-07-16] A derived store's seed is a draft, never an observable value — (**ruled 2026-07-16**, #2897) **A derived store's seed is a draft, never an observable value.** The seed exists for th… | -| A26 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:111` | — | action-await-contract.test.ts×2 fuzz-findings-l2.test.ts×1 posture-store-parity.test.ts×2 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×1 visibility-oracle.test.ts×1 | [ruled 2026-07-17] An ambient transaction window is one flush; parking is flush-driven — (**ruled 2026-07-17**, #2913; **enforcement hardened 2026-08-31**, #3141 — parking is flush-driven, and a trans… | -| A27 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:303` | — | loading-value.test.ts×2 visibility-oracle.states.ts×18 visibility-oracle.test.ts×1 | [ruled 2026-08-10] The commit-#0 loading window is loading-class and verdict-quiet — (**ruled 2026-08-10**) **The commit-#0 loading window is loading-class and verdict-quiet.** A node born committed v… | -| A28 | ruled, mechanism landed | `docs/SPEC-ASYNC-SEMANTICS.md:95` | constants.ts×1 core.ts×12 lanes.ts×2 scheduler.ts×3 seat.ts×3 types.ts×1 verdict.ts×2 store.ts×5 | continuation-sibling-reader-4146.test.ts×1 createOptimistic.test.ts×5 fuzz-findings-l2.test.ts×6 held-derivation-not-a-proposal-3612.test.ts×1 issue-3800-repro.test.ts×1 latest-held-till-flush.test.ts×1 latest-memo-inline-parity.test.ts×1 posture-store-parity.test.ts×5 question-scoped-pending.test.ts×3 snapshot-derived-store-rows.test.ts×1 createOptimisticStore.test.ts×10 optimistic-draft-visibility-3665.test.ts×5 optimistic-list-mutation-matrix.harness.ts×1 optimistic-list-mutation-matrix.test.ts×2 shallow.test.ts×1 woken-transaction-adopts-staged-bump.test.ts×1 treeshake.test.ts×2 verdict-contract.test.ts×1 visibility-oracle-store.states.ts×8 visibility-oracle.states.ts×8 | [ruled, mechanism landed 2026-09-15] A write becomes visible at flush — to every channel — (**ruled 2026-09-08**; supersedes the #2922 mid-tick pull) **A write becomes visible at flush — to every chan… | -| A29 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:119` | boundaries.ts×1 action.ts×1 constants.ts×1 core.ts×14 effect.ts×1 lanes.ts×1 scheduler.ts×4 signals.ts×1 store.ts×1 | adoption-unchanged-key-read-3706.test.ts×9 body-end-supersession-visibility.test.ts×1 born-held.test.ts×3 boundary-not-born-held-3540.test.ts×4 createProjection.draft-lifetime-3585.test.ts×1 direct-commit-readers-posture.test.ts×1 fallback-born-creator-4428.test.ts×1 fuzz-findings-l2.test.ts×6 held-branch-verdict-reader-4010.test.ts×1 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 held-frame-verdict-parity.test.ts×1 held-truth-lane-only.test.ts×3 l2-contract.test.ts×1 latest-held-till-flush.test.ts×2 loading-fallback-in-flush-3540.test.ts×6 loading-on-frame-following-3540.test.ts×1 mount-over-foreign-hold-3761.test.ts×1 optimistic-mount-nested-memo-3835.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×3 posture-born-held-and-observation.test.ts×1 posture-store-parity.test.ts×6 derived-presence-async-3726.test.ts×3 optimistic-untracked-reads-f3-f5.test.ts×1 store-unchanged-read-independent-write-3688.test.ts×1 tick-scoped-pass-transaction.test.ts×2 treeshake.test.ts×3 verdict-born-into-other-flush-4426.test.ts×1 verdict-mount-first-pass-3851.test.ts×2 verdict-mount-loading-flush-loop.test.ts×2 visibility-oracle-store.states.ts×5 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×9 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-09-13 (#3408)] A tracked read served a live transaction's staged value enters that transaction — A tracked computation served a node's staged `_pendingValue` — a value a … | -| A30 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:259` | async.ts×3 attribution.ts×1 constants.ts×1 core.ts×1 effect.ts×1 lanes.ts×1 scheduler.ts×5 projection.ts×1 | async-landing-deps-3461.test.ts×3 fuzz-findings-l2.test.ts×3 held-conditional-effect.test.ts×1 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 ispending-in-boundary-on-3528.test.ts×1 lane-frame-deferred-run-3662.test.ts×1 lane-frame-held-lane-3662.test.ts×1 posture-born-held-and-observation.test.ts×1 projection-peek-loop-4342.test.ts×1 treeshake.test.ts×2 write-proposals-3494.test.ts×2 zombie-rerun-after-commit-3546.test.ts×2 | [ruled 2026-09-13 (#3410)] A memo's dependencies are the committed frame's until the frame is replaced — A pass that _staged_ its value has not replaced the committed frame, so the committed value sti… | -| A31 | live | `docs/SPEC-ASYNC-SEMANTICS.md:133` | boundaries.ts×1 core.ts×1 lanes.ts×1 verdict.ts×2 | fuzz-findings-l2.test.ts×6 ispending-combined-atomic-3442.test.ts×1 latest-pulled-pass-ispending.test.ts×1 probe-pulled-pass-4429.test.ts×1 unowned-memo-ispending-reader-4366.test.ts×1 verdict-memo-held-branch-4544.test.ts×1 | [live 2026-09-14 (#3442)] A memo computes under its own lane posture, never its puller's — A memo's value is one shared slot every reader sees, so its pass runs under the lane posture the memo itself … | -| A32 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:141` | core.ts×4 lanes.ts×1 seat.ts×1 store.ts×1 | visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×9 visibility-oracle.test.ts×1 | [ruled 2026-09-14] Children-forbidden readers see the frame, not the graph — `createTrackedEffect` and `onSettled` callbacks are effect-phase code that runs after the frame is decided. They read the f… | -| A33 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:275` | boundaries.ts×1 scheduler.ts×2 | async-chain-supersession.test.ts×2 boundary-not-born-held-3540.test.ts×2 create-deferred-3710.test.ts×1 fuzz-findings-l2.test.ts×4 ispending-in-boundary-on-3528.test.ts×2 loading-reset-collects-forwarded-3459.test.ts×3 | [ruled 2026-09-12 (#3375)] A fallback-caught flight holds no transaction; a Loading reset moves the hold onto the boundary — A `` boundary showing its fallback is the display of everything un… | -| A34 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:285` | constants.ts×1 core.ts×6 lanes.ts×2 scheduler.ts×2 store.ts×7 | a34-writes-then-derivations.test.ts×2 continuation-sibling-reader-4146.test.ts×1 createMemo.test.ts×1 derived-write-then-derivation-3733.test.ts×1 finalize-reentry.test.ts×2 fuzz-findings-l2.test.ts×5 held-derivation-not-a-proposal-3612.test.ts×6 optimistic-guess-over-held-slot.test.ts×2 optimistic-list-mutation-matrix.test.ts×1 unchanged-presence-no-hold-3743.test.ts×3 woken-transaction-adopts-staged-bump.test.ts×1 transition-corpse-revival.test.ts×1 treeshake.test.ts×2 visibility-oracle.states.ts×2 write-proposals-3494.test.ts×5 | [ruled 2026-09-16 (#3494)] A write is a proposal: one on a held node entangles its tick; one that nets to the committed value is none — A write proposes a value for a node. **Held, both are suggestion… | +| id | status | defined | cited in src | cited in tests | statement (at definition) | +| --- | ----------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| A1 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:321` | seat.ts×3 | 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:329` | — | 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:337` | async.ts×1 core.ts×2 | derived-write-then-derivation-3733.test.ts×5 equals-comparator-errors.test.ts×1 | [ruled] Comparator throws are compute-phase errors — Errors thrown by a user `equals` comparator behave exactly like compute-phase errors (boundary-containable; loud halt without a boundary). | +| A4 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:345` | — | derived-write-then-derivation-3733.test.ts×1 equals-comparator-errors.test.ts×1 | [ruled] A custom `equals` never sees `undefined` prev on first commit — A custom `equals` is never invoked with `undefined` previous value on a node's first commit. | +| A5 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:353` | — | derived-write-then-derivation-3733.test.ts×1 errorHalt.test.ts×1 | [ruled] An error escaping every boundary halts the system — An error escaping every boundary permanently halts the system with `REACTIVITY_HALTED`; later writes log "Update ignored". | +| A6 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:361` | boundaries.ts×4 | derived-write-then-derivation-3733.test.ts×1 enforceLoadingBoundary.test.ts×1 loading-on-errorboundary-through-4639.test.ts×1 | [ruled] `ASYNC_OUTSIDE_LOADING_BOUNDARY` is warn-only — `ASYNC_OUTSIDE_LOADING_BOUNDARY` is a warn-only diagnostic; an `Errored` above must not swallow it and must not show its fallback for a pending. | +| A7 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:167` | seat.ts×1 truth.ts×1 verdict.ts×1 | fuzz-findings-l2.test.ts×1 latest-uninitialized-over-latest-4362.test.ts×2 spec-async-semantics.test.ts×2 visibility-oracle-store.states.ts×1 visibility-oracle.states.ts×1 visibility-oracle.test.ts×1 | [ruled, amended in place] Resolved async never reads `[false, undefined]` — After an async memo resolves, `[isPending(x), latest(x)]` is `[false, resolvedValue]` — never `[false, undefined]`. \*\*Amende… | +| A8 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:175` | — | create-deferred-3710.test.ts×1 createMemo.test.ts×1 visibility-oracle-store.states.ts×1 visibility-oracle.states.ts×2 | [ruled, amended in place 2026-07-07] `isPending(() => latest(x))` follows `x`'s own async only — verdicts are per-channel — (**re-ruled 2026-07-07c** — was "tracks the transition the same as `isPendin… | +| A9 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:183` | async.ts×1 projection.ts×1 target.ts×1 | spec-async-semantics.test.ts×3 visibility-oracle-store.states.ts×4 visibility-oracle-store.test.ts×1 | [ruled, amended in place 2026-07-07] Store leaves behind a firewall report the firewall's new-question refetch — `isPending` on a store leaf behind a firewall reports the firewall's refetch like any a… | +| A10 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:191` | async.ts×1 core.ts×4 lanes.ts×2 scheduler.ts×1 seat.ts×2 verdict.ts×5 | boundary-not-born-held-3540.test.ts×1 createMemo.test.ts×1 error-staged-recovery.test.ts×1 fuzz-findings-l2.test.ts×1 ispending-memo-unstamped-hold-3457.test.ts×2 latest-isPending-consistency.test.ts×3 latest-stale-branch-hold-4661.test.ts×3 latest-uninitialized-over-latest-4362.test.ts×1 verdict-contract.test.ts×2 verdict-memo-flight-no-reask.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:103` | — | latest-isPending-consistency.test.ts×1 loading-fallback-in-flush-3540.test.ts×2 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×1 | [ruled] Sync derivations of held sources are visible through `latest()`/`isPending()` — Sync derivations of transition-held sources are visible through `latest()`/`isPending()` (held sync recompute is… | +| A12 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:199` | — | 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:207` | — | 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:215` | — | spec-async-semantics.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B2)] Companion nodes get child lanes that do not merge with the owner — (was B2) `isPending`/`latest` companion nodes get child lanes that do not mer… | +| A15 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:251` | async.ts×2 constants.ts×1 core.ts×4 effect.ts×1 lanes.ts×7 scheduler.ts×18 seat.ts×1 verdict.ts×5 store.ts×1 | async-chain-supersession.test.ts×1 boundary-output-frame-reader.test.ts×1 create-deferred-3710.test.ts×2 first-observer-stale-reader.test.ts×1 fuzz-findings-l2.test.ts×17 held-restore.test.ts×1 hold-recovers-landing-reruns-4469.test.ts×1 l2-contract.test.ts×7 l2-fuzz-existing-rules.test.ts×2 lane-contract.test.ts×1 lane-hold-on-observation.test.ts×1 lane-outside-view.test.ts×1 lane-pass-stamped-effect-3662.test.ts×1 latest-memo-inline-parity.test.ts×1 latest-stale-branch-hold-4661.test.ts×1 latest-uninitialized-over-latest-4362.test.ts×1 loading-fallback-in-flush-3540.test.ts×4 mount-over-foreign-hold-3761.test.ts×2 optimistic-lane-seam-loop-4643.test.ts×1 overlapping-flights.test.ts×3 posture-born-held-and-observation.test.ts×4 posture-store-parity.test.ts×2 reveal-carve-out.test.ts×2 shared-effect-no-entangle.test.ts×1 show-stale-child-in-action-4137.test.ts×1 spec-async-semantics.test.ts×2 stale-read-uninitialized-cross-transition.test.ts×1 superseded-source-blocks-3462.test.ts×2 tick-scoped-pass-transaction.test.ts×1 tracked-effect-staged-dep-4606-4607.test.ts×1 treeshake.test.ts×4 unread-derive-midflight-mount-4491.test.ts×1 verdict-memo-flight-no-reask.test.ts×1 verdict-memo-held-branch-4544.test.ts×1 verdict-probe-committed-4648.test.ts×2 verdict-shortcut-pending-source-liveness.test.ts×1 visibility-oracle-store.states.ts×6 visibility-oracle.states.ts×7 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B3)] Transition entanglement is graph-driven; lanes settle as one reveal — (was B3) Transition entanglement is graph-driven: writes whose async work … | +| A16 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:223` | scheduler.ts×1 | caught-reactive-read-diagnostic.test.ts×1 latest-uninitialized-over-latest-4362.test.ts×2 spec-async-semantics.test.ts×1 strict-read-pending-store.test.ts×2 uninitialized-visibility.test.ts×1 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×2 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from B5)] `isPending` never throws in untracked contexts — (was B5) `isPending` never throws in untracked contexts — thunks that throw real errors or read… | +| A17 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:71` | async.ts×3 constants.ts×1 lanes.ts×8 scheduler.ts×2 seat.ts×1 verdict.ts×1 map.ts×1 optimistic.ts×2 store.ts×3 | create-deferred-3710.test.ts×1 fuzz-findings-l2.test.ts×6 lane-uninitialized-landing-3648.test.ts×5 optimistic-outside-reask-4690.test.ts×1 optimistic-over-held-row-3796.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×2 optimistic-undefined-override.test.ts×1 posture-store-parity.test.ts×1 refresh-await.test.ts×1 reveal-gating-contract.test.ts×3 spec-async-semantics.test.ts×10 createOptimisticStore.test.ts×2 kanban-a17-fixture.test.ts×3 optimistic-list-mutation-matrix.test.ts×1 optimistic-maparray-index-frame-f1.test.ts×1 optimistic-untracked-reads-f3-f5.test.ts×1 signal-store-twins-qd.test.ts×1 treeshake.test.ts×1 until.test.ts×1 visibility-oracle-store.states.ts×24 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×20 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-06 (promoted from C4)] An active override is the displayed value until its transaction commits, and the graph's value until its own source answers — \*\*Statement (curre… | +| A18 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:81` | action.ts×1 async.ts×2 core.ts×3 lanes.ts×8 scheduler.ts×2 types.ts×1 verdict.ts×1 map.ts×1 optimistic.ts×1 projection.ts×1 | body-end-supersession-visibility.test.ts×4 createOptimistic.test.ts×3 l2-fuzz-existing-rules.test.ts×2 lane-contract.test.ts×1 lane-frame-deferred-run-3662.test.ts×1 lane-outside-view.test.ts×1 lane-uninitialized-landing-3648.test.ts×5 optimistic-move-duplicate-3548.test.ts×2 optimistic-read-lane-not-transaction-3698.test.ts×4 posture-store-parity.test.ts×5 spec-async-semantics.test.ts×5 flight-owned-transaction.test.ts×1 lane-authority-twins.test.ts×1 optimistic-list-mutation-matrix.test.ts×1 optimistic-untracked-reads-f3-f5.test.ts×1 signal-store-twins-qd.test.ts×1 unchanged-presence-no-hold-3743.test.ts×2 superseded-before-first-commit.test.ts×4 visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×17 visibility-oracle.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from B4)] An override lives exactly as long as its own transaction; a newer truth from the source supersedes it in the graph immediately, on screen at com… | +| A19 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:151` | boundaries.ts×1 async.ts×1 constants.ts×1 core.ts×6 lanes.ts×1 scheduler.ts×2 seat.ts×1 types.ts×1 verdict.ts×6 store.ts×2 | create-deferred-3710.test.ts×2 derived-store-first-landing-verdict.test.ts×1 errored-latest-obsolete-flight-4568.test.ts×1 fuzz-findings-l2.test.ts×4 lane-uninitialized-landing-3648.test.ts×2 latest-memo-inline-parity.test.ts×1 latest-uninitialized-over-latest-4362.test.ts×2 mount-over-foreign-hold-3761.test.ts×2 spec-async-semantics.test.ts×3 derived-presence-async-3726.test.ts×2 superseded-before-first-commit.test.ts×3 uninitialized-visibility.test.ts×1 verdict-memo-flight-no-reask.test.ts×2 visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×14 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-07-07 (promoted from C1)] `isPending(x)` ≡ the observable value is not final (three causes) — (was C1 — **partially reverses an earlier decision**) \*\*Definition: `isPendi… | +| A20 | superseded | `docs/SPEC-ASYNC-SEMANTICS.md:410` | — | 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:417` | — | question-scoped-pending.test.ts×3 spec-async-semantics.test.ts×3 | [superseded 2026-07-13 by A24] (superseded) The store-wide mask — (**SUPERSEDED 2026-07-13 by A24** — the store-wide mask is deleted with the mask model; nothing silences a new question. The effective… | +| A22 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:231` | store.ts×1 | createProjection.draft-lifetime-3585.test.ts×1 spec-async-semantics.test.ts×1 visibility-oracle-store.states.ts×1 | [ruled, amended in place 2026-07-08] Pending is per-node; store-wide only for the firewall's own work — \*\*Pending is per-node: store-wide verdicts exist only as the firewall's own in-flight work (A9) … | +| A23 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:241` | seat.ts×1 | 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:159` | affects-marks.ts×1 affects.ts×1 constants.ts×1 lanes.ts×2 types.ts×1 verdict.ts×3 affects.ts×1 optimistic.ts×1 | body-end-supersession-visibility.test.ts×1 create-deferred-3710.test.ts×2 optimistic-undefined-override.test.ts×1 reveal-gating-contract.test.ts×1 spec-async-semantics.test.ts×3 kanban-a17-fixture.test.ts×1 signal-store-twins-qd.test.ts×1 visibility-oracle-store.states.ts×4 visibility-oracle.states.ts×5 visibility-oracle.test.ts×1 | [ruled 2026-07-13] Question-scoped pending: pending iff a value change is in flight or an `affects()` mark is live — (**ruled 2026-07-13** — supersedes A20/A21; the converged model from the #2844/#272… | +| A25 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:311` | projection.ts×2 store.ts×1 | derived-presence-async-3726.test.ts×2 seedless-derived-store.test.ts×1 uninitialized-visibility.test.ts×3 visibility-oracle-store.states.ts×7 visibility-oracle-store.test.ts×1 | [ruled 2026-07-16] A derived store's seed is a draft, never an observable value — (**ruled 2026-07-16**, #2897) **A derived store's seed is a draft, never an observable value.** The seed exists for th… | +| A26 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:111` | — | action-await-contract.test.ts×2 fuzz-findings-l2.test.ts×1 posture-store-parity.test.ts×2 visibility-oracle-store.states.ts×2 visibility-oracle.states.ts×1 visibility-oracle.test.ts×1 | [ruled 2026-07-17] An ambient transaction window is one flush; parking is flush-driven — (**ruled 2026-07-17**, #2913; **enforcement hardened 2026-08-31**, #3141 — parking is flush-driven, and a trans… | +| A27 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:303` | — | loading-value.test.ts×2 visibility-oracle.states.ts×18 visibility-oracle.test.ts×1 | [ruled 2026-08-10] The commit-#0 loading window is loading-class and verdict-quiet — (**ruled 2026-08-10**) **The commit-#0 loading window is loading-class and verdict-quiet.** A node born committed v… | +| A28 | ruled, mechanism landed | `docs/SPEC-ASYNC-SEMANTICS.md:95` | constants.ts×1 core.ts×12 lanes.ts×2 scheduler.ts×3 seat.ts×3 types.ts×1 verdict.ts×2 store.ts×5 | continuation-sibling-reader-4146.test.ts×1 createOptimistic.test.ts×5 fuzz-findings-l2.test.ts×6 held-derivation-not-a-proposal-3612.test.ts×1 issue-3800-repro.test.ts×1 latest-held-till-flush.test.ts×1 latest-memo-inline-parity.test.ts×1 posture-store-parity.test.ts×5 question-scoped-pending.test.ts×3 snapshot-derived-store-rows.test.ts×1 createOptimisticStore.test.ts×10 optimistic-draft-visibility-3665.test.ts×5 optimistic-list-mutation-matrix.harness.ts×1 optimistic-list-mutation-matrix.test.ts×2 shallow.test.ts×1 woken-transaction-adopts-staged-bump.test.ts×1 treeshake.test.ts×2 verdict-contract.test.ts×1 visibility-oracle-store.states.ts×8 visibility-oracle.states.ts×8 | [ruled, mechanism landed 2026-09-15] A write becomes visible at flush — to every channel — (**ruled 2026-09-08**; supersedes the #2922 mid-tick pull) \*\*A write becomes visible at flush — to every chan… | +| A29 | amended | `docs/SPEC-ASYNC-SEMANTICS.md:119` | boundaries.ts×1 action.ts×1 constants.ts×1 core.ts×14 effect.ts×1 lanes.ts×1 scheduler.ts×4 signals.ts×1 store.ts×1 | adoption-unchanged-key-read-3706.test.ts×9 body-end-supersession-visibility.test.ts×1 born-held.test.ts×3 boundary-not-born-held-3540.test.ts×4 createProjection.draft-lifetime-3585.test.ts×1 direct-commit-readers-posture.test.ts×1 fallback-born-creator-4428.test.ts×1 fuzz-findings-l2.test.ts×6 held-branch-verdict-reader-4010.test.ts×1 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 held-frame-verdict-parity.test.ts×1 held-truth-lane-only.test.ts×3 l2-contract.test.ts×1 latest-held-till-flush.test.ts×2 loading-fallback-in-flush-3540.test.ts×6 loading-on-frame-following-3540.test.ts×1 mount-over-foreign-hold-3761.test.ts×1 optimistic-mount-nested-memo-3835.test.ts×1 optimistic-read-lane-not-transaction-3698.test.ts×3 posture-born-held-and-observation.test.ts×1 posture-store-parity.test.ts×6 derived-presence-async-3726.test.ts×3 optimistic-untracked-reads-f3-f5.test.ts×1 store-unchanged-read-independent-write-3688.test.ts×1 tick-scoped-pass-transaction.test.ts×2 treeshake.test.ts×3 verdict-born-into-other-flush-4426.test.ts×1 verdict-mount-first-pass-3851.test.ts×2 verdict-mount-loading-flush-loop.test.ts×2 visibility-oracle-store.states.ts×5 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×9 visibility-oracle.test.ts×1 write-proposals-3494.test.ts×1 | [ruled, amended in place 2026-09-13 (#3408)] A tracked read served a live transaction's staged value enters that transaction — A tracked computation served a node's staged `_pendingValue` — a value a … | +| A30 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:259` | async.ts×3 attribution.ts×1 constants.ts×1 core.ts×1 effect.ts×1 lanes.ts×1 scheduler.ts×5 projection.ts×1 | async-landing-deps-3461.test.ts×3 fuzz-findings-l2.test.ts×3 held-conditional-effect.test.ts×1 held-conditional-memo.test.ts×1 held-frame-dependencies.test.ts×2 ispending-in-boundary-on-3528.test.ts×1 lane-frame-deferred-run-3662.test.ts×1 lane-frame-held-lane-3662.test.ts×1 posture-born-held-and-observation.test.ts×1 projection-peek-loop-4342.test.ts×1 treeshake.test.ts×2 write-proposals-3494.test.ts×2 zombie-rerun-after-commit-3546.test.ts×2 | [ruled 2026-09-13 (#3410)] A memo's dependencies are the committed frame's until the frame is replaced — A pass that _staged_ its value has not replaced the committed frame, so the committed value sti… | +| A31 | live | `docs/SPEC-ASYNC-SEMANTICS.md:133` | boundaries.ts×1 core.ts×1 lanes.ts×1 verdict.ts×2 | fuzz-findings-l2.test.ts×6 ispending-combined-atomic-3442.test.ts×1 latest-pulled-pass-ispending.test.ts×1 probe-pulled-pass-4429.test.ts×1 unowned-memo-ispending-reader-4366.test.ts×1 verdict-memo-held-branch-4544.test.ts×1 | [live 2026-09-14 (#3442)] A memo computes under its own lane posture, never its puller's — A memo's value is one shared slot every reader sees, so its pass runs under the lane posture the memo itself … | +| A32 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:141` | core.ts×4 lanes.ts×1 seat.ts×1 store.ts×1 | visibility-oracle-store.states.ts×8 visibility-oracle-store.test.ts×1 visibility-oracle.states.ts×9 visibility-oracle.test.ts×1 | [ruled 2026-09-14] Children-forbidden readers see the frame, not the graph — `createTrackedEffect` and `onSettled` callbacks are effect-phase code that runs after the frame is decided. They read the f… | +| A33 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:275` | boundaries.ts×1 scheduler.ts×2 | async-chain-supersession.test.ts×2 boundary-not-born-held-3540.test.ts×2 create-deferred-3710.test.ts×1 fuzz-findings-l2.test.ts×4 ispending-in-boundary-on-3528.test.ts×2 loading-reset-collects-forwarded-3459.test.ts×3 | [ruled 2026-09-12 (#3375)] A fallback-caught flight holds no transaction; a Loading reset moves the hold onto the boundary — A `` boundary showing its fallback is the display of everything un… | +| A34 | ruled | `docs/SPEC-ASYNC-SEMANTICS.md:285` | constants.ts×1 core.ts×6 lanes.ts×2 scheduler.ts×2 store.ts×7 | a34-writes-then-derivations.test.ts×2 continuation-sibling-reader-4146.test.ts×1 createMemo.test.ts×1 derived-write-then-derivation-3733.test.ts×1 finalize-reentry.test.ts×2 fuzz-findings-l2.test.ts×5 held-derivation-not-a-proposal-3612.test.ts×6 optimistic-guess-over-held-slot.test.ts×2 optimistic-list-mutation-matrix.test.ts×1 unchanged-presence-no-hold-3743.test.ts×3 woken-transaction-adopts-staged-bump.test.ts×1 transition-corpse-revival.test.ts×1 treeshake.test.ts×2 visibility-oracle.states.ts×2 write-proposals-3494.test.ts×5 | [ruled 2026-09-16 (#3494)] A write is a proposal: one on a held node entangles its tick; one that nets to the committed value is none — A write proposes a value for a node. \*\*Held, both are suggestion… | + ## V — fixed violations -| id | status | defined | cited in src | cited in tests | statement (at definition) | -|---|---|---|---|---|---| -| V1 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:480` | seat.ts×5 | spec-async-semantics.test.ts×7 | - **V1 (violated A13) — FIXED.** A _resting_ optimistic node reported | -| V2 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:490` | seat.ts×2 | 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:496` | seat.ts×4 | spec-async-semantics.test.ts×2 | - **V3 (violated A19) — FIXED.** After a reporter-less transition completed, | -| V4 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:503` | seat.ts×3 | 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:515` | scheduler.ts×1 | spec-async-semantics.test.ts×3 | - **V5 (A17 corollary — found and fixed with the revert-target elimination, | +| id | status | defined | cited in src | cited in tests | statement (at definition) | +| --- | ------ | ---------------------------------- | -------------- | ------------------------------ | ------------------------------------------------------------------------------ | +| V1 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:480` | seat.ts×8 | spec-async-semantics.test.ts×7 | - **V1 (violated A13) — FIXED.** A _resting_ optimistic node reported | +| V2 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:490` | seat.ts×2 | 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:496` | seat.ts×4 | spec-async-semantics.test.ts×2 | - **V3 (violated A19) — FIXED.** After a reporter-less transition completed, | +| V4 | fixed | `docs/SPEC-ASYNC-SEMANTICS.md:503` | seat.ts×3 | 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:515` | scheduler.ts×1 | spec-async-semantics.test.ts×3 | - \*\*V5 (A17 corollary — found and fixed with the revert-target elimination, | + ## B — tier B -| id | status | defined | cited in src | cited in tests | statement (at definition) | -|---|---|---|---|---|---| -| B1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:207` | seat.ts×7 | createRevealOrder.test.ts×16 derived-write-then-derivation-3733.test.ts×1 onCleanup.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A13 (A13's section carries the ruling). | -| B2 | live | `docs/SPEC-ASYNC-SEMANTICS.md:215` | seat.ts×14 | createRevealOrder.test.ts×16 derived-write-then-derivation-3733.test.ts×1 onCleanup.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A14 (A14's section carries the ruling). | -| B3 | live | `docs/SPEC-ASYNC-SEMANTICS.md:251` | — | spec-async-semantics.test.ts×2 | PROMOTED → A15 (A15's section carries the ruling). | -| B4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:81` | — | derived-write-then-derivation-3733.test.ts×1 spec-async-semantics.test.ts×2 | PROMOTED → A18 (A18's section carries the ruling). | -| B5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:223` | boundaries.ts×2 | derived-write-then-derivation-3733.test.ts×1 errored-over-rearmed-loading-4384.test.ts×1 fuzz-findings-l2.test.ts×3 spec-async-semantics.test.ts×2 | PROMOTED → A16 (A16's section carries the ruling). | +| id | status | defined | cited in src | cited in tests | statement (at definition) | +| --- | ------ | ---------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- | +| B1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:207` | seat.ts×6 | createRevealOrder.test.ts×16 derived-write-then-derivation-3733.test.ts×1 onCleanup.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A13 (A13's section carries the ruling). | +| B2 | live | `docs/SPEC-ASYNC-SEMANTICS.md:215` | seat.ts×13 | createRevealOrder.test.ts×16 derived-write-then-derivation-3733.test.ts×1 onCleanup.test.ts×2 spec-async-semantics.test.ts×2 | PROMOTED → A14 (A14's section carries the ruling). | +| B3 | live | `docs/SPEC-ASYNC-SEMANTICS.md:251` | — | spec-async-semantics.test.ts×2 | PROMOTED → A15 (A15's section carries the ruling). | +| B4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:81` | — | derived-write-then-derivation-3733.test.ts×1 spec-async-semantics.test.ts×2 | PROMOTED → A18 (A18's section carries the ruling). | +| B5 | live | `docs/SPEC-ASYNC-SEMANTICS.md:223` | boundaries.ts×2 | derived-write-then-derivation-3733.test.ts×1 errored-over-rearmed-loading-4384.test.ts×1 fuzz-findings-l2.test.ts×3 spec-async-semantics.test.ts×2 | PROMOTED → A16 (A16's section carries the ruling). | + ## C — tier C -| id | status | defined | cited in src | cited in tests | statement (at definition) | -|---|---|---|---|---|---| -| C1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:151` | — | 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:446` | — | 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:456` | — | — | - [x] **C3 — CLOSED by A19 (2026-07-07): early completion is by design.** | -| C4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:71` | — | latest-memo-inline-parity.test.ts×2 spec-async-semantics.test.ts×1 | PROMOTED → A17 (A17's section carries the ruling). | +| id | status | defined | cited in src | cited in tests | statement (at definition) | +| --- | ------ | ---------------------------------- | ------------ | ------------------------------------------------------------------ | --------------------------------------------------------------------------- | +| C1 | live | `docs/SPEC-ASYNC-SEMANTICS.md:151` | — | 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:446` | — | 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:456` | — | — | - [x] **C3 — CLOSED by A19 (2026-07-07): early completion is by design.** | +| C4 | live | `docs/SPEC-ASYNC-SEMANTICS.md:71` | — | latest-memo-inline-parity.test.ts×2 spec-async-semantics.test.ts×1 | PROMOTED → A17 (A17's section carries the ruling). | + ## INV — invariants -| id | status | defined | cited in src | cited in tests | statement (at definition) | -|---|---|---|---|---|---| -| INV-1 | live | `docs/INTERNALS-ASYNC-STATE.md:166` | — | — | - **INV-1 (high)** `pendingProbe` is non-null only inside an `isPending()` call | -| INV-2 | live | `docs/INTERNALS-ASYNC-STATE.md:168` | — | — | - **INV-2 (high)** A node with an _active_ override (`hasActiveOverride`) is | -| INV-3 | live | `docs/INTERNALS-ASYNC-STATE.md:172` | invariants.ts×2 scheduler.ts×1 | first-observer-stale-reader.test.ts×1 lane-hold-on-observation.test.ts×1 loading-reset-collects-forwarded-3459.test.ts×1 | - **INV-3 (high)** `_asyncReporters` gains entries only inside | -| INV-4 | live | `docs/INTERNALS-ASYNC-STATE.md:179` | — | inv4-projection-dispose-shadow.test.ts×3 posture-store-parity.test.ts×1 | - **INV-4 (medium)** After any of the three write paths completes for node `el` | -| INV-5 | live | `docs/INTERNALS-ASYNC-STATE.md:183` | — | — | - **INV-5 (medium)** A lane in `activeLanes` has `_mergedInto === null` | -| INV-6 | live | `docs/INTERNALS-ASYNC-STATE.md:189` | — | — | - **INV-6 (medium)** At the end of a completing-transition flush: every node in | -| INV-7 | live | `docs/INTERNALS-ASYNC-STATE.md:192` | 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:195` | 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:206` | — | inv4-projection-dispose-shadow.test.ts×1 | - **INV-9 (high)** An `isPending` companion of a DISPOSED owner reads `false` | -| INV-10 | live | `docs/INTERNALS-ASYNC-STATE.md:211` | — | 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:216` | core.ts×1 | create-deferred-3710.test.ts×1 spec-async-semantics.test.ts×1 treeshake.test.ts×1 | - **INV-11 (high, structural — pinned, not asserted)** A recompute's equality | +| id | status | defined | cited in src | cited in tests | statement (at definition) | +| ------ | ------- | ----------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | +| INV-1 | live | `docs/INTERNALS-ASYNC-STATE.md:166` | — | — | - **INV-1 (high)** `pendingProbe` is non-null only inside an `isPending()` call | +| INV-2 | live | `docs/INTERNALS-ASYNC-STATE.md:168` | — | — | - **INV-2 (high)** A node with an _active_ override (`hasActiveOverride`) is | +| INV-3 | live | `docs/INTERNALS-ASYNC-STATE.md:172` | invariants.ts×2 scheduler.ts×1 | first-observer-stale-reader.test.ts×1 lane-hold-on-observation.test.ts×1 loading-reset-collects-forwarded-3459.test.ts×1 | - **INV-3 (high)** `_asyncReporters` gains entries only inside | +| INV-4 | live | `docs/INTERNALS-ASYNC-STATE.md:179` | — | inv4-projection-dispose-shadow.test.ts×3 posture-store-parity.test.ts×1 | - **INV-4 (medium)** After any of the three write paths completes for node `el` | +| INV-5 | live | `docs/INTERNALS-ASYNC-STATE.md:183` | — | — | - **INV-5 (medium)** A lane in `activeLanes` has `_mergedInto === null` | +| INV-6 | live | `docs/INTERNALS-ASYNC-STATE.md:189` | — | — | - **INV-6 (medium)** At the end of a completing-transition flush: every node in | +| INV-7 | live | `docs/INTERNALS-ASYNC-STATE.md:192` | 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:195` | 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:206` | — | inv4-projection-dispose-shadow.test.ts×1 | - **INV-9 (high)** An `isPending` companion of a DISPOSED owner reads `false` | +| INV-10 | live | `docs/INTERNALS-ASYNC-STATE.md:211` | — | 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:216` | core.ts×1 | create-deferred-3710.test.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:547` | store.ts×2 | 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:556` | — | createOptimisticStore.test.ts×1 | - **RUL-2 — RULED (Ryan, 2026-08-17): no landing matrix. Two orthogonal | -| RUL-3 | resolved | `docs/INTERNALS-STORE-STATE.md:655` | optimistic.ts×1 | — | - **RUL-3 — RESOLVED (verified 2026-08-16).** Ownership already lives | -| RUL-4 | resolved | `docs/INTERNALS-STORE-STATE.md:662` | — | 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:675` | — | 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:679` | — | — | - **RUL-6 — reclassified: SPEC WORK, not a ruling.** Live chaining (#2941 — | -| RUL-7 | live | `docs/INTERNALS-STORE-STATE.md:688` | — | — | - **RUL-7 — SPEC'D (2026-08-17): §6c.** One status field on the root target, | -| RUL-8 | live | `docs/INTERNALS-STORE-STATE.md:691` | — | 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:694` | — | — | - **RUL-9 — RESOLVED (2026-08-17, parity by construction).** Every piece of | -| RUL-10 | live | `docs/INTERNALS-STORE-STATE.md:699` | — | — | - **RUL-10 — The equality trio.** One precise rule needed spanning: no-op | -| RUL-11 | live | `docs/INTERNALS-STORE-STATE.md:705` | — | — | - **RUL-11 — SPEC'D (2026-08-17): §6d.** Sticky descendants flag ported from | -| RUL-12 | live | `docs/INTERNALS-STORE-STATE.md:707` | store.ts×1 types.ts×1 | 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:753` | — | — | - **RUL-13 — RESOLVED (verified 2026-08-16)**: `optimistic-lane-transaction- | +| id | status | defined | cited in src | cited in tests | statement (at definition) | +| ------ | -------- | ----------------------------------- | --------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| RUL-1 | resolved | `docs/INTERNALS-STORE-STATE.md:547` | store.ts×2 | 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:556` | — | createOptimisticStore.test.ts×1 | - \*\*RUL-2 — RULED (Ryan, 2026-08-17): no landing matrix. Two orthogonal | +| RUL-3 | resolved | `docs/INTERNALS-STORE-STATE.md:655` | optimistic.ts×1 | — | - **RUL-3 — RESOLVED (verified 2026-08-16).** Ownership already lives | +| RUL-4 | resolved | `docs/INTERNALS-STORE-STATE.md:662` | — | 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:675` | — | 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:679` | — | — | - **RUL-6 — reclassified: SPEC WORK, not a ruling.** Live chaining (#2941 — | +| RUL-7 | live | `docs/INTERNALS-STORE-STATE.md:688` | — | — | - **RUL-7 — SPEC'D (2026-08-17): §6c.** One status field on the root target, | +| RUL-8 | live | `docs/INTERNALS-STORE-STATE.md:691` | — | 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:694` | — | — | - **RUL-9 — RESOLVED (2026-08-17, parity by construction).** Every piece of | +| RUL-10 | live | `docs/INTERNALS-STORE-STATE.md:699` | — | — | - **RUL-10 — The equality trio.** One precise rule needed spanning: no-op | +| RUL-11 | live | `docs/INTERNALS-STORE-STATE.md:705` | — | — | - **RUL-11 — SPEC'D (2026-08-17): §6d.** Sticky descendants flag ported from | +| RUL-12 | live | `docs/INTERNALS-STORE-STATE.md:707` | store.ts×1 types.ts×1 | 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:753` | — | — | - **RUL-13 — RESOLVED (verified 2026-08-16)**: `optimistic-lane-transaction- | + ## R — core-store (`CS-R`) -| id | status | defined | cited in src | cited in tests | statement (at definition) | -|---|---|---|---|---|---| -| CS-R1 | live | `docs/rules-mining/core-store.md:9` | store.ts×2 | continuation-sibling-reader-4146.test.ts×2 flatten-async-iterable.test.ts×5 syncThenable.test.ts×14 visibility-oracle-store.states.ts×1 | Wrappable values are wrapped: reading a plain object/array child never returns the raw source (`state.data !== data`).** | -| CS-R2 | live | `docs/rules-mining/core-store.md:13` | — | posture-store-parity.test.ts×1 syncThenable.test.ts×12 visibility-oracle.states.ts×1 | Raw→proxy resolution is global and deduplicating: wrapping the same raw through two different stores yields the same proxy (`outer.list === inner`).** | -| CS-R2a | live | `docs/INTERNALS-STORE-STATE.md:107` | — | — | Corollary R2a (Ryan, 2026-08-17): **take no responsibility for mutation | -| CS-R3 | live | `docs/rules-mining/core-store.md:18` | — | — | A store proxy ingested into another store (deep or shallow) is re-wrapped in the ingesting store's own proxy family — never identity-passed, never raw-marked.** | -| CS-R4 | live | `docs/rules-mining/core-store.md:22` | — | — | Write isolation across a store chain: writing through the last store in a derived chain is visible only there; upstream stores and base objects untouched (shallow or deep middle).** | -| CS-R5 | live | `docs/rules-mining/core-store.md:26` | — | latest-first-one-pass-4402.test.ts×1 latest-two-readers-one-ask-4393.test.ts×2 verdict-memo-flight-no-reask.test.ts×9 visibility-oracle-store.states.ts×4 visibility-oracle.states.ts×4 | Upstream writes propagate downstream through the chain without re-running structural machinery.** | -| CS-R6 | live | `docs/rules-mining/core-store.md:30` | — | — | No store write path ever mutates a user-provided source object.** Aligned with 2026-08-16b. | -| CS-R7 | live | `docs/rules-mining/core-store.md:34` | — | — | Circular references wrap without infinite recursion; cycle consistent through proxy (`state.b.a === state.a`).** | -| CS-R8 | live | `docs/rules-mining/core-store.md:38` | — | — | `snapshot` returns fully unwrapped values (no proxy anywhere, `$TARGET` undefined), incl. frozen objects/arrays; reflects committed written values incl. writes over inherited prototype props.** | -| CS-R9 | live | `docs/rules-mining/core-store.md:42` | store.ts×2 target.ts×1 | — | Proxy identity per logical slot is stable across writes and reconciles** (mapArray keyed flows reuse rows across refetch/reconcile). | -| CS-R10 | live | `docs/rules-mining/core-store.md:48` | — | next-smoke.test.ts×1 | Per-property tracking; same-value writes (direct or functional path setter returning prev) do not re-trigger.** | -| CS-R11 | live | `docs/rules-mining/core-store.md:52` | — | visibility-oracle-store.states.ts×3 visibility-oracle.states.ts×2 | Per-path tracking: reading `state.user.firstName` subscribes to that leaf; reading the reference `store[0]` does not subscribe to `store[0].i`.** | -| CS-R12 | live | `docs/rules-mining/core-store.md:56` | store.ts×1 | proto-key-removal-4672.test.ts×1 | Reading an absent key subscribes to that key: other-key changes don't trigger; defining it later (assignment or defineProperty) does.** | -| CS-R13 | live | `docs/rules-mining/core-store.md:60` | store.ts×2 target.ts×1 | — | `in` tracks presence, not value: undefined-write doesn't retrigger; delete does; adding absent key does. `in`/`has` never invokes source getters.** | -| CS-R14 | live | `docs/rules-mining/core-store.md:64` | — | — | `Object.keys` / `for…in` subscribe to key-set membership (root and nested) — distinct from property nodes.** Aligned: key-set node. | -| CS-R15 | live | `docs/rules-mining/core-store.md:68` | store.ts×1 | — | Array structural tracking is uniform across idioms: indexed length loop, `for…of`, mapArray ($TRACK) all re-run exactly once per flush on add/update/removal.** | -| CS-R16 | live | `docs/rules-mining/core-store.md:72` | — | — | `length` independently trackable; index write extending the array notifies length subscribers.** | -| CS-R17 | live | `docs/rules-mining/core-store.md:76` | — | — | Truncating via `length = N` notifies tracked index reads of removed slots (re-run, observe undefined) and clears has/index/keys for removed indices.** | -| CS-R18 | live | `docs/rules-mining/core-store.md:80` | — | — | `snapshot` is non-tracking.** Aligned with read table. | -| CS-R19 | live | `docs/rules-mining/core-store.md:84` | — | — | `untrack` scopes only the wrapped read; property access on the escaped value afterwards tracks normally.** | -| CS-R20 | live | `docs/rules-mining/core-store.md:88` | store.ts×1 | — | Source getters (own, prototype, merge-installed) execute with the proxy as receiver, so their internal reads track — incl. through projections.** | -| CS-R21 | live | `docs/rules-mining/core-store.md:92` | — | deep-chained-view.test.ts×1 | Structural subscriptions through a wrapper view (store-in-store) chain to the wrapped source: $TRACK/mapArray, ownKeys, snapshot/trackSelf through an outer derived store re-run when the inner store re… | -| CS-R22 | live | `docs/rules-mining/core-store.md:97` | — | — | Slots holding non-wrappable values (markRaw, Map/Date, function) track by reference: reassignment notifies; internal mutation doesn't.** | -| CS-R23 | live | `docs/rules-mining/core-store.md:103` | — | — | The proxy is immutable from outside the setter: direct assignment and delete are silently ignored (no change, no notify, no TypeError — traps report success while discarding).** | -| CS-R24 | live | `docs/rules-mining/core-store.md:107` | — | next-smoke.test.ts×1 | Writes batch like signals: inside the setter draft, reads are read-your-writes (values, length, `in` sync); outside the setter, ALL reads — value, `in`, length — return pre-write state until flush(). … | -| CS-R25 | live | `docs/rules-mining/core-store.md:112` | — | next-smoke.test.ts×1 | Writes to properties with ZERO observers still batch (no effects anywhere; pre-write value visible between setState and flush).** | -| CS-R26 | live | `docs/rules-mining/core-store.md:117` | — | — | Setting a key to undefined is not deletion: key stays present (`in` true, no key-set notify); only delete / storePath.DELETE removes.** | -| CS-R27 | live | `docs/rules-mining/core-store.md:121` | store.ts×2 | — | The setter may return a replacement value that swaps the root wholesale; symbol keys on the replacement preserved.** | -| CS-R28 | live | `docs/rules-mining/core-store.md:125` | reconcile.ts×1 | — | `storePath` addressing: string keys, numeric indices, index arrays, predicate filters ((value, index)), ranges, trailing functional setters address and update intended paths + trigger per-path subscri… | -| CS-R29 | live | `docs/rules-mining/core-store.md:129` | store.ts×1 | overlay.test.ts×1 | Merge/replacement preserve accessor descriptors and keep getters LIVE (re-evaluated per read, reactive reads track), for pre-existing and new keys.** | -| CS-R30 | live | `docs/rules-mining/core-store.md:134` | reconcile.ts×1 store.ts×4 | proto-key-removal-4672.test.ts×3 store-prototype-4670.test.ts×2 | Prototype pollution fully guarded: `__proto__` assignment inert; reading `constructor` on the draft returns undefined; storePath refuses `__proto__`/`constructor`/`prototype` segments; skips unsafe ow… | -| CS-R31 | live | `docs/rules-mining/core-store.md:138` | store.ts×1 | a34-writes-then-derivations.test.ts×1 createMemo.test.ts×6 derived-write-then-derivation-3733.test.ts×1 held-derivation-not-a-proposal-3612.test.ts×3 createStore.test.ts×3 reconcile-resend-identity.test.ts×1 | Derived-store manual writes apply before the derivation.** A manual `setStore` lands at once, staged with any live transaction. When a source of the fold changes, in the same update or later, the fold… | -| CS-R32 | live | `docs/rules-mining/core-store.md:147` | — | visibility-oracle-store.states.ts×1 | A setter-staged replacement followed by reconcile lands the reconciled value — staged writes fold into the diff.** Aligned: O7's resolution (a test already exists). | -| CS-R33 | live | `docs/rules-mining/core-store.md:151` | — | visibility-oracle-store.states.ts×4 | Action/async lane semantics on store properties: a write held by an action makes isPending true for that property (per-property, not whole-store) while showing the committed value; applies on settle.*… | -| CS-R34 | live | `docs/rules-mining/core-store.md:155` | — | visibility-oracle-store.states.ts×1 | ~~Optimistic writes visible immediately at write time (before flush)~~, never touch base raw; ambient (non-action) optimistic writes auto-revert at flush end.** _Visibility superseded 2026-09-10 by A2… | -| CS-R35 | live | `docs/rules-mining/core-store.md:160` | — | — | 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:164` | — | — | 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:169` | projection.ts×1 | createProjection.draft-lifetime-3585.test.ts×1 visibility-oracle-store.states.ts×3 | Setting store state from effect callbacks and promise resolutions works, applying next flush.** | -| CS-R38 | live | `docs/rules-mining/core-store.md:175` | — | — | 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:179` | — | visibility-oracle-store.states.ts×1 | Shallow setter-scope reads serve raws; in-place mutation of a served raw is reactively inert — records replaced, never edited.** | -| CS-R40 | live | `docs/rules-mining/core-store.md:183` | — | — | 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:187` | store.ts×1 | — | 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:192` | store.ts×1 types.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:196` | — | — | 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:200` | 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:205` | — | — | 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:209` | — | — | 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:215` | — | — | 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:220` | — | store-prototype-4670.test.ts×1 | 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:224` | — | — | Null-prototype objects wrap and track; function-valued props callable through the proxy.** | -| CS-R50 | live | `docs/rules-mining/core-store.md:228` | — | store-prototype-4670.test.ts×1 | Frozen sources fully supported (read/snapshot; getters returning frozen don't throw).** | -| CS-R51 | live | `docs/rules-mining/core-store.md:233` | — | overlay.test.ts×1 store-prototype-4670.test.ts×2 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:237` | — | — | 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:241` | — | — | 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:245` | — | — | 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:247` | — | — | Functions stored as values served raw, replaceable, slot-tracked.** | -| CS-R56 | live | `docs/rules-mining/core-store.md:251` | — | — | 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:255` | — | — | Effect ordering: parent effects before child effects created inside them, incl. shared deps through memos.** | -| CS-R58 | live | `docs/rules-mining/core-store.md:259` | — | — | Mid-flush read coherence: untracked store reads inside internal machinery running WITHIN a flush (mapArray keyed:false under a Root owner) must observe the value being written in that flush, not stale… | +| id | status | defined | cited in src | cited in tests | statement (at definition) | +| ------ | ------ | ------------------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| CS-R1 | live | `docs/rules-mining/core-store.md:9` | store.ts×2 | continuation-sibling-reader-4146.test.ts×2 flatten-async-iterable.test.ts×5 syncThenable.test.ts×14 visibility-oracle-store.states.ts×1 | Wrappable values are wrapped: reading a plain object/array child never returns the raw source (`state.data !== data`).\*\* | +| CS-R2 | live | `docs/rules-mining/core-store.md:13` | — | posture-store-parity.test.ts×1 syncThenable.test.ts×12 visibility-oracle.states.ts×1 | Raw→proxy resolution is global and deduplicating: wrapping the same raw through two different stores yields the same proxy (`outer.list === inner`).\*\* | +| CS-R2a | live | `docs/INTERNALS-STORE-STATE.md:107` | — | — | Corollary R2a (Ryan, 2026-08-17): \*\*take no responsibility for mutation | +| CS-R3 | live | `docs/rules-mining/core-store.md:18` | — | — | A store proxy ingested into another store (deep or shallow) is re-wrapped in the ingesting store's own proxy family — never identity-passed, never raw-marked.\*\* | +| CS-R4 | live | `docs/rules-mining/core-store.md:22` | — | — | Write isolation across a store chain: writing through the last store in a derived chain is visible only there; upstream stores and base objects untouched (shallow or deep middle).\*\* | +| CS-R5 | live | `docs/rules-mining/core-store.md:26` | — | latest-first-one-pass-4402.test.ts×1 latest-two-readers-one-ask-4393.test.ts×2 verdict-memo-flight-no-reask.test.ts×9 visibility-oracle-store.states.ts×4 visibility-oracle.states.ts×4 | Upstream writes propagate downstream through the chain without re-running structural machinery.\*\* | +| CS-R6 | live | `docs/rules-mining/core-store.md:30` | — | — | No store write path ever mutates a user-provided source object.\*\* Aligned with 2026-08-16b. | +| CS-R7 | live | `docs/rules-mining/core-store.md:34` | — | — | Circular references wrap without infinite recursion; cycle consistent through proxy (`state.b.a === state.a`).\*\* | +| CS-R8 | live | `docs/rules-mining/core-store.md:38` | — | — | `snapshot` returns fully unwrapped values (no proxy anywhere, `$TARGET` undefined), incl. frozen objects/arrays; reflects committed written values incl. writes over inherited prototype props.\*\* | +| CS-R9 | live | `docs/rules-mining/core-store.md:42` | store.ts×2 target.ts×1 | — | Proxy identity per logical slot is stable across writes and reconciles\*\* (mapArray keyed flows reuse rows across refetch/reconcile). | +| CS-R10 | live | `docs/rules-mining/core-store.md:48` | — | next-smoke.test.ts×1 | Per-property tracking; same-value writes (direct or functional path setter returning prev) do not re-trigger.\*\* | +| CS-R11 | live | `docs/rules-mining/core-store.md:52` | — | visibility-oracle-store.states.ts×3 visibility-oracle.states.ts×2 | Per-path tracking: reading `state.user.firstName` subscribes to that leaf; reading the reference `store[0]` does not subscribe to `store[0].i`.\*\* | +| CS-R12 | live | `docs/rules-mining/core-store.md:56` | store.ts×1 | proto-key-removal-4672.test.ts×1 | Reading an absent key subscribes to that key: other-key changes don't trigger; defining it later (assignment or defineProperty) does.\*\* | +| CS-R13 | live | `docs/rules-mining/core-store.md:60` | store.ts×2 target.ts×1 | — | `in` tracks presence, not value: undefined-write doesn't retrigger; delete does; adding absent key does. `in`/`has` never invokes source getters.\*\* | +| CS-R14 | live | `docs/rules-mining/core-store.md:64` | — | — | `Object.keys` / `for…in` subscribe to key-set membership (root and nested) — distinct from property nodes.\*\* Aligned: key-set node. | +| CS-R15 | live | `docs/rules-mining/core-store.md:68` | store.ts×1 | — | Array structural tracking is uniform across idioms: indexed length loop, `for…of`, mapArray ($TRACK) all re-run exactly once per flush on add/update/removal.\*\* | +| CS-R16 | live | `docs/rules-mining/core-store.md:72` | — | — | `length` independently trackable; index write extending the array notifies length subscribers.\*\* | +| CS-R17 | live | `docs/rules-mining/core-store.md:76` | — | — | Truncating via `length = N` notifies tracked index reads of removed slots (re-run, observe undefined) and clears has/index/keys for removed indices.\*\* | +| CS-R18 | live | `docs/rules-mining/core-store.md:80` | — | — | `snapshot` is non-tracking.\*\* Aligned with read table. | +| CS-R19 | live | `docs/rules-mining/core-store.md:84` | — | — | `untrack` scopes only the wrapped read; property access on the escaped value afterwards tracks normally.\*\* | +| CS-R20 | live | `docs/rules-mining/core-store.md:88` | store.ts×1 | — | Source getters (own, prototype, merge-installed) execute with the proxy as receiver, so their internal reads track — incl. through projections.\*\* | +| CS-R21 | live | `docs/rules-mining/core-store.md:92` | — | deep-chained-view.test.ts×1 | Structural subscriptions through a wrapper view (store-in-store) chain to the wrapped source: $TRACK/mapArray, ownKeys, snapshot/trackSelf through an outer derived store re-run when the inner store re… | +| CS-R22 | live | `docs/rules-mining/core-store.md:97` | — | — | Slots holding non-wrappable values (markRaw, Map/Date, function) track by reference: reassignment notifies; internal mutation doesn't.\*\* | +| CS-R23 | live | `docs/rules-mining/core-store.md:103` | — | — | The proxy is immutable from outside the setter: direct assignment and delete are silently ignored (no change, no notify, no TypeError — traps report success while discarding).\*\* | +| CS-R24 | live | `docs/rules-mining/core-store.md:107` | — | next-smoke.test.ts×1 | Writes batch like signals: inside the setter draft, reads are read-your-writes (values, length, `in` sync); outside the setter, ALL reads — value, `in`, length — return pre-write state until flush(). … | +| CS-R25 | live | `docs/rules-mining/core-store.md:112` | — | next-smoke.test.ts×1 | Writes to properties with ZERO observers still batch (no effects anywhere; pre-write value visible between setState and flush).\*\* | +| CS-R26 | live | `docs/rules-mining/core-store.md:117` | — | — | Setting a key to undefined is not deletion: key stays present (`in` true, no key-set notify); only delete / storePath.DELETE removes.\*\* | +| CS-R27 | live | `docs/rules-mining/core-store.md:121` | store.ts×2 | — | The setter may return a replacement value that swaps the root wholesale; symbol keys on the replacement preserved.\*\* | +| CS-R28 | live | `docs/rules-mining/core-store.md:125` | reconcile.ts×1 | — | `storePath` addressing: string keys, numeric indices, index arrays, predicate filters ((value, index)), ranges, trailing functional setters address and update intended paths + trigger per-path subscri… | +| CS-R29 | live | `docs/rules-mining/core-store.md:129` | store.ts×1 | overlay.test.ts×1 | Merge/replacement preserve accessor descriptors and keep getters LIVE (re-evaluated per read, reactive reads track), for pre-existing and new keys.\*\* | +| CS-R30 | live | `docs/rules-mining/core-store.md:134` | reconcile.ts×1 store.ts×4 | proto-key-removal-4672.test.ts×3 store-prototype-4670.test.ts×2 | Prototype pollution fully guarded: `__proto__` assignment inert; reading `constructor` on the draft returns undefined; storePath refuses `__proto__`/`constructor`/`prototype` segments; skips unsafe ow… | +| CS-R31 | live | `docs/rules-mining/core-store.md:138` | store.ts×1 | a34-writes-then-derivations.test.ts×1 createMemo.test.ts×6 derived-write-then-derivation-3733.test.ts×1 held-derivation-not-a-proposal-3612.test.ts×3 createStore.test.ts×3 reconcile-resend-identity.test.ts×1 | Derived-store manual writes apply before the derivation.\*\* A manual `setStore` lands at once, staged with any live transaction. When a source of the fold changes, in the same update or later, the fold… | +| CS-R32 | live | `docs/rules-mining/core-store.md:147` | — | visibility-oracle-store.states.ts×1 | A setter-staged replacement followed by reconcile lands the reconciled value — staged writes fold into the diff.\*\* Aligned: O7's resolution (a test already exists). | +| CS-R33 | live | `docs/rules-mining/core-store.md:151` | — | visibility-oracle-store.states.ts×4 | Action/async lane semantics on store properties: a write held by an action makes isPending true for that property (per-property, not whole-store) while showing the committed value; applies on settle.\*… | +| CS-R34 | live | `docs/rules-mining/core-store.md:155` | — | visibility-oracle-store.states.ts×1 | ~~Optimistic writes visible immediately at write time (before flush)~~, never touch base raw; ambient (non-action) optimistic writes auto-revert at flush end.\*\* \_Visibility superseded 2026-09-10 by A2… | +| CS-R35 | live | `docs/rules-mining/core-store.md:160` | — | — | 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:164` | — | — | 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:169` | projection.ts×1 | createProjection.draft-lifetime-3585.test.ts×1 visibility-oracle-store.states.ts×3 | Setting store state from effect callbacks and promise resolutions works, applying next flush.\*\* | +| CS-R38 | live | `docs/rules-mining/core-store.md:175` | — | — | 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:179` | — | visibility-oracle-store.states.ts×1 | Shallow setter-scope reads serve raws; in-place mutation of a served raw is reactively inert — records replaced, never edited.\*\* | +| CS-R40 | live | `docs/rules-mining/core-store.md:183` | — | — | 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:187` | store.ts×1 | — | 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:192` | store.ts×1 types.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:196` | — | — | 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:200` | 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:205` | — | — | 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:209` | — | — | 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:215` | — | — | 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:220` | — | store-prototype-4670.test.ts×1 | 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:224` | — | — | Null-prototype objects wrap and track; function-valued props callable through the proxy.\*\* | +| CS-R50 | live | `docs/rules-mining/core-store.md:228` | — | store-prototype-4670.test.ts×1 | Frozen sources fully supported (read/snapshot; getters returning frozen don't throw).\*\* | +| CS-R51 | live | `docs/rules-mining/core-store.md:233` | — | overlay.test.ts×1 store-prototype-4670.test.ts×2 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:237` | — | — | 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:241` | — | — | 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:245` | — | — | 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:247` | — | — | Functions stored as values served raw, replaceable, slot-tracked.\*\* | +| CS-R56 | live | `docs/rules-mining/core-store.md:251` | — | — | 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:255` | — | — | Effect ordering: parent effects before child effects created inside them, incl. shared deps through memos.\*\* | +| CS-R58 | live | `docs/rules-mining/core-store.md:259` | — | — | Mid-flush read coherence: untracked store reads inside internal machinery running WITHIN a flush (mapArray keyed:false under a Root owner) must observe the value being written in that flush, not stale… | + ## R — optimistic-lanes (`OL-R`) -| id | status | defined | cited in src | cited in tests | statement (at definition) | -|---|---|---|---|---|---| -| OL-R1 | live | `docs/rules-mining/optimistic-lanes.md:11` | — | — | `createOptimistic(value \| fn)` returns `[accessor, setter]`; the accessor returns the initial or computed value; the setter accepts a value or an updater function. | -| OL-R2 | superseded | `docs/rules-mining/optimistic-lanes.md:15` | verdict.ts×1 | — | ~~An optimistic write is synchronously visible to direct reads before any flush — inside the action body, outside it, and outside any reactive context.~~ **Superseded 2026-09-10 by A28(5)** (SPEC-ASYN… | -| OL-R3 | live | `docs/rules-mining/optimistic-lanes.md:19` | — | — | The setter's updater receives the current _visible_ (optimistic-if-overridden) value, never the committed value; a plain setter on the underlying source during a transition composes on the transition'… | -| OL-R4 | live | `docs/rules-mining/optimistic-lanes.md:23` | seat.ts×3 | — | Multiple optimistic writes before settle compose sequentially (each updater sees the prior override; last write wins). | -| OL-R5 | live | `docs/rules-mining/optimistic-lanes.md:27` | lanes.ts×2 scheduler.ts×2 seat.ts×2 types.ts×1 verdict.ts×1 | — | An optimistic write **outside any action** reverts at the next flush; subscribers observe the optimistic value and then the reverted value within that single flush (effect log `[1, 2, 1]` after one `f… | -| OL-R6 | live | `docs/rules-mining/optimistic-lanes.md:32` | — | — | An optimistic write inside an `action` holds for the entire action window and reverts when the action's transition completes; each intermediate write during a multi-yield action is observable in order… | -| OL-R7 | live | `docs/rules-mining/optimistic-lanes.md:36` | seat.ts×1 | — | Computed-form `createOptimistic(fn)` with no overrides is a transparent passthrough of its (possibly async) source: promise resolutions, re-fired promises, and async-iterable yields all propagate; ove… | -| OL-R8 | live | `docs/rules-mining/optimistic-lanes.md:40` | — | — | Reset-on-settle targets the source's **newly computed value at settle time**, not the pre-write value: a wrong optimistic guess is auto-corrected to the real result; a correct guess settles silently (… | -| OL-R9 | live | `docs/rules-mining/optimistic-lanes.md:44` | — | — | Regular signals written in the same action are held (transition semantics) while optimistic writes display immediately; downstream memos and chained optimistic computeds see optimistic values and reve… | -| OL-R10 | live | `docs/rules-mining/optimistic-lanes.md:48` | — | — | `refresh()` of an optimistic accessor inside an action clears the override when the refetch settles; calling `refresh()` while the upstream source is still pending must not throw. | -| OL-R11 | live | `docs/rules-mining/optimistic-lanes.md:52` | — | — | Verdict channels: an optimistic override **is the value** on every channel — plain read and `latest()` both return it (including literal `undefined`); the override itself is **verdict-inert** — it nev… | -| OL-R12 | live | `docs/rules-mining/optimistic-lanes.md:56` | — | action-refresh-ispending-loop-4349.test.ts×1 | A bare `refresh()` is a quiet re-ask — never pending; a **declared** reload (`affects(x)` + `refresh(x)` inside an action) pends the slot for the whole reload window, even when the sole consumer is a … | -| OL-R13 | live | `docs/rules-mining/optimistic-lanes.md:60` | — | — | During the pending window, a source recompute that reveals a value **different** from the current override corrects the override in place (before the action settles), triggering downstream refetch; a … | -| OL-R14 | live | `docs/rules-mining/optimistic-lanes.md:67` | — | — | Independent optimistic writes to unrelated signals form independent lanes: notifications scoped to each signal's own subscribers; each action's overrides revert when _that_ action settles, regardless … | -| OL-R15 | live | `docs/rules-mining/optimistic-lanes.md:71` | — | — | A shared subscriber reading multiple optimistic sources merges lanes **for scheduling only**; it must not transfer transaction ownership of overrides. Disjoint-key work settles with its owning action … | -| OL-R16 | live | `docs/rules-mining/optimistic-lanes.md:76` | — | — | Same-key writes from multiple actions **entangle** those actions: the override (and transitively every override of the entangled actions) reverts only when the **last** entangled action settles. | -| OL-R17 | live | `docs/rules-mining/optimistic-lanes.md:81` | — | — | An equal-value write still registers ownership and still performs lane bookkeeping: a second action writing the same value keeps the override alive after the first settles; an override write whose val… | -| OL-R18 | live | `docs/rules-mining/optimistic-lanes.md:86` | — | — | All optimistic writes in one action share one transaction and revert together atomically; lanes/transactions clean up fully between cycles — the Nth cycle behaves exactly like the first, including aft… | -| OL-R19 | live | `docs/rules-mining/optimistic-lanes.md:90` | — | — | A shared **upstream** async resolving must not merge distinct downstream optimistic lanes — independent paths keep updating independently; genuine merge happens only at convergence points (a memo read… | -| OL-R20 | live | `docs/rules-mining/optimistic-lanes.md:94` | — | — | A later action's override wins over an earlier action's background settle: when action 1's refresh resolves _under_ action 2's live override, the visible value is unchanged, downstream must not recomp… | -| OL-R21 | live | `docs/rules-mining/optimistic-lanes.md:100` | — | — | An optimistic write of literal `undefined` is a full-fledged override: visible on plain read and `latest()`, verdict-inert on `isPending`, and it reverts at settle exactly like any other value. | -| OL-R22 | live | `docs/rules-mining/optimistic-lanes.md:105` | — | — | A follow-up optimistic write after an `undefined` override still rides the optimistic path and reverts at settle — `undefined` in the slot must never erase the node's optimistic identity or route late… | -| OL-R23 | live | `docs/rules-mining/optimistic-lanes.md:109` | — | — | Store form distinguishes "override to undefined" from "delete": optimistic set-to-undefined reads `undefined` with the key still present; optimistic `delete` reads `undefined` **and** `"key" in store … | -| OL-R24 | live | `docs/rules-mining/optimistic-lanes.md:116` | — | — | A transition completes only when **all** reachable asyncs (upstream source and downstream lane asyncs) resolve; held source values must never leak to subscribers before completion, even when the upstr… | -| OL-R25 | live | `docs/rules-mining/optimistic-lanes.md:120` | — | — | Lane readiness gating: subscribers reached _through a downstream async memo_ fire with optimistic values only once that async resolves; direct reads show the override immediately. The lane may flush *… | -| OL-R26 | live | `docs/rules-mining/optimistic-lanes.md:124` | — | — | At settle, the commit of held transition writes and the revert of optimistic overrides are delivered **atomically**: one subscriber run observing both, never a torn intermediate. | -| OL-R27 | live | `docs/rules-mining/optimistic-lanes.md:128` | — | — | Rapid successive user writes replay correctly: the latest override wins; earlier lane flushes deliver the values current at their readiness time; final settled state reflects the last action's confirm… | -| OL-R28 | live | `docs/rules-mining/optimistic-lanes.md:134` | — | — | No-op settles are silent: if the optimistic write equals the current value, neither the write nor the revert notifies; if the settle-time computed value equals the override, no extra notification fire… | -| OL-R29 | live | `docs/rules-mining/optimistic-lanes.md:139` | — | — | Pre-flush writes coalesce: subscribers see only the latest override per flush (`[0, 2, 0]`, never intermediate `1`). | -| OL-R30 | live | `docs/rules-mining/optimistic-lanes.md:143` | — | — | Render-tier and user-tier effects must observe **identical value sequences** at every flush, including the mid-transition moment where an action finished but async reporters are still in flight. | -| OL-R31 | live | `docs/rules-mining/optimistic-lanes.md:147` | core.ts×1 | — | Optimistic lane notifications run even while an unrelated transition is stashed/pending; pending async in one lane never blocks another lane's write/revert notifications. | -| OL-R32 | live | `docs/rules-mining/optimistic-lanes.md:151` | — | — | `isPending` granularity: each async path's pending slot clears when its **own** async resolves; merged downstream nodes stay pending — emitting **no intermediate half-state values** — until all inputs… | -| OL-R33 | live | `docs/rules-mining/optimistic-lanes.md:155` | — | — | No pending flicker when the visible value is unchanged: background refresh phases with an unchanged visible override must not re-pend downstream; a genuinely new in-flight question must fire `isPendin… | -| OL-R34 | live | `docs/rules-mining/optimistic-lanes.md:159` | — | — | `latest()` readers opt into progressive per-path display while plain readers of merged memos wait for full resolution. | -| OL-R35 | live | `docs/rules-mining/optimistic-lanes.md:165` | — | — | `createOptimisticStore` returns `[proxy, setter]`; draft-style mutations inside an action are optimistic: immediately visible through the proxy, wholly reverted at settle. | -| OL-R36 | live | `docs/rules-mining/optimistic-lanes.md:169` | — | — | Array structural edits (e.g. filter-removal) are visible during the window through `length`, index reads, and iteration, and fully revert at settle. | -| OL-R37 | live | `docs/rules-mining/optimistic-lanes.md:174` | — | — | Store optimism is per-key: different keys written by different actions settle independently — same ownership rules as signals (R15/R16) at store-key granularity. | +| id | status | defined | cited in src | cited in tests | statement (at definition) | +| ------ | ---------- | ------------------------------------------- | ----------------------------------------------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| OL-R1 | live | `docs/rules-mining/optimistic-lanes.md:11` | — | — | `createOptimistic(value \| fn)` returns `[accessor, setter]`; the accessor returns the initial or computed value; the setter accepts a value or an updater function. | +| OL-R2 | superseded | `docs/rules-mining/optimistic-lanes.md:15` | verdict.ts×1 | — | ~~An optimistic write is synchronously visible to direct reads before any flush — inside the action body, outside it, and outside any reactive context.~~ **Superseded 2026-09-10 by A28(5)** (SPEC-ASYN… | +| OL-R3 | live | `docs/rules-mining/optimistic-lanes.md:19` | — | — | The setter's updater receives the current _visible_ (optimistic-if-overridden) value, never the committed value; a plain setter on the underlying source during a transition composes on the transition'… | +| OL-R4 | live | `docs/rules-mining/optimistic-lanes.md:23` | seat.ts×3 | — | Multiple optimistic writes before settle compose sequentially (each updater sees the prior override; last write wins). | +| OL-R5 | live | `docs/rules-mining/optimistic-lanes.md:27` | lanes.ts×2 scheduler.ts×2 seat.ts×2 types.ts×1 verdict.ts×1 | — | An optimistic write **outside any action** reverts at the next flush; subscribers observe the optimistic value and then the reverted value within that single flush (effect log `[1, 2, 1]` after one `f… | +| OL-R6 | live | `docs/rules-mining/optimistic-lanes.md:32` | — | — | An optimistic write inside an `action` holds for the entire action window and reverts when the action's transition completes; each intermediate write during a multi-yield action is observable in order… | +| OL-R7 | live | `docs/rules-mining/optimistic-lanes.md:36` | seat.ts×1 | — | Computed-form `createOptimistic(fn)` with no overrides is a transparent passthrough of its (possibly async) source: promise resolutions, re-fired promises, and async-iterable yields all propagate; ove… | +| OL-R8 | live | `docs/rules-mining/optimistic-lanes.md:40` | — | — | Reset-on-settle targets the source's **newly computed value at settle time**, not the pre-write value: a wrong optimistic guess is auto-corrected to the real result; a correct guess settles silently (… | +| OL-R9 | live | `docs/rules-mining/optimistic-lanes.md:44` | — | — | Regular signals written in the same action are held (transition semantics) while optimistic writes display immediately; downstream memos and chained optimistic computeds see optimistic values and reve… | +| OL-R10 | live | `docs/rules-mining/optimistic-lanes.md:48` | — | — | `refresh()` of an optimistic accessor inside an action clears the override when the refetch settles; calling `refresh()` while the upstream source is still pending must not throw. | +| OL-R11 | live | `docs/rules-mining/optimistic-lanes.md:52` | — | — | Verdict channels: an optimistic override **is the value** on every channel — plain read and `latest()` both return it (including literal `undefined`); the override itself is **verdict-inert** — it nev… | +| OL-R12 | live | `docs/rules-mining/optimistic-lanes.md:56` | — | action-refresh-ispending-loop-4349.test.ts×1 | A bare `refresh()` is a quiet re-ask — never pending; a **declared** reload (`affects(x)` + `refresh(x)` inside an action) pends the slot for the whole reload window, even when the sole consumer is a … | +| OL-R13 | live | `docs/rules-mining/optimistic-lanes.md:60` | — | — | During the pending window, a source recompute that reveals a value **different** from the current override corrects the override in place (before the action settles), triggering downstream refetch; a … | +| OL-R14 | live | `docs/rules-mining/optimistic-lanes.md:67` | — | — | Independent optimistic writes to unrelated signals form independent lanes: notifications scoped to each signal's own subscribers; each action's overrides revert when _that_ action settles, regardless … | +| OL-R15 | live | `docs/rules-mining/optimistic-lanes.md:71` | — | — | A shared subscriber reading multiple optimistic sources merges lanes **for scheduling only**; it must not transfer transaction ownership of overrides. Disjoint-key work settles with its owning action … | +| OL-R16 | live | `docs/rules-mining/optimistic-lanes.md:76` | — | — | Same-key writes from multiple actions **entangle** those actions: the override (and transitively every override of the entangled actions) reverts only when the **last** entangled action settles. | +| OL-R17 | live | `docs/rules-mining/optimistic-lanes.md:81` | — | — | An equal-value write still registers ownership and still performs lane bookkeeping: a second action writing the same value keeps the override alive after the first settles; an override write whose val… | +| OL-R18 | live | `docs/rules-mining/optimistic-lanes.md:86` | — | — | All optimistic writes in one action share one transaction and revert together atomically; lanes/transactions clean up fully between cycles — the Nth cycle behaves exactly like the first, including aft… | +| OL-R19 | live | `docs/rules-mining/optimistic-lanes.md:90` | — | — | A shared **upstream** async resolving must not merge distinct downstream optimistic lanes — independent paths keep updating independently; genuine merge happens only at convergence points (a memo read… | +| OL-R20 | live | `docs/rules-mining/optimistic-lanes.md:94` | — | — | A later action's override wins over an earlier action's background settle: when action 1's refresh resolves _under_ action 2's live override, the visible value is unchanged, downstream must not recomp… | +| OL-R21 | live | `docs/rules-mining/optimistic-lanes.md:100` | — | — | An optimistic write of literal `undefined` is a full-fledged override: visible on plain read and `latest()`, verdict-inert on `isPending`, and it reverts at settle exactly like any other value. | +| OL-R22 | live | `docs/rules-mining/optimistic-lanes.md:105` | — | — | A follow-up optimistic write after an `undefined` override still rides the optimistic path and reverts at settle — `undefined` in the slot must never erase the node's optimistic identity or route late… | +| OL-R23 | live | `docs/rules-mining/optimistic-lanes.md:109` | — | — | Store form distinguishes "override to undefined" from "delete": optimistic set-to-undefined reads `undefined` with the key still present; optimistic `delete` reads `undefined` **and** `"key" in store … | +| OL-R24 | live | `docs/rules-mining/optimistic-lanes.md:116` | — | — | A transition completes only when **all** reachable asyncs (upstream source and downstream lane asyncs) resolve; held source values must never leak to subscribers before completion, even when the upstr… | +| OL-R25 | live | `docs/rules-mining/optimistic-lanes.md:120` | — | — | Lane readiness gating: subscribers reached _through a downstream async memo_ fire with optimistic values only once that async resolves; direct reads show the override immediately. The lane may flush \*… | +| OL-R26 | live | `docs/rules-mining/optimistic-lanes.md:124` | — | — | At settle, the commit of held transition writes and the revert of optimistic overrides are delivered **atomically**: one subscriber run observing both, never a torn intermediate. | +| OL-R27 | live | `docs/rules-mining/optimistic-lanes.md:128` | — | — | Rapid successive user writes replay correctly: the latest override wins; earlier lane flushes deliver the values current at their readiness time; final settled state reflects the last action's confirm… | +| OL-R28 | live | `docs/rules-mining/optimistic-lanes.md:134` | — | — | No-op settles are silent: if the optimistic write equals the current value, neither the write nor the revert notifies; if the settle-time computed value equals the override, no extra notification fire… | +| OL-R29 | live | `docs/rules-mining/optimistic-lanes.md:139` | — | — | Pre-flush writes coalesce: subscribers see only the latest override per flush (`[0, 2, 0]`, never intermediate `1`). | +| OL-R30 | live | `docs/rules-mining/optimistic-lanes.md:143` | — | — | Render-tier and user-tier effects must observe **identical value sequences** at every flush, including the mid-transition moment where an action finished but async reporters are still in flight. | +| OL-R31 | live | `docs/rules-mining/optimistic-lanes.md:147` | core.ts×1 | — | Optimistic lane notifications run even while an unrelated transition is stashed/pending; pending async in one lane never blocks another lane's write/revert notifications. | +| OL-R32 | live | `docs/rules-mining/optimistic-lanes.md:151` | — | — | `isPending` granularity: each async path's pending slot clears when its **own** async resolves; merged downstream nodes stay pending — emitting **no intermediate half-state values** — until all inputs… | +| OL-R33 | live | `docs/rules-mining/optimistic-lanes.md:155` | — | — | No pending flicker when the visible value is unchanged: background refresh phases with an unchanged visible override must not re-pend downstream; a genuinely new in-flight question must fire `isPendin… | +| OL-R34 | live | `docs/rules-mining/optimistic-lanes.md:159` | — | — | `latest()` readers opt into progressive per-path display while plain readers of merged memos wait for full resolution. | +| OL-R35 | live | `docs/rules-mining/optimistic-lanes.md:165` | — | — | `createOptimisticStore` returns `[proxy, setter]`; draft-style mutations inside an action are optimistic: immediately visible through the proxy, wholly reverted at settle. | +| OL-R36 | live | `docs/rules-mining/optimistic-lanes.md:169` | — | — | Array structural edits (e.g. filter-removal) are visible during the window through `length`, index reads, and iteration, and fully revert at settle. | +| OL-R37 | live | `docs/rules-mining/optimistic-lanes.md:174` | — | — | Store optimism is per-key: different keys written by different actions settle independently — same ownership rules as signals (R15/R16) at store-key granularity. | + ## R — optimistic-store (`OS-R`) -| id | status | defined | cited in src | cited in tests | statement (at definition) | -|---|---|---|---|---|---| -| OS-R1 | superseded | `docs/rules-mining/optimistic-store.md:9` | — | — | ~~Synchronous universal visibility.~~** ~~An optimistic write is visible to every reader immediately at write time, before any flush.~~ **Superseded 2026-09-10 by A28(5)**: visible at the flush that c… | -| OS-R2 | live | `docs/rules-mining/optimistic-store.md:13` | — | — | Drafts compose on the live optimistic view.** Each setter draft reads through all prior optimistic state (same tick, across ticks, across separate actions/refetches). | -| OS-R3 | live | `docs/rules-mining/optimistic-store.md:18` | — | — | Per-change notification.** One notification per distinct optimistic value change; sequences like `[0, 1, 2, 0]` are contract. | -| OS-R4 | live | `docs/rules-mining/optimistic-store.md:22` | — | — | Equality cut.** An optimistic write equal to current committed value: no notification on write or settle. | -| OS-R5 | live | `docs/rules-mining/optimistic-store.md:26` | — | — | Snapshot/deep read the optimistic view (resolves O1).** snapshot()/deep() agree with every other reader: overlays, nested writes, optimistic deletes (key absent), array mutations; after settle show co… | -| OS-R6 | live | `docs/rules-mining/optimistic-store.md:31` | — | — | Snapshot allocates fresh objects while an overlay is live** (not identity-stable across calls); settled returns raw identity. | -| OS-R7 | live | `docs/rules-mining/optimistic-store.md:36` | — | — | Propagation through derived graphs** (memo chains, mapArray) like committed values. | -| OS-R8 | live | `docs/rules-mining/optimistic-store.md:38` | — | — | `latest()` returns the optimistic value** during a pending refetch window. | -| OS-R9 | live | `docs/rules-mining/optimistic-store.md:40` | — | — | Cross-lane atomic flip.** Regular store written in the same action holds old value while optimistic store shows overlay; at settle both land in ONE notification pass (mixed intermediates never observe… | -| OS-R10 | live | `docs/rules-mining/optimistic-store.md:46` | — | — | Settle reverts to base with one notification** (`[0,1,0]`). | -| OS-R11 | live | `docs/rules-mining/optimistic-store.md:48` | — | — | Deep-state restoration.** Revert restores complete pre-overlay state at every depth: nested writes, wholesale replacement, array length/indices/order, deletions (value + key membership). | -| OS-R12 | live | `docs/rules-mining/optimistic-store.md:50` | — | — | Revert target is the CURRENT derived base, not a stale snapshot** (dependency changed mid-overlay → revert to recomputed value). | -| OS-R13 | live | `docs/rules-mining/optimistic-store.md:55` | — | — | Base data is not overlay data.** Async-fetched/derived data commits to base and persists; only setter-originated optimistic state discards. | -| OS-R14 | live | `docs/rules-mining/optimistic-store.md:57` | — | — | No-flicker across the settle/refresh seam.** From action-body return until refresh fetch lands, subscribers never observe the previously-committed value of an overridden property. | -| OS-R15 | live | `docs/rules-mining/optimistic-store.md:62` | — | — | Unaffected subscribers do not rerun on another action's settle.** | -| OS-R16 | live | `docs/rules-mining/optimistic-store.md:64` | — | — | Cycles are independent** (no residue between sequential write/settle cycles). | -| OS-R17 | live | `docs/rules-mining/optimistic-store.md:66` | — | — | Optimistic writes never pend.** A plain optimistic store is never pending; an optimistic write alone never makes isPending true on any read (shallow, deep(), root or nested proxy, value or length, sam… | -| OS-R18 | live | `docs/rules-mining/optimistic-store.md:70` | — | — | Overlay lifetime is transaction-bound, per key** (never a timer, never a mere flush boundary — under an action). | -| OS-R19 | live | `docs/rules-mining/optimistic-store.md:72` | — | — | Disjoint-key concurrent actions revert independently** (incl. different rows, deletes) (#2899 ×3). | -| OS-R20 | live | `docs/rules-mining/optimistic-store.md:76` | — | — | Same-key writes entangle whole transactions:** latest write displays; NOTHING in the merged transaction settles until the last member completes — including keys written by only one of them. | -| OS-R21 | live | `docs/rules-mining/optimistic-store.md:81` | — | — | Optimistic delete is per-transaction scoped** (a concurrent action's settle must not resurrect another action's delete). | -| OS-R22 | live | `docs/rules-mining/optimistic-store.md:85` | — | — | Ambient (transaction-less) writes flash:** visible until end of flush, then revert — without touching in-flight actions' keys. | -| OS-R23 | live | `docs/rules-mining/optimistic-store.md:89` | — | — | Actions scope globally (a transaction, not a store handle):** writes made under action A belong to A regardless of which store; separate stores under separate actions settle independently. | -| OS-R24 | live | `docs/rules-mining/optimistic-store.md:91` | — | — | Re-override of a still-overridden key notifies and wins;** the earlier action's completion never resurfaces its value. | -| OS-R25 | live | `docs/rules-mining/optimistic-store.md:95` | — | — | Array mutation overlays:** push, splice, whole-array replacement, top-level array stores — length, index reads, holes, spread/iteration, .map all coherent mid-pending and restore exactly on revert. | -| OS-R26 | live | `docs/rules-mining/optimistic-store.md:97` | — | — | Length reactively consistent with contents;** a consumer reading length then indices in one computation never observes a torn state. | -| OS-R27 | live | `docs/rules-mining/optimistic-store.md:101` | — | — | Key enumeration and `has` are lane-reactive** (Object.keys / `in` reflect optimistic adds/deletes, notify, revert). | -| OS-R28 | live | `docs/rules-mining/optimistic-store.md:103` | — | — | Proxy identity survives truth adoption of optimistic rows:** server data key-matching an optimistically pushed row recycles the proxy (identity preserved) and adopts server values. Single and multiple… | -| OS-R29 | live | `docs/rules-mining/optimistic-store.md:107` | — | — | Entity-swap key probes read committed base, not overlay** (an optimistic `s.id = 99` must not confuse the swap); `key: null` → positional identity. | -| OS-R30 | live | `docs/rules-mining/optimistic-store.md:113` | — | — | Seed invisibility.** Derived store's seed is a draft, never observable: before first resolution every read — get, `in`, keys, spread — throws NotReadyError untracked. Applies to createStore(fn, seed) … | -| OS-R31 | live | `docs/rules-mining/optimistic-store.md:117` | — | — | Dev strictRead scopes escalate:** uninitialized read in a component body throws the `[PENDING_ASYNC_UNTRACKED_READ]` dev error (exact tag is contract), precedence over plain NotReadyError. | -| OS-R32 | live | `docs/rules-mining/optimistic-store.md:119` | — | — | Post-init untracked reads flow committed values,** including during a later refetch window. | -| OS-R33 | live | `docs/rules-mining/optimistic-store.md:121` | — | — | Refetch window keeps the dev safeguard** (committed value untracked; component-body read still dev-throws). | -| OS-R34 | live | `docs/rules-mining/optimistic-store.md:123` | — | — | isPending probes take the prod path in both builds:** dev safeguard must not fire inside a probe; uninitialized + surrounding context ⇒ NotReadyError propagates out of isPending identically dev/prod; … | -| OS-R35 | live | `docs/rules-mining/optimistic-store.md:125` | — | — | Plain stores unaffected** (read normally in every context incl. component bodies). | -| OS-R36 | live | `docs/rules-mining/optimistic-store.md:129` | — | — | Dependency-driven refetch pends the leaf and holds the committed view** until the fetch lands. | -| OS-R37 | live | `docs/rules-mining/optimistic-store.md:131` | — | — | Optimistic writes are verdict-inert:** a mid-refetch write displays but neither clears nor causes pending; the honest mixed state {value: 999, pending: true} is observable. (Re-ruled 2026-07-13, super… | -| OS-R38 | live | `docs/rules-mining/optimistic-store.md:133` | — | — | No-op setters are fully inert:** trap-firing no-ops (s => s, s => ({...s}), same-value write, delete of absent prop) mid-refetch display nothing, don't silence pending, don't entangle with the surroun… | -| OS-R39 | live | `docs/rules-mining/optimistic-store.md:137` | — | — | Landing truth wins over the override:** fetch resolves → server/computed value displays, override consumed, pending clears — even if written mid-flight. | -| OS-R40 | live | `docs/rules-mining/optimistic-store.md:139` | — | — | Bare refresh is a quiet re-ask; affects + refresh is a declared reload.** refresh(store) alone never pends reads; affects(store) + refresh pends them, clearing when data lands. Sync-back refresh insid… | -| OS-R41 | live | `docs/rules-mining/optimistic-store.md:141` | — | — | Streaming continuations are not pending windows.** A generator-based derive (or wrapped createProjection) that yielded once reads settled while awaiting its next chunk, incl. with an override displaye… | -| OS-R42 | live | `docs/rules-mining/optimistic-store.md:143` | — | — | Bare writes ride an in-flight refetch (#2951).** A transaction-less optimistic write while the store's own truth is in flight does NOT revert at flush end; holds until truth lands. Order-independent w… | -| OS-R43 | live | `docs/rules-mining/optimistic-store.md:147` | — | — | Refresh-in-action landings preserve still-pending overlays** (same key ⇒ merged transaction: landing does not consume the pending action's optimistic value). | -| OS-R44 | live | `docs/rules-mining/optimistic-store.md:149` | — | — | Bare-refresh landings consume key-matched overlay content** (optimistic "Optimistic" → server "Saved"); the action's later settle does not revert it. | -| OS-R45 | live | `docs/rules-mining/optimistic-store.md:151` | — | — | Separate-transition landings clear foreign optimistic rows (#2719):** a different source transition resolving fresh data clears optimistic rows of a still-pending unrelated action immediately; later s… | -| OS-R46 | live | `docs/rules-mining/optimistic-store.md:155` | — | — | Refetch persistence across multi-action windows:** overlay survives arbitrary interleaved refresh landings while any overlapping action is pending. | +| id | status | defined | cited in src | cited in tests | statement (at definition) | +| ------ | ---------- | ------------------------------------------- | ------------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| OS-R1 | superseded | `docs/rules-mining/optimistic-store.md:9` | — | — | ~~Synchronous universal visibility.~~** ~~An optimistic write is visible to every reader immediately at write time, before any flush.~~ **Superseded 2026-09-10 by A28(5)\*\*: visible at the flush that c… | +| OS-R2 | live | `docs/rules-mining/optimistic-store.md:13` | — | — | Drafts compose on the live optimistic view.\*\* Each setter draft reads through all prior optimistic state (same tick, across ticks, across separate actions/refetches). | +| OS-R3 | live | `docs/rules-mining/optimistic-store.md:18` | — | — | Per-change notification.\*\* One notification per distinct optimistic value change; sequences like `[0, 1, 2, 0]` are contract. | +| OS-R4 | live | `docs/rules-mining/optimistic-store.md:22` | — | — | Equality cut.\*\* An optimistic write equal to current committed value: no notification on write or settle. | +| OS-R5 | live | `docs/rules-mining/optimistic-store.md:26` | — | — | Snapshot/deep read the optimistic view (resolves O1).\*\* snapshot()/deep() agree with every other reader: overlays, nested writes, optimistic deletes (key absent), array mutations; after settle show co… | +| OS-R6 | live | `docs/rules-mining/optimistic-store.md:31` | — | — | Snapshot allocates fresh objects while an overlay is live\*\* (not identity-stable across calls); settled returns raw identity. | +| OS-R7 | live | `docs/rules-mining/optimistic-store.md:36` | — | — | Propagation through derived graphs\*\* (memo chains, mapArray) like committed values. | +| OS-R8 | live | `docs/rules-mining/optimistic-store.md:38` | — | — | `latest()` returns the optimistic value\*\* during a pending refetch window. | +| OS-R9 | live | `docs/rules-mining/optimistic-store.md:40` | — | — | Cross-lane atomic flip.\*\* Regular store written in the same action holds old value while optimistic store shows overlay; at settle both land in ONE notification pass (mixed intermediates never observe… | +| OS-R10 | live | `docs/rules-mining/optimistic-store.md:46` | — | — | Settle reverts to base with one notification\*\* (`[0,1,0]`). | +| OS-R11 | live | `docs/rules-mining/optimistic-store.md:48` | — | — | Deep-state restoration.\*\* Revert restores complete pre-overlay state at every depth: nested writes, wholesale replacement, array length/indices/order, deletions (value + key membership). | +| OS-R12 | live | `docs/rules-mining/optimistic-store.md:50` | — | — | Revert target is the CURRENT derived base, not a stale snapshot\*\* (dependency changed mid-overlay → revert to recomputed value). | +| OS-R13 | live | `docs/rules-mining/optimistic-store.md:55` | — | — | Base data is not overlay data.\*\* Async-fetched/derived data commits to base and persists; only setter-originated optimistic state discards. | +| OS-R14 | live | `docs/rules-mining/optimistic-store.md:57` | — | — | No-flicker across the settle/refresh seam.\*\* From action-body return until refresh fetch lands, subscribers never observe the previously-committed value of an overridden property. | +| OS-R15 | live | `docs/rules-mining/optimistic-store.md:62` | — | — | Unaffected subscribers do not rerun on another action's settle.\*\* | +| OS-R16 | live | `docs/rules-mining/optimistic-store.md:64` | — | — | Cycles are independent\*\* (no residue between sequential write/settle cycles). | +| OS-R17 | live | `docs/rules-mining/optimistic-store.md:66` | — | — | Optimistic writes never pend.\*\* A plain optimistic store is never pending; an optimistic write alone never makes isPending true on any read (shallow, deep(), root or nested proxy, value or length, sam… | +| OS-R18 | live | `docs/rules-mining/optimistic-store.md:70` | — | — | Overlay lifetime is transaction-bound, per key\*\* (never a timer, never a mere flush boundary — under an action). | +| OS-R19 | live | `docs/rules-mining/optimistic-store.md:72` | — | — | Disjoint-key concurrent actions revert independently\*\* (incl. different rows, deletes) (#2899 ×3). | +| OS-R20 | live | `docs/rules-mining/optimistic-store.md:76` | — | — | Same-key writes entangle whole transactions:\*\* latest write displays; NOTHING in the merged transaction settles until the last member completes — including keys written by only one of them. | +| OS-R21 | live | `docs/rules-mining/optimistic-store.md:81` | — | — | Optimistic delete is per-transaction scoped\*\* (a concurrent action's settle must not resurrect another action's delete). | +| OS-R22 | live | `docs/rules-mining/optimistic-store.md:85` | — | — | Ambient (transaction-less) writes flash:\*\* visible until end of flush, then revert — without touching in-flight actions' keys. | +| OS-R23 | live | `docs/rules-mining/optimistic-store.md:89` | — | — | Actions scope globally (a transaction, not a store handle):\*\* writes made under action A belong to A regardless of which store; separate stores under separate actions settle independently. | +| OS-R24 | live | `docs/rules-mining/optimistic-store.md:91` | — | — | Re-override of a still-overridden key notifies and wins;\*\* the earlier action's completion never resurfaces its value. | +| OS-R25 | live | `docs/rules-mining/optimistic-store.md:95` | — | — | Array mutation overlays:\*\* push, splice, whole-array replacement, top-level array stores — length, index reads, holes, spread/iteration, .map all coherent mid-pending and restore exactly on revert. | +| OS-R26 | live | `docs/rules-mining/optimistic-store.md:97` | — | — | Length reactively consistent with contents;\*\* a consumer reading length then indices in one computation never observes a torn state. | +| OS-R27 | live | `docs/rules-mining/optimistic-store.md:101` | — | — | Key enumeration and `has` are lane-reactive\*\* (Object.keys / `in` reflect optimistic adds/deletes, notify, revert). | +| OS-R28 | live | `docs/rules-mining/optimistic-store.md:103` | — | — | Proxy identity survives truth adoption of optimistic rows:\*\* server data key-matching an optimistically pushed row recycles the proxy (identity preserved) and adopts server values. Single and multiple… | +| OS-R29 | live | `docs/rules-mining/optimistic-store.md:107` | — | — | Entity-swap key probes read committed base, not overlay\*\* (an optimistic `s.id = 99` must not confuse the swap); `key: null` → positional identity. | +| OS-R30 | live | `docs/rules-mining/optimistic-store.md:113` | — | — | Seed invisibility.\*\* Derived store's seed is a draft, never observable: before first resolution every read — get, `in`, keys, spread — throws NotReadyError untracked. Applies to createStore(fn, seed) … | +| OS-R31 | live | `docs/rules-mining/optimistic-store.md:117` | — | — | Dev strictRead scopes escalate:\*\* uninitialized read in a component body throws the `[PENDING_ASYNC_UNTRACKED_READ]` dev error (exact tag is contract), precedence over plain NotReadyError. | +| OS-R32 | live | `docs/rules-mining/optimistic-store.md:119` | — | — | Post-init untracked reads flow committed values,\*\* including during a later refetch window. | +| OS-R33 | live | `docs/rules-mining/optimistic-store.md:121` | — | — | Refetch window keeps the dev safeguard\*\* (committed value untracked; component-body read still dev-throws). | +| OS-R34 | live | `docs/rules-mining/optimistic-store.md:123` | — | — | isPending probes take the prod path in both builds:\*\* dev safeguard must not fire inside a probe; uninitialized + surrounding context ⇒ NotReadyError propagates out of isPending identically dev/prod; … | +| OS-R35 | live | `docs/rules-mining/optimistic-store.md:125` | — | — | Plain stores unaffected\*\* (read normally in every context incl. component bodies). | +| OS-R36 | live | `docs/rules-mining/optimistic-store.md:129` | — | — | Dependency-driven refetch pends the leaf and holds the committed view\*\* until the fetch lands. | +| OS-R37 | live | `docs/rules-mining/optimistic-store.md:131` | — | — | Optimistic writes are verdict-inert:\*\* a mid-refetch write displays but neither clears nor causes pending; the honest mixed state {value: 999, pending: true} is observable. (Re-ruled 2026-07-13, super… | +| OS-R38 | live | `docs/rules-mining/optimistic-store.md:133` | — | — | No-op setters are fully inert:\*\* trap-firing no-ops (s => s, s => ({...s}), same-value write, delete of absent prop) mid-refetch display nothing, don't silence pending, don't entangle with the surroun… | +| OS-R39 | live | `docs/rules-mining/optimistic-store.md:137` | — | — | Landing truth wins over the override:\*\* fetch resolves → server/computed value displays, override consumed, pending clears — even if written mid-flight. | +| OS-R40 | live | `docs/rules-mining/optimistic-store.md:139` | — | — | Bare refresh is a quiet re-ask; affects + refresh is a declared reload.\*\* refresh(store) alone never pends reads; affects(store) + refresh pends them, clearing when data lands. Sync-back refresh insid… | +| OS-R41 | live | `docs/rules-mining/optimistic-store.md:141` | — | — | Streaming continuations are not pending windows.\*\* A generator-based derive (or wrapped createProjection) that yielded once reads settled while awaiting its next chunk, incl. with an override displaye… | +| OS-R42 | live | `docs/rules-mining/optimistic-store.md:143` | — | — | Bare writes ride an in-flight refetch (#2951).\*\* A transaction-less optimistic write while the store's own truth is in flight does NOT revert at flush end; holds until truth lands. Order-independent w… | +| OS-R43 | live | `docs/rules-mining/optimistic-store.md:147` | — | — | Refresh-in-action landings preserve still-pending overlays\*\* (same key ⇒ merged transaction: landing does not consume the pending action's optimistic value). | +| OS-R44 | live | `docs/rules-mining/optimistic-store.md:149` | — | — | Bare-refresh landings consume key-matched overlay content\*\* (optimistic "Optimistic" → server "Saved"); the action's later settle does not revert it. | +| OS-R45 | live | `docs/rules-mining/optimistic-store.md:151` | — | — | Separate-transition landings clear foreign optimistic rows (#2719):\*\* a different source transition resolving fresh data clears optimistic rows of a still-pending unrelated action immediately; later s… | +| OS-R46 | live | `docs/rules-mining/optimistic-store.md:155` | — | — | Refetch persistence across multi-action windows:\*\* overlay survives arbitrary interleaved refresh landings while any overlapping action is pending. | + ## R — projections (`PJ-R`) -| id | status | defined | cited in src | cited in tests | statement (at definition) | -|---|---|---|---|---|---| -| PJ-R1 | live | `docs/rules-mining/projections.md:9` | — | — | The derive receives the projection's current state as a mutable draft that persists across runs**; prior runs' writes are visible and editable later. | -| PJ-R2 | live | `docs/rules-mining/projections.md:12` | store.ts×1 | projection-absent-key-tracking.test.ts×1 | Draft reads inside the derive never register dependencies (no self-tracking)**, including `has`/index probes from array methods (`findIndex`, `splice`) and inspection traps from `console.log`. | -| PJ-R3 | live | `docs/rules-mining/projections.md:15` | — | — | The derive runs eagerly at creation (before any read/subscriber) and re-runs on flush when tracked sources change, even with zero subscribers.** | -| PJ-R4 | live | `docs/rules-mining/projections.md:19` | — | — | A returned value merges reconcile-style**: changed paths notify, absent keys delete, unchanged paths keep value and identity. | -| PJ-R5 | live | `docs/rules-mining/projections.md:22` | — | — | 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` | — | — | 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` | — | — | 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` | — | — | 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` | — | createProjection.draft-lifetime-3585.test.ts×1 | 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:86` | — | — | 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:88` | — | createProjection.draft-lifetime-3585.test.ts×1 | 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:90` | — | — | Async recompute does not coarsen granularity**: after settle, only changed-path subscribers rerun. | -| PJ-R28 | live | `docs/rules-mining/projections.md:92` | — | — | `refresh(proj)` forces a new derive run; bare refresh is quiet** (no pending published; silent reveal). | -| PJ-R29 | live | `docs/rules-mining/projections.md:94` | — | — | `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:96` | — | — | With no effect subscribed, async work creates no transition** (isPending false throughout initial load). | -| PJ-R31 | live | `docs/rules-mining/projections.md:98` | — | createProjection.draft-lifetime-3585.test.ts×1 | 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:100` | — | — | 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:102` | — | — | 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:104` | — | — | 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:107` | — | — | 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:111` | — | — | Disposing the owning root stops the projection** (no recomputes, no notifications afterward). | -| PJ-R37 | ruled | `docs/rules-mining/projections.md:113` | target.ts×1 | createProjection.draft-lifetime-3585.test.ts×3 | A draft is valid from the start of its run until it is superseded or its owner is disposed — not "until no longer in flight" (#3585, ruled 2026-09-22).** The draft handed to a run stays writable after… | +| id | status | defined | cited in src | cited in tests | statement (at definition) | +| ------ | ------ | -------------------------------------- | ------------ | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| PJ-R1 | live | `docs/rules-mining/projections.md:9` | — | — | The derive receives the projection's current state as a mutable draft that persists across runs\*\*; prior runs' writes are visible and editable later. | +| PJ-R2 | live | `docs/rules-mining/projections.md:12` | store.ts×1 | projection-absent-key-tracking.test.ts×1 | Draft reads inside the derive never register dependencies (no self-tracking)\*\*, including `has`/index probes from array methods (`findIndex`, `splice`) and inspection traps from `console.log`. | +| PJ-R3 | live | `docs/rules-mining/projections.md:15` | — | — | The derive runs eagerly at creation (before any read/subscriber) and re-runs on flush when tracked sources change, even with zero subscribers.\*\* | +| PJ-R4 | live | `docs/rules-mining/projections.md:19` | — | — | A returned value merges reconcile-style\*\*: changed paths notify, absent keys delete, unchanged paths keep value and identity. | +| PJ-R5 | live | `docs/rules-mining/projections.md:22` | — | — | 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` | — | — | 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` | — | — | 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` | — | — | 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` | — | createProjection.draft-lifetime-3585.test.ts×1 | 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:86` | — | — | 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:88` | — | createProjection.draft-lifetime-3585.test.ts×1 | 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:90` | — | — | Async recompute does not coarsen granularity\*\*: after settle, only changed-path subscribers rerun. | +| PJ-R28 | live | `docs/rules-mining/projections.md:92` | — | — | `refresh(proj)` forces a new derive run; bare refresh is quiet\*\* (no pending published; silent reveal). | +| PJ-R29 | live | `docs/rules-mining/projections.md:94` | — | — | `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:96` | — | — | With no effect subscribed, async work creates no transition\*\* (isPending false throughout initial load). | +| PJ-R31 | live | `docs/rules-mining/projections.md:98` | — | createProjection.draft-lifetime-3585.test.ts×1 | 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:100` | — | — | 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:102` | — | — | 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:104` | — | — | 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:107` | — | — | 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:111` | — | — | Disposing the owning root stops the projection\*\* (no recomputes, no notifications afterward). | +| PJ-R37 | ruled | `docs/rules-mining/projections.md:113` | target.ts×1 | createProjection.draft-lifetime-3585.test.ts×3 | A draft is valid from the start of its run until it is superseded or its owner is disposed — not "until no longer in flight" (#3585, ruled 2026-09-22).\*\* The draft handed to a run stays writable after… | + ## R — reconcile-snapshot (`RS-R`) -| id | status | defined | cited in src | cited in tests | statement (at definition) | -|---|---|---|---|---|---| -| RS-R1 | live | `docs/rules-mining/reconcile-snapshot.md:9` | — | — | Keyed object merge deletes absent keys.** Properties present in `next` update; properties absent from `next` are deleted (read `undefined`, removed from `in`/keys). | -| RS-R2 | live | `docs/rules-mining/reconcile-snapshot.md:12` | — | — | Reconcile applies to any nested proxy, not just the root**, with identical semantics. | -| RS-R3 | live | `docs/rules-mining/reconcile-snapshot.md:15` | — | — | Keyed identity mismatch at the target throws** (key differs, or key present on target but missing from `next`). Post-throw state is deliberately unasserted (original expectations commented out) — the … | -| RS-R4 | live | `docs/rules-mining/reconcile-snapshot.md:18` | — | — | `key: null` / `key: ""` disables key matching**: positional merge, no root identity check. | -| RS-R5 | live | `docs/rules-mining/reconcile-snapshot.md:21` | — | — | Key modes: string key, key function, none.** KeyFn's call set is observable (see R17). | -| RS-R6 | live | `docs/rules-mining/reconcile-snapshot.md:24` | — | — | Key-matched items preserve logical (proxy) identity across reorder, insert, delete.** | -| RS-R7 | live | `docs/rules-mining/reconcile-snapshot.md:27` | — | — | Re-sent identical objects preserve raw identity: `snapshot(state.arr[i])` is `Object.is`-equal to the original.** CONFLICT (benign, verify): adoption satisfies this since raw becomes the incoming obje… | -| RS-R8 | live | `docs/rules-mining/reconcile-snapshot.md:30` | — | — | 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` | — | — | 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` | — | — | 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` | — | — | 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` | — | — | 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` | — | — | 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` | — | — | 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` | — | — | 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` | — | — | Symbol-keyed data round-trips through snapshot**: enumerable symbols preserved in copies; writes inside symbol subtrees captured; added-after-snapshot appear; deleted dropped; NON-enumerable symbols e… | -| RS-R30 | live | `docs/rules-mining/reconcile-snapshot.md:99` | — | — | Snapshot-scope machinery (setSnapshotCapture / markSnapshotScope / releaseSnapshotScope / clearSnapshots):** signals/memos created during capture freeze creation-time value for scoped readers; writes … | -| RS-R31 | live | `docs/rules-mining/reconcile-snapshot.md:102` | — | — | Store properties written during capture preserve pre-write value for scoped readers; unwritten use current.** CONFLICT: current mechanism (`STORE_SNAPSHOT_PROPS` in set trap) is layer-adjacent; the wr… | -| RS-R32 | live | `docs/rules-mining/reconcile-snapshot.md:105` | — | — | A pending async projection suppresses snapshot capture**; after resolve + release, readers see resolved value. CONFLICT (mild): guard must move to lane-scoped adoption writes. | -| RS-R33 | live | `docs/rules-mining/reconcile-snapshot.md:110` | — | — | `merge` core contract:** lazy getters (`this` = source); later sources win incl. explicit `undefined`; key union via `in`/keys; value props copied by value; non-enumerable → enumerable on result; firs… | -| RS-R34 | live | `docs/rules-mining/reconcile-snapshot.md:113` | — | — | `merge` reference-return optimization:** same reference for single arg, trailing falsy args, and when last source's own keys cover the union; new proxy otherwise; holds for store proxies. | -| RS-R35 | live | `docs/rules-mining/reconcile-snapshot.md:115` | — | — | `merge` over signal-of-object source is reactive with minimal notifications.** | -| RS-R36 | live | `docs/rules-mining/reconcile-snapshot.md:117` | — | — | `omit` contract:** removed keys disappear from get/`in`/keys incl. store-proxy sources; kept value props copied; descriptors cloned faithfully; pollution-safe; composes with merge. | -| RS-R37 | live | `docs/rules-mining/reconcile-snapshot.md:119` | — | shared-child-multiparent.test.ts×1 | `deep()` contract:** plain data; tracks entire reachable tree (leaf writes, push, branch replacement, symbol subtree writes, symbol add/delete, shared-object writes through other paths); one notificat… | -| RS-R38 | live | `docs/rules-mining/reconcile-snapshot.md:121` | — | — | Untracked read-through (via merge clone) shows pre-write values until flush.** CONFLICT (same as R27, MAJOR): contradicts write-through-immediately unless plain setter writes stay staged until flush. … | +| id | status | defined | cited in src | cited in tests | statement (at definition) | +| ------ | ------ | --------------------------------------------- | ------------ | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| RS-R1 | live | `docs/rules-mining/reconcile-snapshot.md:9` | — | — | Keyed object merge deletes absent keys.\*\* Properties present in `next` update; properties absent from `next` are deleted (read `undefined`, removed from `in`/keys). | +| RS-R2 | live | `docs/rules-mining/reconcile-snapshot.md:12` | — | — | Reconcile applies to any nested proxy, not just the root\*\*, with identical semantics. | +| RS-R3 | live | `docs/rules-mining/reconcile-snapshot.md:15` | — | — | Keyed identity mismatch at the target throws\*\* (key differs, or key present on target but missing from `next`). Post-throw state is deliberately unasserted (original expectations commented out) — the … | +| RS-R4 | live | `docs/rules-mining/reconcile-snapshot.md:18` | — | — | `key: null` / `key: ""` disables key matching\*\*: positional merge, no root identity check. | +| RS-R5 | live | `docs/rules-mining/reconcile-snapshot.md:21` | — | — | Key modes: string key, key function, none.\*\* KeyFn's call set is observable (see R17). | +| RS-R6 | live | `docs/rules-mining/reconcile-snapshot.md:24` | — | — | Key-matched items preserve logical (proxy) identity across reorder, insert, delete.\*\* | +| RS-R7 | live | `docs/rules-mining/reconcile-snapshot.md:27` | — | — | Re-sent identical objects preserve raw identity: `snapshot(state.arr[i])` is `Object.is`-equal to the original.\*\* CONFLICT (benign, verify): adoption satisfies this since raw becomes the incoming obje… | +| RS-R8 | live | `docs/rules-mining/reconcile-snapshot.md:30` | — | — | 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` | — | — | 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` | — | — | 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` | — | — | 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` | — | — | 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` | — | — | 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` | — | — | 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` | — | — | 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` | — | — | Symbol-keyed data round-trips through snapshot\*\*: enumerable symbols preserved in copies; writes inside symbol subtrees captured; added-after-snapshot appear; deleted dropped; NON-enumerable symbols e… | +| RS-R30 | live | `docs/rules-mining/reconcile-snapshot.md:99` | — | — | Snapshot-scope machinery (setSnapshotCapture / markSnapshotScope / releaseSnapshotScope / clearSnapshots):\*\* signals/memos created during capture freeze creation-time value for scoped readers; writes … | +| RS-R31 | live | `docs/rules-mining/reconcile-snapshot.md:102` | — | — | Store properties written during capture preserve pre-write value for scoped readers; unwritten use current.\*\* CONFLICT: current mechanism (`STORE_SNAPSHOT_PROPS` in set trap) is layer-adjacent; the wr… | +| RS-R32 | live | `docs/rules-mining/reconcile-snapshot.md:105` | — | — | A pending async projection suppresses snapshot capture\*\*; after resolve + release, readers see resolved value. CONFLICT (mild): guard must move to lane-scoped adoption writes. | +| RS-R33 | live | `docs/rules-mining/reconcile-snapshot.md:110` | — | — | `merge` core contract:\*\* lazy getters (`this` = source); later sources win incl. explicit `undefined`; key union via `in`/keys; value props copied by value; non-enumerable → enumerable on result; firs… | +| RS-R34 | live | `docs/rules-mining/reconcile-snapshot.md:113` | — | — | `merge` reference-return optimization:\*\* same reference for single arg, trailing falsy args, and when last source's own keys cover the union; new proxy otherwise; holds for store proxies. | +| RS-R35 | live | `docs/rules-mining/reconcile-snapshot.md:115` | — | — | `merge` over signal-of-object source is reactive with minimal notifications.\*\* | +| RS-R36 | live | `docs/rules-mining/reconcile-snapshot.md:117` | — | — | `omit` contract:\*\* removed keys disappear from get/`in`/keys incl. store-proxy sources; kept value props copied; descriptors cloned faithfully; pollution-safe; composes with merge. | +| RS-R37 | live | `docs/rules-mining/reconcile-snapshot.md:119` | — | shared-child-multiparent.test.ts×1 | `deep()` contract:\*\* plain data; tracks entire reachable tree (leaf writes, push, branch replacement, symbol subtree writes, symbol add/delete, shared-object writes through other paths); one notificat… | +| RS-R38 | live | `docs/rules-mining/reconcile-snapshot.md:121` | — | — | Untracked read-through (via merge clone) shows pre-write values until flush.\*\* CONFLICT (same as R27, MAJOR): contradicts write-through-immediately unless plain setter writes stay staged until flush. … | + ## § — design sections -| id | status | defined | cited in src | cited in tests | statement (at definition) | -|---|---|---|---|---|---| -| §1 | live | `docs/INTERNALS-STORE-STATE.md:27` | seat.ts×1 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` | core.ts×2 optimistic.ts×1 reconcile.ts×1 store.ts×2 | seat-shadow-report.ts×1 | Write paths (all must stay equivalent) | -| §4 | live | `docs/INTERNALS-STORE-STATE.md:200` | seat.ts×2 | seat-shadow-report.ts×1 | Identity rules | -| §5 | live | `docs/INTERNALS-STORE-STATE.md:221` | store.ts×1 target.ts×1 | — | Laziness invariants (candidates for `__TEST__` assertions) | -| §5b | live | `docs/INTERNALS-STORE-STATE.md:239` | target.ts×2 | — | Creation budget (phase-1 fitness) | -| §5c | live | `docs/INTERNALS-STORE-STATE.md:258` | — | — | Comparison method (shipped vs rewrite) | -| §6 | live | `docs/INTERNALS-STORE-STATE.md:309` | seat.ts×3 target.ts×1 | — | Structural edits — the key-set node (resolves O2, RUL-8) | -| §6b | live | `docs/INTERNALS-STORE-STATE.md:337` | — | adoption-lane-rollback.test.ts×1 | Lane-aware adoption (RUL-5) | -| §6c | live | `docs/INTERNALS-STORE-STATE.md:356` | — | createProjection.async.test.ts×1 flight-owned-transaction.test.ts×1 | Store-wide status gating (RUL-7) | -| §6d | live | `docs/INTERNALS-STORE-STATE.md:369` | reconcile.ts×2 target.ts×2 | — | Diff reachability (RUL-11) | -| §7 | live | `docs/INTERNALS-STORE-STATE.md:381` | seat.ts×2 optimistic.ts×1 projection.ts×1 | — | Projections & optimism layering | -| §7b | live | `docs/INTERNALS-STORE-STATE.md:391` | scheduler.ts×1 affects.ts×1 optimistic.ts×2 projection.ts×1 reconcile.ts×2 store.ts×9 target.ts×4 | projection-proxy-then-array-4361.test.ts×1 optimistic-chained-revert-3672-memo.test.ts×1 optimistic-removed-prop-3883.test.ts×1 | Chained backing (cross-store) — spec | -| §8 | live | `docs/INTERNALS-STORE-STATE.md:466` | — | l2-contract.test.ts×1 reconcile-resend-identity.test.ts×1 | Assumptions / open questions | -| §8b | live | `docs/INTERNALS-STORE-STATE.md:522` | — | — | Suite-mined rules (2026-08-16) — index & rulings needed | -| §9 | live | `docs/INTERNALS-STORE-STATE.md:758` | — | — | 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` | 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` | — | — | 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 | rules-index.test.ts×1 | Signal-literal diet: `_time`, `_fn`, `_statusFlags` are computed-only — _reconstructed_ | +| id | status | defined | cited in src | cited in tests | statement (at definition) | +| ---- | ------ | ----------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | +| §1 | live | `docs/INTERNALS-STORE-STATE.md:27` | seat.ts×1 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` | core.ts×2 optimistic.ts×1 reconcile.ts×1 store.ts×2 | seat-shadow-report.ts×1 | Write paths (all must stay equivalent) | +| §4 | live | `docs/INTERNALS-STORE-STATE.md:200` | seat.ts×2 | seat-shadow-report.ts×1 | Identity rules | +| §5 | live | `docs/INTERNALS-STORE-STATE.md:221` | store.ts×1 target.ts×1 | — | Laziness invariants (candidates for `__TEST__` assertions) | +| §5b | live | `docs/INTERNALS-STORE-STATE.md:239` | target.ts×2 | — | Creation budget (phase-1 fitness) | +| §5c | live | `docs/INTERNALS-STORE-STATE.md:258` | — | — | Comparison method (shipped vs rewrite) | +| §6 | live | `docs/INTERNALS-STORE-STATE.md:309` | seat.ts×3 target.ts×1 | — | Structural edits — the key-set node (resolves O2, RUL-8) | +| §6b | live | `docs/INTERNALS-STORE-STATE.md:337` | — | adoption-lane-rollback.test.ts×1 | Lane-aware adoption (RUL-5) | +| §6c | live | `docs/INTERNALS-STORE-STATE.md:356` | — | createProjection.async.test.ts×1 flight-owned-transaction.test.ts×1 | Store-wide status gating (RUL-7) | +| §6d | live | `docs/INTERNALS-STORE-STATE.md:369` | reconcile.ts×2 target.ts×2 | — | Diff reachability (RUL-11) | +| §7 | live | `docs/INTERNALS-STORE-STATE.md:381` | seat.ts×2 optimistic.ts×1 projection.ts×1 | — | Projections & optimism layering | +| §7b | live | `docs/INTERNALS-STORE-STATE.md:391` | scheduler.ts×1 affects.ts×1 optimistic.ts×2 projection.ts×1 reconcile.ts×2 store.ts×9 target.ts×4 | projection-proxy-then-array-4361.test.ts×1 optimistic-chained-revert-3672-memo.test.ts×1 optimistic-removed-prop-3883.test.ts×1 | Chained backing (cross-store) — spec | +| §8 | live | `docs/INTERNALS-STORE-STATE.md:466` | seat.ts×1 | l2-contract.test.ts×1 reconcile-resend-identity.test.ts×1 | Assumptions / open questions | +| §8b | live | `docs/INTERNALS-STORE-STATE.md:522` | — | — | Suite-mined rules (2026-08-16) — index & rulings needed | +| §9 | live | `docs/INTERNALS-STORE-STATE.md:758` | — | — | 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` | 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` | — | — | 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 | rules-index.test.ts×1 | Signal-literal diet: `_time`, `_fn`, `_statusFlags` are computed-only — _reconstructed_ | + +## S — scenarios (rulings) + +| id | status | defined | cited in src | cited in tests | statement (at definition) | +| ---- | --------------------------- | ------------------------ | --------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| S-1 | prose | `docs/SCENARIOS.md:18` | — | — | [prose] Navigation: page B appears whole — D-1, F-2, F-7 | +| S-2 | prose | `docs/SCENARIOS.md:41` | — | — | [prose] A memo that starts reading a held value reveals with it — D-1 (#3408) | +| S-3 | executable, standing | `docs/SCENARIOS.md:63` | — | — | [executable, standing] A mount during a hold: a derivation born held holds the mount; the branch appears whole at the commit — D-1, D-3 (A29) | +| S-4 | prose | `docs/SCENARIOS.md:95` | — | — | [prose] One hole, two sources: an unrelated sync write passes through — D-3 (A15 shared hole) | +| S-5 | prose | `docs/SCENARIOS.md:118` | constants.ts×1 | boundary-mounts-separate-flushes-4283.test.ts×1 | [prose] Membership is the tick's — D-4 | +| S-6 | executable, standing | `docs/SCENARIOS.md:139` | — | — | [executable, standing] A new async memo a render effect consumes waits with the hold — D-1, F-2 (#3800) | +| S-7 | prose | `docs/SCENARIOS.md:189` | — | — | [prose] Root mount with uncaught async waits to attach; with a boundary it shows the fallback — D-6, D-12 | +| S-8 | prose | `docs/SCENARIOS.md:211` | — | — | [prose] An action holds its writes; display-ahead readers show them — D-7, F-2, F-6, F-10 | +| S-9 | prose | `docs/SCENARIOS.md:229` | — | — | [prose] `yield` is the hold-safe suspension; a fresh write after `await` escapes — D-7 (A26) | +| S-10 | prose | `docs/SCENARIOS.md:252` | — | — | [prose] A write is a proposal — D-8 (A34) | +| S-11 | prose | `docs/SCENARIOS.md:271` | — | — | [prose] Entanglement through a shared derivation; a reveal that discovers a flight — D-9 (A15) | +| S-12 | prose | `docs/SCENARIOS.md:290` | — | — | [prose] The committed frame keeps its inputs — D-10 (A30) | +| S-13 | prose | `docs/SCENARIOS.md:314` | — | — | [prose] The last on-screen reader leaving releases the hold — D-11 (O3) | +| S-14 | prose | `docs/SCENARIOS.md:334` | — | boundary-fallback-held-swap-3991.test.ts×1 | [prose] A fresh `` a separate change mounts over a held value shows its fallback now — D-12 (#3540) | +| S-15 | prose | `docs/SCENARIOS.md:362` | — | — | [prose] A boundary whose own mount is held appears at that hold's commit — D-13 | +| S-16 | prose | `docs/SCENARIOS.md:382` | — | — | [prose] A `Loading` reset ends the hold on writes only its readers observed; an outside reader keeps it — D-14 (A33, #3375) | +| S-17 | prose | `docs/SCENARIOS.md:402` | — | — | [prose] After a reset the fallback stays until every reader under it settles — D-14 (#3459) | +| S-18 | prose | `docs/SCENARIOS.md:419` | — | — | [prose] `on` follows the frame of the change — D-15 (#3575) | +| S-19 | prose | `docs/SCENARIOS.md:441` | — | — | [prose] A same-source outside read: the fallback is armed off screen and never seen — D-16 | +| S-20 | prose | `docs/SCENARIOS.md:457` | — | — | [prose] The action outlasts the data: a race the fallback lost, not reported — D-16 | +| S-21 | executable, standing | `docs/SCENARIOS.md:471` | — | — | [executable, standing] A display-ahead read in `on` shows the fallback now, beside the held frame — D-17 | +| S-22 | prose | `docs/SCENARIOS.md:548` | boundaries.ts×1 | loading-on-rearm-fallback-effect-4268.test.ts×1 | [prose] A fallback that itself reads pending data — D-18 | +| S-23 | prose | `docs/SCENARIOS.md:571` | — | — | [prose] Queued runs behind a fallback wait; portals included — D-19 | +| S-24 | prose | `docs/SCENARIOS.md:581` | — | — | [prose] An armed boundary lets a lane reveal immediately — D-21 | +| S-25 | executable, standing | `docs/SCENARIOS.md:607` | — | — | [executable, standing] Nothing shows a write before its flush; then every channel at once — D-22 (A28) | +| S-26 | executable, standing | `docs/SCENARIOS.md:652` | — | — | [executable, standing] `isPending` is about a new question — D-23 (A19, A24) | +| S-27 | executable, standing | `docs/SCENARIOS.md:682` | — | — | [executable, standing] `[isPending(x), x()]` is atomic; the `latest` form follows x's own async only — D-23, D-25 (A10, A8) | +| S-28 | prose | `docs/SCENARIOS.md:724` | — | latest-uninitialized-over-latest-4362.test.ts×1 | [prose] `isPending` never throws unowned; `latest` throws uninitialized everywhere — D-24 (A16, A7) | +| S-29 | executable, standing | `docs/SCENARIOS.md:737` | scheduler.ts×1 | — | [executable, standing] A display-ahead reader's own async is held — D-25 (A31, #3442) | +| S-30 | prose | `docs/SCENARIOS.md:786` | — | — | [prose] A `latest()` reader mounted mid-hold shows the committed value and reveals with the lane — D-26 (#3460) | +| S-31 | executable, standing | `docs/SCENARIOS.md:813` | — | display-ahead-plain-write-content.test.ts×3 | [executable, standing] A verdict reader's mount is mainline: `Show when={latest(x)}` → `Loading` shows its fallback — D-1, D-25 (#3851) | +| S-32 | prose | `docs/SCENARIOS.md:1023` | — | — | [prose] An optimistic write shows from its flush; held writes beside it; one frame at the end — D-32, D-33 (A17) | +| S-33 | executable, standing | `docs/SCENARIOS.md:1053` | — | — | [executable, standing] A different truth corrects the graph now and the screen at the commit; a never-shown guess is void — D-34 (#3331) | +| S-34 | prose | `docs/SCENARIOS.md:1079` | — | — | [prose] Provenance: an older action's late answer never corrects a newer guess — D-35 (Q-D) | +| S-35 | prose | `docs/SCENARIOS.md:1097` | — | — | [prose] The body's end corrects a guess nothing authoritative is answering — D-35 (#3427) | +| S-36 | prose | `docs/SCENARIOS.md:1142` | — | — | [prose] A declared first paint is loading, not pending — D-30 (A27) | +| S-37 | prose | `docs/SCENARIOS.md:1160` | — | derived-store-ispending-boundary-4269.test.ts×1 | [prose] A derived store's seed is never visible; pending is per key — D-30 (A25, A22, A9) | +| S-38 | prose | `docs/SCENARIOS.md:1176` | async.ts×1 | — | [prose] An error is a boundary returning to its fallback; a write beside an old error — D-39 (§27 7b) | +| S-39 | prose | `docs/SCENARIOS.md:903` | lanes.ts×1 | latest-first-later-throw-4275.test.ts×1 | [prose] A later unread value still throws — D-28 | +| S-40 | prose | `docs/SCENARIOS.md:924` | — | first-load-superseded-answer-4569.test.ts×3 | [prose] A fresh `` that reads only `latest` shows it now — D-12, F-6 | +| S-41 | prose | `docs/SCENARIOS.md:1272` | — | — | [prose] Independent panels over one selection: the write shows now, each panel lags to its own landing — D-40 | +| S-42 | prose | `docs/SCENARIOS.md:1200` | — | error-is-an-answer.test.ts×2 | [prose] A held rejection reveals with its frame — D-41 (#3886) | +| S-43 | prose | `docs/SCENARIOS.md:1258` | — | error-is-an-answer.test.ts×2 | [prose] A held recovery keeps the shown error; recovering to the last good value is a change — D-41, D-39 (#3886) | +| S-44 | executable, ruled-not-built | `docs/SCENARIOS.md:1109` | — | optimistic-outside-reask-4690.test.ts×2 | [executable, ruled-not-built] An outside input change under a guess is held with the guess's hold — D-34, F-1 (#4336) | +| S-45 | executable, ruled-not-built | `docs/SCENARIOS.md:959` | — | — | [executable, ruled-not-built] A first answer landing in the same tick as an action that writes its input: `latest` shows it, the plain read waits — D-22, F-9 (E9) | +| S-46 | executable, standing | `docs/SCENARIOS.md:838` | verdict.ts×1 | display-ahead-plain-write-content.test.ts×2 | [executable, standing] The content a display-ahead `Show` opens reads the screen, whoever opened the hold — D-25 | +| S-47 | prose | `docs/SCENARIOS.md:1224` | dev.ts×1 | caught-reactive-read-diagnostic.test.ts×2 try-catch-reactive-read.test.ts×1 | [prose] `try/catch` around a reactive read: caught on one pass, bypassed on the next — D-42, F-12 | +| S-48 | executable, standing | `docs/SCENARIOS.md:497` | — | — | [executable, standing] `on={id()}`: the `on` pass reads the held value plainly, so the swap lands with the write's frame — D-17 (#3540) | +| S-49 | executable, ruled-not-built | `docs/SCENARIOS.md:523` | seat.ts×3 | — | [executable, ruled-not-built] A mixed `on`: a plain read of a held value makes the re-arm wait — D-17 | +| S-50 | executable, ruled-not-built | `docs/SCENARIOS.md:762` | seat.ts×4 | — | [executable, ruled-not-built] A memo that probed once, re-run by a sync write, holds the tick — D-25 | diff --git a/packages/signals/docs/SCENARIOS.md b/packages/signals/docs/SCENARIOS.md index 8949fa1dcb..db1bddef61 100644 --- a/packages/signals/docs/SCENARIOS.md +++ b/packages/signals/docs/SCENARIOS.md @@ -58,32 +58,39 @@ const panel = createMemo(() => (show() ? count() : "hidden")); | 3 | `details(1)` lands | `Count 1 \| Details 1 \| Panel 1` | all false | all | | ✗ | | after 2: `Count 0 \| Details 0 \| Panel 1` | | | -### S-3. A mount during a hold: a derivation born held holds the mount; the branch appears whole at the commit — D-2, D-3 (A29) + -_Pinned: `tests/born-held.test.ts`, for a direct mount (see the note under the table); the click-mounted branch below is not pinned yet._ +### S-3. A mount during a hold: a derivation born held holds the mount; the branch appears whole at the commit — D-1, D-3 (A29) + +_Generated from `packages/web/test/async-gen/rulings/S-3.ts` (`RULINGS_WRITE=1 pnpm exec vitest run test/async-gen/rulings` in `packages/web`); do not edit inside the markers. Status: standing. Pins also: `tests/born-held.test.ts`._ ```tsx const [x, setX] = createSignal(0); -const slow = createMemo(() => fetchSlow(x())); -

{x()}

{slow()} -const save = action(function* () { setX(1); yield gate; }); -// on a click, while save() is open: -const m = createMemo(() => x() * 10); -

{m()}

{x()} +const [open, setOpen] = createSignal(0); +const [title, setTitle] = createSignal(0); +const slow = createMemo(async () => fetch(1 + x())); +const ten = createMemo(() => 1 + x()); +{x()} +{slow()} +{title()} + {/* creates ten when it opens */} + {ten()} + {x()} + ``` -| # | Action | Screen | `latest` / `isPending` | Effects ran | -| --- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | --------------------------- | -| 0 | primed | `h1 0 \| i 0` | | h1, i | -| 1 | `save(); flush()` | — (held on `slow(1)`) | `latest(x)` 1 · `isPending(x)` true | — | -| 2 | mount `m`, `

`, ``; `flush()` | `h1 0 \| i 0` — `

{m()}

` throws, so it holds the mount; the branch is not attached | `untrack(m)` throws · `latest(m)` 10 · `isPending(m)` holds its reader (owned), false (unowned) | — | -| 3 | `setTitle("t"); flush()` — an unrelated mainline write | `… \| title t` at once (not swallowed into the action) | | title | -| 4 | `gate` releases, `slow(1)` lands, body returns | `h1 1 \| i 1 \| p 10 \| span 1` | `isPending(x)` false · `isPending(m)` false | h1, i, p, span (first runs) | -| ✗ | | after 2: `p 10` beside `h1 0` (the published future); `p 0` (a committed value `m` never had); or `p (empty) \| span 0` (the branch attached around an empty slot) | | | - -The `p 0` row is the N1 reading (reverted 2026-10-02). The `p 10` row beside `h1 0` is the published future. Both are rejected (ruled 2026-10-09, D-2). The empty slot is rejected the same day: a born-held read with no boundary to catch it holds the change that mounted it (F-2). Inside a fresh `` the read is caught instead and the branch attaches on its fallback (D-12). `latest(m)` is its pending value; `isPending(m)` has no verdict until the commit, because `m` has nothing committed to judge — inside an owner its reader is held as the read is, and runs at the commit (F-9, D-23). +| # | Action | Screen | Notes | +| --- | ------------------------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| 0 | slow(1) lands; tick | `x 0 \| slow 1 \| title 0` | primed | +| 1 | setX(1) in an action; flush() | — | held on `slow` | +| 2 | setOpen(1); flush() | — | `ten` is born held: its first pass read the held `x`; `

{ten()}

` throws, so it holds the mount and the branch is not attached | +| 3 | setTitle(1); flush() | `x 0 \| slow 1 \| title 1` | an unrelated mainline write shows at once — not swallowed into the action | +| 4 | the action lands; tick; slow(2) lands; tick | `x 1 \| slow 2 \| title 1 \| ten 2 \| x 1` | the commit reveals everything, the branch whole | +| ✗ | | after 2: `x 0 \| slow 1 \| title 0 \| ten 2 \| x 0` | `ten 2` beside `x 0`: the published future | +| ✗ | | after 2: `x 0 \| slow 1 \| title 0 \| ten 1 \| x 0` | `ten 1`: a committed value `ten` never had (the N1 reading) | +| ✗ | | after 2: `x 0 \| slow 1 \| title 0 \| x 0` | the branch attached around an empty slot | -Mounted directly instead of by a click-opened branch — the shape the pin covers — nothing gates the bindings: `{x()}` shows the committed `0` now and `1` at the commit (D-3), and only `

{m()}

` waits for the commit. Effects ran at row 2: span (`0`); at row 4: h1, i, p (first run), span (`1`). + ### S-4. One hole, two sources: an unrelated sync write passes through — D-3 (A15 shared hole) @@ -127,29 +134,57 @@ const mountB = () => createRoot(() =>

{createMemo(() => b())()}

); | 2′ | `slowA` lands | `pA 1`; `pB`'s root not attached | `isPending(a)` false · `isPending(b)` true | pA | | 3′ | `slowB` lands | `pA 1 \| pB 1` | | pB | -### S-6. A new async memo a render effect consumes waits with the hold — D-2, F-2 (#3800) + -_The value half is ruled 2026-10-09 and built in #3977 (U-1): the child does not show beside the parent still on the old value. The hold also waits for the child, because a render effect outside a boundary consumes it (F-2). Row E1 was F-8 and is rejected._ +### S-6. A new async memo a render effect consumes waits with the hold — D-1, F-2 (#3800) -```ts +_Generated from `packages/web/test/async-gen/rulings/S-6.ts` (`RULINGS_WRITE=1 pnpm exec vitest run test/async-gen/rulings` in `packages/web`); do not edit inside the markers. Status: standing. Pins also: `tests/issue-3800-repro.test.ts`._ + +**Case 1.** + +```tsx const [count, setCount] = createSignal(1); -const slow = createMemo(() => fetchSlow(count())); // ~1 s -

parent {count()} {slow()}

-setCount(2); flush(); // held on slow(2) -const fast = createMemo(() => Promise.resolve(count())); // created during the hold -

child {fast()}

+const [open, setOpen] = createSignal(0); +const slow = createMemo(async () => fetch(1 + count())); +const fast = createMemo(async () => fetch(1 + count())); +{count()} {slow()} + {/* creates fast when it opens */} + {fast()} + +``` + +| # | Action | Screen | Notes | +| --- | ----------------------------------------- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| 0 | slow(2) lands; tick | `count 1 \| slow 2` | primed | +| 1 | setCount(2); flush(); setOpen(1); flush() | — | `setCount(2)` held on `slow`; the child branch mounts: `fast` is born held and throws, so it holds its mount and is not attached | +| 2 | fast(3) lands; tick | — | `fast`'s answer lands while the hold is live: `slow` is still out, so the hold waits and the answer does not show | +| 3 | slow(3) lands; tick | `count 2 \| slow 3 \| fast 3` | `slow(2)` lands: parent and child together | +| ✗ | | after 2: `count 1 \| slow 2 \| fast 3` | the child beside the parent still on the old value | +| ✗ | | after 1: `count 1 \| slow 2` | `child (empty)` on screen | + +**Case ′.** + +```tsx +const [count, setCount] = createSignal(1); +const [open, setOpen] = createSignal(0); +const slow = createMemo(async () => fetch(1 + count())); +const fast = createMemo(async () => fetch(1 + count())); +{count()} {slow()} + {/* creates fast when it opens */} + {fast()} + ``` -| # | Action | Screen | `isPending` | Effects ran | -| --- | ---------------------------------------------------------- | ------------------------------------------------------------------------ | ----------------------- | ------------- | -| 0 | `setCount(2); flush()`, mount `child` | `parent 1 1` — `child` throws, so it holds its mount; it is not attached | `isPending(count)` true | — | -| 1 | `fast`'s promise resolves (the hold is live) | — (`slow` is still out, so the hold waits; the answer does not show) | — | — | -| 2 | `slow(2)` lands | `parent 2 2 \| child 2` | false | parent, child | -| E1 | variant — `fast` slower than `slow`: `slow(2)` lands first | — (`fast` is still out; the parent does not show yet) | | — | -| E1′ | then `fast` lands | `parent 2 2 \| child 2` | | parent, child | -| ✗ | | after 1: `parent 1 1 \| child 2`; at 0 or E1: `child (empty)` on screen | | | +| # | Action | Screen | Notes | +| --- | -------------------------------------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| 0 | slow(2) lands; tick | `count 1 \| slow 2` | | +| 1 | setCount(2); flush(); setOpen(1); flush(); slow(3) lands; tick | — | `slow(2)` lands first: `fast` is still out, so the parent does not show yet (the hold waits for the child a render effect consumes, F-2) | +| 2 | fast(3) lands; tick | `count 2 \| slow 3 \| fast 3` | | +| ✗ | | after 1: `count 2 \| slow 3` | the parent revealed around an empty child slot | -The sync twin (`createMemo(() => count())`) already waits (S-3); the asymmetry is the bug. +The sync twin (S-3's `ten`) already waits; the asymmetry was the bug. + + ### S-7. Root mount with uncaught async waits to attach; with a boundary it shows the fallback — D-6, D-12 @@ -431,20 +466,84 @@ _Pinned: `loading-on-frame-following-3540.test.ts` §5; `loading-on-latest-fallb | 1′ | the action ends _before_ `comments(2)` lands | `B \| spinner` with the commit, then `comments-B` | the race the fallback won | | 1″ | the fallback reads `latest(id)` | as 1–3: `A` held, then `B \| comments-B` | the fallback's `latest` does not pull the swap ahead (#4337) | + + ### S-21. A display-ahead read in `on` shows the fallback now, beside the held frame — D-17 -_Pinned: `loading-on-frame-following-3540.test.ts` §4; `loading-on-rearm-reveal-3540.test.ts`._ +_Generated from `packages/web/test/async-gen/rulings/S-21.ts` (`RULINGS_WRITE=1 pnpm exec vitest run test/async-gen/rulings` in `packages/web`); do not edit inside the markers. Status: standing. Pins also: `tests/loading-on-frame-following-3540.test.ts`, `tests/loading-on-rearm-reveal-3540.test.ts`._ ```tsx -

{product().name}

-
    {comments()}
+const [id, setId] = createSignal(1); +const product = createMemo(async () => fetch(1 + id())); +const comments = createMemo(async () => fetch(1 + id())); +{product()} + + {comments()} + +``` + +| # | Action | Screen | Notes | +| --- | ----------------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| 0 | product(2) lands; comments(2) lands; tick | `product 2 \| comments 2` | primed | +| 1 | setId(2); flush() | `product 2 \| [fallback]` | the new id's loading state inside the old page: `on` read `latest(id)`, whose frame has landed; the shell's read of `product` stays held | +| 2 | product(3) lands; tick | `product 3 \| [fallback]` | | +| 3 | comments(3) lands; tick | `product 3 \| comments 3` | content that reads nothing held reveals when it lands | +| ✗ | | after 1: `product 2 \| comments 2` | the swap travelled with the shell's frame | + + + + + +### S-48. `on={id()}`: the `on` pass reads the held value plainly, so the swap lands with the write's frame — D-17 (#3540) + +_Generated from `packages/web/test/async-gen/rulings/S-48.ts` (`RULINGS_WRITE=1 pnpm exec vitest run test/async-gen/rulings` in `packages/web`); do not edit inside the markers. Status: standing. Pins also: `tests/loading-on-frame-following-3540.test.ts`._ + +```tsx +const [id, setId] = createSignal(1); +const product = createMemo(async () => fetch(1 + id())); +const comments = createMemo(async () => fetch(1 + id())); +{product()} + + {comments()} + ``` -| # | Action | Screen | Notes | -| --- | ------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------- | -| 1 | `setId(2); flush()` | `A \| spinner` — the new id's loading state inside the old page | `on` read `latest(id)`, whose frame has landed; reads of `id()` stay held | -| 2 | `product(2)` lands | `B \| spinner` | | -| 3 | `comments(2)` lands | `B \| comments-B` — content that reads nothing held reveals when it lands | | +| # | Action | Screen | Notes | +| --- | ----------------------------------------- | ---------------------------------- | -------------------------------------------------------------------------------------- | +| 0 | product(2) lands; comments(2) lands; tick | `product 2 \| comments 2` | primed | +| 1 | setId(2); flush() | — | the `on` pass read the held `id`: it is the hold's work and its re-arm is held with it | +| 2 | product(3) lands; tick | `product 3 \| [fallback]` | `product(2)` lands: the frame commits and the swap lands with it | +| 3 | comments(3) lands; tick | `product 3 \| comments 3` | | +| ✗ | | after 1: `product 2 \| [fallback]` | the fallback ahead of the frame the plain read belongs to | + + + + + +### S-49. A mixed `on`: a plain read of a held value makes the re-arm wait — D-17 + +_Generated from `packages/web/test/async-gen/rulings/S-49.ts` (`RULINGS_WRITE=1 pnpm exec vitest run test/async-gen/rulings` in `packages/web`); do not edit inside the markers. Status: ruled-not-built._ + +```tsx +const [id, setId] = createSignal(1); +const [filter, setFilter] = createSignal(0); +const product = createMemo(async () => fetch(1 + id())); +const comments = createMemo(async () => fetch(1 + id())); +{product()} + + {comments()} + +``` + +| # | Action | Screen | Notes | +| --- | ---------------------------------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| 0 | product(2) lands; comments(2) lands; tick | `product 2 \| comments 2` | primed | +| 1 | action: setId(2), setFilter(1); flush() | — | `filter()` is a plain read of a held staging: the `on` pass is the hold's work and the re-arm waits, whatever `latest(id)` beside it read | +| 2 | the action lands; tick; product(3) lands; tick | `product 3 \| [fallback]` | the hold commits: the swap lands with the frame | +| 3 | comments(3) lands; tick | `product 3 \| comments 3` | | +| ✗ | | after 1: `product 2 \| [fallback]` | today's screen: one probe made the whole `on` pass a verdict reader | + + ### S-22. A fallback that itself reads pending data — D-18 @@ -503,62 +602,124 @@ const details = createMemo(() => fetchDetails(latest(id))); ## C. Display-ahead readers and verdicts (D-22–D-31) -### S-25. Nothing shows a write before its flush; then every channel at once — D-22, D-32 (A28) + -_Pinned: `tests/latest-held-till-flush.test.ts`, `tests/latest-repeated-writes.test.ts`._ +### S-25. Nothing shows a write before its flush; then every channel at once — D-22 (A28) -```ts +_Generated from `packages/web/test/async-gen/rulings/S-25.ts` (`RULINGS_WRITE=1 pnpm exec vitest run test/async-gen/rulings` in `packages/web`); do not edit inside the markers. Status: standing. Pins also: `tests/latest-held-till-flush.test.ts`, `tests/latest-repeated-writes.test.ts`._ + +**Case 1.** + +```tsx const [count, setCount] = createSignal(20); -const doubled = createMemo(() => count() * 2); +const doubled = createMemo(() => 1 + count()); +{count()} +{latest(count)} +{latest(doubled)} +{isPending(count)} ``` -| # | Action | `count()` (untracked) | `latest(count)` | `latest(doubled)` | `isPending(count)` | -| --- | ----------------------------------------- | --------------------------------------------------------- | --------------- | ----------------- | ------------------ | -| 1 | `setCount(30)` — no flush | 20 | 20 | 40 | false | -| 2 | `flush()` — nothing holds | 30 | 30 | 60 | false | -| 2′ | `flush()` — an async reader holds `count` | 20 | 30 | 60 | true | -| ✗ | | after 1: `latest(count)` 30 while `latest(doubled)` is 40 | | | | +| # | Action | Screen | Notes | +| --- | ------------ | --------------------------------------------------------------------------------------- | ------------------------------------------------------ | +| 0 | render | `count 20 \| latest(count) 20 \| latest(doubled) 21 \| isPending(count) false` | | +| 1 | setCount(30) | — | `setCount(30)` with no flush: nothing shows it | +| 2 | flush() | `count 30 \| latest(count) 30 \| latest(doubled) 31 \| isPending(count) false` | nothing holds: every channel at once | +| ✗ | | after 1: `count 20 \| latest(count) 30 \| latest(doubled) 21 \| isPending(count) false` | `latest(count)` 30 while `latest(doubled)` is still 21 | + +**Case ′.** + +```tsx +const [count, setCount] = createSignal(20); +const doubled = createMemo(() => 1 + count()); +const slow = createMemo(async () => fetch(1 + count())); +{count()} +{latest(count)} +{latest(doubled)} +{isPending(count)} +{slow()} +``` + +| # | Action | Screen | Notes | +| --- | --------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| 0 | slow(21) lands; tick | `count 20 \| latest(count) 20 \| latest(doubled) 21 \| isPending(count) false \| slow 21` | primed | +| 1 | setCount(30); flush() | `count 20 \| latest(count) 30 \| latest(doubled) 31 \| isPending(count) true \| slow 21` | an asyncMemo reader holds `count`: the plain read keeps 20, `latest` shows 30, `isPending` true | +| 2 | slow(31) lands; tick | `count 30 \| latest(count) 30 \| latest(doubled) 31 \| isPending(count) false \| slow 31` | | + + + + ### S-26. `isPending` is about a new question — D-23 (A19, A24) -_Pinned: `tests/question-scoped-pending.test.ts`, `tests/loading-value.test.ts`._ +_Generated from `packages/web/test/async-gen/rulings/S-26.ts` (`RULINGS_WRITE=1 pnpm exec vitest run test/async-gen/rulings` in `packages/web`); do not edit inside the markers. Status: standing. Pins also: `tests/question-scoped-pending.test.ts`, `tests/loading-value.test.ts`._ ```tsx const [id, setId] = createSignal(1); -const user = createMemo(() => fetchUser(id())); - -

{user().name}

{isPending(user) ? "updating" : ""}

+const user = createMemo(async () => fetch(1 + id())); + + + {user()} {isPending(user)} + ; ``` -| # | Action | Screen | `isPending(user)` | Why | -| --- | ------------------------------------------------------------------ | -------------------------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------ | -| 0 | first load | `[fallback]` (the probe's own read suspends; nothing observable yet) | no verdict — throws here (owned), `false` unowned | a first load is loading, not pending (D-24) | -| 1 | `user(1)` lands | `Ann \| ` | false | final | -| 2 | `setId(2); flush()` | `Ann \| updating` | true | an input changed — a new question | -| 3 | `user(2)` lands | `Bob \| ` | false | | -| 4 | `refresh(user); flush()` | `Bob \| ` | false | a re-ask of the same question is quiet; the fresh value reveals silently | -| 5 | `action(function* () { affects(user); refresh(user); yield … })()` | `Bob \| updating` | true | a declared reload | -| 6 | the action's refetch lands and it ends | `Bob′ \| ` | false | | -| 7 | an optimistic write to a `createOptimistic` over `user` | shows the guess | false | verdict-inert | +| # | Action | Screen | Notes | +| --- | ---------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------- | +| 0 | render | `[fallback]` | first load: the probe's own read suspends; nothing observable yet — no verdict (D-24) | +| 1 | user(2) lands; tick | `user 2 \| isPending(user) false` | final | +| 2 | setId(2); flush() | `user 2 \| isPending(user) true` | an input changed — a new question | +| 3 | user(3) lands; tick | `user 3 \| isPending(user) false` | | +| 4 | refresh(user); flush() | — | `refresh(user)`: a re-ask of the same question is quiet | +| 5 | user(3) lands; tick | — | the fresh value reveals silently | +| ✗ | | after 4: `user 3 \| isPending(user) true` | a re-ask reported as a new question | -### S-27. `[isPending(x), x()]` is atomic; the `latest` form follows x's own async only — D-23 (A10, A8) +The prose row's `affects()` (rows 5–6) and the optimistic write over `user` (row 7) are not migrated yet. -_Pinned: `tests/latest-isPending-consistency.test.ts`, `tests/createMemo.test.ts`._ + -```ts + + +### S-27. `[isPending(x), x()]` is atomic; the `latest` form follows x's own async only — D-23, D-25 (A10, A8) + +_Generated from `packages/web/test/async-gen/rulings/S-27.ts` (`RULINGS_WRITE=1 pnpm exec vitest run test/async-gen/rulings` in `packages/web`); do not edit inside the markers. Status: standing. Pins also: `tests/latest-isPending-consistency.test.ts`, `tests/createMemo.test.ts`._ + +**Case 1.** + +```tsx +const [n, setN] = createSignal(0); +const x = createMemo(async () => fetch(1 + n())); +const y = createMemo(async () => fetch(1 + x())); + {/* one effect */} + latest(x))} data-a1={latest(x)}> {/* one effect */} +{y()} +``` + +| # | Action | Screen | Notes | +| --- | ---------------------------------- | --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| 0 | x(1) lands; tick; y(2) lands; tick | `isPending(x) false \| x 1 \| isPending(() => latest(x)) false \| latest(x) 1 \| y 2` | primed | +| 1 | setN(1); flush() | `isPending(x) true \| x 1 \| isPending(() => latest(x)) true \| latest(x) 1 \| y 2` | both channels: x's own fetch is up | +| 2 | x(2) lands; tick | `isPending(x) true \| x 1 \| isPending(() => latest(x)) false \| latest(x) 2 \| y 2` | `x` landed, `y` still up: the plain channel is a landed answer awaiting reveal; `latest` saw x's own answer — verdicts are per channel | +| 3 | y(3) lands; tick | `isPending(x) false \| x 2 \| isPending(() => latest(x)) false \| latest(x) 2 \| y 3` | | +| ✗ | | after 2: `isPending(x) true \| x 2 \| isPending(() => latest(x)) false \| latest(x) 2 \| y 2` | `[true, x1]`: pending beside the fresh value | + +**Case memo.** + +```tsx const [n, setN] = createSignal(0); -const x = createMemo(() => fetchX(n())); // async -const y = createMemo(() => fetchY(x())); // async downstream, rendered +const x = createMemo(() => 1 + n() + isPending(n)); +{x()}; ``` -| # | Action | `[isPending(x), x()]` in one scope | `[isPending(() => latest(x)), latest(x)]` | Notes | -| --- | ------------------------------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------- | -| 1 | `setN(1); flush()` | `[true, x0]` | `[true, x0]` | both channels: x's own fetch is up | -| 2 | `x(1)` lands; `y(1)` still up — the frame is held | `[true, x0]` — a landed answer awaiting reveal | `[false, x1]` — `latest` saw x's own answer; its verdict is per channel | never `[true, x1]` in either order, on either channel | -| 3 | `y(1)` lands | `[false, x1]` | `[false, x1]` | | +| # | Action | Screen | Notes | +| --- | ----------------------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 0 | render | `x 1` | | +| 1 | setN(5) in an action; flush() | `x 2` | `m = 1 + x + isPending(x)`: the memo probes x and reads it plainly in one pass — the plain read is the screen's 0 and the probe is true, shown now (A10, D-25); the memo is not the hold's work | +| 2 | the action lands; tick | `x 6` | | +| ✗ | | after 1: `x 1` | the memo held with the write it probed: the spinner would show only at the commit | + +On a plain signal or sync memo the `latest` form is never pending. The memo case is the pass-level half of D-25: a plain read of a node the pass also probed is the screen's. -On a plain signal or sync memo the `latest` form is never pending. + ### S-28. `isPending` never throws unowned; `latest` throws uninitialized everywhere — D-24 (A16, A7) @@ -571,25 +732,56 @@ _Pinned: `tests/spec-async-semantics.test.ts` (A16), `tests/uninitialized-visibi | `isPending(() => { throw new Error() })`, any scope | `false` | n/a | | `isPending(() => store)` — the thunk returns a store, reads nothing | `false` (reads-only; the return value is never inspected) | | -### S-29. A verdict reader's own async is held — D-25 (A31, #3442) + -_Pinned: `tests/ispending-combined-atomic-3442.test.ts`._ +### S-29. A display-ahead reader's own async is held — D-25 (A31, #3442) + +_Generated from `packages/web/test/async-gen/rulings/S-29.ts` (`RULINGS_WRITE=1 pnpm exec vitest run test/async-gen/rulings` in `packages/web`); do not edit inside the markers. Status: standing. Pins also: `tests/ispending-combined-atomic-3442.test.ts`._ ```tsx -const fast = createMemo(() => fetchFast(n())); -const slow = createMemo(() => fetchSlow(n())); -const copy = createMemo(() => slow()); // a sync wrapper -

- Fast {fast()} | Slow {copy()} | Pending {String(isPending(() => [fast(), copy()]))} -

; +const [n, setN] = createSignal(0); +const fast = createMemo(async () => fetch(1 + n())); +const slow = createMemo(async () => fetch(1 + n())); +const copy = createMemo(() => 1 + slow()); +const both = createMemo(() => 1 + fast() + copy()); + {/* one effect */} +``` + +| # | Action | Screen | Notes | +| --- | ---------------------------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------- | +| 0 | fast(1) lands; slow(1) lands; tick | `fast 1 \| copy 2 \| isPending(both) false` | primed | +| 1 | setN(1); flush() | `fast 1 \| copy 2 \| isPending(both) true` | | +| 2 | fast(2) lands; tick | — | `fast` landed; `copy` computes under its own posture: still pending on `slow` | +| 3 | slow(2) lands; tick | `fast 2 \| copy 3 \| isPending(both) false` | | +| ✗ | | after 2: `fast 2 \| copy 2 \| isPending(both) false` | `fast` revealed alone with the probe false | + + + + + +### S-50. A memo that probed once, re-run by a sync write, holds the tick — D-25 + +_Generated from `packages/web/test/async-gen/rulings/S-50.ts` (`RULINGS_WRITE=1 pnpm exec vitest run test/async-gen/rulings` in `packages/web`); do not edit inside the markers. Status: ruled-not-built._ + +```tsx +const [x, setX] = createSignal(0); +const [k, setK] = createSignal(0); +const [probe, setProbe] = createSignal(1); +const m = createMemo(() => 1 + k() + (probe() ? isPending(x) : 0) + x()); + + {m()} {k()} +; ``` -| # | Action | Screen | Notes | -| --- | ------------------ | -------------------------------------------- | -------------------------------------------------------------- | -| 1 | `setN(1); flush()` | `Fast 0 \| Slow 0 \| Pending true` | | -| 2 | `fast(1)` lands | — | `copy` computes under its own posture: still pending on `slow` | -| 3 | `slow(1)` lands | `Fast 1 \| Slow 1 \| Pending false` | | -| ✗ | | after 2: `Fast 1 \| Slow 0 \| Pending false` | | +| # | Action | Screen | Notes | +| --- | ----------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| 0 | render | `m 1 \| k 0` | | +| 1 | setX(1) in an action; flush() | `m 2 \| k 0` | the probe shows busy now; the plain read of `x` is the screen's 0 | +| 2 | setProbe(0); setK(1); flush() | — | this run probes nothing: its read of `x` is a plain read of the held staging, so `m` is the hold's work and the tick that wrote `k` holds with it | +| 3 | the action lands; tick | `m 3 \| k 1` | | +| ✗ | | after 2: `m 2 \| k 1` | today's screen (published twice): the memo read `x` as the screen's because its last run probed, and `k` broke free | + + ### S-30. A `latest()` reader mounted mid-hold shows the committed value and reveals with the lane — D-26 (#3460) @@ -616,50 +808,97 @@ const bump = action(function* () { | 2′ | a sibling sync write re-runs a reader of `latest(v)` mid-hold | publishes at once with the committed `0` | "we wouldn't hold a sync write on a transition. Lanes are the same" | | ✗ | | after 2: `Late 1` beside `Details 0` | | -### S-31. A verdict reader's mount is mainline: `Show when={latest(x) > 0}` → `Loading` shows its fallback — D-27 (#3851) + -_Pinned: `tests/verdict-mount-first-pass-3851.test.ts`._ +### S-31. A verdict reader's mount is mainline: `Show when={latest(x)}` → `Loading` shows its fallback — D-1, D-25 (#3851) + +_Generated from `packages/web/test/async-gen/rulings/S-31.ts` (`RULINGS_WRITE=1 pnpm exec vitest run test/async-gen/rulings` in `packages/web`); do not edit inside the markers. Status: standing. Pins also: `tests/verdict-mount-first-pass-3851.test.ts`._ ```tsx const [x, setX] = createSignal(0); -

{x()}

- 0}> - content {x()} +{x()} + + + {x()} + -const save = action(function* () { setX(1); yield gate; }); ``` -| # | Action | Screen | Notes | -| --- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | -| 0 | primed | `0 \| (closed)` | | -| 1 | `save(); flush()` | `0 \| [fallback]` — the `Show` opens ahead (display-ahead); the boundary is a mainline mount whose content read the write the action holds → born held → fallback | | -| 2 | `gate`, body returns | `1 \| content 1` | | -| ✗ | | after 1: `0 \| content 1` (the torn frame #3869 fixed) | | +| # | Action | Screen | Notes | +| --- | ----------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| 0 | render | `x 0` | the `Show` is closed | +| 1 | setX(1) in an action; flush() | `x 0 \| [fallback]` | the `Show` opens ahead (display-ahead); the boundary is a mainline mount whose content read the write the action holds: born held, so the fallback | +| 2 | the action lands; tick | `x 1 \| x 1` | | +| ✗ | | after 1: `x 0 \| x 1` | the torn frame #3869 fixed: content 1 beside x = 0 | + + + + + +### S-46. The content a display-ahead `Show` opens reads the screen, whoever opened the hold — D-25 + +_Generated from `packages/web/test/async-gen/rulings/S-46.ts` (`RULINGS_WRITE=1 pnpm exec vitest run test/async-gen/rulings` in `packages/web`); do not edit inside the markers. Status: standing. Pins also: `tests/display-ahead-plain-write-content.test.ts`, `web/test/display-ahead-plain-write-content-s46.spec.tsx`._ -### S-46. The content a display-ahead `Show` opens reads the screen, whoever opened the hold — D-27 +**Case 1.** -_Ruled 2026-10-30 ("actions aren't special — they are a way to extend the hold"), reaffirmed 2026-10-31 (A over C: C would re-rule S-31). Pinned: `tests/display-ahead-plain-write-content.test.ts` (signals, passes) and `web/test/display-ahead-plain-write-content-s46.spec.tsx` (through `Show` + `insert`; the plain row is `test.fails` — today the branch attaches with its text hole empty, `selected: ` and nothing, F-2's empty slot and #4417's mechanism). S-31's rule in the plain-write, no-boundary form: a write to `id` is held because an async memo over it is on screen; no action, no `Loading`. Parked 2026-10-31: a fix was attempted and dropped (infinite loop on a generated case, four new findings — D-27 Status); no in-budget mechanism known, #4417 open. Fixed 2026-11-02 (#4417, #4574): a park re-derive that gives back the staging it superseded keeps its held readers and their children (one key, one child), and an insert of a DOM node waits while its subtree holds a child born into the open hold._ +```tsx +const [id, setId] = createSignal(0); +const detail = createMemo(async () => fetch(1 + id())); +{id()} +{detail()} + + {id()} + +``` + +| # | Action | Screen | Notes | +| --- | --------------------- | ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 0 | detail(1) lands; tick | `id 0 \| detail 1 \| [none]` | primed | +| 1 | setId(1); flush() | — | the `Show` opens ahead (`latest` asked), but the branch is a mount whose first pass read the staging plainly: born held (D-27), it does not attach until the landing; the fallback stays | +| 2 | detail(2) lands; tick | `id 1 \| detail 2 \| id 1` | | +| ✗ | | after 1: `id 0 \| detail 1 \| id 1` | two plain reads of one signal disagreeing on one screen | +| ✗ | | after 1: `id 0 \| detail 1 \| id 0` | the branch attached with the committed value; a mount under a display-ahead reader is born held, not a D-3 leaf | +| ✗ | | after 1: `id 0 \| detail 1` | the branch attached with its hole empty (F-2: never an empty slot) | + +**Case ′.** ```tsx const [id, setId] = createSignal(0); -const detail = createMemo(() => fetchDetail(id())); -

{id()}

{detail()}

- 0} fallback="none"> -

selected: {id()}

{/* a plain read, under the display-ahead Show */} +const detail = createMemo(async () => fetch(1 + id())); +{id()} +{detail()} + + {latest(id)} -// primed: detail answered for id = 0 ``` -| # | Action | Screen | Notes | -| --- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- | -| 0 | primed | `0 \| none \| d0` | | -| 1 | `setId(1); flush()` — `fetchDetail(1)` in flight | `0 \| none \| d0` — the `Show` opens ahead (`latest` asked), but the branch is a mount whose first pass read the staging plainly: born held (D-27), it does not attach until the landing; the fallback stays | a plain `id()` means one thing on the whole screen — the `0` outside | -| 1′ | same, content `selected: {latest(id)}` | `0 \| selected: 1 \| d0` | the read that asks sees ahead, at its own site | -| 1″ | same, content inside a `` | `0 \| [fallback] \| d0` | S-31's form: the boundary catches the born-held read | -| 2 | `fetchDetail(1)` lands | `1 \| selected: 1 \| d1` | | -| ✗ | | after 1: `0 \| selected: 1 \| d0` — two plain reads of one signal disagreeing on one screen | | -| ✗ | | after 1: `0 \| selected: 0 \| d0` — the branch attached with the committed value; a mount under a display-ahead reader is born held, not a D-3 leaf | | -| ✗ | | after 1: `0 \| selected: (empty) \| d0` — the branch attached with its hole empty (F-2: never an empty slot). Today's screen through `Show` + `insert` | | +| # | Action | Screen | Notes | +| --- | --------------------- | ---------------------------------- | -------------------------------------------------------------------- | +| 0 | detail(1) lands; tick | `id 0 \| detail 1 \| [none]` | | +| 1 | setId(1); flush() | `id 0 \| detail 1 \| latest(id) 1` | content `latest(id)`: the read that asks sees ahead, at its own site | +| 2 | detail(2) lands; tick | `id 1 \| detail 2 \| latest(id) 1` | | + +**Case ″.** + +```tsx +const [id, setId] = createSignal(0); +const detail = createMemo(async () => fetch(1 + id())); +{id()} +{detail()} + + + {id()} + + +``` + +| # | Action | Screen | Notes | +| --- | --------------------- | -------------------------------- | ---------------------------------------------------- | +| 0 | detail(1) lands; tick | `id 0 \| detail 1 \| [none]` | | +| 1 | setId(1); flush() | `id 0 \| detail 1 \| [fallback]` | S-31's form: the boundary catches the born-held read | +| 2 | detail(2) lands; tick | `id 1 \| detail 2 \| id 1` | | + + ### S-39. A later unread value still throws — D-28 @@ -715,24 +954,67 @@ _Ruled 2026-10-09, from the read-order brief's sibling case (416). Not D-28: thi Pinned: `tests/first-load-superseded-answer-4569.test.ts` (also a 3-deep chain and the no-boundary root, which attaches at 3 with `pending`), `packages/web/test/first-load-superseded-answer-4569.spec.tsx`. -### S-45. A first answer landing in the same tick as an action that writes its input: `latest` shows it, the plain read waits — D-22, F-9 (async generator E9) + + +### S-45. A first answer landing in the same tick as an action that writes its input: `latest` shows it, the plain read waits — D-22, F-9 (E9) -_Ruled 2026-10-28. The leaf form and the through-a-memo form show the same screen; today the leaf keeps the fallback up until the action ends — that is the ✗ row._ +_Generated from `packages/web/test/async-gen/rulings/S-45.ts` (`RULINGS_WRITE=1 pnpm exec vitest run test/async-gen/rulings` in `packages/web`); do not edit inside the markers. Status: ruled-not-built._ + +**Case leaf.** + +```tsx +const [s0, setS0] = createSignal(0); +const m0 = createMemo(async () => fetch(1 + s0())); +{s0()} + + {latest(m0)} + +``` + +| # | Action | Screen | Notes | +| --- | ----------------------------------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| 0 | render | `s0 0 \| [fallback]` | first load | +| 1 | m0(1) lands; setS0(1) in an action; microtask; microtask; flush() | `s0 0 \| latest(m0) 1` | the answer is the action's (D-4) and `latest` returns it (D-22): the committed `s0 = 0` beside its own answer, one frame | +| 2 | the action lands; tick; m0(2) lands; tick | `s0 1 \| latest(m0) 2` | | +| ✗ | | after 1: `s0 0 \| [fallback]` | the leaf waited on the action it did not read; a `Loading` reading only `latest` shows what it has (S-40) | + +**Case memo.** ```tsx -const m0 = createMemo(() => fetch(s0())); // first load, no committed value -

{s0()}

-{latest(m0)} // or: {L()} with L = createMemo(() => latest(m0)) -const save = action(function* () { setS0(1); yield gate; }); +const [s0, setS0] = createSignal(0); +const m0 = createMemo(async () => fetch(1 + s0())); +const L = createMemo(() => 1 + latest(m0)); +{s0()} + + {L()} + ``` -| # | Action | Screen | Notes | -| --- | ------------------------------------------------------------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------ | -| 0 | mount | `0 \| loading` | first load | -| 1 | `save()` and `m0`'s first answer `2` land in one tick; flush | `0 \| 2` | the answer is the action's (D-4) and `latest` returns it (D-22): the committed `s0 = 0` beside its own answer, one frame | -| 2 | `gate`, body returns; `m0(1)` lands | `1 \| m0(1)` | | -| ✗ | | after 1: `0 \| loading` | the leaf waited on the action it did not read; a `Loading` reading only `latest` shows what it has (S-40) | -| — | the same with `{m0()}` in place of `latest(m0)` | `0 \| loading` until 2 | a plain read of a first load is loading (F-9); the fallback is right there | +| # | Action | Screen | Notes | +| --- | ----------------------------------------------------------------- | ------------------------------------ | --------------------------------------------------- | +| 0 | render | `s0 0 \| [fallback]` | first load | +| 1 | m0(1) lands; setS0(1) in an action; microtask; microtask; flush() | `s0 0 \| L 2` | the same screen through a memo `L = latest(m0) + 1` | +| 2 | the action lands; tick; m0(2) lands; tick | `s0 1 \| L 3` | | +| ✗ | | today, after 1: `s0 0 \| [fallback]` | not yet built — the frames test is `it.fails` | + +**Case plain.** + +```tsx +const [s0, setS0] = createSignal(0); +const m0 = createMemo(async () => fetch(1 + s0())); +{s0()} + + {m0()} + +``` + +| # | Action | Screen | Notes | +| --- | ----------------------------------------------------------------- | -------------------- | -------------------------------------------------------------------------- | +| 0 | render | `s0 0 \| [fallback]` | | +| 1 | m0(1) lands; setS0(1) in an action; microtask; microtask; flush() | — | a plain read of a first load is loading (F-9); the fallback is right there | +| 2 | the action lands; tick; m0(2) lands; tick | `s0 1 \| m0 2` | | + + --- @@ -766,28 +1048,33 @@ const rename = action(function* (n) { | 1″ | ambient: `setDraft("Zed")` with no action and nothing held; `flush()` | `Ann \| unsaved` — nothing applied, the effect never sees `Zed` | | a guess outside a hold does not exist. The one-flush flash (`Ann → Zed → Ann`) was the earlier feasible width | | ✗ | | after 2: `Ann \| unsaved` (guess held) or `Bob \| saved` (held write leaked) | | | -### S-33. A different truth corrects the graph now and the screen at the commit; a never-shown guess is void — D-34 (#3331, fuzzer finding F6 vs A18 (c)) + + +### S-33. A different truth corrects the graph now and the screen at the commit; a never-shown guess is void — D-34 (#3331) -_Pinned: `tests/spec-async-semantics.test.ts` "#3331", re-pinned 2026-10-05._ +_Generated from `packages/web/test/async-gen/rulings/S-33.ts` (`RULINGS_WRITE=1 pnpm exec vitest run test/async-gen/rulings` in `packages/web`); do not edit inside the markers. Status: standing. Pins also: `tests/spec-async-semantics.test.ts`._ ```tsx const [value, setValue] = createSignal(0); -const [double, setDouble] = createOptimistic(() => fetchDouble(value())); // truth = v * 2 -const related = createMemo(() => fetchRelated(double())); // downstream async - - double={double()} async={related()} -; -// click: setValue(1); setDouble(3) (the truth will be 2) +const [double, setDouble] = createOptimistic(async () => fetch(1 + value())); +const related = createMemo(async () => fetch(1 + double())); + + {double()} {related()} + +{latest(double)} +{isPending(double)} ``` -| # | Action | Screen | `double()` untracked | `latest(double)` | `isPending(double)` | -| --- | ------------------------------------ | --------------------------------------------------------------------------------------------------- | ------------------------------------------------- | ---------------- | ------------------- | -| 0 | primed | `double=0 async=0` | 0 | 0 | false | -| 1 | `setValue(1); setDouble(3); flush()` | — (the guess's lane holds itself on `related(3)`) | 3 | 3 | true | -| 2 | source lands `2` ≠ 3 | — ; `related` re-asks from `2` now | **0** — the guess never showed; what showed stays | 2 | true | -| 3 | the obsolete `related(3)` lands | — (inert) | 0 | 2 | true | -| 4 | `related(2)` lands | `double=2 async=2` | 2 | 2 | false | -| ✗ | | after 2: untracked `3` (a dead, invisible guess — the pre-#3811 pins); any frame showing `double=3` | | | | +| # | Action | Screen | Notes | +| --- | --------------------------------------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- | +| 0 | double(1) lands; tick; related(2) lands; tick | `double 1 \| related 2 \| latest(double) 1 \| isPending(double) false` | primed | +| 1 | setValue(1); setDouble(3) (a guess); flush() | `double 1 \| related 2 \| latest(double) 3 \| isPending(double) true` | the guess's lane holds itself on `related(3)`; `latest` shows the guess | +| 2 | double(2) lands; tick | `double 1 \| related 2 \| latest(double) 2 \| isPending(double) true` | the source lands 2 ≠ 3: `related` re-asks from 2 now; the guess never showed, so what showed stays | +| 3 | related(4) lands; tick | — | the obsolete `related(3)` lands: inert | +| 4 | related(3) lands; tick | `double 2 \| related 3 \| latest(double) 2 \| isPending(double) false` | | +| ✗ | | after 2: `double 3 \| related 4 \| latest(double) 2 \| isPending(double) true` | a frame showing `double 3`: a dead, invisible guess | + + ### S-34. Provenance: an older action's late answer never corrects a newer guess — D-35 (Q-D) @@ -817,37 +1104,36 @@ _Pinned: `tests/optimistic-lane-release.test.ts`, `tests/body-end-supersession-v | 2 | the body returns | `5` (display unchanged until the commit) | `latest(guess)` = committed truth · `isPending(guess)` true iff it differs | the correction starts now, not after an obsolete derived flight | | 3 | the hold commits | the truth | false | | -### S-44. An outside input change under a guess is held with the guess's action — D-34, F-1 (#4336) + + +### S-44. An outside input change under a guess is held with the guess's hold — D-34, F-1 (#4336) -_Pinned: `tests/optimistic-question-change-4336.test.ts`, `tests/optimistic-layered-filter-4336.test.ts`, `tests/optimistic-held-write-leak.test.ts`, `tests/loading-on-latest-fallback-4337.test.ts`._ +_Generated from `packages/web/test/async-gen/rulings/S-44.ts` (`RULINGS_WRITE=1 pnpm exec vitest run test/async-gen/rulings` in `packages/web`); do not edit inside the markers. Status: ruled-not-built. Pins also: `tests/optimistic-question-change-4336.test.ts`, `tests/optimistic-layered-filter-4336.test.ts`._ ```tsx const [sel, setSel] = createSignal(3); -const [comments, setComments] = createOptimistic(() => fetchComments(sel())); // or createOptimisticStore -

- #{sel()}: {comments().join(" | ")} -

; -const post = action(function* (text) { - setComments(c => [...c, `${text} (sending)`]); - yield api.post(text); - refresh(comments); -}); +const [comments, setComments] = createOptimistic(async () => fetch(1 + sel())); +{sel()} {comments()} +{latest(comments)} +{isPending(comments)} ``` -| # | Action | Screen | `latest` / `isPending` | Notes | -| --- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------ | ----------------------------------------------------------- | -| 0 | primed; `post("mine")` | `#3: dana \| mine (sending)` | `isPending(comments)` false (verdict-inert) | | -| 1 | outside: `setSel(1)`; `flush()` | `#3: dana \| mine (sending)` — the change is held with the post | `latest(sel)` 1 · `isPending(comments)` true | a new question, not the guess's source answering | -| 2 | issue 1's answer `[]` lands | `#3: dana \| mine (sending)` | `latest(comments)` `[]` · true · untracked: the screen | | -| 3 | the post settles; its `refresh` re-asks 1 | `#3: dana \| mine (sending)` | true | the action's refresh after the change stays pending (F-9) | -| 4 | issue 1's answer `["mine"]` lands | `#1: mine` — one frame | false | | -| 1′ | `refresh(comments)` instead of `setSel(1)` | `#3: dana \| mine (sending)` until the post commits | `isPending(comments)` false | the same question: nothing new is held (#4329) | -| 1″ | `sel` a `createOptimistic` over the saved selection; `action { setSel(1); yield; setSaved(1) }` | `#3: dana \| mine (sending)` until issue 1's answer, then `#1: ` — the input's lane waits for its own answer (F-6) | | the post's guess does not answer issue 1 | -| 1⁗ | as 1″, but the action ends without `setSaved` (a pure revert) | `#1: ` until issue 3's re-ask lands, then `#3: dana \| mine (sending)` — one frame; the post lands later as usual | | the displaced guess comes back with the revert (D-33) | -| 1‴ | keyed: `comments[id] = rows`, the post guessed on `comments[3]`; `setSel(1)` | `#1: ` once issue 1's answer lands, not waiting for the post; back on 3: `#3: dana \| mine (sending)` | | a key with no guess is an ordinary pending read (F-2) | -| ✗ | | any `#1:` frame before 4, e.g. `#1: dana \| mine (sending)` | | the same with `#{sel()}` and the list in two render effects | - -Layered: action A guesses `milk`, action B sets a filter optimistically over it. The filter shows at once in both settle orders, `milk` stays until A commits, and when B settles first nothing on screen changes (D-33, D-35). A held write read before the action's first lane read (`setG("b"); yield go; setOther(1)`) shows only at the commit. Boundary (#4337): a keyed list under `` whose fallback reads `latest(filter)`, an action that guesses a row and refreshes, and `filter` set outside: the change is held with the action (row 3's guess is still read) and the boundary never leaves its content for it — no `loading` frame beside the held `filter`, toggled back or not; without the hold the fallback shows with the change and clears at its answer (S-20, D-17). Pinned by `tests/loading-on-latest-fallback-4337.test.ts`. +| # | Action | Screen | Notes | +| --- | -------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | +| 0 | comments(4) lands; tick | `sel 3 \| comments 4 \| latest(comments) 4 \| isPending(comments) false` | primed | +| 1 | action: setComments(9); refresh(comments); flush() | `sel 3 \| comments 9 \| latest(comments) 9 \| isPending(comments) false` | the guess shows; verdict-inert | +| 2 | setSel(1); flush() | `sel 3 \| comments 9 \| latest(comments) 9 \| isPending(comments) true` | a new question, not the guess's source answering: the change is held with the post | +| 3 | comments(2) lands; tick | — | issue 1's answer lands: still held; `latest(comments)` is the displayed guess (Decision 3) | +| 4 | the action lands; tick | — | the post settles; its refresh re-asks issue 1 | +| 5 | comments(2) lands; tick | `sel 1 \| comments 2 \| latest(comments) 2 \| isPending(comments) false` | one frame | +| ✗ | | after 3: `sel 1 \| comments 2 \| latest(comments) 2 \| isPending(comments) false` | a `sel 1` frame before the post settles | +| ✗ | | today, after 2: `sel 3 \| comments 9 \| latest(comments) 9 \| isPending(comments) false` | not yet built — the frames test is `it.fails` | +| ✗ | | today, after 3: `sel 3 \| comments 9 \| latest(comments) 2 \| isPending(comments) true` | not yet built — the frames test is `it.fails` | +| ✗ | | today, after 4: `sel 3 \| comments 9 \| latest(comments) 2 \| isPending(comments) true` | not yet built — the frames test is `it.fails` | + +The prose row's 1″/1⁗ (an optimistic `sel`), 1‴ (a keyed store) and the layered two-action case are not migrated yet. + + --- diff --git a/packages/signals/scripts/rules-index.mjs b/packages/signals/scripts/rules-index.mjs index d2ae6052d5..518d5c22b8 100644 --- a/packages/signals/scripts/rules-index.mjs +++ b/packages/signals/scripts/rules-index.mjs @@ -141,6 +141,23 @@ 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]) ); +// Derived rules, fundamentals and scenarios (hyphenated ids). A scenario's +// status comes from its generated block's "Status:" when it has one +// (packages/web/test/async-gen/rulings), else it is prose. +const DERIVED = path.join(DOCS, "RULES-DERIVED.md"); +scan(DERIVED, /^### (D-\d+)\. (.*)$/, (m, n) => add(m[1], m[1], "D", "", DERIVED, n, m[2])); +const FUND = path.join(DOCS, "RULES-FUNDAMENTALS.md"); +scan(FUND, /^## (F-\d+)\. (.*)$/, (m, n) => add(m[1], m[1], "F", "", FUND, n, m[2])); +const SCEN = path.join(DOCS, "SCENARIOS.md"); +{ + const L = read(SCEN).split("\n"); + for (let i = 0; i < L.length; i++) { + const m = /^### (S-\d+)\. (.*)$/.exec(L[i]); + if (!m) continue; + const st = /Status: (standing|ruled-not-built)/.exec(L[i + 2] ?? ""); + add(m[1], m[1], "S", "", SCEN, i + 1, `[${st ? "executable, " + st[1] : "prose"}] ${m[2]}`); + } +} // ── citations ────────────────────────────────────────────────────────────── const MODULE_NS = [ @@ -158,7 +175,7 @@ function nsFor(file, qualifier) { 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; + /\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]?)|\b([DFS]-\d{1,2})\b(?!\d)/g; function citationsIn(file) { const out = []; const c = read(file); @@ -170,6 +187,7 @@ function citationsIn(file) { // 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]); + else if (m[5]) out.push(m[5]); } return out; } @@ -206,13 +224,39 @@ if (process.argv.includes("--check")) { console.error("rules-index: live A-rules with no test citing them:", unpinned.join(" ")); process.exit(1); } + // Executable rulings: every generated block in SCENARIOS.md has its ruling + // file, and every ruling file has its block. The screens' currency is the + // rulings spec's check (packages/web/test/async-gen/rulings/rulings.spec.ts). + const scen = read(SCEN); + const blocks = [...scen.matchAll(//g)].map(m => m[1]); + const ends = (scen.match(//g) || []).length; + const RULINGS_DIR = path.join(ROOT, "..", "web", "test", "async-gen", "rulings"); + const files = fs.existsSync(RULINGS_DIR) + ? fs + .readdirSync(RULINGS_DIR) + .filter(f => /^S-\d+\.ts$/.test(f)) + .map(f => f.replace(/\.ts$/, "")) + : []; + const missingFile = blocks.filter(b => !files.includes(b)); + const missingBlock = files.filter(f => !blocks.includes(f)); + if (blocks.length !== ends || missingFile.length || missingBlock.length) { + console.error( + "rules-index: ruling blocks and files disagree —", + blocks.length !== ends ? `${blocks.length} starts, ${ends} ends;` : "", + missingFile.length ? `blocks with no ruling file: ${missingFile.join(" ")};` : "", + missingBlock.length ? `ruling files with no block: ${missingBlock.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" + " ids); every live A-rule is cited by a test; every ruling block has a ruling file (" + + blocks.length + + ")" ); } -const order = { A: 0, V: 1, B: 2, C: 3, INV: 4, RUL: 5, R: 6, "§": 7 }; +const order = { F: -2, D: -1, A: 0, V: 1, B: 2, C: 3, INV: 4, RUL: 5, R: 6, "§": 7, S: 8 }; const rows = [...rules.values()].sort( (a, b) => order[a.vocab] - order[b.vocab] || @@ -236,6 +280,9 @@ L.push( "", "| prefix | defined in | meaning |", "|---|---|---|", + "| `F-` | `RULES-FUNDAMENTALS.md` | the fundamentals: immutable statements of visible behavior |", + "| `D-` | `RULES-DERIVED.md` | derived rules: a fundamental narrowed to a shape, naming its scenarios |", + "| `S-` | `SCENARIOS.md` | scenarios — the rulings. `executable` rows are generated from `packages/web/test/async-gen/rulings/S-.ts` and run in `pnpm test`; `prose` rows are hand-written |", "| `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` |", @@ -285,6 +332,9 @@ for (const r of rows) { r.vocab === "R" ? `R — ${Object.keys(NS).find(k => NS[k] === r.ns)} (\`${r.ns}-R\`)` : { + F: "F — fundamentals", + D: "D — derived rules", + S: "S — scenarios (rulings)", A: "A — spec propositions", V: "V — fixed violations", B: "B — tier B", diff --git a/packages/signals/src/core/seat.ts b/packages/signals/src/core/seat.ts index 41a3868ba2..df24ba517b 100644 --- a/packages/signals/src/core/seat.ts +++ b/packages/signals/src/core/seat.ts @@ -74,6 +74,7 @@ import { GlobalQueue, latestOpened, passLane, + liveTx, passTx, resolveTx, sameLane, @@ -198,6 +199,16 @@ export interface Seat { sawStaging: boolean; /** The rows this pass hit, for the report. */ trail: string[]; + /** The nodes this pass read ahead (`latest`/`isPending` windows): a plain + * read of one of them is the screen's (A10, D-25 — the same-node clause). + * Seeded with the previous pass's (today's sticky CONFIG_VERDICT is how a + * plain-first memo is order-independent across runs); the close reports + * where the seed and the rule part. */ + probed: Set; + /** The nodes this pass actually read ahead. */ + probedNow: Set; + /** The nodes this pass read plainly (tracked), first read first. */ + plainReads: AnyNode[]; /** A known difference (filed) happened in this pass: the old seat and * the new have parted by construction; the pass's end is not compared. */ known: boolean; @@ -205,6 +216,8 @@ export interface Seat { const seats: Seat[] = []; let depth = -1; +/** Per node, the set its last pass read ahead (the carry-over's model). */ +const lastProbed = new WeakMap, Set>(); /** The seat of the running pass, or null outside every pass. */ export function currentSeat(): Seat | null { @@ -219,11 +232,10 @@ export function postureOf(c: Computed | null, window: number): number { if (c._config & CONFIG_CHILDREN_FORBIDDEN) return POSTURE_F; if (c._config & CONFIG_AUTHORITATIVE) return POSTURE_A; if (window) return window === 2 ? POSTURE_VL : POSTURE_VP; - // A pass that asked (`latest`/`isPending`) is a verdict reader from the - // window on, and stays one on every re-run until a pass asks nothing - // (D-25 "a verdict posture outlives the pass that asked"): the sticky - // CONFIG_VERDICT, set at the window (`markVerdictReader`). - if (c._config & CONFIG_VERDICT) return POSTURE_V; + // (D-25 as rewritten 2026-11-06: a pass's posture is its own reads in + // that pass — the sticky CONFIG_VERDICT the code carries into the next + // run is the withdrawn carry-over, not a posture. A plain read of a node + // this pass also read ahead takes the V rows: `seat.probed`.) if ((c as any)._type === EFFECT_RENDER || c._config & CONFIG_FRAME) return POSTURE_L; return POSTURE_D; } @@ -338,6 +350,9 @@ export function openSeat( bornLeaf: false, sawStaging: false, trail: [], + probed: new Set(lastProbed.get(el) ?? []), + probedNow: new Set(), + plainReads: [], known: false }; seats[++depth] = seat; @@ -424,6 +439,17 @@ function applyRaise(seat: Seat, p: Prediction): void { } } +/** Is lane `l` (or a lane linked to it) re-based over hold `t`'s answer + * (store/optimistic `Transaction.over`)? */ +function laneOver(l: Transaction, t: Transaction): boolean { + const ht = resolveTx(t); + for (const u of l._links ?? [l]) { + const o = u.over; + if (o && resolveTx(liveTx(o) || o) === ht) return true; + } + return false; +} + /** Is `l` nested under `a`? */ function under(l: Transaction, a: Transaction): boolean { for (let t = l._parent; t !== null && t._lane; t = t._parent) if (t === a) return true; @@ -523,7 +549,11 @@ export function slot( /** f0: a flight with no value yet. */ const firstFlight = pending && uninit; const inLane = seat !== null && seat.lane !== null; - /** Today's `passLane !== null`: lane work, or a hold's verdict reader. */ + /** Today's `passLane !== null`: lane work, or a hold's verdict reader. + * Selects the rows for a lane node's read (a verdict memo's node: its + * value or its display is screen-visible); the held-staging rows use + * `inLane` alone — a verdict seat is membership, not a rule (decision 3, + * 2026-11-06), and the same-node clause covers what it used to. */ const inSeat = seated(seat); const cUninit = c !== null && (c._statusFlags & STATUS_UNINITIALIZED) !== 0; const cType = c !== null ? ((c as any)._type as number | undefined) : undefined; @@ -643,6 +673,15 @@ export function slot( // node answers through the rows above it, D-41.) if (held) { const t = txOf(el); + // The hold's own work (a pass the same hold holds) reads its staging + // plainly, whatever it asks (D-25, #3884, #4341). + if (c !== null && c._config & CONFIG_HELD && resolveTx(txOf(c)) === resolveTx(t)) + return pred( + latest ? "Vl1''" : "Vp1''", + pending ? SLOT_NR : staged ? stagedSlot : SLOT_C, + latest && !seatBornInto(seat, t) ? RAISE_H : 0, + t + ); if (!uninit) { // Vl1' (#3460, as L7): a render effect outside the hold's own flush // while its verdict lane is blocked is a stale reader of it — the @@ -676,6 +715,11 @@ export function slot( if (running) return pred("Vl2", stagedSlot, seam, flushTransaction); return pred("Vl3", SLOT_C); } + // A node born staged has no screen (D-22, D-23: no answer, no verdict): + // the probe's read does what a read does — the staging, and the owner + // joins. + if (uninit) + return pred("Vp2", stagedSlot, running ? seam : tr ? MARK_W : 0, flushTransaction); return pred("Vp1", SLOT_C, running ? seam : tr ? MARK_W : 0, flushTransaction); } if (pending) { @@ -703,7 +747,9 @@ export function slot( // window of its own — reads a hold's node as a verdict reader does: // `truthRead` is for lane nodes only.) const verdictReader = - posture === POSTURE_V || (posture === POSTURE_A && (c!._config & CONFIG_VERDICT) !== 0); + posture === POSTURE_V || + (seat !== null && seat.node === c && seat.probed.has(el)) || + (posture === POSTURE_A && (c!._config & CONFIG_VERDICT) !== 0); const rowBase = posture === POSTURE_A ? "A1/" : ""; const isEffect = !!cType; // D-28's leaf treatment of a lane (L6/L7: a stale reader of the lane, @@ -789,7 +835,8 @@ export function slot( if (never && l._shown) return pred(row || "L7", SLOT_NR, MARK_ST | leave, l); // L7' (D-2): a leaf the flush's hold holds reads a lane derivation's // staging — the hold's own value. - if (!guess && staged && cHeldByFlush) return pred(row || "L7'", SLOT_P, leave); + if (!guess && staged && cHeldByFlush && (!l._shown || vlane)) + return pred(row || "L7'", SLOT_P, leave); if (l._held || flying) { // D-26 D-28: a stale reader of the lane — the display, re-derived // at the reveal; nothing on screen yet (uninitialized): NR @@ -878,19 +925,36 @@ export function slot( return r("L4", pending ? SLOT_NR : staged ? stagedSlot : SLOT_C, RAISE_H); if (seatBornInto(seat, t)) return r("L2", pending ? SLOT_NR : staged ? stagedSlot : SLOT_C, RAISE_H); // D-2 - if (inSeat && !(cUninit && pending)) return r("D6", screen, MARK_ST); // lane work: the screen (D-37) + // D6' (F-1, D-34 ¶3, #4536): lane work re-based over this hold's + // answer (`Transaction.over`) reads the hold's staging — its rows are + // that answer's, so its other reads are too (core `frameRead`). + if (inLane && !(cUninit && pending) && laneOver(seat!.lane!, t)) + return r("D6'", pending ? screen : staged ? stagedSlot : SLOT_C, 0); + if (inLane && !(cUninit && pending)) return r("D6", screen, MARK_ST); // lane work: the screen (D-37) if (own) return r("L2", pending ? SLOT_NR : staged ? stagedSlot : SLOT_C, RAISE_H); // D-3 D-2 return r("L3", screen, MARK_ST); // D-3 D-9 D-26 } if (verdictReader) { // V1 (D-23 A10 [R7]): a verdict reader is a frame reader — the - // screen, re-derived at the landing — unless born into t. + // screen, re-derived at the landing — unless born into t or the + // hold's own work (D-25). + if (c !== null && c._config & CONFIG_HELD && resolveTx(txOf(c)) === resolveTx(t)) + return r( + "V1''", + pending ? SLOT_NR : staged ? stagedSlot : SLOT_C, + seatBornInto(seat, t) ? 0 : RAISE_H + ); if (seatBornInto(seat, t)) return r("V1'", pending ? SLOT_NR : staged ? stagedSlot : SLOT_C, RAISE_H); if (uninit) return r("V1''", pending ? SLOT_NR : stagedSlot, RAISE_H); // no screen to serve return r("V1", screen, MARK_ST); } - if (inSeat) { + if (inLane) { + // D6' (F-1, D-34 ¶3, #4536): lane work re-based over this hold's + // answer reads the hold's staging (`Transaction.over`); the hold's + // flight is not the lane's own — the screen (D-37). + if (!(cUninit && pending) && laneOver(seat!.lane!, t)) + return r(rowBase + "D6'", pending ? screen : staged ? stagedSlot : SLOT_C, 0); // D6 (D-37): lane work reading a hold's node — the screen, re-derived // at that landing; a lane pass with no value yet over a flight enters. if (!(cUninit && pending) && !uninit && !seatBornInto(seat, t)) @@ -938,7 +1002,7 @@ export function slot( if (pending) { // D-37: not the lane's own, the pass has a value — and so does the // node (a first load in flight has no screen: NR, F-2). - if (inSeat && !cUninit && !uninit) return carry(pred(rowBase + "D8", screen)); + if (inLane && !cUninit && !uninit) return carry(pred(rowBase + "D8", screen)); if (verdictReader && !uninit) { if (cfg & CONFIG_INPUTS_PUBLISHED) return carry(pred("V4'", SLOT_NR, 0, flushTransaction)); // D-23 D-9: the screen; in a flush the pass is the hold's verdict @@ -1140,7 +1204,7 @@ export function shadowReport(): { orphanReads, rows: Object.fromEntries(rowHits), mismatches: [...mismatches.values()], - known: Object.fromEntries([...knownHits].map(([k, v]) => [String(k), v])) + known: Object.fromEntries(knownHits) }; } export function shadowReset(): void { @@ -1207,6 +1271,16 @@ export function shadowExpect( f.flagsBefore = c !== null ? c._flags : 0; f.laneBefore = passLane; f.posture = postureOf(c, window); + // A read inside a window: the node is read ahead by this pass from here. + { + const seat = currentSeat(); + if (seat !== null && seat.node === c) { + if (window) { + seat.probed.add(el); + seat.probedNow.add(el); + } else if (tracked && !seat.plainReads.includes(el)) seat.plainReads.push(el); + } + } f.prediction = slot(c, el, tracked, window, spectate, snapshot); } @@ -1292,36 +1366,90 @@ export function shadowThrown(e: unknown): void { * matches is counted under `known #` instead of a mismatch class; * once the swap lands the entry's issue closes and the entry goes. */ interface KnownDifference { - issue: number; - /** What differs: the slot a read is served, the world a read leaves the + /** An issue, or — for a difference the optimism spike removes by + * construction (`documentation/plans/optimism-seam-spike-brief.md`, + * sections 8 and 11) — the rule and rows it is ruled under. */ + label: string; + /** What differs: the slot a read is served, the join a read leaves the * pass in, or the born-held decision at the pass's close. */ kind: "slot" | "world" | "born"; - /** The row (a read's) or "B2" (the close's). */ - row: string; - /** The table's answer and today's (a slot, a world, or 1/0 for born). */ - expected: number; - actual: number; + /** The rows (a read's) or "B2" (the close's). */ + rows: string[]; + /** The table's answers and today's (slots, worlds, or 1/0 for born). */ + expected: number[]; + actual: number[]; when: (c: Computed, el: AnyNode | null, seat: Seat) => boolean; } +/** Today's pass-wide verdict posture: the sticky CONFIG_VERDICT, or a seat in + * a hold's verdict lane (membership). D-25 as rewritten (2026-11-06) has + * neither: a plain read of a node this pass did not read ahead joins. */ +const passWideVerdict = (c: Computed) => + (c._config & CONFIG_VERDICT) !== 0 || oldVerdictSeat() !== null; const known: KnownDifference[] = [ { // frameRead's `bornInto(c, txOf(el))` joins the pass, stagedScreen's // `bornInto(c, flushTransaction)` then serves the screen: a verdict // reader born into T run by another transaction's flush. - issue: 4426, + label: "#4426", kind: "slot", - row: "V1'", - expected: SLOT_P, - actual: SLOT_C, + rows: ["V1'"], + expected: [SLOT_P], + actual: [SLOT_C], when: (c, el, seat) => (c._config & CONFIG_VERDICT) !== 0 && flushTransaction !== null && seatBornInto(seat, txOf(el!)) && resolveTx(flushTransaction) !== resolveTx(txOf(el!)) + }, + { + // Decision 1 (brief §8, S-49): today a probe makes the pass a verdict + // reader for every held read in it, and the bit carries into the next + // run; the rule joins a plain read of a node the pass did not read + // ahead. Ruled; the spike's M3 (no verdict lane, no sticky bit) builds it. + label: "D-25 pass-wide posture (S-49, S-50)", + kind: "slot", + // (A staged error: the verdict reader is served the proposed error where + // the plain read's row serves the staging, and the screen where it + // serves the staged error — D-41 through the same posture.) + rows: ["D1", "D2", "D3", "L1", "L2", "L5", "D2'", "L4"], + expected: [SLOT_P, SLOT_NR, SLOT_E], + actual: [SLOT_C, SLOT_E], + when: c => passWideVerdict(c) + }, + { + // The same pass-wide posture on the join: a verdict-seated pass does not + // join the hold a plain read of another node would (D-1). + label: "D-25 pass-wide posture (S-49, S-50)", + kind: "world", + rows: ["D1", "D2", "D2'", "L1", "L2", "L0", "D0", "L4"], + expected: [WORLD_H], + actual: [WORLD_S], + when: c => passWideVerdict(c) + }, + { + // The hold's own work (a pass the same hold holds) seated in the hold's + // verdict lane by membership reads the screen today; the rule reads the + // hold's staging (D-25, #3884, #4341). Decision 3: membership is not a rule. + label: "D-25 verdict-lane membership over the hold's own work", + kind: "slot", + rows: ["V1''", "Vp1''", "Vl1''"], + expected: [SLOT_P, SLOT_NR], + actual: [SLOT_C], + when: () => oldVerdictSeat() !== null + }, + { + // The same, on the join: seated by membership, the pass does not join + // today where the rule's own-work read does. + label: "D-25 verdict-lane membership over the hold's own work", + kind: "world", + rows: ["V1''", "Vl1''"], + expected: [WORLD_H], + actual: [WORLD_S], + when: () => oldVerdictSeat() !== null } ]; -const knownHits = new Map(); -export function shadowKnownHits(): ReadonlyMap { +const knownHits = new Map(); +export function shadowKnownHits(): ReadonlyMap { return knownHits; } /** The old born-held test's creator (`creatorPass(oldcontext)`), for the @@ -1340,12 +1468,12 @@ function shadowKnown( for (const k of known) if ( k.kind === kind && - k.row === row && - k.expected === expected && - k.actual === actual && + k.rows.includes(row) && + k.expected.includes(expected) && + k.actual.includes(actual) && k.when(c, el, seat) ) { - knownHits.set(k.issue, (knownHits.get(k.issue) ?? 0) + 1); + knownHits.set(k.label, (knownHits.get(k.label) ?? 0) + 1); return true; } return false; @@ -1385,7 +1513,10 @@ export function shadowLeave(thrown: number): void { } return; } - if (p.slot !== SLOT_SNAPSHOT && actual !== p.slot) + // A read inside an `isPending` window is reads-only (A23): the value its + // thunk is served reaches no screen. Its side effects are compared below; + // its slot is not. + if (p.slot !== SLOT_SNAPSHOT && actual !== p.slot && posture !== POSTURE_VP) report( "INV-seat-3", p.row, @@ -1406,10 +1537,15 @@ export function shadowLeave(thrown: number): void { // as amended by D-27, 2026-10-28: a leaf that read a born node or the // hold's own staging is in the hold's world; one served the screen // raises nothing). - if (actual === p.slot && !seat.known) { - const flagWorld = - c._flags & REACTIVE_JOINED ? WORLD_H : c._flags & REACTIVE_STAGED_READ ? WORLD_FL : WORLD_S; - if (flagWorld !== seat.ownWorld) { + if ((actual === p.slot || posture === POSTURE_VP) && !seat.known) { + // The join is a rule (D-1): compared. The Fl rank (REACTIVE_STAGED_READ, + // D-2's in-flush re-derive) and the verdict seat are mechanism the + // rules as rewritten do not name (decisions 2 and 3, 2026-11-06): not + // compared. + const flagWorld = c._flags & REACTIVE_JOINED ? WORLD_H : WORLD_S; + const seatJoined = seat.ownWorld === WORLD_H ? WORLD_H : WORLD_S; + // (A render effect's membership is the tick's, D-4: not a join.) + if (flagWorld !== seatJoined && !(c as any)._type) { // A known difference (filed): the two worlds have parted for the // rest of this pass. if (shadowKnown("world", p.row, seat.ownWorld, flagWorld, c, el, seat)) { @@ -1421,23 +1557,11 @@ export function shadowLeave(thrown: number): void { p.row, c, el, - `world after the read (old ${flagWorld}, new ${seat.ownWorld}; ${POSTURE_NAMES[posture]} posture; raise ${p.raise}, ${stateOf(c, el)})`, + `join after the read (old ${flagWorld === WORLD_H ? "joined" : "not"}, new ${seatJoined === WORLD_H ? "joined" : "not"}; ${POSTURE_NAMES[posture]} posture; raise ${p.raise}, ${stateOf(c, el)})`, p.slot, actual ); } - // Verdict seat agreement (today `passLane` is a verdict lane). - const oldV = oldVerdictSeat(); - if (oldV !== (seat.vhold && resolveTx(seat.vhold)) && seat.lane === laneOf(passLane)) - report( - "INV-seat-3", - p.row, - c, - el, - `verdict seat after the read (old ${oldV ? "seated" : "none"}, new ${seat.vhold ? "seated" : "none"}; ${POSTURE_NAMES[posture]} posture)`, - p.slot, - actual - ); } } else if (c !== null) orphanReads++; } @@ -1464,17 +1588,7 @@ export function shadowSeatOpen( 0, 0 ); - const oldV = oldVerdictSeat(); - if (oldV !== (seat.vhold && resolveTx(seat.vhold))) - report( - "INV-seat-3", - "B1", - el, - null, - `verdict seat (old ${oldV ? "seated" : "none"}, new ${seat.vhold ? "seated" : "none"}; create ${create})`, - 0, - 0 - ); + // (The verdict seat is membership, not a rule: not compared — decision 3.) // INV-seat-2: posture is the node's own. if (seat.posture !== postureOf(el, 0)) report("INV-seat-2", "B1", el, null, "posture inherited", 0, 0); @@ -1501,7 +1615,11 @@ export function shadowSeatOpen( // ancestor pass still running or one not running at all — is the // retired path; the born-held decision at the close still compares.) const ownerIsRunningPass = creator === null || (depth > 0 && seats[depth - 1].node === creator); - if (ownerIsRunningPass && creatorJoined !== (seat.creatorWorld === WORLD_H)) + // (A creator in a known deviation: its world and the code's parted by + // construction, and so does what it hands down.) + const creatorKnown = depth > 0 && seats[depth - 1].known; + if (creatorKnown) seat.known = true; + if (ownerIsRunningPass && !creatorKnown && creatorJoined !== (seat.creatorWorld === WORLD_H)) report( "INV-seat-3", "B1", @@ -1546,17 +1664,6 @@ export function shadowSeatClose( 0, 0 ); - const oldV = oldVerdictSeat(); - if (oldV !== (seat.vhold && resolveTx(seat.vhold)) && seat.lane === old) - report( - "INV-seat-3", - "B2", - el, - null, - `verdict seat at the pass's end (old ${oldV ? "seated" : "none"}, new ${seat.vhold ? "seated" : "none"}; rows ${seat.trail.slice(-4).join(",")})`, - 0, - 0 - ); // (A pass a lane seats publishes as lane work today: the born-held arm is // not reached — the shadow compares only where it is.) if (seat.create && !seated(seat) && passLane === null) { @@ -1583,6 +1690,17 @@ export function shadowSeatClose( 0 ); } + // D-25 as rewritten (2026-11-06) against today's sticky verdict bit: a + // node probed on the previous pass, read plainly on this one and not + // probed again — the carry-over (S-50): today the screen, the rule joins. + // Pass-level, reported once per pass. + for (const n of seat.probed) + if (!seat.probedNow.has(n) && seat.plainReads.includes(n)) { + const label = "D-25 carry-over (S-50)"; + knownHits.set(label, (knownHits.get(label) ?? 0) + 1); + break; + } + lastProbed.set(el, seat.probedNow); // (A boundary's output sets its own CONFIG_FRAME at the head of its body, // and a probe sets CONFIG_VERDICT: the node's own transitions.) const posture = postureOf(el, 0); diff --git a/packages/signals/tests/rules-index.test.ts b/packages/signals/tests/rules-index.test.ts index 9868223a24..3de05de98a 100644 --- a/packages/signals/tests/rules-index.test.ts +++ b/packages/signals/tests/rules-index.test.ts @@ -28,6 +28,15 @@ describe("rules index", () => { expect(out).toContain("every live A-rule is cited by a test"); }); + it("every executable ruling block in docs/SCENARIOS.md has its ruling file, and vice versa", () => { + 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 ruling block has a ruling file"); + }); + it("docs/RULES-INDEX.md is current (regenerate with `node scripts/rules-index.mjs`)", () => { const script = path.resolve( path.dirname(fileURLToPath(import.meta.url)), diff --git a/packages/web/test/async-gen/emit.ts b/packages/web/test/async-gen/emit.ts index e15b3f744d..0495febae8 100644 --- a/packages/web/test/async-gen/emit.ts +++ b/packages/web/test/async-gen/emit.ts @@ -21,24 +21,28 @@ export interface AppContext { const ref = (r: SrcRef) => (r.k === "s" ? `S[${r.i}][0]` : `M[${r.i}]`); const name = (r: SrcRef) => `${r.k}${r.i}`; -const prefix = { plain: "v", latest: "l", pending: "p" } as const; +const prefix = { plain: "v", latest: "l", pending: "p", pendingLatest: "q" } as const; function readExpr(r: Read): string { return r.how === "plain" ? `${ref(r.src)}()` : r.how === "latest" ? `latest(${ref(r.src)})` - : `isPending(${ref(r.src)})`; + : r.how === "pending" + ? `isPending(${ref(r.src)})` + : `isPending(() => latest(${ref(r.src)}))`; } const textExpr = (r: Read) => `T("${prefix[r.how]}${name(r.src)}", () => ${readExpr(r)})`; function node(n: Node): string { switch (n.t) { case "loading": - return `${node(n.child)}`; + return `${node(n.child)}`; case "errored": return ` "[E]"}>${node(n.child)}`; case "show": + if (n.owns?.length) + throw new Error("emitApp: `owns` (branch-owned memos) is not emitted; rulings only"); return `${node(n.child)}`; case "group": return `<>${n.kids.map(node).join("")}`; @@ -50,6 +54,21 @@ function node(n: Node): string { } export function emitApp(sc: Scenario): string { + // Ruling-only grammar has no hydration twin yet: fail loudly rather than + // emit an app that means something else. + if ( + sc.optimistic?.length || + sc.memos.some(m => m.optimistic || m.deps.some(d => d.when || d.how === "pending")) + ) + throw new Error( + "emitApp: optimistic sources, `when` and `pending` inputs are not emitted; rulings only" + ); + if ( + sc.steps.some( + st => st.t === "guess" || st.t === "refresh" || (st.t === "action" && !("s" in st)) + ) + ) + throw new Error("emitApp: guess / refresh / rich action steps are not emitted; rulings only"); const memos = sc.memos .map((m, i) => { const input = m.deps diff --git a/packages/web/test/async-gen/minimize.ts b/packages/web/test/async-gen/minimize.ts index 0ed82ee492..1735231ddc 100644 --- a/packages/web/test/async-gen/minimize.ts +++ b/packages/web/test/async-gen/minimize.ts @@ -10,12 +10,18 @@ const clone = (x: T): T => JSON.parse(JSON.stringify(x)); function refsIn(sc: Scenario): SrcRef[] { const out: SrcRef[] = []; - sc.memos.forEach(m => m.deps.forEach(d => out.push(d.src))); + sc.memos.forEach(m => + m.deps.forEach(d => { + out.push(d.src); + if (d.when) out.push(d.when); + }) + ); const walk = (n: Node) => { if (n.t === "leaf") [...n.text, ...n.attrs].forEach(r => out.push(r.src)); else if (n.t === "group") n.kids.forEach(walk); else { if (n.t === "show") out.push(n.when.src); + if (n.t === "loading") n.on?.forEach(r => out.push(r.src)); walk(n.child); } }; @@ -25,12 +31,18 @@ function refsIn(sc: Scenario): SrcRef[] { function mapRefs(sc: Scenario, f: (r: SrcRef) => SrcRef): Scenario { const c = clone(sc); - c.memos.forEach(m => m.deps.forEach(d => (d.src = f(d.src)))); + c.memos.forEach(m => + m.deps.forEach(d => { + d.src = f(d.src); + if (d.when) d.when = f(d.when); + }) + ); const walk = (n: Node) => { if (n.t === "leaf") [...n.text, ...n.attrs].forEach(r => (r.src = f(r.src))); else if (n.t === "group") n.kids.forEach(walk); else { if (n.t === "show") n.when.src = f(n.when.src); + if (n.t === "loading") n.on?.forEach(r => (r.src = f(r.src))); walk(n.child); } }; @@ -66,6 +78,7 @@ function treeEdits(n: Node): Node[] { break; default: out.push(n.child); + if (n.t === "loading" && n.on) out.push({ ...n, on: undefined }); if (n.t === "show") { if (n.fallback) out.push({ ...n, fallback: false }); if (n.when.how !== "plain") out.push({ ...n, when: { ...n.when, how: "plain" } }); diff --git a/packages/web/test/async-gen/rulings/S-21.ts b/packages/web/test/async-gen/rulings/S-21.ts new file mode 100644 index 0000000000..147ef7f60d --- /dev/null +++ b/packages/web/test/async-gen/rulings/S-21.ts @@ -0,0 +1,60 @@ +import type { Ruling } from "./types.js"; +import { + s, + m, + plain, + lat, + leaf, + group, + loading, + asyncMemo, + write, + settle, + flush, + task +} from "./_h.js"; + +/** `product` and `comments` are `id + 1` (async). The shell reads `product` + * outside the boundary; `comments` is under ``. */ +export default { + id: "S-21", + title: "A display-ahead read in `on` shows the fallback now, beside the held frame", + rules: ["D-17"], + status: "standing", + pins: [ + "tests/loading-on-frame-following-3540.test.ts", + "tests/loading-on-rearm-reveal-3540.test.ts" + ], + names: { s0: "id", m0: "product", m1: "comments" }, + cases: [ + { + scenario: { + signals: [1], + memos: [asyncMemo(s(0)), asyncMemo(s(0))], + tree: group(leaf(plain(m(0))), loading(leaf(plain(m(1))), [lat(s(0))])), + steps: [settle(), settle(), task, write(0, 2), flush, settle(), task, settle(), task] + }, + frames: [ + { after: 2, screen: "vm0:2 vm1:2 ", note: "primed" }, + { + after: 4, + screen: "vm0:2 [L]", + note: "the new id's loading state inside the old page: `on` read `latest(id)`, whose frame has landed; the shell's read of `product` stays held" + }, + { after: 6, screen: "vm0:3 [L]" }, + { + after: 8, + screen: "vm0:3 vm1:3 ", + note: "content that reads nothing held reveals when it lands" + } + ], + forbid: [ + { + after: 4, + screen: "vm0:2 vm1:2 ", + why: "the swap travelled with the shell's frame" + } + ] + } + ] +} satisfies Ruling; diff --git a/packages/web/test/async-gen/rulings/S-25.ts b/packages/web/test/async-gen/rulings/S-25.ts new file mode 100644 index 0000000000..4f1d6e9e0e --- /dev/null +++ b/packages/web/test/async-gen/rulings/S-25.ts @@ -0,0 +1,109 @@ +import type { Ruling } from "./types.js"; + +const s = (i: number) => ({ k: "s" as const, i }); +const m = (i: number) => ({ k: "m" as const, i }); +const leaf = (how: "plain" | "latest" | "pending", src: { k: "s" | "m"; i: number }) => ({ + t: "leaf" as const, + text: [{ how, src }], + attrs: [], + grouped: false +}); + +/** Every channel sees a write at the flush that carries it, none before. + * `doubled` is `count + 1` here (a memo is one plus its inputs). */ +export default { + id: "S-25", + title: "Nothing shows a write before its flush; then every channel at once", + rules: ["D-22"], + status: "standing", + issues: ["A28"], + pins: ["tests/latest-held-till-flush.test.ts", "tests/latest-repeated-writes.test.ts"], + names: { s0: "count", m0: "doubled", m1: "slow" }, + cases: [ + { + scenario: { + signals: [20], + memos: [{ async: false, deps: [{ src: s(0), how: "plain" }], poison: null }], + tree: { + t: "group", + kids: [ + leaf("plain", s(0)), + leaf("latest", s(0)), + leaf("latest", m(0)), + leaf("pending", s(0)) + ] + }, + steps: [{ t: "write", s: 0, v: 30 }, { t: "flush" }] + }, + frames: [ + { + after: "render", + screen: + "vs0:20 ls0:20 lm0:21 ps0:false " + }, + { after: 0, screen: "—", note: "`setCount(30)` with no flush: nothing shows it" }, + { + after: 1, + screen: + "vs0:30 ls0:30 lm0:31 ps0:false ", + note: "nothing holds: every channel at once" + } + ], + forbid: [ + { + after: 0, + screen: + "vs0:20 ls0:30 lm0:21 ps0:false ", + why: "`latest(count)` 30 while `latest(doubled)` is still 21" + } + ] + }, + { + label: "′", + scenario: { + signals: [20], + memos: [ + { async: false, deps: [{ src: s(0), how: "plain" }], poison: null }, + { async: true, deps: [{ src: s(0), how: "plain" }], poison: null } + ], + tree: { + t: "group", + kids: [ + leaf("plain", s(0)), + leaf("latest", s(0)), + leaf("latest", m(0)), + leaf("pending", s(0)), + leaf("plain", m(1)) + ] + }, + steps: [ + { t: "settle", pick: 0 }, + { t: "task" }, + { t: "write", s: 0, v: 30 }, + { t: "flush" }, + { t: "settle", pick: 0 }, + { t: "task" } + ] + }, + frames: [ + { + after: 1, + screen: + "vs0:20 ls0:20 lm0:21 ps0:false vm1:21 ", + note: "primed" + }, + { + after: 3, + screen: + "vs0:20 ls0:30 lm0:31 ps0:true vm1:21 ", + note: "an asyncMemo reader holds `count`: the plain read keeps 20, `latest` shows 30, `isPending` true" + }, + { + after: 5, + screen: + "vs0:30 ls0:30 lm0:31 ps0:false vm1:31 " + } + ] + } + ] +} satisfies Ruling; diff --git a/packages/web/test/async-gen/rulings/S-26.ts b/packages/web/test/async-gen/rulings/S-26.ts new file mode 100644 index 0000000000..544a8e8d9e --- /dev/null +++ b/packages/web/test/async-gen/rulings/S-26.ts @@ -0,0 +1,59 @@ +import type { Ruling } from "./types.js"; +import { s, m, plain, pend, leaf, loading, asyncMemo, write, settle, flush, task } from "./_h.js"; + +/** `user` is `id + 1` (async), under a `` with `isPending(user)` beside it. */ +export default { + id: "S-26", + title: "`isPending` is about a new question", + rules: ["D-23"], + status: "standing", + issues: ["A19", "A24"], + pins: ["tests/question-scoped-pending.test.ts", "tests/loading-value.test.ts"], + names: { s0: "id", m0: "user" }, + cases: [ + { + scenario: { + signals: [1], + memos: [asyncMemo(s(0))], + tree: loading(leaf(plain(m(0)), pend(m(0)))), + steps: [ + settle(), + task, + write(0, 2), + flush, + settle(), + task, + { t: "refresh", m: 0 }, + flush, + settle(), + task + ] + }, + frames: [ + { + after: "render", + screen: "[L]", + note: "first load: the probe's own read suspends; nothing observable yet — no verdict (D-24)" + }, + { after: 1, screen: "vm0:2 pm0:false ", note: "final" }, + { + after: 3, + screen: "vm0:2 pm0:true ", + note: "an input changed — a new question" + }, + { after: 5, screen: "vm0:3 pm0:false " }, + { after: 7, screen: "—", note: "`refresh(user)`: a re-ask of the same question is quiet" }, + { after: 9, screen: "—", note: "the fresh value reveals silently" } + ], + forbid: [ + { + after: 7, + screen: "vm0:3 pm0:true ", + why: "a re-ask reported as a new question" + } + ] + } + ], + prose: + "The prose row's `affects()` (rows 5–6) and the optimistic write over `user` (row 7) are not migrated yet." +} satisfies Ruling; diff --git a/packages/web/test/async-gen/rulings/S-27.ts b/packages/web/test/async-gen/rulings/S-27.ts new file mode 100644 index 0000000000..9f75eef957 --- /dev/null +++ b/packages/web/test/async-gen/rulings/S-27.ts @@ -0,0 +1,137 @@ +import type { Ruling } from "./types.js"; + +const s = (i: number) => ({ k: "s" as const, i }); +const m = (i: number) => ({ k: "m" as const, i }); + +/** `x` is `n + 1`, `y` is `x + 1`. Two scopes: `[isPending(x), x()]` and + * `[isPending(() => latest(x)), latest(x)]` (A8). */ +export default { + id: "S-27", + title: "`[isPending(x), x()]` is atomic; the `latest` form follows x's own async only", + rules: ["D-23", "D-25"], + status: "standing", + issues: ["A10", "A8"], + pins: ["tests/latest-isPending-consistency.test.ts", "tests/createMemo.test.ts"], + names: { s0: "n", m0: "x", m1: "y" }, + cases: [ + { + scenario: { + signals: [0], + memos: [ + { async: true, deps: [{ src: s(0), how: "plain" }], poison: null }, + { async: true, deps: [{ src: m(0), how: "plain" }], poison: null } + ], + tree: { + t: "group", + kids: [ + { + t: "leaf", + text: [], + attrs: [ + { how: "pending", src: m(0) }, + { how: "plain", src: m(0) } + ], + grouped: true + }, + { + t: "leaf", + text: [], + attrs: [ + { how: "pendingLatest", src: m(0) }, + { how: "latest", src: m(0) } + ], + grouped: true + }, + { t: "leaf", text: [{ how: "plain", src: m(1) }], attrs: [], grouped: false } + ] + }, + steps: [ + { t: "settle", pick: 0 }, + { t: "task" }, + { t: "settle", pick: 0 }, + { t: "task" }, + { t: "write", s: 0, v: 1 }, + { t: "flush" }, + { t: "settle", pick: 0 }, + { t: "task" }, + { t: "settle", pick: 0 }, + { t: "task" } + ] + }, + frames: [ + { + after: 3, + screen: + "vm1:2 ", + note: "primed" + }, + { + after: 5, + screen: + "vm1:2 ", + note: "both channels: x's own fetch is up" + }, + { + after: 7, + screen: + "vm1:2 ", + note: "`x` landed, `y` still up: the plain channel is a landed answer awaiting reveal; `latest` saw x's own answer — verdicts are per channel" + }, + { + after: 9, + screen: + "vm1:3 " + } + ], + forbid: [ + { + after: 7, + screen: + "vm1:2 ", + why: "`[true, x1]`: pending beside the fresh value" + } + ] + }, + { + label: "memo", + scenario: { + signals: [0], + memos: [ + { + async: false, + deps: [ + { src: s(0), how: "plain" }, + { src: s(0), how: "pending" } + ], + poison: null + } + ], + tree: { t: "leaf", text: [{ how: "plain", src: m(0) }], attrs: [], grouped: false }, + steps: [ + { t: "action", s: 0, v: 5 }, + { t: "flush" }, + { t: "settle", pick: 0 }, + { t: "task" } + ] + }, + frames: [ + { after: "render", screen: "vm0:1 " }, + { + after: 1, + screen: "vm0:2 ", + note: "`m = 1 + x + isPending(x)`: the memo probes x and reads it plainly in one pass — the plain read is the screen's 0 and the probe is true, shown now (A10, D-25); the memo is not the hold's work" + }, + { after: 3, screen: "vm0:6 " } + ], + forbid: [ + { + after: 1, + screen: "vm0:1 ", + why: "the memo held with the write it probed: the spinner would show only at the commit" + } + ] + } + ], + prose: + "On a plain signal or sync memo the `latest` form is never pending. The memo case is the pass-level half of D-25: a plain read of a node the pass also probed is the screen's." +} satisfies Ruling; diff --git a/packages/web/test/async-gen/rulings/S-29.ts b/packages/web/test/async-gen/rulings/S-29.ts new file mode 100644 index 0000000000..4cb2d5e9a0 --- /dev/null +++ b/packages/web/test/async-gen/rulings/S-29.ts @@ -0,0 +1,79 @@ +import type { Ruling } from "./types.js"; + +const s = (i: number) => ({ k: "s" as const, i }); +const m = (i: number) => ({ k: "m" as const, i }); + +/** `fast` and `slow` are `n + 1` (async), `copy` is `slow + 1` (sync), and + * `both` is `fast + copy + 1` (sync): `isPending(both)` is the combined + * probe `isPending(() => [fast(), copy()])`. */ +export default { + id: "S-29", + title: "A display-ahead reader's own async is held", + rules: ["D-25"], + status: "standing", + issues: ["A31", "#3442"], + pins: ["tests/ispending-combined-atomic-3442.test.ts"], + names: { s0: "n", m0: "fast", m1: "slow", m2: "copy", m3: "both" }, + cases: [ + { + scenario: { + signals: [0], + memos: [ + { async: true, deps: [{ src: s(0), how: "plain" }], poison: null }, + { async: true, deps: [{ src: s(0), how: "plain" }], poison: null }, + { async: false, deps: [{ src: m(1), how: "plain" }], poison: null }, + { + async: false, + deps: [ + { src: m(0), how: "plain" }, + { src: m(2), how: "plain" } + ], + poison: null + } + ], + tree: { + t: "leaf", + text: [], + attrs: [ + { how: "plain", src: m(0) }, + { how: "plain", src: m(2) }, + { how: "pending", src: m(3) } + ], + grouped: true + }, + steps: [ + { t: "settle", pick: 0 }, + { t: "settle", pick: 0 }, + { t: "task" }, + { t: "write", s: 0, v: 1 }, + { t: "flush" }, + { t: "settle", pick: 0 }, + { t: "task" }, + { t: "settle", pick: 0 }, + { t: "task" } + ] + }, + frames: [ + { + after: 2, + screen: "", + note: "primed" + }, + { after: 4, screen: "" }, + { + after: 6, + screen: "—", + note: "`fast` landed; `copy` computes under its own posture: still pending on `slow`" + }, + { after: 8, screen: "" } + ], + forbid: [ + { + after: 6, + screen: "", + why: "`fast` revealed alone with the probe false" + } + ] + } + ] +} satisfies Ruling; diff --git a/packages/web/test/async-gen/rulings/S-3.ts b/packages/web/test/async-gen/rulings/S-3.ts new file mode 100644 index 0000000000..0286824a76 --- /dev/null +++ b/packages/web/test/async-gen/rulings/S-3.ts @@ -0,0 +1,103 @@ +import type { Ruling } from "./types.js"; +import { + s, + m, + plain, + leaf, + group, + show, + asyncMemo, + syncMemo, + write, + settle, + flush, + task, + act +} from "./_h.js"; + +/** `slow` is `x + 1` (async). On a click while the action is open (`open` + * flips), a branch mounts that creates `ten` (`x + 1`, sync — the prose's + * `x * 10`) and reads it beside a direct read of `x`. `title` is an unrelated + * signal written mid-hold. */ +export default { + id: "S-3", + title: + "A mount during a hold: a derivation born held holds the mount; the branch appears whole at the commit", + rules: ["D-1", "D-3"], + status: "standing", + issues: ["A29"], + pins: ["tests/born-held.test.ts"], + names: { s0: "x", s1: "open", s2: "title", m0: "slow", m1: "ten" }, + cases: [ + { + scenario: { + signals: [0, 0, 0], + memos: [asyncMemo(s(0)), syncMemo({ src: s(0), how: "plain" })], + tree: group( + leaf(plain(s(0))), + leaf(plain(m(0))), + leaf(plain(s(2))), + show(plain(s(1)), group(leaf(plain(m(1))), leaf(plain(s(0)))), { owns: [1] }) + ), + steps: [ + settle(), + task, // 0,1 primed + act(0, 1), + flush, // 2,3 the action holds x = 1; slow re-asks + write(1, 1), + flush, // 4,5 the click: the branch mounts + write(2, 1), + flush, // 6,7 an unrelated mainline write + settle(), + task, + settle(), + task // 8..11 the body returns; slow(2) lands + ] + }, + frames: [ + { + after: 1, + screen: "vs0:0 vm0:1 vs2:0 ", + note: "primed" + }, + { after: 3, screen: "—", note: "held on `slow`" }, + { + after: 5, + screen: "—", + note: "`ten` is born held: its first pass read the held `x`; `

{ten()}

` throws, so it holds the mount and the branch is not attached" + }, + { + after: 7, + screen: "vs0:0 vm0:1 vs2:1 ", + note: "an unrelated mainline write shows at once — not swallowed into the action" + }, + { + after: 11, + screen: + "vs0:1 vm0:2 vs2:1 vm1:2 vs0:1 ", + note: "the commit reveals everything, the branch whole" + } + ], + forbid: [ + { + after: 5, + screen: + "vs0:0 vm0:1 vs2:0 vm1:2 vs0:0 ", + why: "`ten 2` beside `x 0`: the published future" + }, + { + after: 5, + screen: + "vs0:0 vm0:1 vs2:0 vm1:1 vs0:0 ", + why: "`ten 1`: a committed value `ten` never had (the N1 reading)" + }, + { + after: 5, + screen: + "vs0:0 vm0:1 vs2:0 vs0:0 ", + why: "the branch attached around an empty slot" + } + ] + } + ] +} satisfies Ruling; diff --git a/packages/web/test/async-gen/rulings/S-31.ts b/packages/web/test/async-gen/rulings/S-31.ts new file mode 100644 index 0000000000..94abcf36cd --- /dev/null +++ b/packages/web/test/async-gen/rulings/S-31.ts @@ -0,0 +1,59 @@ +import type { Ruling } from "./types.js"; + +const s = (i: number) => ({ k: "s" as const, i }); + +export default { + id: "S-31", + title: + "A verdict reader's mount is mainline: `Show when={latest(x)}` → `Loading` shows its fallback", + rules: ["D-1", "D-25"], + status: "standing", + issues: ["#3851"], + pins: ["tests/verdict-mount-first-pass-3851.test.ts"], + names: { s0: "x" }, + cases: [ + { + scenario: { + signals: [0], + memos: [], + tree: { + t: "group", + kids: [ + { t: "leaf", text: [{ how: "plain", src: s(0) }], attrs: [], grouped: false }, + { + t: "show", + when: { how: "latest", src: s(0) }, + fallback: false, + child: { + t: "loading", + child: { t: "leaf", text: [{ how: "plain", src: s(0) }], attrs: [], grouped: false } + } + } + ] + }, + steps: [ + { t: "action", s: 0, v: 1 }, + { t: "flush" }, + { t: "settle", pick: 0 }, + { t: "task" } + ] + }, + frames: [ + { after: "render", screen: "vs0:0 ", note: "the `Show` is closed" }, + { + after: 1, + screen: "vs0:0 [L]", + note: "the `Show` opens ahead (display-ahead); the boundary is a mainline mount whose content read the write the action holds: born held, so the fallback" + }, + { after: 3, screen: "vs0:1 vs0:1 " } + ], + forbid: [ + { + after: 1, + screen: "vs0:0 vs0:1 ", + why: "the torn frame #3869 fixed: content 1 beside x = 0" + } + ] + } + ] +} satisfies Ruling; diff --git a/packages/web/test/async-gen/rulings/S-33.ts b/packages/web/test/async-gen/rulings/S-33.ts new file mode 100644 index 0000000000..c8a2972a65 --- /dev/null +++ b/packages/web/test/async-gen/rulings/S-33.ts @@ -0,0 +1,69 @@ +import type { Ruling } from "./types.js"; +import { s, m, plain, lat, pend, leaf, group, loading, write, settle, flush, task } from "./_h.js"; + +/** `double` is an optimistic async memo over `value` (truth `value + 1`); + * `related` is `double + 1` (async), rendered beside it. The click writes + * `value = 1` and guesses `double = 3` in one tick; the truth will be 2. */ +export default { + id: "S-33", + title: + "A different truth corrects the graph now and the screen at the commit; a never-shown guess is void", + rules: ["D-34"], + status: "standing", + issues: ["#3331"], + pins: ["tests/spec-async-semantics.test.ts"], + names: { s0: "value", m0: "double", m1: "related" }, + cases: [ + { + scenario: { + signals: [0], + memos: [ + { async: true, deps: [{ src: s(0), how: "plain" }], poison: null, optimistic: true }, + { async: true, deps: [{ src: m(0), how: "plain" }], poison: null } + ], + tree: group(loading(leaf(plain(m(0)), plain(m(1)))), leaf(lat(m(0))), leaf(pend(m(0)))), + steps: [ + settle(), + task, + settle(), + task, // 0..3 primed + write(0, 1), + { t: "guess", o: m(0), v: 3 }, + flush, // 4,5,6 the click + settle(), + task, // 7,8 the source lands 2 ≠ 3: related re-asks from 2 + settle(), + task, // 9,10 the obsolete related(3) lands: inert + settle(), + task // 11,12 related(2) lands + ] + }, + frames: [ + { + after: 3, + screen: "vm0:1 vm1:2 lm0:1 pm0:false ", + note: "primed" + }, + { + after: 6, + screen: "vm0:1 vm1:2 lm0:3 pm0:true ", + note: "the guess's lane holds itself on `related(3)`; `latest` shows the guess" + }, + { + after: 8, + screen: "vm0:1 vm1:2 lm0:2 pm0:true ", + note: "the source lands 2 ≠ 3: `related` re-asks from 2 now; the guess never showed, so what showed stays" + }, + { after: 10, screen: "—", note: "the obsolete `related(3)` lands: inert" }, + { after: 12, screen: "vm0:2 vm1:3 lm0:2 pm0:false " } + ], + forbid: [ + { + after: 8, + screen: "vm0:3 vm1:4 lm0:2 pm0:true ", + why: "a frame showing `double 3`: a dead, invisible guess" + } + ] + } + ] +} satisfies Ruling; diff --git a/packages/web/test/async-gen/rulings/S-44.ts b/packages/web/test/async-gen/rulings/S-44.ts new file mode 100644 index 0000000000..dec52b52d2 --- /dev/null +++ b/packages/web/test/async-gen/rulings/S-44.ts @@ -0,0 +1,80 @@ +import type { Ruling } from "./types.js"; +import { s, m, plain, lat, pend, leaf, group, write, settle, flush, task } from "./_h.js"; + +/** `comments` is an optimistic async memo over `sel` (truth `sel + 1`). The + * post guesses `comments = 9` ("mine (sending)") inside a hold that refreshes + * at its end; `sel` is written from outside while the post is out. */ +export default { + id: "S-44", + title: "An outside input change under a guess is held with the guess's hold", + rules: ["D-34", "F-1"], + status: "ruled-not-built", + issues: ["#4336"], + pins: [ + "tests/optimistic-question-change-4336.test.ts", + "tests/optimistic-layered-filter-4336.test.ts" + ], + names: { s0: "sel", m0: "comments" }, + cases: [ + { + scenario: { + signals: [3], + memos: [ + { async: true, deps: [{ src: s(0), how: "plain" }], poison: null, optimistic: true } + ], + tree: group(leaf(plain(s(0)), plain(m(0))), leaf(lat(m(0))), leaf(pend(m(0)))), + steps: [ + settle(), + task, // 0,1 primed + { t: "action", before: [{ o: m(0), v: 9 }], refresh: [0] }, + flush, // 2,3 the post + write(0, 1), + flush, // 4,5 outside: setSel(1) + settle(1), + task, // 6,7 issue 1's answer lands + settle(), + task, // 8,9 the post settles; its refresh re-asks 1 + settle(), + task // 10,11 issue 1's answer lands again + ] + }, + frames: [ + { + after: 1, + screen: "vs0:3 vm0:4 lm0:4 pm0:false ", + note: "primed" + }, + { + after: 3, + screen: "vs0:3 vm0:9 lm0:9 pm0:false ", + note: "the guess shows; verdict-inert" + }, + { + after: 5, + screen: "vs0:3 vm0:9 lm0:9 pm0:true ", + note: "a new question, not the guess's source answering: the change is held with the post" + }, + { + after: 7, + screen: "—", + note: "issue 1's answer lands: still held; `latest(comments)` is the displayed guess (Decision 3)" + }, + { after: 9, screen: "—", note: "the post settles; its refresh re-asks issue 1" }, + { + after: 11, + screen: "vs0:1 vm0:2 lm0:2 pm0:false ", + note: "one frame" + } + ], + forbid: [ + { + after: 7, + screen: "vs0:1 vm0:2 lm0:2 pm0:false ", + why: "a `sel 1` frame before the post settles" + } + ] + } + ], + prose: + "The prose row's 1″/1⁗ (an optimistic `sel`), 1‴ (a keyed store) and the layered two-action case are not migrated yet." +} satisfies Ruling; diff --git a/packages/web/test/async-gen/rulings/S-45.ts b/packages/web/test/async-gen/rulings/S-45.ts new file mode 100644 index 0000000000..4034dff227 --- /dev/null +++ b/packages/web/test/async-gen/rulings/S-45.ts @@ -0,0 +1,140 @@ +import type { Ruling } from "./types.js"; + +const s = (i: number) => ({ k: "s" as const, i }); +const m = (i: number) => ({ k: "m" as const, i }); + +/** `m0` is `s0 + 1` (async, a first load). The first answer and an action + * that writes `s0` land in one tick. */ +export default { + id: "S-45", + title: + "A first answer landing in the same tick as an action that writes its input: `latest` shows it, the plain read waits", + rules: ["D-22", "F-9"], + status: "ruled-not-built", + issues: ["E9"], + names: { s0: "s0", m0: "m0", m1: "L" }, + cases: [ + { + label: "leaf", + scenario: { + signals: [0], + memos: [{ async: true, deps: [{ src: s(0), how: "plain" }], poison: null }], + tree: { + t: "group", + kids: [ + { t: "leaf", text: [{ how: "plain", src: s(0) }], attrs: [], grouped: false }, + { + t: "loading", + child: { t: "leaf", text: [{ how: "latest", src: m(0) }], attrs: [], grouped: false } + } + ] + }, + steps: [ + { t: "settle", pick: 0 }, + { t: "action", s: 0, v: 1 }, + { t: "micro" }, + { t: "micro" }, + { t: "flush" }, + { t: "settle", pick: 0 }, + { t: "task" }, + { t: "settle", pick: 0 }, + { t: "task" } + ] + }, + frames: [ + { after: "render", screen: "vs0:0 [L]", note: "first load" }, + { + after: 4, + screen: "vs0:0 lm0:1 ", + note: "the answer is the action's (D-4) and `latest` returns it (D-22): the committed `s0 = 0` beside its own answer, one frame" + }, + { after: 8, screen: "vs0:1 lm0:2 " } + ], + forbid: [ + { + after: 4, + screen: "vs0:0 [L]", + why: "the leaf waited on the action it did not read; a `Loading` reading only `latest` shows what it has (S-40)" + } + ] + }, + { + label: "memo", + scenario: { + signals: [0], + memos: [ + { async: true, deps: [{ src: s(0), how: "plain" }], poison: null }, + { async: false, deps: [{ src: m(0), how: "latest" }], poison: null } + ], + tree: { + t: "group", + kids: [ + { t: "leaf", text: [{ how: "plain", src: s(0) }], attrs: [], grouped: false }, + { + t: "loading", + child: { t: "leaf", text: [{ how: "plain", src: m(1) }], attrs: [], grouped: false } + } + ] + }, + steps: [ + { t: "settle", pick: 0 }, + { t: "action", s: 0, v: 1 }, + { t: "micro" }, + { t: "micro" }, + { t: "flush" }, + { t: "settle", pick: 0 }, + { t: "task" }, + { t: "settle", pick: 0 }, + { t: "task" } + ] + }, + frames: [ + { after: "render", screen: "vs0:0 [L]", note: "first load" }, + { + after: 4, + screen: "vs0:0 vm1:2 ", + note: "the same screen through a memo `L = latest(m0) + 1`" + }, + { after: 8, screen: "vs0:1 vm1:3 " } + ] + }, + { + label: "plain", + status: "standing", + scenario: { + signals: [0], + memos: [{ async: true, deps: [{ src: s(0), how: "plain" }], poison: null }], + tree: { + t: "group", + kids: [ + { t: "leaf", text: [{ how: "plain", src: s(0) }], attrs: [], grouped: false }, + { + t: "loading", + child: { t: "leaf", text: [{ how: "plain", src: m(0) }], attrs: [], grouped: false } + } + ] + }, + steps: [ + { t: "settle", pick: 0 }, + { t: "action", s: 0, v: 1 }, + { t: "micro" }, + { t: "micro" }, + { t: "flush" }, + { t: "settle", pick: 0 }, + { t: "task" }, + { t: "settle", pick: 0 }, + { t: "task" } + ] + }, + frames: [ + { after: "render", screen: "vs0:0 [L]" }, + { + after: 4, + screen: "—", + note: "a plain read of a first load is loading (F-9); the fallback is right there" + }, + { after: 8, screen: "vs0:1 vm0:2 " } + ] + } + ] +} satisfies Ruling; diff --git a/packages/web/test/async-gen/rulings/S-46.ts b/packages/web/test/async-gen/rulings/S-46.ts new file mode 100644 index 0000000000..d572664662 --- /dev/null +++ b/packages/web/test/async-gen/rulings/S-46.ts @@ -0,0 +1,141 @@ +import type { Ruling } from "./types.js"; + +const s = (i: number) => ({ k: "s" as const, i }); +const m = (i: number) => ({ k: "m" as const, i }); +const leaf = (how: "plain" | "latest" | "pending", src: { k: "s" | "m"; i: number }) => ({ + t: "leaf" as const, + text: [{ how, src }], + attrs: [], + grouped: false +}); +const prime = [{ t: "settle" as const, pick: 0 }, { t: "task" as const }]; +const run = [ + ...prime, + { t: "write" as const, s: 0, v: 1 }, + { t: "flush" as const }, + { t: "settle" as const, pick: 0 }, + { t: "task" as const } +]; +const memos = [{ async: true, deps: [{ src: s(0), how: "plain" as const }], poison: null }]; + +/** `detail` is `id + 1` (async). A plain write to `id` is held by `detail`'s flight. */ +export default { + id: "S-46", + title: "The content a display-ahead `Show` opens reads the screen, whoever opened the hold", + rules: ["D-25"], + status: "standing", + pins: [ + "tests/display-ahead-plain-write-content.test.ts", + "web/test/display-ahead-plain-write-content-s46.spec.tsx" + ], + names: { s0: "id", m0: "detail", F: "[none]" }, + cases: [ + { + status: "ruled-not-built", + scenario: { + signals: [0], + memos, + tree: { + t: "group", + kids: [ + leaf("plain", s(0)), + leaf("plain", m(0)), + { + t: "show", + when: { how: "latest", src: s(0) }, + fallback: true, + child: leaf("plain", s(0)) + } + ] + }, + steps: run + }, + frames: [ + { after: 1, screen: "vs0:0 vm0:1 [F]", note: "primed" }, + { + after: 3, + screen: "—", + note: "the `Show` opens ahead (`latest` asked), but the branch is a mount whose first pass read the staging plainly: born held (D-27), it does not attach until the landing; the fallback stays" + }, + { after: 5, screen: "vs0:1 vm0:2 vs0:1 " } + ], + forbid: [ + { + after: 3, + screen: "vs0:0 vm0:1 vs0:1 ", + why: "two plain reads of one signal disagreeing on one screen" + }, + { + after: 3, + screen: "vs0:0 vm0:1 vs0:0 ", + why: "the branch attached with the committed value; a mount under a display-ahead reader is born held, not a D-3 leaf" + }, + { + after: 3, + screen: "vs0:0 vm0:1 ", + why: "the branch attached with its hole empty (F-2: never an empty slot)" + } + ] + }, + { + label: "′", + scenario: { + signals: [0], + memos, + tree: { + t: "group", + kids: [ + leaf("plain", s(0)), + leaf("plain", m(0)), + { + t: "show", + when: { how: "latest", src: s(0) }, + fallback: true, + child: leaf("latest", s(0)) + } + ] + }, + steps: run + }, + frames: [ + { after: 1, screen: "vs0:0 vm0:1 [F]" }, + { + after: 3, + screen: "vs0:0 vm0:1 ls0:1 ", + note: "content `latest(id)`: the read that asks sees ahead, at its own site" + }, + { after: 5, screen: "vs0:1 vm0:2 ls0:1 " } + ] + }, + { + label: "″", + scenario: { + signals: [0], + memos, + tree: { + t: "group", + kids: [ + leaf("plain", s(0)), + leaf("plain", m(0)), + { + t: "show", + when: { how: "latest", src: s(0) }, + fallback: true, + child: { t: "loading", child: leaf("plain", s(0)) } + } + ] + }, + steps: run + }, + frames: [ + { after: 1, screen: "vs0:0 vm0:1 [F]" }, + { + after: 3, + screen: "vs0:0 vm0:1 [L]", + note: "S-31's form: the boundary catches the born-held read" + }, + { after: 5, screen: "vs0:1 vm0:2 vs0:1 " } + ] + } + ] +} satisfies Ruling; diff --git a/packages/web/test/async-gen/rulings/S-48.ts b/packages/web/test/async-gen/rulings/S-48.ts new file mode 100644 index 0000000000..9dcd567fa5 --- /dev/null +++ b/packages/web/test/async-gen/rulings/S-48.ts @@ -0,0 +1,45 @@ +import type { Ruling } from "./types.js"; +import { s, m, plain, leaf, group, loading, asyncMemo, write, settle, flush, task } from "./_h.js"; + +/** The same page as S-21 with `on={[id()]}`: a plain read of the held `id`. */ +export default { + id: "S-48", + title: + "`on={id()}`: the `on` pass reads the held value plainly, so the swap lands with the write's frame", + rules: ["D-17"], + status: "standing", + issues: ["#3540"], + pins: ["tests/loading-on-frame-following-3540.test.ts"], + names: { s0: "id", m0: "product", m1: "comments" }, + cases: [ + { + scenario: { + signals: [1], + memos: [asyncMemo(s(0)), asyncMemo(s(0))], + tree: group(leaf(plain(m(0))), loading(leaf(plain(m(1))), [plain(s(0))])), + steps: [settle(), settle(), task, write(0, 2), flush, settle(), task, settle(), task] + }, + frames: [ + { after: 2, screen: "vm0:2 vm1:2 ", note: "primed" }, + { + after: 4, + screen: "—", + note: "the `on` pass read the held `id`: it is the hold's work and its re-arm is held with it" + }, + { + after: 6, + screen: "vm0:3 [L]", + note: "`product(2)` lands: the frame commits and the swap lands with it" + }, + { after: 8, screen: "vm0:3 vm1:3 " } + ], + forbid: [ + { + after: 4, + screen: "vm0:2 [L]", + why: "the fallback ahead of the frame the plain read belongs to" + } + ] + } + ] +} satisfies Ruling; diff --git a/packages/web/test/async-gen/rulings/S-49.ts b/packages/web/test/async-gen/rulings/S-49.ts new file mode 100644 index 0000000000..47a2d4e598 --- /dev/null +++ b/packages/web/test/async-gen/rulings/S-49.ts @@ -0,0 +1,60 @@ +import type { Ruling } from "./types.js"; +import { s, m, plain, lat, leaf, group, loading, asyncMemo, settle, flush, task } from "./_h.js"; + +/** `on={[latest(id), filter()]}`; one action writes both `id` and `filter`. */ +export default { + id: "S-49", + title: "A mixed `on`: a plain read of a held value makes the re-arm wait", + rules: ["D-17"], + status: "ruled-not-built", + names: { s0: "id", s1: "filter", m0: "product", m1: "comments" }, + cases: [ + { + scenario: { + signals: [1, 0], + memos: [asyncMemo(s(0)), asyncMemo(s(0))], + tree: group(leaf(plain(m(0))), loading(leaf(plain(m(1))), [lat(s(0)), plain(s(1))])), + steps: [ + settle(), + settle(), + task, + { + t: "action", + before: [ + { s: 0, v: 2 }, + { s: 1, v: 1 } + ] + }, + flush, + settle(), + task, // the action's body returns + settle(), + task, // product(2) lands: the hold commits + settle(), + task // comments(2) lands + ] + }, + frames: [ + { after: 2, screen: "vm0:2 vm1:2 ", note: "primed" }, + { + after: 4, + screen: "—", + note: "`filter()` is a plain read of a held staging: the `on` pass is the hold's work and the re-arm waits, whatever `latest(id)` beside it read" + }, + { + after: 8, + screen: "vm0:3 [L]", + note: "the hold commits: the swap lands with the frame" + }, + { after: 10, screen: "vm0:3 vm1:3 " } + ], + forbid: [ + { + after: 4, + screen: "vm0:2 [L]", + why: "today's screen: one probe made the whole `on` pass a verdict reader" + } + ] + } + ] +} satisfies Ruling; diff --git a/packages/web/test/async-gen/rulings/S-50.ts b/packages/web/test/async-gen/rulings/S-50.ts new file mode 100644 index 0000000000..9a084eaf6f --- /dev/null +++ b/packages/web/test/async-gen/rulings/S-50.ts @@ -0,0 +1,57 @@ +import type { Ruling } from "./types.js"; +import { s, m, plain, leaf, syncMemo, write, flush, task, settle, act } from "./_h.js"; + +/** `m` reads `k`, probes `isPending(x)` while `probe` is on, and reads `x`: + * `m = 1 + k + (probe ? isPending(x) : 0) + x`. */ +export default { + id: "S-50", + title: "A memo that probed once, re-run by a sync write, holds the tick", + rules: ["D-25"], + status: "ruled-not-built", + names: { s0: "x", s1: "k", s2: "probe", m0: "m" }, + cases: [ + { + scenario: { + signals: [0, 0, 1], + memos: [ + syncMemo( + { src: s(1), how: "plain" }, + { src: s(0), how: "pending", when: s(2) }, + { src: s(0), how: "plain" } + ) + ], + tree: leaf(plain(m(0)), plain(s(1))), + steps: [ + act(0, 1), + flush, // 0,1 an action holds x = 1; m probes: busy + write(2, 0), + write(1, 1), + flush, // 2,3,4 the probe is dropped; a sync write to k re-runs m + settle(), + task // 5,6 the body returns + ] + }, + frames: [ + { after: "render", screen: "vm0:1 vs1:0 " }, + { + after: 1, + screen: "vm0:2 vs1:0 ", + note: "the probe shows busy now; the plain read of `x` is the screen's 0" + }, + { + after: 4, + screen: "—", + note: "this run probes nothing: its read of `x` is a plain read of the held staging, so `m` is the hold's work and the tick that wrote `k` holds with it" + }, + { after: 6, screen: "vm0:3 vs1:1 " } + ], + forbid: [ + { + after: 4, + screen: "vm0:2 vs1:1 ", + why: "today's screen (published twice): the memo read `x` as the screen's because its last run probed, and `k` broke free" + } + ] + } + ] +} satisfies Ruling; diff --git a/packages/web/test/async-gen/rulings/S-6.ts b/packages/web/test/async-gen/rulings/S-6.ts new file mode 100644 index 0000000000..0429828795 --- /dev/null +++ b/packages/web/test/async-gen/rulings/S-6.ts @@ -0,0 +1,105 @@ +import type { Ruling } from "./types.js"; +import { s, m, plain, leaf, group, show, asyncMemo, write, settle, flush, task } from "./_h.js"; + +/** `slow` is `count + 1` (async). While the write to `count` is held on it, + * a branch mounts (`open` flips) that creates `fast` (`count + 1`, async) + * and renders it. */ +const scenario = { + signals: [1, 0], + memos: [asyncMemo(s(0)), asyncMemo(s(0))], + tree: group(leaf(plain(s(0)), plain(m(0))), show(plain(s(1)), leaf(plain(m(1))), { owns: [1] })), + steps: [] as ReturnType[] +}; +export default { + id: "S-6", + title: "A new async memo a render effect consumes waits with the hold", + rules: ["D-1", "F-2"], + status: "standing", + issues: ["#3800"], + pins: ["tests/issue-3800-repro.test.ts"], + names: { s0: "count", s1: "open", m0: "slow", m1: "fast" }, + cases: [ + { + scenario: { + ...scenario, + steps: [ + settle(), + task, + write(0, 2), + flush, + write(1, 1), + flush, + settle(1), + task, + settle(0), + task + ] + }, + frames: [ + { after: 1, screen: "vs0:1 vm0:2 ", note: "primed" }, + { + after: 5, + screen: "—", + note: "`setCount(2)` held on `slow`; the child branch mounts: `fast` is born held and throws, so it holds its mount and is not attached" + }, + { + after: 7, + screen: "—", + note: "`fast`'s answer lands while the hold is live: `slow` is still out, so the hold waits and the answer does not show" + }, + { + after: 9, + screen: "vs0:2 vm0:3 vm1:3 ", + note: "`slow(2)` lands: parent and child together" + } + ], + forbid: [ + { + after: 7, + screen: "vs0:1 vm0:2 vm1:3 ", + why: "the child beside the parent still on the old value" + }, + { + after: 5, + screen: "vs0:1 vm0:2 ", + why: "`child (empty)` on screen" + } + ] + }, + { + label: "′", + scenario: { + ...scenario, + steps: [ + settle(), + task, + write(0, 2), + flush, + write(1, 1), + flush, + settle(0), + task, + settle(0), + task + ] + }, + frames: [ + { after: 1, screen: "vs0:1 vm0:2 " }, + { + after: 7, + screen: "—", + note: "`slow(2)` lands first: `fast` is still out, so the parent does not show yet (the hold waits for the child a render effect consumes, F-2)" + }, + { after: 9, screen: "vs0:2 vm0:3 vm1:3 " } + ], + forbid: [ + { + after: 7, + screen: "vs0:2 vm0:3 ", + why: "the parent revealed around an empty child slot" + } + ] + } + ], + prose: "The sync twin (S-3's `ten`) already waits; the asymmetry was the bug." +} satisfies Ruling; diff --git a/packages/web/test/async-gen/rulings/_h.ts b/packages/web/test/async-gen/rulings/_h.ts new file mode 100644 index 0000000000..eba252aab9 --- /dev/null +++ b/packages/web/test/async-gen/rulings/_h.ts @@ -0,0 +1,44 @@ +/** Small constructors shared by the ruling files. */ +import type { Node, Read, SrcRef, Step } from "../scenario.js"; + +export const s = (i: number): SrcRef => ({ k: "s", i }); +export const m = (i: number): SrcRef => ({ k: "m", i }); +export const plain = (src: SrcRef): Read => ({ how: "plain", src }); +export const lat = (src: SrcRef): Read => ({ how: "latest", src }); +export const pend = (src: SrcRef): Read => ({ how: "pending", src }); +export const pendLat = (src: SrcRef): Read => ({ how: "pendingLatest", src }); +/** One `` with one text hole per read, one effect per hole. */ +export const leaf = (...text: Read[]): Node => ({ t: "leaf", text, attrs: [], grouped: false }); +/** One `` with one attribute per read, all in ONE effect (one scope). */ +export const scope = (...attrs: Read[]): Node => ({ t: "leaf", text: [], attrs, grouped: true }); +export const group = (...kids: Node[]): Node => ({ t: "group", kids }); +export const loading = (child: Node, on?: Read[]): Node => ({ + t: "loading", + child, + ...(on ? { on } : {}) +}); +export const show = ( + when: Read, + child: Node, + opts: { fallback?: boolean; owns?: number[] } = {} +): Node => ({ + t: "show", + when, + child, + fallback: !!opts.fallback, + ...(opts.owns ? { owns: opts.owns } : {}) +}); +export const asyncMemo = (src: SrcRef, how: "plain" | "latest" = "plain") => ({ + async: true, + deps: [{ src, how }], + poison: null +}); +export const syncMemo = ( + ...deps: { src: SrcRef; how: "plain" | "latest" | "pending"; when?: SrcRef }[] +) => ({ async: false, deps, poison: null }); +export const write = (sIdx: number, v: number): Step => ({ t: "write", s: sIdx, v }); +export const settle = (pick = 0): Step => ({ t: "settle", pick }); +export const flush: Step = { t: "flush" }; +export const task: Step = { t: "task" }; +export const micro: Step = { t: "micro" }; +export const act = (sIdx: number, v: number): Step => ({ t: "action", s: sIdx, v }); diff --git a/packages/web/test/async-gen/rulings/index.ts b/packages/web/test/async-gen/rulings/index.ts new file mode 100644 index 0000000000..8baf1298bc --- /dev/null +++ b/packages/web/test/async-gen/rulings/index.ts @@ -0,0 +1,34 @@ +import type { Ruling } from "./types.js"; +import S3 from "./S-3.js"; +import S6 from "./S-6.js"; +import S21 from "./S-21.js"; +import S25 from "./S-25.js"; +import S26 from "./S-26.js"; +import S27 from "./S-27.js"; +import S29 from "./S-29.js"; +import S31 from "./S-31.js"; +import S33 from "./S-33.js"; +import S44 from "./S-44.js"; +import S45 from "./S-45.js"; +import S46 from "./S-46.js"; +import S48 from "./S-48.js"; +import S49 from "./S-49.js"; +import S50 from "./S-50.js"; + +export const RULINGS: Ruling[] = [ + S3, + S6, + S21, + S25, + S26, + S27, + S29, + S31, + S33, + S44, + S45, + S46, + S48, + S49, + S50 +]; diff --git a/packages/web/test/async-gen/rulings/rulings.spec.ts b/packages/web/test/async-gen/rulings/rulings.spec.ts new file mode 100644 index 0000000000..70d9e32a37 --- /dev/null +++ b/packages/web/test/async-gen/rulings/rulings.spec.ts @@ -0,0 +1,79 @@ +/** + * @vitest-environment jsdom + * + * Runs every executable ruling (types.ts) and checks the generated + * SCENARIOS.md blocks are current. `RULINGS_WRITE=1` rewrites the blocks. + */ +import { describe, expect, it } from "vitest"; +import { appendFileSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { runScenario, type Result } from "../run.js"; +import { RULINGS } from "./index.js"; +import { norm, renderRuling, replaceBlock } from "./scenarios-md.js"; +import type { Case } from "./types.js"; + +const here = dirname(fileURLToPath(import.meta.url)); +const SCENARIOS = resolve(here, "../../../../signals/docs/SCENARIOS.md"); + +function expectFrames(res: Result, c: Case, tag = "") { + let prev: string | undefined; + for (const f of c.frames) { + const i = f.after === "render" ? 0 : f.after + 1; + const want = f.screen === "—" ? prev! : f.screen; + expect(res.screens[i], `${tag} after ${String(f.after)}`).toBe(want); + prev = want; + } + for (const x of c.forbid ?? []) + expect(res.screens[x.after + 1], `${tag} ✗ ${x.why}`).not.toBe(x.screen); +} + +const results = new Map(); + +for (const r of RULINGS) + describe(`${r.id} ${r.title} (${r.rules.join(", ")})`, () => { + r.cases.forEach((c, k) => { + const status = c.status ?? r.status; + const name = `case ${c.label || k + 1}`; + it(`${name}: laws`, async () => { + const res = await runScenario(c.scenario); + // RULINGS_DUMP=S-44 appends every screen of that ruling to /tmp/rulings-dump.txt. + if (process.env.RULINGS_DUMP === r.id) + appendFileSync( + "/tmp/rulings-dump.txt", + `${r.id} ${name}\n` + + res.screens.map((x, i) => ` [${i - 1}] ${x}`).join("\n") + + `\n settles: ${res.settles + .map((l, i) => (l ? `${i}:${l}` : "")) + .filter(Boolean) + .join(" ")}\n` + ); + (results.get(r.id) ?? results.set(r.id, []).get(r.id)!)[k] = res; + expect(res.findings).toEqual([]); + }); + (status === "ruled-not-built" ? it.fails : it)( + `${name}: screens${status === "ruled-not-built" ? " (ruled, not built)" : ""}`, + async () => { + const res = await runScenario(c.scenario); + expectFrames(res, c); + for (const v of c.variants ?? []) + expectFrames(await runScenario(c.scenario, v), c, JSON.stringify(v)); + } + ); + }); + }); + +describe("docs", () => { + it("SCENARIOS.md ruling blocks are current (RULINGS_WRITE=1 to regenerate)", async () => { + let md = readFileSync(SCENARIOS, "utf8"); + const before = md; + for (const r of RULINGS) { + const observed = results.get(r.id) ?? []; + for (let k = 0; k < r.cases.length; k++) + observed[k] ??= await runScenario(r.cases[k].scenario); + md = replaceBlock(md, r.id, renderRuling(r, observed)); + } + if (process.env.RULINGS_WRITE) writeFileSync(SCENARIOS, md); + else expect(norm(md), "run with RULINGS_WRITE=1 to regenerate").toBe(norm(before)); + }); +}); diff --git a/packages/web/test/async-gen/rulings/scenarios-md.ts b/packages/web/test/async-gen/rulings/scenarios-md.ts new file mode 100644 index 0000000000..5facfb4ab3 --- /dev/null +++ b/packages/web/test/async-gen/rulings/scenarios-md.ts @@ -0,0 +1,292 @@ +/** + * Renders a ruling to its `SCENARIOS.md` block (between + * `` and ``), in the document's vocabulary: + * a code block for the scenario, one table row per frame, the ✗ rows last. + * Pure: no DOM, no runtime. `rulings.spec.ts` feeds it the observed results + * (for the settle labels and, for a ruled-not-built case, today's screen). + */ +import type { Node, Read, Scenario, SrcRef, Step, Write } from "../scenario.js"; +import type { Result } from "../run.js"; +import type { Case, Ruling } from "./types.js"; + +export const START = (id: string) => ``; +export const END = ""; + +// ── names ──────────────────────────────────────────────────── + +const refKey = (r: SrcRef) => `${r.k}${r.i}`; +const nameOf = (names: Record, r: SrcRef) => names[refKey(r)] ?? refKey(r); +const setterOf = (n: string) => `set${n[0].toUpperCase()}${n.slice(1)}`; +const FALLBACK: Record = { + L: "[fallback]", + "ROOT-L": "[root fallback]", + E: "[error]", + "ROOT-E": "[root error]", + F: "[none]" +}; + +function readText(r: Read, names: Record): string { + const n = nameOf(names, r.src); + return r.how === "plain" + ? `${n}()` + : r.how === "latest" + ? `latest(${n})` + : r.how === "pending" + ? `isPending(${n})` + : `isPending(() => latest(${n}))`; +} + +// ── screens ────────────────────────────────────────────────── + +const TOKEN = /\[(ROOT-)?([LEF])\]|([vlpq])([sm]\d+):([^ <]*)/g; + +/** `vm0:1 [L]` → `` `fast 1 | isPending(both) true | [fallback]` ``. */ +export function screenText(raw: string, names: Record): string { + const out: string[] = []; + for (const m of raw.matchAll(TOKEN)) { + if (m[2]) out.push(names[m[2]] ?? FALLBACK[(m[1] ?? "") + m[2]]); + else { + const n = names[m[4]] ?? m[4]; + const label = + m[3] === "l" + ? `latest(${n})` + : m[3] === "p" + ? `isPending(${n})` + : m[3] === "q" + ? `isPending(() => latest(${n}))` + : n; + out.push(`${label} ${m[5]}`); + } + } + return out.length ? "`" + out.join(" | ") + "`" : "`(empty)`"; +} + +// ── the scenario as source ─────────────────────────────────── + +function depText( + d: { src: SrcRef; how: string; when?: SrcRef }, + names: Record +): string { + const n = nameOf(names, d.src); + const read = + d.how === "latest" ? `latest(${n})` : d.how === "pending" ? `isPending(${n})` : `${n}()`; + return d.when ? `(${nameOf(names, d.when)}() ? ${read} : 0)` : read; +} + +function nodeLines(n: Node, names: Record, indent: string): string[] { + switch (n.t) { + case "leaf": { + const text = n.text.map(r => `{${readText(r, names)}}`).join(" "); + const attrs = n.attrs.map((r, j) => ` data-a${j}={${readText(r, names)}}`).join(""); + const tag = n.attrs.length + ? `${text}${n.grouped ? " {/* one effect */}" : " {/* one effect per attribute */}"}` + : `${text}`; + return [indent + tag]; + } + case "loading": + return [ + `${indent} readText(r, names)).join(", ")}]}` : ""}>`, + ...nodeLines(n.child, names, indent + " "), + `${indent}` + ]; + case "errored": + return [ + `${indent}`, + ...nodeLines(n.child, names, indent + " "), + `${indent}` + ]; + case "show": + return [ + `${indent}${n.owns?.length ? ` {/* creates ${n.owns.map(i => names[`m${i}`] ?? `m${i}`).join(", ")} when it opens */}` : ""}`, + ...nodeLines(n.child, names, indent + " "), + `${indent}` + ]; + case "group": + return n.kids.flatMap(k => nodeLines(k, names, indent)); + } +} + +export function describeTsx(sc: Scenario, names: Record): string { + const lines: string[] = []; + sc.signals.forEach((v, i) => { + const n = names[`s${i}`] ?? `s${i}`; + lines.push( + `const [${n}, ${setterOf(n)}] = ${sc.optimistic?.includes(i) ? "createOptimistic" : "createSignal"}(${v});` + ); + }); + sc.memos.forEach((m, i) => { + const n = names[`m${i}`] ?? `m${i}`; + const body = `1 + ${m.deps.map(d => depText(d, names)).join(" + ")}`; + const poison = m.poison !== null ? ` // throws when the input % 4 === ${m.poison}` : ""; + const ctor = m.optimistic ? "createOptimistic" : "createMemo"; + const lhs = m.optimistic ? `const [${n}, ${setterOf(n)}]` : `const ${n}`; + lines.push( + m.async + ? `${lhs} = ${ctor}(async () => fetch(${body}));${poison}` + : `${lhs} = ${ctor}(() => ${body});${poison}` + ); + }); + lines.push(...nodeLines(sc.tree, names, "")); + return lines.join("\n"); +} + +// ── steps as actions ───────────────────────────────────────── + +const writeText = (w: Write, names: Record) => + "s" in w + ? `${setterOf(names[`s${w.s}`] ?? `s${w.s}`)}(${w.v})` + : `${setterOf(nameOf(names, w.o))}(${w.v})`; + +function stepText(st: Step, names: Record, settledLabel: string | null): string { + switch (st.t) { + case "write": + return `${setterOf(names[`s${st.s}`] ?? `s${st.s}`)}(${st.v})`; + case "action": { + if ("s" in st) return `${setterOf(names[`s${st.s}`] ?? `s${st.s}`)}(${st.v}) in an action`; + const parts = [st.before.map(w => writeText(w, names)).join(", ")]; + if (st.after?.length) parts.push(`then ${st.after.map(w => writeText(w, names)).join(", ")}`); + if (st.refresh?.length) + parts.push(`refresh(${st.refresh.map(i => names[`m${i}`] ?? `m${i}`).join(", ")})`); + return `action: ${parts.join("; ")}`; + } + case "guess": + return `${writeText({ o: st.o, v: st.v }, names)} (a guess)`; + case "refresh": + return `refresh(${names[`m${st.m}`] ?? `m${st.m}`})`; + case "settle": + return settledLabel ? `${flightText(settledLabel, names)} lands` : "nothing to settle"; + case "flush": + return "flush()"; + case "micro": + return "microtask"; + case "task": + return "tick"; + } +} + +/** `m0:2` → `fast(2)`; `act:s0=1` → `the action`. */ +function flightText(label: string, names: Record): string { + const m = /^m(\d+):(\d+)$/.exec(label); + if (m) return `${names[`m${m[1]}`] ?? `m${m[1]}`}(${m[2]})`; + if (label.startsWith("act")) return "the action"; + return label; +} + +/** The Action column between the previous frame and this one. */ +export function actionText( + sc: Scenario, + from: number, + to: number, + names: Record, + settles: (string | null)[] +): string { + const parts: string[] = []; + for (let i = from; i <= to; i++) parts.push(stepText(sc.steps[i], names, settles[i] ?? null)); + return parts.join("; "); +} + +// ── the block ──────────────────────────────────────────────── + +const cell = (s: string) => s.replace(/\|/g, "\\|").replace(/\n/g, " "); + +function tableFor( + r: Ruling, + c: Case, + observed: Result | undefined, + status: Ruling["status"] +): string[] { + const names = r.names; + const rows: string[][] = []; + let prev = -1; + let prevScreen = ""; + let n = 0; + for (const f of c.frames) { + const after = f.after === "render" ? -1 : f.after; + const action = + f.after === "render" + ? "render" + : actionText(c.scenario, prev + 1, after, names, observed?.settles ?? []); + const screen = f.screen === "—" ? "—" : screenText(f.screen, names); + if (f.screen !== "—") prevScreen = f.screen; + rows.push([String(n), action, screen, f.note ?? ""]); + prev = after; + n++; + } + for (const x of c.forbid ?? []) + rows.push(["✗", "", `after ${frameIndex(c, x.after)}: ${screenText(x.screen, names)}`, x.why]); + if (status === "ruled-not-built" && observed) { + // Today's screen at every ruled frame it differs from ("—" resolved), so + // the block records the gap and goes stale when a fix closes it. + let want = ""; + for (const f of c.frames) { + if (f.screen !== "—") want = f.screen; + if (f.after === "render") continue; + const got = observed.screens[f.after + 1]; + const listed = (c.forbid ?? []).some(x => x.after === f.after && x.screen === got); + if (got !== want && !listed) + rows.push([ + "✗", + "", + `today, after ${frameIndex(c, f.after)}: ${screenText(got ?? "", names)}`, + "not yet built — the frames test is `it.fails`" + ]); + } + } + void prevScreen; + const header = ["#", "Action", "Screen", "Notes"]; + const all = [header, ...rows]; + const widths = header.map((_, j) => Math.max(...all.map(row => cell(row[j]).length))); + const line = (row: string[]) => + "| " + row.map((v, j) => cell(v).padEnd(widths[j])).join(" | ") + " |"; + return [ + line(header), + "| " + widths.map(w => "-".repeat(Math.max(3, w))).join(" | ") + " |", + ...rows.map(line) + ]; +} + +/** The table row number a step index falls in (for ✗ rows). */ +function frameIndex(c: Case, after: number): number { + let n = -1; + for (const f of c.frames) { + n++; + if (f.after !== "render" && f.after >= after) return n; + } + return n; +} + +export function renderRuling(r: Ruling, observed: (Result | undefined)[]): string { + const lines: string[] = []; + lines.push( + `### ${r.id}. ${r.title} — ${r.rules.join(", ")}${r.issues?.length ? ` (${r.issues.join(", ")})` : ""}` + ); + lines.push(""); + const pins = r.pins?.length ? ` Pins also: ${r.pins.map(p => `\`${p}\``).join(", ")}.` : ""; + lines.push( + `_Generated from \`packages/web/test/async-gen/rulings/${r.id}.ts\` (\`RULINGS_WRITE=1 pnpm exec vitest run test/async-gen/rulings\` in \`packages/web\`); do not edit inside the markers. Status: ${r.status}.${pins}_` + ); + r.cases.forEach((c, k) => { + lines.push(""); + if (r.cases.length > 1) lines.push(`**Case ${c.label || k + 1}.**`, ""); + lines.push("```tsx", describeTsx(c.scenario, r.names), "```", ""); + lines.push(...tableFor(r, c, observed[k], c.status ?? r.status)); + if (c.note) lines.push("", c.note); + }); + if (r.prose) lines.push("", r.prose); + return lines.join("\n"); +} + +/** Replace the block for `id` in `md`; throws when the markers are missing. */ +export function replaceBlock(md: string, id: string, block: string): string { + const re = new RegExp(`${escapeRe(START(id))}[\\s\\S]*?${escapeRe(END)}`); + if (!re.test(md)) throw new Error(`${id}: no marker pair in SCENARIOS.md`); + return md.replace(re, () => `${START(id)}\n${block}\n${END}`); +} + +const escapeRe = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + +/** Prettier reflows tables and escapes, and formats the TSX code blocks + * (semicolons, trailing commas, quotes, wrapping); compare with whitespace, + * backslashes, emphasis markers, statement punctuation and quotes removed and + * dash runs collapsed (as `rules-index.mjs` does for its tables). */ +export const norm = (t: string) => t.replace(/[\s\\*_;,'"`]+/g, "").replace(/-+/g, "-"); diff --git a/packages/web/test/async-gen/rulings/types.ts b/packages/web/test/async-gen/rulings/types.ts new file mode 100644 index 0000000000..687ff0f358 --- /dev/null +++ b/packages/web/test/async-gen/rulings/types.ts @@ -0,0 +1,52 @@ +/** + * Executable rulings (documentation/plans/optimism-seam-spike-brief.md §8; + * packages/signals/docs/SCENARIOS.md). A ruling is a scenario in the async + * generator's grammar plus the screen it must show after given steps and the + * screens it rules out. `rulings.spec.ts` runs every case; the matching + * `SCENARIOS.md` block is generated from the same object (`scenarios-md.ts`), + * so the table the maintainer rules on and the test that pins it are one + * thing. + */ +import type { Scenario } from "../scenario.js"; +import type { Variant } from "../run.js"; + +/** The screen after step `after` (`screens[after + 1]`; `"render"` is the + * first render, `screens[0]`). `"—"` means unchanged from the previous + * frame. Raw `run.ts` serialization — the renderer translates it. */ +export type Frame = { after: number | "render"; screen: string | "—"; note?: string }; +/** A ✗ row: the screen after step `after` must not be this. */ +export type Forbid = { after: number; screen: string; why: string }; + +export type Case = { + /** Row suffix in the table: "" | "′" | "″" … */ + label?: string; + scenario: Scenario; + frames: Frame[]; + forbid?: Forbid[]; + /** Equivalence rewrites that must show the same screen at every frame. */ + variants?: Variant[]; + /** Overrides the ruling's status for this case. */ + status?: Ruling["status"]; + /** Under the table, for this case. */ + note?: string; +}; + +export type Ruling = { + id: `S-${number}`; + title: string; + /** Rule ids the ruling is a row of: ["D-25", "F-6"]. */ + rules: string[]; + /** `standing`: the screens are today's and pinned. `ruled-not-built`: the + * screens are ruled and the frames test is `it.fails` until the fix lands; + * the generated block then also records today's screen. */ + status: "standing" | "ruled-not-built"; + issues?: string[]; + /** Hand-written tests that stay beside the ruling. */ + pins?: string[]; + /** Rendering names: `s0`, `m0` … → the name the doc uses; `L`/`F`/`E` → + * fallback text. */ + names: Record; + cases: Case[]; + /** One paragraph under the table. */ + prose?: string; +}; diff --git a/packages/web/test/async-gen/run.ts b/packages/web/test/async-gen/run.ts index 2a8ad27aa9..25d357c293 100644 --- a/packages/web/test/async-gen/run.ts +++ b/packages/web/test/async-gen/run.ts @@ -20,15 +20,24 @@ import { Show, action, createMemo, + createOptimistic, createSignal, createStore, flush, isPending, latest, + refresh, resetErrorHalt } from "solid-js"; import { createComponent, effect, insert, render, setAttribute } from "../../src/index.js"; -import { describeScenario, type Node, type Read, type Scenario, type SrcRef } from "./scenario.js"; +import { + describeScenario, + type Node, + type Read, + type Scenario, + type SrcRef, + type Write +} from "./scenario.js"; export type Finding = { oracle: "safety" | "liveness" | "equivalence"; @@ -72,6 +81,9 @@ export interface Result { final: string; expected: string; description: string; + /** Per step, the label of the flight a `settle` step landed (null for + * other steps or when nothing was open). Rulings render it. */ + settles: (string | null)[]; } /** * `held`: each grouped attribute effect (by leaf path) whose last pass ended @@ -128,9 +140,34 @@ function buildApp( errors: string[], passes: Passes = { held: {}, memos: {} } ) { - const sigs = values.map(v => createSignal(v)); + // An optimistic signal's setter is a guess; the fresh render (truth only) + // uses plain signals. + const sigs = values.map((v, i) => + sc.optimistic?.includes(i) && !fresh ? createOptimistic(v) : createSignal(v) + ); const memos: (() => any)[] = []; - const get = (r: SrcRef): (() => any) => (r.k === "s" ? sigs[r.i][0] : memos[r.i]); + /** The setter of an optimistic memo (`guess` writes), by memo index. */ + const guessers: ((v: any) => void)[] = []; + const get = (r: SrcRef): (() => any) => { + const x = r.k === "s" ? sigs[r.i][0] : memos[r.i]; + if (x === undefined) + throw new Error( + `async-gen: ${r.k}${r.i} is not created yet (an owned memo read outside its branch?)` + ); + return x; + }; + /** Memos a `show` branch owns: created when the branch opens, not up front. */ + const ownedBy = new Set(); + { + const walk = (n: Node) => { + if (n.t === "group") n.kids.forEach(walk); + else if (n.t !== "leaf") { + if (n.t === "show") n.owns?.forEach(i => ownedBy.add(i)); + walk(n.child); + } + }; + walk(sc.tree); + } // What the tree reads a memo through: the memo, an identity memo of it, or // a derived store's `v`. const views: (() => any)[] = []; @@ -139,65 +176,75 @@ function buildApp( const ended = (i: number, e: unknown) => (passes.memos[i] = e instanceof NotReadyError ? "pending" : "error"); const view = (r: SrcRef): (() => any) => (r.k === "s" ? sigs[r.i][0] : views[r.i]); - const makeMemos = () => - sc.memos.forEach((m, i) => { - const input = () => - m.deps.reduce( - (acc, d) => acc + Number(d.how === "latest" ? latest(get(d.src)) : get(d.src)()), - 1 + const makeMemo = (i: number) => { + const m = sc.memos[i]; + const input = () => + m.deps.reduce((acc, d) => { + if (d.when && !get(d.when)()) return acc; + const x = get(d.src); + return ( + acc + Number(d.how === "latest" ? latest(x) : d.how === "pending" ? isPending(x) : x()) ); - if (m.async && !fresh) { - memos.push( - createMemo(() => { - let v: number; - try { - v = input(); - } catch (e) { - ended(i, e); - throw e; - } - passes.memos[i] = "pending"; - return new Promise((resolve, reject) => { - flights.push({ - label: `m${i}:${v}`, - value: v, - rejects: poisoned(m.poison, v), - resolve: v => { - passes.memos[i] = "value"; - resolve(v); - }, - reject: e => { - passes.memos[i] = "error"; - reject(e); - } - }); - }); - }) - ); - } else { - memos.push( - createMemo(() => { - try { - const v = input(); - if (poisoned(m.poison, v)) throw new Error(`m${i}:${v}`); + }, 1); + // `createOptimistic(fn)` is a memo whose setter writes guesses; the + // fresh render has no guesses, so a plain memo stands in. + const mk = (fn: () => any): (() => any) => { + if (m.optimistic && !fresh) { + const [g, set] = createOptimistic(fn); + guessers[i] = set as (v: any) => void; + return g; + } + return createMemo(fn); + }; + if (m.async && !fresh) { + memos[i] = mk(() => { + let v: number; + try { + v = input(); + } catch (e) { + ended(i, e); + throw e; + } + passes.memos[i] = "pending"; + return new Promise((resolve, reject) => { + flights.push({ + label: `m${i}:${v}`, + value: v, + rejects: poisoned(m.poison, v), + resolve: v => { passes.memos[i] = "value"; - return v; - } catch (e) { - ended(i, e); - throw e; + resolve(v); + }, + reject: e => { + passes.memos[i] = "error"; + reject(e); } - }) - ); - } - const x = memos[i]; - if (variant.identity) views.push(createMemo(() => x())); - else if (variant.store) { - const [store] = createStore<{ v?: number }>(d => { - d.v = x(); - }, {}); - views.push(() => store.v); - } else views.push(x); - }); + }); + }); + }); + } else { + memos[i] = mk(() => { + try { + const v = input(); + if (poisoned(m.poison, v)) throw new Error(`m${i}:${v}`); + passes.memos[i] = "value"; + return v; + } catch (e) { + ended(i, e); + throw e; + } + }); + } + const x = memos[i]; + if (variant.identity) views[i] = createMemo(() => x()); + else if (variant.store) { + const [store] = createStore<{ v?: number }>(d => { + d.v = x(); + }, {}); + views[i] = () => store.v; + } else views[i] = x; + }; + const makeMemos = () => sc.memos.forEach((_, i) => ownedBy.has(i) || makeMemo(i)); // E9: `latest(x)` read in a derivation instead of at the leaf. const latestViews = new Map any>(); const makeLatestViews = () => { @@ -211,6 +258,7 @@ function buildApp( else if (n.t === "group") n.kids.forEach(walk); else { if (n.t === "show" && n.when.how === "latest") srcs.push(n.when.src); + if (n.t === "loading") n.on?.forEach(r => r.how === "latest" && srcs.push(r.src)); walk(n.child); } }; @@ -238,12 +286,14 @@ function buildApp( ? x() : r.how === "latest" ? latest(x) - : isPending(x); - if (r.src.k === "m" && r.how !== "pending") shown.add(r.src.i); + : r.how === "pending" + ? isPending(x) + : isPending(() => latest(x)); + if (r.src.k === "m" && (r.how === "plain" || r.how === "latest")) shown.add(r.src.i); return v; }; const [epoch, setEpoch] = createSignal(1); - const prefix = { plain: "v", latest: "l", pending: "p" } as const; + const prefix = { plain: "v", latest: "l", pending: "p", pendingLatest: "q" } as const; const text = (r: Read) => `${prefix[r.how]}${r.src.k}${r.src.i}:${String(read(r))} `; // A grouped effect's pass: the same reads in order, noting a pass that a @@ -269,13 +319,22 @@ function buildApp( function renderNode(n: Node, id = "r"): any { switch (n.t) { - case "loading": - return createComponent(Loading, { + case "loading": { + const props: any = { fallback: "[L]", get children() { return renderNode(n.child, `${id}.0`); } - }); + }; + // Defined, not spread: a getter in a spread is read at spread time, + // and `Loading` tests `"on" in props`. + if (n.on) + Object.defineProperty(props, "on", { + enumerable: true, + get: () => n.on!.map(read) + }); + return createComponent(Loading, props); + } case "errored": return createComponent(Errored, { fallback: (e: () => any) => (errors.push(errText(e)), "[E]"), @@ -290,6 +349,9 @@ function buildApp( }, fallback: n.fallback ? "[F]" : undefined, get children() { + // Memos the branch owns are born with it (re-created when it + // re-opens), so a branch opened under a hold mounts them held. + n.owns?.forEach(makeMemo); return renderNode(n.child, `${id}.0`); } } as any); @@ -351,7 +413,15 @@ function buildApp( } as any); } }); - return { sigs, app, remount: () => setEpoch(e => e + 1) }; + /** The setter a guess write targets: an optimistic signal's, or an + * optimistic memo's. */ + const guessOf = (o: SrcRef): ((v: any) => void) => { + const g = + o.k === "s" ? (sc.optimistic?.includes(o.i) ? sigs[o.i][1] : undefined) : guessers[o.i]; + if (!g) throw new Error(`async-gen: ${o.k}${o.i} is not optimistic`); + return g as (v: any) => void; + }; + return { sigs, memos, guessOf, app, remount: () => setEpoch(e => e + 1) }; } // ── driving ───────────────────────────────────────────────────────────────── @@ -367,10 +437,11 @@ async function drain(guard: (law: string, fn: () => void) => void) { guard("throw", flush); } -function settleAt(flights: Flight[], index: number) { +function settleAt(flights: Flight[], index: number): string { const [f] = flights.splice(index, 1); if (f.rejects) f.reject(new Error(f.label)); else f.resolve(f.value); + return f.label; } export async function runScenario(sc: Scenario, variant: Variant = {}): Promise { @@ -379,6 +450,7 @@ export async function runScenario(sc: Scenario, variant: Variant = {}): Promise< const settled: boolean[] = []; const quiet: boolean[] = []; const passes: Passes[] = []; + const settles: (string | null)[] = []; const last: Passes = { held: {}, memos: {} }; const snapshot = () => passes.push(structuredClone(last)); const flights: Flight[] = []; @@ -424,19 +496,29 @@ export async function runScenario(sc: Scenario, variant: Variant = {}): Promise< let final = ""; let expected = ""; try { - const { sigs, app, remount } = buildApp(sc, values, false, flights, variant, errors, last); - const act = action(function* (s: number, v: number) { - sigs[s][1](v); - yield new Promise(resolve => { - flights.push({ - label: `act:s${s}=${v}`, - value: v, - rejects: false, - resolve, - reject: () => {} + const { sigs, memos, guessOf, app, remount } = buildApp( + sc, + values, + false, + flights, + variant, + errors, + last + ); + // A truth write updates `values` (the fresh render's inputs); a guess — + // a write to an optimistic signal, or to an optimistic memo — never does. + const truth = (w: Write) => "s" in w && !sc.optimistic?.includes(w.s); + const apply = (w: Write) => ("s" in w ? sigs[w.s][1](w.v) : guessOf(w.o)(w.v)); + let acts = 0; + const act = (label: string, before: Write[], after: Write[] = [], refreshes: number[] = []) => + action(function* () { + before.forEach(apply); + yield new Promise(resolve => { + flights.push({ label, value: 0, rejects: false, resolve, reject: () => {} }); }); - }); - }); + after.forEach(apply); + refreshes.forEach(i => refresh(memos[i] as any)); + })(); guard("throw", () => { dispose = render(app, el); flush(); @@ -457,12 +539,15 @@ export async function runScenario(sc: Scenario, variant: Variant = {}): Promise< }); break; case "settle": - if (flights.length) settleAt(flights, st.pick % flights.length); + settles[step] = flights.length ? settleAt(flights, st.pick % flights.length) : null; break; - case "action": - values[st.s] = st.v; + case "action": { + const a = "s" in st ? { before: [{ s: st.s, v: st.v }], after: [], refresh: [] } : st; + for (const w of [...a.before, ...(a.after ?? [])]) + if (truth(w)) values[(w as { s: number }).s] = w.v; + const label = "s" in st ? `act:s${st.s}=${st.v}` : `act#${++acts}`; guard("throw", () => { - const p = act(st.s, st.v) as Promise; + const p = act(label, a.before, a.after, a.refresh) as Promise; p?.catch?.((e: any) => findings.push({ oracle: "safety", @@ -473,6 +558,15 @@ export async function runScenario(sc: Scenario, variant: Variant = {}): Promise< ); }); break; + } + case "guess": + guard("throw", () => guessOf(st.o)(st.v)); + break; + case "refresh": + guard("throw", () => { + refresh(memos[st.m] as any); + }); + break; case "flush": guard("throw", flush); break; @@ -550,6 +644,7 @@ export async function runScenario(sc: Scenario, variant: Variant = {}): Promise< passes, final, expected, - description: describeScenario(sc) + description: describeScenario(sc), + settles }; } diff --git a/packages/web/test/async-gen/scenario.ts b/packages/web/test/async-gen/scenario.ts index f827a9b19d..c2af8f86a3 100644 --- a/packages/web/test/async-gen/scenario.ts +++ b/packages/web/test/async-gen/scenario.ts @@ -8,10 +8,18 @@ import fc from "fast-check"; /** A signal (`s`) or a memo (`m`), by index. */ export type SrcRef = { k: "s" | "m"; i: number }; -/** How a hole reads its source: `x()`, `latest(x)` or `isPending(x)`. */ -export type How = "plain" | "latest" | "pending"; +/** How a hole reads its source: `x()`, `latest(x)`, `isPending(x)` or + * `isPending(() => latest(x))` (`pendingLatest`: the verdict of the latest + * channel, A8). */ +export type How = "plain" | "latest" | "pending" | "pendingLatest"; export type Read = { how: How; src: SrcRef }; +/** A memo input: `x()`, `latest(x)` or `isPending(x)` (counted as 0/1). + * `when`: the input is read only while that signal is non-zero — a probe a + * memo makes on one run and not the next (S-50). Rulings only; the random + * generator never emits `pending` or `when`. */ +export type Dep = { src: SrcRef; how: "plain" | "latest" | "pending"; when?: SrcRef }; + /** * A memo over earlier sources. Its value is the sum of its deps plus one. An * async memo returns a manual flight per run, settled by the script; a flight @@ -20,26 +28,45 @@ export type Read = { how: How; src: SrcRef }; */ export type MemoDef = { async: boolean; - deps: { src: SrcRef; how: "plain" | "latest" }[]; + deps: Dep[]; poison: number | null; + /** `createOptimistic(fn)` instead of `createMemo(fn)`: its setter is the + * target of `guess` writes (rulings only). */ + optimistic?: true; }; export type Node = - | { t: "loading"; child: Node } + /** `on`: the boundary's dependency list (``); the reads + * are made, the values never compared (rulings only). */ + | { t: "loading"; child: Node; on?: Read[] } | { t: "errored"; child: Node } - | { t: "show"; when: Read; child: Node; fallback: boolean } + /** `owns`: memos created inside this branch when it opens — born held when + * the branch opens under a hold (rulings only; D-2). */ + | { t: "show"; when: Read; child: Node; fallback: boolean; owns?: number[] } | { t: "group"; kids: Node[] } /** A `` with one inserted text hole per `text` read and one * `data-a` attribute per `attrs` read — in ONE compiled effect when * `grouped`, one effect per attribute otherwise (pair E1). */ | { t: "leaf"; text: Read[]; attrs: Read[]; grouped: boolean }; +/** A write inside an action: a truth write to signal `s`, or a guess to an + * optimistic source `o` (an optimistic signal's setter, or an optimistic + * memo's). A truth write to an optimistic signal is a guess too. */ +export type Write = { s: number; v: number } | { o: SrcRef; v: number }; + export type Step = | { t: "write"; s: number; v: number } /** Settle the `pick % open`-th open flight (oldest first). */ | { t: "settle"; pick: number } /** An action: writes s = v, then yields on a flight the script settles. */ | { t: "action"; s: number; v: number } + /** An action (rulings only): `before` writes, a yield on a flight the + * script settles, then `after` writes and `refresh(memos[i])` calls. */ + | { t: "action"; before: Write[]; after?: Write[]; refresh?: number[] } + /** A guess outside an action (rulings only). */ + | { t: "guess"; o: SrcRef; v: number } + /** `refresh(memos[m])` from outside any hold (rulings only). */ + | { t: "refresh"; m: number } | { t: "flush" } | { t: "micro" } | { t: "task" }; @@ -51,6 +78,10 @@ export interface Scenario { steps: Step[]; /** Memos created inside the root component instead of at module level. */ owned?: boolean; + /** Signals created with `createOptimistic(v)` (rulings only). */ + optimistic?: number[]; + /** Names for rendering a ruling's document block; the runner ignores them. */ + names?: Record; } const VALUES = 4; @@ -176,18 +207,20 @@ const rd = (r: Read) => ? `${ref(r.src)}()` : r.how === "latest" ? `latest(${ref(r.src)})` - : `isPending(${ref(r.src)})`; + : r.how === "pending" + ? `isPending(${ref(r.src)})` + : `isPending(() => latest(${ref(r.src)}))`; export function describeNode(n: Node): string { switch (n.t) { case "leaf": return `${n.text.map(rd).join(" ")}`; case "loading": - return `${describeNode(n.child)}`; + return `${describeNode(n.child)}`; case "errored": return `${describeNode(n.child)}`; case "show": - return `${describeNode(n.child)}`; + return ` `m${i}`).join(",")}]` : ""}>${describeNode(n.child)}`; case "group": return n.kids.map(describeNode).join(""); } @@ -197,9 +230,10 @@ export function describeScenario(s: Scenario): string { const memos = s.memos .map( (m, i) => - `m${i}=${m.async ? "async" : "memo"}(${m.deps.map(d => (d.how === "latest" ? `latest(${ref(d.src)})` : ref(d.src))).join("+")}${m.poison !== null ? ` !%4=${m.poison}` : ""})` + `m${i}=${m.optimistic ? "opt:" : ""}${m.async ? "async" : "memo"}(${m.deps.map(d => `${d.how === "latest" ? `latest(${ref(d.src)})` : d.how === "pending" ? `isPending(${ref(d.src)})` : ref(d.src)}${d.when ? `?${ref(d.when)}` : ""}`).join("+")}${m.poison !== null ? ` !%4=${m.poison}` : ""})` ) .join(" "); + const w = (x: Write) => ("s" in x ? `s${x.s}=${x.v}` : `guess ${ref(x.o)}=${x.v}`); const steps = s.steps .map(st => st.t === "write" @@ -207,13 +241,19 @@ export function describeScenario(s: Scenario): string { : st.t === "settle" ? `S${st.pick}` : st.t === "action" - ? `A(s${st.s}=${st.v})` - : st.t === "flush" - ? "f" - : st.t === "micro" - ? "m" - : "t" + ? "s" in st + ? `A(s${st.s}=${st.v})` + : `A(${st.before.map(w).join(",")}${st.after?.length ? ` | after ${st.after.map(w).join(",")}` : ""}${st.refresh?.length ? ` | refresh ${st.refresh.map(i => `m${i}`).join(",")}` : ""})` + : st.t === "guess" + ? `G(${ref(st.o)}=${st.v})` + : st.t === "refresh" + ? `R(m${st.m})` + : st.t === "flush" + ? "f" + : st.t === "micro" + ? "m" + : "t" ) .join(" "); - return `${s.owned ? "owned " : ""}[${s.signals.map((v, i) => `s${i}=${v}`).join(" ")}${memos ? " " + memos : ""}] ${describeNode(s.tree)} :: ${steps}`; + return `${s.owned ? "owned " : ""}[${s.signals.map((v, i) => `${s.optimistic?.includes(i) ? "opt:" : ""}s${i}=${v}`).join(" ")}${memos ? " " + memos : ""}] ${describeNode(s.tree)} :: ${steps}`; } diff --git a/packages/web/test/consistency/async-gen/hydrate.spec.tsx b/packages/web/test/consistency/async-gen/hydrate.spec.tsx index 9660c6182e..480446f462 100644 --- a/packages/web/test/consistency/async-gen/hydrate.spec.tsx +++ b/packages/web/test/consistency/async-gen/hydrate.spec.tsx @@ -235,6 +235,7 @@ async function runCase( } break; case "action": + if (!("s" in st)) throw new Error(`hydrate: unsupported step ${st.t} (rulings only)`); values[st.s] = st.v; guard("throw", () => (act(st.s, st.v) as Promise)?.catch?.(() => {})); break; @@ -248,6 +249,8 @@ async function runCase( await task(); guard("throw", flush); break; + default: + throw new Error(`hydrate: unsupported step ${(st as any).t} (rulings only)`); } screens.push(serialize(container)); }