Field Addressing
How a field name in where, orderBy, or groupBy resolves — one rule, no exceptions, identical on the pure-JS engine and the native accelerator.
In this section
How a field name in where, orderBy, or groupBy resolves — one rule, no exceptions, identical on the pure-JS engine and the native accelerator.
The one rule
A bare field name always means user metadata. Engine scalars are reached explicitly as
system.<field>.
Your metadata is yours. If you store { level: 9 }, { createdAt: 'Q3' }, or { type: 'invoice' }, then where: { level: 9 }, orderBy: 'createdAt', and groupBy: ['type'] operate on your values — an engine-internal field can never shadow a user field, no matter what it is named. This closed a real production bug class: a user field named level used to be silently dropped by the engine's internal never-index list, so filters returned [] and ordering fell back to insertion order with no error.
The ten system fields
Exactly ten engine scalars exist, and only the explicit system. prefix reaches them:
Address | Engine scalar | Typical use |
|---|---|---|
| Entity id (UUID) | Exact-row lookup |
| Entity NounType | Type-scoped queries |
| Entity subtype | Sub-classification filters |
| Creation timestamp (ms) | Age ordering, retention windows |
| Last-write timestamp (ms) | Freshness ordering |
| Classification confidence (0–1) | Quality thresholds |
| Importance/salience (0–1) | Salience ordering |
| Visibility tier |
|
| Multi-tenancy service id | Tenant scoping |
| Creating augmentation | Provenance filters |
system.<anything else> does not exist and refuses (see below).
Invisible plumbing
Five names are engine plumbing — never resolvable, never indexable, never orderable, not as bare names and not through system.:
vectorconnectionsleveldata_rev
Plumbing invisibility applies to the engine's fields only. A user metadata field that happens to share one of these names is ordinary user data and is reachable as the bare name, per the one rule — your { level: 9 } always works.
The index key format (frozen, cross-engine)
The law is also the physical key layout, identical on both engines and in every index surface (posting lists, column store):
User metadata keeps today's bare flattened keys —
{ level: 9 }is keyedlevel,{ a: { b: 1 } }is keyeda.b.The ten system scalars are keyed under the literal string
system.<field>— the engine type lives atsystem.type(the historic internalnounkey is retired), engine timestamps atsystem.createdAt/system.updatedAt, and so on.Origin — never name — decides the key. An engine scalar can only land on a
system.*key; a user metadata field can only land on a bare key. The two never merge, which is what makes the one rule enforceable at the storage level rather than by resolution priority.
Existing brains migrate by a full derived-index rebuild at the epoch bump that ships with the namespace pair; the rebuild is automatic at first open.
Reserved namespace: user fields may not be named system.*
Because system. is the engine's literal key prefix, a user metadata field whose name would flatten into it is refused at write time with a typed error — a field literally named system.foo, or an object field named system (whose children would flatten to system.* keys). This is the one name-based restriction the law imposes on user metadata, and it exists so user data can never forge an engine address. A scalar user field named exactly system (no dot) is ordinary user data and indexes fine.
Refusal semantics
Anything unresolvable is a typed refusal naming both candidates — never an empty result, never a silent fallback to insertion order:
Cannot resolve field 'priority' — no user metadata field 'metadata.priority'
exists in this brain, and 'system.priority' is not one of the ten system
fields. Did you mean one of: metadata.priority (write it first) or a
system.* field?The guarantee that matters: a query that cannot be answered throws. If you get [], the query resolved and honestly matched nothing. If you get results, they are ordered by the field you named. There is no third state.
Accepted-and-ignored options are banned under the same law: an option the engine cannot honor (cursor, includeRelations, writeOnly, …) is either implemented or refused — never swallowed.
Ordering contract
Entities missing the
orderByfield (or holdingnull) sort last in both directions — they are never dropped from the result.Ties break by id ascending, in both directions — ordering is fully deterministic.
Migration notes
Before this law, bare createdAt, updatedAt, type, and friends resolved to the engine fields. Under the law they resolve to user metadata.
Call sites that meant the engine scalar move to
system.*:orderBy: 'createdAt'→orderBy: 'system.createdAt',where: { type: 'thing' }→where: { 'system.type': 'thing' }, and so on for all ten.A missed site does not silently change meaning and keep running: if no user metadata field of that name exists, the bare name is unresolvable and refuses loudly, and the refusal message names the
system.<field>correction. The only silent case is a brain that genuinely has a user field of that name — in which case the bare name now means exactly what the rule says it means.
Machine-readable appendix
Downstream schema layers import this by copy; it is the same vocabulary the executable spec asserts.
{
"systemFields": [
"id",
"type",
"subtype",
"createdAt",
"updatedAt",
"confidence",
"weight",
"visibility",
"service",
"createdBy"
],
"plumbing": ["vector", "connections", "level", "data", "_rev"],
"rule": "bare=metadata"
}JSONThe executable spec
The law is enforced by src/native/fieldAddressingConformance.test.ts — a self-arming conformance suite that skips loudly until the namespace-capable engine pair is installed, then runs every case above (the ten-name collision bag, all five plumbing refusals on both where and orderBy, both-candidate refusal messages, nulls-last in both directions, id-ascending tie-breaks). The same suite runs in brainy's repository — it is the shared drift-proof that keeps the pure-JS engine and the native accelerator on one law.
Your field names are yours — guaranteed
Any bare name you choose is a fully ordinary field — including names the engine also uses internally. A field named data, level, confidence, or id in your metadata indexes, filters, sorts, and aggregates exactly like any other field, and never collides with the engine's own values:
await brain.add({ type: NounType.Person, metadata: { level: 9, data: 'raw', id: 'A-42' } })
await brain.find({ where: { level: 9 } }) // your field — always
await brain.find({ orderBy: 'level' }) // sorts by YOUR values
await brain.find({ orderBy: 'system.createdAt' }) // the engine's timestamp — explicitTSThis is not a documentation promise: the reopen-collider conformance case writes user metadata for every colliding name, flushes, cold-reopens the brain from disk, and asserts your values come back through find() — and both engines must pass it in every release gate, forever.
Content vs fields — what gets embedded, what gets indexed
Each entity has one embedding, computed from its content (the record's payload — the text you pass to add(), a file's body). That vector is what semantic similarity search runs against.
Metadata fields are not individually vector-indexed. They get two other kinds of indexing, matched to what fields are for: structural (exact match, ranges, sorting, aggregation) and lexical (string fields' words feed the hybrid text index, so keyword search sees them).
The rule of thumb: meaning goes in the content; exact values go in fields. If you want text found semantically, it belongs in the content; if you want a value filtered or sorted, it is a metadata field — and string fields get keyword findability for free.
On disk, each entity is two files: the record (engine scalars and the content slot at the top level, your metadata bag nested verbatim beside them) and the vector derived from that content. Indexes are projections of these canonical files and can always be rebuilt from them.