From cc6fa00f30f3e38b3347f035a02209a9a9f01a02 Mon Sep 17 00:00:00 2001 From: David Snelling Date: Thu, 25 Sep 2025 12:54:35 -0700 Subject: [PATCH] fix: Knowledge Layer EntityManager integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add EntityManager base class for standardized entity operations * Update SemanticVersioning to extend EntityManager * Update EventRecorder to extend EntityManager * Update PersistentEntitySystem to extend EntityManager * Update ConceptSystem to extend EntityManager * Fix entity ID mismatch patterns across all Knowledge Layer components * Add proper domain ID to Brainy ID mapping * Replace direct brain.add/find calls with EntityManager methods * Ensure all entity interfaces extend ManagedEntity * Add proper metadata fields for querying (eventType, conceptType, entityType) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- package-lock.json | 4 +- package.json | 2 +- src/vfs/ConceptSystem.ts | 223 +++++++++++------------ src/vfs/EntityManager.ts | 291 ++++++++++++++++++++++++++++++ src/vfs/EventRecorder.ts | 127 ++++++------- src/vfs/PersistentEntitySystem.ts | 166 +++++++++-------- src/vfs/SemanticVersioning.ts | 99 +++++----- src/vfs/VirtualFileSystem.ts | 3 +- 8 files changed, 587 insertions(+), 328 deletions(-) create mode 100644 src/vfs/EntityManager.ts diff --git a/package-lock.json b/package-lock.json index 03637569..ca665293 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@soulcraft/brainy", - "version": "3.10.0", + "version": "3.10.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@soulcraft/brainy", - "version": "3.10.0", + "version": "3.10.1", "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "^3.540.0", diff --git a/package.json b/package.json index fe2ce814..64735a86 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@soulcraft/brainy", - "version": "3.10.0", + "version": "3.10.1", "description": "Universal Knowledge Protocol™ - World's first Triple Intelligence database unifying vector, graph, and document search in one API. 31 nouns × 40 verbs for infinite expressiveness.", "main": "dist/index.js", "module": "dist/index.js", diff --git a/src/vfs/ConceptSystem.ts b/src/vfs/ConceptSystem.ts index 5766f10b..f4a31787 100644 --- a/src/vfs/ConceptSystem.ts +++ b/src/vfs/ConceptSystem.ts @@ -10,11 +10,12 @@ import { Brainy } from '../brainy.js' import { NounType, VerbType } from '../types/graphTypes.js' import { cosineDistance } from '../utils/distance.js' import { v4 as uuidv4 } from '../universal/uuid.js' +import { EntityManager, ManagedEntity } from './EntityManager.js' /** * Universal concept that exists independently of files */ -export interface UniversalConcept { +export interface UniversalConcept extends ManagedEntity { id: string name: string description?: string @@ -28,6 +29,7 @@ export interface UniversalConcept { lastUpdated: number version: number metadata: Record + conceptType?: string // For EntityManager queries } /** @@ -45,7 +47,7 @@ export interface ConceptLink { /** * A manifestation of a concept in a specific location */ -export interface ConceptManifestation { +export interface ConceptManifestation extends ManagedEntity { id: string conceptId: string filePath: string @@ -100,14 +102,15 @@ export interface ConceptGraph { * - "Dependency Injection" pattern across multiple codebases * - "Sustainability" theme in various research papers */ -export class ConceptSystem { +export class ConceptSystem extends EntityManager { private config: Required private conceptCache = new Map() constructor( - private brain: Brainy, + brain: Brainy, config?: ConceptSystemConfig ) { + super(brain, 'vfs-concept') this.config = { autoLink: config?.autoLink ?? false, similarityThreshold: config?.similarityThreshold ?? 0.7, @@ -124,13 +127,20 @@ export class ConceptSystem { const timestamp = Date.now() const universalConcept: UniversalConcept = { - ...concept, id: conceptId, + name: concept.name, + description: concept.description, + domain: concept.domain, + category: concept.category, + keywords: concept.keywords, + strength: concept.strength, + metadata: concept.metadata || {}, created: timestamp, lastUpdated: timestamp, version: 1, links: [], - manifestations: [] + manifestations: [], + conceptType: 'universal' // Add conceptType for querying } // Generate embedding for concept @@ -141,17 +151,13 @@ export class ConceptSystem { console.warn('Failed to generate concept embedding:', error) } - // Store concept in Brainy - const brainyEntity = await this.brain.add({ - type: NounType.Concept, - data: Buffer.from(JSON.stringify(universalConcept)), - metadata: { - ...universalConcept, - conceptType: 'universal', - system: 'vfs-concept' - }, - vector: embedding - }) + // Store concept using EntityManager + await this.storeEntity( + universalConcept, + NounType.Concept, + embedding, + Buffer.from(JSON.stringify(universalConcept)) + ) // Auto-link to similar concepts if enabled if (this.config.autoLink) { @@ -161,7 +167,7 @@ export class ConceptSystem { // Update cache this.conceptCache.set(conceptId, universalConcept) - return brainyEntity + return conceptId // Return domain ID, not Brainy ID } /** @@ -193,17 +199,16 @@ export class ConceptSystem { // File manifestation search if (query.manifestedIn) { // Find concepts that have manifestations in this file - const manifestationResults = await this.brain.find({ - where: { + const manifestationResults = await this.findEntities( + { filePath: query.manifestedIn, - eventType: 'concept-manifestation', - system: 'vfs-concept' + eventType: 'concept-manifestation' }, - type: NounType.Event, - limit: 1000 - }) + NounType.Event, + 1000 + ) - const conceptIds = manifestationResults.map(r => r.entity.metadata.conceptId) + const conceptIds = manifestationResults.map(m => m.conceptId) if (conceptIds.length > 0) { searchQuery.id = { $in: conceptIds } } else { @@ -211,12 +216,12 @@ export class ConceptSystem { } } - // Search in Brainy - let results = await this.brain.find({ - where: searchQuery, - type: NounType.Concept, - limit: 1000 - }) + // Search using EntityManager + const results = await this.findEntities( + searchQuery, + NounType.Concept, + 1000 + ) // If searching for similar concepts, use vector similarity if (query.similar) { @@ -224,29 +229,42 @@ export class ConceptSystem { const queryEmbedding = await this.generateTextEmbedding(query.similar) if (queryEmbedding) { // Get all concepts and rank by similarity - const allConcepts = await this.brain.find({ - where: { conceptType: 'universal', system: 'vfs-concept' }, - type: NounType.Concept, - limit: 10000 - }) + const allConcepts = await this.findEntities( + { conceptType: 'universal' }, + NounType.Concept, + 10000 + ) - const withSimilarity = allConcepts - .filter(c => c.entity.vector && c.entity.vector.length > 0) + // For similarity search, we need to get the actual vector data from Brainy + const conceptsWithVectors = [] + for (const concept of allConcepts) { + if (concept.brainyId) { + const brainyEntity = await this.brain.get(concept.brainyId) + if (brainyEntity?.vector && brainyEntity.vector.length > 0) { + conceptsWithVectors.push({ + concept, + vector: brainyEntity.vector + }) + } + } + } + + const withSimilarity = conceptsWithVectors .map(c => ({ - concept: c, - similarity: 1 - cosineDistance(queryEmbedding, c.entity.vector!) + concept: c.concept, + similarity: 1 - cosineDistance(queryEmbedding, c.vector) })) .filter(s => s.similarity > this.config.similarityThreshold) .sort((a, b) => b.similarity - a.similarity) - results = withSimilarity.map(s => s.concept) + return withSimilarity.map(s => s.concept) } } catch (error) { console.warn('Failed to perform concept similarity search:', error) } } - return results.map(r => r.entity.metadata as UniversalConcept) + return results } /** @@ -302,17 +320,17 @@ export class ConceptSystem { await this.updateConcept(toConcept) } - // Create Brainy relationship - await this.brain.relate({ - from: fromConceptId, - to: toConceptId, - type: this.getVerbType(relationship), - metadata: { + // Create relationship using EntityManager + await this.createRelationship( + fromConceptId, + toConceptId, + this.getVerbType(relationship), + { strength: link.strength, context: link.context, bidirectional: link.bidirectional } - }) + ) return linkId } @@ -351,23 +369,25 @@ export class ConceptSystem { extractedBy: options?.extractedBy ?? 'manual' } - // Store manifestation as Brainy event - await this.brain.add({ - type: NounType.Event, - data: Buffer.from(context), - metadata: { - ...manifestation, - eventType: 'concept-manifestation', - system: 'vfs-concept' - } - }) + // Store manifestation as managed entity (with eventType for manifestations) + const manifestationWithEventType = { + ...manifestation, + eventType: 'concept-manifestation' + } - // Create relationship to concept - await this.brain.relate({ - from: manifestationId, - to: conceptId, - type: VerbType.Implements - }) + await this.storeEntity( + manifestationWithEventType, + NounType.Event, + undefined, + Buffer.from(context) + ) + + // Create relationship to concept using EntityManager + await this.createRelationship( + manifestationId, + conceptId, + VerbType.Implements + ) // Update concept with new manifestation concept.manifestations.push(manifestation) @@ -476,13 +496,11 @@ export class ConceptSystem { query.strength = { $gte: options.minStrength } } - const results = await this.brain.find({ - where: query, - type: NounType.Concept, - limit: options?.maxConcepts || 1000 - }) - - const concepts = results.map(r => r.entity.metadata as UniversalConcept) + const concepts = await this.findEntities( + query, + NounType.Concept, + options?.maxConcepts || 1000 + ) // Build graph structure const graphConcepts = concepts.map(c => ({ @@ -541,15 +559,13 @@ export class ConceptSystem { query.confidence = { $gte: options.minConfidence } } - const results = await this.brain.find({ - where: query, - type: NounType.Event, - limit: options?.limit || 1000 - }) + const manifestations = await this.findEntities( + query, + NounType.Event, + options?.limit || 1000 + ) - return results - .map(r => r.entity.metadata as ConceptManifestation) - .sort((a, b) => b.timestamp - a.timestamp) + return manifestations.sort((a, b) => b.timestamp - a.timestamp) } /** @@ -590,23 +606,11 @@ export class ConceptSystem { return this.conceptCache.get(conceptId)! } - // Query from Brainy - const results = await this.brain.find({ - where: { - id: conceptId, - conceptType: 'universal', - system: 'vfs-concept' - }, - type: NounType.Concept, - limit: 1 - }) - - if (results.length === 0) { - return null + // Query using EntityManager + const concept = await this.getEntity(conceptId) + if (concept) { + this.conceptCache.set(conceptId, concept) } - - const concept = results[0].entity.metadata as UniversalConcept - this.conceptCache.set(conceptId, concept) return concept } @@ -614,28 +618,11 @@ export class ConceptSystem { * Update stored concept */ private async updateConcept(concept: UniversalConcept): Promise { - // Find the Brainy entity - const results = await this.brain.find({ - where: { - id: concept.id, - conceptType: 'universal', - system: 'vfs-concept' - }, - type: NounType.Concept, - limit: 1 - }) + // Add conceptType metadata before updating + concept.conceptType = 'universal' - if (results.length > 0) { - await this.brain.update({ - id: results[0].entity.id, - data: Buffer.from(JSON.stringify(concept)), - metadata: { - ...concept, - conceptType: 'universal', - system: 'vfs-concept' - } - }) - } + // Update using EntityManager + await this.updateEntity(concept) // Update cache this.conceptCache.set(concept.id, concept) diff --git a/src/vfs/EntityManager.ts b/src/vfs/EntityManager.ts new file mode 100644 index 00000000..9afa3ec7 --- /dev/null +++ b/src/vfs/EntityManager.ts @@ -0,0 +1,291 @@ +/** + * EntityManager Base Class + * + * Provides standardized entity ID management for all Knowledge Layer components + * Solves the root cause of ID mismatch issues by establishing clear patterns + */ + +import { Brainy } from '../brainy.js' +import { NounType } from '../types/graphTypes.js' +import { v4 as uuidv4 } from '../universal/uuid.js' + +/** + * Standard entity structure used by all Knowledge Layer components + */ +export interface ManagedEntity { + /** Domain-specific ID (for external references) */ + id: string + /** The actual Brainy entity ID (for internal operations) */ + brainyId?: string + /** Entity metadata */ + [key: string]: any +} + +/** + * ID mapping to track domain IDs to Brainy entity IDs + */ +interface EntityIdMapping { + domainId: string + brainyId: string + system: string + type: string +} + +/** + * EntityManager Base Class + * + * All Knowledge Layer components should extend this to get standardized: + * - Entity storage and retrieval + * - ID management and mapping + * - Query patterns + * - Relationship creation + */ +export abstract class EntityManager { + private idMappings = new Map() // domainId -> brainyId + private brainyToMappings = new Map() // brainyId -> domainId + + constructor( + protected brain: Brainy, + protected systemName: string + ) {} + + /** + * Store an entity with proper ID management + */ + protected async storeEntity( + entity: T, + nounType: NounType, + embedding?: number[], + data?: any + ): Promise { + // Generate domain ID if not provided + if (!entity.id) { + entity.id = uuidv4() + } + + // Store in Brainy and get the Brainy entity ID + const brainyId = await this.brain.add({ + type: nounType, + data: data || Buffer.from(JSON.stringify(entity)), + metadata: { + ...entity, + system: this.systemName, + domainId: entity.id, // Store the domain ID for reference + storedAt: Date.now() + }, + vector: embedding + }) + + // Store ID mapping + this.idMappings.set(entity.id, brainyId) + this.brainyToMappings.set(brainyId, entity.id) + + // Update entity with Brainy ID + entity.brainyId = brainyId + + return entity.id // Return domain ID for external use + } + + /** + * Update an existing entity + */ + protected async updateEntity( + entity: T, + embedding?: number[] + ): Promise { + const brainyId = this.idMappings.get(entity.id) + if (!brainyId) { + throw new Error(`Entity ${entity.id} not found in mappings`) + } + + await this.brain.update({ + id: brainyId, + data: Buffer.from(JSON.stringify(entity)), + metadata: { + ...entity, + system: this.systemName, + domainId: entity.id, + updatedAt: Date.now() + }, + vector: embedding + }) + } + + /** + * Retrieve entity by domain ID + */ + protected async getEntity(domainId: string): Promise { + // First try from cache + let brainyId = this.idMappings.get(domainId) + + // If not in cache, search by domain ID in metadata + if (!brainyId) { + const results = await this.brain.find({ + where: { + domainId, + system: this.systemName + }, + limit: 1 + }) + + if (results.length === 0) { + return null + } + + brainyId = results[0].entity.id + // Cache the mapping + this.idMappings.set(domainId, brainyId) + this.brainyToMappings.set(brainyId, domainId) + } + + // Get entity using Brainy ID + const brainyEntity = await this.brain.get(brainyId) + if (!brainyEntity) { + return null + } + + // Parse entity from metadata + const entity = brainyEntity.metadata as T + entity.brainyId = brainyId + return entity + } + + /** + * Find entities by metadata criteria + */ + protected async findEntities( + criteria: Record, + nounType?: NounType, + limit = 100 + ): Promise { + const query: any = { + ...criteria, + system: this.systemName + } + + const results = await this.brain.find({ + where: query, + type: nounType, + limit + }) + + const entities: T[] = [] + for (const result of results) { + const entity = result.entity.metadata as T + entity.brainyId = result.entity.id + + // Update mappings cache + this.idMappings.set(entity.id, result.entity.id) + this.brainyToMappings.set(result.entity.id, entity.id) + + entities.push(entity) + } + + return entities + } + + /** + * Create relationship between entities using proper Brainy IDs + */ + protected async createRelationship( + fromDomainId: string, + toDomainId: string, + relationshipType: any, + metadata?: Record + ): Promise { + // Get Brainy IDs for both entities + const fromBrainyId = await this.getBrainyId(fromDomainId) + const toBrainyId = await this.getBrainyId(toDomainId) + + if (!fromBrainyId || !toBrainyId) { + throw new Error(`Cannot find Brainy IDs for relationship: ${fromDomainId} -> ${toDomainId}`) + } + + // Create relationship using Brainy IDs + await this.brain.relate({ + from: fromBrainyId, + to: toBrainyId, + type: relationshipType, + metadata + }) + } + + /** + * Get Brainy ID for a domain ID + */ + protected async getBrainyId(domainId: string): Promise { + // Check cache first + let brainyId = this.idMappings.get(domainId) + + if (!brainyId) { + // Search in Brainy + const results = await this.brain.find({ + where: { + domainId, + system: this.systemName + }, + limit: 1 + }) + + if (results.length > 0) { + brainyId = results[0].entity.id + // Cache the mapping + this.idMappings.set(domainId, brainyId) + this.brainyToMappings.set(brainyId, domainId) + } + } + + return brainyId || null + } + + /** + * Get domain ID for a Brainy ID + */ + protected getDomainId(brainyId: string): string | null { + return this.brainyToMappings.get(brainyId) || null + } + + /** + * Delete entity by domain ID + */ + protected async deleteEntity(domainId: string): Promise { + const brainyId = await this.getBrainyId(domainId) + if (brainyId) { + await this.brain.delete(brainyId) + // Clean up mappings + this.idMappings.delete(domainId) + this.brainyToMappings.delete(brainyId) + } + } + + /** + * Clear ID mapping cache (useful for tests) + */ + protected clearMappingCache(): void { + this.idMappings.clear() + this.brainyToMappings.clear() + } + + /** + * Batch load mappings for performance + */ + protected async loadMappings(domainIds: string[]): Promise { + const unknownIds = domainIds.filter(id => !this.idMappings.has(id)) + + if (unknownIds.length === 0) return + + const results = await this.brain.find({ + where: { + domainId: { $in: unknownIds }, + system: this.systemName + }, + limit: unknownIds.length + }) + + for (const result of results) { + const domainId = result.entity.metadata.domainId + this.idMappings.set(domainId, result.entity.id) + this.brainyToMappings.set(result.entity.id, domainId) + } + } +} \ No newline at end of file diff --git a/src/vfs/EventRecorder.ts b/src/vfs/EventRecorder.ts index 8498aca8..fba008e0 100644 --- a/src/vfs/EventRecorder.ts +++ b/src/vfs/EventRecorder.ts @@ -9,11 +9,12 @@ import { Brainy } from '../brainy.js' import { NounType, VerbType } from '../types/graphTypes.js' import { v4 as uuidv4 } from '../universal/uuid.js' import { createHash } from 'crypto' +import { EntityManager, ManagedEntity } from './EntityManager.js' /** * File operation event */ -export interface FileEvent { +export interface FileEvent extends ManagedEntity { id: string type: 'create' | 'write' | 'append' | 'delete' | 'move' | 'rename' | 'mkdir' | 'rmdir' path: string @@ -25,13 +26,16 @@ export interface FileEvent { metadata?: Record previousHash?: string // For tracking changes oldPath?: string // For move/rename operations + eventType?: string // For EntityManager queries } /** * Event Recorder - Stores all file operations as searchable events */ -export class EventRecorder { - constructor(private brain: Brainy) {} +export class EventRecorder extends EntityManager { + constructor(brain: Brainy) { + super(brain, 'vfs') + } /** * Record a file operation event @@ -45,27 +49,44 @@ export class EventRecorder { ? createHash('sha256').update(event.content).digest('hex') : undefined - // Store event as Brainy entity - const entity = await this.brain.add({ - type: NounType.Event, - data: event.content || Buffer.from(JSON.stringify(event)), - metadata: { - ...event, - id: eventId, - timestamp, - hash, - eventType: 'file-operation', - system: 'vfs' - }, - // Generate embedding for content-based events - vector: event.content && event.content.length < 100000 - ? await this.generateEventEmbedding(event) - : undefined - }) + // Create file event + const fileEvent: FileEvent = { + id: eventId, + type: event.type, + path: event.path, + timestamp, + content: event.content, + size: event.size, + hash, + author: event.author, + metadata: event.metadata, + previousHash: event.previousHash, + oldPath: event.oldPath + } - return entity + // Generate embedding for content-based events + const embedding = event.content && event.content.length < 100000 + ? await this.generateEventEmbedding(event) + : undefined + + // Add eventType for event classification + const eventWithType = { + ...fileEvent, + eventType: 'file-operation' + } + + // Store event using EntityManager + await this.storeEntity( + eventWithType, + NounType.Event, + embedding, + event.content || Buffer.from(JSON.stringify(event)) + ) + + return eventId } + /** * Get all events matching criteria */ @@ -95,19 +116,8 @@ export class EventRecorder { query.type = { $in: options.types } } - // Query events from Brainy - const results = await this.brain.find({ - where: query, - type: NounType.Event, - limit - }) - - const events: FileEvent[] = [] - for (const result of results) { - if (result.entity?.metadata) { - events.push(result.entity.metadata as FileEvent) - } - } + // Query using EntityManager + const events = await this.findEntities(query, NounType.Event, limit) return events.sort((a, b) => b.timestamp - a.timestamp) } @@ -138,28 +148,8 @@ export class EventRecorder { query.type = { $in: options.types } } - // Query events from Brainy - const results = await this.brain.find({ - where: query, - type: NounType.Event, - limit: options?.limit || 100, - // Sort by timestamp descending (newest first) - // Note: Sorting would need to be implemented in Brainy - }) - - // Convert results to FileEvent format - const events = results.map(r => ({ - id: r.entity.metadata.id, - type: r.entity.metadata.type, - path: r.entity.metadata.path, - timestamp: r.entity.metadata.timestamp, - content: r.entity.metadata.content, - size: r.entity.metadata.size, - hash: r.entity.metadata.hash, - author: r.entity.metadata.author, - metadata: r.entity.metadata.metadata, - previousHash: r.entity.metadata.previousHash - } as FileEvent)) + // Query using EntityManager + const events = await this.findEntities(query, NounType.Event, options?.limit || 100) // Sort by timestamp (newest first) return events.sort((a, b) => b.timestamp - a.timestamp) @@ -219,20 +209,17 @@ export class EventRecorder { query.timestamp.$lte = until } - const results = await this.brain.find({ - where: query, - type: NounType.Event, - limit: 1000 - }) + // Query using EntityManager + const events = await this.findEntities(query, NounType.Event, 1000) - return results.map(r => ({ - id: r.entity.metadata.id, - type: r.entity.metadata.type, - path: r.entity.metadata.path, - timestamp: r.entity.metadata.timestamp, - size: r.entity.metadata.size, - hash: r.entity.metadata.hash, - author: r.entity.metadata.author + return events.map(event => ({ + id: event.id, + type: event.type, + path: event.path, + timestamp: event.timestamp, + size: event.size, + hash: event.hash, + author: event.author } as FileEvent)) } @@ -357,7 +344,7 @@ export class EventRecorder { for (let i = 0; i < events.length; i++) { // Keep every Nth event for history sampling if (i % keepEvery !== 0) { - await this.brain.delete(events[i].id) + await this.deleteEntity(events[i].id) pruned++ } } diff --git a/src/vfs/PersistentEntitySystem.ts b/src/vfs/PersistentEntitySystem.ts index 00f7a5ce..d2babc51 100644 --- a/src/vfs/PersistentEntitySystem.ts +++ b/src/vfs/PersistentEntitySystem.ts @@ -10,11 +10,12 @@ import { Brainy } from '../brainy.js' import { NounType, VerbType } from '../types/graphTypes.js' import { cosineDistance } from '../utils/distance.js' import { v4 as uuidv4 } from '../universal/uuid.js' +import { EntityManager, ManagedEntity } from './EntityManager.js' /** * Persistent entity that exists across files and evolves over time */ -export interface PersistentEntity { +export interface PersistentEntity extends ManagedEntity { id: string name: string type: string // 'character', 'api', 'service', 'concept', 'customer', etc. @@ -25,12 +26,13 @@ export interface PersistentEntity { created: number lastUpdated: number version: number + entityType?: string // For EntityManager queries } /** * An appearance of an entity in a specific file/location */ -export interface EntityAppearance { +export interface EntityAppearance extends ManagedEntity { id: string entityId: string filePath: string @@ -44,6 +46,7 @@ export interface EntityAppearance { version: number changes?: EntityChange[] confidence: number // How confident we are this is the entity (0-1) + eventType?: string // For EntityManager queries } /** @@ -78,14 +81,15 @@ export interface PersistentEntityConfig { * - Business entities that appear in multiple reports * - Code classes/functions that span multiple files */ -export class PersistentEntitySystem { +export class PersistentEntitySystem extends EntityManager { private config: Required private entityCache = new Map() constructor( - private brain: Brainy, + brain: Brainy, config?: PersistentEntityConfig ) { + super(brain, 'vfs-entity') this.config = { autoExtract: config?.autoExtract ?? false, similarityThreshold: config?.similarityThreshold ?? 0.8, @@ -102,12 +106,17 @@ export class PersistentEntitySystem { const timestamp = Date.now() const persistentEntity: PersistentEntity = { - ...entity, id: entityId, + name: entity.name, + type: entity.type, + description: entity.description, + aliases: entity.aliases, + attributes: entity.attributes, created: timestamp, lastUpdated: timestamp, version: 1, - appearances: [] + appearances: [], + entityType: 'persistent' // Add entityType for querying } // Generate embedding for entity description @@ -120,22 +129,18 @@ export class PersistentEntitySystem { console.warn('Failed to generate entity embedding:', error) } - // Store entity in Brainy - const brainyEntity = await this.brain.add({ - type: NounType.Concept, - data: Buffer.from(JSON.stringify(persistentEntity)), - metadata: { - ...persistentEntity, - entityType: 'persistent', - system: 'vfs-entity' - }, - vector: embedding - }) + // Store entity using EntityManager + await this.storeEntity( + persistentEntity, + NounType.Concept, + embedding, + Buffer.from(JSON.stringify(persistentEntity)) + ) // Update cache this.entityCache.set(entityId, persistentEntity) - return brainyEntity + return entityId // Return domain ID, not Brainy ID } /** @@ -147,10 +152,7 @@ export class PersistentEntitySystem { attributes?: Record similar?: string // Find similar entities to this description }): Promise { - const searchQuery: any = { - entityType: 'persistent', - system: 'vfs-entity' - } + const searchQuery: any = {} if (query.name) { // Search by exact name or aliases @@ -170,42 +172,45 @@ export class PersistentEntitySystem { } } - // Search in Brainy - let results = await this.brain.find({ - where: searchQuery, - type: NounType.Concept, - limit: 100 - }) + // Add system metadata for EntityManager + searchQuery.entityType = 'persistent' + + // Search using EntityManager + let results = await this.findEntities(searchQuery, NounType.Concept, 100) // If searching for similar entities, use vector similarity if (query.similar) { try { const queryEmbedding = await this.generateTextEmbedding(query.similar) if (queryEmbedding) { - // Get all entities and rank by similarity - const allEntities = await this.brain.find({ - where: { entityType: 'persistent', system: 'vfs-entity' }, - type: NounType.Concept, - limit: 1000 - }) + // Get all entities and rank by similarity using EntityManager + const allEntities = await this.findEntities({}, NounType.Concept, 1000) const withSimilarity = allEntities - .filter(e => e.entity.vector && e.entity.vector.length > 0) - .map(e => ({ - entity: e, - similarity: 1 - cosineDistance(queryEmbedding, e.entity.vector!) - })) - .filter(s => s.similarity > this.config.similarityThreshold) - .sort((a, b) => b.similarity - a.similarity) + .filter(e => e.brainyId) // Only entities with brainyId have vectors + .map(async e => { + const brainyEntity = await this.brain.get(e.brainyId!) + if (brainyEntity?.vector) { + return { + entity: e, + similarity: 1 - cosineDistance(queryEmbedding, brainyEntity.vector) + } + } + return null + }) - results = withSimilarity.map(s => s.entity) + const resolved = (await Promise.all(withSimilarity)) + .filter(s => s !== null && s.similarity > this.config.similarityThreshold) + .sort((a, b) => b!.similarity - a!.similarity) + + results = resolved.map(s => s!.entity) } } catch (error) { console.warn('Failed to perform similarity search:', error) } } - return results.map(r => r.entity.metadata as PersistentEntity) + return results } /** @@ -221,7 +226,7 @@ export class PersistentEntitySystem { extractChanges?: boolean } ): Promise { - const entity = await this.getEntity(entityId) + const entity = await this.getPersistentEntity(entityId) if (!entity) { throw new Error(`Entity ${entityId} not found`) } @@ -247,23 +252,25 @@ export class PersistentEntitySystem { confidence: options?.confidence ?? 1.0 } - // Store appearance as Brainy entity - await this.brain.add({ - type: NounType.Event, - data: Buffer.from(context), - metadata: { - ...appearance, - eventType: 'entity-appearance', - system: 'vfs-entity' - } - }) + // Store appearance as managed entity (with eventType for appearances) + const appearanceWithEventType = { + ...appearance, + eventType: 'entity-appearance' + } - // Create relationship to entity - await this.brain.relate({ - from: appearanceId, - to: entityId, - type: VerbType.References - }) + await this.storeEntity( + appearanceWithEventType, + NounType.Event, + undefined, + Buffer.from(context) + ) + + // Create relationship to entity using EntityManager + await this.createRelationship( + appearanceId, + entityId, + VerbType.References + ) // Update entity with new appearance entity.appearances.push(appearance) @@ -287,7 +294,7 @@ export class PersistentEntitySystem { } // Update stored entity - await this.updateEntity(entity) + await this.updatePersistentEntity(entity) return appearanceId } @@ -304,7 +311,7 @@ export class PersistentEntitySystem { appearance?: EntityAppearance }> }> { - const entity = await this.getEntity(entityId) + const entity = await this.getPersistentEntity(entityId) if (!entity) { throw new Error(`Entity ${entityId} not found`) } @@ -377,7 +384,7 @@ export class PersistentEntitySystem { source: string, reason?: string ): Promise { - const entity = await this.getEntity(entityId) + const entity = await this.getPersistentEntity(entityId) if (!entity) { throw new Error(`Entity ${entityId} not found`) } @@ -405,7 +412,7 @@ export class PersistentEntitySystem { entity.version++ // Update stored entity - await this.updateEntity(entity) + await this.updatePersistentEntity(entity) // Record evolution event if (changes.length > 0) { @@ -443,7 +450,7 @@ export class PersistentEntitySystem { // Character names (capitalized words) /\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b/g, // API endpoints - /\/api\/[a-zA-Z0-9\/\-_]+/g, + /\/api\/[a-zA-Z0-9/\-_]+/g, // Class names /class\s+([A-Z][a-zA-Z0-9_]*)/g, // Function names @@ -471,7 +478,7 @@ export class PersistentEntitySystem { } // Record appearance for existing or new entity - const entity = existing[0] || await this.getEntity(entities[entities.length - 1]) + const entity = existing[0] || await this.getPersistentEntity(entities[entities.length - 1]) if (entity) { await this.recordAppearance( entity.id, @@ -524,38 +531,29 @@ export class PersistentEntitySystem { } /** - * Get entity by ID + * Get persistent entity by ID */ - private async getEntity(entityId: string): Promise { + async getPersistentEntity(entityId: string): Promise { // Check cache first if (this.entityCache.has(entityId)) { return this.entityCache.get(entityId)! } - // Query from Brainy - const results = await this.brain.find({ - where: { - id: entityId, - entityType: 'persistent', - system: 'vfs-entity' - }, - type: NounType.Concept, - limit: 1 - }) + // Use parent getEntity method + const entity = await super.getEntity(entityId) - if (results.length === 0) { - return null + if (entity) { + // Cache and return + this.entityCache.set(entityId, entity) } - const entity = results[0].entity.metadata as PersistentEntity - this.entityCache.set(entityId, entity) return entity } /** - * Update stored entity + * Update stored entity (rename to avoid parent method conflict) */ - private async updateEntity(entity: PersistentEntity): Promise { + private async updatePersistentEntity(entity: PersistentEntity): Promise { // Find the Brainy entity const results = await this.brain.find({ where: { diff --git a/src/vfs/SemanticVersioning.ts b/src/vfs/SemanticVersioning.ts index 3151aed0..bbb98a71 100644 --- a/src/vfs/SemanticVersioning.ts +++ b/src/vfs/SemanticVersioning.ts @@ -10,11 +10,12 @@ import { NounType, VerbType } from '../types/graphTypes.js' import { cosineDistance } from '../utils/distance.js' import { createHash } from 'crypto' import { v4 as uuidv4 } from '../universal/uuid.js' +import { EntityManager, ManagedEntity } from './EntityManager.js' /** * Version metadata */ -export interface Version { +export interface Version extends ManagedEntity { id: string path: string version: number @@ -43,14 +44,15 @@ export interface SemanticVersioningConfig { * Creates versions only when content meaning changes significantly * Uses vector embeddings to detect semantic changes */ -export class SemanticVersioning { +export class SemanticVersioning extends EntityManager { private config: Required private versionCache = new Map() constructor( - private brain: Brainy, + brain: Brainy, config?: SemanticVersioningConfig ) { + super(brain, 'vfs-version') this.config = { threshold: config?.threshold ?? 0.3, maxVersions: config?.maxVersions ?? 10, @@ -128,33 +130,31 @@ export class SemanticVersioning { console.warn('Failed to generate embedding for version:', error) } - // Store version as Brainy entity - const entity = await this.brain.add({ - type: NounType.State, - data: content, // Store actual content - metadata: { - id: versionId, - path, - version: versionNumber, - timestamp, - hash, - semanticHash, - size: content.length, - author: metadata?.author, - message: metadata?.message, - parentVersion, - system: 'vfs-version' - } as Version, - vector: embedding - }) + // Create version entity + const version: Version = { + id: versionId, + path, + version: versionNumber, + timestamp, + hash, + semanticHash, + size: content.length, + author: metadata?.author, + message: metadata?.message, + parentVersion + } + + // Store version using EntityManager (with actual content as data) + await this.storeEntity(version, NounType.State, embedding, content) // Create relationship to parent version if exists if (parentVersion) { - await this.brain.relate({ - from: entity, - to: parentVersion, - type: VerbType.Succeeds - }) + try { + await this.createRelationship(versionId, parentVersion, VerbType.Succeeds) + } catch (error) { + console.warn(`Failed to create parent relationship for version ${versionId}:`, error) + // Continue without relationship - non-critical for version functionality + } } // Update cache @@ -189,19 +189,15 @@ export class SemanticVersioning { return this.versionCache.get(path)! } - // Query from Brainy - const results = await this.brain.find({ - where: { - path, - system: 'vfs-version' - }, - type: NounType.State, - limit: this.config.maxVersions * 2 // Get extra in case some are pruned - }) + // Query using EntityManager + const versions = await this.findEntities( + { path }, + NounType.State, + this.config.maxVersions * 2 // Get extra in case some are pruned + ) - const versions = results - .map(r => r.entity.metadata as Version) - .sort((a, b) => b.timestamp - a.timestamp) // Newest first + // Sort by timestamp (newest first) + versions.sort((a, b) => b.timestamp - a.timestamp) // Update cache this.versionCache.set(path, versions) @@ -213,21 +209,20 @@ export class SemanticVersioning { * Get a specific version's content */ async getVersion(path: string, versionId: string): Promise { - const results = await this.brain.find({ - where: { - id: versionId, - path, - system: 'vfs-version' - }, - type: NounType.State, - limit: 1 - }) - - if (results.length === 0) { + // Get the version entity + const version = await this.getEntity(versionId) + if (!version || version.path !== path) { return null } - return results[0].entity.data as Buffer + // Get the content from Brainy using the Brainy ID + const brainyId = await this.getBrainyId(versionId) + if (!brainyId) { + return null + } + + const entity = await this.brain.get(brainyId) + return entity?.data as Buffer | null } /** @@ -319,7 +314,7 @@ export class SemanticVersioning { // Delete excess versions for (const id of toDelete.slice(0, versions.length - this.config.maxVersions)) { - await this.brain.delete(id) + await this.deleteEntity(id) } // Update cache diff --git a/src/vfs/VirtualFileSystem.ts b/src/vfs/VirtualFileSystem.ts index cb519731..3aa0db78 100644 --- a/src/vfs/VirtualFileSystem.ts +++ b/src/vfs/VirtualFileSystem.ts @@ -2057,7 +2057,7 @@ export class VirtualFileSystem implements IVirtualFileSystem { case 'mkdir': await this.mkdir(op.path, op.options) break - case 'update': + case 'update': { // Update only metadata without changing content const entityId = await this.pathResolver.resolve(op.path) await this.brain.update({ @@ -2065,6 +2065,7 @@ export class VirtualFileSystem implements IVirtualFileSystem { metadata: op.options?.metadata }) break + } } result.successful++ } catch (error: any) {