feat(query): the sparse-store cut — where on a never-carried field serves operator truth, never a refusal
A first adopter's namespace migration went 341 red on one class: the never-carried-field refusal firing on CORRECT filters against fresh and sparse stores — a freshly provisioned tenant refused its own first filtered read, with the did-you-mean built for typos firing hardest on day-one stores where nothing is wrong. The ruled cut: a WHERE filter naming a field no row carries is SERVED OPERATOR-TRUTHFULLY — eq/in/range/contains answer [] (nothing carries it, nothing matches); ne and exists:false answer ALL rows (the equally true complement — a blanket empty here would be silently wrong, which is why the simpler cut was rejected); exists:true answers []. Served from the field registry, with the did-you-mean demoted to a once-per-field WARN. orderBy and genuinely ambiguous addresses KEEP their hard typed refusals: no truthful order exists over an uncarried field, and ambiguity is a contract error while absence is data. Mechanics: the negative operator absorbs the FIELD_NOT_INDEXED throw as its empty exclude set (the clause-level catch correctly zeroes positive operators only); the egress matcher already agreed. Plus the provider-seam belt: a field refusal thrown by a replacement metadata manager is normalized to THIS package's UnresolvableFieldError at every filter call site — one class identity for consumers, instanceof works (a first adopter's cross-package finding). Conformance: tests/conformance/sparse-store-cut.test.ts — the shared operator rows both engines run (positive-empty, negative-all, fresh-tenant day-one, orderBy refusal kept, compound composition). Gates: unit 2065/2065 · integration 832 · conformance 36/36.
This commit is contained in:
parent
df96fccfd1
commit
7b67db4d0c
5 changed files with 188 additions and 8 deletions
82
tests/conformance/sparse-store-cut.test.ts
Normal file
82
tests/conformance/sparse-store-cut.test.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* @module tests/conformance/sparse-store-cut
|
||||
* @description THE SPARSE-STORE CUT (ruled 2026-08-12) — the shared
|
||||
* conformance rows both engines run: a WHERE filter naming a field NO row
|
||||
* carries is SERVED OPERATOR-TRUTHFULLY, never refused:
|
||||
* eq / in / range / contains → [] (nothing carries it → nothing matches)
|
||||
* ne / exists:false → ALL rows (equally true — blanket-empty here
|
||||
* would be the outlawed silent wrong)
|
||||
* exists:true → []
|
||||
* orderBy on an unresolvable field KEEPS the hard refusal (no truthful
|
||||
* order exists). The did-you-mean demotes to a throttled WARN on the serve.
|
||||
* A fresh tenant's first filtered read is a correct empty answer — the
|
||||
* 341-red first-adopter class, closed.
|
||||
*/
|
||||
import { describe, it, expect, afterEach } from 'vitest'
|
||||
import { Brainy, UnresolvableFieldError } from '../../src/index.js'
|
||||
import { NounType } from '../../src/types/graphTypes.js'
|
||||
|
||||
const brains: Brainy[] = []
|
||||
afterEach(async () => {
|
||||
for (const b of brains.splice(0)) await b.close().catch(() => {})
|
||||
})
|
||||
|
||||
async function corpus(): Promise<{ brain: Brainy; ids: string[] }> {
|
||||
const b = new Brainy({ storage: { type: 'memory' }, requireSubtype: false })
|
||||
await b.init()
|
||||
brains.push(b)
|
||||
const ids: string[] = []
|
||||
for (let i = 0; i < 4; i++) {
|
||||
ids.push(
|
||||
await b.add({ data: `row ${i}`, type: NounType.Document, metadata: { carried: i } })
|
||||
)
|
||||
}
|
||||
return { brain: b, ids }
|
||||
}
|
||||
|
||||
describe('sparse-store cut — operator-truthful serve on never-carried fields', () => {
|
||||
it('positive matches serve EMPTY: eq, in, range, contains', async () => {
|
||||
const { brain } = await corpus()
|
||||
expect(await brain.find({ where: { ghost: 'x' }, limit: 10 })).toEqual([])
|
||||
expect(await brain.find({ where: { ghost: { in: ['a', 'b'] } }, limit: 10 })).toEqual([])
|
||||
expect(await brain.find({ where: { ghost: { gt: 5 } }, limit: 10 })).toEqual([])
|
||||
expect(await brain.find({ where: { ghost: { exists: true } }, limit: 10 })).toEqual([])
|
||||
})
|
||||
|
||||
it('negative matches serve ALL rows: ne and exists:false (the truth, not blanket-empty)', async () => {
|
||||
const { brain, ids } = await corpus()
|
||||
const ne = await brain.find({ where: { ghost: { ne: 'x' } }, limit: 10 })
|
||||
expect(ne.map((r) => r.id).sort()).toEqual([...ids].sort())
|
||||
const absent = await brain.find({ where: { ghost: { exists: false } }, limit: 10 })
|
||||
expect(absent.map((r) => r.id).sort()).toEqual([...ids].sort())
|
||||
})
|
||||
|
||||
it('the fresh-tenant day-one shape: an EMPTY store answers its first filtered read with [], never a refusal', async () => {
|
||||
const b = new Brainy({ storage: { type: 'memory' }, requireSubtype: false })
|
||||
await b.init()
|
||||
brains.push(b)
|
||||
expect(await b.find({ where: { status: 'open' }, limit: 50 })).toEqual([])
|
||||
expect(await b.find({ where: { date: { gte: '2026-01-01' } }, limit: 50 })).toEqual([])
|
||||
})
|
||||
|
||||
it('orderBy on an unresolvable field KEEPS the typed refusal', async () => {
|
||||
const { brain } = await corpus()
|
||||
await expect(
|
||||
brain.find({ where: { carried: { gte: 0 } }, orderBy: 'system.notAScalar', limit: 10 })
|
||||
).rejects.toThrow(UnresolvableFieldError)
|
||||
})
|
||||
|
||||
it('compound filters: the never-carried clause composes truthfully with carried clauses', async () => {
|
||||
const { brain, ids } = await corpus()
|
||||
// carried>=2 AND ghost ne 'x' → the carried>=2 rows (ne-clause = all).
|
||||
const both = await brain.find({
|
||||
where: { carried: { gte: 2 }, ghost: { ne: 'x' } },
|
||||
limit: 10
|
||||
})
|
||||
expect(both.map((r) => r.id).sort()).toEqual([ids[2], ids[3]].sort())
|
||||
// carried>=2 AND ghost eq 'x' → [] (eq-clause empties the intersection).
|
||||
expect(
|
||||
await brain.find({ where: { carried: { gte: 2 }, ghost: 'x' }, limit: 10 })
|
||||
).toEqual([])
|
||||
})
|
||||
})
|
||||
|
|
@ -37,6 +37,9 @@ const MANUAL_ONLY = new Set<string>([
|
|||
// (byte + fold digests) — runs in the explicit conformance gate stage,
|
||||
// same invocation family as the other conformance suites.
|
||||
'tests/conformance/golden-log-fold.test.ts',
|
||||
// The sparse-store cut's shared operator rows (both engines run these):
|
||||
// explicit conformance-gate invocation, like its siblings.
|
||||
'tests/conformance/sparse-store-cut.test.ts',
|
||||
'tests/api/performance-benchmarks.test.ts',
|
||||
'tests/critical-neural-validation.test.ts',
|
||||
'tests/critical-performance-benchmark.test.ts',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue