From 373a48122d5ae35978a567e2866205101cf85bda Mon Sep 17 00:00:00 2001 From: David Snelling Date: Fri, 19 Jun 2026 12:09:19 -0700 Subject: [PATCH] =?UTF-8?q?refactor(8.0):=20rename=20BackupData=20?= =?UTF-8?q?=E2=86=92=20PortableGraph=20(the=20type=20is=20interchange,=20n?= =?UTF-8?q?ot=20a=20backup)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- RELEASES.md | 21 ++- docs/api/README.md | 10 +- docs/guides/export-and-import.md | 32 ++-- src/brainy.ts | 20 +-- src/db/db.ts | 10 +- src/db/{backup.ts => portableGraph.ts} | 158 +++++++++--------- src/index.ts | 16 +- ...ckup.test.ts => db-portable-graph.test.ts} | 34 ++-- 8 files changed, 156 insertions(+), 145 deletions(-) rename src/db/{backup.ts => portableGraph.ts} (82%) rename tests/unit/db/{db-backup.test.ts => db-portable-graph.test.ts} (92%) diff --git a/RELEASES.md b/RELEASES.md index f41d67ef..cba016c9 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -80,7 +80,7 @@ historical records) are documented in: - [docs/ADR-001-generational-mvcc.md](docs/ADR-001-generational-mvcc.md) — the design record: persisted layout, commit protocol, crash recovery, proof table -### Portable export & import (BackupData v1) +### Portable export & import (PortableGraph v1) A portable, versioned graph format that serializes part or all of a brain to a single JSON document and restores it — the cross-environment, cross-version @@ -88,29 +88,34 @@ single JSON document and restores it — the cross-environment, cross-version ```ts // Export is a method on the immutable Db, so it composes with now()/asOf()/with() -const backup = await brain.export({ collection: id }, { includeVectors: true }) -await otherBrain.import(backup, { onConflict: 'merge' }) // dedup-by-id +const graph = await brain.export({ collection: id }, { includeVectors: true }) +await otherBrain.import(graph, { onConflict: 'merge' }) // dedup-by-id ;(await brain.asOf(gen)).export(sel) // time-travel export brain.now().with(ops).export(sel) // what-if export ``` - **`brain.export(selector?, options?)` / `db.export(...)`** → a versioned - `BackupData` document. Selectors reuse `find()`'s grammar: `{ ids }`, + `PortableGraph` document. Selectors reuse `find()`'s grammar: `{ ids }`, `{ collection }` (alias `memberOf`, transitive `Contains`), `{ connected: { from, depth } }`, `{ vfsPath }`, predicate (`{ type, subtype, where, service }`), or the whole brain (omit) — and they compose. Options: `includeVectors`, `includeContent` (VFS file bytes), `includeSystem`, `edges: 'induced' | 'incident' | 'none'`. -- **`brain.import(backup, options?)`** is polymorphic: a `BackupData` document is +- **`brain.import(graph, options?)`** is polymorphic: a `PortableGraph` document is restored as **one atomic transaction** (`onConflict: 'merge' | 'replace' | 'skip'`, `reembed: 'auto' | 'never'`, `remapIds` for cloning); a file/buffer routes to the existing CSV/PDF/Excel/JSON ingestion. No migration for ingestion callers. -- **`validateBackup(data)`** — dry-run structural/version/endpoint check before import. -- **Format** (`format:'brainy-backup'`, `formatVersion: 1`) is identical on 7.x - (7.32.0) and 8.0; standard fields (`subtype`/`visibility`/`data`/…) are top-level, +- **`validatePortableGraph(data)`** — dry-run structural/version/endpoint check before import. +- **Format** (`format:'brainy-portable-graph'`, `formatVersion: 1`) is identical on + 7.x and 8.0; standard fields (`subtype`/`visibility`/`data`/…) are top-level, `metadata` is custom-only. Current-state (no generation history — that lives in `persist()`). Types exported from the package root. +- **Naming:** the type is `PortableGraph` (with `PortableGraphEntity` / + `PortableGraphRelation`) — it is the portable interchange form of a graph, not a + backup (that role is `persist()`/`load()`). Renamed from the short-lived + `BackupData`/`'brainy-backup'` (introduced in 7.32.0, never adopted) with no + compatibility shim. - Guide: [docs/guides/export-and-import.md](docs/guides/export-and-import.md). ### Aggregation: distinctCount over any value type diff --git a/docs/api/README.md b/docs/api/README.md index 4a9dc3f6..89d101c4 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -1415,16 +1415,16 @@ await brain.import('https://api.example.com/data.json') ### Export & Import (portable) + Snapshots (native) -**Portable graph backup** — `brain.export()` / `brain.import()` (`BackupData` v1, versioned +**Portable graph export/import** — `brain.export()` / `brain.import()` (`PortableGraph` v1, versioned JSON, partial-or-whole, cross-version). `export()` lives on the immutable `Db`, so it composes with `now()`/`asOf()`/`with()`: ```typescript // Export part or all of the brain to a portable, versioned document -const backup = await brain.export({ ids }, { includeVectors: true }) +const graph = await brain.export({ ids }, { includeVectors: true }) -// Restore it — import() routes a BackupData to the graph round-trip (merge by id) -await otherBrain.import(backup, { onConflict: 'merge' }) +// Restore it — import() routes a PortableGraph to the graph round-trip (merge by id) +await otherBrain.import(graph, { onConflict: 'merge' }) // Time-travel export (serialize a past generation) / what-if export (a speculative state) const past = await brain.asOf(gen) @@ -1436,7 +1436,7 @@ await brain.now().with(ops).export({ ids }) Selectors: `{ ids }`, `{ collection }` (alias `memberOf`), `{ connected: { from, depth } }`, `{ vfsPath }`, predicate (`{ type, subtype, where, service }`), or whole brain (omit). See the **[Export & Import guide](../guides/export-and-import.md)**. Distinct from `brain.import(file)` -(CSV/PDF/Excel/JSON ingestion — `import()` dispatches on whether you pass a `BackupData` or a file). +(CSV/PDF/Excel/JSON ingestion — `import()` dispatches on whether you pass a `PortableGraph` or a file). **Native whole-brain snapshot** (generation-preserving, not portable JSON): diff --git a/docs/guides/export-and-import.md b/docs/guides/export-and-import.md index 45f3a47d..042728b1 100644 --- a/docs/guides/export-and-import.md +++ b/docs/guides/export-and-import.md @@ -5,7 +5,7 @@ public: true category: guides template: guide order: 9 -description: Export part or all of a brain to a portable, versioned BackupData document and import it back — by id, collection, connected neighbourhood, VFS subtree, predicate, or the whole brain. export() lives on the immutable Db, so asOf()/with() give time-travel and what-if exports. +description: Export part or all of a brain to a portable, versioned PortableGraph document and import it back — by id, collection, connected neighbourhood, VFS subtree, predicate, or the whole brain. export() lives on the immutable Db, so asOf()/with() give time-travel and what-if exports. next: - guides/subtypes-and-facets - api/README @@ -15,16 +15,16 @@ next: Brainy serializes part or all of a brain — an item, a collection, a connected neighbourhood, a VFS subtree, a predicate match, or the whole brain — into a single -versioned JSON document (`BackupData`), and restores it. +versioned JSON document (`PortableGraph`), and restores it. ```typescript -const backup = await brain.export() // whole brain → BackupData -await brain.import(backup) // restore (merge by id, re-embed if no vectors) +const graph = await brain.export() // whole brain → PortableGraph +await brain.import(graph) // restore (merge by id, re-embed if no vectors) ``` It is **portable** (human-readable JSON), **versioned** (`formatVersion`, so a document written by 7.x imports cleanly into 8.0), and **current-state** (the entities and edges as -they are now — no generation history). Use it for portable artifacts, partial backups, +they are now — no generation history). Use it for portable artifacts, partial exports, cross-environment moves, and version upgrades. `export()` is a method on the **immutable `Db` value**, so it composes with every way of @@ -44,14 +44,14 @@ brain.now().with(ops).export(sel) // what-if export (a specula | A whole-brain snapshot **with generation history** | `brain.now().persist(path)` / `Brainy.load(path)` (native) | | To ingest a CSV / PDF / Excel / JSON **file** as new entities | `brain.import(file)` — see [Import Anything](./import-anything.md) | -`import()` is **polymorphic**: hand it a `BackupData` and it does the graph round-trip; +`import()` is **polymorphic**: hand it a `PortableGraph` and it does the graph round-trip; hand it a file/buffer and it does foreign-file ingestion (dispatched on the document's -`format: 'brainy-backup'` tag). +`format: 'brainy-portable-graph'` tag). ## Exporting ```typescript -brain.export(selector?, options?): Promise +brain.export(selector?, options?): Promise // (also on any Db: brain.now().export(...), (await brain.asOf(g)).export(...)) ``` @@ -92,20 +92,20 @@ await brain.export({ ids: hits.map(r => r.id) }) ## Importing ```typescript -brain.import(backup, options?): Promise +brain.import(graph, options?): Promise ``` -The whole backup is applied as **one atomic transaction** — it advances the brain exactly +The whole graph is applied as **one atomic transaction** — it advances the brain exactly one generation, or none on failure. ```typescript -const result = await brain.import(backup, { onConflict: 'merge' }) +const result = await brain.import(graph, { onConflict: 'merge' }) // → { imported, merged, skipped, reembedded, blobsWritten, errors } ``` | Option | Default | Effect | |--------|---------|--------| -| `onConflict` | `'merge'` | `'merge'` (update existing id in place — assemble many backups), `'replace'` (delete + recreate), or `'skip'`. | +| `onConflict` | `'merge'` | `'merge'` (update existing id in place — assemble many exported graphs), `'replace'` (delete + recreate), or `'skip'`. | | `reembed` | `'auto'` | `'auto'` (use the carried vector, else re-embed from `data`) or `'never'` (require a carried vector; record an error if absent). | | `remapIds` | — | Rewrite every id on the way in, e.g. to clone a template subgraph under fresh ids. | | `meta` | — | Transaction metadata recorded in the tx-log alongside the new generation. | @@ -113,11 +113,11 @@ const result = await brain.import(backup, { onConflict: 'merge' }) The default `onConflict: 'merge'` lets you assemble one working graph from many exported documents that share entity ids — re-importing an id merges rather than duplicates. -## The `BackupData` format +## The `PortableGraph` format ```jsonc { - "format": "brainy-backup", + "format": "brainy-portable-graph", // identifies the document type "formatVersion": 1, // import gates on this (cross-version migration) "brainyVersion": "8.0.0", "createdAt": "2026-06-16T…Z", @@ -145,7 +145,7 @@ documents that share entity ids — re-importing an id merges rather than duplic Standard fields (`subtype`, `visibility`, `data`, `confidence`, `weight`, `service`) sit at the top level of each entity; `metadata` holds **only** custom user fields — mirroring the in-memory `Entity` shape, so `import()` maps each field to its dedicated parameter. The -TypeScript types (`BackupData`, `BackupEntity`, `BackupRelation`, `ExportSelector`, +TypeScript types (`PortableGraph`, `PortableGraphEntity`, `PortableGraphRelation`, `ExportSelector`, `ExportOptions`, `ImportOptions`, `ImportResult`) are exported from the package root. ## Generations & time-travel @@ -164,7 +164,7 @@ generation, so time-travel export differs across transaction boundaries. ## Cross-version (7.x → 8.0) -Because the document is shared and versioned, a backup written by 7.x imports into 8.0: +Because the document is shared and versioned, a PortableGraph written by 7.x imports into 8.0: `formatVersion` is read forward, `subtype` is carried so 8.0 re-types correctly, and the same 384-dimension model on both lines means `includeVectors:false` re-embeds identically (or `true` carries vectors verbatim). diff --git a/src/brainy.ts b/src/brainy.ts index b558a967..5e4f79cf 100644 --- a/src/brainy.ts +++ b/src/brainy.ts @@ -117,13 +117,13 @@ import * as fs from 'node:fs' import { Db, type DbHost, type HistoricalQueryHandle } from './db/db.js' import { importGraph, - isBackupData, - type BackupData, + isPortableGraph, + type PortableGraph, type ExportSelector, type ExportOptions, type ImportOptions, type ImportResult -} from './db/backup.js' +} from './db/portableGraph.js' import { GenerationStore } from './db/generationStore.js' import { isDeterministicEmbedMode } from './embeddings/deterministicEmbedMode.js' import { GenerationConflictError } from './db/errors.js' @@ -5074,22 +5074,22 @@ export class Brainy implements BrainyInterface { } /** - * @description Serialize part or all of the brain into a portable `BackupData` + * @description Serialize part or all of the brain into a portable `PortableGraph` * document — sugar for `brain.now().export(selector, options)` (the current * generation). For a past generation use `(await brain.asOf(g)).export(...)`; * for a speculative state use `brain.now().with(ops).export(...)`. * - * Restore a `BackupData` with {@link Brainy.import} (it dispatches a backup to the + * Restore a `PortableGraph` with {@link Brainy.import} (it dispatches a backup to the * graph round-trip; a file/buffer to ingestion). Distinct from `db.persist()` * (native whole-brain snapshot with generation history). * * @param selector - WHAT to export (omit for the whole brain). See {@link ExportSelector}. * @param options - HOW to export (vectors / VFS bytes / edge policy). See {@link ExportOptions}. - * @returns A versioned, portable `BackupData` document. + * @returns A versioned, portable `PortableGraph` document. * @example * const backup = await brain.export({ ids }, { includeVectors: true }) */ - async export(selector: ExportSelector = {}, options: ExportOptions = {}): Promise { + async export(selector: ExportSelector = {}, options: ExportOptions = {}): Promise { return this.now().export(selector, options) } @@ -6743,7 +6743,7 @@ export class Brainy implements BrainyInterface { * - Reduced confusion (removed redundant options) */ async import( - source: Buffer | string | object | BackupData, + source: Buffer | string | object | PortableGraph, options?: { format?: 'excel' | 'pdf' | 'csv' | 'json' | 'markdown' | 'yaml' | 'docx' | 'image' vfsPath?: string @@ -6770,9 +6770,9 @@ export class Brainy implements BrainyInterface { }) => void } & Partial ): Promise { - // Portable graph round-trip: a BackupData document (produced by export()) is + // Portable graph round-trip: a PortableGraph document (produced by export()) is // restored via ONE atomic transaction — distinct from foreign-file ingestion below. - if (isBackupData(source)) { + if (isPortableGraph(source)) { return importGraph(this, this.storage, source, options as ImportOptions) } diff --git a/src/db/db.ts b/src/db/db.ts index 40db407e..ca19163e 100644 --- a/src/db/db.ts +++ b/src/db/db.ts @@ -57,8 +57,8 @@ import type { Result } from '../types/brainy.types.js' import type { StorageAdapter } from '../coreTypes.js' -import { exportGraph } from './backup.js' -import type { ExportSelector, ExportOptions, BackupData } from './backup.js' +import { exportGraph } from './portableGraph.js' +import type { ExportSelector, ExportOptions, PortableGraph } from './portableGraph.js' import { splitNounMetadataRecord, splitVerbMetadataRecord @@ -416,7 +416,7 @@ export class Db { /** * @description Serialize part or all of this database value into a portable - * `BackupData` document, read **as of this view's generation**. Because `export` + * `PortableGraph` document, read **as of this view's generation**. Because `export` * lives on the immutable `Db`, it composes with every way of obtaining one: * `brain.now().export(sel)` (current), `(await brain.asOf(g)).export(sel)` * (time-travel export), and `brain.now().with(ops).export(sel)` (what-if export). @@ -427,11 +427,11 @@ export class Db { * * @param selector - WHAT to export (omit for the whole brain). See {@link ExportSelector}. * @param options - HOW to export (vectors / VFS bytes / edge policy). See {@link ExportOptions}. - * @returns A versioned, portable `BackupData` document. + * @returns A versioned, portable `PortableGraph` document. * @example * const backup = await brain.now().export({ collection: id }, { includeVectors: true }) */ - async export(selector: ExportSelector = {}, options: ExportOptions = {}): Promise { + async export(selector: ExportSelector = {}, options: ExportOptions = {}): Promise { this.assertUsable('export') return exportGraph(this, this.host.storage, selector, options) } diff --git a/src/db/backup.ts b/src/db/portableGraph.ts similarity index 82% rename from src/db/backup.ts rename to src/db/portableGraph.ts index 8057109b..d8f57b78 100644 --- a/src/db/backup.ts +++ b/src/db/portableGraph.ts @@ -1,18 +1,24 @@ /** - * @module db/backup - * @description Portable graph backup/restore engine for Brainy 8.0 — the - * `BackupData v1` format plus the `export`/`import` engines that power + * @module db/portableGraph + * @description Portable graph export/import engine for Brainy 8.0 — the + * `PortableGraph v1` document format plus the `export`/`import` engines that power * `db.export()` / `brain.export()` (read, at a pinned generation) and - * `brain.import(backup)` (write, applied as one atomic transaction). + * `brain.import(graph)` (write, applied as one atomic transaction). * - * The wire format is identical to the 7.x line (`formatVersion: 1`), so a - * document written by 7.x imports cleanly here and vice-versa. This is the - * **portable** round-trip (human-readable, partial-or-whole, cross-version), - * distinct from `db.persist()`/`Brainy.load()` (the native whole-brain snapshot, - * which preserves generation history) and from `brain.import(file)` (foreign-file - * ingestion). + * A `PortableGraph` is a self-describing, versioned, **portable** representation + * of a graph (entities + relations, optionally vectors and file blobs) — the unit + * Brainy exports for interchange between instances, versions, and products, and the + * payload other artifacts embed (e.g. a workbench file's `graph` field). It is NOT + * a backup: that role belongs to `db.persist()`/`Brainy.load()`, the native + * whole-brain snapshot which preserves generation history. `PortableGraph` is the + * human-readable, partial-or-whole, cross-version round-trip; distinct also from + * `brain.import(file)` (foreign-file ingestion). * - * **Reserved-field split:** each `BackupEntity` carries Brainy's standard fields + * Every document carries `format: 'brainy-portable-graph'` and `formatVersion: 1`; + * import gates on `formatVersion` for forward migration. The format is shared + * verbatim across the 7.x and 8.0 lines. + * + * **Reserved-field split:** each `PortableGraphEntity` carries Brainy's standard fields * (`subtype`, `visibility`, `data`, `confidence`, `weight`, `service`, `createdBy`, * `createdAt`) at the TOP LEVEL and `metadata` holds ONLY custom user fields — * mirroring the in-memory `Entity`, so import maps each to its dedicated `add`/ @@ -27,10 +33,10 @@ import { TxOperation } from './types.js' /** Fixed entity id of the VFS root collection (a `system` entity; excluded unless `includeSystem`). */ const VFS_ROOT_ID = '00000000-0000-0000-0000-000000000000' -/** Magic string identifying a Brainy portable backup document. */ -export const BACKUP_FORMAT = 'brainy-backup' +/** Magic string identifying a Brainy `PortableGraph` document (the `format` tag). */ +export const PORTABLE_GRAPH_FORMAT = 'brainy-portable-graph' /** Current portable-format version (import gates on this for cross-version migration). */ -export const BACKUP_FORMAT_VERSION = 1 +export const PORTABLE_GRAPH_FORMAT_VERSION = 1 /** Default embedding model label (informational; `dimensions` is the real compat gate). */ const DEFAULT_EMBED_MODEL = 'all-MiniLM-L6-v2' /** Page size for find-based enumeration (whole-brain / predicate selectors). */ @@ -92,7 +98,7 @@ export interface ExportOptions { edges?: 'induced' | 'incident' | 'none' } -/** Controls how a `BackupData` is applied on `import()`. */ +/** Controls how a `PortableGraph` is applied on `import()`. */ export interface ImportOptions { /** Conflict policy for an existing id (default: `'merge'` — dedup-by-id). */ onConflict?: 'merge' | 'replace' | 'skip' @@ -104,8 +110,8 @@ export interface ImportOptions { meta?: Record } -/** One entity in a `BackupData`. Standard fields top-level; `metadata` is custom-only. */ -export interface BackupEntity { +/** One entity in a `PortableGraph`. Standard fields top-level; `metadata` is custom-only. */ +export interface PortableGraphEntity { id: string type: string subtype?: string @@ -120,8 +126,8 @@ export interface BackupEntity { metadata?: any } -/** One relation (edge) in a `BackupData`. */ -export interface BackupRelation { +/** One relation (edge) in a `PortableGraph`. */ +export interface PortableGraphRelation { id: string from: string to: string @@ -134,15 +140,15 @@ export interface BackupRelation { } /** A self-describing, versioned, portable graph document (identical on 7.x and 8.0). */ -export interface BackupData { - format: typeof BACKUP_FORMAT +export interface PortableGraph { + format: typeof PORTABLE_GRAPH_FORMAT formatVersion: number brainyVersion: string createdAt: string embedding: { model: string; dimensions: number } selector?: ExportSelector - entities: BackupEntity[] - relations: BackupRelation[] + entities: PortableGraphEntity[] + relations: PortableGraphRelation[] blobs?: Record danglingIds?: string[] stats: { entityCount: number; relationCount: number; blobCount: number; vectorDimensions?: number } @@ -159,30 +165,30 @@ export interface ImportResult { } /** The minimal read surface the export engine needs — satisfied by `Db` and `Brainy`. */ -export interface BackupReader { +export interface PortableGraphReader { get(id: string, options?: { includeVectors?: boolean }): Promise | null> find(query: any): Promise[]> related(paramsOrId?: any): Promise[]> } /** The minimal write surface the import engine needs — satisfied by `Brainy`. */ -export interface BackupWriter { +export interface PortableGraphWriter { get(id: string, options?: { includeVectors?: boolean }): Promise | null> transact(ops: TxOperation[], options?: { meta?: Record }): Promise } -/** Type guard: is this value a Brainy `BackupData` document (vs a file/foreign object)? */ -export function isBackupData(value: unknown): value is BackupData { +/** Type guard: is this value a Brainy `PortableGraph` document (vs a file/foreign object)? */ +export function isPortableGraph(value: unknown): value is PortableGraph { return ( typeof value === 'object' && value !== null && - (value as any).format === BACKUP_FORMAT && + (value as any).format === PORTABLE_GRAPH_FORMAT && Array.isArray((value as any).entities) ) } -/** Result of {@link validateBackup}. `valid` is true iff `errors` is empty. */ -export interface BackupValidation { +/** Result of {@link validatePortableGraph}. `valid` is true iff `errors` is empty. */ +export interface PortableGraphValidation { valid: boolean errors: string[] warnings: string[] @@ -191,29 +197,29 @@ export interface BackupValidation { /** * @description Dry-run check a value before `import()`, without mutating the brain: * structural validity, a supported `formatVersion`, entity-id uniqueness + required - * fields, and relation-endpoint coverage. `import()` throws on a non-`BackupData` or a - * too-new `formatVersion`; `validateBackup()` lets a consumer (e.g. a serializer checking + * fields, and relation-endpoint coverage. `import()` throws on a non-`PortableGraph` or a + * too-new `formatVersion`; `validatePortableGraph()` lets a consumer (e.g. a serializer checking * a `.wbench` graph payload) surface issues first. - * @param data - The value to validate (need not be a `BackupData`). + * @param data - The value to validate (need not be a `PortableGraph`). * @returns `{ valid, errors, warnings }`. * @example - * const { valid, errors } = validateBackup(payload) - * if (!valid) throw new Error(`invalid backup: ${errors.join('; ')}`) + * const { valid, errors } = validatePortableGraph(payload) + * if (!valid) throw new Error(`invalid PortableGraph: ${errors.join('; ')}`) * await brain.import(payload) */ -export function validateBackup(data: unknown): BackupValidation { +export function validatePortableGraph(data: unknown): PortableGraphValidation { const errors: string[] = [] const warnings: string[] = [] - if (!isBackupData(data)) { - errors.push(`not a BackupData document (expected format: '${BACKUP_FORMAT}')`) + if (!isPortableGraph(data)) { + errors.push(`not a PortableGraph document (expected format: '${PORTABLE_GRAPH_FORMAT}')`) return { valid: false, errors, warnings } } if (typeof data.formatVersion !== 'number') { errors.push('formatVersion is missing or not a number') - } else if (data.formatVersion > BACKUP_FORMAT_VERSION) { + } else if (data.formatVersion > PORTABLE_GRAPH_FORMAT_VERSION) { errors.push( - `formatVersion ${data.formatVersion} is newer than supported (max ${BACKUP_FORMAT_VERSION})` + `formatVersion ${data.formatVersion} is newer than supported (max ${PORTABLE_GRAPH_FORMAT_VERSION})` ) } @@ -264,7 +270,7 @@ export function validateBackup(data: unknown): BackupValidation { /** * @description Serialize part or all of a graph (read through `reader` at its pinned - * generation) into a portable `BackupData` document. + * generation) into a portable `PortableGraph` document. * @param reader - Generation-correct read surface (`Db` or `Brainy`). * @param storage - Storage adapter (used only for VFS blob bytes when `includeContent`). * @param selector - WHAT to export (omit for the whole brain). @@ -272,11 +278,11 @@ export function validateBackup(data: unknown): BackupValidation { * @param dimensions - Embedding dimensionality for the manifest. */ export async function exportGraph( - reader: BackupReader, + reader: PortableGraphReader, storage: StorageAdapter | undefined, selector: ExportSelector, options: ExportOptions -): Promise { +): Promise { const { includeVectors = false, includeContent = false, @@ -290,14 +296,14 @@ export async function exportGraph( // 2. Read canonical entities (reserved fields top-level), applying any predicate filter. const usePredicate = hasPredicate(selector) const entityMap = new Map>() - const entities: BackupEntity[] = [] + const entities: PortableGraphEntity[] = [] for (const id of idSet) { const e = await reader.get(id, { includeVectors }) if (!e) continue if (!includeSystem && (e as any).visibility === 'system') continue if (usePredicate && !matchesPredicate(e, selector)) continue entityMap.set(id, e) - entities.push(toBackupEntity(e, includeVectors)) + entities.push(toPortableGraphEntity(e, includeVectors)) } const keptIds = new Set(entityMap.keys()) @@ -314,8 +320,8 @@ export async function exportGraph( const dimensions = entities.find((e) => e.vector && e.vector.length)?.vector?.length ?? 384 return { - format: BACKUP_FORMAT, - formatVersion: BACKUP_FORMAT_VERSION, + format: PORTABLE_GRAPH_FORMAT, + formatVersion: PORTABLE_GRAPH_FORMAT_VERSION, brainyVersion: getBrainyVersion(), createdAt: new Date().toISOString(), embedding: { model: DEFAULT_EMBED_MODEL, dimensions }, @@ -338,31 +344,31 @@ export async function exportGraph( // ============================================================================ /** - * @description Apply a `BackupData` to the brain as ONE atomic transaction (a single + * @description Apply a `PortableGraph` to the brain as ONE atomic transaction (a single * new generation). Dedup-by-id merge by default; re-embeds from `data` when no vector * is carried. VFS blob bytes are written first (content-addressed, idempotent). * @param writer - The brain (`get` + `transact`). * @param storage - Storage adapter (for VFS blob bytes). - * @param data - A `BackupData` document. + * @param data - A `PortableGraph` document. * @param options - Conflict / vector / id-remap policy. - * @throws If `data` is not a `BackupData`, or its `formatVersion` is newer than supported. + * @throws If `data` is not a `PortableGraph`, or its `formatVersion` is newer than supported. */ export async function importGraph( - writer: BackupWriter, + writer: PortableGraphWriter, storage: StorageAdapter | undefined, - data: BackupData, + data: PortableGraph, options: ImportOptions = {} ): Promise { - if (!isBackupData(data)) { + if (!isPortableGraph(data)) { throw new Error( - `import() expects a BackupData document (format:'${BACKUP_FORMAT}'). ` + + `import() expects a PortableGraph document (format:'${PORTABLE_GRAPH_FORMAT}'). ` + `For file ingestion (CSV/PDF/Excel/JSON), pass a file/buffer instead.` ) } - if (typeof data.formatVersion === 'number' && data.formatVersion > BACKUP_FORMAT_VERSION) { + if (typeof data.formatVersion === 'number' && data.formatVersion > PORTABLE_GRAPH_FORMAT_VERSION) { throw new Error( - `Backup formatVersion ${data.formatVersion} is newer than this Brainy supports ` + - `(max ${BACKUP_FORMAT_VERSION}). Upgrade Brainy to import this document.` + `PortableGraph formatVersion ${data.formatVersion} is newer than this Brainy supports ` + + `(max ${PORTABLE_GRAPH_FORMAT_VERSION}). Upgrade Brainy to import this document.` ) } @@ -445,7 +451,7 @@ export async function importGraph( } as TxOperation) } - // 3. Apply atomically — one generation for the whole backup, or none. + // 3. Apply atomically — one generation for the whole graph, or none. if (ops.length > 0) { await writer.transact(ops, meta ? { meta } : undefined) } @@ -472,7 +478,7 @@ function hasPredicate(s: ExportSelector): boolean { } async function resolveSelector( - reader: BackupReader, + reader: PortableGraphReader, s: ExportSelector, includeSystem: boolean ): Promise> { @@ -493,7 +499,7 @@ async function resolveSelector( } /** Whole-brain / predicate enumeration via generation-correct paginated `find()`. */ -async function enumerateAll(reader: BackupReader, s: ExportSelector): Promise> { +async function enumerateAll(reader: PortableGraphReader, s: ExportSelector): Promise> { const params: any = {} if (s.type !== undefined) params.type = s.type if (s.subtype !== undefined) params.subtype = s.subtype @@ -512,7 +518,7 @@ async function enumerateAll(reader: BackupReader, s: ExportSelector): Prom } async function resolveCollectionSubtree( - reader: BackupReader, + reader: PortableGraphReader, rootId: string, depth?: number ): Promise> { @@ -538,7 +544,7 @@ async function resolveCollectionSubtree( } async function resolveConnected( - reader: BackupReader, + reader: PortableGraphReader, c: NonNullable ): Promise> { const { from, depth = 1, verbs, direction = 'out' } = c @@ -562,7 +568,7 @@ async function resolveConnected( } async function neighboursOf( - reader: BackupReader, + reader: PortableGraphReader, id: string, direction: 'out' | 'in' | 'both', verbs?: VerbType[] @@ -580,7 +586,7 @@ async function neighboursOf( } async function resolveVfsPath( - reader: BackupReader, + reader: PortableGraphReader, path: string, recursive: boolean, depth?: number @@ -596,7 +602,7 @@ async function resolveVfsPath( return resolveCollectionSubtree(reader, dirId, depth) } -async function resolveVfsPathToId(reader: BackupReader, path: string): Promise { +async function resolveVfsPathToId(reader: PortableGraphReader, path: string): Promise { const segments = path.split('/').filter(Boolean) let currentId = VFS_ROOT_ID for (const seg of segments) { @@ -645,8 +651,8 @@ function matchesWhere(metadata: any, where: Record): boolean { // Serialization helpers (private) // ============================================================================ -function toBackupEntity(e: Entity, includeVectors: boolean): BackupEntity { - const be: BackupEntity = { id: e.id, type: e.type as string } +function toPortableGraphEntity(e: Entity, includeVectors: boolean): PortableGraphEntity { + const be: PortableGraphEntity = { id: e.id, type: e.type as string } if (e.subtype !== undefined) be.subtype = e.subtype const vis = (e as any).visibility if (vis !== undefined && vis !== 'public') be.visibility = vis @@ -661,8 +667,8 @@ function toBackupEntity(e: Entity, includeVectors: boolean): BackupEntity return be } -function toBackupRelation(r: Relation): BackupRelation { - const br: BackupRelation = { id: r.id, from: r.from, to: r.to, type: r.type as string } +function toPortableGraphRelation(r: Relation): PortableGraphRelation { + const br: PortableGraphRelation = { id: r.id, from: r.from, to: r.to, type: r.type as string } if (r.subtype !== undefined) br.subtype = r.subtype const vis = (r as any).visibility if (vis !== undefined && vis !== 'public') br.visibility = vis @@ -673,13 +679,13 @@ function toBackupRelation(r: Relation): BackupRelation { } async function collectEdges( - reader: BackupReader, + reader: PortableGraphReader, idSet: Set, edges: 'induced' | 'incident' | 'none' -): Promise<{ relations: BackupRelation[]; danglingIds?: string[] }> { +): Promise<{ relations: PortableGraphRelation[]; danglingIds?: string[] }> { if (edges === 'none') return { relations: [] } - const relations: BackupRelation[] = [] + const relations: PortableGraphRelation[] = [] const dangling = new Set() const seen = new Set() @@ -691,7 +697,7 @@ async function collectEdges( if (edges === 'induced' && !toIn) continue if (!toIn) dangling.add(r.to) seen.add(r.id) - relations.push(toBackupRelation(r)) + relations.push(toPortableGraphRelation(r)) } } @@ -703,7 +709,7 @@ async function collectEdges( if (!idSet.has(r.from)) { dangling.add(r.from) seen.add(r.id) - relations.push(toBackupRelation(r)) + relations.push(toPortableGraphRelation(r)) } } } @@ -740,7 +746,7 @@ async function collectBlobs( // add()/update() param mapping (private) // ============================================================================ -function entityAddFields(be: BackupEntity): Record { +function entityAddFields(be: PortableGraphEntity): Record { const fields: Record = { data: be.data, type: be.type as NounType } if (be.subtype !== undefined) fields.subtype = be.subtype if (be.visibility !== undefined) fields.visibility = be.visibility @@ -751,7 +757,7 @@ function entityAddFields(be: BackupEntity): Record { return fields } -function entityUpdateFields(be: BackupEntity): Record { +function entityUpdateFields(be: PortableGraphEntity): Record { const fields: Record = {} if (be.data !== undefined) fields.data = be.data if (be.type !== undefined) fields.type = be.type as NounType diff --git a/src/index.ts b/src/index.ts index 3f060c6d..277ec20b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -142,19 +142,19 @@ export { EntityNotFoundError, RelationNotFoundError } from './errors/notFound.js // db.with() / db.persist() / Brainy.load(). See src/db/ for the record layer. export { Db } from './db/db.js' -// Portable graph backup/restore — db.export() / brain.export() / brain.import() -// (BackupData v1; identical wire format to the 7.x line). -export { isBackupData, validateBackup } from './db/backup.js' +// Portable graph export/import — db.export() / brain.export() / brain.import() +// (PortableGraph v1; identical wire format to the 7.x line). +export { isPortableGraph, validatePortableGraph } from './db/portableGraph.js' export type { - BackupData, - BackupEntity, - BackupRelation, + PortableGraph, + PortableGraphEntity, + PortableGraphRelation, ExportSelector, ExportOptions, ImportOptions, ImportResult, - BackupValidation -} from './db/backup.js' + PortableGraphValidation +} from './db/portableGraph.js' export { GenerationConflictError, SpeculativeOverlayError, diff --git a/tests/unit/db/db-backup.test.ts b/tests/unit/db/db-portable-graph.test.ts similarity index 92% rename from tests/unit/db/db-backup.test.ts rename to tests/unit/db/db-portable-graph.test.ts index 03b7df6c..41bc9954 100644 --- a/tests/unit/db/db-backup.test.ts +++ b/tests/unit/db/db-portable-graph.test.ts @@ -17,10 +17,10 @@ import * as path from 'node:path' import { Brainy } from '../../../src/brainy' import { createTestConfig } from '../../helpers/test-factory' import { NounType, VerbType } from '../../../src/types/graphTypes' -import { validateBackup } from '../../../src/db/backup' -import type { BackupData } from '../../../src/db/backup' +import { validatePortableGraph } from '../../../src/db/portableGraph' +import type { PortableGraph } from '../../../src/db/portableGraph' -describe('8.0 portable graph export/import (BackupData v1)', () => { +describe('8.0 portable graph export/import (PortableGraph v1)', () => { let brain: Brainy beforeEach(async () => { @@ -33,13 +33,13 @@ describe('8.0 portable graph export/import (BackupData v1)', () => { }) describe('format + round-trip', () => { - it('brain.export() produces a versioned BackupData document', async () => { + it('brain.export() produces a versioned PortableGraph document', async () => { 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() - expect(backup.format).toBe('brainy-backup') + expect(backup.format).toBe('brainy-portable-graph') expect(backup.formatVersion).toBe(1) expect(backup.entities.map((e) => e.id).sort()).toEqual([a, b].sort()) expect(backup.relations).toHaveLength(1) @@ -169,8 +169,8 @@ describe('8.0 portable graph export/import (BackupData v1)', () => { describe('import validation', () => { it('rejects a newer formatVersion', async () => { - const future: BackupData = { - format: 'brainy-backup', + const future: PortableGraph = { + format: 'brainy-portable-graph', formatVersion: 999, brainyVersion: 'x', createdAt: new Date().toISOString(), @@ -203,9 +203,9 @@ describe('8.0 portable graph export/import (BackupData v1)', () => { }) }) - describe('validateBackup() — dry-run check', () => { - const valid = (): BackupData => ({ - format: 'brainy-backup', + describe('validatePortableGraph() — dry-run check', () => { + const valid = (): PortableGraph => ({ + format: 'brainy-portable-graph', formatVersion: 1, brainyVersion: 'x', createdAt: new Date().toISOString(), @@ -216,19 +216,19 @@ describe('8.0 portable graph export/import (BackupData v1)', () => { }) it('accepts a well-formed backup', () => { - const r = validateBackup(valid()) + const r = validatePortableGraph(valid()) expect(r.valid).toBe(true) expect(r.errors).toHaveLength(0) }) - it('rejects a non-BackupData value', () => { - const r = validateBackup({ entities: [] }) + it('rejects a non-PortableGraph value', () => { + const r = validatePortableGraph({ entities: [] }) expect(r.valid).toBe(false) - expect(r.errors[0]).toMatch(/BackupData/) + expect(r.errors[0]).toMatch(/PortableGraph/) }) it('rejects a newer formatVersion', () => { - const r = validateBackup({ ...valid(), formatVersion: 999 }) + const r = validatePortableGraph({ ...valid(), formatVersion: 999 }) expect(r.valid).toBe(false) expect(r.errors.some((e) => /formatVersion/.test(e))).toBe(true) }) @@ -239,7 +239,7 @@ describe('8.0 portable graph export/import (BackupData v1)', () => { { id: 'dup', type: NounType.Thing }, { id: 'dup', type: NounType.Thing } ] - const r = validateBackup(b) + const r = validatePortableGraph(b) expect(r.valid).toBe(false) expect(r.errors.some((e) => /duplicate/.test(e))).toBe(true) }) @@ -247,7 +247,7 @@ describe('8.0 portable graph export/import (BackupData v1)', () => { 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 }] - const r = validateBackup(b) + const r = validatePortableGraph(b) expect(r.valid).toBe(true) // a dangling endpoint is a warning, not a hard error expect(r.warnings.some((w) => /missing/.test(w))).toBe(true) })