gitnexus 1.5.2 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/README.md +10 -0
  2. package/dist/_shared/graph/types.d.ts +1 -1
  3. package/dist/_shared/graph/types.d.ts.map +1 -1
  4. package/dist/_shared/index.d.ts +1 -0
  5. package/dist/_shared/index.d.ts.map +1 -1
  6. package/dist/_shared/language-detection.d.ts.map +1 -1
  7. package/dist/_shared/language-detection.js +2 -0
  8. package/dist/_shared/language-detection.js.map +1 -1
  9. package/dist/_shared/languages.d.ts +1 -0
  10. package/dist/_shared/languages.d.ts.map +1 -1
  11. package/dist/_shared/languages.js +1 -0
  12. package/dist/_shared/languages.js.map +1 -1
  13. package/dist/_shared/lbug/schema-constants.d.ts +1 -1
  14. package/dist/_shared/lbug/schema-constants.d.ts.map +1 -1
  15. package/dist/_shared/lbug/schema-constants.js +3 -1
  16. package/dist/_shared/lbug/schema-constants.js.map +1 -1
  17. package/dist/_shared/mro-strategy.d.ts +19 -0
  18. package/dist/_shared/mro-strategy.d.ts.map +1 -0
  19. package/dist/_shared/mro-strategy.js +2 -0
  20. package/dist/_shared/mro-strategy.js.map +1 -0
  21. package/dist/cli/ai-context.d.ts +1 -0
  22. package/dist/cli/ai-context.js +28 -4
  23. package/dist/cli/analyze.d.ts +2 -0
  24. package/dist/cli/analyze.js +2 -1
  25. package/dist/cli/group.d.ts +2 -0
  26. package/dist/cli/group.js +233 -0
  27. package/dist/cli/index.js +3 -0
  28. package/dist/cli/serve.js +4 -1
  29. package/dist/cli/setup.js +34 -3
  30. package/dist/cli/wiki.js +15 -44
  31. package/dist/config/ignore-service.js +8 -3
  32. package/dist/core/augmentation/engine.js +1 -1
  33. package/dist/core/git-staleness.d.ts +13 -0
  34. package/dist/core/git-staleness.js +29 -0
  35. package/dist/core/group/bridge-db.d.ts +82 -0
  36. package/dist/core/group/bridge-db.js +460 -0
  37. package/dist/core/group/bridge-schema.d.ts +27 -0
  38. package/dist/core/group/bridge-schema.js +55 -0
  39. package/dist/core/group/config-parser.d.ts +3 -0
  40. package/dist/core/group/config-parser.js +83 -0
  41. package/dist/core/group/contract-extractor.d.ts +7 -0
  42. package/dist/core/group/contract-extractor.js +1 -0
  43. package/dist/core/group/extractors/grpc-extractor.d.ts +16 -0
  44. package/dist/core/group/extractors/grpc-extractor.js +264 -0
  45. package/dist/core/group/extractors/http-route-extractor.d.ts +24 -0
  46. package/dist/core/group/extractors/http-route-extractor.js +428 -0
  47. package/dist/core/group/extractors/topic-extractor.d.ts +9 -0
  48. package/dist/core/group/extractors/topic-extractor.js +234 -0
  49. package/dist/core/group/matching.d.ts +13 -0
  50. package/dist/core/group/matching.js +198 -0
  51. package/dist/core/group/normalization.d.ts +3 -0
  52. package/dist/core/group/normalization.js +115 -0
  53. package/dist/core/group/service-boundary-detector.d.ts +8 -0
  54. package/dist/core/group/service-boundary-detector.js +155 -0
  55. package/dist/core/group/service.d.ts +46 -0
  56. package/dist/core/group/service.js +160 -0
  57. package/dist/core/group/storage.d.ts +9 -0
  58. package/dist/core/group/storage.js +91 -0
  59. package/dist/core/group/sync.d.ts +21 -0
  60. package/dist/core/group/sync.js +148 -0
  61. package/dist/core/group/types.d.ts +130 -0
  62. package/dist/core/group/types.js +1 -0
  63. package/dist/core/ingestion/binding-accumulator.d.ts +207 -0
  64. package/dist/core/ingestion/binding-accumulator.js +332 -0
  65. package/dist/core/ingestion/call-processor.d.ts +155 -24
  66. package/dist/core/ingestion/call-processor.js +1129 -247
  67. package/dist/core/ingestion/class-extractors/generic.d.ts +2 -0
  68. package/dist/core/ingestion/class-extractors/generic.js +135 -0
  69. package/dist/core/ingestion/class-types.d.ts +34 -0
  70. package/dist/core/ingestion/class-types.js +1 -0
  71. package/dist/core/ingestion/entry-point-scoring.d.ts +1 -0
  72. package/dist/core/ingestion/entry-point-scoring.js +1 -0
  73. package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +5 -1
  74. package/dist/core/ingestion/field-extractors/configs/helpers.js +13 -3
  75. package/dist/core/ingestion/field-types.d.ts +2 -2
  76. package/dist/core/ingestion/filesystem-walker.js +8 -0
  77. package/dist/core/ingestion/framework-detection.d.ts +1 -0
  78. package/dist/core/ingestion/framework-detection.js +1 -0
  79. package/dist/core/ingestion/heritage-processor.d.ts +8 -15
  80. package/dist/core/ingestion/heritage-processor.js +15 -28
  81. package/dist/core/ingestion/import-processor.d.ts +1 -11
  82. package/dist/core/ingestion/import-processor.js +0 -12
  83. package/dist/core/ingestion/import-resolvers/utils.js +1 -0
  84. package/dist/core/ingestion/import-resolvers/vue.d.ts +8 -0
  85. package/dist/core/ingestion/import-resolvers/vue.js +9 -0
  86. package/dist/core/ingestion/language-provider.d.ts +6 -3
  87. package/dist/core/ingestion/languages/c-cpp.js +168 -1
  88. package/dist/core/ingestion/languages/csharp.js +20 -0
  89. package/dist/core/ingestion/languages/dart.js +26 -4
  90. package/dist/core/ingestion/languages/go.js +22 -0
  91. package/dist/core/ingestion/languages/index.d.ts +1 -0
  92. package/dist/core/ingestion/languages/index.js +2 -0
  93. package/dist/core/ingestion/languages/java.js +17 -0
  94. package/dist/core/ingestion/languages/kotlin.js +24 -1
  95. package/dist/core/ingestion/languages/php.js +23 -11
  96. package/dist/core/ingestion/languages/python.js +9 -0
  97. package/dist/core/ingestion/languages/ruby.js +28 -0
  98. package/dist/core/ingestion/languages/rust.js +38 -0
  99. package/dist/core/ingestion/languages/swift.js +31 -0
  100. package/dist/core/ingestion/languages/typescript.d.ts +1 -0
  101. package/dist/core/ingestion/languages/typescript.js +54 -1
  102. package/dist/core/ingestion/languages/vue.d.ts +13 -0
  103. package/dist/core/ingestion/languages/vue.js +81 -0
  104. package/dist/core/ingestion/method-extractors/configs/c-cpp.d.ts +3 -0
  105. package/dist/core/ingestion/method-extractors/configs/c-cpp.js +387 -0
  106. package/dist/core/ingestion/method-extractors/configs/csharp.js +5 -1
  107. package/dist/core/ingestion/method-extractors/configs/dart.d.ts +2 -0
  108. package/dist/core/ingestion/method-extractors/configs/dart.js +376 -0
  109. package/dist/core/ingestion/method-extractors/configs/go.d.ts +2 -0
  110. package/dist/core/ingestion/method-extractors/configs/go.js +176 -0
  111. package/dist/core/ingestion/method-extractors/configs/jvm.js +13 -4
  112. package/dist/core/ingestion/method-extractors/configs/php.d.ts +2 -0
  113. package/dist/core/ingestion/method-extractors/configs/php.js +304 -0
  114. package/dist/core/ingestion/method-extractors/configs/python.d.ts +2 -0
  115. package/dist/core/ingestion/method-extractors/configs/python.js +309 -0
  116. package/dist/core/ingestion/method-extractors/configs/ruby.d.ts +2 -0
  117. package/dist/core/ingestion/method-extractors/configs/ruby.js +285 -0
  118. package/dist/core/ingestion/method-extractors/configs/rust.d.ts +2 -0
  119. package/dist/core/ingestion/method-extractors/configs/rust.js +195 -0
  120. package/dist/core/ingestion/method-extractors/configs/swift.d.ts +2 -0
  121. package/dist/core/ingestion/method-extractors/configs/swift.js +277 -0
  122. package/dist/core/ingestion/method-extractors/configs/typescript-javascript.d.ts +3 -0
  123. package/dist/core/ingestion/method-extractors/configs/typescript-javascript.js +338 -0
  124. package/dist/core/ingestion/method-extractors/generic.js +38 -15
  125. package/dist/core/ingestion/method-types.d.ts +25 -0
  126. package/dist/core/ingestion/model/field-registry.d.ts +18 -0
  127. package/dist/core/ingestion/model/field-registry.js +22 -0
  128. package/dist/core/ingestion/model/heritage-map.d.ts +70 -0
  129. package/dist/core/ingestion/model/heritage-map.js +159 -0
  130. package/dist/core/ingestion/model/index.d.ts +20 -0
  131. package/dist/core/ingestion/model/index.js +41 -0
  132. package/dist/core/ingestion/model/method-registry.d.ts +62 -0
  133. package/dist/core/ingestion/model/method-registry.js +130 -0
  134. package/dist/core/ingestion/model/registration-table.d.ts +139 -0
  135. package/dist/core/ingestion/model/registration-table.js +224 -0
  136. package/dist/core/ingestion/model/resolution-context.d.ts +93 -0
  137. package/dist/core/ingestion/model/resolution-context.js +337 -0
  138. package/dist/core/ingestion/model/resolve.d.ts +56 -0
  139. package/dist/core/ingestion/model/resolve.js +242 -0
  140. package/dist/core/ingestion/model/semantic-model.d.ts +86 -0
  141. package/dist/core/ingestion/model/semantic-model.js +120 -0
  142. package/dist/core/ingestion/model/symbol-table.d.ts +222 -0
  143. package/dist/core/ingestion/model/symbol-table.js +206 -0
  144. package/dist/core/ingestion/model/type-registry.d.ts +39 -0
  145. package/dist/core/ingestion/model/type-registry.js +62 -0
  146. package/dist/core/ingestion/mro-processor.d.ts +4 -3
  147. package/dist/core/ingestion/mro-processor.js +310 -106
  148. package/dist/core/ingestion/parsing-processor.d.ts +5 -4
  149. package/dist/core/ingestion/parsing-processor.js +210 -85
  150. package/dist/core/ingestion/pipeline.d.ts +2 -0
  151. package/dist/core/ingestion/pipeline.js +192 -68
  152. package/dist/core/ingestion/tree-sitter-queries.d.ts +6 -6
  153. package/dist/core/ingestion/tree-sitter-queries.js +37 -0
  154. package/dist/core/ingestion/type-env.d.ts +15 -2
  155. package/dist/core/ingestion/type-env.js +163 -102
  156. package/dist/core/ingestion/type-extractors/csharp.js +17 -0
  157. package/dist/core/ingestion/type-extractors/jvm.js +11 -0
  158. package/dist/core/ingestion/type-extractors/php.js +0 -55
  159. package/dist/core/ingestion/type-extractors/ruby.js +0 -32
  160. package/dist/core/ingestion/type-extractors/swift.js +13 -0
  161. package/dist/core/ingestion/type-extractors/types.d.ts +8 -8
  162. package/dist/core/ingestion/type-extractors/typescript.js +66 -69
  163. package/dist/core/ingestion/utils/ast-helpers.d.ts +33 -43
  164. package/dist/core/ingestion/utils/ast-helpers.js +129 -565
  165. package/dist/core/ingestion/utils/method-props.d.ts +32 -0
  166. package/dist/core/ingestion/utils/method-props.js +147 -0
  167. package/dist/core/ingestion/vue-sfc-extractor.d.ts +44 -0
  168. package/dist/core/ingestion/vue-sfc-extractor.js +94 -0
  169. package/dist/core/ingestion/workers/parse-worker.d.ts +31 -19
  170. package/dist/core/ingestion/workers/parse-worker.js +463 -198
  171. package/dist/core/lbug/lbug-adapter.d.ts +6 -0
  172. package/dist/core/lbug/lbug-adapter.js +68 -3
  173. package/dist/core/lbug/pool-adapter.d.ts +76 -0
  174. package/dist/core/lbug/pool-adapter.js +522 -0
  175. package/dist/core/run-analyze.d.ts +2 -0
  176. package/dist/core/run-analyze.js +1 -1
  177. package/dist/core/search/bm25-index.js +1 -1
  178. package/dist/core/tree-sitter/parser-loader.js +1 -0
  179. package/dist/core/wiki/graph-queries.js +1 -1
  180. package/dist/core/wiki/html-viewer.js +6 -4
  181. package/dist/core/wiki/llm-client.js +4 -6
  182. package/dist/mcp/core/embedder.js +6 -5
  183. package/dist/mcp/core/lbug-adapter.d.ts +3 -63
  184. package/dist/mcp/core/lbug-adapter.js +3 -484
  185. package/dist/mcp/local/local-backend.d.ts +31 -2
  186. package/dist/mcp/local/local-backend.js +255 -46
  187. package/dist/mcp/resources.js +5 -4
  188. package/dist/mcp/staleness.d.ts +3 -13
  189. package/dist/mcp/staleness.js +2 -31
  190. package/dist/mcp/tools.js +80 -4
  191. package/dist/server/analyze-job.d.ts +2 -0
  192. package/dist/server/analyze-job.js +4 -0
  193. package/dist/server/api.d.ts +20 -1
  194. package/dist/server/api.js +306 -71
  195. package/dist/server/git-clone.d.ts +2 -1
  196. package/dist/server/git-clone.js +98 -5
  197. package/dist/storage/git.d.ts +13 -0
  198. package/dist/storage/git.js +25 -0
  199. package/dist/storage/repo-manager.js +1 -1
  200. package/package.json +8 -2
  201. package/scripts/patch-tree-sitter-swift.cjs +78 -0
  202. package/dist/core/ingestion/named-binding-processor.d.ts +0 -18
  203. package/dist/core/ingestion/named-binding-processor.js +0 -42
  204. package/dist/core/ingestion/resolution-context.d.ts +0 -58
  205. package/dist/core/ingestion/resolution-context.js +0 -135
  206. package/dist/core/ingestion/symbol-table.d.ts +0 -79
  207. package/dist/core/ingestion/symbol-table.js +0 -115
