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
@@ -10,14 +10,23 @@
10
10
  */
11
11
  import path from 'path';
12
12
  import fs from 'fs/promises';
13
- import { execFileSync } from 'child_process';
13
+ import { retryRename } from '../storage/fs-atomic.js';
14
14
  import { runPipelineFromRepo } from './ingestion/pipeline.js';
15
+ import { isMoveCompilerInputPath, moveCompilerRequiresFullRebuild, persistedMoveGraphRequiresCompiler, shouldProvisionMoveFlowBeforeFastPath, } from './move/constants.js';
16
+ import { createMoveIngestPhase } from './move/move-ingest.js';
17
+ import { repoHasMove } from './move/discovery.js';
18
+ import { getMoveFlowReleaseSelection } from './move/install.js';
19
+ import { ensureMoveFlowRuntime } from './move/provision.js';
15
20
  import { resetDegradedParseCounter } from './tree-sitter/safe-parse.js';
16
- import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, closeLbugBeforeExit, loadCachedEmbeddings, deleteNodesForFile, deleteAllCommunitiesAndProcesses, deleteAllInterprocTaintPaths, deleteAllCallSummaries, deleteAllInjects, queryImporters, loadFTSExtension, } from './lbug/lbug-adapter.js';
17
- import { createSearchFTSIndexes, initialiseSearchFTSStemmer, verifySearchFTSIndexes, } from './search/fts-indexes.js';
21
+ import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, closeLbugBeforeExit, loadCachedEmbeddings, deleteNodesForFiles, ensureEmbeddingRowDmlSafe, deleteAllCommunitiesAndProcesses, deleteAllInterprocTaintPaths, deleteAllCallSummaries, deleteAllInjects, queryImportersBatch, loadFTSExtension, wipeLbugDbFiles, LbugWipeError, DELETE_FILES_CHUNK_SIZE, } from './lbug/lbug-adapter.js';
22
+ import { estimateBufferPool, setBufferPoolSizeHint } from './lbug/lbug-config.js';
23
+ import { escapeCypherString } from './lbug/cypher-escape.js';
24
+ import { buildSearchIndexesOrDegrade, createSearchFTSIndexes, dropSearchFTSIndexes, initialiseSearchFTSStemmer, verifySearchFTSIndexes, } from './search/fts-indexes.js';
18
25
  import { cjkSegmentationModeMismatch, getSearchFTSCjkSegmentation, initialiseSearchFTSCjkSegmentation, } from './search/cjk-segmentation.js';
19
- import { resolveAnalyzeInstallPolicy } from './lbug/extension-loader.js';
20
- import { startWalCheckpointDriver, } from './lbug/wal-checkpoint-driver.js';
26
+ import { getExtensionCapabilities, resolveAnalyzeInstallPolicy } from './lbug/extension-loader.js';
27
+ import { diagnoseExtensionLoad } from './lbug/extension-load-error.js';
28
+ import { startWalCheckpointDriver, checkpointOnce, } from './lbug/wal-checkpoint-driver.js';
29
+ import { quarantineSidecarsForDirtyRecovery, inspectLbugSidecars, } from './lbug/sidecar-recovery.js';
21
30
  import { getStoragePaths, resolveBranchPlacement, saveMeta, loadMeta, ensureGitNexusIgnored, registerRepo, adoptFlatBranchLabel, isReadOnlyFilesystemError, isRepoRegistered, cleanupOldKuzuFiles, reconcileMetadataFiles, isMissingFilesystemError, INDEX_METADATA_FILE, INCREMENTAL_SCHEMA_VERSION, } from '../storage/repo-manager.js';
22
31
  import { DEFAULT_PDG_MAX_FUNCTION_LINES } from './ingestion/cfg/collect.js';
23
32
  import { DEFAULT_MAX_CFG_EDGES_PER_FUNCTION, DEFAULT_PDG_MAX_REACHING_DEF_EDGES_PER_FUNCTION, DEFAULT_PDG_MAX_CDG_EDGES_PER_FUNCTION, } from './ingestion/cfg/emit.js';
@@ -29,19 +38,61 @@ import { parseTruthyEnv, parsePositiveIntEnv } from './ingestion/utils/env.js';
29
38
  import { computeFileHashes, diffFileHashes } from '../storage/file-hash.js';
30
39
  import { extractChangedSubgraph, computeEffectiveWriteSet, } from './incremental/subgraph-extract.js';
31
40
  import { shadowCandidatesFor } from './incremental/shadow-candidates.js';
41
+ import { shouldEscalateIncrementalWrite } from './incremental/escalation-gate.js';
32
42
  import { loadParseCache, saveParseCache, pruneCache, PARSE_CACHE_VERSION, } from '../storage/parse-cache.js';
33
43
  import { getDurableParsedFileDir, pruneAndSaveDurableParsedFileStore, } from '../storage/parsedfile-store.js';
34
- import { getCurrentCommit, getCurrentBranch, getRemoteUrl, hasGitDir, getInferredRepoName, resolveRepoIdentityRoot, } from '../storage/git.js';
44
+ import { getCurrentCommit, getCurrentBranch, getRemoteUrl, hasGitDir, getInferredRepoName, isWorkingTreeDirty, resolveRepoIdentityRoot, } from '../storage/git.js';
35
45
  import { generateAIContextFiles } from '../cli/ai-context.js';
36
46
  import { sanitizeDetectedBranch } from '../cli/analyze-config.js';
37
47
  import { EMBEDDING_TABLE_NAME } from './lbug/schema.js';
38
48
  import { STALE_HASH_SENTINEL } from './lbug/schema.js';
49
+ import { isSpringBeanCandidateSourceFile } from './ingestion/frameworks/spring/bean-catalog.js';
50
+ import { SPRING_BEAN_INVENTORY_FEATURE } from './ingestion/frameworks/spring/analysis-features.js';
51
+ import { SPRING_CONFIG_BINDINGS_FEATURE } from './ingestion/languages/java/analysis-features.js';
52
+ import { CLASS_FRAMEWORK_ANNOTATIONS_FEATURE, findAnalysisFeatureMismatches, resolveAnalysisFeatureVersions, } from './analysis-features.js';
53
+ import { analyzerRunnerIdentitiesEqual, finalizeAnalyzerRunnerIdentity, resolveAnalyzerRunnerIdentity, } from './analyzer-identity.js';
54
+ const ANALYSIS_FEATURES = [
55
+ CLASS_FRAMEWORK_ANNOTATIONS_FEATURE,
56
+ SPRING_BEAN_INVENTORY_FEATURE,
57
+ SPRING_CONFIG_BINDINGS_FEATURE,
58
+ ];
59
+ function stringList(value) {
60
+ return Array.isArray(value)
61
+ ? value.filter((item) => typeof item === 'string')
62
+ : [];
63
+ }
64
+ function collectFrameworkAnnotationDriftFiles(graph, persistedRows) {
65
+ const persistedById = new Map();
66
+ for (const row of persistedRows) {
67
+ if (typeof row.id === 'string') {
68
+ persistedById.set(row.id, stringList(row.frameworkAnnotations));
69
+ }
70
+ }
71
+ const driftFiles = new Set();
72
+ graph.forEachNode((node) => {
73
+ if (node.label !== 'Class')
74
+ return;
75
+ const current = stringList(node.properties.frameworkAnnotations);
76
+ const persisted = persistedById.get(node.id) ?? [];
77
+ if (current.length !== persisted.length ||
78
+ current.some((annotation, index) => annotation !== persisted[index])) {
79
+ const filePath = node.properties.filePath;
80
+ if (typeof filePath === 'string')
81
+ driftFiles.add(filePath);
82
+ }
83
+ });
84
+ return driftFiles;
85
+ }
39
86
  /**
40
87
  * Logged when the optional FTS extension cannot be loaded or installed during
41
88
  * a full analyze. Kept as a named constant so the env-var/command guidance
42
89
  * stays in one place (mirrors the VECTOR message in embedding-pipeline.ts).
43
90
  */
44
- const FTS_UNAVAILABLE_MESSAGE = 'FTS extension unavailable; skipping search-index creation. ' +
91
+ // Class-neutral lead, reused for the missing-dependency degrade path (#2383 F2):
92
+ // its remedy already explains that reinstalling will NOT help, so appending the
93
+ // generic "install with network access" tail below would contradict it.
94
+ const FTS_UNAVAILABLE_LEAD = 'FTS extension unavailable; skipping search-index creation.';
95
+ const FTS_UNAVAILABLE_MESSAGE = `${FTS_UNAVAILABLE_LEAD} ` +
45
96
  'Full-text/BM25 search will be disabled until the LadybugDB FTS extension is ' +
46
97
  'installed once with network access (GITNEXUS_LBUG_EXTENSION_INSTALL=auto) or ' +
47
98
  'pre-installed for offline use. Run `gitnexus doctor` for details.';
@@ -251,7 +302,7 @@ export const pdgModeMismatch = (recorded, options) => {
251
302
  }
252
303
  return false;
253
304
  };
