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
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue