diff --git a/.changeset/observer-row-container-sharing.md b/.changeset/observer-row-container-sharing.md new file mode 100644 index 0000000000..e36cca7d6e --- /dev/null +++ b/.changeset/observer-row-container-sharing.md @@ -0,0 +1,11 @@ +--- +'@tanstack/db': patch +--- + +Preserve live-query observer data arrays and lazy state Maps across status-only and health-only snapshot updates. Keep value and ordering changes visible while retaining point-in-time snapshots. + +Keep snapshot invalidation fresh across discarded Collection publications. Publish nested record prototype changes, while preserving equal writes with the same shape. + +Keep common equal object-valued Set comparisons linear, with polynomial matching for ambiguous unordered members. Preserve the exported deepEquals record policy while distinguishing prototype changes in Collection rows. Reject observer inputs without valid Collection revision clocks. Keep pooled live-query observers working when layout changes use their ordinary change stream. + +Compare every array position so replacing a hole with a defined value publishes the change and refreshes observers. diff --git a/docs/contributing/glossary.md b/docs/contributing/glossary.md index 0011c521a3..cc9c66f1e1 100644 --- a/docs/contributing/glossary.md +++ b/docs/contributing/glossary.md @@ -188,7 +188,7 @@ crossing subsystem boundaries; these comparisons are not interchangeable. | Term | Meaning | | --- | --- | | D2 value identity | The value domain compared by `hash` and `equalHashValues`, including their documented reference leaves. A matching hash digest alone does not establish identity. The [identity oracle](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/hash-identity-oracle.property.test.ts) defines its bounded value rules. Map/Set insertion-order sensitivity is pinned current behavior, not a universal D2 operator contract. | -| change-event equality | The `deepEquals` comparison used for Collection change-event suppression. Its supported domain ignores Map/Set insertion order, RegExp `lastIndex`, and array holes versus `undefined`. The [change-event equality oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/utils-oracle.property.test.ts) defines its value-class rules. | +| change-event equality | The `sameRowValue` comparison used for Collection change-event suppression. Its supported domain preserves record prototypes and distinguishes signed zero, while ignoring Map/Set insertion order, RegExp `lastIndex`, and array holes versus `undefined`. The [deep-equality oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/utils-oracle.property.test.ts) defines its shared value-class rules; the eager live-source conformance owner also checks row-value fidelity. | | draft equality | The comparison that recognizes a reverted draft value. Its [revert oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/proxy-revert-oracle.property.test.ts) preserves Map/Set insertion order, RegExp `lastIndex`, and array holes, with explicit snapshot rules for class instances. Native mutator methods have separate change-tracking laws. | | persisted-snapshot equality | The value-shape comparison used before skipping a reconciliation reload. It preserves array holes, enumerable array properties, and exact nested prototypes. Equal keyed class, null-prototype, or cross-realm records with different prototypes require reload; Map and Set insertion order remains significant. | diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index da2b6fbbbe..141556cd62 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -1573,6 +1573,68 @@ The separate `live-query-observer-snapshot-oracle.property.test.ts` owner checks unchanged getter identity with and without a completed DbClient query. It runs ordered/single-result and attached/detached histories through ordinary and server getters. It does not model provisional hydration replacement or host scheduling. +Its `live-query-observer.row-container-sharing` lane also checks the maintainer's +2026-11-01 metadata-sharing contract: loading-to-ready and health-only changes +keep data/Map references, while value writes replace them and retained snapshots +keep their old rows. Fixed and random histories cross subscribe/unsubscribe +transitions in granular/wholesale mode, zero through three rows, and +data-first/state-first reads. Equal-content source writes can replace virtual +row wrappers without advancing the row revision. The lane crosses that refresh +with attachment and metadata, and checks full-snapshot identity after actions +that change no public fact. Each campaign runs 24 shared examples and 40 +generated histories. Nonempty attach, detach, and reattach have declared reach +in each mode, including reach from fixed examples. Each history begins with +readiness before health reports; only writes change the independent key/version +model. A failure-list model checks fresh metadata and counts health-only +transitions independently. Readiness-only histories cannot satisfy that reach. +The original fresh-container implementation, same-cardinality reuse, and +unconditional entry replacement on attachment fail the shared comparison. +Equal-revision wrapper comparison also fails; always cloning the full snapshot +survives the old sharing lane but fails its added no-op assertion. +The same owner's `live-query-observer.capture-paths` property uses modelRows and +checkRead through an additional real-query driver. Versions and source ranks +independently predict ordered projection/findOne rows. Each fixed/random +campaign runs 16 shared examples and 40 generated histories, with two or three +rows and up to twelve generated actions. Histories cross initial attachment, +provisional hydration-seed handoff, equal writes, value writes, rank-only moves, +metadata and ownership actions. Declared reach distinguishes both observer +modes, both query shapes, initial attachment, both hydrated shapes, metadata +for every shape/mode, and attached/detached layout-only moves in both modes. +Discarded delivery deferrals add value and layout captures inside one or two +nested holds, followed by a second committed change with no intervening read. +Reach distinguishes each discarded change/mode/reattachment path and nesting. +The row clock stays monotonic across discard. Restoring an unpublished layout +clock advances that row clock, so its numeric reuse cannot retain stale rows. +The observer constructor rejects absent or non-finite row/layout clocks before client binding, row enumeration, subscriptions, or sync activation. Ordinary invariant witnesses cover each invalid clock, while the real Collection and pooled owners retain positive receiving evidence. This is rejection of a contradictory collaborator, not cross-copy compatibility or per-value instance tracking. The prior synthetic clockless input had no supported Collection API authority; +it is replaced by real initial attachment without lowering either 40-run +campaign or its 16 fixed examples. The ordinary synthetic Collection test now +uses an actual Collection and retains its freshness/retention assertions. +Checks after queued query publication compare rows, container identity, full +snapshot no-ops, layoutRevision, health freshness and retained seed/live views. +A narrow repair that trusts only the row clock fails the order-only comparison. +The same owner's `live-query-observer.record-shape` receiving lane enforces the +maintainer's 2026-11-03 decision: equal-content plain/class/null-prototype record +replacements publish updates; fresh equal values with the same prototype do not. +A shape-name/number model is independent of production equality. Each campaign +runs 16 shared examples plus 40 generated histories through both direct +Collections and projected live queries. Records, arrays, Map values and Set +members carry the nested record; granular and wholesale modes cross ownership, +metadata, replacement and refresh actions. Every checkpoint checks data/state +shape and values, a granular mirror when attached, container sharing, and +retained snapshots. Each receiving path has declared reach. The utility equality +owner agrees with this approved record-shape distinction; draft reconstruction +keeps its separate class/plain equivalence, and built-in/typed-array comparison +policies remain unchanged. Original equality fails both receiving comparisons. +This lane does not cover arbitrary built-in subclasses, descriptors, cyclic +payloads, optimistic draft assignments, or framework/provider scheduling. +The guarded replay manifest selects each property's random campaign directly. +Cleanup/reset may discard a row revision and is outside its sharing law. +Existing hydration and observer/layout witnesses retain their scopes. These core-only lanes add no new +framework scheduling promise. The independent Angular PR projects observer +row-state identity before copying its Map; combined integration evidence is +recorded separately from either branch's standalone validation. +The [row-container evidence](oracle-reviews/2026-11-01-observer-row-containers.md) +records RED/GREEN, declared reach, mutant rejection, size, and limits. Focused snapshot checks protect lazy state, one admission walk for an unchanged revision, explicit preload after a loading capture, JSON-compatible non-streaming transport, and pending-stream ownership. The observer history @@ -1846,9 +1908,9 @@ generic visitor behavior outside the named collectors. | Paced mutations | [virtual-clock oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/paced-mutations-oracle.test.ts) | `createPacedMutations` with queue front/back options, bounded waiting capacity at zero and one, cleanup draining in FIFO/LIFO order after a held write, cleanup timing at the configured wait for immediate writes, admitted writes after separate Collection cleanup, debounce and throttle schedules with explicit and omitted options, and first-leading throttle execution at epoch zero. Finite histories compare optimistic rows, returned transaction identity and receipt outcome, execution order and virtual-clock time. Held writes verify queue serialization. Leading-only throttle and debounce witnesses reject skipped calls immediately with their named dropped-call errors, including omitted edges, both edges disabled, and same-row rollback while a prior write is held. Capacity witnesses cover overflow rejection, distinct-key optimistic rollback, same-key admitted-write survival, and in-flight versus waiting admission. Cleanup witnesses check repeated queue cleanup, eventual admitted receipt settlement, direct queue strategy rejection of new callbacks, public post-cleanup rollback with `QueueDisposedError`, pending debounce transactions that wait until the last call's quiet edge after separate Collection cleanup, and a trailing throttle timer that runs at its regular edge after separate Collection cleanup. Deferred custom queue and batch callbacks check compatibility with `void` and `false` execute results, respectively. Frozen options verify factory non-mutation; a mutable option witness checks that debounce uses its construction-time trailing setting. Throwing `onMutate` histories check leading-edge reuse before any row change, failed-row restoration when a pending transaction already has admitted work, eligible leading writes behind a held handler, and throttle pending merges at the throw, start, receipt, and later-timer cuts. New debounce/throttle admission after cleanup, manual commit of a strategy-owned transaction, longer throwing/reentrant bursts, failed persistence, broader capacities and schedules remain outside this owner. | | React paced-mutation render cuts | [render-cut oracle](https://github.com/TanStack/db/blob/main/packages/react-db/tests/usePacedMutations-render-cuts-oracle.test.tsx), [PR #2170 review](oracle-reviews/pr-2170-react-paced-render-cuts.md) | `usePacedMutations` publishes callbacks only from committed renders and does so before descendant layout effects, while callback-only commits preserve mutate identity. An independent committed-document model crosses ordinary and StrictMode schedules with mutation before or after a suspended candidate, mutation after the candidate commits, and delayed or immediate mutation from a descendant layout effect. It compares ordered callback traces and public rows at the mutation cut, the trace after persistence, and mutate identity at the commit cut. Render-time publication, layout-effect-only publication, and split insertion/layout publication mutants fail at the intended comparisons. A commit between transaction admission and persistence, arbitrary React schedules, server delivery, and other frameworks remain outside this receiving owner. | | Optimistic state | [same-key composition](https://github.com/TanStack/db/blob/main/packages/db/tests/optimistic-composition-oracle.test.ts), [history model](https://github.com/TanStack/db/blob/main/packages/db/tests/optimistic-history-oracle.ts), [generated histories](https://github.com/TanStack/db/blob/main/packages/db/tests/optimistic-transaction-oracle.property.test.ts), [outcomes](https://github.com/TanStack/db/blob/main/packages/db/tests/optimistic-history-outcomes-oracle.test.ts), [publication](https://github.com/TanStack/db/blob/main/packages/db/tests/optimistic-history-publication-oracle.test.ts) | Optimistic state drops at settlement and held sync transactions publish with it ([settlement-drop review](oracle-reviews/2026-10-03-settlement-drop.md)); whole-row active overlays, metadata and prior-value events. A raw subscriber without initial state checks that each truncate batch inserts a key once ([2026-10-02 review](oracle-reviews/2026-10-02-truncate-raw-subscriber.md)). Never rebase a pending snapshot merely to simplify the model. Generated histories also write source batches inside mutation handlers, before they return; the Collection must own the request by then ([handler source batch review](oracle-reviews/2026-10-01-handler-source-batch.md)). The same-key transaction lane also authors a delete followed by a reinsert, which must be visible optimistically and net to the truth-table request ([state mutation round 2](oracle-reviews/2026-10-02-state-mutation-round-2.md)). Source rows are written as `insert` for a key the source lacks, and batches may delete keys the source holds ([state-stack mutation gaps](oracle-reviews/2026-10-02-state-stack-mutant-gaps.md)). Batches may also delete a key the source never held. That delete leaves an active request in place ([absent-key source delete review](oracle-reviews/2026-10-02-absent-key-source-delete.md)). A separate partial campaign keeps the default partial row update mode, where source updates may omit a field and merge into the source row, not into an optimistic row ([round 3 review](oracle-reviews/2026-10-05-state-mutation-round-3.md)). An aborted open batch restores the driver's own source rows as well as its keys, so a later partial update still omits the field ([aborted-batch review](oracle-reviews/2026-10-08-partial-update-aborted-batch.md)). A failed synchronous manual `mutate` callback is now checked after same-key update, distinct insert, and same-key delete, with and without earlier intent, plus two two-Collection cases, one with a restoration listener failure. The throw cut compares public rows and pending payload; the later commit checks only surviving intent. A restoration listener error retains the callback error as the aggregate cause and does not stop other Collection restoration. These witnesses allow transient notifications during the callback and do not cover nested throwing callbacks or async work after the synchronous scope. After every step the driver also checks, by transaction identity, that the Collection tracks exactly its unsettled transactions, so a failed or rolled-back transaction leaves the map ([failed transaction retention review](oracle-reviews/failed-transaction-retention.md)). The [transaction ownership oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/transaction-ownership-oracle.property.test.ts) owns release across two Collections: merged-away pairs, conflicting rollbacks with a throwing subscriber, asynchronous success and failure settlements with a throwing subscriber, rollbacks from a `truncate` listener during a sync commit, a reused transaction id, and a `mutate()` callback that throws, checked in the same call stack and after promises flush. It also checks the settlement error shape and that a settled transaction's set of tracking Collections is empty; its mutations still name their Collection, so neither owner claims the Collection is collectable. Focused [restoration ownership](https://github.com/TanStack/db/blob/main/packages/offline-transactions/tests/restoration-ownership.test.ts) witnesses check that repeated offline `mutate()` calls add to one transaction, that a completed restoration settles `when('settled')`, and that a restoration one Collection cannot track leaves no Collection tracking it. Its grammar also has direct writes (`insert`, `update` and `delete` outside a transaction), optionally with a throwing subscriber that may have been sent the rows first; it captures each direct write's own transaction and requires it to fail when the admission throws and to complete otherwise, counts handler calls (none for a rejected write), and keeps a mirror subscriber on each Collection that must match the rows after every step. A write whose admission throws rolls back only its own transaction and rethrows. The [change-event history oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/change-event-history-oracle.test.ts) generates local-only direct writes that meet a throwing subscriber, registered after the mirror, and requires the call to rethrow with its transaction completed and the rows following the model. Focused witnesses cover a local-only subscriber that writes the key it was told about, and a subscriber that throws a value that is not an Error; whether a throwing subscriber may stop a later subscriber is not decided. Open (approved limits): a direct write made inside another write's change subscriber reports its subscriber failure to the outer write; on a local-only Collection a direct write whose subscriber throws completes on the fast path but rolls back on the transaction path, and the change-event oracle generates only the fast path; a direct write admitted from inside a sync-commit listener, where the rollback's recompute is deferred, is not generated; the generated ownership histories read every receipt themselves, so only a focused witness (rolling back an empty manual transaction) checks that an unread receipt reaches no `unhandledrejection`; two Collection instances with one id, a Collection cleaned up before its transaction settles, and offline restoration have focused witnesses only; no generated history combines a held completed row with a second Collection. | -| Drafts and native values | [proxy](https://github.com/TanStack/db/blob/main/packages/db/tests/proxy-oracle.test.ts), [detachment](https://github.com/TanStack/db/blob/main/packages/db/tests/proxy-detachment-contract-oracle.test.ts), [iteration](https://github.com/TanStack/db/blob/main/packages/db/tests/proxy-iteration-contract-oracle.test.ts), [revert](https://github.com/TanStack/db/blob/main/packages/db/tests/proxy-revert-oracle.property.test.ts), [native methods](https://github.com/TanStack/db/blob/main/packages/db/tests/proxy-native-methods-oracle.property.test.ts) | Native-operation controls, exact patches and actual stored rows; alias/cycle/adversarial-key histories. Set and Map reorder histories check draft patches and stored iteration order after replacement and clear-and-readd; RegExp replacement/reversion histories check `lastIndex`. General native-mutator and symbol-write support is not established by a plain-object oracle. The revert owner generates assignment, revert, delete, nested, symbol-key, and `for...of` histories and checks `getChanges()` and the draft against an independent draft-equality model: ordered Map and Set contents (including Sets inside Map values), RegExp `lastIndex`, array holes, and symbol keys inside nested values. Map and Set mutators and top-level symbol writes are outside its grammar; array mutators appear only in the mixed block below. Its round-trip grammar also replaces a field with a new object and restores the original value through a nested write. The law requires every changed key, and permits a written key whose final value equals its original to be reported with that value: a false positive repeats the stored value, and a false negative loses a write ([review](oracle-reviews/proxy-revert-replaced-object.md)). A further revert block mixes array `push`, `pop`, and `reverse` with assignments, a nested object replacement, and a retained handle, and checks that a mutator through a replaced handle changes nothing; pinned witnesses require a native write through a Map or Set value, a typed-array view, or a frozen draft to be reported, and a nested revert inside a Map or Set to keep the container's other write; Map and Set mutators are not mixed with assignments, and an equal-valued assignment keeps a retained handle attached (declared). The proxy owner's stored-function law checks, against a native row, that a function stored as data reads back as stored by every read path and that a stored method sees the draft as `this`, so its writes are tracked. The detachment owner pins which key classes a draft copies: enumerable string keys and every symbol key. The revert owner also generates typed arrays (including a subclass that does not forward its constructor argument), URLs, and a class with only private state as written values, with a property that writes two of them over one field. The native-methods owner generates rows (holes, `undefined`, duplicates, nested arrays, empty rows, typed `-0` and `NaN`) and calls every method of `Array.prototype` and `TypedArray.prototype`, in fixed and random campaigns with replay; it compares the result, whether the method returned the array itself, the identity of each original object, and the published row with native values, and a new built-in method fails until it has arguments. The proxy owner's `defineProperty`, stored-function, and frozen-draft laws are bounded tables against native rows. Its `defineProperty` law compares result, value, descriptor, and changes, for values and accessors. The iteration owner checks that array iterators see a push, removal, index write, or length cut made while they are open. Open: a draft reads a class instance of the original row as a plain object, so private-field getters read `undefined`, and a built-in method called through `this` on a nested Map or Set draft rejects the Proxy receiver; a mutator that changes nothing publishes an equal value; writing back a keyless class instance of the original row is a change, and `DataView` setters are untracked ([draft proxy review](oracle-reviews/code-weight-draft-proxy.md)). | +| Drafts and native values | [proxy](https://github.com/TanStack/db/blob/main/packages/db/tests/proxy-oracle.test.ts), [detachment](https://github.com/TanStack/db/blob/main/packages/db/tests/proxy-detachment-contract-oracle.test.ts), [iteration](https://github.com/TanStack/db/blob/main/packages/db/tests/proxy-iteration-contract-oracle.test.ts), [revert](https://github.com/TanStack/db/blob/main/packages/db/tests/proxy-revert-oracle.property.test.ts), [native methods](https://github.com/TanStack/db/blob/main/packages/db/tests/proxy-native-methods-oracle.property.test.ts) | Native-operation controls, exact patches and actual stored rows; alias/cycle/adversarial-key histories. Set and Map reorder histories check draft patches and stored iteration order after replacement and clear-and-readd; RegExp replacement/reversion histories check `lastIndex`. General native-mutator and symbol-write support is not established by a plain-object oracle. The revert owner generates assignment, revert, delete, nested, symbol-key, and `for...of` histories and checks `getChanges()` and the draft against an independent draft-equality model: ordered Map and Set contents (including Sets inside Map values), RegExp `lastIndex`, array holes, and symbol keys inside nested values. Map and Set mutators and top-level symbol writes are outside its grammar; array mutators appear only in the mixed block below. Its round-trip grammar also replaces a field with a new object and restores the original value through a nested write. The law requires every changed key, and permits a written key whose final value equals its original to be reported with that value: a false positive repeats the stored value, and a false negative loses a write ([review](oracle-reviews/proxy-revert-replaced-object.md)). A further revert block mixes array `push`, `pop`, and `reverse` with assignments, a nested object replacement, and a retained handle, and checks that a mutator through a replaced handle changes nothing; pinned witnesses require a native write through a Map or Set value, a typed-array view, or a frozen draft to be reported, and a nested revert inside a Map or Set to keep the container's other write; Map and Set mutators are not mixed with assignments, and an equal-valued assignment keeps a retained handle attached (declared). The proxy owner's stored-function law checks, against a native row, that a function stored as data reads back as stored by every read path and that a stored method sees the draft as `this`, so its writes are tracked. The detachment owner pins which key classes a draft copies: enumerable string keys and every symbol key. The revert owner also generates typed arrays (including a subclass that does not forward its constructor argument), URLs, and a class with only private state as written values, with a property that writes two of them over one field. The native-methods owner generates rows (holes, `undefined`, duplicates, nested arrays, empty rows, typed `-0` and `NaN`) and calls every method of `Array.prototype` and `TypedArray.prototype`, in fixed and random campaigns with replay; it compares the result, whether the method returned the array itself, the identity of each original object, and the published row with native values, and a new built-in method fails until it has arguments. The proxy owner's `defineProperty`, stored-function, and frozen-draft laws are bounded tables against native rows. Its `defineProperty` law compares result, value, descriptor, and changes, for values and accessors. The iteration owner checks that array iterators see a push, removal, index write, or length cut made while they are open. Open: [#2349](https://github.com/TanStack/db/issues/2349) reproduces an optimistic nested sibling edit converting an untouched class/null-prototype leaf to a plain record, then restoring its shape at sync confirmation. The maintainer identified this as a bug and explicitly kept its repair outside #2346. The draft/revert owners must add generated sibling edits and pending/confirmed shape observations while preserving isolation. A draft also reads a class instance of the original row as a plain object, so private-field getters read `undefined`, and a built-in method called through `this` on a nested Map or Set draft rejects the Proxy receiver; a mutator that changes nothing publishes an equal value; writing back a keyless class instance of the original row is a change, and `DataView` setters are untracked ([draft proxy review](oracle-reviews/code-weight-draft-proxy.md)). | | Query DB and observer | [ownership](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/ownership-lifecycle.oracle.test.ts), [persisted direct-write window](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/persisted-direct-write-window.oracle.test.ts) ([review](oracle-reviews/issue-2029-direct-write-lock-window.md)), [load lifecycle](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/load-subset-lifecycle.test.ts) (fixed tests; no oracle owner (tracked in #2264)), [observer histories](https://github.com/TanStack/db/blob/main/packages/db/tests/live-query-observer-history-oracle.property.test.ts), [retained snapshots](https://github.com/TanStack/db/blob/main/packages/db/tests/live-query-observer-snapshot-oracle.property.test.ts) ([review](oracle-reviews/pr-2043-retained-observer-snapshots.md)) | Real QueryClient boundary, applied-settlement barriers for existing and cached demand, mutation-handler Collection identity, terminal fetch-record lifecycle, and a per-listener eligibility ledger, not a duplicate dispatch queue. The bounded handler grammar crosses insert, update, and delete through handler parameter and mutation-alias access. All calls explicitly refetch; former `refetch`/`clearError` coordinates remain only as historical replay labels, preserving all 12 identity and 12 mutation-overlap executions. The ordinary [health scenarios](https://github.com/TanStack/db/blob/main/packages/query-db-collection/tests/ownership-lifecycle.test.ts) observe exact public causes and settlement. A held refetch retains the current public error. A failed initial load, including a failed initial Collection application, remains a loading error for that sync run regardless of later fetch results. After readiness, a pending retry retains the prior failure reason; failure reports the later cause, and successful result application clears it. Query retry attempts and an older held success are crossed with newer failures, including reuse of one Error value in one clock tick. These scenarios supplement the oracle; they do not claim removed utility fields or error counts. Native SQLite persistence, concurrent retries across multiple tracked Queries, mutation-handler fetch-boundary settlement, and deferred result application remain outside this witness. The mutation overlap grammar crosses both start orders. Check reentry, peer survival, FIFO and disposal independently of final rows. A retained observer snapshot shows its capture-time rows on a later first read of `data` or `state`. Direct-write witnesses now distinguish an idle pending eager cache seed (success with no new freshness timestamp) from an in-flight pending request, and require update, delete, and upsert to reach both eager Collections through their shared physical Query, in both acquisition orders. They check both public Collections and the Query cache immediately after each write. A held older shared fetch also crosses B's release and reacquisition, then compares both public rows at refetch and load settlement; mixed select wrappers and three or more co-owners remain outside this witness. The load-lifecycle tests also restore an intermediate checkpoint that keeps an explicit subset refetch pending until a post-write request applies, and rejects a no-cancel mutant at the replacement-request assertion. . Two sync-metadata session witnesses fail on `30175be48`: a claim that inserts a row keeps applied non-owner metadata, and a claim during a held retention cleanup publishes the cleaned owner (see Sync metadata session) | -| Observer hydration under terminal statuses | `packages/db/tests/live-query-observer-hydration-oracle.property.test.ts` | ARCHITECTURE.md law 16 and the glossary "crash": while a hydration seed is active, a Collection `error` or `crashed` shows in the snapshot, attached or detached, and is announced; an attached observer that sees the failure ends the seed, so cleanup then shows `cleaned-up` and live rows, not the stale failure or the seed. Decision (not a law-16 claim): `cleaned-up` and restart under an active seed keep the seed as `ready`, because cleanup is not a failure and the next sync run's readiness hands off. Generated histories cross both modes, attach order, live writes, `markReady()`, `fail()`, `crash()`, cleanup, and restart, with declared reach; replays pin the crash, fail-then-cleanup, and crash-before-attach traces. Limits: rows under a failure status are not claimed; the source is a plain controlled Collection, not a live-query graph or an on-demand subset; streamed client errors, handoff timing, and framework adapters stay with the focused observer and adapter tests; the property is not in the `oracle-config.ts` path-replay registry. | +| Observer hydration under terminal statuses | `packages/db/tests/live-query-observer-hydration-oracle.property.test.ts` | ARCHITECTURE.md law 16 and the glossary "crash": while a hydration seed is active, a Collection `error` or `crashed` shows in the snapshot, attached or detached, and is announced; an attached observer that sees the failure ends the seed, so cleanup then shows `cleaned-up` and live rows, not the stale failure or the seed. Decision (not a law-16 claim): `cleaned-up` and restart under an active seed keep the seed as `ready`, because cleanup is not a failure and the next sync run's readiness hands off. Generated histories cross both modes, attach order, live writes, `markReady()`, `fail()`, `crash()`, cleanup, and restart, with declared reach; replays pin the crash, fail-then-cleanup, and crash-before-attach traces. Limits: rows under a failure status are not claimed; the source is a plain controlled Collection, not a live-query graph or an on-demand subset; streamed client errors, handoff timing, and framework adapters stay with the focused observer and adapter tests; the property is not in the `oracle-config.ts` path-replay registry. Known unresolved cleanup/restart witness [#2334](https://github.com/TanStack/db/issues/2334): empty hydration seed; attach; ready; write `b=0`; cleanup; restart; write `b=0` again. Replaying the shared model on `162eebf39` fails in both modes. A diagnostic continuation that omits the disputed loading checkpoint and then marks ready still fails: public Collection rows contain `b=0`, while the attached observer retains no rows. The row clock advances 2→3 but the observer capture stays at 2. This is outside metadata-only container sharing and remains open under this hydration/lifecycle owner; do not claim general cleanup/restart freshness. The next witness must distinguish retained subscription rows from the observer reset, extend generated repeated-key histories, and calibrate the repair at a ready checkpoint. The existing comparison is not relaxed. | | Query DB SSR dehydration | `packages/query-db-collection/tests/ssr-dehydration-oracle.test.ts`, [review record](oracle-reviews/issue-1950-ssr-dehydration.md) | A plain Query cache control, a live on-demand compound predicate, a signal-only request, and an ordered cursor with a custom comparator all retain their row through QueryClient dehydration, reconstruction of Seroval's emitted stream payload, and Query Core hydration from that payload. The on-demand query functions still receive the original IR, comparator, and signal; serialized metadata retains enumerable user metadata but excludes request options. The original implementation fails the compound and signal cases at the stream checkpoint. A serializable stream-only request-options leak passes the former chunk-count check but fails the current payload assertion. This owner covers successful Query cache entries at the installed Query Core 5.90.20 and Seroval 1.5.0 versions. A TanStack Start integration owner still needs the reporter's Router 1.171.33 and Seroval 1.6.8 path, browser Collection resume/refetch, and request cancellation across SSR. Query subset and pagination oracles remain owners of predicate/order/cursor meaning and supported value types. The issue's `shouldDehydrateQuery` workaround replaces Query Core's success-only default and admits pending/error queries; any documented workaround must preserve that default filter. | | Query DB ordered refetch | `packages/query-db-collection/tests/refetch-ordered-prefix-oracle.test.ts` | Issue #2136: after preload, and after `utils.refetch()` or a Query invalidation settles, an on-demand Query Collection behind a LEFT-joined, ordered, limited live query must show the window over the current backend rows. The design under test: query-db reports `invalidateLoadedSubsets()` when an ordered page ends earlier in the order than before, including when its keys stay the same, or when its keys change and it has a later page. Each ordered loader runs its bounded prefix repair. A persisting optimistic update or delete can hide the visible order change that otherwise starts repair. The `query-db.refetch-held-page-end` property constructs that same-key earlier-end overlap through the real QueryClient and core receiver, with 24 fixed and 24 random histories (reach minimum 24 each), numeric finite cursor pages, static/function keys, one/two order terms, limits 1–3, offsets 0–1, varied row IDs, and explicit refetch/Query invalidation. Each history requires the same-key earlier end on the deleted held boundary, a fresh gap between the two answers, a delivered report, and a subsequent real core repair acquisition. Forty-eight structural histories cross widths/offsets with delete/update/unrelated-delete/no hold and earlier/later ends. Six delete-gap width/offset states each require structural reach. Label-only edits are negative report controls. The [nine-item review](oracle-reviews/2026-11-02-query-refetch-nine-item-review.md) records the strengthened classifier, experiments, portable commands, and finite limits. The exact failing CI history replays the shared model and comparison. Both the original implementation and the restored equal-key guard fail the public source-prefix comparison after release. See [the held-page-end review](oracle-reviews/2026-11-02-query-refetch-held-page-end.md) for exact source bindings, evidence, and limits. Its model is a pure top-k of marked backend rows. A fixed and a random campaign generate rounds of marker flips (which serve continuation pages), backend changes while the provider lags, a second live query (`observe`) whose own demand can load a page's new row first, optional Query invalidation with further changes, and a refetch, across one or two order terms, a numeric or locale-collated string order, limits 1–3, offsets 0–1, and a static or function Query key. With an unchanged backend, a repeated refetch must serve no more rows, start at most one first-page request, and report nothing. A refetch or invalidation must never start a new full-source request. A coverage law runs at the same checkpoints: every backend row from the start of the order through the window's last row (every row, when the window is short) must be in the source Collection with its current values, so a latent gap fails before a marker exposes it; a named replay of the issue trace with the moved row unmarked passes the window law and fails only the coverage law when Query DB never reports. The grammar can hold an optimistic update across a refetch, with the checkpoint moved to its release, and a premise-directed campaign under a hold (other rows observed, a row moved ahead of the page's end, a new row further ahead, a write to the last-sorting observed row) requires a report during a hold in every case and fails a skip that reads rows held back behind the transaction; the general campaigns did not. The general grammar holds optimistic deletes as well as updates. The generated campaigns declare reach (minimum 3) for a lagging refetch after a continuation, one during a hold, and a refetch that reported while the window was full, where the loader decides between ignoring the report and repairing. A named replay of a refetch during a held delete left the window short after release, also with this PR's report and overlap changes reverted, until the fix that resumes a continuation after a synchronous answer; no window is checked during a hold, where #2148 and #2169 remain open. Reports (`loadedSubsets:invalidated`) are bounded: a step reports at most once, and only when an ordered reanswer changes key membership or moves its end earlier, so unrelated value changes do not justify reports; named replays require no report when a one-page window, or a window's last page, only loses a row, and a sync-boundary replay requires a report when a page ending on a row without an order value has its end move earlier (on the earlier gate, which read that page as empty, it reported nothing), and another requires a report when the provider returns a one-page window's rows in reverse order (the earlier gate took the last array row as the page's end). When a page's end moves earlier, Query DB reports. A loader whose window lies inside its own first page skips the repair only while its page read past the window or it read the tie of the current end; after an insert into a top-N list moves the end, a named replay requires the loader to prove the window again with bounded reads, no full-source read, and an unchanged Query count. A filtered one-page window whose page end moves past an unseen row must still repair (found by the random campaign; a loader that always skips fails it, the f9 tie replay, and the ordered-work oracle's signalling replays). The skip also refuses while the source holds accepted sync rows back: a replay refetches while an optimistic update persists, so the report arrives before the refetched rows are visible, and a skip that reads those older rows fails it. The ordered loader state oracle checks that a prefix read issued before an ordering invalidation, which settles after the bounded repair, does not retire the repair's prefix; the repair-first order failed before that fix. A `loadSubset` call that acquires a released, invalidated page can report, because Query then refetches that page on its own. A failed repair shows only the last window or the right one. Named replays cover the issue trace after a refetch and after an invalidation, with a page-capping provider, with another live query loading the page's new row first, with an empty later page, with a page whose end moves earlier while a tie request holds its old last row, a second refetch after an invalidation, a repair failure, and a provider that honors neither cursor nor offset. Never signalling, the old overlap-to-full-source rule, ignoring empty later pages, reporting on any page change, removing the repair loop guard, and reporting once per page each fail it by assertion. A replay makes a page's application fail and checks the window shows its last result or the right one, then the next refetch restores it; recording a page before its rows are accepted cannot be distinguished, because the failed attempt reports on its own. Open: the report laws bound reports but do not require each one; a page whose end moves earlier reports even with unique order values, where no row can be missing; it does not check whether a repair waits for in-flight work; the generated campaigns use an uncapped cursor provider only (an offset-only provider and a page-capping provider each hit a loader defect that reproduces on main: #2182, #2183); the generated campaigns do not check the window after a marker flip (a named replay checks the trace where the loader stopped refilling after a joined-side removal, which the repair-retirement fix below closed); a failed repair still recovers through the loader's full-source retry; interval, focus, and reconnect refetches share the invalidation path but are not driven; NaN order values have no executable witness; overlapping `refetch: true` acquisitions on one Query hash are counted, so the first to finish does not clear the loop guard for the other, but no witness separates that from a plain set; a Query refetch that joins a running `refetch: true` acquisition on the same hash does not report, and no witness reaches it (#2185); follower tabs of a multi-tab persisted collection never receive the report (#2174). The offset-only and page-capping loader defects have `it.fails` witnesses in the oracle. A finished bounded repair used to discard its boundary when its retirement made Query DB remove a row the loader had sent; the loader then restarted paging with a smaller first page that could not retire the repair's prefix, so every later refetch read both. A named replay found by the random campaign (at four times the run count) and an ordered loader state witness fail on that, and the fix also closed the joined-side refill defect. At four times the run count, the random campaign found a short window that stopped paging before the last row of the order: a report's repair released continuation Queries, and Query DB answered their reacquisition synchronously from its cache, so the graph turn inside each request asked for more while the loader was still requesting and nothing resumed the staged continuation. Only the coverage law failed. A named replay and an ordered loader state witness (a synchronous answer whose writes the graph already processed must continue the chain) fail on that, and pass with the fix. The loader side of the report also has a second owner: the ordered-work oracle's `refetch-signal` provider re-runs each served request alone and reports when any answer changed, across that oracle's generated plans, and a signalling refetch must start no full-source request unless a bounded page in that step ended on a null or NaN rank, which takes the documented full-source fallback (a random seed found that case). Never signalling and the old overlap-to-full-source rule each fail it. It excludes a peer live query, which can keep a short window after a refetch (#2155). The persisted-collection oracle checks that a source's report reaches the Collection through the managed cache-generation wrapper and that a retired source session's report is dropped. Remaining work for this class is tracked in #2186. | | Ordered acquisition | [pagination](https://github.com/TanStack/db/blob/main/packages/db/tests/query/pagination-oracle.property.test.ts), [ordered work](https://github.com/TanStack/db/blob/main/packages/db/tests/query/ordered-work-oracle.property.test.ts), [ordered lifecycle](https://github.com/TanStack/db/blob/main/packages/db/tests/query/ordered-lifecycle-oracle.property.test.ts), [ordered loader state](https://github.com/TanStack/db/blob/main/packages/db/tests/query/ordered-source-loader-state-oracle.test.ts), [graph scheduler](https://github.com/TanStack/db/blob/main/packages/db/tests/query/scheduler.test.ts), [issue #1880 ordered-repair review](oracle-reviews/issue-1880-ordered-repair.md), [issue #1882 custom local collation review](oracle-reviews/issue-1882-custom-local-collation.md), [issue #1898 relation-filter review](oracle-reviews/issue-1898-relation-filter.md), [PR #1909 external review](oracle-reviews/pr-1909-external-review.md), [eager ordered-window repair review](oracle-reviews/perf-ordered-window-repair.md), [reversed index reads](https://github.com/TanStack/db/blob/main/packages/db/tests/reverse-index-oracle.property.test.ts) | Complete finite provider results, inherited locale and bounded/unbounded custom collation with exact request options, query-level inheritance across source aliases, real lexical/numeric disagreement, custom comparator ties, scan/index paths, pending windows, ties/nulls, lease ownership, Effect callback gates, including an indexed source update during Effect startup, and documented repair timing. Direct LEFT-joined filters cover local finite prefixes, pending child demand, child removal, and anti-join publication with custom full-source and unindexed fallback loading; unrelated include demand cannot stall the root continuation, and an empty joined replay wakes held Effect callbacks; remote relation hints remain outside this owner. Graph scheduler checks coalescing, dependency order, synchronous loader input, and repeated-alias failure ownership. The ordered-work oracle checks that a synchronous root refill reaches D2 before joined publication; a mutant that omitted the post-loader graph step failed at the second child demand. Custom collation proves local full-source acquisition only; it does not establish provider cursor capability or backend collation fidelity. Sibling-source input during a synchronous ordered continuation returns to graph work before another ordered acquisition. Request completion is not proof of unrequested source extent. A window move during existing joined demand waits for its root continuation, including an independently held root request; current demand failure rejects the move, while retirement and an obsolete rejection do not. A bounded ordered repair resumes its refill after joined demand settles. The ordered-source-loader state test checks that an explicit failed-request retry blocked by joined demand retains its generation until it can issue a request. Multiple simultaneously pending joined plans during a window move and a public-query failed-retry overlap still need dedicated witnesses. The ordered-work oracle also checks an eager indexed window over generated upserts and deletes: the rows equal the filtered, sorted model, and each change delivers a bounded number of source rows; an unbounded or always-full repair read fails it. Its filter dimension adds a function filter and a LEFT-joined filter, and a campaign fixed to the joined premise checks that a window refilled past a distant row the query already received holds the first eligible rows. Joined filters also run as anti-joins, change eligibility through the marker or the root row's join key, and drive an Effect beside the Collection. A full-join filter keeps every root row and adds a null-ranked rootless row for each marker no root row matches; on main a bounded root prefix showed markers whose root rows were not yet loaded as unmatched rows, and restoring the old inner-or-right full-source rule fails eight tests. Null placement is a generated dimension, with the index built to match, and a query that ignores it fails six tests. A full-join campaign has few or no filler rows, a matched null-ranked root row and at least one orphan marker in every case, so its windows reach the null group where rootless rows and null-ranked root rows tie; a reconstruction check samples exactly the cases its fixed-seed run executes and requires such windows (19 rootless and 7 mixed null-group windows of 40 at one times the run count), and the Effect runs beside the Collection. Full joins read their full source, so only the rows law covers them; paging them under the old join rule fails the window law. Pinned cases cover a null-ranked root row past a prefix that ends in the null group. Paging a nulls-last full join passed this oracle but was not adopted: it cost more per delete when the window reached the null group, which no work law here measures, and #2130 tracks full-join paging. Joined filters can match a row through several markers, and the model keeps one result row per matched pair; a model that collapses them fails. The eager harness also runs an on-demand source whose provider follows the LoadSubsetOptions contract (offset only without a cursor, the applied receipt returned). Its test provider combines a sync adapter and its provider (glossary model alignment rule 2); a held request is a physical acquisition whose provider answer is delayed, a served range is the prefix and boundary one answer establishes (a model abstraction), and the peer is a second live-query Collection. The provider sorts by the requested order and keeps its own ledger of sent rows. A forwarding provider sends live changes only for rows it sent or rows that match a served subset, and its served rows obey the work bound; a loader that asks for 100 extra rows fails it. A refetching provider, like query-db-collection, sends nothing until a refetch. The refetch keeps every served ordered range whole, as the LoadSubset contract requires: it sends every row from the start of the order through the last order value a limited page returned, and rows in no range leave; the rows law over the model applies once no change is pending. After every step on either provider, the Collection must hold exactly the rows in the provider's ledger, and the query and the Effect must show their window over those rows; a mutant that drops marker removals fails this law at steps 0 and 1 of the fixed refetching campaigns, earlier than the model's rows law, and in one more campaign. A holding provider answers each load only at an `answer` step, from the rows at that moment, so live changes arrive while loads are in flight. While a load is in flight, the query and its peer either keep the window they showed at the previous checkpoint or show distinct ledger rows that pass the query, in its order; the Effect is not checked then. That is weaker than the architecture's barrier rule, under which a repair or window move keeps the last public snapshot until it settles: a barrier law fails today on on-demand joined plans, where a visible-delete repair and a held `setWindow` publish before they settle (#2131; the law and traces are kept for that fix). The provider records a request as served only once it answers it, and an abort (or an adapter unload) of a held request follows `LoadSubsetOptions.signal`: a cooperative provider installs none of its rows and its load fulfills at once, while a provider that cannot cancel in-flight work (`late` cancellation, generated) answers it anyway and its load fulfills after those rows become visible. A `release-peer` step cleans up the peer while its loads are held, which aborts them; each request counts once although its acquisition release both aborts and unloads it. The fixed held campaign reaches 5 aborted held requests and 1 late answer, and its reconstruction check requires at least 3 cooperative cancellations and 1 late answer. A held load rejects when its applied receipt rejects, and the step then fails, because no signal or cleanup rejects a receipt in these histories; that branch is unwitnessed. The held campaign pins a forwarding provider, so `release-peer` never reaches a refetching provider; that branch is unwitnessed. A request whose signal is already aborted when the provider receives it is never reached; that branch is unwitnessed. The held campaign uses a forwarding provider and an indexed source filter or a joined filter, deletes three visible rows and then upserts an eligible row with an id no other command uses; its reconstruction check runs in the same test as the fixed-seed campaign and reads its per-case counts: 35 of 40 cases issue a held request, and every one of them changes rows while it is in flight. Loading must reach a fixed point: past a request budget the provider rejects and records it, and the initial load, every step and the end of each history assert the record is clear (a loader that re-requests a prefix it already holds can loop without reaching the provider, which this cannot see). A bounded step makes at most four provider requests, and each consumer's initial proven-prefix load is bounded separately, in total transfers (a repeated request for the same prefix counts) and in distinct rows (at most the source's size); a loader that asks for 100 extra rows on every page fails that bound and the initial-load work law. Loader mutants that never resume after a boundary request, or that ignore a load request while another load is pending, fail it; a mutant that drops a window move blocked behind a pending load still survives, even with the settlement check on held moves; whether it is equivalent is open. Open (#2148): on an on-demand source with a joined or anti-joined filter, an optimistic delete of the visible row leaves the window short until the delete persists, because the next row is never loaded. On an on-demand source with a source filter, and on an eager source with any filter, the same delete refills the window correctly; these are the contrast cases. Owner: this oracle. Needed witness: the optimistic-delete dimension (kept as a patch for #2148) in the grammar, plus a pinned replay of the anti-join trace. A refetch that re-runs each page alone, as query-db-collection does today, leaves a row that moved between two pages unloaded; random seeds found that trace, a named replay fails on it, and a query-db receiving witness reproduces it (#2136 tracks the provider fix). An Electric-style provider, which skips updates to rows it never sent, is not modeled; a mocked-stream witness shows a row moving into a served ordered range never arriving under the default replica mode (#2138). At one times the run count, the campaigns reach 87 refetch steps: 39 change the window, 6 remove a visible row, and 13 lead to a further provider request. A random seed found a refetch that empties a joined window, after which the loader refills it over several page requests and publishes `[]`, one row, then the final rows; a named replay keeps that trace for #2131. No incorrect final window has appeared on that path, and no loader mutant tried so far is caught only by it; the work law does not cover it. For an on-demand joined or anti-joined filter, the initial load must not request the whole source when the first window's worth of source rows in query order all pass the join and no boundary value is null or NaN; 35 executed cases meet that premise, and a compiler that reads the whole source for every joined filter fails eight tests on this law alone. The rows served by that initial load stay within the prefix, its boundary ties and a refill, for each consumer. Later steps of those plans have no work bound. An unindexed join filter over it left the window empty on main, until the plan counted the rows it still needs; restoring the old guard fails the campaigns and a pinned replay. A step that changes the visible rows must publish, and its subscriber starts with every visible key, so a removal-only change reaches it; mutants that drop update-only or removal-only publications fail. A step may publish one intermediate window per provider request it made, and only its last publication must show the final window, because a repair or refill can publish after each round trip (#2131); the content of those intermediate windows is unchecked. The model keeps its own copy of the markers, and full-join histories can insert the root rows of orphan markers. A premise-directed campaign inserts a fresh row and then changes an initial row's eligibility alone, so the row leaves the window only through the join; it and a named replay fail when a window row past the loader's settled boundary counts toward a full window. One case in four adds a second live query over the same sources with a larger window, and its rows law holds at every checkpoint; the work laws do not apply to those cases. #2155: a second live-query Collection over a refetching on-demand provider kept a short joined window after a refetch removed a tied row (the peer showed ranks `[0, 0]` where the model shows `[0, 0, 100]`). The loader fix that resumes a staged continuation after a synchronous answer the graph already processed closed that trace. The pinned replay of that indexed trace now expects the right window. Still open: an unindexed peer over a refetching provider keeps a short joined window (the peer shows `[100]` where the model shows `[100, 101]`), which also fails without that fix; each campaign therefore still excludes the peer and refetching pairing, as its last map. Owner: this oracle. Needed witness: a pinned replay of the unindexed trace flipped to pass, and the pairing restored to the grammar. Limits: Effects are checked for rows after each step, not for publication counts or positions. Histories move the first query's window with `setWindow` (150 moves at one times the run count, 56 on demand), and its rows law and ledger law then follow the moved window; the Effect and the peer keep theirs. On a holding provider every window-move promise must have fulfilled after the final drain. A refetching provider models query-db's unload: it drops the unloaded request and removes rows that no remaining request's last answer holds. The per-step work law applies only to forwarding, immediate, single-consumer cases: it skips refetching, holding and peer cases and move steps; a two-term indexed move scanned the source four times, which no law here bounds. Multi-term joined plans stay outside this harness, on both paths; on the on-demand path, the provider's transfer is bounded only for indexed single-term source-predicate plans. Joined windows compare order values, so a wrong row tied with the expected one passes; a window row tied with the boundary counts as proven before its tie request settles, which no witness exercises. A boundary that cannot advance past a window row would repeat one page per new key; reading the boundary's range locally should always reach that row, but no witness shows it. Joined ties are compared as any valid first rows; whether source-filter ties may be fixed to the source key is open. The same grammar includes NaN and null ranks and a second-order term, and bounds both rows read and rows delivered, including the initial load and index refills; a scan law limits each change to one pass over the source. A pinned case checks that an indexed descending window places nulls first, and a lifecycle witness checks that an eager full-source window is ready with its rows at creation when its source answers synchronously. The initial fallback cells check indexed and unindexed locale strings with distinct and tied values, plus a null boundary after a warm ordered request. The warm null-boundary case crosses preload-started and constructor-started live-query sync runs: the former completes at the synchronous cut, while the latter resumes deferred on-demand acquisition asynchronously and publishes after settlement. They require an unfiltered full-source request, no public row at fallback entry, and the expected row and status at the immediate readiness check. A Promise fallback stays loading at that checkpoint. A React receiving witness checks an eager locale-string `findOne()` at the first layout commit. A Query DB receiving witness checks warm cached subsets: both raw adapter calls return literal `true` with the source rows visible at each return, and the live query is immediately ready without a fetch. The same fallback cells observe subscriber changes from before the first source write: literal-true and Promise fallbacks each publish one nonempty final-window insert after the fallback settles, and the callback sees the complete row. Promise cells show no nonempty event at the immediate cut; a duplicate-event fault fails all eight cells. Other adapters and a real on-demand React first commit remain unwitnessed. Full-source work after initial publication and repair remain outside this synchronous guarantee. A separate post-publication oracle crosses marker deletion and retargeting with literal, Promise, and held-applied-receipt fallback responses. It also settles the child demand before holding the receipt, so the public live-query loading flag isolates the root fallback. The model checks final rows and net events, while event replay checks every callback snapshot; a retained or empty intermediate window is allowed. A loader fault that ignores the pending receipt fails at the held loading-flag check. Two literal-true cleanup-failure histories require that the replacement still publishes and the adapter error reaches the public query status without making the unrelated marker commit throw; the provider's applied receipts are recorded and compared outside provider callbacks. The Promise case checks eventual coherence on an asynchronous callback path, without claiming a separate timing cut. Four held-Promise histories apply fallback writes, change another join marker before provider settlement, and compare each callback and the final winner; an injected stale extra publication fails the event comparison. Other marker actions and overlapping root-source changes remain open; the provider has no later write or rejection, so this schedule does not establish when the loader claims full-source authority. The reversed-index oracle checks every reversed read's order and filter work for both null placements. The retirement-invalidation lane (`ordered-loader.retirement-invalidation`) composes a constructive invalidation prefix, up to four settled answers that let the repair chain complete, and generated command suffixes with index availability, limit 1/2, joined deferral, needed rows, release errors, reentrant truncate and disposal, reentrant ordinary passes, retry timers, offset 0/1, and full-source plans. Its declared `completed-repair-release` reach counts only an invalidating release observed while a successful repair-chain answer settles and issues no further chain step; an instrumented run matched the production retirement path in 66 of 72 such histories, and the rest owed nothing more after a late disposal or full coverage; an ablation that invalidates only on other releases passes the older `release-invalidation` reach and fails this one. Only an on-demand source answers a release with an adapter unload, so neither this lane nor the broad `ordered-loader.request-authority` arbitrary pairs `releaseInvalidates` with a synchronous repair answer, which reads an eager source whose releases bypass the adapter (`CollectionSyncManager.unloadSubset`). The broad arbitrary weights `releaseInvalidates` 5:1 like the other release reactions, because an invalidating release leaves the quiet report laws. The directed lane also places the invalidation after the callback's disposal, reentrant pass, and truncate (`late-release-invalidation`). Broad overlap states require 20 histories each, and the rarer full-source-plan overlap requires 5. An invalidating unload also writes graph input, so the driver's drain runs one more loader pass before it judges progress, as the live-query Collection and Effect graph loops do; without that pass, a repair that finishes inside a loader pass after joined work clears was misjudged as owing no further read. Its independent request ledger records issue and invalidation events on one clock. It requires an authoritative start after the callback's invalidation for a live finite repair after retry/drain; complete full-source coverage absorbs invalidation, cursor reset transfers pre-reset work to replay, disposal ends ownership, and unanswered authoritative work retains its existing no-timeout limit. Each normal broad campaign measures callback overlap with release error, truncate, disposal, pass reentry, offset one, timer-enabled scenarios, full-source plans, and multiple successor chains. A separate `ordered-loader.retirement-receiver` property runs real live-query Collection and Effect consumers, and a live-query Collection whose `setWindow` to the other offset starts in the same turn as the ordering write. It varies indexed/unindexed sources, limit 1/2, offset 0/1, literal/Promise adapter answers, a held newer replacement answer, and the unload's change: moving the repaired prefix, deleting its rows before the boundary, deleting the boundary row, or deleting a prefix row that ties with it, each of which owes a replacement, or evicting a sent row past the repair's boundary, including a row that ties with the boundary but lies outside the prefix, which keeps that boundary and starts no read. A tie row sorts before or after the boundary row by key, as live-query Collections order ties. A same-turn window move may repair either window's prefix, so that kind keeps only the move and row 1's eviction; prefix deletes and ties run on the other kinds. Each consumer has separate declared adapter-unload and held-answer reach, and delete, boundary-row delete, tie delete, tie eviction, and eviction unloads have their own reach; each grammar runs 600 histories. A loader that skipped every retirement-time delete fails the delete histories at the progress relation, one that treated every sent-row delete as new information fails the eviction histories, one that treated every boundary tie as eviction fails the boundary-row and tie deletes, one that treated every boundary tie as new information fails the tie evictions, and one that kept only the boundary row's key fails the tie deletes. The loader keeps the keys of the settled prefix rows that tie with its boundary row. The focused `keeps a repair's boundary when its retirement removes a row the loader sent` test remains as a supplemental example. An instrumented run placed every invalidating receiver unload inside retirement (200 of 200 per campaign). Following Atomic window publication, the window move stays pending while the replacement is held and settles with its chosen window visible; a mutant whose subset loads stop joining the active window operation fails only those move comparisons. Held-answer reach is recorded only after the provider answer is blocked and the prior-window and no-publication comparisons pass. The provider moves its repaired prefix behind a still-uninstalled row during unload and supplies no later external graph input. The shared progress relation checks that an authoritative start follows the invalidation and precedes the first replacement publication. Independent full-provider sorting checks final rows; held answers retain the previous complete public window and publish no callbacks, and completion publishes one complete replacement. Fixed replay witnesses force the held-answer premise for each consumer. An executed public-method call probe observed reentrant `loadMore` inside every sampled unload for both consumers, with no production-state reads. Original debt-clearing and deliberately delayed-one-pass mutants fail the progress/publication judgments; start observations before the invalidation or at publication fail calibration. This evidence does not require recursive synchronous dispatch: deferred replacement remains legal while the public gate holds. The replay-continuation lane (`ordered-loader.replay-continuation`) follows ARCHITECTURE.md, Ordered requests/continuation/recovery: a pre-reset request belongs to truncate replay and cannot revive its discarded cursor; a graph continuation survives bookkeeping until a graph pass consumes it or cursor reset discards it. `continuationOwed` is model-only classification provenance, not product completion state. The unchanged handoff/reset grammar and exact shared-model replay reject consuming that provenance during replay bookkeeping. This is a harness correction; it supplies no production resource-leak claim. Its `deferred-refill` state is reached only when a synchronous repair answer meets a release-triggered truncate (6 of 96 inputs). An eager source's release never reaches the adapter (`CollectionSubscription.createSubsetAcquisitionRecord`), so production cannot produce that pairing, and the lane calibrates continuation provenance without a production-reach claim. The broad request-authority arbitrary pairs synchronous repair answers with release faults, truncates, disposals, and reentrant passes in the same over-approximating way; those histories pass and support no production-reach claim either. The random handoff campaign runs 400 histories so that state stays above its minimum. Limits: the real receiver uses distinct finite numeric ranks, ascending root order, one source and consumer, one qualifying callback invalidation, no failure/truncate/disposal inside the real unload callback, one window move per history and no failed replacement under a move, and no provider-specific network. Its on-demand source settles every repair answer asynchronously in the loader, even a literal adapter answer; the loader's synchronous repair settlement needs an eager source, which has no adapter unload to invalidate. The controlled grammar measures the combined lifecycle cases; it does not transfer all such combinations across the public receiver boundary. No claim about obsolete later cursor-demand retirement (#2223) or arbitrary callback composition follows. Existing run budgets remain unchanged. Random samples do not establish bounded exhaustiveness. The controlled and receiving grammars have distinct registered replay properties, so each seed and shrink path selects only its own driver. The no-leak calibration supplies a retained-lease observation to the same request-ledger comparison after an unmodified run of the same covering-repair history; it reads no private member names. | @@ -1872,12 +1934,12 @@ generic visitor behavior outside the named collectors. | Backend Collection E2E | [shared provider suites](https://github.com/TanStack/db/tree/main/packages/db-collection-e2e/src/suites); [Electric registration](https://github.com/TanStack/db/blob/main/packages/electric-db-collection/e2e/electric.e2e.test.ts); [Query registration](https://github.com/TanStack/db/blob/main/packages/query-db-collection/e2e/query.e2e.test.ts); [TrailBase registration](https://github.com/TanStack/db/blob/main/packages/trailbase-db-collection/e2e/trailbase.e2e.test.ts) | Electric, Query, and TrailBase register reusable predicate, pagination, join, and live-update checks at their provider boundaries; Electric also registers the move suite. These are receiving paths for core query and Collection laws, not framework render cuts. | | Window-controller overlap | [shared infinite-query conformance](https://github.com/TanStack/db/blob/main/packages/db/tests/conformance/infinite-suite-oracle.ts), [React driver](https://github.com/TanStack/db/blob/main/packages/react-db/tests/infinite-query-conformance.test.tsx), [Vue driver](https://github.com/TanStack/db/blob/main/packages/vue-db/tests/infinite-query-conformance.test.ts), [Svelte driver](https://github.com/TanStack/db/blob/main/packages/svelte-db/tests/infinite-query-conformance.svelte.test.ts), [Preact driver](https://github.com/TanStack/db/blob/main/packages/preact-db/tests/infinite-query-conformance.test.ts), [review record](oracle-reviews/dec-03-window-overlap.md) | Four or five ordered source rows, two window-settlement orders, and public snapshots after each settlement, a subscribed observer update, a detached controller read, and explicit recovery. Insertion and removal of the fifth row distinguish both continuation directions while an earlier error remains visible. The detached read follows a source change with no controller subscriber; its preloaded Collection retains the five-row window. The exact overlap uses the exported DB controller in each package realm. Framework hooks expose no controller preload, so this cell does not establish a hook scheduling path; direct hook paging remains in the adjacent shared scenarios. An unsubscribed controller has no notification claim. | | Infinite-query publication | [shared publication laws](https://github.com/TanStack/db/blob/main/packages/db/tests/conformance/infinite-suite-oracle.ts), [React render cuts](https://github.com/TanStack/db/blob/main/packages/react-db/tests/infinite-query-render-cuts-oracle.test.tsx), [Svelte hook tests](https://github.com/TanStack/db/blob/main/packages/svelte-db/tests/useLiveInfiniteQuery.svelte.test.ts), [review record](oracle-reviews/2026-10-05-infinite-first-paint.md) | Each driver records every value its framework published: React layout commits, Vue pre-flushes, and the Svelte construction value plus pre-effect runs. The shared suite checks that a synchronous source shows its ready first page on the first value, for a query callback and for a supplied collection that has not started, that every ready value in a fixed-source, fixed-query scenario equals the source prefix for its own page count, including an equal dependency replacement that keeps page depth, and that a mount requests an on-demand first window once, for a query callback and for a supplied collection whose finite window is wider than or equal to the one the hook needs, judged by the rows the on-demand source holds after mount. Under deferred acquisition, an on-demand first value is either the ready first page or a value with no rows, by framework: React and Svelte publish before the hook subscribes, Vue after. `preloaded-collection-first-paint` checks the documented route-loader pattern: a limited live query preloaded before mount shows its ready first page on the first value in every framework and asks the source for nothing more; a preload that does not count as demand never settles there. Pre-commit work has no shared form, because Vue setup is its commit. The React driver owns renders that never commit, StrictMode double renders (one first-window request on React 19), retries after GC, a mounted replacement whose startup error must reach the error boundary, and a collection kept from an abandoned render that enters terminal error before its query is selected again, each as under `useLiveQuery`. The React source ID reuse oracle drives the infinite hook too. Svelte owns construction and superseded recomputes before the subscribing effect. In both, the hook acquires an on-demand source as often as `useLiveQuery` does under the same history, for either input form, direct or behind a live-query Collection. On-demand requests from renders that never commit are now owned by the live-query deferred acquisition oracle; the React render-cut oracle checks that an abandoned render makes no acquisition. Open cells, owned here: React 18 StrictMode, where neither hook keeps refs across the double render; a React commit held after the 50 ms unsubscribed GC floor, where a controlled-premise controller witness shows no non-ready value but no React scheduling witness exists; ready values in scenarios that write the source or replace the query, which need a per-step source model; a join of eager and on-demand sources, which needs a zero-acquisition check before commit; a supplied collection with no `.limit`, which sends one unbounded request on `main` too, because the controller cannot narrow it before the source request; and Svelte `state_unsafe_mutation` when a start inside `$derived.by` synchronously writes rows a peer is waiting for, shared with Svelte's `useLiveQuery`, which a two-component probe reproduces and the deferred-acquisition follow-up removes. | -| Structural values and ordered primitives | [hash values](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/hash-oracle.property.test.ts), [hash identity](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/hash-identity-oracle.property.test.ts), [MultiSet consolidation](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/multiset-consolidate-oracle.property.test.ts), [hash graphs](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/hash-graph-oracle.property.test.ts), [mixed hash graphs](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/hash-mixed-graph-oracle.property.test.ts), [hash retry](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/hash-failure-retry-oracle.property.test.ts), [murmur stream](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/murmur-stream-oracle.property.test.ts), [comparison](https://github.com/TanStack/db/blob/main/packages/db/tests/comparison-oracle.property.test.ts), [deep equality](https://github.com/TanStack/db/blob/main/packages/db/tests/utils-oracle.property.test.ts), [cursor](https://github.com/TanStack/db/blob/main/packages/db/tests/cursor-oracle.property.test.ts), [indexes](https://github.com/TanStack/db/blob/main/packages/db/tests/index-update-oracle.property.test.ts), [BTree Map](https://github.com/TanStack/db/blob/main/packages/db/tests/btree-map-oracle.test.ts), [query identity](https://github.com/TanStack/db/blob/main/packages/db/tests/query/identity-output-shape-oracle.test.ts), [LIKE semantics](https://github.com/TanStack/db/blob/main/packages/db/tests/query/compiler/evaluators-oracle.test.ts) | Independent flat values, graph topology, algebraic laws, structural object keys in code-unit order (the flat-value, identity and consolidation grammars include keys that tie under locale collation), murmur byte framing against a byte-at-a-time reference (block mixing, the finalizer and symbol framing have no owner there), Map/group/sort recomputation, expression denotation, custom comparator dispatch/reference identity/index compatibility, LIKE wildcard refinement, compiled output bags, declared value-pair agreement between D2 equality keys and IR equality operands, and Collection collation with exact public scan/index order and one-index reuse across repeated reads. The auto-index cells cross BasicIndex and BTreeIndex; scan cells omit indexes. Both paths cover lexical and numeric `en-US` locale defaults plus an explicit ordinary locale override of a numeric default. An unindexed six-row work cell bounds Collection collation resolution to at most two reads per ordered scan: one index-path check and one scan clause. Other locales and ordered histories are outside this owner. The identity pair grammar covers primitives, signed zero, NaN/invalid Date, valid Date/timestamp, binary/Buffer, Temporal, nested references, symbols, and functions; fixed and sampled pairs do not prove every JavaScript value or hash collision freedom. Ref proxies are expressions rather than literal values in this lane. The deep-equality owner also pins that `deepEquals` ignores Map and Set insertion order, RegExp `lastIndex`, array holes, and typed-array class, the rules draft equality keeps. It also pins that objects of another class differ, that plain and null-prototype objects are one class, that a class instance without enumerable keys equals only itself, that URLs compare by `href`, and that typed-array elements compare like numbers. The hash-values owner checks equal-hash consequences for independently constructed flat values. Distinct type identities are checked by the hash-identity owner through `equalHashValues`, including forced digest collisions; distinct 32-bit digests are not promised. The hash-identity owner checks `hash` and `equalHashValues` against one spec-level identity model: primitives with signed zero, NaN, and bigint; reference leaves (functions, registered handles, `File`, binary over 128 bytes); Date, binary, Temporal, and RegExp headers; array length and holes; Map and Set insertion order; and object keys in any order, for acyclic values and for cycles through arrays, Map values, Sets, and plain objects. Map/Set order sensitivity is pinned current behavior, not a promise. Arrays from another realm, RegExps with a `NaN` `lastIndex`, getters, proxies, other cycle carriers, and pairs that differ only in a back-edge target are outside its grammar; pinned cases keep equality's cross-realm length check and strict RegExp field comparison. `hash-work.test.ts` pins how visited values and array/RegExp headers count toward the structural work cap, and that equality copies a shared Map's entries once per distinct pair ([code-weight hash dispatch review](oracle-reviews/code-weight-hash-dispatch.md)). Custom string indexes do not optimize ordinary ranges, and custom cursors remain unsupported. The LIKE owner covers boolean string matching and bounded work, not nullish three-valued logic or a general Unicode collation contract. Unsupported composite cursors reject. The MultiSet consolidation owner checks keyed, single-type, and structural identity, signed zero and NaN in keyed and single-number data, the retained first record, zero-sum removal, and input record contents. It does not assert output order. Its keyed grammar includes cross-type key and value collisions, non-finite numeric keys, the `\|` delimiter, symbol and function identity, and a direct-object/object-tuple delimiter control from #1948. Eight named collision witnesses and both generated campaigns are RED on `c879ba6d` and GREEN with the repair ([#1948 review record](oracle-reviews/issue-1948-keyed-consolidation.md)); the [code-weight consolidation review](oracle-reviews/code-weight-multiset-consolidation.md) records the earlier exclusion. The keyed reference uses direct value/reference equality, including SameValueZero for numeric keys. The unkeyed fallback keeps its original key and value domains. The direct `MultiSet` owner does not prove which `@tanstack/db` query shapes produce primitive keyed values or mixed-type source keys; that needs a live-query production witness. The index owner enforces one invalid-comparator law across both BasicIndex and BTreeIndex: for each comparator, accepted numeric prefix (including the empty prefix), and probe operation (add, update, eq/range lookup, take, build), the operation throws exactly when it receives a `NaN` or non-number result and never when every comparison is valid. A `signed infinity` comparator is the valid control, and a custom collation `compare` supplied through `compareOptions` is checked the same way. Two BTree Map split histories check inserted-key reads, exact whole-range payloads/order, size, and extrema, proving split placement follows the insertion index without a post-mutation comparison. A rejected index add or remove leaves the index refining its accepted rows; update and build are not atomic. At the Collection boundary, both index types and both write paths (optimistic insert, sync commit) crash the collection: status becomes `error` and the next mutation throws, so a usable collection never holds rows its subscribers were not told about. Open: a sync source can still commit writes on a collection in `error` state, which stores unpublished rows; this is existing `markError` behavior and needs a lifecycle-owner witness. Comparator transitivity is outside this evidence. | +| Structural values and ordered primitives | [hash values](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/hash-oracle.property.test.ts), [hash identity](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/hash-identity-oracle.property.test.ts), [MultiSet consolidation](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/multiset-consolidate-oracle.property.test.ts), [hash graphs](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/hash-graph-oracle.property.test.ts), [mixed hash graphs](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/hash-mixed-graph-oracle.property.test.ts), [hash retry](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/hash-failure-retry-oracle.property.test.ts), [murmur stream](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/murmur-stream-oracle.property.test.ts), [comparison](https://github.com/TanStack/db/blob/main/packages/db/tests/comparison-oracle.property.test.ts), [deep equality](https://github.com/TanStack/db/blob/main/packages/db/tests/utils-oracle.property.test.ts), [cursor](https://github.com/TanStack/db/blob/main/packages/db/tests/cursor-oracle.property.test.ts), [indexes](https://github.com/TanStack/db/blob/main/packages/db/tests/index-update-oracle.property.test.ts), [BTree Map](https://github.com/TanStack/db/blob/main/packages/db/tests/btree-map-oracle.test.ts), [query identity](https://github.com/TanStack/db/blob/main/packages/db/tests/query/identity-output-shape-oracle.test.ts), [LIKE semantics](https://github.com/TanStack/db/blob/main/packages/db/tests/query/compiler/evaluators-oracle.test.ts) | Independent flat values, graph topology, algebraic laws, structural object keys in code-unit order (the flat-value, identity and consolidation grammars include keys that tie under locale collation), murmur byte framing against a byte-at-a-time reference (block mixing, the finalizer and symbol framing have no owner there), Map/group/sort recomputation, expression denotation, custom comparator dispatch/reference identity/index compatibility, LIKE wildcard refinement, compiled output bags, declared value-pair agreement between D2 equality keys and IR equality operands, and Collection collation with exact public scan/index order and one-index reuse across repeated reads. The auto-index cells cross BasicIndex and BTreeIndex; scan cells omit indexes. Both paths cover lexical and numeric `en-US` locale defaults plus an explicit ordinary locale override of a numeric default. An unindexed six-row work cell bounds Collection collation resolution to at most two reads per ordered scan: one index-path check and one scan clause. Other locales and ordered histories are outside this owner. The identity pair grammar covers primitives, signed zero, NaN/invalid Date, valid Date/timestamp, binary/Buffer, Temporal, nested references, symbols, and functions; fixed and sampled pairs do not prove every JavaScript value or hash collision freedom. Ref proxies are expressions rather than literal values in this lane. The utility owner additionally generates widths 6–12 of ambiguous object-valued Sets. An independent count/capacity model judges equal reordered bags, late unmatched prototypes, multiplicity deficits, and a generic plain/class bridge that needs reassignment. Row comparison requires exact shape/value counts; generic equality permits keyed class/plain/null bridges. General and row comparison paths run in both directions, with acyclic members or links back to the Set. Separate cubic scalar-field-read and scoped native Map/Set-operation ceilings reject repeated-comparison and cached-candidate factorial searches without a timing assertion. Empty and singleton neighbors use the same model. Thirteen declared states each reach 200 histories. A paired-input lane adds widths 1,024–3,072, distinct/interchangeable members, and root-linked cycles. Its eight states each reach 20 histories; linear scalar-read and native-operation ceilings protect this equal corresponding-member family. Arbitrary unordered inputs retain the polynomial fallback; arbitrary graph topology and nested-value cost remain outside these bounded work laws. The deep-equality owner also pins that `deepEquals` ignores Map and Set insertion order, RegExp `lastIndex`, array holes, and typed-array class, the rules draft equality keeps. It also pins that `sameRowValue` preserves record prototypes (maintainer decision 2026-11-03), while exported `deepEquals` retains its established keyed-class/plain/null/cross-realm bridge, that a class instance without enumerable keys equals only itself, that URLs compare by `href`, and that typed-array elements compare like numbers. The hash-values owner checks equal-hash consequences for independently constructed flat values. Distinct type identities are checked by the hash-identity owner through `equalHashValues`, including forced digest collisions; distinct 32-bit digests are not promised. The hash-identity owner checks `hash` and `equalHashValues` against one spec-level identity model: primitives with signed zero, NaN, and bigint; reference leaves (functions, registered handles, `File`, binary over 128 bytes); Date, binary, Temporal, and RegExp headers; array length and holes; Map and Set insertion order; and object keys in any order, for acyclic values and for cycles through arrays, Map values, Sets, and plain objects. Map/Set order sensitivity is pinned current behavior, not a promise. Arrays from another realm, RegExps with a `NaN` `lastIndex`, getters, proxies, other cycle carriers, and pairs that differ only in a back-edge target are outside its grammar; pinned cases keep equality's cross-realm length check and strict RegExp field comparison. `hash-work.test.ts` pins how visited values and array/RegExp headers count toward the structural work cap, and that equality copies a shared Map's entries once per distinct pair ([code-weight hash dispatch review](oracle-reviews/code-weight-hash-dispatch.md)). Custom string indexes do not optimize ordinary ranges, and custom cursors remain unsupported. The LIKE owner covers boolean string matching and bounded work, not nullish three-valued logic or a general Unicode collation contract. Unsupported composite cursors reject. The MultiSet consolidation owner checks keyed, single-type, and structural identity, signed zero and NaN in keyed and single-number data, the retained first record, zero-sum removal, and input record contents. It does not assert output order. Its keyed grammar includes cross-type key and value collisions, non-finite numeric keys, the `\|` delimiter, symbol and function identity, and a direct-object/object-tuple delimiter control from #1948. Eight named collision witnesses and both generated campaigns are RED on `c879ba6d` and GREEN with the repair ([#1948 review record](oracle-reviews/issue-1948-keyed-consolidation.md)); the [code-weight consolidation review](oracle-reviews/code-weight-multiset-consolidation.md) records the earlier exclusion. The keyed reference uses direct value/reference equality, including SameValueZero for numeric keys. The unkeyed fallback keeps its original key and value domains. The direct `MultiSet` owner does not prove which `@tanstack/db` query shapes produce primitive keyed values or mixed-type source keys; that needs a live-query production witness. The index owner enforces one invalid-comparator law across both BasicIndex and BTreeIndex: for each comparator, accepted numeric prefix (including the empty prefix), and probe operation (add, update, eq/range lookup, take, build), the operation throws exactly when it receives a `NaN` or non-number result and never when every comparison is valid. A `signed infinity` comparator is the valid control, and a custom collation `compare` supplied through `compareOptions` is checked the same way. Two BTree Map split histories check inserted-key reads, exact whole-range payloads/order, size, and extrema, proving split placement follows the insertion index without a post-mutation comparison. A rejected index add or remove leaves the index refining its accepted rows; update and build are not atomic. At the Collection boundary, both index types and both write paths (optimistic insert, sync commit) crash the collection: status becomes `error` and the next mutation throws, so a usable collection never holds rows its subscribers were not told about. Open: a sync source can still commit writes on a collection in `error` state, which stores unpublished rows; this is existing `markError` behavior and needs a lifecycle-owner witness. Comparator transitivity is outside this evidence. | | Mixed-kind ordered prefixes | [mixed-kind public-order oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/query/mixed-index-order-oracle.test.ts) | A separate unindexed ready Collection computes the public ordered two-row prefix for the same keyed rows as an eager BasicIndex or BTreeIndex Collection. Five histories cross number/bigint, boolean/number, object/string, and array/object values, with insert, update, and delete transitions into and out of mixed domains. Ascending and descending reads use one ascending physical index, so descending checks exercise ReverseIndex. After preload and after both source copies commit each step, exact direct and live-query IDs must match the scan. Same-kind checkpoints verify that safe indexes still serve ordered reads. The original type-name index order failed eight ascending public-prefix checks. A second numeric source Map models a finite on-demand provider: a bigint enters and leaves the visible prefix, deletion requires an uninstalled row, and an offset window reaches a cursor after the retained BTree index loses ordered-plan eligibility. Public IDs match the independent provider order at each settled window; controls withholding either requested row fail those assertions. This owner does not define a global precedence among unlike values, cover in-place value mutation or arbitrary mixed-kind pairs, or prove that every retained on-demand traversal is safe after its source domain changes. | | WHERE predicate publication | [WHERE predicate publication oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/query/where-predicate-publication-oracle.property.test.ts), [`in` evaluator](https://github.com/TanStack/db/blob/main/packages/db/tests/query/compiler/in-evaluator-oracle.property.test.ts) ([review](oracle-reviews/perf-in-constant-set.md)) | An independent Kleene evaluator judges `eq`/`not`/`and`/`or` predicates over strings, a normalization-prefixed string, booleans, numbers, `NaN`, a valid Date, `null`, a missing field, and virtual fields. Snapshot histories with pending optimistic inserts compare a live query, direct subscribers with and without initial state, and `currentStateAsChanges`; change histories apply multi-key sync transactions, including reinsertion of a key deleted earlier, and compare every consumer's key set after each commit, on scan and `BasicIndex` paths. Peer subscribers share one field with different literals, plus one on a virtual field, so every published change must reach exactly the subscribers whose predicate it can satisfy; routing mutants that ignored the previous value, ignored the field, delivered a change twice, or routed while stale published rows awaited reconciliation fail here. Fixed witnesses cover a layout-only publication reaching a filtered subscriber as one empty batch, a filtered subscriber's empty Collection-readiness batch, retraction of a vanished row after eager cleanup and restart, and the new row plus exact subscriber update payload when a same-key row stays TRUE. A stale-live-value mutant survives the generated membership checks but fails the payload witness. A focused [property-visibility test](https://github.com/TanStack/db/blob/main/packages/db/tests/query/where-prefilter-property-visibility.test.ts) compares the public unindexed snapshot with the enriched-row contract for inherited, non-enumerable, enumerable-own, and nested getter paths; the pre-fix stored-row shortcut failed three of its four controls. Other property-descriptor and stateful-getter histories remain outside those fixed controls. Hostile mutants for FALSE-for-UNKNOWN `eq`, `or` or number-literal subscription prefilters, a prefilter that ignores the previous value, a skipped readiness batch, a skip while stale published rows await reconciliation, and an unindexed snapshot scan that reads only synced rows while an optimistic insert or delete changes visibility passed the prior `@tanstack/db` suite and fail here. Change routing withholds a dropped row from an `eq` subscription before its sent key could be recorded, so a mutant that records dropped rows as sent is equivalent for routed predicates in this oracle. A focused `collection-subscription.test.ts` witness checks that a dropped insert or update, alone or beside a matching change, cannot advance a limited subscription's page offset under a routed `eq` and an unrouted `or` predicate; the unrouted cases beside a matching change kill that mutant, and the unrouted `alone` case does not. A loose-equality prefilter is an equivalent mutant: it only skips less. Skipping during truncate replay also survives; the replay's baseline diff re-derives the same retraction, so the guard keeps the prior dataflow without its own witness. Generated cleanup and restart histories for filtered subscribers remain open for the lifecycle publication owner. No-op updates, other comparison operators, Temporal and binary operands, joins, ordering, optimistic updates, and truncate are outside this owner. It runs in `@tanstack/db`'s `test:oracles` campaign. The [2026-09-30 review](https://github.com/TanStack/db/blob/main/docs/contributing/oracle-reviews/2026-09-30-where-predicate-and-join-keys.md) records each ORC outcome. Change routing was later removed in favor of pooled live queries; the routing mutants above are historical, and the same peer laws now check plain subscription dispatch. The unindexed snapshot prefilter was restored for compiled queries as one stored-row `eq` test on a string or boolean literal. Its property-visibility test also covers the reverse direction: a stored row with an inherited or non-enumerable field must still match `isUndefined` on that field, which a scan that evaluated the full predicate on stored rows fails. The `in` evaluator owner checks `in` against an independent linear reference over Dates, `NaN`, `-0`, BigInt, bytes, Temporal values, objects and nullish items, for constant and row-supplied lists. | | Joined result keys | [Joined result key oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/query/join-result-key-oracle.property.test.ts) | A nested-loop model recomputes the pair set of an inner, left, or full join over source keys drawn from plain, comma-bearing, bracket-bearing, and quoted strings, numbers beside the strings that print the same, both infinities, and `NaN`. After preload and each synced group change, the published rows must equal the model's pairs and the result key count must equal the row count. The comma-joined key encoding fails its two pinned histories and both campaigns; plain `JSON.stringify`, which prints infinities as the missing-side `null`, fails the pinned infinity history and both campaigns. A pinned history in which a `NaN`-keyed pair leaves and re-forms fails when the join index compares source-key prefixes with `===`. Joins over subqueries, more than two sources, custom `getKey`, and optimistic mutations are outside this owner. It runs in `@tanstack/db`'s `test:oracles` campaign. The [2026-09-30 review](https://github.com/TanStack/db/blob/main/docs/contributing/oracle-reviews/2026-09-30-where-predicate-and-join-keys.md) records each ORC outcome. | | D2 Index storage | [Index refinement oracle](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/index-refinement-oracle.property.test.ts) | A plain-`Map` model sums multiplicities under a type-and-value identity in which `NaN` equals `NaN` and `-0` equals `0`. Histories of up to twelve additions to two keys mix prefixed arrays with `NaN`, `0`, `-0`, `1`, `'1'`, `1n`, and `'a'` prefixes and unprefixed values, with cancellation and reappearance. After each addition, `get` and `has` must equal the model. Each step also checks that `get` returns what `getIterator` yields, in its order, since the two walk the storage separately. Comparing prefixes with `===`, which disagrees with the `Map` that holds them, fails three pinned histories and both campaigns; the `-0` control passes under both. Every history also runs with `prefixIdentity`, where values under one key with the same prefix are one value; mutants that merge unprefixed values or ignore the option in a prefix map fail. The [structural identity oracle](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/structural-identity-oracle.property.test.ts) checks that `structuralIdentity` agrees with D2 value identity for equal representations (array subclasses, class instances, null prototypes, key order, non-enumerable symbols) and independent values, including separator-bearing strings and keys, enumerable symbol keys, Dates, RegExps with numeric `lastIndex`, Maps and Sets with zero to two entries (the second Set member is a fresh tagged array, so primitive deduplication and unwrapped reordering are outside this lane), arrays with an optional enumerable `note` field, binary values, holes, deep nesting, and a cyclic value; an independent normal-form model decides equality and is checked against `equalHashValues`. The model expands `deep` shorthand into nested arrays. A pinned equal `deep`/array pair and adjacent-depth control guard the model boundary exposed by seed 1158727847/path 255. The equal-pair grammar crosses same-spec values with explicit-array/`deep` equivalent specs; the independent-pair grammar uses two specs. Both run fixed and fresh campaigns with guarded one-case seed/path replay. Pinned same-description symbols and equal-body Map/Set/object pairs expose collisions that random independent pairs rarely reach. Mutants that stringify symbols, collapse those three object-kind tags, drop a string's or key's length or an array's length, or omit Date and RegExp headers fail it. A first-entry-only Map/Set mutant and an array-identity mutant that ignores enumerable non-index fields fail the pinned comparisons; lowering the depth cap is equivalent, because the cap only bounds work. Open: reference-valued RegExp `lastIndex` can give D2-unequal values one defined identity and merge `groupBy` contributions ([#2192](https://github.com/TanStack/db/issues/2192)); the structural-identity and incrementalization owners need a production repair and receiving witness. Temporal values, other opaque reference values, large binary values, getters/proxies, and mutation between observations are outside this generated grammar. groupBy's use of it is owned by the incrementalization law: its string-extrema lane fails a string identity without a length prefix, and a focused groupBy witness fails when the group key is left out of the identity. Compaction, presence tracking, and structural payloads are outside this owner; a focused test checks that an index built without presence (as reduce builds its indexes) throws when presence is read; the [reduce fast-path oracle](https://github.com/TanStack/db/blob/main/packages/db-ivm/tests/operators/reduce-fast-path-oracle.property.test.ts) compares the fast path's emitted entries one by one with the general path's, over keys that move between the paths, hash-merged outputs, -0, and zero and negative multiplicities, while an unchanged `NaN` output is owned by a focused reduce test; the join operator tests and incrementalization law own operator behavior. | -| Pooled live queries | [pooled live query oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/query/pooled-live-query-oracle.property.test.ts) | An independent `eq` model (nullish UNKNOWN, Date as timestamp, `NaN` equal to `NaN`, `-0` equal to `0`) gives membership; a live-query Collection compiled for the same query gives order, values, and status. Histories mix sync transactions, optimistic writes confirmed or rolled back, peer mounts and unmounts, and source cleanup and restart, including cleanup while a write is pending and a mount between cleanup and restart. Wholesale observers must match after every step, granular changes must match a reference observer by type, key, value, and previous value and replay to the model's rows, the layout revision must advance when keys reorder, and observing must never build the live-query Collection. Peers may add a residual `not(eq(...))` conjunct, which the model evaluates itself; a view that ignores it, reports an unfiltered `entries()`, or delivers a row entering or leaving it as an update fails it. Other residual operators use the same compiler evaluator but are not generated. Peers may also order their rows by `id` or by a `g` that can be null, with explicit `nulls`; order comes from the reference. A comparator that ignores direction or `nulls`, an order left out of the partition key, and key order instead of the query's order each fail the pinned reorder history and both campaigns. A partition that ignores a row's previous group, orders by arrival, skips normalization, skips the initial snapshot, follows the source's status after cleanup, or never terminates fails it. Two shared conformance scenarios (`eq-filter-rows`, `eq-filter-peers`) run the pooled path under React, Vue, Solid, Svelte, and Angular; a partition that ignores a row's previous group fails both under every adapter, and `eq-filter-peers` checks through `subscriberCount` that each adapter shares one source subscription per partition while mounted and releases every source subscription after the final framework unmount. The [resource-lifetime oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/query/pooled-live-query-gc-oracle.test.ts) compares when a pooled query and a live-query Collection release their sources across `gcTime` 1, 100, 0, and `Infinity`, with and without a subscriber, and with no `gcTime`, where both use the live-query Collection default, checks that a partition waits for its views' longest `gcTime`, checks that a view resubscribed after its partition released follows later source writes as its live-query Collection does, including a view with a residual conjunct, and checks that a partition that subscribes again still serves new mounts from one source subscription. A generated cleanup-admission lane (`pooled-live-query.cleanup-admission`) crosses 2–3 views of one partition, observed or not, read by a subscribed live query or not, and view cleanups with or without `dependents`; it checks the model's rejection rule and rows and status against live-query Collections. A view that gives up its partition share when its cleanup rejects fails it. A view whose own cleanup fulfilled must show `cleaned-up` with no rows (law 14, maintainer decision), checked against the model and the live-query Collection; a view that keeps its source's status or its group's rows fails it. Further focused witnesses check that a partition keeps groups only for values that have rows or watchers, that a detached reader never sees a dropped group's revision reused, keep a pooled query's public Collection live while the view is observed, accept it as a query source, and check that a pooled query turns terminal when source cleanup starts while the adapter's cleanup is still pending, as a live-query Collection does. A detached wholesale reader of every view, kept after its peer unmounts, must match the live-query Collection at every checkpoint, and generated histories cross the dependents cascade with readers on watched and unwatched groups that hold rows (declared reach); a cleanup that empties groups without advancing their revisions, or advances only watched groups, fails it. Every view and its live-query Collection also carry an app status listener that throws; each listener gets its own call and the throw is reported uncaught, so a view that lets a listener's throw stop the others or reject the source's cleanup fails it. Generated callback-boundary histories also compare ordinary compiled and pooled core observers across return/throw, Error/undefined, zero through three healthy peers, callback position before/after peers, and `gcTime` 1–3. Failed granular bootstrap retains no unreturned ownership after grace; a held cascade cleanup (`cleanup({ dependents: true })`) gives every admitted wholesale observer one terminal `cleaned-up` snapshot/callback despite a sibling throw, and an ended partition stops following its source's health, and preserves the exact failure at settlement. Original production and independent rollback-only or all-peer-only repairs fail the respective ownership or cleanup-start comparisons. These controlled core receiving paths do not establish framework schedules, arbitrary callback programs or provider shutdown. A pinned oracle witness checks that an `eq` path whose getter throws excludes the row on both paths; a pooled path that rethrows fails it. The pooled oracle's grammar never releases a partition, so release and resubscribe histories rely on these fixed witnesses; a fixed delay, a missing unsubscribed floor, a last-`gcTime`-wins rule, and releasing at `gcTime` 0 each fail it. Removing the partition's final-listener release survives the mounted row checks but fails the shared framework unmount checkpoint and core release-timing owner. The [2026-10-01 review](oracle-reviews/2026-10-01-pooled-live-queries.md) records each ORC outcome. The shared adapter conformance suite checks the opt-in boundary in every framework: queries without `pool` keep a source subscription each in the function, config, and parameterized forms, and a pooled parameterized query follows its parameter between pool groups beside an unchanged peer. Pooling every query by default, a pooled identity that ignores literal values (React then reuses the old query), and an Angular `params` form that drops `pool` each fail it. `pooled-param-change` also reads each driver's publication log: every value published after a switch holds only the new group's rows. Each driver also logs its keyed `state` map at the same points, and the law applies to it. A pooled view that serves the previous view's rows until a later task replaces them passes every final-state check and fails only the log, in all six drivers. A view that sends the previous rows as its first change set fails the `state` log in Vue; Solid rebuilds its map from a snapshot within the same turn, and Angular builds its map from `entries()`, so that mutant cannot reach them. The `invalid-pool` scenario requires `InvalidPoolOptionError` for `true` and for `false` (the value adapters pass to say a hook cannot pool), alone, beside `gcTime`, beside `queryKey`, on a parameterized query, and on a disabled config or parameterized query, in all six drivers; a check in only one of the three readers of `pool` fails it in four of them, and Angular's `params` form failed the `false` and disabled cases until it checked its pool first. The shared `grouping-operator-switch` scenario re-renders a grouped query without `groupKeys()`, and `compile-error-after-rerender` re-renders to a select that mixes a source field with an aggregate; both require the error and no rows after it. A binding that keeps its last compiled query when compilation throws fails both in Solid, Svelte, and Angular; one that throws once and then serves that query fails the no-rows check in Solid and Svelte, and cannot occur in Angular, whose `computed` rethrows a cached error until an input changes. In `grouping-operator-switch`, React and Preact kept the grouped query before query identity held operator code, and Vue served the previous rows before its compile error stopped reaching readers through a throwing computed (also on `main` at `5649a07d4`). | +| Pooled live queries | [pooled live query oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/query/pooled-live-query-oracle.property.test.ts) | An independent `eq` model (nullish UNKNOWN, Date as timestamp, `NaN` equal to `NaN`, `-0` equal to `0`) gives membership; a live-query Collection compiled for the same query gives order, values, and status. Histories mix sync transactions, optimistic writes confirmed or rolled back, peer mounts and unmounts, and source cleanup and restart, including cleanup while a write is pending and a mount between cleanup and restart. Wholesale observers must match after every step, granular changes must match a reference observer by type, key, value, and previous value and replay to the model's rows, the layout revision must advance when keys reorder, and observing must never build the live-query Collection. Peers may add a residual `not(eq(...))` conjunct, which the model evaluates itself; a view that ignores it, reports an unfiltered `entries()`, or delivers a row entering or leaving it as an update fails it. Other residual operators use the same compiler evaluator but are not generated. Peers may also order their rows by `id` or by a `g` that can be null, with explicit `nulls`; order comes from the reference. A comparator that ignores direction or `nulls`, an order left out of the partition key, and key order instead of the query's order each fail the pinned reorder history and both campaigns. A partition that ignores a row's previous group, orders by arrival, skips normalization, skips the initial snapshot, follows the source's status after cleanup, or never terminates fails it. Two shared conformance scenarios (`eq-filter-rows`, `eq-filter-peers`) run the pooled path under React, Vue, Solid, Svelte, and Angular; a partition that ignores a row's previous group fails both under every adapter, and `eq-filter-peers` checks through `subscriberCount` that each adapter shares one source subscription per partition while mounted and releases every source subscription after the final framework unmount. The [resource-lifetime oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/query/pooled-live-query-gc-oracle.test.ts) compares when a pooled query and a live-query Collection release their sources across `gcTime` 1, 100, 0, and `Infinity`, with and without a subscriber, and with no `gcTime`, where both use the live-query Collection default, checks that a partition waits for its views' longest `gcTime`, checks that a view resubscribed after its partition released follows later source writes as its live-query Collection does, including a view with a residual conjunct, and checks that a partition that subscribes again still serves new mounts from one source subscription. A generated cleanup-admission lane (`pooled-live-query.cleanup-admission`) crosses 2–3 views of one partition, observed or not, read by a subscribed live query or not, and view cleanups with or without `dependents`; it checks the model's rejection rule and rows and status against live-query Collections. A view that gives up its partition share when its cleanup rejects fails it. A view whose own cleanup fulfilled must show `cleaned-up` with no rows (law 14, maintainer decision), checked against the model and the live-query Collection; a view that keeps its source's status or its group's rows fails it. Further focused witnesses check that a partition keeps groups only for values that have rows or watchers, that a detached reader never sees a dropped group's revision reused, keep a pooled query's public Collection live while the view is observed, accept it as a query source, and check that a pooled query turns terminal when source cleanup starts while the adapter's cleanup is still pending, as a live-query Collection does. A detached wholesale reader of every view, kept after its peer unmounts, must match the live-query Collection at every checkpoint, and generated histories cross the dependents cascade with readers on watched and unwatched groups that hold rows (declared reach); a cleanup that empties groups without advancing their revisions, or advances only watched groups, fails it. Every view and its live-query Collection also carry an app status listener that throws; each listener gets its own call and the throw is reported uncaught, so a view that lets a listener's throw stop the others or reject the source's cleanup fails it. Generated callback-boundary histories also compare ordinary compiled and pooled core observers across return/throw, Error/undefined, zero through three healthy peers, callback position before/after peers, and `gcTime` 1–3. Failed granular bootstrap retains no unreturned ownership after grace; a held cascade cleanup (`cleanup({ dependents: true })`) gives every admitted wholesale observer one terminal `cleaned-up` snapshot/callback despite a sibling throw, and an ended partition stops following its source's health, and preserves the exact failure at settlement. Original production and independent rollback-only or all-peer-only repairs fail the respective ownership or cleanup-start comparisons. These controlled core receiving paths do not establish framework schedules, arbitrary callback programs or provider shutdown. A pinned oracle witness checks that an `eq` path whose getter throws excludes the row on both paths; a pooled path that rethrows fails it. The pooled oracle's grammar never releases a partition, so release and resubscribe histories rely on these fixed witnesses; a fixed delay, a missing unsubscribed floor, a last-`gcTime`-wins rule, and releasing at `gcTime` 0 each fail it. Removing the partition's final-listener release survives the mounted row checks but fails the shared framework unmount checkpoint and core release-timing owner. The [2026-10-01 review](oracle-reviews/2026-10-01-pooled-live-queries.md) records each ORC outcome. The shared adapter conformance suite checks the opt-in boundary in every framework: queries without `pool` keep a source subscription each in the function, config, and parameterized forms, and a pooled parameterized query follows its parameter between pool groups beside an unchanged peer. Pooling every query by default, a pooled identity that ignores literal values (React then reuses the old query), and an Angular `params` form that drops `pool` each fail it. `pooled-param-change` also reads each driver's publication log: every value published after a switch holds only the new group's rows. Each driver also logs its keyed `state` map at the same points, and the law applies to it. A pooled view that serves the previous view's rows until a later task replaces them passes every final-state check and fails only the log, in all six drivers. A view that sends the previous rows as its first change set fails the `state` log in Vue; Solid rebuilds its map from a snapshot within the same turn, and Angular builds its map from `entries()`, so that mutant cannot reach them. The `invalid-pool` scenario requires `InvalidPoolOptionError` for `true` and for `false` (the value adapters pass to say a hook cannot pool), alone, beside `gcTime`, beside `queryKey`, on a parameterized query, and on a disabled config or parameterized query, in all six drivers; a check in only one of the three readers of `pool` fails it in four of them, and Angular's `params` form failed the `false` and disabled cases until it checked its pool first. The shared `grouping-operator-switch` scenario re-renders a grouped query without `groupKeys()`, and `compile-error-after-rerender` re-renders to a select that mixes a source field with an aggregate; both require the error and no rows after it. A binding that keeps its last compiled query when compilation throws fails both in Solid, Svelte, and Angular; one that throws once and then serves that query fails the no-rows check in Solid and Svelte, and cannot occur in Angular, whose `computed` rethrows a cached error until an input changes. In `grouping-operator-switch`, React and Preact kept the grouped query before query identity held operator code, and Vue served the previous rows before its compile error stopped reaching readers through a throwing computed (also on `main` at `5649a07d4`). Its declared reach also requires successful wholesale, granular and detached observer comparisons on the same-copy pooled view, whose authoritative clocks do not require a separate layout subscription channel. | | Live-query deferred acquisition | [deferred acquisition oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/live-query-deferred-acquisition-oracle.test.ts), [React Suspense warm-cache witness](https://github.com/TanStack/db/blob/main/packages/react-db/tests/suspense-warm-cache.test.tsx) | An independent model of the law that a live-query Collection starts no provider work before a subscriber or a preload in its current sync run crosses seven source states (eager or on-demand, idle, already started or render-deferred, and an on-demand source that already holds the rows), an unordered query and an ordered window, one or two live-query levels, and eleven histories: start and read, subscribe or preload after a start, subscribe or preload without one, deferred subscription and resumption with or without a start, cleanup before a subscriber, peer start, source truncate, and restart after a run that had one. After every command it checks source sync-run starts, acquisition attempts, readiness when the call returns (the synchronous observation cut), settled readiness, and rows. A pooled block checks that building a pooled view starts no idle eager source until a view subscribes or preloads, and eight DbClient histories check that a preload answered by a pending or settled stream still resumes acquisition across eager/on-demand idle and render-deferred sources. The React streaming witness receives the on-demand render-deferral premise, starts local acquisition before the stream settles, and retains server rows until browser acquisition completes. Mutants that never defer, keep the subscriber-or-preload fact across cleanup, ignore preload, never resume a builder's subscriptions, let a deferring subscriber start a base source, build a pooled partition without deferral, never resume it, mark a preload one microtask late, or drop the stream's preload mark each fail it. The React witness checks that `useLiveSuspenseQuery` over a source that already holds the rows renders without the fallback, for eager and on-demand sources and both shapes; creating the Suspense collection started, or not preloading it in render, fails it. A later review added `peer` and `truncate` commands: a peer that starts an idle on-demand source, or a truncate on a running one, made the live query acquire before any subscriber, and both guards are killed separately. Pinned blocks check that a subscriber surviving cleanup is served after a restart while a surviving deferring subscriber is not, and that `toArrayWhenReady()` and `stateWhenReady()` count as a preload. A pinned block checks that a deferring live query is not counted in its source's `subscriberCount` or `subscribers:change` until its first subscriber, while still keeping the source from garbage collection; a Query Collection test checks that it causes no refetch. Another pinned block checks that a preload or subscriber requested by an inner live query's status handler, while the outer query creates its subscription to it, still resumes acquisition. Solid's Suspense receiving witnesses check that a resource whose preload settles first retains its live-query Collection while its sibling remains pending past the unsubscribed floor, then releases that temporary owner on both commit and abandonment, and that a preload settling after its owner unmounts cannot publish an error through the abandoned result. Open: source failure and asynchronous loads before resumption, which the subscription lifecycle oracle owns once acquisition resumes; a resume that throws, which leaves later sources unstarted only for a live query that is already in error; asynchronous sources; and arbitrary Suspense schedules with a `DbClient` beyond the bounded abandoned-render and streaming handoffs recorded above. | | Live-query notification algebra | [real-query and checker calibration](https://github.com/TanStack/db/blob/main/packages/db/tests/notification-laws-oracle.test.ts), [independent Map checker](https://github.com/TanStack/db/blob/main/packages/db-collection-e2e/src/utils/notification-laws-oracle.ts) | One-key synchronous source histories check exact live-query callback reconstruction and unsubscribe silence. Synthetic multi-key histories calibrate the checker; direct source Collection events remain with the change-event history owner. | | Flat-row change tracking | [flat change tracking oracle](https://github.com/TanStack/db/blob/main/packages/db/tests/flat-change-tracking-oracle.property.test.ts) | Generated callbacks over flat rows, frozen or not and with or without a non-enumerable field (assignments of `0`, `-0`, `NaN`, `undefined`, functions, and fresh objects; reverts; deletions; added fields; data and accessor `defineProperty`; stored drafts; throws), run through the flat tracker, the draft proxy, and an independent model, which must report the same change sets, with `0` and `-0` distinct (maintainer decision, 2026-10-29: writing one over the other is a change), or throw the same error and leave the rows unchanged. Defining a field acts as assigning it, and a non-enumerable field is row data only once written; the proxy broke three of those laws on `main` and the flat tracker one. Diffs using `!==`, `Object.is` alone, or no deletions fail it. It found that the proxy dropped a field added as `undefined` when another field reverted; the pinned history and both campaigns fail without that fix. Non-flat rows must fall back. The [2026-10-01 review](oracle-reviews/2026-10-01-pooled-live-queries.md) records each ORC outcome. | @@ -1958,7 +2020,24 @@ owns the transferred same-realm Date and RegExp equality boundaries: equal copies, changed values, and inequality with timestamp, ISO, or source text. The [round 7 ledger](oracle-reviews/utils-unit-retirement-round-7.md) retains the retired examples. Caller-modified built-ins and Collection publication -are outside this direct-return owner. +are outside this direct-return owner. Its shared sparse-array value part +(`sparse-array-value-oracle.ts`) generates lengths 1–5 with a guaranteed hole, +other holes/undefined/numbers, a target position and a defined replacement. +`utils.31` checks hole/undefined equality and hole/defined inequality through +both APIs and directions, directly, in records, and in singleton Sets. Its seven +states each reach 200 histories. The snapshot owner receives the same authored +positional model through real source Collection/change mirrors and direct or +projected observers, both modes, attached/detached, retaining earlier captures. +Its fourteen states each reach 20 histories per fixed/fresh campaign. Before the +indexed-array repair, source writes to a hole could change reads without an event +or clock bump; these receiving comparisons were RED before production changed. +The original hole-skipping mutant fails direct and receiving guarded replays. +This law permits a hole to equal undefined; draft/persisted presence fidelity is +separate. Framework/provider scheduling and arbitrary nested graph topology are +not claimed. The native-operation work instrument closes the demonstrated cached +DFS gap, but CPU implemented with other bookkeeping, allocations and array scans +remain outside that deterministic measure. See the four-agent supplement in +`oracle-reviews/2026-11-01-observer-row-containers.md`. Set performance work evaluated in PR #2301 is excluded by the maintainer's 2026-10-30 decision: "we've never had this perf problem ... let's remove it." @@ -7005,6 +7084,15 @@ optional field, and FID-1 typed values per declared `valueKinds` on the initial load and live changes. Its reference driver is also the generated witness for core's synced commit change detection with `-0` and `0` (maintainer decision 2026-10-29: distinct row values; `sameRowValue`). +The revert owner's numeric and floating typed-array tokens now retain the +zero sign in both model and observations. Its additive sign-only assignment +grammar generates root, nested and Float64Array paths, with six shared examples +plus 100 generated histories in each fixed/fresh campaign. In the final gate, +root/nested/Float64 reach is 43/26/37 fixed and 36/33/37 fresh. A ROW-removal mutant +that the old encoding accepted now fails each path's required-change assertion; +the flat owner already rejected it, so this repairs a stale owner equivalence, +not a current production defect. Intermediate reads, integer conversion and +Collection settlement are outside this added lane. The decision covers user writes too (2026-10-29): the draft proxy (`proxy-oracle`, `flat-change-tracking-oracle`), optimistic change detection (`optimistic-history-oracle`, with a `flipZeros` edit and @@ -7212,3 +7300,29 @@ the replay can still be taken for it (open limit). A cached valid result served client-cache behavior and is not generated, because the model starts a restarted run empty. A fixed-seed campaign runs the React client (`watchQuery`). + +### Ordered-loader authority CI counterexample, 2026-11-03 + +Owner: `packages/db/tests/query/ordered-source-loader-state-oracle.test.ts`, +property `ordered-loader.request-authority`. CI run `41699469835` on +`5911d5e00` fails the no-leak check: requests `[2,3]` remain held after +authoritative request 4 succeeds. Direct replay reproduces on the PR and an +untouched archive of exact main `1443dc92e5417394acbdc98b782caa9802848ce6`: + +```sh +TANSTACK_DB_ORACLE_SEED=-416164704 TANSTACK_DB_ORACLE_PATH=2540:0:3:3:8:8:12:12:13 TANSTACK_DB_ORACLE_PROPERTY=ordered-loader.request-authority pnpm exec vitest run tests/query/ordered-source-loader-state-oracle.test.ts -t 'holds, retires, and recovers requests under generated settlement orders' --coverage.enabled=false --typecheck.enabled=false --maxWorkers=1 +``` + +Input: `indexed=false`, `limit=1`, `settledFirst=true`, `releaseFault=false`, +`syncRepair=true`, `requiresFullSource=false`, `joined=true`, `snapshotRows=1`, +`needsMore=true`, `retryTimer=false`, `releaseTruncates=false`, `offset=0`, +`releaseReenters=true`, `releaseDisposes=false`, `releaseInvalidates=false`. +History: `truncate; settle(0, success); invalidateThenFailOldest; move(-1); +joinedBusy; settle(0, success)`. + +Status: confirmed baseline oracle failure, underlying repair location unresolved. +Next witness: distinguish whether the final authoritative read covers the held +requests while joined work defers a reentrant pass, then check the real +subscription receiving path. This remains separate from PR #2346's observer +metadata-sharing and discarded-publication scope. The test, grammar and run +budget remain unchanged; a green later random campaign does not close it. diff --git a/docs/contributing/oracle-reviews/2026-11-01-observer-row-containers.md b/docs/contributing/oracle-reviews/2026-11-01-observer-row-containers.md new file mode 100644 index 0000000000..079cd8e78d --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-11-01-observer-row-containers.md @@ -0,0 +1,1338 @@ +# Observer row-container sharing evidence + +This independent PR starts from upstream main at +`15d2d2d328db6f5a4ea0f1a48528c6c5137a1716`. It contains no Angular source or +framework-version change. Observer source SHA-256: +`abb037706e51c05174cb61170afc314f4ac804f611d8da4bc6b7edc6bc20e120`. +The primary owner is `live-query-observer-snapshot-oracle.property.test.ts`. + +- **Law:** metadata-only status/health changes replace the full snapshot while + retaining its data array and lazy state Map. Writes still replace containers, + and retained snapshots keep their capture-time rows. The maintainer approved + this optimization on 2026-11-01; LiveQuerySnapshot documents the new guarantee. +- **Reach:** `live-query-observer.row-container-sharing` executes 12 shared + examples and 40 generated histories per fixed/random campaign. Both campaigns + reach status-only and health-only transitions in 52 histories each. The fixed + campaign reaches attached/detached 26/26, granular/wholesale 31/21, empty rows + in 11 histories, and same-key value writes in 40. Read order varies between + data-first and state-first. Every history becomes ready before health reports. +- **RED:** from `packages/db`, run + `../../node_modules/.bin/vitest --run --coverage.enabled=false tests/live-query-observer-snapshot-oracle.property.test.ts -t 'metadata snapshots'` + with unchanged production. Both campaigns reject a fresh empty array at the + loading-to-ready transition; fixed seed 1101, shrink path `0:0:0:0`. + A first fixture attempted health reporting before readiness and failed setup; + the grammar was corrected with a legal readiness prefix before recording RED. +- **GREEN:** the observer ordinary/history/hydration/retained-snapshot suites + pass 100 tests. The unchanged Angular adapter also passes 89 tests with one + existing TODO against this observer. ESLint, oracle lint, focused TypeScript, + private-member mangle validation, and package build pass on the independent + branch with Node 24.8.0 and pnpm 11.1.0. The documentation snippet check reports + no new failures across 804 examples. Full-workspace pnpm test is not claimed. +- **Mutant:** reusing containers solely by row count fails on two writes to key + `a`: the second value stays stale despite unchanged membership (seed 1101, + path `0:0:1:2`). The shared model's row assertion rejects it. The final code is + restored and the same oracle passes. Earlier point-in-time calibration also + rejects a getter that reads current Collection rows instead of captured rows. +- **Limits:** the new lane covers synchronous direct Collections, zero through + three string-keyed rows, both observer modes and attach states. Existing + ordered/findOne histories and hydration tests protect neighboring behavior; + order-only changes retain the existing observer layout witnesses. No new + framework, browser-rendering, SSR-scheduler, or external-provider claim. + +## Implementation and release impact + +One bounded row cache contains captured entries, their array, and a lazy Map. +Full snapshots capture that cache rather than forwarding through old snapshots, +so metadata updates neither rebuild row containers nor retain an unbounded chain +of full snapshots. Unchanged entry recaptures preserve identity. When a row +revision is unchanged, entry comparison still notices layout-only changes and +supports the existing revisionless-input path. Value-only writes cannot hide +behind layout identity. The private-member mangle cache includes the new field. + +The size comparison reports db +28 gzip bytes and -21 brotli bytes against the +same upstream production source. The one row cache is necessary to preserve a +lazy Map without forwarding chains; the entry-update branch was simplified +alongside it. Tests and this evidence are development-only. A patch Changeset +releases the shared observer optimization independently of the Angular PR. + +The Angular branch separately projects row-state identity before copying its +Map. Both changes together were tested through Angular's generated metadata +receiving histories before the split. That integration evidence does not turn +this core-only PR into a framework contract: Angular's independent PR owns its +receiving behavior and can merge in either order. + +## Oracle rubric assessment + +| Requirement | Evidence or limit | +| --- | --- | +| ORC-001 | Maintainer-approved structural sharing and the LiveQuerySnapshot contract; limits above. | +| ORC-002 | The existing key/version model changes only on writes; metadata references are compared through a metamorphic relation. | +| ORC-003 | Contract prose, modelRows, grammar, real observer driver, and checkRead remain visible in the existing owner. | +| ORC-004 | Shared examples reconstruct metadata, empty rows, and same-key updates. Removing mode, attachment, cardinality, read order, or action kinds loses a distinct cache/laziness path. Health reports before readiness are excluded by the prefix. | +| ORC-005 | Actual observer getSnapshot before/after each metadata or write receipt, followed by retained-snapshot reads. | +| ORC-006 | Unchanged production and the same-cardinality mutant fail at the intended reference/value checks. | +| ORC-007 | Fixed seed 1101 and unseeded campaigns use identical examples, grammar, budget, and checker; shared replay variables select the property and shrink path. | +| ORC-008 | No production cache state is mirrored; the model retains only key/version rows. | +| ORC-009 | Snapshot explicitly projects data/state across direct and virtual query rows; glossary terms are preserved. | +| ORC-010 | finishHistory preserves primary and cleanup failures. | +| ORC-011 | No additional named shared semantic fault; retained old snapshots independently reject in-place Map mutation. | +| ORC-012 | This record identifies the production source and independent branch boundary. | +| ORC-013 | Metadata transitions require sharing, while same-key value writes require replacement. | +| ORC-014 | Core-only synchronous Collection boundary; no unverified host/provider transfer is claimed. | +| ORC-015 | Eight declared reach states, including metadata transitions and value-only changes. | +| ORC-016 | Fixed and generated histories use the same model and comparison; ordinary examples remain in the ordinary test file. | + + +## 2026-11-03: review repairs and smaller production cache + +This entry supersedes the original reach counts and size measurements above. +The reviewed working tree starts at PR head +`62d15faa1295671b176b7736118d29a4865c805a`. SHA-256 identities for this revision: + +- Observer: `047ddcb95c44eed8338c2cb7d3e6e38e18cddbd6b0132c52d740f8dd5a7c71f2`. +- Snapshot oracle: `6aa5be1bdc005ed5d2001cb3ef0c52b8dff005d4937eb56cbefae518d70519e8`. +- Replay manifest: `ff0f9da90a4ee739e2664dd4185eb67bc999ee45f02b6e6c9f5eb1302b9609a3`. + +### Law, repair, and pre-edit gate + +The authority remains the maintainer's 2026-11-01 row-container decision and +`LiveQuerySnapshot`. Within one row revision, status/health updates preserve +both containers. Value writes replace them. Subscribe/unsubscribe changes +observer ownership without changing rows or ending the sync run. Cleanup/reset +can discard a row revision, even for an empty Collection; sharing across that +boundary is not promised. The interface now states that limit explicitly. + +The old sharing grammar chose attachment once. It could not reconstruct a +ready detached read followed by attach and a health report. Unconditionally +replacing cached entries therefore survived all 14 tests in the old snapshot +owner. The extended grammar includes subscribe/unsubscribe. Its shared examples +force nonempty attach, detach, and reattach in both modes. Repeated subscription +actions are idempotent in this single-subscriber driver. Every action ends with +a public snapshot comparison, and retained snapshots are read again at the end. + +The old health-only reach classifier counted loading-to-ready when the health +tree identity also changed. The new classifier requires unchanged status and +rows. An independent failure-list model checks health freshness and predicts +the exact number of report/clear transitions. The shared comparison checks that +count against observed health-only transitions. Twelve readiness-only examples +exercise the exclusion through the same driver and judgment as every other +history. The other twelve examples exercise ownership transitions and health. +Health reports before readiness remain excluded by the legal readiness prefix. + +Before editing production, the expanded oracle and neighboring observer suites +passed all 100 tests on the PR implementation. The unchanged original observer +and the attachment and reach-classifier mutants then failed the expanded +comparison. Thus the test path and neighboring guarantees were established +before simplifying production. + +### Reach + +Each fixed/random campaign runs 24 shared examples plus 40 generated histories. +Seed 1101 remains the fixed seed. The grammar, budget, examples, driver, and +comparison are identical across campaigns. All declared states must occur in a +fixed example as well as meet their minimum count of one history. +The final five-suite normal run recorded: + +| Declared state | Fixed | Random | +| --- | ---: | ---: | +| attached | 49 | 49 | +| detached | 49 | 49 | +| granular | 37 | 38 | +| wholesale | 27 | 26 | +| empty | 15 | 21 | +| health-only | 34 | 39 | +| status-only | 64 | 64 | +| value-only | 35 | 30 | +| attach-granular | 15 | 11 | +| attach-wholesale | 6 | 8 | +| detach-granular | 16 | 12 | +| detach-wholesale | 8 | 7 | +| reattach-granular | 8 | 7 | +| reattach-wholesale | 5 | 5 | + +Counts are histories entering a state, not action counts. Attachment reaches +require a nonempty result. Health-only reach excludes writes and status changes. +The separate per-history transition-count assertion also rejects a readiness +transition being counted as health-only. The original 52/52 health-only counts +included readiness and must not be cited as measured health-only evidence. + +### RED, mutants, and direct replay + +From `packages/db`, the normal sharing command is: + +```sh +pnpm exec vitest run tests/live-query-observer-snapshot-oracle.property.test.ts --coverage.enabled=false -t 'metadata snapshots' +``` + +All four controls below fail assertions at the intended comparison, rather +than failing setup or timing out: + +- **Original implementation RED:** observer source from merge-base + `15d2d2d328db6f5a4ea0f1a48528c6c5137a1716` fails `metadata keeps data` at + loading-to-ready. Seed 1101, path `0:0:0:0`, reduced input: granular, detached, + empty rows, state-first, commands `[report]` after the readiness prefix. +- **Attachment mutant RED:** replace cached entries unconditionally while + retaining the original dirty-flag condition. Seed 1101, path + `2:3:1:6:4:3`, reduced input: granular, detached, one row, state-first, + commands `[subscribe, report]`. The fresh health snapshot exposes replaced + row containers and fails `metadata keeps data`. This is the mutant that + survived the old owner. It also fails guarded replay with one executed + property and one failed witness. +- **Same-cardinality mutant RED:** replace the row cache only when its length + changes. Seed 1101, path `0:0:1:0:2`, reduced input: granular, detached, empty + rows, state-first, commands `[write, write]`. The second write leaves version + zero visible when the model requires one; `step 2: state rows` rejects it. +- **Reach-classifier mutant RED:** remove the unchanged-status condition. + Seed 1101, path `0:0:0:0`. The model expects one health-only transition, + while the observation counter records two; the transition-count comparison + rejects the extra readiness credit. + +The guarded owner/selector repair is checked with: + +```sh +TANSTACK_DB_ORACLE_PROPERTY=live-query-observer.row-container-sharing \ +TANSTACK_DB_ORACLE_SEED=1101 TANSTACK_DB_ORACLE_PATH=2:3:1:6:4:3 \ +node --import tsx tests/oracle-replay.ts tests/live-query-observer-snapshot-oracle.property.test.ts --coverage.enabled=false +``` + +This command runs only the requested random campaign: one test passes, thirteen +are skipped, and the wrapper records `executions: 1`, `numRuns: 1`, and +`failed: false` after restoration. With the attachment mutant, the same command +records `failed: true` and rejects the same identity violation. Using +`node --import tsx` avoids the CLI's sandbox-restricted IPC listener. + +### GREEN and production size + +The row cache now supplies the previous ordered entries for layout comparison. +This removes `lastLayoutKeys` and its allocation. Metadata-only rebuilds skip +key scanning when the capture is unchanged. The snapshot's state getter lazily +fills the shared row cache directly, removing the second getter and closure. +The single entry-comparison call is inlined, entry capture returns its array +without a redundant result wrapper, and identical notification capture branches +are combined. Both removed private names leave the mangle cache. + +The last materialized row cache is an ordered capture kept unchanged through the +readonly API; its arrays and Maps are not runtime-frozen. It supplies the +same key sequence that the removed key array held. Value-only writes retain +layoutRevision; membership and order changes advance it. The initial empty +capture still advances it once. Older snapshots retain their own cache, with no +chain of snapshot references. No contract or model expectation was weakened to +permit these reductions. + +Validation on the restored final source: + +- Five observer suites: **112 tests pass**, including ordinary layout/order, + hydration, retained-snapshot, health, and history checks. +- Replay-manifest checks: **6 tests pass**; the new guarded replay also passes. +- Focused TypeScript compilation and ESLint, minified package build, `pnpm check:mangle` + (436 names), `pnpm test:minified-db`, and `pnpm lint:changed` pass. +- Two independent follow-up reviewers found no new actionable issue in the + oracle repair or layout/cache simplification. + +`pnpm size:compare --packages db --files` compares against the same merge-base, +with production defines and identical minification/compression: + +| Measurement | Delta from original baseline | +| --- | ---: | +| Observer module, minified | -75 bytes | +| DB export bundle, gzip | -34 bytes | +| DB export bundle, brotli | -111 bytes | +| Typical app, gzip | -27 bytes | +| Typical app, brotli | +79 bytes | + +All eight measured app scenarios decrease in gzip by 26–30 bytes. Brotli +scenario deltas vary from -117 to +79 bytes; the reduction is not universal +across compressed applications. This is net-negative shipped observer code and +DB package bytes, without relying on source comments or formatting. + +### Limits and rubric update + +The closure boundary is metadata sharing across legal same-run row, readiness, +health, and single-subscriber attachment histories at the synchronous direct +Collection's post-action snapshot cut. Zero through three initial string-keyed +rows and same-key writes are covered. The existing query/retained and hydration +owners still cover their stated paths; this lane adds no framework scheduling, +SSR, provider, arbitrary subscriber concurrency, cleanup/restart sharing, or +universal performance claim. Layout/order-only behavior retains its ordinary +observer witnesses; the new lane additionally checks layout stability on +metadata and value-only writes and advancement on insertion. + +The original ORC table remains applicable with these corrections: ORC-004 now +reconstructs attachment transitions and readiness-only exclusion; ORC-006 adds +the attachment and classifier kills; ORC-007 adds the guarded owner and selector; +ORC-015 now has fourteen states with fixed-example reach. ORC-008's only new +model state is the failure list and its transition count: report/clear actions +can distinguish clear from nonempty failure frames, independently of row state. +ORC-010 retains `finishHistory` for primary failure and cleanup fidelity. +ORC-012 uses this source-identified append-only record. ORC-013 distinguishes +unchanged rows across attach from changed values with identical keys. ORC-014 +remains explicitly core-only, so it transfers no controlled premise to a host. +ORC-016's readiness-only and attachment regressions are inputs to the shared +model and checker. ORC-001/002/003/009/011 retain their original evidence and +limits; no new shared semantic fault requiring a second formulation was found. + +## 2026-11-03: equal-revision capture and ten-finding review + +This entry supersedes earlier statements about equal-revision entry comparison, +reach, and size. The external review covered PR head +`e02e09c70ada77d5bbc52b51ebcdd4ee801fe028`. The repair is identified by these +SHA-256 source identities, so a later documentation commit cannot obscure the +tested implementation: + +- Observer: `ef53f61c31e43ee6a35d48fbf19024c98ee6890c17954a3eecf60359ba378cef`. +- Snapshot oracle: `89dd68d82c8d355b63ea10aa5f111537fc0e974600456c91d9cdb1743ed411c9`. +- Replay manifest: `df193e11916eacb35c2ff980cc481a6464b61ff1e60308bfc7eee306009acec1`. + +### Law, experiment, and inference + +The authority remains `LiveQuerySnapshot` and the maintainer's 2026-11-01 +sharing decision. Unchanged public rows preserve containers across metadata +and same-run ownership changes. Changes to values or ordered keys remain +visible. An action that changes no public fact also preserves full-snapshot +identity. Retained snapshots continue to expose their captured rows. + +The reviewed code compared wrapper identities even when the Collection's row +revision was unchanged. An equal-content sync write can discard a virtual-row +wrapper without advancing that revision. A later granular attachment captured +the new wrapper. A health report then exposed newly allocated containers for +unchanged rows. The old grammar had no equal-content source write, and its +sharing assertions omitted full-snapshot identity on no-op actions. + +Before any production edit, the primary owner gained a generated `refresh` +action and no-op identity comparison. A second driver in that same owner added +real ordered projection and findOne paths, provisional hydration-seed handoff, +optional row-clock absence, and rank-only source writes. It uses the existing +`modelRows` and `checkRead` judgment. Source ranks independently determine order +but are absent from the selected public Row. The independent version map changes +only on value writes. Both maps are necessary: a move distinguishes equal +versions at different ranks, while a value write distinguishes equal ranks. + +The initial receiving fixture supplied an invalid two-row seed to findOne. It +was corrected to one selected row before the production gate. That setup failure +is not RED evidence. The corrected receiving campaigns passed on unchanged +production. The six-suite pre-edit gate recorded 116 passes and two failures, +both at the sharing comparison described below, with no type errors. + +The repair uses the captured row and layout clocks together for revisioned +inputs. Equal clocks preserve the existing capture despite re-enriched wrappers. +A changed layout clock still replaces the capture on an order-only move. Only +inputs without the optional row clock need key/value identity comparison. +Queued delivery uses the clocks captured with its entries. The update method +stores both clocks and sets the dirty flag, removing redundant assignments in +detached refresh and attachment. No expectation or run budget was weakened. + +### Reach and grammar controls + +The sharing property retains 24 examples plus 40 generated histories in each +campaign, fixed seed 1101. Its added `refresh` action reconstructs the reported +trace and can cross reports and arbitrary attachment changes. Its bounds remain +zero through three initial rows and at most twelve suffix actions. Equal writes +change no model fact. A legal readiness prefix excludes health reporting before +readiness. Removing refresh loses wrapper re-enrichment; removing ownership +transitions loses its later recapture; removing the no-op assertion admits the +snapshot-cloning mutant below. Existing axes retain their prior justification. + +The capture-path property runs 16 examples plus 40 generated histories in each +campaign, fixed seed 2346. It generates two or three rows, string keys a..c, +and at most twelve actions. The two-row case is the smallest order-only witness; +three rows challenge a larger reordered tail. A move changes rank between -1 +and 100, crossing the other rows. Rank ties and invalid multirow findOne seeds +are outside this grammar. Removing query shape loses the single-result +projection, mode loses a subscription path, clock absence loses structural +fallback, hydration loses seed/live capture separation, and read order loses +one lazy Map path. Move, write, refresh, metadata and ownership actions each +distinguish a neighboring layout, row, identity or attachment rule. + +Checks follow each direct Collection action or the real query's queued +publication, then reread every retained seed/live snapshot. The query driver +checks row values/order, state/data identity, full-snapshot no-ops, +layoutRevision and health freshness. Its task-boundary wait is calibrated by +the order-only mutant, which reaches and fails that post-publication comparison. +It establishes no intermediate-publication timing guarantee. + +Every declared state must also occur in a shared example. Counts below are +histories, from the final normal run on restored production: + +| Sharing state | Fixed | Random | +| --- | ---: | ---: | +| attached | 51 | 50 | +| detached | 45 | 44 | +| granular | 37 | 31 | +| wholesale | 27 | 33 | +| empty | 15 | 20 | +| health-only | 34 | 34 | +| status-only | 64 | 64 | +| value-only | 34 | 31 | +| attach-granular | 13 | 11 | +| attach-wholesale | 8 | 11 | +| detach-granular | 12 | 8 | +| detach-wholesale | 10 | 10 | +| reattach-granular | 6 | 7 | +| reattach-wholesale | 5 | 8 | +| noop-granular | 37 | 26 | +| noop-wholesale | 27 | 31 | +| equal-revision-reenrichment | 30 | 23 | + +| Capture-path state | Fixed | Random | +| --- | ---: | ---: | +| ordered-granular | 13 | 12 | +| ordered-wholesale | 16 | 14 | +| single-granular | 17 | 19 | +| single-wholesale | 10 | 11 | +| revisionless-granular | 14 | 17 | +| revisionless-wholesale | 12 | 13 | +| hydrated-ordered | 12 | 13 | +| hydrated-single | 17 | 14 | +| layout-only-granular-attached | 4 | 3 | +| layout-only-granular-detached | 2 | 3 | +| layout-only-wholesale-attached | 3 | 2 | +| layout-only-wholesale-detached | 5 | 3 | +| metadata-ordered-granular | 6 | 10 | +| metadata-ordered-wholesale | 9 | 8 | +| metadata-single-granular | 12 | 13 | +| metadata-single-wholesale | 5 | 9 | + +### RED, GREEN, and calibration + +From `packages/db`, the six-suite gate command is: + +```sh +pnpm exec vitest run tests/live-query-observer-snapshot-oracle.property.test.ts tests/live-query-observer-history-oracle.property.test.ts tests/live-query-observer-hydration-oracle.property.test.ts tests/live-query-observer.test.ts tests/live-query-observer-snapshot.test.ts tests/live-query-order-only-move.test.ts --coverage.enabled=false --maxWorkers=2 +``` + +- **Parent RED:** reviewed observer e02e09c70 fails `metadata keeps data`, seed + 1101, path `2:4:5:6:8:5`. Reduced history: granular, detached, one row, + state-first, `[refresh, subscribe, report]` after readiness. This is also the + original-defect mutant, rerun after repair and rejected at the same comparison. +- **Narrow-repair mutant:** ignore the layout clock while trusting the row + clock. The new capture-path checker rejects stale ordered rows after one + `move`: ordered, granular, revisioned, unhydrated, two rows, state-first. + Seed 2346, path `0:2:0:1:1:0`. This distinguishes the repair from a patch + that fixes wrapper churn but loses layout-only changes. +- **No-op mutant:** return a fresh shallow full-snapshot object on every read. + It passes the original sharing lane. The extended lane rejects it at + `no-op keeps snapshot`, seed 1101, path `0:0:1:0:0:0:2`, granular, detached, + empty, state-first, `[healthy, report]` after readiness. +- **Repaired GREEN:** restored production passes all 118 tests in the six + suites, with no reported type errors. The intended comparisons, not a setup + error or timeout, reject all three mutants. No mutants remain installed. + +Both recorded production failures were also checked through guarded direct +replay. Each runs exactly one random property witness: it fails with its +mutant and passes with repaired production. Substitute the capture-path +property, seed 2346 and path `0:2:0:1:1:0` for the second replay: + +```sh +TANSTACK_DB_ORACLE_PROPERTY=live-query-observer.row-container-sharing \ +TANSTACK_DB_ORACLE_SEED=1101 TANSTACK_DB_ORACLE_PATH=2:4:5:6:8:5 \ +node --import tsx tests/oracle-replay.ts tests/live-query-observer-snapshot-oracle.property.test.ts --coverage.enabled=false +``` + +### Lossless review dispositions + +The user supplied ten findings in source order. The task-local ledger preserves +their exact text and initial experiment plan. This table preserves each claim's +judgment, action and useful remainder without merging overlapping IDs. + +| ID | Claim and evidence | Disposition and action | Durable value | +| --- | --- | --- | --- | +| HR-01 | Equal-revision re-enrichment invalidates containers. Parent RED and guarded replay confirm it. | fixed-now: authoritative row/layout clocks preserve unchanged captures. | Generated refresh/attachment/metadata histories and declared reach. | +| HR-02 | The sharing lane omitted layout-only, revisionless, hydration handoff and findOne paths. Existing neighboring tests did not cross the changed identity law. | fixed-now: capture-path driver adds each boundary before production edits. | Real-query receiving comparison, sixteen reach states and narrow-mutant kill. | +| HR-03 | Metadata snapshots share mutable JS containers. The probe confirms sharing. Calling Map.delete through a cast affects the peer. | accepted-design: LiveQuerySnapshot and window snapshots expose ReadonlyMap/ReadonlyArray. Caller mutation violates that API, while same-revision sharing is explicitly approved. | Retained-snapshot checks reject production mutation. Runtime freezing or hostile caller isolation is a separate API proposal. | +| HR-04 | Ordered-key and key/value loops look similar. | refuted as a useful shared semantic helper: layout depends only on ordered keys, while revisionless capture also depends on values. Keep the short separate comparisons. | Value-only histories require stable layoutRevision. The clock repair removes the second loop entirely for revisioned sources. | +| HR-05 | Detached refresh redundantly sets snapshotDirty after captureEntries. | fixed-now as simplification. No independent wrong public result was demonstrated. | updateCachedEntries owns capture invalidation and both clocks. | +| HR-06 | The two capture references differ only during hydration. A probe finds them different after an ordinary published value write before getSnapshot. | refuted: cachedEntries is the live capture and cachedRows.entries is the last materialized visible capture. They reference arrays, rather than duplicate them. | Preserve pending materialization and seed/live distinctions when simplifying. | +| HR-07 | Equal-clock captures add a linear key/value scan. A deterministic tuple-value probe records 3n reads before and n after, at n=1/8/32. | fixed-now: revisioned comparison uses clocks. Entry capture itself remains O(n), so the total path did not change from O(1) to O(n). | Work-count evidence separates redundant work from an asymptotic or elapsed-time claim. | +| HR-08 | No-op full-snapshot identity was unasserted. The cloning mutant passes the old lane and fails the extended one. | fixed-now: shared comparison checks ownership and repeated metadata no-ops. | Generated no-op actions and reach in both modes. | +| HR-09 | Fixed-history attribution could count shrink/replay runs as examples. | refuted for installed fast-check 3.23.2: examples run first, a failed campaign throws before aggregate reach, and path replay disables aggregate reach. | Successful, failing, shrinking and replay probes preserve the assumption. A code comment documents it. | +| HR-10 | Window data/pages are recopied on metadata. A real window-controller probe confirms fresh arrays and shared state. | accepted-design under this PR's observer-only promise: its Changeset explicitly names live-query observer containers. The infinite contract requires page contents and metadata, not page-array identity across metadata. | Window page sharing remains a separate potential optimization. No infinite-container sharing claim is made here. | + +Loss audit: **10 = 5 fixed-now + 3 refuted + 2 accepted-design**. There are no +deferred items, unresolved in-scope defects, or new sibling violations from +these experiments. The reviewer found a real subtle bug and two useful coverage +gaps. Several correctness labels instead described approved sharing, a redundant +assignment, or an unpromised downstream optimization. The work claim needed +asymptotic qualification. Recommendation: hire with reservations for adversarial +review, with a second reviewer for contract interpretation and prioritization. + +### Production size and remaining limits + +`pnpm size:compare --packages db --files` uses merge-base +`15d2d2d328db6f5a4ea0f1a48528c6c5137a1716`, matching production defines and +compression. Observer minified bytes decrease by **99**, DB gzip by **40**, +and DB brotli by **97**. The typical app decreases by 32 gzip bytes and +increases by 11 brotli bytes. All eight app scenarios decrease by 31–57 gzip +bytes; brotli deltas range from -78 to +125. Compression reductions are not +universal across applications. + +The closure boundary is observer row sharing and no-op identity across the +declared direct-Collection and real-query histories, at the named snapshot cuts. +Existing queued listener, hydration-error and retained-snapshot owners protect +neighboring promises. The optional-clock seam stays within one DB instance. +This does not claim multiple-copy interoperability, arbitrary concurrency, +intermediate query publications, framework rendering, external-provider timing, +cleanup/reset sharing, runtime protection against caller mutation, or infinite +page-array identity. No accepted in-scope counterexample remains. + +The ORC requirements retain prior evidence with these updates: ORC-001/002/003 +use the same authority, independent version/rank model and visible shared +judgment; ORC-004 controls appear above; ORC-005 has real-query receiving paths +and explicit post-publication checks, with no concrete missing-input hypothesis +at those cuts; ORC-006 adds three assertion failures and old-lane survival; +ORC-007 adds guarded replay for both changed properties; ORC-008/009 justify +version/rank state and the clock-absence seam; ORC-010 retains finishHistory; +ORC-011 has no new named shared semantic fault needing a second formulation; +ORC-012 uses the source identities and this append-only audit; ORC-013 separates +equal-content refresh from value change and order-only move; ORC-014 transfers +the direct premise into actual query drivers but makes no host/provider claim; +ORC-015 reports all 33 reach states; ORC-016 keeps fixed witnesses on the shared +model, production driver and comparison. No run budget decreased. + +Final local verification also passes focused TypeScript compilation, ESLint, +`pnpm lint:changed`, the 41-test `tests/oracle-replay.test.ts` suite, +`pnpm --filter @tanstack/db... build:minified`, `pnpm check:mangle` (436 names), +and `pnpm test:minified-db` (113 error names plus index/query/live-update checks). +The full workspace suite did not run locally. CI for the follow-up head must be +checked separately; previous-head CI is not evidence for this semantic change. + +## 2026-11-03: nine-finding follow-up and record-shape decision + +This evaluates the second high-effort review of `71bb4c659`. The task-local +append-only ledger preserves all nine raw findings and three supplementary +claims. Production and oracle repairs follow that ledger's whole-review plan. +The user approved publishing prototype changes on 2026-11-03, after discussing +cost. This changes change-event equality for record-shaped objects; it does +not revise draft reconstruction or the specialized built-in value comparisons. + +### Laws, experiment, and inference + +**Fresh captures.** Collection's `_deferPublication()` promises to defer delivery +while rows and indexes commit normally. `_stateRevision` promises monotonic +invalidation. A detached snapshot read during a deferral must not retain that +capture after another committed write. The old discard rewound both clocks; +a later write could reuse the cached numbers. Keeping the committed row clock +fixes value captures. When restoring a discarded layout clock, advancing the +row clock also fences the old layout capture. Failed facade flushes still +restore rows and publish neither events nor a layout revision. + +The primary snapshot owner's capture-path grammar now generates `discard-write` +and `discard-move`, including one or two nested deferrals, both observer modes, +ordered/findOne projections, hydration, and optional reattachment. It captures +inside the hold, discards it, then writes again without an intervening read. +It compares public rows, containers, layout and retained snapshots after the +query's queued publication. These additions expose the real counter reuse; +assigning arbitrary numbers to private counters is unnecessary. + +**Record shape.** The approved rule distinguishes plain, class and null-prototype +records even when their enumerable fields match. Fresh equal values of one +prototype still suppress changes. `live-query-observer.record-shape` models a +shape name and one numeric leaf independently of production equality. Each +history runs through both a direct Collection and a projected live query. +Records, arrays, Map values and Set members carry that nested value. Both modes +cross refresh, replacement, attachment and health. At every applied checkpoint, +`data` and `state`, and an attached granular mirror, match the model's exact +prototype and numeric value; all retained snapshots are checked again. + +The utility equality owner updates the prior explicit class/plain equivalence +under this maintainer decision. Each of its 100 generated cases per campaign +reaches same-class, plain/class and null/plain comparisons, with a per-case +minimum of one for each construction. The draft-revert owner remains unchanged +and passes: a draft's serialized plain representation still reconstructs a +class instance under its separate equality domain. + +**Distinct APIs.** An observer snapshot retains a row revision's capture. +Granular listeners receive raw Collection changes. Equal re-enrichment can +produce distinct objects under those two contracts; neither contract promises +cross-API reference identity. The new receiving lane enforces agreement on +shape and values, while the existing sharing lane keeps the old equal capture. +The raw-change distinction is now explicit in the observer interface. + +### Evidence block + +- **Law:** monotonic capture invalidation and committed reads, from Collection's + revision/deferral API; retained snapshots and metadata sharing, from the + existing observer contract; record-prototype publication, from the user's + 2026-11-03 decision. The architecture's failed-facade layout law is preserved. +- **Reach:** row-sharing keeps 24 examples plus 40 generated histories per + campaign. Capture paths keep 16 examples plus 40 generated histories; fixed + seed 2346 reaches all 27 states, including 21 nested-discard histories and + 7–14 histories in each of the eight discarded-change/mode/reattachment cells. + Shape uses 16 examples plus 40 generated histories per campaign, each with + both receiving drivers. Fixed seed 234602 reaches source/query granular in + 27 histories each and source/query wholesale in 29 each; plain→class and + class→plain each reach 18, plain→null 20, and equal-shape copies 33. Record, + array, Map and Set carriers reach 18/11/16/11. Both fresh campaigns also pass + all declared minima and fixed-example reach. No run budget decreases. +- **RED:** on unchanged `71bb4c659` production, the five-owner pre-edit gate has + 8 assertion failures and 296 passes. The new snapshot cases reject both + discarded value and layout captures. Seed 2346/path `0` keeps version 2 where + version 3 is owed. Shape seed 234602/path `0:1:0:2:2` rejects plain→class with + equal leaf 0 in **both** direct and query receiving paths. The utility owner + rejects its newly approved shape expectation in both campaigns. Draft revert, + facade rollback and Collection lifecycle publication pass before production + changes. Earlier CLI-option and concurrent-coverage setup errors are excluded. +- **GREEN:** all 1,423 tests in 15 relevant suites pass, including all observer + owners, snapshot and order-only tests, window controller, utilities, draft + revert, facade rollback, Collection publication/reentrancy, change-message + reconstruction and replay manifest. Focused TypeScript, ESLint, changed-file + oracle lint, minified build/API checks and the 434-name mangle cache pass. +- **Mutant:** restoring the original Collection changes manager fails at the + second post-discard value capture. Keeping row monotonicity but omitting the + discarded-layout fence passes that value case and fails the following + order comparison. Restoring class/plain equality fails both shape receivers. + These are assertion failures at the intended comparisons. Guarded direct + replay reproduces each clock/shape defect and passes after restoration, with + one requested witness per replay. No mutant remains installed. +- **Limits:** one/two nested deferrals, two/three query rows, bounded generated + actions and specified queued-publication cuts. Shape checks cover sync writes + with keyed record prototypes and four carriers; they do not establish exotic + built-in subclasses, descriptors, arbitrary cycles, optimistic draft + assignment policy, intermediate query publications, or host/provider timing. + Cleanup/reset sharing and infinite page-array identity remain outside this + PR's promise. The new combined tree with Angular #2327 has not been rerun. + +The gate runs from `packages/db` with `pnpm exec vitest run +--coverage.enabled=false --maxWorkers=2` over the snapshot, utility, draft, +facade-rollback and Collection lifecycle publication owners. The final gate adds +ordinary observer/snapshot/order/window, hydration/history, Collection reentrancy, +change-event history and replay tests. Guarded replay uses the existing +`tests/oracle-replay.ts` interface with property, seed and path. The new manifest +selector isolates `live-query-observer.record-shape`'s random test. + +### Lossless dispositions + +| ID | Technical verdict and evidence | Action | Durable value | +| --- | --- | --- | --- | +| SR-01 | Confirmed: a real discard/read/write history reuses clocks and retains stale rows. | fixed-now at Collection invalidation, with generated value/layout histories and two clock mutants. | Revision monotonicity must include rollback of delivery, independent of event publication. | +| SR-02 | Confirmed observation; the old equality oracle deliberately allowed it. Nested rather than root prototypes provide the actual witness. | fixed-now under the explicit record-shape decision; both source and query receivers reject the original and pass. | Keep change-event and draft equality domains distinct. | +| SR-03 | Confirmed object divergence, but no cross-API reference-identity guarantee was established. Content and virtual values agree, and raw changes match Collection reads. | accepted-design under raw granular delivery and approved per-revision snapshot sharing. Clarify the interface and enforce shape/value agreement. | Do not repair identity divergence by reintroducing equal-write snapshot churn. | +| SR-04 | Confirmed unsupported compatibility machinery: all typed inputs are concrete Collections; the original rationale was cross-copy support forbidden by AGENTS.md. | fixed-now: remove optional clocks/channel and structural fallback. | State supported inputs explicitly. | +| SR-05 | Refuted as duplicate storage: live capture and last materialized visible capture differ after ordinary writes, as well as with seeds. They are references, not duplicate arrays. | Keep both references; document their distinction. Removing unsupported fallback also removes the second comparison loop. | Preserve materialization and hydration boundaries when simplifying. | +| SR-06 | Confirmed redundant enumeration. Before/after deterministic granular attach counts are 2→1; wholesale stays 0. The remaining granular scan is subscription bootstrap. | fixed-now: check both clocks before enumerating an unchanged capture. | Preserve a fresh capture for each queued newer publication. No elapsed-time or universal complexity claim follows. | +| SR-07 | Confirmed duplicate client cache update, 2→1 at real hydration handoff. | fixed-now: queued flush applies the capture once. Existing reentrant/hydration owners pass. | Avoid pre-applying a later queued publication during listener delivery. | +| SR-08 | Confirmed ambiguous ownership wording. | fixed-now: snapshots retain a captured view and metadata snapshots may share its readonly containers. | Separate capture-time isolation from exclusive container ownership. | +| SR-09 | Partly valid: the synthetic input is unsupported. Its hidden row clock did execute the structural branch, contrary to “never run”; the remaining layout channel did not prevent that branch. | fixed-now: replace the invented axis with real initial attachment, retaining both campaign budgets and 16 examples. The ordinary fake-Collection test now checks committed writes and retained rows through a real Collection. | Executing a branch cannot establish a supported production input. | +| SR-10 | Supplementary common-cause claim overlaps SR-01/02/03, but conflates clock rollback, an intentional equality policy and an unpromised identity relation. | duplicate, with these distinctions retained. | Mechanism overlap is not shared contract authority. | +| SR-11 | The review reported 119 passing tests and no parent RED in its own run. That report is historical, not current evidence; it does not erase the prior review's recorded RED. | stale; the new unchanged-production gate and final checks replace it. | Preserve tested head and whether a reviewer actually calibrated the instrument. | +| SR-12 | The earlier spurious-dirty repair works; the new findings target freshness premises exposed by trusting clocks. | duplicate of SR-01/02, preserving the before/after explanation. | Repair upstream invalidation rather than restore unconditional recapture. | + +Raw explicit findings: **9 = 7 fixed-now + 1 accepted-design + 1 refuted**. +Including supplementary claims: **12 = 7 fixed-now + 1 accepted-design + +1 refuted + 1 stale + 2 duplicates**. No deferrals or known accepted-contract +counterexamples remain within the stated boundary. The reviewer found two real +freshness hazards and useful work/scope cleanup, but promoted identity divergence +to correctness without authority and misstated fallback execution. Recommendation: +hire with reservations for adversarial discovery; require contract and execution +verification before treating every label as a defect. + +### Size, provenance, and conformance + +Against merge-base `15d2d2d328db6f5a4ea0f1a48528c6c5137a1716`, +`pnpm size:compare --packages db --files` reports observer **−338** minified +bytes, Collection changes **−7**, utility **+6**, net **−339**. DB gzip is +**−130** and brotli **−171**. All eight app scenarios also shrink: gzip +−109…−142, brotli −15…−119. The prototype distinction adds no traversal; +its cost is a predicate on prototypes the record comparison already reads. + +Reviewed production SHA256: observer +`ba784a54a5b28d79c98e2a39783dbd00782d9584a85717a793b60c0863cd2c7a`, +Collection changes `2824f649107b52a3b24a911b3d3c62baabd5b38f00da083ba51432a8d96551c0`, +utility `9605eb9c38a7c759e2acc757856685d2f46081d161b271c854271563e68f6188`. + +ORC-001/002/003 use the stated authority, small independent version/rank and +shape/number models, and adjacent executable prose. ORC-004 reconstruction adds +both reported traces, ablation separates row versus layout clocks and shape +versus numeric change, and range includes empty earlier sharing cases plus +2/3-row query captures and all record carriers; unsupported clockless objects +and cross-copy Collections are excluded by API/AGENTS authority, rather than +quiet test narrowing. ORC-005 checks the real source and query paths at named +cuts; no concrete missing-input hypothesis remains there. ORC-006/007 have three +mutant assertion failures and guarded RED/GREEN replay. ORC-008/009 retain +version/rank separation and add a model-only shape name, declared above; nested +holds belong to the driver schedule, not copied cache state. ORC-010 preserves +failure/cleanup fidelity with finishHistory and AggregateError. ORC-011 adds no +second reference algorithm: the named shared-fault hypothesis is precisely +class/plain equality, challenged independently by constructor tags and a numeric +model through both production paths. ORC-012 is this versioned record. +ORC-013 uses equal and changed same-prototype neighbors plus an independent +layout-only mutant. ORC-014 transfers the controlled sync premise to actual +projected queries; it claims no external host/provider contract. ORC-015 reports +all changed reach states; ORC-016 retains fixed examples on the shared model, +driver and comparison. Whole-workspace local testing did not run. Final-head CI +and readiness are reported separately; the green checks on `71bb4c659` are only +previous-head evidence after this semantic change. + + +## 2026-11-03: integration with current main + +After pushing `e5c2d6d3c`, GitHub reported a conflict with main at +`1443dc92e`. A normal merge retains main's comparison-mode refactor, signed-zero +row fidelity, and Effect publication identity policy. The only conflict was in +the generic record predicate: preserve this PR's prototype distinction and +main's separate `PUBLICATION` condition. Main already moved cycle detection +above the value-class dispatch, so the old duplicate check is unnecessary. +The glossary now names `sameRowValue` as Collection's change-event comparison. +No framework adapter production edits belong to this PR's diff against main. + +The merged gate passes **1,868 tests in 16 files**, including the original +15-suite gate plus the Effect metadata/callback owner. Both fixed and random +snapshot campaigns meet every declared reach minimum. The three hostile +mutants were repeated against the merged tree: row rollback fails at version +freshness, the layout-fence omission fails ordering after that value step passes, +and the old prototype predicate fails both direct and projected-query shape +comparisons. Guarded replay passes after each restoration. Focused TypeScript, +changed-file oracle lint, ESLint, minified public API validation and the +433-name mangle cache pass. The earlier minified API invocation during mutation +correctly rejected a stale dist; rebuilding after restoration supplies the +successful result. Production utility SHA256 is now +`0aef2b8fdfd4cbbf78eb348ca2bd245dec6e5ca85e98709e382e5bcf2b3b32ab`; +observer and Collection changes hashes above remain unchanged. + +The rebuilt comparison against exact current main `1443dc92e` still yields +**−339 minified production bytes** (observer −338, Collection changes −7, +utility +6). DB gzip is **−126** and brotli **−27**. All eight app scenarios +shrink in gzip, by 104–141 bytes. Brotli ranges from −240 to +23 bytes: the +group-by scenario grows 23 bytes, while the other seven shrink. These current +compression results supersede the earlier baseline's measurements for the +merged tree. Full workspace local testing and the combined Angular #2327 tree +remain unverified. Exact pushed-head CI is tracked separately. + + +## 2026-11-03: CodeRabbit Set work and pooled receiving paths + +This section records changes on parent `f66d68490e827393d18d67ba8e3f6ce9e003138a`. +It supplements the merged-head evidence above. The implementation in this commit +uses candidate-pair comparisons and augmenting paths for object-valued Sets, +and restores optional layout-channel registration for supported pooled views. +No framework adapter production source changes. The historical cache description +above now explicitly distinguishes readonly access from runtime freezing. + +### Law, experiment, inference, and limits + +An unordered Set comparison needs a bijection of structurally equal members: +separate equal objects still occupy separate positions. The approved prototype +policy distinguishes plain, class, and null-prototype records. AGENTS.md's +Algorithm Efficiency rule rejects combinatorial assignment enumeration. For +scalar record members the new utility lane enforces a generous cubic field-read +ceiling, `4 * (maxSize + 1)^3`. This observation bounds comparison work for that +grammar; it promises no wall-clock latency or global bound for nested values. +Primitive Set and draft comparison paths retain their existing policies. + +The primary utility owner now counts authored shape/value tokens independently +of production. Generated widths 6–12, values 1–5, and rotations 0–12 construct +reordered equal bags, late unmatched class/null records, and multiplicity +deficits where every member has a candidate. Derived empty and singleton bags +use that same model. Each pair runs through `deepEquals` and `sameRowValue`, in +both directions, with acyclic members and members that link back to the compared +Set. No alternate matching algorithm supplies the expected answer. Equal-field +records may duplicate structural value, but every Set member is a distinct object. + +**Reach:** normal fixed and random campaigns each run 100 histories. Their +combined measured counts are 200 for each of twelve states: empty, singleton, +wide, equal, unmatched, deficit, cyclic, acyclic, general-forward, +general-reverse, row-forward, and row-reverse. Every generated input constructs +all these regimes and receiving comparisons. The registered property is +`utils.42`; the registry increases from 41 to 42 without renumbering old IDs. + +**RED:** with production restored to f66, the extended utility lane fails at +scalar read 2049 against ceiling 2048. Fixed seed `20260911`, shrink path +`0:0:0`, reaches width 6/value 1/rotation 0; random seed `10715839`, path +`0:0:0:0`, reaches the same input. The renewed baseline gate preserves failures +from both comparators and directions in an AggregateError. It preceded the +final matcher repair. The earlier generic-only RED did not prove row-path RED; +production was restored before extending that receiving driver and rerunning. + +**GREEN:** the final five-file source gate passes 190 tests, including utility, +pooled, snapshot, observer and facade owners. The preceding broader eight-file +gate passed 786 tests. The built-dist lane passes 357 tests in six files; +Preact conformance passes 136 tests in two files. Types, ESLint, changed-file +oracle lint, minified public API and mangle checks are recorded with the final +verification. These are focused gates; the full workspace suite did not run +locally. + +**Mutants:** restoring permutation enumeration fails the work assertion in both +comparators/directions. Adding only CodeRabbit's unmatched-element precheck +still fails on multiplicity deficit; that witness distinguishes complete repair +from a one-trace fix. Allowing candidate reuse fails exact bag equality in both +comparators. These three are intended assertion failures, not timeout or setup +failures. Guarded replay of `utils.42`, seed `20260911`, path `0:0:0`, executes +the selected comparison and passes after restoration. Earlier attempts selecting +`utils.43`, or selecting IDs shifted by a top-level registration, were setup +failures and supply no mutant evidence. The trailing describe registration keeps +all earlier replay IDs stable. Replay machinery reports four internal runs for +this shrink path; it is not evidence of four independent histories. + +A second CI finding concerns supported pooled views, which have authoritative +row/layout clocks but intentionally no separate layout subscription channel. +Their normal change stream carries layout changes. Treating that optional +channel as mandatory throws during observer subscription. The pooled owner +already generated this path; the previous local gate omitted that owner. +The repair keeps clock authority and registers the extra channel only when +available. It adds no clockless or cross-copy compatibility branch. + +The pooled owner now declares successful wholesale, granular, and detached +comparisons. Both normal 80-history campaigns reach each mode in all 160 +histories; cleanup with a detached reader holding rows reaches 32 histories, +and an unwatched detached reader holding rows reaches 10. The unchanged +independent membership model and live-query Collection second formulation judge +rows, order, status and change payloads after every legal step. + +**Pooled RED:** before the production repair, all 13 owner tests fail with +`_subscribeLayoutChanges is not a function`. The extended reach gate reproduces +that failure with fixed seed `44502001`, path `0:0:0:0:0:0`, on an empty source +and one peer. This is a production runtime failure at subscription, before the +refinement checkpoint, not an assertion kill. Current-head CI's Preact +`eq-filter-peers` and `pooled-param-change` cases fail on that same path before +the repair; the repaired real Preact receiving suite is GREEN. + +**Pooled calibration:** a silent fallback that returns from attach when the +channel is absent fails the existing granular comparison. Seed `44502001`, +path `1:0:0:0:3:2:2`, starts empty and optimistically inserts one matching row +before cleanup/rollback. At the pending checkpoint the granular mirror has no +row while the model requires it. This is an intended assertion failure after +setup. The restored owner passes. Thus merely avoiding the throw without +subscribing to changes does not satisfy the law. + +CI also exposed two ordinary facade tests that expected private row clocks to +rewind after a failed flush. The failed-flush architecture law restores public +rows, indexes and layout and emits no publication; it does not require clock +reuse. The tests now require monotonic invalidation and one subsequent row +advance. All public row, ordering, index, event, truncate and layout assertions +remain. Both source and built-dist suites pass these tests. + +### Review ledger and reviewer assessment + +Review `5756078957` covers `e5c2d6d3c`; inline `4457695586` and mutable summary +version `4d967a8643c3` were frozen before experiments. The merge retained the +matcher, so its finding still applied at f66. + +| ID | Original claim or sibling | Verdict and action | Durable destination | +| --- | --- | --- | --- | +| CR-E5-01 | Late unmatched Set member causes factorial search; precheck it | Confirmed, fixed with polynomial matching | Utility work lane and this evidence | +| CR-E5-02 | Precheck does not bound multiplicity deficits | Confirmed sibling, fixed and generated | Utility bag model and precheck mutant | +| CR-E5-03 | Moderate merge risk from Set stalls | Duplicate of 01; prototype trigger retained | Same work lane | +| CR-E5-04 | 16.67% docstring coverage needs 80%; six functions/ten files, five unsupported | Refuted as repository-required remediation; bot says no repository configuration applied. No function-specific missing contract is identified. Existing observer, equality-mode and literate-oracle prose is substantive. Reported arithmetic was not independently reproduced. Keep warning visible without boilerplate docstrings | This rationale and source comments | +| CR-E5-05 | Full local suite unchecked and CI pending | Accurate, accepted validation boundary; preserve checklist | PR validation and final CI outcome | +| CI-DIST-01 | Facade tests expect counter rollback | Fixed internal-clock expectations; public checks retained | Facade test and built-dist gate | +| CI-POOL-01 | Pooled observer subscription throws | Confirmed, fixed with optional channel registration | Pooled owner plus real Preact receiving witness | + +Raw review accounting: five items = two fixed, one duplicate, one refuted, one +accepted design. The two CI siblings remain separately accounted for. Hire +recommendation: **hire as a bug finder**. The performance diagnosis is accurate, +and the reviewer explicitly states its proposed precheck's limitation. The +suggested repair alone does not close the class. The default docstring metric +has little product signal. + +Review `5756250137` covers f66; inline `4457835442` and mutable summary version +`0dd0683f404a` were also frozen. Its raw items remain separate: + +| ID | Claim | Verdict and action | +| --- | --- | --- | +| CR-F66-01 | Historical cache prose calls mutable containers immutable | Fixed: qualify readonly access and lack of runtime freezing | +| CR-F66-02 | Repeat docstring warning, now four unsupported files | Duplicate of CR-E5-04 with changed diagnostic count preserved | +| CR-F66-03 | Full-local-suite and CI caveat | Accepted accurate boundary, same as CR-E5-05 | +| CR-F66-04 | Summary repeats moderate Set-stall risk | Duplicate of CR-E5-01; repair and work evidence apply | + +Four = one fixed, two duplicates, one accepted design. Hire recommendation: +**hire for precise prose review**; the incremental inline comment is a useful +source-based correction. These ledgers account for all raw findings, summary +warnings, merge-risk prose and validation caveats; bot action buttons and tool +instructions supply no additional product finding or authorization. + +### Conformance and size + +ORC-001/002: Set contract, approved prototype policy, independent token counts +and bounded work law are stated above and beside the owner. ORC-003: model, +grammar, production driver and comparison are distinct and literate. ORC-004: +width/shape/value/multiplicity/rotation and cycle axes reconstruct the reported +case and deficit sibling; removing deficit admits the precheck-only repair, +removing work admits factorial search, and removing multiplicity admits candidate +reuse. Bounds include empty/singleton neighbors. Repeated references cannot +create duplicate identity members, and arbitrary mismatched cycle topology is +excluded. ORC-005: both comparison receivers and directions execute before their +result assertion; pooled readers run actual public observers. ORC-006: the +three Set mutants and silent pooled fallback fail intended comparisons. +ORC-007: unchanged utility registration supplies equal 100-run fixed/random +campaigns and guarded direct replay; pooled campaigns retain 80 runs. +ORC-008: no new stateful model. ORC-009: token kinds map to authored record +prototypes; token counts map to distinct structural Set positions. +ORC-010: work errors abort before combinatorial hangs; AggregateError keeps +both receivers' failures. ORC-011: independent counts differ from graph matching; +no additional shared-fault hypothesis was found. The pooled owner retains its +independent membership plus live-query Collection formulation. ORC-012: this +versioned supplement records the evidence and limits for this commit. +ORC-013: the unmatched and deficit neighbors distinguish a precheck from a full +matching repair. ORC-014: the utility work claim is internal and requires no +host premise; Preact supplies a real receiving witness for pooled subscription. +ORC-015: twelve utility and five pooled states have measured reach above. +ORC-016: generated inputs and replays use shared judgment with no separate +handwritten expected answer. + +Against exact main `1443dc92e`, production is **−285 minified bytes**: observer +−311, Collection changes −7, utility +33. DB gzip is **−124** and brotli **−162**. +All eight app gzip scenarios shrink by 91–116 bytes. Brotli spans −211 to +25: +group-by grows 25 bytes; the other seven shrink. These measurements supersede +the preceding −339 and interim −312 values. Unbounded nested comparison cost, +arbitrary cycle topology, and the latest combined tree with Angular #2327 +remain outside this evidence. Final pushed-head CI and the newest CodeRabbit +covered commit are tracked separately from these local results. + + +Reproduce the new owners from `packages/db`: + +```sh +pnpm exec vitest run tests/utils-oracle.property.test.ts tests/query/pooled-live-query-oracle.property.test.ts --coverage.enabled=false --maxWorkers=2 +TANSTACK_DB_ORACLE_PROPERTY=utils.42 TANSTACK_DB_ORACLE_SEED=20260911 TANSTACK_DB_ORACLE_PATH=0:0:0 node --import tsx tests/oracle-replay.ts tests/utils-oracle.property.test.ts --coverage.enabled=false --maxWorkers=1 +``` + +After mutant restoration, this two-owner gate passes 100 tests, with each new +utility reach at 200 and each pooled receiving comparison at 160. The separate +pooled GC owner passes 21 tests. The final TypeScript, ESLint, changed-file oracle +lint, 433-name mangle cache and minified public API commands all exit zero. + + +## 2026-11-03: current-head review and separate cleanup/restart limit + +CodeRabbit summary `6606385727`, frozen version `b09f4de339ec`, reports covered +commit `162eebf398616c0c561b7472557b251ed1cc3a8d` with kind `reviewed`, no actionable +comments, and minimal merge risk. Its automatic-review pause takes effect after +that reviewed commit. It emits no new review ID or inline finding. The two +previous inline comments are marked addressed at this commit. No production +or oracle repair follows this summary; the changes in this supplement only +record review accounting and an unresolved lifecycle limit. + +| ID | Summary claim | Disposition and evidence | +| --- | --- | --- | +| CR-162-01 | No concrete merge-blocking issue found; normal validation pending | Accepted bounded review result, not independent proof of general correctness or green CI | +| CR-162-02 | Docstrings 11.11%, nine functions/twelve files, three unsupported, threshold 80% | Duplicate of CR-E5-04; updated metric retained, default policy rationale unchanged | +| CR-162-03 | Full local workspace suite did not run and CI is pending | Duplicate of CR-E5-05; keep the validation boundary | +| CR-162-04 | Possibly related #2334; no claim that this PR fixes it | Confirmed-open lifecycle witness, recorded below and in the coverage map. No new defect in the declared metadata-sharing or discarded-delivery scope was established | + +Four summary claims = one accepted bounded result, two duplicates, one +confirmed-open separate lifecycle case. Reviewer assessment: useful issue +navigation and accurate scoped change description. No new repair recommendation +was supplied. Its no-actionable-comments result does not erase the linked +counterexample when evaluating wider observer readiness. + +Issue #2334 explicitly left production versus model correctness unresolved. +A temporary replay through the unchanged hydration model and driver reproduces +its exact input in granular and wholesale mode on `162eebf39`: empty seed; +attach; ready; write `b=0`; cleanup; restart; write `b=0` again. The source's +public rows contain `b=0`, but the observer shows none. The Collection row clock +advances from 2 to 3 while the observer capture stays at 2. A separate diagnostic +continuation omits only that loading checkpoint, then marks ready and performs +the original shared comparison: it still fails in both modes at the ready +checkpoint. That diagnostic is not a GREEN run or an oracle expectation change. +No production repair or permanent oracle exclusion was made. + +This narrows the issue's uncertainty: the mismatch also exists after Collection +readiness. Source inspection points to subscription state retained across cleanup +versus the observer's cleared snapshot; a complete causal repair experiment is +still needed. The primary hydration/lifecycle owner and exact trace are now in +the coverage map, with #2334 as the durable issue. Its next work must generate +repeated-key cleanup/restart histories and reject a plausible wrong repair at a +ready checkpoint. This is a separate open lifecycle boundary, not closure +claimed by the metadata-sharing and discarded-publication laws above. The +previous statement that no accepted-contract counterexample remains is limited +to those declared laws; it does not establish general observer lifecycle +correctness. The PR description and final readiness report must retain this +qualification even if current-head CI is green. + +## 2026-11-03: Set oracle counter overhead in CI + +CI run `41682680312` on `c1fa115f5` passes 14,816 database-shard tests, but +both `utils.42` campaigns exceed Vitest's five-second timeout. The random +campaign reports seed `1430169919`; neither failure reports an equality or +scalar-work mismatch. A local run of those same campaigns passes before the +repair, with 3.58 seconds of test time. This local result does not reproduce +the CI timeout. + +The scalar getter previously constructed a Vitest assertion on every read. +It now increments the same counter and throws only when that counter exceeds +the same cubic ceiling. The bag model, generated inputs, comparison paths, +100-run budgets, reach requirements, replay identifiers and test timeout stay +unchanged. No production code changes in this repair. + +The same seeded local command after the change reports 0.313 seconds of test +time. The complete utility owner passes all 87 tests in 0.574 seconds of test +time. Each of the twelve Set reach states still records 200 histories across +the fixed and random campaigns. These elapsed measurements describe this local +run; they create no timing promise. + +Restoring the original factorial matcher and replaying seed `20260911`, path +`0:0:0`, still fails at scalar read 2,049 against ceiling 2,048. This is the +intended work-observation failure, not a timeout or setup failure. Production +was restored byte-for-byte afterward. The unchanged bounded equality/work law +and its limits above continue to apply; this change only reduces checker +overhead. + +## 2026-11-03: Third external review — Set scale and equality scope + +Reviewed and starting head: `22da3b06ea96da2ddaf01fe32f567461e9d7f577`. +The external reviewer supplied seven findings and contextual validation claims. +It reported 3,000-object Set timings of 16,483 ms (interchangeable) and 1,442 ms +(distinct paired members), versus 39/38 ms on main. Those exact old-head timings +were not independently repeated. Deterministic work observations below establish +the regression without depending on host timing. This supplement supersedes the +prior section's generic prototype policy and twelve-state Set model; its original +experimental record remains historical evidence. + +### Laws, experiments, and limits + +**Authority and domain:** the maintainer approved prototype-sensitive Collection +row publication, not a change to exported generic `deepEquals`. Generic equality +retains its keyed-class/plain/null/cross-realm bridge. `sameRowValue` preserves +exact record prototypes. Draft and persisted-snapshot comparisons keep their +separate policies. The utility oracle now models these domains separately. +AGENTS.md's efficiency rule excludes combinatorial search. For equal corresponding +Set members, a linear scalar-read ceiling protects the common paired-input family. +The general fallback must still find a bijection: a greedy first assignment can +fail when a plain record can match either class and a class can match only itself. + +The existing `utils.42` count model becomes a count/capacity formulation. Row +comparison requires exact shape/value counts; generic comparison allows unmatched +class counts to fit opposite plain/null capacity. This independent arithmetic +model imports no production equality or matching algorithm. Its generated width +6–12, value 1–5 and rotation 0–12 dimensions retain empty/singleton, equality, +unmatched, deficit, and root-linked-cycle neighbors. A derived reassignment case +uses a plain record and class A against classes A and B. Its general equality is +true although the first greedy assignment fails; row equality is false. + +New `utils.43` generates widths 1,024–3,072 and values 1–5. Every history crosses +distinct/interchangeable scalar members, acyclic/root-linked forms, both equality +APIs and both directions. The same bag checker observes exact equality and a +`4 * width` scalar-read ceiling. This claims linear work only for equal paired +members, not arbitrary reordered or unequal Sets, nested-value traversal, graph +isomorphism, allocations, or a wall-clock bound. Width zero and one stay in the +small owner. Independent arbitrary cycle topologies remain outside its model. + +**Reach:** `utils.42` keeps 100 fixed and 100 random histories. Each of its thirteen +states reaches 200 histories, including reassignment. `utils.43` runs ten fixed +and ten random histories; each of its eight declared states reaches 20. The new +budget does not reduce any existing budget or renumber earlier replay IDs. +Generic record comparisons also require same-class, plain-class, null-plain, +foreign-plain and row-prototype constructions in every input. + +**RED before production:** the extended utility/observer/draft gate had six +intended failures and 143 passes across three files. The large Set lane fails at +read 4,097 against ceiling 4,096 (fixed seed `20260911`, path `0:0`; width 1,024, +value 1). Generic record equality fails for an equal keyed class/plain pair +(`utils.29`, fixed seed `20260911`, path `0:0`, input `[["a","a",1,1]]`). +The unchanged snapshot and draft-revert owners pass. The reassignment comparison +was already in this gate; its named reach flag was added immediately afterward. + +The observer's separate constructor invariant comes from AGENTS.md's requirement +to reject contradictory collaborators before mutation. Ordinary invariant +witnesses hide one real Collection clock with undefined, null, a string, NaN or +Infinity. They require rejection before client binding, enumeration, subscription +or sync activation. These are deliberately invalid inputs, not supported +Collection histories or cross-copy interoperability. All ten witnesses fail +before the constructor repair because construction does not throw. This focused +RED was obtained after the independent Set/shape repairs but before editing the +constructor. The first GREEN attempt exposed a fixture assumption: the generic +source helper had already started sync. An explicit idle Collection now supplies +the precondition; it does not relax the rejection or resource assertions. +Real Collection and pooled drivers provide the neighboring valid-input evidence. +No per-value instance tracking or structural compatibility fallback was added. + +**Production repair:** try paired Set equality before building candidate lists; +prefer free candidates before reassignment; reuse the ancestor cycle map because +recursive comparisons remove their own pairs. Restore generic record bridging +while keeping row prototypes strict. Route detached refresh and the attach +handshake through `captureEntries`, leaving separate capture-time and queued +publication-time clock checks for their distinct responsibilities. Reject +absent/non-finite clocks at observer construction. Error 262 follows the existing +coded production-message contract. The catalog and generated error page must +both be committed; CI's ordinary generator check enforces page freshness. + +**GREEN:** the final seven-file command passes 381 tests: + +```sh +pnpm exec vitest run tests/live-query-observer.test.ts tests/live-query-observer-snapshot-oracle.property.test.ts tests/utils-oracle.property.test.ts tests/utils.test.ts tests/query/pooled-live-query-oracle.property.test.ts tests/production-error-messages-oracle.test.ts tests/proxy-revert-oracle.property.test.ts --coverage.enabled=false --maxWorkers=2 +``` + +Run from `packages/db`. TypeScript and focused ESLint pass. Changed-file oracle +lint, minified public API, production-error erasure and mangle checks pass. The +public API check covers 113 error names; the erasure check covers 265 literals in +ESM and CommonJS. Existing snapshot sharing, no-op, shape, capture and pooled +receiving comparisons retain their earlier laws. The capture probe observes one +unchanged granular-attachment enumeration (Collection bootstrap), zero wholesale +attachment enumerations, and one hydration-handoff cache update. + +**Calibration:** direct guarded replay rejects removal of the paired fast path +at read 4,097/4,096 (`utils.43`, seed `20260911`, path `0:0`); strict generic +prototypes fail equal-record comparison (`utils.29`, same seed/path); greedy-only +matching fails bag equality (`utils.42`, path `0:0:0`). Restoring the original +factorial matcher still fails at read 2,049/2,048 (`utils.42`, same seed/path). +All are intended assertion/work-observation failures, not timeouts or setup +failures. Every mutation restores production byte-for-byte. A first scratch +benchmark failed ESM setup and supplies no evidence; the corrected benchmark +succeeds. Five-run medians at 3,000 members are 0.843/0.511 ms repaired versus +32.140/30.286 ms main for interchangeable/distinct paired objects, respectively. +These timings supplement rather than define the work law. + +**Size:** `pnpm size:compare --files --packages db,react-db --base origin/main +--exact-base` uses exact main `1443dc92e5417394acbdc98b782caa9802848ce6`. +Observer −408, Collection changes −7, equality utility +153 = **−262 minified +production bytes**. DB export gzip −109 and brotli −93; React is unchanged. +All eight app scenarios shrink by 62–98 gzip bytes. Brotli deltas range −130 to ++41; Effect +41, group-by +26 and optimistic-action +16 are compression increases. +Test and documentation growth is separate from shipped production weight. + +### Lossless finding audit + +| ID | Original claim | Technical verdict and action | Durable value / destination | +| --- | --- | --- | --- | +| R3-01 | utils:203 eager N² pairs and augmentation cause the reported large equal-Set slowdown; width-12 oracle misses it | Confirmed common-case work regression; fixed-now with paired linear path and free-candidate preference. Exact reported times unverified, deterministic RED/GREEN and large paired benchmark executed. | `utils.43` scale dimension; retain small deficit/bijection law and explicit general-fallback limits. | +| R3-02 | utils:296 prototype distinction broadens public deepEquals and changes foreign/null plain-record comparison without release disclosure | Confirmed scope error; fixed-now by restoring generic bridging, preserving strict Collection rows, and documenting each comparison domain. | Generated real foreign record plus class/plain/null neighbors; main's persisted-snapshot unit expectation restored. | +| R3-03 | utils:205 per-pair new Map(visited) wastes allocation because recursion removes additions | Confirmed from every normal return path; fixed-now. Existing ring and Set-cycle comparisons pass using the shared ancestor map. | Reuse ancestor bookkeeping; already-visited pairs are never overwritten and throwing comparisons abort the operation. No allocation-count claim. | +| R3-04 | proxy:334 nested classes/null records become plain during optimistic writes; confirmation can add an event | Shape bug confirmed; extra-event regression unproved (plain control also emits two). Deferred with explicit maintainer agreement, tracked in #2349. No proxy source change. | Maintainer: “clearly a bug” and “outside the scope of our current PR.” Issue includes held-update/confirmation trace, original-shape controls, isolation requirement, and draft/revert oracle work. Resume in that issue with generated sibling edits and pending/confirmed shape cuts. | +| R3-05 | observer:302 shared mutable containers allow one consumer's mutation to affect peer snapshots; repeated in all three reviews | Sharing observation is true; accepted-design under approved same-revision sharing and ReadonlyMap/ReadonlyArray API. No runtime-freezing or hostile caller-mutation isolation promise. | Earlier HR-03 remains canonical. Retained snapshots reject production mutation; runtime freezing would be a separate API proposal. | +| R3-06 | observer:401 missing clocks silently stale instead of failing fast after fallback removal | Confirmed invariant gap for an invalid collaborator; fixed-now at construction, before resource effects. | Ordinary invariant witnesses plus real Collection and pooled positive drivers. No support promised for dynamically corrupted or lying finite clocks. | +| R3-07 | observer:619 duplicates clock checks and writes out captureEntries in attach | Confirmed simplification; fixed-now for refresh/handshake. Two remaining checks serve capture and queued-publication responsibilities. | Existing publication/reentry/capture law and deterministic enumeration/handoff probe. | +| R3-08 | Scope grew 8→17 files; reviewer ran no tests, six non-benchmark findings were code readings, and its throwaway benchmark restored the worktree | Accepted contextual report, not an independent product finding. Local validation is separate from the external review. | Preserve reviewed head, reported times and lack of review test execution; no benchmark file enters the PR. | + +Seven explicit findings = five fixed-now + one accepted-design + one explicitly +deferred bug. Including contextual R3-08 gives eight items = five fixed-now + two +accepted-design/context + one deferred. #2349's remaining risk is temporary loss +of nested runtime type during optimistic sibling edits; the user approved keeping +this existing draft bug outside this PR. Its concrete resumption condition and +oracle owner are named above and in the coverage map; no universal draft repair +or extra-notification claim is made. + +Reviewer assessment: **hire** for the measured Set regression, public-helper scope +analysis and reusable invariant/simplification observations. The strong findings +are actionable and expose missing scale/domain distinctions. The draft finding +correctly identifies shape loss but overstates untested event consequences. The +repeated mutable-container finding ignores the approved readonly sharing contract. +Six findings had no executed tests; this evaluation supplies independent probes +and law coverage rather than treating code inspection as proof. + +### Oracle review and readiness boundary + +ORC-001/002/003: authority, independent capacity arithmetic, and literate model, +grammar, driver/checkpoint prose are in the utility owner. ORC-004: the grammar +reconstructs reported paired scale, adds reassignment, retains deficit/rotation +and zero/one neighbors; unique object members exclude duplicate identities in a +Set. Ablating width loses scale, kind loses bridging/reassignment, rotation loses +unordered comparison, and root links lose cycle bookkeeping. ORC-005/006: both +real equality APIs/directions execute every case and the named wrong designs fail +the intended comparison. ORC-007: fixed/fresh campaigns share all mechanics and +budgets; guarded seed/path replays above execute directly. ORC-008 does not apply +to stateless bag arithmetic; ORC-009 maps kind/value counts to authored distinct +members without adopting production assignment state. ORC-010: aggregate errors +retain comparator/direction failures; these synchronous utility cases acquire no +resources. Observer/draft drivers keep their existing cleanup. ORC-011: the +capacity model differs from production search and exposes the shared greedy-fault +hypothesis; no additional provider model is needed. ORC-012/013: this record names +law × inputs × API × return-cut bounds, adjacent witnesses and unresolved limits. +ORC-014 has no external provider premise for direct equality; Collection/pooled +receivers exercise supported inputs. ORC-015 reach counts are reported above. +ORC-016: fixed and generated inputs use the shared model/checker; invalid-input +unit witnesses stay outside oracle files. + +The previous pushed head `22da3b06e` has 25 successful checks and one skipped +CodeRabbit check. Those checks do not validate this new semantic tree. CI for its +new pushed head must run. CodeRabbit's last covered production commit remains +`162eebf39`; automatic review is paused. No later review is implied. +No counterexample remains in the declared metadata-sharing, discarded-publication +or paired-Set work scope. General readiness remains unproven: separate #2334 +cleanup/restart freshness still has its recorded counterexample, #2349 now owns +the explicitly excluded draft bug, and the combined Angular #2327 tree is untested. + +Implementation stamp: `b21dba437b23a401baa9d17732712c5e21a9a8d5` contains the third-review production, oracle, +invariant and coverage changes evaluated above. The following documentation-only +commit records that exact head; it changes no production or test semantics. + +## Four-agent oracle audit and repairs, 2026-11-03 + +Starting reviewed head: `779466c62c8679a90df940028c7b76d61b7da80f`. +Four independent reviewers challenged the Set work observer, equality domains, +observer snapshots, and the overall closure argument. All returned one finding. +Their results distinguish a production bug, two oracle gaps, and inaccurate +prose. No new observer-cache production defect was found. + +### Law, experiment, and inference + +**Sparse arrays (A-01).** Generic and change-event equality read every array +position. A hole equals undefined, but not a defined number. Symmetry and +Collection change-message reconstruction supply the authority; draft/persisted +presence fidelity remains a separate law. `Array(1)` versus `[1]` compared true +forward and false backward on both the reviewed head and exact main. A real +Collection probe stored `[1]` without emitting a change or advancing its clock; +the observer retained undefined. The existing `utils.31` only generated equal +hole/undefined pairs, and its symmetry arbitrary used dense arrays. + +Before production changed, the utility grammar gained a guaranteed hole, +generated length 1–5, target index and defined number, with other holes, +undefined and numbers. Both equality APIs/directions receive direct arrays, +records and singleton Sets. Shared `sparse-array-value-oracle.ts` retains a +simple positional model; the snapshot owner receives that same model through +real Collection events, source reads, direct/projected observers in both modes, +attached/detached reads and retained snapshots. Every generated case executes +all 24 carrier/receiving-path combinations. Histories write undefined, a defined +number, a hole, and the number again. They preserve the equivalent neighbor's +container identity while requiring defined replacements to become visible. + +**RED:** from `packages/db`, before the production edit: + +```sh +pnpm exec vitest run tests/utils-oracle.property.test.ts tests/live-query-observer-snapshot-oracle.property.test.ts --testNamePattern='31:|publishes changed array positions' --coverage.enabled=false --maxWorkers=1 --typecheck.enabled=false +``` + +Four intended failures, 107 skipped. Utility fixed seed `20260911`, path `0:0`, +shrinks to `{cells:[undefined],at:0,value:0}` and accepts unequal values. Receiving +fixed seed `234603`, path `0:0:0:0:0`, shrinks to `{cells:[0],at:0,value:0}`. +All 24 scenarios report stale positional values: direct paths at the repeated +hole-to-number write and projected paths at the earlier defined write. This is +an assertion failure after real applied writes, not setup or scheduling failure. +The original utility source SHA-256 is +`d217a89f4a3d44f5839e9a6455e279aa051ae309a6a5ae090247f05ad1fbcba0`. + +**Repair:** replace the hole-skipping `Array.every` traversal with an indexed +loop, preserving the ancestor-map cleanup. No draft comparison branch changes. +**Reach/GREEN:** seven utility states each reach 200 histories. Fourteen receiving +states each reach 20 histories in each campaign, including all eight separate +source/query × mode × attached/detached paths. The full two-file gate passes +111 tests, and the final focused gate passes all four selected tests with type +checking. The recorded receiving seed/path also passes guarded direct replay. +**Mutant:** a Vite transform restores exactly the old hole-skipping traversal; +guarded `utils.31` and `live-query-observer.sparse-array` replays each execute once +and fail at positional equality. The first receiving replay attempt lacked its +named owner mapping; that setup failure is excluded, and the completed manifest +mapping supplies the executed RED/GREEN. No mutant enters production source. +Final utility SHA-256: +`4db39cbcf54439490be28da88b509b97b1ba08a3b98388ac8171399f44f7dd91`. + +**Set assignment work (S-01).** The scalar-read bound was honest but did not +measure cached candidate assignment. A permutation-DFS mutant survived guarded +`utils.42` seed `20260911`, path `0:0:0`. At width seven, six plain members plus +one class member versus seven plain members required 96 scalar reads on either +implementation, while native Map/Set work grew from 197 to 42,828 operations +(8,660 DFS nodes). A width-eight deficit reached 63,296 operations and 13,700 +nodes while remaining within its read ceiling. No full-campaign survival or +timeout-based rejection is claimed. + +The repaired observer wraps Map.has/get/set/delete and Set.has/add/delete only +for the synchronous comparator call, after model and inputs are built. It +restores exact native descriptors in `finally`, including a work-limit throw. +Both measures get `4*(width+1)^3` small-family bounds. Paired inputs retain +`4*width` reads and allow `16*(width+1)` native operations. The same cached-DFS +replay now fails both row directions at operation **2,049 > 2,048**, in 31ms test +time. Normal campaigns pass all four tests: all thirteen small-family states +reach 200 histories and all eight paired-family states reach 20. Models, +budgets and property IDs are unchanged. An ordered-only shortcut survives the +two work lanes but existing `utils.27` rejects it; this is positive portfolio +coverage, not another missing law. + +**Signed-zero model (E-01).** FID-1's maintainer decision of 2026-10-29 covers user +writes. The revert model nevertheless encoded -0 and +0 with the same String +token, including floating typed values and observations. `{f:0}; f=-0` survived +a DRAFT|ROW→DRAFT mutant in that owner, although the flat-tracking owner already +rejected it. Production was correct. Model and recorder tokens now preserve the +sign, with an additive generated root/nested/Float64 assignment lane. Six shared +examples and 100 generated histories run per campaign. The final gate's separate +reach is root/nested/Float64 **43/26/37 fixed**, **36/33/37 fresh**. The mutant +fails the shared required-change assertion on all three paths; restoring legacy +tokens accepts all three again. The normal three-owner gate passes 127 tests, +and direct seed/path replay passes. Existing budgets remain. Integer typed-array +coercion is not changed or claimed. + +**Observer prose (O-01).** `checkRead` said its data/state agreement was +identity-level, but used value comparison. A state-row-cloning mutant passed +120 tests across the snapshot, history, ordinary observer and pooled suites. +A separate `toBe` diagnostic rejected it, but no independent API authority +requires row-root alias identity. The comment now says captured values and +order; the approved snapshot/container identity law stays unchanged. This is +a documentation repair, not a weakened assertion or new product promise. + +### Lossless audit and reviewer assessment + +| ID | Validity | Action | Durable destination | +| --- | --- | --- | --- | +| A-01 | Confirmed pre-existing production defect, directly affecting trusted row clocks | Fixed-now, oracle-first indexed traversal | Utility primary owner and shared snapshot receiving driver; coverage map records both | +| S-01 | Confirmed work-observer gap, no demonstrated current Set production defect | Fixed-now, scoped bookkeeping observation and mutant rejection | Existing Set owner, unchanged truth model and budgets | +| E-01 | Confirmed stale equivalence; neighboring owner already protects production | Fixed-now, sign-preserving model/recorder and generated receiving paths | Revert owner and FID-1 coverage entry | +| O-01 | Confirmed prose/assertion mismatch, alias law unsupported | Fixed-now, accurate value/order prose | Existing snapshot checker | + +Four raw findings = four fixed-now, with no dropped or deferred audit item. +Recommend **hire** for all four reviewers: they executed distinguishing attacks, +reported setup failures separately, respected scope, and separated unsupported +law claims from production bugs. The work audit correctly limits its observation +rather than claiming a universal complexity proof. Their positive observations +remain above; they do not erase the sparse-array counterexample. + +### Validation, size and limits + +The eleven-owner final gate passes **508 tests**, with no type errors. It covers +ordinary/snapshot/history/pooled observers, utilities, draft/revert/native/flat +tracking, production-error messages and generated docs. A later sparse-only gate +also passes after adding exact membership and carrier-size checks. The Angular +ordinary/conformance gate passes **109 tests + 1 todo**, no type errors. CI found +two mechanical omissions on the prior head: error 262's generated page was not +committed, and an Angular synthetic Collection had no layout clock. Regeneration +and a coherent row-backed mock layout getter fix them without relaxing checks. +The ordinary generator gate passes 16 tests. + +Exact-main size comparison remains `1443dc92e`: observer −408, Collection −7, +utility +186 = **−229 minified production bytes**. DB gzip −97, brotli +50; React +unchanged. All eight app scenarios shrink 49–83 gzip bytes. Brotli varies −161 to ++55; compression is not uniformly smaller. No framework production source changes. + +ORC-001/002/003: laws/authority, independent positional and numeric-token models, +and literate grammar/driver/checkpoints are named in the executable owners. +ORC-004: length/index/defined value reconstruct the sparse defect, carriers cross +Set/record recursion, and hole/undefined is the legal equal neighbor; out-of-range +indexes are normalized to a legal position. Zero-path constructors exclude +integer conversion and wrong-shaped targets. ORC-005/006/007: real receiving +paths ran before production changed, wrong designs fail at named comparisons, +and fixed/fresh budgets plus direct seed/path replays are recorded. ORC-008/009: +stateless positional/token models introduce no lifecycle state; authored values +map directly to the named public paths. ORC-010: receiving cleanup preserves +primary/secondary failures, and scoped native methods restore in finally. +ORC-011: count/capacity, positional values and sign tokens are structurally +different from production matching/traversal. ORC-012/013/015/016: this supplement +records adjacent equal/unequal cases, per-path reach, bounded claims and shared +comparisons; no unit assertions were inserted as oracle tests. ORC-014: these +are real core receiving boundaries, not evidence of provider or framework timing. + +The work measure does not bound assignments implemented without the observed +native methods, array scans, allocations, arbitrary nested traversal or all +graph topologies. Sparse publication does not establish array presence fidelity, +browser/framework scheduling or provider delivery. Signed-zero checks observe +final changes/draft/isolation, not every intermediate read or settlement. +The separate recorded #2334 cleanup/restart defect remains open, #2349's draft +shape fix remains outside this PR by maintainer decision, and combined #2327 +integration is unverified. General readiness remains unproven; green CI for an +older head does not validate this new tree. CodeRabbit remains paused at its +earlier covered production commit. + +Four-agent implementation stamp: +`ba336f4b22e002e9836a81fc79783b21f8694395` contains the sparse-array repair, +oracle observations, CI fixtures, coverage and evidence evaluated in this +supplement. The following documentation-only commit names that exact head. + +### CI follow-up on `5911d5e00`, 2026-11-03 + +CI run `41699469835` exposed five source-type errors in the sparse receiving +driver. The earlier Vitest output's “no type errors” did not establish that +these source diagnostics were absent. An explicit `tsc --noEmit -p +tsconfig.test.json` reproduced all five. Separate observer construction for +source and projected Collections preserves their inferred key types. Retained +observations use only data/state, and ordered data is checked as an array before +indexing. No comparison, budget, or production behavior changes. Both sparse +campaigns pass again, with every declared receiving state reaching 20 histories +per campaign. Both the focused TypeScript project and the full DB +`tsc --noEmit -p tsconfig.test.json` check pass. + +The same CI run found a separate ordered-loader counterexample after 2,541 +generated histories: seed `-416164704`, path `2540:0:3:3:8:8:12:12:13`, property +`ordered-loader.request-authority`. Direct replay fails at the no-leak +comparison: requests `[2,3]` remain held after authoritative request 4 succeeds. +The identical replay fails on an untouched Git archive of exact main +`1443dc92e5417394acbdc98b782caa9802848ce6`, with the same comparison and request +IDs. This establishes a baseline counterexample, not whether production or the +oracle's coverage premise needs repair. The owning oracle and loader are +unchanged by this PR. The coverage map preserves the full trace and next +experiment. No test is skipped, no seed is substituted, and no loader repair is +included in this observer change. diff --git a/docs/errors.md b/docs/errors.md index 7adb02e1e4..78fa3e85f1 100644 --- a/docs/errors.md +++ b/docs/errors.md @@ -2639,3 +2639,13 @@ Development builds show this message, with `${...}` replaced by values: ```text limit must be nonnegative ``` + + + +## Error 262: `live-query-observer.ts` + +Development builds show this message, with `${...}` replaced by values: + +```text +LiveQueryObserver requires finite Collection row and layout revision clocks +``` diff --git a/packages/angular-db/tests/inject-live-query.test.ts b/packages/angular-db/tests/inject-live-query.test.ts index 8291356792..cc13faff81 100644 --- a/packages/angular-db/tests/inject-live-query.test.ts +++ b/packages/angular-db/tests/inject-live-query.test.ts @@ -112,6 +112,11 @@ function createMockCollection( get _stateRevision() { return stateRevision }, + // This unordered fixture changes membership only in row publications, so + // its row clock also invalidates every possible layout change. + get _layoutRevision() { + return stateRevision + }, on: (event: string, cb: (e: any) => void) => { if (event === `status:change`) statusSubs.add(cb) return () => statusSubs.delete(cb) diff --git a/packages/db/mangle-cache.json b/packages/db/mangle-cache.json index 000d6639d0..a247a23a47 100644 --- a/packages/db/mangle-cache.json +++ b/packages/db/mangle-cache.json @@ -57,7 +57,6 @@ "pendingSyncedProjection": "a7", "orderedLoadFailed": "ba", "syncStartDeferred": "bb", - "getCollectionLayoutRevision": "bc", "leaseGeneration": "bd", "syncEntryActive": "bg", "builderDependencies": "bh", @@ -191,7 +190,6 @@ "handoffScheduled": "dO", "requestPageCount": "dP", "createLiveQueryRecord": "dQ", - "getCollectionRevision": "dR", "materializeCollection": "dS", "resetSnapshotTracking": "dU", "sendChangesToPipeline": "dV", @@ -247,7 +245,6 @@ "cleanupRetiredEntries": "eR", "comparisonOpts": "eS", "createSyncMetadataApi": "eT", - "lastLayoutKeys": "eU", "pendingSourceRecovery": "eV", "composeBatchedChange": "eW", "ensureStandardSchema": "eX", @@ -290,7 +287,6 @@ "getSyncConfig": "fE", "prepareChange": "fF", "applyChanges": "fG", - "entriesEqual": "fH", "leaseVersion": "fI", "loadBoundary": "fJ", "loadPage": "fK", @@ -434,5 +430,6 @@ "awaitsSiblingReplays": "gN", "grownPrefixRows": "gZ", "ownPrefixEnd": "he", - "sourceListenerCleanups": "cb" + "sourceListenerCleanups": "cb", + "cachedRows": "dB" } diff --git a/packages/db/src/collection/changes.ts b/packages/db/src/collection/changes.ts index d8cd6eb195..296f3ea9a0 100644 --- a/packages/db/src/collection/changes.ts +++ b/packages/db/src/collection/changes.ts @@ -46,7 +46,6 @@ export class CollectionChangesManager< | { depth: number discard: boolean - stateRevision: number layoutRevision: number publications: Array<{ changes: Array> @@ -63,7 +62,8 @@ export class CollectionChangesManager< /** * Monotonic revision of the collection's visible state, advanced once per - * committed batch of changes and cleanup — including while nothing is subscribed. + * committed batch of changes, cleanup, and discarded layout epochs — even + * while nothing is subscribed. * Lets consumers (the live-query observer) cheaply detect "did the data * change" without subscribing, and stays untouched by subscription * bootstrap replays, which do not go through emitEvents. @@ -71,8 +71,8 @@ export class CollectionChangesManager< public stateRevision = 0 /** - * Monotonic revision advanced only for explicit layout-only publications. - * Observers use it to detect reordered rows whose values did not change. + * Revision of explicit layout-only publications. A discarded deferral can + * restore it; stateRevision fences that invalidation for detached observers. */ public layoutRevision = 0 @@ -216,7 +216,6 @@ export class CollectionChangesManager< const deferral = (this.deferral ??= { depth: 0, discard: false, - stateRevision: this.stateRevision, layoutRevision: this.layoutRevision, publications: [], }) @@ -235,7 +234,11 @@ export class CollectionChangesManager< deferral.publications = [] this.deferral = undefined if (deferral.discard) { - this.stateRevision = deferral.stateRevision + // State already committed, so its row clock must never rewind. A + // discarded layout clock also needs a fresh invalidation token: a + // detached observer may have read while delivery was deferred. + if (this.layoutRevision !== deferral.layoutRevision) + this.stateRevision++ this.layoutRevision = deferral.layoutRevision return } diff --git a/packages/db/src/collection/index.ts b/packages/db/src/collection/index.ts index 6d4f74b0cf..388acc1fb7 100644 --- a/packages/db/src/collection/index.ts +++ b/packages/db/src/collection/index.ts @@ -576,7 +576,8 @@ export class CollectionImpl< /** * Monotonic revision of the collection's visible state; advances once per - * committed batch of changes and cleanup, even while nothing is subscribed. + * committed batch of changes, cleanup, and discarded layout epochs, even + * while nothing is subscribed. * Internal — used by the live-query observer's snapshot cache. */ public get _stateRevision(): number { @@ -584,8 +585,9 @@ export class CollectionImpl< } /** - * Monotonic revision of explicit layout-only publications. - * Internal — used to distinguish them from empty ready events. + * Revision of explicit layout-only publications, restored on discarded + * delivery. _stateRevision stays monotonic across that discard. + * Internal — used to distinguish layout publications from empty ready events. */ public get _layoutRevision(): number { return this._changes.layoutRevision diff --git a/packages/db/src/live-query-observer.ts b/packages/db/src/live-query-observer.ts index b778c62cf3..c4eec4c106 100644 --- a/packages/db/src/live-query-observer.ts +++ b/packages/db/src/live-query-observer.ts @@ -1,4 +1,5 @@ import { LiveQueryObserverDisposedError } from './errors.js' +import { codedMessage, devBuild } from './error-message.js' import { runAllCallbacks } from './utils/callbacks.js' import { getLiveQueryStatusFlags, @@ -17,8 +18,10 @@ import type { ChangeMessage, CollectionStatus, HealthNode } from './types.js' * * `getSnapshot()` returns a stable object identity that only changes when the * query changes, so `useSyncExternalStore`-style consumers can compare by - * reference. Each snapshot owns a captured view of `state`/`data`, so reading - * an older snapshot cannot expose rows from a later revision. + * reference. Each snapshot retains a captured view of `state`/`data`, so + * reading an older snapshot cannot expose rows from a later revision. + * Within one row revision, metadata-only changes retain the row containers. + * Cleanup/reset may discard that revision even when its rows were empty. */ export interface LiveQuerySnapshot< T extends object, @@ -99,7 +102,9 @@ export interface LiveQueryObserver< * Subscribe to changes. The listener receives the change set (or `undefined` * for the synthetic notify a ready collection emits on attach). Granular * adapters apply the changes; wholesale adapters can ignore them and re-read - * `getSnapshot()`. Returns an unsubscribe function. + * `getSnapshot()`. Raw changes can carry fresh equal row objects while a + * snapshot retains its capture for the unchanged revision. Returns an + * unsubscribe function. */ subscribe: (listener: LiveQueryObserverListener) => () => void /** Resolve once the collection has loaded its first data. */ @@ -168,8 +173,16 @@ class LiveQueryObserverImpl< // Whether the cached snapshot was built for the hydration read. private cachedServerView = false private cachedSnapshot: LiveQuerySnapshot = DISABLED_SNAPSHOT + // Last materialized visible capture, which can be a hydration seed or lag + // the live capture until getSnapshot() runs. These are array references. + private cachedRows: + | { + entries: Array<[TKey, T]> + data: Array + state?: Map + } + | undefined private layoutRevision = 0 - private lastLayoutKeys: Array | undefined private deliveredLayoutRevision: number | undefined private readonly subscriptions = new Set>() // Publications are dispatched FIFO: an emit that happens while another @@ -195,6 +208,17 @@ class LiveQueryObserverImpl< onPreload: (() => void) | undefined, suspendUntilReady: boolean, ) { + if ( + collection && + (!Number.isFinite(collection._stateRevision) || + !Number.isFinite(collection._layoutRevision)) + ) { + throw new Error( + devBuild() && process.env.NODE_ENV !== `production` + ? `LiveQueryObserver requires finite Collection row and layout revision clocks` + : codedMessage(262), + ) + } this.collection = collection this.wholesale = wholesale this.onPreload = onPreload @@ -213,22 +237,16 @@ class LiveQueryObserverImpl< }, publish: (changes, entries) => { const revision = - entries && collection - ? this.getCollectionRevision(collection) - : undefined - if (entries) { - this.updateCachedEntries(entries, revision) - this.snapshotDirty = true - } + entries && collection ? collection._stateRevision : undefined + const layoutRevision = + entries && collection ? collection._layoutRevision : undefined this.emit( this.wholesale ? undefined : changes, undefined, entries, collection?.status, revision, - entries && collection - ? this.getCollectionLayoutRevision(collection) - : undefined, + layoutRevision, entries !== undefined, ) }, @@ -257,7 +275,24 @@ class LiveQueryObserverImpl< if (this.snapshotDirty) { const entries = this.getVisibleEntries(collection) - const data = entries.map(([, value]) => value) + // Capture each row revision once. Sharing this value across metadata + // snapshots keeps rows lazy and avoids retaining a chain of old snapshots. + let rows = this.cachedRows + if (!rows || rows.entries !== entries) { + // Captured entries already contain the previous ordered keys. Compare + // keys directly: serialized signatures can collide for string keys. + const previous = rows?.entries + if ( + previous?.length !== entries.length || + entries.some(([key], index) => previous[index]![0] !== key) + ) { + this.layoutRevision++ + } + rows = this.cachedRows = { + entries, + data: entries.map(([, value]) => value), + } + } const singleResult = isSingleResultCollection(collection) const liveStatus = this.visibleStatus ?? collection.status // The seed stands in only for a result that is still coming, so an @@ -271,39 +306,14 @@ class LiveQueryObserverImpl< : liveStatus this.cachedServerView = forServerSnapshot - // Bump the layout revision when the ordered key sequence changes - // (membership, ordering, or an order-only move). Compare the key sequence - // directly rather than via a serialized signature: a joined-with-separator - // signature can collide when a key value equals the concatenation of - // neighboring keys around the separator. Comparing keys also avoids - // materializing a large string on every rebuild; a new key array is only - // allocated when the layout actually moved. - const prevKeys = this.lastLayoutKeys - let layoutChanged = - prevKeys === undefined || prevKeys.length !== entries.length - if (!layoutChanged) { - for (let i = 0; i < entries.length; i++) { - if (prevKeys![i] !== entries[i]![0]) { - layoutChanged = true - break - } - } - } - if (layoutChanged) { - this.lastLayoutKeys = entries.map(([key]) => key) - this.layoutRevision++ - } - const health = this.publishedHealth ? this.publishedHealth.tree : (collection as { health?: HealthNode }).health - // Most consumers read only `data`; build the keyed view on first read. - let state: Map | undefined this.cachedSnapshot = { get state() { - return (state ??= new Map(entries)) + return (rows.state ??= new Map(rows.entries)) }, - data: singleResult ? data[0] : data, + data: singleResult ? rows.data[0] : rows.data, collection: getPublicCollection(collection), layoutRevision: this.layoutRevision, status, @@ -349,92 +359,61 @@ class LiveQueryObserverImpl< collection: Collection, ): Array<[TKey, T]> { if (this.clientBinding?.seed) return this.clientBinding.seed.entries - return this.cachedEntries ?? this.captureEntries(collection).entries - } - - private getCollectionRevision( - collection: Collection, - ): number | undefined { - const revision = (collection as { _stateRevision?: unknown })._stateRevision - return typeof revision === `number` ? revision : undefined - } - - private getCollectionLayoutRevision( - collection: Collection, - ): number | undefined { - const revision = (collection as { _layoutRevision?: unknown }) - ._layoutRevision - return typeof revision === `number` ? revision : undefined + return this.cachedEntries ?? this.captureEntries(collection) } private readEntries(collection: Collection): { entries: Array<[TKey, T]> - revision?: number + revision: number + layoutRevision: number } { - const entries = Array.from(collection.entries()) as Array<[TKey, T]> - const revision = this.getCollectionRevision(collection) - return { entries, revision } + const revision = collection._stateRevision + const layoutRevision = collection._layoutRevision + // Reuse the last delivered capture only when both clocks match. A queued + // newer publication must still capture its own rows before reentrant writes. + const entries = + this.cachedEntries && + revision === this.cachedCollectionRevision && + layoutRevision === this.cachedCollectionLayoutRevision + ? this.cachedEntries + : (Array.from(collection.entries()) as Array<[TKey, T]>) + return { entries, revision, layoutRevision } } - private captureEntries(collection: Collection): { - entries: Array<[TKey, T]> - revision?: number - } { - const { entries, revision } = this.readEntries(collection) - this.updateCachedEntries(entries, revision) - return { entries, revision } + private captureEntries( + collection: Collection, + ): Array<[TKey, T]> { + const { entries, revision, layoutRevision } = this.readEntries(collection) + this.updateCachedEntries(entries, revision, layoutRevision) + return this.cachedEntries! } private updateCachedEntries( entries: Array<[TKey, T]>, revision: number | undefined, + layoutRevision: number | undefined, ): void { - const changed = - revision !== undefined - ? this.cachedEntries === undefined || - revision !== this.cachedCollectionRevision - : !this.entriesEqual(this.cachedEntries, entries) - - this.cachedEntries = entries + // Collection clocks are authoritative; equal writes may re-enrich a row + // without changing its value. Keep that revision's captured identities. + if ( + this.cachedEntries === undefined || + revision !== this.cachedCollectionRevision || + layoutRevision !== this.cachedCollectionLayoutRevision + ) { + this.cachedEntries = entries + this.snapshotDirty = true + } this.cachedCollectionRevision = revision - if (changed) this.snapshotDirty = true - } - - private entriesEqual( - left: Array<[TKey, T]> | undefined, - right: Array<[TKey, T]>, - ): boolean { - if (!left || left.length !== right.length) return false - return left.every( - ([key, value], index) => - right[index]![0] === key && right[index]![1] === value, - ) + this.cachedCollectionLayoutRevision = layoutRevision } /** - * While detached there is no delivered-publication clock, so fall back to - * the collection revision. Compatible cross-copy collections that predate - * `_stateRevision` are compared structurally instead. + * A detached read catches up from the Collection's row and layout clocks. + * Supported inputs are Collections and pooled views from this DB instance. */ private refreshDetachedState(collection: Collection): void { const status = collection.status - const revision = this.getCollectionRevision(collection) - const layoutRevision = this.getCollectionLayoutRevision(collection) - - if (revision !== undefined) { - if ( - this.cachedEntries === undefined || - revision !== this.cachedCollectionRevision || - layoutRevision !== this.cachedCollectionLayoutRevision - ) { - this.captureEntries(collection) - this.cachedCollectionLayoutRevision = layoutRevision - this.snapshotDirty = true - } - } else { - const entries = Array.from(collection.entries()) as Array<[TKey, T]> - this.updateCachedEntries(entries, undefined) - } + this.captureEntries(collection) // A health-only change moves no row or status. Trees keep their identity // until their content changes, so identity tells whether it went stale. @@ -509,7 +488,7 @@ class LiveQueryObserverImpl< this.refreshDetachedState(collection) this.attached = true this.visibleStatus ??= collection.status - this.deliveredLayoutRevision = this.getCollectionLayoutRevision(collection) + this.deliveredLayoutRevision = collection._layoutRevision const attachedWithHydrationSeed = !!this.clientBinding?.seed this.blockDelivery = this.wholesale || attachedWithHydrationSeed @@ -548,7 +527,7 @@ class LiveQueryObserverImpl< this.clientBinding?.markAuthoritative() } - const layoutRevision = this.getCollectionLayoutRevision(collection) + const layoutRevision = collection._layoutRevision let layoutChanged = explicitLayoutChange if ( !explicitLayoutChange && @@ -558,23 +537,18 @@ class LiveQueryObserverImpl< // Empty ready events predate the explicit layout signal and share its // empty-array payload. Only forward an empty batch when the collection // confirms that a new layout-only publication occurred. - if ( - layoutRevision === undefined || - layoutRevision === this.deliveredLayoutRevision - ) { + if (layoutRevision === this.deliveredLayoutRevision) { return } layoutChanged = true } - if (changes !== undefined && layoutRevision !== undefined) { + if (changes !== undefined) { this.deliveredLayoutRevision = layoutRevision } const captured = - changes !== undefined + changes !== undefined || status === `cleaned-up` ? this.readEntries(collection) - : status === `cleaned-up` - ? this.readEntries(collection) - : undefined + : undefined this.emit( changes, undefined, @@ -615,14 +589,12 @@ class LiveQueryObserverImpl< notify(undefined, collection.status) }), ) - const subscribeLayoutChanges = ( - collection as Collection & { - _subscribeLayoutChanges?: (listener: () => void) => () => void - } - )._subscribeLayoutChanges - if (typeof subscribeLayoutChanges === `function`) { + // Pooled views carry layout changes in their ordinary change stream. + if ( + (collection as Partial>)._subscribeLayoutChanges + ) { ownRelease( - subscribeLayoutChanges.call(collection, () => + collection._subscribeLayoutChanges(() => notify([], collection.status, true), ), ) @@ -647,18 +619,7 @@ class LiveQueryObserverImpl< // immediately after subscribe returns. this.flushPublications(!this.wholesale) // The render-time read is still current unless the handshake published. - const revision = this.getCollectionRevision(collection) - const layoutRevision = this.getCollectionLayoutRevision(collection) - if ( - revision === undefined || - this.cachedEntries === undefined || - revision !== this.cachedCollectionRevision || - layoutRevision !== this.cachedCollectionLayoutRevision - ) { - const { entries } = this.readEntries(collection) - this.updateCachedEntries(entries, revision) - this.cachedCollectionLayoutRevision = layoutRevision - } + this.captureEntries(collection) } if (this.clientBinding?.seed) { if (!this.wholesale) this.seed(Array.from(this.subscriptions)[0]!) @@ -714,12 +675,9 @@ class LiveQueryObserverImpl< this.updateCachedEntries( publication.entries, publication.collectionRevision, + publication.collectionLayoutRevision, ) } - if (publication.collectionLayoutRevision !== undefined) { - this.cachedCollectionLayoutRevision = - publication.collectionLayoutRevision - } if (publication.layoutChanged) { this.snapshotDirty = true } diff --git a/packages/db/src/utils.ts b/packages/db/src/utils.ts index 5b75b3857c..411eea20e9 100644 --- a/packages/db/src/utils.ts +++ b/packages/db/src/utils.ts @@ -28,6 +28,7 @@ export type DurationFields = Record<(typeof durationFields)[number], number> /** * Deep equality function that compares two values recursively * Handles primitives, objects, arrays, Date, RegExp, Map, Set, TypedArrays, and Temporal objects + * Plain records can compare by keys with keyed class instances and records from other realms. * * @param a - First value to compare * @param b - Second value to compare @@ -48,7 +49,8 @@ export function deepEquals(a: any, b: any): boolean { /** * @internal Whether a synced row changed observably: deep equality that - * tells -0 from 0, as row values do (FID-1; maintainer decision 2026-10-29). + * tells -0 from 0 and preserves record prototypes, as public row values do. + * Maintainer decisions: FID-1, 2026-10-29; record shape, 2026-11-03. */ export function sameRowValue(a: unknown, b: unknown): boolean { return deepEqualsInternal(a, b, ROW) @@ -197,40 +199,41 @@ export function deepEqualsInternal( return aValues.every((val) => b.has(val)) } - // Object-valued Sets are unordered. Match each value once, carrying a - // branch-local cycle map so a failed candidate cannot poison the next. - const matchValues = ( - index: number, - remaining: ReadonlyArray, - branchVisited: Map, - ): boolean => { - if (index === aValues.length) return true - const value = aValues[index] - for (const [remainingIndex, candidateIndex] of remaining.entries()) { - const candidateVisited = new Map(branchVisited) - if ( - deepEqualsInternal( - value, - bValues[candidateIndex], - mode, - candidateVisited, - ) && - matchValues( - index + 1, - remaining.filter((_, otherIndex) => otherIndex !== remainingIndex), - candidateVisited, - ) - ) { - return true - } - } - return false + // A paired equality is already a bijection. Keep common equal inputs linear. + if ( + aValues.every((value, index) => + deepEqualsInternal(value, bValues[index], mode, visited), + ) + ) { + visited.delete(a) + return true } - const result = matchValues( - 0, - bValues.map((_, index) => index), - visited, + + // Recursive comparisons remove their own pairs before returning. Reuse the + // ancestor map; only failed assignments need augmenting-path search. + const candidates = aValues.map((value) => + bValues.flatMap((candidate, index) => + deepEqualsInternal(value, candidate, mode, visited) ? [index] : [], + ), ) + const assigned = new Map() + const match = (index: number, seen: Set): boolean => { + const options = candidates[index]! + const free = options.find((candidate) => !assigned.has(candidate)) + if (free !== undefined) { + assigned.set(free, index) + return true + } + return options.some((candidate) => { + if (seen.has(candidate)) return false + seen.add(candidate) + const previous = assigned.get(candidate) + if (previous !== undefined && !match(previous, seen)) return false + assigned.set(candidate, index) + return true + }) + } + const result = aValues.every((_, index) => match(index, new Set())) visited.delete(a) return result } @@ -292,22 +295,29 @@ export function deepEqualsInternal( if (Array.isArray(a) && !draft) { ;(visited ??= new Map()).set(a, b) - const result = a.every((item, index) => - deepEqualsInternal(item, b[index], mode, visited), - ) + let result = true + for (let index = 0; index < a.length; index++) { + if (!deepEqualsInternal(a[index], b[index], mode, visited)) { + result = false + break + } + } visited.delete(a) return result } // Handle objects if (typeof a === `object`) { - // Instances of two different classes differ. A plain or null-prototype - // object, from any realm, compares by keys with any class: a draft - // snapshot and plain JSON both hold class instances as plain objects. + // Row publication preserves record prototypes. Generic equality and draft + // reconstruction keep their established plain-record/class bridge. const prototype = Object.getPrototypeOf(a) const prototypeB = Object.getPrototypeOf(b) const plain = isPlainPrototype(prototype) const plainB = isPlainPrototype(prototypeB) - if (prototype !== prototypeB && !plain && !plainB) return false + if ( + prototype !== prototypeB && + ((mode & ROW && !draft) || (!plain && !plainB)) + ) + return false if (mode & PUBLICATION && !(plain && plainB)) return false // Compare enumerable symbol keys as well as string keys. Query results may diff --git a/packages/db/tests/fixtures/error-site-messages.json b/packages/db/tests/fixtures/error-site-messages.json index 7f9f22ec3b..76d804aace 100644 --- a/packages/db/tests/fixtures/error-site-messages.json +++ b/packages/db/tests/fixtures/error-site-messages.json @@ -991,5 +991,12 @@ "file": "query/validate-limit.ts", "template": "limit must be nonnegative", "literals": ["limit must be nonnegative"] + }, + "262": { + "file": "live-query-observer.ts", + "template": "LiveQueryObserver requires finite Collection row and layout revision clocks", + "literals": [ + "LiveQueryObserver requires finite Collection row and layout revision clocks" + ] } } diff --git a/packages/db/tests/live-query-observer-snapshot-oracle.property.test.ts b/packages/db/tests/live-query-observer-snapshot-oracle.property.test.ts index 48afa97e66..8566802c40 100644 --- a/packages/db/tests/live-query-observer-snapshot-oracle.property.test.ts +++ b/packages/db/tests/live-query-observer-snapshot-oracle.property.test.ts @@ -6,10 +6,29 @@ import { getLiveQueryHash } from '../src/live-query-options.js' import { localOnlyCollectionOptions } from '../src/local-only.js' import { createLiveQueryObserver } from '../src/live-query-observer.js' import { Query, createLiveQueryCollection } from '../src/query/index.js' -import { oraclePropertyOptions, oracleRuns } from './oracle-config.js' +import { BTreeIndex } from '../src/indexes/btree-index.js' +import { + oraclePropertyOptions, + oracleReplayTest, + oracleRuns, +} from './oracle-config.js' import { declareOracle } from './oracle-declaration.js' import { createOracleReach } from './oracle-reach.js' -import type { HealthNode } from '../src/types.js' +import { finishHistory } from './observer-history-cleanup.js' +import { + checkSparseArray, + makeSparseArray, + sparseArrayCarriers, + sparseArrayCase, + sparseArrayValues, +} from './sparse-array-value-oracle.js' +import type { + SparseArrayCarrier, + SparseArrayCase, + SparseArrayForm, +} from './sparse-array-value-oracle.js' +import type { HealthNode, SyncConfig } from '../src/types.js' +import type { LiveQuerySnapshot } from '../src/live-query-observer.js' /** * # Is a retained live-query snapshot point-in-time? @@ -48,9 +67,10 @@ import type { HealthNode } from '../src/types.js' | health freshness across attach cuts | report, healthy, write, subscribe, unsubscribe, read; one ready source | detached refreshDetachedState, attach, publishedHealth | `health`, `isSyncError`, and tree identity of each snapshot, at every `read` | this generated oracle (second block below); status scheduling and hydration have other owners | * The source is a local-only Collection behind an `orderBy` live query, so - * visible order is key order. Prepared client seeds are checked only for getter - * identity. Hydration replacement, persisted status, and framework - * wiring have other owners. The calibration test feeds the checker a snapshot + * visible order is key order. This first driver checks prepared client seeds + * only for getter identity. The capture-path driver below adds provisional + * seed handoff. Persisted status and framework wiring have other owners. + * The calibration test feeds the checker a snapshot * whose `data` and `state` read the live Collection on first access, the design * #1542 proposed, and requires the checker to reject it. */ @@ -62,7 +82,9 @@ type Command = | { type: `delete`; id: string } | { type: `capture` } type Read = { snapshot: number; dataFirst: boolean } -type Snapshot = ReturnType[`observer`][`getSnapshot`]> +// This projection observes row containers, whether a driver uses a source +// Collection directly or query rows that also carry virtual metadata. +type Snapshot = Pick, `data` | `state`> type Captured = { snapshot: Snapshot rows: Array @@ -191,7 +213,7 @@ function checkRead( } else { const data = read.data as ReadonlyArray expect(data.map(plain), `${label}: data rows`).toEqual(rows) - // Agreement is identity-level: both views list the same row objects. + // Both views expose the captured row values in the same order. expect(data, `${label}: data agrees with state`).toEqual([ ...(read.state?.values() ?? []), ]) @@ -638,3 +660,1359 @@ describe(`a snapshot shows the current health tree`, () => { ]) }) }) + +/** + * Within one row revision, metadata changes replace the full snapshot but + * retain its row containers. Cleanup/reset can discard that revision. + * Authority: maintainer decision 2026-11-01, preserving point-in-time rows while + * preventing status/health changes from invalidating row-only consumers. + * A value write must still create fresh containers, even with the same keys. + * An action that changes no public fact also retains full-snapshot identity. + * + * This extends the retained-snapshot owner with a direct Collection boundary: + * Every history first marks the source ready without writing rows; its suffix + * may then report health legally. Subscribe/unsubscribe actions cross the + * attach cut without ending the sync run. Repeated subscription actions are + * idempotent in this single-subscriber driver. The independent model is + * modelRows' key/version map; only `write` changes it. `refresh` supplies an + * equal source write that can re-enrich wrappers without changing rows. + * At every action the + * driver compares row values and container identities, then checks all retained + * snapshots again to reject a mutable shared Map. The failure list changes + * only on reports and clears; readiness cannot count as health-only reach. + * Attached/detached transitions and both + * observer modes cross distinct cache paths. Empty and nonempty rows test lazy + * Map materialization. The capture-path driver below adds query shapes and + * hydration handoff; this lane claims no provider or framework scheduling. + */ +const containerHistory = fc.record({ + mode: fc.constantFrom(`granular` as const, `wholesale` as const), + attached: fc.boolean(), + count: fc.integer({ min: 0, max: 3 }), + dataFirst: fc.boolean(), + commands: fc.array( + fc.constantFrom( + `report`, + `healthy`, + `ready`, + `write`, + `refresh`, + `subscribe`, + `unsubscribe`, + ), + { minLength: 1, maxLength: 12 }, + ), +}) +type ContainerHistory = + typeof containerHistory extends fc.Arbitrary ? T : never +type ContainerReach = + | `attached` + | `detached` + | `granular` + | `wholesale` + | `empty` + | `health-only` + | `status-only` + | `value-only` + | `attach-granular` + | `attach-wholesale` + | `detach-granular` + | `detach-wholesale` + | `reattach-granular` + | `reattach-wholesale` + | `noop-granular` + | `noop-wholesale` + | `equal-revision-reenrichment` + +async function checkContainerHistory( + scenario: ContainerHistory, + reach: ReturnType>, +): Promise { + const model = new Map( + Array.from( + { length: scenario.count }, + (_, index) => [String.fromCharCode(97 + index), 0] as const, + ), + ) + let controls: Parameters[`sync`]>[0] | undefined + const source = createCollection({ + getKey: (row) => row.id, + gcTime: 0, + startSync: true, + sync: { + sync: (params) => { + controls = params + params.begin() + for (const row of modelRows(model)) + params.write({ type: `insert`, value: row }) + params.commit() + }, + }, + }) + const observer = createLiveQueryObserver(source, { mode: scenario.mode }) + let stop = scenario.attached ? observer.subscribe(() => {}) : undefined + let hasAttached = scenario.attached + let failures: Array = [] + let expectedHealthOnly = 0 + let observedHealthOnly = 0 + const retained: Array = [] + reach.reach(scenario.attached ? `attached` : `detached`) + reach.reach(scenario.mode) + if (model.size === 0) reach.reach(`empty`) + let failure: { error: unknown } | undefined + try { + let before = observer.getSnapshot() + let beforeRead = checkRead( + `ordered`, + { snapshot: before, rows: modelRows(model) }, + scenario.dataFirst, + `initial`, + ) + retained.push({ snapshot: before, rows: modelRows(model) }) + for (const [index, step] of [ + `ready` as const, + ...scenario.commands, + ].entries()) { + const input = controls! + const hadRow = model.has(`a`) + const wasAttached = stop !== undefined + if (step === `write`) { + const version = (model.get(`a`) ?? -1) + 1 + input.begin() + input.write({ + type: model.has(`a`) ? `update` : `insert`, + value: { id: `a`, version }, + }) + input.commit() + model.set(`a`, version) + } else if (step === `refresh`) { + // An equal source write can discard its virtual-property wrapper + // without publishing a row revision. It changes no modeled fact. + if (hadRow) { + const revision = source._stateRevision + const previous = source.get(`a`) + input.begin() + input.write({ + type: `update`, + value: { id: `a`, version: model.get(`a`)! }, + }) + input.commit() + if ( + source._stateRevision === revision && + source.get(`a`) !== previous + ) + reach.reach(`equal-revision-reenrichment`) + } + } else if (step === `report`) { + const error = new Error(`problem ${index}`) + failures.push(error) + input.reportError(error) + expectedHealthOnly++ + } else if (step === `healthy`) { + if (failures.length > 0) expectedHealthOnly++ + failures = [] + input.reportHealthy() + } else if (step === `subscribe`) { + stop ??= observer.subscribe(() => {}) + } else if (step === `unsubscribe`) { + stop?.() + stop = undefined + } else input.markReady() + const after = observer.getSnapshot() + const read = checkRead( + `ordered`, + { snapshot: after, rows: modelRows(model) }, + scenario.dataFirst, + `step ${index}`, + ) + // The direct source is ready after the legal prefix. Attachment never + // changes its status or failure frame; each read must show fresh metadata. + expect(after.status, `step ${index}: status`).toBe(`ready`) + expect(after.isSyncError, `step ${index}: sync error`).toBe( + failures.length > 0, + ) + expect(after.health?.hasErrors, `step ${index}: health`).toBe( + failures.length > 0, + ) + expect( + after.health?.failureReason?.errors.map((error) => error.cause) ?? [], + `step ${index}: reported failures`, + ).toEqual(failures) + reach.reach(stop ? `attached` : `detached`) + if (step === `write`) { + expect(read.data, `value writes replace data`).not.toBe(beforeRead.data) + expect(read.state, `value writes replace state`).not.toBe( + beforeRead.state, + ) + if (beforeRead.state?.has(`a`)) reach.reach(`value-only`) + } else { + expect(read.data, `metadata keeps data`).toBe(beforeRead.data) + expect(read.state, `metadata keeps state`).toBe(beforeRead.state) + if (after.status !== before.status) reach.reach(`status-only`) + if (after.status === before.status && after.health !== before.health) { + reach.reach(`health-only`) + observedHealthOnly++ + } + if (after.status !== before.status || after.health !== before.health) + expect(after).not.toBe(before) + else { + // Ownership and repeated metadata actions change no public fact. + // The full-snapshot contract therefore preserves identity too. + expect(after, `no-op keeps snapshot`).toBe(before) + reach.reach(`noop-${scenario.mode}`) + } + } + if (step !== `write` || hadRow) + expect(after.layoutRevision, `unchanged ordered keys`).toBe( + before.layoutRevision, + ) + else + expect(after.layoutRevision, `new key changes layout`).toBeGreaterThan( + before.layoutRevision, + ) + if (model.size > 0 && !wasAttached && stop) { + reach.reach(`attach-${scenario.mode}`) + if (hasAttached) reach.reach(`reattach-${scenario.mode}`) + hasAttached = true + } + if (model.size > 0 && wasAttached && !stop) + reach.reach(`detach-${scenario.mode}`) + retained.push({ snapshot: after, rows: modelRows(model) }) + before = after + beforeRead = read + } + for (const [index, captured] of retained.entries()) + checkRead(`ordered`, captured, !scenario.dataFirst, `retained ${index}`) + // Calibrate reach through the same model and driver, including histories + // containing only readiness: loading -> ready is never health-only. + expect( + observedHealthOnly, + `health-only transitions exclude readiness`, + ).toBe(expectedHealthOnly) + } catch (error) { + failure = { error } + } + await finishHistory( + [() => stop?.(), () => observer.dispose(), () => source.cleanup()], + failure, + ) +} + +declareOracle({ check: [checkContainerHistory], grammar: [containerHistory] }) + +describe(`metadata snapshots share immutable row containers`, () => { + const examples: Array<[ContainerHistory]> = ( + [`granular`, `wholesale`] as const + ).flatMap((mode) => + [false, true].flatMap((attached) => + [0, 1, 3].flatMap((count) => [ + [ + { + mode, + attached, + count, + dataFirst: count % 2 === 0, + commands: [ + `unsubscribe`, + `refresh`, + `subscribe`, + `unsubscribe`, + `subscribe`, + `report`, + `ready`, + `write`, + `healthy`, + `write`, + ], + }, + ] as [ContainerHistory], + [ + { + mode, + attached, + count, + dataFirst: count % 2 === 0, + commands: [`ready`], + }, + ] as [ContainerHistory], + ]), + ), + ) + for (const campaign of [`fixed`, `random`] as const) { + oracleReplayTest( + it, + ...(campaign === `fixed` + ? [] + : [`live-query-observer.row-container-sharing`]), + )(`reuses rows across metadata changes (${campaign})`, async () => { + const reach = createOracleReach( + `live-query-observer.row-container-sharing`, + [ + `attached`, + `detached`, + `granular`, + `wholesale`, + `empty`, + `health-only`, + `status-only`, + `value-only`, + `attach-granular`, + `attach-wholesale`, + `detach-granular`, + `detach-wholesale`, + `reattach-granular`, + `reattach-wholesale`, + `noop-granular`, + `noop-wholesale`, + `equal-revision-reenrichment`, + ], + { fixedHistories: examples.length }, + ) + // fast-check 3 emits examples before generated histories. A failed + // campaign throws before aggregate reach; path replay disables it. + let histories = 0 + await fc.assert( + fc.asyncProperty(containerHistory, async (scenario) => { + reach.beginHistory({ fixed: histories++ < examples.length }) + await checkContainerHistory(scenario, reach) + }), + { + ...oraclePropertyOptions( + 40, + `live-query-observer.row-container-sharing`, + { examples }, + ), + ...(campaign === `fixed` ? { seed: 1101 } : {}), + }, + ) + reach.assertReached() + process.stdout.write( + `observer row-container sharing ${campaign}: ${JSON.stringify(Object.fromEntries(reach.counts()))}\n`, + ) + }) + } +}) + +/** + * The same snapshot laws also cross query projection, findOne, initial + * attachment, and hydration. This driver adds those receiving paths to checkRead; + * it does not give them a second expected answer. Source rank is deliberately + * absent from the selected Row, so moving it can change only ordered keys. + * + * The independent model keeps version and rank separately. Its sorted rows + * predict the selected view. Equal writes and ownership actions change neither. + * A hydration seed is an earlier capture with version -1; the ready live view + * replaces it and must leave that retained seed unchanged. Checks occur after + * the query's queued publication, not at an intermediate source callback. + * A discarded delivery deferral keeps committed state (Collection's + * _deferPublication contract). A detached read inside it and a later write + * must remain distinct captures even when no read observes the close. + * The discard actions cross this schedule with value and layout changes. + * Reattachment after discard is the neighboring delivery path. + */ +const capturePathHistory = fc.record({ + shape: fc.constantFrom(`ordered` as const, `single` as const), + mode: fc.constantFrom(`granular` as const, `wholesale` as const), + attached: fc.boolean(), + hydrated: fc.boolean(), + count: fc.integer({ min: 2, max: 3 }), + deferralDepth: fc.integer({ min: 1, max: 2 }), + dataFirst: fc.boolean(), + commands: fc.array( + fc.constantFrom( + `move`, + `write`, + `refresh`, + `report`, + `healthy`, + `subscribe`, + `unsubscribe`, + `ready`, + `discard-write`, + `discard-move`, + ), + { minLength: 1, maxLength: 12 }, + ), +}) +type CapturePathHistory = + typeof capturePathHistory extends fc.Arbitrary ? T : never +type CapturePathReach = + | `nested-discard` + | `ordered-granular` + | `ordered-wholesale` + | `single-granular` + | `single-wholesale` + | `initial-attached-granular` + | `initial-attached-wholesale` + | `initial-detached-granular` + | `initial-detached-wholesale` + | `hydrated-ordered` + | `hydrated-single` + | `layout-only-granular-attached` + | `layout-only-granular-detached` + | `layout-only-wholesale-attached` + | `layout-only-wholesale-detached` + | `metadata-ordered-granular` + | `metadata-ordered-wholesale` + | `metadata-single-granular` + | `metadata-single-wholesale` + | `discarded-write-granular-attached` + | `discarded-write-granular-detached` + | `discarded-write-wholesale-attached` + | `discarded-write-wholesale-detached` + | `discarded-move-granular-attached` + | `discarded-move-granular-detached` + | `discarded-move-wholesale-attached` + | `discarded-move-wholesale-detached` + +async function checkCapturePathHistory( + scenario: CapturePathHistory, + reach: ReturnType>, +): Promise { + type SourceRow = Row & { rank: number } + const versions = new Map() + const ranks = new Map() + for (let index = 0; index < scenario.count; index++) { + const id = String.fromCharCode(97 + index) + versions.set(id, 0) + ranks.set(id, index) + } + const expectedRows = () => + modelRows(versions).sort( + (left, right) => ranks.get(left.id)! - ranks.get(right.id)!, + ) + let controls: Parameters[`sync`]>[0] | undefined + const source = createCollection({ + getKey: (row) => row.id, + gcTime: Infinity, + startSync: true, + sync: { + sync: (input) => { + controls = input + input.begin() + for (const row of expectedRows()) + input.write({ + type: `insert`, + value: { ...row, rank: ranks.get(row.id)! }, + }) + input.commit() + if (!scenario.hydrated) input.markReady() + }, + }, + }) + source.createIndex((row) => row.rank, { indexType: BTreeIndex }) + const ordered = new Query() + .from({ row: source }) + .orderBy(({ row }) => row.rank) + .select(({ row }) => ({ id: row.id, version: row.version })) + const query = createLiveQueryCollection({ + query: scenario.shape === `single` ? ordered.findOne() : ordered, + getKey: (row) => row.id, + gcTime: Infinity, + startSync: true, + }) + const client = scenario.hydrated ? new DbClient() : undefined + const queryHash = `capture-path-${sequence++}` + const seedRows = expectedRows() + .slice(0, scenario.shape === `single` ? 1 : undefined) + .map((row) => ({ ...row, version: -1 })) + client?.hydrate({ + collections: [], + liveQueries: [ + { + queryHash, + dehydratedAt: 1, + snapshot: { rows: seedRows.map((value) => ({ key: value.id, value })) }, + }, + ], + }) + if (!scenario.hydrated) await query.preload() + const observer = createLiveQueryObserver(query, { + mode: scenario.mode, + client, + queryHash, + }) + let stop: (() => void) | undefined + const retained: Array = [] + const settle = () => new Promise((resolve) => setTimeout(resolve, 0)) + let failure: { error: unknown } | undefined + try { + if (scenario.hydrated) { + const seed = observer.getSnapshot() + checkRead( + scenario.shape, + { snapshot: seed, rows: seedRows }, + scenario.dataFirst, + `seed`, + ) + retained.push({ snapshot: seed, rows: seedRows }) + stop = observer.subscribe(() => {}) + controls!.markReady() + await query.preload() + await settle() + reach.reach(`hydrated-${scenario.shape}`) + } else if (scenario.attached) stop = observer.subscribe(() => {}) + if (!scenario.attached) { + stop?.() + stop = undefined + } + reach.reach(`${scenario.shape}-${scenario.mode}`) + reach.reach( + `initial-${scenario.attached ? `attached` : `detached`}-${scenario.mode}`, + ) + let before = observer.getSnapshot() + let hasErrors = false + let beforeRows = expectedRows() + let beforeRead = checkRead( + scenario.shape, + { snapshot: before, rows: beforeRows }, + scenario.dataFirst, + `initial live`, + ) + retained.push({ snapshot: before, rows: beforeRows }) + const change = (kind: `move` | `write` | `refresh`) => { + if (kind === `move`) ranks.set(`a`, ranks.get(`a`)! < 50 ? 100 : -1) + if (kind === `write`) versions.set(`a`, versions.get(`a`)! + 1) + controls!.begin() + controls!.write({ + type: `update`, + value: { id: `a`, version: versions.get(`a`)!, rank: ranks.get(`a`)! }, + }) + controls!.commit() + } + for (const [index, step] of scenario.commands.entries()) { + const revision = query._stateRevision + if (step === `discard-write` || step === `discard-move`) { + stop?.() + stop = undefined + const deferrals = Array.from({ length: scenario.deferralDepth }, () => + query._deferPublication(), + ) + const kind = step === `discard-write` ? `write` : `move` + try { + change(kind) + await settle() + before = observer.getSnapshot() + beforeRows = expectedRows() + beforeRead = checkRead( + scenario.shape, + { snapshot: before, rows: beforeRows }, + scenario.dataFirst, + `inside deferral ${index}`, + ) + retained.push({ snapshot: before, rows: beforeRows }) + } finally { + for (const deferral of deferrals.reverse()) deferral.discard() + if (deferrals.length > 1) reach.reach(`nested-discard`) + } + // No read sees the close before the next write. A snapshot cache + // cannot rely on observing every intermediate Collection transition. + if (scenario.attached) stop = observer.subscribe(() => {}) + change(kind) + reach.reach( + `discarded-${kind}-${scenario.mode}-${stop ? `attached` : `detached`}`, + ) + } else if (step === `move` || step === `write` || step === `refresh`) { + change(step) + } else if (step === `report`) { + hasErrors = true + controls!.reportError(new Error(`path ${index}`)) + } else if (step === `healthy`) { + hasErrors = false + controls!.reportHealthy() + } else if (step === `subscribe`) stop ??= observer.subscribe(() => {}) + else if (step === `unsubscribe`) { + stop?.() + stop = undefined + } else controls!.markReady() + await settle() + const after = observer.getSnapshot() + expect(after.status, `query is ready`).toBe(`ready`) + expect(after.isSyncError, `query health follows reports`).toBe(hasErrors) + expect(after.health?.hasErrors, `query health tree is fresh`).toBe( + hasErrors, + ) + const rows = expectedRows() + const visible = (values: Array) => + scenario.shape === `single` ? values.slice(0, 1) : values + const oldRows = visible(beforeRows) + const newRows = visible(rows) + const rowChanged = JSON.stringify(oldRows) !== JSON.stringify(newRows) + const keysChanged = + oldRows.length !== newRows.length || + oldRows.some((row, at) => row.id !== newRows[at]!.id) + const read = checkRead( + scenario.shape, + { snapshot: after, rows }, + scenario.dataFirst, + `path step ${index}`, + ) + if (rowChanged) { + expect(read.state, `changed query rows replace state`).not.toBe( + beforeRead.state, + ) + expect(read.data, `changed query rows replace data`).not.toBe( + beforeRead.data, + ) + } else { + expect(read.state, `unchanged query rows keep state`).toBe( + beforeRead.state, + ) + expect(read.data, `unchanged query rows keep data`).toBe( + beforeRead.data, + ) + if (after.health === before.health) + expect(after, `query no-op keeps snapshot`).toBe(before) + else { + expect(after, `query metadata replaces snapshot`).not.toBe(before) + reach.reach(`metadata-${scenario.shape}-${scenario.mode}`) + } + } + if (keysChanged) { + expect(after.layoutRevision, `query layout advances`).toBeGreaterThan( + before.layoutRevision, + ) + if (query._stateRevision === revision) + reach.reach( + `layout-only-${scenario.mode}-${stop ? `attached` : `detached`}`, + ) + } else + expect(after.layoutRevision, `query layout stays`).toBe( + before.layoutRevision, + ) + retained.push({ snapshot: after, rows }) + before = after + beforeRows = rows + beforeRead = read + } + for (const [index, captured] of retained.entries()) + checkRead( + scenario.shape, + captured, + !scenario.dataFirst, + `retained path ${index}`, + ) + } catch (error) { + failure = { error } + } + await finishHistory( + [ + () => stop?.(), + () => observer.dispose(), + () => query.cleanup(), + () => source.cleanup(), + () => client?.cleanup(), + ], + failure, + ) +} + +declareOracle({ + check: [checkCapturePathHistory], + grammar: [capturePathHistory], +}) + +describe(`snapshot capture paths preserve row and layout laws`, () => { + // These replay the same driver/check before the broad campaign so a value + // failure cannot hide the distinct layout premise during RED verification. + for (const action of [`discard-write`, `discard-move`] as const) + it(`reads committed rows after ${action}`, async () => { + const reach = createOracleReach( + `capture-discard-replay`, + [ + `ordered-granular`, + `initial-detached-granular`, + `discarded-write-granular-detached`, + `discarded-move-granular-detached`, + `layout-only-granular-detached`, + ], + ) + reach.beginHistory() + await checkCapturePathHistory( + { + shape: `ordered`, + mode: `granular`, + attached: false, + hydrated: false, + count: 2, + deferralDepth: 1, + dataFirst: false, + commands: [action], + }, + reach, + ) + }) + const examples: Array<[CapturePathHistory]> = ( + [`ordered`, `single`] as const + ).flatMap((shape) => + ([`granular`, `wholesale`] as const).flatMap((mode) => + [false, true].flatMap((attached) => + [false, true].map( + (hydrated) => + [ + { + shape, + mode, + attached, + hydrated, + count: 2, + deferralDepth: hydrated ? 2 : 1, + dataFirst: false, + commands: [ + `unsubscribe`, + `move`, + `subscribe`, + `move`, + `write`, + `refresh`, + `report`, + `healthy`, + `unsubscribe`, + `subscribe`, + `ready`, + `discard-write`, + `discard-move`, + ], + }, + ] as [CapturePathHistory], + ), + ), + ), + ) + for (const campaign of [`fixed`, `random`] as const) + oracleReplayTest( + it, + ...(campaign === `fixed` ? [] : [`live-query-observer.capture-paths`]), + )(`checks query capture boundaries (${campaign})`, async () => { + const reach = createOracleReach( + `live-query-observer.capture-paths`, + [ + `nested-discard`, + `ordered-granular`, + `ordered-wholesale`, + `single-granular`, + `single-wholesale`, + `initial-attached-granular`, + `initial-attached-wholesale`, + `initial-detached-granular`, + `initial-detached-wholesale`, + `hydrated-ordered`, + `hydrated-single`, + `layout-only-granular-attached`, + `layout-only-granular-detached`, + `layout-only-wholesale-attached`, + `layout-only-wholesale-detached`, + `metadata-ordered-granular`, + `metadata-ordered-wholesale`, + `metadata-single-granular`, + `metadata-single-wholesale`, + `discarded-write-granular-attached`, + `discarded-write-granular-detached`, + `discarded-write-wholesale-attached`, + `discarded-write-wholesale-detached`, + `discarded-move-granular-attached`, + `discarded-move-granular-detached`, + `discarded-move-wholesale-attached`, + `discarded-move-wholesale-detached`, + ], + { fixedHistories: examples.length }, + ) + let histories = 0 + await fc.assert( + fc.asyncProperty(capturePathHistory, async (scenario) => { + reach.beginHistory({ fixed: histories++ < examples.length }) + await checkCapturePathHistory(scenario, reach) + }), + { + ...oraclePropertyOptions(40, `live-query-observer.capture-paths`, { + examples, + }), + ...(campaign === `fixed` ? { seed: 2346 } : {}), + }, + ) + reach.assertReached() + process.stdout.write( + `observer capture paths ${campaign}: ${JSON.stringify(Object.fromEntries(reach.counts()))}\n`, + ) + }) +}) + +/** + * A public row's nested record prototype is observable shape. Maintainer + * decision 2026-11-03: replacing a plain record with a class instance (or the + * reverse) publishes an update even when enumerable fields match. Draft + * snapshots keep their separate class/plain reconstruction equivalence. + * + * The model is a shape name and one number; neither comes from production's + * equality function. Fresh values of the same shape and number are no-ops. + * Records, array elements, Map values and Set members carry the nested value. + * Direct Collections and projected live queries receive the same authored + * changes. At each applied checkpoint, both snapshot views and a granular + * consumer must agree with the model, and all earlier snapshots stay intact. + * No same-object promise joins a raw granular change to an older equal capture. + * This bounded grammar covers keyed record prototypes, not exotic built-in + * subclasses, descriptors, or arbitrary reference-sensitive query expressions. + */ +class ShapePayload { + constructor(public value: number) {} +} +const payloadKinds = [`plain`, `class`, `null`] as const +const payloadCarriers = [`record`, `array`, `map`, `set`] as const +type PayloadKind = (typeof payloadKinds)[number] +type PayloadCarrier = (typeof payloadCarriers)[number] +type PayloadSpec = { kind: PayloadKind; value: number } +const payloadSpec = fc.record({ + kind: fc.constantFrom(...payloadKinds), + value: fc.integer({ min: 0, max: 2 }), +}) +const payloadHistory = fc.record({ + mode: fc.constantFrom(`granular` as const, `wholesale` as const), + attached: fc.boolean(), + carrier: fc.constantFrom(...payloadCarriers), + initial: payloadSpec, + commands: fc.array( + fc.oneof( + payloadSpec, + fc.constantFrom( + `refresh` as const, + `subscribe` as const, + `unsubscribe` as const, + `report` as const, + `healthy` as const, + ), + ), + { minLength: 1, maxLength: 12 }, + ), +}) +type PayloadHistory = + typeof payloadHistory extends fc.Arbitrary ? T : never +type PayloadReach = + | `source-granular` + | `source-wholesale` + | `query-granular` + | `query-wholesale` + | `plain-class` + | `class-plain` + | `plain-null` + | `same-shape-copy` + | PayloadCarrier +function makePayload(spec: PayloadSpec, carrier: PayloadCarrier): object { + const leaf = + spec.kind === `class` + ? new ShapePayload(spec.value) + : spec.kind === `null` + ? Object.assign(Object.create(null) as object, { value: spec.value }) + : { value: spec.value } + if (carrier === `array`) return [leaf] + if (carrier === `map`) return new Map([[`leaf`, leaf]]) + if (carrier === `set`) return new Set([leaf]) + return { leaf } +} +function payloadObservation( + payload: object, + carrier: PayloadCarrier, +): PayloadSpec { + const leaf: { value: number } = + carrier === `array` + ? (payload as Array<{ value: number }>)[0]! + : carrier === `map` + ? (payload as Map).get(`leaf`)! + : carrier === `set` + ? (payload as Set<{ value: number }>).values().next().value! + : (payload as { leaf: { value: number } }).leaf + const prototype: unknown = Object.getPrototypeOf(leaf) + expect( + [Object.prototype, ShapePayload.prototype, null], + `authored prototype`, + ).toContain(prototype) + return { + kind: + prototype === ShapePayload.prototype + ? `class` + : prototype === null + ? `null` + : `plain`, + value: leaf.value, + } +} +async function checkPayloadHistory( + scenario: PayloadHistory & { query: boolean }, + reach: ReturnType>, +): Promise { + type PayloadRow = Row & { payload: object } + let model = scenario.initial + let controls!: Parameters[`sync`]>[0] + const source = createCollection({ + getKey: (row) => row.id, + startSync: true, + gcTime: Infinity, + sync: { + sync(input) { + controls = input + input.begin() + input.write({ + type: `insert`, + value: { + id: `a`, + version: 0, + payload: makePayload(model, scenario.carrier), + }, + }) + input.commit() + input.markReady() + }, + }, + }) + const query = scenario.query + ? createLiveQueryCollection({ + query: new Query().from({ row: source }).select(({ row }) => ({ + id: row.id, + version: row.version, + payload: row.payload, + })), + getKey: (row) => row.id, + gcTime: Infinity, + startSync: true, + }) + : undefined + if (query) await query.preload() + const observer = query + ? createLiveQueryObserver(query, { mode: scenario.mode }) + : createLiveQueryObserver(source, { mode: scenario.mode }) + const granular = new Map() + const subscribe = () => + observer.subscribe((changes) => { + for (const change of changes ?? []) { + if (change.type === `delete`) granular.delete(change.key) + else granular.set(change.key, change.value) + } + }) + let stop = scenario.attached ? subscribe() : undefined + type PayloadSnapshot = Pick< + LiveQuerySnapshot, + `state` | `data` + > + const retained: Array<{ snapshot: PayloadSnapshot; spec: PayloadSpec }> = [] + const check = ( + snapshot: PayloadSnapshot, + spec: PayloadSpec, + label: string, + ) => { + checkRead( + `ordered`, + { snapshot, rows: [{ id: `a`, version: 0 }] }, + false, + label, + ) + const data = snapshot.data as ReadonlyArray + expect( + payloadObservation(data[0]!.payload, scenario.carrier), + `${label}: data shape and value`, + ).toEqual(spec) + expect( + payloadObservation(snapshot.state!.get(`a`)!.payload, scenario.carrier), + `${label}: state shape and value`, + ).toEqual(spec) + } + let failure: { error: unknown } | undefined + try { + reach.reach(`${scenario.query ? `query` : `source`}-${scenario.mode}`) + reach.reach(scenario.carrier) + let before = observer.getSnapshot() + retained.push({ snapshot: before, spec: model }) + for (const [index, step] of scenario.commands.entries()) { + const next = typeof step === `object` ? step : model + const changed = next.kind !== model.kind || next.value !== model.value + if (typeof step === `object` || step === `refresh`) { + if (next.value === model.value) { + if (model.kind === `plain` && next.kind === `class`) + reach.reach(`plain-class`) + if (model.kind === `class` && next.kind === `plain`) + reach.reach(`class-plain`) + if (model.kind === `plain` && next.kind === `null`) + reach.reach(`plain-null`) + if (!changed) reach.reach(`same-shape-copy`) + } + controls.begin() + controls.write({ + type: `update`, + value: { + id: `a`, + version: 0, + payload: makePayload(next, scenario.carrier), + }, + }) + controls.commit() + model = next + } else if (step === `subscribe`) stop ??= subscribe() + else if (step === `unsubscribe`) { + stop?.() + stop = undefined + granular.clear() + } else if (step === `report`) + controls.reportError(new Error(`payload ${index}`)) + else controls.reportHealthy() + if (query) await new Promise((resolve) => setTimeout(resolve, 0)) + const after = observer.getSnapshot() + check( + after, + model, + `${scenario.query ? `query` : `source`} payload ${index}`, + ) + if (changed) { + expect(after.data, `shape/value changes replace data`).not.toBe( + before.data, + ) + expect(after.state, `shape/value changes replace state`).not.toBe( + before.state, + ) + } else { + expect(after.data, `same rows keep data`).toBe(before.data) + expect(after.state, `same rows keep state`).toBe(before.state) + } + if (stop && scenario.mode === `granular`) { + expect([...granular.keys()], `granular membership`).toEqual([`a`]) + expect( + payloadObservation(granular.get(`a`)!.payload, scenario.carrier), + `granular agrees with snapshot shape and value`, + ).toEqual(model) + } + retained.push({ snapshot: after, spec: model }) + before = after + } + for (const [index, entry] of retained.entries()) + check(entry.snapshot, entry.spec, `retained payload ${index}`) + } catch (error) { + failure = { error } + } + await finishHistory( + [ + () => stop?.(), + () => observer.dispose(), + () => query?.cleanup(), + () => source.cleanup(), + ], + failure, + ) +} +declareOracle({ check: [checkPayloadHistory], grammar: [payloadHistory] }) +describe(`publications preserve nested record shape`, () => { + const examples: Array<[PayloadHistory]> = ( + [`granular`, `wholesale`] as const + ).flatMap((mode) => + [false, true].flatMap((attached) => + payloadCarriers.map((carrier): [PayloadHistory] => [ + { + mode, + attached, + carrier, + initial: { kind: `plain`, value: 0 }, + commands: [ + { kind: `class`, value: 0 }, + `refresh`, + `report`, + { kind: `plain`, value: 0 }, + { kind: `null`, value: 0 }, + `subscribe`, + { kind: `class`, value: 0 }, + `unsubscribe`, + { kind: `plain`, value: 0 }, + `healthy`, + ], + }, + ]), + ), + ) + for (const campaign of [`fixed`, `random`] as const) + oracleReplayTest( + it, + ...(campaign === `fixed` ? [] : [`live-query-observer.record-shape`]), + )(`compares authored shape at every read (${campaign})`, async () => { + const reach = createOracleReach( + `live-query-observer.record-shape`, + [ + `source-granular`, + `source-wholesale`, + `query-granular`, + `query-wholesale`, + `plain-class`, + `class-plain`, + `plain-null`, + `same-shape-copy`, + ...payloadCarriers, + ], + { fixedHistories: examples.length }, + ) + let histories = 0 + await fc.assert( + fc.asyncProperty(payloadHistory, async (scenario) => { + reach.beginHistory({ fixed: histories++ < examples.length }) + // Both receiving paths run before reporting a mismatch, so an early + // direct-Collection failure cannot hide the query's pre-edit result. + const results = await Promise.allSettled( + [false, true].map((query) => + checkPayloadHistory({ ...scenario, query }, reach), + ), + ) + const errors = results.flatMap((result) => + result.status === `rejected` ? [result.reason] : [], + ) + if (errors.length) + throw new AggregateError( + errors, + `source and query shape comparisons: ${errors.map(String).join(`; `)}`, + { cause: errors[0] }, + ) + }), + { + ...oraclePropertyOptions(40, `live-query-observer.record-shape`, { + examples, + }), + ...(campaign === `fixed` ? { seed: 234602 } : {}), + }, + ) + reach.assertReached() + process.stdout.write( + `observer record shape ${campaign}: ${JSON.stringify(Object.fromEntries(reach.counts()))}\n`, + ) + }) +}) + +/** + * A defined array position must reach every row consumer after its sync write + * applies. Change-event equality treats a hole as undefined, so that neighbor + * keeps row containers stable; it cannot suppress a hole-to-number change. + * The utility owner's shared grammar supplies values and its positional model. + * Real Collection events, direct/projected observers, both modes, and retained + * snapshots receive the same law. Each generated case executes every receiving + * path, rather than relying on a random choice to reach one. Provider delivery + * and framework scheduling remain outside this core publication boundary. + */ +const sparseReceivingStates = [ + `source-granular-attached`, + `source-granular-detached`, + `source-wholesale-attached`, + `source-wholesale-detached`, + `query-granular-attached`, + `query-granular-detached`, + `query-wholesale-attached`, + `query-wholesale-detached`, + `hole-undefined`, + `hole-defined`, + `defined-hole`, + ...sparseArrayCarriers, +] as const +async function checkSparsePublication( + spec: SparseArrayCase, + carrier: SparseArrayCarrier, + mode: `granular` | `wholesale`, + projected: boolean, + attached: boolean, + reach: ReturnType< + typeof createOracleReach<(typeof sparseReceivingStates)[number]> + >, +): Promise { + type SparseRow = { id: string; payload: object } + let sync!: Parameters[`sync`]>[0] + const row = (form: SparseArrayForm): SparseRow => ({ + id: `a`, + payload: makeSparseArray(spec, form, carrier), + }) + const source = createCollection({ + getKey: (value) => value.id, + startSync: true, + gcTime: Infinity, + sync: { + sync(input) { + sync = input + input.begin() + input.write({ type: `insert`, value: row(`hole`) }) + input.commit() + input.markReady() + }, + }, + }) + const query = projected + ? createLiveQueryCollection({ + query: new Query().from({ row: source }).select(({ row: value }) => ({ + id: value.id, + payload: value.payload, + })), + getKey: (value) => value.id, + startSync: true, + gcTime: Infinity, + }) + : undefined + const observer = query + ? createLiveQueryObserver(query, { mode }) + : createLiveQueryObserver(source, { mode }) + const mirror = new Map(source.entries()) + const subscription = source.subscribeChanges( + (changes) => { + for (const change of changes) { + if (change.type === `delete`) mirror.delete(change.key) + else mirror.set(change.key, change.value) + } + }, + { includeInitialState: false }, + ) + const stop = attached ? observer.subscribe(() => {}) : undefined + type SparseSnapshot = Pick< + LiveQuerySnapshot, + `data` | `state` + > + const rows = (snapshot: SparseSnapshot): ReadonlyArray => { + if (!Array.isArray(snapshot.data)) + throw new Error(`ordered sparse snapshot must contain an array`) + return snapshot.data + } + const retained: Array<{ + snapshot: SparseSnapshot + values: Array + }> = [] + let failure: { error: unknown } | undefined + try { + if (query) await query.preload() + reach.reach( + `${projected ? `query` : `source`}-${mode}-${attached ? `attached` : `detached`}`, + ) + reach.reach(carrier) + let previous = observer.getSnapshot() + retained.push({ + snapshot: previous, + values: sparseArrayValues(spec, `hole`), + }) + const forms = [`undefined`, `defined`, `hole`, `defined`] as const + for (const [index, form] of forms.entries()) { + const expected = sparseArrayValues(spec, form) + sync.begin() + sync.write({ type: `update`, value: row(form) }) + sync.commit() + if (query) await new Promise((resolve) => setTimeout(resolve, 0)) + const snapshot = observer.getSnapshot() + // Check independently received values before proceeding to the next write. + expect([...source.keys()], `source membership`).toEqual([`a`]) + expect([...mirror.keys()], `change-message membership`).toEqual([`a`]) + expect( + rows(snapshot).map((value) => value.id), + `snapshot membership`, + ).toEqual([`a`]) + expect([...snapshot.state!.keys()], `snapshot state membership`).toEqual([ + `a`, + ]) + const observations = [ + source.get(`a`)!.payload, + mirror.get(`a`)!.payload, + rows(snapshot)[0]!.payload, + snapshot.state!.get(`a`)!.payload, + ] + const errors: Array = [] + for (const actual of observations) { + try { + checkSparseArray(actual, expected, carrier) + } catch (error) { + errors.push(error) + } + } + if (errors.length) + throw new AggregateError( + errors, + `sparse publication ${index}: ${errors.map(String).join(`; `)}`, + { cause: errors[0] }, + ) + if (index === 0) { + expect(snapshot.data, `equivalent array values keep data`).toBe( + previous.data, + ) + expect(snapshot.state, `equivalent array values keep state`).toBe( + previous.state, + ) + reach.reach(`hole-undefined`) + } else { + expect(snapshot.data, `changed array value replaces data`).not.toBe( + previous.data, + ) + reach.reach(form === `hole` ? `defined-hole` : `hole-defined`) + } + retained.push({ snapshot, values: expected }) + previous = snapshot + } + for (const { snapshot, values } of retained) { + checkSparseArray(rows(snapshot)[0]!.payload, values, carrier) + checkSparseArray(snapshot.state!.get(`a`)!.payload, values, carrier) + } + } catch (error) { + failure = { + error: new Error( + `${projected ? `query` : `source`}/${mode}/${attached ? `attached` : `detached`}/${carrier}: ${String(error)}`, + { cause: error }, + ), + } + } + await finishHistory( + [ + () => stop?.(), + () => subscription.unsubscribe(), + () => observer.dispose(), + () => query?.cleanup(), + () => source.cleanup(), + ], + failure, + ) +} +declareOracle({ check: [checkSparsePublication] }) +describe(`array value publication`, () => { + for (const campaign of [`fixed`, `random`] as const) + oracleReplayTest( + it, + ...(campaign === `fixed` ? [] : [`live-query-observer.sparse-array`]), + )( + `publishes changed array positions to every receiver (${campaign})`, + async () => { + const reach = createOracleReach( + `live-query-observer.sparse-array`, + sparseReceivingStates, + ) + await fc.assert( + fc.asyncProperty(sparseArrayCase, async (spec) => { + reach.beginHistory() + const results = await Promise.allSettled( + sparseArrayCarriers.flatMap((carrier) => + ([`granular`, `wholesale`] as const).flatMap((mode) => + [false, true].flatMap((projected) => + [false, true].map((attached) => + checkSparsePublication( + spec, + carrier, + mode, + projected, + attached, + reach, + ), + ), + ), + ), + ), + ) + const errors = results.flatMap((result) => + result.status === `rejected` ? [result.reason] : [], + ) + if (errors.length) + throw new AggregateError( + errors, + `sparse receiving paths: ${errors.map(String).join(`; `)}`, + { cause: errors[0] }, + ) + }), + { + ...oraclePropertyOptions(20, `live-query-observer.sparse-array`), + ...(campaign === `fixed` ? { seed: 234603 } : {}), + }, + ) + reach.assertReached() + process.stdout.write( + `observer sparse array ${campaign}: ${JSON.stringify(Object.fromEntries(reach.counts()))}\n`, + ) + }, + ) +}) diff --git a/packages/db/tests/live-query-observer.test.ts b/packages/db/tests/live-query-observer.test.ts index f4517d1818..c4f99d4c30 100644 --- a/packages/db/tests/live-query-observer.test.ts +++ b/packages/db/tests/live-query-observer.test.ts @@ -20,6 +20,39 @@ const SEED: Array = [ ] let seq = 0 + +// Clocks are a Collection invariant, not a compatibility extension point. +// Reject a contradictory collaborator before binding the client or starting +// any Collection resource (AGENTS.md: fail before mutation). +describe(`observer Collection revision invariant`, () => { + for (const clock of [`_stateRevision`, `_layoutRevision`] as const) { + it.each([undefined, null, `0`, NaN, Infinity])( + `rejects ${clock}=%s before acquiring resources`, + (invalid) => { + const collection = createCollection({ + getKey: (row) => row.id, + startSync: false, + sync: { sync: () => {} }, + }) + vi.spyOn(collection, clock, `get`).mockReturnValue(invalid as number) + const entries = vi.spyOn(collection, `entries`) + const subscribe = vi.spyOn(collection, `subscribeChanges`) + const start = vi.spyOn(collection, `startSyncImmediate`) + const client = new DbClient() + const bind = vi.spyOn(client, `_bindLiveQueryObserver`) + expect(() => createLiveQueryObserver(collection, { client })).toThrow( + /revision clocks/, + ) + expect(bind).not.toHaveBeenCalled() + expect(entries).not.toHaveBeenCalled() + expect(subscribe).not.toHaveBeenCalled() + expect(start).not.toHaveBeenCalled() + expect(collection.status).toBe(`idle`) + }, + ) + } +}) + function makeSource(data: Array = SEED) { return createCollection( mockSyncCollectionOptions({ @@ -1268,39 +1301,21 @@ describe(`createLiveQueryObserver`, () => { observer.dispose() }) - it(`invalidates snapshots for compatible collections without a state revision`, () => { - const rows = new Map([[`1`, { id: `1`, name: `A` }]]) - const listeners = new Set< - (changes: Array>) => void - >() - const collection = { - status: `ready`, - entries: () => rows.entries(), - on: () => () => {}, - subscribeChanges: ( - listener: (changes: Array>) => void, - ) => { - listeners.add(listener) - return { unsubscribe: () => listeners.delete(listener) } - }, - preload: async () => {}, - } - const observer = createLiveQueryObserver(collection as any, { - mode: `wholesale`, - }) + it(`invalidates snapshots after committed Collection writes`, async () => { + const { collection, writeRow } = makeLoadSubsetSource() + const observer = createLiveQueryObserver(collection, { mode: `wholesale` }) observer.subscribe(() => {}) - const before = observer.getSnapshot() - - const row = { id: `2`, name: `B` } - rows.set(row.id, row) - for (const listener of listeners) { - listener([{ type: `insert`, key: row.id, value: row }]) + try { + const before = observer.getSnapshot() + writeRow(`insert`, { id: `3`, name: `C` }) + const after = observer.getSnapshot() + expect(after).not.toBe(before) + expect(after.data).toHaveLength(3) + expect(before.data).toHaveLength(2) + } finally { + observer.dispose() + await collection.cleanup() } - - const after = observer.getSnapshot() - expect(after).not.toBe(before) - expect(after.data).toHaveLength(2) - observer.dispose() }) it(`does not expose a truncate while its subscription is buffering a refetch`, async () => { diff --git a/packages/db/tests/oracle-config.ts b/packages/db/tests/oracle-config.ts index 5238c61180..7b3b4e41e8 100644 --- a/packages/db/tests/oracle-config.ts +++ b/packages/db/tests/oracle-config.ts @@ -85,6 +85,10 @@ const staticOracleProperties = [ `live-query-observer.partial-attach`, `live-query-observer.release-failure`, `live-query-observer.hydration-error-timing`, + `live-query-observer.row-container-sharing`, + `live-query-observer.capture-paths`, + `live-query-observer.record-shape`, + `live-query-observer.sparse-array`, `subscription-lifecycle.notification-error`, `live-query.start-handoff`, `react.descriptor-error-surface`, @@ -129,6 +133,7 @@ const staticOracleProperties = [ `proxy.callback-history`, `indexeddb.pending-history`, `proxy.revert-history`, + `proxy.revert-signed-zero`, `proxy.revert-nested-round-trips`, `proxy.revert-class-writes`, `proxy.revert-partial-reverts`, @@ -443,7 +448,7 @@ const publicationProperties = [ // utils-oracle and comparison-oracle number their properties in file order. const utilsProperties = Array.from( - { length: 41 }, + { length: 43 }, (_, index) => `utils.${index + 1}`, ) const comparisonProperties = Array.from( diff --git a/packages/db/tests/oracle-replay-manifest.ts b/packages/db/tests/oracle-replay-manifest.ts index e1d6db09ec..c7474f5c9f 100644 --- a/packages/db/tests/oracle-replay-manifest.ts +++ b/packages/db/tests/oracle-replay-manifest.ts @@ -164,7 +164,7 @@ const ownerGroups: ReadonlyArray< [ `db/tests/live-query-observer-snapshot-oracle.property.test.ts`, `live-query-observer`, - `retained-snapshot`, + `retained-snapshot row-container-sharing capture-paths record-shape sparse-array`, ], [ `db/tests/cleanup-queue-oracle.property.test.ts`, @@ -582,7 +582,7 @@ const ownerGroups: ReadonlyArray< [ `db/tests/proxy-revert-oracle.property.test.ts`, `proxy`, - `revert-history revert-nested-round-trips revert-class-writes revert-partial-reverts revert-native-histories`, + `revert-history revert-signed-zero revert-nested-round-trips revert-class-writes revert-partial-reverts revert-native-histories`, ], [ `db/tests/proxy-native-methods-oracle.property.test.ts`, @@ -956,6 +956,22 @@ const replayTestNames = new Map([ `live-query-observer.retained-snapshot`, `first reads match the capture-time rows.*random`, ], + [ + `live-query-observer.row-container-sharing`, + `reuses rows across metadata changes.*random`, + ], + [ + `live-query-observer.capture-paths`, + `checks query capture boundaries.*random`, + ], + [ + `live-query-observer.record-shape`, + `compares authored shape at every read.*random`, + ], + [ + `live-query-observer.sparse-array`, + `publishes changed array positions to every receiver.*random`, + ], [`live-query.start-handoff`, `reports only its own thrown starts.*random`], [ `pooled-live-query.late-cleanup`, diff --git a/packages/db/tests/production-error-messages-oracle.test.ts b/packages/db/tests/production-error-messages-oracle.test.ts index f44e6e3965..afb3124e23 100644 --- a/packages/db/tests/production-error-messages-oracle.test.ts +++ b/packages/db/tests/production-error-messages-oracle.test.ts @@ -25,9 +25,9 @@ * bundler drops the full text; a separate bundle check owns that. * - **Docs:** the freshly generated errors page has a heading for every code * that names its class, and an HTML ID matching its production-message link. - * Source PRs judge the actual generator's stdout. - * The release docs pipeline owns tracked generated-page freshness, following - * the maintainer's decision to keep generated docs out of source PRs. + * This oracle judges the actual generator's stdout. The ordinary + * `generate-error-docs.test.ts` also requires the committed page to match + * the current catalogs. * * - **Error sites:** code outside `errors.ts` also throws plain `Error`, * `TypeError`, and `RangeError` values with library text. Each such site diff --git a/packages/db/tests/proxy-native-methods-oracle.property.test.ts b/packages/db/tests/proxy-native-methods-oracle.property.test.ts index 8bc5546cd6..a1e03f72a8 100644 --- a/packages/db/tests/proxy-native-methods-oracle.property.test.ts +++ b/packages/db/tests/proxy-native-methods-oracle.property.test.ts @@ -99,9 +99,9 @@ describe(`typed-array methods behave like native typed arrays`, () => { for (const write of [false, true]) expectTypedCall([3, 1, 2], m, 1, write) }) - // A mutator counts as a change without a revert check, even when the - // result equals the original under draft equality (-0 equals 0). - it(`reversing [-0, 0] is a change although the result is draft-equal`, () => { + // Reversal swaps distinct signed-zero values. The native comparison checks + // their final positions, including the detached change-set value. + it(`reversing signed zeros preserves their native positions`, () => { expectTypedCall([-0, 0], `reverse`, 0, false) }) diff --git a/packages/db/tests/proxy-revert-oracle.property.test.ts b/packages/db/tests/proxy-revert-oracle.property.test.ts index 31f420969e..e54dc246cd 100644 --- a/packages/db/tests/proxy-revert-oracle.property.test.ts +++ b/packages/db/tests/proxy-revert-oracle.property.test.ts @@ -7,6 +7,7 @@ import { oracleRuns, } from './oracle-config' import { declareOracle } from './oracle-declaration' +import { createOracleReach } from './oracle-reach' /** * # Which draft writes count as changes, and which count as reverts? @@ -23,7 +24,7 @@ import { declareOracle } from './oracle-declaration' * * "Equal" here is draft equality, which is stricter than `deepEquals`: * - * 1. Primitives compare by value. `-0` equals `0`, and `NaN` equals `NaN`. + * 1. Primitives compare by SameValue: `-0` differs from `0`, and `NaN` equals `NaN`. * 2. Dates compare by timestamp. Invalid dates are equal. * 3. Regular expressions compare by source, flags, and `lastIndex`. * 4. Maps and Sets compare by entries or values in insertion order. @@ -55,6 +56,9 @@ import { declareOracle } from './oracle-declaration' * `docs/contributing/oracle-reviews/code-weight-draft-proxy.md`. The * false-positive permission is a maintainer decision (2026-10-09), recorded in * `docs/contributing/oracle-reviews/proxy-revert-replaced-object.md`. + * The maintainer's FID-1 decision (2026-10-29), recorded in the coverage map, + * also requires user writes to preserve signed zero, including nested and + * floating typed-array values. This supersedes rule 1's earlier zero equivalence. * * Limits: * - The main histories write by assignment, delete, nested property write, @@ -149,8 +153,13 @@ type Root = Partial> function encode(spec: Spec | undefined): string { return JSON.stringify(canon(spec)) } +// SameValue preserves the sign that String(number) and JSON numbers erase. +// This token is independent of the production draft comparator. +function numberToken(value: number): string { + return Object.is(value, -0) ? `-0` : String(value) +} function prim(v: Primitive | undefined): unknown { - return typeof v === `number` ? [`num`, String(v)] : [typeof v, v ?? null] + return typeof v === `number` ? [`num`, numberToken(v)] : [typeof v, v ?? null] } function canon(spec: Spec | undefined): unknown { if (spec === undefined) return [`absent`] @@ -181,9 +190,9 @@ function canon(spec: Spec | undefined): unknown { case `rows`: return [`rows`, spec.rows.map((row) => prim(row.a))] case `typed`: - // Elements follow rule 1: -0 equals 0 and NaN equals NaN. The class is + // Elements follow rule 1: -0 differs from 0 and NaN equals NaN. The class is // part of the value. - return [`typed`, spec.ctor, spec.values.map(String)] + return [`typed`, spec.ctor, spec.values.map(numberToken)] case `url`: return [`url`, spec.path] case `point`: @@ -509,6 +518,47 @@ const nestedRoundTripArb: fc.Arbitrary = fc return { original: { f, g }, ops: [first, ...between, last] } }) +// A sign-only write must survive each assignment path. Random fields, nested +// keys and typed-array positions vary the access path without another value +// change masking a lost zero sign. Both signs are legal; Uint8Array is excluded +// because native integer conversion cannot retain -0. Removing a path loses +// its draft boundary; removing the sign distinction admits the old comparator. +const signedZeroPaths = [`root`, `nested`, `float64`] as const +const signedZeroArb = fc.record({ + path: fc.constantFrom(...signedZeroPaths), + field: fc.constantFrom(...FIELDS), + negative: fc.boolean(), + key: fc.constantFrom(`a` as const, `b` as const, `sym` as const), + width: fc.integer({ min: 1, max: 3 }), + index: fc.nat(2), +}) +type SignedZeroInput = + typeof signedZeroArb extends fc.Arbitrary ? T : never +function signedZeroHistory(input: SignedZeroInput): History { + const before = input.negative ? -0 : 0 + const after = input.negative ? 0 : -0 + let original: Spec + let op: Op + if (input.path === `root`) { + original = { k: `prim`, v: before } + op = { op: `set`, field: input.field, value: { k: `prim`, v: after } } + } else if (input.path === `nested`) { + original = { k: `obj`, [input.key]: before } + op = { op: `nested`, field: input.field, key: input.key, value: after } + } else { + const index = input.index % input.width + original = { + k: `typed`, + ctor: `f64`, + values: Array.from({ length: input.width }, (_, at) => + at === index ? before : 1, + ), + } + op = { op: `index`, field: input.field, index, value: after } + } + return { original: { [input.field]: original }, ops: [op] } +} + // --------------------------------------------------------------------------- // Model: apply each op to the spec state. An op whose target has the wrong // shape does nothing, and the driver skips it the same way. @@ -797,10 +847,48 @@ const NESTED_ROUND_TRIPS = `proxy.revert-nested-round-trips` const CLASS_WRITES = `proxy.revert-class-writes` const PARTIAL_REVERTS = `proxy.revert-partial-reverts` const NATIVE_HISTORIES = `proxy.revert-native-histories` +const SIGNED_ZERO = `proxy.revert-signed-zero` const it = oracleReplayTest(vitestIt) describe(`draft revert oracle`, () => { + // These six fixed histories reconstruct the scalar false-green witness and + // its nested/Float64 neighbors in both directions. They use the generated + // campaign's model, draft driver and final getChanges/draft/isolation check. + const zeroExamples: Array<[SignedZeroInput]> = signedZeroPaths.flatMap( + (path) => + [false, true].map((negative): [SignedZeroInput] => [ + { path, negative, field: `f`, key: `a`, width: 1, index: 0 }, + ]), + ) + for (const campaign of [`fixed`, `random`] as const) + oracleReplayTest(vitestIt, ...(campaign === `fixed` ? [] : [SIGNED_ZERO]))( + `preserves sign-only writes through each draft path (${campaign})`, + () => { + const reach = createOracleReach(SIGNED_ZERO, signedZeroPaths, { + fixedHistories: zeroExamples.length, + }) + let histories = 0 + fc.assert( + fc.property(signedZeroArb, (input) => { + reach.beginHistory({ fixed: histories++ < zeroExamples.length }) + expectHistory(signedZeroHistory(input)) + reach.reach(input.path) + }), + { + ...oraclePropertyOptions(100, SIGNED_ZERO, { + examples: zeroExamples, + }), + ...(campaign === `fixed` ? { seed: FIXED_SEED } : {}), + }, + ) + reach.assertReached() + process.stdout.write( + `draft signed-zero ${campaign}: ${JSON.stringify(Object.fromEntries(reach.counts()))}\n`, + ) + }, + ) + it(`matches the model across generated write histories (${FIXED_SEED})`, () => { fc.assert(fc.property(historyArb, expectHistory), { numRuns: oracleRuns(400), @@ -1204,7 +1292,7 @@ describe(`draft revert oracle`, () => { }, ], [ - `-0 and NaN rewrites are not changes`, + `a zero-sign rewrite changes while NaN remains equal`, { original: { f: { k: `prim`, v: 0 }, g: { k: `prim`, v: NaN } }, ops: [ @@ -1421,8 +1509,8 @@ function expectNativeHistory(history: NativeHistory): void { declareOracle({ check: [expectHistory, expectNativeResult], - model: [step, expectedChanges, nativeModel], - grammar: [nativeHistoryArb], + model: [numberToken, step, expectedChanges, nativeModel], + grammar: [signedZeroArb, signedZeroHistory, nativeHistoryArb], }) describe(`draft revert oracle: native mutators mixed with assignments`, () => { diff --git a/packages/db/tests/query/bucket-facade-adapter.test.ts b/packages/db/tests/query/bucket-facade-adapter.test.ts index 3ae1aa633d..b3425f711f 100644 --- a/packages/db/tests/query/bucket-facade-adapter.test.ts +++ b/packages/db/tests/query/bucket-facade-adapter.test.ts @@ -363,7 +363,9 @@ describe(`BucketFacadeAdapter`, () => { expect(layoutPublications).toBe(0) expect(statusChanges).toBe(0) expect(truncates).toBe(0) - expect(facade._stateRevision).toBe(stateRevision) + // Discarded delivery restores public rows, but committed clocks never rewind. + expect(facade._stateRevision).toBeGreaterThan(stateRevision) + const restoredRevision = facade._stateRevision expect(facade._layoutRevision).toBe(layoutRevision) expect(facade.status).toBe(`ready`) @@ -387,7 +389,7 @@ describe(`BucketFacadeAdapter`, () => { expect(publications[0]).toHaveLength(2) expect(statusChanges).toBe(0) expect(truncates).toBe(0) - expect(facade._stateRevision).toBe(stateRevision + 1) + expect(facade._stateRevision).toBe(restoredRevision + 1) expect(facade._layoutRevision).toBe(layoutRevision + 1) expect(facade.toArray.map((row) => facade.getKeyFromItem(row))).toEqual([ fixed.id, @@ -419,7 +421,7 @@ describe(`BucketFacadeAdapter`, () => { expect(layoutPublications).toBe(1) expect(publications).toHaveLength(2) expect(publications[1]).toEqual([]) - expect(facade._stateRevision).toBe(stateRevision + 1) + expect(facade._stateRevision).toBe(restoredRevision + 1) expect(facade._layoutRevision).toBe(layoutRevision + 2) unsubscribeTruncate() @@ -611,7 +613,8 @@ describe(`BucketFacadeAdapter`, () => { expect(facade.status).toBe(`ready`) expect(facade._state.syncedData.get(original.id)).toMatchObject(original) expect(publications).toEqual([]) - expect(facade._stateRevision).toBe(revision) + expect(facade._stateRevision).toBeGreaterThan(revision) + const restoredRevision = facade._stateRevision const final = { id: 1, value: `final` } rows.sendData( @@ -626,7 +629,7 @@ describe(`BucketFacadeAdapter`, () => { expect(facade.toArray.map(stripVirtualProps)).toEqual([final]) expect(publications).toHaveLength(1) expect(publications[0]).toHaveLength(1) - expect(facade._stateRevision).toBe(revision + 1) + expect(facade._stateRevision).toBe(restoredRevision + 1) expect(index.lookup(`eq`, `original`)).toEqual(new Set()) expect(index.lookup(`eq`, `replacement`)).toEqual(new Set()) expect(index.lookup(`eq`, `final`)).toEqual(new Set([original.id])) diff --git a/packages/db/tests/query/pooled-live-query-oracle.property.test.ts b/packages/db/tests/query/pooled-live-query-oracle.property.test.ts index 64e62362f1..1fd1112836 100644 --- a/packages/db/tests/query/pooled-live-query-oracle.property.test.ts +++ b/packages/db/tests/query/pooled-live-query-oracle.property.test.ts @@ -113,6 +113,9 @@ const property = `pooled-live-query.publication` // A detached reader distinguishes a view that empties at cleanup from one // whose rows stay readable only if it held rows when the cleanup started. const reachStates = [ + `wholesale-reader-compared`, + `granular-reader-compared`, + `detached-reader-compared`, `detached-reader-holds-rows-at-cleanup`, // The reader's peer was unmounted, so no listener watches its group. `unwatched-reader-holds-rows-at-cleanup`, @@ -795,6 +798,7 @@ async function runHistory(history: History, reach?: Reach): Promise { `${label} rows`, ).toEqual(reference.toArray.map((row) => describeRow(row))) expect(snapshot.status, `${label} status`).toBe(reference.status) + reach?.reach(`wholesale-reader-compared`) // The view itself, which the observer reads only when notified. expect( [...view.entries()].map(([, row]) => describeRow(row)), @@ -803,6 +807,7 @@ async function runHistory(history: History, reach?: Reach): Promise { expect(view.status, `${label} view status`).toBe(reference.status) // A reader that never subscribed sees the same rows and status. expectDetached(label, entry.detached, reference) + reach?.reach(`detached-reader-compared`) const model = entry.frozen ?? expectedRows(rows, peer) expect( [...snapshot.state!.values()].map(describeFields).sort(), @@ -818,6 +823,7 @@ async function runHistory(history: History, reach?: Reach): Promise { expect(entry.pooledChanges.sort(), `${label} granular changes`).toEqual( entry.referenceChanges.sort(), ) + reach?.reach(`granular-reader-compared`) entry.pooledChanges.length = 0 entry.referenceChanges.length = 0 // A change in the ordered keys always advances the layout revision. @@ -1114,7 +1120,12 @@ describe(`pooled live query oracle`, () => { numRuns: oracleRuns(80), })(`matches the live-query Collection (fixed)`, runGenerated) afterAll(() => { - if (generatedRan) reach.assertReached() + if (generatedRan) { + process.stdout.write( + `pooled observer reach ${JSON.stringify(Object.fromEntries(reach.counts()))}\n`, + ) + reach.assertReached() + } }) } diff --git a/packages/db/tests/sparse-array-value-oracle.ts b/packages/db/tests/sparse-array-value-oracle.ts new file mode 100644 index 0000000000..a0bb08f2f2 --- /dev/null +++ b/packages/db/tests/sparse-array-value-oracle.ts @@ -0,0 +1,91 @@ +/** + * Array value equality reads every position. The utility equality contract + * equates a hole with undefined, but a defined number at that position differs. + * This shared part of the utility owner supplies the same authored values and + * observation to its direct comparison and the observer's publication driver. + * It does not promise array presence/descriptor fidelity: draft and persisted + * equality own those stricter domains. + */ +import { fc } from '@fast-check/vitest' +import { expect } from 'vitest' +import { declareOracle } from './oracle-declaration.js' + +export const sparseArrayCase = fc.record({ + cells: fc.array( + fc.oneof( + fc.integer(), + fc.constant(undefined), + fc.constant(`hole` as const), + ), + { + minLength: 1, + maxLength: 5, + }, + ), + at: fc.nat(4), + value: fc.integer(), +}) +export type SparseArrayCase = + typeof sparseArrayCase extends fc.Arbitrary ? T : never +export type SparseArrayForm = `hole` | `undefined` | `defined` +export const sparseArrayCarriers = [`direct`, `record`, `set`] as const +export type SparseArrayCarrier = (typeof sparseArrayCarriers)[number] + +export function makeSparseArray( + spec: SparseArrayCase, + form: SparseArrayForm, + carrier: SparseArrayCarrier, +): object { + const values: Array = new Array(spec.cells.length) + spec.cells.forEach((cell, index) => { + if (cell !== `hole`) values[index] = cell + }) + const index = spec.at % values.length + if (form === `hole`) delete values[index] + else values[index] = form === `defined` ? spec.value : undefined + return carrier === `record` + ? { values } + : carrier === `set` + ? new Set([values]) + : values +} + +// The model retains values by position, without implementing a recursive walk. +export function sparseArrayValues( + spec: SparseArrayCase, + form: SparseArrayForm, +) { + return spec.cells.map((cell, index) => + index === spec.at % spec.cells.length + ? form === `defined` + ? spec.value + : undefined + : cell === `hole` + ? undefined + : cell, + ) +} +export function checkSparseArray( + actual: object, + expected: ReadonlyArray, + carrier: SparseArrayCarrier, +): void { + if (carrier === `set`) { + expect((actual as Set).size, `singleton Set carrier`).toBe(1) + } + const values = + carrier === `record` + ? (actual as { values: Array }).values + : carrier === `set` + ? (actual as Set>).values().next().value + : actual + expect(Array.isArray(values), `array value carrier`).toBe(true) + expect(Array.from(values as Array), `array positions`).toEqual( + expected, + ) +} +declareOracle({ + grammar: [sparseArrayCase], + model: [sparseArrayValues], + check: [checkSparseArray], +}) diff --git a/packages/db/tests/utils-oracle.property.test.ts b/packages/db/tests/utils-oracle.property.test.ts index dc8852ee78..bba9dab041 100644 --- a/packages/db/tests/utils-oracle.property.test.ts +++ b/packages/db/tests/utils-oracle.property.test.ts @@ -1,7 +1,8 @@ -import { describe, expect, it } from 'vitest' +import { runInNewContext } from 'node:vm' +import { afterAll, describe, expect, it } from 'vitest' import { fc, test as fcTest } from '@fast-check/vitest' import { Temporal } from 'temporal-polyfill' -import { deepEquals } from '../src/utils' +import { deepEquals, sameRowValue } from '../src/utils' import { oraclePropertyOptions, oracleReplayTest, @@ -15,11 +16,19 @@ import { expectEqualityPair, expectRingLaw, } from './deep-equals-support' +import { createOracleReach } from './oracle-reach.js' +import { declareOracle } from './oracle-declaration.js' +import { + makeSparseArray, + sparseArrayCarriers, + sparseArrayCase, +} from './sparse-array-value-oracle.js' /** * The `deepEquals` API contract in `../src/utils.ts`, reinforced by the - * established examples in `utils.test.ts`, promises change-event equality for - * its supported value classes. This oracle checks bounded equality and + * established examples in `utils.test.ts`, promises generic value equality. + * `sameRowValue` additionally preserves record prototypes and signed zero for + * Collection change events. This oracle checks bounded equality and * inequality laws for those classes, not general graph isomorphism. * Independent value constructors exercise reflexivity, symmetry, copied * structure, and changed-leaf inequality for primitives, arrays, records, @@ -52,7 +61,11 @@ let nextPropertyId = 0 function utilsProperty]>(arbitraries: { [K in keyof Ts]: fc.Arbitrary }) { - return (name: string, check: (...values: Ts) => void): void => { + return ( + name: string, + check: (...values: Ts) => void, + runs = campaignRuns, + ): void => { const id = ++nextPropertyId const property = `utils.${id}` const label = `${id}: ${name}` @@ -62,11 +75,11 @@ function utilsProperty]>(arbitraries: { oracleReplayTest(fcTest).prop([argumentsArbitrary], { seed: fixedSeed, - numRuns: oracleRuns(campaignRuns), + numRuns: oracleRuns(runs), })(`${label} fixed`, (values) => check(...values)) oracleReplayTest(fcTest, property).prop( [argumentsArbitrary], - oraclePropertyOptions(campaignRuns, property), + oraclePropertyOptions(runs, property), )(`${label} random`, (values) => check(...values)) } } @@ -604,6 +617,16 @@ describe(`deepEquals property-based tests`, () => { ])( `objects compare by class and keys, and keyless instances by identity`, (pathA, pathB, v, w) => { + // Every generated value pair exercises all three record relations. + // Per-case reach prevents a campaign from losing one construction. + const reach = createOracleReach(`utils.record-prototypes`, [ + `same-class`, + `plain-class`, + `null-plain`, + `foreign-plain`, + `row-prototypes`, + ]) + reach.beginHistory() class Secret { #v: number constructor(value: number) { @@ -638,16 +661,28 @@ describe(`deepEquals property-based tests`, () => { expectEqualityPair(secret, new Secret(w), false) expectEqualityPair({ s: secret }, { s: new Secret(v) }, false) expectEqualityPair(new Secret(v), {}, false) - // Class instances with keys compare by keys within their class, and - // with a plain object, which is how JSON and draft snapshots hold - // them. Two different classes differ. + // Generic equality retains main's JSON/record bridge. The approved + // publication decision changes row equality, not the exported helper. expectEqualityPair(new Point(v), new Point(w), v === w) + reach.reach(`same-class`) expectEqualityPair(new Point(v), { a: w }, v === w) + reach.reach(`plain-class`) expectEqualityPair(new Point(v), new Other(v), false) - // Plain and null-prototype objects are one class. const bare = Object.assign(Object.create(null), { a: v }) expectEqualityPair(bare, { a: w }, v === w) expectEqualityPair(Object.create(null), {}, true) + reach.reach(`null-plain`) + const foreign = runInNewContext(`({})`) as { a: number } + foreign.a = v + expectEqualityPair(foreign, { a: w }, v === w) + reach.reach(`foreign-plain`) + for (const record of [new Point(v), bare, foreign]) { + expect(sameRowValue(record, { a: w })).toBe(false) + expect(sameRowValue({ a: w }, record)).toBe(false) + } + expect(sameRowValue(new Point(v), new Point(w))).toBe(v === w) + reach.reach(`row-prototypes`) + reach.assertReached() }, ) @@ -679,22 +714,46 @@ describe(`deepEquals property-based tests`, () => { }, ) - utilsProperty([ - fc.array(fc.oneof(fc.integer(), fc.constant(`hole`)), { - minLength: 1, - maxLength: 5, - }), - ])(`an array hole equals undefined at the same index`, (cells) => { - const sparse: Array = [] - sparse.length = cells.length - const dense: Array = [] - cells.forEach((cell, index) => { - if (cell !== `hole`) sparse[index] = cell - dense[index] = cell === `hole` ? undefined : cell - }) - expectEqualityPair(sparse, dense, true) - expectEqualityPair({ items: sparse }, { items: dense }, true) + const sparseReach = createOracleReach(`utils.sparse-array`, [ + `hole-undefined`, + `hole-defined`, + `general`, + `row`, + ...sparseArrayCarriers, + ]) + afterAll(() => { + if (sparseReach.counts().get(`general`)) { + sparseReach.assertReached() + process.stdout.write( + `Sparse array reach ${JSON.stringify(Object.fromEntries(sparseReach.counts()))}\n`, + ) + } }) + utilsProperty([sparseArrayCase])( + `array holes equal undefined but differ from defined values`, + (spec) => { + sparseReach.beginHistory() + for (const [name, equal] of [ + [`general`, deepEquals], + [`row`, sameRowValue], + ] as const) { + for (const carrier of sparseArrayCarriers) { + const hole = makeSparseArray(spec, `hole`, carrier) + for (const form of [`undefined`, `defined`] as const) { + expectEqualityPair( + hole, + makeSparseArray(spec, form, carrier), + form === `undefined`, + equal, + ) + sparseReach.reach(`hole-${form}`) + } + sparseReach.reach(carrier) + } + sparseReach.reach(name) + } + }, + ) }) describe(`nested structure consistency`, () => { utilsProperty([ @@ -829,3 +888,342 @@ describe(`typed-array value class`, () => { }, ) }) + +/** + * Object-valued Sets compare unordered structural members with multiplicity: + * distinct objects with equal fields still occupy distinct Set positions. + * Row equality distinguishes plain/class/null records; generic equality keeps + * the established plain-record/class bridge. + * AGENTS.md's Algorithm Efficiency rule excludes combinatorial assignment + * searches. Scalar reads and native Map/Set operations below get generous + * cubic ceilings. Cached candidate comparisons can keep reads polynomial + * while a permutation search does factorial assignment work, so both measures + * matter. Neither measure bounds elapsed time or arbitrary nested-value work. + * + * The model counts authored shape/value tokens. Row equality requires exact + * counts. Generic equality permits a plain/null record to bridge either keyed + * class, so each side's excess class counts must fit the other's plain slots. + * This counts capacity without searching assignments or calling production. + * Generated widths and values + * construct equal bags, a late unmatched prototype, and a count deficit where + * every member has some candidate. Rotation challenges insertion order. Empty + * and singleton neighbors use the same model. Optional parent links close a + * cycle through the compared Sets without changing token identity. + */ +type SetRecord = { kind: `plain` | `class` | `other` | `null`; value: number } +const ambiguousSets = fc.record({ + width: fc.integer({ min: 6, max: 12 }), + value: fc.integer({ min: 1, max: 5 }), + rotation: fc.nat(12), +}) +function setBag(records: ReadonlyArray) { + const counts = new Map>() + for (const { kind, value } of records) { + const entry = counts.get(value) ?? { plain: 0, class: 0, other: 0, null: 0 } + entry[kind]++ + counts.set(value, entry) + } + return counts +} +function setBagsEqual( + left: ReadonlyArray, + right: ReadonlyArray, + preserveShape: boolean, +): boolean { + if (left.length !== right.length) return false + const a = setBag(left) + const b = setBag(right) + if (a.size !== b.size) return false + for (const [value, x] of a) { + const y = b.get(value) + if (!y) return false + if (preserveShape) { + if ( + (Object.keys(x) as Array).some( + (kind) => x[kind] !== y[kind], + ) + ) + return false + } else { + const total = (counts: typeof x) => + Object.values(counts).reduce((sum, count) => sum + count, 0) + const excess = (from: typeof x, to: typeof x) => + Math.max(0, from.class - to.class) + Math.max(0, from.other - to.other) + if ( + total(x) !== total(y) || + excess(x, y) > y.plain + y.null || + excess(y, x) > x.plain + x.null + ) + return false + } + } + return true +} +class SetPayload {} +class OtherSetPayload {} +const setReachStates = [ + `empty`, + `singleton`, + `wide`, + `equal`, + `unmatched`, + `deficit`, + `reassignment`, + `cyclic`, + `acyclic`, + `general-forward`, + `general-reverse`, + `row-forward`, + `row-reverse`, +] as const + +// Count comparison work after constructing both inputs and the independent +// expectation. These synchronous fixtures cannot interleave another comparison +// while native methods are wrapped. Restore the exact descriptors even when a +// work ceiling throws; no test-only hook enters production. +function withSetComparisonOperations( + limit: number, + compare: () => boolean, +): boolean { + let operations = 0 + const wrap = (prototype: object, method: string): (() => void) => { + const original = Object.getOwnPropertyDescriptor(prototype, method) + if (!original || typeof original.value !== `function`) { + throw new Error(`Missing native comparison method: ${method}`) + } + Object.defineProperty(prototype, method, { + ...original, + value: function (this: unknown, ...args: Array): unknown { + if (++operations > limit) { + throw new Error( + `Set collection-operation work: ${operations} exceeds ${limit}`, + ) + } + return Reflect.apply(original.value, this, args) + }, + }) + return () => Object.defineProperty(prototype, method, original) + } + const restore: Array<() => void> = [] + try { + for (const method of [`has`, `get`, `set`, `delete`]) { + restore.push(wrap(Map.prototype, method)) + } + for (const method of [`has`, `add`, `delete`]) { + restore.push(wrap(Set.prototype, method)) + } + return compare() + } finally { + for (const method of restore) method() + } +} + +function checkSetBag( + left: ReadonlyArray, + right: ReadonlyArray, + cyclic: boolean, + reach: (state: (typeof setReachStates)[number]) => void, + workLimits?: { scalarReads: number; collectionOperations: number }, +): void { + const errors: Array = [] + for (const [name, equal] of [ + [`general`, deepEquals], + [`row`, sameRowValue], + ] as const) { + const expected = setBagsEqual(left, right, name === `row`) + for (const [a, b, direction] of [ + [left, right, `forward`], + [right, left, `reverse`], + ] as const) { + try { + reach(`${name}-${direction}`) + let reads = 0 + const scalarCeiling = + workLimits?.scalarReads ?? 4 * (Math.max(a.length, b.length) + 1) ** 3 + const make = (records: ReadonlyArray) => { + const result = new Set() + for (const spec of records) { + const member: object = + spec.kind === `class` + ? new SetPayload() + : spec.kind === `other` + ? new OtherSetPayload() + : spec.kind === `null` + ? (Object.create(null) as object) + : {} + Object.defineProperty(member, `value`, { + enumerable: true, + get() { + // Fail at the work observation, before exponential code can hang. + // Allocate diagnostics only on failure; each scalar read is counted. + if (++reads > scalarCeiling) { + throw new Error( + `Set scalar-field comparison work: ${reads} exceeds ${scalarCeiling}`, + ) + } + return spec.value + }, + }) + if (cyclic) Object.assign(member, { parent: result }) + result.add(member) + } + return result + } + const leftValue = make(a) + const rightValue = make(b) + const actual = withSetComparisonOperations( + workLimits?.collectionOperations ?? scalarCeiling, + () => equal(leftValue, rightValue), + ) + expect(actual, `Set bag equality`).toBe(expected) + } catch (error) { + errors.push( + new Error(`${name} ${direction} Set comparison: ${String(error)}`, { + cause: error, + }), + ) + } + } + } + if (errors.length) + throw new AggregateError(errors, errors.map(String).join(`; `), { + cause: errors[0], + }) +} +declareOracle({ + model: [setBag, setBagsEqual], + check: [checkSetBag], + grammar: [ambiguousSets], +}) +// Match the existing describe registration order to preserve IDs: utils.42. +describe(`object Set matching work`, () => { + const reach = createOracleReach(`utils.set-matching`, setReachStates) + afterAll(() => { + if (reach.counts().get(`empty`)) { + process.stdout.write( + `Set matching reach ${JSON.stringify(Object.fromEntries(reach.counts()))}\n`, + ) + reach.assertReached() + } + }) + utilsProperty([ambiguousSets])( + `object Set equality respects multiplicity with polynomial comparison work`, + ({ width, value, rotation }) => { + reach.beginHistory() + const plain = (count: number, leaf = value): Array => + Array.from({ length: count }, () => ({ kind: `plain`, value: leaf })) + for (const cyclic of [false, true]) { + reach.reach(cyclic ? `cyclic` : `acyclic`) + for (const count of [0, 1, width]) { + reach.reach( + count === 0 ? `empty` : count === 1 ? `singleton` : `wide`, + ) + const records = plain(count) + checkSetBag(records, [...records].reverse(), cyclic, reach.reach) + reach.reach(`equal`) + } + for (const kind of [`class`, `null`] as const) { + const records = plain(width) + checkSetBag( + [...records, { kind, value }], + plain(width + 1), + cyclic, + reach.reach, + ) + reach.reach(`unmatched`) + } + // Every member has a candidate, but the value counts cannot be paired. + const a = [...plain(width + 1), ...plain(1, value + 1)] + const b = [...plain(width), ...plain(2, value + 1)] + const offset = rotation % b.length + checkSetBag( + a, + [...b.slice(offset), ...b.slice(0, offset)], + cyclic, + reach.reach, + ) + reach.reach(`deficit`) + // A greedy first match needs reassignment: the plain member can use + // either class, but the class member can use only its own class. + checkSetBag( + [ + { kind: `plain`, value }, + { kind: `class`, value }, + ], + [ + { kind: `class`, value }, + { kind: `other`, value }, + ], + cyclic, + reach.reach, + ) + reach.reach(`reassignment`) + } + reach.assertReached() + }, + ) +}) + +/** + * Equal Sets whose corresponding members are equal need no assignment search. + * Scalar reads and native Map/Set operations have linear bounds for this + * common paired-input family. They do not impose linear work on arbitrary + * unordered or unequal Sets. The independent bag model checks distinct and + * interchangeable scalar members, + * with and without links back to the root Set, through both equality APIs. + * Width varies across 1,024–3,072, including the reported 3,000-member scale. + */ +const pairedSets = fc.record({ + width: fc.integer({ min: 1024, max: 3072 }), + value: fc.integer({ min: 1, max: 5 }), +}) +declareOracle({ grammar: [pairedSets] }) +describe(`paired object Set matching work`, () => { + const reach = createOracleReach(`utils.paired-set-matching`, [ + ...setReachStates.filter((state) => + [ + `general-forward`, + `general-reverse`, + `row-forward`, + `row-reverse`, + ].includes(state), + ), + `distinct`, + `interchangeable`, + `cyclic`, + `acyclic`, + ]) + afterAll(() => { + if (reach.counts().get(`distinct`)) { + reach.assertReached() + process.stdout.write( + `Paired Set reach ${JSON.stringify(Object.fromEntries(reach.counts()))}\n`, + ) + } + }) + utilsProperty([pairedSets])( + `paired equal Sets use linear comparison work at large widths`, + ({ width, value }) => { + reach.beginHistory() + for (const distinct of [false, true]) { + const records: Array = Array.from( + { length: width }, + (_, at) => ({ + kind: `plain`, + value: value + (distinct ? at : 0), + }), + ) + for (const cyclic of [false, true]) { + checkSetBag(records, records, cyclic, reach.reach, { + scalarReads: 4 * width, + collectionOperations: 16 * (width + 1), + }) + reach.reach(cyclic ? `cyclic` : `acyclic`) + } + reach.reach(distinct ? `distinct` : `interchangeable`) + } + reach.assertReached() + }, + 10, + ) +})