fix(versioning): clean architecture with index pollution prevention

v6.3.0 Versioning System Overhaul:
- Rewrite VersionIndex to use pure key-value storage (not entities)
- Fix restore() to use brain.update() - updates all indexes (HNSW, metadata, graph)
- Remove 525 LOC dead code (versioningAugmentation.ts - untested, unused)
- Fix branch isolation in tests (fork() vs checkout() semantics)

Key improvements:
- Versions no longer pollute find() results
- restore() properly updates all indexes
- 75 tests passing (60 unit + 15 integration)
- Net reduction of ~290 lines while fixing bugs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
David Snelling 2025-12-09 09:35:45 -08:00
parent 292be1b9cd
commit f145fa1fc8
8 changed files with 712 additions and 1002 deletions

View file

@ -1,19 +1,18 @@
/**
* VersionStorage - Hybrid Storage for Entity Versions (v5.3.0)
* VersionStorage - Hybrid Storage for Entity Versions (v5.3.0, v6.3.0 fix)
*
* Implements content-addressable storage for entity versions:
* - SHA-256 content hashing for deduplication
* - Stores versions in .brainy/versions/ directory
* - Uses BaseStorage.saveMetadata/getMetadata for storage (v6.3.0)
* - Integrates with COW commit system
* - Space-efficient: Only stores unique content
*
* Storage structure:
* .brainy/versions/
* entities/
* {entityId}/
* {contentHash}.json # Entity version data
* index/
* {entityId}.json # Version index (managed by VersionIndex)
* Storage structure (v6.3.0):
* Version content is stored using system metadata keys:
* __system_version_{entityId}_{contentHash}
*
* This integrates with BaseStorage's routing which places system keys
* in the _system/ directory, keeping version data separate from entities.
*
* NO MOCKS - Production implementation
*/
@ -151,33 +150,35 @@ export class VersionStorage {
}
/**
* Get version storage path
* Get version storage key
*
* Uses __system_ prefix so BaseStorage routes to system storage (_system/ directory)
* This keeps version data separate from entity data.
*
* @param entityId Entity ID
* @param contentHash Content hash
* @returns Storage path
* @returns Storage key for version content
*/
private getVersionPath(entityId: string, contentHash: string): string {
return `versions/entities/${entityId}/${contentHash}.json`
// v6.3.0: Use system-prefixed key for BaseStorage.saveMetadata/getMetadata
// BaseStorage recognizes __system_ prefix and routes to _system/ directory
return `__system_version_${entityId}_${contentHash}`
}
/**
* Check if content exists in storage
*
* @param path Storage path
* @param key Storage key
* @returns True if exists
*/
private async contentExists(path: string): Promise<boolean> {
private async contentExists(key: string): Promise<boolean> {
try {
// Use storage adapter's exists check if available
// v6.3.0: Use getMetadata to check existence
const adapter = this.brain.storageAdapter
if (adapter && typeof adapter.exists === 'function') {
return await adapter.exists(path)
}
if (!adapter) return false
// Fallback: Try to read and catch error
await this.readVersionData(path)
return true
const data = await adapter.getMetadata(key)
return data !== null
} catch {
return false
}
@ -186,11 +187,11 @@ export class VersionStorage {
/**
* Write version data to storage
*
* @param path Storage path
* @param key Storage key
* @param entity Entity data
*/
private async writeVersionData(
path: string,
key: string,
entity: NounMetadata
): Promise<void> {
const adapter = this.brain.storageAdapter
@ -199,67 +200,51 @@ export class VersionStorage {
throw new Error('Storage adapter not available')
}
// Serialize entity data
const data = JSON.stringify(entity, null, 2)
// Write to storage using adapter
if (typeof adapter.writeFile === 'function') {
await adapter.writeFile(path, data)
} else if (typeof adapter.set === 'function') {
await adapter.set(path, data)
} else {
throw new Error('Storage adapter does not support write operations')
}
// v6.3.0: Use saveMetadata for storing version content
// The key is system-prefixed so it routes to _system/ directory
await adapter.saveMetadata(key, entity)
}
/**
* Read version data from storage
*
* @param path Storage path
* @param key Storage key
* @returns Entity data
*/
private async readVersionData(path: string): Promise<NounMetadata> {
private async readVersionData(key: string): Promise<NounMetadata> {
const adapter = this.brain.storageAdapter
if (!adapter) {
throw new Error('Storage adapter not available')
}
// Read from storage using adapter
let data: string
if (typeof adapter.readFile === 'function') {
data = await adapter.readFile(path)
} else if (typeof adapter.get === 'function') {
data = await adapter.get(path)
} else {
throw new Error('Storage adapter does not support read operations')
// v6.3.0: Use getMetadata for reading version content
const entity = await adapter.getMetadata(key)
if (!entity) {
throw new Error(`Version data not found: ${key}`)
}
// Parse entity data
return JSON.parse(data)
return entity
}
/**
* Delete version data from storage
*
* @param path Storage path
* Note: Version content is content-addressed and immutable.
* Deleting the version index entry (via VersionIndex.removeVersion) is sufficient.
* The content may be shared with other versions (same contentHash).
*
* v6.3.0: We don't actually delete version content to avoid breaking
* other versions that may reference the same content hash.
* A separate garbage collection process could clean up unreferenced content.
*
* @param key Storage key (unused - kept for API compatibility)
*/
private async deleteVersionData(path: string): Promise<void> {
const adapter = this.brain.storageAdapter
if (!adapter) {
throw new Error('Storage adapter not available')
}
// Delete from storage using adapter
if (typeof adapter.deleteFile === 'function') {
await adapter.deleteFile(path)
} else if (typeof adapter.delete === 'function') {
await adapter.delete(path)
} else {
throw new Error('Storage adapter does not support delete operations')
}
private async deleteVersionData(_key: string): Promise<void> {
// v6.3.0: Version content is content-addressed and may be shared.
// We don't delete it here to prevent breaking other versions.
// The version INDEX is deleted via VersionIndex.removeVersion().
// A GC process could clean up unreferenced content in the future.
}
}