brainy/src/versioning/VersionStorage.ts

266 lines
6.8 KiB
TypeScript
Raw Normal View History

feat: add entity versioning system with critical bug fixes (v5.3.0) Entity Versioning (NEW): - Add complete entity versioning API (brain.versions.*) with 18 methods - Content-addressable storage with SHA-256 deduplication - Git-style version control: save, restore, compare, undo, prune - Auto-versioning augmentation with pattern-based filtering - Branch-isolated version histories - Complete integration tests and API documentation Critical Bug Fixes: - Fix commit() not updating branch refs (brainy.ts:2385) - Root cause: Passed "heads/main" which normalized to "refs/heads/heads/main" - Impact: All Git-style versioning features were broken - Fix: Pass branch name directly for correct normalization - Fix VFS entities missing isVFSEntity flag - Add isVFSEntity: true to all VFS files/folders for filtering - Resolves pollution of semantic search with filesystem entities - Updated in writeFile(), mkdir(), and root directory init Implementation: - src/versioning/VersionManager.ts - Core versioning engine - src/versioning/VersionStorage.ts - Content-addressable storage - src/versioning/VersionIndex.ts - Metadata indexing - src/versioning/VersionDiff.ts - Version comparison - src/versioning/VersioningAPI.ts - Public API interface - src/augmentations/versioningAugmentation.ts - Auto-versioning - tests/integration/versioning.test.ts - Full integration tests - tests/unit/versioning/ - Unit test suite Documentation: - Complete Entity Versioning API section in docs/api/README.md - VFS entity filtering guide with examples - Updated "What's New" section for v5.3.0 - Strategy docs for both critical bugs Test Results: - 1168 tests passing - Build: PASSING (no TypeScript errors) - Integration tests: ALL PASSING 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 11:19:02 -08:00
/**
* VersionStorage - Hybrid Storage for Entity Versions (v5.3.0)
*
* Implements content-addressable storage for entity versions:
* - SHA-256 content hashing for deduplication
* - Stores versions in .brainy/versions/ directory
* - 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)
*
* NO MOCKS - Production implementation
*/
import { createHash } from 'crypto'
import { BaseStorage } from '../storage/baseStorage.js'
import type { NounMetadata } from '../coreTypes.js'
import type { EntityVersion } from './VersionManager.js'
/**
* VersionStorage - Content-addressable version storage
*/
export class VersionStorage {
private brain: any // Brainy instance
private initialized: boolean = false
constructor(brain: any) {
this.brain = brain
}
/**
* Initialize version storage directories
*/
async initialize(): Promise<void> {
if (this.initialized) return
// Version storage uses the same storage adapter as the main database
// Directories are created automatically by the storage adapter
this.initialized = true
}
/**
* Calculate SHA-256 hash of entity content
*
* Used for content-addressable storage and deduplication
*
* @param entity Entity to hash
* @returns SHA-256 hash (hex string)
*/
hashEntity(entity: NounMetadata): string {
// Create stable JSON representation (sorted keys)
const stableJson = this.toStableJson(entity)
return createHash('sha256').update(stableJson).digest('hex')
}
/**
* Convert entity to stable JSON (sorted keys for consistent hashing)
*/
private toStableJson(obj: any): string {
if (obj === null) return 'null'
if (obj === undefined) return 'undefined'
if (typeof obj !== 'object') return JSON.stringify(obj)
if (Array.isArray(obj)) {
const items = obj.map((item) => this.toStableJson(item))
return `[${items.join(',') }]`
}
// Sort object keys for stable hashing
const sortedKeys = Object.keys(obj).sort()
const pairs = sortedKeys.map((key) => {
const value = this.toStableJson(obj[key])
return `"${key}":${value}`
})
return `{${pairs.join(',')}}`
}
/**
* Save entity version to content-addressable storage
*
* @param version Version metadata
* @param entity Entity data
*/
async saveVersion(
version: EntityVersion,
entity: NounMetadata
): Promise<void> {
await this.initialize()
// Content-addressable path: .brainy/versions/entities/{entityId}/{contentHash}.json
const versionPath = this.getVersionPath(
version.entityId,
version.contentHash
)
// Check if content already exists (deduplication)
const exists = await this.contentExists(versionPath)
if (exists) {
// Content already stored - no need to write again
return
}
// Store entity data
await this.writeVersionData(versionPath, entity)
}
/**
* Load entity version from storage
*
* @param version Version metadata
* @returns Entity data or null if not found
*/
async loadVersion(version: EntityVersion): Promise<NounMetadata | null> {
await this.initialize()
const versionPath = this.getVersionPath(
version.entityId,
version.contentHash
)
try {
return await this.readVersionData(versionPath)
} catch (error) {
console.error(`Failed to load version ${version.version}:`, error)
return null
}
}
/**
* Delete entity version from storage
*
* @param version Version to delete
*/
async deleteVersion(version: EntityVersion): Promise<void> {
await this.initialize()
const versionPath = this.getVersionPath(
version.entityId,
version.contentHash
)
await this.deleteVersionData(versionPath)
}
/**
* Get version storage path
*
* @param entityId Entity ID
* @param contentHash Content hash
* @returns Storage path
*/
private getVersionPath(entityId: string, contentHash: string): string {
return `versions/entities/${entityId}/${contentHash}.json`
}
/**
* Check if content exists in storage
*
* @param path Storage path
* @returns True if exists
*/
private async contentExists(path: string): Promise<boolean> {
try {
// Use storage adapter's exists check if available
const adapter = this.brain.storageAdapter
if (adapter && typeof adapter.exists === 'function') {
return await adapter.exists(path)
}
// Fallback: Try to read and catch error
await this.readVersionData(path)
return true
} catch {
return false
}
}
/**
* Write version data to storage
*
* @param path Storage path
* @param entity Entity data
*/
private async writeVersionData(
path: string,
entity: NounMetadata
): Promise<void> {
const adapter = this.brain.storageAdapter
if (!adapter) {
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')
}
}
/**
* Read version data from storage
*
* @param path Storage path
* @returns Entity data
*/
private async readVersionData(path: 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')
}
// Parse entity data
return JSON.parse(data)
}
/**
* Delete version data from storage
*
* @param path Storage path
*/
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')
}
}
}