From b58ea02de199f4c6843345a91366ea7e79a606f0 Mon Sep 17 00:00:00 2001 From: David Snelling Date: Mon, 23 Mar 2026 15:45:46 -0700 Subject: [PATCH] fix: commit() now flushes and captures state by default commit() previously defaulted captureState to false, creating commits with NULL_HASH tree that could never be restored from. Now: - flush() runs first to persist deferred HNSW nodes, count batches, and index state before snapshotting - captureState defaults to true, creating real content-addressed trees - captureState: false still available for lightweight metadata-only commits --- src/brainy.ts | 35 +++++++++++++++---- .../integration/commit-state-capture.test.ts | 21 +++++------ 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/src/brainy.ts b/src/brainy.ts index 2f3baf4b..ddb69f90 100644 --- a/src/brainy.ts +++ b/src/brainy.ts @@ -3411,8 +3411,21 @@ export class Brainy implements BrainyInterface { /** * Create a commit with current state - * @param options - Commit options (message, author, metadata) - * @returns Commit hash + * + * Flushes all buffered data to disk, captures a full state snapshot + * (entities + relationships) into a content-addressed tree, then creates + * an immutable commit object pointing to that tree. + * + * By default, every commit captures a complete snapshot that can be + * restored via time-travel. Pass `captureState: false` for a lightweight + * metadata-only commit (tree hash will be NULL_HASH). + * + * @param options - Commit options + * @param options.message - Human-readable commit message + * @param options.author - Author identifier (email, username, or 'system') + * @param options.metadata - Arbitrary key-value metadata stored with the commit + * @param options.captureState - Capture entity snapshots for time-travel (default: true) + * @returns Commit hash (64-char hex SHA-256) * * @example * ```typescript @@ -3427,7 +3440,7 @@ export class Brainy implements BrainyInterface { message?: string author?: string metadata?: Record - captureState?: boolean // Capture entity snapshots for time-travel + captureState?: boolean }): Promise { await this.ensureInitialized() @@ -3435,6 +3448,11 @@ export class Brainy implements BrainyInterface { throw new Error('Commit requires COW-enabled storage') } + // Flush all buffered data to disk before creating the snapshot. + // Without this, deferred HNSW nodes, pending count batches, and + // unflushed index state could be missing from the commit tree. + await this.flush() + const refManager = (this.storage as any).refManager const blobStorage = (this.storage as any).blobStorage const currentBranch = await this.getCurrentBranch() @@ -3449,15 +3467,18 @@ export class Brainy implements BrainyInterface { // Import NULL_HASH constant const { NULL_HASH } = await import('./storage/cow/constants.js') - // Capture entity state if requested (for time-travel) + // Capture entity state to tree (default: true). + // Only skip when caller explicitly passes captureState: false + // for a lightweight metadata-only commit. + const shouldCapture = options?.captureState !== false let treeHash = NULL_HASH - if (options?.captureState) { + if (shouldCapture) { treeHash = await this.captureStateToTree() } // Build commit object using builder pattern const builder = CommitBuilder.create(blobStorage) - .tree(treeHash) // Use captured state tree or NULL_HASH + .tree(treeHash) .message(options?.message || 'Snapshot commit') .author(options?.author || 'unknown') .timestamp(Date.now()) @@ -3490,7 +3511,7 @@ export class Brainy implements BrainyInterface { /** * Capture current entity and relationship state to tree object - * Used by commit({ captureState: true }) for time-travel + * Called by commit() by default for full state snapshots * * Serializes ALL entities + relationships to blobs and builds a tree. * BlobStorage automatically deduplicates unchanged data. diff --git a/tests/integration/commit-state-capture.test.ts b/tests/integration/commit-state-capture.test.ts index aea96f8a..2d85bb8f 100644 --- a/tests/integration/commit-state-capture.test.ts +++ b/tests/integration/commit-state-capture.test.ts @@ -5,8 +5,8 @@ * historical time-travel by capturing entity snapshots in trees. * * Tests: - * 1. Backward compatibility (captureState: false uses NULL_HASH) - * 2. State capture (captureState: true creates tree) + * 1. Lightweight commit (captureState: false uses NULL_HASH) + * 2. Default behavior (captureState omitted → captures state) * 3. Empty workspace handling * 4. Performance benchmarks (100, 1K, 10K entities) * 5. BlobStorage deduplication @@ -56,10 +56,10 @@ describe('Commit State Capture (v5.4.0)', () => { // Add some entities await brain.add({ type: 'concept', data: { title: 'Test' } }) - // Commit WITHOUT captureState (default behavior) + // Commit with explicit captureState: false (lightweight metadata-only commit) const commitId = await brain.commit({ message: 'Test commit', - captureState: false // Explicit false + captureState: false // Explicit false — skips state capture }) console.log(` Commit ID: ${commitId.slice(0, 8)}`) @@ -76,15 +76,15 @@ describe('Commit State Capture (v5.4.0)', () => { console.log(` āœ… Tree is NULL_HASH (backward compatible)`) }) - it('should use NULL_HASH when captureState is omitted (default)', async () => { - console.log('\nšŸ“‹ Test 2: Default behavior (captureState omitted)') + it('should capture state when captureState is omitted (default: true)', async () => { + console.log('\nšŸ“‹ Test 2: Default behavior (captureState omitted → captures state)') await brain.add({ type: 'concept', data: { title: 'Test' } }) - // Commit WITHOUT captureState parameter (omitted = default) + // Commit WITHOUT captureState parameter (omitted = default true) const commitId = await brain.commit({ message: 'Test commit' - // captureState not specified + // captureState not specified → defaults to true }) const blobStorage = (brain.storage as any).blobStorage @@ -93,8 +93,9 @@ describe('Commit State Capture (v5.4.0)', () => { const commit = await CommitObject.read(blobStorage, commitId) - expect(commit.tree).toBe(NULL_HASH) - console.log(` āœ… Tree is NULL_HASH (default behavior)`) + // Default behavior now captures state (tree hash is NOT null) + expect(commit.tree).not.toBe(NULL_HASH) + console.log(` āœ… Tree is ${commit.tree.slice(0, 8)}... (state captured by default)`) }) it('should capture entity state when captureState is true', async () => {