/** * Vault-level diff orchestrator. * * Compares a live `Vault`'s plaintext state against a candidate state * (another vault, a plain `{ collection: records[] }` map, or a vault * dump JSON) and returns a structured `VaultDiff` plan listing the * records that would be added, modified, or deleted to bring the live * vault into the candidate's shape. * * Builds on two existing record-level helpers: * * 1. `diff(a, b)` from `./history/diff.ts` — emits dot-pathed * `DiffEntry[]` with `type: 'added' | 'removed' | 'changed'` for * each changed field of two records. Used here for the * `fieldDiffs` of every `modified` entry, and (with empty result) * as the default deep-equal check. * * 2. `Vault.exportStream()` from `./vault.ts` — the canonical * decrypt-and-stream-records iterator. Used to walk both sides * when the candidate is itself a `Vault`. ACL-scoped: collections * the caller can't read silently drop out, the same way every * other plaintext-emitting export pipeline filters them. * * The new orchestration is the **vault-level** enumeration: bucket * each record id into added (only in candidate), deleted (only in * vault), or modified (in both with field changes); leave the * field-level granularity to the existing `diff()`. * * Use cases: * * - Import preview (`@noy-db/as-*` `fromString` returns a plan * whose body is a `VaultDiff`). * - Backup verification ("does this `.noydb` bundle from yesterday * match the current vault?"). * - Two-vault reconciliation ("what's different between Office A * and Office B before we sync?"). * - Test assertions (golden-file testing with one-liner * `expect(plan.summary).toEqual(...)`). * * @module */ import type { Vault } from '../kernel/vault.js'; import { type DiffEntry as FieldDiffEntry } from '../with-commit/history/diff.js'; /** Per-record entry shape — added and deleted records carry only the record value. */ export interface VaultDiffEntry { readonly collection: string; readonly id: string; readonly record: T; /** * Unencrypted envelope metadata for the RECEIVER-side record. * Populated only when `diffVault` is called with `{includeMetadata: true}`. * Default: `undefined` (zero extra reads, no behavior change). * * Populated for `modified` (the CURRENT receiver state, before the candidate * is applied) and `deleted` (the receiver envelope of the record being * removed). `added` entries have no receiver envelope at diff time, so their * `metadata` is always absent. */ readonly metadata?: { readonly version: number; readonly timestamp: string; readonly by?: string; readonly source?: string; readonly sourceTs?: string; }; } /** Modified records carry both halves of the diff plus the field-level breakdown. */ export interface VaultDiffModifiedEntry extends VaultDiffEntry { /** The record as it stands in the live vault. */ readonly before: T; /** Top-level keys whose values differ between `before` and `record`. */ readonly fieldsChanged: readonly string[]; /** * Field-level diff entries from `diff(before, record)`. Reuses the * existing per-record diff helper so consumers can render git-style * `path: from → to` rows without re-walking the records. */ readonly fieldDiffs: readonly FieldDiffEntry[]; } export interface VaultDiff { readonly added: readonly VaultDiffEntry[]; readonly modified: readonly VaultDiffModifiedEntry[]; readonly deleted: readonly VaultDiffEntry[]; /** Only populated when `options.includeUnchanged: true`. */ readonly unchanged: readonly VaultDiffEntry[] | undefined; readonly summary: { readonly add: number; readonly modify: number; readonly delete: number; readonly total: number; }; /** * Format the diff as a human-readable string. * * - `'count'` — one line, just the numbers (`12 added · 3 modified · 0 deleted`) * - `'one-line'` — count plus a single overview line * - `'full'` — count + one row per added/modified/deleted record (default) */ format(opts?: { detail?: 'count' | 'one-line' | 'full'; }): string; } export interface DiffOptions { /** Restrict the diff to a subset of collections. */ readonly collections?: readonly string[]; /** Field on each record that carries its id. Defaults to `'id'`. */ readonly idKey?: string; /** Override the default deep-equal check for "modified vs unchanged". */ readonly compareFn?: (a: unknown, b: unknown) => boolean; /** If true, include unchanged records in the diff (off by default to save memory). */ readonly includeUnchanged?: boolean; /** * When `true`, each entry in `added`, `modified`, and `deleted` will carry a * `metadata` field with the RECEIVER-side envelope metadata * (`version`, `timestamp`, `by?`, `source?`, `sourceTs?`). * * Off by default — no extra adapter reads, no behavior change for existing callers. */ readonly includeMetadata?: boolean; } /** * Candidate state to diff the vault against: * * - A `Vault` instance — both sides are walked via `exportStream()`. * - A `Record` map — same shape `as-json.toObject()` * produces. Useful for diffing parsed file content against the live vault. * - A `VaultDump` (output of `vault.dump()`) — a JSON string carrying the * full vault state. Parsed and reduced to the map shape above. */ export type DiffCandidate = Vault | Record | string; /** * Compute the diff between a live vault and a candidate state. * * Ungated shared import/merge infrastructure: the `as-*` exporters call this * internally from their `fromString`/import merge logic, so it is always * available (not behind `withCargo()`). Only `extractPartition` is a gated * cargo capability. * * Returns a fully buffered `VaultDiff` — no streaming. Memory cost is * O(n + m) in the row count of vault + candidate. For documented * 1K-50K-record vaults this is fine; a streaming variant lands as a * follow-up if a > 100K-record consumer arrives. */ export declare function diffVault(vault: Vault, candidate: DiffCandidate, options?: DiffOptions): Promise>;