@@ -0,0 +1,522 @@
1
+ /**
2
+ * LadybugDB connection pool (core). Used by MCP, sync, search, wiki, etc.
3
+ *
4
+ * LadybugDB Adapter (Connection Pool)
5
+ *
6
+ * Manages a pool of LadybugDB databases keyed by repoId, each with
7
+ * multiple Connection objects for safe concurrent query execution.
8
+ *
9
+ * LadybugDB Connections are NOT thread-safe — a single Connection
10
+ * segfaults if concurrent .query() calls hit it simultaneously.
11
+ * This adapter provides a checkout/return connection pool so each
12
+ * concurrent query gets its own Connection from the same Database.
13
+ *
14
+ * @see https://docs.ladybugdb.com/concurrency — multiple Connections
15
+ * from the same Database is the officially supported concurrency pattern.
16
+ */
17
+ import fs from 'fs/promises';
18
+ import lbug from '@ladybugdb/core';
19
+ const pool = new Map();
20
+ const dbCache = new Map();
21
+ /** Max repos in the pool (LRU eviction) */
22
+ const MAX_POOL_SIZE = 5;
23
+ /** Idle timeout before closing a repo's connections */
24
+ const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
25
+ /** Max connections per repo (caps concurrent queries per repo) */
26
+ const MAX_CONNS_PER_REPO = 8;
27
+ let idleTimer = null;
28
+ /** Saved real stdout/stderr write — used to silence native module output without race conditions */
29
+ export const realStdoutWrite = process.stdout.write.bind(process.stdout);
30
+ export const realStderrWrite = process.stderr.write.bind(process.stderr);
31
+ let stdoutSilenceCount = 0;
32
+ /** True while pre-warming connections — prevents watchdog from prematurely restoring stdout */
33
+ let preWarmActive = false;
34
+ /**
35
+ * Start the idle cleanup timer (runs every 60s)
36
+ */
37
+ function ensureIdleTimer() {
38
+ if (idleTimer)
39
+ return;
40
+ idleTimer = setInterval(() => {
41
+ const now = Date.now();
42
+ for (const [repoId, entry] of pool) {
43
+ if (now - entry.lastUsed > IDLE_TIMEOUT_MS && entry.checkedOut === 0) {
44
+ closeOne(repoId);
45
+ }
46
+ }
47
+ }, 60_000);
48
+ if (idleTimer && typeof idleTimer === 'object' && 'unref' in idleTimer) {
49
+ idleTimer.unref();
50
+ }
51
+ }
52
+ /**
53
+ * Touch a repo to reset its idle timeout.
54
+ * Call this during long-running operations to prevent the connection from being closed.
55
+ */
56
+ export const touchRepo = (repoId) => {
57
+ const entry = pool.get(repoId);
58
+ if (entry) {
59
+ entry.lastUsed = Date.now();
60
+ }
61
+ };
62
+ /**
63
+ * Evict the least-recently-used repo if pool is at capacity
64
+ */
65
+ function evictLRU() {
66
+ if (pool.size < MAX_POOL_SIZE)
67
+ return;
68
+ let oldestId = null;
69
+ let oldestTime = Infinity;
70
+ for (const [id, entry] of pool) {
71
+ if (entry.checkedOut === 0 && entry.lastUsed < oldestTime) {
72
+ oldestTime = entry.lastUsed;
73
+ oldestId = id;
74
+ }
75
+ }
76
+ if (oldestId) {
77
+ closeOne(oldestId);
78
+ }
79
+ }
80
+ /**
81
+ * Remove a repo from the pool, close its connections, and release its
82
+ * shared Database ref. Only closes the Database when no other repoIds
83
+ * reference it (refCount === 0).
84
+ */
85
+ function closeOne(repoId) {
86
+ const entry = pool.get(repoId);
87
+ if (!entry)
88
+ return;
89
+ entry.closed = true;
90
+ // Close available connections — fire-and-forget with .catch() to prevent
91
+ // unhandled rejections. Native close() returns Promise<void> but can crash
92
+ // the N-API destructor on macOS/Windows; deferring to process exit lets
93
+ // dangerouslyIgnoreUnhandledErrors absorb the crash.
94
+ for (const conn of entry.available) {
95
+ conn.close().catch(() => { });
96
+ }
97
+ entry.available.length = 0;
98
+ // Checked-out connections can't be closed here — they're in-flight.
99
+ // The checkin() function detects entry.closed and closes them on return.
100
+ // Only close the Database when no other repoIds reference it.
101
+ // External databases (injected via initLbugWithDb) are never closed here —
102
+ // the core adapter owns them and handles their lifecycle.
103
+ const shared = dbCache.get(entry.dbPath);
104
+ if (shared) {
105
+ shared.refCount--;
106
+ if (shared.refCount === 0) {
107
+ if (shared.external) {
108
+ // External databases are owned by the core adapter — don't close
109
+ // or remove from cache. Keep the entry so future initLbug() calls
110
+ // for the same dbPath reuse it instead of hitting a file lock.
111
+ shared.refCount = 0;
112
+ shared.ftsLoaded = false;
113
+ shared.vectorLoaded = false;
114
+ }
115
+ else {
116
+ shared.db.close().catch(() => { });
117
+ dbCache.delete(entry.dbPath);
118
+ }
119
+ }
120
+ }
121
+ pool.delete(repoId);
122
+ }
123
+ /**
124
+ * Create a new Connection from a repo's Database.
125
+ * Silences stdout to prevent native module output from corrupting MCP stdio.
126
+ */
127
+ let activeQueryCount = 0;
128
+ /**
129
+ * Silence stdout by replacing process.stdout.write with a no-op.
130
+ * Uses a reference counter so nested silence/restore pairs are safe.
131
+ * Exported so other modules (e.g. embedder) use the same mechanism instead
132
+ * of independently patching stdout, which causes restore-order conflicts.
133
+ */
134
+ export function silenceStdout() {
135
+ if (stdoutSilenceCount++ === 0) {
136
+ process.stdout.write = (() => true);
137
+ }
138
+ }
139
+ export function restoreStdout() {
140
+ if (--stdoutSilenceCount <= 0) {
141
+ stdoutSilenceCount = 0;
142
+ process.stdout.write = realStdoutWrite;
143
+ }
144
+ }
145
+ // Safety watchdog: restore stdout if it gets stuck silenced (e.g. native crash
146
+ // inside createConnection before restoreStdout runs).
147
+ // Exempts active queries and pre-warm — these legitimately hold silence for
148
+ // longer than 1 second (queries can take up to QUERY_TIMEOUT_MS = 30s).
149
+ setInterval(() => {
150
+ if (stdoutSilenceCount > 0 && !preWarmActive && activeQueryCount === 0) {
151
+ stdoutSilenceCount = 0;
152
+ process.stdout.write = realStdoutWrite;
153
+ }
154
+ }, 1000).unref();
155
+ function createConnection(db) {
156
+ silenceStdout();
157
+ try {
158
+ return new lbug.Connection(db);
159
+ }
160
+ finally {
161
+ restoreStdout();
162
+ }
163
+ }
164
+ /** Query timeout in milliseconds */
165
+ const QUERY_TIMEOUT_MS = 30_000;
166
+ /** Waiter queue timeout in milliseconds */
167
+ const WAITER_TIMEOUT_MS = 15_000;
168
+ const LOCK_RETRY_ATTEMPTS = 3;
169
+ const LOCK_RETRY_DELAY_MS = 2000;
170
+ /** Deduplicates concurrent initLbug calls for the same repoId */
171
+ const initPromises = new Map();
172
+ /**
173
+ * Initialize (or reuse) a Database + connection pool for a specific repo.
174
+ * Retries on lock errors (e.g., when `gitnexus analyze` is running).
175
+ *
176
+ * Concurrent calls for the same repoId are deduplicated — the second caller
177
+ * awaits the first's in-progress init rather than starting a redundant one.
178
+ */
179
+ export const initLbug = async (repoId, dbPath) => {
180
+ const existing = pool.get(repoId);
181
+ if (existing) {
182
+ existing.lastUsed = Date.now();
183
+ return;
184
+ }
185
+ // Deduplicate concurrent init calls for the same repoId —
186
+ // prevents double-init race when multiple parallel tool calls
187
+ // trigger initialization for the same repo simultaneously.
188
+ const pending = initPromises.get(repoId);
189
+ if (pending)
190
+ return pending;
191
+ const promise = doInitLbug(repoId, dbPath);
192
+ initPromises.set(repoId, promise);
193
+ try {
194
+ await promise;
195
+ }
196
+ finally {
197
+ initPromises.delete(repoId);
198
+ }
199
+ };
200
+ /**
201
+ * Internal init — creates DB, pre-warms connections, loads FTS, then registers pool.
202
+ * Pool entry is registered LAST so concurrent executeQuery calls see either
203
+ * "not initialized" (and throw) or a fully ready pool — never a half-built one.
204
+ */
205
+ async function doInitLbug(repoId, dbPath) {
206
+ // Check if database exists
207
+ try {
208
+ await fs.stat(dbPath);
209
+ }
210
+ catch {
211
+ throw new Error(`LadybugDB not found at ${dbPath}. Run: gitnexus analyze`);
212
+ }
213
+ evictLRU();
214
+ // Reuse an existing native Database if another repoId already opened this path.
215
+ // This prevents buffer manager exhaustion from multiple mmap regions on the same file.
216
+ let shared = dbCache.get(dbPath);
217
+ if (!shared) {
218
+ // Open in read-only mode — MCP server never writes to the database.
219
+ // This allows multiple MCP server instances to read concurrently, and
220
+ // avoids lock conflicts when `gitnexus analyze` is writing.
221
+ let lastError = null;
222
+ for (let attempt = 1; attempt <= LOCK_RETRY_ATTEMPTS; attempt++) {
223
+ silenceStdout();
224
+ try {
225
+ const db = new lbug.Database(dbPath, 0, // bufferManagerSize (default)
226
+ false, // enableCompression (default)
227
+ true);
228
+ restoreStdout();
229
+ shared = { db, refCount: 0, ftsLoaded: false, vectorLoaded: false };
230
+ dbCache.set(dbPath, shared);
231
+ break;
232
+ }
233
+ catch (err) {
234
+ restoreStdout();
235
+ lastError = err instanceof Error ? err : new Error(String(err));
236
+ const isLockError = lastError.message.includes('Could not set lock') || lastError.message.includes('lock');
237
+ if (!isLockError || attempt === LOCK_RETRY_ATTEMPTS)
238
+ break;
239
+ await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_DELAY_MS * attempt));
240
+ }
241
+ }
242
+ if (!shared) {
243
+ throw new Error(`LadybugDB unavailable for ${repoId}. Another process may be rebuilding the index. ` +
244
+ `Retry later. (${lastError?.message || 'unknown error'})`);
245
+ }
246
+ }
247
+ shared.refCount++;
248
+ const db = shared.db;
249
+ // Pre-create the full pool upfront so createConnection() (which silences
250
+ // stdout) is never called lazily during active query execution.
251
+ // Mark preWarmActive so the watchdog timer doesn't interfere.
252
+ preWarmActive = true;
253
+ const available = [];
254
+ try {
255
+ for (let i = 0; i < MAX_CONNS_PER_REPO; i++) {
256
+ available.push(createConnection(db));
257
+ }
258
+ }
259
+ finally {
260
+ preWarmActive = false;
261
+ }
262
+ // Load FTS extension once per shared Database.
263
+ // Done BEFORE pool registration so no concurrent checkout can grab
264
+ // the connection while the async FTS load is in progress.
265
+ if (!shared.ftsLoaded) {
266
+ try {
267
+ await available[0].query('LOAD EXTENSION fts');
268
+ shared.ftsLoaded = true;
269
+ }
270
+ catch {
271
+ // Extension may not be installed — FTS queries will fail gracefully
272
+ }
273
+ }
274
+ // Load VECTOR extension once per shared Database for semantic search support.
275
+ if (!shared.vectorLoaded) {
276
+ try {
277
+ await available[0].query('INSTALL VECTOR');
278
+ await available[0].query('LOAD EXTENSION VECTOR');
279
+ shared.vectorLoaded = true;
280
+ }
281
+ catch {
282
+ // VECTOR extension may not be available
283
+ }
284
+ }
285
+ // Register pool entry only after all connections are pre-warmed and FTS is
286
+ // loaded. Concurrent executeQuery calls see either "not initialized"
287
+ // (and throw cleanly) or a fully ready pool — never a half-built one.
288
+ pool.set(repoId, {
289
+ db,
290
+ available,
291
+ checkedOut: 0,
292
+ waiters: [],
293
+ lastUsed: Date.now(),
294
+ dbPath,
295
+ closed: false,
296
+ });
297
+ ensureIdleTimer();
298
+ }
299
+ /**
300
+ * Initialize a pool entry from a pre-existing Database object.
301
+ *
302
+ * Used in tests to avoid the writable→close→read-only cycle that crashes
303
+ * on macOS due to N-API destructor segfaults. The pool adapter reuses
304
+ * the core adapter's writable Database instead of opening a new read-only one.
305
+ *
306
+ * The Database is registered in the shared dbCache so closeOne() decrements
307
+ * the refCount correctly. If the Database is already cached (e.g. another
308
+ * repoId already injected it), the existing entry is reused.
309
+ */
310
+ export async function initLbugWithDb(repoId, existingDb, dbPath) {
311
+ const existing = pool.get(repoId);
312
+ if (existing) {
313
+ existing.lastUsed = Date.now();
314
+ return;
315
+ }
316
+ // Register in dbCache with external: true so other initLbug() calls
317
+ // for the same dbPath reuse this Database instead of trying to open
318
+ // a new one (which would fail with a file lock error).
319
+ // closeOne() respects the external flag and skips db.close().
320
+ let shared = dbCache.get(dbPath);
321
+ if (!shared) {
322
+ shared = { db: existingDb, refCount: 0, ftsLoaded: false, vectorLoaded: false, external: true };
323
+ dbCache.set(dbPath, shared);
324
+ }
325
+ shared.refCount++;
326
+ const available = [];
327
+ preWarmActive = true;
328
+ try {
329
+ for (let i = 0; i < MAX_CONNS_PER_REPO; i++) {
330
+ available.push(createConnection(existingDb));
331
+ }
332
+ }
333
+ finally {
334
+ preWarmActive = false;
335
+ }
336
+ // Load FTS extension if not already loaded on this Database
337
+ if (!shared.ftsLoaded) {
338
+ try {
339
+ await available[0].query('LOAD EXTENSION fts');
340
+ shared.ftsLoaded = true;
341
+ }
342
+ catch {
343
+ // Extension may already be loaded or not installed
344
+ }
345
+ }
346
+ // Load VECTOR extension for semantic search support
347
+ if (!shared.vectorLoaded) {
348
+ try {
349
+ await available[0].query('INSTALL VECTOR');
350
+ await available[0].query('LOAD EXTENSION VECTOR');
351
+ shared.vectorLoaded = true;
352
+ }
353
+ catch {
354
+ // VECTOR extension may not be available
355
+ }
356
+ }
357
+ pool.set(repoId, {
358
+ db: existingDb,
359
+ available,
360
+ checkedOut: 0,
361
+ waiters: [],
362
+ lastUsed: Date.now(),
363
+ dbPath,
364
+ closed: false,
365
+ });
366
+ ensureIdleTimer();
367
+ }
368
+ /**
369
+ * Checkout a connection from the pool.
370
+ * Returns an available connection, or creates a new one if under the cap.
371
+ * If all connections are busy and at cap, queues the caller until one is returned.
372
+ */
373
+ function checkout(entry) {
374
+ // Fast path: grab an available connection
375
+ if (entry.available.length > 0) {
376
+ entry.checkedOut++;
377
+ return Promise.resolve(entry.available.pop());
378
+ }
379
+ // Pool was pre-warmed to MAX_CONNS_PER_REPO during init. If we're here
380
+ // with fewer total connections, something leaked — surface the bug rather
381
+ // than silently creating a connection (which would silence stdout mid-query).
382
+ const totalConns = entry.available.length + entry.checkedOut;
383
+ if (totalConns < MAX_CONNS_PER_REPO) {
384
+ throw new Error(`Connection pool integrity error: expected ${MAX_CONNS_PER_REPO} ` +
385
+ `connections but found ${totalConns} (${entry.available.length} available, ` +
386
+ `${entry.checkedOut} checked out)`);
387
+ }
388
+ // At capacity — queue the caller with a timeout.
389
+ return new Promise((resolve, reject) => {
390
+ const waiter = (conn) => {
391
+ clearTimeout(timer);
392
+ resolve(conn);
393
+ };
394
+ const timer = setTimeout(() => {
395
+ const idx = entry.waiters.indexOf(waiter);
396
+ if (idx !== -1)
397
+ entry.waiters.splice(idx, 1);
398
+ reject(new Error(`Connection pool exhausted: timed out after ${WAITER_TIMEOUT_MS}ms waiting for a free connection`));
399
+ }, WAITER_TIMEOUT_MS);
400
+ entry.waiters.push(waiter);
401
+ });
402
+ }
403
+ /**
404
+ * Return a connection to the pool after use.
405
+ * If the pool entry was closed while the connection was checked out (e.g.
406
+ * LRU eviction), close the orphaned connection instead of returning it.
407
+ * If there are queued waiters, hand the connection directly to the next one
408
+ * instead of putting it back in the available array (avoids race conditions).
409
+ */
410
+ function checkin(entry, conn) {
411
+ if (entry.closed) {
412
+ // Pool entry was deleted during checkout — close the orphaned connection
413
+ conn.close().catch(() => { });
414
+ return;
415
+ }
416
+ if (entry.waiters.length > 0) {
417
+ // Hand directly to the next waiter — no intermediate available state
418
+ const waiter = entry.waiters.shift();
419
+ waiter(conn);
420
+ }
421
+ else {
422
+ entry.checkedOut--;
423
+ entry.available.push(conn);
424
+ }
425
+ }
426
+ /**
427
+ * Execute a query on a specific repo's connection pool.
428
+ * Automatically checks out a connection, runs the query, and returns it.
429
+ */
430
+ /** Race a promise against a timeout */
431
+ function withTimeout(promise, ms, label) {
432
+ let timer;
433
+ const timeout = new Promise((_, reject) => {
434
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
435
+ });
436
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
437
+ }
438
+ export const executeQuery = async (repoId, cypher) => {
439
+ const entry = pool.get(repoId);
440
+ if (!entry) {
441
+ throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initLbug first.`);
442
+ }
443
+ if (isWriteQuery(cypher)) {
444
+ throw new Error('Write operations are not allowed. The pool adapter is read-only.');
445
+ }
446
+ entry.lastUsed = Date.now();
447
+ const conn = await checkout(entry);
448
+ silenceStdout();
449
+ activeQueryCount++;
450
+ try {
451
+ const queryResult = await withTimeout(conn.query(cypher), QUERY_TIMEOUT_MS, 'Query');
452
+ const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
453
+ const rows = await result.getAll();
454
+ return rows;
455
+ }
456
+ finally {
457
+ activeQueryCount--;
458
+ restoreStdout();
459
+ checkin(entry, conn);
460
+ }
461
+ };
462
+ /**
463
+ * Execute a parameterized query on a specific repo's connection pool.
464
+ * Uses prepare/execute pattern to prevent Cypher injection.
465
+ */
466
+ export const executeParameterized = async (repoId, cypher, params) => {
467
+ const entry = pool.get(repoId);
468
+ if (!entry) {
469
+ throw new Error(`LadybugDB not initialized for repo "${repoId}". Call initLbug first.`);
470
+ }
471
+ entry.lastUsed = Date.now();
472
+ const conn = await checkout(entry);
473
+ silenceStdout();
474
+ activeQueryCount++;
475
+ try {
476
+ const stmt = await withTimeout(conn.prepare(cypher), QUERY_TIMEOUT_MS, 'Prepare');
477
+ if (!stmt.isSuccess()) {
478
+ const errMsg = await stmt.getErrorMessage();
479
+ throw new Error(`Prepare failed: ${errMsg}`);
480
+ }
481
+ const queryResult = await withTimeout(conn.execute(stmt, params), QUERY_TIMEOUT_MS, 'Execute');
482
+ const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
483
+ const rows = await result.getAll();
484
+ return rows;
485
+ }
486
+ finally {
487
+ activeQueryCount--;
488
+ restoreStdout();
489
+ checkin(entry, conn);
490
+ }
491
+ };
492
+ /**
493
+ * Close one or all repo pools.
494
+ * If repoId is provided, close only that repo's connections.
495
+ * If omitted, close all repos.
496
+ */
497
+ export const closeLbug = async (repoId) => {
498
+ if (repoId) {
499
+ closeOne(repoId);
500
+ return;
501
+ }
502
+ for (const id of [...pool.keys()]) {
503
+ closeOne(id);
504
+ }
505
+ if (idleTimer) {
506
+ clearInterval(idleTimer);
507
+ idleTimer = null;
508
+ }
509
+ };
510
+ /**
511
+ * Check if a specific repo's pool is active
512
+ */
513
+ export const isLbugReady = (repoId) => pool.has(repoId);
514
+ /** Regex to detect write operations in user-supplied Cypher queries.
515
+ * Note: CALL is NOT blocked — it's used for read-only FTS (CALL QUERY_FTS_INDEX)
516
+ * and vector search (CALL QUERY_VECTOR_INDEX). The database is opened in
517
+ * read-only mode as defense-in-depth against write procedures. */
518
+ export const CYPHER_WRITE_RE = /(?<!:)\b(CREATE|DELETE|SET|MERGE|REMOVE|DROP|ALTER|COPY|DETACH|FOREACH|INSTALL|LOAD)\b/i;
519
+ /** Check if a Cypher query contains write operations */
520
+ export function isWriteQuery(query) {
521
+ return CYPHER_WRITE_RE.test(query);
522
+ }
@@ -18,6 +18,8 @@ export interface AnalyzeOptions {
18
18
  skipGit?: boolean;
19
19
  /** Skip AGENTS.md and CLAUDE.md gitnexus block updates. */
20
20
  skipAgentsMd?: boolean;
21
+ /** Omit volatile symbol/relationship counts from AGENTS.md and CLAUDE.md. */
22
+ noStats?: boolean;
21
23
  }
22
24
  export interface AnalyzeResult {
23
25
  repoName: string;
@@ -229,7 +229,7 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
229
229
  communities: pipelineResult.communityResult?.stats.totalCommunities,
230
230
  clusters: aggregatedClusterCount,
231
231
  processes: pipelineResult.processResult?.stats.totalProcesses,
232
- }, undefined, { skipAgentsMd: options.skipAgentsMd });
232
+ }, undefined, { skipAgentsMd: options.skipAgentsMd, noStats: options.noStats });
233
233
  }
234
234
  catch {
235
235
  // Best-effort — don't fail the entire analysis for context file issues
@@ -50,7 +50,7 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId) => {
50
50
  // Use MCP connection pool via dynamic import
51
51
  // IMPORTANT: FTS queries run sequentially to avoid connection contention.
52
52
  // The MCP pool supports multiple connections, but FTS is best run serially.
53
- const { executeQuery } = await import('../../mcp/core/lbug-adapter.js');
53
+ const { executeQuery } = await import('../lbug/pool-adapter.js');
54
54
  const executor = (cypher) => executeQuery(repoId, cypher);
55
55
  fileResults = await queryFTSViaExecutor(executor, 'File', 'file_fts', query, limit);
56
56
  functionResults = await queryFTSViaExecutor(executor, 'Function', 'function_fts', query, limit);
@@ -45,6 +45,7 @@ const languageMap = {
45
45
  ...(Kotlin ? { [SupportedLanguages.Kotlin]: Kotlin } : {}),
46
46
  [SupportedLanguages.PHP]: PHP.php_only,
47
47
  [SupportedLanguages.Ruby]: Ruby,
48
+ [SupportedLanguages.Vue]: TypeScript.typescript,
48
49
  ...(Dart ? { [SupportedLanguages.Dart]: Dart } : {}),
49
50
  ...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}),
50
51
  };
@@ -4,7 +4,7 @@
4
4
  * Encapsulated Cypher queries against the GitNexus knowledge graph.
5
5
  * Uses the MCP-style pooled lbug-adapter for connection management.
6
6
  */
7
- import { initLbug, executeQuery, closeLbug, touchRepo } from '../../mcp/core/lbug-adapter.js';
7
+ import { initLbug, executeQuery, closeLbug, touchRepo } from '../lbug/pool-adapter.js';
8
8
  const REPO_ID = '__wiki__';
9
9
  /**
10
10
  * Touch the wiki DB connection to prevent idle timeout during long LLM calls.
@@ -49,10 +49,12 @@ function esc(text) {
49
49
  .replace(/"/g, '&quot;');
50
50
  }
51
51
  function buildHTML(projectName, moduleTree, pages, meta) {
52
- // Embed data as JSON inside the HTML
53
- const pagesJSON = JSON.stringify(pages);
54
- const treeJSON = JSON.stringify(moduleTree);
55
- const metaJSON = JSON.stringify(meta);
52
+ // Embed data as JSON inside the HTML.
53
+ // Escape </script> sequences so they don't prematurely close the <script> tag.
54
+ const escScript = (s) => s.replace(/<\//g, '<\\/');
55
+ const pagesJSON = escScript(JSON.stringify(pages));
56
+ const treeJSON = escScript(JSON.stringify(moduleTree));
57
+ const metaJSON = escScript(JSON.stringify(meta));
56
58
  const parts = [];
57
59
  // ── Head ──
58
60
  parts.push('<!DOCTYPE html>');
@@ -103,12 +103,10 @@ export async function callLLM(prompt, config, systemPrompt, options) {
103
103
  model: config.model,
104
104
  messages,
105
105
  };
106
- if (reasoning) {
107
- body.max_completion_tokens = config.maxTokens;
108
- // Do NOT include temperature, top_p, presence_penalty, frequency_penalty
109
- }
110
- else {
111
- body.max_tokens = config.maxTokens;
106
+ // max_tokens is deprecated; use max_completion_tokens for all models
107
+ body.max_completion_tokens = config.maxTokens;
108
+ // Only send temperature for non-Azure providers — some Azure models reject non-default values
109
+ if (!reasoning && !azure && config.temperature !== undefined) {
112
110
  body.temperature = config.temperature;
113
111
  }
114
112
  if (useStream)
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { pipeline, env } from '@huggingface/transformers';
8
8
  import { isHttpMode, getHttpDimensions, httpEmbedQuery, } from '../../core/embeddings/http-client.js';
9
+ import { silenceStdout, restoreStdout, realStderrWrite } from '../../core/lbug/pool-adapter.js';
9
10
  // Model config
10
11
  const MODEL_ID = 'Snowflake/snowflake-arctic-embed-xs';
11
12
  // Module-level state for singleton pattern
@@ -39,9 +40,9 @@ export const initEmbedder = async () => {
39
40
  // Silence stdout and stderr during model load — ONNX Runtime and transformers.js
40
41
  // may write progress/init messages that corrupt MCP stdio protocol or produce
41
42
  // noisy warnings (e.g. node assignment to execution providers).
42
- const origStdout = process.stdout.write;
43
- const origStderr = process.stderr.write;
44
- process.stdout.write = (() => true);
43
+ // Use the centralized silenceStdout() to avoid conflicts with pool-adapter's
44
+ // own stdout patching (independent patching caused restore-order bugs).
45
+ silenceStdout();
45
46
  process.stderr.write = (() => true);
46
47
  try {
47
48
  embedderInstance = await pipeline('feature-extraction', MODEL_ID, {
@@ -50,8 +51,8 @@ export const initEmbedder = async () => {
50
51
  });
51
52
  }
52
53
  finally {
53
- process.stdout.write = origStdout;
54
- process.stderr.write = origStderr;
54
+ restoreStdout();
55
+ process.stderr.write = realStderrWrite;
55
56
  }
56
57
  console.error(`GitNexus: Embedding model loaded (${device})`);
57
58
  return embedderInstance;