feat(namespace)!: NO SPECIAL NAMES + storage fidelity — the ruled completion of the field-addressing law
The write side of the law, ruled 2026-08-03: data is either in main space where developers can use anything, or it is in system.*. - The reserved-name write door DIES: add/update/relate/updateRelation metadata bags accept EVERY name (confidence, type, id, data, level, content, ...) as ordinary user fields — indexed, filterable, sortable, aggregatable, identical to any other field. The remap/enforce/warn machinery, the reservedFieldPolicy config (now a typed init refusal), and the compile-time metadata key bans are all removed. The one write refusal left: keys spelled 'system.*' (namespace forgery), now enforced on all four write doors. - STORED RECORDS GO NESTED (v2): engine fields top-level, the user bag nested verbatim under 'metadata', sealed by a format stamp — by-name storage discrimination is unsound once colliders are admitted. Legacy flat records stay readable forever through the shape-aware splitters (sound for them: the old door refused colliders). Time travel rides the same split (generation store snapshots whole records). - Name-based index exclusions DIE: user frame indexes every name; the excludeFields/indexedFields knobs and their silent-[] holes are gone; bulk-payload protection is value-shape only, uniform across names. - Consumer-sweep findings fixed in the same wave: per-type counts read the frozen 'system.type' column (addToIndex sort, affinity tracking, cold-count rehydration, VFS type bitmaps — legacy 'noun' fallback for pre-rebuild reads); resolveHiddenIds addresses 'system.visibility' (bare 'visibility' was a silent no-op under the law — VFS/system entities leaked into default reads). - Fidelity fallout fixed in the owning layers: readEntityFieldAddress reads the bag first (colliders were absent-shadowed by its own guard) and never serves system addresses from the bag; blob history refs read the bag shape-aware; migration transforms now receive ONE normalized view (engine fields + nested bag) regardless of stored era, and stray flat-habit keys refuse with the fix in the message. - THE REOPEN-COLLIDER CONFORMANCE CASE (required before any RC counts as gates-green): all ten collider names + plumbing names written as user fields, verified verbatim + queryable across live reads, flush+reopen, a forced epoch rebuild, and asOf time travel; relation mirror; forgery refusals; legacy flat-record compat. 8/8 green. Gates: unit 1901/1901 (exit 0) · integration 758 (exit 0) · conformance 27/27 (exit 0) · consumer test sweep migrated (10 files).
This commit is contained in:
parent
48a6130a50
commit
b11b5f7e68
32 changed files with 1355 additions and 1905 deletions
|
|
@ -42,7 +42,8 @@ describe('find({ where, orderBy }) bounds the sort to the page (CTX-BR-FIND-ORDE
|
|||
return real(f, ob, o, topK)
|
||||
}
|
||||
|
||||
const results = await brain.find({ where: { bucket: 'x' }, orderBy: 'createdAt', order: 'desc', limit: 5 })
|
||||
// system.createdAt — entity age, not a user metadata field named 'createdAt'.
|
||||
const results = await brain.find({ where: { bucket: 'x' }, orderBy: 'system.createdAt', order: 'desc', limit: 5 })
|
||||
|
||||
expect(results).toHaveLength(5)
|
||||
// Page-bounded: ~ limit (5) + a small hidden-tier over-fetch — NOT all 50 matches.
|
||||
|
|
|
|||
|
|
@ -1,251 +0,0 @@
|
|||
/**
|
||||
* @module tests/unit/brainy/reserved-field-policy
|
||||
* @description The 8.0 `reservedFieldPolicy` matrix — what happens when an
|
||||
* untyped (JavaScript) caller smuggles a Brainy-reserved field INSIDE the
|
||||
* `metadata` bag of a write call, past the compile-time guard.
|
||||
*
|
||||
* 8.0 is a clean break with no silent failures. The decided contract:
|
||||
* - `'throw'` (DEFAULT): a reserved key in the bag throws a clear Error naming
|
||||
* the offending key(s) and the correct write path. No remap, no data loss.
|
||||
* - `'warn'`: legacy remap PLUS a one-shot (per method+field, per process)
|
||||
* warning for EVERY reserved key found.
|
||||
* - `'remap'`: the pre-8.0 silent remap, no warning.
|
||||
*
|
||||
* The deep correctness of the remap itself (top-level precedence, system-managed
|
||||
* drops, transact()/with() mirrors, read-side splitting) lives in
|
||||
* tests/unit/brainy/update-reserved-metadata-remap.test.ts (which now runs under
|
||||
* `reservedFieldPolicy: 'remap'`). This file pins the POLICY SELECTION and the
|
||||
* throw/warn behaviors.
|
||||
*
|
||||
* Compile-time callers can't write these shapes at all (see
|
||||
* tests/unit/types/reserved-metadata-keys.test-d.ts); the `as object` widenings
|
||||
* below simulate untyped callers.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { Brainy } from '../../../src/index.js'
|
||||
import { NounType, VerbType } from '../../../src/types/graphTypes.js'
|
||||
import { createTestConfig } from '../../helpers/test-factory.js'
|
||||
import { prodLog } from '../../../src/utils/logger.js'
|
||||
|
||||
describe('reservedFieldPolicy', () => {
|
||||
describe("default policy is 'throw'", () => {
|
||||
let brain: Brainy
|
||||
|
||||
beforeEach(async () => {
|
||||
// No reservedFieldPolicy override → resolves to 'throw'.
|
||||
brain = new Brainy(createTestConfig())
|
||||
await brain.init()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await brain.close()
|
||||
})
|
||||
|
||||
it('add() throws naming the offending key and the correct write path', async () => {
|
||||
await expect(
|
||||
brain.add({
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'x',
|
||||
metadata: { confidence: 0.8 } as object
|
||||
})
|
||||
).rejects.toThrow(/metadata\.confidence is a reserved field/)
|
||||
|
||||
// The error names the right param and the reserved list for discoverability.
|
||||
await expect(
|
||||
brain.add({
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'x',
|
||||
metadata: { confidence: 0.8 } as object
|
||||
})
|
||||
).rejects.toThrow(/'confidence' param.*RESERVED_ENTITY_FIELDS/s)
|
||||
})
|
||||
|
||||
it('add() lists EVERY offending key when several are present', async () => {
|
||||
const err = await brain
|
||||
.add({
|
||||
type: NounType.Person,
|
||||
data: 'multi',
|
||||
metadata: { confidence: 0.5, weight: 0.6, subtype: 'employee' } as object
|
||||
})
|
||||
.catch((e) => e as Error)
|
||||
expect(err).toBeInstanceOf(Error)
|
||||
expect(err.message).toMatch(/confidence/)
|
||||
expect(err.message).toMatch(/weight/)
|
||||
expect(err.message).toMatch(/subtype/)
|
||||
})
|
||||
|
||||
it('update() throws on a reserved key in the patch', async () => {
|
||||
const id = await brain.add({ type: NounType.Concept, subtype: 'general', data: 'y' })
|
||||
await expect(
|
||||
brain.update({ id, metadata: { confidence: 0.3 } as object })
|
||||
).rejects.toThrow(/metadata\.confidence is a reserved field/)
|
||||
})
|
||||
|
||||
it('relate() throws on a reserved key in the bag', async () => {
|
||||
const a = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'A' })
|
||||
const b = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'B' })
|
||||
await expect(
|
||||
brain.relate({
|
||||
from: a,
|
||||
to: b,
|
||||
type: VerbType.RelatedTo,
|
||||
subtype: 'colleague',
|
||||
metadata: { confidence: 0.4 } as object
|
||||
})
|
||||
).rejects.toThrow(/metadata\.confidence is a reserved field.*RESERVED_RELATION_FIELDS/s)
|
||||
})
|
||||
|
||||
it('updateRelation() throws on a reserved key in the patch', async () => {
|
||||
const a = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'A' })
|
||||
const b = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'B' })
|
||||
const relId = await brain.relate({
|
||||
from: a,
|
||||
to: b,
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'direct'
|
||||
})
|
||||
await expect(
|
||||
brain.updateRelation({ id: relId, metadata: { weight: 0.2 } as object })
|
||||
).rejects.toThrow(/metadata\.weight is a reserved field/)
|
||||
})
|
||||
|
||||
it('transact() add op throws on a reserved key in the bag', async () => {
|
||||
await expect(
|
||||
brain.transact([
|
||||
{
|
||||
op: 'add',
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'tx',
|
||||
metadata: { confidence: 0.7 } as object
|
||||
}
|
||||
])
|
||||
).rejects.toThrow(/metadata\.confidence is a reserved field/)
|
||||
})
|
||||
|
||||
it('a custom (non-reserved) key in the bag does NOT throw', async () => {
|
||||
const id = await brain.add({
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'ok',
|
||||
metadata: { status: 'draft', rating: 4 }
|
||||
})
|
||||
const entity = await brain.get(id)
|
||||
expect(entity?.metadata).toEqual({ status: 'draft', rating: 4 })
|
||||
})
|
||||
})
|
||||
|
||||
describe("'remap' policy remaps silently (no warning)", () => {
|
||||
let brain: Brainy
|
||||
let warnSpy: ReturnType<typeof vi.spyOn>
|
||||
|
||||
beforeEach(async () => {
|
||||
warnSpy = vi.spyOn(prodLog, 'warn').mockImplementation(() => {})
|
||||
brain = new Brainy(createTestConfig({ reservedFieldPolicy: 'remap' }))
|
||||
await brain.init()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await brain.close()
|
||||
warnSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('lifts user-mutable reserved fields to top-level without warning', async () => {
|
||||
const id = await brain.add({
|
||||
type: NounType.Person,
|
||||
data: 'remap lift',
|
||||
metadata: { confidence: 0.8, weight: 0.6, subtype: 'employee', dept: 'eng' } as object
|
||||
})
|
||||
const entity = await brain.get(id)
|
||||
expect(entity?.confidence).toBe(0.8)
|
||||
expect(entity?.weight).toBe(0.6)
|
||||
expect(entity?.subtype).toBe('employee')
|
||||
expect(entity?.metadata).toEqual({ dept: 'eng' })
|
||||
// 'remap' is silent about reserved fields (unrelated storage logs may fire,
|
||||
// so assert specifically that no reserved-field warning was emitted).
|
||||
const reservedWarned = warnSpy.mock.calls.some((c) =>
|
||||
String(c[0]).includes('reserved field')
|
||||
)
|
||||
expect(reservedWarned).toBe(false)
|
||||
})
|
||||
|
||||
it('preserves _originalId on natural-key ids through the remap path', async () => {
|
||||
// A speculative view applies the same normalization and maps a natural-key
|
||||
// id to a stable UUID, preserving the caller's original string.
|
||||
const base = await brain.now()
|
||||
const speculative = await base.with([
|
||||
{
|
||||
op: 'add',
|
||||
id: 'remap-spec-entity',
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'spec',
|
||||
metadata: { confidence: 0.65, custom: 'spec' } as object
|
||||
}
|
||||
])
|
||||
const entity = await speculative.get('remap-spec-entity')
|
||||
expect(entity?.confidence).toBe(0.65)
|
||||
expect(entity?.metadata).toEqual({ custom: 'spec', _originalId: 'remap-spec-entity' })
|
||||
await speculative.release()
|
||||
await base.release()
|
||||
})
|
||||
})
|
||||
|
||||
describe("'warn' policy remaps AND warns once per key", () => {
|
||||
let brain: Brainy
|
||||
let warnSpy: ReturnType<typeof vi.spyOn>
|
||||
|
||||
beforeEach(async () => {
|
||||
warnSpy = vi.spyOn(prodLog, 'warn').mockImplementation(() => {})
|
||||
brain = new Brainy(createTestConfig({ reservedFieldPolicy: 'warn' }))
|
||||
await brain.init()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await brain.close()
|
||||
warnSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('remaps the value (same as remap) and emits a warning naming the field', async () => {
|
||||
// Use a method+field combo unique to this test so the per-process one-shot
|
||||
// registry has not already consumed it.
|
||||
const id = await brain.add({
|
||||
type: NounType.Person,
|
||||
data: 'warn lift',
|
||||
// weight is user-mutable → remapped; this is the only 'warn'-policy
|
||||
// add({ weight }) in the suite, so the one-shot warning fires here.
|
||||
metadata: { weight: 0.42, dept: 'eng' } as object
|
||||
})
|
||||
const entity = await brain.get(id)
|
||||
// Value is honored (remap still happens under 'warn').
|
||||
expect(entity?.weight).toBe(0.42)
|
||||
expect(entity?.metadata).toEqual({ dept: 'eng' })
|
||||
// And a warning was emitted naming the reserved field.
|
||||
expect(warnSpy).toHaveBeenCalled()
|
||||
const warned = warnSpy.mock.calls.some((c) =>
|
||||
String(c[0]).includes("'weight'")
|
||||
)
|
||||
expect(warned).toBe(true)
|
||||
})
|
||||
|
||||
it('warns for system-managed keys too (closes the historical gap)', async () => {
|
||||
// Pre-8.0 only system-managed fields warned; 'warn' warns for every key.
|
||||
// 'createdBy' (system-managed on update) is unique to this test.
|
||||
const id = await brain.add({ type: NounType.Concept, subtype: 'general', data: 'sys' })
|
||||
warnSpy.mockClear()
|
||||
await brain.update({ id, metadata: { createdBy: 'nope', keep: 'me' } as object })
|
||||
const entity = await brain.get(id)
|
||||
// System-managed key dropped; custom field merged.
|
||||
expect((entity?.metadata as Record<string, unknown>)?.createdBy).toBeUndefined()
|
||||
expect((entity?.metadata as Record<string, unknown>)?.keep).toBe('me')
|
||||
// A warning was emitted for the dropped system-managed key.
|
||||
const warned = warnSpy.mock.calls.some((c) =>
|
||||
String(c[0]).includes("'createdBy'")
|
||||
)
|
||||
expect(warned).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,403 +0,0 @@
|
|||
/**
|
||||
* @module tests/unit/brainy/update-reserved-metadata-remap
|
||||
* @description Regression tests for the reserved-field metadata-bag trap,
|
||||
* ported from the 7.x fix and extended to the full 8.0 contract.
|
||||
*
|
||||
* History: `add({metadata: {confidence}})` lifted reserved fields to their
|
||||
* canonical top-level location, but `update({metadata: {confidence}})`
|
||||
* silently dropped the same shape — the patch value survived the merge and
|
||||
* was then clobbered by the preserve-existing spread. A production
|
||||
* consumer's confidence-evolution writes no-oped for weeks before being
|
||||
* caught by reading values back.
|
||||
*
|
||||
* These tests pin the LEGACY REMAP behavior, which in 8.0 is opt-in via
|
||||
* `reservedFieldPolicy: 'remap'` (the default is `'throw'` — see the policy
|
||||
* matrix in tests/unit/brainy/reserved-field-policy.test.ts). The brain in
|
||||
* every test below is constructed with `reservedFieldPolicy: 'remap'` so these
|
||||
* deep correctness assertions about the remap path stay exercised.
|
||||
*
|
||||
* Remap contract under test (every write path, entities AND relationships):
|
||||
* - user-mutable reserved fields (`confidence`, `weight`, `subtype` — plus
|
||||
* `service`/`createdBy` at add()/relate() time) remap from the metadata
|
||||
* bag to their dedicated top-level param, with top-level winning when both
|
||||
* are present;
|
||||
* - system-managed reserved fields (`createdAt`, `_rev`, `noun`/`verb`,
|
||||
* `data`, …) are dropped from the bag;
|
||||
* - the same normalization applies to `transact()` operations and `with()`
|
||||
* speculative views;
|
||||
* - reads NEVER echo a reserved field inside `metadata`.
|
||||
*
|
||||
* TypeScript callers can't write these shapes at all (compile-time guard on
|
||||
* the metadata param types — see tests/unit/types/reserved-metadata-keys.test-d.ts);
|
||||
* these tests simulate untyped (JavaScript) callers, hence the `as object`
|
||||
* widenings on the metadata literals.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||
import { Brainy } from '../../../src/index.js'
|
||||
import { NounType, VerbType } from '../../../src/types/graphTypes.js'
|
||||
import { createTestConfig } from '../../helpers/test-factory.js'
|
||||
|
||||
describe('reserved-field metadata remap (8.0 legacy remap path)', () => {
|
||||
let brain: Brainy
|
||||
|
||||
beforeEach(async () => {
|
||||
// The remap path is opt-in in 8.0 (default policy is 'throw').
|
||||
brain = new Brainy(createTestConfig({ reservedFieldPolicy: 'remap' }))
|
||||
await brain.init()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await brain.close()
|
||||
})
|
||||
|
||||
describe('update() — the ported 7.x regression', () => {
|
||||
it('remaps metadata.confidence to the top-level field (the production repro)', async () => {
|
||||
const id = await brain.add({
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'x',
|
||||
metadata: { confidence: 0.8 } as object
|
||||
})
|
||||
|
||||
// Top-level write works (always did)
|
||||
await brain.update({ id, confidence: 0.42 })
|
||||
let entity = await brain.get(id)
|
||||
expect(entity?.confidence).toBe(0.42)
|
||||
|
||||
// Metadata-patch write — silently dropped pre-fix, remapped now
|
||||
await brain.update({ id, metadata: { confidence: 0.33 } as object })
|
||||
entity = await brain.get(id)
|
||||
expect(entity?.confidence).toBe(0.33)
|
||||
// The reserved key must not linger inside the metadata bag
|
||||
expect((entity?.metadata as Record<string, unknown>)?.confidence).toBeUndefined()
|
||||
})
|
||||
|
||||
it('remaps metadata.weight and metadata.subtype the same way', async () => {
|
||||
const id = await brain.add({
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'y',
|
||||
metadata: {}
|
||||
})
|
||||
|
||||
await brain.update({ id, metadata: { weight: 0.7, subtype: 'specialized' } as object })
|
||||
const entity = await brain.get(id)
|
||||
expect(entity?.weight).toBe(0.7)
|
||||
expect(entity?.subtype).toBe('specialized')
|
||||
expect((entity?.metadata as Record<string, unknown>)?.weight).toBeUndefined()
|
||||
expect((entity?.metadata as Record<string, unknown>)?.subtype).toBeUndefined()
|
||||
})
|
||||
|
||||
it('top-level param wins when both top-level and metadata-patch carry the field', async () => {
|
||||
const id = await brain.add({
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'z',
|
||||
metadata: { confidence: 0.5 } as object
|
||||
})
|
||||
|
||||
await brain.update({ id, confidence: 0.9, metadata: { confidence: 0.1 } as object })
|
||||
const entity = await brain.get(id)
|
||||
expect(entity?.confidence).toBe(0.9)
|
||||
})
|
||||
|
||||
it('drops system-managed fields from patches without corrupting the entity', async () => {
|
||||
const id = await brain.add({
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'w',
|
||||
metadata: { keep: 'me' }
|
||||
})
|
||||
const before = await brain.get(id)
|
||||
|
||||
await brain.update({
|
||||
id,
|
||||
metadata: { createdAt: 1, _rev: 999, noun: 'organization', other: 'applied' } as object
|
||||
})
|
||||
const after = await brain.get(id)
|
||||
|
||||
expect(after?.createdAt).toBe(before?.createdAt) // immutable
|
||||
expect(after?.type).toBe('concept') // noun patch ignored
|
||||
expect(after?._rev).toBe((before?._rev ?? 1) + 1) // _rev patch ignored; normal bump applied
|
||||
expect((after?.metadata as Record<string, unknown>)?.other).toBe('applied') // custom fields still merge
|
||||
expect((after?.metadata as Record<string, unknown>)?.keep).toBe('me')
|
||||
expect((after?.metadata as Record<string, unknown>)?._rev).toBeUndefined()
|
||||
expect((after?.metadata as Record<string, unknown>)?.createdAt).toBeUndefined()
|
||||
expect((after?.metadata as Record<string, unknown>)?.noun).toBeUndefined()
|
||||
})
|
||||
|
||||
it('custom (non-reserved) metadata patches are unaffected by the remap', async () => {
|
||||
const id = await brain.add({
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'v',
|
||||
metadata: { status: 'draft' }
|
||||
})
|
||||
|
||||
await brain.update({ id, metadata: { status: 'reviewed', rating: 4.5 } })
|
||||
const entity = await brain.get(id)
|
||||
expect((entity?.metadata as Record<string, unknown>)?.status).toBe('reviewed')
|
||||
expect((entity?.metadata as Record<string, unknown>)?.rating).toBe(4.5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('add() — explicit lift, identical contract', () => {
|
||||
it('lifts confidence/weight/subtype out of the bag to top level', async () => {
|
||||
const id = await brain.add({
|
||||
type: NounType.Person,
|
||||
data: 'lift check',
|
||||
metadata: { confidence: 0.8, weight: 0.6, subtype: 'employee', dept: 'eng' } as object
|
||||
})
|
||||
|
||||
const entity = await brain.get(id)
|
||||
expect(entity?.confidence).toBe(0.8)
|
||||
expect(entity?.weight).toBe(0.6)
|
||||
expect(entity?.subtype).toBe('employee')
|
||||
expect(entity?.metadata).toEqual({ dept: 'eng' })
|
||||
})
|
||||
|
||||
it('lifts service (settable at add time) and lets the top-level param win', async () => {
|
||||
const lifted = await brain.add({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'service lift',
|
||||
metadata: { service: 'orders' } as object
|
||||
})
|
||||
expect((await brain.get(lifted))?.service).toBe('orders')
|
||||
|
||||
const topLevelWins = await brain.add({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'service precedence',
|
||||
service: 'billing',
|
||||
metadata: { service: 'orders' } as object
|
||||
})
|
||||
const entity = await brain.get(topLevelWins)
|
||||
expect(entity?.service).toBe('billing')
|
||||
expect((entity?.metadata as Record<string, unknown>)?.service).toBeUndefined()
|
||||
})
|
||||
|
||||
it('a remapped subtype satisfies subtype enforcement like a top-level one', async () => {
|
||||
brain.requireSubtype(NounType.Document)
|
||||
|
||||
// Top-level missing, but the bag carries it — must not throw.
|
||||
const id = await brain.add({
|
||||
type: NounType.Document,
|
||||
data: 'enforcement via remap',
|
||||
metadata: { subtype: 'invoice' } as object
|
||||
})
|
||||
expect((await brain.get(id))?.subtype).toBe('invoice')
|
||||
|
||||
// Neither place carries it — must throw.
|
||||
await expect(
|
||||
brain.add({ type: NounType.Document, data: 'no subtype anywhere' })
|
||||
).rejects.toThrow(/subtype/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('transact() — same remap on add and update ops', () => {
|
||||
it('normalizes reserved fields in transact add + update ops', async () => {
|
||||
const db1 = await brain.transact([
|
||||
{
|
||||
op: 'add',
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'tx',
|
||||
metadata: { confidence: 0.7, custom: 'a' } as object
|
||||
}
|
||||
])
|
||||
const id = db1.receipt!.ids[0]
|
||||
|
||||
let entity = await brain.get(id)
|
||||
expect(entity?.confidence).toBe(0.7)
|
||||
expect(entity?.metadata).toEqual({ custom: 'a' })
|
||||
|
||||
await brain.transact([
|
||||
{ op: 'update', id, metadata: { confidence: 0.25, custom: 'b' } as object }
|
||||
])
|
||||
entity = await brain.get(id)
|
||||
expect(entity?.confidence).toBe(0.25)
|
||||
expect(entity?.metadata).toEqual({ custom: 'b' })
|
||||
expect((entity?.metadata as Record<string, unknown>)?.confidence).toBeUndefined()
|
||||
})
|
||||
|
||||
it('historical asOf() reads surface reserved fields ONLY top-level', async () => {
|
||||
const db1 = await brain.transact([
|
||||
{
|
||||
op: 'add',
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'historical',
|
||||
metadata: { confidence: 0.9, custom: 'past' } as object
|
||||
}
|
||||
])
|
||||
const id = db1.receipt!.ids[0]
|
||||
|
||||
// Move the world forward so generation db1 is historical.
|
||||
await brain.transact([{ op: 'update', id, confidence: 0.1, metadata: { custom: 'now' } }])
|
||||
|
||||
const past = await brain.asOf(db1.generation)
|
||||
const historical = await past.get(id)
|
||||
expect(historical?.confidence).toBe(0.9)
|
||||
expect(historical?.metadata).toEqual({ custom: 'past' })
|
||||
await past.release()
|
||||
})
|
||||
|
||||
it('with() speculative views apply the same normalization', async () => {
|
||||
const base = await brain.now()
|
||||
const speculative = await base.with([
|
||||
{
|
||||
op: 'add',
|
||||
id: 'spec-entity',
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'spec',
|
||||
metadata: { confidence: 0.65, custom: 'spec' } as object
|
||||
}
|
||||
])
|
||||
|
||||
const entity = await speculative.get('spec-entity')
|
||||
expect(entity?.confidence).toBe(0.65)
|
||||
// 8.0 id normalization: a natural-key id is mapped to a stable UUID and
|
||||
// the caller's original string is preserved under _originalId — surfaced
|
||||
// here exactly as the durable transact()/add() paths do.
|
||||
expect(entity?.metadata).toEqual({ custom: 'spec', _originalId: 'spec-entity' })
|
||||
await speculative.release()
|
||||
await base.release()
|
||||
})
|
||||
})
|
||||
|
||||
describe('read paths never echo reserved fields inside metadata', () => {
|
||||
it('find() (storage pagination path) returns custom-only metadata with reserved fields top-level', async () => {
|
||||
const id = await brain.add({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'pagination echo check',
|
||||
confidence: 0.8,
|
||||
weight: 0.6,
|
||||
metadata: { dept: 'eng' }
|
||||
})
|
||||
|
||||
// No query/filter → served by the direct storage pagination path
|
||||
// (getNounsWithPagination), which historically echoed the full flat
|
||||
// record (noun/subtype/createdAt/… inside metadata).
|
||||
const results = await brain.find({ limit: 50 })
|
||||
const result = results.find((r) => r.id === id)
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.entity.metadata).toEqual({ dept: 'eng' })
|
||||
expect(result?.entity.type).toBe(NounType.Person)
|
||||
expect(result?.entity.subtype).toBe('employee')
|
||||
expect(result?.entity.confidence).toBe(0.8)
|
||||
expect(result?.entity.weight).toBe(0.6)
|
||||
expect(typeof result?.entity.createdAt).toBe('number')
|
||||
expect(result?.entity._rev).toBe(1)
|
||||
})
|
||||
|
||||
it('related() by target surfaces reserved fields top-level, custom-only metadata', async () => {
|
||||
const a = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'src' })
|
||||
const b = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'tgt' })
|
||||
const relId = await brain.relate({
|
||||
from: a,
|
||||
to: b,
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'direct',
|
||||
confidence: 0.9,
|
||||
weight: 0.5,
|
||||
service: 'orders',
|
||||
metadata: { note: 'target path' }
|
||||
})
|
||||
|
||||
const relations = await brain.related({ to: b })
|
||||
const rel = relations.find((r) => r.id === relId)
|
||||
expect(rel).toBeDefined()
|
||||
expect(rel?.metadata).toEqual({ note: 'target path' })
|
||||
expect(rel?.subtype).toBe('direct')
|
||||
expect(rel?.confidence).toBe(0.9)
|
||||
expect(rel?.weight).toBe(0.5)
|
||||
expect(rel?.service).toBe('orders')
|
||||
expect(typeof rel?.createdAt).toBe('number')
|
||||
})
|
||||
})
|
||||
|
||||
describe('relationships — relate() / updateRelation() mirror', () => {
|
||||
let a: string
|
||||
let b: string
|
||||
|
||||
beforeEach(async () => {
|
||||
a = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'A' })
|
||||
b = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'B' })
|
||||
})
|
||||
|
||||
it('relate() persists the top-level confidence and service params', async () => {
|
||||
const relId = await brain.relate({
|
||||
from: a,
|
||||
to: b,
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'direct',
|
||||
confidence: 0.77,
|
||||
service: 'orders'
|
||||
})
|
||||
|
||||
const relations = await brain.related({ from: a })
|
||||
const rel = relations.find((r) => r.id === relId)
|
||||
expect(rel?.confidence).toBe(0.77)
|
||||
expect(rel?.service).toBe('orders')
|
||||
})
|
||||
|
||||
it('relate() remaps reserved fields out of the metadata bag', async () => {
|
||||
const relId = await brain.relate({
|
||||
from: a,
|
||||
to: b,
|
||||
type: VerbType.RelatedTo,
|
||||
subtype: 'colleague',
|
||||
metadata: { confidence: 0.4, weight: 0.3, role: 'peer' } as object
|
||||
})
|
||||
|
||||
const relations = await brain.related({ from: a })
|
||||
const rel = relations.find((r) => r.id === relId)
|
||||
expect(rel?.confidence).toBe(0.4)
|
||||
expect(rel?.weight).toBe(0.3)
|
||||
expect(rel?.metadata).toEqual({ role: 'peer' })
|
||||
})
|
||||
|
||||
it('relation.metadata never echoes the verb type key', async () => {
|
||||
const relId = await brain.relate({
|
||||
from: a,
|
||||
to: b,
|
||||
type: VerbType.RelatedTo,
|
||||
subtype: 'colleague',
|
||||
metadata: { note: 'no echo' }
|
||||
})
|
||||
|
||||
const relations = await brain.related({ from: a })
|
||||
const rel = relations.find((r) => r.id === relId)
|
||||
expect(rel?.type).toBe(VerbType.RelatedTo)
|
||||
expect((rel?.metadata as Record<string, unknown>)?.verb).toBeUndefined()
|
||||
expect(rel?.metadata).toEqual({ note: 'no echo' })
|
||||
})
|
||||
|
||||
it('updateRelation() remaps the user-mutable trio and preserves service', async () => {
|
||||
const relId = await brain.relate({
|
||||
from: a,
|
||||
to: b,
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'direct',
|
||||
service: 'orders',
|
||||
metadata: { keep: 'me' }
|
||||
})
|
||||
|
||||
await brain.updateRelation({
|
||||
id: relId,
|
||||
metadata: { confidence: 0.55, subtype: 'dotted-line', extra: 'applied' } as object
|
||||
})
|
||||
|
||||
const relations = await brain.related({ from: a })
|
||||
const rel = relations.find((r) => r.id === relId)
|
||||
expect(rel?.confidence).toBe(0.55)
|
||||
expect(rel?.subtype).toBe('dotted-line')
|
||||
expect(rel?.service).toBe('orders') // fixed at relate() time, never erased by updates
|
||||
expect(rel?.metadata).toEqual({ keep: 'me', extra: 'applied' })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -198,60 +198,47 @@ describe('visibility (8.0 reserved field)', () => {
|
|||
expect(entity?.visibility).toBeUndefined()
|
||||
})
|
||||
|
||||
it('an untyped caller passing visibility inside metadata is normalized under reservedFieldPolicy:"remap" (lifted to top-level)', async () => {
|
||||
// Simulate a JavaScript caller smuggling the reserved key past the compile-time guard.
|
||||
// The legacy remap behavior is now opt-in (8.0 default is 'throw').
|
||||
const remapBrain = new Brainy(createTestConfig({ reservedFieldPolicy: 'remap' }))
|
||||
await remapBrain.init()
|
||||
try {
|
||||
const id = await remapBrain.add({
|
||||
type: NounType.Concept,
|
||||
data: 'y',
|
||||
metadata: { visibility: 'internal', tag: 't' } as object
|
||||
})
|
||||
const entity = await remapBrain.get(id)
|
||||
// Lifted to the top-level field…
|
||||
expect(entity?.visibility).toBe('internal')
|
||||
// …and stripped from the metadata bag.
|
||||
expect((entity?.metadata as Record<string, unknown>)?.visibility).toBeUndefined()
|
||||
expect((entity?.metadata as Record<string, unknown>)?.tag).toBe('t')
|
||||
// It is excluded from the default count, exactly like a top-level internal write.
|
||||
expect(await remapBrain.getNounCount()).toBe(0)
|
||||
} finally {
|
||||
await remapBrain.close()
|
||||
}
|
||||
it('metadata.visibility is the USER’s field (field-addressing law) — stored verbatim, never lifted to the engine tier', async () => {
|
||||
const id = await brain.add({
|
||||
type: NounType.Concept,
|
||||
data: 'y',
|
||||
metadata: { visibility: 'internal', tag: 't' } as object
|
||||
})
|
||||
const entity = await brain.get(id)
|
||||
// The user's field lives in the bag, verbatim…
|
||||
expect((entity?.metadata as Record<string, unknown>)?.visibility).toBe('internal')
|
||||
expect((entity?.metadata as Record<string, unknown>)?.tag).toBe('t')
|
||||
// …and the ENGINE tier is untouched: absent === public, so the entity
|
||||
// stays visible on default reads (the engine tier is set only via the
|
||||
// dedicated visibility param and reads at system.visibility).
|
||||
expect(entity?.visibility).toBeUndefined()
|
||||
const visible = await brain.find({ type: NounType.Concept, limit: 20 })
|
||||
expect(visible.map((r) => r.id)).toContain(id)
|
||||
})
|
||||
|
||||
it('a "system" value smuggled through metadata is dropped under reservedFieldPolicy:"remap", not honored', async () => {
|
||||
// 'system' is Brainy-only; an untyped caller must not be able to set it.
|
||||
const remapBrain = new Brainy(createTestConfig({ reservedFieldPolicy: 'remap' }))
|
||||
await remapBrain.init()
|
||||
try {
|
||||
const id = await remapBrain.add({
|
||||
type: NounType.Concept,
|
||||
data: 'z',
|
||||
metadata: { visibility: 'system' } as object
|
||||
})
|
||||
const entity = await remapBrain.get(id)
|
||||
// The smuggled 'system' was dropped → entity stays public (counted, visible).
|
||||
expect(entity?.visibility).toBeUndefined()
|
||||
expect(await remapBrain.getNounCount()).toBe(1)
|
||||
const found = await remapBrain.find({ type: NounType.Concept, limit: 10 })
|
||||
expect(found.map((r) => r.id)).toContain(id)
|
||||
} finally {
|
||||
await remapBrain.close()
|
||||
}
|
||||
it('a user field valued "system" cannot smuggle the Brainy-only tier — it is just user data', async () => {
|
||||
const id = await brain.add({
|
||||
type: NounType.Concept,
|
||||
data: 'z',
|
||||
metadata: { visibility: 'system' } as object
|
||||
})
|
||||
const entity = await brain.get(id)
|
||||
// Engine tier unaffected → entity stays public (counted, visible);
|
||||
// the string 'system' is ordinary user data in the bag.
|
||||
expect(entity?.visibility).toBeUndefined()
|
||||
expect((entity?.metadata as Record<string, unknown>)?.visibility).toBe('system')
|
||||
const found = await brain.find({ type: NounType.Concept, limit: 10 })
|
||||
expect(found.map((r) => r.id)).toContain(id)
|
||||
})
|
||||
|
||||
it('an untyped caller passing visibility inside metadata throws under the default policy', async () => {
|
||||
// 8.0 default: no silent remap — a reserved key in the bag is a loud error.
|
||||
it('a forged system.visibility key in metadata refuses loudly at the write door', async () => {
|
||||
await expect(
|
||||
brain.add({
|
||||
type: NounType.Concept,
|
||||
data: 'throws',
|
||||
metadata: { visibility: 'internal', tag: 't' } as object
|
||||
metadata: { 'system.visibility': 'internal' } as object
|
||||
})
|
||||
).rejects.toThrow(/visibility.*reserved field/)
|
||||
).rejects.toThrow(/system\./)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Reference in a new issue