@jungjaehoon/mama-core 1.0.1

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.
@@ -0,0 +1,671 @@
1
+ /**
2
+ * MAMA Database Manager (SQLite-only)
3
+ *
4
+ * SQLite-exclusive database interface for MAMA Plugin.
5
+ * Uses better-sqlite3 + sqlite-vec for local storage.
6
+ *
7
+ * PostgreSQL support is only available in the legacy mcp-server repository.
8
+ *
9
+ * Features:
10
+ * - WAL mode for better concurrency
11
+ * - synchronous=NORMAL for performance
12
+ * - Automatic migration management
13
+ * - Vector similarity search (when sqlite-vec available)
14
+ *
15
+ * @module db-manager
16
+ * @version 2.1 (Plugin - SQLite-only)
17
+ * @date 2026-02-01
18
+ * @source-of-truth packages/mama-core/src/db-manager.js (mama-core)
19
+ */
20
+
21
+ const { info, warn, error: logError } = require('./debug-logger');
22
+ const { logProgress: _logProgress, logComplete, logSearching } = require('./progress-indicator');
23
+ const { createAdapter } = require('./db-adapter');
24
+ const path = require('path');
25
+
26
+ // Database adapter instance (singleton)
27
+ let dbAdapter = null;
28
+ let dbConnection = null;
29
+ let isInitialized = false;
30
+ let initializingPromise = null; // Single-flight guard for concurrent callers
31
+
32
+ // Migration directory (moved to src/db/migrations for M1.2)
33
+ const MIGRATIONS_DIR = path.join(__dirname, '..', 'db', 'migrations');
34
+
35
+ /**
36
+ * Initialize SQLite database adapter and connect
37
+ *
38
+ * Lazy initialization: Only connects when first accessed
39
+ * Creates database file at ~/.claude/mama-memory.db by default
40
+ *
41
+ * Single-flight guard: Concurrent callers await the same promise
42
+ * to prevent multiple adapters/migrations running simultaneously.
43
+ *
44
+ * @returns {Promise<Object>} SQLite database connection
45
+ */
46
+ async function initDB() {
47
+ // Already initialized - return immediately
48
+ if (isInitialized) {
49
+ return dbConnection;
50
+ }
51
+
52
+ // Single-flight guard: If initialization is in progress, wait for it
53
+ if (initializingPromise) {
54
+ return initializingPromise;
55
+ }
56
+
57
+ // Start initialization and store promise for concurrent callers
58
+ initializingPromise = (async () => {
59
+ try {
60
+ logSearching('Initializing database...');
61
+
62
+ // Create SQLite adapter
63
+ dbAdapter = createAdapter();
64
+
65
+ // Connect to database
66
+ dbConnection = await dbAdapter.connect();
67
+
68
+ // Run migrations (includes 012-create-checkpoints-table.sql)
69
+ await dbAdapter.runMigrations(MIGRATIONS_DIR);
70
+
71
+ isInitialized = true;
72
+
73
+ info(`[db-manager] Database initialized (${dbAdapter.constructor.name})`);
74
+ logComplete('Database ready');
75
+
76
+ return dbConnection;
77
+ } catch (error) {
78
+ // Clear state on failure so retry is possible
79
+ initializingPromise = null;
80
+ dbAdapter = null;
81
+ dbConnection = null;
82
+ isInitialized = false;
83
+ throw new Error(`Failed to initialize database: ${error.message}`);
84
+ }
85
+ })();
86
+
87
+ return initializingPromise;
88
+ }
89
+
90
+ /**
91
+ * Get database connection (singleton pattern)
92
+ *
93
+ * Returns better-sqlite3 Database instance
94
+ *
95
+ * Note: Synchronous for backward compatibility with memory-store.js
96
+ * Will throw if database not initialized
97
+ *
98
+ * @returns {Object} SQLite database connection
99
+ */
100
+ function getDB() {
101
+ if (!dbConnection) {
102
+ throw new Error('Database not initialized. Call await initDB() first.');
103
+ }
104
+ return dbConnection;
105
+ }
106
+
107
+ /**
108
+ * Get database adapter instance
109
+ *
110
+ * Used for advanced operations (vectorSearch, insertEmbedding, etc.)
111
+ *
112
+ * @returns {DatabaseAdapter} Adapter instance
113
+ */
114
+ function getAdapter() {
115
+ if (!dbAdapter) {
116
+ throw new Error('Database adapter not initialized. Call await initDB() first.');
117
+ }
118
+ return dbAdapter;
119
+ }
120
+
121
+ /**
122
+ * Close database connection
123
+ *
124
+ * Call this on process exit
125
+ */
126
+ async function closeDB() {
127
+ if (dbAdapter) {
128
+ await dbAdapter.disconnect();
129
+ dbAdapter = null;
130
+ dbConnection = null;
131
+ isInitialized = false;
132
+ initializingPromise = null; // Clear to allow re-initialization
133
+ info('[db-manager] Database connection closed');
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Insert embedding into vector search table
139
+ *
140
+ * Uses sqlite-vec for vector similarity search
141
+ * Gracefully degrades if sqlite-vec is not available
142
+ *
143
+ * @param {number} decisionRowid - SQLite rowid
144
+ * @param {Float32Array|Array<number>} embedding - 384-dim embedding vector
145
+ * @returns {Promise<void>}
146
+ */
147
+ async function insertEmbedding(decisionRowid, embedding) {
148
+ const adapter = getAdapter();
149
+
150
+ try {
151
+ await adapter.insertEmbedding(decisionRowid, embedding);
152
+ } catch (error) {
153
+ // Graceful degradation: Log warning but don't fail
154
+ logError(
155
+ `[db-manager] Failed to insert embedding (vector search unavailable): ${error.message}`
156
+ );
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Perform vector similarity search
162
+ *
163
+ * Returns empty array if vector search not available (no keyword fallback)
164
+ *
165
+ * @param {Float32Array|Array<number>} queryEmbedding - Query embedding (384-dim)
166
+ * @param {number} limit - Max results to return (default: 5)
167
+ * @param {number} threshold - Minimum similarity threshold (default: 0.7)
168
+ * @returns {Promise<Array<Object>>} Array of decisions with similarity scores, or empty array
169
+ */
170
+ async function vectorSearch(queryEmbedding, limit = 5, threshold = 0.7) {
171
+ const adapter = getAdapter();
172
+
173
+ try {
174
+ // SQLite adapter returns null if sqlite-vec not available
175
+ const results = await adapter.vectorSearch(queryEmbedding, limit * 3);
176
+
177
+ if (!results || results.length === 0) {
178
+ return []; // No keyword fallback - fast fail
179
+ }
180
+
181
+ const stmt = adapter.prepare(`SELECT * FROM decisions WHERE rowid = ?`);
182
+ const decisions = [];
183
+
184
+ for (const row of results) {
185
+ const decision = stmt.get(row.rowid);
186
+
187
+ if (!decision) {
188
+ continue;
189
+ }
190
+
191
+ const similarity = row.similarity ?? Math.max(0, 1.0 - (row.distance ?? 1));
192
+ const distance = row.distance ?? Math.max(0, 1.0 - similarity);
193
+
194
+ if (similarity >= threshold) {
195
+ decisions.push({
196
+ ...decision,
197
+ distance,
198
+ similarity,
199
+ });
200
+ }
201
+
202
+ if (decisions.length >= limit) {
203
+ break;
204
+ }
205
+ }
206
+
207
+ return decisions;
208
+ } catch (error) {
209
+ logError(`[db-manager] Vector search failed: ${error.message}`);
210
+ return []; // No keyword fallback - fast fail
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Insert decision with embedding
216
+ *
217
+ * Combined operation: Insert decision + Generate embedding + Insert embedding
218
+ * SQLite-only implementation
219
+ *
220
+ * @param {Object} decision - Decision object
221
+ * @returns {Promise<string>} Decision ID
222
+ */
223
+ async function insertDecisionWithEmbedding(decision) {
224
+ const adapter = getAdapter();
225
+ const { generateEnhancedEmbedding } = require('./embeddings');
226
+
227
+ try {
228
+ // Generate embedding BEFORE transaction (required for SQLite's sync transaction)
229
+ // Note: Redact topic for privacy - only log length
230
+ info(
231
+ `[db-manager] Generating embedding for decision (topic length: ${decision.topic?.length || 0})`
232
+ );
233
+ const embedding = await generateEnhancedEmbedding(decision);
234
+ info(`[db-manager] Embedding generated: ${embedding ? embedding.length : 'null'} dimensions`);
235
+
236
+ // SQLite: Synchronous transaction including embedding
237
+ // eslint-disable-next-line no-unused-vars
238
+ const decisionRowid = adapter.transaction(() => {
239
+ // Prepare INSERT statement
240
+ const stmt = adapter.prepare(`
241
+ INSERT INTO decisions (
242
+ id, topic, decision, reasoning,
243
+ outcome, failure_reason, limitation,
244
+ user_involvement, session_id,
245
+ supersedes, superseded_by, refined_from,
246
+ confidence, created_at, updated_at,
247
+ needs_validation, validation_attempts, last_validated_at, usage_count,
248
+ trust_context, usage_success, usage_failure, time_saved,
249
+ evidence, alternatives, risks
250
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
251
+ `);
252
+
253
+ const insertResult = stmt.run(
254
+ decision.id,
255
+ decision.topic,
256
+ decision.decision,
257
+ decision.reasoning || null,
258
+ decision.outcome || null,
259
+ decision.failure_reason || null,
260
+ decision.limitation || null,
261
+ decision.user_involvement || null,
262
+ decision.session_id || null,
263
+ decision.supersedes || null,
264
+ decision.superseded_by || null,
265
+ decision.refined_from ? JSON.stringify(decision.refined_from) : null,
266
+ decision.confidence !== undefined ? decision.confidence : 0.5,
267
+ // IMPORTANT: All timestamps are stored in milliseconds (Date.now()).
268
+ // The schema DEFAULT uses unixepoch() (seconds) but is never used
269
+ // since all inserts go through this function which always provides ms.
270
+ decision.created_at || Date.now(),
271
+ decision.updated_at || Date.now(),
272
+ decision.needs_validation !== undefined ? decision.needs_validation : 0,
273
+ decision.validation_attempts || 0,
274
+ decision.last_validated_at || null,
275
+ decision.usage_count || 0,
276
+ decision.trust_context || null,
277
+ decision.usage_success || 0,
278
+ decision.usage_failure || 0,
279
+ decision.time_saved || 0,
280
+ decision.evidence || null,
281
+ decision.alternatives || null,
282
+ decision.risks || null
283
+ );
284
+
285
+ const rowid = Number(insertResult.lastInsertRowid);
286
+
287
+ // Insert embedding in same transaction to ensure rowid matching
288
+ info(`[db-manager] Vector search enabled: ${adapter.vectorSearchEnabled}`);
289
+ if (adapter.vectorSearchEnabled) {
290
+ try {
291
+ info(`[db-manager] Inserting embedding for rowid: ${rowid}`);
292
+ adapter.insertEmbedding(rowid, embedding);
293
+ info(`[db-manager] ✅ Embedding inserted successfully`);
294
+ } catch (embErr) {
295
+ // Log but don't fail transaction if embedding fails
296
+ logError(`[db-manager] ❌ Embedding insert failed: ${embErr.message}`);
297
+ }
298
+ } else {
299
+ info(`[db-manager] ⚠️ Vector search disabled, skipping embedding`);
300
+ }
301
+
302
+ return rowid;
303
+ });
304
+
305
+ if (process.env.MAMA_DEBUG) {
306
+ info(`[db-manager] Decision stored: ${decision.id}`);
307
+ }
308
+
309
+ return decision.id;
310
+ } catch (error) {
311
+ throw new Error(`Failed to insert decision with embedding: ${error.message}`);
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Query decision graph for topic
317
+ *
318
+ * Recursive CTE to traverse supersedes chain
319
+ * SQLite implementation using WITH RECURSIVE
320
+ *
321
+ * @param {string} topic - Decision topic to query
322
+ * @returns {Promise<Array<Object>>} Array of decisions (ordered by recency)
323
+ */
324
+ async function queryDecisionGraph(topic) {
325
+ const adapter = getAdapter();
326
+
327
+ try {
328
+ // Story 014.14 Fix: Prioritize exact topic match over fuzzy matching
329
+ // First try exact match, then fallback to fuzzy if no results
330
+
331
+ // Try exact match first
332
+ let stmt = adapter.prepare(`
333
+ WITH RECURSIVE decision_chain AS (
334
+ -- Base case: Get current decision (not superseded)
335
+ SELECT * FROM decisions
336
+ WHERE topic = ? AND superseded_by IS NULL
337
+
338
+ UNION ALL
339
+
340
+ -- Recursive case: Get previous decisions
341
+ SELECT d.* FROM decisions d
342
+ JOIN decision_chain dc ON d.id = dc.supersedes
343
+ )
344
+ SELECT * FROM decision_chain
345
+ ORDER BY created_at DESC
346
+ `);
347
+
348
+ let decisions = await stmt.all(topic);
349
+
350
+ // If no exact match, try fuzzy matching as fallback
351
+ if (decisions.length === 0) {
352
+ const topicKeyword = topic.split('_')[0];
353
+
354
+ stmt = adapter.prepare(`
355
+ WITH RECURSIVE decision_chain AS (
356
+ -- Base case: Get current decision (not superseded)
357
+ SELECT * FROM decisions
358
+ WHERE topic LIKE ? || '%' AND superseded_by IS NULL
359
+
360
+ UNION ALL
361
+
362
+ -- Recursive case: Get previous decisions
363
+ SELECT d.* FROM decisions d
364
+ JOIN decision_chain dc ON d.id = dc.supersedes
365
+ )
366
+ SELECT * FROM decision_chain
367
+ ORDER BY created_at DESC
368
+ `);
369
+
370
+ decisions = await stmt.all(topicKeyword);
371
+ }
372
+
373
+ // Join with decision_edges to include relationships
374
+ // Prepare statement once outside loop for performance
375
+ const edgesStmt = adapter.prepare(`
376
+ SELECT * FROM decision_edges
377
+ WHERE from_id = ?
378
+ AND (approved_by_user = 1 OR approved_by_user IS NULL)
379
+ `);
380
+ for (const decision of decisions) {
381
+ decision.edges = await edgesStmt.all(decision.id);
382
+
383
+ // Parse refined_from JSON if exists
384
+ if (decision.refined_from) {
385
+ try {
386
+ decision.refined_from = JSON.parse(decision.refined_from);
387
+ } catch (e) {
388
+ decision.refined_from = [];
389
+ }
390
+ }
391
+ }
392
+
393
+ return decisions;
394
+ } catch (error) {
395
+ throw new Error(`Decision graph query failed: ${error.message}`);
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Query semantic edges for a list of decisions
401
+ *
402
+ * Returns both outgoing (from_id) and incoming (to_id) edges
403
+ * for refines and contradicts relationships
404
+ *
405
+ * @param {Array<string>} decisionIds - Decision IDs to query edges for
406
+ * @returns {Promise<Object>} Categorized edges { refines, refined_by, contradicts, contradicted_by, builds_on, built_on_by, debates, debated_by, synthesizes, synthesized_by }
407
+ */
408
+ async function querySemanticEdges(decisionIds) {
409
+ const adapter = getAdapter();
410
+
411
+ if (!decisionIds || decisionIds.length === 0) {
412
+ return {
413
+ refines: [],
414
+ refined_by: [],
415
+ contradicts: [],
416
+ contradicted_by: [],
417
+ // Story 2.1: Extended edge types
418
+ builds_on: [],
419
+ built_on_by: [],
420
+ debates: [],
421
+ debated_by: [],
422
+ synthesizes: [],
423
+ synthesized_by: [],
424
+ };
425
+ }
426
+
427
+ try {
428
+ // Build placeholders for IN clause
429
+ const placeholders = decisionIds.map(() => '?').join(',');
430
+
431
+ // Story 2.1: Include new edge types in query
432
+ const edgeTypes = ['refines', 'contradicts', 'builds_on', 'debates', 'synthesizes'];
433
+ const edgeTypePlaceholders = edgeTypes.map(() => '?').join(',');
434
+
435
+ // Query outgoing edges (from_id = decision)
436
+ const outgoingStmt = adapter.prepare(`
437
+ SELECT e.*, d.topic, d.decision, d.confidence, d.created_at
438
+ FROM decision_edges e
439
+ JOIN decisions d ON e.to_id = d.id
440
+ WHERE e.from_id IN (${placeholders})
441
+ AND e.relationship IN (${edgeTypePlaceholders})
442
+ AND (e.approved_by_user = 1 OR e.approved_by_user IS NULL)
443
+ ORDER BY e.created_at DESC
444
+ `);
445
+ const outgoingEdges = await outgoingStmt.all(...decisionIds, ...edgeTypes);
446
+
447
+ // Query incoming edges (to_id = decision)
448
+ const incomingStmt = adapter.prepare(`
449
+ SELECT e.*, d.topic, d.decision, d.confidence, d.created_at
450
+ FROM decision_edges e
451
+ JOIN decisions d ON e.from_id = d.id
452
+ WHERE e.to_id IN (${placeholders})
453
+ AND e.relationship IN (${edgeTypePlaceholders})
454
+ AND (e.approved_by_user = 1 OR e.approved_by_user IS NULL)
455
+ ORDER BY e.created_at DESC
456
+ `);
457
+ const incomingEdges = await incomingStmt.all(...decisionIds, ...edgeTypes);
458
+
459
+ // Categorize edges (original + v1.3 extended)
460
+ const refines = outgoingEdges.filter((e) => e.relationship === 'refines');
461
+ const refined_by = incomingEdges.filter((e) => e.relationship === 'refines');
462
+ const contradicts = outgoingEdges.filter((e) => e.relationship === 'contradicts');
463
+ const contradicted_by = incomingEdges.filter((e) => e.relationship === 'contradicts');
464
+ // Story 2.1: New edge type categories
465
+ const builds_on = outgoingEdges.filter((e) => e.relationship === 'builds_on');
466
+ const built_on_by = incomingEdges.filter((e) => e.relationship === 'builds_on');
467
+ const debates = outgoingEdges.filter((e) => e.relationship === 'debates');
468
+ const debated_by = incomingEdges.filter((e) => e.relationship === 'debates');
469
+ const synthesizes = outgoingEdges.filter((e) => e.relationship === 'synthesizes');
470
+ const synthesized_by = incomingEdges.filter((e) => e.relationship === 'synthesizes');
471
+
472
+ return {
473
+ refines,
474
+ refined_by,
475
+ contradicts,
476
+ contradicted_by,
477
+ builds_on,
478
+ built_on_by,
479
+ debates,
480
+ debated_by,
481
+ synthesizes,
482
+ synthesized_by,
483
+ };
484
+ } catch (error) {
485
+ throw new Error(`Semantic edges query failed: ${error.message}`);
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Query vector search with time window and threshold
491
+ *
492
+ * Story 014.14: AC #1 - Vector Search for Related Decisions
493
+ *
494
+ * @param {Object} params - Search parameters
495
+ * @param {string} params.query - Search query text
496
+ * @param {number} params.limit - Max results (default: 10)
497
+ * @param {number} params.threshold - Minimum cosine similarity (0.0-1.0, default: 0.75)
498
+ * @param {number} params.timeWindow - Time window in ms (optional, default: 90 days)
499
+ * @returns {Promise<Array>} Results with similarity scores and decision data
500
+ */
501
+ async function queryVectorSearch({
502
+ query,
503
+ limit = 10,
504
+ threshold = 0.75,
505
+ timeWindow = 90 * 24 * 60 * 60 * 1000,
506
+ }) {
507
+ const adapter = getAdapter();
508
+ const { generateEmbedding } = require('./embeddings');
509
+
510
+ try {
511
+ // Generate embedding for query
512
+ const embedding = await generateEmbedding(query);
513
+
514
+ const cutoffTime = Date.now() - timeWindow;
515
+ const candidates = await adapter.vectorSearch(embedding, limit * 5);
516
+
517
+ if (!candidates || candidates.length === 0) {
518
+ return [];
519
+ }
520
+
521
+ const stmt = adapter.prepare(`SELECT * FROM decisions WHERE rowid = ?`);
522
+ const results = [];
523
+
524
+ for (const candidate of candidates) {
525
+ const decision = stmt.get(candidate.rowid);
526
+ if (!decision) {
527
+ continue;
528
+ }
529
+
530
+ if (decision.created_at < cutoffTime) {
531
+ continue;
532
+ }
533
+
534
+ const similarity = candidate.similarity ?? Math.max(0, 1 - (candidate.distance ?? 1));
535
+ const distance = candidate.distance ?? Math.max(0, 1 - similarity);
536
+
537
+ if (similarity < threshold) {
538
+ continue;
539
+ }
540
+
541
+ results.push({
542
+ ...decision,
543
+ similarity,
544
+ distance,
545
+ });
546
+
547
+ if (results.length >= limit) {
548
+ break;
549
+ }
550
+ }
551
+
552
+ return results;
553
+ } catch (error) {
554
+ logError(`[db-manager] queryVectorSearch failed: ${error.message}`);
555
+ return []; // Return empty array on error (graceful degradation)
556
+ }
557
+ }
558
+
559
+ /**
560
+ * Update decision outcome
561
+ *
562
+ * @param {string} decisionId - Decision ID
563
+ * @param {Object} outcomeData - Outcome data
564
+ * @returns {Promise<void>}
565
+ */
566
+ async function updateDecisionOutcome(decisionId, outcomeData) {
567
+ const adapter = getAdapter();
568
+
569
+ try {
570
+ const stmt = adapter.prepare(`
571
+ UPDATE decisions
572
+ SET
573
+ outcome = ?,
574
+ failure_reason = ?,
575
+ limitation = ?,
576
+ duration_days = ?,
577
+ confidence = COALESCE(?, confidence),
578
+ updated_at = ?
579
+ WHERE id = ?
580
+ `);
581
+
582
+ await stmt.run(
583
+ outcomeData.outcome || null,
584
+ outcomeData.failure_reason || null,
585
+ outcomeData.limitation || null,
586
+ outcomeData.duration_days || null,
587
+ outcomeData.confidence !== undefined ? outcomeData.confidence : null,
588
+ Date.now(),
589
+ decisionId
590
+ );
591
+
592
+ info(`[db-manager] Decision outcome updated: ${decisionId} → ${outcomeData.outcome}`);
593
+ } catch (error) {
594
+ throw new Error(`Failed to update decision outcome: ${error.message}`);
595
+ }
596
+ }
597
+
598
+ /**
599
+ * Get prepared statement
600
+ *
601
+ * For backward compatibility with memory-store.js
602
+ * Returns a compatibility shim that proxies to adapter.prepare()
603
+ *
604
+ * @param {string} sql - SQL statement
605
+ * @returns {Object} Statement-like object with run/get/all methods
606
+ */
607
+ function getPreparedStmt(sql) {
608
+ if (!dbAdapter) {
609
+ warn('[db-manager] getPreparedStmt() called before initialization');
610
+ // Return no-op object for feature detection (won't throw)
611
+ return {
612
+ run: () => ({ changes: 0, lastInsertRowid: 0 }),
613
+ get: () => null,
614
+ all: () => [],
615
+ };
616
+ }
617
+
618
+ // Proxy to adapter.prepare() for actual usage
619
+ try {
620
+ return dbAdapter.prepare(sql);
621
+ } catch (error) {
622
+ warn(`[db-manager] getPreparedStmt() failed: ${error.message}`);
623
+ // Return no-op object on error (graceful degradation)
624
+ return {
625
+ run: () => ({ changes: 0, lastInsertRowid: 0 }),
626
+ get: () => null,
627
+ all: () => [],
628
+ };
629
+ }
630
+ }
631
+
632
+ /**
633
+ * Get database file path
634
+ *
635
+ * @returns {string} Actual database path or 'Not initialized'
636
+ */
637
+ function getDbPath() {
638
+ if (!dbAdapter) {
639
+ return 'Not initialized';
640
+ }
641
+ // Use adapter's getDbPath method if available, fallback to description
642
+ if (typeof dbAdapter.getDbPath === 'function') {
643
+ return dbAdapter.getDbPath();
644
+ }
645
+ // Fallback: try to get path from adapter properties
646
+ if (dbAdapter.dbPath) {
647
+ return dbAdapter.dbPath;
648
+ }
649
+ return `${dbAdapter.constructor.name} (path unavailable)`;
650
+ }
651
+
652
+ // Export API (same interface as memory-store.js, but async where needed)
653
+ module.exports = {
654
+ initDB, // Async
655
+ getDB, // Sync (throws if not initialized)
656
+ getAdapter, // Sync (throws if not initialized)
657
+ closeDB, // Async
658
+ insertEmbedding, // Async
659
+ vectorSearch, // Async
660
+ queryVectorSearch, // Async - Story 014.14
661
+ querySemanticEdges, // Async - Graph traversal enhancement
662
+ insertDecisionWithEmbedding, // Async
663
+ queryDecisionGraph, // Async
664
+ updateDecisionOutcome, // Async
665
+ getPreparedStmt, // Compatibility shim
666
+ getDbPath, // Returns actual path
667
+ };
668
+
669
+ // Note: Removed auto-registered SIGINT/SIGTERM handlers that called process.exit(0)
670
+ // This was causing issues with host cleanup in parent processes.
671
+ // If graceful shutdown is needed, the host application should handle closeDB().