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.
- package/README.md +22 -1
- package/dist/cli/ai-context.d.ts +1 -1
- package/dist/cli/ai-context.js +1 -1
- package/dist/cli/analyze.d.ts +2 -0
- package/dist/cli/analyze.js +54 -21
- package/dist/cli/index.js +2 -1
- package/dist/cli/setup.js +78 -1
- package/dist/config/supported-languages.d.ts +30 -0
- package/dist/config/supported-languages.js +30 -0
- package/dist/core/embeddings/embedder.d.ts +6 -1
- package/dist/core/embeddings/embedder.js +65 -5
- package/dist/core/embeddings/embedding-pipeline.js +11 -9
- package/dist/core/embeddings/http-client.d.ts +31 -0
- package/dist/core/embeddings/http-client.js +179 -0
- package/dist/core/embeddings/index.d.ts +1 -0
- package/dist/core/embeddings/index.js +1 -0
- package/dist/core/embeddings/types.d.ts +1 -1
- package/dist/core/graph/types.d.ts +4 -3
- package/dist/core/ingestion/ast-helpers.d.ts +80 -0
- package/dist/core/ingestion/ast-helpers.js +738 -0
- package/dist/core/ingestion/call-analysis.d.ts +73 -0
- package/dist/core/ingestion/call-analysis.js +490 -0
- package/dist/core/ingestion/call-processor.d.ts +55 -2
- package/dist/core/ingestion/call-processor.js +673 -108
- package/dist/core/ingestion/call-routing.d.ts +23 -2
- package/dist/core/ingestion/call-routing.js +21 -0
- package/dist/core/ingestion/entry-point-scoring.js +36 -26
- package/dist/core/ingestion/framework-detection.d.ts +10 -2
- package/dist/core/ingestion/framework-detection.js +49 -12
- package/dist/core/ingestion/heritage-processor.js +47 -49
- package/dist/core/ingestion/import-processor.d.ts +1 -1
- package/dist/core/ingestion/import-processor.js +103 -194
- package/dist/core/ingestion/import-resolution.d.ts +101 -0
- package/dist/core/ingestion/import-resolution.js +251 -0
- package/dist/core/ingestion/language-config.d.ts +3 -0
- package/dist/core/ingestion/language-config.js +13 -0
- package/dist/core/ingestion/markdown-processor.d.ts +17 -0
- package/dist/core/ingestion/markdown-processor.js +124 -0
- package/dist/core/ingestion/mro-processor.js +8 -3
- package/dist/core/ingestion/named-binding-extraction.d.ts +9 -43
- package/dist/core/ingestion/named-binding-extraction.js +89 -79
- package/dist/core/ingestion/parsing-processor.d.ts +3 -2
- package/dist/core/ingestion/parsing-processor.js +27 -60
- package/dist/core/ingestion/pipeline.d.ts +10 -0
- package/dist/core/ingestion/pipeline.js +425 -4
- package/dist/core/ingestion/resolution-context.d.ts +5 -0
- package/dist/core/ingestion/resolution-context.js +7 -4
- package/dist/core/ingestion/resolvers/index.d.ts +1 -1
- package/dist/core/ingestion/resolvers/index.js +1 -1
- package/dist/core/ingestion/resolvers/jvm.d.ts +2 -1
- package/dist/core/ingestion/resolvers/jvm.js +25 -9
- package/dist/core/ingestion/resolvers/php.d.ts +14 -0
- package/dist/core/ingestion/resolvers/php.js +43 -3
- package/dist/core/ingestion/resolvers/utils.d.ts +5 -0
- package/dist/core/ingestion/resolvers/utils.js +16 -0
- package/dist/core/ingestion/symbol-table.d.ts +29 -3
- package/dist/core/ingestion/symbol-table.js +42 -9
- package/dist/core/ingestion/tree-sitter-queries.d.ts +12 -12
- package/dist/core/ingestion/tree-sitter-queries.js +243 -2
- package/dist/core/ingestion/type-env.d.ts +28 -1
- package/dist/core/ingestion/type-env.js +451 -72
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +5 -0
- package/dist/core/ingestion/type-extractors/c-cpp.js +146 -2
- package/dist/core/ingestion/type-extractors/csharp.js +189 -16
- package/dist/core/ingestion/type-extractors/go.js +45 -0
- package/dist/core/ingestion/type-extractors/index.d.ts +1 -1
- package/dist/core/ingestion/type-extractors/index.js +1 -1
- package/dist/core/ingestion/type-extractors/jvm.js +244 -69
- package/dist/core/ingestion/type-extractors/php.js +31 -4
- package/dist/core/ingestion/type-extractors/python.js +89 -17
- package/dist/core/ingestion/type-extractors/ruby.js +17 -2
- package/dist/core/ingestion/type-extractors/rust.js +72 -4
- package/dist/core/ingestion/type-extractors/shared.d.ts +12 -2
- package/dist/core/ingestion/type-extractors/shared.js +115 -13
- package/dist/core/ingestion/type-extractors/swift.js +7 -6
- package/dist/core/ingestion/type-extractors/types.d.ts +54 -11
- package/dist/core/ingestion/type-extractors/typescript.js +171 -9
- package/dist/core/ingestion/utils.d.ts +2 -95
- package/dist/core/ingestion/utils.js +3 -892
- package/dist/core/ingestion/workers/parse-worker.d.ts +36 -11
- package/dist/core/ingestion/workers/parse-worker.js +116 -95
- package/dist/core/lbug/csv-generator.js +18 -1
- package/dist/core/lbug/lbug-adapter.d.ts +12 -0
- package/dist/core/lbug/lbug-adapter.js +71 -4
- package/dist/core/lbug/schema.d.ts +6 -4
- package/dist/core/lbug/schema.js +27 -3
- package/dist/mcp/core/embedder.js +11 -3
- package/dist/mcp/core/lbug-adapter.d.ts +22 -0
- package/dist/mcp/core/lbug-adapter.js +178 -23
- package/dist/mcp/local/local-backend.d.ts +22 -0
- package/dist/mcp/local/local-backend.js +136 -32
- package/dist/mcp/resources.js +13 -0
- package/dist/mcp/server.js +26 -4
- package/dist/mcp/tools.js +17 -7
- package/dist/server/api.d.ts +19 -1
- package/dist/server/api.js +66 -6
- package/dist/storage/git.d.ts +12 -0
- package/dist/storage/git.js +21 -0
- 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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
45
|
-
export declare const
|
|
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
|
package/dist/core/lbug/schema.js
CHANGED
|
@@ -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[
|
|
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 = () =>
|
|
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
|
|
69
|
-
*
|
|
70
|
-
*
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
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
|
-
|
|
172
|
-
|
|
234
|
+
try {
|
|
235
|
+
for (let i = 0; i < MAX_CONNS_PER_REPO; i++) {
|
|
236
|
+
available.push(createConnection(db));
|
|
237
|
+
}
|
|
173
238
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
202
|
-
|
|
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
|
+
}
|