feat: portable graph export()/import() (BackupData v1) on brain.data()
brain.data() now serializes part or all of a brain to a versioned, portable
JSON document (BackupData) and restores it — the path for partial backups,
cross-environment moves, and 7.x→8.0 migration.
- export(selector?, options?): select by ids, collection (+transitive Contains),
connected neighbourhood, vfsPath subtree, predicate, or whole brain; structural
and predicate selectors compose. Options: includeVectors, includeContent (VFS
blobs), includeSystem, edges ('induced'|'incident'|'none').
- import(backup, options?): onConflict 'merge' (dedup-by-id) | 'replace' | 'skip';
reembed 'auto' (re-embed from data when no vector carried) | 'never'; remapIds
to clone a subgraph under fresh ids.
- BackupData v1: format/formatVersion/brainyVersion/createdAt/embedding/entities/
relations/blobs?/danglingIds?/stats. Reserved fields (subtype, data, confidence,
weight, service) top-level; metadata custom-only. Current-state, no generations.
- Replaces the prior flat-entity export() (dropped relations) with a graph-complete
document. Distinct from brain.import(file) ingestion, which is unchanged.
- Export BackupData/BackupEntity/BackupRelation/ExportSelector/ExportOptions/
ImportOptions/ImportResult/DataAPI from the package root. CLI `brainy export`
now writes a BackupData document.
Guide: docs/guides/backup-and-export.md. Tests: tests/unit/api/data-backup.test.ts.
2026-06-16 15:52:42 -07:00
|
|
|
/**
|
|
|
|
|
* Unit tests for the portable graph backup/restore API (brain.data()).
|
|
|
|
|
*
|
refactor: rename BackupData → PortableGraph (the type is interchange, not a backup)
Parity with the 8.0 rename, backported to the 7.x line. The brain.data()
export()/import() document type was BackupData, but it is the portable, versioned
interchange representation of a graph (entities + relations + optional vectors),
NOT a backup — exported for transport between instances, versions, and products.
The actual backup is the native snapshot, so "Backup*" mis-signalled.
Rename every developer-visible symbol, JSDoc, comment and doc:
- BackupData→PortableGraph, BackupEntity→PortableGraphEntity,
BackupRelation→PortableGraphRelation, BACKUP_FORMAT[_VERSION]→
PORTABLE_GRAPH_FORMAT[_VERSION], internal toBackup* helpers→toPortableGraph*.
- src/api/DataAPI.ts, src/index.ts, src/cli/commands/core.ts, docs, and the test
(renamed data-backup.test.ts → data-portable-graph.test.ts).
The on-the-wire `format` tag is also renamed 'brainy-backup' → 'brainy-portable-graph':
the export/import 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 forward. No deprecated aliases (nothing to alias).
1505 unit green; build clean; data-portable-graph.test.ts 20/20.
2026-06-19 12:15:07 -07:00
|
|
|
* Exercises PortableGraph v1 export()/import(): real round-trips against in-memory
|
feat: portable graph export()/import() (BackupData v1) on brain.data()
brain.data() now serializes part or all of a brain to a versioned, portable
JSON document (BackupData) and restores it — the path for partial backups,
cross-environment moves, and 7.x→8.0 migration.
- export(selector?, options?): select by ids, collection (+transitive Contains),
connected neighbourhood, vfsPath subtree, predicate, or whole brain; structural
and predicate selectors compose. Options: includeVectors, includeContent (VFS
blobs), includeSystem, edges ('induced'|'incident'|'none').
- import(backup, options?): onConflict 'merge' (dedup-by-id) | 'replace' | 'skip';
reembed 'auto' (re-embed from data when no vector carried) | 'never'; remapIds
to clone a subgraph under fresh ids.
- BackupData v1: format/formatVersion/brainyVersion/createdAt/embedding/entities/
relations/blobs?/danglingIds?/stats. Reserved fields (subtype, data, confidence,
weight, service) top-level; metadata custom-only. Current-state, no generations.
- Replaces the prior flat-entity export() (dropped relations) with a graph-complete
document. Distinct from brain.import(file) ingestion, which is unchanged.
- Export BackupData/BackupEntity/BackupRelation/ExportSelector/ExportOptions/
ImportOptions/ImportResult/DataAPI from the package root. CLI `brainy export`
now writes a BackupData document.
Guide: docs/guides/backup-and-export.md. Tests: tests/unit/api/data-backup.test.ts.
2026-06-16 15:52:42 -07:00
|
|
|
* storage (no mocks of the API under test) — selectors, edge policy, vectors,
|
|
|
|
|
* conflict handling, id remapping, subtype fidelity, and cross-version guards.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
|
|
|
import { randomUUID } from 'node:crypto'
|
|
|
|
|
import { Brainy } from '../../../src/brainy'
|
|
|
|
|
import { createTestConfig } from '../../helpers/test-factory'
|
|
|
|
|
import { NounType, VerbType } from '../../../src/types/graphTypes'
|
refactor: rename BackupData → PortableGraph (the type is interchange, not a backup)
Parity with the 8.0 rename, backported to the 7.x line. The brain.data()
export()/import() document type was BackupData, but it is the portable, versioned
interchange representation of a graph (entities + relations + optional vectors),
NOT a backup — exported for transport between instances, versions, and products.
The actual backup is the native snapshot, so "Backup*" mis-signalled.
Rename every developer-visible symbol, JSDoc, comment and doc:
- BackupData→PortableGraph, BackupEntity→PortableGraphEntity,
BackupRelation→PortableGraphRelation, BACKUP_FORMAT[_VERSION]→
PORTABLE_GRAPH_FORMAT[_VERSION], internal toBackup* helpers→toPortableGraph*.
- src/api/DataAPI.ts, src/index.ts, src/cli/commands/core.ts, docs, and the test
(renamed data-backup.test.ts → data-portable-graph.test.ts).
The on-the-wire `format` tag is also renamed 'brainy-backup' → 'brainy-portable-graph':
the export/import 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 forward. No deprecated aliases (nothing to alias).
1505 unit green; build clean; data-portable-graph.test.ts 20/20.
2026-06-19 12:15:07 -07:00
|
|
|
import type { PortableGraph } from '../../../src/api/DataAPI'
|
feat: portable graph export()/import() (BackupData v1) on brain.data()
brain.data() now serializes part or all of a brain to a versioned, portable
JSON document (BackupData) and restores it — the path for partial backups,
cross-environment moves, and 7.x→8.0 migration.
- export(selector?, options?): select by ids, collection (+transitive Contains),
connected neighbourhood, vfsPath subtree, predicate, or whole brain; structural
and predicate selectors compose. Options: includeVectors, includeContent (VFS
blobs), includeSystem, edges ('induced'|'incident'|'none').
- import(backup, options?): onConflict 'merge' (dedup-by-id) | 'replace' | 'skip';
reembed 'auto' (re-embed from data when no vector carried) | 'never'; remapIds
to clone a subgraph under fresh ids.
- BackupData v1: format/formatVersion/brainyVersion/createdAt/embedding/entities/
relations/blobs?/danglingIds?/stats. Reserved fields (subtype, data, confidence,
weight, service) top-level; metadata custom-only. Current-state, no generations.
- Replaces the prior flat-entity export() (dropped relations) with a graph-complete
document. Distinct from brain.import(file) ingestion, which is unchanged.
- Export BackupData/BackupEntity/BackupRelation/ExportSelector/ExportOptions/
ImportOptions/ImportResult/DataAPI from the package root. CLI `brainy export`
now writes a BackupData document.
Guide: docs/guides/backup-and-export.md. Tests: tests/unit/api/data-backup.test.ts.
2026-06-16 15:52:42 -07:00
|
|
|
|
|
|
|
|
const VFS_ROOT_ID = '00000000-0000-0000-0000-000000000000'
|
|
|
|
|
|
refactor: rename BackupData → PortableGraph (the type is interchange, not a backup)
Parity with the 8.0 rename, backported to the 7.x line. The brain.data()
export()/import() document type was BackupData, but it is the portable, versioned
interchange representation of a graph (entities + relations + optional vectors),
NOT a backup — exported for transport between instances, versions, and products.
The actual backup is the native snapshot, so "Backup*" mis-signalled.
Rename every developer-visible symbol, JSDoc, comment and doc:
- BackupData→PortableGraph, BackupEntity→PortableGraphEntity,
BackupRelation→PortableGraphRelation, BACKUP_FORMAT[_VERSION]→
PORTABLE_GRAPH_FORMAT[_VERSION], internal toBackup* helpers→toPortableGraph*.
- src/api/DataAPI.ts, src/index.ts, src/cli/commands/core.ts, docs, and the test
(renamed data-backup.test.ts → data-portable-graph.test.ts).
The on-the-wire `format` tag is also renamed 'brainy-backup' → 'brainy-portable-graph':
the export/import 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 forward. No deprecated aliases (nothing to alias).
1505 unit green; build clean; data-portable-graph.test.ts 20/20.
2026-06-19 12:15:07 -07:00
|
|
|
describe('DataAPI — portable graph backup/restore (PortableGraph v1)', () => {
|
feat: portable graph export()/import() (BackupData v1) on brain.data()
brain.data() now serializes part or all of a brain to a versioned, portable
JSON document (BackupData) and restores it — the path for partial backups,
cross-environment moves, and 7.x→8.0 migration.
- export(selector?, options?): select by ids, collection (+transitive Contains),
connected neighbourhood, vfsPath subtree, predicate, or whole brain; structural
and predicate selectors compose. Options: includeVectors, includeContent (VFS
blobs), includeSystem, edges ('induced'|'incident'|'none').
- import(backup, options?): onConflict 'merge' (dedup-by-id) | 'replace' | 'skip';
reembed 'auto' (re-embed from data when no vector carried) | 'never'; remapIds
to clone a subgraph under fresh ids.
- BackupData v1: format/formatVersion/brainyVersion/createdAt/embedding/entities/
relations/blobs?/danglingIds?/stats. Reserved fields (subtype, data, confidence,
weight, service) top-level; metadata custom-only. Current-state, no generations.
- Replaces the prior flat-entity export() (dropped relations) with a graph-complete
document. Distinct from brain.import(file) ingestion, which is unchanged.
- Export BackupData/BackupEntity/BackupRelation/ExportSelector/ExportOptions/
ImportOptions/ImportResult/DataAPI from the package root. CLI `brainy export`
now writes a BackupData document.
Guide: docs/guides/backup-and-export.md. Tests: tests/unit/api/data-backup.test.ts.
2026-06-16 15:52:42 -07:00
|
|
|
let brain: Brainy
|
|
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
|
|
|
|
brain = new Brainy(createTestConfig())
|
|
|
|
|
await brain.init()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
afterEach(async () => {
|
|
|
|
|
await brain.close()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('format + whole-brain round-trip', () => {
|
refactor: rename BackupData → PortableGraph (the type is interchange, not a backup)
Parity with the 8.0 rename, backported to the 7.x line. The brain.data()
export()/import() document type was BackupData, but it is the portable, versioned
interchange representation of a graph (entities + relations + optional vectors),
NOT a backup — exported for transport between instances, versions, and products.
The actual backup is the native snapshot, so "Backup*" mis-signalled.
Rename every developer-visible symbol, JSDoc, comment and doc:
- BackupData→PortableGraph, BackupEntity→PortableGraphEntity,
BackupRelation→PortableGraphRelation, BACKUP_FORMAT[_VERSION]→
PORTABLE_GRAPH_FORMAT[_VERSION], internal toBackup* helpers→toPortableGraph*.
- src/api/DataAPI.ts, src/index.ts, src/cli/commands/core.ts, docs, and the test
(renamed data-backup.test.ts → data-portable-graph.test.ts).
The on-the-wire `format` tag is also renamed 'brainy-backup' → 'brainy-portable-graph':
the export/import 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 forward. No deprecated aliases (nothing to alias).
1505 unit green; build clean; data-portable-graph.test.ts 20/20.
2026-06-19 12:15:07 -07:00
|
|
|
it('exports a self-describing, versioned PortableGraph document', async () => {
|
feat: portable graph export()/import() (BackupData v1) on brain.data()
brain.data() now serializes part or all of a brain to a versioned, portable
JSON document (BackupData) and restores it — the path for partial backups,
cross-environment moves, and 7.x→8.0 migration.
- export(selector?, options?): select by ids, collection (+transitive Contains),
connected neighbourhood, vfsPath subtree, predicate, or whole brain; structural
and predicate selectors compose. Options: includeVectors, includeContent (VFS
blobs), includeSystem, edges ('induced'|'incident'|'none').
- import(backup, options?): onConflict 'merge' (dedup-by-id) | 'replace' | 'skip';
reembed 'auto' (re-embed from data when no vector carried) | 'never'; remapIds
to clone a subgraph under fresh ids.
- BackupData v1: format/formatVersion/brainyVersion/createdAt/embedding/entities/
relations/blobs?/danglingIds?/stats. Reserved fields (subtype, data, confidence,
weight, service) top-level; metadata custom-only. Current-state, no generations.
- Replaces the prior flat-entity export() (dropped relations) with a graph-complete
document. Distinct from brain.import(file) ingestion, which is unchanged.
- Export BackupData/BackupEntity/BackupRelation/ExportSelector/ExportOptions/
ImportOptions/ImportResult/DataAPI from the package root. CLI `brainy export`
now writes a BackupData document.
Guide: docs/guides/backup-and-export.md. Tests: tests/unit/api/data-backup.test.ts.
2026-06-16 15:52:42 -07:00
|
|
|
const a = await brain.add({ data: 'Alice', type: NounType.Person, subtype: 'employee' })
|
|
|
|
|
const b = await brain.add({ data: 'Acme', type: NounType.Organization })
|
|
|
|
|
await brain.relate({ from: a, to: b, type: VerbType.WorksWith, subtype: 'full-time' })
|
|
|
|
|
|
|
|
|
|
const backup = await brain.data().then((d) => d.export())
|
|
|
|
|
|
refactor: rename BackupData → PortableGraph (the type is interchange, not a backup)
Parity with the 8.0 rename, backported to the 7.x line. The brain.data()
export()/import() document type was BackupData, but it is the portable, versioned
interchange representation of a graph (entities + relations + optional vectors),
NOT a backup — exported for transport between instances, versions, and products.
The actual backup is the native snapshot, so "Backup*" mis-signalled.
Rename every developer-visible symbol, JSDoc, comment and doc:
- BackupData→PortableGraph, BackupEntity→PortableGraphEntity,
BackupRelation→PortableGraphRelation, BACKUP_FORMAT[_VERSION]→
PORTABLE_GRAPH_FORMAT[_VERSION], internal toBackup* helpers→toPortableGraph*.
- src/api/DataAPI.ts, src/index.ts, src/cli/commands/core.ts, docs, and the test
(renamed data-backup.test.ts → data-portable-graph.test.ts).
The on-the-wire `format` tag is also renamed 'brainy-backup' → 'brainy-portable-graph':
the export/import 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 forward. No deprecated aliases (nothing to alias).
1505 unit green; build clean; data-portable-graph.test.ts 20/20.
2026-06-19 12:15:07 -07:00
|
|
|
expect(backup.format).toBe('brainy-portable-graph')
|
feat: portable graph export()/import() (BackupData v1) on brain.data()
brain.data() now serializes part or all of a brain to a versioned, portable
JSON document (BackupData) and restores it — the path for partial backups,
cross-environment moves, and 7.x→8.0 migration.
- export(selector?, options?): select by ids, collection (+transitive Contains),
connected neighbourhood, vfsPath subtree, predicate, or whole brain; structural
and predicate selectors compose. Options: includeVectors, includeContent (VFS
blobs), includeSystem, edges ('induced'|'incident'|'none').
- import(backup, options?): onConflict 'merge' (dedup-by-id) | 'replace' | 'skip';
reembed 'auto' (re-embed from data when no vector carried) | 'never'; remapIds
to clone a subgraph under fresh ids.
- BackupData v1: format/formatVersion/brainyVersion/createdAt/embedding/entities/
relations/blobs?/danglingIds?/stats. Reserved fields (subtype, data, confidence,
weight, service) top-level; metadata custom-only. Current-state, no generations.
- Replaces the prior flat-entity export() (dropped relations) with a graph-complete
document. Distinct from brain.import(file) ingestion, which is unchanged.
- Export BackupData/BackupEntity/BackupRelation/ExportSelector/ExportOptions/
ImportOptions/ImportResult/DataAPI from the package root. CLI `brainy export`
now writes a BackupData document.
Guide: docs/guides/backup-and-export.md. Tests: tests/unit/api/data-backup.test.ts.
2026-06-16 15:52:42 -07:00
|
|
|
expect(backup.formatVersion).toBe(1)
|
|
|
|
|
expect(typeof backup.brainyVersion).toBe('string')
|
|
|
|
|
expect(backup.brainyVersion.length).toBeGreaterThan(0)
|
|
|
|
|
expect(typeof backup.createdAt).toBe('string')
|
|
|
|
|
expect(backup.embedding.dimensions).toBeGreaterThan(0)
|
|
|
|
|
expect(backup.stats.entityCount).toBe(backup.entities.length)
|
|
|
|
|
expect(backup.stats.relationCount).toBe(backup.relations.length)
|
|
|
|
|
// Two user entities, one relation (VFS root excluded by default).
|
|
|
|
|
expect(backup.entities.map((e) => e.id).sort()).toEqual([a, b].sort())
|
|
|
|
|
expect(backup.relations).toHaveLength(1)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('round-trips entities and relations through clear() + import()', async () => {
|
|
|
|
|
const a = await brain.add({ data: 'Alice', type: NounType.Person })
|
|
|
|
|
const b = await brain.add({ data: 'Bob', type: NounType.Person })
|
|
|
|
|
const c = await brain.add({ data: 'Carol', type: NounType.Person })
|
|
|
|
|
await brain.relate({ from: a, to: b, type: VerbType.FriendOf })
|
|
|
|
|
await brain.relate({ from: b, to: c, type: VerbType.FriendOf })
|
|
|
|
|
|
|
|
|
|
const backup = await brain.data().then((d) => d.export({}, { includeVectors: true }))
|
|
|
|
|
|
|
|
|
|
await brain.data().then((d) => d.clear())
|
|
|
|
|
const afterClear = await brain.find({ limit: 1000 })
|
|
|
|
|
expect(afterClear.filter((r) => [a, b, c].includes(r.id))).toHaveLength(0)
|
|
|
|
|
|
|
|
|
|
const result = await brain.data().then((d) => d.import(backup))
|
|
|
|
|
expect(result.imported).toBe(3)
|
|
|
|
|
expect(result.errors).toHaveLength(0)
|
|
|
|
|
|
|
|
|
|
const ra = await brain.get(a)
|
|
|
|
|
expect(ra?.id).toBe(a)
|
|
|
|
|
const rels = await brain.getRelations({ from: a })
|
|
|
|
|
expect(rels.some((r) => r.to === b && r.type === VerbType.FriendOf)).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('preserves subtype on both entities and relations', async () => {
|
|
|
|
|
const a = await brain.add({ data: 'Doc', type: NounType.Document, subtype: 'invoice' })
|
|
|
|
|
const b = await brain.add({ data: 'Doc2', type: NounType.Document, subtype: 'receipt' })
|
|
|
|
|
await brain.relate({ from: a, to: b, type: VerbType.RelatedTo, subtype: 'supersedes' })
|
|
|
|
|
|
|
|
|
|
const backup = await brain.data().then((d) => d.export())
|
|
|
|
|
|
|
|
|
|
const ea = backup.entities.find((e) => e.id === a)
|
|
|
|
|
expect(ea?.subtype).toBe('invoice')
|
|
|
|
|
expect(backup.relations[0].subtype).toBe('supersedes')
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('selectors', () => {
|
|
|
|
|
it('ids — exports exactly the requested entities', async () => {
|
|
|
|
|
const a = await brain.add({ data: 'A', type: NounType.Thing })
|
|
|
|
|
const b = await brain.add({ data: 'B', type: NounType.Thing })
|
|
|
|
|
await brain.add({ data: 'C', type: NounType.Thing })
|
|
|
|
|
|
|
|
|
|
const backup = await brain.data().then((d) => d.export({ ids: [a, b] }))
|
|
|
|
|
expect(backup.entities.map((e) => e.id).sort()).toEqual([a, b].sort())
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('collection — exports the collection plus its transitive Contains members', async () => {
|
|
|
|
|
const root = await brain.add({ data: 'Folder', type: NounType.Collection })
|
|
|
|
|
const child1 = await brain.add({ data: 'Child1', type: NounType.Document })
|
|
|
|
|
const child2 = await brain.add({ data: 'Child2', type: NounType.Document })
|
|
|
|
|
const grandchild = await brain.add({ data: 'Grandchild', type: NounType.Document })
|
|
|
|
|
await brain.add({ data: 'Outside', type: NounType.Document })
|
|
|
|
|
await brain.relate({ from: root, to: child1, type: VerbType.Contains })
|
|
|
|
|
await brain.relate({ from: root, to: child2, type: VerbType.Contains })
|
|
|
|
|
await brain.relate({ from: child1, to: grandchild, type: VerbType.Contains })
|
|
|
|
|
|
|
|
|
|
const backup = await brain.data().then((d) => d.export({ collection: root }))
|
|
|
|
|
expect(backup.entities.map((e) => e.id).sort()).toEqual(
|
|
|
|
|
[root, child1, child2, grandchild].sort()
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('connected — exports an N-hop neighbourhood', async () => {
|
|
|
|
|
const a = await brain.add({ data: 'A', type: NounType.Thing })
|
|
|
|
|
const b = await brain.add({ data: 'B', type: NounType.Thing })
|
|
|
|
|
const c = await brain.add({ data: 'C', type: NounType.Thing })
|
|
|
|
|
await brain.relate({ from: a, to: b, type: VerbType.RelatedTo })
|
|
|
|
|
await brain.relate({ from: b, to: c, type: VerbType.RelatedTo })
|
|
|
|
|
|
|
|
|
|
const depth1 = await brain.data().then((d) => d.export({ connected: { from: a, depth: 1 } }))
|
|
|
|
|
expect(depth1.entities.map((e) => e.id).sort()).toEqual([a, b].sort())
|
|
|
|
|
|
|
|
|
|
const depth2 = await brain.data().then((d) => d.export({ connected: { from: a, depth: 2 } }))
|
|
|
|
|
expect(depth2.entities.map((e) => e.id).sort()).toEqual([a, b, c].sort())
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('predicate — filters by type', async () => {
|
|
|
|
|
await brain.add({ data: 'P', type: NounType.Person })
|
|
|
|
|
await brain.add({ data: 'D', type: NounType.Document })
|
|
|
|
|
const backup = await brain.data().then((d) => d.export({ type: NounType.Person }))
|
|
|
|
|
expect(backup.entities.every((e) => e.type === NounType.Person)).toBe(true)
|
|
|
|
|
expect(backup.entities).toHaveLength(1)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('compose — structural selector + predicate filter', async () => {
|
|
|
|
|
const root = await brain.add({ data: 'Folder', type: NounType.Collection })
|
|
|
|
|
const open = await brain.add({
|
|
|
|
|
data: 'Open',
|
|
|
|
|
type: NounType.Document,
|
|
|
|
|
metadata: { status: 'open' }
|
|
|
|
|
})
|
|
|
|
|
const closed = await brain.add({
|
|
|
|
|
data: 'Closed',
|
|
|
|
|
type: NounType.Document,
|
|
|
|
|
metadata: { status: 'closed' }
|
|
|
|
|
})
|
|
|
|
|
await brain.relate({ from: root, to: open, type: VerbType.Contains })
|
|
|
|
|
await brain.relate({ from: root, to: closed, type: VerbType.Contains })
|
|
|
|
|
|
|
|
|
|
const backup = await brain
|
|
|
|
|
.data()
|
|
|
|
|
.then((d) => d.export({ collection: root, where: { status: 'open' } }))
|
|
|
|
|
expect(backup.entities.map((e) => e.id)).toContain(open)
|
|
|
|
|
expect(backup.entities.map((e) => e.id)).not.toContain(closed)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('edge policy', () => {
|
|
|
|
|
let a: string
|
|
|
|
|
let b: string
|
|
|
|
|
let c: string
|
|
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
|
|
|
|
a = await brain.add({ data: 'A', type: NounType.Thing })
|
|
|
|
|
b = await brain.add({ data: 'B', type: NounType.Thing })
|
|
|
|
|
c = await brain.add({ data: 'C', type: NounType.Thing })
|
|
|
|
|
await brain.relate({ from: a, to: b, type: VerbType.RelatedTo }) // both in {a,b}
|
|
|
|
|
await brain.relate({ from: b, to: c, type: VerbType.RelatedTo }) // dangles to c
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('induced (default) — only edges with both endpoints in the set', async () => {
|
|
|
|
|
const backup = await brain.data().then((d) => d.export({ ids: [a, b] }))
|
|
|
|
|
expect(backup.relations).toHaveLength(1)
|
|
|
|
|
expect(backup.relations[0].from).toBe(a)
|
|
|
|
|
expect(backup.danglingIds).toBeUndefined()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('incident — includes dangling edges + records danglingIds', async () => {
|
|
|
|
|
const backup = await brain.data().then((d) => d.export({ ids: [a, b] }, { edges: 'incident' }))
|
|
|
|
|
expect(backup.relations).toHaveLength(2)
|
|
|
|
|
expect(backup.danglingIds).toContain(c)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('none — nodes only', async () => {
|
|
|
|
|
const backup = await brain.data().then((d) => d.export({ ids: [a, b] }, { edges: 'none' }))
|
|
|
|
|
expect(backup.relations).toHaveLength(0)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('vectors + re-embedding', () => {
|
|
|
|
|
it('omits vectors by default and re-embeds on import', async () => {
|
|
|
|
|
const a = await brain.add({ data: 'Re-embed me', type: NounType.Thing })
|
|
|
|
|
const backup = await brain.data().then((d) => d.export({ ids: [a] }))
|
|
|
|
|
expect(backup.entities[0].vector).toBeUndefined()
|
|
|
|
|
|
|
|
|
|
await brain.data().then((d) => d.clear())
|
|
|
|
|
const result = await brain.data().then((d) => d.import(backup))
|
|
|
|
|
expect(result.reembedded).toBe(1)
|
|
|
|
|
|
|
|
|
|
const restored = await brain.get(a, { includeVectors: true })
|
|
|
|
|
expect(restored?.vector?.length).toBeGreaterThan(0)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('carries vectors verbatim with includeVectors (no re-embed)', async () => {
|
|
|
|
|
const a = await brain.add({ data: 'Keep my vector', type: NounType.Thing })
|
|
|
|
|
const original = await brain.get(a, { includeVectors: true })
|
|
|
|
|
|
|
|
|
|
const backup = await brain.data().then((d) => d.export({ ids: [a] }, { includeVectors: true }))
|
|
|
|
|
expect(backup.entities[0].vector?.length).toBe(original?.vector?.length)
|
|
|
|
|
|
|
|
|
|
await brain.data().then((d) => d.clear())
|
|
|
|
|
const result = await brain.data().then((d) => d.import(backup))
|
|
|
|
|
expect(result.reembedded).toBe(0)
|
|
|
|
|
|
|
|
|
|
const restored = await brain.get(a, { includeVectors: true })
|
|
|
|
|
expect(restored?.vector).toEqual(original?.vector)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('reembed:never records an error when no vector is carried', async () => {
|
|
|
|
|
const a = await brain.add({ data: 'No vector', type: NounType.Thing })
|
|
|
|
|
const backup = await brain.data().then((d) => d.export({ ids: [a] })) // no vectors
|
|
|
|
|
await brain.data().then((d) => d.clear())
|
|
|
|
|
|
|
|
|
|
const result = await brain.data().then((d) => d.import(backup, { reembed: 'never' }))
|
|
|
|
|
expect(result.imported).toBe(0)
|
|
|
|
|
expect(result.errors).toHaveLength(1)
|
|
|
|
|
expect(result.errors[0].error).toMatch(/reembed:never/)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('conflict handling', () => {
|
|
|
|
|
it('merge (default) — updates existing entities in place', async () => {
|
|
|
|
|
const a = await brain.add({ data: 'V1', type: NounType.Thing, metadata: { n: 1 } })
|
|
|
|
|
const backup = await brain.data().then((d) => d.export({ ids: [a] }, { includeVectors: true }))
|
|
|
|
|
|
|
|
|
|
await brain.update({ id: a, metadata: { n: 99 }, merge: false })
|
|
|
|
|
const result = await brain.data().then((d) => d.import(backup, { onConflict: 'merge' }))
|
|
|
|
|
expect(result.merged).toBe(1)
|
|
|
|
|
expect(result.imported).toBe(0)
|
|
|
|
|
|
|
|
|
|
const restored = await brain.get(a)
|
|
|
|
|
expect(restored?.metadata?.n).toBe(1)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('skip — leaves existing entities untouched', async () => {
|
|
|
|
|
const a = await brain.add({ data: 'Orig', type: NounType.Thing, metadata: { n: 1 } })
|
|
|
|
|
const backup = await brain.data().then((d) => d.export({ ids: [a] }))
|
|
|
|
|
await brain.update({ id: a, metadata: { n: 2 }, merge: false })
|
|
|
|
|
|
|
|
|
|
const result = await brain.data().then((d) => d.import(backup, { onConflict: 'skip' }))
|
|
|
|
|
expect(result.skipped).toBe(1)
|
|
|
|
|
const restored = await brain.get(a)
|
|
|
|
|
expect(restored?.metadata?.n).toBe(2)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('id remapping (clone)', () => {
|
|
|
|
|
it('imports a subgraph under fresh ids', async () => {
|
|
|
|
|
const a = await brain.add({ data: 'A', type: NounType.Thing })
|
|
|
|
|
const b = await brain.add({ data: 'B', type: NounType.Thing })
|
|
|
|
|
await brain.relate({ from: a, to: b, type: VerbType.RelatedTo })
|
|
|
|
|
const backup = await brain.data().then((d) => d.export({ ids: [a, b] }, { includeVectors: true }))
|
|
|
|
|
|
|
|
|
|
const remap = new Map<string, string>([
|
|
|
|
|
[a, randomUUID()],
|
|
|
|
|
[b, randomUUID()]
|
|
|
|
|
])
|
|
|
|
|
const result = await brain
|
|
|
|
|
.data()
|
|
|
|
|
.then((d) => d.import(backup, { remapIds: (id) => remap.get(id) ?? id }))
|
|
|
|
|
expect(result.imported).toBe(2)
|
|
|
|
|
|
|
|
|
|
const clonedA = await brain.get(remap.get(a)!)
|
|
|
|
|
expect(clonedA?.id).toBe(remap.get(a))
|
|
|
|
|
// The original entities still exist (a clone, not a move).
|
|
|
|
|
expect((await brain.get(a))?.id).toBe(a)
|
|
|
|
|
const clonedRels = await brain.getRelations({ from: remap.get(a)! })
|
|
|
|
|
expect(clonedRels.some((r) => r.to === remap.get(b))).toBe(true)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('system entities', () => {
|
|
|
|
|
it('excludes the VFS root from a whole-brain export by default', async () => {
|
|
|
|
|
await brain.add({ data: 'User entity', type: NounType.Thing })
|
|
|
|
|
const backup = await brain.data().then((d) => d.export())
|
|
|
|
|
expect(backup.entities.map((e) => e.id)).not.toContain(VFS_ROOT_ID)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe('import validation', () => {
|
refactor: rename BackupData → PortableGraph (the type is interchange, not a backup)
Parity with the 8.0 rename, backported to the 7.x line. The brain.data()
export()/import() document type was BackupData, but it is the portable, versioned
interchange representation of a graph (entities + relations + optional vectors),
NOT a backup — exported for transport between instances, versions, and products.
The actual backup is the native snapshot, so "Backup*" mis-signalled.
Rename every developer-visible symbol, JSDoc, comment and doc:
- BackupData→PortableGraph, BackupEntity→PortableGraphEntity,
BackupRelation→PortableGraphRelation, BACKUP_FORMAT[_VERSION]→
PORTABLE_GRAPH_FORMAT[_VERSION], internal toBackup* helpers→toPortableGraph*.
- src/api/DataAPI.ts, src/index.ts, src/cli/commands/core.ts, docs, and the test
(renamed data-backup.test.ts → data-portable-graph.test.ts).
The on-the-wire `format` tag is also renamed 'brainy-backup' → 'brainy-portable-graph':
the export/import 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 forward. No deprecated aliases (nothing to alias).
1505 unit green; build clean; data-portable-graph.test.ts 20/20.
2026-06-19 12:15:07 -07:00
|
|
|
it('rejects a non-PortableGraph payload', async () => {
|
feat: portable graph export()/import() (BackupData v1) on brain.data()
brain.data() now serializes part or all of a brain to a versioned, portable
JSON document (BackupData) and restores it — the path for partial backups,
cross-environment moves, and 7.x→8.0 migration.
- export(selector?, options?): select by ids, collection (+transitive Contains),
connected neighbourhood, vfsPath subtree, predicate, or whole brain; structural
and predicate selectors compose. Options: includeVectors, includeContent (VFS
blobs), includeSystem, edges ('induced'|'incident'|'none').
- import(backup, options?): onConflict 'merge' (dedup-by-id) | 'replace' | 'skip';
reembed 'auto' (re-embed from data when no vector carried) | 'never'; remapIds
to clone a subgraph under fresh ids.
- BackupData v1: format/formatVersion/brainyVersion/createdAt/embedding/entities/
relations/blobs?/danglingIds?/stats. Reserved fields (subtype, data, confidence,
weight, service) top-level; metadata custom-only. Current-state, no generations.
- Replaces the prior flat-entity export() (dropped relations) with a graph-complete
document. Distinct from brain.import(file) ingestion, which is unchanged.
- Export BackupData/BackupEntity/BackupRelation/ExportSelector/ExportOptions/
ImportOptions/ImportResult/DataAPI from the package root. CLI `brainy export`
now writes a BackupData document.
Guide: docs/guides/backup-and-export.md. Tests: tests/unit/api/data-backup.test.ts.
2026-06-16 15:52:42 -07:00
|
|
|
const d = await brain.data()
|
refactor: rename BackupData → PortableGraph (the type is interchange, not a backup)
Parity with the 8.0 rename, backported to the 7.x line. The brain.data()
export()/import() document type was BackupData, but it is the portable, versioned
interchange representation of a graph (entities + relations + optional vectors),
NOT a backup — exported for transport between instances, versions, and products.
The actual backup is the native snapshot, so "Backup*" mis-signalled.
Rename every developer-visible symbol, JSDoc, comment and doc:
- BackupData→PortableGraph, BackupEntity→PortableGraphEntity,
BackupRelation→PortableGraphRelation, BACKUP_FORMAT[_VERSION]→
PORTABLE_GRAPH_FORMAT[_VERSION], internal toBackup* helpers→toPortableGraph*.
- src/api/DataAPI.ts, src/index.ts, src/cli/commands/core.ts, docs, and the test
(renamed data-backup.test.ts → data-portable-graph.test.ts).
The on-the-wire `format` tag is also renamed 'brainy-backup' → 'brainy-portable-graph':
the export/import 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 forward. No deprecated aliases (nothing to alias).
1505 unit green; build clean; data-portable-graph.test.ts 20/20.
2026-06-19 12:15:07 -07:00
|
|
|
await expect(d.import({ entities: [] } as any)).rejects.toThrow(/PortableGraph/)
|
feat: portable graph export()/import() (BackupData v1) on brain.data()
brain.data() now serializes part or all of a brain to a versioned, portable
JSON document (BackupData) and restores it — the path for partial backups,
cross-environment moves, and 7.x→8.0 migration.
- export(selector?, options?): select by ids, collection (+transitive Contains),
connected neighbourhood, vfsPath subtree, predicate, or whole brain; structural
and predicate selectors compose. Options: includeVectors, includeContent (VFS
blobs), includeSystem, edges ('induced'|'incident'|'none').
- import(backup, options?): onConflict 'merge' (dedup-by-id) | 'replace' | 'skip';
reembed 'auto' (re-embed from data when no vector carried) | 'never'; remapIds
to clone a subgraph under fresh ids.
- BackupData v1: format/formatVersion/brainyVersion/createdAt/embedding/entities/
relations/blobs?/danglingIds?/stats. Reserved fields (subtype, data, confidence,
weight, service) top-level; metadata custom-only. Current-state, no generations.
- Replaces the prior flat-entity export() (dropped relations) with a graph-complete
document. Distinct from brain.import(file) ingestion, which is unchanged.
- Export BackupData/BackupEntity/BackupRelation/ExportSelector/ExportOptions/
ImportOptions/ImportResult/DataAPI from the package root. CLI `brainy export`
now writes a BackupData document.
Guide: docs/guides/backup-and-export.md. Tests: tests/unit/api/data-backup.test.ts.
2026-06-16 15:52:42 -07:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('rejects a newer formatVersion', async () => {
|
|
|
|
|
const d = await brain.data()
|
refactor: rename BackupData → PortableGraph (the type is interchange, not a backup)
Parity with the 8.0 rename, backported to the 7.x line. The brain.data()
export()/import() document type was BackupData, but it is the portable, versioned
interchange representation of a graph (entities + relations + optional vectors),
NOT a backup — exported for transport between instances, versions, and products.
The actual backup is the native snapshot, so "Backup*" mis-signalled.
Rename every developer-visible symbol, JSDoc, comment and doc:
- BackupData→PortableGraph, BackupEntity→PortableGraphEntity,
BackupRelation→PortableGraphRelation, BACKUP_FORMAT[_VERSION]→
PORTABLE_GRAPH_FORMAT[_VERSION], internal toBackup* helpers→toPortableGraph*.
- src/api/DataAPI.ts, src/index.ts, src/cli/commands/core.ts, docs, and the test
(renamed data-backup.test.ts → data-portable-graph.test.ts).
The on-the-wire `format` tag is also renamed 'brainy-backup' → 'brainy-portable-graph':
the export/import 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 forward. No deprecated aliases (nothing to alias).
1505 unit green; build clean; data-portable-graph.test.ts 20/20.
2026-06-19 12:15:07 -07:00
|
|
|
const future: PortableGraph = {
|
|
|
|
|
format: 'brainy-portable-graph',
|
feat: portable graph export()/import() (BackupData v1) on brain.data()
brain.data() now serializes part or all of a brain to a versioned, portable
JSON document (BackupData) and restores it — the path for partial backups,
cross-environment moves, and 7.x→8.0 migration.
- export(selector?, options?): select by ids, collection (+transitive Contains),
connected neighbourhood, vfsPath subtree, predicate, or whole brain; structural
and predicate selectors compose. Options: includeVectors, includeContent (VFS
blobs), includeSystem, edges ('induced'|'incident'|'none').
- import(backup, options?): onConflict 'merge' (dedup-by-id) | 'replace' | 'skip';
reembed 'auto' (re-embed from data when no vector carried) | 'never'; remapIds
to clone a subgraph under fresh ids.
- BackupData v1: format/formatVersion/brainyVersion/createdAt/embedding/entities/
relations/blobs?/danglingIds?/stats. Reserved fields (subtype, data, confidence,
weight, service) top-level; metadata custom-only. Current-state, no generations.
- Replaces the prior flat-entity export() (dropped relations) with a graph-complete
document. Distinct from brain.import(file) ingestion, which is unchanged.
- Export BackupData/BackupEntity/BackupRelation/ExportSelector/ExportOptions/
ImportOptions/ImportResult/DataAPI from the package root. CLI `brainy export`
now writes a BackupData document.
Guide: docs/guides/backup-and-export.md. Tests: tests/unit/api/data-backup.test.ts.
2026-06-16 15:52:42 -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(d.import(future)).rejects.toThrow(/formatVersion/)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
})
|