fix: Knowledge Layer EntityManager integration

* 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 <noreply@anthropic.com>
This commit is contained in:
David Snelling 2025-09-25 12:54:35 -07:00
parent 6e6da5011f
commit cc6fa00f30
8 changed files with 587 additions and 328 deletions

View file

@ -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<string, any>
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<ConceptSystemConfig>
private conceptCache = new Map<string, UniversalConcept>()
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<ConceptManifestation>(
{
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<UniversalConcept>(
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<UniversalConcept>(
{ 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<UniversalConcept>(
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<ConceptManifestation>(
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<UniversalConcept>(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<void> {
// 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)

291
src/vfs/EntityManager.ts Normal file
View file

@ -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<string, string>() // domainId -> brainyId
private brainyToMappings = new Map<string, string>() // brainyId -> domainId
constructor(
protected brain: Brainy,
protected systemName: string
) {}
/**
* Store an entity with proper ID management
*/
protected async storeEntity<T extends ManagedEntity>(
entity: T,
nounType: NounType,
embedding?: number[],
data?: any
): Promise<string> {
// 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<T extends ManagedEntity>(
entity: T,
embedding?: number[]
): Promise<void> {
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<T extends ManagedEntity>(domainId: string): Promise<T | null> {
// 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<T extends ManagedEntity>(
criteria: Record<string, any>,
nounType?: NounType,
limit = 100
): Promise<T[]> {
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<string, any>
): Promise<void> {
// 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<string | null> {
// 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<void> {
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<void> {
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)
}
}
}

View file

@ -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<string, any>
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<FileEvent>(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<FileEvent>(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<FileEvent>(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++
}
}

View file

@ -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<PersistentEntityConfig>
private entityCache = new Map<string, PersistentEntity>()
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<string, any>
similar?: string // Find similar entities to this description
}): Promise<PersistentEntity[]> {
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<PersistentEntity>(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<PersistentEntity>({}, 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<string> {
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<void> {
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<PersistentEntity | null> {
async getPersistentEntity(entityId: string): Promise<PersistentEntity | null> {
// 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<PersistentEntity>(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<void> {
private async updatePersistentEntity(entity: PersistentEntity): Promise<void> {
// Find the Brainy entity
const results = await this.brain.find({
where: {

View file

@ -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<SemanticVersioningConfig>
private versionCache = new Map<string, Version[]>()
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<Version>(
{ 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<Buffer | null> {
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<Version>(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

View file

@ -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) {