gitnexus 1.5.3 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) 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 +30 -4
  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/config/ignore-service.js +8 -3
  31. package/dist/core/augmentation/engine.js +1 -1
  32. package/dist/core/git-staleness.d.ts +13 -0
  33. package/dist/core/git-staleness.js +29 -0
  34. package/dist/core/group/bridge-db.d.ts +82 -0
  35. package/dist/core/group/bridge-db.js +460 -0
  36. package/dist/core/group/bridge-schema.d.ts +27 -0
  37. package/dist/core/group/bridge-schema.js +55 -0
  38. package/dist/core/group/config-parser.d.ts +3 -0
  39. package/dist/core/group/config-parser.js +83 -0
  40. package/dist/core/group/contract-extractor.d.ts +7 -0
  41. package/dist/core/group/contract-extractor.js +1 -0
  42. package/dist/core/group/extractors/fs-utils.d.ts +10 -0
  43. package/dist/core/group/extractors/fs-utils.js +24 -0
  44. package/dist/core/group/extractors/grpc-extractor.d.ts +25 -0
  45. package/dist/core/group/extractors/grpc-extractor.js +386 -0
  46. package/dist/core/group/extractors/grpc-patterns/go.d.ts +2 -0
  47. package/dist/core/group/extractors/grpc-patterns/go.js +97 -0
  48. package/dist/core/group/extractors/grpc-patterns/index.d.ts +19 -0
  49. package/dist/core/group/extractors/grpc-patterns/index.js +46 -0
  50. package/dist/core/group/extractors/grpc-patterns/java.d.ts +2 -0
  51. package/dist/core/group/extractors/grpc-patterns/java.js +173 -0
  52. package/dist/core/group/extractors/grpc-patterns/node.d.ts +4 -0
  53. package/dist/core/group/extractors/grpc-patterns/node.js +290 -0
  54. package/dist/core/group/extractors/grpc-patterns/proto.d.ts +9 -0
  55. package/dist/core/group/extractors/grpc-patterns/proto.js +134 -0
  56. package/dist/core/group/extractors/grpc-patterns/python.d.ts +2 -0
  57. package/dist/core/group/extractors/grpc-patterns/python.js +67 -0
  58. package/dist/core/group/extractors/grpc-patterns/types.d.ts +50 -0
  59. package/dist/core/group/extractors/grpc-patterns/types.js +1 -0
  60. package/dist/core/group/extractors/http-patterns/go.d.ts +2 -0
  61. package/dist/core/group/extractors/http-patterns/go.js +215 -0
  62. package/dist/core/group/extractors/http-patterns/index.d.ts +17 -0
  63. package/dist/core/group/extractors/http-patterns/index.js +44 -0
  64. package/dist/core/group/extractors/http-patterns/java.d.ts +2 -0
  65. package/dist/core/group/extractors/http-patterns/java.js +253 -0
  66. package/dist/core/group/extractors/http-patterns/node.d.ts +4 -0
  67. package/dist/core/group/extractors/http-patterns/node.js +354 -0
  68. package/dist/core/group/extractors/http-patterns/php.d.ts +2 -0
  69. package/dist/core/group/extractors/http-patterns/php.js +70 -0
  70. package/dist/core/group/extractors/http-patterns/python.d.ts +2 -0
  71. package/dist/core/group/extractors/http-patterns/python.js +133 -0
  72. package/dist/core/group/extractors/http-patterns/types.d.ts +61 -0
  73. package/dist/core/group/extractors/http-patterns/types.js +1 -0
  74. package/dist/core/group/extractors/http-route-extractor.d.ts +21 -0
  75. package/dist/core/group/extractors/http-route-extractor.js +391 -0
  76. package/dist/core/group/extractors/manifest-extractor.d.ts +54 -0
  77. package/dist/core/group/extractors/manifest-extractor.js +235 -0
  78. package/dist/core/group/extractors/topic-extractor.d.ts +8 -0
  79. package/dist/core/group/extractors/topic-extractor.js +97 -0
  80. package/dist/core/group/extractors/topic-patterns/go.d.ts +2 -0
  81. package/dist/core/group/extractors/topic-patterns/go.js +120 -0
  82. package/dist/core/group/extractors/topic-patterns/index.d.ts +14 -0
  83. package/dist/core/group/extractors/topic-patterns/index.js +38 -0
  84. package/dist/core/group/extractors/topic-patterns/java.d.ts +2 -0
  85. package/dist/core/group/extractors/topic-patterns/java.js +80 -0
  86. package/dist/core/group/extractors/topic-patterns/node.d.ts +4 -0
  87. package/dist/core/group/extractors/topic-patterns/node.js +155 -0
  88. package/dist/core/group/extractors/topic-patterns/python.d.ts +2 -0
  89. package/dist/core/group/extractors/topic-patterns/python.js +116 -0
  90. package/dist/core/group/extractors/topic-patterns/types.d.ts +25 -0
  91. package/dist/core/group/extractors/topic-patterns/types.js +10 -0
  92. package/dist/core/group/extractors/tree-sitter-scanner.d.ts +113 -0
  93. package/dist/core/group/extractors/tree-sitter-scanner.js +94 -0
  94. package/dist/core/group/matching.d.ts +13 -0
  95. package/dist/core/group/matching.js +198 -0
  96. package/dist/core/group/normalization.d.ts +3 -0
  97. package/dist/core/group/normalization.js +115 -0
  98. package/dist/core/group/service-boundary-detector.d.ts +8 -0
  99. package/dist/core/group/service-boundary-detector.js +155 -0
  100. package/dist/core/group/service.d.ts +46 -0
  101. package/dist/core/group/service.js +160 -0
  102. package/dist/core/group/storage.d.ts +9 -0
  103. package/dist/core/group/storage.js +91 -0
  104. package/dist/core/group/sync.d.ts +21 -0
  105. package/dist/core/group/sync.js +148 -0
  106. package/dist/core/group/types.d.ts +130 -0
  107. package/dist/core/group/types.js +1 -0
  108. package/dist/core/ingestion/binding-accumulator.d.ts +212 -0
  109. package/dist/core/ingestion/binding-accumulator.js +336 -0
  110. package/dist/core/ingestion/call-processor.d.ts +155 -24
  111. package/dist/core/ingestion/call-processor.js +1129 -247
  112. package/dist/core/ingestion/class-extractors/generic.d.ts +2 -0
  113. package/dist/core/ingestion/class-extractors/generic.js +135 -0
  114. package/dist/core/ingestion/class-types.d.ts +34 -0
  115. package/dist/core/ingestion/class-types.js +1 -0
  116. package/dist/core/ingestion/cobol-processor.d.ts +1 -1
  117. package/dist/core/ingestion/entry-point-scoring.d.ts +1 -0
  118. package/dist/core/ingestion/entry-point-scoring.js +1 -0
  119. package/dist/core/ingestion/field-types.d.ts +2 -2
  120. package/dist/core/ingestion/filesystem-walker.js +8 -0
  121. package/dist/core/ingestion/framework-detection.d.ts +1 -0
  122. package/dist/core/ingestion/framework-detection.js +1 -0
  123. package/dist/core/ingestion/heritage-processor.d.ts +8 -15
  124. package/dist/core/ingestion/heritage-processor.js +15 -28
  125. package/dist/core/ingestion/import-processor.d.ts +1 -11
  126. package/dist/core/ingestion/import-processor.js +1 -13
  127. package/dist/core/ingestion/import-resolvers/utils.js +1 -0
  128. package/dist/core/ingestion/import-resolvers/vue.d.ts +8 -0
  129. package/dist/core/ingestion/import-resolvers/vue.js +9 -0
  130. package/dist/core/ingestion/language-config.js +1 -1
  131. package/dist/core/ingestion/language-provider.d.ts +14 -3
  132. package/dist/core/ingestion/languages/c-cpp.js +168 -1
  133. package/dist/core/ingestion/languages/csharp.js +20 -0
  134. package/dist/core/ingestion/languages/dart.js +26 -4
  135. package/dist/core/ingestion/languages/go.js +22 -0
  136. package/dist/core/ingestion/languages/index.d.ts +1 -0
  137. package/dist/core/ingestion/languages/index.js +2 -0
  138. package/dist/core/ingestion/languages/java.js +17 -0
  139. package/dist/core/ingestion/languages/kotlin.js +24 -1
  140. package/dist/core/ingestion/languages/php.js +23 -11
  141. package/dist/core/ingestion/languages/python.js +9 -0
  142. package/dist/core/ingestion/languages/ruby.js +43 -0
  143. package/dist/core/ingestion/languages/rust.js +38 -0
  144. package/dist/core/ingestion/languages/swift.js +31 -0
  145. package/dist/core/ingestion/languages/typescript.d.ts +1 -0
  146. package/dist/core/ingestion/languages/typescript.js +52 -3
  147. package/dist/core/ingestion/languages/vue.d.ts +13 -0
  148. package/dist/core/ingestion/languages/vue.js +81 -0
  149. package/dist/core/ingestion/markdown-processor.d.ts +1 -1
  150. package/dist/core/ingestion/method-extractors/configs/c-cpp.d.ts +3 -0
  151. package/dist/core/ingestion/method-extractors/configs/c-cpp.js +387 -0
  152. package/dist/core/ingestion/method-extractors/configs/csharp.js +5 -1
  153. package/dist/core/ingestion/method-extractors/configs/dart.d.ts +2 -0
  154. package/dist/core/ingestion/method-extractors/configs/dart.js +376 -0
  155. package/dist/core/ingestion/method-extractors/configs/go.d.ts +2 -0
  156. package/dist/core/ingestion/method-extractors/configs/go.js +176 -0
  157. package/dist/core/ingestion/method-extractors/configs/jvm.js +14 -4
  158. package/dist/core/ingestion/method-extractors/configs/php.d.ts +2 -0
  159. package/dist/core/ingestion/method-extractors/configs/php.js +304 -0
  160. package/dist/core/ingestion/method-extractors/configs/python.d.ts +2 -0
  161. package/dist/core/ingestion/method-extractors/configs/python.js +309 -0
  162. package/dist/core/ingestion/method-extractors/configs/ruby.d.ts +2 -0
  163. package/dist/core/ingestion/method-extractors/configs/ruby.js +286 -0
  164. package/dist/core/ingestion/method-extractors/configs/rust.d.ts +2 -0
  165. package/dist/core/ingestion/method-extractors/configs/rust.js +195 -0
  166. package/dist/core/ingestion/method-extractors/configs/swift.d.ts +2 -0
  167. package/dist/core/ingestion/method-extractors/configs/swift.js +277 -0
  168. package/dist/core/ingestion/method-extractors/configs/typescript-javascript.js +85 -8
  169. package/dist/core/ingestion/method-extractors/generic.d.ts +6 -0
  170. package/dist/core/ingestion/method-extractors/generic.js +84 -17
  171. package/dist/core/ingestion/method-types.d.ts +29 -0
  172. package/dist/core/ingestion/model/field-registry.d.ts +18 -0
  173. package/dist/core/ingestion/model/field-registry.js +22 -0
  174. package/dist/core/ingestion/model/heritage-map.d.ts +70 -0
  175. package/dist/core/ingestion/model/heritage-map.js +159 -0
  176. package/dist/core/ingestion/model/index.d.ts +20 -0
  177. package/dist/core/ingestion/model/index.js +41 -0
  178. package/dist/core/ingestion/model/method-registry.d.ts +62 -0
  179. package/dist/core/ingestion/model/method-registry.js +130 -0
  180. package/dist/core/ingestion/model/registration-table.d.ts +139 -0
  181. package/dist/core/ingestion/model/registration-table.js +224 -0
  182. package/dist/core/ingestion/model/resolution-context.d.ts +93 -0
  183. package/dist/core/ingestion/model/resolution-context.js +337 -0
  184. package/dist/core/ingestion/model/resolve.d.ts +56 -0
  185. package/dist/core/ingestion/model/resolve.js +297 -0
  186. package/dist/core/ingestion/model/semantic-model.d.ts +86 -0
  187. package/dist/core/ingestion/model/semantic-model.js +120 -0
  188. package/dist/core/ingestion/model/symbol-table.d.ts +222 -0
  189. package/dist/core/ingestion/model/symbol-table.js +206 -0
  190. package/dist/core/ingestion/model/type-registry.d.ts +39 -0
  191. package/dist/core/ingestion/model/type-registry.js +62 -0
  192. package/dist/core/ingestion/mro-processor.d.ts +5 -4
  193. package/dist/core/ingestion/mro-processor.js +311 -107
  194. package/dist/core/ingestion/parsing-processor.d.ts +5 -4
  195. package/dist/core/ingestion/parsing-processor.js +224 -87
  196. package/dist/core/ingestion/pipeline-phases/cobol.d.ts +16 -0
  197. package/dist/core/ingestion/pipeline-phases/cobol.js +45 -0
  198. package/dist/core/ingestion/pipeline-phases/communities.d.ts +16 -0
  199. package/dist/core/ingestion/pipeline-phases/communities.js +62 -0
  200. package/dist/core/ingestion/pipeline-phases/cross-file-impl.d.ts +17 -0
  201. package/dist/core/ingestion/pipeline-phases/cross-file-impl.js +156 -0
  202. package/dist/core/ingestion/pipeline-phases/cross-file.d.ts +37 -0
  203. package/dist/core/ingestion/pipeline-phases/cross-file.js +63 -0
  204. package/dist/core/ingestion/pipeline-phases/index.d.ts +21 -0
  205. package/dist/core/ingestion/pipeline-phases/index.js +22 -0
  206. package/dist/core/ingestion/pipeline-phases/markdown.d.ts +17 -0
  207. package/dist/core/ingestion/pipeline-phases/markdown.js +33 -0
  208. package/dist/core/ingestion/pipeline-phases/mro.d.ts +18 -0
  209. package/dist/core/ingestion/pipeline-phases/mro.js +36 -0
  210. package/dist/core/ingestion/pipeline-phases/orm-extraction.d.ts +22 -0
  211. package/dist/core/ingestion/pipeline-phases/orm-extraction.js +92 -0
  212. package/dist/core/ingestion/pipeline-phases/orm.d.ts +15 -0
  213. package/dist/core/ingestion/pipeline-phases/orm.js +74 -0
  214. package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +47 -0
  215. package/dist/core/ingestion/pipeline-phases/parse-impl.js +437 -0
  216. package/dist/core/ingestion/pipeline-phases/parse.d.ts +49 -0
  217. package/dist/core/ingestion/pipeline-phases/parse.js +33 -0
  218. package/dist/core/ingestion/pipeline-phases/processes.d.ts +16 -0
  219. package/dist/core/ingestion/pipeline-phases/processes.js +143 -0
  220. package/dist/core/ingestion/pipeline-phases/routes.d.ts +21 -0
  221. package/dist/core/ingestion/pipeline-phases/routes.js +243 -0
  222. package/dist/core/ingestion/pipeline-phases/runner.d.ts +22 -0
  223. package/dist/core/ingestion/pipeline-phases/runner.js +203 -0
  224. package/dist/core/ingestion/pipeline-phases/scan.d.ts +21 -0
  225. package/dist/core/ingestion/pipeline-phases/scan.js +46 -0
  226. package/dist/core/ingestion/pipeline-phases/structure.d.ts +27 -0
  227. package/dist/core/ingestion/pipeline-phases/structure.js +35 -0
  228. package/dist/core/ingestion/pipeline-phases/tools.d.ts +20 -0
  229. package/dist/core/ingestion/pipeline-phases/tools.js +79 -0
  230. package/dist/core/ingestion/pipeline-phases/types.d.ts +79 -0
  231. package/dist/core/ingestion/pipeline-phases/types.js +37 -0
  232. package/dist/core/ingestion/pipeline-phases/wildcard-synthesis.d.ts +35 -0
  233. package/dist/core/ingestion/pipeline-phases/wildcard-synthesis.js +174 -0
  234. package/dist/core/ingestion/pipeline.d.ts +18 -10
  235. package/dist/core/ingestion/pipeline.js +66 -1410
  236. package/dist/core/ingestion/process-processor.js +1 -1
  237. package/dist/core/ingestion/tree-sitter-queries.d.ts +5 -5
  238. package/dist/core/ingestion/tree-sitter-queries.js +90 -0
  239. package/dist/core/ingestion/type-env.d.ts +15 -2
  240. package/dist/core/ingestion/type-env.js +163 -102
  241. package/dist/core/ingestion/type-extractors/csharp.js +17 -0
  242. package/dist/core/ingestion/type-extractors/jvm.js +11 -0
  243. package/dist/core/ingestion/type-extractors/php.js +0 -55
  244. package/dist/core/ingestion/type-extractors/ruby.js +0 -32
  245. package/dist/core/ingestion/type-extractors/swift.js +13 -0
  246. package/dist/core/ingestion/type-extractors/types.d.ts +8 -8
  247. package/dist/core/ingestion/type-extractors/typescript.js +66 -69
  248. package/dist/core/ingestion/utils/ast-helpers.d.ts +32 -44
  249. package/dist/core/ingestion/utils/ast-helpers.js +157 -573
  250. package/dist/core/ingestion/utils/env.d.ts +10 -0
  251. package/dist/core/ingestion/utils/env.js +10 -0
  252. package/dist/core/ingestion/utils/graph-sort.d.ts +58 -0
  253. package/dist/core/ingestion/utils/graph-sort.js +100 -0
  254. package/dist/core/ingestion/utils/method-props.d.ts +32 -0
  255. package/dist/core/ingestion/utils/method-props.js +147 -0
  256. package/dist/core/ingestion/vue-sfc-extractor.d.ts +44 -0
  257. package/dist/core/ingestion/vue-sfc-extractor.js +94 -0
  258. package/dist/core/ingestion/workers/parse-worker.d.ts +31 -19
  259. package/dist/core/ingestion/workers/parse-worker.js +469 -200
  260. package/dist/core/lbug/lbug-adapter.d.ts +6 -0
  261. package/dist/core/lbug/lbug-adapter.js +134 -27
  262. package/dist/core/lbug/pool-adapter.d.ts +76 -0
  263. package/dist/core/lbug/pool-adapter.js +522 -0
  264. package/dist/core/run-analyze.d.ts +2 -0
  265. package/dist/core/run-analyze.js +1 -1
  266. package/dist/core/search/bm25-index.js +1 -1
  267. package/dist/core/tree-sitter/parser-loader.js +1 -0
  268. package/dist/core/wiki/graph-queries.js +1 -1
  269. package/dist/mcp/core/embedder.js +6 -5
  270. package/dist/mcp/core/lbug-adapter.d.ts +3 -63
  271. package/dist/mcp/core/lbug-adapter.js +3 -484
  272. package/dist/mcp/local/local-backend.d.ts +31 -2
  273. package/dist/mcp/local/local-backend.js +255 -46
  274. package/dist/mcp/resources.js +5 -4
  275. package/dist/mcp/staleness.d.ts +3 -13
  276. package/dist/mcp/staleness.js +2 -31
  277. package/dist/mcp/tools.js +80 -4
  278. package/dist/server/analyze-job.d.ts +2 -0
  279. package/dist/server/analyze-job.js +4 -0
  280. package/dist/server/api.d.ts +20 -1
  281. package/dist/server/api.js +306 -71
  282. package/dist/server/git-clone.d.ts +2 -1
  283. package/dist/server/git-clone.js +98 -5
  284. package/dist/storage/git.d.ts +13 -0
  285. package/dist/storage/git.js +25 -0
  286. package/dist/storage/repo-manager.js +1 -1
  287. package/package.json +9 -3
  288. package/scripts/patch-tree-sitter-swift.cjs +78 -0
  289. package/vendor/tree-sitter-proto/binding.gyp +30 -0
  290. package/vendor/tree-sitter-proto/bindings/node/binding.cc +20 -0
  291. package/vendor/tree-sitter-proto/bindings/node/index.d.ts +28 -0
  292. package/vendor/tree-sitter-proto/bindings/node/index.js +7 -0
  293. package/vendor/tree-sitter-proto/package.json +18 -0
  294. package/vendor/tree-sitter-proto/src/node-types.json +1145 -0
  295. package/vendor/tree-sitter-proto/src/parser.c +10149 -0
  296. package/vendor/tree-sitter-proto/src/tree_sitter/alloc.h +54 -0
  297. package/vendor/tree-sitter-proto/src/tree_sitter/array.h +291 -0
  298. package/vendor/tree-sitter-proto/src/tree_sitter/parser.h +266 -0
  299. package/dist/core/ingestion/named-binding-processor.d.ts +0 -18
  300. package/dist/core/ingestion/named-binding-processor.js +0 -42
  301. package/dist/core/ingestion/resolution-context.d.ts +0 -58
  302. package/dist/core/ingestion/resolution-context.js +0 -135
  303. package/dist/core/ingestion/symbol-table.d.ts +0 -79
  304. package/dist/core/ingestion/symbol-table.js +0 -115
