gitnexus 1.6.9 → 1.6.10-aptos.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 (539) hide show
  1. package/README.md +211 -53
  2. package/dist/_shared/graph/types.d.ts +67 -3
  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/index.js.map +1 -1
  7. package/dist/_shared/integrations/resilient-fetch.d.ts +3 -1
  8. package/dist/_shared/integrations/resilient-fetch.d.ts.map +1 -1
  9. package/dist/_shared/integrations/resilient-fetch.js +4 -3
  10. package/dist/_shared/integrations/resilient-fetch.js.map +1 -1
  11. package/dist/_shared/language-detection.d.ts.map +1 -1
  12. package/dist/_shared/language-detection.js +2 -0
  13. package/dist/_shared/language-detection.js.map +1 -1
  14. package/dist/_shared/languages.d.ts +2 -0
  15. package/dist/_shared/languages.d.ts.map +1 -1
  16. package/dist/_shared/languages.js +2 -0
  17. package/dist/_shared/languages.js.map +1 -1
  18. package/dist/_shared/lbug/schema-constants.d.ts +2 -2
  19. package/dist/_shared/lbug/schema-constants.d.ts.map +1 -1
  20. package/dist/_shared/lbug/schema-constants.js +8 -0
  21. package/dist/_shared/lbug/schema-constants.js.map +1 -1
  22. package/dist/_shared/pipeline.d.ts +1 -1
  23. package/dist/_shared/pipeline.d.ts.map +1 -1
  24. package/dist/_shared/scope-resolution/callable-flow-site.d.ts +110 -0
  25. package/dist/_shared/scope-resolution/callable-flow-site.d.ts.map +1 -0
  26. package/dist/_shared/scope-resolution/callable-flow-site.js +11 -0
  27. package/dist/_shared/scope-resolution/callable-flow-site.js.map +1 -0
  28. package/dist/_shared/scope-resolution/finalize-algorithm.d.ts +1 -1
  29. package/dist/_shared/scope-resolution/finalize-algorithm.d.ts.map +1 -1
  30. package/dist/_shared/scope-resolution/finalize-algorithm.js +1 -1
  31. package/dist/_shared/scope-resolution/finalize-algorithm.js.map +1 -1
  32. package/dist/_shared/scope-resolution/language-classification.d.ts.map +1 -1
  33. package/dist/_shared/scope-resolution/language-classification.js +1 -0
  34. package/dist/_shared/scope-resolution/language-classification.js.map +1 -1
  35. package/dist/_shared/scope-resolution/parsed-file.d.ts +7 -0
  36. package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -1
  37. package/dist/_shared/scope-resolution/reference-site.d.ts +9 -1
  38. package/dist/_shared/scope-resolution/reference-site.d.ts.map +1 -1
  39. package/dist/_shared/scope-resolution/types.d.ts +20 -3
  40. package/dist/_shared/scope-resolution/types.d.ts.map +1 -1
  41. package/dist/cli/ai-context.d.ts +1 -1
  42. package/dist/cli/ai-context.js +25 -44
  43. package/dist/cli/analyze.d.ts +5 -2
  44. package/dist/cli/analyze.js +197 -11
  45. package/dist/cli/clean.js +17 -3
  46. package/dist/cli/cli-message.d.ts +1 -1
  47. package/dist/cli/doctor.d.ts +18 -0
  48. package/dist/cli/doctor.js +96 -5
  49. package/dist/cli/editor-targets.d.ts +17 -2
  50. package/dist/cli/editor-targets.js +44 -0
  51. package/dist/cli/embeddings.d.ts +12 -0
  52. package/dist/cli/embeddings.js +49 -0
  53. package/dist/cli/eval-server.d.ts +23 -1
  54. package/dist/cli/eval-server.js +126 -8
  55. package/dist/cli/help-i18n.js +6 -0
  56. package/dist/cli/i18n/en.d.ts +18 -8
  57. package/dist/cli/i18n/en.js +18 -8
  58. package/dist/cli/i18n/resources.d.ts +28 -8
  59. package/dist/cli/i18n/zh-CN.d.ts +10 -0
  60. package/dist/cli/i18n/zh-CN.js +18 -8
  61. package/dist/cli/index.js +21 -7
  62. package/dist/cli/lazy-action.d.ts +7 -0
  63. package/dist/cli/lazy-action.js +27 -0
  64. package/dist/cli/mcp.js +6 -3
  65. package/dist/cli/move-availability.d.ts +5 -0
  66. package/dist/cli/move-availability.js +11 -0
  67. package/dist/cli/setup.d.ts +8 -0
  68. package/dist/cli/setup.js +230 -32
  69. package/dist/cli/skill-gen.js +38 -18
  70. package/dist/cli/standard-skills.d.ts +76 -0
  71. package/dist/cli/standard-skills.js +45 -0
  72. package/dist/cli/status.d.ts +4 -1
  73. package/dist/cli/status.js +57 -4
  74. package/dist/cli/tool.js +11 -0
  75. package/dist/cli/uninstall.js +77 -18
  76. package/dist/cli/wiki.d.ts +1 -0
  77. package/dist/cli/wiki.js +7 -1
  78. package/dist/config/ignore-service.d.ts +2 -0
  79. package/dist/config/ignore-service.js +27 -0
  80. package/dist/core/analysis-features.d.ts +21 -0
  81. package/dist/core/analysis-features.js +59 -0
  82. package/dist/core/analyzer-identity.d.ts +74 -0
  83. package/dist/core/analyzer-identity.js +1820 -0
  84. package/dist/core/augmentation/engine.js +13 -8
  85. package/dist/core/embeddings/embedder.d.ts +3 -2
  86. package/dist/core/embeddings/embedder.js +24 -7
  87. package/dist/core/embeddings/embedding-identity.d.ts +11 -0
  88. package/dist/core/embeddings/embedding-identity.js +24 -0
  89. package/dist/core/embeddings/embedding-pipeline.d.ts +36 -1
  90. package/dist/core/embeddings/embedding-pipeline.js +100 -9
  91. package/dist/core/embeddings/http-client.d.ts +33 -3
  92. package/dist/core/embeddings/http-client.js +237 -25
  93. package/dist/core/embeddings/node-module-compat.d.ts +25 -0
  94. package/dist/core/embeddings/node-module-compat.js +25 -0
  95. package/dist/core/embeddings/onnxruntime-common-resolver.js +8 -5
  96. package/dist/core/embeddings/onnxruntime-node-resolver.js +33 -7
  97. package/dist/core/embeddings/runtime-install.d.ts +119 -0
  98. package/dist/core/embeddings/runtime-install.js +372 -0
  99. package/dist/core/embeddings/runtime-support.d.ts +48 -17
  100. package/dist/core/embeddings/runtime-support.js +96 -0
  101. package/dist/core/git-staleness.js +1 -1
  102. package/dist/core/group/bridge-db.js +2 -0
  103. package/dist/core/group/extractors/http-patterns/python.js +246 -108
  104. package/dist/core/group/extractors/manifest-extractor.js +4 -0
  105. package/dist/core/incremental/escalation-gate.d.ts +38 -0
  106. package/dist/core/incremental/escalation-gate.js +48 -0
  107. package/dist/core/incremental/shadow-candidates.d.ts +3 -3
  108. package/dist/core/incremental/shadow-candidates.js +3 -3
  109. package/dist/core/incremental/subgraph-extract.d.ts +2 -2
  110. package/dist/core/incremental/subgraph-extract.js +2 -2
  111. package/dist/core/index-freshness.d.ts +5 -0
  112. package/dist/core/index-freshness.js +13 -0
  113. package/dist/core/ingestion/class-extractors/configs/jvm.js +29 -0
  114. package/dist/core/ingestion/cobol/cobol-preprocessor.js +1 -1
  115. package/dist/core/ingestion/cobol/jcl-processor.js +9 -8
  116. package/dist/core/ingestion/cobol-processor.js +27 -26
  117. package/dist/core/ingestion/community-processor.d.ts +51 -1
  118. package/dist/core/ingestion/community-processor.js +323 -64
  119. package/dist/core/ingestion/emit-references.js +7 -2
  120. package/dist/core/ingestion/filesystem-walker.js +4 -0
  121. package/dist/core/ingestion/frameworks/spring/analysis-features.d.ts +3 -0
  122. package/dist/core/ingestion/frameworks/spring/analysis-features.js +7 -0
  123. package/dist/core/ingestion/frameworks/spring/bean-candidates.d.ts +30 -0
  124. package/dist/core/ingestion/frameworks/spring/bean-candidates.js +182 -0
  125. package/dist/core/ingestion/frameworks/spring/bean-catalog.d.ts +12 -0
  126. package/dist/core/ingestion/frameworks/spring/bean-catalog.js +26 -0
  127. package/dist/core/ingestion/frameworks/spring/config-bindings.d.ts +24 -0
  128. package/dist/core/ingestion/frameworks/spring/config-bindings.js +115 -0
  129. package/dist/core/ingestion/languages/c/captures.js +56 -0
  130. package/dist/core/ingestion/languages/c/query.js +17 -0
  131. package/dist/core/ingestion/languages/c/scope-resolver.js +6 -0
  132. package/dist/core/ingestion/languages/cobol/captures.js +93 -0
  133. package/dist/core/ingestion/languages/cobol/scope-resolver.js +7 -0
  134. package/dist/core/ingestion/languages/cpp/captures.js +365 -75
  135. package/dist/core/ingestion/languages/cpp/scope-resolver.js +14 -0
  136. package/dist/core/ingestion/languages/csharp/captures.js +25 -0
  137. package/dist/core/ingestion/languages/dart/captures.js +90 -2
  138. package/dist/core/ingestion/languages/dart/extension-type-preprocess.d.ts +7 -0
  139. package/dist/core/ingestion/languages/dart/extension-type-preprocess.js +30 -0
  140. package/dist/core/ingestion/languages/dart.js +2 -0
  141. package/dist/core/ingestion/languages/go/captures.js +34 -0
  142. package/dist/core/ingestion/languages/index.d.ts +1 -0
  143. package/dist/core/ingestion/languages/index.js +2 -0
  144. package/dist/core/ingestion/languages/java/analysis-features.d.ts +3 -0
  145. package/dist/core/ingestion/languages/java/analysis-features.js +13 -0
  146. package/dist/core/ingestion/languages/java/capture-side-channel.d.ts +22 -0
  147. package/dist/core/ingestion/languages/java/capture-side-channel.js +58 -0
  148. package/dist/core/ingestion/languages/java/captures.d.ts +4 -3
  149. package/dist/core/ingestion/languages/java/captures.js +193 -17
  150. package/dist/core/ingestion/languages/java/package-facts.d.ts +5 -0
  151. package/dist/core/ingestion/languages/java/package-facts.js +9 -0
  152. package/dist/core/ingestion/languages/java/package-siblings.d.ts +3 -16
  153. package/dist/core/ingestion/languages/java/package-siblings.js +8 -147
  154. package/dist/core/ingestion/languages/java/query.js +23 -0
  155. package/dist/core/ingestion/languages/java/scope-resolver.js +23 -0
  156. package/dist/core/ingestion/languages/java/spring-bean-metadata.d.ts +2 -0
  157. package/dist/core/ingestion/languages/java/spring-bean-metadata.js +8 -0
  158. package/dist/core/ingestion/languages/java/spring-config-bindings.d.ts +23 -0
  159. package/dist/core/ingestion/languages/java/spring-config-bindings.js +215 -0
  160. package/dist/core/ingestion/languages/java.js +3 -0
  161. package/dist/core/ingestion/languages/javascript/captures.js +16 -0
  162. package/dist/core/ingestion/languages/javascript/query.js +21 -0
  163. package/dist/core/ingestion/languages/jvm/package-facts.d.ts +35 -0
  164. package/dist/core/ingestion/languages/jvm/package-facts.js +40 -0
  165. package/dist/core/ingestion/languages/jvm/package-siblings.d.ts +14 -0
  166. package/dist/core/ingestion/languages/jvm/package-siblings.js +129 -0
  167. package/dist/core/ingestion/languages/kotlin/capture-side-channel.d.ts +17 -3
  168. package/dist/core/ingestion/languages/kotlin/capture-side-channel.js +42 -8
  169. package/dist/core/ingestion/languages/kotlin/captures.js +59 -0
  170. package/dist/core/ingestion/languages/kotlin/package-facts.d.ts +5 -0
  171. package/dist/core/ingestion/languages/kotlin/package-facts.js +9 -0
  172. package/dist/core/ingestion/languages/kotlin/package-siblings.d.ts +4 -0
  173. package/dist/core/ingestion/languages/kotlin/package-siblings.js +8 -0
  174. package/dist/core/ingestion/languages/kotlin/query.js +19 -0
  175. package/dist/core/ingestion/languages/kotlin/scope-resolver.js +9 -1
  176. package/dist/core/ingestion/languages/kotlin/spring-bean-metadata.d.ts +2 -0
  177. package/dist/core/ingestion/languages/kotlin/spring-bean-metadata.js +8 -0
  178. package/dist/core/ingestion/languages/kotlin.js +5 -6
  179. package/dist/core/ingestion/languages/move.d.ts +1 -0
  180. package/dist/core/ingestion/languages/move.js +27 -0
  181. package/dist/core/ingestion/languages/php/captures.js +40 -0
  182. package/dist/core/ingestion/languages/php/import-decomposer.js +12 -0
  183. package/dist/core/ingestion/languages/php/import-target.d.ts +2 -1
  184. package/dist/core/ingestion/languages/php/import-target.js +110 -2
  185. package/dist/core/ingestion/languages/php/interpret.js +10 -0
  186. package/dist/core/ingestion/languages/php/scope-resolver.js +1 -1
  187. package/dist/core/ingestion/languages/python/captures.d.ts +3 -4
  188. package/dist/core/ingestion/languages/python/captures.js +24 -5
  189. package/dist/core/ingestion/languages/python/interpret.js +4 -0
  190. package/dist/core/ingestion/languages/python/receiver-binding.d.ts +15 -2
  191. package/dist/core/ingestion/languages/python/receiver-binding.js +96 -2
  192. package/dist/core/ingestion/languages/python/simple-hooks.d.ts +5 -5
  193. package/dist/core/ingestion/languages/python/simple-hooks.js +15 -5
  194. package/dist/core/ingestion/languages/ruby/captures.js +42 -0
  195. package/dist/core/ingestion/languages/rust/captures.js +13 -0
  196. package/dist/core/ingestion/languages/rust/interpret.js +18 -0
  197. package/dist/core/ingestion/languages/rust/query.js +9 -0
  198. package/dist/core/ingestion/languages/rust/range-binding.js +6 -2
  199. package/dist/core/ingestion/languages/swift/captures.js +12 -0
  200. package/dist/core/ingestion/languages/typescript/captures.js +21 -0
  201. package/dist/core/ingestion/languages/typescript/nuxt-auto-imports.js +2 -1
  202. package/dist/core/ingestion/languages/typescript/query.js +37 -0
  203. package/dist/core/ingestion/markdown-processor.js +3 -2
  204. package/dist/core/ingestion/model/registration-table.js +1 -0
  205. package/dist/core/ingestion/parsing-processor.d.ts +3 -1
  206. package/dist/core/ingestion/parsing-processor.js +36 -1
  207. package/dist/core/ingestion/pipeline-phases/index.d.ts +2 -0
  208. package/dist/core/ingestion/pipeline-phases/index.js +2 -0
  209. package/dist/core/ingestion/pipeline-phases/parse-impl.js +56 -2
  210. package/dist/core/ingestion/pipeline-phases/parse.d.ts +0 -2
  211. package/dist/core/ingestion/pipeline-phases/parse.js +12 -3
  212. package/dist/core/ingestion/pipeline-phases/processes.d.ts +8 -0
  213. package/dist/core/ingestion/pipeline-phases/processes.js +11 -1
  214. package/dist/core/ingestion/pipeline-phases/spring-config.d.ts +37 -0
  215. package/dist/core/ingestion/pipeline-phases/spring-config.js +433 -0
  216. package/dist/core/ingestion/pipeline-phases/standalone-ingest.d.ts +10 -0
  217. package/dist/core/ingestion/pipeline-phases/standalone-ingest.js +6 -0
  218. package/dist/core/ingestion/pipeline.d.ts +4 -2
  219. package/dist/core/ingestion/pipeline.js +5 -2
  220. package/dist/core/ingestion/resolve-references.js +12 -0
  221. package/dist/core/ingestion/route-extractors/constant-resolver.d.ts +83 -0
  222. package/dist/core/ingestion/route-extractors/constant-resolver.js +131 -0
  223. package/dist/core/ingestion/route-extractors/python-const-resolver.d.ts +75 -0
  224. package/dist/core/ingestion/route-extractors/python-const-resolver.js +299 -0
  225. package/dist/core/ingestion/scope-extractor.js +306 -43
  226. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +71 -2
  227. package/dist/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.d.ts +7 -7
  228. package/dist/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.js +8 -7
  229. package/dist/core/ingestion/scope-resolution/graph-bridge/edges.d.ts +3 -1
  230. package/dist/core/ingestion/scope-resolution/graph-bridge/edges.js +6 -0
  231. package/dist/core/ingestion/scope-resolution/graph-bridge/ids.js +1 -0
  232. package/dist/core/ingestion/scope-resolution/graph-bridge/node-lookup.js +4 -1
  233. package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.js +7 -2
  234. package/dist/core/ingestion/scope-resolution/passes/callable-value-flow.d.ts +47 -0
  235. package/dist/core/ingestion/scope-resolution/passes/callable-value-flow.js +1138 -0
  236. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +16 -4
  237. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +97 -1
  238. package/dist/core/ingestion/scope-resolution/passes/property-dispatch.d.ts +60 -0
  239. package/dist/core/ingestion/scope-resolution/passes/property-dispatch.js +111 -0
  240. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +4 -3
  241. package/dist/core/ingestion/scope-resolution/pipeline/phase.d.ts +5 -0
  242. package/dist/core/ingestion/scope-resolution/pipeline/phase.js +25 -38
  243. package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +6 -0
  244. package/dist/core/ingestion/scope-resolution/pipeline/run.js +147 -35
  245. package/dist/core/ingestion/scope-resolution/resolution-outcome.d.ts +1 -1
  246. package/dist/core/ingestion/scope-resolution/scope/walkers.js +73 -44
  247. package/dist/core/ingestion/taint/typescript-model.js +34 -2
  248. package/dist/core/ingestion/tree-sitter-queries.d.ts +2 -2
  249. package/dist/core/ingestion/tree-sitter-queries.js +23 -2
  250. package/dist/core/ingestion/type-extractors/jvm.js +17 -2
  251. package/dist/core/ingestion/utils/ast-helpers.d.ts +1 -0
  252. package/dist/core/ingestion/utils/ast-helpers.js +153 -2
  253. package/dist/core/ingestion/utils/callable-flow-captures.d.ts +94 -0
  254. package/dist/core/ingestion/utils/callable-flow-captures.js +878 -0
  255. package/dist/core/ingestion/utils/line-base.d.ts +20 -0
  256. package/dist/core/ingestion/utils/line-base.js +20 -0
  257. package/dist/core/ingestion/utils/symbol-labels.d.ts +21 -0
  258. package/dist/core/ingestion/utils/symbol-labels.js +45 -0
  259. package/dist/core/ingestion/workers/parse-worker.d.ts +34 -0
  260. package/dist/core/ingestion/workers/parse-worker.js +40 -5
  261. package/dist/core/ingestion/workers/result-merge.js +4 -0
  262. package/dist/core/ingestion/workers/worker-pool.d.ts +28 -0
  263. package/dist/core/ingestion/workers/worker-pool.js +128 -28
  264. package/dist/core/lbug/csv-generator.d.ts +15 -0
  265. package/dist/core/lbug/csv-generator.js +124 -27
  266. package/dist/core/lbug/cypher-escape.d.ts +20 -0
  267. package/dist/core/lbug/cypher-escape.js +20 -0
  268. package/dist/core/lbug/extension-load-error.d.ts +67 -0
  269. package/dist/core/lbug/extension-load-error.js +326 -0
  270. package/dist/core/lbug/extension-loader.d.ts +17 -2
  271. package/dist/core/lbug/extension-loader.js +38 -13
  272. package/dist/core/lbug/lbug-adapter.d.ts +203 -2
  273. package/dist/core/lbug/lbug-adapter.js +527 -86
  274. package/dist/core/lbug/lbug-config.d.ts +56 -2
  275. package/dist/core/lbug/lbug-config.js +267 -7
  276. package/dist/core/lbug/move-columns.d.ts +7 -0
  277. package/dist/core/lbug/move-columns.js +86 -0
  278. package/dist/core/lbug/native-check.d.ts +40 -0
  279. package/dist/core/lbug/native-check.js +90 -0
  280. package/dist/core/lbug/node-table-layout.d.ts +29 -0
  281. package/dist/core/lbug/node-table-layout.js +119 -0
  282. package/dist/core/lbug/pool-adapter.d.ts +23 -1
  283. package/dist/core/lbug/pool-adapter.js +115 -8
  284. package/dist/core/lbug/schema.d.ts +4 -3
  285. package/dist/core/lbug/schema.js +16 -16
  286. package/dist/core/lbug/sidecar-recovery.d.ts +172 -0
  287. package/dist/core/lbug/sidecar-recovery.js +418 -16
  288. package/dist/core/lbug/wal-checkpoint-driver.js +3 -0
  289. package/dist/core/move/artifact-download.d.ts +19 -0
  290. package/dist/core/move/artifact-download.js +179 -0
  291. package/dist/core/move/compiler-facts.d.ts +120 -0
  292. package/dist/core/move/compiler-facts.js +9 -0
  293. package/dist/core/move/consistency.d.ts +28 -0
  294. package/dist/core/move/consistency.js +98 -0
  295. package/dist/core/move/constants.d.ts +98 -0
  296. package/dist/core/move/constants.js +136 -0
  297. package/dist/core/move/discovery.d.ts +10 -0
  298. package/dist/core/move/discovery.js +22 -0
  299. package/dist/core/move/entry-points.d.ts +14 -0
  300. package/dist/core/move/entry-points.js +40 -0
  301. package/dist/core/move/facts-mapper.d.ts +75 -0
  302. package/dist/core/move/facts-mapper.js +534 -0
  303. package/dist/core/move/install-lock.d.ts +14 -0
  304. package/dist/core/move/install-lock.js +102 -0
  305. package/dist/core/move/install.d.ts +39 -0
  306. package/dist/core/move/install.js +391 -0
  307. package/dist/core/move/mcp-client.d.ts +115 -0
  308. package/dist/core/move/mcp-client.js +436 -0
  309. package/dist/core/move/move-ingest.d.ts +49 -0
  310. package/dist/core/move/move-ingest.js +320 -0
  311. package/dist/core/move/provision.d.ts +19 -0
  312. package/dist/core/move/provision.js +91 -0
  313. package/dist/core/move/release.d.ts +6 -0
  314. package/dist/core/move/release.js +10 -0
  315. package/dist/core/move/symbol-id.d.ts +35 -0
  316. package/dist/core/move/symbol-id.js +65 -0
  317. package/dist/core/move/type-parser.d.ts +11 -0
  318. package/dist/core/move/type-parser.js +38 -0
  319. package/dist/core/platform/capabilities.d.ts +0 -1
  320. package/dist/core/platform/capabilities.js +39 -8
  321. package/dist/core/run-analyze.d.ts +3 -3
  322. package/dist/core/run-analyze.js +1001 -184
  323. package/dist/core/search/fts-indexes.d.ts +31 -0
  324. package/dist/core/search/fts-indexes.js +73 -0
  325. package/dist/core/search/fts-schema.js +2 -0
  326. package/dist/core/tree-sitter/safe-parse.d.ts +8 -3
  327. package/dist/core/tree-sitter/safe-parse.js +21 -7
  328. package/dist/core/wiki/graph-queries.js +6 -5
  329. package/dist/core/wiki/llm-client.d.ts +8 -2
  330. package/dist/core/wiki/llm-client.js +41 -5
  331. package/dist/mcp/core/embedder.js +16 -2
  332. package/dist/mcp/http-transport.d.ts +5 -0
  333. package/dist/mcp/http-transport.js +19 -5
  334. package/dist/mcp/local/bean-metadata.d.ts +2 -0
  335. package/dist/mcp/local/bean-metadata.js +25 -0
  336. package/dist/mcp/local/line-display.d.ts +22 -0
  337. package/dist/mcp/local/line-display.js +3 -0
  338. package/dist/mcp/local/local-backend.d.ts +19 -1
  339. package/dist/mcp/local/local-backend.js +402 -155
  340. package/dist/mcp/local/pdg-impact.d.ts +5 -3
  341. package/dist/mcp/local/pdg-impact.js +5 -2
  342. package/dist/mcp/output-budget.d.ts +5 -0
  343. package/dist/mcp/output-budget.js +52 -0
  344. package/dist/mcp/read-only-policy.d.ts +12 -0
  345. package/dist/mcp/read-only-policy.js +102 -0
  346. package/dist/mcp/repository-policy.d.ts +34 -0
  347. package/dist/mcp/repository-policy.js +328 -0
  348. package/dist/mcp/resources.js +32 -5
  349. package/dist/mcp/server.d.ts +5 -2
  350. package/dist/mcp/server.js +29 -9
  351. package/dist/mcp/tools.d.ts +0 -8
  352. package/dist/mcp/tools.js +102 -5
  353. package/dist/server/analyze-job.d.ts +3 -0
  354. package/dist/server/analyze-job.js +17 -0
  355. package/dist/server/analyze-launch.d.ts +6 -0
  356. package/dist/server/analyze-launch.js +82 -6
  357. package/dist/server/analyze-worker-core.d.ts +2 -1
  358. package/dist/server/analyze-worker-core.js +5 -2
  359. package/dist/server/analyze-worker.js +29 -6
  360. package/dist/server/api.d.ts +30 -0
  361. package/dist/server/api.js +120 -34
  362. package/dist/server/mcp-http.d.ts +1 -1
  363. package/dist/server/mcp-http.js +4 -2
  364. package/dist/server/upload-ingest.js +2 -1
  365. package/dist/server/validation.js +2 -1
  366. package/dist/storage/git.d.ts +31 -0
  367. package/dist/storage/git.js +111 -1
  368. package/dist/storage/parse-cache.js +7 -1
  369. package/dist/storage/parsedfile-store.d.ts +8 -0
  370. package/dist/storage/parsedfile-store.js +165 -1
  371. package/dist/storage/repo-manager.d.ts +194 -2
  372. package/dist/storage/repo-manager.js +56 -1
  373. package/hooks/antigravity/gitnexus-antigravity-hook.cjs +50 -3
  374. package/hooks/claude/gitnexus-hook.cjs +59 -9
  375. package/hooks/claude/pre-tool-use.sh +0 -0
  376. package/hooks/claude/session-start.sh +0 -0
  377. package/package.json +21 -15
  378. package/scripts/cross-platform-tests.ts +56 -0
  379. package/scripts/ensure-fts.ts +43 -0
  380. package/scripts/install-duckdb-extension.mjs +85 -24
  381. package/scripts/run-cross-platform.ts +61 -6
  382. package/scripts/shard-arg.ts +30 -0
  383. package/scripts/sync-plugin-manifests.mjs +192 -0
  384. package/skills/gitnexus-lfg/README.md +55 -0
  385. package/skills/gitnexus-lfg/SKILL.md +86 -0
  386. package/skills/gitnexus-plan/README.md +142 -0
  387. package/skills/gitnexus-plan/SKILL.md +348 -0
  388. package/skills/gitnexus-plan/references/context-ledger.md +137 -0
  389. package/skills/gitnexus-plan/references/context-pack.md +126 -0
  390. package/skills/gitnexus-plan/references/evidence-provenance.md +272 -0
  391. package/skills/gitnexus-plan/references/pdg-slice.md +109 -0
  392. package/skills/gitnexus-plan/references/plan-template.md +201 -0
  393. package/skills/gitnexus-plan/scripts/evidence-provenance.mjs +2084 -0
  394. package/skills/gitnexus-review/SKILL.md +273 -0
  395. package/skills/gitnexus-review/ci-personas/ci-adversarial-lens.md +42 -0
  396. package/skills/gitnexus-review/ci-personas/ci-blast-radius-lens.md +39 -0
  397. package/skills/gitnexus-review/ci-personas/ci-correctness-lens.md +37 -0
  398. package/skills/gitnexus-review/ci-personas/ci-coverage-lens.md +40 -0
  399. package/skills/gitnexus-review/ci-personas/ci-critic-lens.md +42 -0
  400. package/skills/gitnexus-review/ci-personas/ci-security-lens.md +39 -0
  401. package/skills/gitnexus-work/README.md +71 -0
  402. package/skills/gitnexus-work/SKILL.md +269 -0
  403. package/skills/gitnexus-work/references/evidence-provenance.md +272 -0
  404. package/skills/gitnexus-work/scripts/evidence-provenance.mjs +2084 -0
  405. package/web/assets/ProcessFlowModal-B_bfBySy.js +22 -0
  406. package/web/assets/__vite-browser-external-bMKyYQkO.js +1 -0
  407. package/web/assets/agent-Bqxn-abU.js +626 -0
  408. package/web/assets/architecture-7EHR7CIX-6QZW5X65-BVWFvYz2.js +1 -0
  409. package/web/assets/architectureDiagram-UL44E2DR-CmG3uPWf.js +36 -0
  410. package/web/assets/{blockDiagram-7IZFK4PR-BkWhpzWB.js → blockDiagram-7IZFK4PR-DBefcjof.js} +7 -7
  411. package/web/assets/{c4Diagram-Y2BXMSZH-DtBvpzWD.js → c4Diagram-Y2BXMSZH-EjWnjEJl.js} +6 -6
  412. package/web/assets/chunk-2T2R6R2M-Bb6Sy3_j.js +4 -0
  413. package/web/assets/chunk-2UTLFMKG-CXlhF3Pt.js +1 -0
  414. package/web/assets/chunk-3SSMPTDK-rPsRM1x0.js +321 -0
  415. package/web/assets/chunk-4R4BOZG6-Be_F0JoX.js +159 -0
  416. package/web/assets/{chunk-6764PJDD-DpIS7hxD.js → chunk-6764PJDD-C7xxk063.js} +1 -1
  417. package/web/assets/chunk-67TQ5CYL-CA7OhIEf.js +124 -0
  418. package/web/assets/chunk-7FYTHRHK-CsmYIswB.js +32 -0
  419. package/web/assets/chunk-7J6CGLKN-BlKwgXKs.js +10 -0
  420. package/web/assets/chunk-7W6UQGC5-pyKGssV3.js +1 -0
  421. package/web/assets/chunk-C62D2QBJ-3gdL8EKc.js +1 -0
  422. package/web/assets/chunk-CEXFNPSA-CjwtMDkc.js +1 -0
  423. package/web/assets/{chunk-INKRHTLW-Nu4ri9P-.js → chunk-INKRHTLW-Ckl3_KkO.js} +13 -13
  424. package/web/assets/chunk-J5EP6P6S-L19oz55R.js +1 -0
  425. package/web/assets/{chunk-KGFNY3KK-JGMJvKCK.js → chunk-KGFNY3KK-BZeP-55u.js} +17 -17
  426. package/web/assets/chunk-KGYTTC2M-zm6n242I.js +1 -0
  427. package/web/assets/{chunk-KRXBNO2N-BW5o0KUN.js → chunk-KRXBNO2N-C-UNuKKf.js} +1 -1
  428. package/web/assets/{chunk-LCXTWHL2-CfLLCywY.js → chunk-LCXTWHL2-C162wE5z.js} +5 -5
  429. package/web/assets/chunk-LII3EMHJ-BL6lKJ_P.js +1 -0
  430. package/web/assets/chunk-LRIF4GLE-n59lhprL.js +1 -0
  431. package/web/assets/chunk-QA3QBVWF-DVXPOIxe.js +2 -0
  432. package/web/assets/chunk-RERM46MO-B5dAdWpP.js +1 -0
  433. package/web/assets/chunk-RG4AUYOV-ZNgyLFq6.js +206 -0
  434. package/web/assets/chunk-RKZBBQEN-MyIQbGgD.js +1 -0
  435. package/web/assets/chunk-RLI5ZMPA-CkXZ0y0U.js +1 -0
  436. package/web/assets/chunk-T2UQINTJ-BEyXhZWe.js +1 -0
  437. package/web/assets/chunk-UP6H54XL-BG-n7qDp.js +1 -0
  438. package/web/assets/chunk-UXSXWOXI-CW1_r5cS.js +1 -0
  439. package/web/assets/chunk-UY5QBCOK-ByqTVLvh.js +1 -0
  440. package/web/assets/chunk-VU6ZFW4Y-BLeasdkU.js +2 -0
  441. package/web/assets/{chunk-W44A43WB-JqMLIpR0.js → chunk-W44A43WB-BN8EQc_u.js} +1 -1
  442. package/web/assets/chunk-ZXARS5L4-C3v-4Fsw.js +1 -0
  443. package/web/assets/classDiagram-KGZ6W3CR-olpO3fMe.js +1 -0
  444. package/web/assets/classDiagram-v2-72OJOZXJ-olpO3fMe.js +1 -0
  445. package/web/assets/cose-bilkent-UX7MHV2Q-BmnZ7Mvj.js +1 -0
  446. package/web/assets/dagre-ND4H6XIP-CY4iIF7v.js +4 -0
  447. package/web/assets/diagram-3NCE3AQN-CO6QpzEE.js +43 -0
  448. package/web/assets/diagram-GF46GFSD-B2atbzmU.js +24 -0
  449. package/web/assets/diagram-HNR7UZ2L-DVRltWbL.js +3 -0
  450. package/web/assets/diagram-QXG6HAR7-DR6AHD2h.js +24 -0
  451. package/web/assets/diagram-WEQXMOUZ-DkpoMwTL.js +10 -0
  452. package/web/assets/erDiagram-L5TCEMPS-n8g_90bc.js +85 -0
  453. package/web/assets/eventmodeling-FCH6USID-MREXMVOE-CgFBM7e-.js +1 -0
  454. package/web/assets/flowDiagram-H6V6AXG4-SGMDiCkB.js +162 -0
  455. package/web/assets/ganttDiagram-JCBTUEKG-Cb3UBqRS.js +292 -0
  456. package/web/assets/gitGraph-WXDBUCRP-R675I2BI-DCJv7SII.js +1 -0
  457. package/web/assets/gitGraphDiagram-S2ZK5IYY-Bs9CR7tq.js +106 -0
  458. package/web/assets/index-CX_fADmQ.css +2 -0
  459. package/web/assets/index-D1Z3dGGz.js +575 -0
  460. package/web/assets/info-J43DQDTF-KCYPFFUO-BfNy-9Nd.js +1 -0
  461. package/web/assets/infoDiagram-3YFTVSEB-T368on4D.js +2 -0
  462. package/web/assets/{ishikawaDiagram-BNXS4ZKH-DfYpxnE5.js → ishikawaDiagram-BNXS4ZKH-XpjJRU9h.js} +6 -6
  463. package/web/assets/journeyDiagram-M6C3CM3L-C6wjooas.js +139 -0
  464. package/web/assets/kanban-definition-75IXJCU3-5_UVY79g.js +89 -0
  465. package/web/assets/{katex-K3KEBU37-CTc5BslQ.js → katex-K3KEBU37-qRQBy-EZ.js} +2 -2
  466. package/web/assets/{mindmap-definition-2TDM6QVE-DypvbOMQ.js → mindmap-definition-2TDM6QVE-8xDLzk2e.js} +29 -29
  467. package/web/assets/node-Ct_zna8U.js +8 -0
  468. package/web/assets/packet-YPE3B663-LP52Z2RK-D4qkeG9k.js +1 -0
  469. package/web/assets/pie-LRSECV5Y-TCRJHUBD-B_yMKyQa.js +1 -0
  470. package/web/assets/pieDiagram-CU6KROY3-Bv4LpAbr.js +30 -0
  471. package/web/assets/preload-helper-Czpn1I53.js +1 -0
  472. package/web/assets/{quadrantDiagram-VICAPDV7-yXSZ2lLz.js → quadrantDiagram-VICAPDV7-DR54jOeR.js} +6 -6
  473. package/web/assets/radar-GUYGQ44K-RDLRG3WG-DeD6yN_0.js +1 -0
  474. package/web/assets/requirementDiagram-JXO7QTGE-PEkg7sKC.js +84 -0
  475. package/web/assets/rolldown-runtime-QTnfLwEv.js +1 -0
  476. package/web/assets/sankeyDiagram-URQDO5SZ-DDWEtjcs.js +40 -0
  477. package/web/assets/sdk-CzHHH8tD.js +37 -0
  478. package/web/assets/sequenceDiagram-VS2MUI6T-Bv-uwANx.js +162 -0
  479. package/web/assets/src-JV30wyV8.js +1 -0
  480. package/web/assets/stateDiagram-7D4R322I-DVNjmkEq.js +1 -0
  481. package/web/assets/stateDiagram-v2-36443NZ5-CyN4Fh7_.js +1 -0
  482. package/web/assets/timeline-definition-O6YCAMPW-Qmsuo7qS.js +120 -0
  483. package/web/assets/treeView-BLDUP644-QA4HXRO3-CDuSYnd4.js +1 -0
  484. package/web/assets/treemap-LRROVOQU-LLAWBHMP-imazucyJ.js +1 -0
  485. package/web/assets/vennDiagram-MWXL3ELB-BD1iK1SX.js +34 -0
  486. package/web/assets/wardley-L42UT6IY-5TKZOOLJ-DqrT9tB8.js +1 -0
  487. package/web/assets/wardleyDiagram-CUQ6CDDI-D6TzLVob.js +78 -0
  488. package/web/assets/{xychartDiagram-N2JHSOCM-BOLm0eNm.js → xychartDiagram-N2JHSOCM-CoZ_Tits.js} +6 -6
  489. package/web/index.html +18 -10
  490. package/skills/gitnexus-pr-review.md +0 -163
  491. package/web/assets/agent-Dli_x7bU.js +0 -660
  492. package/web/assets/architecture-7EHR7CIX-6QZW5X65-DxJw65fT.js +0 -1
  493. package/web/assets/architectureDiagram-UL44E2DR-dEkqUZN9.js +0 -36
  494. package/web/assets/chunk-3SSMPTDK-B4_etUhr.js +0 -321
  495. package/web/assets/chunk-67TQ5CYL-4sNPJU1q.js +0 -124
  496. package/web/assets/chunk-7W6UQGC5-KyEG0HQg.js +0 -1
  497. package/web/assets/chunk-KGYTTC2M-CWC_c3H9.js +0 -161
  498. package/web/assets/chunk-LII3EMHJ-D-jm-dLa.js +0 -1
  499. package/web/assets/chunk-QA3QBVWF-BgRW3SXd.js +0 -2
  500. package/web/assets/chunk-RG4AUYOV-CqWzWsV3.js +0 -206
  501. package/web/assets/chunk-T2UQINTJ-DSBqkzH7.js +0 -1
  502. package/web/assets/chunk-UY5QBCOK-DyF0vyGd.js +0 -1
  503. package/web/assets/chunk-ZXARS5L4-CxN8oiwI.js +0 -1
  504. package/web/assets/classDiagram-KGZ6W3CR-CMne4tG9.js +0 -1
  505. package/web/assets/classDiagram-v2-72OJOZXJ-CMne4tG9.js +0 -1
  506. package/web/assets/cose-bilkent-UX7MHV2Q-D71wNYRJ.js +0 -1
  507. package/web/assets/dagre-ND4H6XIP-Co5rRx9X.js +0 -4
  508. package/web/assets/diagram-3NCE3AQN-DBp4O00j.js +0 -43
  509. package/web/assets/diagram-GF46GFSD-6N_8yS0i.js +0 -24
  510. package/web/assets/diagram-HNR7UZ2L-DFCfd5LI.js +0 -3
  511. package/web/assets/diagram-QXG6HAR7-Di093rFg.js +0 -24
  512. package/web/assets/diagram-WEQXMOUZ-DZWi4SUN.js +0 -10
  513. package/web/assets/erDiagram-L5TCEMPS-8X1y6R4Y.js +0 -85
  514. package/web/assets/eventmodeling-FCH6USID-MREXMVOE-BQm9QzEa.js +0 -1
  515. package/web/assets/flowDiagram-H6V6AXG4-CMpY8Ufn.js +0 -162
  516. package/web/assets/ganttDiagram-JCBTUEKG-D3iL2Aet.js +0 -292
  517. package/web/assets/gitGraph-WXDBUCRP-R675I2BI-Cr_Bm2Nb.js +0 -1
  518. package/web/assets/gitGraphDiagram-S2ZK5IYY-CPIDgxGm.js +0 -106
  519. package/web/assets/index-KctnI3Vq.js +0 -635
  520. package/web/assets/index-_lgn7hs5.css +0 -2
  521. package/web/assets/info-J43DQDTF-KCYPFFUO-Cs4p2oyk.js +0 -1
  522. package/web/assets/infoDiagram-3YFTVSEB-BaLMZkUg.js +0 -2
  523. package/web/assets/journeyDiagram-M6C3CM3L-CvArszLo.js +0 -139
  524. package/web/assets/kanban-definition-75IXJCU3-CpfVLLf9.js +0 -89
  525. package/web/assets/packet-YPE3B663-LP52Z2RK-CKCrztD2.js +0 -1
  526. package/web/assets/pie-LRSECV5Y-TCRJHUBD-BB7pHqoD.js +0 -1
  527. package/web/assets/pieDiagram-CU6KROY3-s27EFWgP.js +0 -30
  528. package/web/assets/radar-GUYGQ44K-RDLRG3WG-DhoTOcuK.js +0 -1
  529. package/web/assets/requirementDiagram-JXO7QTGE-CWhqInuf.js +0 -84
  530. package/web/assets/sankeyDiagram-URQDO5SZ-1qEjlFIB.js +0 -40
  531. package/web/assets/sequenceDiagram-VS2MUI6T-RyTlZ5cR.js +0 -162
  532. package/web/assets/stateDiagram-7D4R322I-DTjVfX4A.js +0 -1
  533. package/web/assets/stateDiagram-v2-36443NZ5-xY9_k9_Q.js +0 -1
  534. package/web/assets/timeline-definition-O6YCAMPW-DrrqLYAf.js +0 -120
  535. package/web/assets/treeView-BLDUP644-QA4HXRO3-ODehyGKL.js +0 -1
  536. package/web/assets/treemap-LRROVOQU-LLAWBHMP-BfpgC-7o.js +0 -1
  537. package/web/assets/vennDiagram-MWXL3ELB-B0RkWAim.js +0 -34
  538. package/web/assets/wardley-L42UT6IY-5TKZOOLJ-CNeL6VHE.js +0 -1
  539. package/web/assets/wardleyDiagram-CUQ6CDDI-boUxiQ5F.js +0 -78
@@ -6,15 +6,17 @@ import { finished } from 'stream/promises';
6
6
  import path from 'path';
7
7
  import lbug from '@ladybugdb/core';
8
8
  import { closeQueryResults } from './query-result-utils.js';
9
+ import { escapeCypherString } from './cypher-escape.js';
9
10
  import { withConnLock } from './conn-lock.js';
10
11
  import { isWalDriverActive } from './wal-driver-state.js';
11
12
  import { NODE_TABLES, REL_TABLE_NAME, SCHEMA_QUERIES, EMBEDDING_TABLE_NAME, CREATE_VECTOR_INDEX_QUERY, STALE_HASH_SENTINEL, } from './schema.js';
12
13
  import { streamAllCSVsToDisk } from './csv-generator.js';
13
14
  import { getNodeLabel as deriveNodeLabel } from './rel-pair-routing.js';
14
- import { extensionManager } from './extension-loader.js';
15
- import { classifyDeleteAllError, closeLbugConnection, isDbBusyError, isOpenRetryExhausted, isWalCorruptionError, openLbugConnection, toNativeSafePath, resolveNativeSafeStorageDir, WAL_RECOVERY_SUGGESTION, waitForWindowsHandleRelease, } from './lbug-config.js';
16
- import { finalizeLbugSidecarsAfterClose, inspectLbugSidecars, isMissingShadowSidecarError, isReadOnlyShadowReplayError, preflightLbugSidecars, quarantineWalForMissingShadow, renameFailureMessage, shadowSidecarRecoveryMessage, } from './sidecar-recovery.js';
17
- import { isVectorExtensionSupportedByPlatform } from '../platform/capabilities.js';
15
+ import { EMBEDDABLE_LABELS } from '../embeddings/types.js';
16
+ import { getNodeTableColumnNames } from './node-table-layout.js';
17
+ import { extensionManager, resolveAnalyzeInstallPolicy, } from './extension-loader.js';
18
+ import { classifyDeleteAllError, closeLbugConnection, HANDLE_RELEASE_PROBE_ATTEMPTS, HANDLE_RELEASE_PROBE_DELAY_MS, isDbBusyError, isOpenRetryExhausted, isWalCorruptionError, openLbugConnection, sleep, toNativeSafePath, resolveNativeSafeStorageDir, WAL_RECOVERY_SUGGESTION, waitForWindowsHandleRelease, } from './lbug-config.js';
19
+ import { finalizeLbugSidecarsAfterClose, guardWalQuarantine, isMissingShadowSidecarError, isReadOnlyShadowReplayError, lbugLockRemediation, preflightLbugSidecars, quarantineWalForMissingShadow, renameFailureMessage, shadowSidecarRecoveryMessage, } from './sidecar-recovery.js';
18
20
  import { logger } from '../logger.js';
