diff --git a/docs/contributing/oracle-coverage.md b/docs/contributing/oracle-coverage.md index 13a8493188..ea678fba9b 100644 --- a/docs/contributing/oracle-coverage.md +++ b/docs/contributing/oracle-coverage.md @@ -631,7 +631,7 @@ generic visitor behavior outside the named collectors. | 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, Maps, Sets, binary values, holes, deep nesting, and a cyclic value; an independent normal-form model decides equality and is checked against `equalHashValues`. Mutants that drop a string's or key's length, an array's length, or the Date and RegExp headers fail it; lowering the depth cap is equivalent, because the cap only bounds work. 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. | +| 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. A focused [release-timing test](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. 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 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. | | 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 five source states (eager or on-demand, idle or already started, and an on-demand source that already holds the rows), an unordered query and an ordered window, one or two live-query levels, and seven histories: start and read, subscribe or preload after a start, subscribe or preload without one, cleanup before a subscriber, 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 a DbClient block checks that a preload answered by a pending or settled stream still resumes acquisition. 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 Suspense with a `DbClient`, where render already defers source starts. | | 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. | diff --git a/docs/contributing/oracle-reviews/2026-10-14-ivm-structural-identity-normal-form.md b/docs/contributing/oracle-reviews/2026-10-14-ivm-structural-identity-normal-form.md new file mode 100644 index 0000000000..6fae86979a --- /dev/null +++ b/docs/contributing/oracle-reviews/2026-10-14-ivm-structural-identity-normal-form.md @@ -0,0 +1,175 @@ +# Structural identity oracle review — 2026-10-14 + +Reviewed executable head: `e1cd28afb6b9bd76641b42a06110eb184b5fac1a`. +Base: `87bc07354fa47df48e9b05c599f24dad74dea0f3` (`origin/main` at review). +The binary diff for the oracle, replay registry and manifest, and coverage map +has SHA-256 `9e94b37b8a03caa07f457d80d82a7aff399f70e3b47a6c59fe10190b43071c84`. +This record is added after the reviewed executable head; it changes no test or +production behavior. + +## Law, failure, and observation + +`structuralIdentity` must give equal D2 values the same result, including two +undefined results, and must not give unequal values the same defined string. +The contract is stated beside `structuralIdentity` in `hash.ts`; `groupBy` +depends on it when it merges contributions. The oracle checks the direct +`structuralIdentity` and `equalHashValues` return values after each call. It +does not claim to prove `groupBy` integration, which the incrementalization +law owns. + +On untouched main, the independent-pair property failed at seed `1158727847`, +path `255`: an explicit array around `deep(5, value)` and `deep(6, value)` +built equal arrays, but the model gave their specs different normal forms. This +was an oracle model error. The repair expands `deep` shorthand into repeated +one-element array normal forms. A named equal pair preserves the original +witness, and an adjacent-depth pair checks that the model still distinguishes +one missing array level. The equal-pair grammar now generates both same-spec +and explicit-array/`deep` spellings. A structural grammar filter excludes +RegExps with a NaN `lastIndex` from that equal-pair lane; the independent-pair +lane still generates them. + +The collision sweep added two distinct symbols with the same description and +Map, Set, and plain-object pairs with equal entry bodies. Their different D2 +values must retain different defined identities. The alphabet includes `0` so +an object can reconstruct the index key of a Map or Set body. + +## Grammar controls and executed calibration + +The generated specs cover primitive leaves, short strings, arrays with holes, +objects with unique keys and optional enumerable symbol keys, Dates, RegExps, +Maps, Sets, small byte arrays, and `deep` nesting. Arrays have 0–3 items; +objects have 0–3 unique string keys; strings and keys have 0–4 characters; +binary values have 0–3 bytes from 0–2; generated `deep` nodes have depth 3–6. +The equal-spelling bridge uses depth 3–5 plus one explicit array. The +equal-pair lane varies prototypes, key order, and hidden symbols through a +64-Boolean choice stream. Duplicate object keys and cyclic specs are excluded +from the grammar. A separate fixed case checks that a cyclic object gets no +identity. The NaN-RegExp exclusion applies only to equal pairs. + +Two fixed equal-pair witnesses assert that the builder produces an array +subclass with a hidden symbol and class/null-prototype objects with the +requested key order and hidden symbol. Each compares the variant against a +plain value through the model and production entry points. + +The named equal/adjacent `deep` pairs reconstruct the reported and neighboring +cases. Same-description symbols and equal-body Map/Set, Map/object, and +Set/object pairs reconstruct the four collision boundaries. With the old +`deep` model and the equal-spelling bridge removed, both 2,000-case campaigns +passed; restoring the bridge made the fixed campaign fail after three cases at +`model vs equalHashValues`. Replacing symbol identity with its description or +collapsing any of the three object-kind tags fails a named pair at the shared +identity assertion. Date-as-number, binary-as-string, and bigint-as-number tag +mutants fail the generated comparison. The earlier length/header controls +exercise string length, key length, array length, and Date/RegExp headers. +Four isolated mutants made identity sensitive to key order, object prototype, +hidden symbols, or array subclass. Each failed its named representation witness +at `equal values`. Holes versus undefined, enumerable symbols, inner Map/Set +content, and byte content are generated and modeled; this review did not +execute a fresh mutant for each of those axes. + +The old `deep:${depth}` model mutant fails the ordinary fixed equal-pair +campaign at seed `20261014`. The shrunk counterexample is an explicit array +around `deep(3, 0)` versus `deep(4, 0)`. Both the unshrunk and shrunk failures +reach `model vs equalHashValues`. Guarded replay of the shrunk path records one +run with `failed: true` for that mutant; the reviewed head records one run with +`failed: false`. The direct replay path is +`2:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0`. +The independent-pair replay at seed `1158727847`, path `8092`, also records +one successful run. Each generated property uses the same 2,000-run generator, +driver, and comparison for its fixed and seedless campaigns. The manifest +selects just the requested property for seed/path replay. + +The reviewed oracle file passes 17/17 tests, including guarded replay of the +shrunk path. Typecheck, lint, formatting, and diff checks pass. Hostile-mutant +results above are assertion failures at the named comparison, not setup errors +or timeouts. + +## ORC disposition at the reviewed head + +| Requirement | Disposition and evidence | +| --- | --- | +| ORC-001 | Met. The opening law and `hash.ts` contract identify authority; the coverage map bounds the owner. | +| ORC-002 | Met. `normal(Spec)` computes expected identity independently of hashing. The equal-pair exclusion uses a separate structural NaN-RegExp predicate, not `normal`. | +| ORC-003 | Met. Opening prose states law and limits; the spec grammar, `build` path, `normal` model, and `expectAgreement` return-time comparison remain distinct beside their code. | +| ORC-004 | Met for the bounded grammar above. Named and generated witnesses reconstruct the repair and collision axes; fixed representation witnesses confirm subclass, prototype, key-order, and hidden-symbol choices reach production. Mutants reject those four sensitivities, and removal of the equal-spelling bridge lets the old model pass both campaigns. Bounds and excluded states are explicit. Other axes were not freshly mutated. | +| ORC-005 | Met. The real hash entry points run, and equality/identity are compared at synchronous return. No concrete missing-input hypothesis applies to these fully constructed values. | +| ORC-006 | Met. The old-model and collision mutants fail at `model vs equalHashValues` or `a shared identity`. The reduced old-model failure preserves the same law and checkpoint. | +| ORC-007 | Met. Both properties run fixed and seedless 2,000-case campaigns; manifest-backed guarded seed/path replay selects one property and emits one-run witnesses. | +| ORC-008 | Not applicable. `normal` is a stateless recomputation; this change adds no model state or transition. | +| ORC-009 | Met. The model maps `deep` to repeated arrays and symbol leaf indices to stable symbol instances; the shared D2 terms follow the glossary. | +| ORC-010 | Met for this harness. The reporter preserves the original comparison failure, seed, path, and reduced failure; tests acquire no resource needing cleanup. | +| ORC-011 | Not applicable. The review identified no remaining concrete shared semantic fault for which a second formulation would distinguish this bounded law. `equalHashValues` is a sanity observation, not the model source. | +| ORC-012 | Met by this exact-head record, including the applicable and non-applicable requirements and the bounded repair claim. | +| ORC-013 | Met. The equal array/`deep` pair and adjacent-depth pair distinguish the nesting rule from a lost-array-level rule at the model/equality comparison. | +| ORC-014 | Not applicable. Values are constructed in-process; no controlled provider or host premise is handed to a real integration claim. | + +The repair closes the known false-red trace and tested nearby structural +identity collisions for finite acyclic specs at the direct helper boundary. +Temporal values, opaque reference values and functions, large binary values, +getters or proxies, arbitrary cyclic pairs, and mutation between observations +remain outside this owner. The coverage map assigns the separate `groupBy` path +to the incrementalization law. + + +## Post-merge independent review — 2026-10-16 + +Executable head: `4d9f3fffce65e9ed34567bc2ebf4a3051e20c159`. +Base: `2c83fdc53bd762fa8b8d5e5a8b82e092710618be` (`origin/main` at merge). +Merge commit `e61027695` preserved the earlier test behavior and retained the +coverage entries added on main. Three independent reviewers examined the merged +oracle's model, input grammar, and replay integration. + +The model review found a pre-existing product defect outside the numeric +RegExp grammar: two frozen RegExps with separate frozen object-valued +`lastIndex` headers compare unequal under D2 but share a defined structural +identity. A real `groupBy` witness merges their contributions. This remains +open in [#2192](https://github.com/TanStack/db/issues/2192); the numeric bound +is now explicit in the oracle and coverage map. Returning no identity alone +would leave equal structural hashes on this pair. + +The grammar review found two false greens in the previous 17-test oracle: +a Map/Set identity that serializes only its first entry, and an array identity +that omits enumerable non-index keys, each passed its fixed and fresh campaigns. +The executable head above generates zero to two Map/Set entries and an optional +enumerable array field. Pinned second-entry and field-difference pairs reach +the direct `structuralIdentity` and `equalHashValues` comparison. In scratch +mutant runs, the first-entry-only mutant failed both pinned Map/Set cases at +`a shared identity`; the array-field mutant failed its pinned case and a +generated independent pair at the same checkpoint. These were coverage gaps, +not observed production defects. + +The revised Set builder wraps its second member in a fresh tagged array, so +JavaScript cannot deduplicate two equal primitive inputs before the comparison. +This lane does not test native Set deduplication or reorder two unwrapped +members; the separate hash-identity owner covers insertion-order behavior. + +The old `deep:${depth}` model mutant still fails the fixed equal-pair campaign +after three cases at `model vs equalHashValues`. The reduced replay seed +`20261014` and path +`2:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0:0` +executed one failed mutant case and one passing repaired-model case. The +updated focused suite passed 20/20 tests, including fixed and fresh generated +campaigns; five repeated suite runs passed. Typecheck, lint, formatting, and +diff checks passed. The integration reviewer found no merge defect. + +| Requirement | Post-merge disposition | +| --- | --- | +| ORC-001 | Met within the declared finite grammar; reference-valued RegExp headers remain an open product defect in #2192. | +| ORC-002 | Met. Spec normal forms judge the generated values independently of hash code. | +| ORC-003 | Met. The opening law and limits, model, grammar, builder, production calls, and synchronous comparison remain visible together. | +| ORC-004 | Met for the revised bounds: zero to two Map/Set entries, optional array field, and numeric RegExp headers. Pinned pairs reconstruct the two survivor boundaries; their mutants now fail. | +| ORC-005 | Met for the direct helper boundary. `groupBy` integration is a separate receiving path. | +| ORC-006 | Met. The old deep model and both former survivor mutants fail at the stated comparisons. | +| ORC-007 | Met. Fixed and seedless campaigns retain the same properties and 2,000-case budget; direct seed/path replay retains one executed case. | +| ORC-008 | Not applicable: the normal-form model is stateless. | +| ORC-009 | Met. `deep` is declared model shorthand for nested arrays; the second Set member's wrapper preserves its JS insertion count. | +| ORC-010 | Met for this harness. Shrinking and direct replay preserve the comparison failure; no resource cleanup is involved. | +| ORC-011 | Open for the larger RegExp header domain: #2192 owns a reference-identity model and a `groupBy` receiving witness. The numeric grammar does not claim that domain. | +| ORC-012 | This addendum binds the revised evidence and open cells to the executable head above. | +| ORC-013 | Met. Equal and adjacent-depth pairs distinguish the nesting law; second-entry and array-field pairs distinguish the new grammar boundaries. | +| ORC-014 | Not applicable: all values are built in-process without a controlled external provider. | + +The PR repairs the false oracle failure for equivalent nested arrays at the +direct helper boundary. It changes no production code. Structural identity +correctness for reference-valued RegExp headers and the receiving `groupBy` +path remains open in #2192. diff --git a/packages/db-ivm/tests/structural-identity-oracle.property.test.ts b/packages/db-ivm/tests/structural-identity-oracle.property.test.ts index 93def32b26..9b0e23d6f0 100644 --- a/packages/db-ivm/tests/structural-identity-oracle.property.test.ts +++ b/packages/db-ivm/tests/structural-identity-oracle.property.test.ts @@ -1,7 +1,10 @@ import { describe, expect, it } from 'vitest' import { fc } from '@fast-check/vitest' import { equalHashValues, structuralIdentity } from '../src/hashing/hash.js' -import { oraclePropertyOptions } from '../../db/tests/oracle-config.js' +import { + oraclePropertyOptions, + readOracleRunConfig, +} from '../../db/tests/oracle-config.js' /** * # Does a structural identity agree with D2 value identity? @@ -17,39 +20,49 @@ import { oraclePropertyOptions } from '../../db/tests/oracle-config.js' * contributions merge. * * Model: values are built from generated specs, and two values are equal - * exactly when their specs are equal after normalization: -0 as 0, every - * NaN as one value, object entries in key order, and holes kept. A RegExp - * whose lastIndex is NaN equals nothing, because D2 compares headers with - * ===. The model shares no code with the hashing module; a sanity check also - * compares it with `equalHashValues`. + * exactly when their specs denote equal values after normalization: -0 as 0, + * every NaN as one value, object entries in key order, and holes kept. + * `deep` and explicit one-element `array` nodes denote the same nested arrays. + * Symbol leaf indices denote stable symbol instances, not their descriptions. + * Distinct RegExp objects whose lastIndex is NaN compare unequal because D2 + * compares headers with ===. The model shares no code with the hashing module; + * a sanity check also compares it with `equalHashValues`. * - * Histories: pairs built from one spec, once directly and once with - * representations D2 identity ignores (an array subclass for an array, a - * class instance or null-prototype object for a plain object, reversed key - * order, a non-enumerable symbol), and pairs built from two specs. Strings - * and keys include every separator the identity uses and digits, so an - * encoding that loses where one part ends fails law 2. Values include - * enumerable symbol keys, Dates (also invalid), RegExps, Maps, Sets, small - * binary values, holes, and nesting deeper than the identity's depth cap. - * Pinned pairs cover the joins a random short string cannot build. The depth - * cap bounds work and recursion, including on a cyclic value; it does not - * decide equality, so raising it changes no law here. + * Histories: equal pairs use one comparable spec or two spellings of the same + * nested arrays. Their builds vary array subclasses, object prototypes, key order, + * and non-enumerable symbols; D2 identity ignores those differences. + * Independent pairs use two specs. Strings and keys include every separator + * the identity uses and digits, so an encoding that loses where one part + * ends fails law 2. Values include distinct symbols with the same description, + * enumerable symbol keys, Dates (also invalid), RegExps with numeric + * `lastIndex`, Maps and Sets with zero to two entries, arrays with an + * enumerable non-index field, small binary values, holes, and nesting deeper + * than the identity's depth cap. + * Pinned pairs cover joins a random short string cannot build, distinct + * symbols with the same description, and different object kinds with the same + * body. Fixed representation witnesses verify that the driver reaches each + * prototype, key-order, and hidden-symbol variant it claims. The depth cap + * bounds work and recursion, including on a cyclic value; + * it does not decide equality, so raising it changes no law here. + * Both generated pair grammars run fixed and fresh-seed campaigns. A replay + * seed and path select the requested property without running the fixed one. */ type Spec = | { k: `leaf`; i: number } | { k: `text`; s: string } - | { k: `array`; items: Array } + | { k: `array`; items: Array; extra?: Spec } | { k: `object`; entries: Array<[string, Spec]>; symbol?: Spec } | { k: `date`; t: number } | { k: `regexp`; flags: string; lastIndex: number } - | { k: `map`; v: Spec } - | { k: `set`; v: Spec } + | { k: `map`; values: Array } + | { k: `set`; values: Array } | { k: `binary`; bytes: Array } | { k: `deep`; depth: number; v: Spec } const SYMBOL = Symbol(`s`) const ENUMERABLE_SYMBOL = Symbol(`enumerable`) +const { replayProperty } = readOracleRunConfig() const LEAVES: ReadonlyArray = [ 0, -0, @@ -62,10 +75,11 @@ const LEAVES: ReadonlyArray = [ null, undefined, SYMBOL, + Symbol(`s`), ] // Every separator and tag the identity uses, plus digits. const textArbitrary = fc.string({ - unit: fc.constantFrom(`a`, `|`, `=`, `:`, `"`, `#`, `[`, `{`, `1`, `2`), + unit: fc.constantFrom(`a`, `|`, `=`, `:`, `"`, `#`, `[`, `{`, `0`, `1`, `2`), maxLength: 4, }) @@ -89,13 +103,17 @@ const specArbitrary = fc.letrec<{ spec: Spec }>((tie) => ({ s: textArbitrary, }), }, - fc.record({ - k: fc.constant(`array` as const), - // null marks a hole. - items: fc.array(fc.option(tie(`spec`), { nil: null, freq: 5 }), { - maxLength: 3, - }), - }), + fc.record( + { + k: fc.constant(`array` as const), + // null marks a hole. + items: fc.array(fc.option(tie(`spec`), { nil: null, freq: 5 }), { + maxLength: 3, + }), + extra: tie(`spec`), + }, + { requiredKeys: [`k`, `items`] }, + ), fc.record( { k: fc.constant(`object` as const), @@ -116,8 +134,14 @@ const specArbitrary = fc.letrec<{ spec: Spec }>((tie) => ({ flags: fc.constantFrom(``, `g`), lastIndex: fc.constantFrom(0, 1, Number.NaN), }), - fc.record({ k: fc.constant(`map` as const), v: tie(`spec`) }), - fc.record({ k: fc.constant(`set` as const), v: tie(`spec`) }), + fc.record({ + k: fc.constant(`map` as const), + values: fc.array(tie(`spec`), { maxLength: 2 }), + }), + fc.record({ + k: fc.constant(`set` as const), + values: fc.array(tie(`spec`), { maxLength: 2 }), + }), fc.record({ k: fc.constant(`binary` as const), bytes: fc.array(fc.nat(2), { maxLength: 3 }), @@ -130,6 +154,46 @@ const specArbitrary = fc.letrec<{ spec: Spec }>((tie) => ({ ), })).spec +// Distinct RegExp instances with a NaN lastIndex are not equal, even when +// built from one spec. This grammar check does not consult the normal-form +// model; the independent-pair grammar still generates those RegExps. +function containsNaNRegExp(spec: Spec): boolean { + switch (spec.k) { + case `regexp`: + return Number.isNaN(spec.lastIndex) + case `array`: + return ( + spec.items.some((item) => item !== null && containsNaNRegExp(item)) || + (spec.extra !== undefined && containsNaNRegExp(spec.extra)) + ) + case `object`: + return ( + spec.entries.some(([, value]) => containsNaNRegExp(value)) || + (spec.symbol !== undefined && containsNaNRegExp(spec.symbol)) + ) + case `map`: + case `set`: + return spec.values.some(containsNaNRegExp) + case `deep`: + return containsNaNRegExp(spec.v) + default: + return false + } +} +const comparableSpecArbitrary = specArbitrary.filter( + (spec) => !containsNaNRegExp(spec), +) + +const equalSpecPairArbitrary = fc.oneof( + comparableSpecArbitrary.map((spec): [Spec, Spec] => [spec, spec]), + fc + .tuple(comparableSpecArbitrary, fc.integer({ min: 3, max: 5 })) + .map(([spec, depth]): [Spec, Spec] => [ + { k: `array`, items: [{ k: `deep`, depth, v: spec }] }, + { k: `deep`, depth: depth + 1, v: spec }, + ]), +) + /** Build a value; `vary` picks representations that D2 identity ignores. */ function build(spec: Spec, vary: () => boolean): unknown { switch (spec.k) { @@ -143,6 +207,9 @@ function build(spec: Spec, vary: () => boolean): unknown { spec.items.forEach((item, index) => { if (item !== null) array[index] = build(item, vary) }) + if (spec.extra !== undefined) { + Object.assign(array, { note: build(spec.extra, vary) }) + } if (vary()) Object.defineProperty(array, Symbol(`hidden`), { value: 1 }) return array } @@ -172,9 +239,20 @@ function build(spec: Spec, vary: () => boolean): unknown { return regexp } case `map`: - return new Map([[`k`, build(spec.v, vary)]]) + return new Map( + spec.values.map((value, index): [string, unknown] => [ + index === 0 ? `k` : `x`, + build(value, vary), + ]), + ) case `set`: - return new Set([build(spec.v, vary)]) + // Wrap the second value so JS Set never deduplicates two primitive values + // before D2 can compare their ordered entries. + return new Set( + spec.values.map((value, index) => + index === 0 ? build(value, vary) : [index, build(value, vary)], + ), + ) case `binary`: return new Uint8Array(spec.bytes) case `deep`: { @@ -207,7 +285,7 @@ function normal(spec: Spec): string | undefined { return Number.isNaN(value) ? `number:NaN` : `number:${value === 0 ? 0 : value}` - if (typeof value === `symbol`) return `symbol:s` + if (typeof value === `symbol`) return `symbol:${spec.i}` if (typeof value === `string`) return `string:${JSON.stringify(value)}` return `${typeof value}:${String(value)}` } @@ -215,7 +293,13 @@ function normal(spec: Spec): string | undefined { return `string:${JSON.stringify(spec.s)}` case `array`: { const items = parts(spec.items) - return items && `array:${JSON.stringify(items)}` + if (items === undefined) return undefined + const base = `array:${JSON.stringify(items)}` + if (spec.extra === undefined) return base + const extra = normal(spec.extra) + return extra === undefined + ? undefined + : `${base}:note:${JSON.stringify(extra)}` } case `object`: { const entries: Array<[string, string]> = [] @@ -237,25 +321,28 @@ function normal(spec: Spec): string | undefined { ? undefined : `regexp:${spec.flags}:${spec.lastIndex}` case `map`: { - const v = normal(spec.v) - return v && `map:${v}` + const values = parts(spec.values) + return values && `map:${JSON.stringify(values)}` } case `set`: { - const v = normal(spec.v) - return v && `set:${v}` + const values = parts(spec.values) + if (values === undefined) return undefined + // The second Set member is a fresh [index, value] array in build(). + if (values.length === 2) { + values[1] = `array:${JSON.stringify([`number:1`, values[1]])}` + } + return `set:${JSON.stringify(values)}` } case `binary`: return `binary:${spec.bytes.join(`,`)}` case `deep`: { - // Collapse nested deep nodes by summing their depths - let innerSpec = spec.v - let totalDepth = spec.depth - while (innerSpec.k === `deep`) { - totalDepth += innerSpec.depth - innerSpec = innerSpec.v + // `deep` is model shorthand for repeated single-element arrays. + let value = normal(spec.v) + if (value === undefined) return undefined + for (let level = 0; level < spec.depth; level++) { + value = `array:${JSON.stringify([value])}` } - const v = normal(innerSpec) - return v && `deep:${totalDepth}:${v}` + return value } } } @@ -289,7 +376,48 @@ const collisionPairs: Array<[string, unknown, unknown]> = [ [`header spans the body`, [`#1`], [1]], ] +function decisions(...values: Array): () => boolean { + let index = 0 + return () => values[index++] ?? false +} + describe(`structuralIdentity`, () => { + it(`ignores array subclass and hidden symbol representations`, () => { + const spec: Spec = { k: `array`, items: [{ k: `leaf`, i: 3 }] } + const plain = build(spec, () => false) + const variant = build(spec, decisions(true, true)) as Sub + expect(variant).toBeInstanceOf(Sub) + const [hidden] = Object.getOwnPropertySymbols(variant) + expect(hidden).toBeDefined() + expect(Object.getOwnPropertyDescriptor(variant, hidden!)?.enumerable).toBe( + false, + ) + expectAgreement(spec, spec, plain, variant) + }) + + it(`ignores object prototype, key order, and hidden symbol representations`, () => { + const spec: Spec = { + k: `object`, + entries: [ + [`a`, { k: `leaf`, i: 0 }], + [`b`, { k: `leaf`, i: 3 }], + ], + } + const plain = build(spec, () => false) + const classVariant = build(spec, decisions(true, true, true, true)) + const nullPrototype = build(spec, decisions(true, false, false, false)) + expect(classVariant).toBeInstanceOf(Instance) + expect(Object.keys(classVariant as object)).toEqual([`b`, `a`]) + const [hidden] = Object.getOwnPropertySymbols(classVariant as object) + expect(hidden).toBeDefined() + expect( + Object.getOwnPropertyDescriptor(classVariant, hidden!)?.enumerable, + ).toBe(false) + expect(Object.getPrototypeOf(nullPrototype)).toBeNull() + expectAgreement(spec, spec, plain, classVariant) + expectAgreement(spec, spec, plain, nullPrototype) + }) + it.each(collisionPairs)( `gives different values different identities: %s`, (_, left, right) => { @@ -306,23 +434,37 @@ describe(`structuralIdentity`, () => { expect(structuralIdentity(cyclic)).toBeUndefined() }) - it(`agrees with D2 value identity for equal representations`, () => { - fc.assert( - fc.property( - specArbitrary, - fc.array(fc.boolean(), { minLength: 64, maxLength: 64 }), - (spec, coins) => { - let index = 0 - const vary = () => coins[index++ % coins.length]! - expectAgreement( - spec, - spec, - build(spec, () => false), - build(spec, vary), - ) - }, - ), - { numRuns: 2000 }, + it(`agrees with D2 value identity for equal representations`, async () => { + if ( + replayProperty !== undefined && + replayProperty !== `structural-identity.equal-representations` + ) + return + // The guarded replay records completed runs through an async reporter. + const property = fc.asyncProperty( + equalSpecPairArbitrary, + fc.array(fc.boolean(), { minLength: 64, maxLength: 64 }), + ([leftSpec, rightSpec], coins) => { + let index = 0 + const vary = () => coins[index++ % coins.length]! + expectAgreement( + leftSpec, + rightSpec, + build(leftSpec, () => false), + build(rightSpec, vary), + ) + return Promise.resolve() + }, + ) + const options = oraclePropertyOptions( + 2000, + `structural-identity.equal-representations`, + ) + if (options.seed === undefined) + await fc.assert(property, { numRuns: options.numRuns, seed: 20261014 }) + await fc.assert( + property, + options.path === undefined ? options : { ...options, numRuns: 1 }, ) }) @@ -341,8 +483,110 @@ describe(`structuralIdentity`, () => { ], [ `an empty string in a set`, - { k: `set`, v: { k: `text`, s: `` } }, - { k: `set`, v: { k: `leaf`, i: 5 } }, + { k: `set`, values: [{ k: `text`, s: `` }] }, + { k: `set`, values: [{ k: `leaf`, i: 5 }] }, + ], + [ + `a deep node and an explicit array`, + { k: `array`, items: [{ k: `deep`, depth: 5, v: { k: `leaf`, i: 3 } }] }, + { k: `deep`, depth: 6, v: { k: `leaf`, i: 3 } }, + ], + [ + `adjacent nesting depths`, + { k: `array`, items: [{ k: `deep`, depth: 5, v: { k: `leaf`, i: 3 } }] }, + { k: `deep`, depth: 5, v: { k: `leaf`, i: 3 } }, + ], + [ + `distinct symbols with the same description`, + { k: `leaf`, i: 10 }, + { k: `leaf`, i: 11 }, + ], + [ + `a Map and Set with the same entry body`, + { k: `map`, values: [{ k: `leaf`, i: 0 }] }, + { + k: `set`, + values: [ + { + k: `array`, + items: [ + { k: `text`, s: `k` }, + { k: `leaf`, i: 0 }, + ], + }, + ], + }, + ], + [ + `a Map and object with the same entry body`, + { k: `map`, values: [{ k: `leaf`, i: 0 }] }, + { + k: `object`, + entries: [ + [ + `0`, + { + k: `array`, + items: [ + { k: `text`, s: `k` }, + { k: `leaf`, i: 0 }, + ], + }, + ], + ], + }, + ], + [ + `a Set and object with the same entry body`, + { k: `set`, values: [{ k: `leaf`, i: 0 }] }, + { k: `object`, entries: [[`0`, { k: `leaf`, i: 0 }]] }, + ], + [ + `Maps that differ at the second entry`, + { + k: `map`, + values: [ + { k: `leaf`, i: 0 }, + { k: `leaf`, i: 0 }, + ], + }, + { + k: `map`, + values: [ + { k: `leaf`, i: 0 }, + { k: `leaf`, i: 3 }, + ], + }, + ], + [ + `Sets that differ at the second entry`, + { + k: `set`, + values: [ + { k: `leaf`, i: 0 }, + { k: `leaf`, i: 0 }, + ], + }, + { + k: `set`, + values: [ + { k: `leaf`, i: 0 }, + { k: `leaf`, i: 3 }, + ], + }, + ], + [ + `arrays that differ at an enumerable non-index field`, + { + k: `array`, + items: [{ k: `leaf`, i: 0 }], + extra: { k: `leaf`, i: 0 }, + }, + { + k: `array`, + items: [{ k: `leaf`, i: 0 }], + extra: { k: `leaf`, i: 3 }, + }, ], ] as Array<[string, Spec, Spec]>)( `agrees with D2 value identity for %s`, @@ -356,17 +600,34 @@ describe(`structuralIdentity`, () => { }, ) - it(`agrees with D2 value identity for independent values`, () => { - fc.assert( - fc.property(specArbitrary, specArbitrary, (left, right) => { + it(`agrees with D2 value identity for independent values`, async () => { + if ( + replayProperty !== undefined && + replayProperty !== `structural-identity.independent-values` + ) + return + const property = fc.asyncProperty( + specArbitrary, + specArbitrary, + (left, right) => { expectAgreement( left, right, build(left, () => false), build(right, () => false), ) - }), - oraclePropertyOptions(2000, `structural-identity.independent-values`), + return Promise.resolve() + }, + ) + const options = oraclePropertyOptions( + 2000, + `structural-identity.independent-values`, + ) + if (options.seed === undefined) + await fc.assert(property, { numRuns: options.numRuns, seed: 1158727847 }) + await fc.assert( + property, + options.path === undefined ? options : { ...options, numRuns: 1 }, ) }) }) diff --git a/packages/db/tests/oracle-config.ts b/packages/db/tests/oracle-config.ts index 53f1718c6d..444684262a 100644 --- a/packages/db/tests/oracle-config.ts +++ b/packages/db/tests/oracle-config.ts @@ -20,6 +20,7 @@ const staticOracleProperties = [ `cursor-pagination.failure`, `query-db.refetch-ordered-prefix`, `oracle-replay.calibration`, + `structural-identity.equal-representations`, `structural-identity.independent-values`, `sqlite-persistence.source-fifo-order`, `sqlite-persistence.abort-graph`, diff --git a/packages/db/tests/oracle-replay-manifest.ts b/packages/db/tests/oracle-replay-manifest.ts index ed60a1e5f4..7bcf5f9337 100644 --- a/packages/db/tests/oracle-replay-manifest.ts +++ b/packages/db/tests/oracle-replay-manifest.ts @@ -19,6 +19,11 @@ const ownerGroups: ReadonlyArray = [ `key ascending descending`, ], [`db/tests/oracle-replay.fixture.test.ts`, `oracle-replay`, `calibration`], + [ + `db-ivm/tests/structural-identity-oracle.property.test.ts`, + `structural-identity`, + `equal-representations independent-values`, + ], [ `db-sqlite-persistence-core/tests/persisted-oracle.test.ts`, `sqlite-persistence`, @@ -261,7 +266,15 @@ export type OracleReplayManifestEntry = { // These patterns name the random/replay lane, never its fixed-seed sibling. // Pagination cells carry their property name in the test title so the guard // can select one cell without running the rest of the matrix. -const indexReplayTestNames = new Map([ +const replayTestNames = new Map([ + [ + `structural-identity.equal-representations`, + `agrees with D2 value identity for equal representations`, + ], + [ + `structural-identity.independent-values`, + `agrees with D2 value identity for independent values`, + ], [ `index-update.reference-model`, `matches a reference model across valid operation sequences \\(seed undefined\\)`, @@ -287,7 +300,7 @@ function directReplayTestName(property: string): string | undefined { return `executes the replay calibration property` if (property.startsWith(`pagination.matrix.`)) return `\\[${property.replaceAll(`.`, `\\.`)}\\]` - return indexReplayTestNames.get(property) + return replayTestNames.get(property) } export const oracleReplayManifest: ReadonlyArray = [