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