19
21
  /**
20
22
  * Split a relationship CSV into per-label-pair files on disk.
@@ -409,23 +411,14 @@ const queryAndDrain = async (targetConn, cypher) => {
409
411
  };
410
412
  const READ_ONLY_SHADOW_REPLAY_PROBE = 'MATCH (n) RETURN n LIMIT 1';
411
413
  /**
412
- * Reject the quarantine path when the orphan WAL is too large to safely
413
- * discard (>TINY_ORPHAN_WAL_BYTES). Mirrors the preflight policy at
414
- * sidecar-recovery.ts:153-160 ("warn, do not quarantine"). Symmetric across
415
- * read-only and writable recovery paths (PR #1747 review D2).
416
- *
417
- * Throws shadowSidecarRecoveryMessage immediately when the WAL is large,
418
- * preserving the uncheckpointed pages for explicit operator recovery.
419
- * Returns silently when the WAL is absent, tiny, or in any other state
420
- * where the existing recovery path is safe to proceed.
414
+ * Serve-side entry to the shared WAL-quarantine safety gate. Refuses (throws)
415
+ * when the `.shadow` is present on disk or the orphan WAL is too large to
416
+ * safely discard; returns silently otherwise. The policy itself lives in
417
+ * `guardWalQuarantine` (sidecar-recovery.ts) so serve and the MCP pool share
418
+ * one source of truth (PR #1747 review D2; issue #2382 review, Finding B).
421
419
  */
