gitnexus 1.4.6 → 1.4.8

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 (99) hide show
  1. package/README.md +22 -1
  2. package/dist/cli/ai-context.d.ts +1 -1
  3. package/dist/cli/ai-context.js +1 -1
  4. package/dist/cli/analyze.d.ts +2 -0
  5. package/dist/cli/analyze.js +54 -21
  6. package/dist/cli/index.js +2 -1
  7. package/dist/cli/setup.js +78 -1
  8. package/dist/config/supported-languages.d.ts +30 -0
  9. package/dist/config/supported-languages.js +30 -0
  10. package/dist/core/embeddings/embedder.d.ts +6 -1
  11. package/dist/core/embeddings/embedder.js +65 -5
  12. package/dist/core/embeddings/embedding-pipeline.js +11 -9
  13. package/dist/core/embeddings/http-client.d.ts +31 -0
  14. package/dist/core/embeddings/http-client.js +179 -0
  15. package/dist/core/embeddings/index.d.ts +1 -0
  16. package/dist/core/embeddings/index.js +1 -0
  17. package/dist/core/embeddings/types.d.ts +1 -1
  18. package/dist/core/graph/types.d.ts +4 -3
  19. package/dist/core/ingestion/ast-helpers.d.ts +80 -0
  20. package/dist/core/ingestion/ast-helpers.js +738 -0
  21. package/dist/core/ingestion/call-analysis.d.ts +73 -0
  22. package/dist/core/ingestion/call-analysis.js +490 -0
  23. package/dist/core/ingestion/call-processor.d.ts +55 -2
  24. package/dist/core/ingestion/call-processor.js +673 -108
  25. package/dist/core/ingestion/call-routing.d.ts +23 -2
  26. package/dist/core/ingestion/call-routing.js +21 -0
  27. package/dist/core/ingestion/entry-point-scoring.js +36 -26
  28. package/dist/core/ingestion/framework-detection.d.ts +10 -2
  29. package/dist/core/ingestion/framework-detection.js +49 -12
  30. package/dist/core/ingestion/heritage-processor.js +47 -49
  31. package/dist/core/ingestion/import-processor.d.ts +1 -1
  32. package/dist/core/ingestion/import-processor.js +103 -194
  33. package/dist/core/ingestion/import-resolution.d.ts +101 -0
  34. package/dist/core/ingestion/import-resolution.js +251 -0
  35. package/dist/core/ingestion/language-config.d.ts +3 -0
  36. package/dist/core/ingestion/language-config.js +13 -0
  37. package/dist/core/ingestion/markdown-processor.d.ts +17 -0
  38. package/dist/core/ingestion/markdown-processor.js +124 -0
  39. package/dist/core/ingestion/mro-processor.js +8 -3
  40. package/dist/core/ingestion/named-binding-extraction.d.ts +9 -43
  41. package/dist/core/ingestion/named-binding-extraction.js +89 -79
  42. package/dist/core/ingestion/parsing-processor.d.ts +3 -2
  43. package/dist/core/ingestion/parsing-processor.js +27 -60
  44. package/dist/core/ingestion/pipeline.d.ts +10 -0
  45. package/dist/core/ingestion/pipeline.js +425 -4
  46. package/dist/core/ingestion/resolution-context.d.ts +5 -0
  47. package/dist/core/ingestion/resolution-context.js +7 -4
  48. package/dist/core/ingestion/resolvers/index.d.ts +1 -1
  49. package/dist/core/ingestion/resolvers/index.js +1 -1
  50. package/dist/core/ingestion/resolvers/jvm.d.ts +2 -1
  51. package/dist/core/ingestion/resolvers/jvm.js +25 -9
  52. package/dist/core/ingestion/resolvers/php.d.ts +14 -0
  53. package/dist/core/ingestion/resolvers/php.js +43 -3
  54. package/dist/core/ingestion/resolvers/utils.d.ts +5 -0
  55. package/dist/core/ingestion/resolvers/utils.js +16 -0
  56. package/dist/core/ingestion/symbol-table.d.ts +29 -3
  57. package/dist/core/ingestion/symbol-table.js +42 -9
  58. package/dist/core/ingestion/tree-sitter-queries.d.ts +12 -12
  59. package/dist/core/ingestion/tree-sitter-queries.js +243 -2
  60. package/dist/core/ingestion/type-env.d.ts +28 -1
  61. package/dist/core/ingestion/type-env.js +451 -72
  62. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +5 -0
  63. package/dist/core/ingestion/type-extractors/c-cpp.js +146 -2
  64. package/dist/core/ingestion/type-extractors/csharp.js +189 -16
  65. package/dist/core/ingestion/type-extractors/go.js +45 -0
  66. package/dist/core/ingestion/type-extractors/index.d.ts +1 -1
  67. package/dist/core/ingestion/type-extractors/index.js +1 -1
  68. package/dist/core/ingestion/type-extractors/jvm.js +244 -69
  69. package/dist/core/ingestion/type-extractors/php.js +31 -4
  70. package/dist/core/ingestion/type-extractors/python.js +89 -17
  71. package/dist/core/ingestion/type-extractors/ruby.js +17 -2
  72. package/dist/core/ingestion/type-extractors/rust.js +72 -4
  73. package/dist/core/ingestion/type-extractors/shared.d.ts +12 -2
  74. package/dist/core/ingestion/type-extractors/shared.js +115 -13
  75. package/dist/core/ingestion/type-extractors/swift.js +7 -6
  76. package/dist/core/ingestion/type-extractors/types.d.ts +54 -11
  77. package/dist/core/ingestion/type-extractors/typescript.js +171 -9
  78. package/dist/core/ingestion/utils.d.ts +2 -95
  79. package/dist/core/ingestion/utils.js +3 -892
  80. package/dist/core/ingestion/workers/parse-worker.d.ts +36 -11
  81. package/dist/core/ingestion/workers/parse-worker.js +116 -95
  82. package/dist/core/lbug/csv-generator.js +18 -1
  83. package/dist/core/lbug/lbug-adapter.d.ts +12 -0
  84. package/dist/core/lbug/lbug-adapter.js +71 -4
  85. package/dist/core/lbug/schema.d.ts +6 -4
  86. package/dist/core/lbug/schema.js +27 -3
  87. package/dist/mcp/core/embedder.js +11 -3
  88. package/dist/mcp/core/lbug-adapter.d.ts +22 -0
  89. package/dist/mcp/core/lbug-adapter.js +178 -23
  90. package/dist/mcp/local/local-backend.d.ts +22 -0
  91. package/dist/mcp/local/local-backend.js +136 -32
  92. package/dist/mcp/resources.js +13 -0
  93. package/dist/mcp/server.js +26 -4
  94. package/dist/mcp/tools.js +17 -7
  95. package/dist/server/api.d.ts +19 -1
  96. package/dist/server/api.js +66 -6
  97. package/dist/storage/git.d.ts +12 -0
  98. package/dist/storage/git.js +21 -0
  99. package/package.json +12 -4
@@ -9,9 +9,27 @@ let db = null;
9
9
  let conn = null;
10
10
  let currentDbPath = null;
11
11
  let ftsLoaded = false;
12
+ /** Expose the current Database for pool adapter reuse in tests. */
13
+ export const getDatabase = () => db;
12
14
  // Global session lock for operations that touch module-level lbug globals.
13
15
  // This guarantees no DB switch can happen while an operation is running.
14
16
  let sessionLock = Promise.resolve();
17
+ /** Number of times to retry on a BUSY / lock-held error before giving up. */
18
+ const DB_LOCK_RETRY_ATTEMPTS = 3;
19
+ /** Base back-off in ms between BUSY retries (multiplied by attempt number). */
20
+ const DB_LOCK_RETRY_DELAY_MS = 500;
21
+ /**
22
+ * Return true when the error message indicates that another process holds
23
+ * an exclusive lock on the LadybugDB file (e.g. `gitnexus analyze` or
24
+ * `gitnexus serve` running at the same time).
25
+ */
26
+ export const isDbBusyError = (err) => {
27
+ const msg = (err instanceof Error ? err.message : String(err)).toLowerCase();
28
+ return (msg.includes('busy')
29
+ || msg.includes('lock')
30
+ || msg.includes('already in use')
31
+ || msg.includes('could not set lock'));
32
+ };
15
33
  const runWithSessionLock = async (operation) => {
16
34
  const previous = sessionLock;
17
35
  let release = null;
@@ -33,12 +51,50 @@ export const initLbug = async (dbPath) => {
33
51
  /**
34
52
  * Execute multiple queries against one repo DB atomically.
35
53
  * While the callback runs, no other request can switch the active DB.
54
+ *
55
+ * Automatically retries up to DB_LOCK_RETRY_ATTEMPTS times when the
56
+ * database is busy (e.g. `gitnexus analyze` holds the write lock).
57
+ * Each retry waits DB_LOCK_RETRY_DELAY_MS * attempt milliseconds.
36
58
  */
37
59
  export const withLbugDb = async (dbPath, operation) => {
38
- return runWithSessionLock(async () => {
39
- await ensureLbugInitialized(dbPath);
40
- return operation();
41
- });
60
+ let lastError;
61
+ for (let attempt = 1; attempt <= DB_LOCK_RETRY_ATTEMPTS; attempt++) {
62
+ try {
63
+ return await runWithSessionLock(async () => {
64
+ await ensureLbugInitialized(dbPath);
65
+ return operation();
66
+ });
67
+ }
68
+ catch (err) {
69
+ lastError = err;
70
+ if (!isDbBusyError(err) || attempt === DB_LOCK_RETRY_ATTEMPTS) {
71
+ throw err;
72
+ }
73
+ // Close stale connection inside the session lock to prevent race conditions
74
+ // with concurrent operations that might acquire the lock between cleanup steps
75
+ await runWithSessionLock(async () => {
76
+ try {
77
+ if (conn)
78
+ await conn.close();
79
+ }
80
+ catch { /* best-effort */ }
81
+ try {
82
+ if (db)
83
+ await db.close();
84
+ }
85
+ catch { /* best-effort */ }
86
+ conn = null;
87
+ db = null;
88
+ currentDbPath = null;
89
+ ftsLoaded = false;
90
+ });
91
+ // Sleep outside the lock — no need to block others while waiting
92
+ await new Promise(resolve => setTimeout(resolve, DB_LOCK_RETRY_DELAY_MS * attempt));
93
+ }
94
+ }
95
+ // This line is unreachable — the loop either returns or throws inside,
96
+ // but TypeScript needs an explicit throw to satisfy the return type.
97
+ throw lastError;
42
98
  };
43
99
  const ensureLbugInitialized = async (dbPath) => {
44
100
  if (conn && currentDbPath === dbPath) {
@@ -321,6 +377,9 @@ const getCopyQuery = (table, filePath) => {
321
377
  if (table === 'Process') {
322
378
  return `COPY ${t}(id, label, heuristicLabel, processType, stepCount, communities, entryPointId, terminalId) FROM "${filePath}" ${COPY_CSV_OPTS}`;
323
379
  }
380
+ if (table === 'Section') {
381
+ return `COPY ${t}(id, name, filePath, startLine, endLine, level, content, description) FROM "${filePath}" ${COPY_CSV_OPTS}`;
382
+ }
324
383
  if (table === 'Method') {
325
384
  return `COPY ${t}(id, name, filePath, startLine, endLine, isExported, content, description, parameterCount, returnType) FROM "${filePath}" ${COPY_CSV_OPTS}`;
326
385
  }
@@ -361,6 +420,10 @@ export const insertNodeToLbug = async (label, properties, dbPath) => {
361
420
  else if (label === 'Folder') {
362
421
  query = `CREATE (n:Folder {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}})`;
363
422
  }
423
+ else if (label === 'Section') {
424
+ const descPart = properties.description ? `, description: ${escapeValue(properties.description)}` : '';
425
+ query = `CREATE (n:Section {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, level: ${properties.level || 1}, content: ${escapeValue(properties.content || '')}${descPart}})`;
426
+ }
364
427
  else if (TABLES_WITH_EXPORTED.has(label)) {
365
428
  const descPart = properties.description ? `, description: ${escapeValue(properties.description)}` : '';
366
429
  query = `CREATE (n:${t} {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, isExported: ${!!properties.isExported}, content: ${escapeValue(properties.content || '')}${descPart}})`;
@@ -436,6 +499,10 @@ export const batchInsertNodesToLbug = async (nodes, dbPath) => {
436
499
  else if (label === 'Folder') {
437
500
  query = `MERGE (n:Folder {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}`;
438
501
  }
502
+ else if (label === 'Section') {
503
+ const descPart = properties.description ? `, n.description = ${escapeValue(properties.description)}` : '';
504
+ query = `MERGE (n:Section {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.level = ${properties.level || 1}, n.content = ${escapeValue(properties.content || '')}${descPart}`;
505
+ }
439
506
  else if (TABLES_WITH_EXPORTED.has(label)) {
440
507
  const descPart = properties.description ? `, n.description = ${escapeValue(properties.description)}` : '';
441
508
  query = `MERGE (n:${t} {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.isExported = ${!!properties.isExported}, n.content = ${escapeValue(properties.content || '')}${descPart}`;
@@ -8,10 +8,10 @@
8
8
  * This allows LLMs to write natural Cypher queries like:
9
9
  * MATCH (f:Function)-[r:CodeRelation {type: 'CALLS'}]->(g:Function) RETURN f, g
10
10
  */
11
- export declare const NODE_TABLES: readonly ["File", "Folder", "Function", "Class", "Interface", "Method", "CodeElement", "Community", "Process", "Struct", "Enum", "Macro", "Typedef", "Union", "Namespace", "Trait", "Impl", "TypeAlias", "Const", "Static", "Property", "Record", "Delegate", "Annotation", "Constructor", "Template", "Module"];
11
+ export declare const NODE_TABLES: readonly ["File", "Folder", "Function", "Class", "Interface", "Method", "CodeElement", "Community", "Process", "Section", "Struct", "Enum", "Macro", "Typedef", "Union", "Namespace", "Trait", "Impl", "TypeAlias", "Const", "Static", "Property", "Record", "Delegate", "Annotation", "Constructor", "Template", "Module"];
12
12
  export type NodeTableName = typeof NODE_TABLES[number];
13
13
  export declare const REL_TABLE_NAME = "CodeRelation";
14
- export declare const REL_TYPES: readonly ["CONTAINS", "DEFINES", "IMPORTS", "CALLS", "EXTENDS", "IMPLEMENTS", "HAS_METHOD", "OVERRIDES", "MEMBER_OF", "STEP_IN_PROCESS"];
14
+ export declare const REL_TYPES: readonly ["CONTAINS", "DEFINES", "IMPORTS", "CALLS", "EXTENDS", "IMPLEMENTS", "HAS_METHOD", "HAS_PROPERTY", "ACCESSES", "OVERRIDES", "MEMBER_OF", "STEP_IN_PROCESS"];
15
15
  export type RelType = typeof REL_TYPES[number];
16
16
  export declare const EMBEDDING_TABLE_NAME = "CodeEmbedding";
17
17
  export declare const FILE_SCHEMA = "\nCREATE NODE TABLE File (\n id STRING,\n name STRING,\n filePath STRING,\n content STRING,\n PRIMARY KEY (id)\n)";
@@ -41,8 +41,10 @@ export declare const ANNOTATION_SCHEMA: string;
41
41
  export declare const CONSTRUCTOR_SCHEMA: string;
42
42
  export declare const TEMPLATE_SCHEMA: string;
43
43
  export declare const MODULE_SCHEMA: string;
44
- export declare const RELATION_SCHEMA = "\nCREATE REL TABLE CodeRelation (\n FROM File TO File,\n FROM File TO Folder,\n FROM File TO Function,\n FROM File TO Class,\n FROM File TO Interface,\n FROM File TO Method,\n FROM File TO CodeElement,\n FROM File TO `Struct`,\n FROM File TO `Enum`,\n FROM File TO `Macro`,\n FROM File TO `Typedef`,\n FROM File TO `Union`,\n FROM File TO `Namespace`,\n FROM File TO `Trait`,\n FROM File TO `Impl`,\n FROM File TO `TypeAlias`,\n FROM File TO `Const`,\n FROM File TO `Static`,\n FROM File TO `Property`,\n FROM File TO `Record`,\n FROM File TO `Delegate`,\n FROM File TO `Annotation`,\n FROM File TO `Constructor`,\n FROM File TO `Template`,\n FROM File TO `Module`,\n FROM Folder TO Folder,\n FROM Folder TO File,\n FROM Function TO Function,\n FROM Function TO Method,\n FROM Function TO Class,\n FROM Function TO Community,\n FROM Function TO `Macro`,\n FROM Function TO `Struct`,\n FROM Function TO `Template`,\n FROM Function TO `Enum`,\n FROM Function TO `Namespace`,\n FROM Function TO `TypeAlias`,\n FROM Function TO `Module`,\n FROM Function TO `Impl`,\n FROM Function TO Interface,\n FROM Function TO `Constructor`,\n FROM Function TO `Const`,\n FROM Function TO `Typedef`,\n FROM Function TO `Union`,\n FROM Function TO `Property`,\n FROM Class TO Method,\n FROM Class TO Function,\n FROM Class TO Class,\n FROM Class TO Interface,\n FROM Class TO Community,\n FROM Class TO `Template`,\n FROM Class TO `TypeAlias`,\n FROM Class TO `Struct`,\n FROM Class TO `Enum`,\n FROM Class TO `Annotation`,\n FROM Class TO `Constructor`,\n FROM Class TO `Trait`,\n FROM Class TO `Macro`,\n FROM Class TO `Impl`,\n FROM Class TO `Union`,\n FROM Class TO `Namespace`,\n FROM Class TO `Typedef`,\n FROM Class TO `Property`,\n FROM Method TO Function,\n FROM Method TO Method,\n FROM Method TO Class,\n FROM Method TO Community,\n FROM Method TO `Template`,\n FROM Method TO `Struct`,\n FROM Method TO `TypeAlias`,\n FROM Method TO `Enum`,\n FROM Method TO `Macro`,\n FROM Method TO `Namespace`,\n FROM Method TO `Module`,\n FROM Method TO `Impl`,\n FROM Method TO Interface,\n FROM Method TO `Constructor`,\n FROM Method TO `Property`,\n FROM `Template` TO `Template`,\n FROM `Template` TO Function,\n FROM `Template` TO Method,\n FROM `Template` TO Class,\n FROM `Template` TO `Struct`,\n FROM `Template` TO `TypeAlias`,\n FROM `Template` TO `Enum`,\n FROM `Template` TO `Macro`,\n FROM `Template` TO Interface,\n FROM `Template` TO `Constructor`,\n FROM `Module` TO `Module`,\n FROM CodeElement TO Community,\n FROM Interface TO Community,\n FROM Interface TO Function,\n FROM Interface TO Method,\n FROM Interface TO Class,\n FROM Interface TO Interface,\n FROM Interface TO `TypeAlias`,\n FROM Interface TO `Struct`,\n FROM Interface TO `Constructor`,\n FROM Interface TO `Property`,\n FROM `Struct` TO Community,\n FROM `Struct` TO `Trait`,\n FROM `Struct` TO `Struct`,\n FROM `Struct` TO Class,\n FROM `Struct` TO `Enum`,\n FROM `Struct` TO Function,\n FROM `Struct` TO Method,\n FROM `Struct` TO Interface,\n FROM `Struct` TO `Constructor`,\n FROM `Struct` TO `Property`,\n FROM `Enum` TO `Enum`,\n FROM `Enum` TO Community,\n FROM `Enum` TO Class,\n FROM `Enum` TO Interface,\n FROM `Macro` TO Community,\n FROM `Macro` TO Function,\n FROM `Macro` TO Method,\n FROM `Module` TO Function,\n FROM `Module` TO Method,\n FROM `Typedef` TO Community,\n FROM `Union` TO Community,\n FROM `Namespace` TO Community,\n FROM `Namespace` TO `Struct`,\n FROM `Trait` TO Method,\n FROM `Trait` TO `Constructor`,\n FROM `Trait` TO `Property`,\n FROM `Trait` TO Community,\n FROM `Impl` TO Method,\n FROM `Impl` TO `Constructor`,\n FROM `Impl` TO `Property`,\n FROM `Impl` TO Community,\n FROM `Impl` TO `Trait`,\n FROM `Impl` TO `Struct`,\n FROM `Impl` TO `Impl`,\n FROM `TypeAlias` TO Community,\n FROM `TypeAlias` TO `Trait`,\n FROM `TypeAlias` TO Class,\n FROM `Const` TO Community,\n FROM `Static` TO Community,\n FROM `Property` TO Community,\n FROM `Record` TO Method,\n FROM `Record` TO `Constructor`,\n FROM `Record` TO `Property`,\n FROM `Record` TO Community,\n FROM `Delegate` TO Community,\n FROM `Annotation` TO Community,\n FROM `Constructor` TO Community,\n FROM `Constructor` TO Interface,\n FROM `Constructor` TO Class,\n FROM `Constructor` TO Method,\n FROM `Constructor` TO Function,\n FROM `Constructor` TO `Constructor`,\n FROM `Constructor` TO `Struct`,\n FROM `Constructor` TO `Macro`,\n FROM `Constructor` TO `Template`,\n FROM `Constructor` TO `TypeAlias`,\n FROM `Constructor` TO `Enum`,\n FROM `Constructor` TO `Annotation`,\n FROM `Constructor` TO `Impl`,\n FROM `Constructor` TO `Namespace`,\n FROM `Constructor` TO `Module`,\n FROM `Constructor` TO `Property`,\n FROM `Constructor` TO `Typedef`,\n FROM `Template` TO Community,\n FROM `Module` TO Community,\n FROM Function TO Process,\n FROM Method TO Process,\n FROM Class TO Process,\n FROM Interface TO Process,\n FROM `Struct` TO Process,\n FROM `Constructor` TO Process,\n FROM `Module` TO Process,\n FROM `Macro` TO Process,\n FROM `Impl` TO Process,\n FROM `Typedef` TO Process,\n FROM `TypeAlias` TO Process,\n FROM `Enum` TO Process,\n FROM `Union` TO Process,\n FROM `Namespace` TO Process,\n FROM `Trait` TO Process,\n FROM `Const` TO Process,\n FROM `Static` TO Process,\n FROM `Property` TO Process,\n FROM `Record` TO Process,\n FROM `Delegate` TO Process,\n FROM `Annotation` TO Process,\n FROM `Template` TO Process,\n FROM CodeElement TO Process,\n type STRING,\n confidence DOUBLE,\n reason STRING,\n step INT32\n)";
45
- export declare const EMBEDDING_SCHEMA = "\nCREATE NODE TABLE CodeEmbedding (\n nodeId STRING,\n embedding FLOAT[384],\n PRIMARY KEY (nodeId)\n)";
44
+ export declare const SECTION_SCHEMA = "\nCREATE NODE TABLE Section (\n id STRING,\n name STRING,\n filePath STRING,\n startLine INT64,\n endLine INT64,\n level INT64,\n content STRING,\n description STRING,\n PRIMARY KEY (id)\n)";
45
+ export declare const RELATION_SCHEMA = "\nCREATE REL TABLE CodeRelation (\n FROM File TO File,\n FROM File TO Folder,\n FROM File TO Function,\n FROM File TO Class,\n FROM File TO Interface,\n FROM File TO Method,\n FROM File TO CodeElement,\n FROM File TO `Struct`,\n FROM File TO `Enum`,\n FROM File TO `Macro`,\n FROM File TO `Typedef`,\n FROM File TO `Union`,\n FROM File TO `Namespace`,\n FROM File TO `Trait`,\n FROM File TO `Impl`,\n FROM File TO `TypeAlias`,\n FROM File TO `Const`,\n FROM File TO `Static`,\n FROM File TO `Property`,\n FROM File TO `Record`,\n FROM File TO `Delegate`,\n FROM File TO `Annotation`,\n FROM File TO `Constructor`,\n FROM File TO `Template`,\n FROM File TO `Module`,\n FROM File TO Section,\n FROM Folder TO Folder,\n FROM Folder TO File,\n FROM Function TO Function,\n FROM Function TO Method,\n FROM Function TO Class,\n FROM Function TO Community,\n FROM Function TO `Macro`,\n FROM Function TO `Struct`,\n FROM Function TO `Template`,\n FROM Function TO `Enum`,\n FROM Function TO `Namespace`,\n FROM Function TO `TypeAlias`,\n FROM Function TO `Module`,\n FROM Function TO `Impl`,\n FROM Function TO Interface,\n FROM Function TO `Constructor`,\n FROM Function TO `Const`,\n FROM Function TO `Typedef`,\n FROM Function TO `Union`,\n FROM Function TO `Property`,\n FROM Class TO Method,\n FROM Class TO Function,\n FROM Class TO Class,\n FROM Class TO Interface,\n FROM Class TO Community,\n FROM Class TO `Template`,\n FROM Class TO `TypeAlias`,\n FROM Class TO `Struct`,\n FROM Class TO `Enum`,\n FROM Class TO `Annotation`,\n FROM Class TO `Constructor`,\n FROM Class TO `Trait`,\n FROM Class TO `Macro`,\n FROM Class TO `Impl`,\n FROM Class TO `Union`,\n FROM Class TO `Namespace`,\n FROM Class TO `Typedef`,\n FROM Class TO `Property`,\n FROM Method TO Function,\n FROM Method TO Method,\n FROM Method TO Class,\n FROM Method TO Community,\n FROM Method TO `Template`,\n FROM Method TO `Struct`,\n FROM Method TO `TypeAlias`,\n FROM Method TO `Enum`,\n FROM Method TO `Macro`,\n FROM Method TO `Namespace`,\n FROM Method TO `Module`,\n FROM Method TO `Impl`,\n FROM Method TO Interface,\n FROM Method TO `Constructor`,\n FROM Method TO `Property`,\n FROM `Template` TO `Template`,\n FROM `Template` TO Function,\n FROM `Template` TO Method,\n FROM `Template` TO Class,\n FROM `Template` TO `Struct`,\n FROM `Template` TO `TypeAlias`,\n FROM `Template` TO `Enum`,\n FROM `Template` TO `Macro`,\n FROM `Template` TO Interface,\n FROM `Template` TO `Constructor`,\n FROM `Module` TO `Module`,\n FROM Section TO Section,\n FROM Section TO File,\n FROM CodeElement TO Community,\n FROM Interface TO Community,\n FROM Interface TO Function,\n FROM Interface TO Method,\n FROM Interface TO Class,\n FROM Interface TO Interface,\n FROM Interface TO `TypeAlias`,\n FROM Interface TO `Struct`,\n FROM Interface TO `Constructor`,\n FROM Interface TO `Property`,\n FROM `Struct` TO Community,\n FROM `Struct` TO `Trait`,\n FROM `Struct` TO `Struct`,\n FROM `Struct` TO Class,\n FROM `Struct` TO `Enum`,\n FROM `Struct` TO Function,\n FROM `Struct` TO Method,\n FROM `Struct` TO Interface,\n FROM `Struct` TO `Constructor`,\n FROM `Struct` TO `Property`,\n FROM `Enum` TO `Enum`,\n FROM `Enum` TO Community,\n FROM `Enum` TO Class,\n FROM `Enum` TO Interface,\n FROM `Macro` TO Community,\n FROM `Macro` TO Function,\n FROM `Macro` TO Method,\n FROM `Module` TO Function,\n FROM `Module` TO Method,\n FROM `Typedef` TO Community,\n FROM `Union` TO Community,\n FROM `Namespace` TO Community,\n FROM `Namespace` TO `Struct`,\n FROM `Trait` TO Method,\n FROM `Trait` TO `Constructor`,\n FROM `Trait` TO `Property`,\n FROM `Trait` TO Community,\n FROM `Impl` TO Method,\n FROM `Impl` TO `Constructor`,\n FROM `Impl` TO `Property`,\n FROM `Impl` TO Community,\n FROM `Impl` TO `Trait`,\n FROM `Impl` TO `Struct`,\n FROM `Impl` TO `Impl`,\n FROM `TypeAlias` TO Community,\n FROM `TypeAlias` TO `Trait`,\n FROM `TypeAlias` TO Class,\n FROM `Const` TO Community,\n FROM `Static` TO Community,\n FROM `Property` TO Community,\n FROM `Record` TO Method,\n FROM `Record` TO `Constructor`,\n FROM `Record` TO `Property`,\n FROM `Record` TO Community,\n FROM `Delegate` TO Community,\n FROM `Annotation` TO Community,\n FROM `Constructor` TO Community,\n FROM `Constructor` TO Interface,\n FROM `Constructor` TO Class,\n FROM `Constructor` TO Method,\n FROM `Constructor` TO Function,\n FROM `Constructor` TO `Constructor`,\n FROM `Constructor` TO `Struct`,\n FROM `Constructor` TO `Macro`,\n FROM `Constructor` TO `Template`,\n FROM `Constructor` TO `TypeAlias`,\n FROM `Constructor` TO `Enum`,\n FROM `Constructor` TO `Annotation`,\n FROM `Constructor` TO `Impl`,\n FROM `Constructor` TO `Namespace`,\n FROM `Constructor` TO `Module`,\n FROM `Constructor` TO `Property`,\n FROM `Constructor` TO `Typedef`,\n FROM `Template` TO Community,\n FROM `Module` TO Community,\n FROM Function TO Process,\n FROM Method TO Process,\n FROM Class TO Process,\n FROM Interface TO Process,\n FROM `Struct` TO Process,\n FROM `Constructor` TO Process,\n FROM `Module` TO Process,\n FROM `Macro` TO Process,\n FROM `Impl` TO Process,\n FROM `Typedef` TO Process,\n FROM `TypeAlias` TO Process,\n FROM `Enum` TO Process,\n FROM `Union` TO Process,\n FROM `Namespace` TO Process,\n FROM `Trait` TO Process,\n FROM `Const` TO Process,\n FROM `Static` TO Process,\n FROM `Property` TO Process,\n FROM `Record` TO Process,\n FROM `Delegate` TO Process,\n FROM `Annotation` TO Process,\n FROM `Template` TO Process,\n FROM CodeElement TO Process,\n type STRING,\n confidence DOUBLE,\n reason STRING,\n step INT32\n)";
46
+ export declare const EMBEDDING_DIMS: number;
47
+ export declare const EMBEDDING_SCHEMA: string;
46
48
  /**
47
49
  * Create vector index for semantic search
48
50
  * Uses HNSW (Hierarchical Navigable Small World) algorithm with cosine similarity
@@ -12,7 +12,7 @@
12
12
  // NODE TABLE NAMES
13
13
  // ============================================================================
14
14
  export const NODE_TABLES = [
15
- 'File', 'Folder', 'Function', 'Class', 'Interface', 'Method', 'CodeElement', 'Community', 'Process',
15
+ 'File', 'Folder', 'Function', 'Class', 'Interface', 'Method', 'CodeElement', 'Community', 'Process', 'Section',
16
16
  // Multi-language support
17
17
  'Struct', 'Enum', 'Macro', 'Typedef', 'Union', 'Namespace', 'Trait', 'Impl',
18
18
  'TypeAlias', 'Const', 'Static', 'Property', 'Record', 'Delegate', 'Annotation', 'Constructor', 'Template', 'Module'
@@ -22,7 +22,7 @@ export const NODE_TABLES = [
22
22
  // ============================================================================
23
23
  export const REL_TABLE_NAME = 'CodeRelation';
24
24
  // Valid relation types
25
- export const REL_TYPES = ['CONTAINS', 'DEFINES', 'IMPORTS', 'CALLS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'OVERRIDES', 'MEMBER_OF', 'STEP_IN_PROCESS'];
25
+ export const REL_TYPES = ['CONTAINS', 'DEFINES', 'IMPORTS', 'CALLS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'ACCESSES', 'OVERRIDES', 'MEMBER_OF', 'STEP_IN_PROCESS'];
26
26
  // ============================================================================
27
27
  // EMBEDDING TABLE
28
28
  // ============================================================================
@@ -171,6 +171,19 @@ export const ANNOTATION_SCHEMA = CODE_ELEMENT_BASE('Annotation');
171
171
  export const CONSTRUCTOR_SCHEMA = CODE_ELEMENT_BASE('Constructor');
172
172
  export const TEMPLATE_SCHEMA = CODE_ELEMENT_BASE('Template');
173
173
  export const MODULE_SCHEMA = CODE_ELEMENT_BASE('Module');
174
+ // Markdown heading sections
175
+ export const SECTION_SCHEMA = `
176
+ CREATE NODE TABLE Section (
177
+ id STRING,
178
+ name STRING,
179
+ filePath STRING,
180
+ startLine INT64,
181
+ endLine INT64,
182
+ level INT64,
183
+ content STRING,
184
+ description STRING,
185
+ PRIMARY KEY (id)
186
+ )`;
174
187
  // ============================================================================
175
188
  // RELATION TABLE SCHEMA
176
189
  // Single table with 'type' property - connects all node tables
@@ -202,6 +215,7 @@ CREATE REL TABLE ${REL_TABLE_NAME} (
202
215
  FROM File TO \`Constructor\`,
203
216
  FROM File TO \`Template\`,
204
217
  FROM File TO \`Module\`,
218
+ FROM File TO Section,
205
219
  FROM Folder TO Folder,
206
220
  FROM Folder TO File,
207
221
  FROM Function TO Function,
@@ -266,6 +280,8 @@ CREATE REL TABLE ${REL_TABLE_NAME} (
266
280
  FROM \`Template\` TO Interface,
267
281
  FROM \`Template\` TO \`Constructor\`,
268
282
  FROM \`Module\` TO \`Module\`,
283
+ FROM Section TO Section,
284
+ FROM Section TO File,
269
285
  FROM CodeElement TO Community,
270
286
  FROM Interface TO Community,
271
287
  FROM Interface TO Function,
@@ -373,10 +389,16 @@ CREATE REL TABLE ${REL_TABLE_NAME} (
373
389
  // EMBEDDING TABLE SCHEMA
374
390
  // Separate table for vector storage to avoid copy-on-write overhead
375
391
  // ============================================================================
392
+ /** Embedding vector dimensions. Default 384 (snowflake-arctic-embed-xs). */
393
+ const _rawDims = parseInt(process.env.GITNEXUS_EMBEDDING_DIMS ?? '384', 10);
394
+ if (Number.isNaN(_rawDims) || _rawDims <= 0) {
395
+ throw new Error(`GITNEXUS_EMBEDDING_DIMS must be a positive integer, got "${process.env.GITNEXUS_EMBEDDING_DIMS}"`);
396
+ }
397
+ export const EMBEDDING_DIMS = _rawDims;
376
398
  export const EMBEDDING_SCHEMA = `
377
399
  CREATE NODE TABLE ${EMBEDDING_TABLE_NAME} (
378
400
  nodeId STRING,
379
- embedding FLOAT[384],
401
+ embedding FLOAT[${EMBEDDING_DIMS}],
380
402
  PRIMARY KEY (nodeId)
381
403
  )`;
382
404
  /**
@@ -419,6 +441,8 @@ export const NODE_SCHEMA_QUERIES = [
419
441
  CONSTRUCTOR_SCHEMA,
420
442
  TEMPLATE_SCHEMA,
421
443
  MODULE_SCHEMA,
444
+ // Markdown support
445
+ SECTION_SCHEMA,
422
446
  ];
423
447
  export const REL_SCHEMA_QUERIES = [
424
448
  RELATION_SCHEMA,
@@ -5,9 +5,9 @@
5
5
  * For MCP, we only need to compute query embeddings, not batch embed.
6
6
  */
7
7
  import { pipeline, env } from '@huggingface/transformers';
8
+ import { isHttpMode, getHttpDimensions, httpEmbedQuery } from '../../core/embeddings/http-client.js';
8
9
  // Model config
9
10
  const MODEL_ID = 'Snowflake/snowflake-arctic-embed-xs';
10
- const EMBEDDING_DIMS = 384;
11
11
  // Module-level state for singleton pattern
12
12
  let embedderInstance = null;
13
13
  let isInitializing = false;
@@ -16,6 +16,9 @@ let initPromise = null;
16
16
  * Initialize the embedding model (lazy, on first search)
17
17
  */
18
18
  export const initEmbedder = async () => {
19
+ if (isHttpMode()) {
20
+ throw new Error('initEmbedder() should not be called in HTTP mode.');
21
+ }
19
22
  if (embedderInstance) {
20
23
  return embedderInstance;
21
24
  }
@@ -75,11 +78,14 @@ export const initEmbedder = async () => {
75
78
  /**
76
79
  * Check if embedder is ready
77
80
  */
78
- export const isEmbedderReady = () => embedderInstance !== null;
81
+ export const isEmbedderReady = () => isHttpMode() || embedderInstance !== null;
79
82
  /**
80
83
  * Embed a query text for semantic search
81
84
  */
82
85
  export const embedQuery = async (query) => {
86
+ if (isHttpMode()) {
87
+ return httpEmbedQuery(query);
88
+ }
83
89
  const embedder = await initEmbedder();
84
90
  const result = await embedder(query, {
85
91
  pooling: 'mean',
@@ -90,7 +96,9 @@ export const embedQuery = async (query) => {
90
96
  /**
91
97
  * Get embedding dimensions
92
98
  */
93
- export const getEmbeddingDims = () => EMBEDDING_DIMS;
99
+ export const getEmbeddingDims = () => {
100
+ return getHttpDimensions() ?? 384;
101
+ };
94
102
  /**
95
103
  * Cleanup embedder
96
104
  */
@@ -12,11 +12,29 @@
12
12
  * @see https://docs.ladybugdb.com/concurrency — multiple Connections
13
13
  * from the same Database is the officially supported concurrency pattern.
14
14
  */
15
+ import lbug from '@ladybugdb/core';
16
+ /** Saved real stdout.write — used to silence LadybugDB native output without race conditions */
17
+ export declare const realStdoutWrite: any;
15
18
  /**
16
19
  * Initialize (or reuse) a Database + connection pool for a specific repo.
17
20
  * Retries on lock errors (e.g., when `gitnexus analyze` is running).
21
+ *
22
+ * Concurrent calls for the same repoId are deduplicated — the second caller
23
+ * awaits the first's in-progress init rather than starting a redundant one.
18
24
  */
19
25
  export declare const initLbug: (repoId: string, dbPath: string) => Promise<void>;
26
+ /**
27
+ * Initialize a pool entry from a pre-existing Database object.
28
+ *
29
+ * Used in tests to avoid the writable→close→read-only cycle that crashes
30
+ * on macOS due to N-API destructor segfaults. The pool adapter reuses
31
+ * the core adapter's writable Database instead of opening a new read-only one.
32
+ *
33
+ * The Database is registered in the shared dbCache so closeOne() decrements
34
+ * the refCount correctly. If the Database is already cached (e.g. another
35
+ * repoId already injected it), the existing entry is reused.
36
+ */
37
+ export declare function initLbugWithDb(repoId: string, existingDb: lbug.Database, dbPath: string): Promise<void>;
20
38
  export declare const executeQuery: (repoId: string, cypher: string) => Promise<any[]>;
21
39
  /**
22
40
  * Execute a parameterized query on a specific repo's connection pool.
@@ -33,3 +51,7 @@ export declare const closeLbug: (repoId?: string) => Promise<void>;
33
51
  * Check if a specific repo's pool is active
34
52
  */
35
53
  export declare const isLbugReady: (repoId: string) => boolean;
54
+ /** Regex to detect write operations in user-supplied Cypher queries */
55
+ export declare const CYPHER_WRITE_RE: RegExp;
56
+ /** Check if a Cypher query contains write operations */
57
+ export declare function isWriteQuery(query: string): boolean;
@@ -22,12 +22,12 @@ const MAX_POOL_SIZE = 5;
22
22
  const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
23
23
  /** Max connections per repo (caps concurrent queries per repo) */
24
24
  const MAX_CONNS_PER_REPO = 8;
25
- /** Connections created eagerly on init */
26
- const INITIAL_CONNS_PER_REPO = 2;
27
25
  let idleTimer = null;
28
26
  /** Saved real stdout.write — used to silence LadybugDB native output without race conditions */
29
- const realStdoutWrite = process.stdout.write.bind(process.stdout);
27
+ export const realStdoutWrite = process.stdout.write.bind(process.stdout);
30
28
  let stdoutSilenceCount = 0;
29
+ /** True while pre-warming connections — prevents watchdog from prematurely restoring stdout */
30
+ let preWarmActive = false;
31
31
  /**
32
32
  * Start the idle cleanup timer (runs every 60s)
33
33
  */
@@ -65,19 +65,42 @@ function evictLRU() {
65
65
  }
66
66
  }
67
67
  /**
68
- * Remove a repo from the pool and release its shared Database ref.
69
- *
70
- * LadybugDB's native .closeSync() triggers N-API destructor hooks that
71
- * segfault on Linux/macOS. Pool databases are opened read-only, so
72
- * there is no WAL to flush — just deleting the pool entry and letting
73
- * the GC (or process exit) reclaim native resources is safe.
68
+ * Remove a repo from the pool, close its connections, and release its
69
+ * shared Database ref. Only closes the Database when no other repoIds
70
+ * reference it (refCount === 0).
74
71
  */
75
72
  function closeOne(repoId) {
76
73
  const entry = pool.get(repoId);
77
- if (entry) {
78
- const shared = dbCache.get(entry.dbPath);
79
- if (shared && shared.refCount > 0) {
80
- shared.refCount--;
74
+ if (!entry)
75
+ return;
76
+ entry.closed = true;
77
+ // Close available connections — fire-and-forget with .catch() to prevent
78
+ // unhandled rejections. Native close() returns Promise<void> but can crash
79
+ // the N-API destructor on macOS/Windows; deferring to process exit lets
80
+ // dangerouslyIgnoreUnhandledErrors absorb the crash.
81
+ for (const conn of entry.available) {
82
+ conn.close().catch(() => { });
83
+ }
84
+ entry.available.length = 0;
85
+ // Checked-out connections can't be closed here — they're in-flight.
86
+ // The checkin() function detects entry.closed and closes them on return.
87
+ // Only close the Database when no other repoIds reference it.
88
+ // External databases (injected via initLbugWithDb) are never closed here —
89
+ // the core adapter owns them and handles their lifecycle.
90
+ const shared = dbCache.get(entry.dbPath);
91
+ if (shared) {
92
+ shared.refCount--;
93
+ if (shared.refCount === 0) {
94
+ if (shared.external) {
95
+ // External databases are owned by the core adapter — don't close
96
+ // or remove from cache. Keep the entry so future initLbug() calls
97
+ // for the same dbPath reuse it instead of hitting a file lock.
98
+ shared.refCount = 0;
99
+ }
100
+ else {
101
+ shared.db.close().catch(() => { });
102
+ dbCache.delete(entry.dbPath);
103
+ }
81
104
  }
82
105
  }
83
106
  pool.delete(repoId);
@@ -86,6 +109,7 @@ function closeOne(repoId) {
86
109
  * Create a new Connection from a repo's Database.
87
110
  * Silences stdout to prevent native module output from corrupting MCP stdio.
88
111
  */
112
+ let activeQueryCount = 0;
89
113
  function silenceStdout() {
90
114
  if (stdoutSilenceCount++ === 0) {
91
115
  process.stdout.write = (() => true);
@@ -97,6 +121,16 @@ function restoreStdout() {
97
121
  process.stdout.write = realStdoutWrite;
98
122
  }
99
123
  }
124
+ // Safety watchdog: restore stdout if it gets stuck silenced (e.g. native crash
125
+ // inside createConnection before restoreStdout runs).
126
+ // Exempts active queries and pre-warm — these legitimately hold silence for
127
+ // longer than 1 second (queries can take up to QUERY_TIMEOUT_MS = 30s).
128
+ setInterval(() => {
129
+ if (stdoutSilenceCount > 0 && !preWarmActive && activeQueryCount === 0) {
130
+ stdoutSilenceCount = 0;
131
+ process.stdout.write = realStdoutWrite;
132
+ }
133
+ }, 1000).unref();
100
134
  function createConnection(db) {
101
135
  silenceStdout();
102
136
  try {
@@ -112,9 +146,14 @@ const QUERY_TIMEOUT_MS = 30_000;
112
146
  const WAITER_TIMEOUT_MS = 15_000;
113
147
  const LOCK_RETRY_ATTEMPTS = 3;
114
148
  const LOCK_RETRY_DELAY_MS = 2000;
149
+ /** Deduplicates concurrent initLbug calls for the same repoId */
150
+ const initPromises = new Map();
115
151
  /**
116
152
  * Initialize (or reuse) a Database + connection pool for a specific repo.
117
153
  * Retries on lock errors (e.g., when `gitnexus analyze` is running).
154
+ *
155
+ * Concurrent calls for the same repoId are deduplicated — the second caller
156
+ * awaits the first's in-progress init rather than starting a redundant one.
118
157
  */
119
158
  export const initLbug = async (repoId, dbPath) => {
120
159
  const existing = pool.get(repoId);
@@ -122,6 +161,27 @@ export const initLbug = async (repoId, dbPath) => {
122
161
  existing.lastUsed = Date.now();
123
162
  return;
124
163
  }
164
+ // Deduplicate concurrent init calls for the same repoId —
165
+ // prevents double-init race when multiple parallel tool calls
166
+ // trigger initialization for the same repo simultaneously.
167
+ const pending = initPromises.get(repoId);
168
+ if (pending)
169
+ return pending;
170
+ const promise = doInitLbug(repoId, dbPath);
171
+ initPromises.set(repoId, promise);
172
+ try {
173
+ await promise;
174
+ }
175
+ finally {
176
+ initPromises.delete(repoId);
177
+ }
178
+ };
179
+ /**
180
+ * Internal init — creates DB, pre-warms connections, loads FTS, then registers pool.
181
+ * Pool entry is registered LAST so concurrent executeQuery calls see either
182
+ * "not initialized" (and throw) or a fully ready pool — never a half-built one.
183
+ */
184
+ async function doInitLbug(repoId, dbPath) {
125
185
  // Check if database exists
126
186
  try {
127
187
  await fs.stat(dbPath);
@@ -166,14 +226,22 @@ export const initLbug = async (repoId, dbPath) => {
166
226
  }
167
227
  shared.refCount++;
168
228
  const db = shared.db;
169
- // Pre-create a small pool of connections
229
+ // Pre-create the full pool upfront so createConnection() (which silences
230
+ // stdout) is never called lazily during active query execution.
231
+ // Mark preWarmActive so the watchdog timer doesn't interfere.
232
+ preWarmActive = true;
170
233
  const available = [];
171
- for (let i = 0; i < INITIAL_CONNS_PER_REPO; i++) {
172
- available.push(createConnection(db));
234
+ try {
235
+ for (let i = 0; i < MAX_CONNS_PER_REPO; i++) {
236
+ available.push(createConnection(db));
237
+ }
173
238
  }
174
- pool.set(repoId, { db, available, checkedOut: 0, waiters: [], lastUsed: Date.now(), dbPath });
175
- ensureIdleTimer();
176
- // Load FTS extension once per shared Database
239
+ finally {
240
+ preWarmActive = false;
241
+ }
242
+ // Load FTS extension once per shared Database.
243
+ // Done BEFORE pool registration so no concurrent checkout can grab
244
+ // the connection while the async FTS load is in progress.
177
245
  if (!shared.ftsLoaded) {
178
246
  try {
179
247
  await available[0].query('LOAD EXTENSION fts');
@@ -183,7 +251,67 @@ export const initLbug = async (repoId, dbPath) => {
183
251
  // Extension may not be installed — FTS queries will fail gracefully
184
252
  }
185
253
  }
186
- };
254
+ // Register pool entry only after all connections are pre-warmed and FTS is
255
+ // loaded. Concurrent executeQuery calls see either "not initialized"
256
+ // (and throw cleanly) or a fully ready pool — never a half-built one.
257
+ pool.set(repoId, { db, available, checkedOut: 0, waiters: [], lastUsed: Date.now(), dbPath, closed: false });
258
+ ensureIdleTimer();
259
+ }
260
+ /**
261
+ * Initialize a pool entry from a pre-existing Database object.
262
+ *
263
+ * Used in tests to avoid the writable→close→read-only cycle that crashes
264
+ * on macOS due to N-API destructor segfaults. The pool adapter reuses
265
+ * the core adapter's writable Database instead of opening a new read-only one.
266
+ *
267
+ * The Database is registered in the shared dbCache so closeOne() decrements
268
+ * the refCount correctly. If the Database is already cached (e.g. another
269
+ * repoId already injected it), the existing entry is reused.
270
+ */
271
+ export async function initLbugWithDb(repoId, existingDb, dbPath) {
272
+ const existing = pool.get(repoId);
273
+ if (existing) {
274
+ existing.lastUsed = Date.now();
275
+ return;
276
+ }
277
+ // Register in dbCache with external: true so other initLbug() calls
278
+ // for the same dbPath reuse this Database instead of trying to open
279
+ // a new one (which would fail with a file lock error).
280
+ // closeOne() respects the external flag and skips db.close().
281
+ let shared = dbCache.get(dbPath);
282
+ if (!shared) {
283
+ shared = { db: existingDb, refCount: 0, ftsLoaded: false, external: true };
284
+ dbCache.set(dbPath, shared);
285
+ }
286
+ shared.refCount++;
287
+ const available = [];
288
+ preWarmActive = true;
289
+ try {
290
+ for (let i = 0; i < MAX_CONNS_PER_REPO; i++) {
291
+ available.push(createConnection(existingDb));
292
+ }
293
+ }
294
+ finally {
295
+ preWarmActive = false;
296
+ }
297
+ // Load FTS extension if not already loaded on this Database
298
+ try {
299
+ await available[0].query('LOAD EXTENSION fts');
300
+ }
301
+ catch {
302
+ // Extension may already be loaded or not installed
303
+ }
304
+ pool.set(repoId, {
305
+ db: existingDb,
306
+ available,
307
+ checkedOut: 0,
308
+ waiters: [],
309
+ lastUsed: Date.now(),
310
+ dbPath,
311
+ closed: false
312
+ });
313
+ ensureIdleTimer();
314
+ }
187
315
  /**
188
316
  * Checkout a connection from the pool.
189
317
  * Returns an available connection, or creates a new one if under the cap.
@@ -195,11 +323,14 @@ function checkout(entry) {
195
323
  entry.checkedOut++;
196
324
  return Promise.resolve(entry.available.pop());
197
325
  }
198
- // Grow the pool if under the cap
326
+ // Pool was pre-warmed to MAX_CONNS_PER_REPO during init. If we're here
327
+ // with fewer total connections, something leaked — surface the bug rather
328
+ // than silently creating a connection (which would silence stdout mid-query).
199
329
  const totalConns = entry.available.length + entry.checkedOut;
200
330
  if (totalConns < MAX_CONNS_PER_REPO) {
201
- entry.checkedOut++;
202
- return Promise.resolve(createConnection(entry.db));
331
+ throw new Error(`Connection pool integrity error: expected ${MAX_CONNS_PER_REPO} ` +
332
+ `connections but found ${totalConns} (${entry.available.length} available, ` +
333
+ `${entry.checkedOut} checked out)`);
203
334
  }
204
335
  // At capacity — queue the caller with a timeout.
205
336
  return new Promise((resolve, reject) => {
@@ -218,10 +349,17 @@ function checkout(entry) {
218
349
  }
219
350
  /**
220
351
  * Return a connection to the pool after use.
352
+ * If the pool entry was closed while the connection was checked out (e.g.
353
+ * LRU eviction), close the orphaned connection instead of returning it.
221
354
  * If there are queued waiters, hand the connection directly to the next one
222
355
  * instead of putting it back in the available array (avoids race conditions).
223
356
  */
224
357
  function checkin(entry, conn) {
358
+ if (entry.closed) {
359
+ // Pool entry was deleted during checkout — close the orphaned connection
360
+ conn.close().catch(() => { });
361
+ return;
362
+ }
225
363
  if (entry.waiters.length > 0) {
226
364
  // Hand directly to the next waiter — no intermediate available state
227
365
  const waiter = entry.waiters.shift();
@@ -249,8 +387,13 @@ export const executeQuery = async (repoId, cypher) => {
249
387
  if (!entry) {
250
388
  throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initLbug first.`);
251
389
  }
390
+ if (isWriteQuery(cypher)) {
391
+ throw new Error('Write operations are not allowed. The pool adapter is read-only.');
392
+ }
252
393
  entry.lastUsed = Date.now();
253
394
  const conn = await checkout(entry);
395
+ silenceStdout();
396
+ activeQueryCount++;
254
397
  try {
255
398
  const queryResult = await withTimeout(conn.query(cypher), QUERY_TIMEOUT_MS, 'Query');
256
399
  const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
@@ -258,6 +401,8 @@ export const executeQuery = async (repoId, cypher) => {
258
401
  return rows;
259
402
  }
260
403
  finally {
404
+ activeQueryCount--;
405
+ restoreStdout();
261
406
  checkin(entry, conn);
262
407
  }
263
408
  };
@@ -272,6 +417,8 @@ export const executeParameterized = async (repoId, cypher, params) => {
272
417
  }
273
418
  entry.lastUsed = Date.now();
274
419
  const conn = await checkout(entry);
420
+ silenceStdout();
421
+ activeQueryCount++;
275
422
  try {
276
423
  const stmt = await withTimeout(conn.prepare(cypher), QUERY_TIMEOUT_MS, 'Prepare');
277
424
  if (!stmt.isSuccess()) {
@@ -284,6 +431,8 @@ export const executeParameterized = async (repoId, cypher, params) => {
284
431
  return rows;
285
432
  }
286
433
  finally {
434
+ activeQueryCount--;
435
+ restoreStdout();
287
436
  checkin(entry, conn);
288
437
  }
289
438
  };
@@ -309,3 +458,9 @@ export const closeLbug = async (repoId) => {
309
458
  * Check if a specific repo's pool is active
310
459
  */
311
460
  export const isLbugReady = (repoId) => pool.has(repoId);
461
+ /** Regex to detect write operations in user-supplied Cypher queries */
462
+ export const CYPHER_WRITE_RE = /\b(CREATE|DELETE|SET|MERGE|REMOVE|DROP|ALTER|COPY|DETACH)\b/i;
463
+ /** Check if a Cypher query contains write operations */
464
+ export function isWriteQuery(query) {
465
+ return CYPHER_WRITE_RE.test(query);
466
+ }