open-brainy/tests/conformance/namespace-law.test.ts

484 lines
18 KiB
TypeScript

/**
* @module tests/conformance/namespace-law
* @description Conformance suite for the ruled field-addressing contract
* announced in RELEASES.md ("Coming next... one field-addressing law — bare
* names = user metadata, `system.<field>` for engine fields, typed refusals
* for unresolvable names"). This suite is the drift-proof shared by this
* engine and its native accelerator: both must satisfy every test here
* bit-for-bit, because they implement the SAME contract independently.
*
* The rule, in full:
* 1. A bare field name in `where` / `orderBy` / `groupBy` / aggregation
* `source.where` ALWAYS means the caller's own `metadata` field. No
* priority resolution, no engine fallback — ever.
* 2. `system.<field>` reaches an engine scalar, and ONLY an engine scalar,
* and ONLY when spelled explicitly. The addressable entity map is exactly
* ten names: id, type, subtype, createdAt, updatedAt, confidence, weight,
* visibility, service, createdBy. The relationship map is system.verb,
* system.sourceId, system.targetId, plus the eight scalars shared with
* entities.
* 3. Some names are invisible plumbing and are never addressable in either
* spelling: vector, connections, level, data, _rev. `system.level`,
* `system.vector`, and `system.data` all refuse — they are not in the
* system map. Bare `level` is a perfectly ordinary user field.
* 4. `metadata.<field>` is the explicit-user-scope spelling: identical
* semantics to the bare spelling, valid everywhere the bare spelling is.
* 5. Anything that resolves to neither a user field nor a system scalar is a
* typed refusal naming both candidates (`UnresolvableFieldError`).
* Unimplemented `find()` options (`cursor`, `includeRelations`,
* `writeOnly`) refuse with `UnsupportedFindOptionError` instead of being
* silently accepted and ignored.
* 6. Ordering is identical on both engines: rows missing/null on the
* `orderBy` field sort LAST in BOTH directions and are never dropped;
* ties break by id ascending.
*
* The motivating incident (told generically — see CLAUDE.md naming rule): an
* internal report from a production deployment showed a user metadata field
* literally named `level` silently shadowed by the engine's internal HNSW
* node layer, breaking sort order with zero errors raised. This contract
* makes that class of bug impossible, and testable forever.
*
* SELF-SKIP: the resolver this suite pins is being built in a parallel
* session and has not landed on every branch yet. Rather than going red on
* a branch that simply hasn't caught up, the suite detects whether the
* contract is live by the one thing any conformant implementation must
* export — `UnresolvableFieldError` from the package root — and skips
* loudly (never silently) until it does. This is the house pattern: a
* sibling engine's gate once went red because a test armed before its
* feature existed.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { Brainy } from '../../src/brainy.js'
import { NounType } from '../../src/types/graphTypes.js'
import * as brainyExports from '../../src/index.js'
const stubEmbedding = async (text: string): Promise<number[]> => {
const hash = text.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0)
return new Array(384).fill(0).map((_, i) => Math.sin(hash + i))
}
// Detected purely by the exported error-class NAME — never by reaching into
// implementation internals. Both engines building this contract must export
// it from the package root, so this is a legitimate, implementation-agnostic
// readiness probe.
const lawActive = 'UnresolvableFieldError' in brainyExports
const UnresolvableFieldError = (brainyExports as Record<string, unknown>).UnresolvableFieldError as new (
...args: any[]
) => Error
const UnsupportedFindOptionError = (brainyExports as Record<string, unknown>)
.UnsupportedFindOptionError as new (...args: any[]) => Error
// Always runs, regardless of lawActive — the loud signal that the rest of
// this file was skipped, and why.
it('namespace law armed?', () => {
if (!lawActive) {
console.warn(
'[conformance] namespace-law suite SKIPPED — UnresolvableFieldError not exported yet; arms when the resolver lands'
)
}
expect(true).toBe(true)
})
/**
* Awaits `promise`, asserting it rejects with an instance of `ErrorClass`
* whose `.message` contains every string in `mustContain`. Fails loudly if
* the promise resolves instead of rejecting.
*/
async function expectRefusal(
promise: Promise<unknown>,
ErrorClass: new (...args: any[]) => Error,
...mustContain: string[]
): Promise<void> {
let threw = false
try {
await promise
} catch (err) {
threw = true
expect(err).toBeInstanceOf(ErrorClass)
for (const fragment of mustContain) {
expect((err as Error).message).toContain(fragment)
}
}
expect(threw).toBe(true)
}
describe.skipIf(!lawActive)('namespace law — bare/system/metadata field addressing', () => {
let brain: Brainy
beforeEach(async () => {
brain = new Brainy({
requireSubtype: false,
storage: { type: 'memory' as const },
embeddingFunction: stubEmbedding
})
await brain.init()
})
afterEach(async () => {
await brain.close()
})
/** The star case from the motivating incident: metadata.level 3/9/6. */
async function addLevelRows(): Promise<string[]> {
const ids: string[] = []
for (const level of [3, 9, 6]) {
ids.push(
await brain.add({
data: `probe level ${level}`,
type: NounType.Person,
subtype: 'ns-law-level',
metadata: { name: `p-${level}`, level }
})
)
}
return ids
}
// -------------------------------------------------------------------
// Rule 1 — bare field name = the user's metadata field, always.
// -------------------------------------------------------------------
it("bare orderBy 'level' reads user metadata, desc and asc (the star case)", async () => {
await addLevelRows()
const desc = await brain.find({
type: NounType.Person,
subtype: 'ns-law-level',
orderBy: 'level',
order: 'desc',
limit: 100
})
expect(desc.map((r: any) => r.metadata?.level)).toEqual([9, 6, 3])
const asc = await brain.find({
type: NounType.Person,
subtype: 'ns-law-level',
orderBy: 'level',
order: 'asc',
limit: 100
})
expect(asc.map((r: any) => r.metadata?.level)).toEqual([3, 6, 9])
})
it("bare where { level: N } matches the user's field", async () => {
const ids = await addLevelRows()
const hit = await brain.find({ type: NounType.Person, subtype: 'ns-law-level', where: { level: 9 } })
expect(hit).toHaveLength(1)
expect(hit[0].id).toBe(ids[1])
expect(hit[0].metadata?.level).toBe(9)
})
// -------------------------------------------------------------------
// Rule 4 — metadata.<field> is the explicit-user-scope spelling,
// identical semantics to bare, valid on every path including orderBy.
// -------------------------------------------------------------------
it("'metadata.level' resolves identically to bare 'level'", async () => {
await addLevelRows()
const desc = await brain.find({
type: NounType.Person,
subtype: 'ns-law-level',
orderBy: 'metadata.level',
order: 'desc',
limit: 100
})
expect(desc.map((r: any) => r.metadata?.level)).toEqual([9, 6, 3])
})
// -------------------------------------------------------------------
// Rule 2 — system.<field> reaches an engine scalar explicitly.
// -------------------------------------------------------------------
it('system.createdAt sorts by entity age', async () => {
const ids: string[] = []
for (const name of ['first', 'second', 'third']) {
ids.push(
await brain.add({
data: `aged ${name}`,
type: NounType.Person,
subtype: 'ns-law-aged',
metadata: { name }
})
)
// Guarantee distinct createdAt timestamps between adds.
await new Promise((resolve) => setTimeout(resolve, 5))
}
const asc = await brain.find({
type: NounType.Person,
subtype: 'ns-law-aged',
orderBy: 'system.createdAt',
order: 'asc',
limit: 100
})
expect(asc.map((r: any) => r.id)).toEqual(ids)
const desc = await brain.find({
type: NounType.Person,
subtype: 'ns-law-aged',
orderBy: 'system.createdAt',
order: 'desc',
limit: 100
})
expect(desc.map((r: any) => r.id)).toEqual([...ids].reverse())
})
it('where on system.confidence filters by the engine scalar', async () => {
const highId = await brain.add({
data: 'high confidence row',
type: NounType.Person,
subtype: 'ns-law-confidence',
confidence: 0.95,
metadata: { name: 'hi' }
})
await brain.add({
data: 'low confidence row',
type: NounType.Person,
subtype: 'ns-law-confidence',
confidence: 0.4,
metadata: { name: 'lo' }
})
const hit = await brain.find({
type: NounType.Person,
subtype: 'ns-law-confidence',
where: { 'system.confidence': 0.95 }
})
expect(hit).toHaveLength(1)
expect(hit[0].id).toBe(highId)
})
it('groupBy on system.subtype groups by the engine scalar, not user metadata', async () => {
await brain.add({ data: 'i1', type: NounType.Document, subtype: 'invoice' })
await brain.add({ data: 'i2', type: NounType.Document, subtype: 'invoice' })
await brain.add({ data: 'r1', type: NounType.Document, subtype: 'receipt' })
brain.defineAggregate({
name: 'ns_law_by_subtype_system',
source: { type: NounType.Document },
groupBy: ['system.subtype'],
metrics: { count: { op: 'count' } }
})
const groups = await brain.queryAggregate('ns_law_by_subtype_system')
const invoiceGroup = groups.find((g) => Object.values(g.groupKey).includes('invoice'))
const receiptGroup = groups.find((g) => Object.values(g.groupKey).includes('receipt'))
expect(invoiceGroup?.metrics.count).toBe(2)
expect(receiptGroup?.metrics.count).toBe(1)
})
// -------------------------------------------------------------------
// Rule 1 (groupBy face) — bare groupBy dimensions read user metadata,
// never the engine's own notion of the same-sounding name.
// -------------------------------------------------------------------
it('groupBy on a bare user metadata field groups by that field', async () => {
await brain.add({
data: 'd1',
type: NounType.Document,
subtype: 'ns-law-group-bare',
metadata: { team: 'alpha' }
})
await brain.add({
data: 'd2',
type: NounType.Document,
subtype: 'ns-law-group-bare',
metadata: { team: 'alpha' }
})
await brain.add({
data: 'd3',
type: NounType.Document,
subtype: 'ns-law-group-bare',
metadata: { team: 'beta' }
})
brain.defineAggregate({
name: 'ns_law_by_team_bare',
source: { type: NounType.Document, where: { subtype: 'ns-law-group-bare' } },
groupBy: ['team'],
metrics: { count: { op: 'count' } }
})
const groups = await brain.queryAggregate('ns_law_by_team_bare')
const alphaGroup = groups.find((g) => Object.values(g.groupKey).includes('alpha'))
const betaGroup = groups.find((g) => Object.values(g.groupKey).includes('beta'))
expect(alphaGroup?.metrics.count).toBe(2)
expect(betaGroup?.metrics.count).toBe(1)
})
it('where on a bare user metadata field filters normally (score, not a system name)', async () => {
await brain.add({
data: 'high score',
type: NounType.Person,
subtype: 'ns-law-score',
metadata: { score: 42 }
})
await brain.add({
data: 'low score',
type: NounType.Person,
subtype: 'ns-law-score',
metadata: { score: 7 }
})
const hit = await brain.find({ type: NounType.Person, subtype: 'ns-law-score', where: { score: 42 } })
expect(hit).toHaveLength(1)
expect(hit[0].metadata?.score).toBe(42)
})
// -------------------------------------------------------------------
// Rule 5 — typed refusals, naming both candidates.
// -------------------------------------------------------------------
it("bare orderBy 'createdAt' refuses when no such metadata field exists — names both candidates", async () => {
await brain.add({
data: 'no metadata.createdAt here',
type: NounType.Person,
subtype: 'ns-law-refuse-createdAt',
metadata: { name: 'x' }
})
await expectRefusal(
brain.find({
type: NounType.Person,
subtype: 'ns-law-refuse-createdAt',
orderBy: 'createdAt',
limit: 10
}),
UnresolvableFieldError,
'system.createdAt',
'metadata.createdAt'
)
})
// -------------------------------------------------------------------
// Rule 3 — invisible plumbing refuses in either spelling; system.<name>
// for a name that isn't in the ten-scalar map is unresolvable.
// -------------------------------------------------------------------
it('system.level refuses — level is invisible plumbing, never a system scalar', async () => {
await brain.add({
data: 'has a level metadata field',
type: NounType.Person,
metadata: { level: 5 }
})
await expectRefusal(brain.find({ orderBy: 'system.level', limit: 10 }), UnresolvableFieldError)
})
it('system.vector refuses — vector is invisible plumbing, never a system scalar', async () => {
await brain.add({ data: 'row', type: NounType.Person, metadata: { name: 'x' } })
await expectRefusal(brain.find({ orderBy: 'system.vector', limit: 10 }), UnresolvableFieldError)
})
it('system.data refuses — data is a payload container, never a system scalar', async () => {
await brain.add({ data: 'row', type: NounType.Person, metadata: { name: 'x' } })
await expectRefusal(brain.find({ orderBy: 'system.data', limit: 10 }), UnresolvableFieldError)
})
// -------------------------------------------------------------------
// Rule 6 — the ordering contract.
// -------------------------------------------------------------------
async function addOrderingProbeRows(): Promise<{ ranked: string[]; missing: string }> {
const low = await brain.add({
data: 'low score',
type: NounType.Person,
subtype: 'ns-law-ordering',
metadata: { score: 5 }
})
const high = await brain.add({
data: 'high score',
type: NounType.Person,
subtype: 'ns-law-ordering',
metadata: { score: 9 }
})
const missing = await brain.add({
data: 'no score field at all',
type: NounType.Person,
subtype: 'ns-law-ordering',
metadata: { name: 'no-score' }
})
return { ranked: [low, high], missing }
}
it('a row missing the orderBy field sorts LAST in desc — and is never dropped', async () => {
const { ranked, missing } = await addOrderingProbeRows()
const desc = await brain.find({
type: NounType.Person,
subtype: 'ns-law-ordering',
orderBy: 'score',
order: 'desc',
limit: 100
})
expect(desc).toHaveLength(3)
expect(desc.map((r: any) => r.id)).toEqual([ranked[1], ranked[0], missing])
})
it('a row missing the orderBy field sorts LAST in asc too — and is never dropped', async () => {
const { ranked, missing } = await addOrderingProbeRows()
const asc = await brain.find({
type: NounType.Person,
subtype: 'ns-law-ordering',
orderBy: 'score',
order: 'asc',
limit: 100
})
expect(asc).toHaveLength(3)
expect(asc.map((r: any) => r.id)).toEqual([ranked[0], ranked[1], missing])
})
it('ties on the orderBy field break by id ascending, in BOTH directions', async () => {
const tiedIds: string[] = []
for (let i = 0; i < 4; i++) {
tiedIds.push(
await brain.add({
data: `tied ${i}`,
type: NounType.Person,
subtype: 'ns-law-ties',
metadata: { score: 5 }
})
)
}
const expectedOrder = [...tiedIds].sort()
const asc = await brain.find({
type: NounType.Person,
subtype: 'ns-law-ties',
orderBy: 'score',
order: 'asc',
limit: 100
})
expect(asc.map((r: any) => r.id)).toEqual(expectedOrder)
const desc = await brain.find({
type: NounType.Person,
subtype: 'ns-law-ties',
orderBy: 'score',
order: 'desc',
limit: 100
})
// Same tie-break ordering regardless of the primary direction — the
// contract states one universal rule ("id ascending"), not "reverse of
// the primary order".
expect(desc.map((r: any) => r.id)).toEqual(expectedOrder)
})
// -------------------------------------------------------------------
// Rule 5 (options face) — unimplemented find() options refuse loudly
// instead of being accepted and silently ignored.
// -------------------------------------------------------------------
it('find({ cursor }) refuses with UnsupportedFindOptionError', async () => {
await brain.add({ data: 'row', type: NounType.Person, metadata: { name: 'x' } })
await expectRefusal(brain.find({ cursor: 'anything', limit: 10 }), UnsupportedFindOptionError)
})
it('find({ includeRelations }) refuses with UnsupportedFindOptionError', async () => {
await brain.add({ data: 'row', type: NounType.Person, metadata: { name: 'x' } })
await expectRefusal(brain.find({ includeRelations: true, limit: 10 }), UnsupportedFindOptionError)
})
it('find({ writeOnly }) refuses with UnsupportedFindOptionError', async () => {
await brain.add({ data: 'row', type: NounType.Person, metadata: { name: 'x' } })
await expectRefusal(brain.find({ writeOnly: true, limit: 10 }), UnsupportedFindOptionError)
})
})