422
420
  const refuseLargeWalQuarantine = async (dbPath, mode, triggeringErr) => {
423
- const state = await inspectLbugSidecars(dbPath);
424
- if (state.kind === 'orphan-wal') {
425
- logger.warn(`GitNexus: refusing to quarantine large WAL (${state.walBytes} bytes) at ${dbPath}.wal during ${mode} recovery; ` +
426
- 'manual recovery required — run `gitnexus analyze --force <repo-path> --index-only`.');
427
- throw new Error(shadowSidecarRecoveryMessage(dbPath, triggeringErr));
428
- }
421
+ await guardWalQuarantine(dbPath, mode, triggeringErr, logger);
429
422
  };
430
423
  const reopenReadOnlyAfterMissingShadow = async (dbPath, err) => {
431
424
  await refuseLargeWalQuarantine(dbPath, 'read-only', err);
@@ -673,7 +666,8 @@ const doInitLbug = async (dbPath, readOnly = false) => {
673
666
  const realPath = await fs.realpath(dbPath);
674
667
  const parentDir = path.dirname(dbPath);
675
668
  const realParent = await fs.realpath(parentDir);
676
- if (!realPath.startsWith(realParent + path.sep) && realPath !== realParent) {
669
+ const safePrefix = realParent.endsWith(path.sep) ? realParent : realParent + path.sep;
670
+ if (!realPath.startsWith(safePrefix) && realPath !== realParent) {
677
671
  throw new Error(`Refusing to delete ${dbPath}: resolved path ${realPath} is outside storage directory`);
678
672
  }
679
673
  // Old-style directory database or empty leftover - remove it
@@ -1091,8 +1085,49 @@ const BACKTICK_TABLES = new Set([
1091
1085
  const escapeTableName = (table) => {
1092
1086
  return BACKTICK_TABLES.has(table) ? `\`${table}\`` : table;
1093
1087
  };
1094
- /** Fallback: insert relationships one-by-one if COPY fails */
1095
- const fallbackRelationshipInserts = async (validRelLines, validTables, getNodeLabel) => {
1088
+ /**
1089
+ * Format one JS value as a Cypher literal for the adapter's string-built
1090
+ * statements: NULL/undefined → `NULL`, numbers pass through unquoted,
1091
+ * everything else becomes a single-quoted string literal escaped via
1092
+ * {@link escapeCypherString} (backslashes first, then quotes).
1093
+ *
1094
+ * Replaces three per-function closures that used SQL-style `''` doubling —
1095
+ * LadybugDB REJECTS doubling, so every value containing a quote made the
1096
+ * whole statement a parser error, invisible wherever the call site swallowed
1097
+ * per-row failures (#2409 escaping sweep, completed for tri-review
1098
+ * 4669518496 P2-2). Those closures also rewrote literal `\n`/`\r` into
1099
+ * two-character escape sequences; raw LF/CR are legal inside LadybugDB
1100
+ * single-quoted literals (live-probed on @ladybugdb/core 0.18.0), so the
1101
+ * replaces are gone and content now round-trips byte-identical.
1102
+ */
1103
+ const formatCypherValue = (v) => {
1104
+ if (v === null || v === undefined)
1105
+ return 'NULL';
1106
+ if (typeof v === 'number')
1107
+ return String(v);
1108
+ return `'${escapeCypherString(String(v))}'`;
1109
+ };
1110
+ const formatCypherStringArray = (value) => {
1111
+ const items = Array.isArray(value)
1112
+ ? value.filter((item) => typeof item === 'string')
1113
+ : [];
1114
+ return `[${items.map(formatCypherValue).join(', ')}]`;
1115
+ };
1116
+ /**
1117
+ * Fallback: insert relationships one-by-one if COPY fails.
1118
+ *
1119
+ * Exported for the quoted-id round-trip tests in
1120
+ * `test/integration/lbug-core-adapter.test.ts` (the `DELETE_FILES_CHUNK_SIZE`
1121
+ * exported-for-tests precedent); production callers stay in this module.
1122
+ * Bails silently when the adapter singleton is closed.
1123
+ *
1124
+ * KNOWN PRE-EXISTING NARROWING (distinct from the `''` escaping bug, NOT
1125
+ * fixed here): the row regex below matches CSV fields with `[^"]*`, so an id
1126
+ * containing a double quote (CSV-escaped as `""`) never matches and the edge
1127
+ * is skipped. Tracked as part of the quote-in-id divergence documented in
1128
+ * `rel-pair-routing.ts`.
1129
+ */
1130
+ export const fallbackRelationshipInserts = async (validRelLines, validTables, getNodeLabel) => {
1096
1131
  if (!conn)
1097
1132
  return;
1098
1133
  const escapeLabel = (label) => {
@@ -1111,11 +1146,10 @@ const fallbackRelationshipInserts = async (validRelLines, validTables, getNodeLa
1111
1146
  continue;
1112
1147
  const confidence = parseFloat(confidenceStr) || 1.0;
1113
1148
  const step = parseInt(stepStr) || 0;
1114
- const esc = (s) => s.replace(/'/g, "''").replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/\r/g, '\\r');
1115
1149
  await queryAndDrain(conn, `
1116
- MATCH (a:${escapeLabel(fromLabel)} {id: '${esc(fromId)}' }),
1117
- (b:${escapeLabel(toLabel)} {id: '${esc(toId)}' })
1118
- CREATE (a)-[:${REL_TABLE_NAME} {type: '${esc(relType)}', confidence: ${confidence}, reason: '${esc(reason)}', step: ${step}}]->(b)
1150
+ MATCH (a:${escapeLabel(fromLabel)} {id: ${formatCypherValue(fromId)} }),
1151
+ (b:${escapeLabel(toLabel)} {id: ${formatCypherValue(toId)} })
1152
+ CREATE (a)-[:${REL_TABLE_NAME} {type: ${formatCypherValue(relType)}, confidence: ${confidence}, reason: ${formatCypherValue(reason)}, step: ${step}}]->(b)
1119
1153
  `);
1120
1154
  }
1121
1155
  catch {
@@ -1131,8 +1165,13 @@ const TABLES_WITH_EXPORTED = new Set([
1131
1165
  'Method',
1132
1166
  'CodeElement',
1133
1167
  ]);
1168
+ const copyColumns = (columns) => columns.join(', ');
1134
1169
  export const getCopyQuery = (table, filePath) => {
1135
1170
  const t = escapeTableName(table);
1171
+ const layoutColumns = getNodeTableColumnNames(table);
1172
+ if (layoutColumns) {
1173
+ return `COPY ${t}(${copyColumns(layoutColumns)}) FROM "${filePath}" ${COPY_CSV_OPTS}`;
1174
+ }
1136
1175
  if (table === 'File') {
1137
1176
  return `COPY ${t}(id, name, filePath, content) FROM "${filePath}" ${COPY_CSV_OPTS}`;
1138
1177
  }
@@ -1160,6 +1199,9 @@ export const getCopyQuery = (table, filePath) => {
1160
1199
  // `calleeIds` is its SOUND parallel (space-joined resolved callee ids, #2227).
1161
1200
  return `COPY ${t}(id, filePath, startLine, endLine, text, callees, calleeIds) FROM "${filePath}" ${COPY_CSV_OPTS}`;
1162
1201
  }
1202
+ if (table === 'Class') {
1203
+ return `COPY ${t}(id, name, filePath, startLine, endLine, isExported, content, description, frameworkAnnotations) FROM "${filePath}" ${COPY_CSV_OPTS}`;
1204
+ }
1163
1205
  if (table === 'Method') {
1164
1206
  return `COPY ${t}(id, name, filePath, startLine, endLine, isExported, content, description, parameterCount, returnType) FROM "${filePath}" ${COPY_CSV_OPTS}`;
1165
1207
  }
@@ -1186,52 +1228,53 @@ export const insertNodeToLbug = async (label, properties, dbPath) => {
1186
1228
  throw new Error('LadybugDB not initialized. Provide dbPath or call initLbug first.');
1187
1229
  }
1188
1230
  try {
1189
- const escapeValue = (v) => {
1190
- if (v === null || v === undefined)
1191
- return 'NULL';
1192
- if (typeof v === 'number')
1193
- return String(v);
1194
- // Escape backslashes first (for Windows paths), then single quotes
1195
- return `'${String(v).replace(/\\/g, '\\\\').replace(/'/g, "''").replace(/\n/g, '\\n').replace(/\r/g, '\\r')}'`;
1196
- };
1231
+ // Values go through the module-scope formatCypherValue — the old local
1232
+ // closure used `''` doubling, which LadybugDB rejects (#2409 escaping
1233
+ // sweep, tri-review 4669518496 P2-2).
1197
1234
  // Build INSERT query based on node type
1198
1235
  const t = escapeTableName(label);
1199
1236
  let query;
1200
1237
  if (label === 'File') {
1201
- query = `CREATE (n:File {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}, content: ${escapeValue(properties.content || '')}})`;
1238
+ query = `CREATE (n:File {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, content: ${formatCypherValue(properties.content || '')}})`;
1202
1239
  }
1203
1240
  else if (label === 'Folder') {
1204
- query = `CREATE (n:Folder {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}})`;
1241
+ query = `CREATE (n:Folder {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}})`;
1205
1242
  }
1206
1243
  else if (label === 'Section') {
1207
1244
  const descPart = properties.description
1208
- ? `, description: ${escapeValue(properties.description)}`
1245
+ ? `, description: ${formatCypherValue(properties.description)}`
1209
1246
  : '';
1210
- 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}})`;
1247
+ query = `CREATE (n:Section {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, level: ${properties.level || 1}, content: ${formatCypherValue(properties.content || '')}${descPart}})`;
1211
1248
  }
1212
1249
  else if (label === 'BasicBlock') {
1213
1250
  // Taint/PDG substrate (issue #2080) — no name column. `calleeIds` (#2227)
1214
1251
  // is the sound resolved-id parallel to the leaf-name `callees` set.
1215
- query = `CREATE (n:BasicBlock {id: ${escapeValue(properties.id)}, filePath: ${escapeValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, text: ${escapeValue(properties.text || '')}, callees: ${escapeValue(properties.callees || '')}, calleeIds: ${escapeValue(properties.calleeIds || '')}})`;
1252
+ query = `CREATE (n:BasicBlock {id: ${formatCypherValue(properties.id)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, text: ${formatCypherValue(properties.text || '')}, callees: ${formatCypherValue(properties.callees || '')}, calleeIds: ${formatCypherValue(properties.calleeIds || '')}})`;
1253
+ }
1254
+ else if (label === 'Class') {
1255
+ const descPart = properties.description
1256
+ ? `, description: ${formatCypherValue(properties.description)}`
1257
+ : '';
1258
+ query = `CREATE (n:Class {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, isExported: ${!!properties.isExported}, content: ${formatCypherValue(properties.content || '')}${descPart}, frameworkAnnotations: ${formatCypherStringArray(properties.frameworkAnnotations)}})`;
1216
1259
  }
1217
1260
  else if (TABLES_WITH_EXPORTED.has(label)) {
1218
1261
  const descPart = properties.description
1219
- ? `, description: ${escapeValue(properties.description)}`
1262
+ ? `, description: ${formatCypherValue(properties.description)}`
1220
1263
  : '';
1221
- 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}})`;
1264
+ query = `CREATE (n:${t} {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, isExported: ${!!properties.isExported}, content: ${formatCypherValue(properties.content || '')}${descPart}})`;
1222
1265
  }
1223
1266
  else if (label === 'Property') {
1224
1267
  const descPart = properties.description
1225
- ? `, description: ${escapeValue(properties.description)}`
1268
+ ? `, description: ${formatCypherValue(properties.description)}`
1226
1269
  : '';
1227
- query = `CREATE (n:${t} {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, content: ${escapeValue(properties.content || '')}${descPart}, declaredType: ${escapeValue(properties.declaredType || '')}})`;
1270
+ query = `CREATE (n:${t} {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, content: ${formatCypherValue(properties.content || '')}${descPart}, declaredType: ${formatCypherValue(properties.declaredType || '')}})`;
1228
1271
  }
1229
1272
  else {
1230
1273
  // Multi-language tables (Struct, Impl, Trait, Macro, etc.) — no isExported
1231
1274
  const descPart = properties.description
1232
- ? `, description: ${escapeValue(properties.description)}`
1275
+ ? `, description: ${formatCypherValue(properties.description)}`
1233
1276
  : '';
1234
- query = `CREATE (n:${t} {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, content: ${escapeValue(properties.content || '')}${descPart}})`;
1277
+ query = `CREATE (n:${t} {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, content: ${formatCypherValue(properties.content || '')}${descPart}})`;
1235
1278
  }
1236
1279
  // Use per-query connection if dbPath provided (avoids lock conflicts)
1237
1280
  if (targetDbPath) {
@@ -1266,14 +1309,10 @@ export const insertNodeToLbug = async (label, properties, dbPath) => {
1266
1309
  export const batchInsertNodesToLbug = async (nodes, dbPath) => {
1267
1310
  if (nodes.length === 0)
1268
1311
  return { inserted: 0, failed: 0 };
1269
- const escapeValue = (v) => {
1270
- if (v === null || v === undefined)
1271
- return 'NULL';
1272
- if (typeof v === 'number')
1273
- return String(v);
1274
- // Escape backslashes first (for Windows paths), then single quotes, then newlines
1275
- return `'${String(v).replace(/\\/g, '\\\\').replace(/'/g, "''").replace(/\n/g, '\\n').replace(/\r/g, '\\r')}'`;
1276
- };
1312
+ // Values go through the module-scope formatCypherValue — the old local
1313
+ // closure used `''` doubling, which LadybugDB rejects; the per-node catch
1314
+ // below counted every quoted value as a silent `failed` (#2409 escaping
1315
+ // sweep, tri-review 4669518496 P2-2).
1277
1316
  // Open a single connection for all inserts
1278
1317
  const tempHandle = await openLbugConnection(lbug, dbPath);
1279
1318
  const tempConn = tempHandle.conn;
@@ -1286,39 +1325,45 @@ export const batchInsertNodesToLbug = async (nodes, dbPath) => {
1286
1325
  // Use MERGE instead of CREATE for upsert behavior (handles duplicates gracefully)
1287
1326
  const t = escapeTableName(label);
1288
1327
  if (label === 'File') {
1289
- query = `MERGE (n:File {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}, n.content = ${escapeValue(properties.content || '')}`;
1328
+ query = `MERGE (n:File {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.content = ${formatCypherValue(properties.content || '')}`;
1290
1329
  }
1291
1330
  else if (label === 'Folder') {
1292
- query = `MERGE (n:Folder {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}`;
1331
+ query = `MERGE (n:Folder {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}`;
1293
1332
  }
1294
1333
  else if (label === 'Section') {
1295
1334
  const descPart = properties.description
1296
- ? `, n.description = ${escapeValue(properties.description)}`
1335
+ ? `, n.description = ${formatCypherValue(properties.description)}`
1297
1336
  : '';
1298
- 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}`;
1337
+ query = `MERGE (n:Section {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.level = ${properties.level || 1}, n.content = ${formatCypherValue(properties.content || '')}${descPart}`;
1299
1338
  }
1300
1339
  else if (label === 'BasicBlock') {
1301
1340
  // Taint/PDG substrate (issue #2080) — no name column. `calleeIds`
1302
1341
  // (#2227) is the sound resolved-id parallel to the `callees` set.
1303
- query = `MERGE (n:BasicBlock {id: ${escapeValue(properties.id)}}) SET n.filePath = ${escapeValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.text = ${escapeValue(properties.text || '')}, n.callees = ${escapeValue(properties.callees || '')}, n.calleeIds = ${escapeValue(properties.calleeIds || '')}`;
1342
+ query = `MERGE (n:BasicBlock {id: ${formatCypherValue(properties.id)}}) SET n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.text = ${formatCypherValue(properties.text || '')}, n.callees = ${formatCypherValue(properties.callees || '')}, n.calleeIds = ${formatCypherValue(properties.calleeIds || '')}`;
1343
+ }
1344
+ else if (label === 'Class') {
1345
+ const descPart = properties.description
1346
+ ? `, n.description = ${formatCypherValue(properties.description)}`
1347
+ : '';
1348
+ query = `MERGE (n:Class {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.isExported = ${!!properties.isExported}, n.content = ${formatCypherValue(properties.content || '')}${descPart}, n.frameworkAnnotations = ${formatCypherStringArray(properties.frameworkAnnotations)}`;
1304
1349
  }
1305
1350
  else if (TABLES_WITH_EXPORTED.has(label)) {
1306
1351
  const descPart = properties.description
1307
- ? `, n.description = ${escapeValue(properties.description)}`
1352
+ ? `, n.description = ${formatCypherValue(properties.description)}`
1308
1353
  : '';
1309
- 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}`;
1354
+ query = `MERGE (n:${t} {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.isExported = ${!!properties.isExported}, n.content = ${formatCypherValue(properties.content || '')}${descPart}`;
1310
1355
  }
1311
1356
  else if (label === 'Property') {
1312
1357
  const descPart = properties.description
1313
- ? `, n.description = ${escapeValue(properties.description)}`
1358
+ ? `, n.description = ${formatCypherValue(properties.description)}`
1314
1359
  : '';
1315
- 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.content = ${escapeValue(properties.content || '')}${descPart}, n.declaredType = ${escapeValue(properties.declaredType || '')}`;
1360
+ query = `MERGE (n:${t} {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.content = ${formatCypherValue(properties.content || '')}${descPart}, n.declaredType = ${formatCypherValue(properties.declaredType || '')}`;
1316
1361
  }
1317
1362
  else {
1318
1363
  const descPart = properties.description
1319
- ? `, n.description = ${escapeValue(properties.description)}`
1364
+ ? `, n.description = ${formatCypherValue(properties.description)}`
1320
1365
  : '';
1321
- 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.content = ${escapeValue(properties.content || '')}${descPart}`;
1366
+ query = `MERGE (n:${t} {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.content = ${formatCypherValue(properties.content || '')}${descPart}`;
1322
1367
  }
1323
1368
  await queryAndDrain(tempConn, query);
1324
1369
  inserted++;
@@ -1749,7 +1794,142 @@ export const closeLbug = async () => {
1749
1794
  vectorIndexEnsured = false;
1750
1795
  ensuredFTSIndexes.clear();
1751
1796
  };
1797
+ /**
1798
+ * Thrown by {@link wipeLbugDbFiles} when a data-bearing member of the
1799
+ * LadybugDB file family is still present after the bounded
1800
+ * remove-and-verify retries (#2409, tri-review 4669518496 P2-4), and by
1801
+ * run-analyze's dirty-recovery block when the crashed run's sidecars can
1802
+ * neither be parked nor removed (this shipping review, FIX 1 — same lock
1803
+ * class, same remediation, and the CLI already renders this type).
1804
+ *
1805
+ * Classify by TYPE (`err instanceof LbugWipeError`) — the repo norm from
1806
+ * #2385 — never by message text. The MESSAGE is nonetheless fully
1807
+ * self-contained (headline + blocked paths + remediation) because
1808
+ * `gitnexus serve` forwards only `err.message` over worker IPC
1809
+ * (analyze-worker-core.ts), so the serve surface has nothing but this
1810
+ * string to show the user. The holder framing deliberately covers the
1811
+ * own-process case (FIX 2, finder A): the blocking handle is often a
1812
+ * lingering one from THIS process's just-closed DB or a transient AV scan
1813
+ * — not necessarily another process — so an immediate re-run often
1814
+ * succeeds.
1815
+ */
1816
+ export class LbugWipeError extends Error {
1817
+ /** Paths still present (or unverifiable) after all retries. */
1818
+ survivors;
1819
+ constructor(survivors, options) {
1820
+ super(`${options?.headline ??
1821
+ `Failed to remove the LadybugDB index files — still present after ` +
1822
+ `${HANDLE_RELEASE_PROBE_ATTEMPTS} attempts:`}\n` +
1823
+ survivors.map((p) => ` - ${p}`).join('\n') +
1824
+ `\nThe blocking handle may be another process, a lingering handle from this ` +
1825
+ `process's just-closed database, or an antivirus scan — an immediate re-run ` +
1826
+ `often succeeds. If it persists, ${lbugLockRemediation('re-run the analyze')}.`);
1827
+ this.name = 'LbugWipeError';
1828
+ this.survivors = survivors;
1829
+ }
1830
+ }
1831
+ /**
1832
+ * Remove the LadybugDB file family and VERIFY each member is really gone.
1833
+ *
1834
+ * Owns the canonical 4-file family list — `<lbugPath>`, `.wal`, `.shadow`,
1835
+ * `.lock` — so run-analyze's two wipe sites (full rebuild + the #2409
1836
+ * escalation valve) can never drift apart. `.shadow` is included because a
1837
+ * checkpoint-in-flight crash leaves a shadow sidecar, and a stale shadow next
1838
+ * to a freshly created DB file is replay poison on the next open (#2409).
1839
+ *
1840
+ * Verification contract (tri-review 4669518496 P2-4 — the old inline loops
1841
+ * swallowed rm failures and let `initLbug` reopen a still-populated DB the
1842
+ * run believed it wiped): after `fs.rm({ recursive, force })`, each path is
1843
+ * probed and counts as GONE only when the probe rejects with **ENOENT**. A
1844
+ * resolving probe, or a rejection in the EPERM/EBUSY/EACCES class (Windows
1845
+ * delete-pending / handle-release lag — see HANDLE_RELEASE_LOCK_CODES in
1846
+ * lbug-config.ts), or any other code means the path is not verifiably gone:
1847
+ * it is retried on the shared handle-release budget
1848
+ * (HANDLE_RELEASE_PROBE_ATTEMPTS × linear HANDLE_RELEASE_PROBE_DELAY_MS,
1849
+ * lbug-config.ts — the previous private mirror constants were
1850
+ * documentation-coupled copies) and then handled by CLASS (this shipping
1851
+ * review, FIX 2):
1852
+ *
1853
+ * - DATA-BEARING members (`<lbugPath>`, `.wal`, `.shadow`) — a survivor
1854
+ * means the reopen would resurrect rows this run believes wiped: throw
1855
+ * a typed {@link LbugWipeError}.
1856
+ * - `.lock` — contentless: `initLbug` recreates it, and a genuinely held
1857
+ * lock surfaces as initLbug's own lock-busy classification (a better
1858
+ * error than this one). A `.lock`-only survivor (an AV-held
1859
+ * delete-pending handle outlasting the budget previously failed a
1860
+ * perfectly sound rebuild) logs a warning and CONTINUES.
1861
+ *
1862
+ * Linux unlinked-but-open (name gone, holder keeps the old inode) probes
1863
+ * ENOENT and is accepted by design — both production wipe sites run after a
1864
+ * real `closeLbug()`.
1865
+ *
1866
+ * Deliberately OUT of this contract: `cleanupOldKuzuFiles`
1867
+ * (repo-manager.ts) sweeps the LEGACY kuzu-era file family during storage
1868
+ * migration — different family, best-effort by design; and
1869
+ * `sweepStaleSidecars` (lbug-config.ts) is a test-fixture-gated open-retry
1870
+ * fallback that must never delete production files. Neither wipes the live
1871
+ * DB the run is about to recreate, so neither needs (or may share) the
1872
+ * loud-failure contract here.
1873
+ */
1874
+ export const wipeLbugDbFiles = async (lbugPath) => {
1875
+ const lockPath = `${lbugPath}.lock`;
1876
+ const family = [lbugPath, `${lbugPath}.wal`, `${lbugPath}.shadow`, lockPath];
1877
+ let survivors = [];
1878
+ for (let attempt = 1; attempt <= HANDLE_RELEASE_PROBE_ATTEMPTS; attempt++) {
1879
+ survivors = [];
1880
+ for (const f of family) {
1881
+ try {
1882
+ await fs.rm(f, { recursive: true, force: true });
1883
+ }
1884
+ catch {
1885
+ // `force: true` swallows ENOENT, so a rejection is a real failure —
1886
+ // but the ENOENT-probe below stays authoritative either way (another
1887
+ // process may have removed the path between the rm and the probe).
1888
+ }
1889
+ const gone = await fs.access(f).then(() => false, // still present
1890
+ (err) => err?.code === 'ENOENT');
1891
+ if (!gone)
1892
+ survivors.push(f);
1893
+ }
1894
+ if (survivors.length === 0)
1895
+ return;
1896
+ if (attempt < HANDLE_RELEASE_PROBE_ATTEMPTS) {
1897
+ await sleep(HANDLE_RELEASE_PROBE_DELAY_MS * attempt);
1898
+ }
1899
+ }
1900
+ // Class split (FIX 2): the contentless `.lock` never fails the wipe.
1901
+ const dataSurvivors = survivors.filter((f) => f !== lockPath);
1902
+ if (survivors.includes(lockPath)) {
1903
+ logger.warn(`GitNexus: ${lockPath} is still present after the wipe retries — continuing: the ` +
1904
+ 'lock file is contentless and initLbug recreates it; a genuinely held lock will ' +
1905
+ "surface as the reopen's own lock-busy error.");
1906
+ }
1907
+ if (dataSurvivors.length > 0) {
1908
+ throw new LbugWipeError(dataSurvivors);
1909
+ }
1910
+ };
1752
1911
  export const isLbugReady = () => conn !== null && db !== null;
1912
+ /**
1913
+ * Multi-label alternation over exactly the labels that can own embedding
1914
+ * rows: EMBEDDABLE_LABELS plus File, which embedding-pipeline.ts embeds as
1915
+ * the zero-symbol fallback for text-only repositories (#2454). Reserved
1916
+ * keywords are backtick-escaped via {@link escapeTableName}. Probed on
1917
+ * @ladybugdb/core 0.18.0 (this shipping review, FIX 4): the full multi-label
1918
+ * alternation parses, executes, and deletes exactly the joined rows —
1919
+ * replacing the unlabeled `MATCH (n)` that scanned EVERY node table per
1920
+ * chunk (BasicBlock-dominated under `--pdg`) when only embeddable labels
1921
+ * can match an embedding row. Including File is free for code repositories:
1922
+ * they never hold File embedding rows, so the extra label joins nothing.
1923
+ */
1924
+ const embeddableLabelMatch = () => ['File', ...EMBEDDABLE_LABELS].map((l) => escapeTableName(l)).join('|');
1925
+ // LADYBUGDB-CONTRACT: matches @ladybugdb/core ^0.18.0 native binder text,
1926
+ // probe-recorded: `Binder exception: Table CodeEmbedding does not exist.`
1927
+ // When bumping LadybugDB, re-validate — `git grep "LADYBUGDB-CONTRACT"`
1928
+ // enumerates every version-coupled spot.
1929
+ const isMissingEmbeddingTableError = (err) => {
1930
+ const msg = err instanceof Error ? err.message : String(err);
1931
+ return msg.includes(`Table ${EMBEDDING_TABLE_NAME} does not exist`);
1932
+ };
1753
1933
  /**
1754
1934
  * Delete all nodes (and their relationships) for a specific file from LadybugDB
1755
1935
  * @param filePath - The file path to delete nodes for
@@ -1772,7 +1952,34 @@ export const deleteNodesForFile = async (filePath, dbPath) => {
1772
1952
  }
1773
1953
  try {
1774
1954
  let deletedNodes = 0;
1775
- const escapedPath = filePath.replace(/'/g, "''");
1955
+ const escapedPath = escapeCypherString(filePath);
1956
+ // Delete the file's embedding rows FIRST, while their owning nodes are
1957
+ // still present: node ids are label-first — generateId = `${label}:${name}`
1958
+ // (src/lib/utils.ts) with qualified names that embed the file path — so
1959
+ // the old `e.nodeId STARTS WITH '<filePath>'` shape never matched a row
1960
+ // (tri-review 4669518496 P2-1). Join through the nodes on exact id
1961
+ // equality instead, scoped to the embeddable labels (FIX 4 — see
1962
+ // embeddableLabelMatch); ordering is load-bearing — after the DETACH
1963
+ // DELETE loop below the join would match nothing.
1964
+ try {
1965
+ await queryAndDrain(targetConn, `MATCH (n:${embeddableLabelMatch()}) WHERE n.filePath = '${escapedPath}' ` +
1966
+ `MATCH (e:${EMBEDDING_TABLE_NAME}) WHERE e.nodeId = n.id DELETE e`);
1967
+ }
1968
+ catch (err) {
1969
+ // Deliberately legacy-permissive (pinned contract:
1970
+ // lbug-conn-serialization U5 and lbug-core-adapter expect this variant
1971
+ // to resolve `{deletedNodes: 0}` even on a bogus dbPath): the singular
1972
+ // variant swallows per-statement failures wholesale — its per-table
1973
+ // loop below does the same — so a partial rethrow here would be
1974
+ // incoherent with the rest of the function. The STRICT
1975
+ // rethrow-except-missing-table policy lives in deleteNodesForFiles,
1976
+ // the #2409 incremental writeback path (FIX 4). The one case worth a
1977
+ // diagnostic is the missing embedding table.
1978
+ if (isMissingEmbeddingTableError(err)) {
1979
+ logger.warn({ err }, `deleteNodesForFile: ${EMBEDDING_TABLE_NAME} table does not exist — ` +
1980
+ 'skipping embedding-row deletes for this DB.');
1981
+ }
1982
+ }
1776
1983
  // Delete nodes from each table that has filePath
1777
1984
  // DETACH DELETE removes the node and all its relationships
1778
1985
  for (const tableName of NODE_TABLES) {
@@ -1802,13 +2009,6 @@ export const deleteNodesForFile = async (filePath, dbPath) => {
1802
2009
  // Some tables may not support this query, skip
1803
2010
  }
1804
2011
  }
1805
- // Also delete any embeddings for nodes in this file
1806
- try {
1807
- await queryAndDrain(targetConn, `MATCH (e:${EMBEDDING_TABLE_NAME}) WHERE e.nodeId STARTS WITH '${escapedPath}' DELETE e`);
1808
- }
1809
- catch {
1810
- // Embedding table may not exist or nodeId format may differ
1811
- }
1812
2012
  return { deletedNodes };
1813
2013
  }
1814
2014
  finally {
@@ -1817,6 +2017,91 @@ export const deleteNodesForFile = async (filePath, dbPath) => {
1817
2017
  await closeLbugConnection(tempHandle);
1818
2018
  }
1819
2019
  };
2020
+ /**
2021
+ * Chunk size for {@link deleteNodesForFiles}. 200 paths keeps each
2022
+ * statement ~13KB (well inside parser limits) while a ~700-file write set
2023
+ * still collapses from ~13,000 statements to 124: 31 statements per chunk
2024
+ * (1 CodeEmbedding join-delete + 30 filePath-bearing node tables — the
2025
+ * 32-table NODE_TABLES roster minus Community/Process) × 4 chunks. The
2026
+ * original "~40" claim under-counted the per-chunk statement fan-out
2027
+ * (tri-review 4669518496 accuracy sweep).
2028
+ */
2029
+ export const DELETE_FILES_CHUNK_SIZE = 200;
2030
+ /**
2031
+ * Batched variant of {@link deleteNodesForFile} for the incremental
2032
+ * writeback (#2409). One `DETACH DELETE … WHERE n.filePath IN […]` per
2033
+ * node table per chunk of paths, instead of a count + delete per table
2034
+ * per FILE. The per-file loop issued ~13,000 single-row write
2035
+ * transactions on a ~700-file write set — a WAL-append storm that made
2036
+ * the incremental path slower than a full rebuild and is the write
2037
+ * pattern behind the native mid-writeback deaths reported in #2409.
2038
+ *
2039
+ * NO general error swallowing: a zero-match chunk is a no-op success by
2040
+ * construction (every node table except Community/Process has a filePath
2041
+ * column), so anything thrown here is a real engine failure the caller
2042
+ * must see — silently skipping was exactly how #2409 hid its root cause.
2043
+ * The single tolerated exception (FIX 4) is the missing-embedding-table
2044
+ * binder error on the embedding join-delete: a DB created without
2045
+ * EMBEDDING_SCHEMA cannot own embedding rows, so skipping that one
2046
+ * statement is sound, while failing would brick every incremental run on
2047
+ * such a DB until `--force`. Statement count per chunk is unchanged by the
2048
+ * multi-label join: 1 embedding join-delete + 30 node-table deletes = 31
2049
+ * (the rejected per-label fallback shape would have been 19 + 30 = 49).
2050
+ * Singleton-connection only: the analyze writeback owns the write lock,
2051
+ * and `queryAndDrain` routes through `withConnLock` for it (the WAL
2052
+ * checkpoint driver is live during this).
2053
+ */
2054
+ export const deleteNodesForFiles = async (filePaths, options = {}) => {
2055
+ if (!conn) {
2056
+ throw new Error('LadybugDB not initialized. Call initLbug first.');
2057
+ }
2058
+ const targetConn = conn;
2059
+ let warnedMissingEmbeddingTable = false;
2060
+ for (let i = 0; i < filePaths.length; i += DELETE_FILES_CHUNK_SIZE) {
2061
+ const chunk = filePaths.slice(i, i + DELETE_FILES_CHUNK_SIZE);
2062
+ const listLiteral = `[${chunk.map((p) => `'${escapeCypherString(p)}'`).join(', ')}]`;
2063
+ // Embedding rows key on their OWNING NODE's id: generateId builds
2064
+ // label-first ids — `${label}:${name}` (src/lib/utils.ts) with qualified
2065
+ // names that embed the file path (e.g. `Function:src/f.ts:fn0:1`) — so
2066
+ // the previous bare-path `e.nodeId STARTS WITH '<filePath>'` OR-chain
2067
+ // could never match anything (tri-review 4669518496 P2-1: the embedding
2068
+ // delete was a no-op). Join through the nodes instead: one multi-label
2069
+ // MATCH over exactly the embeddable labels (FIX 4, probe-proven on
2070
+ // 0.18.0 — see embeddableLabelMatch; the old unlabeled `MATCH (n)`
2071
+ // scanned every node table per chunk, BasicBlock-dominated under
2072
+ // `--pdg`, when only embeddable labels can own rows), and
2073
+ // `e.nodeId = n.id` equality is exact — no `File:a.ts` / `File:a.tsx`
2074
+ // prefix collisions. ORDER IS LOAD-BEARING: this must run BEFORE the
2075
+ // DETACH DELETE loop below — once the nodes are gone the join matches
2076
+ // nothing (empirically verified against @ladybugdb/core 0.18.0).
2077
+ try {
2078
+ await queryAndDrain(targetConn, `MATCH (n:${embeddableLabelMatch()}) WHERE n.filePath IN ${listLiteral} ` +
2079
+ `MATCH (e:${EMBEDDING_TABLE_NAME}) WHERE e.nodeId = n.id DELETE e`);
2080
+ }
2081
+ catch (err) {
2082
+ // Tolerate exactly the missing-embedding-table binder error: a
2083
+ // build-variant DB without EMBEDDING_SCHEMA would otherwise brick
2084
+ // every incremental run until `--force` (FIX 4). The no-swallow
2085
+ // policy stays for every real failure — anything else rethrows.
2086
+ if (!isMissingEmbeddingTableError(err))
2087
+ throw err;
2088
+ if (!warnedMissingEmbeddingTable) {
2089
+ warnedMissingEmbeddingTable = true;
2090
+ logger.warn({ err }, `deleteNodesForFiles: ${EMBEDDING_TABLE_NAME} table does not exist — ` +
2091
+ 'skipping embedding-row deletes for this writeback.');
2092
+ }
2093
+ }
2094
+ for (const tableName of NODE_TABLES) {
2095
+ // Community/Process are graph-wide (no filePath); the orchestrator
2096
+ // drops them wholesale via deleteAllCommunitiesAndProcesses.
2097
+ if (tableName === 'Community' || tableName === 'Process')
2098
+ continue;
2099
+ const tn = escapeTableName(tableName);
2100
+ await queryAndDrain(targetConn, `MATCH (n:${tn}) WHERE n.filePath IN ${listLiteral} DETACH DELETE n`);
2101
+ }
2102
+ options.onChunk?.(Math.min(i + DELETE_FILES_CHUNK_SIZE, filePaths.length), filePaths.length);
2103
+ }
2104
+ };
1820
2105
  export const getEmbeddingTableName = () => EMBEDDING_TABLE_NAME;
1821
2106
  /**
1822
2107
  * Return the distinct repo-relative paths of files that import
@@ -1838,7 +2123,7 @@ export const queryImporters = async (targetFilePath) => {
1838
2123
  if (!c) {
1839
2124
  throw new Error('LadybugDB not initialized. Call initLbug first.');
1840
2125
  }
1841
- const escaped = targetFilePath.replace(/'/g, "''");
2126
+ const escaped = escapeCypherString(targetFilePath);
1842
2127
  const cypher = `
1843
2128
  MATCH (a)-[r:${REL_TABLE_NAME}]->(b)
1844
2129
  WHERE r.type = 'IMPORTS' AND b.filePath = '${escaped}'
@@ -1870,6 +2155,70 @@ export const queryImporters = async (targetFilePath) => {
1870
2155
  }
1871
2156
  });
1872
2157
  };
2158
+ /**
2159
+ * Batched variant of {@link queryImporters} for the incremental importer
2160
+ * BFS (#2409): distinct importers of ANY of the target paths, one query per
2161
+ * chunk per BFS depth instead of one query per frontier FILE (a ~700-file
2162
+ * frontier was ~700 sequential round-trips, each taking the connection lock
2163
+ * against the live WAL checkpoint driver — ~5.6s of the writeback measured).
2164
+ *
2165
+ * Same contract as the singular form: reads the pre-pipeline DB state and
2166
+ * swallows per-chunk query failures into a smaller result (correctness
2167
+ * degrades on that branch — under-expansion means possibly-stale edges —
2168
+ * but the DB stays writable and the writeback proceeds). Unlike the singular
2169
+ * form the degradation is not silent (tri-review 4669518496 P2-5): every
2170
+ * dropped chunk is logged and reported through `options.onChunkFailure`, so
2171
+ * the orchestrator can count it into the #2410 crash diagnostics
2172
+ * (`incrementalInProgress.droppedImporterChunks`).
2173
+ */
2174
+ export const queryImportersBatch = async (targetFilePaths, options = {}) => {
2175
+ const c = conn;
2176
+ if (!c) {
2177
+ throw new Error('LadybugDB not initialized. Call initLbug first.');
2178
+ }
2179
+ const importers = new Set();
2180
+ for (let i = 0; i < targetFilePaths.length; i += DELETE_FILES_CHUNK_SIZE) {
2181
+ // `i` only ever advances in whole chunk strides, so this is exact.
2182
+ const chunkIndex = i / DELETE_FILES_CHUNK_SIZE;
2183
+ const chunk = targetFilePaths.slice(i, i + DELETE_FILES_CHUNK_SIZE);
2184
+ const listLiteral = `[${chunk.map((p) => `'${escapeCypherString(p)}'`).join(', ')}]`;
2185
+ const cypher = `
2186
+ MATCH (a)-[r:${REL_TABLE_NAME}]->(b)
2187
+ WHERE r.type = 'IMPORTS' AND b.filePath IN ${listLiteral}
2188
+ RETURN DISTINCT a.filePath AS importer
2189
+ `;
2190
+ await withConnLock(async () => {
2191
+ let queryResult;
2192
+ try {
2193
+ queryResult = await c.query(cypher);
2194
+ const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
2195
+ const rows = await result.getAll();
2196
+ for (const row of rows) {
2197
+ const v = row.importer;
2198
+ if (typeof v === 'string' && v.length > 0)
2199
+ importers.add(v);
2200
+ }
2201
+ }
2202
+ catch (err) {
2203
+ // Degrade-don't-fail, mirroring queryImporters — but LOUDLY
2204
+ // (tri-review 4669518496 P2-5): a dropped chunk means every importer
2205
+ // it would have surfaced keeps possibly-stale edges this run, and the
2206
+ // old bare `catch {}` left no trace of that anywhere. pino idiom:
2207
+ // `err` key — `error` serializes to `{}`.
2208
+ logger.warn({ err }, `Incremental importer BFS: dropped chunk ${chunkIndex} (${chunk.length} target path(s)) — ` +
2209
+ 'importer expansion degrades for this run; affected importers may keep stale edges until the next full rebuild.');
2210
+ options.onChunkFailure?.(chunkIndex, chunk.length, err);
2211
+ }
2212
+ finally {
2213
+ if (queryResult)
2214
+ await closeQueryResults(queryResult);
2215
+ }
2216
+ });
2217
+ }
2218
+ // Cypher without ORDER BY is unordered — sort so downstream chunking and
2219
+ // logs are stable run-to-run (matches diffFileHashes' sorted outputs).
2220
+ return [...importers].sort();
2221
+ };
1873
2222
  /**
1874
2223
  * Drop every Community and Process node (and their MEMBER_OF /
1875
2224
  * STEP_IN_PROCESS edges via DETACH DELETE). Used at the start of an
@@ -2052,16 +2401,16 @@ export const loadVectorExtension = async (targetConn, opts = {}) => {
2052
2401
  const useModuleState = targetConn === undefined;
2053
2402
  if (useModuleState && vectorExtensionLoaded)
2054
2403
  return true;
2055
- // INSTALL VECTOR crashes with SIGSEGV on Windows: the KuzuDB native extension
2056
- // installer has an unhandled error path on Windows that raises a fatal signal
2057
- // that JS try/catch cannot intercept. Skip loading — vector/embedding search
2058
- // is unavailable but all graph index queries still work. Do NOT set
2059
- // vectorExtensionLoaded here: the flag means "successfully loaded", and a
2060
- // subsequent call would otherwise short-circuit to `return true` at the top.
2061
- if (process.platform === 'win32')
2062
- return false;
2063
- if (!isVectorExtensionSupportedByPlatform())
2064
- return false;
2404
+ // No platform gate. Windows was hard-refused here for years on the strength
2405
+ // of an early-era report that in-process INSTALL VECTOR could SIGSEGV
2406
+ // (#1365) — but the extension server ships win_amd64 VECTOR artifacts for
2407
+ // every 0.18.x extension version (probed live: v0.18.0 and v0.18.1 both
2408
+ // serve a real PE32+ DLL; the pinned 0.18.2 core resolves its extension
2409
+ // directory to 0.18.1, strace-verified), and INSTALL now runs in a spawned
2410
+ // child process (installDuckDbExtensionOutOfProcess), so even a crashing
2411
+ // installer kills only the child and degrades to `false` here. LOAD of a
2412
+ // present extension file is an ordinary in-process load whose failures
2413
+ // surface as catchable errors, exactly like FTS.
2065
2414
  const c = targetConn ?? conn;
2066
2415
  if (!c) {
2067
2416
  throw new Error('LadybugDB not initialized. Call initLbug first.');
@@ -2155,6 +2504,74 @@ export const createVectorIndex = async () => {
2155
2504
  throw e;
2156
2505
  }
2157
2506
  };
2507
+ /**
2508
+ * Make DML against {@link EMBEDDING_TABLE_NAME} legal on the writable
2509
+ * connection when it can be, and report whether it is.
2510
+ *
2511
+ * LadybugDB refuses EVERY mutation of a table carrying an HNSW index while
2512
+ * the VECTOR extension is not loaded on that connection: `DELETE` fails with
2513
+ * "Trying to delete from an index on table CodeEmbedding but its extension is
2514
+ * not loaded", `CREATE` with the matching "insert into an index" variant,
2515
+ * `DROP TABLE` is refused while the index references it, and `SET` — even on
2516
+ * a NON-indexed property — segfaults the process outright. Probed against
2517
+ * @ladybugdb/core 0.18.2 (the lockfile-pinned version) and 0.18.0 — every
2518
+ * result identical on both (#2623).
2519
+ *
2520
+ * Dropping the index is NOT an available recovery: `CALL DROP_VECTOR_INDEX`
2521
+ * is itself a VECTOR-extension function and resolves to "Catalog exception:
2522
+ * function DROP_VECTOR_INDEX is not defined" in exactly the state it would
2523
+ * need to rescue. Loading the extension is the only in-place repair, which is
2524
+ * why this returns a verdict instead of attempting a fixup.
2525
+ *
2526
+ * `true` = embedding-row DML is safe: either VECTOR is now loaded, or the
2527
+ * table carries no index to trip over. `false` = genuinely blocked (index
2528
+ * present, extension unloadable); the analyze orchestrator answers that by
2529
+ * escalating to the wipe-and-rebuild write plan instead of failing
2530
+ * mid-writeback.
2531
+ *
2532
+ * Cheap by construction: one local `SHOW_INDEXES` read settles the common
2533
+ * "this repo never built an embedding index" case without touching the
2534
+ * extension machinery at all, so a VECTOR-less machine is not charged a
2535
+ * bounded INSTALL attempt on every incremental analyze. `SHOW_INDEXES` is
2536
+ * readable WITHOUT the extension and reports `extension_loaded` per index, so
2537
+ * no error-string sniffing is needed; it runs through the unprepared
2538
+ * `conn.query()` path like every other `CALL` procedure here (#2114).
2539
+ */
2540
+ export const ensureEmbeddingRowDmlSafe = async () => {
2541
+ const targetConn = conn;
2542
+ if (!targetConn) {
2543
+ throw new Error('LadybugDB not initialized. Call initLbug first.');
2544
+ }
2545
+ // Catalog FIRST. The overwhelmingly common case on a repo that never enabled
2546
+ // embeddings is "no index at all", and that is provable with one local read
2547
+ // — no extension needed. Loading first would make every incremental analyze
2548
+ // on a VECTOR-less machine pay a bounded out-of-process INSTALL attempt (the
2549
+ // `auto` policy) plus an "extension unavailable" warning, for a repo that
2550
+ // can never hit this hazard.
2551
+ let indexRows;
2552
+ try {
2553
+ indexRows = await withConnLock(async () => readQueryRows(await targetConn.query('CALL SHOW_INDEXES() RETURN *')));
2554
+ }
2555
+ catch (err) {
2556
+ // Fall through to the load attempt: unable to prove the index is absent,
2557
+ // so the extension is the only thing that can make DML safe.
2558
+ logger.warn({ err }, `Could not read the index catalog to check for a ${EMBEDDING_TABLE_NAME} vector index; ` +
2559
+ 'falling back to loading the VECTOR extension.');
2560
+ }
2561
+ // Any non-HASH index on the embedding table gates DML. Keyed on index TYPE,
2562
+ // not name, so an index built under a different name still counts; the
2563
+ // implicit primary-key HASH index is engine-internal and never gates.
2564
+ const indexGatesDml = indexRows === undefined ||
2565
+ indexRows.some((row) => {
2566
+ const table = row?.table_name ?? row?.[0];
2567
+ if (table !== EMBEDDING_TABLE_NAME)
2568
+ return false;
2569
+ return (row?.index_type ?? row?.[2]) !== 'HASH';
2570
+ });
2571
+ if (!indexGatesDml)
2572
+ return true;
2573
+ return await loadVectorExtension(undefined, { policy: resolveAnalyzeInstallPolicy() });
2574
+ };
2158
2575
  /**
2159
2576
  * Lazy-create an FTS index, caching the fact in-process.
2160
2577
  *
@@ -2232,7 +2649,28 @@ export const queryFTS = async (tableName, indexName, query, limit = 20, conjunct
2232
2649
  }
2233
2650
  };
2234
2651
  /**
2235
- * Drop an FTS index
2652
+ * True for the two benign "nothing to drop" `DROP_FTS_INDEX` failures —
2653
+ * both catalog/binder exceptions, LadybugDB's classes for "this name isn't
2654
+ * bound to anything right now" (probe-verified end-to-end through
2655
+ * `dropFTSIndex`'s real `conn.query()` path against @ladybugdb/core
2656
+ * 0.18.x): the named index was never created (`Binder exception: Table <T>
2657
+ * doesn't have an index with name <name>.`), or the FTS extension/function
2658
+ * isn't registered at all (`Catalog exception: function DROP_FTS_INDEX is
2659
+ * not defined...`). A real engine failure — e.g. the `Runtime exception:
2660
+ * FTS index '<name>' is inconsistent: ...` class from #2589 — is a
2661
+ * DIFFERENT exception class (an execution-time failure, not a catalog/bind
2662
+ * lookup miss), so this returns false for it. Anchored to the START of the
2663
+ * message (not a bare substring search): every probed LadybugDB error leads
2664
+ * with its exception class, and anchoring means a future message that merely
2665
+ * mentions "Binder exception" or "Catalog exception" further in in the body
2666
+ * of an otherwise-genuine failure can't be misclassified as benign. Pure
2667
+ * string logic so it is unit-testable without a native LadybugDB connection.
2668
+ */
2669
+ export const isBenignDropFtsIndexError = (message) => message.startsWith('Binder exception:') || message.startsWith('Catalog exception:');
2670
+ /**
2671
+ * Drop an FTS index. Tolerates only {@link isBenignDropFtsIndexError} —
2672
+ * anything else rethrows instead of being silently masked, which previously
2673
+ * let a corrupted index persist across analyze runs undetected.
2236
2674
  */
2237
2675
  export const dropFTSIndex = async (tableName, indexName) => {
2238
2676
  if (!conn) {
@@ -2241,8 +2679,11 @@ export const dropFTSIndex = async (tableName, indexName) => {
2241
2679
  try {
2242
2680
  await queryAndDrain(conn, `CALL DROP_FTS_INDEX('${tableName}', '${indexName}')`);
2243
2681
  }
2244
- catch {
2245
- // Index may not exist
2682
+ catch (e) {
2683
+ const msg = e instanceof Error ? e.message : String(e);
2684
+ if (!isBenignDropFtsIndexError(msg)) {
2685
+ throw e;
2686
+ }
2246
2687
  }
2247
2688
  finally {
2248
2689
  ensuredFTSIndexes.delete(ftsIndexKey(tableName, indexName));