refactor(signals): move 3b — one implementation per rule for value selection; five store/signal parity fixes - #3523
Conversation
🦋 Changeset detectedLatest commit: aec7ecd The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
…le 1, one implementation) Step 2 of DESIGN-CONSOLIDATION move 3b. nodeValue — the store's untracked node-read selection — restated core read()'s committed-vs-staged arm by hand (inOwnerContext, a stale-of-foreign 'foreignHold' twin, the HELD-truth mask). It now calls readerSeesCommitted with core's context selection (readerContext: a root reads as its parent computed) behind the two store-only tunnels, latest() (#3075) and authoritativeServe (truth authors). Not zero-change — the twin was missing a precondition of the rule it copied: a stale reader (render effect) whose untracked read is of a store key held by a foreign action was served committed (correct, A15/A26) but never recorded for replay at the commit (heldFromStale's _gatedSubs contract, which the signal path performs), so the effect stayed on the pre-action value after the action settled. Pinned as S4 in posture-store-parity (signal vs store, both now [0, 0, 1]). The backing twin (no node: pendingBackingVisible / heldFromReader) has the same gap and no node to record the reader on — pinned it.fails as the step-3 item. The lane arm (laneReadsCommitted) now also applies to untracked store node reads as it does to tracked ones; no matrix cell or test distinguishes. Gate: suite green (+2 pins, 1 expected fail); visibility oracles and the 621-cell posture matrix byte-identical to next; minified store −5 B, full bundle brotli −44 B vs step 1. Co-authored-by: Claude via Cursor <noreply@cursor.com>
…ule 1, backing-level form) Step 3 of DESIGN-CONSOLIDATION move 3b. The stale-of-foreign clause has two halves — serve committed, and record the reader for replay at the hold's commit (A15 / A26, _gatedSubs). Core heldFromStale does both; the store's backing-level twins (heldFromReader for a key with no node, readSource's adoption-hold view for a reconcile inside an action) did only the first. A render effect whose untracked read hit either stayed on the pre-action value after the action settled. The registration is now one exported helper, recordStaleReplay(txn, c), called by heldFromStale and by the store's two backing sites (staleReplay: reader in context, not children-forbidden — the same gate core applies before it reaches heldFromStale). Pins: posture-store-parity S4 grows to the four store shapes (fold hold × adoption hold × with/without a node) beside the signal; all four fail on next, the it.fails from step 2 flips to a pass. Gate: suite green; oracles and the 621-cell posture matrix byte-identical to next; +31 B minified core (the helper), +123 B minified store (the fix); three brotli caps ratcheted with notes. Co-authored-by: Claude via Cursor <noreply@cursor.com>
…born held (A29 at the store's untracked paths) Step 4 of DESIGN-CONSOLIDATION move 3b. Core read() serves a staged value to a deriving reader and enters its transaction in the same arm — an untracked read inside a memo is still that memo's read (context persists under untrack), so a mainline memo over a held signal is born held. The store's untracked paths — nodeValue, pendingBackingVisible, readSource's adoption-hold view — restated the value half and dropped the entry: a mainline memo's or user effect's untrack(() => s.n) PUBLISHED the action's unrevealed write. They now call enterStagedRead on the arm that serves the staged value; enterStagedRead takes a null node for a backing (the fold's transaction is passed). The two store tunnels (latest() for store keys, authoritativeServe) keep serving without entering, as before. Discovery: the posture matrix gains memoUntracked / effectUntracked reader kinds (621 -> 851 cells) — the kind that sees S4 and S5, and that shows two more store/signal divergences recorded, not ruled, as S6 (staged-ambient write read by a derivation created inside a foreign action) and S7 (optimistic override with the only reader gated away). Commits docs/DESIGN-CONSOLIDATION.md (dropped from next by c7ec862's squash accident) with a ledger of moves 3a/3b so far; the code citations now point at it. Gate: suite green (+14 pins); oracles identical; the 5-reader subset of the matrix identical to next; +1 B minified core, +130 B minified store; two brotli caps ratcheted. Co-authored-by: Claude via Cursor <noreply@cursor.com>
…unobserved (S7; store follows signal) Ruling 2026-09-17: store rules follow signal rules. Of the two divergences the 7-reader posture matrix recorded in #3520, this ships S7 and defers S6. S7 — the slot hook released a property node the moment its last subscriber left, with the optimistic override on it (overrides live on nodes over a clone the setter discards): s.n read 0 while the action was live, where an optimistic signal keeps its override whether or not anything reads it. A node carrying an override or a staged write now defers its release to the flush that resolves it (deferSlotRelease feeds the scheduler's transientStoreNodes sweep — present since the monorepo absorb with exactly these checks, and unfed). +55 B minified store. Pinned: override visible after gating away; node released at settle. S6 — A28 at the backing (a mainline setStore invisible to a derivation created inside a foreign action until the carrying flush) is ruled the same way but DEFERRED: done at the store twin it cost +402 B minified (a node born in the unflushed window must stage the write, and every arm existed twice — core's and the twin's). Pinned at the store's current value with the ruling attached; it closes when the store's value selection shares core's serve (DESIGN-CONSOLIDATION move 3b step 6). Ledger updated. Gate: suite green; oracles identical; matrix: only the S7 cells moved, to the signal's values; solid/web unchanged from next; no size caps exceeded. Co-authored-by: Claude via Cursor <noreply@cursor.com>
… 3b step 6b); record the lane rulings Step 6b of DESIGN-CONSOLIDATION move 3b. Three tests of 'is this hold the running pass's world' existed: heldFromStale's t === activeTransition, the store's foreignHold (resolved-transition equality), and readsHeldCommitted's 'same resolved transition OR same lane'. One relation now: core ownsHold(hold) — the pass runs under the hold's transaction, through merges — and lanes.ts ownsLane(lane, owner) built on it, adding the lane clause. heldFromStale's identity test becomes the resolved form the other two already used; no cell of the 851-cell matrix, no oracle cell and no test moves. §6 of the design doc records the three rulings from 2026-09-17: store rules follow signal rules; a lane is a transaction with an override whose world includes the transition that owns it (an ownership relation, not a value arm — serve gets no lane arm); collapse in place on agreement, absorption into the open parent on disagreement (A18's mark; 3c's third cargo op). Gate: suite green; oracles and matrix byte-identical; solid/web unchanged from next; +44 B minified core (the function is not inlined; -4 B brotli on the pure-signals fixture), four brotli caps and the floor budget ratcheted. Co-authored-by: Claude via Cursor <noreply@cursor.com>
…and store nodes (move 3b step 6c); A18 for untracked store reads read()'s slow tail — the override arm (A17, engine-routed under a lane or a supersession), the lane entanglement gate, the born-held throw (A19 exc. 1), the A28 unflushed arm and readerSeesCommitted / A29 — is one function, serve(el, c, owner, committed). read() passes el._value; the store's untracked node path (nodeValue) passes its BACKING as the committed value (single-home rule, O6) behind its two tunnels (authoritativeServe for truth authors, latest() for store keys, #3075). unflushedValue takes the same committed parameter. read()'s auto-dispose sweep registration keeps its former early-return shape (an untracked read served a visible override returned before it) via a guard. Not zero-change — the fold carried A18 to the store: a derivation's UNTRACKED read of a derived optimistic store's key, after its own truth (2) landed differently from the action's edit (3), was served the override and published 3; the signal serves the landed truth and holds the memo with the action. nodeValue's own override arm lacked read()'s supersession routing (overrideRead). Pinned as S8; the matrix state 'derived optimistic store: own truth landed 2 ≠ override 3' now matches the signal's superseded state row for row (37/37) — the only cells that moved. Gate: suite green; oracles identical; solid/web unchanged from next; +88 B minified core (wrapper, parameter, guard), floor budget and two brotli caps ratcheted. The additive half of the consolidation; the deletions it enables (overrideRead's wrapper, nodeValue, the verdict re-derivations) are next. Co-authored-by: Claude via Cursor <noreply@cursor.com>
…ted note per scenario)
d308aa2 to
e9c49a1
Compare
…cates — one holdVisible for both hold kinds (move 3b, the deletion half) readSource (adoption hold) and pendingBackingVisible (fold) each restated Rule 1's hold arms for a CONTAINER — committed or staged — with their own helpers: heldFromReader, foreignHold, enterHeldBacking, staleReplay, inOwnerContext, inForbiddenScope. One holdVisible(txn, c) now, on core's ownsHold / recordStaleReplay / enterStagedRead: the pass owns the hold (or there is none) → staged; a foreign hold → a stale pass keeps committed and is recorded for replay, a deriving pass takes staged and enters (A29). Both callers serve context-free and children-forbidden readers the committed container themselves (A32), with the store-only contracts kept as written: the speculative peek, the projection's authoritative-elect backing, HELD truth on an optimistic family (heldTruthMasked), drafts and write-override. This is the twin serve() could not absorb: a container has no node to select on, so the arms stay at the store — once, on the shared predicates, instead of six times. Zero semantic change: the 851-cell posture matrix and both oracles are byte-identical; suite green. Store −313 B minified / −87 B brotli (the +createStore scenario −48 B, hydrating + stores −112 B). The size ceiling with the whole owner-context arm removed was −738 B; the remainder is the container choice itself. Co-authored-by: Claude via Cursor <noreply@cursor.com>
Coverage Report for CI Build 35275421055Coverage remained the same at 71.304%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Merging this PR will not alter performance
Comparing Footnotes
|
|
…bserver leaving (S7, structural half; review of #3523) An optimistic add or delete lives on the key's PRESENCE node (target.h, getHasNode) as its membership override. Its unobserved callback released the node the moment the last structural observer left — with the override on it — so `in`, Object.keys and descriptors fell back to committed structure while the action was live: the structural twin of the value-slot bug S7 fixed. The presence node now defers its release through deferSlotRelease while it carries an override or a staged value; the transient-node sweep runs its callback once the state resolves. Pinned for optimistic add and delete: the only structural observer gates away, in / Object.keys / getOwnPropertyDescriptor keep the optimistic structure while the action is live, settlement restores committed structure and releases target.h[key]. Suite green; oracles and matrix identical; +35 B minified store. Co-authored-by: Claude via Cursor <noreply@cursor.com>
|
…/ Object.keys / descriptor under a held add and optimistic add/delete The oracle and the posture matrix only ever read a VALUE; structure — a key's presence through `in`, Object.keys, getOwnPropertyDescriptor — rides presence nodes and the key-set node and had no state, so the class 'structure under a hold or override, by posture' was invisible (the review of #3523 found an optimistic add lost to committed structure the moment its only structural observer left; no cell could have shown it). Nine states now: the plain store's held add and the optimistic add and delete, each through the three structural channels. The matrix grows 851 -> 1184 cells; the gatedAway posture over these is the reviewer's shape, kept optimistic after the gate. Six pre-existing divergences between structure and value under the same rules, pinned as violations (suite green, red in the oracle report): - latest() over a held add sees the parked value of s.n but not the parked structure — none of the three structural channels has a latest() tunnel; - the descriptor trap reads no node: isPending() over it witnesses nothing, and a pre-existing render effect inspecting a key through getOwnPropertyDescriptor never re-runs for an optimistic add or delete. Co-authored-by: Claude via Cursor <noreply@cursor.com>
…/ Object.keys / descriptor under a held add and optimistic add/delete The oracle and the posture matrix only ever read a VALUE; structure — a key's presence through `in`, Object.keys, getOwnPropertyDescriptor — rides presence nodes and the key-set node and had no state, so the class 'structure under a hold or override, by posture' was invisible (the review of #3523 found an optimistic add lost to committed structure the moment its only structural observer left; no cell could have shown it). Nine states now: the plain store's held add and the optimistic add and delete, each through the three structural channels. The matrix grows 851 -> 1184 cells; the gatedAway posture over these is the reviewer's shape, kept optimistic after the gate. Six pre-existing divergences between structure and value under the same rules, pinned as violations (suite green, red in the oracle report): - latest() over a held add sees the parked value of s.n but not the parked structure — none of the three structural channels has a latest() tunnel; - the descriptor trap reads no node: isPending() over it witnesses nothing, and a pre-existing render effect inspecting a key through getOwnPropertyDescriptor never re-runs for an optimistic add or delete. Co-authored-by: Claude via Cursor <noreply@cursor.com>
02ace2c to
aec7ecd
Compare
|
Collapses the stack #3516 → #3517 → #3520 → #3521 → #3523 into one PR against
next(step 1, #3515, is already merged). The per-step commits are preserved; each carries its own gate results in its message.Why
The last two months of async fixes were ~four rules each fixed several times at different sites.
docs/DESIGN-CONSOLIDATION.md(committed here) proposes one implementation per rule, gated on the posture matrix, both oracles and the fuzzer. This PR is move 3b — Rule 1, value selection — through step 6c, plus the store/signal parity fixes the fold surfaced.What each step did and found
readerSeesCommittedrecordStaleReplay— one replay registration for node and backing pathsreconcile-adoption forms (four store shapes, all failing onnext)enterStagedRead, null-node form); matrix gainsmemoUntracked/effectUntracked(621 → 851 cells); design doc committeddeferSlotRelease)s.nread committed while the action was liveownsHold/ownsLane— one ownership relation; §6 records the lane rulingsserve(el, c, owner, committed)— Rule 1's one slow selection;read()and the store'snodeValuecall itreadSource,pendingBackingVisible) on the shared predicates — oneholdVisible(txn, c)for both hold kinds; six store-local helpers deleted (heldFromReader,foreignHold,enterHeldBacking,staleReplay,inOwnerContext,inForbiddenScope)Ruling applied throughout: "store rules should follow signal rules" (2026-09-17). Every fix cites an existing rule (A15/A26, A29, A17, A18); no rule text changed.
Deferred, ruled: S6 (A28 at the store backing) — at the store twin it cost +402 B; pinned at the store's current value with the ruling attached.
Result
Every paired signal/store state in the 851-cell matrix is row-for-row identical (six pairs × 37 rows). The 5-reader subset of the matrix is byte-identical to
nextexcept the cells the fixes above flipped, each to the signal's value.Cost (honest)
Core minified +164 B (replay helper +31, null-node
enterStagedRead+1,ownsHold+44,serve+88). Store minified ≈ +50 B net: the four fixes ≈ +370, the refactors ≈ −320 (the backing-visibility deletion alone −313). Brotli:+createStorescenario now −48 B and hydrating + stores −112 B relative to the pre-deletion stack; core-floor scenarios +24…+48 B; caps ratcheted with one consolidated note each.Two deletion probes on top of 6c, to test whether the consolidation pays bytes back:
overrideRead/readsHeldCommitted→ already single implementations; nothing to delete.read()'s fast block →readNodeFast: −128 B minified in a store bundle but +125 B on the core floor (readNodeFast was never in it) and +31 B brotli (the compressor already dedup'd the twin). Reverted.The backing twin was where the bytes came back: its Rule-1 arms restated on the shared predicates deleted six helpers for −313 B with zero semantic change. The size ceiling with the whole owner-context arm removed (semantics broken) was −738 B; the remaining ~400 B is the container choice for
in/Object.keys/ descriptors under a hold, which no node path can replace. S6 (A28 at the backing, ruled store-follows-signal) is #3526, stacked on this, kept separate so CodSpeed judges the fix apart from the consolidation (+283 B store).Gate (rebased over #3518 / #3522)
next(run per step)origin/nextin this worktree (compiler binary; unrelated)