@danielsimonjr/memory-mcp 12.2.3 → 12.5.2

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 (158) hide show
  1. package/README.md +88 -102
  2. package/dist/index.js +6 -0
  3. package/dist/server/toolDefinitions.d.ts.map +1 -1
  4. package/dist/server/toolDefinitions.js +745 -3
  5. package/dist/server/toolHandlers.d.ts +1 -9
  6. package/dist/server/toolHandlers.d.ts.map +1 -1
  7. package/dist/server/toolHandlers.js +859 -11
  8. package/package.json +9 -6
  9. package/dist/core/EntityManager.d.ts +0 -263
  10. package/dist/core/EntityManager.d.ts.map +0 -1
  11. package/dist/core/EntityManager.js +0 -478
  12. package/dist/core/GraphStorage.d.ts +0 -344
  13. package/dist/core/GraphStorage.d.ts.map +0 -1
  14. package/dist/core/GraphStorage.js +0 -708
  15. package/dist/core/GraphTraversal.d.ts +0 -136
  16. package/dist/core/GraphTraversal.d.ts.map +0 -1
  17. package/dist/core/GraphTraversal.js +0 -556
  18. package/dist/core/HierarchyManager.d.ts +0 -111
  19. package/dist/core/HierarchyManager.d.ts.map +0 -1
  20. package/dist/core/HierarchyManager.js +0 -224
  21. package/dist/core/ManagerContext.d.ts +0 -72
  22. package/dist/core/ManagerContext.d.ts.map +0 -1
  23. package/dist/core/ManagerContext.js +0 -118
  24. package/dist/core/ObservationManager.d.ts +0 -85
  25. package/dist/core/ObservationManager.d.ts.map +0 -1
  26. package/dist/core/ObservationManager.js +0 -123
  27. package/dist/core/RelationManager.d.ts +0 -131
  28. package/dist/core/RelationManager.d.ts.map +0 -1
  29. package/dist/core/RelationManager.js +0 -210
  30. package/dist/core/SQLiteStorage.d.ts +0 -354
  31. package/dist/core/SQLiteStorage.d.ts.map +0 -1
  32. package/dist/core/SQLiteStorage.js +0 -917
  33. package/dist/core/StorageFactory.d.ts +0 -45
  34. package/dist/core/StorageFactory.d.ts.map +0 -1
  35. package/dist/core/StorageFactory.js +0 -64
  36. package/dist/core/TransactionManager.d.ts +0 -259
  37. package/dist/core/TransactionManager.d.ts.map +0 -1
  38. package/dist/core/TransactionManager.js +0 -392
  39. package/dist/core/index.d.ts +0 -16
  40. package/dist/core/index.d.ts.map +0 -1
  41. package/dist/core/index.js +0 -17
  42. package/dist/features/AnalyticsManager.d.ts +0 -44
  43. package/dist/features/AnalyticsManager.d.ts.map +0 -1
  44. package/dist/features/AnalyticsManager.js +0 -223
  45. package/dist/features/ArchiveManager.d.ts +0 -121
  46. package/dist/features/ArchiveManager.d.ts.map +0 -1
  47. package/dist/features/ArchiveManager.js +0 -240
  48. package/dist/features/CompressionManager.d.ts +0 -109
  49. package/dist/features/CompressionManager.d.ts.map +0 -1
  50. package/dist/features/CompressionManager.js +0 -404
  51. package/dist/features/IOManager.d.ts +0 -221
  52. package/dist/features/IOManager.d.ts.map +0 -1
  53. package/dist/features/IOManager.js +0 -997
  54. package/dist/features/StreamingExporter.d.ts +0 -105
  55. package/dist/features/StreamingExporter.d.ts.map +0 -1
  56. package/dist/features/StreamingExporter.js +0 -150
  57. package/dist/features/TagManager.d.ts +0 -147
  58. package/dist/features/TagManager.d.ts.map +0 -1
  59. package/dist/features/TagManager.js +0 -210
  60. package/dist/features/index.d.ts +0 -12
  61. package/dist/features/index.d.ts.map +0 -1
  62. package/dist/features/index.js +0 -11
  63. package/dist/search/BasicSearch.d.ts +0 -51
  64. package/dist/search/BasicSearch.d.ts.map +0 -1
  65. package/dist/search/BasicSearch.js +0 -137
  66. package/dist/search/BooleanSearch.d.ts +0 -98
  67. package/dist/search/BooleanSearch.d.ts.map +0 -1
  68. package/dist/search/BooleanSearch.js +0 -430
  69. package/dist/search/EmbeddingService.d.ts +0 -178
  70. package/dist/search/EmbeddingService.d.ts.map +0 -1
  71. package/dist/search/EmbeddingService.js +0 -358
  72. package/dist/search/FuzzySearch.d.ts +0 -118
  73. package/dist/search/FuzzySearch.d.ts.map +0 -1
  74. package/dist/search/FuzzySearch.js +0 -312
  75. package/dist/search/RankedSearch.d.ts +0 -71
  76. package/dist/search/RankedSearch.d.ts.map +0 -1
  77. package/dist/search/RankedSearch.js +0 -238
  78. package/dist/search/SavedSearchManager.d.ts +0 -79
  79. package/dist/search/SavedSearchManager.d.ts.map +0 -1
  80. package/dist/search/SavedSearchManager.js +0 -145
  81. package/dist/search/SearchFilterChain.d.ts +0 -120
  82. package/dist/search/SearchFilterChain.d.ts.map +0 -1
  83. package/dist/search/SearchFilterChain.js +0 -185
  84. package/dist/search/SearchManager.d.ts +0 -285
  85. package/dist/search/SearchManager.d.ts.map +0 -1
  86. package/dist/search/SearchManager.js +0 -338
  87. package/dist/search/SearchSuggestions.d.ts +0 -27
  88. package/dist/search/SearchSuggestions.d.ts.map +0 -1
  89. package/dist/search/SearchSuggestions.js +0 -57
  90. package/dist/search/SemanticSearch.d.ts +0 -146
  91. package/dist/search/SemanticSearch.d.ts.map +0 -1
  92. package/dist/search/SemanticSearch.js +0 -313
  93. package/dist/search/TFIDFIndexManager.d.ts +0 -63
  94. package/dist/search/TFIDFIndexManager.d.ts.map +0 -1
  95. package/dist/search/TFIDFIndexManager.js +0 -215
  96. package/dist/search/VectorStore.d.ts +0 -235
  97. package/dist/search/VectorStore.d.ts.map +0 -1
  98. package/dist/search/VectorStore.js +0 -311
  99. package/dist/search/index.d.ts +0 -18
  100. package/dist/search/index.d.ts.map +0 -1
  101. package/dist/search/index.js +0 -21
  102. package/dist/types/index.d.ts +0 -13
  103. package/dist/types/index.d.ts.map +0 -1
  104. package/dist/types/index.js +0 -12
  105. package/dist/types/types.d.ts +0 -1234
  106. package/dist/types/types.d.ts.map +0 -1
  107. package/dist/types/types.js +0 -9
  108. package/dist/utils/compressedCache.d.ts +0 -192
  109. package/dist/utils/compressedCache.d.ts.map +0 -1
  110. package/dist/utils/compressedCache.js +0 -309
  111. package/dist/utils/compressionUtil.d.ts +0 -214
  112. package/dist/utils/compressionUtil.d.ts.map +0 -1
  113. package/dist/utils/compressionUtil.js +0 -247
  114. package/dist/utils/constants.d.ts +0 -245
  115. package/dist/utils/constants.d.ts.map +0 -1
  116. package/dist/utils/constants.js +0 -252
  117. package/dist/utils/entityUtils.d.ts +0 -321
  118. package/dist/utils/entityUtils.d.ts.map +0 -1
  119. package/dist/utils/entityUtils.js +0 -538
  120. package/dist/utils/errors.d.ts +0 -77
  121. package/dist/utils/errors.d.ts.map +0 -1
  122. package/dist/utils/errors.js +0 -121
  123. package/dist/utils/formatters.d.ts +0 -145
  124. package/dist/utils/formatters.d.ts.map +0 -1
  125. package/dist/utils/formatters.js +0 -132
  126. package/dist/utils/index.d.ts +0 -22
  127. package/dist/utils/index.d.ts.map +0 -1
  128. package/dist/utils/index.js +0 -75
  129. package/dist/utils/indexes.d.ts +0 -270
  130. package/dist/utils/indexes.d.ts.map +0 -1
  131. package/dist/utils/indexes.js +0 -526
  132. package/dist/utils/logger.d.ts +0 -24
  133. package/dist/utils/logger.d.ts.map +0 -1
  134. package/dist/utils/logger.js +0 -33
  135. package/dist/utils/parallelUtils.d.ts +0 -72
  136. package/dist/utils/parallelUtils.d.ts.map +0 -1
  137. package/dist/utils/parallelUtils.js +0 -169
  138. package/dist/utils/schemas.d.ts +0 -374
  139. package/dist/utils/schemas.d.ts.map +0 -1
  140. package/dist/utils/schemas.js +0 -484
  141. package/dist/utils/searchAlgorithms.d.ts +0 -99
  142. package/dist/utils/searchAlgorithms.d.ts.map +0 -1
  143. package/dist/utils/searchAlgorithms.js +0 -167
  144. package/dist/utils/searchCache.d.ts +0 -108
  145. package/dist/utils/searchCache.d.ts.map +0 -1
  146. package/dist/utils/searchCache.js +0 -209
  147. package/dist/utils/taskScheduler.d.ts +0 -290
  148. package/dist/utils/taskScheduler.d.ts.map +0 -1
  149. package/dist/utils/taskScheduler.js +0 -466
  150. package/dist/workers/WorkerPool.d.ts +0 -81
  151. package/dist/workers/WorkerPool.d.ts.map +0 -1
  152. package/dist/workers/WorkerPool.js +0 -121
  153. package/dist/workers/index.d.ts +0 -12
  154. package/dist/workers/index.d.ts.map +0 -1
  155. package/dist/workers/index.js +0 -9
  156. package/dist/workers/levenshteinWorker.d.ts +0 -60
  157. package/dist/workers/levenshteinWorker.d.ts.map +0 -1
  158. package/dist/workers/levenshteinWorker.js +0 -98
@@ -1,109 +0,0 @@
1
- /**
2
- * Compression Manager
3
- *
4
- * Handles duplicate detection, entity merging, and graph compression.
5
- * Extracted from SearchManager (Phase 4: Consolidate God Objects).
6
- *
7
- * @module features/CompressionManager
8
- */
9
- import type { Entity, CompressionResult, KnowledgeGraph } from '../types/index.js';
10
- import type { GraphStorage } from '../core/GraphStorage.js';
11
- /**
12
- * Manages compression operations for the knowledge graph.
13
- */
14
- export declare class CompressionManager {
15
- private storage;
16
- constructor(storage: GraphStorage);
17
- /**
18
- * Prepare an entity for efficient similarity comparisons.
19
- * Pre-computes all normalized data to avoid repeated computation.
20
- *
21
- * @param entity - The entity to prepare
22
- * @returns PreparedEntity with pre-computed data
23
- */
24
- private prepareEntity;
25
- /**
26
- * Prepare multiple entities for efficient similarity comparisons.
27
- * Use this before batch comparison operations.
28
- *
29
- * @param entities - Entities to prepare
30
- * @returns Map of entity name to PreparedEntity
31
- */
32
- private prepareEntities;
33
- /**
34
- * Calculate similarity between two entities using multiple heuristics.
35
- *
36
- * Uses configurable weights defined in SIMILARITY_WEIGHTS constant.
37
- * See SIMILARITY_WEIGHTS for the breakdown of scoring factors.
38
- *
39
- * NOTE: For batch comparisons, use prepareEntities() + calculatePreparedSimilarity() for better performance.
40
- *
41
- * @param e1 - First entity
42
- * @param e2 - Second entity
43
- * @returns Similarity score from 0 (completely different) to 1 (identical)
44
- */
45
- calculateEntitySimilarity(e1: Entity, e2: Entity): number;
46
- /**
47
- * Efficiently calculate intersection size of two Sets without creating a new Set.
48
- * Iterates over the smaller set for O(min(m,n)) complexity.
49
- */
50
- private setIntersectionSize;
51
- /**
52
- * Calculate similarity between two prepared entities.
53
- * OPTIMIZED: Uses pre-computed Sets to avoid O(n) set creation per comparison.
54
- *
55
- * @param p1 - First prepared entity
56
- * @param p2 - Second prepared entity
57
- * @returns Similarity score from 0 (completely different) to 1 (identical)
58
- */
59
- private calculatePreparedSimilarity;
60
- /**
61
- * Find duplicate entities in the graph based on similarity threshold.
62
- *
63
- * OPTIMIZED: Uses bucketing strategies to reduce O(n²) comparisons:
64
- * 1. Buckets entities by entityType (only compare same types)
65
- * 2. Within each type, buckets by name prefix (first 2 chars normalized)
66
- * 3. Only compares entities within same or adjacent buckets
67
- *
68
- * Complexity: O(n·k) where k is average bucket size (typically << n)
69
- *
70
- * @param threshold - Similarity threshold (0.0 to 1.0), default DEFAULT_DUPLICATE_THRESHOLD
71
- * @returns Array of duplicate groups (each group has similar entities)
72
- */
73
- findDuplicates(threshold?: number): Promise<string[][]>;
74
- /**
75
- * Merge a group of entities into a single entity.
76
- *
77
- * Merging strategy:
78
- * - First entity is kept (or renamed to targetName)
79
- * - Observations: Union of all observations
80
- * - Tags: Union of all tags
81
- * - Importance: Maximum importance value
82
- * - createdAt: Earliest date
83
- * - lastModified: Current timestamp
84
- * - Relations: Redirected to kept entity, duplicates removed
85
- *
86
- * @param entityNames - Names of entities to merge (first one is kept)
87
- * @param targetName - Optional new name for merged entity (default: first entity name)
88
- * @param options - Optional configuration
89
- * @param options.graph - Pre-loaded graph to use (avoids reload)
90
- * @param options.skipSave - If true, don't save (caller will save)
91
- * @returns The merged entity
92
- * @throws {InsufficientEntitiesError} If less than 2 entities provided
93
- * @throws {EntityNotFoundError} If any entity not found
94
- */
95
- mergeEntities(entityNames: string[], targetName?: string, options?: {
96
- graph?: KnowledgeGraph;
97
- skipSave?: boolean;
98
- }): Promise<Entity>;
99
- /**
100
- * Compress the knowledge graph by finding and merging duplicates.
101
- * OPTIMIZED: Loads graph once, performs all merges, saves once.
102
- *
103
- * @param threshold - Similarity threshold for duplicate detection (0.0 to 1.0), default DEFAULT_DUPLICATE_THRESHOLD
104
- * @param dryRun - If true, only report what would be compressed without applying changes
105
- * @returns Compression result with statistics
106
- */
107
- compressGraph(threshold?: number, dryRun?: boolean): Promise<CompressionResult>;
108
- }
109
- //# sourceMappingURL=CompressionManager.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"CompressionManager.d.ts","sourceRoot":"","sources":["../../src/features/CompressionManager.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAY,iBAAiB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAC7F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAsB5D;;GAEG;AACH,qBAAa,kBAAkB;IACjB,OAAO,CAAC,OAAO;gBAAP,OAAO,EAAE,YAAY;IAEzC;;;;;;OAMG;IACH,OAAO,CAAC,aAAa;IAUrB;;;;;;OAMG;IACH,OAAO,CAAC,eAAe;IAQvB;;;;;;;;;;;OAWG;IACH,yBAAyB,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM;IAwCzD;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IAU3B;;;;;;;OAOG;IACH,OAAO,CAAC,2BAA2B;IAoCnC;;;;;;;;;;;;OAYG;IACG,cAAc,CAAC,SAAS,GAAE,MAAoC,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;IAiF1F;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,aAAa,CACjB,WAAW,EAAE,MAAM,EAAE,EACrB,UAAU,CAAC,EAAE,MAAM,EACnB,OAAO,GAAE;QACP,KAAK,CAAC,EAAE,cAAc,CAAC;QACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;KACf,GACL,OAAO,CAAC,MAAM,CAAC;IA8FlB;;;;;;;OAOG;IACG,aAAa,CAAC,SAAS,GAAE,MAAoC,EAAE,MAAM,GAAE,OAAe,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAoE1H"}
@@ -1,404 +0,0 @@
1
- /**
2
- * Compression Manager
3
- *
4
- * Handles duplicate detection, entity merging, and graph compression.
5
- * Extracted from SearchManager (Phase 4: Consolidate God Objects).
6
- *
7
- * @module features/CompressionManager
8
- */
9
- import { levenshteinDistance } from '../utils/index.js';
10
- import { EntityNotFoundError, InsufficientEntitiesError } from '../utils/errors.js';
11
- import { SIMILARITY_WEIGHTS, DEFAULT_DUPLICATE_THRESHOLD } from '../utils/constants.js';
12
- /**
13
- * Manages compression operations for the knowledge graph.
14
- */
15
- export class CompressionManager {
16
- storage;
17
- constructor(storage) {
18
- this.storage = storage;
19
- }
20
- /**
21
- * Prepare an entity for efficient similarity comparisons.
22
- * Pre-computes all normalized data to avoid repeated computation.
23
- *
24
- * @param entity - The entity to prepare
25
- * @returns PreparedEntity with pre-computed data
26
- */
27
- prepareEntity(entity) {
28
- return {
29
- entity,
30
- nameLower: entity.name.toLowerCase(),
31
- typeLower: entity.entityType.toLowerCase(),
32
- observationSet: new Set(entity.observations.map(o => o.toLowerCase())),
33
- tagSet: new Set((entity.tags ?? []).map(t => t.toLowerCase())),
34
- };
35
- }
36
- /**
37
- * Prepare multiple entities for efficient similarity comparisons.
38
- * Use this before batch comparison operations.
39
- *
40
- * @param entities - Entities to prepare
41
- * @returns Map of entity name to PreparedEntity
42
- */
43
- prepareEntities(entities) {
44
- const prepared = new Map();
45
- for (const entity of entities) {
46
- prepared.set(entity.name, this.prepareEntity(entity));
47
- }
48
- return prepared;
49
- }
50
- /**
51
- * Calculate similarity between two entities using multiple heuristics.
52
- *
53
- * Uses configurable weights defined in SIMILARITY_WEIGHTS constant.
54
- * See SIMILARITY_WEIGHTS for the breakdown of scoring factors.
55
- *
56
- * NOTE: For batch comparisons, use prepareEntities() + calculatePreparedSimilarity() for better performance.
57
- *
58
- * @param e1 - First entity
59
- * @param e2 - Second entity
60
- * @returns Similarity score from 0 (completely different) to 1 (identical)
61
- */
62
- calculateEntitySimilarity(e1, e2) {
63
- let score = 0;
64
- let factors = 0;
65
- // Name similarity (Levenshtein-based)
66
- const nameDistance = levenshteinDistance(e1.name.toLowerCase(), e2.name.toLowerCase());
67
- const maxNameLength = Math.max(e1.name.length, e2.name.length);
68
- const nameSimilarity = 1 - nameDistance / maxNameLength;
69
- score += nameSimilarity * SIMILARITY_WEIGHTS.NAME;
70
- factors += SIMILARITY_WEIGHTS.NAME;
71
- // Type similarity (exact match)
72
- if (e1.entityType.toLowerCase() === e2.entityType.toLowerCase()) {
73
- score += SIMILARITY_WEIGHTS.TYPE;
74
- }
75
- factors += SIMILARITY_WEIGHTS.TYPE;
76
- // Observation overlap (Jaccard similarity)
77
- const obs1Set = new Set(e1.observations.map(o => o.toLowerCase()));
78
- const obs2Set = new Set(e2.observations.map(o => o.toLowerCase()));
79
- const intersection = new Set([...obs1Set].filter(x => obs2Set.has(x)));
80
- const union = new Set([...obs1Set, ...obs2Set]);
81
- const observationSimilarity = union.size > 0 ? intersection.size / union.size : 0;
82
- score += observationSimilarity * SIMILARITY_WEIGHTS.OBSERVATIONS;
83
- factors += SIMILARITY_WEIGHTS.OBSERVATIONS;
84
- // Tag overlap (Jaccard similarity)
85
- if (e1.tags && e2.tags && (e1.tags.length > 0 || e2.tags.length > 0)) {
86
- const tags1Set = new Set(e1.tags.map(t => t.toLowerCase()));
87
- const tags2Set = new Set(e2.tags.map(t => t.toLowerCase()));
88
- const tagIntersection = new Set([...tags1Set].filter(x => tags2Set.has(x)));
89
- const tagUnion = new Set([...tags1Set, ...tags2Set]);
90
- const tagSimilarity = tagUnion.size > 0 ? tagIntersection.size / tagUnion.size : 0;
91
- score += tagSimilarity * SIMILARITY_WEIGHTS.TAGS;
92
- factors += SIMILARITY_WEIGHTS.TAGS;
93
- }
94
- return factors > 0 ? score / factors : 0;
95
- }
96
- /**
97
- * Efficiently calculate intersection size of two Sets without creating a new Set.
98
- * Iterates over the smaller set for O(min(m,n)) complexity.
99
- */
100
- setIntersectionSize(a, b) {
101
- // Always iterate over smaller set
102
- const [smaller, larger] = a.size <= b.size ? [a, b] : [b, a];
103
- let count = 0;
104
- for (const item of smaller) {
105
- if (larger.has(item))
106
- count++;
107
- }
108
- return count;
109
- }
110
- /**
111
- * Calculate similarity between two prepared entities.
112
- * OPTIMIZED: Uses pre-computed Sets to avoid O(n) set creation per comparison.
113
- *
114
- * @param p1 - First prepared entity
115
- * @param p2 - Second prepared entity
116
- * @returns Similarity score from 0 (completely different) to 1 (identical)
117
- */
118
- calculatePreparedSimilarity(p1, p2) {
119
- let score = 0;
120
- let factors = 0;
121
- // Name similarity (Levenshtein-based) - use pre-computed lowercase
122
- const nameDistance = levenshteinDistance(p1.nameLower, p2.nameLower);
123
- const maxNameLength = Math.max(p1.nameLower.length, p2.nameLower.length);
124
- const nameSimilarity = 1 - nameDistance / maxNameLength;
125
- score += nameSimilarity * SIMILARITY_WEIGHTS.NAME;
126
- factors += SIMILARITY_WEIGHTS.NAME;
127
- // Type similarity (exact match) - use pre-computed lowercase
128
- if (p1.typeLower === p2.typeLower) {
129
- score += SIMILARITY_WEIGHTS.TYPE;
130
- }
131
- factors += SIMILARITY_WEIGHTS.TYPE;
132
- // Observation overlap (Jaccard similarity) - use pre-computed Sets
133
- const obsIntersectionSize = this.setIntersectionSize(p1.observationSet, p2.observationSet);
134
- const obsUnionSize = p1.observationSet.size + p2.observationSet.size - obsIntersectionSize;
135
- const observationSimilarity = obsUnionSize > 0 ? obsIntersectionSize / obsUnionSize : 0;
136
- score += observationSimilarity * SIMILARITY_WEIGHTS.OBSERVATIONS;
137
- factors += SIMILARITY_WEIGHTS.OBSERVATIONS;
138
- // Tag overlap (Jaccard similarity) - use pre-computed Sets
139
- if (p1.tagSet.size > 0 || p2.tagSet.size > 0) {
140
- const tagIntersectionSize = this.setIntersectionSize(p1.tagSet, p2.tagSet);
141
- const tagUnionSize = p1.tagSet.size + p2.tagSet.size - tagIntersectionSize;
142
- const tagSimilarity = tagUnionSize > 0 ? tagIntersectionSize / tagUnionSize : 0;
143
- score += tagSimilarity * SIMILARITY_WEIGHTS.TAGS;
144
- factors += SIMILARITY_WEIGHTS.TAGS;
145
- }
146
- return factors > 0 ? score / factors : 0;
147
- }
148
- /**
149
- * Find duplicate entities in the graph based on similarity threshold.
150
- *
151
- * OPTIMIZED: Uses bucketing strategies to reduce O(n²) comparisons:
152
- * 1. Buckets entities by entityType (only compare same types)
153
- * 2. Within each type, buckets by name prefix (first 2 chars normalized)
154
- * 3. Only compares entities within same or adjacent buckets
155
- *
156
- * Complexity: O(n·k) where k is average bucket size (typically << n)
157
- *
158
- * @param threshold - Similarity threshold (0.0 to 1.0), default DEFAULT_DUPLICATE_THRESHOLD
159
- * @returns Array of duplicate groups (each group has similar entities)
160
- */
161
- async findDuplicates(threshold = DEFAULT_DUPLICATE_THRESHOLD) {
162
- const graph = await this.storage.loadGraph();
163
- const duplicateGroups = [];
164
- const processed = new Set();
165
- // OPTIMIZATION: Pre-prepare all entities once before comparisons
166
- const preparedEntities = this.prepareEntities(graph.entities);
167
- // Step 1: Bucket entities by type (reduces comparisons drastically)
168
- const typeMap = new Map();
169
- for (const entity of graph.entities) {
170
- const normalizedType = entity.entityType.toLowerCase();
171
- if (!typeMap.has(normalizedType)) {
172
- typeMap.set(normalizedType, []);
173
- }
174
- typeMap.get(normalizedType).push(entity);
175
- }
176
- // Step 2: For each type bucket, sub-bucket by name prefix
177
- for (const entities of typeMap.values()) {
178
- // Skip single-entity types (no duplicates possible)
179
- if (entities.length < 2)
180
- continue;
181
- // Create name prefix buckets (first 2 chars, normalized)
182
- const prefixMap = new Map();
183
- for (const entity of entities) {
184
- const prefix = entity.name.toLowerCase().slice(0, 2);
185
- if (!prefixMap.has(prefix)) {
186
- prefixMap.set(prefix, []);
187
- }
188
- prefixMap.get(prefix).push(entity);
189
- }
190
- // Step 3: Compare only within buckets (or adjacent buckets for fuzzy matching)
191
- const prefixKeys = Array.from(prefixMap.keys()).sort();
192
- for (let bucketIdx = 0; bucketIdx < prefixKeys.length; bucketIdx++) {
193
- const currentPrefix = prefixKeys[bucketIdx];
194
- const currentBucket = prefixMap.get(currentPrefix);
195
- // Collect entities to compare: current bucket + adjacent buckets
196
- const candidateEntities = [...currentBucket];
197
- // Add next bucket if exists (handles fuzzy prefix matching)
198
- if (bucketIdx + 1 < prefixKeys.length) {
199
- candidateEntities.push(...prefixMap.get(prefixKeys[bucketIdx + 1]));
200
- }
201
- // Compare entities within candidate pool
202
- for (let i = 0; i < currentBucket.length; i++) {
203
- const entity1 = currentBucket[i];
204
- if (processed.has(entity1.name))
205
- continue;
206
- // OPTIMIZATION: Use prepared entity for comparison
207
- const prepared1 = preparedEntities.get(entity1.name);
208
- const group = [entity1.name];
209
- for (let j = 0; j < candidateEntities.length; j++) {
210
- const entity2 = candidateEntities[j];
211
- if (entity1.name === entity2.name || processed.has(entity2.name))
212
- continue;
213
- // OPTIMIZATION: Use prepared entity and optimized similarity
214
- const prepared2 = preparedEntities.get(entity2.name);
215
- const similarity = this.calculatePreparedSimilarity(prepared1, prepared2);
216
- if (similarity >= threshold) {
217
- group.push(entity2.name);
218
- processed.add(entity2.name);
219
- }
220
- }
221
- if (group.length > 1) {
222
- duplicateGroups.push(group);
223
- processed.add(entity1.name);
224
- }
225
- }
226
- }
227
- }
228
- return duplicateGroups;
229
- }
230
- /**
231
- * Merge a group of entities into a single entity.
232
- *
233
- * Merging strategy:
234
- * - First entity is kept (or renamed to targetName)
235
- * - Observations: Union of all observations
236
- * - Tags: Union of all tags
237
- * - Importance: Maximum importance value
238
- * - createdAt: Earliest date
239
- * - lastModified: Current timestamp
240
- * - Relations: Redirected to kept entity, duplicates removed
241
- *
242
- * @param entityNames - Names of entities to merge (first one is kept)
243
- * @param targetName - Optional new name for merged entity (default: first entity name)
244
- * @param options - Optional configuration
245
- * @param options.graph - Pre-loaded graph to use (avoids reload)
246
- * @param options.skipSave - If true, don't save (caller will save)
247
- * @returns The merged entity
248
- * @throws {InsufficientEntitiesError} If less than 2 entities provided
249
- * @throws {EntityNotFoundError} If any entity not found
250
- */
251
- async mergeEntities(entityNames, targetName, options = {}) {
252
- if (entityNames.length < 2) {
253
- throw new InsufficientEntitiesError('merging', 2, entityNames.length);
254
- }
255
- // Use provided graph or load fresh
256
- const graph = options.graph ?? await this.storage.getGraphForMutation();
257
- const entitiesToMerge = entityNames.map(name => {
258
- const entity = graph.entities.find(e => e.name === name);
259
- if (!entity) {
260
- throw new EntityNotFoundError(name);
261
- }
262
- return entity;
263
- });
264
- const keepEntity = entitiesToMerge[0];
265
- const mergeEntities = entitiesToMerge.slice(1);
266
- // Merge observations (unique)
267
- const allObservations = new Set();
268
- for (const entity of entitiesToMerge) {
269
- entity.observations.forEach(obs => allObservations.add(obs));
270
- }
271
- keepEntity.observations = Array.from(allObservations);
272
- // Merge tags (unique)
273
- const allTags = new Set();
274
- for (const entity of entitiesToMerge) {
275
- if (entity.tags) {
276
- entity.tags.forEach(tag => allTags.add(tag));
277
- }
278
- }
279
- if (allTags.size > 0) {
280
- keepEntity.tags = Array.from(allTags);
281
- }
282
- // Use highest importance
283
- const importances = entitiesToMerge
284
- .map(e => e.importance)
285
- .filter(imp => imp !== undefined);
286
- if (importances.length > 0) {
287
- keepEntity.importance = Math.max(...importances);
288
- }
289
- // Use earliest createdAt
290
- const createdDates = entitiesToMerge
291
- .map(e => e.createdAt)
292
- .filter(date => date !== undefined);
293
- if (createdDates.length > 0) {
294
- keepEntity.createdAt = createdDates.sort()[0];
295
- }
296
- // Update lastModified
297
- keepEntity.lastModified = new Date().toISOString();
298
- // Rename if requested
299
- if (targetName && targetName !== keepEntity.name) {
300
- // Update all relations pointing to old name
301
- graph.relations.forEach(rel => {
302
- if (rel.from === keepEntity.name)
303
- rel.from = targetName;
304
- if (rel.to === keepEntity.name)
305
- rel.to = targetName;
306
- });
307
- keepEntity.name = targetName;
308
- }
309
- // Update relations from merged entities to point to kept entity
310
- for (const mergeEntity of mergeEntities) {
311
- graph.relations.forEach(rel => {
312
- if (rel.from === mergeEntity.name)
313
- rel.from = keepEntity.name;
314
- if (rel.to === mergeEntity.name)
315
- rel.to = keepEntity.name;
316
- });
317
- }
318
- // Remove duplicate relations
319
- const uniqueRelations = new Map();
320
- for (const relation of graph.relations) {
321
- const key = `${relation.from}|${relation.to}|${relation.relationType}`;
322
- if (!uniqueRelations.has(key)) {
323
- uniqueRelations.set(key, relation);
324
- }
325
- }
326
- graph.relations = Array.from(uniqueRelations.values());
327
- // Remove merged entities
328
- const mergeNames = new Set(mergeEntities.map(e => e.name));
329
- graph.entities = graph.entities.filter(e => !mergeNames.has(e.name));
330
- // Save unless caller said to skip
331
- if (!options.skipSave) {
332
- await this.storage.saveGraph(graph);
333
- }
334
- return keepEntity;
335
- }
336
- /**
337
- * Compress the knowledge graph by finding and merging duplicates.
338
- * OPTIMIZED: Loads graph once, performs all merges, saves once.
339
- *
340
- * @param threshold - Similarity threshold for duplicate detection (0.0 to 1.0), default DEFAULT_DUPLICATE_THRESHOLD
341
- * @param dryRun - If true, only report what would be compressed without applying changes
342
- * @returns Compression result with statistics
343
- */
344
- async compressGraph(threshold = DEFAULT_DUPLICATE_THRESHOLD, dryRun = false) {
345
- const duplicateGroups = await this.findDuplicates(threshold);
346
- // OPTIMIZATION: Load graph once for all operations
347
- const graph = await this.storage.getGraphForMutation();
348
- const initialSize = JSON.stringify(graph).length;
349
- const result = {
350
- duplicatesFound: duplicateGroups.reduce((sum, group) => sum + group.length, 0),
351
- entitiesMerged: 0,
352
- observationsCompressed: 0,
353
- relationsConsolidated: 0,
354
- spaceFreed: 0,
355
- mergedEntities: [],
356
- };
357
- if (dryRun) {
358
- // Just report what would happen
359
- for (const group of duplicateGroups) {
360
- result.mergedEntities.push({
361
- kept: group[0],
362
- merged: group.slice(1),
363
- });
364
- result.entitiesMerged += group.length - 1;
365
- }
366
- return result;
367
- }
368
- // Merge all duplicates using the same graph instance
369
- for (const group of duplicateGroups) {
370
- try {
371
- // Count observations before merge using loaded graph
372
- let totalObservationsBefore = 0;
373
- for (const name of group) {
374
- const entity = graph.entities.find(e => e.name === name);
375
- if (entity) {
376
- totalObservationsBefore += entity.observations.length;
377
- }
378
- }
379
- // OPTIMIZATION: Pass graph and skip individual saves
380
- const mergedEntity = await this.mergeEntities(group, undefined, {
381
- graph,
382
- skipSave: true,
383
- });
384
- const observationsAfter = mergedEntity.observations.length;
385
- result.observationsCompressed += totalObservationsBefore - observationsAfter;
386
- result.mergedEntities.push({
387
- kept: group[0],
388
- merged: group.slice(1),
389
- });
390
- result.entitiesMerged += group.length - 1;
391
- }
392
- catch (error) {
393
- // Skip groups that fail to merge
394
- console.error(`Failed to merge group ${group}:`, error);
395
- }
396
- }
397
- // OPTIMIZATION: Save once after all merges complete
398
- await this.storage.saveGraph(graph);
399
- const finalSize = JSON.stringify(graph).length;
400
- result.spaceFreed = initialSize - finalSize;
401
- result.relationsConsolidated = result.entitiesMerged;
402
- return result;
403
- }
404
- }