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:
parent
6e6da5011f
commit
cc6fa00f30
8 changed files with 587 additions and 328 deletions
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
291
src/vfs/EntityManager.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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++
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue