brainy/tests/unit/db/db-portable-graph.test.ts

296 lines
12 KiB
TypeScript
Raw Normal View History

feat(8.0): portable graph export()/import() (BackupData v1) — Db.export + polymorphic import Re-adds the portable graph round-trip on 8.0 (deleted with DataAPI), now on the immutable Db so it composes with the generational model. - src/db/backup.ts: BackupData v1 engine (identical wire format to the 7.32.0 line). exportGraph() reads through a Db at its pinned generation; importGraph() applies the whole backup as ONE atomic transact() (a single generation). - Db.export(selector?, options?) — read at this view's generation, so brain.asOf(g).export() is a time-travel export and brain.now().with(ops).export() a what-if export. - brain.export(selector?, options?) — sugar for now().export(). - brain.import() is polymorphic: a BackupData document -> graph round-trip; a file/buffer -> existing foreign-file ingestion (dispatched on the 'brainy-backup' tag; zero migration for ingestion callers). - Selectors (ids/collection/connected/vfsPath/predicate/whole) + edge policy (induced/incident/none) + includeVectors/includeContent/includeSystem; system entities excluded by visibility. onConflict merge/replace/skip, reembed auto/never, remapIds. DbHost gains storage (VFS blob bytes). - Types exported from the package root: BackupData/BackupEntity/BackupRelation/ ExportSelector/ExportOptions/ImportOptions/ImportResult + isBackupData. Tests: tests/unit/db/db-backup.test.ts (10, green) — round-trip, selectors, edges, vectors+re-embed, merge, asOf/with composition, validation. Full unit suite green. Follow-ups: docs (8.0 guide + RELEASES + api/README), completeness adds (since(prior).export() delta, exportStream/importStream, validate()), includeContent filesystem test, ids-at-asOf get() refinement, @soulcraft/formats Zod schema mirror.
2026-06-16 16:38:18 -07:00
/**
* Unit tests for the 8.0 portable graph backup/restore surface:
* db.export() / brain.export() (read, at a pinned generation)
* brain.import(backup) (write, one atomic transaction; polymorphic)
*
* Real round-trips against in-memory storage (no mocks of the API under test),
* including the 8.0-specific compositions (asOf/with export) and the polymorphic
* import() dispatch. Entities carry subtypes so the suite passes under 8.0's
* subtype-required default.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { randomUUID } from 'node:crypto'
import * as fs from 'node:fs/promises'
import * as os from 'node:os'
import * as path from 'node:path'
feat(8.0): portable graph export()/import() (BackupData v1) — Db.export + polymorphic import Re-adds the portable graph round-trip on 8.0 (deleted with DataAPI), now on the immutable Db so it composes with the generational model. - src/db/backup.ts: BackupData v1 engine (identical wire format to the 7.32.0 line). exportGraph() reads through a Db at its pinned generation; importGraph() applies the whole backup as ONE atomic transact() (a single generation). - Db.export(selector?, options?) — read at this view's generation, so brain.asOf(g).export() is a time-travel export and brain.now().with(ops).export() a what-if export. - brain.export(selector?, options?) — sugar for now().export(). - brain.import() is polymorphic: a BackupData document -> graph round-trip; a file/buffer -> existing foreign-file ingestion (dispatched on the 'brainy-backup' tag; zero migration for ingestion callers). - Selectors (ids/collection/connected/vfsPath/predicate/whole) + edge policy (induced/incident/none) + includeVectors/includeContent/includeSystem; system entities excluded by visibility. onConflict merge/replace/skip, reembed auto/never, remapIds. DbHost gains storage (VFS blob bytes). - Types exported from the package root: BackupData/BackupEntity/BackupRelation/ ExportSelector/ExportOptions/ImportOptions/ImportResult + isBackupData. Tests: tests/unit/db/db-backup.test.ts (10, green) — round-trip, selectors, edges, vectors+re-embed, merge, asOf/with composition, validation. Full unit suite green. Follow-ups: docs (8.0 guide + RELEASES + api/README), completeness adds (since(prior).export() delta, exportStream/importStream, validate()), includeContent filesystem test, ids-at-asOf get() refinement, @soulcraft/formats Zod schema mirror.
2026-06-16 16:38:18 -07:00
import { Brainy } from '../../../src/brainy'
import { createTestConfig } from '../../helpers/test-factory'
import { NounType, VerbType } from '../../../src/types/graphTypes'
refactor(8.0): rename BackupData → PortableGraph (the type is interchange, not a backup) The export()/import() document type was named BackupData, but it is not a backup: it is the portable, versioned, partial-or-whole interchange representation of a graph (entities + relations + optional vectors/blobs) — the unit Brainy exports for transport between instances, versions, and products, and the payload other artifacts embed. The actual backup is persist()/load() (the native whole-brain, generation-preserving snapshot), so "Backup*" actively collided with that concept. Rename every developer-visible symbol, file, doc, JSDoc and comment: - BackupData→PortableGraph, BackupEntity→PortableGraphEntity, BackupRelation→PortableGraphRelation, BackupReader/Writer→PortableGraphReader/Writer, BackupValidation→PortableGraphValidation, isBackupData→isPortableGraph, validateBackup→validatePortableGraph, BACKUP_FORMAT[_VERSION]→PORTABLE_GRAPH_FORMAT[_VERSION]. - src/db/backup.ts → src/db/portableGraph.ts; test → db-portable-graph.test.ts. - Guide, api/README, RELEASES updated. The on-the-wire `format` tag is renamed 'brainy-backup' → 'brainy-portable-graph': the format was introduced in 7.32.0 and has not been adopted by any consumer, so there are no stored documents to stay compatible with — a clean rename beats carrying a legacy tag. No deprecated aliases (nothing to alias). 7.x shipped the same rename as 7.32.2. 1471 unit green; build clean; db-portable-graph.test.ts 17/17.
2026-06-19 12:09:19 -07:00
import { validatePortableGraph } from '../../../src/db/portableGraph'
import type { PortableGraph } from '../../../src/db/portableGraph'
feat(8.0): portable graph export()/import() (BackupData v1) — Db.export + polymorphic import Re-adds the portable graph round-trip on 8.0 (deleted with DataAPI), now on the immutable Db so it composes with the generational model. - src/db/backup.ts: BackupData v1 engine (identical wire format to the 7.32.0 line). exportGraph() reads through a Db at its pinned generation; importGraph() applies the whole backup as ONE atomic transact() (a single generation). - Db.export(selector?, options?) — read at this view's generation, so brain.asOf(g).export() is a time-travel export and brain.now().with(ops).export() a what-if export. - brain.export(selector?, options?) — sugar for now().export(). - brain.import() is polymorphic: a BackupData document -> graph round-trip; a file/buffer -> existing foreign-file ingestion (dispatched on the 'brainy-backup' tag; zero migration for ingestion callers). - Selectors (ids/collection/connected/vfsPath/predicate/whole) + edge policy (induced/incident/none) + includeVectors/includeContent/includeSystem; system entities excluded by visibility. onConflict merge/replace/skip, reembed auto/never, remapIds. DbHost gains storage (VFS blob bytes). - Types exported from the package root: BackupData/BackupEntity/BackupRelation/ ExportSelector/ExportOptions/ImportOptions/ImportResult + isBackupData. Tests: tests/unit/db/db-backup.test.ts (10, green) — round-trip, selectors, edges, vectors+re-embed, merge, asOf/with composition, validation. Full unit suite green. Follow-ups: docs (8.0 guide + RELEASES + api/README), completeness adds (since(prior).export() delta, exportStream/importStream, validate()), includeContent filesystem test, ids-at-asOf get() refinement, @soulcraft/formats Zod schema mirror.
2026-06-16 16:38:18 -07:00
refactor(8.0): rename BackupData → PortableGraph (the type is interchange, not a backup) The export()/import() document type was named BackupData, but it is not a backup: it is the portable, versioned, partial-or-whole interchange representation of a graph (entities + relations + optional vectors/blobs) — the unit Brainy exports for transport between instances, versions, and products, and the payload other artifacts embed. The actual backup is persist()/load() (the native whole-brain, generation-preserving snapshot), so "Backup*" actively collided with that concept. Rename every developer-visible symbol, file, doc, JSDoc and comment: - BackupData→PortableGraph, BackupEntity→PortableGraphEntity, BackupRelation→PortableGraphRelation, BackupReader/Writer→PortableGraphReader/Writer, BackupValidation→PortableGraphValidation, isBackupData→isPortableGraph, validateBackup→validatePortableGraph, BACKUP_FORMAT[_VERSION]→PORTABLE_GRAPH_FORMAT[_VERSION]. - src/db/backup.ts → src/db/portableGraph.ts; test → db-portable-graph.test.ts. - Guide, api/README, RELEASES updated. The on-the-wire `format` tag is renamed 'brainy-backup' → 'brainy-portable-graph': the format was introduced in 7.32.0 and has not been adopted by any consumer, so there are no stored documents to stay compatible with — a clean rename beats carrying a legacy tag. No deprecated aliases (nothing to alias). 7.x shipped the same rename as 7.32.2. 1471 unit green; build clean; db-portable-graph.test.ts 17/17.
2026-06-19 12:09:19 -07:00
describe('8.0 portable graph export/import (PortableGraph v1)', () => {
feat(8.0): portable graph export()/import() (BackupData v1) — Db.export + polymorphic import Re-adds the portable graph round-trip on 8.0 (deleted with DataAPI), now on the immutable Db so it composes with the generational model. - src/db/backup.ts: BackupData v1 engine (identical wire format to the 7.32.0 line). exportGraph() reads through a Db at its pinned generation; importGraph() applies the whole backup as ONE atomic transact() (a single generation). - Db.export(selector?, options?) — read at this view's generation, so brain.asOf(g).export() is a time-travel export and brain.now().with(ops).export() a what-if export. - brain.export(selector?, options?) — sugar for now().export(). - brain.import() is polymorphic: a BackupData document -> graph round-trip; a file/buffer -> existing foreign-file ingestion (dispatched on the 'brainy-backup' tag; zero migration for ingestion callers). - Selectors (ids/collection/connected/vfsPath/predicate/whole) + edge policy (induced/incident/none) + includeVectors/includeContent/includeSystem; system entities excluded by visibility. onConflict merge/replace/skip, reembed auto/never, remapIds. DbHost gains storage (VFS blob bytes). - Types exported from the package root: BackupData/BackupEntity/BackupRelation/ ExportSelector/ExportOptions/ImportOptions/ImportResult + isBackupData. Tests: tests/unit/db/db-backup.test.ts (10, green) — round-trip, selectors, edges, vectors+re-embed, merge, asOf/with composition, validation. Full unit suite green. Follow-ups: docs (8.0 guide + RELEASES + api/README), completeness adds (since(prior).export() delta, exportStream/importStream, validate()), includeContent filesystem test, ids-at-asOf get() refinement, @soulcraft/formats Zod schema mirror.
2026-06-16 16:38:18 -07:00
let brain: Brainy
beforeEach(async () => {
brain = new Brainy(createTestConfig())
await brain.init()
})
afterEach(async () => {
await brain.close()
})
describe('format + round-trip', () => {
refactor(8.0): rename BackupData → PortableGraph (the type is interchange, not a backup) The export()/import() document type was named BackupData, but it is not a backup: it is the portable, versioned, partial-or-whole interchange representation of a graph (entities + relations + optional vectors/blobs) — the unit Brainy exports for transport between instances, versions, and products, and the payload other artifacts embed. The actual backup is persist()/load() (the native whole-brain, generation-preserving snapshot), so "Backup*" actively collided with that concept. Rename every developer-visible symbol, file, doc, JSDoc and comment: - BackupData→PortableGraph, BackupEntity→PortableGraphEntity, BackupRelation→PortableGraphRelation, BackupReader/Writer→PortableGraphReader/Writer, BackupValidation→PortableGraphValidation, isBackupData→isPortableGraph, validateBackup→validatePortableGraph, BACKUP_FORMAT[_VERSION]→PORTABLE_GRAPH_FORMAT[_VERSION]. - src/db/backup.ts → src/db/portableGraph.ts; test → db-portable-graph.test.ts. - Guide, api/README, RELEASES updated. The on-the-wire `format` tag is renamed 'brainy-backup' → 'brainy-portable-graph': the format was introduced in 7.32.0 and has not been adopted by any consumer, so there are no stored documents to stay compatible with — a clean rename beats carrying a legacy tag. No deprecated aliases (nothing to alias). 7.x shipped the same rename as 7.32.2. 1471 unit green; build clean; db-portable-graph.test.ts 17/17.
2026-06-19 12:09:19 -07:00
it('brain.export() produces a versioned PortableGraph document', async () => {
feat(8.0): portable graph export()/import() (BackupData v1) — Db.export + polymorphic import Re-adds the portable graph round-trip on 8.0 (deleted with DataAPI), now on the immutable Db so it composes with the generational model. - src/db/backup.ts: BackupData v1 engine (identical wire format to the 7.32.0 line). exportGraph() reads through a Db at its pinned generation; importGraph() applies the whole backup as ONE atomic transact() (a single generation). - Db.export(selector?, options?) — read at this view's generation, so brain.asOf(g).export() is a time-travel export and brain.now().with(ops).export() a what-if export. - brain.export(selector?, options?) — sugar for now().export(). - brain.import() is polymorphic: a BackupData document -> graph round-trip; a file/buffer -> existing foreign-file ingestion (dispatched on the 'brainy-backup' tag; zero migration for ingestion callers). - Selectors (ids/collection/connected/vfsPath/predicate/whole) + edge policy (induced/incident/none) + includeVectors/includeContent/includeSystem; system entities excluded by visibility. onConflict merge/replace/skip, reembed auto/never, remapIds. DbHost gains storage (VFS blob bytes). - Types exported from the package root: BackupData/BackupEntity/BackupRelation/ ExportSelector/ExportOptions/ImportOptions/ImportResult + isBackupData. Tests: tests/unit/db/db-backup.test.ts (10, green) — round-trip, selectors, edges, vectors+re-embed, merge, asOf/with composition, validation. Full unit suite green. Follow-ups: docs (8.0 guide + RELEASES + api/README), completeness adds (since(prior).export() delta, exportStream/importStream, validate()), includeContent filesystem test, ids-at-asOf get() refinement, @soulcraft/formats Zod schema mirror.
2026-06-16 16:38:18 -07:00
const a = await brain.add({ data: 'Alice', type: NounType.Person, subtype: 'employee' })
const b = await brain.add({ data: 'Acme', type: NounType.Organization, subtype: 'vendor' })
await brain.relate({ from: a, to: b, type: VerbType.WorksWith, subtype: 'full-time' })
const backup = await brain.export()
refactor(8.0): rename BackupData → PortableGraph (the type is interchange, not a backup) The export()/import() document type was named BackupData, but it is not a backup: it is the portable, versioned, partial-or-whole interchange representation of a graph (entities + relations + optional vectors/blobs) — the unit Brainy exports for transport between instances, versions, and products, and the payload other artifacts embed. The actual backup is persist()/load() (the native whole-brain, generation-preserving snapshot), so "Backup*" actively collided with that concept. Rename every developer-visible symbol, file, doc, JSDoc and comment: - BackupData→PortableGraph, BackupEntity→PortableGraphEntity, BackupRelation→PortableGraphRelation, BackupReader/Writer→PortableGraphReader/Writer, BackupValidation→PortableGraphValidation, isBackupData→isPortableGraph, validateBackup→validatePortableGraph, BACKUP_FORMAT[_VERSION]→PORTABLE_GRAPH_FORMAT[_VERSION]. - src/db/backup.ts → src/db/portableGraph.ts; test → db-portable-graph.test.ts. - Guide, api/README, RELEASES updated. The on-the-wire `format` tag is renamed 'brainy-backup' → 'brainy-portable-graph': the format was introduced in 7.32.0 and has not been adopted by any consumer, so there are no stored documents to stay compatible with — a clean rename beats carrying a legacy tag. No deprecated aliases (nothing to alias). 7.x shipped the same rename as 7.32.2. 1471 unit green; build clean; db-portable-graph.test.ts 17/17.
2026-06-19 12:09:19 -07:00
expect(backup.format).toBe('brainy-portable-graph')
feat(8.0): portable graph export()/import() (BackupData v1) — Db.export + polymorphic import Re-adds the portable graph round-trip on 8.0 (deleted with DataAPI), now on the immutable Db so it composes with the generational model. - src/db/backup.ts: BackupData v1 engine (identical wire format to the 7.32.0 line). exportGraph() reads through a Db at its pinned generation; importGraph() applies the whole backup as ONE atomic transact() (a single generation). - Db.export(selector?, options?) — read at this view's generation, so brain.asOf(g).export() is a time-travel export and brain.now().with(ops).export() a what-if export. - brain.export(selector?, options?) — sugar for now().export(). - brain.import() is polymorphic: a BackupData document -> graph round-trip; a file/buffer -> existing foreign-file ingestion (dispatched on the 'brainy-backup' tag; zero migration for ingestion callers). - Selectors (ids/collection/connected/vfsPath/predicate/whole) + edge policy (induced/incident/none) + includeVectors/includeContent/includeSystem; system entities excluded by visibility. onConflict merge/replace/skip, reembed auto/never, remapIds. DbHost gains storage (VFS blob bytes). - Types exported from the package root: BackupData/BackupEntity/BackupRelation/ ExportSelector/ExportOptions/ImportOptions/ImportResult + isBackupData. Tests: tests/unit/db/db-backup.test.ts (10, green) — round-trip, selectors, edges, vectors+re-embed, merge, asOf/with composition, validation. Full unit suite green. Follow-ups: docs (8.0 guide + RELEASES + api/README), completeness adds (since(prior).export() delta, exportStream/importStream, validate()), includeContent filesystem test, ids-at-asOf get() refinement, @soulcraft/formats Zod schema mirror.
2026-06-16 16:38:18 -07:00
expect(backup.formatVersion).toBe(1)
expect(backup.entities.map((e) => e.id).sort()).toEqual([a, b].sort())
expect(backup.relations).toHaveLength(1)
expect(backup.entities.find((e) => e.id === a)?.subtype).toBe('employee')
expect(backup.relations[0].subtype).toBe('full-time')
})
it('round-trips into a second brain via polymorphic brain.import()', async () => {
const a = await brain.add({ data: 'Alice', type: NounType.Person, subtype: 'employee' })
const b = await brain.add({ data: 'Bob', type: NounType.Person, subtype: 'employee' })
await brain.relate({ from: a, to: b, type: VerbType.FriendOf, subtype: 'close' })
const backup = await brain.export({}, { includeVectors: true })
const target = new Brainy(createTestConfig())
await target.init()
try {
const result = await target.import(backup)
expect(result.imported).toBe(2)
expect(result.errors).toHaveLength(0)
expect((await target.get(a))?.id).toBe(a)
const rels = await target.related({ from: a })
expect(rels.some((r) => r.to === b && r.type === VerbType.FriendOf)).toBe(true)
} finally {
await target.close()
}
})
})
describe('selectors', () => {
it('ids — exactly the requested entities', async () => {
const a = await brain.add({ data: 'A', type: NounType.Thing, subtype: 'x' })
const b = await brain.add({ data: 'B', type: NounType.Thing, subtype: 'x' })
await brain.add({ data: 'C', type: NounType.Thing, subtype: 'x' })
const backup = await brain.export({ ids: [a, b] })
expect(backup.entities.map((e) => e.id).sort()).toEqual([a, b].sort())
})
it('collection — collection + transitive Contains members', async () => {
const root = await brain.add({ data: 'Folder', type: NounType.Collection, subtype: 'dir' })
const c1 = await brain.add({ data: 'C1', type: NounType.Document, subtype: 'doc' })
const c2 = await brain.add({ data: 'C2', type: NounType.Document, subtype: 'doc' })
await brain.add({ data: 'Outside', type: NounType.Document, subtype: 'doc' })
await brain.relate({ from: root, to: c1, type: VerbType.Contains, subtype: 'has' })
await brain.relate({ from: c1, to: c2, type: VerbType.Contains, subtype: 'has' })
const backup = await brain.export({ collection: root })
expect(backup.entities.map((e) => e.id).sort()).toEqual([root, c1, c2].sort())
})
it('induced edges only by default', async () => {
const a = await brain.add({ data: 'A', type: NounType.Thing, subtype: 'x' })
const b = await brain.add({ data: 'B', type: NounType.Thing, subtype: 'x' })
const c = await brain.add({ data: 'C', type: NounType.Thing, subtype: 'x' })
await brain.relate({ from: a, to: b, type: VerbType.RelatedTo, subtype: 'r' })
await brain.relate({ from: b, to: c, type: VerbType.RelatedTo, subtype: 'r' })
const backup = await brain.export({ ids: [a, b] })
expect(backup.relations).toHaveLength(1)
expect(backup.relations[0].from).toBe(a)
})
})
describe('vectors + conflict', () => {
it('omits vectors by default and re-embeds on import', async () => {
const a = await brain.add({ data: 'Re-embed me', type: NounType.Thing, subtype: 'x' })
const backup = await brain.export({ ids: [a] })
expect(backup.entities[0].vector).toBeUndefined()
const target = new Brainy(createTestConfig())
await target.init()
try {
const result = await target.import(backup)
expect(result.reembedded).toBe(1)
expect((await target.get(a, { includeVectors: true }))?.vector?.length).toBeGreaterThan(0)
} finally {
await target.close()
}
})
it('onConflict:merge updates an existing entity in place', async () => {
const a = await brain.add({ data: 'V1', type: NounType.Thing, subtype: 'x', metadata: { n: 1 } })
const backup = await brain.export({ ids: [a] }, { includeVectors: true })
await brain.update({ id: a, metadata: { n: 99 }, merge: false })
const result = await brain.import(backup, { onConflict: 'merge' })
expect(result.merged).toBe(1)
expect((await brain.get(a))?.metadata?.n).toBe(1)
})
})
describe('8.0 composition — export on the immutable Db', () => {
it('asOf(generation).export() is a time-travel export', async () => {
// Only transact() advances the committed generation (add() does not), so
// distinct generations are created via transact for a true time-travel pin.
const a = '22222222-2222-4222-8222-222222222222'
const b = '33333333-3333-4333-8333-333333333333'
await brain.transact([{ op: 'add', id: a, data: 'First', type: NounType.Thing, subtype: 'x' }])
const g1 = brain.generation()
await brain.transact([{ op: 'add', id: b, data: 'Second', type: NounType.Thing, subtype: 'x' }])
const past = await brain.asOf(g1)
try {
// Whole-brain export at g1 enumerates via the generation-correct find() path.
const backup = await past.export()
const ids = backup.entities.map((e) => e.id)
expect(ids).toContain(a)
expect(ids).not.toContain(b) // b was committed in a later generation
} finally {
await past.release()
}
})
it('now().with(ops).export() is a what-if export', async () => {
const a = await brain.add({ data: 'Real', type: NounType.Thing, subtype: 'x' })
const speculativeId = '11111111-1111-4111-8111-111111111111'
const view = await brain.now().with([
{ op: 'add', id: speculativeId, data: 'Speculative', type: NounType.Thing, subtype: 'x' }
])
try {
const backup = await view.export({ ids: [a, speculativeId] })
expect(backup.entities.map((e) => e.id).sort()).toEqual([a, speculativeId].sort())
} finally {
await view.release()
}
// The speculative entity never touched the durable brain.
expect(await brain.get(speculativeId)).toBeNull()
})
})
describe('import validation', () => {
it('rejects a newer formatVersion', async () => {
refactor(8.0): rename BackupData → PortableGraph (the type is interchange, not a backup) The export()/import() document type was named BackupData, but it is not a backup: it is the portable, versioned, partial-or-whole interchange representation of a graph (entities + relations + optional vectors/blobs) — the unit Brainy exports for transport between instances, versions, and products, and the payload other artifacts embed. The actual backup is persist()/load() (the native whole-brain, generation-preserving snapshot), so "Backup*" actively collided with that concept. Rename every developer-visible symbol, file, doc, JSDoc and comment: - BackupData→PortableGraph, BackupEntity→PortableGraphEntity, BackupRelation→PortableGraphRelation, BackupReader/Writer→PortableGraphReader/Writer, BackupValidation→PortableGraphValidation, isBackupData→isPortableGraph, validateBackup→validatePortableGraph, BACKUP_FORMAT[_VERSION]→PORTABLE_GRAPH_FORMAT[_VERSION]. - src/db/backup.ts → src/db/portableGraph.ts; test → db-portable-graph.test.ts. - Guide, api/README, RELEASES updated. The on-the-wire `format` tag is renamed 'brainy-backup' → 'brainy-portable-graph': the format was introduced in 7.32.0 and has not been adopted by any consumer, so there are no stored documents to stay compatible with — a clean rename beats carrying a legacy tag. No deprecated aliases (nothing to alias). 7.x shipped the same rename as 7.32.2. 1471 unit green; build clean; db-portable-graph.test.ts 17/17.
2026-06-19 12:09:19 -07:00
const future: PortableGraph = {
format: 'brainy-portable-graph',
feat(8.0): portable graph export()/import() (BackupData v1) — Db.export + polymorphic import Re-adds the portable graph round-trip on 8.0 (deleted with DataAPI), now on the immutable Db so it composes with the generational model. - src/db/backup.ts: BackupData v1 engine (identical wire format to the 7.32.0 line). exportGraph() reads through a Db at its pinned generation; importGraph() applies the whole backup as ONE atomic transact() (a single generation). - Db.export(selector?, options?) — read at this view's generation, so brain.asOf(g).export() is a time-travel export and brain.now().with(ops).export() a what-if export. - brain.export(selector?, options?) — sugar for now().export(). - brain.import() is polymorphic: a BackupData document -> graph round-trip; a file/buffer -> existing foreign-file ingestion (dispatched on the 'brainy-backup' tag; zero migration for ingestion callers). - Selectors (ids/collection/connected/vfsPath/predicate/whole) + edge policy (induced/incident/none) + includeVectors/includeContent/includeSystem; system entities excluded by visibility. onConflict merge/replace/skip, reembed auto/never, remapIds. DbHost gains storage (VFS blob bytes). - Types exported from the package root: BackupData/BackupEntity/BackupRelation/ ExportSelector/ExportOptions/ImportOptions/ImportResult + isBackupData. Tests: tests/unit/db/db-backup.test.ts (10, green) — round-trip, selectors, edges, vectors+re-embed, merge, asOf/with composition, validation. Full unit suite green. Follow-ups: docs (8.0 guide + RELEASES + api/README), completeness adds (since(prior).export() delta, exportStream/importStream, validate()), includeContent filesystem test, ids-at-asOf get() refinement, @soulcraft/formats Zod schema mirror.
2026-06-16 16:38:18 -07:00
formatVersion: 999,
brainyVersion: 'x',
createdAt: new Date().toISOString(),
embedding: { model: 'm', dimensions: 384 },
entities: [],
relations: [],
stats: { entityCount: 0, relationCount: 0, blobCount: 0 }
}
await expect(brain.import(future)).rejects.toThrow(/formatVersion/)
})
})
describe('id remapping (clone)', () => {
it('imports a subgraph under fresh ids (a copy, not a move)', async () => {
const a = await brain.add({ data: 'A', type: NounType.Thing, subtype: 'x' })
const b = await brain.add({ data: 'B', type: NounType.Thing, subtype: 'x' })
await brain.relate({ from: a, to: b, type: VerbType.RelatedTo, subtype: 'r' })
const backup = await brain.export({ ids: [a, b] }, { includeVectors: true })
const remap = new Map<string, string>([
[a, randomUUID()],
[b, randomUUID()]
])
const result = await brain.import(backup, { remapIds: (id) => remap.get(id) ?? id })
expect(result.imported).toBe(2)
expect((await brain.get(remap.get(a)!))?.id).toBe(remap.get(a))
expect((await brain.get(a))?.id).toBe(a) // original still present
const rels = await brain.related({ from: remap.get(a)! })
expect(rels.some((r) => r.to === remap.get(b))).toBe(true)
})
})
refactor(8.0): rename BackupData → PortableGraph (the type is interchange, not a backup) The export()/import() document type was named BackupData, but it is not a backup: it is the portable, versioned, partial-or-whole interchange representation of a graph (entities + relations + optional vectors/blobs) — the unit Brainy exports for transport between instances, versions, and products, and the payload other artifacts embed. The actual backup is persist()/load() (the native whole-brain, generation-preserving snapshot), so "Backup*" actively collided with that concept. Rename every developer-visible symbol, file, doc, JSDoc and comment: - BackupData→PortableGraph, BackupEntity→PortableGraphEntity, BackupRelation→PortableGraphRelation, BackupReader/Writer→PortableGraphReader/Writer, BackupValidation→PortableGraphValidation, isBackupData→isPortableGraph, validateBackup→validatePortableGraph, BACKUP_FORMAT[_VERSION]→PORTABLE_GRAPH_FORMAT[_VERSION]. - src/db/backup.ts → src/db/portableGraph.ts; test → db-portable-graph.test.ts. - Guide, api/README, RELEASES updated. The on-the-wire `format` tag is renamed 'brainy-backup' → 'brainy-portable-graph': the format was introduced in 7.32.0 and has not been adopted by any consumer, so there are no stored documents to stay compatible with — a clean rename beats carrying a legacy tag. No deprecated aliases (nothing to alias). 7.x shipped the same rename as 7.32.2. 1471 unit green; build clean; db-portable-graph.test.ts 17/17.
2026-06-19 12:09:19 -07:00
describe('validatePortableGraph() — dry-run check', () => {
const valid = (): PortableGraph => ({
format: 'brainy-portable-graph',
formatVersion: 1,
brainyVersion: 'x',
createdAt: new Date().toISOString(),
embedding: { model: 'all-MiniLM-L6-v2', dimensions: 384 },
entities: [{ id: 'a', type: NounType.Thing }],
relations: [],
stats: { entityCount: 1, relationCount: 0, blobCount: 0, vectorDimensions: 384 }
})
it('accepts a well-formed backup', () => {
refactor(8.0): rename BackupData → PortableGraph (the type is interchange, not a backup) The export()/import() document type was named BackupData, but it is not a backup: it is the portable, versioned, partial-or-whole interchange representation of a graph (entities + relations + optional vectors/blobs) — the unit Brainy exports for transport between instances, versions, and products, and the payload other artifacts embed. The actual backup is persist()/load() (the native whole-brain, generation-preserving snapshot), so "Backup*" actively collided with that concept. Rename every developer-visible symbol, file, doc, JSDoc and comment: - BackupData→PortableGraph, BackupEntity→PortableGraphEntity, BackupRelation→PortableGraphRelation, BackupReader/Writer→PortableGraphReader/Writer, BackupValidation→PortableGraphValidation, isBackupData→isPortableGraph, validateBackup→validatePortableGraph, BACKUP_FORMAT[_VERSION]→PORTABLE_GRAPH_FORMAT[_VERSION]. - src/db/backup.ts → src/db/portableGraph.ts; test → db-portable-graph.test.ts. - Guide, api/README, RELEASES updated. The on-the-wire `format` tag is renamed 'brainy-backup' → 'brainy-portable-graph': the format was introduced in 7.32.0 and has not been adopted by any consumer, so there are no stored documents to stay compatible with — a clean rename beats carrying a legacy tag. No deprecated aliases (nothing to alias). 7.x shipped the same rename as 7.32.2. 1471 unit green; build clean; db-portable-graph.test.ts 17/17.
2026-06-19 12:09:19 -07:00
const r = validatePortableGraph(valid())
expect(r.valid).toBe(true)
expect(r.errors).toHaveLength(0)
})
refactor(8.0): rename BackupData → PortableGraph (the type is interchange, not a backup) The export()/import() document type was named BackupData, but it is not a backup: it is the portable, versioned, partial-or-whole interchange representation of a graph (entities + relations + optional vectors/blobs) — the unit Brainy exports for transport between instances, versions, and products, and the payload other artifacts embed. The actual backup is persist()/load() (the native whole-brain, generation-preserving snapshot), so "Backup*" actively collided with that concept. Rename every developer-visible symbol, file, doc, JSDoc and comment: - BackupData→PortableGraph, BackupEntity→PortableGraphEntity, BackupRelation→PortableGraphRelation, BackupReader/Writer→PortableGraphReader/Writer, BackupValidation→PortableGraphValidation, isBackupData→isPortableGraph, validateBackup→validatePortableGraph, BACKUP_FORMAT[_VERSION]→PORTABLE_GRAPH_FORMAT[_VERSION]. - src/db/backup.ts → src/db/portableGraph.ts; test → db-portable-graph.test.ts. - Guide, api/README, RELEASES updated. The on-the-wire `format` tag is renamed 'brainy-backup' → 'brainy-portable-graph': the format was introduced in 7.32.0 and has not been adopted by any consumer, so there are no stored documents to stay compatible with — a clean rename beats carrying a legacy tag. No deprecated aliases (nothing to alias). 7.x shipped the same rename as 7.32.2. 1471 unit green; build clean; db-portable-graph.test.ts 17/17.
2026-06-19 12:09:19 -07:00
it('rejects a non-PortableGraph value', () => {
const r = validatePortableGraph({ entities: [] })
expect(r.valid).toBe(false)
refactor(8.0): rename BackupData → PortableGraph (the type is interchange, not a backup) The export()/import() document type was named BackupData, but it is not a backup: it is the portable, versioned, partial-or-whole interchange representation of a graph (entities + relations + optional vectors/blobs) — the unit Brainy exports for transport between instances, versions, and products, and the payload other artifacts embed. The actual backup is persist()/load() (the native whole-brain, generation-preserving snapshot), so "Backup*" actively collided with that concept. Rename every developer-visible symbol, file, doc, JSDoc and comment: - BackupData→PortableGraph, BackupEntity→PortableGraphEntity, BackupRelation→PortableGraphRelation, BackupReader/Writer→PortableGraphReader/Writer, BackupValidation→PortableGraphValidation, isBackupData→isPortableGraph, validateBackup→validatePortableGraph, BACKUP_FORMAT[_VERSION]→PORTABLE_GRAPH_FORMAT[_VERSION]. - src/db/backup.ts → src/db/portableGraph.ts; test → db-portable-graph.test.ts. - Guide, api/README, RELEASES updated. The on-the-wire `format` tag is renamed 'brainy-backup' → 'brainy-portable-graph': the format was introduced in 7.32.0 and has not been adopted by any consumer, so there are no stored documents to stay compatible with — a clean rename beats carrying a legacy tag. No deprecated aliases (nothing to alias). 7.x shipped the same rename as 7.32.2. 1471 unit green; build clean; db-portable-graph.test.ts 17/17.
2026-06-19 12:09:19 -07:00
expect(r.errors[0]).toMatch(/PortableGraph/)
})
it('rejects a newer formatVersion', () => {
refactor(8.0): rename BackupData → PortableGraph (the type is interchange, not a backup) The export()/import() document type was named BackupData, but it is not a backup: it is the portable, versioned, partial-or-whole interchange representation of a graph (entities + relations + optional vectors/blobs) — the unit Brainy exports for transport between instances, versions, and products, and the payload other artifacts embed. The actual backup is persist()/load() (the native whole-brain, generation-preserving snapshot), so "Backup*" actively collided with that concept. Rename every developer-visible symbol, file, doc, JSDoc and comment: - BackupData→PortableGraph, BackupEntity→PortableGraphEntity, BackupRelation→PortableGraphRelation, BackupReader/Writer→PortableGraphReader/Writer, BackupValidation→PortableGraphValidation, isBackupData→isPortableGraph, validateBackup→validatePortableGraph, BACKUP_FORMAT[_VERSION]→PORTABLE_GRAPH_FORMAT[_VERSION]. - src/db/backup.ts → src/db/portableGraph.ts; test → db-portable-graph.test.ts. - Guide, api/README, RELEASES updated. The on-the-wire `format` tag is renamed 'brainy-backup' → 'brainy-portable-graph': the format was introduced in 7.32.0 and has not been adopted by any consumer, so there are no stored documents to stay compatible with — a clean rename beats carrying a legacy tag. No deprecated aliases (nothing to alias). 7.x shipped the same rename as 7.32.2. 1471 unit green; build clean; db-portable-graph.test.ts 17/17.
2026-06-19 12:09:19 -07:00
const r = validatePortableGraph({ ...valid(), formatVersion: 999 })
expect(r.valid).toBe(false)
expect(r.errors.some((e) => /formatVersion/.test(e))).toBe(true)
})
it('flags duplicate entity ids as an error', () => {
const b = valid()
b.entities = [
{ id: 'dup', type: NounType.Thing },
{ id: 'dup', type: NounType.Thing }
]
refactor(8.0): rename BackupData → PortableGraph (the type is interchange, not a backup) The export()/import() document type was named BackupData, but it is not a backup: it is the portable, versioned, partial-or-whole interchange representation of a graph (entities + relations + optional vectors/blobs) — the unit Brainy exports for transport between instances, versions, and products, and the payload other artifacts embed. The actual backup is persist()/load() (the native whole-brain, generation-preserving snapshot), so "Backup*" actively collided with that concept. Rename every developer-visible symbol, file, doc, JSDoc and comment: - BackupData→PortableGraph, BackupEntity→PortableGraphEntity, BackupRelation→PortableGraphRelation, BackupReader/Writer→PortableGraphReader/Writer, BackupValidation→PortableGraphValidation, isBackupData→isPortableGraph, validateBackup→validatePortableGraph, BACKUP_FORMAT[_VERSION]→PORTABLE_GRAPH_FORMAT[_VERSION]. - src/db/backup.ts → src/db/portableGraph.ts; test → db-portable-graph.test.ts. - Guide, api/README, RELEASES updated. The on-the-wire `format` tag is renamed 'brainy-backup' → 'brainy-portable-graph': the format was introduced in 7.32.0 and has not been adopted by any consumer, so there are no stored documents to stay compatible with — a clean rename beats carrying a legacy tag. No deprecated aliases (nothing to alias). 7.x shipped the same rename as 7.32.2. 1471 unit green; build clean; db-portable-graph.test.ts 17/17.
2026-06-19 12:09:19 -07:00
const r = validatePortableGraph(b)
expect(r.valid).toBe(false)
expect(r.errors.some((e) => /duplicate/.test(e))).toBe(true)
})
it('warns (not errors) on a relation endpoint missing from entities', () => {
const b = valid()
b.relations = [{ id: 'r1', from: 'a', to: 'missing', type: VerbType.RelatedTo }]
refactor(8.0): rename BackupData → PortableGraph (the type is interchange, not a backup) The export()/import() document type was named BackupData, but it is not a backup: it is the portable, versioned, partial-or-whole interchange representation of a graph (entities + relations + optional vectors/blobs) — the unit Brainy exports for transport between instances, versions, and products, and the payload other artifacts embed. The actual backup is persist()/load() (the native whole-brain, generation-preserving snapshot), so "Backup*" actively collided with that concept. Rename every developer-visible symbol, file, doc, JSDoc and comment: - BackupData→PortableGraph, BackupEntity→PortableGraphEntity, BackupRelation→PortableGraphRelation, BackupReader/Writer→PortableGraphReader/Writer, BackupValidation→PortableGraphValidation, isBackupData→isPortableGraph, validateBackup→validatePortableGraph, BACKUP_FORMAT[_VERSION]→PORTABLE_GRAPH_FORMAT[_VERSION]. - src/db/backup.ts → src/db/portableGraph.ts; test → db-portable-graph.test.ts. - Guide, api/README, RELEASES updated. The on-the-wire `format` tag is renamed 'brainy-backup' → 'brainy-portable-graph': the format was introduced in 7.32.0 and has not been adopted by any consumer, so there are no stored documents to stay compatible with — a clean rename beats carrying a legacy tag. No deprecated aliases (nothing to alias). 7.x shipped the same rename as 7.32.2. 1471 unit green; build clean; db-portable-graph.test.ts 17/17.
2026-06-19 12:09:19 -07:00
const r = validatePortableGraph(b)
expect(r.valid).toBe(true) // a dangling endpoint is a warning, not a hard error
expect(r.warnings.some((w) => /missing/.test(w))).toBe(true)
})
})
})
describe('8.0 export includeContent (VFS blobs, filesystem)', () => {
let dir: string
let brain: Brainy
beforeEach(async () => {
dir = await fs.mkdtemp(path.join(os.tmpdir(), 'brainy-blob-'))
feat(8.0): API simplification — remove neural()/Db.search, one storage `path` key, integration→0 8.0 RC cleanup toward "one place per thing, zero-config, no deprecation": - Remove the `brain.neural()` clustering namespace (ImprovedNeuralAPI + the dead legacy NeuralAPI + the neural CLI + neural-only types). Similarity is `find({vector})` / `similar({to})`; attribute grouping is the aggregation `GROUP BY` engine. The separate entity-extraction / smart-import feature (NeuralImport, NeuralEntityExtractor, SmartExtractor, NaturalLanguageProcessor, `brain.extract()`/`brain.nlp()`) is kept. - Remove `Db.search()`; `find()` is the one query verb (accepts a bare string or FindParams). Fix the bundled MCP client, which called a non-existent `brain.search(query, limit)` → now `find({ query, limit })`. - Storage config: collapse to one canonical top-level `path` key. The pre-8.0 aliases (`rootDirectory`, `options.*`, `fileSystemStorage.*`) are removed and now THROW with the exact rename instead of silently defaulting to `./brainy-data` on upgrade. A single resolver feeds createStorage, the 7.x→8.0 migration probe, and the plugin-factory handoff, so a native storage provider resolves the identical root (no split-brain). - Fix `similar({ threshold })`: the min-similarity filter was silently dropped; it is now applied as a post-filter on `result.score` (the documented way to bound semantic results). - Fix `vfs.rename()` on a directory: child path updates spread the entity vector into `update()` and failed dimension validation; they are metadata-only updates now. - Fix `vfs.move()`: copy+delete orphaned the content-addressed content blob (the destination shared the source hash, then unlink removed it). `move()` now delegates to `rename()` — an in-place path change that preserves the blob and the entity id, for files and directories. - Fix streaming import: the bulk fast path never flushed mid-import nor signalled queryability. Entity writes are now chunked by a progressive flush interval (100 → 1000 → 5000); each chunk flushes and emits `progress.queryable`, so imported data is queryable during the import. - Sweep all docs, comments, and JSDoc for the removed/changed APIs. Integration suite: 49 files / 588 passed / 0 failed. Unit: 80 files / 1456 passed, no type errors.
2026-06-20 13:31:11 -07:00
brain = new Brainy({ storage: { type: 'filesystem', path: dir } })
await brain.init()
})
afterEach(async () => {
await brain.close()
await fs.rm(dir, { recursive: true, force: true })
})
it('captures VFS file bytes in blobs and writes them on import', async () => {
await brain.vfs.writeFile('/hello.txt', 'Hello blobs')
const backup = await brain.export(
{ vfsPath: '/hello.txt' },
{ includeVectors: true, includeContent: true }
)
expect(backup.stats.blobCount).toBeGreaterThanOrEqual(1)
const b64 = Object.values(backup.blobs ?? {})[0]
expect(Buffer.from(b64, 'base64').toString()).toBe('Hello blobs')
const dir2 = await fs.mkdtemp(path.join(os.tmpdir(), 'brainy-blob2-'))
feat(8.0): API simplification — remove neural()/Db.search, one storage `path` key, integration→0 8.0 RC cleanup toward "one place per thing, zero-config, no deprecation": - Remove the `brain.neural()` clustering namespace (ImprovedNeuralAPI + the dead legacy NeuralAPI + the neural CLI + neural-only types). Similarity is `find({vector})` / `similar({to})`; attribute grouping is the aggregation `GROUP BY` engine. The separate entity-extraction / smart-import feature (NeuralImport, NeuralEntityExtractor, SmartExtractor, NaturalLanguageProcessor, `brain.extract()`/`brain.nlp()`) is kept. - Remove `Db.search()`; `find()` is the one query verb (accepts a bare string or FindParams). Fix the bundled MCP client, which called a non-existent `brain.search(query, limit)` → now `find({ query, limit })`. - Storage config: collapse to one canonical top-level `path` key. The pre-8.0 aliases (`rootDirectory`, `options.*`, `fileSystemStorage.*`) are removed and now THROW with the exact rename instead of silently defaulting to `./brainy-data` on upgrade. A single resolver feeds createStorage, the 7.x→8.0 migration probe, and the plugin-factory handoff, so a native storage provider resolves the identical root (no split-brain). - Fix `similar({ threshold })`: the min-similarity filter was silently dropped; it is now applied as a post-filter on `result.score` (the documented way to bound semantic results). - Fix `vfs.rename()` on a directory: child path updates spread the entity vector into `update()` and failed dimension validation; they are metadata-only updates now. - Fix `vfs.move()`: copy+delete orphaned the content-addressed content blob (the destination shared the source hash, then unlink removed it). `move()` now delegates to `rename()` — an in-place path change that preserves the blob and the entity id, for files and directories. - Fix streaming import: the bulk fast path never flushed mid-import nor signalled queryability. Entity writes are now chunked by a progressive flush interval (100 → 1000 → 5000); each chunk flushes and emits `progress.queryable`, so imported data is queryable during the import. - Sweep all docs, comments, and JSDoc for the removed/changed APIs. Integration suite: 49 files / 588 passed / 0 failed. Unit: 80 files / 1456 passed, no type errors.
2026-06-20 13:31:11 -07:00
const target = new Brainy({ storage: { type: 'filesystem', path: dir2 } })
await target.init()
try {
const result = await target.import(backup)
expect(result.blobsWritten).toBeGreaterThanOrEqual(1)
expect(result.imported).toBeGreaterThanOrEqual(1)
} finally {
await target.close()
await fs.rm(dir2, { recursive: true, force: true })
}
})
feat(8.0): portable graph export()/import() (BackupData v1) — Db.export + polymorphic import Re-adds the portable graph round-trip on 8.0 (deleted with DataAPI), now on the immutable Db so it composes with the generational model. - src/db/backup.ts: BackupData v1 engine (identical wire format to the 7.32.0 line). exportGraph() reads through a Db at its pinned generation; importGraph() applies the whole backup as ONE atomic transact() (a single generation). - Db.export(selector?, options?) — read at this view's generation, so brain.asOf(g).export() is a time-travel export and brain.now().with(ops).export() a what-if export. - brain.export(selector?, options?) — sugar for now().export(). - brain.import() is polymorphic: a BackupData document -> graph round-trip; a file/buffer -> existing foreign-file ingestion (dispatched on the 'brainy-backup' tag; zero migration for ingestion callers). - Selectors (ids/collection/connected/vfsPath/predicate/whole) + edge policy (induced/incident/none) + includeVectors/includeContent/includeSystem; system entities excluded by visibility. onConflict merge/replace/skip, reembed auto/never, remapIds. DbHost gains storage (VFS blob bytes). - Types exported from the package root: BackupData/BackupEntity/BackupRelation/ ExportSelector/ExportOptions/ImportOptions/ImportResult + isBackupData. Tests: tests/unit/db/db-backup.test.ts (10, green) — round-trip, selectors, edges, vectors+re-embed, merge, asOf/with composition, validation. Full unit suite green. Follow-ups: docs (8.0 guide + RELEASES + api/README), completeness adds (since(prior).export() delta, exportStream/importStream, validate()), includeContent filesystem test, ids-at-asOf get() refinement, @soulcraft/formats Zod schema mirror.
2026-06-16 16:38:18 -07:00
})