/** * @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.` 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.` 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.` 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 => { 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).UnresolvableFieldError as new ( ...args: any[] ) => Error const UnsupportedFindOptionError = (brainyExports as Record) .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, ErrorClass: new (...args: any[]) => Error, ...mustContain: string[] ): Promise { 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 { 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. 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. 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', // system.subtype — bare 'subtype' would address user metadata under the // law (the exact migration every fleet consumer's aggregates make). source: { type: NounType.Document, where: { 'system.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. // 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) }) })