feat(8.0)!: delete fork/branch/commit/history/versions — superseded by the Db API

The COW version-control surface (fork, branches, checkout, commit,
getHistory/streamHistory, asOfCommit, brain.versions) is gone, along with
its subsystems: src/versioning/, the COW object store (CommitLog,
CommitObject, RefManager, TreeObject), HistoricalStorageAdapter, and the
TypeAwareHNSWIndex path it kept alive. The Db API (now/transact/asOf/
with/persist/restore) is the one versioning model in 8.0.

Survivors and replacements:
- BlobStorage survives (the VFS stores file content through it), relocated
  to src/storage/blobStorage.ts with binaryDataCodec.ts; its adapter
  interface is now BlobStoreAdapter, slimmed to the consumed surface
  (write/read/has/delete/getMetadata + MIME-aware compression policy).
- brain.migrate() backup branches are replaced by persist-before-migrate:
  MigrateOptions.backupTo persists a hard-link snapshot of the current
  generation before any transform runs; MigrationResult.backupPath reports
  it, and brain.restore(path) brings it back wholesale.
- CLI: cow.ts (fork/branch/checkout/history/migrate) is replaced by
  snapshot.ts — snapshot <path>, restore <path>, history (tx-log),
  generation.
- New public read API: brain.transactionLog({limit}) exposes the reified
  tx-log (generation/timestamp/meta, newest first) that backs the CLI
  history command; TxLogEntry is exported.

Tests: superseded suites deleted; fork/commit blocks excised from shared
suites; BlobStorage tests relocated + reworked against the slimmed store;
migration tests now prove the backupTo snapshot/restore round trip; new
transactionLog coverage in db-mvcc.
This commit is contained in:
David Snelling 2026-06-10 15:22:47 -07:00
parent 431cd64406
commit 8f93add705
64 changed files with 1444 additions and 16313 deletions

View file

