test(namespace)+docs: the cross-engine conformance suite (self-arming — skips until the resolver exports land) + the public field-addressing docs page; sidebar order deconflicted to 7

This commit is contained in:
David Snelling 2026-08-03 13:36:05 -07:00
parent 8f9a9989e9
commit d8d0b55f9d
2 changed files with 680 additions and 0 deletions

View file

@ -0,0 +1,196 @@
---
title: Field addressing: your fields and system fields
slug: concepts/field-addressing
public: true
category: concepts
template: concept
order: 7
description: The one rule for every query-surface field name — a bare name always means your metadata, system.<field> reaches the ten engine scalars explicitly, and anything else refuses by name.
next:
- concepts/consistency-model
---
# Field addressing: your fields and system fields
Every query surface in Brainy — `find()`'s `where`, `orderBy`, aggregation
`groupBy`, and aggregation `source.where` — resolves field names by one rule,
with no exceptions:
> **A bare field name always means your metadata. `system.<field>` reaches an
> engine scalar, and only when you spell it explicitly.**
```typescript
await brain.find({ orderBy: 'level' }) // reads entity.metadata.level — YOUR field
await brain.find({ orderBy: 'system.createdAt' }) // reads the engine's createdAt scalar
await brain.find({ orderBy: 'metadata.level' }) // identical to bare 'level' — explicit scope
```
There is no priority list, no "try the system field, fall back to metadata"
behavior, and no name that resolves differently depending on what else
happens to exist on your entities. A field called `level`, `score`,
`createdAt`, or `type` in your own `metadata` is read as *your* field, every
time, by its bare name.
## Why this rule exists
An internal report from a production deployment found that a user metadata
field literally named `level` was being silently shadowed by the engine's
own internal index layer field of the same name — every sort by `level`
returned insertion order, with no error raised. This rule makes that class of
bug structurally impossible: bare names belong to you, unconditionally, and
anything that isn't yours has to be spelled out.
## The system scalars
`system.<field>` addresses exactly ten scalars on an entity — no more, no
fewer:
| System field | What it is |
|---|---|
| `system.id` | The entity's id |
| `system.type` | The entity's `NounType` |
| `system.subtype` | The per-app sub-classification passed to `add()` |
| `system.createdAt` | When the entity was created |
| `system.updatedAt` | When the entity was last written |
| `system.confidence` | The `confidence` param (01) |
| `system.weight` | The `weight` param |
| `system.visibility` | `'public'` / `'internal'` (see the visibility tiers in [Consistency Model](./consistency-model.md)) |
| `system.service` | The multi-tenancy `service` tag |
| `system.createdBy` | Who/what created the entity |
Relationships mirror the same eight shared scalars (`subtype`, `createdAt`,
`updatedAt`, `confidence`, `weight`, `visibility`, `service`, `createdBy`)
plus three of their own:
| System field (relationship) | What it is |
|---|---|
| `system.verb` | The relationship's `VerbType` |
| `system.sourceId` | The id of the entity the relationship starts from |
| `system.targetId` | The id of the entity the relationship points to |
Anything not on these two lists is not a system scalar — `system.<name>` for
any other name refuses (see "Refusal semantics" below), even if that name
sounds like it should be engine-owned.
## Invisible plumbing — never addressable, in either spelling
Five names are pure engine internals. They are not reachable as a bare name,
and not reachable as `system.<name>` either — they simply have no place on
the query surface:
- **`vector`** — the stored embedding. It participates in similarity search
(`query`, `near`, vector `find()`), never in `where`/`orderBy`/`groupBy`.
- **`connections`** — graph adjacency. Reached through `connected` and
`brain.related()`, not through field addressing.
- **`level`** — the internal index layer number used by the nearest-neighbor
graph. It is pure index plumbing with no query-surface meaning at all —
which is exactly why a user field of the same name must never be shadowed
by it. `level` as a bare name is always yours; there is no engine-owned
spelling of it to compete with.
- **`data`** — your entity's content payload, not a scalar. It can be a
string, a number, or an arbitrary object, so sorting or filtering it as a
single comparable value would lie about its actual shape. Content is
reached through the content/text-search APIs (`query`, `searchMode:
'text'`), not through `where`/`orderBy`.
- **`_rev`** — the per-entity revision counter used for optimistic
concurrency (`ifRev`). It is a CAS token, not a queryable dimension.
`system.level`, `system.vector`, and `system.data` all refuse for the same
reason: they are not in the ten-scalar system map, full stop.
## `metadata.<field>` — the explicit spelling of "mine"
Prefix any field with `metadata.` to say the same thing a bare name already
says, spelled out. The two are interchangeable everywhere a field name is
accepted, including `orderBy`:
```typescript
await brain.find({ where: { 'customer.tier': 'gold' } })
await brain.find({ where: { 'metadata.customer.tier': 'gold' } }) // identical
await brain.find({ orderBy: 'metadata.score', order: 'desc' }) // identical to orderBy: 'score'
```
Reach for the explicit spelling when it reads more clearly next to a
`system.` field in the same query — for example, sorting by your own `score`
while filtering on `system.confidence`.
## Refusal semantics
A name that resolves to neither your metadata nor a system scalar is a typed
refusal, not a silent empty result and not a guess. Refusals name **both**
candidates, so the fix is always in the error text:
```typescript
await brain.find({ orderBy: 'createdAt' })
// UnresolvableFieldError: no metadata field 'createdAt' — did you mean
// system.createdAt or metadata.createdAt?
```
`UnresolvableFieldError` is exported from the package root:
```typescript
import { UnresolvableFieldError } from '@soulcraft/brainy'
try {
await brain.find({ orderBy: 'createdAt' })
} catch (err) {
if (err instanceof UnresolvableFieldError) {
// err.message names both candidates — usually enough to fix the call site.
}
}
```
A handful of `find()` options are not implemented yet: `cursor`,
`includeRelations`, and `writeOnly`. Rather than accepting them and quietly
ignoring the option, `find()` refuses with `UnsupportedFindOptionError`
also exported from the package root — so a call site can never believe an
unimplemented option took effect when it didn't.
## The ordering contract
`orderBy` behaves identically regardless of which engine (the pure-TypeScript
path or a native accelerator) is serving the query:
- An entity missing the `orderBy` field, or holding `null` on it, sorts
**LAST — in both `asc` and `desc`**. It is never treated as "smaller than
everything" in one direction and "larger than everything" in the other; it
is simply last, either way.
- Rows are **never dropped** from an ordered read because they lack the
field — a missing value changes position, never presence.
- Ties on the `orderBy` field break by **id ascending**, regardless of the
primary sort direction.
```typescript
// employees: [{ score: 9 }, { score: 5 }, { /* no score field */ }]
await brain.find({ orderBy: 'score', order: 'desc' }) // [9, 5, missing] — missing is last
await brain.find({ orderBy: 'score', order: 'asc' }) // [5, 9, missing] — missing is STILL last
```
## Migrating existing call sites
If you have call sites written before this rule shipped that rely on a bare
system name — `orderBy: 'createdAt'`, `where: { confidence: { greaterThan:
0.8 } }`, and similar — they now refuse instead of silently resolving to the
engine field. The fix is always in the error: swap the bare name for
`system.<field>` (or `metadata.<field>` if you actually meant your own field
of that name, and it happens to share a name with a system scalar):
```typescript
// Before: bare 'createdAt' silently meant the engine's timestamp.
await brain.find({ orderBy: 'createdAt' })
// After: say which one you meant.
await brain.find({ orderBy: 'system.createdAt' }) // the engine timestamp
await brain.find({ orderBy: 'metadata.createdAt' }) // your own field named createdAt, if you have one
```
There is no silent migration path by design — every ambiguous call site
surfaces as a refusal naming its own fix, once, the first time it runs
against the new rule.
## Where to go next
- [Consistency Model](./consistency-model.md) — the separate (and
longer-standing) contract for *reserved* fields: which names may never
appear inside a `metadata` bag at write time, distinct from this page's
read-time addressing rule.

View file

@ -0,0 +1,484 @@
/**
* @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)
})
})