feat(namespace): NO SPECIAL NAMES + storage fidelity — the ruled completion of the field-addressing law
All checks were successful
CI / Node 22 (push) Successful in 12m9s
CI / Node 24 (push) Successful in 12m4s
CI / Bun (latest) (push) Successful in 12m52s

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:
David Snelling 2026-08-03 16:59:13 -07:00
parent 48a6130a50
commit 24bf6cdbc5
32 changed files with 1355 additions and 1905 deletions

View file

@ -20,6 +20,16 @@ import { MigrationRunner, MIGRATIONS } from '../../src/migration/index.js'
import type { Migration } from '../../src/migration/index.js'
import { NounType, VerbType } from '../../src/types/graphTypes.js'
// THE VIEW CONTRACT (field-addressing law): transforms receive engine fields
// top-level and the USER's bag nested under `metadata` — user-field changes
// go inside the bag. These two helpers keep the one-liner migrations tidy.
const bagOf = (m: Record<string, unknown>): Record<string, unknown> =>
m.metadata as Record<string, unknown>
const withBag = (
m: Record<string, unknown>,
patch: Record<string, unknown>
): Record<string, unknown> => ({ ...m, metadata: { ...bagOf(m), ...patch } })
// Helper to temporarily inject migrations into the MIGRATIONS array
function withMigrations(migrations: Migration[], fn: () => Promise<void>): Promise<void> {
const original = MIGRATIONS.splice(0, MIGRATIONS.length)
@ -78,9 +88,11 @@ describe('Migration System', () => {
description: 'Add version field to entities with status',
applies: 'nouns',
transform: (m) => {
// Only transform entities that have our specific 'status' field
if ('status' in m && !('version' in m)) {
return { ...m, version: 1 }
// Only transform entities that have our specific 'status' USER field
// (user fields live in the nested bag — the view contract).
const bag = m.metadata as Record<string, unknown>
if ('status' in bag && !('version' in bag)) {
return { ...m, metadata: { ...bag, version: 1 } }
}
return null
}
@ -94,7 +106,8 @@ describe('Migration System', () => {
// All 3 entities have 'status' metadata
expect(p.affectedEntities).toBeGreaterThanOrEqual(3)
expect(p.sampleChanges.length).toBeGreaterThan(0)
expect(p.sampleChanges[0].after.version).toBe(1)
// Samples carry the VIEW shape: user fields inside `.metadata`.
expect(p.sampleChanges[0].after.metadata.version).toBe(1)
// Verify no data was modified (dry-run)
const entity = await brain.get(id1)
@ -111,9 +124,10 @@ describe('Migration System', () => {
description: 'Rename state to status',
applies: 'nouns',
transform: (m) => {
if ('state' in m) {
const { state, ...rest } = m
return { ...rest, status: state }
const bag = m.metadata as Record<string, unknown>
if ('state' in bag) {
const { state, ...rest } = bag
return { ...m, metadata: { ...rest, status: state } }
}
return null
}
@ -124,11 +138,12 @@ describe('Migration System', () => {
const p = preview as any
expect(p.sampleChanges.length).toBeGreaterThanOrEqual(1)
// Find the sample for our entity (it has the 'state' field)
const sample = p.sampleChanges.find((s: any) => s.before.state === 'draft')
// Find the sample for our entity (it has the 'state' USER field —
// samples carry the VIEW shape, user fields inside `.metadata`)
const sample = p.sampleChanges.find((s: any) => s.before.metadata.state === 'draft')
expect(sample).toBeDefined()
expect(sample.after.status).toBe('draft')
expect(sample.after.state).toBeUndefined()
expect(sample.after.metadata.status).toBe('draft')
expect(sample.after.metadata.state).toBeUndefined()
})
})
})
@ -149,8 +164,8 @@ describe('Migration System', () => {
description: 'Add migrated flag to entities with priority',
applies: 'nouns',
transform: (m) => {
if ('priority' in m && !('migrated' in m)) {
return { ...m, migrated: true }
if ('priority' in bagOf(m) && !('migrated' in bagOf(m))) {
return withBag(m, { migrated: true })
}
return null
}
@ -179,8 +194,8 @@ describe('Migration System', () => {
description: 'Uppercase status field only when present',
applies: 'nouns',
transform: (m) => {
if (typeof m.status === 'string') {
return { ...m, status: (m.status as string).toUpperCase() }
if (typeof bagOf(m).status === 'string') {
return withBag(m, { status: (bagOf(m).status as string).toUpperCase() })
}
return null
}
@ -203,7 +218,7 @@ describe('Migration System', () => {
version: '1.0.0',
description: 'Double count',
applies: 'nouns',
transform: (m) => typeof m.count === 'number' ? { ...m, count: (m.count as number) * 2 } : null
transform: (m) => typeof bagOf(m).count === 'number' ? withBag(m, { count: (bagOf(m).count as number) * 2 }) : null
}
const migration2: Migration = {
@ -211,7 +226,7 @@ describe('Migration System', () => {
version: '1.1.0',
description: 'Add 10 to count',
applies: 'nouns',
transform: (m) => typeof m.count === 'number' ? { ...m, count: (m.count as number) + 10 } : null
transform: (m) => typeof bagOf(m).count === 'number' ? withBag(m, { count: (bagOf(m).count as number) + 10 }) : null
}
await withMigrations([migration1, migration2], async () => {
@ -229,7 +244,7 @@ describe('Migration System', () => {
version: '1.0.0',
description: 'Increment v',
applies: 'nouns',
transform: (m) => typeof m.v === 'number' ? { ...m, v: (m.v as number) + 1 } : null
transform: (m) => typeof bagOf(m).v === 'number' ? withBag(m, { v: (bagOf(m).v as number) + 1 }) : null
}
await withMigrations([migration], async () => {
@ -266,7 +281,7 @@ describe('Migration System', () => {
version: '2.0.0',
description: 'Add y field to entities with x',
applies: 'nouns',
transform: (m) => 'x' in m && !('y' in m) ? { ...m, y: 2 } : null
transform: (m) => 'x' in bagOf(m) && !('y' in bagOf(m)) ? withBag(m, { y: 2 }) : null
}
await withMigrations([migration], async () => {
@ -290,8 +305,8 @@ describe('Migration System', () => {
description: 'Replace original with migrated',
applies: 'nouns',
transform: (m) => {
if (m.original === true) {
return { ...m, original: false, migrated: true }
if (bagOf(m).original === true) {
return withBag(m, { original: false, migrated: true })
}
return null
}
@ -323,7 +338,7 @@ describe('Migration System', () => {
version: '4.0.0',
description: 'Add field',
applies: 'nouns',
transform: (m) => 'q' in m && !('r' in m) ? { ...m, r: 2 } : null
transform: (m) => 'q' in bagOf(m) && !('r' in bagOf(m)) ? withBag(m, { r: 2 }) : null
}
await withMigrations([migration], async () => {
@ -384,7 +399,7 @@ describe('Migration System', () => {
version: '1.0.0',
description: 'Auto migrate test',
applies: 'nouns',
transform: (m) => 'legacy' in m ? { ...m, legacy: false, upgraded: true } : null
transform: (m) => 'legacy' in bagOf(m) ? withBag(m, { legacy: false, upgraded: true }) : null
}
await withMigrations([migration], async () => {
@ -410,7 +425,7 @@ describe('Migration System', () => {
version: '1.0.0',
description: 'Add y to entities with x',
applies: 'nouns',
transform: (m) => 'x' in m ? { ...m, y: true } : null
transform: (m) => 'x' in bagOf(m) ? withBag(m, { y: true }) : null
}
const progressCalls: any[] = []
@ -444,7 +459,7 @@ describe('Migration System', () => {
version: '1.0.0',
description: 'Increment v on entities that have it',
applies: 'nouns',
transform: (m) => typeof m.v === 'number' ? { ...m, v: (m.v as number) + 1 } : null
transform: (m) => typeof bagOf(m).v === 'number' ? withBag(m, { v: (bagOf(m).v as number) + 1 }) : null
}
await withMigrations([migration], async () => {
@ -477,9 +492,10 @@ describe('Migration System', () => {
description: 'Rename strength to intensity',
applies: 'verbs',
transform: (m) => {
if ('strength' in m) {
const { strength, ...rest } = m
return { ...rest, intensity: strength }
const bag = bagOf(m)
if ('strength' in bag) {
const { strength, ...rest } = bag
return { ...m, metadata: { ...rest, intensity: strength } }
}
return null
}
@ -507,7 +523,7 @@ describe('Migration System', () => {
version: '1.0.0',
description: 'Update tag from old to new',
applies: 'both',
transform: (m) => m.tag === 'old' ? { ...m, tag: 'new' } : null
transform: (m) => bagOf(m).tag === 'old' ? withBag(m, { tag: 'new' }) : null
}
await withMigrations([migration], async () => {
@ -577,11 +593,11 @@ describe('Migration System', () => {
description: 'Transform that throws on non-number values',
applies: 'nouns',
transform: (m) => {
if ('value' in m) {
if (typeof m.value !== 'number') {
if ('value' in bagOf(m)) {
if (typeof bagOf(m).value !== 'number') {
throw new Error('value must be a number')
}
return { ...m, value: (m.value as number) * 10 }
return withBag(m, { value: (bagOf(m).value as number) * 10 })
}
return null
}
@ -615,7 +631,7 @@ describe('Migration System', () => {
description: 'Always throws',
applies: 'nouns',
transform: (m) => {
if ('boom' in m) {
if ('boom' in bagOf(m)) {
throw new Error('deliberate failure')
}
return null