package/dist/cli/setup.js CHANGED
@@ -8,7 +8,7 @@
8
8
  import fs from 'fs/promises';
9
9
  import path from 'path';
10
10
  import os from 'os';
11
- import { execFile } from 'child_process';
11
+ import { execFile, execFileSync } from 'child_process';
12
12
  import { promisify } from 'util';
13
13
  import { fileURLToPath } from 'url';
14
14
  import { glob } from 'glob';
@@ -16,11 +16,42 @@ import { getGlobalDir } from '../storage/repo-manager.js';
16
16
  const __filename = fileURLToPath(import.meta.url);
17
17
  const __dirname = path.dirname(__filename);
18
18
  const execFileAsync = promisify(execFile);
19
+ /**
20
+ * Resolve the absolute path to the `gitnexus` binary if it's installed
21
+ * globally (or via npm -g / yarn global). Returns null when not found.
22
+ */
23
+ function resolveGitnexusBin() {
24
+ try {
25
+ const cmd = process.platform === 'win32' ? 'where' : 'which';
26
+ const resolved = execFileSync(cmd, ['gitnexus'], {
27
+ encoding: 'utf-8',
28
+ timeout: 5000,
29
+ stdio: ['ignore', 'pipe', 'ignore'],
30
+ })
31
+ .split('\n')[0]
32
+ .trim();
33
+ return resolved || null;
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
19
39
  /**
20
40
  * The MCP server entry for all editors.
21
- * On Windows, npx must be invoked via cmd /c since it's a .cmd script.
41
+ *
42
+ * Prefers the globally-installed `gitnexus` binary (starts in ~1 s) over
43
+ * `npx -y gitnexus@latest` (cold-cache install of native deps can take
44
+ * >60 s, exceeding Claude Code's 30 s MCP connection timeout).
45
+ *
46
+ * Falls back to npx when the binary isn't on PATH — e.g. first-time
47
+ * users who ran `npx gitnexus analyze` but haven't done `npm i -g`.
22
48
  */
23
49
  function getMcpEntry() {
50
+ const bin = resolveGitnexusBin();
51
+ if (bin) {
52
+ return { command: bin, args: ['mcp'] };
53
+ }
54
+ // Fallback: npx (works without a global install, but slow cold-start)
24
55
  if (process.platform === 'win32') {
25
56
  return {
26
57
  command: 'cmd',
@@ -193,7 +224,7 @@ async function setupOpenCode(result) {
193
224
  result.skipped.push('OpenCode (not installed)');
194
225
  return;
195
226
  }
196
- const configPath = path.join(opencodeDir, 'config.json');
227
+ const configPath = path.join(opencodeDir, 'opencode.json');
197
228
  try {
198
229
  const existing = await readJsonFile(configPath);
199
230
  const config = existing || {};
@@ -351,11 +351,16 @@ export const createIgnoreFilter = async (repoPath, options) => {
351
351
  if (DEFAULT_IGNORE_LIST.has(p.name))
352
352
  return true;
353
353
  // Check against .gitignore / .gitnexusignore patterns.
354
- // Test both bare path and path with trailing slash to handle
355
- // bare-name patterns (e.g. `local`) and dir-only patterns (e.g. `local/`).
354
+ // Since childrenIgnored is only called for directories, always test with
355
+ // a trailing slash. This ensures directory-only negation patterns (e.g.
356
+ // `!iOS/`) are applied correctly — without the slash, `ig.ignores('iOS')`
357
+ // treats the path as a file and misses the negation.
358
+ // Bare-name patterns (e.g. `local`) still match `local/` per gitignore spec:
359
+ // the `ignore` package normalizes `dir` and `dir/` to match directories.
360
+ // See: https://github.com/kaelzhang/node-ignore#2-filenames-and-dirnames
356
361
  if (ig) {
357
362
  const rel = p.relative();
358
- if (rel && (ig.ignores(rel) || ig.ignores(rel + '/')))
363
+ if (rel && ig.ignores(rel + '/'))
359
364
  return true;
360
365
  }
361
366
  return false;
@@ -82,7 +82,7 @@ export async function augment(pattern, cwd) {
82
82
  if (!repo)
83
83
  return '';
84
84
  // Lazy-load lbug adapter (skip unnecessary init)
85
- const { initLbug, executeQuery, isLbugReady } = await import('../../mcp/core/lbug-adapter.js');
85
+ const { initLbug, executeQuery, isLbugReady } = await import('../lbug/pool-adapter.js');
86
86
  const { searchFTSFromLbug } = await import('../search/bm25-index.js');
87
87
  const repoId = repo.name.toLowerCase();
88
88
  // Init LadybugDB if not already
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Git working tree vs index commit staleness (used by MCP resources, group status, etc.).
3
+ * Lives in core/ so application code does not depend on the MCP package layer.
4
+ */
5
+ export interface StalenessInfo {
6
+ isStale: boolean;
7
+ commitsBehind: number;
8
+ hint?: string;
9
+ }
10
+ /**
11
+ * Check how many commits the index is behind HEAD (synchronous; uses git CLI).
12
+ */
13
+ export declare function checkStaleness(repoPath: string, lastCommit: string): StalenessInfo;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Git working tree vs index commit staleness (used by MCP resources, group status, etc.).
3
+ * Lives in core/ so application code does not depend on the MCP package layer.
4
+ */
5
+ import { execFileSync } from 'node:child_process';
6
+ /**
7
+ * Check how many commits the index is behind HEAD (synchronous; uses git CLI).
8
+ */
9
+ export function checkStaleness(repoPath, lastCommit) {
10
+ try {
11
+ const result = execFileSync('git', ['rev-list', '--count', `${lastCommit}..HEAD`], {
12
+ cwd: repoPath,
13
+ encoding: 'utf-8',
14
+ stdio: ['pipe', 'pipe', 'pipe'],
15
+ }).trim();
16
+ const commitsBehind = parseInt(result, 10) || 0;
17
+ if (commitsBehind > 0) {
18
+ return {
19
+ isStale: true,
20
+ commitsBehind,
21
+ hint: `⚠️ Index is ${commitsBehind} commit${commitsBehind > 1 ? 's' : ''} behind HEAD. Run analyze tool to update.`,
22
+ };
23
+ }
24
+ return { isStale: false, commitsBehind: 0 };
25
+ }
26
+ catch {
27
+ return { isStale: false, commitsBehind: 0 };
28
+ }
29
+ }
@@ -0,0 +1,82 @@
1
+ import type { LbugValue } from '@ladybugdb/core';
2
+ import type { BridgeHandle, BridgeMeta, StoredContract, CrossLink, RepoSnapshot } from './types.js';
3
+ export declare function contractNodeId(repo: string, contractId: string, role: string, filePath: string): string;
4
+ /**
5
+ * In-memory index of contract node IDs keyed three ways, mirroring the
6
+ * three-tier fallback lookup in {@link findContractNode}. Built once per
7
+ * `writeBridge` call after all contracts are successfully inserted, then
8
+ * consulted for every cross-link — which eliminates the former N+1 query
9
+ * pattern (up to `6 × cross-links` DB round-trips) and turns cross-link
10
+ * resolution into constant-time per link.
11
+ *
12
+ * Keys are deliberately flat strings (not tuples) so `Map<string, ...>`
13
+ * works; the separator `\0` can't occur in any legal repo path / file
14
+ * path / symbol identifier, which makes the encoding injection-safe.
15
+ */
16
+ export interface ContractLookupIndex {
17
+ /** tier 1: `repo + role + symbolUid` → contract node id */
18
+ byUid: Map<string, string>;
19
+ /** tier 2: `repo + role + filePath + symbolName` → contract node id */
20
+ byRef: Map<string, string>;
21
+ /** tier 3: `repo + role + filePath` → list of contract node ids in that file */
22
+ byFile: Map<string, string[]>;
23
+ }
24
+ export declare function createContractLookupIndex(): ContractLookupIndex;
25
+ /**
26
+ * Add a successfully-inserted contract to the lookup index. Must be called
27
+ * AFTER the DB insert succeeds (not before) so failed inserts don't poison
28
+ * the index and cause cross-links to point at non-existent rows.
29
+ */
30
+ export declare function indexContract(index: ContractLookupIndex, contract: StoredContract, nodeId: string): void;
31
+ /**
32
+ * Resolve a cross-link endpoint (consumer or provider reference) to an
33
+ * already-inserted contract node id. Returns `null` if no match — the
34
+ * caller is expected to count that as a dropped link in `WriteBridgeReport`.
35
+ *
36
+ * The resolution order matches the pre-cache DB-query behavior:
37
+ * 1. exact `symbolUid` match in the same `(repo, role)` scope
38
+ * 2. exact `(filePath, symbolName)` match
39
+ * 3. if exactly one contract lives in the file → that one (fallback for
40
+ * legacy graph-assisted extractors that couldn't resolve a symbol name)
41
+ *
42
+ * This is a pure function — no I/O, no DB — so it's trivial to unit-test
43
+ * in isolation (which was the reviewer's main clean-code concern on the
44
+ * original 35-line inner closure in `writeBridge`).
45
+ */
46
+ export declare function findContractNode(index: ContractLookupIndex, repo: string, role: 'consumer' | 'provider', symbolUid: string, filePath: string, symbolName: string): string | null;
47
+ export declare function openBridgeDb(dbPath: string): Promise<BridgeHandle>;
48
+ export declare function ensureBridgeSchema(handle: BridgeHandle): Promise<void>;
49
+ export declare function queryBridge<T>(handle: BridgeHandle, cypher: string, params?: Record<string, LbugValue>): Promise<T[]>;
50
+ export declare function closeBridgeDb(handle: BridgeHandle): Promise<void>;
51
+ export declare function retryRename(src: string, dst: string, attempts?: number): Promise<void>;
52
+ export declare function writeBridgeMeta(groupDir: string, meta: BridgeMeta): Promise<void>;
53
+ export declare function readBridgeMeta(groupDir: string): Promise<BridgeMeta>;
54
+ export interface WriteBridgeInput {
55
+ contracts: StoredContract[];
56
+ crossLinks: CrossLink[];
57
+ repoSnapshots: Record<string, RepoSnapshot>;
58
+ missingRepos: string[];
59
+ }
60
+ /**
61
+ * Non-fatal issues encountered during writeBridge. Callers can log these to
62
+ * surface partial-success state without aborting the whole sync.
63
+ * `sampleErrors` is capped at MAX_SAMPLE_ERRORS per category to bound memory.
64
+ */
65
+ export interface WriteBridgeReport {
66
+ contractsInserted: number;
67
+ contractsFailed: number;
68
+ snapshotsInserted: number;
69
+ snapshotsFailed: number;
70
+ linksInserted: number;
71
+ linksFailed: number;
72
+ /** Cross-links skipped because their from/to contract nodes weren't found. */
73
+ linksDroppedMissingNode: number;
74
+ sampleErrors: Array<{
75
+ kind: 'contract' | 'snapshot' | 'link';
76
+ id: string;
77
+ message: string;
78
+ }>;
79
+ }
80
+ export declare function writeBridge(groupDir: string, input: WriteBridgeInput): Promise<WriteBridgeReport>;
81
+ export declare function openBridgeDbReadOnly(groupDir: string): Promise<BridgeHandle | null>;
82
+ export declare function bridgeExists(groupDir: string): Promise<boolean>;
@@ -0,0 +1,460 @@
1
+ import fsp from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import lbug from '@ladybugdb/core';
5
+ import { BRIDGE_SCHEMA_QUERIES, BRIDGE_SCHEMA_VERSION } from './bridge-schema.js';
6
+ import { dedupeContracts, dedupeCrossLinks } from './normalization.js';
7
+ export function contractNodeId(repo, contractId, role, filePath) {
8
+ return createHash('sha256').update(`${repo}\0${contractId}\0${role}\0${filePath}`).digest('hex');
9
+ }
10
+ export function createContractLookupIndex() {
11
+ return {
12
+ byUid: new Map(),
13
+ byRef: new Map(),
14
+ byFile: new Map(),
15
+ };
16
+ }
17
+ function uidKey(repo, role, symbolUid) {
18
+ return `${repo}\0${role}\0${symbolUid}`;
19
+ }
20
+ function refKey(repo, role, filePath, symbolName) {
21
+ return `${repo}\0${role}\0${filePath}\0${symbolName}`;
22
+ }
23
+ function fileKey(repo, role, filePath) {
24
+ return `${repo}\0${role}\0${filePath}`;
25
+ }
26
+ /**
27
+ * Add a successfully-inserted contract to the lookup index. Must be called
28
+ * AFTER the DB insert succeeds (not before) so failed inserts don't poison
29
+ * the index and cause cross-links to point at non-existent rows.
30
+ */
31
+ export function indexContract(index, contract, nodeId) {
32
+ if (contract.symbolUid) {
33
+ index.byUid.set(uidKey(contract.repo, contract.role, contract.symbolUid), nodeId);
34
+ }
35
+ index.byRef.set(refKey(contract.repo, contract.role, contract.symbolRef.filePath, contract.symbolRef.name), nodeId);
36
+ const fk = fileKey(contract.repo, contract.role, contract.symbolRef.filePath);
37
+ const existing = index.byFile.get(fk);
38
+ if (existing) {
39
+ existing.push(nodeId);
40
+ }
41
+ else {
42
+ index.byFile.set(fk, [nodeId]);
43
+ }
44
+ }
45
+ /**
46
+ * Resolve a cross-link endpoint (consumer or provider reference) to an
47
+ * already-inserted contract node id. Returns `null` if no match — the
48
+ * caller is expected to count that as a dropped link in `WriteBridgeReport`.
49
+ *
50
+ * The resolution order matches the pre-cache DB-query behavior:
51
+ * 1. exact `symbolUid` match in the same `(repo, role)` scope
52
+ * 2. exact `(filePath, symbolName)` match
53
+ * 3. if exactly one contract lives in the file → that one (fallback for
54
+ * legacy graph-assisted extractors that couldn't resolve a symbol name)
55
+ *
56
+ * This is a pure function — no I/O, no DB — so it's trivial to unit-test
57
+ * in isolation (which was the reviewer's main clean-code concern on the
58
+ * original 35-line inner closure in `writeBridge`).
59
+ */
60
+ export function findContractNode(index, repo, role, symbolUid, filePath, symbolName) {
61
+ if (symbolUid) {
62
+ const uidHit = index.byUid.get(uidKey(repo, role, symbolUid));
63
+ if (uidHit !== undefined)
64
+ return uidHit;
65
+ }
66
+ const refHit = index.byRef.get(refKey(repo, role, filePath, symbolName));
67
+ if (refHit !== undefined)
68
+ return refHit;
69
+ const fileCandidates = index.byFile.get(fileKey(repo, role, filePath));
70
+ if (fileCandidates && fileCandidates.length === 1)
71
+ return fileCandidates[0];
72
+ return null;
73
+ }
74
+ export async function openBridgeDb(dbPath) {
75
+ const parentDir = path.dirname(dbPath);
76
+ await fsp.mkdir(parentDir, { recursive: true });
77
+ const db = new lbug.Database(dbPath, 0, false, false); // writable
78
+ const conn = new lbug.Connection(db);
79
+ return { _db: db, _conn: conn, groupDir: parentDir };
80
+ }
81
+ /**
82
+ * LadybugDB returns an error whose message contains this substring when a
83
+ * CREATE NODE TABLE or CREATE REL TABLE statement hits an already-existing
84
+ * table. LadybugDB DDL doesn't support IF NOT EXISTS, and its JS driver
85
+ * doesn't expose typed error codes, so we match on the message substring —
86
+ * the same pattern used by `core/lbug/lbug-adapter.ts`. If a future
87
+ * LadybugDB release changes the wording, update this constant.
88
+ */
89
+ const LBUG_ALREADY_EXISTS_MSG = 'already exists';
90
+ export async function ensureBridgeSchema(handle) {
91
+ const conn = handle._conn;
92
+ for (const q of BRIDGE_SCHEMA_QUERIES) {
93
+ try {
94
+ await conn.query(q);
95
+ }
96
+ catch (err) {
97
+ const msg = err instanceof Error ? err.message : String(err);
98
+ if (!msg.includes(LBUG_ALREADY_EXISTS_MSG))
99
+ throw err;
100
+ }
101
+ }
102
+ }
103
+ export async function queryBridge(handle, cypher, params) {
104
+ const conn = handle._conn;
105
+ if (params && Object.keys(params).length > 0) {
106
+ const stmt = await conn.prepare(cypher);
107
+ if (!stmt.isSuccess()) {
108
+ const errMsg = await stmt.getErrorMessage();
109
+ throw new Error(`Bridge query prepare failed: ${errMsg}`);
110
+ }
111
+ const queryResult = await conn.execute(stmt, params);
112
+ const result = unwrapQueryResult(queryResult);
113
+ return (await result.getAll());
114
+ }
115
+ const queryResult = await conn.query(cypher);
116
+ const result = unwrapQueryResult(queryResult);
117
+ return (await result.getAll());
118
+ }
119
+ /**
120
+ * LadybugDB's `conn.query` / `conn.execute` can return either a single
121
+ * `QueryResult` (for a single statement) or an array of them (when a
122
+ * multi-statement script is dispatched). We always pass a single statement,
123
+ * so the array form is a wrapper we unwrap here — but an empty top-level
124
+ * array would cause `.getAll()` on `undefined` and crash with a confusing
125
+ * stack. Throwing an explicit error makes a driver-contract regression
126
+ * visible immediately instead of masking it.
127
+ */
128
+ function unwrapQueryResult(queryResult) {
129
+ if (Array.isArray(queryResult)) {
130
+ if (queryResult.length === 0) {
131
+ throw new Error('Bridge query returned an empty QueryResult array');
132
+ }
133
+ return queryResult[0];
134
+ }
135
+ return queryResult;
136
+ }
137
+ export async function closeBridgeDb(handle) {
138
+ try {
139
+ await handle._conn.close();
140
+ }
141
+ catch {
142
+ /* ignore */
143
+ }
144
+ try {
145
+ await handle._db.close();
146
+ }
147
+ catch {
148
+ /* ignore */
149
+ }
150
+ }
151
+ /* ------------------------------------------------------------------ */
152
+ /* retryRename — handles transient EBUSY/EPERM/EACCES on Windows */
153
+ /* ------------------------------------------------------------------ */
154
+ const RETRY_CODES = new Set(['EBUSY', 'EPERM', 'EACCES']);
155
+ export async function retryRename(src, dst, attempts = 3) {
156
+ for (let i = 1; i <= attempts; i++) {
157
+ try {
158
+ await fsp.rename(src, dst);
159
+ return;
160
+ }
161
+ catch (err) {
162
+ const code = err.code;
163
+ if (!code || !RETRY_CODES.has(code) || i === attempts)
164
+ throw err;
165
+ await new Promise((r) => setTimeout(r, 100 * Math.pow(2, i - 1)));
166
+ }
167
+ }
168
+ }
169
+ /* ------------------------------------------------------------------ */
170
+ /* writeBridgeMeta / readBridgeMeta */
171
+ /* ------------------------------------------------------------------ */
172
+ export async function writeBridgeMeta(groupDir, meta) {
173
+ const target = path.join(groupDir, 'meta.json');
174
+ const tmp = `${target}.tmp.${Date.now()}`;
175
+ await fsp.writeFile(tmp, JSON.stringify(meta, null, 2), 'utf-8');
176
+ // Use retryRename for consistency with writeBridge's atomic swap — on
177
+ // Windows a concurrent reader can cause EBUSY/EPERM even on a tiny
178
+ // meta.json, and we don't want meta write to be less robust than the
179
+ // bridge.lbug swap it accompanies.
180
+ await retryRename(tmp, target);
181
+ }
182
+ export async function readBridgeMeta(groupDir) {
183
+ try {
184
+ const content = await fsp.readFile(path.join(groupDir, 'meta.json'), 'utf-8');
185
+ return JSON.parse(content);
186
+ }
187
+ catch {
188
+ return { version: 0, generatedAt: '', missingRepos: [] };
189
+ }
190
+ }
191
+ const MAX_SAMPLE_ERRORS = 10;
192
+ function errMessage(err) {
193
+ if (err instanceof Error)
194
+ return err.message;
195
+ try {
196
+ return String(err);
197
+ }
198
+ catch {
199
+ return 'unknown error';
200
+ }
201
+ }
202
+ export async function writeBridge(groupDir, input) {
203
+ await fsp.mkdir(groupDir, { recursive: true });
204
+ const contracts = dedupeContracts(input.contracts);
205
+ const crossLinks = dedupeCrossLinks(input.crossLinks);
206
+ const finalPath = path.join(groupDir, 'bridge.lbug');
207
+ const tmpPath = path.join(groupDir, 'bridge.lbug.tmp');
208
+ const bakPath = path.join(groupDir, 'bridge.lbug.bak');
209
+ const report = {
210
+ contractsInserted: 0,
211
+ contractsFailed: 0,
212
+ snapshotsInserted: 0,
213
+ snapshotsFailed: 0,
214
+ linksInserted: 0,
215
+ linksFailed: 0,
216
+ linksDroppedMissingNode: 0,
217
+ sampleErrors: [],
218
+ };
219
+ const recordError = (kind, id, err) => {
220
+ if (report.sampleErrors.length < MAX_SAMPLE_ERRORS) {
221
+ report.sampleErrors.push({ kind, id, message: errMessage(err) });
222
+ }
223
+ };
224
+ // Clean up any leftover tmp
225
+ try {
226
+ await fsp.rm(tmpPath, { recursive: true, force: true });
227
+ }
228
+ catch {
229
+ /* ignore */
230
+ }
231
+ // 1. Create temp DB, insert all data.
232
+ //
233
+ // Everything after `openBridgeDb` must run inside a try/finally so that
234
+ // if ANY step before the explicit `closeBridgeDb` throws — schema
235
+ // creation, a contract insert loop that rethrows, a snapshot write, the
236
+ // cross-link loop, or anything else — the handle is still released. A
237
+ // leaked handle holds the native LadybugDB file lock on tmpPath, which
238
+ // (a) leaks a FD and (b) prevents the next writeBridge call from
239
+ // reusing the same tmp slot.
240
+ const handle = await openBridgeDb(tmpPath);
241
+ let handleClosed = false;
242
+ try {
243
+ await ensureBridgeSchema(handle);
244
+ // Build the lookup index incrementally as contracts are inserted, so
245
+ // failed inserts are never in the index (and therefore never resolved
246
+ // by the cross-link loop below). This replaces a previous N+1 query
247
+ // pattern where each link made up to 6 DB round-trips to find its
248
+ // endpoints — see ContractLookupIndex.
249
+ const lookupIndex = createContractLookupIndex();
250
+ // Insert contracts — tolerate individual failures (e.g., a corrupt meta
251
+ // that can't be serialized). The whole sync must not fail because one
252
+ // contract is broken.
253
+ for (const c of contracts) {
254
+ const id = contractNodeId(c.repo, c.contractId, c.role, c.symbolRef.filePath);
255
+ try {
256
+ await queryBridge(handle, `CREATE (n:Contract {
257
+ id: $id,
258
+ contractId: $contractId,
259
+ type: $type,
260
+ role: $role,
261
+ repo: $repo,
262
+ service: $service,
263
+ symbolUid: $symbolUid,
264
+ filePath: $filePath,
265
+ symbolName: $symbolName,
266
+ confidence: $confidence,
267
+ meta: $meta
268
+ })`, {
269
+ id,
270
+ contractId: c.contractId,
271
+ type: c.type,
272
+ role: c.role,
273
+ repo: c.repo,
274
+ service: c.service ?? '',
275
+ symbolUid: c.symbolUid,
276
+ filePath: c.symbolRef.filePath,
277
+ symbolName: c.symbolName,
278
+ confidence: c.confidence,
279
+ meta: JSON.stringify(c.meta),
280
+ });
281
+ report.contractsInserted++;
282
+ // Only index on successful insert — the cross-link loop must never
283
+ // resolve to a row that isn't actually in the DB.
284
+ indexContract(lookupIndex, c, id);
285
+ }
286
+ catch (err) {
287
+ report.contractsFailed++;
288
+ recordError('contract', id, err);
289
+ }
290
+ }
291
+ // Insert repo snapshots
292
+ for (const [repoId, snap] of Object.entries(input.repoSnapshots)) {
293
+ try {
294
+ await queryBridge(handle, `CREATE (s:RepoSnapshot {
295
+ id: $id,
296
+ indexedAt: $indexedAt,
297
+ lastCommit: $lastCommit
298
+ })`, {
299
+ id: repoId,
300
+ indexedAt: snap.indexedAt,
301
+ lastCommit: snap.lastCommit,
302
+ });
303
+ report.snapshotsInserted++;
304
+ }
305
+ catch (err) {
306
+ report.snapshotsFailed++;
307
+ recordError('snapshot', repoId, err);
308
+ }
309
+ }
310
+ // Insert cross-links (tolerating missing nodes).
311
+ //
312
+ // `findContractNode` consults the in-memory lookup index built above,
313
+ // not the DB — that's an O(1) pure-function lookup per endpoint instead
314
+ // of the previous 2-3 DB queries. For M cross-links, the previous code
315
+ // issued up to 6M round-trips; this version issues zero.
316
+ //
317
+ // `link.contractId` may differ between the consumer and provider sides
318
+ // (e.g. wildcard consumer `grpc::Service/*` → method-level provider
319
+ // `grpc::Service/Method`) — that's why we resolve each endpoint
320
+ // independently via its own `(repo, role, symbolUid, filePath, symbolName)`
321
+ // tuple rather than matching on contractId.
322
+ for (const link of crossLinks) {
323
+ const linkId = `${link.from.repo}::${link.contractId}->${link.to.repo}::${link.contractId}`;
324
+ try {
325
+ const fromId = findContractNode(lookupIndex, link.from.repo, 'consumer', link.from.symbolUid, link.from.symbolRef.filePath, link.from.symbolRef.name);
326
+ const toId = findContractNode(lookupIndex, link.to.repo, 'provider', link.to.symbolUid, link.to.symbolRef.filePath, link.to.symbolRef.name);
327
+ if (!fromId || !toId) {
328
+ report.linksDroppedMissingNode++;
329
+ continue;
330
+ }
331
+ await queryBridge(handle, `
332
+ MATCH (a:Contract), (b:Contract)
333
+ WHERE a.id = $fromId AND b.id = $toId
334
+ CREATE (a)-[:ContractLink {
335
+ matchType: $matchType,
336
+ confidence: $confidence,
337
+ contractId: $contractId,
338
+ fromRepo: $fromRepo,
339
+ toRepo: $toRepo
340
+ }]->(b)
341
+ `, {
342
+ fromId,
343
+ toId,
344
+ matchType: link.matchType,
345
+ confidence: link.confidence,
346
+ contractId: link.contractId,
347
+ fromRepo: link.from.repo,
348
+ toRepo: link.to.repo,
349
+ });
350
+ report.linksInserted++;
351
+ }
352
+ catch (err) {
353
+ report.linksFailed++;
354
+ recordError('link', linkId, err);
355
+ }
356
+ }
357
+ // 2. Close temp DB (happy path). The finally block also calls
358
+ // closeBridgeDb if we threw above; `handleClosed` prevents a
359
+ // double-close on the native handle.
360
+ await closeBridgeDb(handle);
361
+ handleClosed = true;
362
+ }
363
+ finally {
364
+ if (!handleClosed) {
365
+ await closeBridgeDb(handle).catch(() => {
366
+ /* ignore: cleanup path, best effort */
367
+ });
368
+ }
369
+ }
370
+ // 3. Atomic swap: old→.bak, tmp→final, rm .bak
371
+ try {
372
+ await fsp.access(finalPath);
373
+ await retryRename(finalPath, bakPath);
374
+ }
375
+ catch {
376
+ /* no existing db */
377
+ }
378
+ await retryRename(tmpPath, finalPath);
379
+ try {
380
+ await fsp.rm(bakPath, { recursive: true, force: true });
381
+ }
382
+ catch {
383
+ /* ignore */
384
+ }
385
+ // 4. Write meta.json
386
+ await writeBridgeMeta(groupDir, {
387
+ version: BRIDGE_SCHEMA_VERSION,
388
+ generatedAt: new Date().toISOString(),
389
+ missingRepos: input.missingRepos,
390
+ });
391
+ return report;
392
+ }
393
+ /* ------------------------------------------------------------------ */
394
+ /* openBridgeDbReadOnly */
395
+ /* ------------------------------------------------------------------ */
396
+ export async function openBridgeDbReadOnly(groupDir) {
397
+ const dbPath = path.join(groupDir, 'bridge.lbug');
398
+ try {
399
+ await fsp.access(dbPath);
400
+ }
401
+ catch {
402
+ // Check for .bak recovery. Use `retryRename` (not `fsp.rename`) for the
403
+ // exact same reason the rest of this file does: the scenario that
404
+ // triggers bak recovery is an interrupted writer, which on Windows may
405
+ // still be holding an open handle on `.bak` for a few milliseconds when
406
+ // a reader races in. EBUSY/EPERM retries recover that case silently.
407
+ const bakPath = path.join(groupDir, 'bridge.lbug.bak');
408
+ try {
409
+ await fsp.access(bakPath);
410
+ await retryRename(bakPath, dbPath);
411
+ }
412
+ catch {
413
+ return null;
414
+ }
415
+ }
416
+ // Version gate: check meta.json version compatibility
417
+ const meta = await readBridgeMeta(groupDir);
418
+ if (meta.version > 0 && meta.version !== BRIDGE_SCHEMA_VERSION) {
419
+ return null; // incompatible schema version — fallback to JSON or re-sync
420
+ }
421
+ // Open the native handle. If Connection construction throws AFTER
422
+ // Database was successfully allocated, we'd leak the native Database
423
+ // object. Wrap each step separately and tear down the partial handle.
424
+ let db;
425
+ let conn;
426
+ try {
427
+ db = new lbug.Database(dbPath, 0, false, true); // readOnly
428
+ conn = new lbug.Connection(db);
429
+ return { _db: db, _conn: conn, groupDir };
430
+ }
431
+ catch {
432
+ if (conn) {
433
+ try {
434
+ await conn.close();
435
+ }
436
+ catch {
437
+ /* ignore */
438
+ }
439
+ }
440
+ if (db) {
441
+ try {
442
+ await db.close();
443
+ }
444
+ catch {
445
+ /* ignore */
446
+ }
447
+ }
448
+ return null;
449
+ }
450
+ }
451
+ /* ------------------------------------------------------------------ */
452
+ /* bridgeExists */
453
+ /* ------------------------------------------------------------------ */
454
+ export async function bridgeExists(groupDir) {
455
+ const handle = await openBridgeDbReadOnly(groupDir);
456
+ if (!handle)
457
+ return false;
458
+ await closeBridgeDb(handle);
459
+ return true;
460
+ }