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
This commit is contained in:
parent
74bc61a89c
commit
b58ea02de1
2 changed files with 39 additions and 17 deletions
|
|
@ -3411,8 +3411,21 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a commit with current state
|
* 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
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
|
|
@ -3427,7 +3440,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
message?: string
|
message?: string
|
||||||
author?: string
|
author?: string
|
||||||
metadata?: Record<string, any>
|
metadata?: Record<string, any>
|
||||||
captureState?: boolean // Capture entity snapshots for time-travel
|
captureState?: boolean
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
await this.ensureInitialized()
|
await this.ensureInitialized()
|
||||||
|
|
||||||
|
|
@ -3435,6 +3448,11 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
throw new Error('Commit requires COW-enabled storage')
|
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 refManager = (this.storage as any).refManager
|
||||||
const blobStorage = (this.storage as any).blobStorage
|
const blobStorage = (this.storage as any).blobStorage
|
||||||
const currentBranch = await this.getCurrentBranch()
|
const currentBranch = await this.getCurrentBranch()
|
||||||
|
|
@ -3449,15 +3467,18 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
// Import NULL_HASH constant
|
// Import NULL_HASH constant
|
||||||
const { NULL_HASH } = await import('./storage/cow/constants.js')
|
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
|
let treeHash = NULL_HASH
|
||||||
if (options?.captureState) {
|
if (shouldCapture) {
|
||||||
treeHash = await this.captureStateToTree()
|
treeHash = await this.captureStateToTree()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build commit object using builder pattern
|
// Build commit object using builder pattern
|
||||||
const builder = CommitBuilder.create(blobStorage)
|
const builder = CommitBuilder.create(blobStorage)
|
||||||
.tree(treeHash) // Use captured state tree or NULL_HASH
|
.tree(treeHash)
|
||||||
.message(options?.message || 'Snapshot commit')
|
.message(options?.message || 'Snapshot commit')
|
||||||
.author(options?.author || 'unknown')
|
.author(options?.author || 'unknown')
|
||||||
.timestamp(Date.now())
|
.timestamp(Date.now())
|
||||||
|
|
@ -3490,7 +3511,7 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Capture current entity and relationship state to tree object
|
* 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.
|
* Serializes ALL entities + relationships to blobs and builds a tree.
|
||||||
* BlobStorage automatically deduplicates unchanged data.
|
* BlobStorage automatically deduplicates unchanged data.
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
* historical time-travel by capturing entity snapshots in trees.
|
* historical time-travel by capturing entity snapshots in trees.
|
||||||
*
|
*
|
||||||
* Tests:
|
* Tests:
|
||||||
* 1. Backward compatibility (captureState: false uses NULL_HASH)
|
* 1. Lightweight commit (captureState: false uses NULL_HASH)
|
||||||
* 2. State capture (captureState: true creates tree)
|
* 2. Default behavior (captureState omitted → captures state)
|
||||||
* 3. Empty workspace handling
|
* 3. Empty workspace handling
|
||||||
* 4. Performance benchmarks (100, 1K, 10K entities)
|
* 4. Performance benchmarks (100, 1K, 10K entities)
|
||||||
* 5. BlobStorage deduplication
|
* 5. BlobStorage deduplication
|
||||||
|
|
@ -56,10 +56,10 @@ describe('Commit State Capture (v5.4.0)', () => {
|
||||||
// Add some entities
|
// Add some entities
|
||||||
await brain.add({ type: 'concept', data: { title: 'Test' } })
|
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({
|
const commitId = await brain.commit({
|
||||||
message: 'Test commit',
|
message: 'Test commit',
|
||||||
captureState: false // Explicit false
|
captureState: false // Explicit false — skips state capture
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(` Commit ID: ${commitId.slice(0, 8)}`)
|
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)`)
|
console.log(` ✅ Tree is NULL_HASH (backward compatible)`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should use NULL_HASH when captureState is omitted (default)', async () => {
|
it('should capture state when captureState is omitted (default: true)', async () => {
|
||||||
console.log('\n📋 Test 2: Default behavior (captureState omitted)')
|
console.log('\n📋 Test 2: Default behavior (captureState omitted → captures state)')
|
||||||
|
|
||||||
await brain.add({ type: 'concept', data: { title: 'Test' } })
|
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({
|
const commitId = await brain.commit({
|
||||||
message: 'Test commit'
|
message: 'Test commit'
|
||||||
// captureState not specified
|
// captureState not specified → defaults to true
|
||||||
})
|
})
|
||||||
|
|
||||||
const blobStorage = (brain.storage as any).blobStorage
|
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)
|
const commit = await CommitObject.read(blobStorage, commitId)
|
||||||
|
|
||||||
expect(commit.tree).toBe(NULL_HASH)
|
// Default behavior now captures state (tree hash is NOT null)
|
||||||
console.log(` ✅ Tree is NULL_HASH (default behavior)`)
|
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 () => {
|
it('should capture entity state when captureState is true', async () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue