@danielsimonjr/memory-mcp 0.47.1 → 9.8.0

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.
Files changed (207) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +2000 -194
  3. package/dist/__tests__/file-path.test.js +5 -5
  4. package/dist/__tests__/knowledge-graph.test.js +3 -8
  5. package/dist/core/EntityManager.d.ts +266 -0
  6. package/dist/core/EntityManager.d.ts.map +1 -0
  7. package/dist/core/EntityManager.js +85 -133
  8. package/dist/core/GraphEventEmitter.d.ts +202 -0
  9. package/dist/core/GraphEventEmitter.d.ts.map +1 -0
  10. package/dist/core/GraphEventEmitter.js +346 -0
  11. package/dist/core/GraphStorage.d.ts +395 -0
  12. package/dist/core/GraphStorage.d.ts.map +1 -0
  13. package/dist/core/GraphStorage.js +643 -31
  14. package/dist/core/GraphTraversal.d.ts +141 -0
  15. package/dist/core/GraphTraversal.d.ts.map +1 -0
  16. package/dist/core/GraphTraversal.js +573 -0
  17. package/dist/core/HierarchyManager.d.ts +111 -0
  18. package/dist/core/HierarchyManager.d.ts.map +1 -0
  19. package/dist/{features → core}/HierarchyManager.js +14 -9
  20. package/dist/core/ManagerContext.d.ts +72 -0
  21. package/dist/core/ManagerContext.d.ts.map +1 -0
  22. package/dist/core/ManagerContext.js +118 -0
  23. package/dist/core/ObservationManager.d.ts +85 -0
  24. package/dist/core/ObservationManager.d.ts.map +1 -0
  25. package/dist/core/ObservationManager.js +51 -57
  26. package/dist/core/RelationManager.d.ts +131 -0
  27. package/dist/core/RelationManager.d.ts.map +1 -0
  28. package/dist/core/RelationManager.js +31 -7
  29. package/dist/core/SQLiteStorage.d.ts +354 -0
  30. package/dist/core/SQLiteStorage.d.ts.map +1 -0
  31. package/dist/core/SQLiteStorage.js +917 -0
  32. package/dist/core/StorageFactory.d.ts +45 -0
  33. package/dist/core/StorageFactory.d.ts.map +1 -0
  34. package/dist/core/StorageFactory.js +64 -0
  35. package/dist/core/TransactionManager.d.ts +464 -0
  36. package/dist/core/TransactionManager.d.ts.map +1 -0
  37. package/dist/core/TransactionManager.js +490 -13
  38. package/dist/core/index.d.ts +17 -0
  39. package/dist/core/index.d.ts.map +1 -0
  40. package/dist/core/index.js +12 -2
  41. package/dist/features/AnalyticsManager.d.ts +44 -0
  42. package/dist/features/AnalyticsManager.d.ts.map +1 -0
  43. package/dist/features/AnalyticsManager.js +14 -13
  44. package/dist/features/ArchiveManager.d.ts +133 -0
  45. package/dist/features/ArchiveManager.d.ts.map +1 -0
  46. package/dist/features/ArchiveManager.js +221 -14
  47. package/dist/features/CompressionManager.d.ts +117 -0
  48. package/dist/features/CompressionManager.d.ts.map +1 -0
  49. package/dist/features/CompressionManager.js +189 -20
  50. package/dist/features/IOManager.d.ts +225 -0
  51. package/dist/features/IOManager.d.ts.map +1 -0
  52. package/dist/features/IOManager.js +1041 -0
  53. package/dist/features/StreamingExporter.d.ts +123 -0
  54. package/dist/features/StreamingExporter.d.ts.map +1 -0
  55. package/dist/features/StreamingExporter.js +203 -0
  56. package/dist/features/TagManager.d.ts +147 -0
  57. package/dist/features/TagManager.d.ts.map +1 -0
  58. package/dist/features/index.d.ts +12 -0
  59. package/dist/features/index.d.ts.map +1 -0
  60. package/dist/features/index.js +5 -6
  61. package/dist/index.d.ts +9 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +12 -45
  64. package/dist/memory.jsonl +1 -18
  65. package/dist/search/BasicSearch.d.ts +51 -0
  66. package/dist/search/BasicSearch.d.ts.map +1 -0
  67. package/dist/search/BasicSearch.js +9 -3
  68. package/dist/search/BooleanSearch.d.ts +98 -0
  69. package/dist/search/BooleanSearch.d.ts.map +1 -0
  70. package/dist/search/BooleanSearch.js +156 -9
  71. package/dist/search/EmbeddingService.d.ts +178 -0
  72. package/dist/search/EmbeddingService.d.ts.map +1 -0
  73. package/dist/search/EmbeddingService.js +358 -0
  74. package/dist/search/FuzzySearch.d.ts +118 -0
  75. package/dist/search/FuzzySearch.d.ts.map +1 -0
  76. package/dist/search/FuzzySearch.js +241 -25
  77. package/dist/search/QueryCostEstimator.d.ts +111 -0
  78. package/dist/search/QueryCostEstimator.d.ts.map +1 -0
  79. package/dist/search/QueryCostEstimator.js +355 -0
  80. package/dist/search/RankedSearch.d.ts +71 -0
  81. package/dist/search/RankedSearch.d.ts.map +1 -0
  82. package/dist/search/RankedSearch.js +54 -6
  83. package/dist/search/SavedSearchManager.d.ts +79 -0
  84. package/dist/search/SavedSearchManager.d.ts.map +1 -0
  85. package/dist/search/SearchFilterChain.d.ts +120 -0
  86. package/dist/search/SearchFilterChain.d.ts.map +1 -0
  87. package/dist/search/SearchFilterChain.js +2 -4
  88. package/dist/search/SearchManager.d.ts +326 -0
  89. package/dist/search/SearchManager.d.ts.map +1 -0
  90. package/dist/search/SearchManager.js +148 -0
  91. package/dist/search/SearchSuggestions.d.ts +27 -0
  92. package/dist/search/SearchSuggestions.d.ts.map +1 -0
  93. package/dist/search/SearchSuggestions.js +1 -1
  94. package/dist/search/SemanticSearch.d.ts +149 -0
  95. package/dist/search/SemanticSearch.d.ts.map +1 -0
  96. package/dist/search/SemanticSearch.js +323 -0
  97. package/dist/search/TFIDFEventSync.d.ts +85 -0
  98. package/dist/search/TFIDFEventSync.d.ts.map +1 -0
  99. package/dist/search/TFIDFEventSync.js +133 -0
  100. package/dist/search/TFIDFIndexManager.d.ts +151 -0
  101. package/dist/search/TFIDFIndexManager.d.ts.map +1 -0
  102. package/dist/search/TFIDFIndexManager.js +232 -17
  103. package/dist/search/VectorStore.d.ts +235 -0
  104. package/dist/search/VectorStore.d.ts.map +1 -0
  105. package/dist/search/VectorStore.js +311 -0
  106. package/dist/search/index.d.ts +21 -0
  107. package/dist/search/index.d.ts.map +1 -0
  108. package/dist/search/index.js +12 -0
  109. package/dist/server/MCPServer.d.ts +21 -0
  110. package/dist/server/MCPServer.d.ts.map +1 -0
  111. package/dist/server/MCPServer.js +4 -4
  112. package/dist/server/responseCompressor.d.ts +94 -0
  113. package/dist/server/responseCompressor.d.ts.map +1 -0
  114. package/dist/server/responseCompressor.js +127 -0
  115. package/dist/server/toolDefinitions.d.ts +27 -0
  116. package/dist/server/toolDefinitions.d.ts.map +1 -0
  117. package/dist/server/toolDefinitions.js +189 -18
  118. package/dist/server/toolHandlers.d.ts +41 -0
  119. package/dist/server/toolHandlers.d.ts.map +1 -0
  120. package/dist/server/toolHandlers.js +467 -75
  121. package/dist/types/index.d.ts +13 -0
  122. package/dist/types/index.d.ts.map +1 -0
  123. package/dist/types/index.js +1 -1
  124. package/dist/types/types.d.ts +1654 -0
  125. package/dist/types/types.d.ts.map +1 -0
  126. package/dist/types/types.js +9 -0
  127. package/dist/utils/compressedCache.d.ts +192 -0
  128. package/dist/utils/compressedCache.d.ts.map +1 -0
  129. package/dist/utils/compressedCache.js +309 -0
  130. package/dist/utils/compressionUtil.d.ts +214 -0
  131. package/dist/utils/compressionUtil.d.ts.map +1 -0
  132. package/dist/utils/compressionUtil.js +247 -0
  133. package/dist/utils/constants.d.ts +245 -0
  134. package/dist/utils/constants.d.ts.map +1 -0
  135. package/dist/utils/constants.js +124 -0
  136. package/dist/utils/entityUtils.d.ts +321 -0
  137. package/dist/utils/entityUtils.d.ts.map +1 -0
  138. package/dist/utils/entityUtils.js +434 -4
  139. package/dist/utils/errors.d.ts +95 -0
  140. package/dist/utils/errors.d.ts.map +1 -0
  141. package/dist/utils/errors.js +24 -0
  142. package/dist/utils/formatters.d.ts +145 -0
  143. package/dist/utils/formatters.d.ts.map +1 -0
  144. package/dist/utils/{paginationUtils.js → formatters.js} +54 -3
  145. package/dist/utils/index.d.ts +23 -0
  146. package/dist/utils/index.d.ts.map +1 -0
  147. package/dist/utils/index.js +69 -31
  148. package/dist/utils/indexes.d.ts +270 -0
  149. package/dist/utils/indexes.d.ts.map +1 -0
  150. package/dist/utils/indexes.js +526 -0
  151. package/dist/utils/logger.d.ts +24 -0
  152. package/dist/utils/logger.d.ts.map +1 -0
  153. package/dist/utils/operationUtils.d.ts +124 -0
  154. package/dist/utils/operationUtils.d.ts.map +1 -0
  155. package/dist/utils/operationUtils.js +175 -0
  156. package/dist/utils/parallelUtils.d.ts +72 -0
  157. package/dist/utils/parallelUtils.d.ts.map +1 -0
  158. package/dist/utils/parallelUtils.js +169 -0
  159. package/dist/utils/schemas.d.ts +374 -0
  160. package/dist/utils/schemas.d.ts.map +1 -0
  161. package/dist/utils/schemas.js +302 -2
  162. package/dist/utils/searchAlgorithms.d.ts +99 -0
  163. package/dist/utils/searchAlgorithms.d.ts.map +1 -0
  164. package/dist/utils/searchAlgorithms.js +167 -0
  165. package/dist/utils/searchCache.d.ts +108 -0
  166. package/dist/utils/searchCache.d.ts.map +1 -0
  167. package/dist/utils/taskScheduler.d.ts +290 -0
  168. package/dist/utils/taskScheduler.d.ts.map +1 -0
  169. package/dist/utils/taskScheduler.js +466 -0
  170. package/dist/workers/index.d.ts +12 -0
  171. package/dist/workers/index.d.ts.map +1 -0
  172. package/dist/workers/index.js +9 -0
  173. package/dist/workers/levenshteinWorker.d.ts +60 -0
  174. package/dist/workers/levenshteinWorker.d.ts.map +1 -0
  175. package/dist/workers/levenshteinWorker.js +98 -0
  176. package/package.json +17 -4
  177. package/dist/__tests__/edge-cases/edge-cases.test.js +0 -406
  178. package/dist/__tests__/integration/workflows.test.js +0 -449
  179. package/dist/__tests__/performance/benchmarks.test.js +0 -413
  180. package/dist/__tests__/unit/core/EntityManager.test.js +0 -334
  181. package/dist/__tests__/unit/core/GraphStorage.test.js +0 -205
  182. package/dist/__tests__/unit/core/RelationManager.test.js +0 -274
  183. package/dist/__tests__/unit/features/CompressionManager.test.js +0 -350
  184. package/dist/__tests__/unit/search/BasicSearch.test.js +0 -311
  185. package/dist/__tests__/unit/search/BooleanSearch.test.js +0 -432
  186. package/dist/__tests__/unit/search/FuzzySearch.test.js +0 -448
  187. package/dist/__tests__/unit/search/RankedSearch.test.js +0 -379
  188. package/dist/__tests__/unit/utils/levenshtein.test.js +0 -77
  189. package/dist/core/KnowledgeGraphManager.js +0 -423
  190. package/dist/features/BackupManager.js +0 -311
  191. package/dist/features/ExportManager.js +0 -305
  192. package/dist/features/ImportExportManager.js +0 -50
  193. package/dist/features/ImportManager.js +0 -328
  194. package/dist/types/analytics.types.js +0 -6
  195. package/dist/types/entity.types.js +0 -7
  196. package/dist/types/import-export.types.js +0 -7
  197. package/dist/types/search.types.js +0 -7
  198. package/dist/types/tag.types.js +0 -6
  199. package/dist/utils/dateUtils.js +0 -89
  200. package/dist/utils/filterUtils.js +0 -155
  201. package/dist/utils/levenshtein.js +0 -62
  202. package/dist/utils/pathUtils.js +0 -115
  203. package/dist/utils/responseFormatter.js +0 -55
  204. package/dist/utils/tagUtils.js +0 -107
  205. package/dist/utils/tfidf.js +0 -90
  206. package/dist/utils/validationHelper.js +0 -99
  207. package/dist/utils/validationUtils.js +0 -109
@@ -2,20 +2,23 @@
2
2
  * Entity Manager
3
3
  *
4
4
  * Handles CRUD operations for entities in the knowledge graph.
5
+ * Focused on core entity and tag operations only (Phase 4: Consolidate God Objects).
5
6
  *
6
7
  * @module core/EntityManager
7
8
  */
8
9
  import { EntityNotFoundError, InvalidImportanceError, ValidationError } from '../utils/errors.js';
9
- import { BatchCreateEntitiesSchema, UpdateEntitySchema, EntityNamesSchema } from '../utils/index.js';
10
+ import { BatchCreateEntitiesSchema, UpdateEntitySchema, EntityNamesSchema, checkCancellation, createProgressReporter, createProgress, } from '../utils/index.js';
10
11
  import { GRAPH_LIMITS } from '../utils/constants.js';
11
12
  /**
12
13
  * Minimum importance value (least important).
14
+ * Note: Use IMPORTANCE_RANGE from constants.ts for external access.
13
15
  */
14
- export const MIN_IMPORTANCE = 0;
16
+ const MIN_IMPORTANCE = 0;
15
17
  /**
16
18
  * Maximum importance value (most important).
19
+ * Note: Use IMPORTANCE_RANGE from constants.ts for external access.
17
20
  */
18
- export const MAX_IMPORTANCE = 10;
21
+ const MAX_IMPORTANCE = 10;
19
22
  /**
20
23
  * Manages entity operations with automatic timestamp handling.
21
24
  */
@@ -33,9 +36,13 @@ export class EntityManager {
33
36
  * - Normalizes all tags to lowercase for consistent searching
34
37
  * - Validates importance values (must be between 0-10)
35
38
  *
39
+ * Phase 9B: Supports progress tracking and cancellation via LongRunningOperationOptions.
40
+ *
36
41
  * @param entities - Array of entities to create. Each entity must have a unique name.
42
+ * @param options - Optional progress/cancellation options (Phase 9B)
37
43
  * @returns Promise resolving to array of newly created entities (excludes duplicates)
38
44
  * @throws {InvalidImportanceError} If any entity has importance outside the valid range [0-10]
45
+ * @throws {OperationCancelledError} If operation is cancelled via signal (Phase 9B)
39
46
  *
40
47
  * @example
41
48
  * ```typescript
@@ -55,24 +62,43 @@ export class EntityManager {
55
62
  * { name: 'Bob', entityType: 'person', observations: [] },
56
63
  * { name: 'Charlie', entityType: 'person', observations: [] }
57
64
  * ]);
65
+ *
66
+ * // With progress tracking and cancellation (Phase 9B)
67
+ * const controller = new AbortController();
68
+ * const results = await manager.createEntities(largeEntityArray, {
69
+ * signal: controller.signal,
70
+ * onProgress: (p) => console.log(`${p.percentage}% complete`),
71
+ * });
58
72
  * ```
59
73
  */
60
- async createEntities(entities) {
74
+ async createEntities(entities, options) {
75
+ // Check for early cancellation
76
+ checkCancellation(options?.signal, 'createEntities');
61
77
  // Validate input
62
78
  const validation = BatchCreateEntitiesSchema.safeParse(entities);
63
79
  if (!validation.success) {
64
80
  const errors = validation.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`);
65
81
  throw new ValidationError('Invalid entity data', errors);
66
82
  }
67
- const graph = await this.storage.loadGraph();
83
+ // Setup progress reporter
84
+ const reportProgress = createProgressReporter(options?.onProgress);
85
+ const total = entities.length;
86
+ reportProgress?.(createProgress(0, total, 'createEntities'));
87
+ // Use read-only graph for checking existing entities
88
+ const readGraph = await this.storage.loadGraph();
68
89
  const timestamp = new Date().toISOString();
69
90
  // Check graph size limits
70
- const entitiesToAdd = entities.filter(e => !graph.entities.some(existing => existing.name === e.name));
71
- if (graph.entities.length + entitiesToAdd.length > GRAPH_LIMITS.MAX_ENTITIES) {
91
+ const entitiesToAdd = entities.filter(e => !readGraph.entities.some(existing => existing.name === e.name));
92
+ if (readGraph.entities.length + entitiesToAdd.length > GRAPH_LIMITS.MAX_ENTITIES) {
72
93
  throw new ValidationError('Graph size limit exceeded', [`Adding ${entitiesToAdd.length} entities would exceed maximum of ${GRAPH_LIMITS.MAX_ENTITIES} entities`]);
73
94
  }
74
- const newEntities = entitiesToAdd
75
- .map(e => {
95
+ // Check for cancellation before processing
96
+ checkCancellation(options?.signal, 'createEntities');
97
+ const newEntities = [];
98
+ let processed = 0;
99
+ for (const e of entitiesToAdd) {
100
+ // Check for cancellation periodically
101
+ checkCancellation(options?.signal, 'createEntities');
76
102
  const entity = {
77
103
  ...e,
78
104
  createdAt: e.createdAt || timestamp,
@@ -89,10 +115,22 @@ export class EntityManager {
89
115
  }
90
116
  entity.importance = e.importance;
91
117
  }
92
- return entity;
93
- });
94
- graph.entities.push(...newEntities);
95
- await this.storage.saveGraph(graph);
118
+ newEntities.push(entity);
119
+ processed++;
120
+ reportProgress?.(createProgress(processed, entitiesToAdd.length, 'createEntities'));
121
+ }
122
+ // OPTIMIZED: Use append for single entity, bulk save for multiple
123
+ // (N individual appends is slower than one bulk write)
124
+ if (newEntities.length === 1) {
125
+ await this.storage.appendEntity(newEntities[0]);
126
+ }
127
+ else if (newEntities.length > 1) {
128
+ const graph = await this.storage.getGraphForMutation();
129
+ graph.entities.push(...newEntities);
130
+ await this.storage.saveGraph(graph);
131
+ }
132
+ // Report completion
133
+ reportProgress?.(createProgress(entitiesToAdd.length, entitiesToAdd.length, 'createEntities'));
96
134
  return newEntities;
97
135
  }
98
136
  /**
@@ -127,9 +165,11 @@ export class EntityManager {
127
165
  const errors = validation.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`);
128
166
  throw new ValidationError('Invalid entity names', errors);
129
167
  }
130
- const graph = await this.storage.loadGraph();
131
- graph.entities = graph.entities.filter(e => !entityNames.includes(e.name));
132
- graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to));
168
+ const graph = await this.storage.getGraphForMutation();
169
+ // OPTIMIZED: Use Set for O(1) lookups instead of O(n) includes()
170
+ const namesToDelete = new Set(entityNames);
171
+ graph.entities = graph.entities.filter(e => !namesToDelete.has(e.name));
172
+ graph.relations = graph.relations.filter(r => !namesToDelete.has(r.from) && !namesToDelete.has(r.to));
133
173
  await this.storage.saveGraph(graph);
134
174
  }
135
175
  /**
@@ -205,7 +245,7 @@ export class EntityManager {
205
245
  const errors = validation.error.issues.map((e) => `${e.path.join('.')}: ${e.message}`);
206
246
  throw new ValidationError('Invalid update data', errors);
207
247
  }
208
- const graph = await this.storage.loadGraph();
248
+ const graph = await this.storage.getGraphForMutation();
209
249
  const entity = graph.entities.find(e => e.name === name);
210
250
  if (!entity) {
211
251
  throw new EntityNotFoundError(name);
@@ -258,14 +298,18 @@ export class EntityManager {
258
298
  throw new ValidationError('Invalid update data', errors);
259
299
  }
260
300
  }
261
- const graph = await this.storage.loadGraph();
301
+ const graph = await this.storage.getGraphForMutation();
262
302
  const timestamp = new Date().toISOString();
263
303
  const updatedEntities = [];
304
+ // OPTIMIZED: Build Map for O(1) lookups instead of O(n) find() per update
305
+ const entityIndex = new Map();
306
+ graph.entities.forEach((e, i) => entityIndex.set(e.name, i));
264
307
  for (const { name, updates: updateData } of updates) {
265
- const entity = graph.entities.find(e => e.name === name);
266
- if (!entity) {
308
+ const idx = entityIndex.get(name);
309
+ if (idx === undefined) {
267
310
  throw new EntityNotFoundError(name);
268
311
  }
312
+ const entity = graph.entities[idx];
269
313
  // Apply updates
270
314
  Object.assign(entity, updateData);
271
315
  entity.lastModified = timestamp;
@@ -274,96 +318,9 @@ export class EntityManager {
274
318
  await this.storage.saveGraph(graph);
275
319
  return updatedEntities;
276
320
  }
277
- /**
278
- * Add observations to multiple entities in a single batch operation.
279
- *
280
- * This method performs the following operations:
281
- * - Adds new observations to specified entities
282
- * - Filters out duplicate observations (already present)
283
- * - Updates lastModified timestamp only if new observations were added
284
- *
285
- * @param observations - Array of entity names and observations to add
286
- * @returns Promise resolving to array of results showing which observations were added
287
- * @throws {EntityNotFoundError} If any entity is not found
288
- *
289
- * @example
290
- * ```typescript
291
- * const manager = new EntityManager(storage);
292
- *
293
- * // Add observations to multiple entities
294
- * const results = await manager.addObservations([
295
- * { entityName: 'Alice', contents: ['Completed project X', 'Started project Y'] },
296
- * { entityName: 'Bob', contents: ['Joined team meeting'] }
297
- * ]);
298
- *
299
- * // Check what was added (duplicates are filtered out)
300
- * results.forEach(r => {
301
- * console.log(`${r.entityName}: added ${r.addedObservations.length} new observations`);
302
- * });
303
- * ```
304
- */
305
- async addObservations(observations) {
306
- const graph = await this.storage.loadGraph();
307
- const timestamp = new Date().toISOString();
308
- const results = observations.map(o => {
309
- const entity = graph.entities.find(e => e.name === o.entityName);
310
- if (!entity) {
311
- throw new EntityNotFoundError(o.entityName);
312
- }
313
- const newObservations = o.contents.filter(content => !entity.observations.includes(content));
314
- entity.observations.push(...newObservations);
315
- // Update lastModified timestamp if observations were added
316
- if (newObservations.length > 0) {
317
- entity.lastModified = timestamp;
318
- }
319
- return { entityName: o.entityName, addedObservations: newObservations };
320
- });
321
- await this.storage.saveGraph(graph);
322
- return results;
323
- }
324
- /**
325
- * Delete observations from multiple entities in a single batch operation.
326
- *
327
- * This method performs the following operations:
328
- * - Removes specified observations from entities
329
- * - Updates lastModified timestamp only if observations were deleted
330
- * - Silently ignores entities that don't exist (no error thrown)
331
- *
332
- * @param deletions - Array of entity names and observations to delete
333
- * @returns Promise that resolves when deletion is complete
334
- *
335
- * @example
336
- * ```typescript
337
- * const manager = new EntityManager(storage);
338
- *
339
- * // Delete observations from multiple entities
340
- * await manager.deleteObservations([
341
- * { entityName: 'Alice', observations: ['Old observation 1', 'Old observation 2'] },
342
- * { entityName: 'Bob', observations: ['Outdated info'] }
343
- * ]);
344
- *
345
- * // Safe to delete from non-existent entities (no error)
346
- * await manager.deleteObservations([
347
- * { entityName: 'NonExistent', observations: ['Some text'] }
348
- * ]); // No error thrown
349
- * ```
350
- */
351
- async deleteObservations(deletions) {
352
- const graph = await this.storage.loadGraph();
353
- const timestamp = new Date().toISOString();
354
- deletions.forEach(d => {
355
- const entity = graph.entities.find(e => e.name === d.entityName);
356
- if (entity) {
357
- const originalLength = entity.observations.length;
358
- entity.observations = entity.observations.filter(o => !d.observations.includes(o));
359
- // Update lastModified timestamp if observations were deleted
360
- if (entity.observations.length < originalLength) {
361
- entity.lastModified = timestamp;
362
- }
363
- }
364
- });
365
- await this.storage.saveGraph(graph);
366
- }
321
+ // ============================================================
322
+ // TAG OPERATIONS
323
+ // ============================================================
367
324
  /**
368
325
  * Add tags to an entity.
369
326
  *
@@ -375,25 +332,20 @@ export class EntityManager {
375
332
  * @throws {EntityNotFoundError} If entity is not found
376
333
  */
377
334
  async addTags(entityName, tags) {
378
- const graph = await this.storage.loadGraph();
379
- const timestamp = new Date().toISOString();
380
- const entity = graph.entities.find(e => e.name === entityName);
335
+ // OPTIMIZED: Use O(1) NameIndex lookup instead of loadGraph() + O(n) find()
336
+ const entity = this.storage.getEntityByName(entityName);
381
337
  if (!entity) {
382
338
  throw new EntityNotFoundError(entityName);
383
339
  }
384
340
  // Initialize tags array if it doesn't exist
385
- if (!entity.tags) {
386
- entity.tags = [];
387
- }
341
+ const existingTags = entity.tags || [];
388
342
  // Normalize tags to lowercase and filter out duplicates
389
343
  const normalizedTags = tags.map(tag => tag.toLowerCase());
390
- const newTags = normalizedTags.filter(tag => !entity.tags.includes(tag));
391
- entity.tags.push(...newTags);
392
- // Update lastModified timestamp if tags were added
344
+ const newTags = normalizedTags.filter(tag => !existingTags.includes(tag));
393
345
  if (newTags.length > 0) {
394
- entity.lastModified = timestamp;
346
+ // OPTIMIZED: Use updateEntity for in-place update + append
347
+ await this.storage.updateEntity(entityName, { tags: [...existingTags, ...newTags] });
395
348
  }
396
- await this.storage.saveGraph(graph);
397
349
  return { entityName, addedTags: newTags };
398
350
  }
399
351
  /**
@@ -405,7 +357,7 @@ export class EntityManager {
405
357
  * @throws {EntityNotFoundError} If entity is not found
406
358
  */
407
359
  async removeTags(entityName, tags) {
408
- const graph = await this.storage.loadGraph();
360
+ const graph = await this.storage.getGraphForMutation();
409
361
  const timestamp = new Date().toISOString();
410
362
  const entity = graph.entities.find(e => e.name === entityName);
411
363
  if (!entity) {
@@ -417,10 +369,12 @@ export class EntityManager {
417
369
  // Normalize tags to lowercase
418
370
  const normalizedTags = tags.map(tag => tag.toLowerCase());
419
371
  const originalLength = entity.tags.length;
372
+ // Capture existing tags (lowercase) BEFORE filtering to accurately track removals
373
+ const existingTagsLower = entity.tags.map(t => t.toLowerCase());
420
374
  // Filter out the tags to remove
421
375
  entity.tags = entity.tags.filter(tag => !normalizedTags.includes(tag.toLowerCase()));
422
- const removedTags = normalizedTags.filter(tag => originalLength > entity.tags.length ||
423
- !entity.tags.map(t => t.toLowerCase()).includes(tag));
376
+ // A tag was removed if it existed in the original tags
377
+ const removedTags = normalizedTags.filter(tag => existingTagsLower.includes(tag));
424
378
  // Update lastModified timestamp if tags were removed
425
379
  if (entity.tags.length < originalLength) {
426
380
  entity.lastModified = timestamp;
@@ -438,19 +392,17 @@ export class EntityManager {
438
392
  * @throws {Error} If importance is out of range
439
393
  */
440
394
  async setImportance(entityName, importance) {
441
- const graph = await this.storage.loadGraph();
442
- const timestamp = new Date().toISOString();
443
395
  // Validate importance range (0-10)
444
396
  if (importance < 0 || importance > 10) {
445
397
  throw new Error(`Importance must be between 0 and 10, got ${importance}`);
446
398
  }
447
- const entity = graph.entities.find(e => e.name === entityName);
399
+ // OPTIMIZED: Use O(1) NameIndex lookup instead of loadGraph() + O(n) find()
400
+ const entity = this.storage.getEntityByName(entityName);
448
401
  if (!entity) {
449
402
  throw new EntityNotFoundError(entityName);
450
403
  }
451
- entity.importance = importance;
452
- entity.lastModified = timestamp;
453
- await this.storage.saveGraph(graph);
404
+ // Use updateEntity for in-place update + append
405
+ await this.storage.updateEntity(entityName, { importance });
454
406
  return { entityName, importance };
455
407
  }
456
408
  /**
@@ -461,7 +413,7 @@ export class EntityManager {
461
413
  * @returns Array of results showing which tags were added to each entity
462
414
  */
463
415
  async addTagsToMultipleEntities(entityNames, tags) {
464
- const graph = await this.storage.loadGraph();
416
+ const graph = await this.storage.getGraphForMutation();
465
417
  const timestamp = new Date().toISOString();
466
418
  const normalizedTags = tags.map(tag => tag.toLowerCase());
467
419
  const results = [];
@@ -494,7 +446,7 @@ export class EntityManager {
494
446
  * @returns Result with affected entities and count
495
447
  */
496
448
  async replaceTag(oldTag, newTag) {
497
- const graph = await this.storage.loadGraph();
449
+ const graph = await this.storage.getGraphForMutation();
498
450
  const timestamp = new Date().toISOString();
499
451
  const normalizedOldTag = oldTag.toLowerCase();
500
452
  const normalizedNewTag = newTag.toLowerCase();
@@ -524,7 +476,7 @@ export class EntityManager {
524
476
  * @returns Object with affected entity names and count
525
477
  */
526
478
  async mergeTags(tag1, tag2, targetTag) {
527
- const graph = await this.storage.loadGraph();
479
+ const graph = await this.storage.getGraphForMutation();
528
480
  const timestamp = new Date().toISOString();
529
481
  const normalizedTag1 = tag1.toLowerCase();
530
482
  const normalizedTag2 = tag2.toLowerCase();
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Graph Event Emitter
3
+ *
4
+ * Phase 10 Sprint 2: Provides event-based notifications for graph changes.
5
+ * Enables loose coupling between graph operations and dependent systems
6
+ * like search indexes, analytics, and external integrations.
7
+ *
8
+ * @module core/GraphEventEmitter
9
+ */
10
+ import type { GraphEventType, GraphEvent, GraphEventListener, GraphEventMap, Entity, Relation } from '../types/index.js';
11
+ /**
12
+ * Phase 10 Sprint 2: Event emitter for graph change notifications.
13
+ *
14
+ * Provides a type-safe event system for subscribing to and emitting
15
+ * graph change events. Supports wildcard listeners for all events.
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const emitter = new GraphEventEmitter();
20
+ *
21
+ * // Listen to specific event types
22
+ * emitter.on('entity:created', (event) => {
23
+ * console.log(`Entity ${event.entity.name} created`);
24
+ * });
25
+ *
26
+ * // Listen to all events
27
+ * emitter.onAny((event) => {
28
+ * console.log(`Event: ${event.type}`);
29
+ * });
30
+ *
31
+ * // Emit an event
32
+ * emitter.emitEntityCreated(entity);
33
+ *
34
+ * // Remove listener
35
+ * const unsubscribe = emitter.on('entity:deleted', handler);
36
+ * unsubscribe();
37
+ * ```
38
+ */
39
+ export declare class GraphEventEmitter {
40
+ /**
41
+ * Map of event types to their registered listeners.
42
+ */
43
+ private listeners;
44
+ /**
45
+ * Listeners that receive all events regardless of type.
46
+ */
47
+ private wildcardListeners;
48
+ /**
49
+ * Whether to suppress errors from listeners (default: true).
50
+ * When true, listener errors are logged but don't stop event propagation.
51
+ */
52
+ private suppressListenerErrors;
53
+ /**
54
+ * Create a new GraphEventEmitter instance.
55
+ *
56
+ * @param options - Optional configuration
57
+ */
58
+ constructor(options?: {
59
+ suppressListenerErrors?: boolean;
60
+ });
61
+ /**
62
+ * Subscribe to a specific event type.
63
+ *
64
+ * @template K - The event type key
65
+ * @param eventType - The event type to listen for
66
+ * @param listener - Callback function to invoke when event occurs
67
+ * @returns Unsubscribe function to remove the listener
68
+ *
69
+ * @example
70
+ * ```typescript
71
+ * const unsubscribe = emitter.on('entity:created', (event) => {
72
+ * console.log(`Created: ${event.entity.name}`);
73
+ * });
74
+ *
75
+ * // Later: unsubscribe();
76
+ * ```
77
+ */
78
+ on<K extends GraphEventType>(eventType: K, listener: GraphEventListener<GraphEventMap[K]>): () => void;
79
+ /**
80
+ * Unsubscribe from a specific event type.
81
+ *
82
+ * @template K - The event type key
83
+ * @param eventType - The event type to unsubscribe from
84
+ * @param listener - The listener function to remove
85
+ */
86
+ off<K extends GraphEventType>(eventType: K, listener: GraphEventListener<GraphEventMap[K]>): void;
87
+ /**
88
+ * Subscribe to all event types.
89
+ *
90
+ * @param listener - Callback function to invoke for any event
91
+ * @returns Unsubscribe function to remove the listener
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * emitter.onAny((event) => {
96
+ * console.log(`Event: ${event.type} at ${event.timestamp}`);
97
+ * });
98
+ * ```
99
+ */
100
+ onAny(listener: GraphEventListener<GraphEvent>): () => void;
101
+ /**
102
+ * Unsubscribe from all events.
103
+ *
104
+ * @param listener - The listener function to remove
105
+ */
106
+ offAny(listener: GraphEventListener<GraphEvent>): void;
107
+ /**
108
+ * Subscribe to an event type, but only receive the first occurrence.
109
+ *
110
+ * @template K - The event type key
111
+ * @param eventType - The event type to listen for once
112
+ * @param listener - Callback function to invoke once
113
+ * @returns Unsubscribe function to cancel before event occurs
114
+ */
115
+ once<K extends GraphEventType>(eventType: K, listener: GraphEventListener<GraphEventMap[K]>): () => void;
116
+ /**
117
+ * Remove all listeners for all event types.
118
+ */
119
+ removeAllListeners(): void;
120
+ /**
121
+ * Get the count of listeners for a specific event type.
122
+ *
123
+ * @param eventType - The event type to count listeners for
124
+ * @returns Number of listeners registered
125
+ */
126
+ listenerCount(eventType?: GraphEventType): number;
127
+ /**
128
+ * Emit an event to all registered listeners.
129
+ *
130
+ * @param event - The event to emit
131
+ */
132
+ emit(event: GraphEvent): void;
133
+ /**
134
+ * Emit an entity:created event.
135
+ *
136
+ * @param entity - The entity that was created
137
+ */
138
+ emitEntityCreated(entity: Entity): void;
139
+ /**
140
+ * Emit an entity:updated event.
141
+ *
142
+ * @param entityName - Name of the updated entity
143
+ * @param changes - The changes that were applied
144
+ * @param previousValues - Optional previous values before update
145
+ */
146
+ emitEntityUpdated(entityName: string, changes: Partial<Entity>, previousValues?: Partial<Entity>): void;
147
+ /**
148
+ * Emit an entity:deleted event.
149
+ *
150
+ * @param entityName - Name of the deleted entity
151
+ * @param entity - Optional entity data before deletion
152
+ */
153
+ emitEntityDeleted(entityName: string, entity?: Entity): void;
154
+ /**
155
+ * Emit a relation:created event.
156
+ *
157
+ * @param relation - The relation that was created
158
+ */
159
+ emitRelationCreated(relation: Relation): void;
160
+ /**
161
+ * Emit a relation:deleted event.
162
+ *
163
+ * @param from - Source entity name
164
+ * @param to - Target entity name
165
+ * @param relationType - Type of the deleted relation
166
+ */
167
+ emitRelationDeleted(from: string, to: string, relationType: string): void;
168
+ /**
169
+ * Emit an observation:added event.
170
+ *
171
+ * @param entityName - Name of the entity
172
+ * @param observations - Observations that were added
173
+ */
174
+ emitObservationAdded(entityName: string, observations: string[]): void;
175
+ /**
176
+ * Emit an observation:deleted event.
177
+ *
178
+ * @param entityName - Name of the entity
179
+ * @param observations - Observations that were deleted
180
+ */
181
+ emitObservationDeleted(entityName: string, observations: string[]): void;
182
+ /**
183
+ * Emit a graph:saved event.
184
+ *
185
+ * @param entityCount - Number of entities in the saved graph
186
+ * @param relationCount - Number of relations in the saved graph
187
+ */
188
+ emitGraphSaved(entityCount: number, relationCount: number): void;
189
+ /**
190
+ * Emit a graph:loaded event.
191
+ *
192
+ * @param entityCount - Number of entities in the loaded graph
193
+ * @param relationCount - Number of relations in the loaded graph
194
+ */
195
+ emitGraphLoaded(entityCount: number, relationCount: number): void;
196
+ /**
197
+ * Safely invoke a listener, optionally catching errors.
198
+ * @private
199
+ */
200
+ private invokeListener;
201
+ }
202
+ //# sourceMappingURL=GraphEventEmitter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GraphEventEmitter.d.ts","sourceRoot":"","sources":["../../src/core/GraphEventEmitter.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,UAAU,EACV,kBAAkB,EAClB,aAAa,EACb,MAAM,EACN,QAAQ,EAUT,MAAM,mBAAmB,CAAC;AAE3B;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,iBAAiB;IAC5B;;OAEG;IACH,OAAO,CAAC,SAAS,CAAgE;IAEjF;;OAEG;IACH,OAAO,CAAC,iBAAiB,CAAkD;IAE3E;;;OAGG;IACH,OAAO,CAAC,sBAAsB,CAAiB;IAE/C;;;;OAIG;gBACS,OAAO,CAAC,EAAE;QAAE,sBAAsB,CAAC,EAAE,OAAO,CAAA;KAAE;IAQ1D;;;;;;;;;;;;;;;;OAgBG;IACH,EAAE,CAAC,CAAC,SAAS,cAAc,EACzB,SAAS,EAAE,CAAC,EACZ,QAAQ,EAAE,kBAAkB,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,GAC7C,MAAM,IAAI;IAYb;;;;;;OAMG;IACH,GAAG,CAAC,CAAC,SAAS,cAAc,EAC1B,SAAS,EAAE,CAAC,EACZ,QAAQ,EAAE,kBAAkB,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,GAC7C,IAAI;IAOP;;;;;;;;;;;;OAYG;IACH,KAAK,CAAC,QAAQ,EAAE,kBAAkB,CAAC,UAAU,CAAC,GAAG,MAAM,IAAI;IAO3D;;;;OAIG;IACH,MAAM,CAAC,QAAQ,EAAE,kBAAkB,CAAC,UAAU,CAAC,GAAG,IAAI;IAItD;;;;;;;OAOG;IACH,IAAI,CAAC,CAAC,SAAS,cAAc,EAC3B,SAAS,EAAE,CAAC,EACZ,QAAQ,EAAE,kBAAkB,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,GAC7C,MAAM,IAAI;IASb;;OAEG;IACH,kBAAkB,IAAI,IAAI;IAK1B;;;;;OAKG;IACH,aAAa,CAAC,SAAS,CAAC,EAAE,cAAc,GAAG,MAAM;IAcjD;;;;OAIG;IACH,IAAI,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAe7B;;;;OAIG;IACH,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IASvC;;;;;;OAMG;IACH,iBAAiB,CACf,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,EACxB,cAAc,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,GAC/B,IAAI;IAWP;;;;;OAKG;IACH,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI;IAU5D;;;;OAIG;IACH,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI;IAS7C;;;;;;OAMG;IACH,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI;IAWzE;;;;;OAKG;IACH,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,IAAI;IAYtE;;;;;OAKG;IACH,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,IAAI;IAYxE;;;;;OAKG;IACH,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI;IAUhE;;;;;OAKG;IACH,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,IAAI;IAYjE;;;OAGG;IACH,OAAO,CAAC,cAAc;CAYvB"}