fix(architecture): singleton GraphAdjacencyIndex via storage.getGraphIndex() (v6.3.0)
BREAKING: This is a critical architectural fix for the VFS tree corruption bug
reported by Soulcraft Workshop team. The fix addresses the root cause: dual
ownership of GraphAdjacencyIndex causing verbIdSet to be out of sync.
## Root Cause Analysis
The bug was caused by TWO separate GraphAdjacencyIndex instances:
1. Storage.graphIndex (created in BaseStorage.init())
2. Brainy.graphIndex (created in Brainy.init())
When verbs were saved, both instances were updated. But if Storage's graphIndex
was recreated (via ensureInitialized()), the new instance had an empty verbIdSet.
Queries filtered through this empty verbIdSet returned nothing - making data
appear lost even though it existed in the LSM-trees.
## Fix Summary
1. **GraphAdjacencyIndex Singleton Pattern**
- Removed direct creation from BaseStorage.init()
- Brainy now uses `storage.getGraphIndex()` instead of creating its own
- getGraphIndex() has proper singleton pattern with concurrent access protection
- Added `invalidateGraphIndex()` for branch switches
2. **Auto-rebuild verbIdSet Defense**
- Added check in ensureInitialized(): if LSM-trees have data but verbIdSet
is empty, automatically populate verbIdSet from storage
- This is a safety net for edge cases
3. **Removed Double-Add Bug**
- Removed graphIndex.addVerb() from saveVerb_internal()
- Graph index updates now happen ONLY via AddToGraphIndexOperation in
Brainy.relate() transaction system
- This prevents duplicate counting in relationshipCountsByType
4. **PathResolver Cache Invalidation**
- Added invalidateAllCaches() method to PathResolver and SemanticPathResolver
- checkout() now clears VFS caches before recreating VFS for new branch
## Files Changed
- src/storage/baseStorage.ts: Removed graphIndex creation from init(), added
invalidateGraphIndex(), removed addVerb from saveVerb_internal()
- src/brainy.ts: Use storage.getGraphIndex() in init/fork/checkout
- src/graph/graphAdjacencyIndex.ts: Auto-rebuild verbIdSet in ensureInitialized()
- src/vfs/PathResolver.ts: Added invalidateAllCaches()
- src/vfs/semantic/SemanticPathResolver.ts: Added invalidateAllCaches()
## Testing
All VFS tests pass (7/7), including:
- mkdir() should not corrupt VFS index
- Delete and recreate folder cycles
- Contains relationship queries
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
810b75647c
commit
c15892ef04
5 changed files with 148 additions and 42 deletions
|
|
@ -117,6 +117,7 @@ export class GraphAdjacencyIndex {
|
|||
|
||||
/**
|
||||
* Initialize the graph index (lazy initialization)
|
||||
* v6.3.0: Added defensive auto-rebuild check for verbIdSet consistency
|
||||
*/
|
||||
private async ensureInitialized(): Promise<void> {
|
||||
if (this.initialized) {
|
||||
|
|
@ -128,12 +129,65 @@ export class GraphAdjacencyIndex {
|
|||
await this.lsmTreeVerbsBySource.init()
|
||||
await this.lsmTreeVerbsByTarget.init()
|
||||
|
||||
// v6.3.0: Defensive check - if LSM-trees have data but verbIdSet is empty,
|
||||
// the index was created without proper rebuild (shouldn't happen with singleton
|
||||
// pattern but protects against edge cases and future refactoring)
|
||||
const lsmTreeSize = this.lsmTreeVerbsBySource.size()
|
||||
if (lsmTreeSize > 0 && this.verbIdSet.size === 0) {
|
||||
prodLog.warn(
|
||||
`GraphAdjacencyIndex: LSM-trees have ${lsmTreeSize} relationships but verbIdSet is empty. ` +
|
||||
`Triggering auto-rebuild to restore consistency.`
|
||||
)
|
||||
// Note: We don't await rebuild() here to avoid infinite loop
|
||||
// (rebuild calls ensureInitialized). Instead, we'll populate verbIdSet
|
||||
// by loading all verb IDs from storage.
|
||||
await this.populateVerbIdSetFromStorage()
|
||||
}
|
||||
|
||||
// Start auto-flush timer after initialization
|
||||
this.startAutoFlush()
|
||||
|
||||
this.initialized = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate verbIdSet from storage without full rebuild (v6.3.0)
|
||||
* Lighter weight than full rebuild - only loads verb IDs, not all verb data
|
||||
* @private
|
||||
*/
|
||||
private async populateVerbIdSetFromStorage(): Promise<void> {
|
||||
prodLog.info('GraphAdjacencyIndex: Populating verbIdSet from storage...')
|
||||
const startTime = Date.now()
|
||||
|
||||
// Use pagination to load all verb IDs
|
||||
let hasMore = true
|
||||
let cursor: string | undefined = undefined
|
||||
let count = 0
|
||||
|
||||
while (hasMore) {
|
||||
const result = await this.storage.getVerbs({
|
||||
pagination: { limit: 10000, cursor }
|
||||
})
|
||||
|
||||
for (const verb of result.items) {
|
||||
this.verbIdSet.add(verb.id)
|
||||
// Also update counts
|
||||
const verbType = verb.verb || 'unknown'
|
||||
this.relationshipCountsByType.set(
|
||||
verbType,
|
||||
(this.relationshipCountsByType.get(verbType) || 0) + 1
|
||||
)
|
||||
count++
|
||||
}
|
||||
|
||||
hasMore = result.hasMore
|
||||
cursor = result.nextCursor
|
||||
}
|
||||
|
||||
const elapsed = Date.now() - startTime
|
||||
prodLog.info(`GraphAdjacencyIndex: Populated verbIdSet with ${count} verb IDs in ${elapsed}ms`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Core API - Neighbor lookup with LSM-tree storage
|
||||
*
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue