@danielsimonjr/memory-mcp 0.48.0 → 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 (209) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +2000 -194
  3. package/dist/__tests__/file-path.test.js +7 -11
  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 +3 -2
  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 +10 -10
  64. package/dist/memory.jsonl +1 -26
  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 +188 -17
  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/memory-saved-searches.jsonl +0 -0
  195. package/dist/memory-tag-aliases.jsonl +0 -0
  196. package/dist/types/analytics.types.js +0 -6
  197. package/dist/types/entity.types.js +0 -7
  198. package/dist/types/import-export.types.js +0 -7
  199. package/dist/types/search.types.js +0 -7
  200. package/dist/types/tag.types.js +0 -6
  201. package/dist/utils/dateUtils.js +0 -89
  202. package/dist/utils/filterUtils.js +0 -155
  203. package/dist/utils/levenshtein.js +0 -62
  204. package/dist/utils/pathUtils.js +0 -115
  205. package/dist/utils/responseFormatter.js +0 -55
  206. package/dist/utils/tagUtils.js +0 -107
  207. package/dist/utils/tfidf.js +0 -90
  208. package/dist/utils/validationHelper.js +0 -99
  209. package/dist/utils/validationUtils.js +0 -109
@@ -0,0 +1,917 @@
1
+ /**
2
+ * SQLite Storage
3
+ *
4
+ * Handles storage operations for the knowledge graph using better-sqlite3 (native SQLite).
5
+ * Implements IGraphStorage interface for storage abstraction.
6
+ *
7
+ * Benefits over sql.js (WASM):
8
+ * - 3-10x faster than WASM-based SQLite
9
+ * - Native FTS5 full-text search support
10
+ * - ACID transactions with proper durability
11
+ * - Concurrent read access support
12
+ * - No memory overhead from WASM runtime
13
+ * - Direct disk I/O (no manual export/import)
14
+ *
15
+ * Features:
16
+ * - Built-in indexes for O(1) lookups
17
+ * - Referential integrity with ON DELETE CASCADE
18
+ * - FTS5 full-text search on entity names and observations
19
+ *
20
+ * @module core/SQLiteStorage
21
+ */
22
+ import Database from 'better-sqlite3';
23
+ import { Mutex } from 'async-mutex';
24
+ import { clearAllSearchCaches } from '../utils/searchCache.js';
25
+ import { NameIndex, TypeIndex } from '../utils/indexes.js';
26
+ /**
27
+ * SQLiteStorage manages persistence of the knowledge graph using native SQLite.
28
+ *
29
+ * Uses better-sqlite3 for native SQLite bindings with full FTS5 support,
30
+ * referential integrity, and proper ACID transactions.
31
+ *
32
+ * @example
33
+ * ```typescript
34
+ * const storage = new SQLiteStorage('/path/to/memory.db');
35
+ * await storage.ensureLoaded();
36
+ * const graph = await storage.loadGraph();
37
+ * ```
38
+ */
39
+ export class SQLiteStorage {
40
+ dbFilePath;
41
+ /**
42
+ * Mutex for thread-safe access to storage operations.
43
+ * Prevents concurrent writes from corrupting the cache.
44
+ * Note: SQLite itself handles file-level locking, but we need
45
+ * to protect our in-memory cache and index operations.
46
+ */
47
+ mutex = new Mutex();
48
+ /**
49
+ * SQLite database instance.
50
+ */
51
+ db = null;
52
+ /**
53
+ * Whether the database has been initialized.
54
+ */
55
+ initialized = false;
56
+ /**
57
+ * In-memory cache for fast read operations.
58
+ * Synchronized with SQLite on writes.
59
+ */
60
+ cache = null;
61
+ /**
62
+ * O(1) entity lookup by name.
63
+ */
64
+ nameIndex = new NameIndex();
65
+ /**
66
+ * O(1) entity lookup by type.
67
+ */
68
+ typeIndex = new TypeIndex();
69
+ /**
70
+ * Pre-computed lowercase data for search optimization.
71
+ */
72
+ lowercaseCache = new Map();
73
+ /**
74
+ * Pending changes counter for batching disk writes.
75
+ * Note: better-sqlite3 writes to disk immediately, but we track for API compatibility.
76
+ */
77
+ pendingChanges = 0;
78
+ /**
79
+ * Phase 4 Sprint 1: Bidirectional relation cache for O(1) repeated lookups.
80
+ * Maps entity name -> all relations involving that entity (both incoming and outgoing).
81
+ */
82
+ bidirectionalRelationCache = new Map();
83
+ /**
84
+ * Create a new SQLiteStorage instance.
85
+ *
86
+ * @param dbFilePath - Absolute path to the SQLite database file
87
+ */
88
+ constructor(dbFilePath) {
89
+ this.dbFilePath = dbFilePath;
90
+ }
91
+ /**
92
+ * Initialize the database connection and schema.
93
+ */
94
+ initialize() {
95
+ if (this.initialized)
96
+ return;
97
+ // Open database (creates file if it doesn't exist)
98
+ this.db = new Database(this.dbFilePath);
99
+ // Enable foreign keys and WAL mode for better performance
100
+ this.db.pragma('foreign_keys = ON');
101
+ this.db.pragma('journal_mode = WAL');
102
+ // Create tables and indexes
103
+ this.createTables();
104
+ // Load cache from database
105
+ this.loadCache();
106
+ this.initialized = true;
107
+ }
108
+ /**
109
+ * Create database tables, indexes, and FTS5 virtual table.
110
+ */
111
+ createTables() {
112
+ if (!this.db)
113
+ throw new Error('Database not initialized');
114
+ // Entities table with referential integrity for parentId
115
+ this.db.exec(`
116
+ CREATE TABLE IF NOT EXISTS entities (
117
+ name TEXT PRIMARY KEY,
118
+ entityType TEXT NOT NULL,
119
+ observations TEXT NOT NULL,
120
+ tags TEXT,
121
+ importance INTEGER,
122
+ parentId TEXT REFERENCES entities(name) ON DELETE SET NULL,
123
+ createdAt TEXT NOT NULL,
124
+ lastModified TEXT NOT NULL
125
+ )
126
+ `);
127
+ // Relations table with referential integrity (CASCADE delete)
128
+ this.db.exec(`
129
+ CREATE TABLE IF NOT EXISTS relations (
130
+ fromEntity TEXT NOT NULL REFERENCES entities(name) ON DELETE CASCADE,
131
+ toEntity TEXT NOT NULL REFERENCES entities(name) ON DELETE CASCADE,
132
+ relationType TEXT NOT NULL,
133
+ createdAt TEXT NOT NULL,
134
+ lastModified TEXT NOT NULL,
135
+ PRIMARY KEY (fromEntity, toEntity, relationType)
136
+ )
137
+ `);
138
+ // Indexes for fast lookups
139
+ this.db.exec(`CREATE INDEX IF NOT EXISTS idx_entity_type ON entities(entityType)`);
140
+ this.db.exec(`CREATE INDEX IF NOT EXISTS idx_entity_parent ON entities(parentId)`);
141
+ this.db.exec(`CREATE INDEX IF NOT EXISTS idx_relation_from ON relations(fromEntity)`);
142
+ this.db.exec(`CREATE INDEX IF NOT EXISTS idx_relation_to ON relations(toEntity)`);
143
+ // Phase 4 Sprint 1: Additional indexes for range queries (O(n) -> O(log n))
144
+ this.db.exec(`CREATE INDEX IF NOT EXISTS idx_entity_importance ON entities(importance)`);
145
+ this.db.exec(`CREATE INDEX IF NOT EXISTS idx_entity_lastmodified ON entities(lastModified)`);
146
+ this.db.exec(`CREATE INDEX IF NOT EXISTS idx_entity_createdat ON entities(createdAt)`);
147
+ this.db.exec(`CREATE INDEX IF NOT EXISTS idx_relation_type ON relations(relationType)`);
148
+ // Composite index for common query patterns (type + importance filtering)
149
+ this.db.exec(`CREATE INDEX IF NOT EXISTS idx_entity_type_importance ON entities(entityType, importance)`);
150
+ // FTS5 virtual table for full-text search
151
+ // content='' makes it an external content table (we manage content ourselves)
152
+ this.db.exec(`
153
+ CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
154
+ name,
155
+ entityType,
156
+ observations,
157
+ tags,
158
+ content='entities',
159
+ content_rowid='rowid'
160
+ )
161
+ `);
162
+ // Triggers to keep FTS5 index in sync with entities table
163
+ this.db.exec(`
164
+ CREATE TRIGGER IF NOT EXISTS entities_ai AFTER INSERT ON entities BEGIN
165
+ INSERT INTO entities_fts(rowid, name, entityType, observations, tags)
166
+ VALUES (NEW.rowid, NEW.name, NEW.entityType, NEW.observations, NEW.tags);
167
+ END
168
+ `);
169
+ this.db.exec(`
170
+ CREATE TRIGGER IF NOT EXISTS entities_ad AFTER DELETE ON entities BEGIN
171
+ INSERT INTO entities_fts(entities_fts, rowid, name, entityType, observations, tags)
172
+ VALUES ('delete', OLD.rowid, OLD.name, OLD.entityType, OLD.observations, OLD.tags);
173
+ END
174
+ `);
175
+ this.db.exec(`
176
+ CREATE TRIGGER IF NOT EXISTS entities_au AFTER UPDATE ON entities BEGIN
177
+ INSERT INTO entities_fts(entities_fts, rowid, name, entityType, observations, tags)
178
+ VALUES ('delete', OLD.rowid, OLD.name, OLD.entityType, OLD.observations, OLD.tags);
179
+ INSERT INTO entities_fts(rowid, name, entityType, observations, tags)
180
+ VALUES (NEW.rowid, NEW.name, NEW.entityType, NEW.observations, NEW.tags);
181
+ END
182
+ `);
183
+ }
184
+ /**
185
+ * Load all data from SQLite into memory cache.
186
+ */
187
+ loadCache() {
188
+ if (!this.db)
189
+ throw new Error('Database not initialized');
190
+ const entities = [];
191
+ const relations = [];
192
+ // Load entities
193
+ const entityRows = this.db.prepare(`SELECT * FROM entities`).all();
194
+ for (const row of entityRows) {
195
+ const entity = this.rowToEntity(row);
196
+ entities.push(entity);
197
+ this.updateLowercaseCache(entity);
198
+ }
199
+ // Load relations
200
+ const relationRows = this.db.prepare(`SELECT * FROM relations`).all();
201
+ for (const row of relationRows) {
202
+ relations.push(this.rowToRelation(row));
203
+ }
204
+ this.cache = { entities, relations };
205
+ // Build indexes for O(1) lookups
206
+ this.nameIndex.build(entities);
207
+ this.typeIndex.build(entities);
208
+ }
209
+ /**
210
+ * Convert a database row to an Entity object.
211
+ */
212
+ rowToEntity(row) {
213
+ return {
214
+ name: row.name,
215
+ entityType: row.entityType,
216
+ observations: JSON.parse(row.observations),
217
+ tags: row.tags ? JSON.parse(row.tags) : undefined,
218
+ importance: row.importance ?? undefined,
219
+ parentId: row.parentId ?? undefined,
220
+ createdAt: row.createdAt,
221
+ lastModified: row.lastModified,
222
+ };
223
+ }
224
+ /**
225
+ * Convert a database row to a Relation object.
226
+ */
227
+ rowToRelation(row) {
228
+ return {
229
+ from: row.fromEntity,
230
+ to: row.toEntity,
231
+ relationType: row.relationType,
232
+ createdAt: row.createdAt,
233
+ lastModified: row.lastModified,
234
+ };
235
+ }
236
+ /**
237
+ * Update lowercase cache for an entity.
238
+ */
239
+ updateLowercaseCache(entity) {
240
+ this.lowercaseCache.set(entity.name, {
241
+ name: entity.name.toLowerCase(),
242
+ entityType: entity.entityType.toLowerCase(),
243
+ observations: entity.observations.map(o => o.toLowerCase()),
244
+ tags: entity.tags?.map(t => t.toLowerCase()) || [],
245
+ });
246
+ }
247
+ // ==================== IGraphStorage Implementation ====================
248
+ /**
249
+ * Load the knowledge graph (read-only access).
250
+ *
251
+ * @returns Promise resolving to read-only knowledge graph reference
252
+ */
253
+ async loadGraph() {
254
+ await this.ensureLoaded();
255
+ return this.cache;
256
+ }
257
+ /**
258
+ * Get a mutable copy of the graph for write operations.
259
+ *
260
+ * @returns Promise resolving to mutable knowledge graph copy
261
+ */
262
+ async getGraphForMutation() {
263
+ await this.ensureLoaded();
264
+ return {
265
+ entities: this.cache.entities.map(e => ({
266
+ ...e,
267
+ observations: [...e.observations],
268
+ tags: e.tags ? [...e.tags] : undefined,
269
+ })),
270
+ relations: this.cache.relations.map(r => ({ ...r })),
271
+ };
272
+ }
273
+ /**
274
+ * Ensure the storage is loaded/initialized.
275
+ *
276
+ * @returns Promise resolving when ready
277
+ */
278
+ async ensureLoaded() {
279
+ if (!this.initialized) {
280
+ this.initialize();
281
+ }
282
+ }
283
+ /**
284
+ * Phase 4 Sprint 1: Invalidate bidirectional relation cache for an entity.
285
+ *
286
+ * @param entityName - Entity name to invalidate cache for
287
+ */
288
+ invalidateBidirectionalCache(entityName) {
289
+ this.bidirectionalRelationCache.delete(entityName);
290
+ }
291
+ /**
292
+ * Phase 4 Sprint 1: Clear the entire bidirectional relation cache.
293
+ */
294
+ clearBidirectionalCache() {
295
+ this.bidirectionalRelationCache.clear();
296
+ }
297
+ /**
298
+ * Save the entire knowledge graph to storage.
299
+ *
300
+ * THREAD-SAFE: Uses mutex to prevent concurrent write operations.
301
+ *
302
+ * @param graph - The knowledge graph to save
303
+ * @returns Promise resolving when save is complete
304
+ */
305
+ async saveGraph(graph) {
306
+ await this.ensureLoaded();
307
+ return this.mutex.runExclusive(async () => {
308
+ if (!this.db)
309
+ throw new Error('Database not initialized');
310
+ // Disable foreign keys for bulk replace operation
311
+ // This allows inserting entities with parentId references that may not exist
312
+ // and relations with dangling references (which matches the original JSONL behavior)
313
+ this.db.pragma('foreign_keys = OFF');
314
+ // Use transaction for atomicity
315
+ const transaction = this.db.transaction(() => {
316
+ // Clear existing data
317
+ this.db.exec('DELETE FROM relations');
318
+ this.db.exec('DELETE FROM entities');
319
+ // Insert all entities
320
+ const entityStmt = this.db.prepare(`
321
+ INSERT INTO entities (name, entityType, observations, tags, importance, parentId, createdAt, lastModified)
322
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
323
+ `);
324
+ for (const entity of graph.entities) {
325
+ entityStmt.run(entity.name, entity.entityType, JSON.stringify(entity.observations), entity.tags ? JSON.stringify(entity.tags) : null, entity.importance ?? null, entity.parentId ?? null, entity.createdAt || new Date().toISOString(), entity.lastModified || new Date().toISOString());
326
+ }
327
+ // Insert all relations
328
+ const relationStmt = this.db.prepare(`
329
+ INSERT INTO relations (fromEntity, toEntity, relationType, createdAt, lastModified)
330
+ VALUES (?, ?, ?, ?, ?)
331
+ `);
332
+ for (const relation of graph.relations) {
333
+ relationStmt.run(relation.from, relation.to, relation.relationType, relation.createdAt || new Date().toISOString(), relation.lastModified || new Date().toISOString());
334
+ }
335
+ });
336
+ transaction();
337
+ // Re-enable foreign keys for future operations
338
+ this.db.pragma('foreign_keys = ON');
339
+ // Update cache
340
+ this.cache = graph;
341
+ this.lowercaseCache.clear();
342
+ for (const entity of graph.entities) {
343
+ this.updateLowercaseCache(entity);
344
+ }
345
+ // Rebuild indexes
346
+ this.nameIndex.build(graph.entities);
347
+ this.typeIndex.build(graph.entities);
348
+ this.pendingChanges = 0;
349
+ // Clear search caches
350
+ clearAllSearchCaches();
351
+ // Phase 4 Sprint 1: Clear bidirectional relation cache on full save
352
+ this.clearBidirectionalCache();
353
+ });
354
+ }
355
+ /**
356
+ * Append a single entity to storage.
357
+ *
358
+ * THREAD-SAFE: Uses mutex to prevent concurrent write operations.
359
+ *
360
+ * @param entity - The entity to append
361
+ * @returns Promise resolving when append is complete
362
+ */
363
+ async appendEntity(entity) {
364
+ await this.ensureLoaded();
365
+ return this.mutex.runExclusive(async () => {
366
+ if (!this.db)
367
+ throw new Error('Database not initialized');
368
+ // Use INSERT OR REPLACE to handle updates
369
+ const stmt = this.db.prepare(`
370
+ INSERT OR REPLACE INTO entities (name, entityType, observations, tags, importance, parentId, createdAt, lastModified)
371
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
372
+ `);
373
+ stmt.run(entity.name, entity.entityType, JSON.stringify(entity.observations), entity.tags ? JSON.stringify(entity.tags) : null, entity.importance ?? null, entity.parentId ?? null, entity.createdAt || new Date().toISOString(), entity.lastModified || new Date().toISOString());
374
+ // Update cache
375
+ const existingIndex = this.cache.entities.findIndex(e => e.name === entity.name);
376
+ if (existingIndex >= 0) {
377
+ this.cache.entities[existingIndex] = entity;
378
+ }
379
+ else {
380
+ this.cache.entities.push(entity);
381
+ }
382
+ // Update indexes
383
+ this.nameIndex.add(entity);
384
+ this.typeIndex.add(entity);
385
+ this.updateLowercaseCache(entity);
386
+ clearAllSearchCaches();
387
+ this.pendingChanges++;
388
+ });
389
+ }
390
+ /**
391
+ * Append a single relation to storage.
392
+ *
393
+ * THREAD-SAFE: Uses mutex to prevent concurrent write operations.
394
+ *
395
+ * @param relation - The relation to append
396
+ * @returns Promise resolving when append is complete
397
+ */
398
+ async appendRelation(relation) {
399
+ await this.ensureLoaded();
400
+ return this.mutex.runExclusive(async () => {
401
+ if (!this.db)
402
+ throw new Error('Database not initialized');
403
+ // Use INSERT OR REPLACE to handle updates
404
+ const stmt = this.db.prepare(`
405
+ INSERT OR REPLACE INTO relations (fromEntity, toEntity, relationType, createdAt, lastModified)
406
+ VALUES (?, ?, ?, ?, ?)
407
+ `);
408
+ stmt.run(relation.from, relation.to, relation.relationType, relation.createdAt || new Date().toISOString(), relation.lastModified || new Date().toISOString());
409
+ // Update cache
410
+ const existingIndex = this.cache.relations.findIndex(r => r.from === relation.from && r.to === relation.to && r.relationType === relation.relationType);
411
+ if (existingIndex >= 0) {
412
+ this.cache.relations[existingIndex] = relation;
413
+ }
414
+ else {
415
+ this.cache.relations.push(relation);
416
+ }
417
+ clearAllSearchCaches();
418
+ // Phase 4 Sprint 1: Invalidate bidirectional cache for both entities
419
+ this.invalidateBidirectionalCache(relation.from);
420
+ this.invalidateBidirectionalCache(relation.to);
421
+ this.pendingChanges++;
422
+ });
423
+ }
424
+ /**
425
+ * Update an entity in storage.
426
+ *
427
+ * THREAD-SAFE: Uses mutex to prevent concurrent write operations.
428
+ *
429
+ * @param entityName - Name of the entity to update
430
+ * @param updates - Partial entity updates to apply
431
+ * @returns Promise resolving to true if found and updated
432
+ */
433
+ async updateEntity(entityName, updates) {
434
+ await this.ensureLoaded();
435
+ return this.mutex.runExclusive(async () => {
436
+ if (!this.db)
437
+ throw new Error('Database not initialized');
438
+ // Find entity in cache using index (O(1))
439
+ const entity = this.nameIndex.get(entityName);
440
+ if (!entity) {
441
+ return false;
442
+ }
443
+ // Track old type for index update
444
+ const oldType = entity.entityType;
445
+ // Apply updates to cached entity
446
+ Object.assign(entity, updates);
447
+ entity.lastModified = new Date().toISOString();
448
+ // Update in database
449
+ const stmt = this.db.prepare(`
450
+ UPDATE entities SET
451
+ entityType = ?,
452
+ observations = ?,
453
+ tags = ?,
454
+ importance = ?,
455
+ parentId = ?,
456
+ lastModified = ?
457
+ WHERE name = ?
458
+ `);
459
+ stmt.run(entity.entityType, JSON.stringify(entity.observations), entity.tags ? JSON.stringify(entity.tags) : null, entity.importance ?? null, entity.parentId ?? null, entity.lastModified, entityName);
460
+ // Update indexes
461
+ this.nameIndex.add(entity); // Update reference
462
+ if (updates.entityType && updates.entityType !== oldType) {
463
+ this.typeIndex.updateType(entityName, oldType, updates.entityType);
464
+ }
465
+ this.updateLowercaseCache(entity);
466
+ clearAllSearchCaches();
467
+ this.pendingChanges++;
468
+ return true;
469
+ });
470
+ }
471
+ /**
472
+ * Compact the storage (runs VACUUM to reclaim space).
473
+ *
474
+ * THREAD-SAFE: Uses mutex to prevent concurrent operations.
475
+ *
476
+ * @returns Promise resolving when compaction is complete
477
+ */
478
+ async compact() {
479
+ await this.ensureLoaded();
480
+ return this.mutex.runExclusive(async () => {
481
+ if (!this.db)
482
+ return;
483
+ // Run SQLite VACUUM to reclaim space and defragment
484
+ this.db.exec('VACUUM');
485
+ // Rebuild FTS index for optimal search performance
486
+ this.db.exec(`INSERT INTO entities_fts(entities_fts) VALUES('rebuild')`);
487
+ this.pendingChanges = 0;
488
+ });
489
+ }
490
+ /**
491
+ * Clear any in-memory cache.
492
+ */
493
+ clearCache() {
494
+ this.cache = null;
495
+ this.nameIndex.clear();
496
+ this.typeIndex.clear();
497
+ this.lowercaseCache.clear();
498
+ // Phase 4 Sprint 1: Clear bidirectional relation cache
499
+ this.bidirectionalRelationCache.clear();
500
+ this.initialized = false;
501
+ if (this.db) {
502
+ this.db.close();
503
+ this.db = null;
504
+ }
505
+ }
506
+ // ==================== Index Operations ====================
507
+ /**
508
+ * Get an entity by name in O(1) time.
509
+ *
510
+ * OPTIMIZED: Uses NameIndex for constant-time lookup.
511
+ *
512
+ * @param name - Entity name to look up
513
+ * @returns Entity if found, undefined otherwise
514
+ */
515
+ getEntityByName(name) {
516
+ return this.nameIndex.get(name);
517
+ }
518
+ /**
519
+ * Check if an entity exists by name in O(1) time.
520
+ *
521
+ * @param name - Entity name to check
522
+ * @returns True if entity exists
523
+ */
524
+ hasEntity(name) {
525
+ return this.nameIndex.has(name);
526
+ }
527
+ /**
528
+ * Get all entities of a given type in O(1) time.
529
+ *
530
+ * OPTIMIZED: Uses TypeIndex for constant-time lookup of entity names,
531
+ * then uses NameIndex for O(1) entity retrieval.
532
+ *
533
+ * @param entityType - Entity type to filter by (case-insensitive)
534
+ * @returns Array of entities with the given type
535
+ */
536
+ getEntitiesByType(entityType) {
537
+ const names = this.typeIndex.getNames(entityType);
538
+ const entities = [];
539
+ for (const name of names) {
540
+ const entity = this.nameIndex.get(name);
541
+ if (entity) {
542
+ entities.push(entity);
543
+ }
544
+ }
545
+ return entities;
546
+ }
547
+ /**
548
+ * Get all unique entity types in the graph.
549
+ *
550
+ * @returns Array of unique entity types (lowercase)
551
+ */
552
+ getEntityTypes() {
553
+ return this.typeIndex.getTypes();
554
+ }
555
+ /**
556
+ * Get pre-computed lowercase data for an entity.
557
+ *
558
+ * @param entityName - Entity name to get lowercase data for
559
+ * @returns LowercaseData if entity exists, undefined otherwise
560
+ */
561
+ getLowercased(entityName) {
562
+ return this.lowercaseCache.get(entityName);
563
+ }
564
+ // ==================== FTS5 Full-Text Search ====================
565
+ /**
566
+ * Perform full-text search using FTS5.
567
+ *
568
+ * @param query - Search query (supports FTS5 query syntax)
569
+ * @returns Array of matching entity names with relevance scores
570
+ */
571
+ fullTextSearch(query) {
572
+ if (!this.db || !this.initialized)
573
+ return [];
574
+ try {
575
+ // Use FTS5 MATCH for full-text search with BM25 ranking
576
+ const stmt = this.db.prepare(`
577
+ SELECT name, bm25(entities_fts, 10, 5, 3, 1) as score
578
+ FROM entities_fts
579
+ WHERE entities_fts MATCH ?
580
+ ORDER BY score
581
+ LIMIT 100
582
+ `);
583
+ const results = stmt.all(query);
584
+ return results;
585
+ }
586
+ catch {
587
+ // If FTS query fails (invalid syntax), fall back to empty results
588
+ return [];
589
+ }
590
+ }
591
+ /**
592
+ * Perform a simple text search (LIKE-based, case-insensitive).
593
+ *
594
+ * @param searchTerm - Term to search for
595
+ * @returns Array of matching entity names
596
+ */
597
+ simpleSearch(searchTerm) {
598
+ if (!this.db || !this.initialized)
599
+ return [];
600
+ const pattern = `%${searchTerm}%`;
601
+ const stmt = this.db.prepare(`
602
+ SELECT name FROM entities
603
+ WHERE name LIKE ? COLLATE NOCASE
604
+ OR entityType LIKE ? COLLATE NOCASE
605
+ OR observations LIKE ? COLLATE NOCASE
606
+ OR tags LIKE ? COLLATE NOCASE
607
+ `);
608
+ const results = stmt.all(pattern, pattern, pattern, pattern);
609
+ return results.map(r => r.name);
610
+ }
611
+ // ==================== Utility Operations ====================
612
+ /**
613
+ * Get the storage path/location.
614
+ *
615
+ * @returns The storage path
616
+ */
617
+ getFilePath() {
618
+ return this.dbFilePath;
619
+ }
620
+ /**
621
+ * Get the current pending changes count.
622
+ *
623
+ * @returns Number of pending changes since last reset
624
+ */
625
+ getPendingAppends() {
626
+ return this.pendingChanges;
627
+ }
628
+ /**
629
+ * Force persistence to disk (no-op for better-sqlite3 as it writes immediately).
630
+ *
631
+ * @returns Promise resolving when persistence is complete
632
+ */
633
+ async flush() {
634
+ // better-sqlite3 writes to disk immediately, but we run a checkpoint for WAL mode
635
+ if (this.db) {
636
+ this.db.pragma('wal_checkpoint(TRUNCATE)');
637
+ }
638
+ this.pendingChanges = 0;
639
+ }
640
+ /**
641
+ * Close the database connection.
642
+ */
643
+ close() {
644
+ if (this.db) {
645
+ this.db.close();
646
+ this.db = null;
647
+ }
648
+ this.initialized = false;
649
+ }
650
+ // ==================== Relation Index Operations ====================
651
+ /**
652
+ * Get all relations where the entity is the source (outgoing relations).
653
+ *
654
+ * OPTIMIZED: Uses SQLite index on fromEntity for O(log n) lookup.
655
+ *
656
+ * @param entityName - Entity name to look up outgoing relations for
657
+ * @returns Array of relations where entity is the source
658
+ */
659
+ getRelationsFrom(entityName) {
660
+ // Check cache first
661
+ if (this.cache) {
662
+ return this.cache.relations.filter(r => r.from === entityName);
663
+ }
664
+ // Fall back to database query
665
+ if (!this.db || !this.initialized)
666
+ return [];
667
+ const stmt = this.db.prepare('SELECT fromEntity, toEntity, relationType, createdAt, lastModified FROM relations WHERE fromEntity = ?');
668
+ const rows = stmt.all(entityName);
669
+ return rows.map(row => ({
670
+ from: row.fromEntity,
671
+ to: row.toEntity,
672
+ relationType: row.relationType,
673
+ createdAt: row.createdAt,
674
+ lastModified: row.lastModified,
675
+ }));
676
+ }
677
+ /**
678
+ * Get all relations where the entity is the target (incoming relations).
679
+ *
680
+ * OPTIMIZED: Uses SQLite index on toEntity for O(log n) lookup.
681
+ *
682
+ * @param entityName - Entity name to look up incoming relations for
683
+ * @returns Array of relations where entity is the target
684
+ */
685
+ getRelationsTo(entityName) {
686
+ // Check cache first
687
+ if (this.cache) {
688
+ return this.cache.relations.filter(r => r.to === entityName);
689
+ }
690
+ // Fall back to database query
691
+ if (!this.db || !this.initialized)
692
+ return [];
693
+ const stmt = this.db.prepare('SELECT fromEntity, toEntity, relationType, createdAt, lastModified FROM relations WHERE toEntity = ?');
694
+ const rows = stmt.all(entityName);
695
+ return rows.map(row => ({
696
+ from: row.fromEntity,
697
+ to: row.toEntity,
698
+ relationType: row.relationType,
699
+ createdAt: row.createdAt,
700
+ lastModified: row.lastModified,
701
+ }));
702
+ }
703
+ /**
704
+ * Get all relations involving the entity (both incoming and outgoing).
705
+ *
706
+ * OPTIMIZED: Phase 4 Sprint 1 - Uses bidirectional cache for O(1) repeated lookups.
707
+ *
708
+ * @param entityName - Entity name to look up all relations for
709
+ * @returns Array of all relations involving the entity
710
+ */
711
+ getRelationsFor(entityName) {
712
+ // Phase 4 Sprint 1: Check bidirectional cache first for O(1) repeated lookups
713
+ const cached = this.bidirectionalRelationCache.get(entityName);
714
+ if (cached !== undefined) {
715
+ return cached;
716
+ }
717
+ // Check main cache and compute result
718
+ let relations;
719
+ if (this.cache) {
720
+ relations = this.cache.relations.filter(r => r.from === entityName || r.to === entityName);
721
+ }
722
+ else if (this.db && this.initialized) {
723
+ // Fall back to database query
724
+ const stmt = this.db.prepare('SELECT fromEntity, toEntity, relationType, createdAt, lastModified FROM relations WHERE fromEntity = ? OR toEntity = ?');
725
+ const rows = stmt.all(entityName, entityName);
726
+ relations = rows.map(row => ({
727
+ from: row.fromEntity,
728
+ to: row.toEntity,
729
+ relationType: row.relationType,
730
+ createdAt: row.createdAt,
731
+ lastModified: row.lastModified,
732
+ }));
733
+ }
734
+ else {
735
+ return [];
736
+ }
737
+ // Cache the result for O(1) subsequent lookups
738
+ this.bidirectionalRelationCache.set(entityName, relations);
739
+ return relations;
740
+ }
741
+ /**
742
+ * Check if an entity has any relations.
743
+ *
744
+ * @param entityName - Entity name to check
745
+ * @returns True if entity has any relations
746
+ */
747
+ hasRelations(entityName) {
748
+ // Check cache first
749
+ if (this.cache) {
750
+ return this.cache.relations.some(r => r.from === entityName || r.to === entityName);
751
+ }
752
+ // Fall back to database query
753
+ if (!this.db || !this.initialized)
754
+ return false;
755
+ const stmt = this.db.prepare('SELECT 1 FROM relations WHERE fromEntity = ? OR toEntity = ? LIMIT 1');
756
+ const row = stmt.get(entityName, entityName);
757
+ return row !== undefined;
758
+ }
759
+ // ==================== Embedding Storage (Phase 4 Sprint 11) ====================
760
+ /**
761
+ * Phase 4 Sprint 11: Ensure embeddings table exists.
762
+ *
763
+ * Creates the embeddings table if it doesn't exist.
764
+ * Separate table from entities to avoid schema migration complexity.
765
+ */
766
+ ensureEmbeddingsTable() {
767
+ if (!this.db)
768
+ throw new Error('Database not initialized');
769
+ this.db.exec(`
770
+ CREATE TABLE IF NOT EXISTS embeddings (
771
+ entityName TEXT PRIMARY KEY REFERENCES entities(name) ON DELETE CASCADE,
772
+ embedding BLOB NOT NULL,
773
+ embeddingModel TEXT NOT NULL,
774
+ embeddingUpdatedAt TEXT NOT NULL,
775
+ dimensions INTEGER NOT NULL
776
+ )
777
+ `);
778
+ // Index for quick lookup by model
779
+ this.db.exec(`CREATE INDEX IF NOT EXISTS idx_embedding_model ON embeddings(embeddingModel)`);
780
+ }
781
+ /**
782
+ * Phase 4 Sprint 11: Store an embedding for an entity.
783
+ *
784
+ * @param entityName - Name of the entity
785
+ * @param vector - Embedding vector
786
+ * @param model - Model name used for the embedding
787
+ */
788
+ storeEmbedding(entityName, vector, model) {
789
+ if (!this.db || !this.initialized) {
790
+ throw new Error('Database not initialized');
791
+ }
792
+ this.ensureEmbeddingsTable();
793
+ // Convert to Float32Array for efficient storage
794
+ const buffer = Buffer.from(new Float32Array(vector).buffer);
795
+ const stmt = this.db.prepare(`
796
+ INSERT OR REPLACE INTO embeddings (entityName, embedding, embeddingModel, embeddingUpdatedAt, dimensions)
797
+ VALUES (?, ?, ?, ?, ?)
798
+ `);
799
+ stmt.run(entityName, buffer, model, new Date().toISOString(), vector.length);
800
+ }
801
+ /**
802
+ * Phase 4 Sprint 11: Get an embedding for an entity.
803
+ *
804
+ * @param entityName - Name of the entity
805
+ * @returns Embedding vector if found, null otherwise
806
+ */
807
+ getEmbedding(entityName) {
808
+ if (!this.db || !this.initialized)
809
+ return null;
810
+ try {
811
+ this.ensureEmbeddingsTable();
812
+ const stmt = this.db.prepare(`SELECT embedding FROM embeddings WHERE entityName = ?`);
813
+ const row = stmt.get(entityName);
814
+ if (!row)
815
+ return null;
816
+ // Convert from Buffer to number array
817
+ const float32Array = new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.embedding.length / 4);
818
+ return Array.from(float32Array);
819
+ }
820
+ catch {
821
+ return null;
822
+ }
823
+ }
824
+ /**
825
+ * Phase 4 Sprint 11: Load all embeddings from storage.
826
+ *
827
+ * @returns Array of [entityName, vector] pairs
828
+ */
829
+ async loadAllEmbeddings() {
830
+ if (!this.db || !this.initialized)
831
+ return [];
832
+ try {
833
+ this.ensureEmbeddingsTable();
834
+ const stmt = this.db.prepare(`SELECT entityName, embedding FROM embeddings`);
835
+ const rows = stmt.all();
836
+ return rows.map(row => {
837
+ const float32Array = new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.embedding.length / 4);
838
+ return [row.entityName, Array.from(float32Array)];
839
+ });
840
+ }
841
+ catch {
842
+ return [];
843
+ }
844
+ }
845
+ /**
846
+ * Phase 4 Sprint 11: Remove an embedding for an entity.
847
+ *
848
+ * @param entityName - Name of the entity
849
+ */
850
+ removeEmbedding(entityName) {
851
+ if (!this.db || !this.initialized)
852
+ return;
853
+ try {
854
+ this.ensureEmbeddingsTable();
855
+ const stmt = this.db.prepare(`DELETE FROM embeddings WHERE entityName = ?`);
856
+ stmt.run(entityName);
857
+ }
858
+ catch {
859
+ // Ignore errors if table doesn't exist
860
+ }
861
+ }
862
+ /**
863
+ * Phase 4 Sprint 11: Clear all embeddings from storage.
864
+ */
865
+ clearAllEmbeddings() {
866
+ if (!this.db || !this.initialized)
867
+ return;
868
+ try {
869
+ this.ensureEmbeddingsTable();
870
+ this.db.exec(`DELETE FROM embeddings`);
871
+ }
872
+ catch {
873
+ // Ignore errors if table doesn't exist
874
+ }
875
+ }
876
+ /**
877
+ * Phase 4 Sprint 11: Check if an entity has an embedding.
878
+ *
879
+ * @param entityName - Name of the entity
880
+ * @returns True if embedding exists
881
+ */
882
+ hasEmbedding(entityName) {
883
+ if (!this.db || !this.initialized)
884
+ return false;
885
+ try {
886
+ this.ensureEmbeddingsTable();
887
+ const stmt = this.db.prepare(`SELECT 1 FROM embeddings WHERE entityName = ? LIMIT 1`);
888
+ const row = stmt.get(entityName);
889
+ return row !== undefined;
890
+ }
891
+ catch {
892
+ return false;
893
+ }
894
+ }
895
+ /**
896
+ * Phase 4 Sprint 11: Get embedding statistics.
897
+ *
898
+ * @returns Stats about stored embeddings
899
+ */
900
+ getEmbeddingStats() {
901
+ if (!this.db || !this.initialized) {
902
+ return { count: 0, models: [] };
903
+ }
904
+ try {
905
+ this.ensureEmbeddingsTable();
906
+ const countRow = this.db.prepare(`SELECT COUNT(*) as count FROM embeddings`).get();
907
+ const modelRows = this.db.prepare(`SELECT DISTINCT embeddingModel FROM embeddings`).all();
908
+ return {
909
+ count: countRow.count,
910
+ models: modelRows.map(r => r.embeddingModel),
911
+ };
912
+ }
913
+ catch {
914
+ return { count: 0, models: [] };
915
+ }
916
+ }
917
+ }