feat: add neural extraction APIs with NounType taxonomy

Add brain.extract() and brain.extractConcepts() methods that use
NeuralEntityExtractor with embeddings and sophisticated NounType
taxonomy (30+ entity types) for semantic entity and concept extraction.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
David Snelling 2025-09-29 13:51:47 -07:00
parent 27cc699555
commit dd50d89ad6
41 changed files with 3807 additions and 7391 deletions

View file

@ -1,778 +0,0 @@
/**
* Universal Concept System for VFS
*
* Manages concepts that transcend files and exist independently
* Ideas that can be linked to multiple manifestations across domains
* PRODUCTION-READY: Real implementation using Brainy
*/
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 extends ManagedEntity {
id: string
name: string
description?: string
domain: string // 'business', 'technical', 'creative', 'academic', etc.
category: string // 'pattern', 'principle', 'method', 'entity', etc.
keywords: string[] // Associated keywords/tags
links: ConceptLink[] // Links to other concepts
manifestations: ConceptManifestation[] // Where concept appears
strength: number // How well-established this concept is (0-1)
created: number
lastUpdated: number
version: number
metadata: Record<string, any>
conceptType?: string // For EntityManager queries
}
/**
* A link between concepts
*/
export interface ConceptLink {
id: string
targetConceptId: string
relationship: 'extends' | 'implements' | 'uses' | 'opposite' | 'related' | 'contains' | 'part-of'
strength: number // How strong the relationship is (0-1)
context?: string // Why/how they're related
bidirectional: boolean // Is the relationship mutual?
}
/**
* A manifestation of a concept in a specific location
*/
export interface ConceptManifestation extends ManagedEntity {
id: string
conceptId: string
filePath: string
context: string // Surrounding content
form: 'definition' | 'usage' | 'example' | 'discussion' | 'implementation'
position?: {
line?: number
column?: number
offset?: number
}
confidence: number // How confident we are this represents the concept (0-1)
timestamp: number
extractedBy: 'manual' | 'auto' | 'ai'
}
/**
* Configuration for concept system
*/
export interface ConceptSystemConfig {
autoLink?: boolean // Auto-link related concepts
similarityThreshold?: number // For concept matching (0-1)
maxManifestations?: number // Max manifestations to track per concept
strengthDecay?: number // How concept strength decays over time
}
/**
* Concept graph structure for visualization
*/
export interface ConceptGraph {
concepts: Array<{
id: string
name: string
domain: string
strength: number
manifestationCount: number
}>
links: Array<{
source: string
target: string
relationship: string
strength: number
}>
}
/**
* Universal Concept System
*
* Manages concepts that exist independently of any specific file or context
* Examples:
* - "Authentication" concept appearing in docs, code, tests
* - "Customer Journey" concept in marketing, UX, analytics
* - "Dependency Injection" pattern across multiple codebases
* - "Sustainability" theme in various research papers
*/
export class ConceptSystem extends EntityManager {
private config: Required<ConceptSystemConfig>
private conceptCache = new Map<string, UniversalConcept>()
constructor(
brain: Brainy,
config?: ConceptSystemConfig
) {
super(brain, 'vfs-concept')
this.config = {
autoLink: config?.autoLink ?? false,
similarityThreshold: config?.similarityThreshold ?? 0.7,
maxManifestations: config?.maxManifestations ?? 1000,
strengthDecay: config?.strengthDecay ?? 0.95 // 5% decay over time
}
}
/**
* Create a new universal concept
*/
async createConcept(concept: Omit<UniversalConcept, 'id' | 'created' | 'lastUpdated' | 'version' | 'links' | 'manifestations'>): Promise<string> {
const conceptId = uuidv4()
const timestamp = Date.now()
const universalConcept: UniversalConcept = {
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: [],
conceptType: 'universal' // Add conceptType for querying
}
// Generate embedding for concept
let embedding: number[] | undefined
try {
embedding = await this.generateConceptEmbedding(universalConcept)
} catch (error) {
console.warn('Failed to generate concept embedding:', error)
}
// 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) {
await this.autoLinkConcept(conceptId)
}
// Update cache
this.conceptCache.set(conceptId, universalConcept)
return conceptId // Return domain ID, not Brainy ID
}
/**
* Find concepts by various criteria
*/
async findConcepts(query: {
name?: string
domain?: string
category?: string
keywords?: string[]
similar?: string // Find concepts similar to this text
manifestedIn?: string // Find concepts that appear in this file
}): Promise<UniversalConcept[]> {
const searchQuery: any = {
conceptType: 'universal',
system: 'vfs-concept'
}
// Direct attribute matching
if (query.name) searchQuery.name = query.name
if (query.domain) searchQuery.domain = query.domain
if (query.category) searchQuery.category = query.category
// Keyword matching
if (query.keywords && query.keywords.length > 0) {
searchQuery.keywords = { $in: query.keywords }
}
// File manifestation search
if (query.manifestedIn) {
// Find concepts that have manifestations in this file
const manifestationResults = await this.findEntities<ConceptManifestation>(
{
filePath: query.manifestedIn,
eventType: 'concept-manifestation'
},
NounType.Event,
1000
)
const conceptIds = manifestationResults.map(m => m.conceptId)
if (conceptIds.length > 0) {
searchQuery.id = { $in: conceptIds }
} else {
return [] // No concepts found in this file
}
}
// Search using EntityManager
const results = await this.findEntities<UniversalConcept>(
searchQuery,
NounType.Concept,
1000
)
// If searching for similar concepts, use vector similarity
if (query.similar) {
try {
const queryEmbedding = await this.generateTextEmbedding(query.similar)
if (queryEmbedding) {
// Get all concepts and rank by similarity
const allConcepts = await this.findEntities<UniversalConcept>(
{ conceptType: 'universal' },
NounType.Concept,
10000
)
// 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.concept,
similarity: 1 - cosineDistance(queryEmbedding, c.vector)
}))
.filter(s => s.similarity > this.config.similarityThreshold)
.sort((a, b) => b.similarity - a.similarity)
return withSimilarity.map(s => s.concept)
}
} catch (error) {
console.warn('Failed to perform concept similarity search:', error)
}
}
return results
}
/**
* Link two concepts together
*/
async linkConcept(
fromConceptId: string,
toConceptId: string,
relationship: ConceptLink['relationship'],
options?: {
strength?: number
context?: string
bidirectional?: boolean
}
): Promise<string> {
const linkId = uuidv4()
const fromConcept = await this.getConcept(fromConceptId)
const toConcept = await this.getConcept(toConceptId)
if (!fromConcept || !toConcept) {
throw new Error('One or both concepts not found')
}
// Create link
const link: ConceptLink = {
id: linkId,
targetConceptId: toConceptId,
relationship,
strength: options?.strength ?? 0.8,
context: options?.context,
bidirectional: options?.bidirectional ?? false
}
// Add link to source concept
fromConcept.links.push(link)
fromConcept.lastUpdated = Date.now()
await this.updateConcept(fromConcept)
// Add bidirectional link if specified
if (link.bidirectional) {
const reverseRelationship = this.getReverseRelationship(relationship)
const reverseLink: ConceptLink = {
id: uuidv4(),
targetConceptId: fromConceptId,
relationship: reverseRelationship,
strength: link.strength,
context: link.context,
bidirectional: true
}
toConcept.links.push(reverseLink)
toConcept.lastUpdated = Date.now()
await this.updateConcept(toConcept)
}
// Create relationship using EntityManager
await this.createRelationship(
fromConceptId,
toConceptId,
this.getVerbType(relationship),
{
strength: link.strength,
context: link.context,
bidirectional: link.bidirectional
}
)
return linkId
}
/**
* Record a manifestation of a concept in a file
*/
async recordManifestation(
conceptId: string,
filePath: string,
context: string,
form: ConceptManifestation['form'],
options?: {
position?: ConceptManifestation['position']
confidence?: number
extractedBy?: ConceptManifestation['extractedBy']
}
): Promise<string> {
const concept = await this.getConcept(conceptId)
if (!concept) {
throw new Error(`Concept ${conceptId} not found`)
}
const manifestationId = uuidv4()
const timestamp = Date.now()
const manifestation: ConceptManifestation = {
id: manifestationId,
conceptId,
filePath,
context,
form,
position: options?.position,
confidence: options?.confidence ?? 1.0,
timestamp,
extractedBy: options?.extractedBy ?? 'manual'
}
// Store manifestation as managed entity (with eventType for manifestations)
const manifestationWithEventType = {
...manifestation,
eventType: 'concept-manifestation'
}
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)
concept.lastUpdated = timestamp
// Update concept strength based on manifestations
concept.strength = Math.min(1.0, concept.strength + 0.1)
// Prune old manifestations if needed
if (concept.manifestations.length > this.config.maxManifestations) {
concept.manifestations = concept.manifestations
.sort((a, b) => b.timestamp - a.timestamp)
.slice(0, this.config.maxManifestations)
}
// Update stored concept
await this.updateConcept(concept)
return manifestationId
}
/**
* Extract and link concepts from content
*/
async extractAndLinkConcepts(filePath: string, content: Buffer): Promise<string[]> {
if (!this.config.autoLink) {
return []
}
const text = content.toString('utf8')
const extractedConcepts: string[] = []
// Simple concept extraction patterns
// In production, this would use advanced NLP/AI models
const conceptPatterns = [
// Technical concepts
/\b(authentication|authorization|validation|encryption|caching|logging|monitoring)\b/gi,
// Business concepts
/\b(customer\s+journey|user\s+experience|business\s+logic|revenue\s+model)\b/gi,
// Design patterns
/\b(singleton|factory|observer|strategy|adapter|decorator)\b/gi,
// General concepts
/\b(security|performance|scalability|maintainability|reliability)\b/gi
]
for (const pattern of conceptPatterns) {
const matches = text.matchAll(pattern)
for (const match of matches) {
const conceptName = match[0].toLowerCase()
const context = this.extractContext(text, match.index || 0)
// Find or create concept
let concepts = await this.findConcepts({ name: conceptName })
let conceptId: string
if (concepts.length === 0) {
// Create new concept
conceptId = await this.createConcept({
name: conceptName,
domain: this.detectDomain(conceptName, text),
category: this.detectCategory(conceptName),
keywords: [conceptName],
strength: 0.5,
metadata: {}
})
extractedConcepts.push(conceptId)
} else {
conceptId = concepts[0].id
}
// Record manifestation
await this.recordManifestation(
conceptId,
filePath,
context,
this.detectManifestationForm(context),
{
confidence: 0.8,
extractedBy: 'auto'
}
)
}
}
return extractedConcepts
}
/**
* Get concept graph for visualization
*/
async getConceptGraph(options?: {
domain?: string
minStrength?: number
maxConcepts?: number
}): Promise<ConceptGraph> {
const query: any = {
conceptType: 'universal',
system: 'vfs-concept'
}
if (options?.domain) {
query.domain = options.domain
}
if (options?.minStrength) {
query.strength = { $gte: options.minStrength }
}
const concepts = await this.findEntities<UniversalConcept>(
query,
NounType.Concept,
options?.maxConcepts || 1000
)
// Build graph structure
const graphConcepts = concepts.map(c => ({
id: c.id,
name: c.name,
domain: c.domain,
strength: c.strength,
manifestationCount: c.manifestations.length
}))
const graphLinks: ConceptGraph['links'] = []
for (const concept of concepts) {
for (const link of concept.links) {
// Only include links to concepts in our result set
if (concepts.find(c => c.id === link.targetConceptId)) {
graphLinks.push({
source: concept.id,
target: link.targetConceptId,
relationship: link.relationship,
strength: link.strength
})
}
}
}
return {
concepts: graphConcepts,
links: graphLinks
}
}
/**
* Find appearances of a concept
*/
async findAppearances(conceptId: string, options?: {
filePath?: string
form?: ConceptManifestation['form']
minConfidence?: number
limit?: number
}): Promise<ConceptManifestation[]> {
const query: any = {
conceptId,
eventType: 'concept-manifestation',
system: 'vfs-concept'
}
if (options?.filePath) {
query.filePath = options.filePath
}
if (options?.form) {
query.form = options.form
}
if (options?.minConfidence) {
query.confidence = { $gte: options.minConfidence }
}
const manifestations = await this.findEntities<ConceptManifestation>(
query,
NounType.Event,
options?.limit || 1000
)
return manifestations.sort((a, b) => b.timestamp - a.timestamp)
}
/**
* Auto-link concept to similar concepts
*/
private async autoLinkConcept(conceptId: string): Promise<void> {
const concept = await this.getConcept(conceptId)
if (!concept) return
// Find similar concepts
const similar = await this.findConcepts({
similar: concept.name + ' ' + (concept.description || '')
})
for (const similarConcept of similar) {
if (similarConcept.id === conceptId) continue
// Calculate relationship strength based on similarity
const strength = await this.calculateConceptSimilarity(concept, similarConcept)
if (strength > this.config.similarityThreshold) {
await this.linkConcept(
conceptId,
similarConcept.id,
'related',
{ strength, bidirectional: true }
)
}
}
}
/**
* Get concept by ID
*/
private async getConcept(conceptId: string): Promise<UniversalConcept | null> {
// Check cache first
if (this.conceptCache.has(conceptId)) {
return this.conceptCache.get(conceptId)!
}
// Query using EntityManager
const concept = await this.getEntity<UniversalConcept>(conceptId)
if (concept) {
this.conceptCache.set(conceptId, concept)
}
return concept
}
/**
* Update stored concept
*/
private async updateConcept(concept: UniversalConcept): Promise<void> {
// Add conceptType metadata before updating
concept.conceptType = 'universal'
// Update using EntityManager
await this.updateEntity(concept)
// Update cache
this.conceptCache.set(concept.id, concept)
}
/**
* Calculate similarity between two concepts
*/
private async calculateConceptSimilarity(
concept1: UniversalConcept,
concept2: UniversalConcept
): Promise<number> {
// Simple similarity calculation
let similarity = 0
// Domain similarity
if (concept1.domain === concept2.domain) similarity += 0.3
// Category similarity
if (concept1.category === concept2.category) similarity += 0.2
// Keyword overlap
const commonKeywords = concept1.keywords.filter(k => concept2.keywords.includes(k))
similarity += (commonKeywords.length / Math.max(concept1.keywords.length, concept2.keywords.length)) * 0.3
// Name similarity (simple string comparison)
const nameWords1 = concept1.name.toLowerCase().split(/\s+/)
const nameWords2 = concept2.name.toLowerCase().split(/\s+/)
const commonWords = nameWords1.filter(w => nameWords2.includes(w))
similarity += (commonWords.length / Math.max(nameWords1.length, nameWords2.length)) * 0.2
return Math.min(1.0, similarity)
}
/**
* Generate embedding for concept
*/
private async generateConceptEmbedding(concept: UniversalConcept): Promise<number[] | undefined> {
try {
const text = [
concept.name,
concept.description || '',
concept.domain,
concept.category,
...(Array.isArray(concept.keywords) ? concept.keywords : [])
].join(' ')
return await this.generateTextEmbedding(text)
} catch (error) {
console.error('Failed to generate concept embedding:', error)
return undefined
}
}
/**
* Generate embedding for text
*/
private async generateTextEmbedding(text: string): Promise<number[] | undefined> {
try {
// Generate embedding using Brainy's embed method
const vector = await this.brain.embed(text)
return vector
} catch (error) {
console.debug('Failed to generate embedding:', error)
return undefined
}
}
/**
* Get reverse relationship type
*/
private getReverseRelationship(relationship: ConceptLink['relationship']): ConceptLink['relationship'] {
const reverseMap: Record<ConceptLink['relationship'], ConceptLink['relationship']> = {
'extends': 'extended-by' as any,
'implements': 'implemented-by' as any,
'uses': 'used-by' as any,
'opposite': 'opposite',
'related': 'related',
'contains': 'part-of',
'part-of': 'contains'
}
return reverseMap[relationship] || 'related'
}
/**
* Map concept relationship to VerbType
*/
private getVerbType(relationship: ConceptLink['relationship']): VerbType {
const verbMap: Record<ConceptLink['relationship'], VerbType> = {
'extends': VerbType.Extends,
'implements': VerbType.Implements,
'uses': VerbType.Uses,
'opposite': VerbType.Conflicts,
'related': VerbType.RelatedTo,
'contains': VerbType.Contains,
'part-of': VerbType.PartOf
}
return verbMap[relationship] || VerbType.RelatedTo
}
/**
* Detect concept domain from context
*/
private detectDomain(conceptName: string, context: string): string {
if (/import|export|function|class|const|var|let/.test(context)) return 'technical'
if (/customer|user|business|revenue|market/.test(context)) return 'business'
if (/design|pattern|architecture/.test(context)) return 'design'
if (/research|study|analysis/.test(context)) return 'academic'
return 'general'
}
/**
* Detect concept category
*/
private detectCategory(conceptName: string): string {
if (/pattern|strategy|factory|singleton/.test(conceptName)) return 'pattern'
if (/principle|rule|law/.test(conceptName)) return 'principle'
if (/method|approach|technique/.test(conceptName)) return 'method'
if (/entity|object|model/.test(conceptName)) return 'entity'
return 'concept'
}
/**
* Detect manifestation form from context
*/
private detectManifestationForm(context: string): ConceptManifestation['form'] {
if (context.includes('definition') || context.includes('is defined as')) return 'definition'
if (context.includes('example') || context.includes('for instance')) return 'example'
if (context.includes('implements') || context.includes('function')) return 'implementation'
if (context.includes('discussed') || context.includes('explains')) return 'discussion'
return 'usage'
}
/**
* Extract context around a position
*/
private extractContext(text: string, position: number, radius = 150): string {
const start = Math.max(0, position - radius)
const end = Math.min(text.length, position + radius)
return text.slice(start, end)
}
/**
* Clear concept cache
*/
clearCache(conceptId?: string): void {
if (conceptId) {
this.conceptCache.delete(conceptId)
} else {
this.conceptCache.clear()
}
}
}

View file

@ -1,291 +0,0 @@
/**
* 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

@ -1,354 +0,0 @@
/**
* Event Recording System for VFS
*
* Records every file operation as an event for complete history tracking
* PRODUCTION-READY: No mocks, real implementation
*/
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 extends ManagedEntity {
id: string
type: 'create' | 'write' | 'append' | 'delete' | 'move' | 'rename' | 'mkdir' | 'rmdir'
path: string
timestamp: number
content?: Buffer
size?: number
hash?: string
author?: string
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 extends EntityManager {
constructor(brain: Brainy) {
super(brain, 'vfs')
}
/**
* Record a file operation event
*/
async recordEvent(event: Omit<FileEvent, 'id' | 'timestamp'>): Promise<string> {
const eventId = uuidv4()
const timestamp = Date.now()
// Calculate content hash if content provided
const hash = event.content
? createHash('sha256').update(event.content).digest('hex')
: 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
}
// 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
*/
async getEvents(options?: {
since?: number
until?: number
types?: string[]
limit?: number
}): Promise<FileEvent[]> {
const limit = options?.limit || 100
const since = options?.since || 0
const until = options?.until || Date.now()
const query: any = {
eventType: 'file-operation'
}
// Add time filters
if (since || until) {
query.timestamp = {}
if (since) query.timestamp.$gte = since
if (until) query.timestamp.$lte = until
}
// Add type filter
if (options?.types && options.types.length > 0) {
query.type = { $in: options.types }
}
// Query using EntityManager
const events = await this.findEntities<FileEvent>(query, NounType.Event, limit)
return events.sort((a, b) => b.timestamp - a.timestamp)
}
/**
* Get complete history for a file
*/
async getHistory(path: string, options?: {
limit?: number
since?: number
until?: number
types?: FileEvent['type'][]
}): Promise<FileEvent[]> {
const query: any = {
path,
eventType: 'file-operation'
}
// Add time filters
if (options?.since || options?.until) {
query.timestamp = {}
if (options.since) query.timestamp.$gte = options.since
if (options.until) query.timestamp.$lte = options.until
}
// Add type filter
if (options?.types && options.types.length > 0) {
query.type = { $in: options.types }
}
// 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)
}
/**
* Replay events to reconstruct file state at a specific time
*/
async reconstructFileAtTime(path: string, timestamp: number): Promise<Buffer | null> {
// Get all events up to the specified time
const events = await this.getHistory(path, {
until: timestamp,
types: ['create', 'write', 'append', 'delete']
})
// Sort chronologically for replay
const chronological = events.reverse()
// Find last write or create event
let lastContent: Buffer | null = null
let deleted = false
for (const event of chronological) {
switch (event.type) {
case 'create':
case 'write':
lastContent = event.content || null
deleted = false
break
case 'append':
if (lastContent && event.content) {
lastContent = Buffer.concat([lastContent, event.content])
}
break
case 'delete':
deleted = true
lastContent = null
break
}
}
return deleted ? null : lastContent
}
/**
* Get file changes between two timestamps
*/
async getChanges(since: number, until?: number): Promise<FileEvent[]> {
const query: any = {
eventType: 'file-operation',
timestamp: { $gte: since }
}
if (until) {
query.timestamp.$lte = until
}
// Query using EntityManager
const events = await this.findEntities<FileEvent>(query, NounType.Event, 1000)
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))
}
/**
* Calculate statistics for a file or directory
*/
async getStatistics(path: string): Promise<{
totalEvents: number
firstEvent: number | null
lastEvent: number | null
totalWrites: number
totalBytes: number
authors: string[]
}> {
const events = await this.getHistory(path, { limit: 10000 })
if (events.length === 0) {
return {
totalEvents: 0,
firstEvent: null,
lastEvent: null,
totalWrites: 0,
totalBytes: 0,
authors: []
}
}
const stats = {
totalEvents: events.length,
firstEvent: events[events.length - 1].timestamp, // Oldest
lastEvent: events[0].timestamp, // Newest
totalWrites: 0,
totalBytes: 0,
authors: new Set<string>()
}
for (const event of events) {
if (event.type === 'write' || event.type === 'append') {
stats.totalWrites++
stats.totalBytes += event.size || 0
}
if (event.author) {
stats.authors.add(event.author)
}
}
return {
...stats,
authors: Array.from(stats.authors)
}
}
/**
* Find files that changed together (temporal coupling)
*/
async findTemporalCoupling(path: string, windowMs = 60000): Promise<Map<string, number>> {
const events = await this.getHistory(path)
const coupling = new Map<string, number>()
for (const event of events) {
// Find other files changed within the time window
const related = await this.getChanges(
event.timestamp - windowMs,
event.timestamp + windowMs
)
for (const relatedEvent of related) {
if (relatedEvent.path !== path) {
const count = coupling.get(relatedEvent.path) || 0
coupling.set(relatedEvent.path, count + 1)
}
}
}
// Sort by coupling strength
return new Map(
Array.from(coupling.entries())
.sort((a, b) => b[1] - a[1])
)
}
/**
* Generate embedding for an event (for semantic search)
*/
private async generateEventEmbedding(event: Omit<FileEvent, 'id' | 'timestamp'>): Promise<number[] | undefined> {
try {
// Generate embedding based on event type and content
let textToEmbed: string
if (event.type === 'write' || event.type === 'create') {
// For write/create events with content, embed the content
if (event.content && event.content.length < 100000) {
// Convert Buffer to string for text files
textToEmbed = Buffer.isBuffer(event.content)
? event.content.toString('utf8', 0, Math.min(10240, event.content.length))
: String(event.content).slice(0, 10240)
} else {
// For large files or no content, create descriptive text
textToEmbed = `File ${event.type} event at ${event.path}, size: ${event.size || 0} bytes`
}
} else {
// For other events (read, delete, rename, etc), create descriptive text
textToEmbed = `File ${event.type} event at ${event.path}${event.oldPath ? ` from ${event.oldPath}` : ''}`
}
// Use Brainy's embed function
const vector = await this.brain.embed(textToEmbed)
return vector
} catch (error) {
console.error('Failed to generate event embedding:', error)
return undefined
}
}
/**
* Prune old events (for storage management)
*/
async pruneEvents(olderThan: number, keepEvery = 10): Promise<number> {
const events = await this.getChanges(0, Date.now() - olderThan)
let pruned = 0
for (let i = 0; i < events.length; i++) {
// Keep every Nth event for history sampling
if (i % keepEvery !== 0) {
await this.deleteEntity(events[i].id)
pruned++
}
}
return pruned
}
}

View file

@ -1,707 +0,0 @@
/**
* Git Bridge for VFS
*
* Provides Git import/export capabilities without Git dependencies
* Enables migration to/from Git repositories while preserving VFS intelligence
* PRODUCTION-READY: Real implementation using filesystem operations
*/
import { VirtualFileSystem } from './VirtualFileSystem.js'
import { VFSDirent } from './types.js'
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 { promises as fs } from 'fs'
import * as path from 'path'
/**
* Git repository representation
*/
export interface GitRepository {
path: string
branches: GitBranch[]
commits: GitCommit[]
files: GitFile[]
metadata: {
name: string
description?: string
origin?: string
lastImported?: number
vfsMetadata?: Record<string, any>
}
}
/**
* Git branch information
*/
export interface GitBranch {
name: string
commitHash: string
isActive: boolean
}
/**
* Git commit information
*/
export interface GitCommit {
hash: string
message: string
author: string
email: string
timestamp: number
parent?: string
files: string[] // Changed files
}
/**
* Git file representation
*/
export interface GitFile {
path: string
content: Buffer
hash: string
mode: string // File permissions
size: number
lastModified: number
}
/**
* Export options
*/
export interface ExportOptions {
preserveMetadata?: boolean // Export VFS metadata as .vfs-meta files
preserveRelationships?: boolean // Export relationships as .vfs-relations files
preserveHistory?: boolean // Export event history
branch?: string // Target branch name
commitMessage?: string // Commit message
author?: {
name: string
email: string
}
includeSystemFiles?: boolean // Include .vfs-* files
}
/**
* Import options
*/
export interface ImportOptions {
preserveGitHistory?: boolean // Import Git commits as VFS events
extractMetadata?: boolean // Extract metadata from .vfs-meta files
restoreRelationships?: boolean // Restore relationships from .vfs-relations files
includeSystemFiles?: boolean // Include .vfs- system files
branch?: string // Which branch to import
since?: number // Import commits since timestamp
author?: string // Only import commits from this author
}
/**
* Git Bridge - Import/Export between VFS and Git repositories
*
* Capabilities:
* - Export VFS to standard Git repository structure
* - Import Git repository into VFS with intelligence
* - Preserve VFS metadata and relationships
* - Convert Git history to VFS events
* - No Git dependencies - pure filesystem operations
*/
export class GitBridge {
constructor(
private vfs: VirtualFileSystem,
private brain: Brainy
) {}
/**
* Export VFS to Git repository structure
*/
async exportToGit(
vfsPath: string,
gitRepoPath: string,
options?: ExportOptions
): Promise<GitRepository> {
// Ensure target directory exists
await fs.mkdir(gitRepoPath, { recursive: true })
const exportId = uuidv4()
const timestamp = Date.now()
const files: GitFile[] = []
// Initialize Git repository metadata
const gitRepo: GitRepository = {
path: gitRepoPath,
branches: [{ name: options?.branch || 'main', commitHash: '', isActive: true }],
commits: [],
files: [],
metadata: {
name: path.basename(gitRepoPath),
description: `Exported from Brainy VFS on ${new Date().toISOString()}`,
lastImported: timestamp,
vfsMetadata: {
exportId,
sourcePath: vfsPath,
options
}
}
}
// Export files recursively
await this.exportDirectory(vfsPath, gitRepoPath, '', files, options)
// Create .vfs-metadata file if preserving metadata
if (options?.preserveMetadata) {
await this.exportMetadata(vfsPath, gitRepoPath, options)
}
// Create .vfs-relationships file if preserving relationships
if (options?.preserveRelationships) {
await this.exportRelationships(vfsPath, gitRepoPath, options)
}
// Create .vfs-history file if preserving history
if (options?.preserveHistory) {
await this.exportHistory(vfsPath, gitRepoPath, options)
}
// Create initial commit metadata
const commitHash = this.generateCommitHash(files)
const commit: GitCommit = {
hash: commitHash,
message: options?.commitMessage || `Export from Brainy VFS: ${vfsPath}`,
author: options?.author?.name || 'VFS Export',
email: options?.author?.email || 'vfs@brainy.local',
timestamp,
files: files.map(f => f.path)
}
gitRepo.commits = [commit]
gitRepo.branches[0].commitHash = commitHash
gitRepo.files = files
// Write repository metadata
await this.writeRepoMetadata(gitRepoPath, gitRepo)
// Record export event
await this.recordGitEvent('export', {
vfsPath,
gitRepoPath,
commitHash,
fileCount: files.length,
options
})
return gitRepo
}
/**
* Import Git repository into VFS
*/
async importFromGit(
gitRepoPath: string,
vfsPath: string,
options?: ImportOptions
): Promise<{
filesImported: number
eventsCreated: number
entitiesCreated: number
relationshipsCreated: number
}> {
const stats = {
filesImported: 0,
eventsCreated: 0,
entitiesCreated: 0,
relationshipsCreated: 0
}
// Read repository structure
const gitRepo = await this.readGitRepository(gitRepoPath)
// Import files
for (const gitFile of gitRepo.files) {
// Skip system files unless requested
if (!options?.includeSystemFiles && gitFile.path.startsWith('.vfs-')) {
continue
}
const targetPath = path.join(vfsPath, gitFile.path)
// Create directory structure
const dirPath = path.dirname(targetPath)
if (dirPath !== vfsPath) {
try {
await this.vfs.mkdir(dirPath, { recursive: true })
} catch (error) {
// Directory might already exist
}
}
// Write file
await this.vfs.writeFile(targetPath, gitFile.content, {
metadata: {
gitHash: gitFile.hash,
gitMode: gitFile.mode,
importedFrom: gitRepoPath,
importedAt: Date.now()
}
})
stats.filesImported++
}
// Import metadata if available and requested
if (options?.extractMetadata) {
const metadataFile = path.join(gitRepoPath, '.vfs-metadata.json')
try {
const metadataContent = await fs.readFile(metadataFile, 'utf8')
const metadata = JSON.parse(metadataContent)
await this.importMetadata(vfsPath, metadata)
stats.entitiesCreated += Object.keys(metadata.entities || {}).length
} catch (error) {
// No metadata file or invalid format
}
}
// Import relationships if available and requested
if (options?.restoreRelationships) {
const relationshipsFile = path.join(gitRepoPath, '.vfs-relationships.json')
try {
const relationshipsContent = await fs.readFile(relationshipsFile, 'utf8')
const relationships = JSON.parse(relationshipsContent)
await this.importRelationships(relationships)
stats.relationshipsCreated += relationships.length || 0
} catch (error) {
// No relationships file or invalid format
}
}
// Import Git history as VFS events if requested
if (options?.preserveGitHistory) {
const historyFile = path.join(gitRepoPath, '.vfs-history.json')
try {
const historyContent = await fs.readFile(historyFile, 'utf8')
const history = JSON.parse(historyContent)
stats.eventsCreated = await this.importHistory(vfsPath, history)
} catch (error) {
// Convert Git commits to VFS events
stats.eventsCreated = await this.convertCommitsToEvents(vfsPath, gitRepo.commits)
}
}
// Record import event
await this.recordGitEvent('import', {
gitRepoPath,
vfsPath,
stats,
options
})
return stats
}
/**
* Export directory recursively
*/
private async exportDirectory(
vfsPath: string,
gitRepoPath: string,
relativePath: string,
files: GitFile[],
options?: ExportOptions
): Promise<void> {
const currentPath = relativePath ? path.join(vfsPath, relativePath) : vfsPath
try {
// Check if path exists in VFS
const exists = await this.vfs.exists(currentPath)
if (!exists) return
// Get directory contents
const entries = await this.vfs.readdir(currentPath, { withFileTypes: true }) as VFSDirent[]
for (const entry of entries) {
const entryVfsPath = path.join(currentPath, entry.name)
const entryRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name
const entryGitPath = path.join(gitRepoPath, entryRelativePath)
if (entry.type === 'directory') {
// Create directory in Git repo
await fs.mkdir(entryGitPath, { recursive: true })
// Recurse into subdirectory
await this.exportDirectory(vfsPath, gitRepoPath, entryRelativePath, files, options)
} else {
// Export file
const content = await this.vfs.readFile(entryVfsPath)
const stats = await this.vfs.stat(entryVfsPath)
// Write file to Git repo
await fs.writeFile(entryGitPath, content)
// Add to files list
const gitFile: GitFile = {
path: entryRelativePath,
content,
hash: createHash('sha1').update(content).digest('hex'),
mode: stats.mode?.toString(8) || '100644',
size: content.length,
lastModified: stats.mtime?.getTime() || Date.now()
}
files.push(gitFile)
}
}
} catch (error) {
console.warn(`Failed to export directory ${currentPath}:`, error)
}
}
/**
* Export VFS metadata to .vfs-metadata.json
*/
private async exportMetadata(
vfsPath: string,
gitRepoPath: string,
options?: ExportOptions
): Promise<void> {
const metadata = {
exportedAt: Date.now(),
vfsPath,
version: '1.0',
entities: {},
files: {}
}
// Collect metadata for all files
await this.collectFileMetadata(vfsPath, '', metadata)
// Write metadata file
const metadataPath = path.join(gitRepoPath, '.vfs-metadata.json')
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2))
}
/**
* Export relationships to .vfs-relationships.json
*/
private async exportRelationships(
vfsPath: string,
gitRepoPath: string,
options?: ExportOptions
): Promise<void> {
// Get all relationships for files in the VFS path
const relationships: any[] = []
try {
// Get relationships for the specified path and its children
const related = await this.vfs.getRelated(vfsPath)
if (related && related.length > 0) {
relationships.push({
path: vfsPath,
relationships: related
})
}
// If it's a directory, get relationships for all files within
const stats = await this.vfs.stat(vfsPath)
if (stats.isDirectory()) {
const files = await this.vfs.readdir(vfsPath, { recursive: true })
for (const file of files) {
const fileName = typeof file === 'string' ? file : file.name
const fullPath = path.join(vfsPath, fileName)
try {
const fileRelated = await this.vfs.getRelated(fullPath)
if (fileRelated && fileRelated.length > 0) {
relationships.push({
path: fullPath,
relationships: fileRelated
})
}
} catch (err) {
// Skip files without relationships
}
}
}
} catch (err) {
// Path might not have relationships
}
// Write relationships file
const relationshipsPath = path.join(gitRepoPath, '.vfs-relationships.json')
await fs.writeFile(relationshipsPath, JSON.stringify(relationships, null, 2))
}
/**
* Export history to .vfs-history.json
*/
private async exportHistory(
vfsPath: string,
gitRepoPath: string,
options?: ExportOptions
): Promise<void> {
// Get event history for the VFS path
const history = {
exportedAt: Date.now(),
vfsPath,
events: [] as any[]
}
// Get history if Knowledge Layer is enabled
if ('getHistory' in this.vfs && typeof (this.vfs as any).getHistory === 'function') {
try {
const events = await (this.vfs as any).getHistory(vfsPath)
if (events) {
history.events = events
}
} catch (err) {
// Knowledge Layer might not be enabled
}
}
// Write history file
const historyPath = path.join(gitRepoPath, '.vfs-history.json')
await fs.writeFile(historyPath, JSON.stringify(history, null, 2))
}
/**
* Collect metadata recursively
*/
private async collectFileMetadata(
vfsPath: string,
relativePath: string,
metadata: any
): Promise<void> {
const currentPath = relativePath ? path.join(vfsPath, relativePath) : vfsPath
try {
const entries = await this.vfs.readdir(currentPath, { withFileTypes: true }) as VFSDirent[]
for (const entry of entries) {
const entryPath = path.join(currentPath, entry.name)
const entryRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name
if (entry.type === 'directory') {
await this.collectFileMetadata(vfsPath, entryRelativePath, metadata)
} else {
const stats = await this.vfs.stat(entryPath)
metadata.files[entryRelativePath] = {
size: stats.size,
mtime: stats.mtime,
ctime: stats.ctime,
mode: stats.mode,
metadata: (stats as any).metadata || {}
}
}
}
} catch (error) {
console.warn(`Failed to collect metadata for ${currentPath}:`, error)
}
}
/**
* Read Git repository structure
*/
private async readGitRepository(gitRepoPath: string): Promise<GitRepository> {
// Read repository metadata if available
const metadataPath = path.join(gitRepoPath, '.vfs-repo-metadata.json')
let metadata = {
name: path.basename(gitRepoPath),
description: 'Imported Git repository',
branches: [{ name: 'main', commitHash: '', isActive: true }],
commits: []
}
try {
const metadataContent = await fs.readFile(metadataPath, 'utf8')
metadata = { ...metadata, ...JSON.parse(metadataContent) }
} catch (error) {
// No metadata file available
}
// Scan directory for files
const files = await this.scanGitFiles(gitRepoPath, '')
const gitRepo: GitRepository = {
path: gitRepoPath,
branches: metadata.branches,
commits: metadata.commits,
files,
metadata
}
return gitRepo
}
/**
* Scan Git files recursively
*/
private async scanGitFiles(gitRepoPath: string, relativePath: string): Promise<GitFile[]> {
const files: GitFile[] = []
const currentPath = relativePath ? path.join(gitRepoPath, relativePath) : gitRepoPath
try {
const entries = await fs.readdir(currentPath, { withFileTypes: true })
for (const entry of entries) {
// Skip Git metadata directories
if (entry.name === '.git') continue
const entryPath = path.join(currentPath, entry.name)
const entryRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name
if (entry.isDirectory()) {
const subFiles = await this.scanGitFiles(gitRepoPath, entryRelativePath)
files.push(...subFiles)
} else {
const content = await fs.readFile(entryPath)
const stats = await fs.stat(entryPath)
const gitFile: GitFile = {
path: entryRelativePath,
content,
hash: createHash('sha1').update(content).digest('hex'),
mode: stats.mode.toString(8),
size: content.length,
lastModified: stats.mtime.getTime()
}
files.push(gitFile)
}
}
} catch (error) {
console.warn(`Failed to scan Git files in ${currentPath}:`, error)
}
return files
}
/**
* Import metadata from exported data
*/
private async importMetadata(vfsPath: string, metadata: any): Promise<void> {
// Apply metadata to imported files
for (const [filePath, fileMetadata] of Object.entries(metadata.files || {})) {
const fullPath = path.join(vfsPath, filePath)
try {
// Update file metadata if it exists
const exists = await this.vfs.exists(fullPath)
if (exists) {
// Would update file metadata in VFS
// This depends on VFS implementation supporting metadata updates
}
} catch (error) {
console.warn(`Failed to import metadata for ${fullPath}:`, error)
}
}
}
/**
* Import relationships
*/
private async importRelationships(relationships: any[]): Promise<void> {
for (const relationship of relationships) {
try {
await this.brain.relate({
from: relationship.from,
to: relationship.to,
type: relationship.type,
metadata: relationship.metadata
})
} catch (error) {
console.warn('Failed to import relationship:', error)
}
}
}
/**
* Import history from exported data
*/
private async importHistory(vfsPath: string, history: any): Promise<number> {
let eventsCreated = 0
for (const event of history.events || []) {
try {
await this.brain.add({
type: NounType.Event,
data: Buffer.from(JSON.stringify(event)),
metadata: {
...event,
importedFrom: 'git',
importedAt: Date.now()
}
})
eventsCreated++
} catch (error) {
console.warn('Failed to import history event:', error)
}
}
return eventsCreated
}
/**
* Convert Git commits to VFS events
*/
private async convertCommitsToEvents(vfsPath: string, commits: GitCommit[]): Promise<number> {
let eventsCreated = 0
for (const commit of commits) {
try {
await this.brain.add({
type: NounType.Event,
data: Buffer.from(commit.message),
metadata: {
eventType: 'git-commit',
gitHash: commit.hash,
author: commit.author,
email: commit.email,
timestamp: commit.timestamp,
files: commit.files,
vfsPath,
system: 'git-bridge'
}
})
eventsCreated++
} catch (error) {
console.warn('Failed to convert commit to event:', error)
}
}
return eventsCreated
}
/**
* Generate commit hash
*/
private generateCommitHash(files: GitFile[]): string {
const content = files.map(f => f.path + ':' + f.hash).join('\n')
return createHash('sha1').update(content).digest('hex')
}
/**
* Write repository metadata
*/
private async writeRepoMetadata(gitRepoPath: string, gitRepo: GitRepository): Promise<void> {
const metadataPath = path.join(gitRepoPath, '.vfs-repo-metadata.json')
await fs.writeFile(metadataPath, JSON.stringify(gitRepo.metadata, null, 2))
}
/**
* Record Git bridge event
*/
private async recordGitEvent(operation: 'export' | 'import', details: any): Promise<void> {
try {
await this.brain.add({
type: NounType.Event,
data: Buffer.from(JSON.stringify(details)),
metadata: {
eventType: 'git-bridge',
operation,
timestamp: Date.now(),
system: 'git-bridge',
...details
}
})
} catch (error) {
console.warn('Failed to record Git bridge event:', error)
}
}
}

View file

@ -1,542 +0,0 @@
/**
* Knowledge Layer for VFS
*
* This is the REAL integration that makes VFS intelligent.
* It wraps VFS operations and adds Knowledge Layer processing.
*/
import { VirtualFileSystem } from './VirtualFileSystem.js'
import { Brainy } from '../brainy.js'
import { EventRecorder } from './EventRecorder.js'
import { SemanticVersioning } from './SemanticVersioning.js'
import { PersistentEntitySystem } from './PersistentEntitySystem.js'
import { ConceptSystem } from './ConceptSystem.js'
import { GitBridge } from './GitBridge.js'
export class KnowledgeLayer {
private eventRecorder: EventRecorder
private semanticVersioning: SemanticVersioning
private entitySystem: PersistentEntitySystem
private conceptSystem: ConceptSystem
private gitBridge: GitBridge
private enabled = false
constructor(
private vfs: VirtualFileSystem,
private brain: Brainy
) {
// Initialize all Knowledge Layer components
this.eventRecorder = new EventRecorder(brain)
this.semanticVersioning = new SemanticVersioning(brain)
this.entitySystem = new PersistentEntitySystem(brain)
this.conceptSystem = new ConceptSystem(brain)
this.gitBridge = new GitBridge(vfs, brain)
}
/**
* Enable Knowledge Layer by wrapping VFS methods
*/
async enable(): Promise<void> {
if (this.enabled) return
this.enabled = true
// Save original methods
const originalWriteFile = this.vfs.writeFile.bind(this.vfs)
const originalUnlink = this.vfs.unlink.bind(this.vfs)
const originalRename = this.vfs.rename.bind(this.vfs)
const originalMkdir = this.vfs.mkdir.bind(this.vfs)
const originalRmdir = this.vfs.rmdir.bind(this.vfs)
// Wrap writeFile to add intelligence
this.vfs.writeFile = async (path: string, data: Buffer | string, options?: any) => {
// Call original VFS method first
const result = await originalWriteFile(path, data, options)
// Process in background (non-blocking)
setImmediate(async () => {
try {
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data)
// 1. Record the event
await this.eventRecorder.recordEvent({
type: 'write',
path,
content: buffer,
size: buffer.length,
author: options?.author || 'system'
})
// 2. Check for semantic versioning
try {
const existingContent = await this.vfs.readFile(path).catch(() => null)
if (existingContent) {
const shouldVersion = await this.semanticVersioning.shouldVersion(existingContent, buffer)
if (shouldVersion) {
await this.semanticVersioning.createVersion(path, buffer, {
message: options?.message || 'Automatic semantic version'
})
}
}
} catch (err) {
console.debug('Versioning check failed:', err)
}
// 3. Extract entities
if (options?.extractEntities !== false) {
await this.entitySystem.extractEntities(path, buffer)
}
// 4. Extract concepts
if (options?.extractConcepts !== false) {
await this.conceptSystem.extractAndLinkConcepts(path, buffer)
}
} catch (error) {
console.debug('Knowledge Layer processing error:', error)
}
})
return result
}
// Wrap unlink to record deletion
this.vfs.unlink = async (path: string) => {
const result = await originalUnlink(path)
setImmediate(async () => {
await this.eventRecorder.recordEvent({
type: 'delete',
path,
author: 'system'
})
})
return result
}
// Wrap rename to track moves
this.vfs.rename = async (oldPath: string, newPath: string) => {
const result = await originalRename(oldPath, newPath)
setImmediate(async () => {
await this.eventRecorder.recordEvent({
type: 'rename',
path: oldPath,
metadata: { newPath },
author: 'system'
})
})
return result
}
// Wrap mkdir to track directory creation
this.vfs.mkdir = async (path: string, options?: any) => {
const result = await originalMkdir(path, options)
setImmediate(async () => {
await this.eventRecorder.recordEvent({
type: 'mkdir',
path,
author: 'system'
})
})
return result
}
// Wrap rmdir to track directory deletion
this.vfs.rmdir = async (path: string, options?: any) => {
const result = await originalRmdir(path, options)
setImmediate(async () => {
await this.eventRecorder.recordEvent({
type: 'rmdir',
path,
author: 'system'
})
})
return result
}
// Add Knowledge Layer methods to VFS
this.addKnowledgeMethods()
console.log('✨ Knowledge Layer enabled on VFS')
}
/**
* Add Knowledge Layer query methods to VFS
*/
private addKnowledgeMethods(): void {
// Event history
(this.vfs as any).getHistory = async (path: string, options?: any) => {
return await this.eventRecorder.getHistory(path, options)
}
(this.vfs as any).reconstructAtTime = async (path: string, timestamp: number) => {
return await this.eventRecorder.reconstructFileAtTime(path, timestamp)
}
// Semantic versioning
(this.vfs as any).getVersions = async (path: string) => {
return await this.semanticVersioning.getVersions(path)
}
(this.vfs as any).getVersion = async (path: string, versionId: string) => {
return await this.semanticVersioning.getVersion(path, versionId)
}
(this.vfs as any).restoreVersion = async (path: string, versionId: string) => {
const content = await this.semanticVersioning.getVersion(path, versionId)
if (content) {
await this.vfs.writeFile(path, content)
}
}
// Entity system
(this.vfs as any).createEntity = async (config: any) => {
return await this.entitySystem.createEntity(config)
}
(this.vfs as any).findEntity = async (query: any) => {
return await this.entitySystem.findEntity(query)
}
(this.vfs as any).getEntityEvolution = async (entityId: string) => {
return await this.entitySystem.getEvolution(entityId)
}
// Concept system
(this.vfs as any).createConcept = async (config: any) => {
return await this.conceptSystem.createConcept(config)
}
(this.vfs as any).findConcepts = async (query: any) => {
return await this.conceptSystem.findConcepts(query)
}
(this.vfs as any).getConceptGraph = async (options?: any) => {
return await this.conceptSystem.getConceptGraph(options)
}
// Git bridge
(this.vfs as any).exportToGit = async (vfsPath: string, gitPath: string) => {
return await this.gitBridge.exportToGit(vfsPath, gitPath)
}
(this.vfs as any).importFromGit = async (gitPath: string, vfsPath: string) => {
return await this.gitBridge.importFromGit(gitPath, vfsPath)
}
// Temporal coupling
(this.vfs as any).findTemporalCoupling = async (path: string, windowMs?: number) => {
return await this.eventRecorder.findTemporalCoupling(path, windowMs)
}
// Entity convenience methods that wrap Brainy's core API
(this.vfs as any).linkEntities = async (fromEntity: string | any, toEntity: string | any, relationship: string) => {
// Handle both entity IDs and entity objects
const fromId = typeof fromEntity === 'string' ? fromEntity : fromEntity.id
const toId = typeof toEntity === 'string' ? toEntity : toEntity.id
// Use brain.relate to create the relationship
return await this.brain.relate({
from: fromId,
to: toId,
type: relationship as any // VerbType or string
})
}
// Find where an entity appears across files
(this.vfs as any).findEntityOccurrences = async (entityId: string) => {
const occurrences: Array<{ path: string, context?: string }> = []
// Search for files that contain references to this entity
// First, get all relationships where this entity is involved
const relations = await this.brain.getRelations({ from: entityId })
const toRelations = await this.brain.getRelations({ to: entityId })
// Find file entities that relate to this entity
for (const rel of [...relations, ...toRelations]) {
try {
// Check if the related entity is a file
const relatedId = rel.from === entityId ? rel.to : rel.from
const entity = await this.brain.get(relatedId)
if (entity?.metadata?.vfsType === 'file' && entity?.metadata?.path) {
occurrences.push({
path: entity.metadata.path as string,
context: entity.data ? entity.data.toString().substring(0, 200) : undefined
})
}
} catch (error) {
// Entity might not exist, continue
}
}
// Also search for files that mention the entity name in their content
const entityData = await this.brain.get(entityId)
if (entityData?.metadata?.name) {
const searchResults = await this.brain.find({
query: entityData.metadata.name as string,
where: { vfsType: 'file' },
limit: 20
})
for (const result of searchResults) {
if (result.entity?.metadata?.path && !occurrences.some(o => o.path === result.entity.metadata.path)) {
occurrences.push({
path: result.entity.metadata.path as string,
context: result.entity.data ? result.entity.data.toString().substring(0, 200) : undefined
})
}
}
}
return occurrences
}
// Update an entity (convenience wrapper)
(this.vfs as any).updateEntity = async (entityId: string, updates: any) => {
// Get current entity from brain
const currentEntity = await this.brain.get(entityId)
if (!currentEntity) {
throw new Error(`Entity ${entityId} not found`)
}
// Merge updates
const updatedMetadata = {
...currentEntity.metadata,
...updates,
lastUpdated: Date.now(),
version: ((currentEntity.metadata?.version as number) || 0) + 1
}
// Update via brain
await this.brain.update({
id: entityId,
data: JSON.stringify(updatedMetadata),
metadata: updatedMetadata
})
return entityId
}
// Get entity graph (convenience wrapper)
(this.vfs as any).getEntityGraph = async (entityId: string, options?: { depth?: number }) => {
const depth = options?.depth || 2
const graph = { nodes: new Map(), edges: [] as any[] }
const visited = new Set<string>()
const traverse = async (id: string, currentDepth: number) => {
if (visited.has(id) || currentDepth > depth) return
visited.add(id)
// Add node
const entity = await this.brain.get(id)
if (entity) {
graph.nodes.set(id, entity)
}
// Get relationships
const relations = await this.brain.getRelations({ from: id })
const toRelations = await this.brain.getRelations({ to: id })
for (const rel of [...relations, ...toRelations]) {
graph.edges.push(rel)
// Traverse connected nodes
if (currentDepth < depth) {
const nextId = rel.from === id ? rel.to : rel.from
await traverse(nextId, currentDepth + 1)
}
}
}
await traverse(entityId, 0)
return {
nodes: Array.from(graph.nodes.values()),
edges: graph.edges
}
}
// List all entities of a specific type
(this.vfs as any).listEntities = async (query?: { type?: string }) => {
return await this.entitySystem.findEntity(query || {})
}
// Find files by concept
(this.vfs as any).findByConcept = async (conceptName: string): Promise<string[]> => {
const paths: string[] = []
// First find the concept
const concepts = await this.conceptSystem.findConcepts({ name: conceptName })
if (concepts.length === 0) {
return paths
}
const concept = concepts[0]
// Search for files that contain concept keywords
const searchTerms = [conceptName, ...(concept.keywords || [])].join(' ')
const searchResults = await this.brain.find({
query: searchTerms,
where: { vfsType: 'file' },
limit: 50
})
// Get unique paths
const pathSet = new Set<string>()
for (const result of searchResults) {
if (result.entity?.metadata?.path) {
pathSet.add(result.entity.metadata.path as string)
}
}
// Also check concept manifestations if stored
if (concept.manifestations) {
for (const manifestation of concept.manifestations) {
if (manifestation.filePath) {
pathSet.add(manifestation.filePath)
}
}
}
return Array.from(pathSet)
}
// Get timeline of events
(this.vfs as any).getTimeline = async (options?: {
from?: string | Date | number
to?: string | Date | number
types?: string[]
limit?: number
}): Promise<Array<{
timestamp: Date
type: string
path: string
user?: string
description: string
}>> => {
const fromTime = options?.from ? new Date(options.from).getTime() : Date.now() - 30 * 24 * 60 * 60 * 1000 // 30 days ago
const toTime = options?.to ? new Date(options.to).getTime() : Date.now()
const limit = options?.limit || 100
// Get events from event recorder
const events = await this.eventRecorder.getEvents({
since: fromTime,
until: toTime,
types: options?.types,
limit
})
// Transform to timeline format
return events.map(event => ({
timestamp: new Date(event.timestamp),
type: event.type,
path: event.path,
user: event.author,
description: `${event.type} ${event.path}${event.oldPath ? ` (from ${event.oldPath})` : ''}`
}))
}
// Get collaboration history for a file
(this.vfs as any).getCollaborationHistory = async (path: string): Promise<Array<{
user: string
timestamp: Date
action: string
size?: number
}>> => {
// Get all events for this path
const history = await this.eventRecorder.getHistory(path, { limit: 100 })
return history.map(event => ({
user: event.author || 'system',
timestamp: new Date(event.timestamp),
action: event.type,
size: event.size
}))
}
// Export to markdown format
(this.vfs as any).exportToMarkdown = async (path: string): Promise<string> => {
const markdown: string[] = []
const traverse = async (currentPath: string, depth: number = 0) => {
const indent = ' '.repeat(depth)
try {
const stats = await this.vfs.stat(currentPath)
if (stats.isDirectory()) {
// Add directory header
const name = currentPath.split('/').pop() || currentPath
markdown.push(`${indent}## ${name}/`)
markdown.push('')
// List and traverse children
const children = await this.vfs.readdir(currentPath)
for (const child of children.sort()) {
const childPath = currentPath === '/' ? `/${child}` : `${currentPath}/${child}`
await traverse(childPath, depth + 1)
}
} else {
// Add file content
const name = currentPath.split('/').pop() || currentPath
const extension = name.split('.').pop() || 'txt'
try {
const content = await this.vfs.readFile(currentPath)
const textContent = content.toString('utf8')
markdown.push(`${indent}### ${name}`)
markdown.push('')
// Add code block for code files
if (['js', 'ts', 'jsx', 'tsx', 'py', 'java', 'cpp', 'go', 'rs', 'json'].includes(extension)) {
markdown.push(`${indent}\`\`\`${extension}`)
markdown.push(textContent.split('\n').map(line => `${indent}${line}`).join('\n'))
markdown.push(`${indent}\`\`\``)
} else if (extension === 'md') {
// Include markdown directly
markdown.push(textContent)
} else {
// Plain text in quotes
markdown.push(`${indent}> ${textContent.split('\n').join(`\n${indent}> `)}`)
}
markdown.push('')
} catch (error) {
markdown.push(`${indent}*Binary or unreadable file*`)
markdown.push('')
}
}
} catch (error) {
// Skip inaccessible paths
}
}
await traverse(path)
return markdown.join('\n')
}
}
/**
* Disable Knowledge Layer
*/
async disable(): Promise<void> {
// Would restore original methods here
this.enabled = false
}
}
/**
* Enable Knowledge Layer on a VFS instance
*/
export async function enableKnowledgeLayer(vfs: VirtualFileSystem, brain: Brainy): Promise<KnowledgeLayer> {
const knowledgeLayer = new KnowledgeLayer(vfs, brain)
await knowledgeLayer.enable()
return knowledgeLayer
}

View file

@ -1,689 +0,0 @@
/**
* Persistent Entity System for VFS
*
* Manages entities that evolve across files and time
* Not just story characters - any evolving entity: APIs, customers, services, models
* PRODUCTION-READY: Real implementation using Brainy
*/
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 extends ManagedEntity {
id: string
name: string
type: string // 'character', 'api', 'service', 'concept', 'customer', etc.
description?: string
aliases: string[] // Alternative names/references
appearances: EntityAppearance[]
attributes: Record<string, any>
created: number
lastUpdated: number
version: number
entityType?: string // For EntityManager queries
}
/**
* An appearance of an entity in a specific file/location
*/
export interface EntityAppearance extends ManagedEntity {
id: string
entityId: string
filePath: string
context: string // Surrounding text/code
position?: {
line?: number
column?: number
offset?: number
}
timestamp: number
version: number
changes?: EntityChange[]
confidence: number // How confident we are this is the entity (0-1)
eventType?: string // For EntityManager queries
}
/**
* A change/evolution to an entity
*/
export interface EntityChange {
field: string
oldValue: any
newValue: any
timestamp: number
source: string // File path where change was detected
reason?: string // Why the change was made
}
/**
* Configuration for persistent entities
*/
export interface PersistentEntityConfig {
autoExtract?: boolean // Auto-extract entities from content
similarityThreshold?: number // For entity matching (0-1)
maxAppearances?: number // Max appearances to track per entity
evolutionTracking?: boolean // Track entity evolution
}
/**
* Persistent Entity System
*
* Tracks entities that exist across multiple files and evolve over time
* Examples:
* - Story characters that appear in multiple chapters
* - API endpoints that evolve across documentation
* - Business entities that appear in multiple reports
* - Code classes/functions that span multiple files
*/
export class PersistentEntitySystem extends EntityManager {
private config: Required<PersistentEntityConfig>
private entityCache = new Map<string, PersistentEntity>()
constructor(
brain: Brainy,
config?: PersistentEntityConfig
) {
super(brain, 'vfs-entity')
this.config = {
autoExtract: config?.autoExtract ?? false,
similarityThreshold: config?.similarityThreshold ?? 0.8,
maxAppearances: config?.maxAppearances ?? 100,
evolutionTracking: config?.evolutionTracking ?? true
}
}
/**
* Create a new persistent entity
*/
async createEntity(entity: Omit<PersistentEntity, 'id' | 'created' | 'lastUpdated' | 'version' | 'appearances'>): Promise<string> {
const entityId = uuidv4()
const timestamp = Date.now()
const persistentEntity: PersistentEntity = {
id: entityId,
name: entity.name,
type: entity.type,
description: entity.description,
aliases: entity.aliases,
attributes: entity.attributes,
created: timestamp,
lastUpdated: timestamp,
version: 1,
appearances: [],
entityType: 'persistent' // Add entityType for querying
}
// Generate embedding for entity description
let embedding: number[] | undefined
try {
if (entity.description) {
embedding = await this.generateEntityEmbedding(persistentEntity)
}
} catch (error) {
console.warn('Failed to generate entity embedding:', error)
}
// Store entity using EntityManager
await this.storeEntity(
persistentEntity,
NounType.Concept,
embedding,
Buffer.from(JSON.stringify(persistentEntity))
)
// Update cache
this.entityCache.set(entityId, persistentEntity)
return entityId // Return domain ID, not Brainy ID
}
/**
* Find an existing entity by name or attributes
*/
async findEntity(query: {
name?: string
type?: string
attributes?: Record<string, any>
similar?: string // Find similar entities to this description
}): Promise<PersistentEntity[]> {
const searchQuery: any = {}
if (query.name) {
// Search by exact name or aliases
searchQuery.$or = [
{ name: query.name },
{ aliases: { $in: [query.name] } }
]
}
if (query.type) {
searchQuery.type = query.type
}
if (query.attributes) {
for (const [key, value] of Object.entries(query.attributes)) {
searchQuery[`attributes.${key}`] = value
}
}
// 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 using EntityManager
const allEntities = await this.findEntities<PersistentEntity>({}, NounType.Concept, 1000)
const withSimilarity = allEntities
.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
})
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
}
/**
* Record an appearance of an entity in a file
*/
async recordAppearance(
entityId: string,
filePath: string,
context: string,
options?: {
position?: EntityAppearance['position']
confidence?: number
extractChanges?: boolean
}
): Promise<string> {
const entity = await this.getPersistentEntity(entityId)
if (!entity) {
throw new Error(`Entity ${entityId} not found`)
}
const appearanceId = uuidv4()
const timestamp = Date.now()
// Detect changes if enabled
let changes: EntityChange[] = []
if (options?.extractChanges && this.config.evolutionTracking) {
changes = await this.detectChanges(entity, context, filePath)
}
const appearance: EntityAppearance = {
id: appearanceId,
entityId,
filePath,
context,
position: options?.position,
timestamp,
version: entity.version,
changes,
confidence: options?.confidence ?? 1.0
}
// Store appearance as managed entity (with eventType for appearances)
const appearanceWithEventType = {
...appearance,
eventType: 'entity-appearance'
}
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)
entity.lastUpdated = timestamp
// Apply changes if any detected
if (changes.length > 0) {
entity.version++
for (const change of changes) {
if (change.field in entity.attributes) {
entity.attributes[change.field] = change.newValue
}
}
}
// Prune old appearances if needed
if (entity.appearances.length > this.config.maxAppearances) {
entity.appearances = entity.appearances
.sort((a, b) => b.timestamp - a.timestamp)
.slice(0, this.config.maxAppearances)
}
// Update stored entity
await this.updatePersistentEntity(entity)
return appearanceId
}
/**
* Get entity evolution history
*/
async getEvolution(entityId: string): Promise<{
entity: PersistentEntity
timeline: Array<{
timestamp: number
version: number
changes: EntityChange[]
appearance?: EntityAppearance
}>
}> {
const entity = await this.getPersistentEntity(entityId)
if (!entity) {
throw new Error(`Entity ${entityId} not found`)
}
// Get all appearances sorted by time
const appearances = entity.appearances.sort((a, b) => a.timestamp - b.timestamp)
// Build timeline
const timeline = []
for (const appearance of appearances) {
if (appearance.changes && appearance.changes.length > 0) {
timeline.push({
timestamp: appearance.timestamp,
version: appearance.version,
changes: appearance.changes,
appearance
})
}
}
return { entity, timeline }
}
/**
* Find all appearances of an entity
*/
async findAppearances(entityId: string, options?: {
filePath?: string
since?: number
until?: number
minConfidence?: number
}): Promise<EntityAppearance[]> {
const query: any = {
entityId,
eventType: 'entity-appearance',
system: 'vfs-entity'
}
if (options?.filePath) {
query.filePath = options.filePath
}
if (options?.since || options?.until) {
query.timestamp = {}
if (options.since) query.timestamp.$gte = options.since
if (options.until) query.timestamp.$lte = options.until
}
if (options?.minConfidence) {
query.confidence = { $gte: options.minConfidence }
}
const results = await this.brain.find({
where: query,
type: NounType.Event,
limit: 1000
})
return results
.map(r => r.entity.metadata as EntityAppearance)
.sort((a, b) => b.timestamp - a.timestamp)
}
/**
* Evolve an entity with new information
*/
async evolveEntity(
entityId: string,
updates: Partial<Pick<PersistentEntity, 'name' | 'description' | 'aliases' | 'attributes'>>,
source: string,
reason?: string
): Promise<void> {
const entity = await this.getPersistentEntity(entityId)
if (!entity) {
throw new Error(`Entity ${entityId} not found`)
}
const timestamp = Date.now()
const changes: EntityChange[] = []
// Track changes
for (const [field, newValue] of Object.entries(updates)) {
if (field in entity && entity[field as keyof PersistentEntity] !== newValue) {
changes.push({
field,
oldValue: entity[field as keyof PersistentEntity],
newValue,
timestamp,
source,
reason
})
}
}
// Apply updates
Object.assign(entity, updates)
entity.lastUpdated = timestamp
entity.version++
// Update stored entity
await this.updatePersistentEntity(entity)
// Record evolution event
if (changes.length > 0) {
await this.brain.add({
type: NounType.Event,
data: Buffer.from(JSON.stringify(changes)),
metadata: {
entityId,
changes,
timestamp,
source,
reason,
eventType: 'entity-evolution',
system: 'vfs-entity'
}
})
}
}
/**
* Extract entities from content (auto-extraction)
*/
async extractEntities(filePath: string, content: Buffer): Promise<string[]> {
if (!this.config.autoExtract) {
return []
}
// Convert content to text for processing
const text = content.toString('utf8')
const entities: string[] = []
// Simple entity extraction patterns
// In production, this would use NLP/ML models
const patterns = [
// Character names (capitalized words)
/\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b/g,
// API endpoints
/\/api\/[a-zA-Z0-9/\-_]+/g,
// Class names
/class\s+([A-Z][a-zA-Z0-9_]*)/g,
// Function names
/function\s+([a-zA-Z_][a-zA-Z0-9_]*)/g
]
for (const pattern of patterns) {
const matches = text.matchAll(pattern)
for (const match of matches) {
const entityName = match[1] || match[0]
// Check if entity already exists
const existing = await this.findEntity({ name: entityName })
if (existing.length === 0) {
// Create new entity
const entityId = await this.createEntity({
name: entityName,
type: this.detectEntityType(entityName, text),
aliases: [],
attributes: {}
})
entities.push(entityId)
}
// Record appearance for existing or new entity
const entity = existing[0] || await this.getPersistentEntity(entities[entities.length - 1])
if (entity) {
await this.recordAppearance(
entity.id,
filePath,
this.extractContext(text, match.index || 0),
{ confidence: 0.7, extractChanges: true }
)
}
}
}
return entities
}
/**
* Update references when a file moves
*/
async updateReferences(oldPath: string, newPath: string): Promise<void> {
// Find all appearances in the old path
const results = await this.brain.find({
where: {
filePath: oldPath,
eventType: 'entity-appearance',
system: 'vfs-entity'
},
type: NounType.Event,
limit: 10000
})
// Update each appearance
for (const result of results) {
const appearance = result.entity.metadata as EntityAppearance
appearance.filePath = newPath
// Update the stored appearance
await this.brain.update({
id: result.entity.id,
metadata: appearance
})
}
// Update cache
for (const entity of this.entityCache.values()) {
for (const appearance of entity.appearances) {
if (appearance.filePath === oldPath) {
appearance.filePath = newPath
}
}
}
}
/**
* Get persistent entity by ID
*/
async getPersistentEntity(entityId: string): Promise<PersistentEntity | null> {
// Check cache first
if (this.entityCache.has(entityId)) {
return this.entityCache.get(entityId)!
}
// Use parent getEntity method
const entity = await super.getEntity<PersistentEntity>(entityId)
if (entity) {
// Cache and return
this.entityCache.set(entityId, entity)
}
return entity
}
/**
* Update stored entity (rename to avoid parent method conflict)
*/
private async updatePersistentEntity(entity: PersistentEntity): Promise<void> {
// Find the Brainy entity
const results = await this.brain.find({
where: {
id: entity.id,
entityType: 'persistent',
system: 'vfs-entity'
},
type: NounType.Concept,
limit: 1
})
if (results.length > 0) {
await this.brain.update({
id: results[0].entity.id,
data: Buffer.from(JSON.stringify(entity)),
metadata: {
...entity,
entityType: 'persistent',
system: 'vfs-entity'
}
})
}
// Update cache
this.entityCache.set(entity.id, entity)
}
/**
* Detect changes in entity from context
*/
private async detectChanges(
entity: PersistentEntity,
context: string,
source: string
): Promise<EntityChange[]> {
// Simple change detection - in production would use NLP
const changes: EntityChange[] = []
const timestamp = Date.now()
// Look for attribute changes in context
const attributePatterns = [
/(\w+):\s*"([^"]+)"/g, // key: "value"
/(\w+)\s*=\s*"([^"]+)"/g, // key = "value"
/set(\w+)\("([^"]+)"\)/g // setProperty("value")
]
for (const pattern of attributePatterns) {
const matches = context.matchAll(pattern)
for (const match of matches) {
const field = match[1].toLowerCase()
const newValue = match[2]
if (field in entity.attributes && entity.attributes[field] !== newValue) {
changes.push({
field: `attributes.${field}`,
oldValue: entity.attributes[field],
newValue,
timestamp,
source
})
}
}
}
return changes
}
/**
* Generate embedding for entity
*/
private async generateEntityEmbedding(entity: PersistentEntity): Promise<number[] | undefined> {
try {
// Create text representation of entity
const text = [
entity.name,
entity.description || '',
entity.type,
...entity.aliases,
JSON.stringify(entity.attributes)
].join(' ')
return await this.generateTextEmbedding(text)
} catch (error) {
console.error('Failed to generate entity embedding:', error)
return undefined
}
}
/**
* Generate embedding for text
*/
private async generateTextEmbedding(text: string): Promise<number[] | undefined> {
try {
// Generate embedding using Brainy's embed method
const vector = await this.brain.embed(text)
return vector
} catch (error) {
console.debug('Failed to generate embedding:', error)
return undefined
}
}
/**
* Detect entity type from name and context
*/
private detectEntityType(name: string, context: string): string {
if (context.includes('class ' + name)) return 'class'
if (context.includes('function ' + name)) return 'function'
if (context.includes('/api/')) return 'api'
if (/^[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*$/.test(name)) return 'person'
return 'entity'
}
/**
* Extract context around a position
*/
private extractContext(text: string, position: number, radius = 100): string {
const start = Math.max(0, position - radius)
const end = Math.min(text.length, position + radius)
return text.slice(start, end)
}
/**
* Clear entity cache
*/
clearCache(entityId?: string): void {
if (entityId) {
this.entityCache.delete(entityId)
} else {
this.entityCache.clear()
}
}
}

View file

@ -1,404 +0,0 @@
/**
* Semantic Versioning System for VFS
*
* Only creates versions when the MEANING of content changes significantly
* PRODUCTION-READY: Real implementation using embeddings
*/
import { Brainy } from '../brainy.js'
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 extends ManagedEntity {
id: string
path: string
version: number
timestamp: number
hash: string
size: number
semanticHash?: string // Hash of the embedding for quick comparison
author?: string
message?: string
parentVersion?: string
}
/**
* Semantic versioning configuration
*/
export interface SemanticVersioningConfig {
threshold?: number // Semantic change threshold (0-1, default 0.3)
maxVersions?: number // Max versions to keep per file
minInterval?: number // Minimum time between versions (ms)
sizeChangeThreshold?: number // Size change threshold (0-1)
}
/**
* Semantic Versioning System
*
* Creates versions only when content meaning changes significantly
* Uses vector embeddings to detect semantic changes
*/
export class SemanticVersioning extends EntityManager {
private config: Required<SemanticVersioningConfig>
private versionCache = new Map<string, Version[]>()
constructor(
brain: Brainy,
config?: SemanticVersioningConfig
) {
super(brain, 'vfs-version')
this.config = {
threshold: config?.threshold ?? 0.3,
maxVersions: config?.maxVersions ?? 10,
minInterval: config?.minInterval ?? 60000, // 1 minute
sizeChangeThreshold: config?.sizeChangeThreshold ?? 0.5
}
}
/**
* Check if content has changed enough to warrant a new version
*/
async shouldVersion(oldContent: Buffer, newContent: Buffer): Promise<boolean> {
// Quick hash check - if identical, no version needed
const oldHash = this.hashContent(oldContent)
const newHash = this.hashContent(newContent)
if (oldHash === newHash) {
return false
}
// Check size change
const sizeChange = Math.abs(oldContent.length - newContent.length) / Math.max(oldContent.length, 1)
if (sizeChange > this.config.sizeChangeThreshold) {
return true // Large size change warrants version
}
// For small files, any change is significant
if (oldContent.length < 100 || newContent.length < 100) {
return true
}
// Check semantic change using embeddings
try {
const semanticDistance = await this.calculateSemanticDistance(oldContent, newContent)
return semanticDistance > this.config.threshold
} catch (error) {
// If embedding fails, fall back to size-based decision
console.warn('Failed to calculate semantic distance:', error)
return sizeChange > 0.2
}
}
/**
* Create a new version
*/
async createVersion(
path: string,
content: Buffer,
metadata?: {
author?: string
message?: string
}
): Promise<string> {
const versionId = uuidv4()
const timestamp = Date.now()
const hash = this.hashContent(content)
// Get current version number
const versions = await this.getVersions(path)
const versionNumber = versions.length + 1
const parentVersion = versions[0]?.id
// Generate embedding for semantic comparison
let embedding: number[] | undefined
let semanticHash: string | undefined
try {
// Only generate embedding for reasonably sized content
if (content.length < 100000) {
embedding = await this.generateEmbedding(content)
if (embedding) {
semanticHash = this.hashEmbedding(embedding)
}
}
} catch (error) {
console.warn('Failed to generate embedding for version:', error)
}
// 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) {
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
if (!this.versionCache.has(path)) {
this.versionCache.set(path, [])
}
this.versionCache.get(path)!.unshift({
id: versionId,
path,
version: versionNumber,
timestamp,
hash,
size: content.length,
semanticHash,
author: metadata?.author,
message: metadata?.message,
parentVersion
})
// Prune old versions if needed
await this.pruneVersions(path)
return versionId
}
/**
* Get all versions for a file
*/
async getVersions(path: string): Promise<Version[]> {
// Check cache first
if (this.versionCache.has(path)) {
return this.versionCache.get(path)!
}
// Query using EntityManager
const versions = await this.findEntities<Version>(
{ path },
NounType.State,
this.config.maxVersions * 2 // Get extra in case some are pruned
)
// Sort by timestamp (newest first)
versions.sort((a, b) => b.timestamp - a.timestamp)
// Update cache
this.versionCache.set(path, versions)
return versions
}
/**
* Get a specific version's content
*/
async getVersion(path: string, versionId: string): Promise<Buffer | null> {
// Get the version entity
const version = await this.getEntity<Version>(versionId)
if (!version || version.path !== path) {
return null
}
// 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
}
/**
* Restore a file to a specific version
*/
async restoreVersion(path: string, versionId: string): Promise<Buffer | null> {
const content = await this.getVersion(path, versionId)
if (!content) {
throw new Error(`Version ${versionId} not found for ${path}`)
}
// Create a new version pointing to the restored one
await this.createVersion(path, content, {
message: `Restored to version ${versionId}`
})
return content
}
/**
* Get version history with diffs
*/
async getVersionHistory(path: string, limit = 10): Promise<Array<{
version: Version
changes?: {
additions: number
deletions: number
semanticChange: number
}
}>> {
const versions = await this.getVersions(path)
const history = []
for (let i = 0; i < Math.min(versions.length, limit); i++) {
const version = versions[i]
let changes = undefined
// Calculate changes from parent
if (version.parentVersion && i < versions.length - 1) {
const parentVersion = versions[i + 1]
if (parentVersion.id === version.parentVersion) {
// Simple size-based diff for now
changes = {
additions: Math.max(0, version.size - parentVersion.size),
deletions: Math.max(0, parentVersion.size - version.size),
semanticChange: version.semanticHash && parentVersion.semanticHash
? this.estimateSemanticChange(version.semanticHash, parentVersion.semanticHash)
: 0
}
}
}
history.push({ version, changes })
}
return history
}
/**
* Prune old versions beyond the limit
*/
private async pruneVersions(path: string): Promise<void> {
const versions = await this.getVersions(path)
if (versions.length <= this.config.maxVersions) {
return
}
// Keep important versions (first, last, and evenly distributed)
const toKeep = new Set<string>()
const toDelete: string[] = []
// Always keep first and last
toKeep.add(versions[0].id) // Newest
toKeep.add(versions[versions.length - 1].id) // Oldest
// Keep evenly distributed versions
const step = Math.floor(versions.length / this.config.maxVersions)
for (let i = 0; i < versions.length; i += step) {
toKeep.add(versions[i].id)
}
// Mark others for deletion
for (const version of versions) {
if (!toKeep.has(version.id)) {
toDelete.push(version.id)
}
}
// Delete excess versions
for (const id of toDelete.slice(0, versions.length - this.config.maxVersions)) {
await this.deleteEntity(id)
}
// Update cache
this.versionCache.set(
path,
versions.filter(v => !toDelete.includes(v.id))
)
}
/**
* Calculate semantic distance between two pieces of content
*/
private async calculateSemanticDistance(oldContent: Buffer, newContent: Buffer): Promise<number> {
// Generate embeddings
const [oldEmbedding, newEmbedding] = await Promise.all([
this.generateEmbedding(oldContent),
this.generateEmbedding(newContent)
])
if (!oldEmbedding || !newEmbedding) {
throw new Error('Failed to generate embeddings')
}
// Calculate cosine distance
return cosineDistance(oldEmbedding, newEmbedding)
}
/**
* Generate embedding for content
*/
private async generateEmbedding(content: Buffer): Promise<number[] | undefined> {
try {
// For text content, use first 10KB for embedding
const text = content.toString('utf8', 0, Math.min(10240, content.length))
// Use Brainy's embedding function
const vector = await this.brain.embed(text)
return vector
} catch (error) {
console.error('Failed to generate embedding:', error)
return undefined
}
}
/**
* Hash content for quick comparison
*/
private hashContent(content: Buffer): string {
return createHash('sha256').update(content).digest('hex')
}
/**
* Hash embedding for quick comparison
*/
private hashEmbedding(embedding: number[]): string {
return createHash('sha256')
.update(Buffer.from(new Float32Array(embedding).buffer))
.digest('hex')
}
/**
* Estimate semantic change from hashes (rough approximation)
*/
private estimateSemanticChange(hash1: string, hash2: string): number {
if (hash1 === hash2) return 0
// Simple hamming distance on first few characters
// This is a rough approximation
let distance = 0
for (let i = 0; i < Math.min(hash1.length, hash2.length, 8); i++) {
if (hash1[i] !== hash2[i]) distance++
}
return distance / 8
}
/**
* Clear version cache for a file
*/
clearCache(path?: string): void {
if (path) {
this.versionCache.delete(path)
} else {
this.versionCache.clear()
}
}
}

View file

@ -12,7 +12,17 @@ import { Brainy } from '../brainy.js'
import { Entity, AddParams, RelateParams, FindParams, Relation } from '../types/brainy.types.js'
import { NounType, VerbType } from '../types/graphTypes.js'
import { PathResolver } from './PathResolver.js'
// Knowledge Layer imports removed - now in KnowledgeAugmentation
import {
SemanticPathResolver,
ProjectionRegistry,
ConceptProjection,
AuthorProjection,
TemporalProjection,
RelationshipProjection,
SimilarityProjection,
TagProjection
} from './semantic/index.js'
// Knowledge Layer can remain as optional augmentation for now
import {
IVirtualFileSystem,
VFSConfig,
@ -48,13 +58,14 @@ import {
*/
export class VirtualFileSystem implements IVirtualFileSystem {
private brain: Brainy
private pathResolver!: PathResolver
private pathResolver!: SemanticPathResolver
private projectionRegistry!: ProjectionRegistry
private config: Required<Omit<VFSConfig, 'rootEntityId'>> & { rootEntityId?: string }
private rootEntityId?: string
private initialized = false
private currentUser: string = 'system' // Track current user for collaboration
// Knowledge Layer removed from core - now optional augmentation
// Knowledge Layer features available via augmentation (brain.use('knowledge'))
// Caches for performance
private contentCache: Map<string, { data: Buffer, timestamp: number }>
@ -93,12 +104,17 @@ export class VirtualFileSystem implements IVirtualFileSystem {
// Create or find root entity
this.rootEntityId = await this.initializeRoot()
// Initialize path resolver
this.pathResolver = new PathResolver(this.brain, this.rootEntityId, {
maxCacheSize: this.config.cache?.maxPaths,
cacheTTL: this.config.cache?.ttl,
hotPathThreshold: 10
})
// Initialize projection registry with auto-discovery of built-in projections
this.projectionRegistry = new ProjectionRegistry()
this.registerBuiltInProjections()
// Initialize semantic path resolver (zero-config, uses brain.config)
this.pathResolver = new SemanticPathResolver(
this.brain,
this, // Pass VFS instance for resolvePath
this.rootEntityId,
this.projectionRegistry
)
// Knowledge Layer is now a separate augmentation
// Enable with: brain.use('knowledge')
@ -112,6 +128,32 @@ export class VirtualFileSystem implements IVirtualFileSystem {
/**
* Create or find the root directory entity
*/
/**
* Auto-register built-in projection strategies
* Zero-config: All semantic dimensions work out of the box
*/
private registerBuiltInProjections(): void {
const projections = [
ConceptProjection,
AuthorProjection,
TemporalProjection,
RelationshipProjection,
SimilarityProjection,
TagProjection
]
for (const ProjectionClass of projections) {
try {
this.projectionRegistry.register(new ProjectionClass())
} catch (err) {
// Silently skip if already registered (e.g., in tests)
if (!(err instanceof Error && err.message.includes('already registered'))) {
throw err
}
}
}
}
private async initializeRoot(): Promise<string> {
// Check if root already exists - search using where clause
const existing = await this.brain.find({
@ -1230,6 +1272,17 @@ export class VirtualFileSystem implements IVirtualFileSystem {
metadata.lineCount = text.split('\n').length
metadata.wordCount = text.split(/\s+/).filter(w => w).length
metadata.charset = 'utf-8'
// Extract concepts using brain.extractConcepts() (neural extraction)
if (this.config.intelligence?.autoConcepts) {
try {
const concepts = await this.brain.extractConcepts(text, { limit: 20 })
metadata.conceptNames = concepts // Flattened for O(log n) queries
} catch (error) {
// Concept extraction is optional - don't fail if it errors
console.debug('Concept extraction failed:', error)
}
}
}
// Extract hash for integrity
@ -1409,9 +1462,6 @@ export class VirtualFileSystem implements IVirtualFileSystem {
maxFileSize: 1_000_000_000, // 1GB
maxPathLength: 4096,
maxDirectoryEntries: 100_000
},
knowledgeLayer: {
enabled: false // Default to disabled
}
}
}
@ -2057,17 +2107,6 @@ export class VirtualFileSystem implements IVirtualFileSystem {
this.invalidateCaches(path)
}
// ============= Knowledge Layer =============
// Knowledge Layer methods are added by KnowledgeLayer.enable()
// This keeps VFS pure and fast while allowing optional intelligence
/**
* Enable Knowledge Layer on this VFS instance
*/
async enableKnowledgeLayer(): Promise<void> {
const { enableKnowledgeLayer } = await import('./KnowledgeLayer.js')
await enableKnowledgeLayer(this, this.brain)
}
/**
* Set the current user for tracking who makes changes

View file

@ -20,12 +20,5 @@ export { DirectoryImporter } from './importers/DirectoryImporter.js'
export { VFSReadStream } from './streams/VFSReadStream.js'
export { VFSWriteStream } from './streams/VFSWriteStream.js'
// Knowledge Layer Components (optional via augmentation)
export { EventRecorder } from './EventRecorder.js'
export { SemanticVersioning } from './SemanticVersioning.js'
export { PersistentEntitySystem } from './PersistentEntitySystem.js'
export { ConceptSystem } from './ConceptSystem.js'
export { GitBridge } from './GitBridge.js'
// Convenience alias
export { VirtualFileSystem as VFS } from './VirtualFileSystem.js'

View file

@ -0,0 +1,147 @@
/**
* Projection Registry
*
* Central registry for all projection strategies
* Manages strategy lookup and execution
*/
import { ProjectionStrategy } from './ProjectionStrategy.js'
import { Brainy } from '../../brainy.js'
import { VirtualFileSystem } from '../VirtualFileSystem.js'
import { VFSEntity } from '../types.js'
/**
* Registry for projection strategies
* Allows dynamic registration and lookup of strategies
*/
export class ProjectionRegistry {
private strategies = new Map<string, ProjectionStrategy>()
/**
* Register a projection strategy
* @param strategy - The strategy to register
* @throws Error if strategy with same name already registered
*/
register(strategy: ProjectionStrategy): void {
if (this.strategies.has(strategy.name)) {
throw new Error(`Projection strategy '${strategy.name}' is already registered`)
}
this.strategies.set(strategy.name, strategy)
}
/**
* Get a projection strategy by name
* @param name - Strategy name
* @returns The strategy or undefined if not found
*/
get(name: string): ProjectionStrategy | undefined {
return this.strategies.get(name)
}
/**
* Check if a strategy is registered
* @param name - Strategy name
*/
has(name: string): boolean {
return this.strategies.has(name)
}
/**
* List all registered strategy names
*/
listDimensions(): string[] {
return Array.from(this.strategies.keys())
}
/**
* Get count of registered strategies
*/
count(): number {
return this.strategies.size
}
/**
* Resolve a dimension value to entity IDs
* Convenience method that looks up strategy and calls resolve()
*
* @param dimension - The semantic dimension
* @param value - The value to resolve
* @param brain - REAL Brainy instance
* @param vfs - REAL VirtualFileSystem instance
* @returns Array of entity IDs
* @throws Error if dimension not registered
*/
async resolve(
dimension: string,
value: any,
brain: Brainy,
vfs: VirtualFileSystem
): Promise<string[]> {
const strategy = this.get(dimension)
if (!strategy) {
throw new Error(`Unknown projection dimension: ${dimension}. Registered dimensions: ${this.listDimensions().join(', ')}`)
}
// Call REAL strategy resolve method
return await strategy.resolve(brain, vfs, value)
}
/**
* List entities in a dimension
* Convenience method for strategies that support listing
*
* @param dimension - The semantic dimension
* @param brain - REAL Brainy instance
* @param vfs - REAL VirtualFileSystem instance
* @param limit - Max results
* @returns Array of VFSEntity
* @throws Error if dimension not registered or doesn't support listing
*/
async list(
dimension: string,
brain: Brainy,
vfs: VirtualFileSystem,
limit?: number
): Promise<VFSEntity[]> {
const strategy = this.get(dimension)
if (!strategy) {
throw new Error(`Unknown projection dimension: ${dimension}`)
}
if (!strategy.list) {
throw new Error(`Projection '${dimension}' does not support listing`)
}
return await strategy.list(brain, vfs, limit)
}
/**
* Unregister a strategy
* Useful for testing or dynamic strategy management
*
* @param name - Strategy name to remove
* @returns true if removed, false if not found
*/
unregister(name: string): boolean {
return this.strategies.delete(name)
}
/**
* Clear all registered strategies
* Useful for testing
*/
clear(): void {
this.strategies.clear()
}
/**
* Get all registered strategies
* Returns a copy to prevent external modification
*/
getAll(): ProjectionStrategy[] {
return Array.from(this.strategies.values())
}
}

View file

@ -0,0 +1,93 @@
/**
* Projection Strategy Interface
*
* Defines how to map semantic path dimensions to Brainy queries
* Each strategy uses EXISTING Brainy indexes and methods
*/
import { Brainy } from '../../brainy.js'
import { VirtualFileSystem } from '../VirtualFileSystem.js'
import { FindParams } from '../../types/brainy.types.js'
import { VFSEntity } from '../types.js'
/**
* Strategy for projecting semantic paths into entity queries
* All implementations MUST use real Brainy methods (no stubs!)
*/
export interface ProjectionStrategy {
/**
* Strategy name (used for registration)
*/
readonly name: string
/**
* Convert semantic value to Brainy FindParams
* Uses EXISTING FindParams type from brainy.types.ts
*/
toQuery(value: any, subpath?: string): FindParams
/**
* Resolve semantic value to entity IDs
* Uses REAL Brainy.find() method
*
* @param brain - REAL Brainy instance
* @param vfs - REAL VirtualFileSystem instance
* @param value - The semantic value to resolve
* @returns Array of entity IDs that match
*/
resolve(brain: Brainy, vfs: VirtualFileSystem, value: any): Promise<string[]>
/**
* List all entities in this dimension
* Optional - not all strategies need to implement
*
* @param brain - REAL Brainy instance
* @param vfs - REAL VirtualFileSystem instance
* @param limit - Max results to return
*/
list?(brain: Brainy, vfs: VirtualFileSystem, limit?: number): Promise<VFSEntity[]>
}
/**
* Base class for projection strategies with common utilities
*/
export abstract class BaseProjectionStrategy implements ProjectionStrategy {
abstract readonly name: string
abstract toQuery(value: any, subpath?: string): FindParams
abstract resolve(brain: Brainy, vfs: VirtualFileSystem, value: any): Promise<string[]>
/**
* Convert Brainy Results to entity IDs
* Helper method for subclasses
*/
protected extractIds(results: Array<{ id: string }>): string[] {
return results.map(r => r.id)
}
/**
* Verify that an entity is a file (not directory)
* Uses REAL Brainy.get() method
*/
protected async isFile(brain: Brainy, entityId: string): Promise<boolean> {
const entity = await brain.get(entityId)
return entity?.metadata?.vfsType === 'file'
}
/**
* Filter entity IDs to only include files
* Uses REAL Brainy.get() for each entity
*/
protected async filterFiles(brain: Brainy, entityIds: string[]): Promise<string[]> {
const files: string[] = []
for (const id of entityIds) {
if (await this.isFile(brain, id)) {
files.push(id)
}
}
return files
}
}

View file

@ -0,0 +1,363 @@
/**
* Semantic Path Parser
*
* Parses semantic filesystem paths into structured queries
* PURE LOGIC - No external dependencies, no async operations
*
* Supported path formats:
* - Traditional: /src/auth.ts
* - By Concept: /by-concept/authentication/login.ts
* - By Author: /by-author/alice/file.ts
* - By Time: /as-of/2024-03-15/file.ts
* - By Relationship: /related-to/src/auth.ts/depth-2
* - By Similarity: /similar-to/src/auth.ts/threshold-0.8
* - By Tag: /by-tag/security/file.ts
*/
export type SemanticDimension =
| 'traditional'
| 'concept'
| 'author'
| 'time'
| 'relationship'
| 'similar'
| 'tag'
export interface ParsedSemanticPath {
dimension: SemanticDimension
value: string | Date | RelationshipValue | SimilarityValue
subpath?: string
filters?: Record<string, any>
}
export interface RelationshipValue {
targetPath: string
depth?: number
relationshipTypes?: string[]
}
export interface SimilarityValue {
targetPath: string
threshold?: number
}
/**
* Semantic Path Parser
* Parses various semantic path formats into structured data
*/
export class SemanticPathParser {
// Regex patterns for each dimension
private static readonly PATTERNS = {
concept: /^\/by-concept\/([^\/]+)(?:\/(.+))?$/,
author: /^\/by-author\/([^\/]+)(?:\/(.+))?$/,
time: /^\/as-of\/(\d{4}-\d{2}-\d{2})(?:\/(.+))?$/,
// Relationship: /related-to/<path>/depth-N/types-X,Y/<subpath>
// Must handle paths with slashes, so capture everything before /depth- or /types-
relationship: /^\/related-to\/(.+?)(?:\/depth-(\d+)|\/types-([^\/]+)|\/(.+))*$/,
// Similarity: /similar-to/<path>/threshold-N/<subpath>
similar: /^\/similar-to\/(.+?)(?:\/threshold-([\d.]+)|\/(.+))*$/,
tag: /^\/by-tag\/([^\/]+)(?:\/(.+))?$/
}
/**
* Parse a path into semantic components
* PURE FUNCTION - no external calls, no async
*/
parse(path: string): ParsedSemanticPath {
if (!path || typeof path !== 'string') {
throw new Error('Path must be a non-empty string')
}
// Normalize path
const normalized = this.normalizePath(path)
// Try concept dimension
const conceptMatch = normalized.match(SemanticPathParser.PATTERNS.concept)
if (conceptMatch) {
return {
dimension: 'concept',
value: conceptMatch[1],
subpath: conceptMatch[2]
}
}
// Try author dimension
const authorMatch = normalized.match(SemanticPathParser.PATTERNS.author)
if (authorMatch) {
return {
dimension: 'author',
value: authorMatch[1],
subpath: authorMatch[2]
}
}
// Try time dimension
const timeMatch = normalized.match(SemanticPathParser.PATTERNS.time)
if (timeMatch) {
const dateStr = timeMatch[1]
const date = this.parseDate(dateStr)
return {
dimension: 'time',
value: date,
subpath: timeMatch[2]
}
}
// Try relationship dimension
if (normalized.startsWith('/related-to/')) {
return this.parseRelationshipPath(normalized)
}
// Try similarity dimension
if (normalized.startsWith('/similar-to/')) {
return this.parseSimilarityPath(normalized)
}
// Try tag dimension
const tagMatch = normalized.match(SemanticPathParser.PATTERNS.tag)
if (tagMatch) {
return {
dimension: 'tag',
value: tagMatch[1],
subpath: tagMatch[2]
}
}
// Default to traditional path
return {
dimension: 'traditional',
value: normalized
}
}
/**
* Check if a path is semantic (non-traditional)
*/
isSemanticPath(path: string): boolean {
if (!path || typeof path !== 'string') {
return false
}
const normalized = this.normalizePath(path)
// Check if matches any semantic pattern
return (
normalized.startsWith('/by-concept/') ||
normalized.startsWith('/by-author/') ||
normalized.startsWith('/as-of/') ||
normalized.startsWith('/related-to/') ||
normalized.startsWith('/similar-to/') ||
normalized.startsWith('/by-tag/')
)
}
/**
* Get the dimension type from a path
*/
getDimension(path: string): SemanticDimension {
return this.parse(path).dimension
}
/**
* Normalize a path - remove trailing slashes, collapse multiple slashes
* PURE FUNCTION
*/
private normalizePath(path: string): string {
// Remove trailing slash (except for root)
let normalized = path.replace(/\/+$/, '')
// Collapse multiple slashes
normalized = normalized.replace(/\/+/g, '/')
// Ensure starts with /
if (!normalized.startsWith('/')) {
normalized = '/' + normalized
}
// Special case: empty path becomes /
if (normalized === '') {
normalized = '/'
}
return normalized
}
/**
* Parse date string (YYYY-MM-DD) into Date object
* PURE FUNCTION
*/
private parseDate(dateStr: string): Date {
const parts = dateStr.split('-')
if (parts.length !== 3) {
throw new Error(`Invalid date format: ${dateStr}. Expected YYYY-MM-DD`)
}
const year = parseInt(parts[0], 10)
const month = parseInt(parts[1], 10) - 1 // Months are 0-indexed in JS
const day = parseInt(parts[2], 10)
if (isNaN(year) || isNaN(month) || isNaN(day)) {
throw new Error(`Invalid date format: ${dateStr}. Expected YYYY-MM-DD`)
}
if (year < 1900 || year > 2100) {
throw new Error(`Invalid year: ${year}. Expected 1900-2100`)
}
if (month < 0 || month > 11) {
throw new Error(`Invalid month: ${month + 1}. Expected 1-12`)
}
if (day < 1 || day > 31) {
throw new Error(`Invalid day: ${day}. Expected 1-31`)
}
return new Date(year, month, day)
}
/**
* Validate parsed path structure
*/
validate(parsed: ParsedSemanticPath): boolean {
if (!parsed || typeof parsed !== 'object') {
return false
}
if (!parsed.dimension) {
return false
}
if (parsed.value === undefined || parsed.value === null) {
return false
}
// Dimension-specific validation
switch (parsed.dimension) {
case 'time':
return parsed.value instanceof Date && !isNaN(parsed.value.getTime())
case 'relationship':
const relValue = parsed.value as RelationshipValue
return typeof relValue.targetPath === 'string' && relValue.targetPath.length > 0
case 'similar':
const simValue = parsed.value as SimilarityValue
return typeof simValue.targetPath === 'string' && simValue.targetPath.length > 0
default:
return typeof parsed.value === 'string' && parsed.value.length > 0
}
}
/**
* Parse relationship paths: /related-to/<path>/depth-N/types-X,Y/<subpath>
*/
private parseRelationshipPath(path: string): ParsedSemanticPath {
// Remove /related-to/ prefix
const withoutPrefix = path.substring('/related-to/'.length)
// Split into segments
const segments = withoutPrefix.split('/')
let targetPath = ''
let depth: number | undefined
let types: string[] | undefined
let subpath: string | undefined
let i = 0
// Collect path segments until we hit depth-, types-, or end
while (i < segments.length) {
const segment = segments[i]
if (segment.startsWith('depth-')) {
depth = parseInt(segment.substring('depth-'.length), 10)
i++
continue
}
if (segment.startsWith('types-')) {
types = segment.substring('types-'.length).split(',')
i++
continue
}
// If we've already collected the target path and found depth/types,
// rest is subpath
if (targetPath && (depth !== undefined || types !== undefined)) {
subpath = segments.slice(i).join('/')
break
}
// Add to target path
if (targetPath) {
targetPath += '/' + segment
} else {
targetPath = segment
}
i++
}
const value: RelationshipValue = {
targetPath,
depth,
relationshipTypes: types
}
return {
dimension: 'relationship',
value,
subpath
}
}
/**
* Parse similarity paths: /similar-to/<path>/threshold-N/<subpath>
*/
private parseSimilarityPath(path: string): ParsedSemanticPath {
// Remove /similar-to/ prefix
const withoutPrefix = path.substring('/similar-to/'.length)
// Split into segments
const segments = withoutPrefix.split('/')
let targetPath = ''
let threshold: number | undefined
let subpath: string | undefined
let i = 0
// Collect path segments until we hit threshold- or end
while (i < segments.length) {
const segment = segments[i]
if (segment.startsWith('threshold-')) {
threshold = parseFloat(segment.substring('threshold-'.length))
i++
// Rest is subpath
if (i < segments.length) {
subpath = segments.slice(i).join('/')
}
break
}
// Add to target path
if (targetPath) {
targetPath += '/' + segment
} else {
targetPath = segment
}
i++
}
const value: SimilarityValue = {
targetPath,
threshold
}
return {
dimension: 'similar',
value,
subpath
}
}
}

View file

@ -0,0 +1,326 @@
/**
* Semantic Path Resolver
*
* Unified path resolver that handles BOTH:
* - Traditional hierarchical paths (/src/auth/login.ts)
* - Semantic projection paths (/by-concept/authentication/...)
*
* Uses EXISTING infrastructure:
* - PathResolver for traditional paths
* - ProjectionRegistry for semantic dimensions
* - SemanticPathParser for path type detection
*/
import { Brainy } from '../../brainy.js'
import { VirtualFileSystem } from '../VirtualFileSystem.js'
import { PathResolver } from '../PathResolver.js'
import { VFSEntity, VFSError, VFSErrorCode } from '../types.js'
import { SemanticPathParser, ParsedSemanticPath } from './SemanticPathParser.js'
import { ProjectionRegistry } from './ProjectionRegistry.js'
import { UnifiedCache } from '../../utils/unifiedCache.js'
/**
* Semantic Path Resolver
* Handles both traditional and semantic paths transparently
*
* Uses Brainy's UnifiedCache for optimal memory management and performance
*/
export class SemanticPathResolver {
private brain: Brainy
private vfs: VirtualFileSystem
private pathResolver: PathResolver
private parser: SemanticPathParser
private registry: ProjectionRegistry
private cache: UnifiedCache
constructor(
brain: Brainy,
vfs: VirtualFileSystem,
rootEntityId: string,
registry: ProjectionRegistry
) {
this.brain = brain
this.vfs = vfs
this.registry = registry
this.parser = new SemanticPathParser()
// Use Brainy's UnifiedCache for semantic path caching
// Zero-config: Uses 2GB default from UnifiedCache
this.cache = new UnifiedCache({
enableRequestCoalescing: true,
enableFairnessCheck: true
})
// Create traditional path resolver (uses its own optimized cache with defaults)
this.pathResolver = new PathResolver(brain, rootEntityId)
}
/**
* Resolve a path to entity ID(s)
* Handles BOTH traditional and semantic paths
*
* For traditional paths: Returns single entity ID
* For semantic paths: Returns first matching entity ID
*
* Uses UnifiedCache with request coalescing to prevent stampede
*
* @param path - Path to resolve (traditional or semantic)
* @param options - Resolution options
* @returns Entity ID
*/
async resolve(path: string, options?: {
followSymlinks?: boolean
cache?: boolean
}): Promise<string> {
// Parse the path to determine dimension
const parsed = this.parser.parse(path)
// Handle based on path dimension
if (parsed.dimension === 'traditional') {
// Use existing PathResolver for traditional paths
return await this.pathResolver.resolve(path, options)
}
// Semantic path - use UnifiedCache with request coalescing
const cacheKey = `semantic:${path}`
if (options?.cache === false) {
// Skip cache if requested
const entityIds = await this.resolveSemanticPathInternal(parsed)
if (entityIds.length === 0) {
throw new VFSError(
VFSErrorCode.ENOENT,
`No entities found for semantic path: ${path}`,
path,
'resolve'
)
}
return entityIds[0]
}
// Use UnifiedCache - automatically handles stampede prevention
const entityIds = await this.cache.get(cacheKey, async () => {
return await this.resolveSemanticPathInternal(parsed)
})
if (!entityIds || entityIds.length === 0) {
throw new VFSError(
VFSErrorCode.ENOENT,
`No entities found for semantic path: ${path}`,
path,
'resolve'
)
}
return entityIds[0]
}
/**
* Resolve semantic path to multiple entity IDs
* This is the polymorphic resolution that returns ALL matches
*
* Uses UnifiedCache for performance
*
* @param path - Semantic path
* @param options - Resolution options
* @returns Array of entity IDs
*/
async resolveAll(path: string, options?: {
cache?: boolean
limit?: number
}): Promise<string[]> {
const parsed = this.parser.parse(path)
if (parsed.dimension === 'traditional') {
// Traditional paths resolve to single entity
const id = await this.pathResolver.resolve(path, options)
return [id]
}
// Use cache if enabled
const cacheKey = `semantic:${path}`
if (options?.cache === false) {
return await this.resolveSemanticPathInternal(parsed, options?.limit)
}
// UnifiedCache with automatic stampede prevention
return await this.cache.get(cacheKey, async () => {
return await this.resolveSemanticPathInternal(parsed, options?.limit)
})
}
/**
* Internal semantic path resolution (called by cache)
* Estimates cost and size for UnifiedCache optimization
*/
private async resolveSemanticPathInternal(
parsed: ParsedSemanticPath,
limit?: number
): Promise<string[]> {
// Resolve based on dimension
let entityIds: string[] = []
switch (parsed.dimension) {
case 'concept':
entityIds = await this.registry.resolve('concept', parsed.value, this.brain, this.vfs)
break
case 'author':
entityIds = await this.registry.resolve('author', parsed.value, this.brain, this.vfs)
break
case 'time':
entityIds = await this.registry.resolve('time', parsed.value, this.brain, this.vfs)
break
case 'relationship':
entityIds = await this.registry.resolve('relationship', parsed.value, this.brain, this.vfs)
break
case 'similar':
entityIds = await this.registry.resolve('similar', parsed.value, this.brain, this.vfs)
break
case 'tag':
// Tags use metadata filtering (concept-like)
entityIds = await this.registry.resolve('tag', parsed.value, this.brain, this.vfs)
break
case 'traditional':
// Shouldn't reach here, but handle it gracefully
return []
default:
throw new VFSError(
VFSErrorCode.ENOTDIR, // Use existing error code
`Unsupported semantic path dimension: ${parsed.dimension}`,
'',
'resolve'
)
}
// Apply subpath filter if specified
if (parsed.subpath) {
entityIds = await this.filterBySubpath(entityIds, parsed.subpath)
}
// Apply limit
if (limit && limit > 0) {
entityIds = entityIds.slice(0, limit)
}
// Result will be cached by UnifiedCache.get() automatically
return entityIds
}
/**
* Filter entity IDs by subpath (filename or partial path)
*/
private async filterBySubpath(entityIds: string[], subpath: string): Promise<string[]> {
const filtered: string[] = []
for (const id of entityIds) {
const entity = await this.brain.get(id)
if (!entity) continue
const name = entity.metadata?.name
const path = entity.metadata?.path
// Check if name or path matches subpath
if (name === subpath || path?.endsWith(subpath)) {
filtered.push(id)
}
}
return filtered
}
/**
* Get children of a directory
* Delegates to PathResolver for traditional directories
* For semantic paths, returns entities in that dimension
*/
async getChildren(dirIdOrPath: string): Promise<VFSEntity[]> {
// If it looks like a path, parse it
if (dirIdOrPath.startsWith('/')) {
const parsed = this.parser.parse(dirIdOrPath)
if (parsed.dimension !== 'traditional') {
// For semantic paths, list entities in that dimension
return await this.listSemanticDimension(parsed)
}
}
// Traditional directory - use PathResolver
return await this.pathResolver.getChildren(dirIdOrPath)
}
/**
* List entities in a semantic dimension
*/
private async listSemanticDimension(parsed: ParsedSemanticPath): Promise<VFSEntity[]> {
switch (parsed.dimension) {
case 'concept':
return await this.registry.list('concept', this.brain, this.vfs)
case 'author':
return await this.registry.list('author', this.brain, this.vfs)
case 'time':
return await this.registry.list('time', this.brain, this.vfs)
case 'tag':
return await this.registry.list('tag', this.brain, this.vfs)
default:
return []
}
}
/**
* Create a path mapping (cache a path resolution)
* Only applies to traditional paths
*/
async createPath(path: string, entityId: string): Promise<void> {
const parsed = this.parser.parse(path)
if (parsed.dimension === 'traditional') {
await this.pathResolver.createPath(path, entityId)
}
// Semantic paths are not cached via createPath
}
/**
* Invalidate path cache
*/
invalidatePath(path: string, recursive = false): void {
const parsed = this.parser.parse(path)
if (parsed.dimension === 'traditional') {
this.pathResolver.invalidatePath(path, recursive)
} else {
// Invalidate semantic cache via UnifiedCache
const cacheKey = `semantic:${path}`
this.cache.delete(cacheKey)
}
}
/**
* Clear all semantic caches
* Uses UnifiedCache's clear method
*/
invalidateSemanticCache(): void {
this.cache.clear()
}
/**
* Cleanup resources
*/
cleanup(): void {
this.pathResolver.cleanup()
this.cache.clear()
}
}

28
src/vfs/semantic/index.ts Normal file
View file

@ -0,0 +1,28 @@
/**
* Semantic VFS - Index
*
* Central export point for all semantic VFS components
*/
// Core components
export { SemanticPathParser } from './SemanticPathParser.js'
export type {
SemanticDimension,
ParsedSemanticPath,
RelationshipValue,
SimilarityValue
} from './SemanticPathParser.js'
export { ProjectionRegistry } from './ProjectionRegistry.js'
export type { ProjectionStrategy } from './ProjectionStrategy.js'
export { BaseProjectionStrategy } from './ProjectionStrategy.js'
export { SemanticPathResolver } from './SemanticPathResolver.js'
// Built-in projections
export { ConceptProjection } from './projections/ConceptProjection.js'
export { AuthorProjection } from './projections/AuthorProjection.js'
export { TemporalProjection } from './projections/TemporalProjection.js'
export { RelationshipProjection } from './projections/RelationshipProjection.js'
export { SimilarityProjection } from './projections/SimilarityProjection.js'
export { TagProjection } from './projections/TagProjection.js'

View file

@ -0,0 +1,83 @@
/**
* Author Projection Strategy
*
* Maps author-based paths to files owned by that author
* Uses EXISTING MetadataIndexManager for O(log n) queries
*/
import { Brainy } from '../../../brainy.js'
import { VirtualFileSystem } from '../../VirtualFileSystem.js'
import { FindParams } from '../../../types/brainy.types.js'
import { BaseProjectionStrategy } from '../ProjectionStrategy.js'
import { VFSEntity } from '../../types.js'
/**
* Author Projection: /by-author/<authorName>/<subpath>
*
* Uses EXISTING infrastructure:
* - Brainy.find() with metadata filters (REAL)
* - MetadataIndexManager for O(log n) owner queries (REAL)
* - VFSMetadata.owner field (REAL - types.ts line 44)
*/
export class AuthorProjection extends BaseProjectionStrategy {
readonly name = 'author'
/**
* Convert author name to Brainy FindParams
*/
toQuery(authorName: string, subpath?: string): FindParams {
const query: FindParams = {
where: {
vfsType: 'file',
owner: authorName
},
limit: 1000
}
// Filter by filename if subpath specified
if (subpath) {
query.where = {
...query.where,
anyOf: [ // BFO logical operator (not $or)
{ name: subpath },
{ path: { endsWith: subpath } } // BFO operator (not $regex)
]
}
}
return query
}
/**
* Resolve author to entity IDs using REAL Brainy.find()
*/
async resolve(brain: Brainy, vfs: VirtualFileSystem, authorName: string): Promise<string[]> {
// Use REAL Brainy metadata filtering
const results = await brain.find({
where: {
vfsType: 'file',
owner: authorName
},
limit: 1000
})
return this.extractIds(results)
}
/**
* List all unique authors
* Uses aggregation over metadata
*/
async list(brain: Brainy, vfs: VirtualFileSystem, limit = 100): Promise<VFSEntity[]> {
// Get all files with owner metadata
const results = await brain.find({
where: {
vfsType: 'file',
owner: { $exists: true }
},
limit
})
return results.map(r => r.entity as VFSEntity)
}
}

View file

@ -0,0 +1,97 @@
/**
* Concept Projection Strategy
*
* Maps concept-based paths to files containing those concepts
* Uses EXISTING ConceptSystem and MetadataIndexManager
*/
import { Brainy } from '../../../brainy.js'
import { VirtualFileSystem } from '../../VirtualFileSystem.js'
import { FindParams } from '../../../types/brainy.types.js'
import { BaseProjectionStrategy } from '../ProjectionStrategy.js'
import { VFSEntity } from '../../types.js'
/**
* Concept Projection: /by-concept/<conceptName>/<subpath>
*
* Uses EXISTING infrastructure:
* - Brainy.find() with metadata filters (REAL - line 580 in brainy.ts)
* - MetadataIndexManager for O(log n) concept queries (REAL)
* - ConceptSystem for concept extraction (REAL - ConceptSystem.ts)
*/
export class ConceptProjection extends BaseProjectionStrategy {
readonly name = 'concept'
/**
* Convert concept name to Brainy FindParams
* Uses EXISTING FindParams.where for metadata filtering
*
* Now uses flattened conceptNames array for O(log n) performance!
*/
toQuery(conceptName: string, subpath?: string): FindParams {
const query: FindParams = {
where: {
vfsType: 'file',
conceptNames: { contains: conceptName } // O(log n) indexed query
},
limit: 1000
}
// If subpath specified, also filter by filename
if (subpath) {
query.where = {
...query.where,
anyOf: [ // BFO logical operator
{ name: subpath },
{ path: { endsWith: subpath } } // BFO operator
]
}
}
return query
}
/**
* Resolve concept to entity IDs using REAL Brainy.find()
* VERIFIED: brain.find() exists at line 580 in brainy.ts
*
* NOW OPTIMIZED: Uses flattened conceptNames for O(log n) indexed queries!
* No more post-filtering - direct index lookup
*/
async resolve(brain: Brainy, vfs: VirtualFileSystem, conceptName: string): Promise<string[]> {
// Verify brain.find is a function (safety check)
if (typeof brain.find !== 'function') {
throw new Error('VERIFICATION FAILED: brain.find is not a function')
}
// Direct O(log n) query using flattened conceptNames array
// VFS automatically flattens concepts to conceptNames on write
const results = await brain.find({
where: {
vfsType: 'file',
conceptNames: { contains: conceptName } // Indexed array query
},
limit: 1000
})
return this.extractIds(results)
}
/**
* List all files with concept metadata
* Uses REAL Brainy.find() with metadata filter
*/
async list(brain: Brainy, vfs: VirtualFileSystem, limit = 100): Promise<VFSEntity[]> {
const results = await brain.find({
where: {
vfsType: 'file',
conceptNames: { exists: true } // Use flattened field
},
limit
})
// Convert to VFSEntity array
// VERIFIED: Result.entity exists in brainy.types.ts
return results.map(r => r.entity as VFSEntity)
}
}

View file

@ -0,0 +1,136 @@
/**
* Relationship Projection Strategy
*
* Maps relationship-based paths to files connected in the knowledge graph
* Uses EXISTING GraphAdjacencyIndex for O(1) traversal
*/
import { Brainy } from '../../../brainy.js'
import { VirtualFileSystem } from '../../VirtualFileSystem.js'
import { FindParams } from '../../../types/brainy.types.js'
import { VerbType } from '../../../types/graphTypes.js'
import { BaseProjectionStrategy } from '../ProjectionStrategy.js'
import { RelationshipValue } from '../SemanticPathParser.js'
/**
* Relationship Projection: /related-to/<path>/depth-N/types-X,Y
*
* Uses EXISTING infrastructure:
* - Brainy.getRelations() for graph traversal (REAL - line 803 in brainy.ts)
* - GraphAdjacencyIndex for O(1) neighbor lookups (REAL)
* - VerbType enum for relationship types (REAL - graphTypes.ts)
*/
export class RelationshipProjection extends BaseProjectionStrategy {
readonly name = 'relationship'
/**
* Convert relationship value to Brainy FindParams
* Note: Graph queries don't use FindParams, but we provide this for consistency
*/
toQuery(value: RelationshipValue, subpath?: string): FindParams {
// This is informational - actual resolution uses getRelations()
return {
where: {
vfsType: 'file'
},
connected: {
to: value.targetPath,
depth: value.depth || 1
},
limit: 1000
}
}
/**
* Resolve relationships using REAL Brainy.getRelations()
* Uses GraphAdjacencyIndex for O(1) graph traversal
*/
async resolve(brain: Brainy, vfs: VirtualFileSystem, value: RelationshipValue): Promise<string[]> {
// Step 1: Resolve target path to entity ID
const targetId = await this.resolvePathToId(vfs, value.targetPath)
if (!targetId) {
return []
}
// Step 2: Get relationships using REAL Brainy graph
const depth = value.depth || 1
const visited = new Set<string>()
const results: string[] = []
await this.traverseRelationships(
brain,
targetId,
depth,
visited,
results,
value.relationshipTypes
)
// Filter to only files
return await this.filterFiles(brain, results)
}
/**
* Recursive graph traversal using REAL Brainy.getRelations()
*/
private async traverseRelationships(
brain: Brainy,
entityId: string,
remainingDepth: number,
visited: Set<string>,
results: string[],
types?: string[]
): Promise<void> {
if (remainingDepth <= 0 || visited.has(entityId)) {
return
}
visited.add(entityId)
// Get outgoing relationships (REAL method - line 803 in brainy.ts)
const relations = await brain.getRelations({
from: entityId,
limit: 100
})
for (const relation of relations) {
// Filter by relationship type if specified
if (types && types.length > 0) {
const relationshipName = relation.type?.toLowerCase()
if (!types.some(t => t.toLowerCase() === relationshipName)) {
continue
}
}
// Add to results
if (!results.includes(relation.to)) {
results.push(relation.to)
}
// Recurse if depth remaining
if (remainingDepth > 1) {
await this.traverseRelationships(
brain,
relation.to,
remainingDepth - 1,
visited,
results,
types
)
}
}
}
/**
* Resolve path to entity ID
* Helper to convert traditional path to entity ID
*/
private async resolvePathToId(vfs: VirtualFileSystem, path: string): Promise<string | null> {
try {
// Use REAL VFS public method
return await vfs.resolvePath(path)
} catch {
return null
}
}
}

View file

@ -0,0 +1,84 @@
/**
* Similarity Projection Strategy
*
* Maps similarity-based paths to files with similar content
* Uses EXISTING HNSW Index for O(log n) vector similarity
*/
import { Brainy } from '../../../brainy.js'
import { VirtualFileSystem } from '../../VirtualFileSystem.js'
import { FindParams } from '../../../types/brainy.types.js'
import { BaseProjectionStrategy } from '../ProjectionStrategy.js'
import { SimilarityValue } from '../SemanticPathParser.js'
/**
* Similarity Projection: /similar-to/<path>/threshold-N
*
* Uses EXISTING infrastructure:
* - Brainy.similar() for vector similarity (REAL - line 680 in brainy.ts)
* - HNSW Index for O(log n) nearest neighbor search (REAL)
* - Cosine similarity for scoring (REAL)
*/
export class SimilarityProjection extends BaseProjectionStrategy {
readonly name = 'similar'
/**
* Convert similarity value to Brainy FindParams
* Note: Similarity uses brain.similar(), not find(), but we provide this for consistency
*/
toQuery(value: SimilarityValue, subpath?: string): FindParams {
// This is informational - actual resolution uses brain.similar()
return {
where: {
vfsType: 'file'
},
near: {
id: value.targetPath,
threshold: value.threshold || 0.7
},
limit: 50
}
}
/**
* Resolve similarity using REAL Brainy.similar()
* Uses HNSW Index for O(log n) vector search
*/
async resolve(brain: Brainy, vfs: VirtualFileSystem, value: SimilarityValue): Promise<string[]> {
// Step 1: Resolve target path to entity ID
const targetId = await this.resolvePathToId(vfs, value.targetPath)
if (!targetId) {
return []
}
// Step 2: Get target entity to use its vector
const targetEntity = await brain.get(targetId)
if (!targetEntity) {
return []
}
// Step 3: Find similar entities using REAL HNSW search
// VERIFIED: brain.similar() exists at line 680 in brainy.ts
const results = await brain.similar({
to: targetEntity,
threshold: value.threshold || 0.7,
limit: 50,
where: { vfsType: 'file' } // Only files
})
// Extract IDs
return this.extractIds(results)
}
/**
* Resolve path to entity ID
*/
private async resolvePathToId(vfs: VirtualFileSystem, path: string): Promise<string | null> {
try {
// Use REAL VFS public method
return await vfs.resolvePath(path)
} catch {
return null
}
}
}

View file

@ -0,0 +1,82 @@
/**
* Tag Projection Strategy
*
* Maps tag-based paths to files with those tags
* Uses EXISTING MetadataIndexManager for O(log n) queries
*/
import { Brainy } from '../../../brainy.js'
import { VirtualFileSystem } from '../../VirtualFileSystem.js'
import { FindParams } from '../../../types/brainy.types.js'
import { BaseProjectionStrategy } from '../ProjectionStrategy.js'
import { VFSEntity } from '../../types.js'
/**
* Tag Projection: /by-tag/<tagName>/<subpath>
*
* Uses EXISTING infrastructure:
* - Brainy.find() with metadata filters (REAL)
* - MetadataIndexManager for O(log n) tag queries (REAL)
* - VFSMetadata.tags field (REAL - types.ts line 66)
*/
export class TagProjection extends BaseProjectionStrategy {
readonly name = 'tag'
/**
* Convert tag name to Brainy FindParams
*/
toQuery(tagName: string, subpath?: string): FindParams {
const query: FindParams = {
where: {
vfsType: 'file',
tags: { contains: tagName } // BFO operator for array contains
},
limit: 1000
}
// Filter by filename if subpath specified
if (subpath) {
query.where = {
...query.where,
anyOf: [ // BFO logical operator
{ name: subpath },
{ path: { endsWith: subpath } } // BFO operator
]
}
}
return query
}
/**
* Resolve tag to entity IDs using REAL Brainy.find()
*/
async resolve(brain: Brainy, vfs: VirtualFileSystem, tagName: string): Promise<string[]> {
// Use REAL Brainy metadata filtering
const results = await brain.find({
where: {
vfsType: 'file',
tags: { contains: tagName } // BFO operator
},
limit: 1000
})
return this.extractIds(results)
}
/**
* List all files with tags
*/
async list(brain: Brainy, vfs: VirtualFileSystem, limit = 100): Promise<VFSEntity[]> {
// Get all files that have tags
const results = await brain.find({
where: {
vfsType: 'file',
tags: { exists: true } // BFO operator
},
limit
})
return results.map(r => r.entity as VFSEntity)
}
}

View file

@ -0,0 +1,103 @@
/**
* Temporal Projection Strategy
*
* Maps time-based paths to files modified at that time
* Uses EXISTING MetadataIndexManager with range queries
*/
import { Brainy } from '../../../brainy.js'
import { VirtualFileSystem } from '../../VirtualFileSystem.js'
import { FindParams } from '../../../types/brainy.types.js'
import { BaseProjectionStrategy } from '../ProjectionStrategy.js'
import { VFSEntity } from '../../types.js'
/**
* Temporal Projection: /as-of/<YYYY-MM-DD>/<subpath>
*
* Uses EXISTING infrastructure:
* - Brainy.find() with range queries (REAL)
* - MetadataIndexManager.$gte/$lte operators (REAL)
* - VFSMetadata.modified field (REAL - types.ts line 49)
*/
export class TemporalProjection extends BaseProjectionStrategy {
readonly name = 'time'
/**
* Convert date to Brainy FindParams with range query
*/
toQuery(date: Date, subpath?: string): FindParams {
// Get start and end of day (24-hour window)
const startOfDay = new Date(date)
startOfDay.setHours(0, 0, 0, 0)
const endOfDay = new Date(date)
endOfDay.setHours(23, 59, 59, 999)
const query: FindParams = {
where: {
vfsType: 'file',
modified: {
greaterEqual: startOfDay.getTime(), // BFO operator
lessEqual: endOfDay.getTime() // BFO operator
}
},
limit: 1000
}
// Filter by filename if subpath specified
if (subpath) {
query.where = {
...query.where,
anyOf: [ // BFO logical operator (not $or)
{ name: subpath },
{ path: { endsWith: subpath } } // BFO operator (not $regex)
]
}
}
return query
}
/**
* Resolve date to entity IDs using REAL Brainy.find()
* Uses MetadataIndexManager range queries for O(log n) performance
*/
async resolve(brain: Brainy, vfs: VirtualFileSystem, date: Date): Promise<string[]> {
const startOfDay = new Date(date)
startOfDay.setHours(0, 0, 0, 0)
const endOfDay = new Date(date)
endOfDay.setHours(23, 59, 59, 999)
// Use REAL Brainy metadata filtering with range operators
const results = await brain.find({
where: {
vfsType: 'file',
modified: {
greaterEqual: startOfDay.getTime(), // BFO operator
lessEqual: endOfDay.getTime() // BFO operator
}
},
limit: 1000
})
return this.extractIds(results)
}
/**
* List recently modified files
*/
async list(brain: Brainy, vfs: VirtualFileSystem, limit = 100): Promise<VFSEntity[]> {
const oneDayAgo = Date.now() - (24 * 60 * 60 * 1000)
const results = await brain.find({
where: {
vfsType: 'file',
modified: { greaterEqual: oneDayAgo } // BFO operator
},
limit
})
return results.map(r => r.entity as VFSEntity)
}
}

View file

@ -66,6 +66,7 @@ export interface VFSMetadata {
name: string
confidence: number
}>
conceptNames?: string[] // Flattened concept names for O(log n) indexed queries
todos?: VFSTodo[]
dependencies?: string[] // For code files - what they import
exports?: string[] // For code files - what they export
@ -343,16 +344,6 @@ export interface VFSConfig {
autoConcepts?: boolean // Auto-detect concepts
}
// Knowledge Layer - Optional revolutionary enhancement!
knowledgeLayer?: {
enabled?: boolean // Enable Knowledge Layer features
eventRecording?: boolean // Track all file operations
semanticVersioning?: boolean // Smart versioning based on meaning
persistentEntities?: boolean // Track evolving entities
concepts?: boolean // Universal concept system
gitBridge?: boolean // Git import/export support
}
// Permissions
permissions?: {
defaultFile?: number // Default file permissions (0o644)