254
- export async function runFullAnalysis(repoPath, options, callbacks) {
305
+ export async function runFullAnalysis(repoPath, options, callbacks, runnerIdentityAtBootstrap) {
255
306
  const log = (msg) => callbacks.onLog?.(msg);
256
307
  const progress = (phase, percent, message) => callbacks.onProgress(phase, percent, message);
257
308
  // Resolve + validate operator-provided FTS config once, before the expensive
@@ -270,6 +321,10 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
270
321
  // (parse-cache, parsedfile-store) and the kuzu-migration cleanup live there
271
322
  // and are shared across branches (#2106 KTD7).
272
323
  const { storagePath } = getStoragePaths(repoPath);
324
+ // Start each analyze with a clean buffer-pool hint: any pre-pipeline DB open
325
+ // (e.g. the embeddings-cache open) falls back to the default until the hint is
326
+ // set from the built graph below, so a prior run's size can't leak in.
327
+ setBufferPoolSizeHint(undefined);
273
328
  // Clean up stale KuzuDB files from before the LadybugDB migration.
274
329
  const kuzuResult = await cleanupOldKuzuFiles(storagePath);
275
330
  if (kuzuResult.found && kuzuResult.needsReindex) {
@@ -328,6 +383,18 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
328
383
  throw new Error('Cannot repair FTS indexes because this repository has not been analyzed yet. ' +
329
384
  'Run `gitnexus analyze` first to create the initial index, then retry `--repair-fts`.');
330
385
  }
386
+ if (existingMeta.incrementalInProgress) {
387
+ // #2409 / tri-review 4669518496 (R6): a dirty flag means the previous
388
+ // run died mid-writeback — the graph may be half-written and its WAL
389
+ // possibly poisoned. This branch returns early, so the dirty-recovery
390
+ // sidecar quarantine below would never run: repairing FTS now would
391
+ // open the DB and replay that WAL pre-quarantine, and even a
392
+ // survivable open would certify FTS over a half-written graph.
393
+ throw new Error('Cannot repair FTS indexes: the index is mid-incremental-recovery ' +
394
+ '(a previous analyze run did not complete cleanly). ' +
395
+ 'Run `gitnexus analyze` first — it recovers the index automatically — ' +
396
+ 'then retry `--repair-fts`.');
397
+ }
331
398
  let lbugStat;
332
399
  try {
333
400
  lbugStat = await fs.lstat(lbugPath);
@@ -365,9 +432,23 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
365
432
  policy: resolveAnalyzeInstallPolicy(),
366
433
  });
367
434
  if (!repairFtsAvailable) {
368
- throw new Error('Cannot repair FTS indexes: the LadybugDB FTS extension is unavailable ' +
369
- '(not pre-installed and could not be installed on this machine). ' +
370
- 'Run `gitnexus doctor` to install it, then retry `--repair-fts`.');
435
+ // Surface the load-side reason (#2374): "not pre-installed" was wrong
436
+ // and doctor never installed anything, so the old message trapped
437
+ // users in a query → repair-fts → doctor loop with no way out.
438
+ const rawFtsReason = getExtensionCapabilities().find((c) => c.name === 'fts')?.reason;
439
+ const ftsReason = rawFtsReason?.replace(/\.$/, '');
440
+ // A missing runtime dependency (Windows error 126, #2374) is not healed
441
+ // by re-installing — the file is already present. Route that class to the
442
+ // classified remedy (install VC++ redist / OpenSSL) instead of the old
443
+ // "retry the network install" text that trapped the user in a loop.
444
+ const { kind, remedy } = diagnoseExtensionLoad(rawFtsReason);
445
+ const remedyTail = kind === 'missing_dependency'
446
+ ? ` ${remedy}`
447
+ : '. Retry with network access and GITNEXUS_LBUG_EXTENSION_INSTALL=auto to install it, ' +
448
+ 'or pre-install the extension file; run `gitnexus doctor` for live FTS status.';
449
+ throw new Error('Cannot repair FTS indexes: the LadybugDB FTS extension failed to load' +
450
+ (ftsReason ? ` — ${ftsReason}` : '') +
451
+ remedyTail);
371
452
  }
372
453
  progress('fts', 85, 'Repairing search indexes...');
373
454
  await createSearchFTSIndexes({
@@ -400,20 +481,114 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
400
481
  await closeLbug().catch(() => { });
401
482
  }
402
483
  }
484
+ // Resolve once per real analysis run so every successful metadata write
485
+ // carries one coherent receipt. The FTS-only repair path above intentionally
486
+ // returns without restamping: it does not regenerate the graph represented by
487
+ // RepoMeta and therefore must not claim a new analyzer identity.
488
+ const runnerIdentity = runnerIdentityAtBootstrap ?? resolveAnalyzerRunnerIdentity(import.meta.url);
489
+ if (!analyzerRunnerIdentitiesEqual(runnerIdentity, runnerIdentity)) {
490
+ throw new Error('Analyzer bootstrap supplied a malformed runner identity receipt');
491
+ }
492
+ let resumeEmbeddingCheckpoint = false;
493
+ let pendingEmbeddingNodeIds = new Set();
494
+ let embeddingIdentityForRun;
495
+ if (existingMeta?.embeddingCheckpoint) {
496
+ if (options.dropEmbeddings) {
497
+ log('Discarding the interrupted embedding checkpoint (--drop-embeddings).');
498
+ options = { ...options, force: true };
499
+ }
500
+ else {
501
+ const { resolveEmbeddingIdentity } = await import('./embeddings/embedding-identity.js');
502
+ embeddingIdentityForRun = resolveEmbeddingIdentity();
503
+ const checkpoint = existingMeta.embeddingCheckpoint;
504
+ if (checkpoint.provider !== embeddingIdentityForRun.provider) {
505
+ throw new Error('Cannot resume embedding checkpoint: the embedding provider configuration differs. ' +
506
+ 'Restore the matching endpoint configuration or pass --drop-embeddings to rebuild without it.');
507
+ }
508
+ if (checkpoint.model !== embeddingIdentityForRun.model ||
509
+ checkpoint.dimensions !== embeddingIdentityForRun.dimensions) {
510
+ throw new Error(`Cannot resume embedding checkpoint: it uses ${checkpoint.model} at ` +
511
+ `${checkpoint.dimensions} dimensions, but this run resolves ` +
512
+ `${embeddingIdentityForRun.model} at ${embeddingIdentityForRun.dimensions}. ` +
513
+ 'Restore the matching embedding configuration or pass --drop-embeddings to rebuild without it.');
514
+ }
515
+ resumeEmbeddingCheckpoint = true;
516
+ pendingEmbeddingNodeIds = new Set(checkpoint.pendingNodeIds ?? []);
517
+ log(`Previous analyze ended at an embedding checkpoint ` +
518
+ `(${checkpoint.nodesProcessed}/${checkpoint.totalNodes} nodes); resuming from persisted hashes` +
519
+ `${pendingEmbeddingNodeIds.size > 0 ? ` and regenerating ${pendingEmbeddingNodeIds.size} pending node(s)` : ''}.`);
520
+ }
521
+ }
403
522
  // ── Crash recovery: dirty flag forces full rebuild ────────────────
404
523
  // If the previous incremental run set incrementalInProgress and didn't
405
524
  // clear it, the on-disk index may be in a half-state. Cheapest path
406
525
  // back to a known-good index is to wipe + rebuild from scratch.
407
526
  if (existingMeta?.incrementalInProgress) {
527
+ const dirty = existingMeta.incrementalInProgress;
528
+ const dirtyDetails = typeof dirty === 'object'
529
+ ? [
530
+ dirty.phase ? `phase=${dirty.phase}` : undefined,
531
+ `toWrite=${dirty.toWriteCount}`,
532
+ dirty.importerExpansion !== undefined
533
+ ? `importerExpansion=${dirty.importerExpansion}`
534
+ : undefined,
535
+ dirty.effectiveWriteCount !== undefined
536
+ ? `effectiveWrite=${dirty.effectiveWriteCount}`
537
+ : undefined,
538
+ dirty.deleteCount !== undefined ? `deleteCount=${dirty.deleteCount}` : undefined,
539
+ // Only stamped when > 0 (tri-review 4669518496 P2-5): its
540
+ // presence means the crashed run's importer expansion was
541
+ // already degraded — the write set may have been under-expanded
542
+ // before the crash.
543
+ dirty.droppedImporterChunks !== undefined
544
+ ? `droppedImporterChunks=${dirty.droppedImporterChunks}`
545
+ : undefined,
546
+ ]
547
+ .filter(Boolean)
548
+ .join(', ')
549
+ : 'legacy dirty flag';
408
550
  log(
409
551
  // "analyze run", not "incremental run" — since #2099 F1 the flag is a
410
552
  // generic dirty marker written by BOTH writeback branches.
411
553
  'Previous analyze run did not complete cleanly (incrementalInProgress flag set); ' +
554
+ `last dirty state: ${dirtyDetails}; ` +
412
555
  'forcing full rebuild to restore a known-good index.');
413
556
  options = { ...options, force: true };
414
557
  // Reload meta after clearing the flag in-memory; we still want fileHashes
415
558
  // for the post-rebuild meta carry-over, but force=true ensures the
416
559
  // rebuild path executes.
560
+ //
561
+ // #2409 defect 2: the crashed writeback's WAL can be poisoned — replaying
562
+ // it kills the process natively, and the first DB open of this recovery
563
+ // run (the embedding-cache preservation open below) happens BEFORE the
564
+ // rebuild wipe that would discard it. Park the WAL/shadow sidecars aside
565
+ // now, while nothing is open, so every open in this run is replay-free.
566
+ // The rebuild wipes the DB regardless, so no committed data is at stake.
567
+ const { removed, failed } = await quarantineSidecarsForDirtyRecovery(lbugPath, log);
568
+ if (removed.length > 0) {
569
+ log(`Dirty-state recovery discarded ${removed.map((p) => path.basename(p)).join(', ')} ` +
570
+ 'from the interrupted run (the file could not be moved aside, so its bytes were ' +
571
+ 'removed — post-mortem forensics lost). Recovery proceeds with full embedding ' +
572
+ 'preservation.');
573
+ }
574
+ if (failed.length > 0) {
575
+ // FIX 1 (this shipping review, replacing the tri-review 4669518496
576
+ // P2-3 drop-shape design): under a persistent lock the old drop-shape
577
+ // run derived its embedding mode as "drop", ran the WHOLE pipeline,
578
+ // and then died at the rebuild wipe on the very same handle — wasting
579
+ // minutes and zeroing embeddings on the way. A possibly-poisoned
580
+ // sidecar still sits next to the DB (any pre-wipe open would replay it
581
+ // and die), so failing here, in seconds, with the same actionable
582
+ // typed error the wipe would eventually throw is strictly better —
583
+ // and the CLI's LbugWipeError handler already renders it
584
+ // (recoveryHint 'lbug-wipe-failed'). The message is self-contained
585
+ // (headline + paths + lock guidance) because serve forwards only
586
+ // err.message over worker IPC.
587
+ throw new LbugWipeError(failed, {
588
+ headline: "Cannot start dirty-state recovery — the interrupted run's LadybugDB sidecars " +
589
+ 'could neither be moved aside nor removed:',
590
+ });
591
+ }
417
592
  }
418
593
  // ── pdg-mode flip forces full writeback (#2099 F1) ─────────────────
419
594
  // The incremental writeback persists only changed-file nodes, so a pdg
@@ -457,6 +632,36 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
457
632
  `forcing a full rebuild so persisted rows match the current schema.`);
458
633
  options = { ...options, force: true };
459
634
  }
635
+ // ── independently-versioned analysis capabilities ────────────────
636
+ // `schemaVersion` is reserved for graph-wide incremental invariants. Some
637
+ // persisted semantics apply only to repositories containing relevant source
638
+ // files, so they carry exact feature versions instead. This guard must also
639
+ // run before alreadyUpToDate: current main and this PR both use schema v8,
640
+ // while pre-PR v8 indexes lack the Class frameworkAnnotations column and
641
+ // Java/Kotlin Bean evidence.
642
+ const persistedFilePaths = Object.keys(existingMeta?.fileHashes ?? {});
643
+ const expectedPersistedAnalysisFeatures = resolveAnalysisFeatureVersions(ANALYSIS_FEATURES, persistedFilePaths);
644
+ const persistedAnalysisFeatureMismatches = existingMeta
645
+ ? findAnalysisFeatureMismatches(existingMeta.analysisFeatures, expectedPersistedAnalysisFeatures)
646
+ : [];
647
+ let analysisFeatureMismatchLogged = false;
648
+ if (existingMeta && persistedAnalysisFeatureMismatches.length > 0) {
649
+ log(`analysis capabilities changed (${persistedAnalysisFeatureMismatches.join(', ')}); ` +
650
+ `forcing a full rebuild so persisted feature evidence is complete.`);
651
+ options = { ...options, force: true };
652
+ analysisFeatureMismatchLogged = true;
653
+ }
654
+ // Analyzer provenance is part of freshness, not merely diagnostics. A
655
+ // same-commit fast path must not preserve metadata produced by an older,
656
+ // malformed, or dependency/native-different runner. Force a real rebuild so
657
+ // the graph and its schema-v4 receipt are finalized atomically together.
658
+ if (existingMeta && !analyzerRunnerIdentitiesEqual(existingMeta.runnerIdentity, runnerIdentity)) {
659
+ const stampedRunnerSchema = existingMeta.runnerIdentity?.schemaVersion;
660
+ log(`analyzer runner identity changed (stamped schema ${String(stampedRunnerSchema ?? 'missing')}, ` +
661
+ `this build uses schema ${runnerIdentity.schemaVersion}); forcing a full rebuild so the ` +
662
+ 'index provenance matches the analyzer and dependency/native runtime that produced it.');
663
+ options = { ...options, force: true };
664
+ }
460
665
  if (existingMeta &&
461
666
  cjkSegmentationModeMismatch(existingMeta.cjkSegmentation, getSearchFTSCjkSegmentation())) {
462
667
  log(`CJK segmentation mode changed (index built with '${existingMeta.cjkSegmentation ?? 'none'}', ` +
@@ -464,8 +669,39 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
464
669
  `text and query-time segmentation stay in sync.`);
465
670
  options = { ...options, force: true };
466
671
  }
672
+ // Resolve local runtimes before the same-commit fast path. Managed
673
+ // provisioning is needed for a degraded/legacy graph and whenever the
674
+ // configured dynamic release differs from the one that built the index.
675
+ const hasMovePackages = await repoHasMove(repoPath);
676
+ const desiredMoveFlowRelease = hasMovePackages ? await getMoveFlowReleaseSelection() : undefined;
677
+ const persistedMoveCompilerRequired = persistedMoveGraphRequiresCompiler(existingMeta?.moveIngestAvailable, existingMeta?.fileHashes);
678
+ const provisionBeforeFastPath = shouldProvisionMoveFlowBeforeFastPath(hasMovePackages, persistedMoveCompilerRequired, existingMeta?.moveCompilerIdentity, existingMeta?.moveFlowReleaseSelection, desiredMoveFlowRelease);
679
+ let moveFlow = hasMovePackages
680
+ ? await ensureMoveFlowRuntime({
681
+ onLog: log,
682
+ install: provisionBeforeFastPath,
683
+ })
684
+ : null;
685
+ let moveCompilerRebuildApplied = false;
686
+ const applyMoveCompilerRebuildPolicy = () => {
687
+ if (!existingMeta ||
688
+ !moveCompilerRequiresFullRebuild(hasMovePackages, moveFlow?.identity, existingMeta.moveIngestAvailable, existingMeta.moveCompilerIdentity)) {
689
+ return;
690
+ }
691
+ if (!moveCompilerRebuildApplied) {
692
+ log(existingMeta.moveIngestAvailable === true
693
+ ? 'Move compiler changed; forcing a full rebuild so persisted facts match it.'
694
+ : 'Move compiler became available; forcing a full rebuild to backfill Move symbols.');
695
+ }
696
+ moveCompilerRebuildApplied = true;
697
+ options = { ...options, force: true };
698
+ };
699
+ applyMoveCompilerRebuildPolicy();
467
700
  // ── Early-return: already up to date ──────────────────────────────
