@soulcraft/brainy 6.2.0 → 6.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,503 @@
1
+ /**
2
+ * Persistent Entity System for VFS
3
+ *
4
+ * Manages entities that evolve across files and time
5
+ * Not just story characters - any evolving entity: APIs, customers, services, models
6
+ * PRODUCTION-READY: Real implementation using Brainy
7
+ */
8
+ import { NounType, VerbType } from '../types/graphTypes.js';
9
+ import { cosineDistance } from '../utils/distance.js';
10
+ import { v4 as uuidv4 } from '../universal/uuid.js';
11
+ import { EntityManager } from './EntityManager.js';
12
+ /**
13
+ * Persistent Entity System
14
+ *
15
+ * Tracks entities that exist across multiple files and evolve over time
16
+ * Examples:
17
+ * - Story characters that appear in multiple chapters
18
+ * - API endpoints that evolve across documentation
19
+ * - Business entities that appear in multiple reports
20
+ * - Code classes/functions that span multiple files
21
+ */
22
+ export class PersistentEntitySystem extends EntityManager {
23
+ constructor(brain, config) {
24
+ super(brain, 'vfs-entity');
25
+ this.entityCache = new Map();
26
+ this.config = {
27
+ autoExtract: config?.autoExtract ?? false,
28
+ similarityThreshold: config?.similarityThreshold ?? 0.8,
29
+ maxAppearances: config?.maxAppearances ?? 100,
30
+ evolutionTracking: config?.evolutionTracking ?? true
31
+ };
32
+ }
33
+ /**
34
+ * Create a new persistent entity
35
+ */
36
+ async createEntity(entity) {
37
+ const entityId = uuidv4();
38
+ const timestamp = Date.now();
39
+ const persistentEntity = {
40
+ id: entityId,
41
+ name: entity.name,
42
+ type: entity.type,
43
+ description: entity.description,
44
+ aliases: entity.aliases,
45
+ attributes: entity.attributes,
46
+ created: timestamp,
47
+ lastUpdated: timestamp,
48
+ version: 1,
49
+ appearances: [],
50
+ entityType: 'persistent' // Add entityType for querying
51
+ };
52
+ // Generate embedding for entity description
53
+ let embedding;
54
+ try {
55
+ if (entity.description) {
56
+ embedding = await this.generateEntityEmbedding(persistentEntity);
57
+ }
58
+ }
59
+ catch (error) {
60
+ console.warn('Failed to generate entity embedding:', error);
61
+ }
62
+ // Store entity using EntityManager
63
+ await this.storeEntity(persistentEntity, NounType.Concept, embedding, Buffer.from(JSON.stringify(persistentEntity)));
64
+ // Update cache
65
+ this.entityCache.set(entityId, persistentEntity);
66
+ return entityId; // Return domain ID, not Brainy ID
67
+ }
68
+ /**
69
+ * Find an existing entity by name or attributes
70
+ */
71
+ async findEntity(query) {
72
+ const searchQuery = {};
73
+ if (query.name) {
74
+ // Search by exact name or aliases
75
+ searchQuery.$or = [
76
+ { name: query.name },
77
+ { aliases: { $in: [query.name] } }
78
+ ];
79
+ }
80
+ if (query.type) {
81
+ searchQuery.type = query.type;
82
+ }
83
+ if (query.attributes) {
84
+ for (const [key, value] of Object.entries(query.attributes)) {
85
+ searchQuery[`attributes.${key}`] = value;
86
+ }
87
+ }
88
+ // Add system metadata for EntityManager
89
+ searchQuery.entityType = 'persistent';
90
+ // Search using EntityManager
91
+ let results = await this.findEntities(searchQuery, NounType.Concept, 100);
92
+ // If searching for similar entities, use vector similarity
93
+ if (query.similar) {
94
+ try {
95
+ const queryEmbedding = await this.generateTextEmbedding(query.similar);
96
+ if (queryEmbedding) {
97
+ // Get all entities and rank by similarity using EntityManager
98
+ const allEntities = await this.findEntities({}, NounType.Concept, 1000);
99
+ const withSimilarity = allEntities
100
+ .filter(e => e.brainyId) // Only entities with brainyId have vectors
101
+ .map(async (e) => {
102
+ const brainyEntity = await this.brain.get(e.brainyId);
103
+ if (brainyEntity?.vector) {
104
+ return {
105
+ entity: e,
106
+ similarity: 1 - cosineDistance(queryEmbedding, brainyEntity.vector)
107
+ };
108
+ }
109
+ return null;
110
+ });
111
+ const resolved = (await Promise.all(withSimilarity))
112
+ .filter(s => s !== null && s.similarity > this.config.similarityThreshold)
113
+ .sort((a, b) => b.similarity - a.similarity);
114
+ results = resolved.map(s => s.entity);
115
+ }
116
+ }
117
+ catch (error) {
118
+ console.warn('Failed to perform similarity search:', error);
119
+ }
120
+ }
121
+ return results;
122
+ }
123
+ /**
124
+ * Record an appearance of an entity in a file
125
+ */
126
+ async recordAppearance(entityId, filePath, context, options) {
127
+ const entity = await this.getPersistentEntity(entityId);
128
+ if (!entity) {
129
+ throw new Error(`Entity ${entityId} not found`);
130
+ }
131
+ const appearanceId = uuidv4();
132
+ const timestamp = Date.now();
133
+ // Detect changes if enabled
134
+ let changes = [];
135
+ if (options?.extractChanges && this.config.evolutionTracking) {
136
+ changes = await this.detectChanges(entity, context, filePath);
137
+ }
138
+ const appearance = {
139
+ id: appearanceId,
140
+ entityId,
141
+ filePath,
142
+ context,
143
+ position: options?.position,
144
+ timestamp,
145
+ version: entity.version,
146
+ changes,
147
+ confidence: options?.confidence ?? 1.0
148
+ };
149
+ // Store appearance as managed entity (with eventType for appearances)
150
+ const appearanceWithEventType = {
151
+ ...appearance,
152
+ eventType: 'entity-appearance'
153
+ };
154
+ await this.storeEntity(appearanceWithEventType, NounType.Event, undefined, Buffer.from(context));
155
+ // Create relationship to entity using EntityManager
156
+ await this.createRelationship(appearanceId, entityId, VerbType.References);
157
+ // Update entity with new appearance
158
+ entity.appearances.push(appearance);
159
+ entity.lastUpdated = timestamp;
160
+ // Apply changes if any detected
161
+ if (changes.length > 0) {
162
+ entity.version++;
163
+ for (const change of changes) {
164
+ if (change.field in entity.attributes) {
165
+ entity.attributes[change.field] = change.newValue;
166
+ }
167
+ }
168
+ }
169
+ // Prune old appearances if needed
170
+ if (entity.appearances.length > this.config.maxAppearances) {
171
+ entity.appearances = entity.appearances
172
+ .sort((a, b) => b.timestamp - a.timestamp)
173
+ .slice(0, this.config.maxAppearances);
174
+ }
175
+ // Update stored entity
176
+ await this.updatePersistentEntity(entity);
177
+ return appearanceId;
178
+ }
179
+ /**
180
+ * Get entity evolution history
181
+ */
182
+ async getEvolution(entityId) {
183
+ const entity = await this.getPersistentEntity(entityId);
184
+ if (!entity) {
185
+ throw new Error(`Entity ${entityId} not found`);
186
+ }
187
+ // Get all appearances sorted by time
188
+ const appearances = entity.appearances.sort((a, b) => a.timestamp - b.timestamp);
189
+ // Build timeline
190
+ const timeline = [];
191
+ for (const appearance of appearances) {
192
+ if (appearance.changes && appearance.changes.length > 0) {
193
+ timeline.push({
194
+ timestamp: appearance.timestamp,
195
+ version: appearance.version,
196
+ changes: appearance.changes,
197
+ appearance
198
+ });
199
+ }
200
+ }
201
+ return { entity, timeline };
202
+ }
203
+ /**
204
+ * Find all appearances of an entity
205
+ */
206
+ async findAppearances(entityId, options) {
207
+ const query = {
208
+ entityId,
209
+ eventType: 'entity-appearance',
210
+ system: 'vfs-entity'
211
+ };
212
+ if (options?.filePath) {
213
+ query.filePath = options.filePath;
214
+ }
215
+ if (options?.since || options?.until) {
216
+ query.timestamp = {};
217
+ if (options.since)
218
+ query.timestamp.$gte = options.since;
219
+ if (options.until)
220
+ query.timestamp.$lte = options.until;
221
+ }
222
+ if (options?.minConfidence) {
223
+ query.confidence = { $gte: options.minConfidence };
224
+ }
225
+ const results = await this.brain.find({
226
+ where: query,
227
+ type: NounType.Event,
228
+ limit: 1000
229
+ });
230
+ return results
231
+ .map(r => r.entity.metadata)
232
+ .sort((a, b) => b.timestamp - a.timestamp);
233
+ }
234
+ /**
235
+ * Evolve an entity with new information
236
+ */
237
+ async evolveEntity(entityId, updates, source, reason) {
238
+ const entity = await this.getPersistentEntity(entityId);
239
+ if (!entity) {
240
+ throw new Error(`Entity ${entityId} not found`);
241
+ }
242
+ const timestamp = Date.now();
243
+ const changes = [];
244
+ // Track changes
245
+ for (const [field, newValue] of Object.entries(updates)) {
246
+ if (field in entity && entity[field] !== newValue) {
247
+ changes.push({
248
+ field,
249
+ oldValue: entity[field],
250
+ newValue,
251
+ timestamp,
252
+ source,
253
+ reason
254
+ });
255
+ }
256
+ }
257
+ // Apply updates
258
+ Object.assign(entity, updates);
259
+ entity.lastUpdated = timestamp;
260
+ entity.version++;
261
+ // Update stored entity
262
+ await this.updatePersistentEntity(entity);
263
+ // Record evolution event
264
+ if (changes.length > 0) {
265
+ await this.brain.add({
266
+ type: NounType.Event,
267
+ data: Buffer.from(JSON.stringify(changes)),
268
+ metadata: {
269
+ entityId,
270
+ changes,
271
+ timestamp,
272
+ source,
273
+ reason,
274
+ eventType: 'entity-evolution',
275
+ system: 'vfs-entity'
276
+ }
277
+ });
278
+ }
279
+ }
280
+ /**
281
+ * Extract entities from content (auto-extraction)
282
+ */
283
+ async extractEntities(filePath, content) {
284
+ if (!this.config.autoExtract) {
285
+ return [];
286
+ }
287
+ // Convert content to text for processing
288
+ const text = content.toString('utf8');
289
+ const entities = [];
290
+ // Simple entity extraction patterns
291
+ // In production, this would use NLP/ML models
292
+ const patterns = [
293
+ // Character names (capitalized words)
294
+ /\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*\b/g,
295
+ // API endpoints
296
+ /\/api\/[a-zA-Z0-9/\-_]+/g,
297
+ // Class names
298
+ /class\s+([A-Z][a-zA-Z0-9_]*)/g,
299
+ // Function names
300
+ /function\s+([a-zA-Z_][a-zA-Z0-9_]*)/g
301
+ ];
302
+ for (const pattern of patterns) {
303
+ const matches = text.matchAll(pattern);
304
+ for (const match of matches) {
305
+ const entityName = match[1] || match[0];
306
+ // Check if entity already exists
307
+ const existing = await this.findEntity({ name: entityName });
308
+ if (existing.length === 0) {
309
+ // Create new entity
310
+ const entityId = await this.createEntity({
311
+ name: entityName,
312
+ type: this.detectEntityType(entityName, text),
313
+ aliases: [],
314
+ attributes: {}
315
+ });
316
+ entities.push(entityId);
317
+ }
318
+ // Record appearance for existing or new entity
319
+ const entity = existing[0] || await this.getPersistentEntity(entities[entities.length - 1]);
320
+ if (entity) {
321
+ await this.recordAppearance(entity.id, filePath, this.extractContext(text, match.index || 0), { confidence: 0.7, extractChanges: true });
322
+ }
323
+ }
324
+ }
325
+ return entities;
326
+ }
327
+ /**
328
+ * Update references when a file moves
329
+ */
330
+ async updateReferences(oldPath, newPath) {
331
+ // Find all appearances in the old path
332
+ const results = await this.brain.find({
333
+ where: {
334
+ filePath: oldPath,
335
+ eventType: 'entity-appearance',
336
+ system: 'vfs-entity'
337
+ },
338
+ type: NounType.Event,
339
+ limit: 10000
340
+ });
341
+ // Update each appearance
342
+ for (const result of results) {
343
+ const appearance = result.entity.metadata;
344
+ appearance.filePath = newPath;
345
+ // Update the stored appearance
346
+ await this.brain.update({
347
+ id: result.entity.id,
348
+ metadata: appearance
349
+ });
350
+ }
351
+ // Update cache
352
+ for (const entity of this.entityCache.values()) {
353
+ for (const appearance of entity.appearances) {
354
+ if (appearance.filePath === oldPath) {
355
+ appearance.filePath = newPath;
356
+ }
357
+ }
358
+ }
359
+ }
360
+ /**
361
+ * Get persistent entity by ID
362
+ */
363
+ async getPersistentEntity(entityId) {
364
+ // Check cache first
365
+ if (this.entityCache.has(entityId)) {
366
+ return this.entityCache.get(entityId);
367
+ }
368
+ // Use parent getEntity method
369
+ const entity = await super.getEntity(entityId);
370
+ if (entity) {
371
+ // Cache and return
372
+ this.entityCache.set(entityId, entity);
373
+ }
374
+ return entity;
375
+ }
376
+ /**
377
+ * Update stored entity (rename to avoid parent method conflict)
378
+ */
379
+ async updatePersistentEntity(entity) {
380
+ // Find the Brainy entity
381
+ const results = await this.brain.find({
382
+ where: {
383
+ id: entity.id,
384
+ entityType: 'persistent',
385
+ system: 'vfs-entity'
386
+ },
387
+ type: NounType.Concept,
388
+ limit: 1
389
+ });
390
+ if (results.length > 0) {
391
+ await this.brain.update({
392
+ id: results[0].entity.id,
393
+ data: Buffer.from(JSON.stringify(entity)),
394
+ metadata: {
395
+ ...entity,
396
+ entityType: 'persistent',
397
+ system: 'vfs-entity'
398
+ }
399
+ });
400
+ }
401
+ // Update cache
402
+ this.entityCache.set(entity.id, entity);
403
+ }
404
+ /**
405
+ * Detect changes in entity from context
406
+ */
407
+ async detectChanges(entity, context, source) {
408
+ // Simple change detection - in production would use NLP
409
+ const changes = [];
410
+ const timestamp = Date.now();
411
+ // Look for attribute changes in context
412
+ const attributePatterns = [
413
+ /(\w+):\s*"([^"]+)"/g, // key: "value"
414
+ /(\w+)\s*=\s*"([^"]+)"/g, // key = "value"
415
+ /set(\w+)\("([^"]+)"\)/g // setProperty("value")
416
+ ];
417
+ for (const pattern of attributePatterns) {
418
+ const matches = context.matchAll(pattern);
419
+ for (const match of matches) {
420
+ const field = match[1].toLowerCase();
421
+ const newValue = match[2];
422
+ if (field in entity.attributes && entity.attributes[field] !== newValue) {
423
+ changes.push({
424
+ field: `attributes.${field}`,
425
+ oldValue: entity.attributes[field],
426
+ newValue,
427
+ timestamp,
428
+ source
429
+ });
430
+ }
431
+ }
432
+ }
433
+ return changes;
434
+ }
435
+ /**
436
+ * Generate embedding for entity
437
+ */
438
+ async generateEntityEmbedding(entity) {
439
+ try {
440
+ // Create text representation of entity
441
+ const text = [
442
+ entity.name,
443
+ entity.description || '',
444
+ entity.type,
445
+ ...entity.aliases,
446
+ JSON.stringify(entity.attributes)
447
+ ].join(' ');
448
+ return await this.generateTextEmbedding(text);
449
+ }
450
+ catch (error) {
451
+ console.error('Failed to generate entity embedding:', error);
452
+ return undefined;
453
+ }
454
+ }
455
+ /**
456
+ * Generate embedding for text
457
+ */
458
+ async generateTextEmbedding(text) {
459
+ try {
460
+ // Generate embedding using Brainy's embed method
461
+ const vector = await this.brain.embed(text);
462
+ return vector;
463
+ }
464
+ catch (error) {
465
+ console.debug('Failed to generate embedding:', error);
466
+ return undefined;
467
+ }
468
+ }
469
+ /**
470
+ * Detect entity type from name and context
471
+ */
472
+ detectEntityType(name, context) {
473
+ if (context.includes('class ' + name))
474
+ return 'class';
475
+ if (context.includes('function ' + name))
476
+ return 'function';
477
+ if (context.includes('/api/'))
478
+ return 'api';
479
+ if (/^[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*$/.test(name))
480
+ return 'person';
481
+ return 'entity';
482
+ }
483
+ /**
484
+ * Extract context around a position
485
+ */
486
+ extractContext(text, position, radius = 100) {
487
+ const start = Math.max(0, position - radius);
488
+ const end = Math.min(text.length, position + radius);
489
+ return text.slice(start, end);
490
+ }
491
+ /**
492
+ * Clear entity cache
493
+ */
494
+ clearCache(entityId) {
495
+ if (entityId) {
496
+ this.entityCache.delete(entityId);
497
+ }
498
+ else {
499
+ this.entityCache.clear();
500
+ }
501
+ }
502
+ }
503
+ //# sourceMappingURL=PersistentEntitySystem.js.map
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Semantic Versioning System for VFS
3
+ *
4
+ * Only creates versions when the MEANING of content changes significantly
5
+ * PRODUCTION-READY: Real implementation using embeddings
6
+ */
7
+ import { Brainy } from '../brainy.js';
8
+ import { EntityManager, ManagedEntity } from './EntityManager.js';
9
+ /**
10
+ * Version metadata
11
+ */
12
+ export interface Version extends ManagedEntity {
13
+ id: string;
14
+ path: string;
15
+ version: number;
16
+ timestamp: number;
17
+ hash: string;
18
+ size: number;
19
+ semanticHash?: string;
20
+ author?: string;
21
+ message?: string;
22
+ parentVersion?: string;
23
+ }
24
+ /**
25
+ * Semantic versioning configuration
26
+ */
27
+ export interface SemanticVersioningConfig {
28
+ threshold?: number;
29
+ maxVersions?: number;
30
+ minInterval?: number;
31
+ sizeChangeThreshold?: number;
32
+ }
33
+ /**
34
+ * Semantic Versioning System
35
+ *
36
+ * Creates versions only when content meaning changes significantly
37
+ * Uses vector embeddings to detect semantic changes
38
+ */
39
+ export declare class SemanticVersioning extends EntityManager {
40
+ private config;
41
+ private versionCache;
42
+ constructor(brain: Brainy, config?: SemanticVersioningConfig);
43
+ /**
44
+ * Check if content has changed enough to warrant a new version
45
+ */
46
+ shouldVersion(oldContent: Buffer, newContent: Buffer): Promise<boolean>;
47
+ /**
48
+ * Create a new version
49
+ */
50
+ createVersion(path: string, content: Buffer, metadata?: {
51
+ author?: string;
52
+ message?: string;
53
+ }): Promise<string>;
54
+ /**
55
+ * Get all versions for a file
56
+ */
57
+ getVersions(path: string): Promise<Version[]>;
58
+ /**
59
+ * Get a specific version's content
60
+ */
61
+ getVersion(path: string, versionId: string): Promise<Buffer | null>;
62
+ /**
63
+ * Restore a file to a specific version
64
+ */
65
+ restoreVersion(path: string, versionId: string): Promise<Buffer | null>;
66
+ /**
67
+ * Get version history with diffs
68
+ */
69
+ getVersionHistory(path: string, limit?: number): Promise<Array<{
70
+ version: Version;
71
+ changes?: {
72
+ additions: number;
73
+ deletions: number;
74
+ semanticChange: number;
75
+ };
76
+ }>>;
77
+ /**
78
+ * Prune old versions beyond the limit
79
+ */
80
+ private pruneVersions;
81
+ /**
82
+ * Calculate semantic distance between two pieces of content
83
+ */
84
+ private calculateSemanticDistance;
85
+ /**
86
+ * Generate embedding for content
87
+ */
88
+ private generateEmbedding;
89
+ /**
90
+ * Hash content for quick comparison
91
+ */
92
+ private hashContent;
93
+ /**
94
+ * Hash embedding for quick comparison
95
+ */
96
+ private hashEmbedding;
97
+ /**
98
+ * Estimate semantic change from hashes (rough approximation)
99
+ */
100
+ private estimateSemanticChange;
101
+ /**
102
+ * Clear version cache for a file
103
+ */
104
+ clearCache(path?: string): void;
105
+ }