feat(namespace): the one field-addressing law as a single source of truth — parseFieldAddress + the ruled ten-scalar system maps + plumbing invisibility + refusal builders (module only; query surfaces wire in next)
This commit is contained in:
parent
f6b14d21c0
commit
8f9a9989e9
1 changed files with 246 additions and 0 deletions
246
src/db/fieldAddressing.ts
Normal file
246
src/db/fieldAddressing.ts
Normal file
|
|
@ -0,0 +1,246 @@
|
||||||
|
/**
|
||||||
|
* @module db/fieldAddressing
|
||||||
|
* @description The one field-addressing law for every query surface (find()'s
|
||||||
|
* `where` / `orderBy` / `groupBy`, aggregation `source.where`), ruled
|
||||||
|
* 2026-08-03 after a production incident in which a user metadata field
|
||||||
|
* named `level` was silently shadowed by the engine's internal HNSW node
|
||||||
|
* layer (VENUE-BRAINY-ORDERBY-NOOP — thread id kept verbatim as the audit
|
||||||
|
* key; it names no product):
|
||||||
|
*
|
||||||
|
* 1. A BARE field name addresses the user's metadata field. Always.
|
||||||
|
* No priority resolution, no fallback chain — `orderBy: 'level'`
|
||||||
|
* reads `entity.metadata.level`, full stop.
|
||||||
|
* 2. `system.<field>` addresses an engine scalar, reachable ONLY with the
|
||||||
|
* explicit prefix. The entity map is exactly ten scalars; the relation
|
||||||
|
* map mirrors it with `verb`/`sourceId`/`targetId` as the structural
|
||||||
|
* members.
|
||||||
|
* 3. Engine plumbing (`vector`, `connections`, `level`, `data`, `_rev`) is
|
||||||
|
* INVISIBLE to the query surface in either spelling — `system.level`
|
||||||
|
* refuses; bare `level` is the user's field.
|
||||||
|
* 4. `metadata.<field>` is the explicit spelling of the bare form —
|
||||||
|
* identical semantics on every path.
|
||||||
|
* 5. Anything unresolvable refuses with a TYPED error naming both
|
||||||
|
* candidate spellings — an accepted name either works or refuses;
|
||||||
|
* there is no third state.
|
||||||
|
*
|
||||||
|
* This module is the SINGLE source of truth for the law: parsing, the maps,
|
||||||
|
* and the refusal builders live here so the JS engine, the provider seams,
|
||||||
|
* and the cross-engine conformance suite can never drift on the contract.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { HNSWNounWithMetadata, HNSWVerbWithMetadata } from '../coreTypes.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description The entity-side `system.*` map — EXACTLY the ten engine
|
||||||
|
* scalars David ruled queryable (2026-08-03). Adding a name here is a
|
||||||
|
* cross-engine contract change: the native accelerator's conformance suite
|
||||||
|
* pins this list verbatim, so any edit must ship as a paired release.
|
||||||
|
*/
|
||||||
|
export const SYSTEM_ENTITY_SCALARS: ReadonlySet<string> = new Set([
|
||||||
|
'id',
|
||||||
|
'type',
|
||||||
|
'subtype',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'confidence',
|
||||||
|
'weight',
|
||||||
|
'visibility',
|
||||||
|
'service',
|
||||||
|
'createdBy'
|
||||||
|
])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description The relation-side `system.*` map — the verb mirror of
|
||||||
|
* {@link SYSTEM_ENTITY_SCALARS}: `verb`, `sourceId`, `targetId` are the
|
||||||
|
* structural members beside the eight shared scalars. Same one law, same
|
||||||
|
* pairing rule for edits.
|
||||||
|
*/
|
||||||
|
export const SYSTEM_RELATION_SCALARS: ReadonlySet<string> = new Set([
|
||||||
|
'verb',
|
||||||
|
'sourceId',
|
||||||
|
'targetId',
|
||||||
|
'subtype',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'confidence',
|
||||||
|
'weight',
|
||||||
|
'visibility',
|
||||||
|
'service',
|
||||||
|
'createdBy'
|
||||||
|
])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Engine plumbing — never addressable from the query surface in
|
||||||
|
* ANY spelling. `level` is the HNSW node layer (the incident field: listing
|
||||||
|
* it as resolvable shadowed real user data); `data` is the payload container,
|
||||||
|
* not a scalar — content is reached through the content/text-search APIs,
|
||||||
|
* and addressing it as a sortable field would lie about its shape.
|
||||||
|
*/
|
||||||
|
export const PLUMBING_FIELDS: ReadonlySet<string> = new Set([
|
||||||
|
'vector',
|
||||||
|
'connections',
|
||||||
|
'level',
|
||||||
|
'data',
|
||||||
|
'_rev'
|
||||||
|
])
|
||||||
|
|
||||||
|
/** @description Which record kind a field address is being resolved against. */
|
||||||
|
export type FieldAddressKind = 'entity' | 'relation'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description A parsed, law-valid field address. `scope` says which side of
|
||||||
|
* the record the name lives on; `field` is the unprefixed name to read.
|
||||||
|
*/
|
||||||
|
export interface FieldAddress {
|
||||||
|
/** 'metadata' = the user's field (bare or `metadata.`-prefixed); 'system' = an engine scalar. */
|
||||||
|
scope: 'metadata' | 'system'
|
||||||
|
/** The field name with any scope prefix removed. */
|
||||||
|
field: string
|
||||||
|
/** The exact spelling the caller used — preserved for error text and telemetry. */
|
||||||
|
raw: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a query-surface field name under the one law. Pure and data-blind:
|
||||||
|
* this validates the ADDRESS (spelling + map membership), not whether any
|
||||||
|
* row actually carries the field — data-aware refusals (the did-you-mean
|
||||||
|
* for a bare system-scalar name no row carries) belong to the query layer,
|
||||||
|
* which calls {@link buildUnresolvableMessage} with index knowledge.
|
||||||
|
*
|
||||||
|
* @param raw - The field name as the caller wrote it (`level`,
|
||||||
|
* `metadata.level`, `system.createdAt`, …)
|
||||||
|
* @param kind - Entity or relation resolution (selects the system map)
|
||||||
|
* @returns The parsed {@link FieldAddress}
|
||||||
|
* @throws {InvalidFieldAddressError} for a `system.*` name outside the ruled
|
||||||
|
* map (including every plumbing field) or a malformed spelling — the error
|
||||||
|
* text enumerates the valid system scalars so the fix is in the message.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* parseFieldAddress('level', 'entity') // { scope: 'metadata', field: 'level' }
|
||||||
|
* parseFieldAddress('metadata.level', 'entity') // { scope: 'metadata', field: 'level' }
|
||||||
|
* parseFieldAddress('system.createdAt', 'entity') // { scope: 'system', field: 'createdAt' }
|
||||||
|
* parseFieldAddress('system.level', 'entity') // throws — plumbing is invisible
|
||||||
|
*/
|
||||||
|
export function parseFieldAddress(
|
||||||
|
raw: string,
|
||||||
|
kind: FieldAddressKind
|
||||||
|
): FieldAddress {
|
||||||
|
const systemMap =
|
||||||
|
kind === 'entity' ? SYSTEM_ENTITY_SCALARS : SYSTEM_RELATION_SCALARS
|
||||||
|
|
||||||
|
if (raw.startsWith('system.')) {
|
||||||
|
const field = raw.slice('system.'.length)
|
||||||
|
if (!systemMap.has(field)) {
|
||||||
|
throw new InvalidFieldAddressError(raw, kind, systemMap)
|
||||||
|
}
|
||||||
|
return { scope: 'system', field, raw }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raw.startsWith('metadata.')) {
|
||||||
|
const field = raw.slice('metadata.'.length)
|
||||||
|
if (field.length === 0) {
|
||||||
|
throw new InvalidFieldAddressError(raw, kind, systemMap)
|
||||||
|
}
|
||||||
|
return { scope: 'metadata', field, raw }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raw.length === 0) {
|
||||||
|
throw new InvalidFieldAddressError(raw, kind, systemMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bare name = the user's metadata field. Always. Even when the same name
|
||||||
|
// exists in the system map — `confidence` as a bare name is the user's
|
||||||
|
// metadata field named confidence; the engine scalar is system.confidence.
|
||||||
|
return { scope: 'metadata', field: raw, raw }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the addressed value off an entity. The ONLY sanctioned way a query
|
||||||
|
* surface turns a {@link FieldAddress} into a value — direct property reads
|
||||||
|
* against records re-create the shadow class this module exists to kill.
|
||||||
|
*
|
||||||
|
* @returns The value, or `undefined` when the record does not carry it
|
||||||
|
* (missing values sort LAST in both directions per the ordering contract —
|
||||||
|
* they are never grounds for dropping a row).
|
||||||
|
*/
|
||||||
|
export function readEntityFieldAddress(
|
||||||
|
entity: HNSWNounWithMetadata,
|
||||||
|
address: FieldAddress
|
||||||
|
): unknown {
|
||||||
|
if (address.scope === 'system') {
|
||||||
|
return (entity as unknown as Record<string, unknown>)[address.field]
|
||||||
|
}
|
||||||
|
return entity.metadata?.[address.field]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relation twin of {@link readEntityFieldAddress}. The stored flat record
|
||||||
|
* keys the relation type under `verb`; public Relation shapes may carry it
|
||||||
|
* as `type` — both spellings of the record are read, the ADDRESS is always
|
||||||
|
* `system.verb`.
|
||||||
|
*/
|
||||||
|
export function readRelationFieldAddress(
|
||||||
|
verb: HNSWVerbWithMetadata,
|
||||||
|
address: FieldAddress
|
||||||
|
): unknown {
|
||||||
|
if (address.scope === 'system') {
|
||||||
|
const rec = verb as unknown as Record<string, unknown>
|
||||||
|
if (address.field === 'verb') return rec.verb ?? rec.type
|
||||||
|
return rec[address.field]
|
||||||
|
}
|
||||||
|
return verb.metadata?.[address.field]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the ruled did-you-mean refusal text for a bare name that resolved to
|
||||||
|
* metadata but is UNKNOWN to the index — the data-aware half of the law,
|
||||||
|
* called by the query layer once it has consulted the known-field set:
|
||||||
|
*
|
||||||
|
* "no metadata field 'createdAt' — did you mean system.createdAt or
|
||||||
|
* metadata.createdAt?"
|
||||||
|
*
|
||||||
|
* When the bare name is NOT a system scalar the system candidate is omitted
|
||||||
|
* (there is only one thing the caller could have meant; the refusal exists
|
||||||
|
* because refusing beats silently sorting nothing).
|
||||||
|
*/
|
||||||
|
export function buildUnresolvableMessage(
|
||||||
|
raw: string,
|
||||||
|
kind: FieldAddressKind
|
||||||
|
): string {
|
||||||
|
const systemMap =
|
||||||
|
kind === 'entity' ? SYSTEM_ENTITY_SCALARS : SYSTEM_RELATION_SCALARS
|
||||||
|
if (systemMap.has(raw)) {
|
||||||
|
return (
|
||||||
|
`no metadata field '${raw}' — did you mean system.${raw} or metadata.${raw}? ` +
|
||||||
|
`(bare names always address your metadata; engine fields need the system. prefix)`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
`no metadata field '${raw}' on this store — nothing carries it, so an ordered or ` +
|
||||||
|
`filtered read against it cannot mean anything. Spell it metadata.${raw} once the ` +
|
||||||
|
`field exists, or check the field name.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Refusal for a malformed or out-of-map field ADDRESS —
|
||||||
|
* `system.<anything-not-in-the-map>` (including all plumbing), an empty
|
||||||
|
* name, or a bare `metadata.` prefix. The message carries the full valid
|
||||||
|
* system map so the fix never needs a docs lookup.
|
||||||
|
*/
|
||||||
|
export class InvalidFieldAddressError extends Error {
|
||||||
|
public readonly raw: string
|
||||||
|
public readonly kind: FieldAddressKind
|
||||||
|
|
||||||
|
constructor(raw: string, kind: FieldAddressKind, systemMap: ReadonlySet<string>) {
|
||||||
|
const valid = [...systemMap].map((f) => `system.${f}`).join(', ')
|
||||||
|
super(
|
||||||
|
`'${raw}' is not an addressable ${kind} field. Bare names address your own ` +
|
||||||
|
`metadata fields; engine fields are exactly: ${valid}. Engine plumbing ` +
|
||||||
|
`(vector, connections, level, data, _rev) is not part of the query surface.`
|
||||||
|
)
|
||||||
|
this.name = 'InvalidFieldAddressError'
|
||||||
|
this.raw = raw
|
||||||
|
this.kind = kind
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue