@danielsimonjr/memory-mcp 0.7.2 → 0.41.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 (61) hide show
  1. package/dist/__tests__/edge-cases/edge-cases.test.js +406 -0
  2. package/dist/__tests__/file-path.test.js +5 -5
  3. package/dist/__tests__/integration/workflows.test.js +449 -0
  4. package/dist/__tests__/knowledge-graph.test.js +8 -3
  5. package/dist/__tests__/performance/benchmarks.test.js +411 -0
  6. package/dist/__tests__/unit/core/EntityManager.test.js +334 -0
  7. package/dist/__tests__/unit/core/GraphStorage.test.js +205 -0
  8. package/dist/__tests__/unit/core/RelationManager.test.js +274 -0
  9. package/dist/__tests__/unit/features/CompressionManager.test.js +350 -0
  10. package/dist/__tests__/unit/search/BasicSearch.test.js +311 -0
  11. package/dist/__tests__/unit/search/BooleanSearch.test.js +432 -0
  12. package/dist/__tests__/unit/search/FuzzySearch.test.js +448 -0
  13. package/dist/__tests__/unit/search/RankedSearch.test.js +379 -0
  14. package/dist/__tests__/unit/utils/levenshtein.test.js +77 -0
  15. package/dist/core/EntityManager.js +554 -0
  16. package/dist/core/GraphStorage.js +172 -0
  17. package/dist/core/KnowledgeGraphManager.js +400 -0
  18. package/dist/core/ObservationManager.js +129 -0
  19. package/dist/core/RelationManager.js +186 -0
  20. package/dist/core/TransactionManager.js +389 -0
  21. package/dist/core/index.js +9 -0
  22. package/dist/features/AnalyticsManager.js +222 -0
  23. package/dist/features/ArchiveManager.js +74 -0
  24. package/dist/features/BackupManager.js +311 -0
  25. package/dist/features/CompressionManager.js +310 -0
  26. package/dist/features/ExportManager.js +305 -0
  27. package/dist/features/HierarchyManager.js +219 -0
  28. package/dist/features/ImportExportManager.js +50 -0
  29. package/dist/features/ImportManager.js +328 -0
  30. package/dist/features/TagManager.js +210 -0
  31. package/dist/features/index.js +12 -0
  32. package/dist/index.js +13 -996
  33. package/dist/memory.jsonl +225 -0
  34. package/dist/search/BasicSearch.js +161 -0
  35. package/dist/search/BooleanSearch.js +304 -0
  36. package/dist/search/FuzzySearch.js +115 -0
  37. package/dist/search/RankedSearch.js +206 -0
  38. package/dist/search/SavedSearchManager.js +145 -0
  39. package/dist/search/SearchManager.js +305 -0
  40. package/dist/search/SearchSuggestions.js +57 -0
  41. package/dist/search/TFIDFIndexManager.js +217 -0
  42. package/dist/search/index.js +10 -0
  43. package/dist/server/MCPServer.js +889 -0
  44. package/dist/types/analytics.types.js +6 -0
  45. package/dist/types/entity.types.js +7 -0
  46. package/dist/types/import-export.types.js +7 -0
  47. package/dist/types/index.js +12 -0
  48. package/dist/types/search.types.js +7 -0
  49. package/dist/types/tag.types.js +6 -0
  50. package/dist/utils/constants.js +127 -0
  51. package/dist/utils/dateUtils.js +89 -0
  52. package/dist/utils/errors.js +121 -0
  53. package/dist/utils/index.js +13 -0
  54. package/dist/utils/levenshtein.js +62 -0
  55. package/dist/utils/logger.js +33 -0
  56. package/dist/utils/pathUtils.js +115 -0
  57. package/dist/utils/schemas.js +184 -0
  58. package/dist/utils/searchCache.js +209 -0
  59. package/dist/utils/tfidf.js +90 -0
  60. package/dist/utils/validationUtils.js +109 -0
  61. package/package.json +50 -48
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Graph Storage
3
+ *
4
+ * Handles file I/O operations for the knowledge graph using JSONL format.
5
+ * Provides persistence layer abstraction for graph data.
6
+ *
7
+ * @module core/GraphStorage
8
+ */
9
+ import { promises as fs } from 'fs';
10
+ import { clearAllSearchCaches } from '../utils/searchCache.js';
11
+ /**
12
+ * GraphStorage manages persistence of the knowledge graph to disk.
13
+ *
14
+ * Uses JSONL (JSON Lines) format where each line is a separate JSON object
15
+ * representing either an entity or a relation.
16
+ *
17
+ * OPTIMIZED: Implements in-memory caching to avoid repeated disk reads.
18
+ * Cache is invalidated on every write operation to ensure consistency.
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * const storage = new GraphStorage('/path/to/memory.jsonl');
23
+ * const graph = await storage.loadGraph();
24
+ * graph.entities.push(newEntity);
25
+ * await storage.saveGraph(graph);
26
+ * ```
27
+ */
28
+ export class GraphStorage {
29
+ memoryFilePath;
30
+ /**
31
+ * In-memory cache of the knowledge graph.
32
+ * Null when cache is empty or invalidated.
33
+ */
34
+ cache = null;
35
+ /**
36
+ * Create a new GraphStorage instance.
37
+ *
38
+ * @param memoryFilePath - Absolute path to the JSONL file
39
+ */
40
+ constructor(memoryFilePath) {
41
+ this.memoryFilePath = memoryFilePath;
42
+ }
43
+ /**
44
+ * Load the knowledge graph from disk.
45
+ *
46
+ * OPTIMIZED: Uses in-memory cache to avoid repeated disk reads.
47
+ * Cache is populated on first load and invalidated on writes.
48
+ *
49
+ * Reads the JSONL file and reconstructs the graph structure.
50
+ * Returns empty graph if file doesn't exist.
51
+ *
52
+ * @returns Promise resolving to the loaded knowledge graph
53
+ * @throws Error if file exists but cannot be read or parsed
54
+ */
55
+ async loadGraph() {
56
+ // Return cached graph if available
57
+ if (this.cache !== null) {
58
+ // Return a deep copy to prevent external mutations from affecting cache
59
+ return {
60
+ entities: this.cache.entities.map(e => ({ ...e })),
61
+ relations: this.cache.relations.map(r => ({ ...r })),
62
+ };
63
+ }
64
+ // Cache miss - load from disk
65
+ try {
66
+ const data = await fs.readFile(this.memoryFilePath, 'utf-8');
67
+ const lines = data.split('\n').filter((line) => line.trim() !== '');
68
+ const graph = lines.reduce((graph, line) => {
69
+ const item = JSON.parse(line);
70
+ if (item.type === 'entity') {
71
+ // Add createdAt if missing for backward compatibility
72
+ if (!item.createdAt)
73
+ item.createdAt = new Date().toISOString();
74
+ // Add lastModified if missing for backward compatibility
75
+ if (!item.lastModified)
76
+ item.lastModified = item.createdAt;
77
+ graph.entities.push(item);
78
+ }
79
+ if (item.type === 'relation') {
80
+ // Add createdAt if missing for backward compatibility
81
+ if (!item.createdAt)
82
+ item.createdAt = new Date().toISOString();
83
+ // Add lastModified if missing for backward compatibility
84
+ if (!item.lastModified)
85
+ item.lastModified = item.createdAt;
86
+ graph.relations.push(item);
87
+ }
88
+ return graph;
89
+ }, { entities: [], relations: [] });
90
+ // Populate cache
91
+ this.cache = graph;
92
+ // Return a deep copy
93
+ return {
94
+ entities: graph.entities.map(e => ({ ...e })),
95
+ relations: graph.relations.map(r => ({ ...r })),
96
+ };
97
+ }
98
+ catch (error) {
99
+ // File doesn't exist - return empty graph
100
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
101
+ const emptyGraph = { entities: [], relations: [] };
102
+ this.cache = emptyGraph;
103
+ return { entities: [], relations: [] };
104
+ }
105
+ throw error;
106
+ }
107
+ }
108
+ /**
109
+ * Save the knowledge graph to disk.
110
+ *
111
+ * OPTIMIZED: Invalidates cache after write to ensure consistency.
112
+ *
113
+ * Writes the graph to JSONL format, with one JSON object per line.
114
+ *
115
+ * @param graph - The knowledge graph to save
116
+ * @returns Promise resolving when save is complete
117
+ * @throws Error if file cannot be written
118
+ */
119
+ async saveGraph(graph) {
120
+ const lines = [
121
+ ...graph.entities.map(e => {
122
+ const entityData = {
123
+ type: 'entity',
124
+ name: e.name,
125
+ entityType: e.entityType,
126
+ observations: e.observations,
127
+ createdAt: e.createdAt,
128
+ lastModified: e.lastModified,
129
+ };
130
+ // Only include optional fields if they exist
131
+ if (e.tags !== undefined)
132
+ entityData.tags = e.tags;
133
+ if (e.importance !== undefined)
134
+ entityData.importance = e.importance;
135
+ if (e.parentId !== undefined)
136
+ entityData.parentId = e.parentId;
137
+ return JSON.stringify(entityData);
138
+ }),
139
+ ...graph.relations.map(r => JSON.stringify({
140
+ type: 'relation',
141
+ from: r.from,
142
+ to: r.to,
143
+ relationType: r.relationType,
144
+ createdAt: r.createdAt,
145
+ lastModified: r.lastModified,
146
+ })),
147
+ ];
148
+ await fs.writeFile(this.memoryFilePath, lines.join('\n'));
149
+ // Invalidate cache to ensure next load reads fresh data
150
+ this.cache = null;
151
+ // Clear all search caches since graph data has changed
152
+ clearAllSearchCaches();
153
+ }
154
+ /**
155
+ * Manually clear the cache.
156
+ *
157
+ * Useful for testing or when external processes modify the file.
158
+ *
159
+ * @returns void
160
+ */
161
+ clearCache() {
162
+ this.cache = null;
163
+ }
164
+ /**
165
+ * Get the file path being used for storage.
166
+ *
167
+ * @returns The memory file path
168
+ */
169
+ getFilePath() {
170
+ return this.memoryFilePath;
171
+ }
172
+ }
@@ -0,0 +1,400 @@
1
+ /**
2
+ * Knowledge Graph Manager
3
+ *
4
+ * Central manager coordinating all knowledge graph operations.
5
+ * Delegates to specialized managers for different concerns.
6
+ *
7
+ * @module core/KnowledgeGraphManager
8
+ */
9
+ import path from 'path';
10
+ import { DEFAULT_DUPLICATE_THRESHOLD, SEARCH_LIMITS } from '../utils/constants.js';
11
+ import { GraphStorage } from './GraphStorage.js';
12
+ import { EntityManager } from './EntityManager.js';
13
+ import { RelationManager } from './RelationManager.js';
14
+ import { SearchManager } from '../search/SearchManager.js';
15
+ import { CompressionManager } from '../features/CompressionManager.js';
16
+ import { HierarchyManager } from '../features/HierarchyManager.js';
17
+ import { ExportManager } from '../features/ExportManager.js';
18
+ import { ImportManager } from '../features/ImportManager.js';
19
+ import { AnalyticsManager } from '../features/AnalyticsManager.js';
20
+ import { TagManager } from '../features/TagManager.js';
21
+ import { ArchiveManager } from '../features/ArchiveManager.js';
22
+ /**
23
+ * Central manager coordinating all knowledge graph operations.
24
+ *
25
+ * This class serves as the main facade for interacting with the knowledge graph,
26
+ * delegating to specialized managers for different concerns:
27
+ * - EntityManager: Entity CRUD operations
28
+ * - RelationManager: Relation CRUD operations
29
+ * - SearchManager: All search operations (basic, ranked, boolean, fuzzy)
30
+ * - CompressionManager: Duplicate detection and merging
31
+ * - HierarchyManager: Parent-child relationships and tree operations
32
+ * - ExportManager: Export to various formats
33
+ * - ImportManager: Import from various formats
34
+ * - AnalyticsManager: Statistics and validation
35
+ * - TagManager: Tag alias management
36
+ * - ArchiveManager: Entity archiving
37
+ */
38
+ export class KnowledgeGraphManager {
39
+ savedSearchesFilePath;
40
+ tagAliasesFilePath;
41
+ storage;
42
+ entityManager;
43
+ relationManager;
44
+ searchManager;
45
+ compressionManager;
46
+ hierarchyManager;
47
+ exportManager;
48
+ importManager;
49
+ analyticsManager;
50
+ tagManager;
51
+ archiveManager;
52
+ constructor(memoryFilePath) {
53
+ // Saved searches file is stored alongside the memory file
54
+ const dir = path.dirname(memoryFilePath);
55
+ const basename = path.basename(memoryFilePath, path.extname(memoryFilePath));
56
+ this.savedSearchesFilePath = path.join(dir, `${basename}-saved-searches.jsonl`);
57
+ this.tagAliasesFilePath = path.join(dir, `${basename}-tag-aliases.jsonl`);
58
+ this.storage = new GraphStorage(memoryFilePath);
59
+ this.entityManager = new EntityManager(this.storage);
60
+ this.relationManager = new RelationManager(this.storage);
61
+ this.searchManager = new SearchManager(this.storage, this.savedSearchesFilePath);
62
+ this.compressionManager = new CompressionManager(this.storage);
63
+ this.hierarchyManager = new HierarchyManager(this.storage);
64
+ this.exportManager = new ExportManager();
65
+ this.importManager = new ImportManager(this.storage);
66
+ this.analyticsManager = new AnalyticsManager(this.storage);
67
+ this.tagManager = new TagManager(this.tagAliasesFilePath);
68
+ this.archiveManager = new ArchiveManager(this.storage);
69
+ }
70
+ async loadGraph() {
71
+ return this.storage.loadGraph();
72
+ }
73
+ /**
74
+ * Phase 4: Create multiple entities in a single batch operation.
75
+ * Batch optimization: All entities are processed and saved in a single saveGraph() call,
76
+ * minimizing disk I/O. This is significantly more efficient than creating entities one at a time.
77
+ */
78
+ async createEntities(entities) {
79
+ return this.entityManager.createEntities(entities);
80
+ }
81
+ /**
82
+ * Phase 4: Create multiple relations in a single batch operation.
83
+ * Batch optimization: All relations are processed and saved in a single saveGraph() call,
84
+ * minimizing disk I/O. This is significantly more efficient than creating relations one at a time.
85
+ */
86
+ async createRelations(relations) {
87
+ return this.relationManager.createRelations(relations);
88
+ }
89
+ async addObservations(observations) {
90
+ return this.entityManager.addObservations(observations);
91
+ }
92
+ async deleteEntities(entityNames) {
93
+ return this.entityManager.deleteEntities(entityNames);
94
+ }
95
+ async deleteObservations(deletions) {
96
+ return this.entityManager.deleteObservations(deletions);
97
+ }
98
+ async deleteRelations(relations) {
99
+ return this.relationManager.deleteRelations(relations);
100
+ }
101
+ async readGraph() {
102
+ return this.loadGraph();
103
+ }
104
+ // Phase 3: Enhanced search function with tags and importance filters
105
+ async searchNodes(query, tags, minImportance, maxImportance) {
106
+ return this.searchManager.searchNodes(query, tags, minImportance, maxImportance);
107
+ }
108
+ async openNodes(names) {
109
+ return this.searchManager.openNodes(names);
110
+ }
111
+ // Phase 3: Enhanced searchByDateRange with tags filter
112
+ async searchByDateRange(startDate, endDate, entityType, tags) {
113
+ return this.searchManager.searchByDateRange(startDate, endDate, entityType, tags);
114
+ }
115
+ async getGraphStats() {
116
+ return this.analyticsManager.getGraphStats();
117
+ }
118
+ // Phase 3: Add tags to an entity
119
+ async addTags(entityName, tags) {
120
+ return this.entityManager.addTags(entityName, tags);
121
+ }
122
+ // Phase 3: Remove tags from an entity
123
+ async removeTags(entityName, tags) {
124
+ return this.entityManager.removeTags(entityName, tags);
125
+ }
126
+ // Phase 3: Set importance level for an entity
127
+ async setImportance(entityName, importance) {
128
+ return this.entityManager.setImportance(entityName, importance);
129
+ }
130
+ // Tier 0 B5: Bulk tag operations for efficient tag management
131
+ /**
132
+ * Add tags to multiple entities in a single operation
133
+ */
134
+ async addTagsToMultipleEntities(entityNames, tags) {
135
+ return this.entityManager.addTagsToMultipleEntities(entityNames, tags);
136
+ }
137
+ /**
138
+ * Replace a tag with a new tag across all entities (rename tag)
139
+ */
140
+ async replaceTag(oldTag, newTag) {
141
+ return this.entityManager.replaceTag(oldTag, newTag);
142
+ }
143
+ /**
144
+ * Merge two tags into one (combine tag1 and tag2 into targetTag)
145
+ */
146
+ async mergeTags(tag1, tag2, targetTag) {
147
+ return this.entityManager.mergeTags(tag1, tag2, targetTag);
148
+ }
149
+ // Tier 0 A1: Graph validation for data integrity
150
+ /**
151
+ * Validate the knowledge graph for integrity issues and provide a detailed report
152
+ */
153
+ async validateGraph() {
154
+ return this.analyticsManager.validateGraph();
155
+ }
156
+ // Tier 0 C4: Saved searches for efficient query management
157
+ /**
158
+ * Save a search query for later reuse
159
+ */
160
+ async saveSearch(search) {
161
+ return this.searchManager.saveSearch(search);
162
+ }
163
+ /**
164
+ * List all saved searches
165
+ */
166
+ async listSavedSearches() {
167
+ return this.searchManager.listSavedSearches();
168
+ }
169
+ /**
170
+ * Get a specific saved search by name
171
+ */
172
+ async getSavedSearch(name) {
173
+ return this.searchManager.getSavedSearch(name);
174
+ }
175
+ /**
176
+ * Execute a saved search by name
177
+ */
178
+ async executeSavedSearch(name) {
179
+ return this.searchManager.executeSavedSearch(name);
180
+ }
181
+ /**
182
+ * Delete a saved search
183
+ */
184
+ async deleteSavedSearch(name) {
185
+ return this.searchManager.deleteSavedSearch(name);
186
+ }
187
+ /**
188
+ * Update a saved search
189
+ */
190
+ async updateSavedSearch(name, updates) {
191
+ return this.searchManager.updateSavedSearch(name, updates);
192
+ }
193
+ /**
194
+ * Fuzzy search for entities with typo tolerance
195
+ * @param query - Search query
196
+ * @param threshold - Similarity threshold (0.0 to 1.0), default 0.7
197
+ * @param tags - Optional tags filter
198
+ * @param minImportance - Optional minimum importance
199
+ * @param maxImportance - Optional maximum importance
200
+ * @returns Filtered knowledge graph with fuzzy matches
201
+ */
202
+ async fuzzySearch(query, threshold = 0.7, tags, minImportance, maxImportance) {
203
+ return this.searchManager.fuzzySearch(query, threshold, tags, minImportance, maxImportance);
204
+ }
205
+ /**
206
+ * Get "did you mean?" suggestions for a query
207
+ * @param query - The search query
208
+ * @param maxSuggestions - Maximum number of suggestions to return
209
+ * @returns Array of suggested entity/type names
210
+ */
211
+ async getSearchSuggestions(query, maxSuggestions = 5) {
212
+ return this.searchManager.getSearchSuggestions(query, maxSuggestions);
213
+ }
214
+ /**
215
+ * Search nodes with TF-IDF ranking for relevance scoring
216
+ * @param query - Search query
217
+ * @param tags - Optional tags filter
218
+ * @param minImportance - Optional minimum importance
219
+ * @param maxImportance - Optional maximum importance
220
+ * @param limit - Optional maximum number of results (default 50)
221
+ * @returns Array of search results with scores, sorted by relevance
222
+ */
223
+ async searchNodesRanked(query, tags, minImportance, maxImportance, limit = SEARCH_LIMITS.DEFAULT) {
224
+ return this.searchManager.searchNodesRanked(query, tags, minImportance, maxImportance, limit);
225
+ }
226
+ /**
227
+ * Boolean search with support for AND, OR, NOT operators and field-specific queries
228
+ * @param query - Boolean query string (e.g., "name:Alice AND (type:person OR observation:programming)")
229
+ * @param tags - Optional tags filter
230
+ * @param minImportance - Optional minimum importance
231
+ * @param maxImportance - Optional maximum importance
232
+ * @returns Filtered knowledge graph matching the boolean query
233
+ */
234
+ async booleanSearch(query, tags, minImportance, maxImportance) {
235
+ return this.searchManager.booleanSearch(query, tags, minImportance, maxImportance);
236
+ }
237
+ // Phase 2: Hierarchical nesting - hierarchy navigation methods
238
+ /**
239
+ * Set the parent of an entity (creates hierarchy relationship)
240
+ * @param entityName - Name of the entity to set parent for
241
+ * @param parentName - Name of the parent entity (or null to remove parent)
242
+ * @returns Updated entity
243
+ */
244
+ async setEntityParent(entityName, parentName) {
245
+ return this.hierarchyManager.setEntityParent(entityName, parentName);
246
+ }
247
+ /**
248
+ * Get the immediate children of an entity
249
+ */
250
+ async getChildren(entityName) {
251
+ return this.hierarchyManager.getChildren(entityName);
252
+ }
253
+ /**
254
+ * Get the parent of an entity
255
+ */
256
+ async getParent(entityName) {
257
+ return this.hierarchyManager.getParent(entityName);
258
+ }
259
+ /**
260
+ * Get all ancestors of an entity (parent, grandparent, etc.)
261
+ */
262
+ async getAncestors(entityName) {
263
+ return this.hierarchyManager.getAncestors(entityName);
264
+ }
265
+ /**
266
+ * Get all descendants of an entity (children, grandchildren, etc.)
267
+ */
268
+ async getDescendants(entityName) {
269
+ return this.hierarchyManager.getDescendants(entityName);
270
+ }
271
+ /**
272
+ * Get the entire subtree rooted at an entity (entity + all descendants)
273
+ */
274
+ async getSubtree(entityName) {
275
+ return this.hierarchyManager.getSubtree(entityName);
276
+ }
277
+ /**
278
+ * Get root entities (entities with no parent)
279
+ */
280
+ async getRootEntities() {
281
+ return this.hierarchyManager.getRootEntities();
282
+ }
283
+ /**
284
+ * Get the depth of an entity in the hierarchy (0 for root, 1 for child of root, etc.)
285
+ */
286
+ async getEntityDepth(entityName) {
287
+ return this.hierarchyManager.getEntityDepth(entityName);
288
+ }
289
+ /**
290
+ * Move an entity to a new parent (maintaining its descendants)
291
+ */
292
+ async moveEntity(entityName, newParentName) {
293
+ return this.hierarchyManager.moveEntity(entityName, newParentName);
294
+ }
295
+ // Phase 3: Memory compression - duplicate detection and merging
296
+ /**
297
+ * Find duplicate entities in the graph based on similarity threshold
298
+ * @param threshold - Similarity threshold (0.0 to 1.0), default 0.8
299
+ * @returns Array of duplicate groups (each group has similar entities)
300
+ */
301
+ async findDuplicates(threshold = DEFAULT_DUPLICATE_THRESHOLD) {
302
+ return this.compressionManager.findDuplicates(threshold);
303
+ }
304
+ /**
305
+ * Merge a group of entities into a single entity
306
+ * @param entityNames - Names of entities to merge (first one is kept)
307
+ * @param targetName - Optional new name for merged entity (default: first entity name)
308
+ * @returns The merged entity
309
+ */
310
+ async mergeEntities(entityNames, targetName) {
311
+ return this.compressionManager.mergeEntities(entityNames, targetName);
312
+ }
313
+ /**
314
+ * Compress the knowledge graph by finding and merging duplicates
315
+ * @param threshold - Similarity threshold for duplicate detection (0.0 to 1.0)
316
+ * @param dryRun - If true, only report what would be compressed without applying changes
317
+ * @returns Compression result with statistics
318
+ */
319
+ async compressGraph(threshold = 0.8, dryRun = false) {
320
+ return this.compressionManager.compressGraph(threshold, dryRun);
321
+ }
322
+ // Phase 4: Memory archiving system
323
+ /**
324
+ * Archive old or low-importance entities to a separate storage
325
+ * @param criteria - Criteria for archiving (age, importance, tags)
326
+ * @param dryRun - If true, preview what would be archived
327
+ * @returns Number of entities archived
328
+ */
329
+ async archiveEntities(criteria, dryRun = false) {
330
+ return this.archiveManager.archiveEntities(criteria, dryRun);
331
+ }
332
+ // Tier 0 B2: Tag aliases for synonym management
333
+ /**
334
+ * Resolve a tag through aliases to get its canonical form
335
+ * @param tag - Tag to resolve (can be alias or canonical)
336
+ * @returns Canonical tag name
337
+ */
338
+ async resolveTag(tag) {
339
+ return this.tagManager.resolveTag(tag);
340
+ }
341
+ /**
342
+ * Add a tag alias (synonym mapping)
343
+ * @param alias - The alias/synonym
344
+ * @param canonical - The canonical (main) tag name
345
+ * @param description - Optional description of the alias
346
+ */
347
+ async addTagAlias(alias, canonical, description) {
348
+ return this.tagManager.addTagAlias(alias, canonical, description);
349
+ }
350
+ /**
351
+ * List all tag aliases
352
+ */
353
+ async listTagAliases() {
354
+ return this.tagManager.listTagAliases();
355
+ }
356
+ /**
357
+ * Remove a tag alias
358
+ */
359
+ async removeTagAlias(alias) {
360
+ return this.tagManager.removeTagAlias(alias);
361
+ }
362
+ /**
363
+ * Get all aliases for a canonical tag
364
+ */
365
+ async getAliasesForTag(canonicalTag) {
366
+ return this.tagManager.getAliasesForTag(canonicalTag);
367
+ }
368
+ // Phase 4 & Tier 0 D1: Export graph in various formats
369
+ /**
370
+ * Export the knowledge graph in the specified format with optional filtering.
371
+ * Supports JSON, CSV, GraphML, GEXF, DOT, Markdown, and Mermaid formats.
372
+ *
373
+ * @param format - Export format: 'json', 'csv', 'graphml', 'gexf', 'dot', 'markdown', 'mermaid'
374
+ * @param filter - Optional filter object with same structure as searchByDateRange
375
+ * @returns Exported graph data as a formatted string
376
+ */
377
+ async exportGraph(format, filter) {
378
+ // Get filtered or full graph based on filter parameter
379
+ let graph;
380
+ if (filter) {
381
+ graph = await this.searchByDateRange(filter.startDate, filter.endDate, filter.entityType, filter.tags);
382
+ }
383
+ else {
384
+ graph = await this.loadGraph();
385
+ }
386
+ return this.exportManager.exportGraph(graph, format);
387
+ }
388
+ // Tier 0 D2: Import capabilities with merge strategies
389
+ /**
390
+ * Import knowledge graph from various formats
391
+ * @param format - Import format: 'json', 'csv', or 'graphml'
392
+ * @param data - The import data as a string
393
+ * @param mergeStrategy - How to handle conflicts: 'replace', 'skip', 'merge', 'fail'
394
+ * @param dryRun - If true, preview changes without applying them
395
+ * @returns Import result with statistics
396
+ */
397
+ async importGraph(format, data, mergeStrategy = 'skip', dryRun = false) {
398
+ return this.importManager.importGraph(format, data, mergeStrategy, dryRun);
399
+ }
400
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Observation Manager
3
+ *
4
+ * Handles CRUD operations for observations within entities.
5
+ *
6
+ * @module core/ObservationManager
7
+ */
8
+ import { EntityNotFoundError } from '../utils/errors.js';
9
+ /**
10
+ * Manages observation operations with automatic timestamp handling.
11
+ */
12
+ export class ObservationManager {
13
+ storage;
14
+ constructor(storage) {
15
+ this.storage = storage;
16
+ }
17
+ /**
18
+ * Add observations to multiple entities in a single batch operation.
19
+ *
20
+ * This method performs the following operations:
21
+ * - Validates that all specified entities exist
22
+ * - Filters out duplicate observations (observations already present)
23
+ * - Adds new observations to each entity
24
+ * - Automatically updates lastModified timestamp for modified entities
25
+ *
26
+ * @param observations - Array of objects, each containing entityName and array of observation contents
27
+ * @returns Promise resolving to array of results showing added observations per entity
28
+ * @throws {EntityNotFoundError} If any specified entity does not exist
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * const manager = new ObservationManager(storage);
33
+ *
34
+ * // Add observations to single entity
35
+ * const results = await manager.addObservations([
36
+ * {
37
+ * entityName: 'Alice',
38
+ * contents: ['Works on frontend', 'Expertise in React']
39
+ * }
40
+ * ]);
41
+ * console.log(results[0].addedObservations); // ['Works on frontend', 'Expertise in React']
42
+ *
43
+ * // Add observations to multiple entities at once
44
+ * await manager.addObservations([
45
+ * { entityName: 'Bob', contents: ['Team lead', 'Experienced in Node.js'] },
46
+ * { entityName: 'Charlie', contents: ['New hire', 'Learning the codebase'] }
47
+ * ]);
48
+ *
49
+ * // Duplicate observations are filtered out
50
+ * await manager.addObservations([
51
+ * { entityName: 'Alice', contents: ['Works on frontend'] } // Already exists, won't be added
52
+ * ]);
53
+ * ```
54
+ */
55
+ async addObservations(observations) {
56
+ const graph = await this.storage.loadGraph();
57
+ const timestamp = new Date().toISOString();
58
+ const results = observations.map(o => {
59
+ const entity = graph.entities.find(e => e.name === o.entityName);
60
+ if (!entity) {
61
+ throw new EntityNotFoundError(o.entityName);
62
+ }
63
+ const newObservations = o.contents.filter(content => !entity.observations.includes(content));
64
+ entity.observations.push(...newObservations);
65
+ // Update lastModified if observations were added
66
+ if (newObservations.length > 0) {
67
+ entity.lastModified = timestamp;
68
+ }
69
+ return {
70
+ entityName: o.entityName,
71
+ addedObservations: newObservations,
72
+ };
73
+ });
74
+ await this.storage.saveGraph(graph);
75
+ return results;
76
+ }
77
+ /**
78
+ * Delete specific observations from multiple entities in a single batch operation.
79
+ *
80
+ * This method performs the following operations:
81
+ * - Removes specified observations from each entity
82
+ * - Silently ignores entities that don't exist (no error thrown)
83
+ * - Silently ignores observations that don't exist in the entity
84
+ * - Automatically updates lastModified timestamp for modified entities
85
+ *
86
+ * @param deletions - Array of objects, each containing entityName and array of observations to delete
87
+ * @returns Promise that resolves when deletion is complete
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * const manager = new ObservationManager(storage);
92
+ *
93
+ * // Delete specific observations from single entity
94
+ * await manager.deleteObservations([
95
+ * {
96
+ * entityName: 'Alice',
97
+ * observations: ['Works on frontend', 'Expertise in React']
98
+ * }
99
+ * ]);
100
+ *
101
+ * // Delete observations from multiple entities at once
102
+ * await manager.deleteObservations([
103
+ * { entityName: 'Bob', observations: ['Team lead'] },
104
+ * { entityName: 'Charlie', observations: ['New hire', 'Learning the codebase'] }
105
+ * ]);
106
+ *
107
+ * // Safe to delete non-existent observations or from non-existent entities
108
+ * await manager.deleteObservations([
109
+ * { entityName: 'NonExistent', observations: ['Some observation'] } // No error
110
+ * ]);
111
+ * ```
112
+ */
113
+ async deleteObservations(deletions) {
114
+ const graph = await this.storage.loadGraph();
115
+ const timestamp = new Date().toISOString();
116
+ deletions.forEach(d => {
117
+ const entity = graph.entities.find(e => e.name === d.entityName);
118
+ if (entity) {
119
+ const originalLength = entity.observations.length;
120
+ entity.observations = entity.observations.filter(o => !d.observations.includes(o));
121
+ // Update lastModified if observations were deleted
122
+ if (entity.observations.length < originalLength) {
123
+ entity.lastModified = timestamp;
124
+ }
125
+ }
126
+ });
127
+ await this.storage.saveGraph(graph);
128
+ }
129
+ }