468
- if (existingMeta && !options.force && existingMeta.lastCommit === currentCommit) {
701
+ if (existingMeta &&
702
+ !existingMeta.embeddingCheckpoint &&
703
+ !options.force &&
704
+ existingMeta.lastCommit === currentCommit) {
469
705
  // Non-git folders have currentCommit = '' — always rebuild since we can't detect changes
470
706
  if (currentCommit !== '') {
471
707
  // For git repos, even if HEAD matches lastCommit, the working tree
@@ -480,33 +716,7 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
480
716
  // Counting them as dirty would perpetually defeat the up-to-date
481
717
  // fast path because the previous analyze just wrote them
482
718
  // (regression vs PR #1233 behavior).
483
- const dirty = (() => {
484
- try {
485
- const out = execFileSync('git', [
486
- 'status',
487
- '--porcelain',
488
- '--',
489
- '.',
490
- ':(exclude).gitnexus',
491
- ':(exclude).gitnexus/**',
492
- ':(exclude).claude',
493
- ':(exclude).claude/**',
494
- ':(exclude).cursor',
495
- ':(exclude).cursor/**',
496
- ':(exclude)AGENTS.md',
497
- ':(exclude)CLAUDE.md',
498
- ], {
499
- cwd: repoPath,
500
- stdio: ['ignore', 'pipe', 'ignore'],
501
- windowsHide: true,
502
- encoding: 'utf8',
503
- });
504
- return out.trim().length > 0;
505
- }
506
- catch {
507
- return true; // conservative on git failure
508
- }
509
- })();
719
+ const dirty = isWorkingTreeDirty(repoPath);
510
720
  // Registration wrinkle around the fast path (#2264). A prior
511
721
  // `analyze --name X` that hit a name collision writes meta.json (meta-save
512
722
  // runs before registerRepo) then fails before registering, leaving the
@@ -554,6 +764,7 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
554
764
  }
555
765
  }
556
766
  await ensureGitNexusIgnored(repoPath);
767
+ await moveFlow?.client.shutdown();
557
768
  return {
558
769
  // `resolveRepoIdentityRoot` collapses worktree roots to the
559
770
  // canonical repo basename (#1259) but leaves arbitrary subdirs
@@ -569,6 +780,19 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
569
780
  }
570
781
  }
571
782
  }
783
+ // From this point onward analysis may write or derive graph data. Never run
784
+ // that plan without the compiler that produced an existing Move graph: a
785
+ // partial rebuild would silently erase Move rows or degrade global clusters.
786
+ if (hasMovePackages && !moveFlow) {
787
+ moveFlow = await ensureMoveFlowRuntime({ onLog: log });
788
+ if (persistedMoveCompilerRequired && !moveFlow) {
789
+ throw new Error('move-flow is unavailable; the existing Move graph was left unchanged. ' +
790
+ 'Restore move-flow or set MOVE_FLOW, then retry analysis.');
791
+ }
792
+ }
793
+ const moveFlowClient = moveFlow?.client ?? null;
794
+ const moveIngestAvailable = hasMovePackages ? moveFlow !== null : undefined;
795
+ applyMoveCompilerRebuildPolicy();
572
796
  // ── Cache embeddings from existing index before rebuild ────────────
573
797
  // Four modes:
574
798
  // --embeddings -> load cache, restore, then generate any new ones
@@ -587,7 +811,9 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
587
811
  let cachedEmbeddingNodeIds = new Set();
588
812
  let cachedEmbeddings = [];
589
813
  const existingEmbeddingCount = existingMeta?.stats?.embeddings ?? 0;
590
- const { forceRegenerateEmbeddings, preserveExistingEmbeddings, shouldGenerateEmbeddings, shouldLoadCache, } = _deriveEmbeddingMode(options, existingEmbeddingCount);
814
+ const { forceRegenerateEmbeddings, preserveExistingEmbeddings, shouldGenerateEmbeddings: derivedShouldGenerateEmbeddings, shouldLoadCache: derivedShouldLoadCache, } = _deriveEmbeddingMode(options, existingEmbeddingCount);
815
+ const shouldGenerateEmbeddings = derivedShouldGenerateEmbeddings || resumeEmbeddingCheckpoint;
816
+ const shouldLoadCache = derivedShouldLoadCache || resumeEmbeddingCheckpoint;
591
817
  if (options.dropEmbeddings && existingEmbeddingCount > 0) {
592
818
  log(`Dropping ${existingEmbeddingCount} existing embeddings (--drop-embeddings). ` +
593
819
  `Re-run with --embeddings to regenerate.`);
@@ -610,6 +836,12 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
610
836
  // silently dropping embeddings on a mispredicted run. The re-insert
611
837
  // step gates itself on the actual `isIncremental` value to avoid
612
838
  // PK-conflicts when the incremental writeback path keeps the rows.
839
+ //
840
+ // This is the FIRST DB open of the run — the one #2409 defect 2 is about.
841
+ // On a dirty-recovery run it happens only after the sidecar quarantine
842
+ // moved (or removed) the crashed run's WAL/shadow; when neither was
843
+ // possible the dirty block above already threw a LbugWipeError, so this
844
+ // open is replay-free by construction (FIX 1 of this shipping review).
613
845
  if (shouldLoadCache && existingMeta) {
614
846
  try {
615
847
  progress('embeddings', 0, 'Caching embeddings...');
@@ -643,35 +875,44 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
643
875
  // in-place (cache hits leave entries unchanged; misses add new ones).
644
876
  const parseCache = await loadParseCache(storagePath);
645
877
  // ── Phase 1: Full Pipeline (0–60%) ────────────────────────────────
646
- const pipelineResult = await runPipelineFromRepo(repoPath, (p) => {
647
- const phaseLabel = PHASE_LABELS[p.phase] || p.phase;
648
- const scaled = Math.round(p.percent * 0.6);
649
- const message = p.detail
650
- ? `${p.message || phaseLabel} (${p.detail})`
651
- : p.message || phaseLabel;
652
- progress(p.phase, scaled, message);
653
- }, {
654
- parseCache,
655
- workerPoolSize: options.workerPoolSize,
656
- // CFG/PDG opt-in (#2081 M1). PipelineOptions.pdg fans out to the worker
657
- // build gate (workerData.pdg) and the scope-resolution emit gate.
658
- pdg: options.pdg === true,
659
- pdgMaxFunctionLines: options.pdgMaxFunctionLines,
660
- pdgMaxEdgesPerFunction: options.pdgMaxEdgesPerFunction,
661
- pdgMaxReachingDefEdgesPerFunction: options.pdgMaxReachingDefEdgesPerFunction,
662
- pdgMaxCdgEdgesPerFunction: options.pdgMaxCdgEdgesPerFunction,
663
- pdgMaxTaintFindingsPerFunction: options.pdgMaxTaintFindingsPerFunction,
664
- pdgMaxTaintHops: options.pdgMaxTaintHops,
665
- pdgMaxInterprocFindings: options.pdgMaxInterprocFindings,
666
- pdgMaxInterprocHops: options.pdgMaxInterprocHops,
667
- pdgMaxInterprocEdges: options.pdgMaxInterprocEdges,
668
- // Streaming/chunked PDG emit (#2202) — gated to full-rebuild runs
669
- // (force === true) so the incremental writeback never reads back an
670
- // offloaded BasicBlock layer. Memory-only; byte-identical output.
671
- streamPdgEmit: resolveStreamPdgEmit(options),
672
- pdgEmitChunkSize: resolvePdgEmitChunkSize(options),
673
- fetchWrappers: options.fetchWrappers,
674
- });
878
+ // `finally` guarantees the spawned move-flow child is released even if the
879
+ // pipeline throws — important for long-running hosts (MCP daemon, eval-server).
880
+ let pipelineResult;
881
+ try {
882
+ pipelineResult = await runPipelineFromRepo(repoPath, (p) => {
883
+ const phaseLabel = PHASE_LABELS[p.phase] || p.phase;
884
+ const scaled = Math.round(p.percent * 0.6);
885
+ const message = p.detail
886
+ ? `${p.message || phaseLabel} (${p.detail})`
887
+ : p.message || phaseLabel;
888
+ progress(p.phase, scaled, message);
889
+ }, {
890
+ parseCache,
891
+ workerPoolSize: options.workerPoolSize,
892
+ standaloneIngestPhase: createMoveIngestPhase(moveFlowClient),
893
+ // CFG/PDG opt-in (#2081 M1). PipelineOptions.pdg fans out to the worker
894
+ // build gate (workerData.pdg) and the scope-resolution emit gate.
895
+ pdg: options.pdg === true,
896
+ pdgMaxFunctionLines: options.pdgMaxFunctionLines,
897
+ pdgMaxEdgesPerFunction: options.pdgMaxEdgesPerFunction,
898
+ pdgMaxReachingDefEdgesPerFunction: options.pdgMaxReachingDefEdgesPerFunction,
899
+ pdgMaxCdgEdgesPerFunction: options.pdgMaxCdgEdgesPerFunction,
900
+ pdgMaxTaintFindingsPerFunction: options.pdgMaxTaintFindingsPerFunction,
901
+ pdgMaxTaintHops: options.pdgMaxTaintHops,
902
+ pdgMaxInterprocFindings: options.pdgMaxInterprocFindings,
903
+ pdgMaxInterprocHops: options.pdgMaxInterprocHops,
904
+ pdgMaxInterprocEdges: options.pdgMaxInterprocEdges,
905
+ // Streaming/chunked PDG emit (#2202) — gated to full-rebuild runs
906
+ // (force === true) so the incremental writeback never reads back an
907
+ // offloaded BasicBlock layer. Memory-only; byte-identical output.
908
+ streamPdgEmit: resolveStreamPdgEmit(options),
909
+ pdgEmitChunkSize: resolvePdgEmitChunkSize(options),
910
+ fetchWrappers: options.fetchWrappers,
911
+ });
912
+ }
913
+ finally {
914
+ await moveFlowClient?.shutdown();
915
+ }
675
916
  // ── Phase 2: LadybugDB (60–85%) ──────────────────────────────────
676
917
  progress('lbug', 60, 'Loading into LadybugDB...');
677
918
  // Compute current per-file content hashes from the pipeline's File nodes.
@@ -686,22 +927,88 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
686
927
  }
687
928
  });
688
929
  const newFileHashes = await computeFileHashes(repoPath, allFilePaths);
930
+ const currentAnalysisFeatures = resolveAnalysisFeatureVersions(ANALYSIS_FEATURES, allFilePaths);
931
+ const currentAnalysisFeatureMismatches = existingMeta
932
+ ? findAnalysisFeatureMismatches(existingMeta.analysisFeatures, currentAnalysisFeatures)
933
+ : [];
934
+ if (existingMeta &&
935
+ currentAnalysisFeatureMismatches.length > 0 &&
936
+ !analysisFeatureMismatchLogged) {
937
+ // Covers a repository gaining or losing its first applicable source file:
938
+ // the persisted file list cannot predict that transition before the
939
+ // pipeline, but an incremental top-up would leave unchanged rows incomplete.
940
+ log(`analysis capabilities changed (${currentAnalysisFeatureMismatches.join(', ')}); ` +
941
+ `forcing a full rebuild so persisted feature evidence is complete.`);
942
+ options = { ...options, force: true };
943
+ }
689
944
  // Decide incremental vs full at THIS point (post-pipeline, pre-DB).
690
945
  // All eligibility conditions are checked here against the actual
691
946
  // pipeline output — no separate pre-pipeline prediction to desync from
692
947
  // (Bugbot review on PR #1479: a prediction that flipped post-pipeline
693
948
  // could skip the embedding cache load and then take the full-rebuild
694
949
  // path, silently losing embeddings).
695
- const isIncremental = !options.force &&
950
+ const incrementalCandidate = !options.force &&
696
951
  !!existingMeta &&
697
952
  existingMeta.schemaVersion === INCREMENTAL_SCHEMA_VERSION &&
953
+ currentAnalysisFeatureMismatches.length === 0 &&
698
954
  !!existingMeta.fileHashes &&
699
955
  Object.keys(existingMeta.fileHashes).length > 0 &&
700
956
  repoHasGit &&
701
957
  allFilePaths.length > 0;
702
- const hashDiff = isIncremental
958
+ const candidateHashDiff = incrementalCandidate
703
959
  ? diffFileHashes(newFileHashes, existingMeta.fileHashes)
704
960
  : undefined;
961
+ const moveCompilerInputsChanged = candidateHashDiff !== undefined &&
962
+ [...candidateHashDiff.changed, ...candidateHashDiff.added, ...candidateHashDiff.deleted].some(isMoveCompilerInputPath);
963
+ const isIncremental = incrementalCandidate && !moveCompilerInputsChanged;
964
+ const hashDiff = isIncremental ? candidateHashDiff : undefined;
965
+ if (incrementalCandidate && moveCompilerInputsChanged) {
966
+ log('Move compiler inputs changed; using a full rebuild for package-wide consistency.');
967
+ }
968
+ // #2 atomic index publish: on a full rebuild, build the fresh DB at a temp
969
+ // path and swap it over the live index in one rename at the very end, so a
970
+ // concurrent MCP reader opening mid-build only ever sees the previous
971
+ // complete index (never a wiped/half-built file) and a crash leaves the old
972
+ // index intact. The whole build flows through the singleton connection, so
973
+ // only initLbug/wipeLbugDbFiles below take the temp target.
974
+ //
975
+ // POSIX only: the common CLI/serve-worker analyze paths skip the native close
976
+ // (closeLbugBeforeExit, #2264) and leave the build handle open at swap time.
977
+ // POSIX renames an open file cleanly; a same-process open handle blocks the
978
+ // rename on Windows. Windows keeps the current in-place behavior
979
+ // (buildPath === lbugPath, no swap) until that is resolved (see §12/follow-up).
980
+ const isFullRebuild = !(isIncremental && hashDiff);
981
+ // Where the swap is allowed:
982
+ // - POSIX renames an open file, so the usual skip-native-close (#2264) is
983
+ // fine and the swap always applies.
984
+ // - Windows can swap only when a real close is safe to release the build
985
+ // handle before the rename — i.e. NOT a --pdg run (the #2264 destructor
986
+ // crash). Unverified on Windows CI; falls back to in-place otherwise.
987
+ const posixSwap = process.platform !== 'win32';
988
+ // #2614 Windows: the forced real-close before the rename re-bets that #2264 is
989
+ // --pdg-only, which is unproven (the CLI/worker skip the native close
990
+ // UNCONDITIONALLY) and unverifiable without a Windows runner. Keep it opt-in
991
+ // (GITNEXUS_ATOMIC_WINDOWS_SWAP=1) so the default Windows analyze stays on the
992
+ // proven in-place path; enable it only to test the Windows swap.
993
+ const windowsSwapOk = process.platform === 'win32' &&
994
+ options.pdg !== true &&
995
+ process.env.GITNEXUS_ATOMIC_WINDOWS_SWAP === '1';
996
+ // Incremental atomicity copies the whole index into the temp before mutating
997
+ // it, which negates incremental's speed premise — so it is opt-in
998
+ // (GITNEXUS_ATOMIC_INCREMENTAL=1) pending a benchmark. Full rebuilds always
999
+ // swap where the platform allows.
1000
+ const wantAtomicIncremental = isIncremental && !!hashDiff && process.env.GITNEXUS_ATOMIC_INCREMENTAL === '1';
1001
+ // #2614 F3: the copy-then-swap stages ONLY the main lbug file, so a live index
1002
+ // carrying an orphan .wal/.shadow (a silently-failed prior checkpoint) would
1003
+ // be copied incompletely and lose that delta. Only take the atomic path when
1004
+ // the live index is a consolidated single file; otherwise fall back to the
1005
+ // in-place writeback, which the next open replays correctly.
1006
+ const atomicIncremental = wantAtomicIncremental && (await inspectLbugSidecars(lbugPath)).kind === 'clean';
1007
+ if (wantAtomicIncremental && !atomicIncremental) {
1008
+ log('atomic-incremental: live index carries orphan sidecars — using in-place writeback');
1009
+ }
1010
+ const useAtomicSwap = (isFullRebuild || atomicIncremental) && (posixSwap || windowsSwapOk);
1011
+ const buildPath = useAtomicSwap ? `${lbugPath}.new` : lbugPath;
705
1012
  if (isIncremental && hashDiff) {
706
1013
  log(`Incremental: changed=${hashDiff.changed.length}, ` +
707
1014
  `added=${hashDiff.added.length}, ` +
@@ -709,13 +1016,25 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
709
1016
  `(skipping wipe + ${allFilePaths.length - hashDiff.toWrite.length} unchanged file rows preserved)`);
710
1017
  // Set the dirty flag BEFORE any destructive DB mutation. Cleared on
711
1018
  // success at the meta-save step. Scoped to this branch's meta.json.
1019
+ const now = Date.now();
712
1020
  await saveMeta(metaDir, {
713
1021
  ...existingMeta,
714
1022
  incrementalInProgress: {
715
- startedAt: Date.now(),
1023
+ startedAt: now,
1024
+ updatedAt: now,
1025
+ phase: 'pre-write',
716
1026
  toWriteCount: hashDiff.toWrite.length,
1027
+ directWriteCount: hashDiff.toWrite.length,
717
1028
  },
718
1029
  });
1030
+ if (atomicIncremental) {
1031
+ // Stage the live index into the temp so the in-place delete/writeback
1032
+ // below mutates the COPY, and the end-of-run swap publishes it atomically.
1033
+ // Clear any stale temp first (a crashed run), then copy the (consolidated,
1034
+ // single-file) live index. Whole-file copy — hence opt-in.
1035
+ await wipeLbugDbFiles(buildPath);
1036
+ await fs.copyFile(lbugPath, buildPath);
1037
+ }
719
1038
  }
720
1039
  else {
721
1040
  // Full rebuild path: wipe DB files first.
@@ -727,23 +1046,42 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
727
1046
  // pdg flip, certify zombie/missing BasicBlock rows indefinitely).
728
1047
  // toWriteCount: 0 is the full-path sentinel (no incremental write set).
729
1048
  if (existingMeta) {
1049
+ const now = Date.now();
730
1050
  await saveMeta(metaDir, {
731
1051
  ...existingMeta,
732
- incrementalInProgress: { startedAt: Date.now(), toWriteCount: 0 },
1052
+ incrementalInProgress: {
1053
+ startedAt: now,
1054
+ updatedAt: now,
1055
+ phase: 'full-rebuild',
1056
+ toWriteCount: 0,
1057
+ },
733
1058
  });
734
1059
  }
735
1060
  await closeLbug();
736
- const lbugFiles = [lbugPath, `${lbugPath}.wal`, `${lbugPath}.lock`];
737
- for (const f of lbugFiles) {
738
- try {
739
- await fs.rm(f, { recursive: true, force: true });
740
- }
741
- catch {
742
- /* swallow */
743
- }
744
- }
1061
+ // Shared loud wipe (#2409 + tri-review 4669518496 P2-4). The 4-file
1062
+ // family list — `.shadow` included, because a checkpoint-in-flight crash
1063
+ // leaves a shadow sidecar that is replay poison next to a freshly created
1064
+ // DB file — lives in wipeLbugDbFiles so this site and the escalation
1065
+ // valve below can never drift. Failures now throw a typed LbugWipeError
1066
+ // (ENOENT-verified removal) instead of silently letting initLbug reopen
1067
+ // a still-populated DB this run believes it wiped.
1068
+ //
1069
+ // With the atomic swap (POSIX), this wipes the TEMP build target
1070
+ // (`buildPath` = `<lbugPath>.new`, clearing any stragglers from a crashed
1071
+ // run) and leaves the live index untouched until the end-of-run swap. On
1072
+ // Windows buildPath === lbugPath, so this is the original in-place wipe.
1073
+ await wipeLbugDbFiles(buildPath);
745
1074
  }
746
- await initLbug(lbugPath);
1075
+ // Size the buffer pool to the graph just built by the pipeline (a page cache
1076
+ // over the on-disk index, which scales with node/edge count) instead of the
1077
+ // fixed 2 GiB default, whose eager commit dominates large-repo analyze. The
1078
+ // size is clamped to [COPY-safety floor, default], so it only ever shrinks
1079
+ // the pool; env override / no-hint paths are unchanged. See
1080
+ // resolveBufferManagerSize / estimateBufferPool.
1081
+ setBufferPoolSizeHint(estimateBufferPool(pipelineResult.graph.nodeCount + pipelineResult.graph.relationshipCount));
1082
+ // Full rebuild (POSIX) builds into the temp `buildPath`; incremental and
1083
+ // Windows use `buildPath === lbugPath` in place.
1084
+ await initLbug(buildPath);
747
1085
  // Manual WAL checkpoint driver (#1741): periodically drain the WAL
748
1086
  // from JS so the un-retriable native auto-checkpoint almost never
749
1087
  // has work left to do. Failures of the manual CHECKPOINT are absorbed
@@ -752,12 +1090,31 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
752
1090
  // Opt-out via `GITNEXUS_WAL_MANUAL_CHECKPOINT=0` (the driver itself
753
1091
  // returns a no-op handle when disabled). Analyze-only: MCP and serve
754
1092
  // paths continue to rely on the close-time CHECKPOINT in `safeClose`.
755
- const walCheckpointDriver = startWalCheckpointDriver();
1093
+ // `let`: the incremental branch's escalation valve (#2409) stops this driver
1094
+ // around its close→wipe→reopen strategy switch and starts a fresh one.
1095
+ let walCheckpointDriver = startWalCheckpointDriver();
756
1096
  try {
757
1097
  // All work after initLbug is wrapped in try/finally to ensure closeLbug()
758
1098
  // is called even if an error occurs — the module-level singleton DB handle
759
1099
  // must be released to avoid blocking subsequent invocations.
760
1100
  let lbugMsgCount = 0;
1101
+ // #2409 escalation valve outcome, hoisted above the incremental branch so
1102
+ // the vector-index recreation seam in Phase 4 below can tell "surgical
1103
+ // incremental" (DB files survived — the HNSW index with them) apart from
1104
+ // "escalated full write" (DB wiped, index destroyed) — tri-review
1105
+ // 4669518496 P1.
1106
+ let escalatedFullWrite = false;
1107
+ // Phase 3.5's restore scope (FIX 3 of this shipping review): on the
1108
+ // SURGICAL write plan this is the exact file set whose rows
1109
+ // deleteNodesForFiles just removed — only THOSE files' cached embedding
1110
+ // rows need re-inserting (everything else still sits in the DB, and
1111
+ // re-inserting it would PK-conflict). `null` means the DB was wiped
1112
+ // (full rebuild or escalated write): the embedding table is fresh and
1113
+ // every cached row must come back. Deriving this in memory replaces the
1114
+ // old whole-table `RETURN e.id` pre-read, which rescanned data this
1115
+ // process already holds and — worse — ran a read against the DB between
1116
+ // writeback and finalize for no recovery benefit.
1117
+ let deletedFilePathsForRestore = null;
761
1118
  if (isIncremental && hashDiff) {
762
1119
  // ── Incremental DB writeback ───────────────────────────────────
763
1120
  // 0. Expand the writable set with transitive importers of
@@ -782,21 +1139,50 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
782
1139
  // self-acknowledged as best-effort; `--force` remains the
783
1140
  // escape hatch documented in GUARDRAILS.md.
784
1141
  //
785
- // `queryImporters` reads `IMPORTS` from the pre-pipeline DB
1142
+ // `queryImportersBatch` reads `IMPORTS` from the pre-pipeline DB
786
1143
  // state, so the result is "files that USED TO import the
787
1144
  // target" — exactly the set whose previously-stored edges may
788
1145
  // no longer match what cross-file resolution produces this run.
789
1146
  const MAX_IMPORTER_BFS_DEPTH = 4;
1147
+ // Escalation thresholds (#2409) live with shouldEscalateIncrementalWrite
1148
+ // in incremental/escalation-gate.ts (pure predicate, boundary-tested).
790
1149
  const writableFiles = new Set(hashDiff.toWrite);
791
1150
  const directlyChangedCount = writableFiles.size;
792
- // Shadow-seed: for ADDED files, queryImporters returns 0 (the new
1151
+ const dirtyStartedAt = existingMeta.incrementalInProgress?.startedAt ?? Date.now();
1152
+ // Dropped-chunk observability (tri-review 4669518496 P2-5): counts
1153
+ // importer-BFS chunks whose IMPORTS query failed across ALL depths
1154
+ // (degrade-don't-fail — the expansion shrinks instead of the run
1155
+ // dying). Stamped into the #2410 crash diagnostics by
1156
+ // saveIncrementalDirtyState ITSELF (FIX 6 of this shipping review),
1157
+ // not by per-call-site spreads: the closure rebuilds its object from
1158
+ // scratch on every call, so a count riding along at only some sites
1159
+ // meant any newly added save site would silently erase it — exactly
1160
+ // the phases where #2409-class crashes happen. >0-only semantics
1161
+ // unchanged: unconditional zero-stamping would churn every
1162
+ // strict-equality consumer of the diagnostics shape.
1163
+ let droppedImporterChunks = 0;
1164
+ const saveIncrementalDirtyState = async (phase, extra = {}) => {
1165
+ await saveMeta(metaDir, {
1166
+ ...existingMeta,
1167
+ incrementalInProgress: {
1168
+ startedAt: dirtyStartedAt,
1169
+ updatedAt: Date.now(),
1170
+ phase,
1171
+ toWriteCount: writableFiles.size,
1172
+ directWriteCount: directlyChangedCount,
1173
+ ...(droppedImporterChunks > 0 ? { droppedImporterChunks } : {}),
1174
+ ...extra,
1175
+ },
1176
+ });
1177
+ };
1178
+ // Shadow-seed: for ADDED files, the importer query returns 0 (the new
793
1179
  // file has no IMPORTS rows in the pre-pipeline DB yet). But pre-
794
1180
  // existing unchanged files may have IMPORTS edges whose module-
795
1181
  // resolution claim the newcomer can steal under standard JS/TS
796
1182
  // resolution (Bugbot review on PR #1479). For each added file we
797
1183
  // derive the shadow candidates and, if the candidate was a known
798
1184
  // file in the prior meta, seed it into the BFS frontier so its
799
- // importers — surfaced via queryImporters — get their CALLS edges
1185
+ // importers — surfaced via the importer BFS — get their CALLS edges
800
1186
  // re-resolved against the new file. See shadow-candidates.ts for
801
1187
  // the full pattern catalogue.
802
1188
  const priorFileSet = new Set(existingMeta?.fileHashes ? Object.keys(existingMeta.fileHashes) : []);
@@ -809,28 +1195,33 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
809
1195
  }
810
1196
  }
811
1197
  {
1198
+ // Batched per depth level (#2409): one IN-list query per ~200-path
1199
+ // chunk instead of one query per frontier file — a ~700-file frontier
1200
+ // used to cost ~700 sequential lock-taking round-trips (~5.6s). The
1201
+ // closure is identical: importers already in writableFiles are not
1202
+ // re-frontiered, exactly like the per-file loop's membership check.
812
1203
  let frontier = [...hashDiff.toWrite, ...hashDiff.deleted, ...shadowSeed];
813
1204
  for (let depth = 0; depth < MAX_IMPORTER_BFS_DEPTH && frontier.length > 0; depth++) {
1205
+ const importers = await queryImportersBatch(frontier, {
1206
+ onChunkFailure: () => {
1207
+ droppedImporterChunks += 1;
1208
+ },
1209
+ });
814
1210
  const nextFrontier = [];
815
- for (const f of frontier) {
816
- try {
817
- const importers = await queryImporters(f);
818
- for (const i of importers) {
819
- if (!writableFiles.has(i)) {
820
- writableFiles.add(i);
821
- nextFrontier.push(i);
822
- }
823
- }
824
- }
825
- catch {
826
- /* per-file importer query failure → skip; correctness degrades on
827
- that branch, but DB stays writable. */
1211
+ for (const i of importers) {
1212
+ if (!writableFiles.has(i)) {
1213
+ writableFiles.add(i);
1214
+ nextFrontier.push(i);
828
1215
  }
829
1216
  }
830
1217
  frontier = nextFrontier;
831
1218
  }
832
1219
  }
833
1220
  const importerExpansion = writableFiles.size - directlyChangedCount;
1221
+ await saveIncrementalDirtyState('importer-bfs', {
1222
+ importerExpansion,
1223
+ shadowSeedCount: shadowSeed.length,
1224
+ });
834
1225
  if (importerExpansion > 0) {
835
1226
  log(`Incremental: +${importerExpansion} importer(s) added to writable set ` +
836
1227
  `(BFS depth ≤ ${MAX_IMPORTER_BFS_DEPTH}` +
@@ -848,69 +1239,221 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
848
1239
  // cross-file CALLS edges that the pre-run DB couldn't
849
1240
  // predict, e.g. a barrel re-export shifting `foo` from
850
1241
  // B to D).
851
- // The composed set is the input to BOTH deleteNodesForFile
1242
+ // The composed set is the input to BOTH deleteNodesForFiles
852
1243
  // and extractChangedSubgraph — asymmetry between the two would
853
1244
  // leave stale rows or PK-conflict at COPY time.
854
1245
  const effectiveWriteSet = computeEffectiveWriteSet(pipelineResult.graph, writableFiles);
1246
+ // `frameworkAnnotations` is derived from cross-file JVM visibility, so
1247
+ // an unchanged Class row can change when a same-package declaration is
1248
+ // added or removed without producing an IMPORTS edge. Compare the fresh
1249
+ // graph against the pre-write DB and rewrite only files whose persisted
1250
+ // value drifted. Add them after edge-boundary expansion: relationships
1251
+ // touching these files are already included by extractChangedSubgraph,
1252
+ // while pulling every unchanged neighbor would add no correctness.
1253
+ // Only supported Spring Bean source changes can alter this property;
1254
+ // avoid materializing every persisted Class row for unrelated language
1255
+ // updates. Check deleted paths too so removing/renaming a Java shadowing
1256
+ // declaration still refreshes unchanged Spring candidates.
1257
+ const beanSourceChanged = hashDiff.toWrite.some(isSpringBeanCandidateSourceFile) ||
1258
+ hashDiff.deleted.some(isSpringBeanCandidateSourceFile);
1259
+ if (beanSourceChanged) {
1260
+ const persistedFrameworkAnnotations = (await executeQuery('MATCH (c:Class) ' + 'RETURN c.id AS id, c.frameworkAnnotations AS frameworkAnnotations'));
1261
+ const frameworkAnnotationDriftFiles = collectFrameworkAnnotationDriftFiles(pipelineResult.graph, persistedFrameworkAnnotations);
1262
+ for (const filePath of frameworkAnnotationDriftFiles)
1263
+ effectiveWriteSet.add(filePath);
1264
+ if (frameworkAnnotationDriftFiles.size > 0) {
1265
+ log(`Incremental: +${frameworkAnnotationDriftFiles.size} file(s) added for ` +
1266
+ 'framework annotation property drift');
1267
+ }
1268
+ }
855
1269
  // Deduped: deleted entries may already appear via importer-BFS
856
- // expansion (queryImporters can return a now-deleted path), which
857
- // would otherwise call deleteNodesForFile twice for the same file
858
- // (Bugbot LOW finding on PR #1479).
1270
+ // expansion (the importer BFS can return a now-deleted path), which
1271
+ // would otherwise hand deleteNodesForFiles the same path twice in one
1272
+ // batch (Bugbot LOW finding on PR #1479).
859
1273
  const filesToDelete = [...new Set([...effectiveWriteSet, ...hashDiff.deleted])];
860
- for (let i = 0; i < filesToDelete.length; i++) {
861
- const f = filesToDelete[i];
862
- try {
863
- await deleteNodesForFile(f);
864
- }
865
- catch {
866
- /* file may not have rows (e.g. an unparseable file) — fine */
867
- }
868
- if (i % 20 === 0) {
869
- progress('lbug', 62, `Removing rows for changed files (${i}/${filesToDelete.length})...`);
1274
+ await saveIncrementalDirtyState('effective-write-set', {
1275
+ importerExpansion,
1276
+ shadowSeedCount: shadowSeed.length,
1277
+ effectiveWriteCount: effectiveWriteSet.size,
1278
+ deleteCount: filesToDelete.length,
1279
+ });
1280
+ // Escalation valve (#2409): when the effective write set covers most of
1281
+ // the repo, per-file surgery is strictly worse than the proven
1282
+ // wipe-and-bulk-COPY plan — the same data volume lands either way, but
1283
+ // the surgical plan pays per-table deletes plus COPY-into-non-empty
1284
+ // tables, and at this size it measured SLOWER than a full DB load. The
1285
+ // pipeline already produced the FULL graph (it always does), so only the
1286
+ // DB write plan changes here; fileHashes/meta bookkeeping is identical.
1287
+ // Thresholds + the AND-gate live in incremental/escalation-gate.ts.
1288
+ const writeFraction = effectiveWriteSet.size / Math.max(1, allFilePaths.length);
1289
+ // VECTOR gate (#2623) — load the extension BEFORE a single embedding row
1290
+ // is touched. `deleteNodesForFiles` below opens with the CodeEmbedding
1291
+ // join-delete, and LadybugDB refuses all DML on a table carrying its HNSW
1292
+ // index unless VECTOR is loaded on this connection; nothing else on this
1293
+ // path loads it until Phase 4, so every incremental run over a DB that
1294
+ // already built `code_embedding_idx` died here. Same seam the FTS drop
1295
+ // occupies at the head of this branch (#2589): index lifecycle first,
1296
+ // then rows. UNCONDITIONAL — not gated on `shouldGenerateEmbeddings` —
1297
+ // because a DB carrying the index from an earlier `--embeddings` run hits
1298
+ // the identical wall on a plain incremental run.
1299
+ //
1300
+ // When VECTOR genuinely cannot load, the table is immutable (the index
1301
+ // cannot be dropped without the extension either), so surgery is
1302
+ // impossible: fall through to the escalation valve's wipe-and-COPY plan,
1303
+ // which rebuilds the DB files outright and needs no embedding-row DML.
1304
+ const embeddingRowDmlSafe = await ensureEmbeddingRowDmlSafe();
1305
+ if (!embeddingRowDmlSafe && cachedEmbeddings.length === 0) {
1306
+ // The escalation below WIPES the DB files, and Phase 3.5 restores
1307
+ // embedding rows from `cachedEmbeddings` — which is only populated when
1308
+ // `deriveEmbeddingMode` saw `meta.stats.embeddings > 0`. A DB whose meta
1309
+ // under-reports its embeddings (meta restored from an older run, or a
1310
+ // count that never got stamped) would therefore have every vector
1311
+ // silently destroyed by a rebuild it did not ask for. Read them now,
1312
+ // while the DB is still intact — a plain MATCH, which needs no VECTOR
1313
+ // extension. Rows whose owning node is gone are dropped by Phase 3.5's
1314
+ // live-graph filter, exactly as on any other wiped path.
1315
+ const rescued = await loadCachedEmbeddings();
1316
+ if (rescued.embeddings.length > 0) {
1317
+ cachedEmbeddings = rescued.embeddings;
1318
+ cachedEmbeddingNodeIds = rescued.embeddingNodeIds;
1319
+ log(`Preserving ${rescued.embeddings.length} embedding row(s) across the forced rebuild ` +
1320
+ `(the index metadata did not account for them).`);
870
1321
  }
871
1322
  }
872
- // 2. Drop graph-wide nodes (Community, Process). They'll be re-inserted
873
- // from the fresh pipeline output below. Required for the
874
- // "Leiden runs on the FULL graph" correctness invariant.
875
- await deleteAllCommunitiesAndProcesses();
876
- // 2a. Drop INJECTS edges (DI collection injection, #2200) — their
877
- // validity is a whole-program property (a third-file change to the
878
- // interface or an implementer creates/invalidates edges between two
879
- // untouched files), so endpoint-writability extraction can't refresh
880
- // them; extractChangedSubgraph re-includes all of them from the
881
- // fresh graph (isGraphWideRelType). UNCONDITIONAL, next to the
882
- // Communities delete — NOT inside the `options.pdg` block below: the
883
- // di phase runs on every persisting analyze (same !skipGraphPhases
884
- // regime as communities/processes) while the graph-wide re-include
885
- // is unconditional, so a pdg-gated delete would append without
886
- // deleting on every non-pdg incremental run (N runs = N copies of
887
- // every INJECTS row; CodeRelation has no PK and no read-side dedup).
888
- await deleteAllInjects();
889
- // 2b. Drop interprocedural TAINT_PATH edges (#2084 M4 U6) when pdg is on
890
- // — their validity is a whole-program property (an A→C flow can be
891
- // invalidated by a change to an intermediate function on a third
892
- // file), so endpoint-writability extraction can't refresh them.
893
- // extractChangedSubgraph re-includes all of them from the fresh
894
- // graph (isGraphWideRelType), mirroring Community/Process.
895
- if (options.pdg === true) {
896
- await deleteAllInterprocTaintPaths();
897
- // 2c. Drop CALL_SUMMARY edges (PDG FU-C) on an incremental `--pdg`
898
- // writeback. They are re-included from the FULL fresh graph
899
- // (isGraphWideRelType) and the callSummaries phase recomputes every
900
- // summary each run, so delete-all-then-rebuild keeps an unchanged
901
- // function's summary from being lost — same contract as TAINT_PATH.
902
- await deleteAllCallSummaries();
1323
+ if (!embeddingRowDmlSafe ||
1324
+ shouldEscalateIncrementalWrite(filesToDelete.length, effectiveWriteSet.size, allFilePaths.length)) {
1325
+ escalatedFullWrite = true;
1326
+ log(!embeddingRowDmlSafe
1327
+ ? `Incremental: the ${EMBEDDING_TABLE_NAME} vector index exists but the VECTOR ` +
1328
+ `extension could not be loaded, so embedding rows cannot be rewritten in place — ` +
1329
+ `switching to a full DB write (wipe + bulk COPY) for this run. Semantic search ` +
1330
+ `falls back to exact scan until VECTOR is available; run \`gitnexus doctor\` for ` +
1331
+ `live extension status, or set GITNEXUS_LBUG_EXTENSION_INSTALL=auto to allow one ` +
1332
+ `bounded install attempt.`
1333
+ : `Incremental: effective write set covers ${effectiveWriteSet.size}/${allFilePaths.length} ` +
1334
+ // Display clamp only (predicate unchanged): BFS-found deleted
1335
+ // importers can push the numerator past the CURRENT file list, so
1336
+ // the raw fraction can exceed 1 — see the population-mismatch note
1337
+ // on shouldEscalateIncrementalWrite (tri-review 4669518496).
1338
+ `files (${Math.min(100, Math.round(writeFraction * 100))}%) — switching to a full DB write ` +
1339
+ `(wipe + bulk COPY) for this run; file-level incremental bookkeeping is unaffected.`);
1340
+ // toWriteCount: 0 is the established full-path dirty-flag sentinel;
1341
+ // the real counters ride along for crash diagnostics.
1342
+ await saveIncrementalDirtyState('escalated-full-write', {
1343
+ toWriteCount: 0,
1344
+ importerExpansion,
1345
+ shadowSeedCount: shadowSeed.length,
1346
+ effectiveWriteCount: effectiveWriteSet.size,
1347
+ deleteCount: filesToDelete.length,
1348
+ });
1349
+ // Strategy switch: stop the checkpoint driver around the close so its
1350
+ // in-flight CHECKPOINT can't race the reopen, drop the DB files
1351
+ // (sidecars included), and bulk-load the full graph into a fresh DB —
1352
+ // byte-for-byte the full-rebuild write plan. The wipe is the shared
1353
+ // ENOENT-verified helper (#2409 + tri-review 4669518496 P2-4): a
1354
+ // surviving family member throws a typed LbugWipeError here instead
1355
+ // of letting the reopen below resurrect the rows this run just chose
1356
+ // to replace wholesale.
1357
+ await walCheckpointDriver.stop();
1358
+ await closeLbug();
1359
+ await wipeLbugDbFiles(buildPath);
1360
+ await initLbug(buildPath);
1361
+ walCheckpointDriver = startWalCheckpointDriver();
1362
+ await loadGraphToLbug(pipelineResult.graph, pipelineResult.repoPath, storagePath, (msg) => {
1363
+ lbugMsgCount++;
1364
+ const pct = Math.min(84, 65 + Math.round((lbugMsgCount / (lbugMsgCount + 10)) * 19));
1365
+ progress('lbug', pct, msg);
1366
+ });
903
1367
  }
904
- // 3. Extract the changed subgraph from the FULL ctx.graph and write
905
- // only that. Unchanged-file rows in the DB stay untouched. Pass
906
- // the SAME effectiveWriteSet so the subgraph and the deletes
907
- // cover identical files (asymmetry would silently corrupt).
908
- const subgraph = extractChangedSubgraph(pipelineResult.graph, effectiveWriteSet);
909
- await loadGraphToLbug(subgraph, pipelineResult.repoPath, storagePath, (msg) => {
910
- lbugMsgCount++;
911
- const pct = Math.min(84, 65 + Math.round((lbugMsgCount / (lbugMsgCount + 10)) * 19));
912
- progress('lbug', pct, msg);
913
- });
1368
+ else {
1369
+ // 1a. Drop every FTS index before touching a single row (#2589).
1370
+ // `deleteNodesForFiles` below DETACH DELETEs rows out of tables
1371
+ // that otherwise still carry the FTS index built at the end of
1372
+ // the PREVIOUS analyze run — Phase 3 doesn't drop+rebuild it
1373
+ // until well after this delete completes. LadybugDB's FTS
1374
+ // extension is not proven to survive DML against an indexed
1375
+ // table (its own docs never demonstrate it), and that ordering
1376
+ // is exactly what produced "FTS index 'file_fts' is
1377
+ // inconsistent: term is missing during delete". Dropping first
1378
+ // removes the hazard outright; Phase 3's createSearchFTSIndexes
1379
+ // rebuilds every index from the final row set regardless, so
1380
+ // this is a no-op on its own drop step there.
1381
+ await dropSearchFTSIndexes();
1382
+ // 1b. Remove the write set's existing rows — batched (#2409): one
1383
+ // DETACH DELETE per table per 200-file chunk. The former per-file
1384
+ // loop issued a count + delete per table per FILE — ~13k
1385
+ // single-row write transactions on a ~700-file write set — which
1386
+ // made this phase slower than a full rebuild and is the WAL-append
1387
+ // storm behind the native mid-writeback deaths in #2409. Errors
1388
+ // are NOT swallowed anymore: a zero-match file is a no-op by
1389
+ // construction, so anything thrown is a real engine failure that
1390
+ // must surface instead of silently skipping (that silent skip was
1391
+ // how #2409 hid its root cause).
1392
+ progress('lbug', 62, `Removing rows for changed files (0/${filesToDelete.length})...`);
1393
+ await deleteNodesForFiles(filesToDelete, {
1394
+ onChunk: (done, total) => progress('lbug', 62, `Removing rows for changed files (${done}/${total})...`),
1395
+ });
1396
+ // Surgical path: Phase 3.5 restores exactly these files' embedding
1397
+ // rows (FIX 3). Sound because deleteNodesForFiles propagates errors
1398
+ // — reaching this line means every listed file's rows are gone
1399
+ // deterministically — and this process holds the exclusive DB lock,
1400
+ // so no concurrent writer can disturb the derivation.
1401
+ deletedFilePathsForRestore = new Set(filesToDelete);
1402
+ // 2. Drop graph-wide nodes (Community, Process). They'll be re-inserted
1403
+ // from the fresh pipeline output below. Required for the
1404
+ // "Leiden runs on the FULL graph" correctness invariant.
1405
+ await deleteAllCommunitiesAndProcesses();
1406
+ // 2a. Drop INJECTS edges (DI collection injection, #2200) — their
1407
+ // validity is a whole-program property (a third-file change to the
1408
+ // interface or an implementer creates/invalidates edges between two
1409
+ // untouched files), so endpoint-writability extraction can't refresh
1410
+ // them; extractChangedSubgraph re-includes all of them from the
1411
+ // fresh graph (isGraphWideRelType). UNCONDITIONAL, next to the
1412
+ // Communities delete — NOT inside the `options.pdg` block below: the
1413
+ // di phase runs on every persisting analyze (same !skipGraphPhases
1414
+ // regime as communities/processes) while the graph-wide re-include
1415
+ // is unconditional, so a pdg-gated delete would append without
1416
+ // deleting on every non-pdg incremental run (N runs = N copies of
1417
+ // every INJECTS row; CodeRelation has no PK and no read-side dedup).
1418
+ await deleteAllInjects();
1419
+ // 2b. Drop interprocedural TAINT_PATH edges (#2084 M4 U6) when pdg is on
1420
+ // — their validity is a whole-program property (an A→C flow can be
1421
+ // invalidated by a change to an intermediate function on a third
1422
+ // file), so endpoint-writability extraction can't refresh them.
1423
+ // extractChangedSubgraph re-includes all of them from the fresh
1424
+ // graph (isGraphWideRelType), mirroring Community/Process.
1425
+ if (options.pdg === true) {
1426
+ await deleteAllInterprocTaintPaths();
1427
+ // 2c. Drop CALL_SUMMARY edges (PDG FU-C) on an incremental `--pdg`
1428
+ // writeback. They are re-included from the FULL fresh graph
1429
+ // (isGraphWideRelType) and the callSummaries phase recomputes every
1430
+ // summary each run, so delete-all-then-rebuild keeps an unchanged
1431
+ // function's summary from being lost — same contract as TAINT_PATH.
1432
+ await deleteAllCallSummaries();
1433
+ }
1434
+ // 3. Extract the changed subgraph from the FULL ctx.graph and write
1435
+ // only that. Unchanged-file rows in the DB stay untouched. Pass
1436
+ // the SAME effectiveWriteSet so the subgraph and the deletes
1437
+ // cover identical files (asymmetry would silently corrupt).
1438
+ const subgraph = extractChangedSubgraph(pipelineResult.graph, effectiveWriteSet);
1439
+ await saveIncrementalDirtyState('load-graph', {
1440
+ importerExpansion,
1441
+ shadowSeedCount: shadowSeed.length,
1442
+ effectiveWriteCount: effectiveWriteSet.size,
1443
+ deleteCount: filesToDelete.length,
1444
+ });
1445
+ await loadGraphToLbug(subgraph, pipelineResult.repoPath, storagePath, (msg) => {
1446
+ lbugMsgCount++;
1447
+ const pct = Math.min(84, 65 + Math.round((lbugMsgCount / (lbugMsgCount + 10)) * 19));
1448
+ progress('lbug', pct, msg);
1449
+ });
1450
+ }
1451
+ // Boundary drain (#2409): checkpoint at the end of the incremental
1452
+ // writeback so the WAL it accumulated never lingers into the FTS and
1453
+ // embedding phases — a later crash leaves only post-checkpoint WAL for
1454
+ // the next open to replay. Near-instant when the periodic driver has
1455
+ // kept up; rides the driver's bounded retry via runCheckpointWithRetry.
1456
+ await checkpointOnce();
914
1457
  }
915
1458
  else {
916
1459
  // ── Full rebuild ───────────────────────────────────────────────
@@ -939,8 +1482,17 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
939
1482
  const ftsAvailable = await loadFTSExtension(undefined, {
940
1483
  policy: resolveAnalyzeInstallPolicy(),
941
1484
  });
1485
+ // Tracks whether search indexes actually ended up usable this run — starts
1486
+ // as ftsAvailable (extension loaded) but flips to false below when the
1487
+ // build/verify step itself fails, so capabilities.fts.status / ftsSkipped
1488
+ // stay honest even though that failure no longer aborts the whole analyze.
1489
+ let ftsReady = ftsAvailable;
942
1490
  if (ftsAvailable) {
943
- await createSearchFTSIndexes({
1491
+ // Degrade rather than throw: createSearchFTSIndexes re-tokenizes every
1492
+ // stored row on every run, so a native tokenizer error on a single
1493
+ // pre-existing row (#2544/#2546) must not discard this run's otherwise-
1494
+ // successful graph/embeddings work — only keyword search degrades.
1495
+ const ftsResult = await buildSearchIndexesOrDegrade(executeQuery, {
944
1496
  onIndexStart: options.verbose
945
1497
  ? (table, indexName) => log(`FTS: creating ${table}.${indexName}`)
946
1498
  : undefined,
@@ -948,31 +1500,61 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
948
1500
  ? (table, indexName) => log(`FTS: ready ${table}.${indexName}`)
949
1501
  : undefined,
950
1502
  });
951
- const missingIndexNames = await verifySearchFTSIndexes(executeQuery);
952
- if (missingIndexNames.length > 0) {
953
- throw new Error(`FTS verification failed - missing indexes after analyze: ${missingIndexNames.join(', ')}. ` +
954
- 'Check FTS extension availability, then retry `gitnexus analyze --force` for a full rebuild.');
1503
+ if (ftsResult.ok) {
1504
+ progress('fts', 90, 'Search indexes ready');
1505
+ }
1506
+ else {
1507
+ ftsReady = false;
1508
+ log(`FTS index build failed (${ftsResult.error}) — keyword search degraded this run. ` +
1509
+ 'Graph and embeddings analysis completed successfully. Run `gitnexus analyze --repair-fts` to retry.');
1510
+ progress('fts', 90, 'Search indexes skipped (build failed)');
955
1511
  }
956
- progress('fts', 90, 'Search indexes ready');
957
1512
  }
958
1513
  else {
959
- log(FTS_UNAVAILABLE_MESSAGE);
1514
+ // For a missing runtime dependency (#2374) the file is present, so the
1515
+ // generic "install it with network access" tail in FTS_UNAVAILABLE_MESSAGE
1516
+ // contradicts the remedy's own "reinstalling will NOT help" (#2383 F2). Lead
1517
+ // with the class-neutral sentence and append only the classified remedy.
1518
+ const ftsReason = getExtensionCapabilities().find((c) => c.name === 'fts')?.reason;
1519
+ const { kind, remedy } = diagnoseExtensionLoad(ftsReason);
1520
+ log(kind === 'missing_dependency'
1521
+ ? `${FTS_UNAVAILABLE_LEAD} ${remedy}`
1522
+ : FTS_UNAVAILABLE_MESSAGE);
960
1523
  progress('fts', 90, 'Search indexes skipped (FTS unavailable)');
961
1524
  }
962
1525
  // ── Phase 3.5: Re-insert cached embeddings ────────────────────────
963
1526
  // Runs on BOTH the full-rebuild path and the incremental path:
964
- // - Full rebuild: DB was wiped, every cached row needs to come back.
965
- // - Incremental: changed-file rows were just deleted by
966
- // deleteNodesForFile (which cascades to their
967
- // embedding rows) — so their cached vectors need
968
- // to come back too. Unchanged-file rows still
969
- // exist; re-inserting their cached vectors would
970
- // PK-conflict, but the per-batch try/catch below
971
- // silently ignores those (matches the existing
972
- // "some may fail if node was removed, that's
973
- // fine" semantics). Bugbot review on PR #1479
974
- // flagged that gating this on `!isIncremental`
975
- // silently lost changed-file embeddings.
1527
+ // - Full rebuild / escalated write: DB was wiped, every cached row
1528
+ // needs to come back.
1529
+ // - Incremental (surgical): changed/deleted files' rows were just
1530
+ // deleted by deleteNodesForFiles (a REAL delete since tri-review
1531
+ // 4669518496 P2-1 — it joins embedding rows through their owning
1532
+ // nodes), so changed-file vectors need to come back; unchanged-file
1533
+ // rows still exist. Bugbot review on PR #1479 flagged that gating
1534
+ // this on `!isIncremental` silently lost changed-file embeddings.
1535
+ //
1536
+ // Restore discipline (tri-review 4669518496 / KTD10, restore scope
1537
+ // derived in memory since FIX 3 of this shipping review) — filtered and
1538
+ // conflict-free, replacing the old insert-everything-and-swallow shape:
1539
+ // 1. Live-graph filter: rows whose nodeId no longer exists in the
1540
+ // freshly-built FULL graph are dropped. The cache was read BEFORE
1541
+ // the pipeline ran, so it still carries deleted files' rows —
1542
+ // re-inserting them resurrected orphans (wholesale onto the wiped
1543
+ // paths' empty table) now that the delete above is real.
1544
+ // 2. Restore-scope filter, derived WITHOUT touching the DB (the old
1545
+ // shape pre-read every surviving embedding id back out of the
1546
+ // table it had just written): on a wiped path
1547
+ // (`deletedFilePathsForRestore === null`) the table is fresh, so
1548
+ // every live row comes back; on the surgical path only rows whose
1549
+ // owning node's filePath is in the just-join-deleted set are
1550
+ // inserted — everything else still sits in the DB and would
1551
+ // PK-conflict. The derivation is sound because deleteNodesForFiles
1552
+ // propagates errors (a completed writeback means a deterministic
1553
+ // delete outcome) and this process holds the exclusive DB lock (no
1554
+ // concurrent writer).
1555
+ // The per-batch try/catch stays as a last-resort guard only — it no
1556
+ // longer fires on the happy path.
1557
+ let restoredEmbeddingCount = 0;
976
1558
  if (cachedEmbeddings.length > 0) {
977
1559
  const cachedDims = cachedEmbeddings[0].embedding.length;
978
1560
  const { EMBEDDING_DIMS } = await import('./lbug/schema.js');
@@ -983,16 +1565,65 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
983
1565
  cachedEmbeddingNodeIds = new Set();
984
1566
  }
985
1567
  else {
986
- progress('embeddings', 88, `Restoring ${cachedEmbeddings.length} cached embeddings...`);
987
1568
  const { batchInsertEmbeddings: batchInsert } = await import('./embeddings/embedding-pipeline.js');
1569
+ // (1) Live-graph filter — the FULL pipeline graph (always produced),
1570
+ // NOT the incremental subgraph, or unchanged files' rows would be
1571
+ // dropped from the restore set.
1572
+ const liveEmbeddings = cachedEmbeddings.filter((e) => pipelineResult.graph.getNode(e.nodeId) !== undefined);
1573
+ // (2) Restore-scope filter (see the discipline note above).
1574
+ const rowsToRestore = deletedFilePathsForRestore === null
1575
+ ? liveEmbeddings
1576
+ : liveEmbeddings.filter((e) => {
1577
+ const filePath = pipelineResult.graph.getNode(e.nodeId)?.properties?.filePath;
1578
+ return typeof filePath === 'string' && deletedFilePathsForRestore.has(filePath);
1579
+ });
1580
+ progress('embeddings', 88, `Restoring ${rowsToRestore.length} cached embeddings...`);
988
1581
  const EMBED_BATCH = 200;
989
- for (let i = 0; i < cachedEmbeddings.length; i += EMBED_BATCH) {
990
- const batch = cachedEmbeddings.slice(i, i + EMBED_BATCH);
1582
+ for (let i = 0; i < rowsToRestore.length; i += EMBED_BATCH) {
1583
+ const batch = rowsToRestore.slice(i, i + EMBED_BATCH);
991
1584
  try {
992
1585
  await batchInsert(executeWithReusedStatement, batch);
1586
+ restoredEmbeddingCount += batch.length;
993
1587
  }
994
1588
  catch {
995
- /* some may fail if node was removed, that's fine */
1589
+ /* last-resort guard — conflict-free by construction above */
1590
+ }
1591
+ }
1592
+ // Legacy-orphan sweep (FIX 3, finder B): the live-graph filter's
1593
+ // REJECTS — cached rows whose owning node no longer exists — are the
1594
+ // rows stranded by the era when the embedding delete was a no-op
1595
+ // (tri-review 4669518496 P2-1; schema version stays 6), plus this
1596
+ // run's just-deleted files' rows (already join-deleted above — the
1597
+ // exact-id DELETE matches nothing for those, so including them is a
1598
+ // harmless no-op rather than worth a fragile nodeId parse to
1599
+ // exclude). On the SURGICAL path the true legacy orphans still sit
1600
+ // in the DB and the node join can never reach them again (no owning
1601
+ // node), so delete them by exact row id. On wiped paths the rejects
1602
+ // were simply not restored — nothing to sweep. Legacy-tolerant: a
1603
+ // sweep failure must never fail a completed writeback, so the whole
1604
+ // sweep warns-and-continues.
1605
+ if (deletedFilePathsForRestore !== null) {
1606
+ const orphanRowIds = cachedEmbeddings
1607
+ .filter((e) => pipelineResult.graph.getNode(e.nodeId) === undefined)
1608
+ .map((e) => `${e.nodeId}:${e.chunkIndex}`);
1609
+ if (orphanRowIds.length > 0) {
1610
+ try {
1611
+ for (let i = 0; i < orphanRowIds.length; i += DELETE_FILES_CHUNK_SIZE) {
1612
+ const chunk = orphanRowIds.slice(i, i + DELETE_FILES_CHUNK_SIZE);
1613
+ const listLiteral = `[${chunk
1614
+ .map((id) => `'${escapeCypherString(id)}'`)
1615
+ .join(', ')}]`;
1616
+ await executeQuery(`MATCH (e:${EMBEDDING_TABLE_NAME}) WHERE e.id IN ${listLiteral} DELETE e`);
1617
+ }
1618
+ log(`Swept ${orphanRowIds.length} cached embedding row(s) with no live owning ` +
1619
+ 'node — legacy orphans stranded while the embedding delete was a no-op; ' +
1620
+ 'ids already removed with their files match nothing.');
1621
+ }
1622
+ catch (err) {
1623
+ log(`Warning: could not sweep ${orphanRowIds.length} orphaned embedding ` +
1624
+ `row(s) (${err.message}); they are unreachable by search ` +
1625
+ 'joins and will be retried next run.');
1626
+ }
996
1627
  }
997
1628
  }
998
1629
  }
@@ -1002,7 +1633,7 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
1002
1633
  let embeddingSkipped = true;
1003
1634
  let semanticMode;
1004
1635
  if (shouldGenerateEmbeddings) {
1005
- const { skipForCap, capDisabled, nodeLimit } = deriveEmbeddingCap(stats.nodes, options.embeddingsNodeLimit);
1636
+ const { skipForCap, capDisabled, nodeLimit } = deriveEmbeddingCap(stats.nodes, resumeEmbeddingCheckpoint ? 0 : options.embeddingsNodeLimit);
1006
1637
  if (!skipForCap) {
1007
1638
  embeddingSkipped = false;
1008
1639
  if (capDisabled && stats.nodes > DEFAULT_EMBEDDING_NODE_LIMIT) {
@@ -1019,11 +1650,50 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
1019
1650
  `\`--embeddings <n>\` to set a custom cap.`);
1020
1651
  }
1021
1652
  }
1653
+ // ── Vector-index recreation after a wipe-and-restore (tri-review
1654
+ // 4669518496 P1 / KTD1) ────────────────────────────────────────────
1655
+ // The full-rebuild and escalated-incremental write plans wipe the DB
1656
+ // files — the HNSW index with them. Phase 3.5 brought the embedding ROWS
1657
+ // back, but on a preserve-only run nothing recreates the index: semantic
1658
+ // search silently loses its vector lane (>10k-embedding repos return
1659
+ // empty under the exact-scan cap) while meta certified 'vector-index'.
1660
+ // Recreate it here, where every gate input is settled:
1661
+ // - restoredEmbeddingCount > 0 — rows actually came back;
1662
+ // - dbWasWiped — surgical incremental runs keep their index (HNSW
1663
+ // self-maintains on insert/delete); only wiped DBs lost it;
1664
+ // - embeddingSkipped — evaluated AFTER the deriveEmbeddingCap decision
1665
+ // above, NOT `!shouldGenerateEmbeddings`: when Phase 4 really runs,
1666
+ // the pipeline builds the index itself after all inserts (firing this
1667
+ // seam first would swap its bulk build for per-row live HNSW
1668
+ // maintenance on the hottest flow), while a capped >50k-node repo has
1669
+ // shouldGenerateEmbeddings=true yet never runs the pipeline — exactly
1670
+ // the case a naive gate would leave index-less again.
1671
+ // buildVectorIndex carries its own extension-policy gate and
1672
+ // warn-on-failure; the boolean feeds semanticMode so the finalize stamp
1673
+ // reflects the DB's ACTUAL state even when recreation fails (extension
1674
+ // unavailable → 'exact-scan').
1675
+ const dbWasWiped = !isIncremental || escalatedFullWrite;
1676
+ if (restoredEmbeddingCount > 0 && dbWasWiped && embeddingSkipped) {
1677
+ // Re-import at the seam rather than thread a mutable capture from
1678
+ // Phase 3.5 (FIX 3 of this shipping review — the captured function was
1679
+ // a fragile moving part): dynamic imports are memoized, and
1680
+ // `restoredEmbeddingCount > 0` proves Phase 3.5 already loaded the
1681
+ // module, so the lazy-embeddings convention (#2370) holds — no
1682
+ // embeddings module loads unless a restore actually happened.
1683
+ const { buildVectorIndex } = await import('./embeddings/embedding-pipeline.js');
1684
+ const vectorIndexReady = await buildVectorIndex();
1685
+ semanticMode = vectorIndexReady ? 'vector-index' : 'exact-scan';
1686
+ }
1022
1687
  if (!embeddingSkipped) {
1023
1688
  const { isHttpMode } = await import('./embeddings/http-client.js');
1024
1689
  const httpMode = isHttpMode();
1025
1690
  progress('embeddings', 90, httpMode ? 'Connecting to embedding endpoint...' : 'Loading embedding model...');
1026
1691
  const { runEmbeddingPipeline } = await import('./embeddings/embedding-pipeline.js');
1692
+ if (!embeddingIdentityForRun) {
1693
+ const { resolveEmbeddingIdentity } = await import('./embeddings/embedding-identity.js');
1694
+ embeddingIdentityForRun = resolveEmbeddingIdentity();
1695
+ }
1696
+ const embeddingIdentity = embeddingIdentityForRun;
1027
1697
  // Build a Map<nodeId, contentHash> from cached embeddings for incremental mode
1028
1698
  let existingEmbeddings;
1029
1699
  if (cachedEmbeddingNodeIds.size > 0) {
@@ -1032,6 +1702,43 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
1032
1702
  existingEmbeddings.set(e.nodeId, e.contentHash ?? STALE_HASH_SENTINEL);
1033
1703
  }
1034
1704
  }
1705
+ const saveEmbeddingCheckpoint = async (checkpoint, pendingNodeIds, embeddings) => {
1706
+ const fileHashes = {};
1707
+ for (const [key, value] of newFileHashes)
1708
+ fileHashes[key] = value;
1709
+ await saveMeta(metaDir, {
1710
+ ...(existingMeta ?? {}),
1711
+ repoPath,
1712
+ lastCommit: currentCommit,
1713
+ indexedAt: new Date().toISOString(),
1714
+ runnerIdentity,
1715
+ branch: branchLabel ?? existingMeta?.branch,
1716
+ remoteUrl: hasGitDir(repoPath) ? getRemoteUrl(repoPath) : undefined,
1717
+ stats: {
1718
+ files: pipelineResult.totalFileCount,
1719
+ nodes: stats.nodes,
1720
+ edges: stats.edges,
1721
+ communities: pipelineResult.communityResult?.stats.totalCommunities,
1722
+ processes: pipelineResult.processResult?.stats.totalProcesses,
1723
+ embeddings,
1724
+ },
1725
+ schemaVersion: hasGitDir(repoPath) ? INCREMENTAL_SCHEMA_VERSION : undefined,
1726
+ analysisFeatures: currentAnalysisFeatures,
1727
+ cjkSegmentation: getSearchFTSCjkSegmentation(),
1728
+ fileHashes: hasGitDir(repoPath) ? fileHashes : undefined,
1729
+ cacheKeys: [...parseCache.usedKeys],
1730
+ incrementalInProgress: undefined,
1731
+ embeddingCheckpoint: {
1732
+ at: new Date().toISOString(),
1733
+ ...checkpoint,
1734
+ model: embeddingIdentity.model,
1735
+ dimensions: embeddingIdentity.dimensions,
1736
+ provider: embeddingIdentity.provider,
1737
+ pendingNodeIds,
1738
+ },
1739
+ pdg: resolvePdgConfig(options),
1740
+ });
1741
+ };
1035
1742
  const embeddingResult = await runEmbeddingPipeline(executeQuery, executeWithReusedStatement, (p) => {
1036
1743
  const scaled = 90 + Math.round((p.percent / 100) * 8);
1037
1744
  const label = p.phase === 'loading-model'
@@ -1040,7 +1747,19 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
1040
1747
  : 'Loading embedding model...'
1041
1748
  : `Embedding ${p.nodesProcessed || 0}/${p.totalNodes || '?'}`;
1042
1749
  progress('embeddings', scaled, label);
1043
- }, {}, cachedEmbeddingNodeIds.size > 0 ? cachedEmbeddingNodeIds : undefined, existingEmbeddings);
1750
+ }, {}, cachedEmbeddingNodeIds.size > 0 ? cachedEmbeddingNodeIds : undefined, existingEmbeddings, {
1751
+ forceReembedNodeIds: pendingEmbeddingNodeIds,
1752
+ onCheckpointWindowStart: async ({ nodeIds, ...checkpoint }) => {
1753
+ await saveEmbeddingCheckpoint(checkpoint, nodeIds, existingMeta?.stats?.embeddings);
1754
+ },
1755
+ onCheckpoint: async (checkpoint) => {
1756
+ await checkpointOnce();
1757
+ const countResult = await executeQuery(`MATCH (e:${EMBEDDING_TABLE_NAME}) RETURN count(e) AS cnt`);
1758
+ const countRow = countResult?.[0];
1759
+ const embeddings = Number(countRow?.cnt ?? countRow?.[0] ?? 0);
1760
+ await saveEmbeddingCheckpoint(checkpoint, [], embeddings);
1761
+ },
1762
+ });
1044
1763
  if (embeddingResult.semanticMode === 'exact-scan') {
1045
1764
  semanticMode = 'exact-scan';
1046
1765
  log('Semantic embeddings were generated without a VECTOR index; ' +
@@ -1068,17 +1787,44 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
1068
1787
  }
1069
1788
  const { getRuntimeCapabilities } = await import('./platform/capabilities.js');
1070
1789
  const runtimeCapabilities = getRuntimeCapabilities();
1790
+ // `semanticMode` is authoritative when set (Phase 4 reported what it
1791
+ // built, or the wipe-and-restore seam above verified/recreated the index
1792
+ // — tri-review 4669518496 P1). When unset, prefer the PREVIOUS run's
1793
+ // persisted stamp over the platform capability (FIX 3, finder A): the
1794
+ // unset case is exactly a run that neither wiped nor generated — e.g. a
1795
+ // surgical incremental whose index survived in place — and such a run
1796
+ // cannot change whether the HNSW index exists, so carrying the persisted
1797
+ // observation forward is strictly more truthful than re-deriving from
1798
+ // what the platform COULD do. Only the two positive observations carry
1799
+ // ('vector-index'/'exact-scan'); 'unavailable'/absent falls through to
1800
+ // the platform default rather than pinning a stale negative.
1801
+ const persistedStatus = existingMeta?.capabilities?.vectorSearch.status;
1802
+ const persistedSemanticMode = persistedStatus === 'vector-index' || persistedStatus === 'exact-scan'
1803
+ ? persistedStatus
1804
+ : undefined;
1071
1805
  const effectiveSemanticMode = semanticMode ??
1806
+ persistedSemanticMode ??
1072
1807
  (runtimeCapabilities.semanticMode === 'vector-index' ? 'vector-index' : 'exact-scan');
1073
1808
  // Convert the post-run file-hash map to the on-disk Record<string,string>
1074
1809
  // shape consumed by RepoMeta.fileHashes.
1075
1810
  const newFileHashesRecord = {};
1076
1811
  for (const [k, v] of newFileHashes)
1077
1812
  newFileHashesRecord[k] = v;
1813
+ // Incremental runs never rewrite Move facts (changed Move compiler inputs
1814
+ // force a full rebuild), so when the compiler is transiently unavailable
1815
+ // the previous record still describes the persisted facts. Stamping
1816
+ // false/undefined here would force a needless full rebuild the moment the
1817
+ // compiler returns.
1818
+ const preserveMoveMeta = hasMovePackages && !moveFlow && isIncremental;
1819
+ // Annotated so the capabilities stamp below is compile-checked against
1820
+ // RepoMeta's status unions (tri-review 4669518496 P1/U3) — an unannotated
1821
+ // literal widens the vectorSearch.status ternary to `string` and the
1822
+ // honesty contract silently decays to "whatever interpolates".
1078
1823
  const meta = {
1079
1824
  repoPath,
1080
1825
  lastCommit: currentCommit,
1081
1826
  indexedAt: new Date().toISOString(),
1827
+ runnerIdentity,
1082
1828
  // Branch identity this index represents (#2106). Recorded for the flat
1083
1829
  // slot too (so resolveBranchPlacement knows which branch owns it). When
1084
1830
  // the label is null (detached HEAD / non-git re-analyze) we PRESERVE an
@@ -1110,7 +1856,7 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
1110
1856
  // meta.json / `gitnexus doctor` honest about degraded search.
1111
1857
  fts: {
1112
1858
  provider: 'ladybugdb-fts',
1113
- status: ftsAvailable ? runtimeCapabilities.fts : 'unavailable',
1859
+ status: ftsReady ? runtimeCapabilities.fts : 'unavailable',
1114
1860
  },
1115
1861
  vectorSearch: {
1116
1862
  provider: effectiveSemanticMode === 'vector-index' ? 'ladybugdb-vector' : 'exact-scan',
@@ -1124,17 +1870,30 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
1124
1870
  // incrementalInProgress to undefined explicitly clears any prior
1125
1871
  // dirty flag (full and incremental success paths converge here).
1126
1872
  schemaVersion: hasGitDir(repoPath) ? INCREMENTAL_SCHEMA_VERSION : undefined,
1873
+ analysisFeatures: currentAnalysisFeatures,
1127
1874
  // Always stamped with the live resolved mode (#2331/#2339) — unlike
1128
1875
  // `pdg` below, 'none' is a meaningful value to compare, not an
1129
1876
  // absence, so this is never conditionally omitted.
1130
1877
  cjkSegmentation: getSearchFTSCjkSegmentation(),
1131
1878
  fileHashes: hasGitDir(repoPath) ? newFileHashesRecord : undefined,
1879
+ moveIngestAvailable: preserveMoveMeta
1880
+ ? existingMeta?.moveIngestAvailable
1881
+ : moveIngestAvailable,
1882
+ moveCompilerIdentity: preserveMoveMeta
1883
+ ? existingMeta?.moveCompilerIdentity
1884
+ : moveFlow?.identity,
1885
+ moveFlowReleaseSelection: preserveMoveMeta
1886
+ ? existingMeta?.moveFlowReleaseSelection
1887
+ : moveFlow?.identity.source === 'release'
1888
+ ? desiredMoveFlowRelease
1889
+ : undefined,
1132
1890
  // This branch's full live chunk-key set (#2106 R6). `usedKeys` is every
1133
1891
  // chunk hash touched in this scan — cache HITS included (see parse-impl
1134
1892
  // usedKeys.add) — so it's complete even on an incremental run. Persisted
1135
1893
  // so a sibling branch's prune can union it and not evict our shards.
1136
1894
  cacheKeys: [...parseCache.usedKeys],
1137
1895
  incrementalInProgress: undefined,
1896
+ embeddingCheckpoint: undefined,
1138
1897
  // The effective pdg config this run's DB rows were built under
1139
1898
  // (#2099 F1). `undefined` on pdg-off runs — this meta is a fresh
1140
1899
  // literal (no spread of existingMeta), so omission is what CLEARS the
@@ -1142,7 +1901,18 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
1142
1901
  // off==off and incremental eligibility is restored.
1143
1902
  pdg: resolvePdgConfig(options),
1144
1903
  };
1145
- await saveMeta(metaDir, meta);
1904
+ // Re-resolve at the commit boundary. Long analyses can overlap an npm
1905
+ // upgrade, rebuilt dist tree, or native dependency replacement; stamping
1906
+ // the start-of-run receipt after such a mutation would falsely certify a
1907
+ // graph produced by two analyzer identities. Stable-read validation lives
1908
+ // inside the resolver, and a mismatch leaves the dirty flag intact so the
1909
+ // next run takes the established full-recovery path.
1910
+ meta.runnerIdentity = finalizeAnalyzerRunnerIdentity(import.meta.url, runnerIdentity);
1911
+ // #2614 F1: the freshness stamp (saveMeta) is written AFTER the atomic swap
1912
+ // below — never here — so a concurrent MCP reader can't observe
1913
+ // meta.indexedAt = T_new while lbugPath still resolves to the pre-swap
1914
+ // inode (which latched the reader on the stale index permanently). The meta
1915
+ // object is fully computed at this point; only its write is deferred.
1146
1916
  // Persist the incremental parse cache for the next run. Wraps in
1147
1917
  // try/catch so a cache-write failure never breaks an otherwise
1148
1918
  // successful indexing run. Prune stale chunk-hash entries first so
@@ -1266,14 +2036,61 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
1266
2036
  // LadybugDB destructor double-free after --pdg writes — closeLbugBeforeExit
1267
2037
  // CHECKPOINTs for durability then leaves the handles for process exit to
1268
2038
  // reclaim (#2264). Long-lived callers close for real.
1269
- await (options.skipNativeCloseOnExit ? closeLbugBeforeExit() : closeLbug());
2039
+ //
2040
+ // On Windows a swap must release the build handle before the rename (a
2041
+ // same-process open file can't be renamed), so it forces a real close —
2042
+ // safe because windowsSwapOk excludes --pdg (the #2264 case). POSIX renames
2043
+ // an open file, so it keeps the skip-native-close there.
2044
+ const forceRealCloseForSwap = useAtomicSwap && process.platform === 'win32';
2045
+ await (options.skipNativeCloseOnExit && !forceRealCloseForSwap
2046
+ ? closeLbugBeforeExit()
2047
+ : closeLbug());
2048
+ // #2 atomic publish: the fresh index was built at buildPath (a full rebuild,
2049
+ // or an opt-in atomic incremental that copied the live index in first). Swap
2050
+ // it over the live lbugPath in one rename so an MCP reader that opened
2051
+ // mid-build only ever saw the previous complete index — never a wiped/
2052
+ // half-built file. The close above checkpoint-consolidated buildPath to a
2053
+ // single file (no .wal), so the rename publishes a complete index; a reader
2054
+ // holding the old inode keeps a consistent stale snapshot until the pool
2055
+ // re-opens onto the new one (the pool staleness invalidation). Runs only on
2056
+ // success — a thrown error skips this, leaving the live index intact and the
2057
+ // temp build to be cleared by the next run's wipe.
2058
+ // Only publish if the build actually produced a DB at buildPath. A
2059
+ // degenerate run (empty repo, or a mocked pipeline that never opened the
2060
+ // store) leaves nothing to swap — skip rather than throw ENOENT.
2061
+ const builtDbExists = useAtomicSwap
2062
+ ? await fs.stat(buildPath).then(() => true, () => false)
2063
+ : false;
2064
+ if (useAtomicSwap && builtDbExists) {
2065
+ await retryRename(buildPath, lbugPath);
2066
+ // Clear any sidecars orphaned beside the replaced file. A cleanly-closed
2067
+ // prior index has none; a crashed one could, and it would be replay
2068
+ // poison next to the freshly published index. Best-effort.
2069
+ for (const suffix of ['.wal', '.shadow', '.wal.checkpoint']) {
2070
+ await fs.rm(`${lbugPath}${suffix}`, { force: true }).catch(() => { });
2071
+ }
2072
+ // #2614 F4: if the final checkpoint silently failed, the build may still
2073
+ // carry a residual .wal/.shadow under the temp name. MOVE it beside the
2074
+ // published index (not orphan/delete it) so the next open replays the
2075
+ // delta, rather than leaving it under a name LadybugDB never reconciles.
2076
+ for (const suffix of ['.wal', '.shadow']) {
2077
+ await fs.rename(`${buildPath}${suffix}`, `${lbugPath}${suffix}`).catch(() => { });
2078
+ }
2079
+ }
2080
+ // #2614 F1: stamp the freshness metadata now that the index is published.
2081
+ // When meta.indexedAt becomes visible, lbugPath already resolves to the new
2082
+ // inode, so a reader reiniting on the stamp opens the fresh graph rather
2083
+ // than latching on the old one. Leaving the dirty flag set across the swap
2084
+ // is a crash-safety improvement: a failed swap leaves the previous index
2085
+ // live and the next run recovers via the full-rebuild path.
2086
+ await saveMeta(metaDir, meta);
1270
2087
  progress('done', 100, 'Done');
1271
2088
  return {
1272
2089
  repoName: projectName,
1273
2090
  repoPath,
1274
2091
  stats: meta.stats,
1275
2092
  pipelineResult,
1276
- ftsSkipped: !ftsAvailable,
2093
+ ftsSkipped: !ftsReady,
1277
2094
  isPrimaryBranch: !placement.branch,
1278
2095
  };
1279
2096
  }