@ -4,7 +4,7 @@
* Verifies:
* - MigrationRunner with in-memory storage
* - brain.migrate() public API (dry-run and apply)
* - Backup branch creation with metadata tagging
* - Pre-migration snapshots via `backupTo` (persist-before-migrate + restore)
* - No-op when MIGRATIONS array is empty
* - Warning log when autoMigrate is false
* - Multi-tenant independent migration state
@ -12,6 +12,9 @@
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import * as fs from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { Brainy } from '../../src/brainy.js'
import { MigrationRunner, MIGRATIONS } from '../../src/migration/index.js'
import type { Migration } from '../../src/migration/index.js'
@ -242,13 +245,21 @@ describe('Migration System', () => {
})
})
// ─── Backup branch tests ──────────────────────────────────────
// ─── Pre-migration snapshot tests ─────────────────────────────
describe('Backup branches', () => {
it('should create a backup branch before migrating', async () => {
describe('Pre-migration snapshots (backupTo)', () => {
let backupDir: string
beforeEach(() => {
backupDir = path.join(os.tmpdir(), `brainy-migration-backup-${Date.now()}-${Math.random().toString(36).slice(2)}`)
})
afterEach(() => {
fs.rmSync(backupDir, { recursive: true, force: true })
})
it('should persist a snapshot before migrating and report its path', async () => {
await brain.add({ type: NounType.Concept, data: { name: 'Test' }, metadata: { x: 1 } })
// Need a commit for fork to work
await brain.commit({ message: 'test', author: 'test' })
const migration: Migration = {
id: 'test-backup',
@ -259,45 +270,19 @@ describe('Migration System', () => {
}
await withMigrations([migration], async () => {
const result = await brain.migrate()
const result = await brain.migrate({ backupTo: backupDir })
const r = result as any
expect(r.backupBranch).toBe('pre-migration-2.0.0')
expect(r.backupPath).toBe(backupDir)
expect(r.migrationsApplied).toContain('test-backup')
const branches = await brain.listBranches()
expect(branches).toContain('pre-migration-2.0.0')
// The snapshot is a self-contained store directory
expect(fs.existsSync(backupDir)).toBe(true)
expect(fs.readdirSync(backupDir).length).toBeGreaterThan(0)
})
})
it('should tag backup branch with system:backup metadata', async () => {
await brain.add({ type: NounType.Concept, data: { name: 'Test' }, metadata: { z: 1 } })
await brain.commit({ message: 'test', author: 'test' })
const migration: Migration = {
id: 'test-tag',
version: '4.0.0',
description: 'Tag test',
applies: 'nouns',
transform: (m) => 'z' in m ? { ...m, tagged: true } : null
}
await withMigrations([migration], async () => {
await brain.migrate()
// Verify metadata tag on the backup branch ref
const refManager = (brain as any).storage.refManager
if (refManager) {
const ref = await refManager.getRef('pre-migration-4.0.0')
expect(ref).toBeDefined()
expect(ref?.metadata?.type).toBe('system:backup')
expect(ref?.metadata?.migrationVersion).toBe('4.0.0')
expect(ref?.metadata?.author).toBe('brainy-migration')
}
})
})
it('should keep backup branch as a named restore point', async () => {
it('should restore the pre-migration state wholesale from the snapshot', async () => {
const id = await brain.add({ type: NounType.Concept, data: { name: 'Restore Test' }, metadata: { original: true } })
await brain.commit({ message: 'before migration', author: 'test' })
const migration: Migration = {
id: 'test-restore',
@ -313,29 +298,39 @@ describe('Migration System', () => {
}
await withMigrations([migration], async () => {
const result = await brain.migrate()
const result = await brain.migrate({ backupTo: backupDir })
const r = result as any
expect(r.backupPath).toBe(backupDir)
// Verify migration was applied
const migrated = await brain.get(id)
expect(migrated?.metadata?.migrated).toBe(true)
expect(migrated?.metadata?.original).toBe(false)
// Verify backup branch exists as a named restore point
expect(r.backupBranch).toBe('pre-migration-3.0.0')
const branches = await brain.listBranches()
expect(branches).toContain('pre-migration-3.0.0')
// Restore the snapshot — the store returns to its pre-migration state
await brain.restore(backupDir, { confirm: true })
const restored = await brain.get(id)
expect(restored?.metadata?.original).toBe(true)
expect(restored?.metadata?.migrated).toBeUndefined()
})
})
// Verify the backup ref points to the pre-migration commit
const refManager = (brain as any).storage.refManager
if (refManager) {
const mainRef = await refManager.getRef('main')
const backupRef = await refManager.getRef('pre-migration-3.0.0')
expect(backupRef).toBeDefined()
// Backup was forked before migration, so both share the same commit
// (the pre-migration commit). After migration, main's overlay has new data,
// but the commit pointer is unchanged.
expect(backupRef.commitHash).toBe(mainRef.commitHash)
}
it('should report backupPath null when no backupTo is supplied', async () => {
await brain.add({ type: NounType.Concept, data: { name: 'NoBackup' }, metadata: { q: 1 } })
const migration: Migration = {
id: 'test-no-backup',
version: '4.0.0',
description: 'Add field',
applies: 'nouns',
transform: (m) => 'q' in m && !('r' in m) ? { ...m, r: 2 } : null
}
await withMigrations([migration], async () => {
const result = await brain.migrate()
const r = result as any
expect(r.backupPath).toBeNull()
expect(r.migrationsApplied).toContain('test-no-backup')
})
})
})
@ -520,86 +515,6 @@ describe('Migration System', () => {
})
})
// ─── Multi-branch migration ─────────────────────────────────
describe('Multi-branch migration', () => {
it('should migrate entities on all branches including feature branches', async () => {
// Setup: create entities on main, fork a branch, add entities on the branch
const mainId = await brain.add({
type: NounType.Concept,
data: { name: 'Main Entity' },
metadata: { legacy: true, source: 'main' }
})
await brain.commit({ message: 'initial', author: 'test' })
// Fork a feature branch and add branch-local entity
const fork = await brain.fork('feature-x')
const branchId = await fork.add({
type: NounType.Concept,
data: { name: 'Branch Entity' },
metadata: { legacy: true, source: 'branch' }
})
// Define migration that transforms the 'legacy' field
const migration: Migration = {
id: 'test-multi-branch',
version: '1.0.0',
description: 'Mark legacy entities as upgraded',
applies: 'nouns',
transform: (m) => {
if (m.legacy === true) {
return { ...m, legacy: false, upgraded: true }
}
return null // Already migrated or not applicable
}
}
await withMigrations([migration], async () => {
// Migrate from main — should reach all branches
const result = await brain.migrate()
const r = result as any
expect(r.migrationsApplied).toContain('test-multi-branch')
// Main entity + branch-local entity should both be modified
expect(r.entitiesModified).toBeGreaterThanOrEqual(2)
// Verify main entity was transformed
const mainEntity = await brain.get(mainId)
expect(mainEntity?.metadata?.upgraded).toBe(true)
expect(mainEntity?.metadata?.legacy).toBe(false)
})
await fork.close()
})
it('should skip system:backup branches during multi-branch migration', async () => {
await brain.add({ type: NounType.Concept, data: { name: 'Test' }, metadata: { v: 1 } })
await brain.commit({ message: 'test', author: 'test' })
// Create two migrations — first creates a backup branch, second should skip it
const migration1: Migration = {
id: 'test-skip-backup-1',
version: '5.0.0',
description: 'First migration',
applies: 'nouns',
transform: (m) => typeof m.v === 'number' && !('m1' in m) ? { ...m, m1: true } : null
}
await withMigrations([migration1], async () => {
const result = await brain.migrate()
const r = result as any
expect(r.backupBranch).toBe('pre-migration-5.0.0')
// Verify backup branch exists
const branches = await brain.listBranches()
expect(branches).toContain('pre-migration-5.0.0')
// Migration should not have errored from trying to migrate the backup
expect(r.migrationsApplied).toContain('test-skip-backup-1')
})
})
})
// ─── MigrationRunner unit-level tests ─────────────────────────
describe('MigrationRunner', () => {
@ -684,47 +599,6 @@ describe('Migration System', () => {
})
})
it('should propagate errors from branch migrations into the result', async () => {
// Create entity on main, commit, fork a branch, add a branch-local entity that will fail
await brain.add({ type: NounType.Concept, data: { name: 'MainOk' }, metadata: { branchTest: 1 } })
await brain.commit({ message: 'initial', author: 'test' })
const fork = await brain.fork('branch-with-error')
await fork.add({ type: NounType.Concept, data: { name: 'BranchBad' }, metadata: { branchTest: 'crash' } })
const migration: Migration = {
id: 'test-branch-error-prop',
version: '1.0.0',
description: 'Throws on non-number branchTest',
applies: 'nouns',
transform: (m) => {
if ('branchTest' in m) {
if (typeof m.branchTest !== 'number') {
throw new Error('branchTest must be a number')
}
return { ...m, branchTest: (m.branchTest as number) * 10 }
}
return null
}
}
await withMigrations([migration], async () => {
const result = await brain.migrate()
const r = result as any
// Main entity should have migrated successfully
expect(r.migrationsApplied).toContain('test-branch-error-prop')
expect(r.entitiesModified).toBeGreaterThanOrEqual(1)
// Branch error should appear in the combined result
expect(r.errors.length).toBeGreaterThanOrEqual(1)
const branchError = r.errors.find((e: any) => e.error.includes('branchTest must be a number'))
expect(branchError).toBeDefined()
})
await fork.close()
})
it('should stop early when maxErrors is exceeded', async () => {
// Add many entities that will all fail
for (let i = 0; i < 5; i++) {