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
24bf6cdbc5
32 changed files with 1355 additions and 1905 deletions
127
tests/unit/types/nestedBagRecord.test.ts
Normal file
127
tests/unit/types/nestedBagRecord.test.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
/**
|
||||
* @module tests/unit/types/nestedBagRecord
|
||||
* @description Unit pins for the v2 (nested-bag) stored-record layer — the
|
||||
* storage half of the field-addressing law. The write door accepts ANY user
|
||||
* metadata name; what makes that lossless on disk is the record shape:
|
||||
* engine fields top-level, the user bag NESTED verbatim, discriminated by
|
||||
* the engine-written format stamp (never by names — names are the user's).
|
||||
* These pins hold the builders, the discriminator, and the shape-aware
|
||||
* split that every read path (live, batch, historical) routes through.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import {
|
||||
buildNounMetadataRecord,
|
||||
buildVerbMetadataRecord,
|
||||
splitNounMetadataRecord,
|
||||
splitVerbMetadataRecord,
|
||||
isNestedBagRecord,
|
||||
METADATA_RECORD_FORMAT_KEY,
|
||||
NESTED_BAG_FORMAT
|
||||
} from '../../../src/types/reservedFields.js'
|
||||
|
||||
const COLLIDER_BAG = {
|
||||
confidence: 'user-confidence',
|
||||
weight: 'user-weight',
|
||||
subtype: 'user-subtype',
|
||||
createdAt: 'user-createdAt',
|
||||
service: 'user-service',
|
||||
data: 'user-data',
|
||||
noun: 'user-noun',
|
||||
_rev: 'user-rev',
|
||||
level: 7,
|
||||
plain: 'control'
|
||||
}
|
||||
|
||||
describe('v2 nested-bag stored records — build / discriminate / split', () => {
|
||||
it('build → split round-trips a fully colliding user bag VERBATIM', () => {
|
||||
const record = buildNounMetadataRecord(
|
||||
{ noun: 'document', confidence: 0.25, createdAt: 111, updatedAt: 222, _rev: 1 },
|
||||
{ ...COLLIDER_BAG }
|
||||
)
|
||||
expect(isNestedBagRecord(record)).toBe(true)
|
||||
expect(record[METADATA_RECORD_FORMAT_KEY]).toBe(NESTED_BAG_FORMAT)
|
||||
|
||||
const { reserved, custom } = splitNounMetadataRecord(record)
|
||||
// The engine half is exactly what the engine wrote…
|
||||
expect(reserved.noun).toBe('document')
|
||||
expect(reserved.confidence).toBe(0.25)
|
||||
expect(reserved._rev).toBe(1)
|
||||
// …and the user bag comes back byte-for-byte, colliders included.
|
||||
expect(custom).toEqual(COLLIDER_BAG)
|
||||
})
|
||||
|
||||
it('the verb mirror round-trips an edge collider bag verbatim', () => {
|
||||
const record = buildVerbMetadataRecord(
|
||||
{ verb: 'relatedTo', weight: 1.0, confidence: 0.5, createdAt: 333 },
|
||||
{ verb: 'user-verb', confidence: 'user-c', tag: 't' }
|
||||
)
|
||||
expect(isNestedBagRecord(record)).toBe(true)
|
||||
const { reserved, custom } = splitVerbMetadataRecord(record)
|
||||
expect(reserved.verb).toBe('relatedTo')
|
||||
expect(reserved.confidence).toBe(0.5)
|
||||
expect(custom).toEqual({ verb: 'user-verb', confidence: 'user-c', tag: 't' })
|
||||
})
|
||||
|
||||
it('a LEGACY flat record (no stamp) splits BY NAME — sound because the pre-law door refused colliders', () => {
|
||||
const legacy = {
|
||||
noun: 'document',
|
||||
confidence: 0.75,
|
||||
createdAt: 111,
|
||||
_rev: 2,
|
||||
legacyField: 'legacy-value'
|
||||
}
|
||||
expect(isNestedBagRecord(legacy)).toBe(false)
|
||||
const { reserved, custom } = splitNounMetadataRecord(legacy)
|
||||
expect(reserved.confidence).toBe(0.75)
|
||||
expect(reserved._rev).toBe(2)
|
||||
expect(custom).toEqual({ legacyField: 'legacy-value' })
|
||||
})
|
||||
|
||||
it('the stamp is the discriminator, never the name: a legacy user OBJECT field named `metadata` does not fake a v2 record', () => {
|
||||
// Pre-law, 'metadata' was never a reserved name — a flat record could
|
||||
// legally carry a user object field spelled exactly 'metadata'. Without
|
||||
// the engine-written stamp it must split as legacy, with that object
|
||||
// preserved as an ordinary user field.
|
||||
const legacyWithMetadataField = {
|
||||
noun: 'document',
|
||||
confidence: 0.5,
|
||||
metadata: { nested: 'user-object' }
|
||||
}
|
||||
expect(isNestedBagRecord(legacyWithMetadataField)).toBe(false)
|
||||
const { reserved, custom } = splitNounMetadataRecord(legacyWithMetadataField)
|
||||
expect(reserved.confidence).toBe(0.5)
|
||||
expect(custom).toEqual({ metadata: { nested: 'user-object' } })
|
||||
})
|
||||
|
||||
it('a malformed stamp (right key, wrong value / non-object bag) never discriminates as v2', () => {
|
||||
expect(
|
||||
isNestedBagRecord({ [METADATA_RECORD_FORMAT_KEY]: 999, metadata: {} })
|
||||
).toBe(false)
|
||||
expect(
|
||||
isNestedBagRecord({ [METADATA_RECORD_FORMAT_KEY]: NESTED_BAG_FORMAT, metadata: 'not-a-bag' })
|
||||
).toBe(false)
|
||||
expect(
|
||||
isNestedBagRecord({ [METADATA_RECORD_FORMAT_KEY]: NESTED_BAG_FORMAT, metadata: [1, 2] })
|
||||
).toBe(false)
|
||||
expect(isNestedBagRecord(null)).toBe(false)
|
||||
expect(isNestedBagRecord(undefined)).toBe(false)
|
||||
})
|
||||
|
||||
it('the v2 split never surfaces the stamp or the bag container as fields', () => {
|
||||
const record = buildNounMetadataRecord({ noun: 'document', _rev: 1 }, { a: 1 })
|
||||
const { reserved, custom } = splitNounMetadataRecord(record)
|
||||
expect(METADATA_RECORD_FORMAT_KEY in reserved).toBe(false)
|
||||
expect(METADATA_RECORD_FORMAT_KEY in custom).toBe(false)
|
||||
expect('metadata' in reserved).toBe(false)
|
||||
expect(custom).toEqual({ a: 1 })
|
||||
})
|
||||
|
||||
it('builders copy the bag (no aliasing): later caller mutation cannot reach the record', () => {
|
||||
const bag: Record<string, unknown> = { a: 1 }
|
||||
const record = buildNounMetadataRecord({ noun: 'document' }, bag)
|
||||
bag.a = 999
|
||||
bag.b = 'sneaky'
|
||||
expect((record.metadata as Record<string, unknown>).a).toBe(1)
|
||||
expect('b' in (record.metadata as Record<string, unknown>)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,265 +0,0 @@
|
|||
/**
|
||||
* @module tests/unit/types/reserved-metadata-keys.test-d
|
||||
* @description Compile-time tests for the reserved-field contract (layer 1 of
|
||||
* three — see src/types/reservedFields.ts): a literal reserved key inside any
|
||||
* `metadata` param is a TypeScript error, while the generic `T` ergonomics
|
||||
* stay intact (typed bags, untyped brains, index-signature shapes, and the
|
||||
* documented exemption for consumers who explicitly declare a reserved key in
|
||||
* their own metadata type).
|
||||
*
|
||||
* Runs under vitest typecheck mode (`test.typecheck` in
|
||||
* tests/configs/vitest.unit.config.ts) — these assertions are validated by
|
||||
* `tsc`, never executed. The runtime half of the contract (the write-path
|
||||
* remap for untyped callers) is pinned by
|
||||
* tests/unit/brainy/update-reserved-metadata-remap.test.ts.
|
||||
*/
|
||||
|
||||
import { describe, it, assertType } from 'vitest'
|
||||
import type {
|
||||
AddParams,
|
||||
UpdateParams,
|
||||
RelateParams,
|
||||
UpdateRelationParams,
|
||||
TxOperation
|
||||
} from '../../../src/index.js'
|
||||
import { NounType, VerbType } from '../../../src/types/graphTypes.js'
|
||||
|
||||
describe('reserved entity keys in metadata are compile errors', () => {
|
||||
it('AddParams (untyped brain) rejects every reserved key but stays open for custom fields', () => {
|
||||
// Custom fields of any shape remain legal — exactly the pre-8.0 latitude.
|
||||
assertType<AddParams>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
metadata: { dept: 'eng', level: 3, tags: ['a', 'b'], nested: { ok: true } }
|
||||
})
|
||||
|
||||
assertType<AddParams>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
// @ts-expect-error — 'noun' is reserved (the entity type travels via the top-level 'type' param)
|
||||
metadata: { noun: 'organization' }
|
||||
})
|
||||
assertType<AddParams>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
// @ts-expect-error — 'subtype' is reserved (use the top-level 'subtype' param)
|
||||
metadata: { subtype: 'contractor' }
|
||||
})
|
||||
assertType<AddParams>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
// @ts-expect-error — 'createdAt' is reserved (system-managed)
|
||||
metadata: { createdAt: Date.now() }
|
||||
})
|
||||
assertType<AddParams>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
// @ts-expect-error — 'updatedAt' is reserved (system-managed)
|
||||
metadata: { updatedAt: Date.now() }
|
||||
})
|
||||
assertType<AddParams>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
// @ts-expect-error — 'confidence' is reserved (use the top-level 'confidence' param)
|
||||
metadata: { confidence: 0.8 }
|
||||
})
|
||||
assertType<AddParams>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
// @ts-expect-error — 'weight' is reserved (use the top-level 'weight' param)
|
||||
metadata: { weight: 0.5 }
|
||||
})
|
||||
assertType<AddParams>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
// @ts-expect-error — 'service' is reserved (use the top-level 'service' param)
|
||||
metadata: { service: 'orders' }
|
||||
})
|
||||
assertType<AddParams>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
// @ts-expect-error — 'data' is reserved (use the top-level 'data' param)
|
||||
metadata: { data: 'content' }
|
||||
})
|
||||
assertType<AddParams>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
// @ts-expect-error — 'createdBy' is reserved (use the top-level 'createdBy' param)
|
||||
metadata: { createdBy: { augmentation: 'importer', version: '1.0' } }
|
||||
})
|
||||
assertType<AddParams>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
// @ts-expect-error — '_rev' is reserved (system-managed revision counter)
|
||||
metadata: { _rev: 7 }
|
||||
})
|
||||
})
|
||||
|
||||
it('AddParams<T> (typed brain) rejects reserved keys alongside the declared shape', () => {
|
||||
interface EmployeeMeta {
|
||||
dept: string
|
||||
level: number
|
||||
}
|
||||
|
||||
assertType<AddParams<EmployeeMeta>>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
metadata: { dept: 'eng', level: 3 }
|
||||
})
|
||||
|
||||
assertType<AddParams<EmployeeMeta>>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
// @ts-expect-error — 'confidence' is reserved even when T declares other fields
|
||||
metadata: { dept: 'eng', level: 3, confidence: 0.8 }
|
||||
})
|
||||
})
|
||||
|
||||
it('documented exemptions: T-declared reserved keys and index-signature shapes stay assignable', () => {
|
||||
// A consumer who *explicitly* types a reserved key into their metadata
|
||||
// shape keeps a working (if unwise) type — the guard exempts keyof T.
|
||||
interface LegacyMeta {
|
||||
confidence: number
|
||||
note: string
|
||||
}
|
||||
assertType<AddParams<LegacyMeta>>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
metadata: { confidence: 0.8, note: 'declared by the consumer type' }
|
||||
})
|
||||
|
||||
// Index-signature metadata types (keyof T = string) remain fully open.
|
||||
assertType<AddParams<Record<string, unknown>>>({
|
||||
type: NounType.Person,
|
||||
subtype: 'employee',
|
||||
data: 'x',
|
||||
metadata: { anything: 'goes', confidence: 0.8 }
|
||||
})
|
||||
})
|
||||
|
||||
it('UpdateParams patch rejects reserved keys but accepts partial custom patches', () => {
|
||||
interface EmployeeMeta {
|
||||
dept: string
|
||||
level: number
|
||||
}
|
||||
|
||||
// Partial patch of the declared shape is legal.
|
||||
assertType<UpdateParams<EmployeeMeta>>({ id: 'e1', metadata: { dept: 'sales' } })
|
||||
// Untyped patch with custom fields is legal.
|
||||
assertType<UpdateParams>({ id: 'e1', metadata: { status: 'reviewed', rating: 4.5 } })
|
||||
|
||||
// @ts-expect-error — 'confidence' is reserved (use the top-level 'confidence' param)
|
||||
assertType<UpdateParams>({ id: 'e1', metadata: { confidence: 0.33 } })
|
||||
// @ts-expect-error — 'subtype' is reserved (use the top-level 'subtype' param)
|
||||
assertType<UpdateParams>({ id: 'e1', metadata: { subtype: 'specialized' } })
|
||||
// @ts-expect-error — '_rev' is reserved (pass 'ifRev' for optimistic concurrency)
|
||||
assertType<UpdateParams>({ id: 'e1', metadata: { _rev: 3 } })
|
||||
// @ts-expect-error — 'confidence' is reserved even when T declares other fields
|
||||
assertType<UpdateParams<EmployeeMeta>>({ id: 'e1', metadata: { confidence: 0.1 } })
|
||||
})
|
||||
})
|
||||
|
||||
describe('reserved relationship keys in metadata are compile errors', () => {
|
||||
it('RelateParams rejects reserved keys but stays open for custom edge fields', () => {
|
||||
assertType<RelateParams>({
|
||||
from: 'a',
|
||||
to: 'b',
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'direct',
|
||||
metadata: { role: 'peer', since: 2024 }
|
||||
})
|
||||
|
||||
assertType<RelateParams>({
|
||||
from: 'a',
|
||||
to: 'b',
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'direct',
|
||||
// @ts-expect-error — 'verb' is reserved (the relationship type travels via the top-level 'type' param)
|
||||
metadata: { verb: 'relatedTo' }
|
||||
})
|
||||
assertType<RelateParams>({
|
||||
from: 'a',
|
||||
to: 'b',
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'direct',
|
||||
// @ts-expect-error — 'confidence' is reserved (use the top-level 'confidence' param)
|
||||
metadata: { confidence: 0.9 }
|
||||
})
|
||||
assertType<RelateParams>({
|
||||
from: 'a',
|
||||
to: 'b',
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'direct',
|
||||
// @ts-expect-error — 'weight' is reserved (use the top-level 'weight' param)
|
||||
metadata: { weight: 0.4 }
|
||||
})
|
||||
assertType<RelateParams>({
|
||||
from: 'a',
|
||||
to: 'b',
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'direct',
|
||||
// @ts-expect-error — 'service' is reserved (use the top-level 'service' param)
|
||||
metadata: { service: 'orders' }
|
||||
})
|
||||
})
|
||||
|
||||
it('UpdateRelationParams patch rejects reserved keys', () => {
|
||||
assertType<UpdateRelationParams>({ id: 'r1', metadata: { note: 'fine' } })
|
||||
|
||||
// @ts-expect-error — 'confidence' is reserved (use the top-level 'confidence' param)
|
||||
assertType<UpdateRelationParams>({ id: 'r1', metadata: { confidence: 0.5 } })
|
||||
// @ts-expect-error — 'subtype' is reserved (use the top-level 'subtype' param)
|
||||
assertType<UpdateRelationParams>({ id: 'r1', metadata: { subtype: 'dotted-line' } })
|
||||
// @ts-expect-error — 'createdAt' is reserved (system-managed)
|
||||
assertType<UpdateRelationParams>({ id: 'r1', metadata: { createdAt: 1 } })
|
||||
})
|
||||
})
|
||||
|
||||
describe('transact() operations inherit the same guard', () => {
|
||||
it('TxOperation add/update/relate metadata rejects reserved keys', () => {
|
||||
assertType<TxOperation>({
|
||||
op: 'add',
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'tx',
|
||||
metadata: { custom: 'a' }
|
||||
})
|
||||
assertType<TxOperation>({
|
||||
op: 'add',
|
||||
type: NounType.Concept,
|
||||
subtype: 'general',
|
||||
data: 'tx',
|
||||
// @ts-expect-error — 'confidence' is reserved on transact add ops too
|
||||
metadata: { confidence: 0.7 }
|
||||
})
|
||||
assertType<TxOperation>({
|
||||
op: 'update',
|
||||
id: 'e1',
|
||||
// @ts-expect-error — 'weight' is reserved on transact update ops too
|
||||
metadata: { weight: 0.2 }
|
||||
})
|
||||
assertType<TxOperation>({
|
||||
op: 'relate',
|
||||
from: 'a',
|
||||
to: 'b',
|
||||
type: VerbType.RelatedTo,
|
||||
subtype: 'colleague',
|
||||
// @ts-expect-error — 'verb' is reserved on transact relate ops too
|
||||
metadata: { verb: 'contains' }
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in a new issue