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 .
* /
2026-07-27 11:08:19 -07:00
import { describe , it , expect , beforeEach , afterEach , vi } from 'vitest'
2026-06-17 11:44:29 -07:00
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'
2026-07-27 11:08:19 -07:00
import { CanonicalEnumerationUnavailableError } from '../../../src/db/errors'
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/ )
} )
} )
2026-06-17 11:44:29 -07:00
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' ,
2026-06-17 11:44:29 -07:00
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 ( ) )
2026-06-17 11:44:29 -07:00
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 : [ ] } )
2026-06-17 11:44:29 -07:00
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/ )
2026-06-17 11:44:29 -07:00
} )
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 } )
2026-06-17 11:44:29 -07:00
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 )
2026-06-17 11:44:29 -07:00
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 )
2026-06-17 11:44:29 -07:00
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 } } )
2026-06-17 11:44:29 -07:00
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 } } )
2026-06-17 11:44:29 -07:00
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
} )
2026-07-27 11:08:19 -07:00
describe ( '8.0 export enumeration:"canonical" — canon-complete against index blindness' , ( ) = > {
let brain : Brainy
beforeEach ( async ( ) = > {
brain = new Brainy ( createTestConfig ( ) )
await brain . init ( )
} )
afterEach ( async ( ) = > {
await brain . close ( )
} )
it ( '(i) equals the index-based export when the index is healthy — same entity ids, relations, vectors' , 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' } )
const c = await brain . add ( { data : 'Acme' , type : NounType . Organization , subtype : 'vendor' } )
await brain . relate ( { from : a , to : b , type : VerbType . FriendOf , subtype : 'close' } )
await brain . relate ( { from : a , to : c , type : VerbType . WorksWith , subtype : 'full-time' } )
const indexExport = await brain . export ( { } , { includeVectors : true , enumeration : 'index' } )
const canonicalExport = await brain . export ( { } , { includeVectors : true , enumeration : 'canonical' } )
expect ( canonicalExport . entities . map ( ( e ) = > e . id ) . sort ( ) ) . toEqual (
indexExport . entities . map ( ( e ) = > e . id ) . sort ( )
)
expect ( canonicalExport . relations . map ( ( r ) = > r . id ) . sort ( ) ) . toEqual (
indexExport . relations . map ( ( r ) = > r . id ) . sort ( )
)
expect ( canonicalExport . entities . map ( ( e ) = > e . id ) . sort ( ) ) . toEqual ( [ a , b , c ] . sort ( ) )
for ( const e of canonicalExport . entities ) {
expect ( e . vector ? . length ) . toBeGreaterThan ( 0 )
}
expect ( canonicalExport . drift ) . toBeUndefined ( ) // reportIndexDrift not requested
} )
it ( '(ii) survives simulated metadata-index blindness; the index export misses the record; drift names it canonicalOnly' , async ( ) = > {
const staff = await brain . add ( {
data : 'Staff' ,
type : NounType . Person ,
subtype : 'employee' ,
metadata : { role : 'staff' }
} )
const other = await brain . add ( {
data : 'Other' ,
type : NounType . Person ,
subtype : 'employee' ,
metadata : { role : 'staff' }
} )
// Surgically poison the metadata index (the lowest-level seam the existing
// find() phantom-row guard tests use — see find-index-integrity-guard.test.ts,
// which does the mirror-image ADD case) so the predicate query
// enumeration:'index' issues (find({ type: Person })) never returns `staff` —
// a real canonical record the index has lost track of, the exact
// canon-present/index-missing state canonical mode exists to survive.
const mi = ( brain as any ) . metadataIndex
const original = mi . getIdsForFilter . bind ( mi )
mi . getIdsForFilter = async ( filter : any , opts? : any ) : Promise < string [ ] > = > {
const ids : string [ ] = await original ( filter , opts )
return ids . filter ( ( id : string ) = > id !== staff )
}
try {
const indexExport = await brain . export ( { type : NounType . Person } , { enumeration : 'index' } )
expect ( indexExport . entities . map ( ( e ) = > e . id ) ) . not . toContain ( staff )
expect ( indexExport . entities . map ( ( e ) = > e . id ) ) . toContain ( other )
const canonicalExport = await brain . export (
{ type : NounType . Person } ,
{ enumeration : 'canonical' , reportIndexDrift : true }
)
expect ( canonicalExport . entities . map ( ( e ) = > e . id ) ) . toContain ( staff )
expect ( canonicalExport . entities . map ( ( e ) = > e . id ) ) . toContain ( other )
expect ( canonicalExport . drift ? . canonicalOnly ) . toEqual ( [ staff ] )
expect ( canonicalExport . drift ? . indexOnly ) . toEqual ( [ ] )
} finally {
mi . getIdsForFilter = original
}
} )
it ( '(iii) drift report shape + loud console.warn only when nonzero' , async ( ) = > {
const staff = await brain . add ( { data : 'Staff' , type : NounType . Person , subtype : 'employee' } )
await brain . add ( { data : 'Other' , type : NounType . Person , subtype : 'employee' } )
const mi = ( brain as any ) . metadataIndex
const original = mi . getIdsForFilter . bind ( mi )
mi . getIdsForFilter = async ( filter : any , opts? : any ) : Promise < string [ ] > = > {
const ids : string [ ] = await original ( filter , opts )
return ids . filter ( ( id : string ) = > id !== staff )
}
const warnSpy = vi . spyOn ( console , 'warn' ) . mockImplementation ( ( ) = > { } )
try {
const drifted = await brain . export (
{ type : NounType . Person } ,
{ enumeration : 'canonical' , reportIndexDrift : true }
)
expect ( drifted . drift ) . toEqual ( { canonicalOnly : [ staff ] , indexOnly : [ ] } )
expect ( warnSpy ) . toHaveBeenCalledTimes ( 1 )
expect ( warnSpy . mock . calls [ 0 ] . join ( ' ' ) ) . toMatch ( /drift/i )
} finally {
mi . getIdsForFilter = original
warnSpy . mockClear ( )
}
// Healthy index: drift is reported (both lists present) but never warned about.
try {
const healthy = await brain . export (
{ type : NounType . Person } ,
{ enumeration : 'canonical' , reportIndexDrift : true }
)
expect ( healthy . drift ) . toEqual ( { canonicalOnly : [ ] , indexOnly : [ ] } )
expect ( warnSpy ) . not . toHaveBeenCalled ( )
} finally {
warnSpy . mockRestore ( )
}
} )
it ( '(iv) throws CanonicalEnumerationUnavailableError on a historical asOf() view and a speculative with() overlay' , async ( ) = > {
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 {
await expect ( past . export ( { } , { enumeration : 'canonical' } ) ) . rejects . toThrow (
CanonicalEnumerationUnavailableError
)
// The default (index) mode is unaffected — still a valid time-travel export.
const backup = await past . export ( )
expect ( backup . entities . map ( ( e ) = > e . id ) ) . toContain ( a )
} finally {
await past . release ( )
}
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 {
await expect ( view . export ( { } , { enumeration : 'canonical' } ) ) . rejects . toThrow (
CanonicalEnumerationUnavailableError
)
} finally {
await view . release ( )
}
} )
it ( 'throws a plain Error when enumeration:"canonical" has no storage adapter to walk' , async ( ) = > {
const { exportGraph } = await import ( '../../../src/db/portableGraph' )
const readerOnly = { get : async ( ) = > null , find : async ( ) = > [ ] , related : async ( ) = > [ ] }
await expect (
exportGraph ( readerOnly as any , undefined , { } , { enumeration : 'canonical' } )
) . rejects . toThrow ( /enumeration:'canonical' requires a storage adapter/ )
} )
} )
2026-07-27 11:22:25 -07:00
describe ( '8.0 export includeHidden — every visibility tier for migration-grade canon completeness' , ( ) = > {
// The fixed-id VFS root Brainy.init() always creates is the one 'system'-visibility
// entity a consumer can rely on existing (visibility:'system' is not settable via the
// public add() API — "intentionally not accepted", per AddParams.visibility's doc).
const VFS_ROOT_ID = '00000000-0000-0000-0000-000000000000'
let brain : Brainy
beforeEach ( async ( ) = > {
brain = new Brainy ( createTestConfig ( ) )
await brain . init ( )
} )
afterEach ( async ( ) = > {
await brain . close ( )
} )
it ( 'canonical + includeHidden carries an internal row AND the system row; round-trips through import' , async ( ) = > {
const publicId = await brain . add ( { data : 'Public' , type : NounType . Thing , subtype : 'x' } )
const internalId = await brain . add ( {
data : 'Internal' ,
type : NounType . Thing ,
subtype : 'x' ,
visibility : 'internal'
} )
const migrationExport = await brain . export ( { } , { enumeration : 'canonical' , includeHidden : true } )
const ids = migrationExport . entities . map ( ( e ) = > e . id )
expect ( ids ) . toContain ( publicId )
expect ( ids ) . toContain ( internalId )
expect ( ids ) . toContain ( VFS_ROOT_ID )
expect ( migrationExport . entities . find ( ( e ) = > e . id === internalId ) ? . visibility ) . toBe ( 'internal' )
expect ( migrationExport . entities . find ( ( e ) = > e . id === VFS_ROOT_ID ) ? . visibility ) . toBe ( 'system' )
const target = new Brainy ( createTestConfig ( ) )
await target . init ( )
try {
const result = await target . import ( migrationExport )
expect ( result . errors ) . toHaveLength ( 0 )
expect ( ( await target . get ( internalId ) ) ? . visibility ) . toBe ( 'internal' )
} finally {
await target . close ( )
}
} )
it ( 'default export (includeHidden omitted) still excludes both hidden tiers — pins today\'s behavior' , async ( ) = > {
const publicId = await brain . add ( { data : 'Public' , type : NounType . Thing , subtype : 'x' } )
const internalId = await brain . add ( {
data : 'Internal' ,
type : NounType . Thing ,
subtype : 'x' ,
visibility : 'internal'
} )
for ( const opts of [ { enumeration : 'index' as const } , { enumeration : 'canonical' as const } ] ) {
const backup = await brain . export ( { } , opts )
const ids = backup . entities . map ( ( e ) = > e . id )
expect ( ids ) . toContain ( publicId )
expect ( ids ) . not . toContain ( internalId )
expect ( ids ) . not . toContain ( VFS_ROOT_ID )
}
} )
it ( 'index mode + includeHidden also reaches both tiers — find() takes includeInternal + includeSystem in one pass' , async ( ) = > {
const publicId = await brain . add ( { data : 'Public' , type : NounType . Thing , subtype : 'x' } )
const internalId = await brain . add ( {
data : 'Internal' ,
type : NounType . Thing ,
subtype : 'x' ,
visibility : 'internal'
} )
const indexExport = await brain . export ( { } , { enumeration : 'index' , includeHidden : true } )
const canonicalExport = await brain . export ( { } , { enumeration : 'canonical' , includeHidden : true } )
const indexIds = indexExport . entities . map ( ( e ) = > e . id ) . sort ( )
const canonicalIds = canonicalExport . entities . map ( ( e ) = > e . id ) . sort ( )
expect ( indexIds ) . toEqual ( canonicalIds )
expect ( indexIds ) . toContain ( publicId )
expect ( indexIds ) . toContain ( internalId )
expect ( indexIds ) . toContain ( VFS_ROOT_ID )
} )
it ( 'drift stays pure under includeHidden — no tier-policy noise when the index is healthy' , async ( ) = > {
await brain . add ( { data : 'Public' , type : NounType . Thing , subtype : 'x' } )
await brain . add ( { data : 'Internal' , type : NounType . Thing , subtype : 'x' , visibility : 'internal' } )
const audited = await brain . export (
{ } ,
{ enumeration : 'canonical' , includeHidden : true , reportIndexDrift : true }
)
expect ( audited . drift ) . toEqual ( { canonicalOnly : [ ] , indexOnly : [ ] } )
} )
} )