/** * VersionIndex - Pure Key-Value Version Storage * * Stores version metadata using simple key-value storage: * - Version list per entity: __version_meta_{entityId}_{branch} * - Content stored separately by VersionStorage * * Key Design Decisions: * - Versions are NOT entities (no brain.add()) * - Versions do NOT pollute find() results * - Simple O(1) lookups per entity * - Versions per entity is typically small (10-1000) * * NO MOCKS - Production implementation */ import type { EntityVersion, VersionQuery } from './VersionManager.js' /** * Internal storage structure for version metadata */ interface VersionMetadataStore { entityId: string branch: string versions: VersionEntry[] } /** * Individual version entry in the store */ interface VersionEntry { version: number timestamp: number contentHash: string commitHash?: string tag?: string description?: string author?: string } /** * VersionIndex - Pure key-value version metadata storage * * Uses simple JSON storage instead of creating entities. * This ensures versions never appear in find() results. */ export class VersionIndex { private brain: any // Brainy instance private initialized: boolean = false constructor(brain: any) { this.brain = brain } /** * Initialize version index */ async initialize(): Promise { if (this.initialized) return this.initialized = true } /** * Add version to index * * Stores version entry in key-value storage. * Handles deduplication by content hash. * * @param version Version metadata to store */ async addVersion(version: EntityVersion): Promise { await this.initialize() const key = this.getMetaKey(version.entityId, version.branch) const store = await this.loadStore(key) || { entityId: version.entityId, branch: version.branch, versions: [] } // Check for duplicate content hash (deduplication) const existing = store.versions.find(v => v.contentHash === version.contentHash) if (existing) { // Update tag/description if provided on duplicate save let updated = false if (version.tag && version.tag !== existing.tag) { existing.tag = version.tag updated = true } if (version.description && version.description !== existing.description) { existing.description = version.description updated = true } if (updated) { await this.saveStore(key, store) } return } // Add new version entry store.versions.push({ version: version.version, timestamp: version.timestamp, contentHash: version.contentHash, commitHash: version.commitHash, tag: version.tag, description: version.description, author: version.author }) await this.saveStore(key, store) } /** * Get versions for an entity * * @param query Version query with filters * @returns List of versions (newest first) */ async getVersions(query: VersionQuery): Promise { await this.initialize() const branch = query.branch || this.brain.currentBranch const key = this.getMetaKey(query.entityId, branch) const store = await this.loadStore(key) if (!store) return [] // Convert entries to EntityVersion format let versions: EntityVersion[] = store.versions.map(entry => ({ version: entry.version, entityId: store.entityId, branch: store.branch, timestamp: entry.timestamp, contentHash: entry.contentHash, commitHash: entry.commitHash || '', tag: entry.tag, description: entry.description, author: entry.author })) // Apply filters if (query.tag) { versions = versions.filter(v => v.tag === query.tag) } if (query.startDate) { versions = versions.filter(v => v.timestamp >= query.startDate!) } if (query.endDate) { versions = versions.filter(v => v.timestamp <= query.endDate!) } // Sort newest first (highest version number first) versions.sort((a, b) => b.version - a.version) // Apply pagination const start = query.offset || 0 const end = query.limit ? start + query.limit : undefined return versions.slice(start, end) } /** * Get specific version * * @param entityId Entity ID * @param version Version number * @param branch Branch name * @returns Version metadata or null */ async getVersion( entityId: string, version: number, branch: string ): Promise { const versions = await this.getVersions({ entityId, branch }) return versions.find(v => v.version === version) || null } /** * Get version by tag * * @param entityId Entity ID * @param tag Version tag * @param branch Branch name * @returns Version metadata or null */ async getVersionByTag( entityId: string, tag: string, branch: string ): Promise { const versions = await this.getVersions({ entityId, branch, tag }) return versions[0] || null } /** * Get version count for entity * * @param entityId Entity ID * @param branch Branch name * @returns Number of versions */ async getVersionCount(entityId: string, branch: string): Promise { const key = this.getMetaKey(entityId, branch) const store = await this.loadStore(key) return store?.versions.length || 0 } /** * Remove version from index * * @param entityId Entity ID * @param version Version number * @param branch Branch name */ async removeVersion( entityId: string, version: number, branch: string ): Promise { await this.initialize() const key = this.getMetaKey(entityId, branch) const store = await this.loadStore(key) if (!store) return const initialLength = store.versions.length store.versions = store.versions.filter(v => v.version !== version) // Only save if something was removed if (store.versions.length < initialLength) { await this.saveStore(key, store) } } /** * Clear all versions for an entity * * @param entityId Entity ID * @param branch Branch name * @returns Number of versions deleted */ async clearVersions(entityId: string, branch: string): Promise { const key = this.getMetaKey(entityId, branch) const store = await this.loadStore(key) if (!store) return 0 const count = store.versions.length // Delete the store by saving null/empty await this.saveStore(key, { entityId, branch, versions: [] }) return count } /** * Get all versioned entities (for cleanup/debugging) * * Note: This is an expensive operation that requires scanning. * In the simple key-value approach, we don't maintain a global index. * This method returns an empty array - use storage-level scanning if needed. * * @returns Empty array (not supported in simple approach) */ async getVersionedEntities(): Promise { // In the simple key-value approach, we don't maintain a global index // of all versioned entities. This would require scanning storage. // For most use cases, you know which entities you've versioned. return [] } // ============= Private Helpers ============= /** * Generate storage key for version metadata * * @param entityId Entity ID * @param branch Branch name * @returns Storage key */ private getMetaKey(entityId: string, branch: string): string { return `__version_meta_${entityId}_${branch}` } /** * Load version store from storage * * @param key Storage key * @returns Version store or null */ private async loadStore(key: string): Promise { try { const store = await this.brain.storageAdapter.getMetadata(key) // Handle empty store if (!store || !store.versions) return null return store as VersionMetadataStore } catch { return null } } /** * Save version store to storage * * @param key Storage key * @param store Version store */ private async saveStore(key: string, store: VersionMetadataStore): Promise { await this.brain.storageAdapter.saveMetadata(key, store) } }