gitnexus 1.6.10-rc.2 → 1.6.10-rc.21

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 (219) hide show
  1. package/README.md +56 -12
  2. package/dist/cli/analyze.js +166 -5
  3. package/dist/cli/clean.js +17 -3
  4. package/dist/cli/cli-message.d.ts +1 -1
  5. package/dist/cli/doctor.d.ts +18 -0
  6. package/dist/cli/doctor.js +76 -3
  7. package/dist/cli/embeddings.d.ts +12 -0
  8. package/dist/cli/embeddings.js +49 -0
  9. package/dist/cli/help-i18n.js +4 -0
  10. package/dist/cli/i18n/en.d.ts +10 -5
  11. package/dist/cli/i18n/en.js +10 -5
  12. package/dist/cli/i18n/resources.d.ts +15 -5
  13. package/dist/cli/i18n/zh-CN.d.ts +5 -0
  14. package/dist/cli/i18n/zh-CN.js +10 -5
  15. package/dist/cli/index.js +12 -1
  16. package/dist/core/augmentation/engine.js +4 -3
  17. package/dist/core/embeddings/embedder.js +16 -2
  18. package/dist/core/embeddings/embedding-pipeline.d.ts +20 -0
  19. package/dist/core/embeddings/embedding-pipeline.js +10 -2
  20. package/dist/core/embeddings/http-client.d.ts +28 -1
  21. package/dist/core/embeddings/http-client.js +107 -15
  22. package/dist/core/embeddings/node-module-compat.d.ts +23 -0
  23. package/dist/core/embeddings/node-module-compat.js +23 -0
  24. package/dist/core/embeddings/onnxruntime-common-resolver.js +6 -3
  25. package/dist/core/embeddings/onnxruntime-node-resolver.js +31 -5
  26. package/dist/core/embeddings/runtime-install.d.ts +119 -0
  27. package/dist/core/embeddings/runtime-install.js +372 -0
  28. package/dist/core/embeddings/runtime-support.d.ts +48 -17
  29. package/dist/core/embeddings/runtime-support.js +96 -0
  30. package/dist/core/group/extractors/http-patterns/python.js +246 -108
  31. package/dist/core/group/extractors/manifest-extractor.js +4 -0
  32. package/dist/core/incremental/escalation-gate.d.ts +38 -0
  33. package/dist/core/incremental/escalation-gate.js +48 -0
  34. package/dist/core/incremental/shadow-candidates.d.ts +3 -3
  35. package/dist/core/incremental/shadow-candidates.js +3 -3
  36. package/dist/core/incremental/subgraph-extract.d.ts +2 -2
  37. package/dist/core/incremental/subgraph-extract.js +2 -2
  38. package/dist/core/ingestion/cobol/jcl-processor.js +9 -8
  39. package/dist/core/ingestion/cobol-processor.js +27 -26
  40. package/dist/core/ingestion/community-processor.d.ts +51 -1
  41. package/dist/core/ingestion/community-processor.js +316 -63
  42. package/dist/core/ingestion/emit-references.js +3 -2
  43. package/dist/core/ingestion/languages/cpp/captures.js +157 -74
  44. package/dist/core/ingestion/markdown-processor.js +3 -2
  45. package/dist/core/ingestion/parsing-processor.d.ts +3 -1
  46. package/dist/core/ingestion/parsing-processor.js +4 -0
  47. package/dist/core/ingestion/pipeline-phases/parse-impl.js +42 -0
  48. package/dist/core/ingestion/route-extractors/constant-resolver.d.ts +83 -0
  49. package/dist/core/ingestion/route-extractors/constant-resolver.js +131 -0
  50. package/dist/core/ingestion/route-extractors/python-const-resolver.d.ts +75 -0
  51. package/dist/core/ingestion/route-extractors/python-const-resolver.js +299 -0
  52. package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -1
  53. package/dist/core/ingestion/tree-sitter-queries.js +12 -1
  54. package/dist/core/ingestion/utils/line-base.d.ts +20 -0
  55. package/dist/core/ingestion/utils/line-base.js +20 -0
  56. package/dist/core/ingestion/utils/symbol-labels.d.ts +21 -0
  57. package/dist/core/ingestion/utils/symbol-labels.js +45 -0
  58. package/dist/core/ingestion/workers/parse-worker.d.ts +34 -0
  59. package/dist/core/ingestion/workers/parse-worker.js +40 -5
  60. package/dist/core/ingestion/workers/result-merge.js +4 -0
  61. package/dist/core/ingestion/workers/worker-pool.d.ts +14 -0
  62. package/dist/core/ingestion/workers/worker-pool.js +85 -5
  63. package/dist/core/lbug/csv-generator.js +8 -2
  64. package/dist/core/lbug/cypher-escape.d.ts +20 -0
  65. package/dist/core/lbug/cypher-escape.js +20 -0
  66. package/dist/core/lbug/extension-load-error.d.ts +67 -0
  67. package/dist/core/lbug/extension-load-error.js +320 -0
  68. package/dist/core/lbug/extension-loader.d.ts +17 -2
  69. package/dist/core/lbug/extension-loader.js +38 -13
  70. package/dist/core/lbug/lbug-adapter.d.ts +147 -1
  71. package/dist/core/lbug/lbug-adapter.js +390 -70
  72. package/dist/core/lbug/lbug-config.d.ts +28 -0
  73. package/dist/core/lbug/lbug-config.js +114 -4
  74. package/dist/core/lbug/native-check.d.ts +22 -0
  75. package/dist/core/lbug/native-check.js +66 -0
  76. package/dist/core/lbug/pool-adapter.js +10 -1
  77. package/dist/core/lbug/sidecar-recovery.d.ts +172 -0
  78. package/dist/core/lbug/sidecar-recovery.js +418 -16
  79. package/dist/core/platform/capabilities.js +27 -1
  80. package/dist/core/run-analyze.js +470 -108
  81. package/dist/core/search/fts-indexes.d.ts +7 -0
  82. package/dist/core/search/fts-indexes.js +37 -0
  83. package/dist/core/tree-sitter/safe-parse.d.ts +8 -3
  84. package/dist/core/tree-sitter/safe-parse.js +21 -7
  85. package/dist/core/wiki/graph-queries.js +6 -5
  86. package/dist/mcp/core/embedder.js +16 -2
  87. package/dist/mcp/local/line-display.d.ts +22 -0
  88. package/dist/mcp/local/line-display.js +3 -0
  89. package/dist/mcp/local/local-backend.d.ts +14 -0
  90. package/dist/mcp/local/local-backend.js +63 -19
  91. package/dist/mcp/local/pdg-impact.d.ts +5 -3
  92. package/dist/mcp/local/pdg-impact.js +5 -2
  93. package/dist/mcp/resources.js +1 -0
  94. package/dist/mcp/tools.js +1 -1
  95. package/dist/server/analyze-launch.d.ts +6 -0
  96. package/dist/server/analyze-launch.js +82 -6
  97. package/dist/server/api.d.ts +30 -0
  98. package/dist/server/api.js +63 -21
  99. package/dist/storage/parse-cache.js +1 -1
  100. package/dist/storage/repo-manager.d.ts +65 -1
  101. package/dist/storage/repo-manager.js +18 -1
  102. package/hooks/antigravity/gitnexus-antigravity-hook.cjs +50 -3
  103. package/hooks/claude/gitnexus-hook.cjs +59 -9
  104. package/package.json +5 -3
  105. package/scripts/cross-platform-tests.ts +43 -0
  106. package/scripts/ensure-fts.ts +32 -0
  107. package/scripts/install-duckdb-extension.mjs +85 -24
  108. package/scripts/run-cross-platform.ts +46 -6
  109. package/scripts/shard-arg.ts +30 -0
  110. package/web/assets/ProcessFlowModal-Dna7GkwO.js +22 -0
  111. package/web/assets/{agent-Dli_x7bU.js → agent-DNYDy-KA.js} +49 -49
  112. package/web/assets/architecture-7EHR7CIX-6QZW5X65-BVWFvYz2.js +1 -0
  113. package/web/assets/architectureDiagram-UL44E2DR-BU5ZRITg.js +36 -0
  114. package/web/assets/{blockDiagram-7IZFK4PR-BkWhpzWB.js → blockDiagram-7IZFK4PR-Dut1BpTV.js} +2 -2
  115. package/web/assets/{c4Diagram-Y2BXMSZH-DtBvpzWD.js → c4Diagram-Y2BXMSZH-DOXVs7-C.js} +2 -2
  116. package/web/assets/chunk-2T2R6R2M-pkw2oXHH.js +4 -0
  117. package/web/assets/chunk-2UTLFMKG-CXlhF3Pt.js +1 -0
  118. package/web/assets/chunk-3SSMPTDK-rPsRM1x0.js +321 -0
  119. package/web/assets/chunk-4R4BOZG6-Be_F0JoX.js +159 -0
  120. package/web/assets/{chunk-6764PJDD-DpIS7hxD.js → chunk-6764PJDD-C7xxk063.js} +1 -1
  121. package/web/assets/{chunk-67TQ5CYL-4sNPJU1q.js → chunk-67TQ5CYL-zxhDWgyF.js} +3 -3
  122. package/web/assets/chunk-7FYTHRHK-CsmYIswB.js +32 -0
  123. package/web/assets/chunk-7J6CGLKN-AmkN8jJN.js +10 -0
  124. package/web/assets/chunk-7W6UQGC5-pyKGssV3.js +1 -0
  125. package/web/assets/chunk-C62D2QBJ-3gdL8EKc.js +1 -0
  126. package/web/assets/chunk-CEXFNPSA-CjwtMDkc.js +1 -0
  127. package/web/assets/{chunk-INKRHTLW-Nu4ri9P-.js → chunk-INKRHTLW-D9BM9HWZ.js} +1 -1
  128. package/web/assets/chunk-J5EP6P6S-L19oz55R.js +1 -0
  129. package/web/assets/{chunk-KGFNY3KK-JGMJvKCK.js → chunk-KGFNY3KK-tD2F6wxK.js} +1 -1
  130. package/web/assets/chunk-KGYTTC2M-zm6n242I.js +1 -0
  131. package/web/assets/{chunk-KRXBNO2N-BW5o0KUN.js → chunk-KRXBNO2N-C-UNuKKf.js} +1 -1
  132. package/web/assets/{chunk-LCXTWHL2-CfLLCywY.js → chunk-LCXTWHL2-Ba9JK-6F.js} +2 -2
  133. package/web/assets/{chunk-LII3EMHJ-D-jm-dLa.js → chunk-LII3EMHJ-EGx-eBIu.js} +1 -1
  134. package/web/assets/chunk-LRIF4GLE-C3_PqUBq.js +1 -0
  135. package/web/assets/{chunk-QA3QBVWF-BgRW3SXd.js → chunk-QA3QBVWF-D4cSJBf6.js} +1 -1
  136. package/web/assets/chunk-RERM46MO-B5dAdWpP.js +1 -0
  137. package/web/assets/{chunk-RG4AUYOV-CqWzWsV3.js → chunk-RG4AUYOV-OqBBMwuQ.js} +4 -4
  138. package/web/assets/chunk-RKZBBQEN-MyIQbGgD.js +1 -0
  139. package/web/assets/chunk-RLI5ZMPA-CkXZ0y0U.js +1 -0
  140. package/web/assets/{chunk-T2UQINTJ-DSBqkzH7.js → chunk-T2UQINTJ-BJusEiGe.js} +1 -1
  141. package/web/assets/chunk-UP6H54XL-BG-n7qDp.js +1 -0
  142. package/web/assets/chunk-UXSXWOXI-CW1_r5cS.js +1 -0
  143. package/web/assets/{chunk-UY5QBCOK-DyF0vyGd.js → chunk-UY5QBCOK-BCpUlARQ.js} +1 -1
  144. package/web/assets/chunk-VU6ZFW4Y-BeXVk45_.js +2 -0
  145. package/web/assets/{chunk-W44A43WB-JqMLIpR0.js → chunk-W44A43WB-BN8EQc_u.js} +1 -1
  146. package/web/assets/{chunk-ZXARS5L4-CxN8oiwI.js → chunk-ZXARS5L4-BI_qR2Nm.js} +1 -1
  147. package/web/assets/classDiagram-KGZ6W3CR-D9zSIg0x.js +1 -0
  148. package/web/assets/classDiagram-v2-72OJOZXJ-D9zSIg0x.js +1 -0
  149. package/web/assets/cose-bilkent-UX7MHV2Q-BmnZ7Mvj.js +1 -0
  150. package/web/assets/dagre-ND4H6XIP-CpcV5kbl.js +4 -0
  151. package/web/assets/diagram-3NCE3AQN-6SMxBRpj.js +43 -0
  152. package/web/assets/diagram-GF46GFSD-BZEdNcRZ.js +24 -0
  153. package/web/assets/{diagram-HNR7UZ2L-DFCfd5LI.js → diagram-HNR7UZ2L-DxKW2a2Y.js} +2 -2
  154. package/web/assets/diagram-QXG6HAR7-HfTHcXX_.js +24 -0
  155. package/web/assets/diagram-WEQXMOUZ-CyijUS8r.js +10 -0
  156. package/web/assets/{erDiagram-L5TCEMPS-8X1y6R4Y.js → erDiagram-L5TCEMPS-D0fvpWEX.js} +3 -3
  157. package/web/assets/eventmodeling-FCH6USID-MREXMVOE-CgFBM7e-.js +1 -0
  158. package/web/assets/flowDiagram-H6V6AXG4-Bvr9bbhk.js +162 -0
  159. package/web/assets/{ganttDiagram-JCBTUEKG-D3iL2Aet.js → ganttDiagram-JCBTUEKG-TzODrLzv.js} +2 -2
  160. package/web/assets/gitGraph-WXDBUCRP-R675I2BI-DCJv7SII.js +1 -0
  161. package/web/assets/gitGraphDiagram-S2ZK5IYY-ONQSwJIY.js +106 -0
  162. package/web/assets/index-B4eB4dNZ.js +575 -0
  163. package/web/assets/index-CX_fADmQ.css +2 -0
  164. package/web/assets/info-J43DQDTF-KCYPFFUO-BfNy-9Nd.js +1 -0
  165. package/web/assets/infoDiagram-3YFTVSEB-DIPVUIQ6.js +2 -0
  166. package/web/assets/{ishikawaDiagram-BNXS4ZKH-DfYpxnE5.js → ishikawaDiagram-BNXS4ZKH-BLjivjXL.js} +3 -3
  167. package/web/assets/{journeyDiagram-M6C3CM3L-CvArszLo.js → journeyDiagram-M6C3CM3L-Cetgs3NH.js} +3 -3
  168. package/web/assets/{kanban-definition-75IXJCU3-CpfVLLf9.js → kanban-definition-75IXJCU3-DMlCaIpf.js} +5 -5
  169. package/web/assets/{katex-K3KEBU37-CTc5BslQ.js → katex-K3KEBU37-qRQBy-EZ.js} +2 -2
  170. package/web/assets/{mindmap-definition-2TDM6QVE-DypvbOMQ.js → mindmap-definition-2TDM6QVE-B4GbXQBc.js} +3 -3
  171. package/web/assets/packet-YPE3B663-LP52Z2RK-D4qkeG9k.js +1 -0
  172. package/web/assets/pie-LRSECV5Y-TCRJHUBD-B_yMKyQa.js +1 -0
  173. package/web/assets/{pieDiagram-CU6KROY3-s27EFWgP.js → pieDiagram-CU6KROY3-D3YC_xyg.js} +2 -2
  174. package/web/assets/{quadrantDiagram-VICAPDV7-yXSZ2lLz.js → quadrantDiagram-VICAPDV7-B1TQnlm0.js} +2 -2
  175. package/web/assets/radar-GUYGQ44K-RDLRG3WG-DeD6yN_0.js +1 -0
  176. package/web/assets/{requirementDiagram-JXO7QTGE-CWhqInuf.js → requirementDiagram-JXO7QTGE-f8wczLQf.js} +3 -3
  177. package/web/assets/rolldown-runtime-QTnfLwEv.js +1 -0
  178. package/web/assets/{sankeyDiagram-URQDO5SZ-1qEjlFIB.js → sankeyDiagram-URQDO5SZ-BTxzG5j0.js} +2 -2
  179. package/web/assets/{sequenceDiagram-VS2MUI6T-RyTlZ5cR.js → sequenceDiagram-VS2MUI6T-DuamM-IR.js} +5 -5
  180. package/web/assets/src-BNCXICdA.js +1 -0
  181. package/web/assets/stateDiagram-7D4R322I-Ch2pEUcw.js +1 -0
  182. package/web/assets/stateDiagram-v2-36443NZ5-7ANuoZLL.js +1 -0
  183. package/web/assets/{timeline-definition-O6YCAMPW-DrrqLYAf.js → timeline-definition-O6YCAMPW-07BR8R8G.js} +3 -3
  184. package/web/assets/treeView-BLDUP644-QA4HXRO3-CDuSYnd4.js +1 -0
  185. package/web/assets/treemap-LRROVOQU-LLAWBHMP-imazucyJ.js +1 -0
  186. package/web/assets/{vennDiagram-MWXL3ELB-B0RkWAim.js → vennDiagram-MWXL3ELB-BQX2ossk.js} +4 -4
  187. package/web/assets/wardley-L42UT6IY-5TKZOOLJ-DqrT9tB8.js +1 -0
  188. package/web/assets/{wardleyDiagram-CUQ6CDDI-boUxiQ5F.js → wardleyDiagram-CUQ6CDDI-CGIJBBiK.js} +3 -3
  189. package/web/assets/{xychartDiagram-N2JHSOCM-BOLm0eNm.js → xychartDiagram-N2JHSOCM-B3HNMuWp.js} +4 -4
  190. package/web/index.html +17 -10
  191. package/web/assets/architecture-7EHR7CIX-6QZW5X65-DxJw65fT.js +0 -1
  192. package/web/assets/architectureDiagram-UL44E2DR-dEkqUZN9.js +0 -36
  193. package/web/assets/chunk-3SSMPTDK-B4_etUhr.js +0 -321
  194. package/web/assets/chunk-7W6UQGC5-KyEG0HQg.js +0 -1
  195. package/web/assets/chunk-KGYTTC2M-CWC_c3H9.js +0 -161
  196. package/web/assets/classDiagram-KGZ6W3CR-CMne4tG9.js +0 -1
  197. package/web/assets/classDiagram-v2-72OJOZXJ-CMne4tG9.js +0 -1
  198. package/web/assets/cose-bilkent-UX7MHV2Q-D71wNYRJ.js +0 -1
  199. package/web/assets/dagre-ND4H6XIP-Co5rRx9X.js +0 -4
  200. package/web/assets/diagram-3NCE3AQN-DBp4O00j.js +0 -43
  201. package/web/assets/diagram-GF46GFSD-6N_8yS0i.js +0 -24
  202. package/web/assets/diagram-QXG6HAR7-Di093rFg.js +0 -24
  203. package/web/assets/diagram-WEQXMOUZ-DZWi4SUN.js +0 -10
  204. package/web/assets/eventmodeling-FCH6USID-MREXMVOE-BQm9QzEa.js +0 -1
  205. package/web/assets/flowDiagram-H6V6AXG4-CMpY8Ufn.js +0 -162
  206. package/web/assets/gitGraph-WXDBUCRP-R675I2BI-Cr_Bm2Nb.js +0 -1
  207. package/web/assets/gitGraphDiagram-S2ZK5IYY-CPIDgxGm.js +0 -106
  208. package/web/assets/index-KctnI3Vq.js +0 -635
  209. package/web/assets/index-_lgn7hs5.css +0 -2
  210. package/web/assets/info-J43DQDTF-KCYPFFUO-Cs4p2oyk.js +0 -1
  211. package/web/assets/infoDiagram-3YFTVSEB-BaLMZkUg.js +0 -2
  212. package/web/assets/packet-YPE3B663-LP52Z2RK-CKCrztD2.js +0 -1
  213. package/web/assets/pie-LRSECV5Y-TCRJHUBD-BB7pHqoD.js +0 -1
  214. package/web/assets/radar-GUYGQ44K-RDLRG3WG-DhoTOcuK.js +0 -1
  215. package/web/assets/stateDiagram-7D4R322I-DTjVfX4A.js +0 -1
  216. package/web/assets/stateDiagram-v2-36443NZ5-xY9_k9_Q.js +0 -1
  217. package/web/assets/treeView-BLDUP644-QA4HXRO3-ODehyGKL.js +0 -1
  218. package/web/assets/treemap-LRROVOQU-LLAWBHMP-BfpgC-7o.js +0 -1
  219. package/web/assets/wardley-L42UT6IY-5TKZOOLJ-CNeL6VHE.js +0 -1
@@ -6,14 +6,16 @@ import { finished } from 'stream/promises';
6
6
  import path from 'path';
7
7
  import lbug from '@ladybugdb/core';
8
8
  import { closeQueryResults } from './query-result-utils.js';
9
+ import { escapeCypherString } from './cypher-escape.js';
9
10
  import { withConnLock } from './conn-lock.js';
10
11
  import { isWalDriverActive } from './wal-driver-state.js';
11
12
  import { NODE_TABLES, REL_TABLE_NAME, SCHEMA_QUERIES, EMBEDDING_TABLE_NAME, CREATE_VECTOR_INDEX_QUERY, STALE_HASH_SENTINEL, } from './schema.js';
12
13
  import { streamAllCSVsToDisk } from './csv-generator.js';
13
14
  import { getNodeLabel as deriveNodeLabel } from './rel-pair-routing.js';
15
+ import { EMBEDDABLE_LABELS } from '../embeddings/types.js';
14
16
  import { extensionManager } from './extension-loader.js';
15
- import { classifyDeleteAllError, closeLbugConnection, isDbBusyError, isOpenRetryExhausted, isWalCorruptionError, openLbugConnection, toNativeSafePath, resolveNativeSafeStorageDir, WAL_RECOVERY_SUGGESTION, waitForWindowsHandleRelease, } from './lbug-config.js';
16
- import { finalizeLbugSidecarsAfterClose, inspectLbugSidecars, isMissingShadowSidecarError, isReadOnlyShadowReplayError, preflightLbugSidecars, quarantineWalForMissingShadow, renameFailureMessage, shadowSidecarRecoveryMessage, } from './sidecar-recovery.js';
17
+ import { classifyDeleteAllError, closeLbugConnection, HANDLE_RELEASE_PROBE_ATTEMPTS, HANDLE_RELEASE_PROBE_DELAY_MS, isDbBusyError, isOpenRetryExhausted, isWalCorruptionError, openLbugConnection, sleep, toNativeSafePath, resolveNativeSafeStorageDir, WAL_RECOVERY_SUGGESTION, waitForWindowsHandleRelease, } from './lbug-config.js';
18
+ import { finalizeLbugSidecarsAfterClose, guardWalQuarantine, isMissingShadowSidecarError, isReadOnlyShadowReplayError, lbugLockRemediation, preflightLbugSidecars, quarantineWalForMissingShadow, renameFailureMessage, shadowSidecarRecoveryMessage, } from './sidecar-recovery.js';
17
19
  import { isVectorExtensionSupportedByPlatform } from '../platform/capabilities.js';
18
20
  import { logger } from '../logger.js';
19
21
  /**
@@ -409,23 +411,14 @@ const queryAndDrain = async (targetConn, cypher) => {
409
411
  };
410
412
  const READ_ONLY_SHADOW_REPLAY_PROBE = 'MATCH (n) RETURN n LIMIT 1';
411
413
  /**
412
- * Reject the quarantine path when the orphan WAL is too large to safely
413
- * discard (>TINY_ORPHAN_WAL_BYTES). Mirrors the preflight policy at
414
- * sidecar-recovery.ts:153-160 ("warn, do not quarantine"). Symmetric across
415
- * read-only and writable recovery paths (PR #1747 review D2).
416
- *
417
- * Throws shadowSidecarRecoveryMessage immediately when the WAL is large,
418
- * preserving the uncheckpointed pages for explicit operator recovery.
419
- * Returns silently when the WAL is absent, tiny, or in any other state
420
- * where the existing recovery path is safe to proceed.
414
+ * Serve-side entry to the shared WAL-quarantine safety gate. Refuses (throws)
415
+ * when the `.shadow` is present on disk or the orphan WAL is too large to
416
+ * safely discard; returns silently otherwise. The policy itself lives in
417
+ * `guardWalQuarantine` (sidecar-recovery.ts) so serve and the MCP pool share
418
+ * one source of truth (PR #1747 review D2; issue #2382 review, Finding B).
421
419
  */
422
420
  const refuseLargeWalQuarantine = async (dbPath, mode, triggeringErr) => {
423
- const state = await inspectLbugSidecars(dbPath);
424
- if (state.kind === 'orphan-wal') {
425
- logger.warn(`GitNexus: refusing to quarantine large WAL (${state.walBytes} bytes) at ${dbPath}.wal during ${mode} recovery; ` +
426
- 'manual recovery required — run `gitnexus analyze --force <repo-path> --index-only`.');
427
- throw new Error(shadowSidecarRecoveryMessage(dbPath, triggeringErr));
428
- }
421
+ await guardWalQuarantine(dbPath, mode, triggeringErr, logger);
429
422
  };
430
423
  const reopenReadOnlyAfterMissingShadow = async (dbPath, err) => {
431
424
  await refuseLargeWalQuarantine(dbPath, 'read-only', err);
@@ -1091,8 +1084,43 @@ const BACKTICK_TABLES = new Set([
1091
1084
  const escapeTableName = (table) => {
1092
1085
  return BACKTICK_TABLES.has(table) ? `\`${table}\`` : table;
1093
1086
  };
1094
- /** Fallback: insert relationships one-by-one if COPY fails */
1095
- const fallbackRelationshipInserts = async (validRelLines, validTables, getNodeLabel) => {
1087
+ /**
1088
+ * Format one JS value as a Cypher literal for the adapter's string-built
1089
+ * statements: NULL/undefined → `NULL`, numbers pass through unquoted,
1090
+ * everything else becomes a single-quoted string literal escaped via
1091
+ * {@link escapeCypherString} (backslashes first, then quotes).
1092
+ *
1093
+ * Replaces three per-function closures that used SQL-style `''` doubling —
1094
+ * LadybugDB REJECTS doubling, so every value containing a quote made the
1095
+ * whole statement a parser error, invisible wherever the call site swallowed
1096
+ * per-row failures (#2409 escaping sweep, completed for tri-review
1097
+ * 4669518496 P2-2). Those closures also rewrote literal `\n`/`\r` into
1098
+ * two-character escape sequences; raw LF/CR are legal inside LadybugDB
1099
+ * single-quoted literals (live-probed on @ladybugdb/core 0.18.0), so the
1100
+ * replaces are gone and content now round-trips byte-identical.
1101
+ */
1102
+ const formatCypherValue = (v) => {
1103
+ if (v === null || v === undefined)
1104
+ return 'NULL';
1105
+ if (typeof v === 'number')
1106
+ return String(v);
1107
+ return `'${escapeCypherString(String(v))}'`;
1108
+ };
1109
+ /**
1110
+ * Fallback: insert relationships one-by-one if COPY fails.
1111
+ *
1112
+ * Exported for the quoted-id round-trip tests in
1113
+ * `test/integration/lbug-core-adapter.test.ts` (the `DELETE_FILES_CHUNK_SIZE`
1114
+ * exported-for-tests precedent); production callers stay in this module.
1115
+ * Bails silently when the adapter singleton is closed.
1116
+ *
1117
+ * KNOWN PRE-EXISTING NARROWING (distinct from the `''` escaping bug, NOT
1118
+ * fixed here): the row regex below matches CSV fields with `[^"]*`, so an id
1119
+ * containing a double quote (CSV-escaped as `""`) never matches and the edge
1120
+ * is skipped. Tracked as part of the quote-in-id divergence documented in
1121
+ * `rel-pair-routing.ts`.
1122
+ */
1123
+ export const fallbackRelationshipInserts = async (validRelLines, validTables, getNodeLabel) => {
1096
1124
  if (!conn)
1097
1125
  return;
1098
1126
  const escapeLabel = (label) => {
@@ -1111,11 +1139,10 @@ const fallbackRelationshipInserts = async (validRelLines, validTables, getNodeLa
1111
1139
  continue;
1112
1140
  const confidence = parseFloat(confidenceStr) || 1.0;
1113
1141
  const step = parseInt(stepStr) || 0;
1114
- const esc = (s) => s.replace(/'/g, "''").replace(/\\/g, '\\\\').replace(/\n/g, '\\n').replace(/\r/g, '\\r');
1115
1142
  await queryAndDrain(conn, `
1116
- MATCH (a:${escapeLabel(fromLabel)} {id: '${esc(fromId)}' }),
1117
- (b:${escapeLabel(toLabel)} {id: '${esc(toId)}' })
1118
- CREATE (a)-[:${REL_TABLE_NAME} {type: '${esc(relType)}', confidence: ${confidence}, reason: '${esc(reason)}', step: ${step}}]->(b)
1143
+ MATCH (a:${escapeLabel(fromLabel)} {id: ${formatCypherValue(fromId)} }),
1144
+ (b:${escapeLabel(toLabel)} {id: ${formatCypherValue(toId)} })
1145
+ CREATE (a)-[:${REL_TABLE_NAME} {type: ${formatCypherValue(relType)}, confidence: ${confidence}, reason: ${formatCypherValue(reason)}, step: ${step}}]->(b)
1119
1146
  `);
1120
1147
  }
1121
1148
  catch {
@@ -1186,52 +1213,47 @@ export const insertNodeToLbug = async (label, properties, dbPath) => {
1186
1213
  throw new Error('LadybugDB not initialized. Provide dbPath or call initLbug first.');
1187
1214
  }
1188
1215
  try {
1189
- const escapeValue = (v) => {
1190
- if (v === null || v === undefined)
1191
- return 'NULL';
1192
- if (typeof v === 'number')
1193
- return String(v);
1194
- // Escape backslashes first (for Windows paths), then single quotes
1195
- return `'${String(v).replace(/\\/g, '\\\\').replace(/'/g, "''").replace(/\n/g, '\\n').replace(/\r/g, '\\r')}'`;
1196
- };
1216
+ // Values go through the module-scope formatCypherValue — the old local
1217
+ // closure used `''` doubling, which LadybugDB rejects (#2409 escaping
1218
+ // sweep, tri-review 4669518496 P2-2).
1197
1219
  // Build INSERT query based on node type
1198
1220
  const t = escapeTableName(label);
1199
1221
  let query;
1200
1222
  if (label === 'File') {
1201
- query = `CREATE (n:File {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}, content: ${escapeValue(properties.content || '')}})`;
1223
+ query = `CREATE (n:File {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, content: ${formatCypherValue(properties.content || '')}})`;
1202
1224
  }
1203
1225
  else if (label === 'Folder') {
1204
- query = `CREATE (n:Folder {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}})`;
1226
+ query = `CREATE (n:Folder {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}})`;
1205
1227
  }
1206
1228
  else if (label === 'Section') {
1207
1229
  const descPart = properties.description
1208
- ? `, description: ${escapeValue(properties.description)}`
1230
+ ? `, description: ${formatCypherValue(properties.description)}`
1209
1231
  : '';
1210
- query = `CREATE (n:Section {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, level: ${properties.level || 1}, content: ${escapeValue(properties.content || '')}${descPart}})`;
1232
+ query = `CREATE (n:Section {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, level: ${properties.level || 1}, content: ${formatCypherValue(properties.content || '')}${descPart}})`;
1211
1233
  }
1212
1234
  else if (label === 'BasicBlock') {
1213
1235
  // Taint/PDG substrate (issue #2080) — no name column. `calleeIds` (#2227)
1214
1236
  // is the sound resolved-id parallel to the leaf-name `callees` set.
1215
- query = `CREATE (n:BasicBlock {id: ${escapeValue(properties.id)}, filePath: ${escapeValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, text: ${escapeValue(properties.text || '')}, callees: ${escapeValue(properties.callees || '')}, calleeIds: ${escapeValue(properties.calleeIds || '')}})`;
1237
+ query = `CREATE (n:BasicBlock {id: ${formatCypherValue(properties.id)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, text: ${formatCypherValue(properties.text || '')}, callees: ${formatCypherValue(properties.callees || '')}, calleeIds: ${formatCypherValue(properties.calleeIds || '')}})`;
1216
1238
  }
1217
1239
  else if (TABLES_WITH_EXPORTED.has(label)) {
1218
1240
  const descPart = properties.description
1219
- ? `, description: ${escapeValue(properties.description)}`
1241
+ ? `, description: ${formatCypherValue(properties.description)}`
1220
1242
  : '';
1221
- query = `CREATE (n:${t} {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, isExported: ${!!properties.isExported}, content: ${escapeValue(properties.content || '')}${descPart}})`;
1243
+ query = `CREATE (n:${t} {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, isExported: ${!!properties.isExported}, content: ${formatCypherValue(properties.content || '')}${descPart}})`;
1222
1244
  }
1223
1245
  else if (label === 'Property') {
1224
1246
  const descPart = properties.description
1225
- ? `, description: ${escapeValue(properties.description)}`
1247
+ ? `, description: ${formatCypherValue(properties.description)}`
1226
1248
  : '';
1227
- query = `CREATE (n:${t} {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, content: ${escapeValue(properties.content || '')}${descPart}, declaredType: ${escapeValue(properties.declaredType || '')}})`;
1249
+ query = `CREATE (n:${t} {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, content: ${formatCypherValue(properties.content || '')}${descPart}, declaredType: ${formatCypherValue(properties.declaredType || '')}})`;
1228
1250
  }
1229
1251
  else {
1230
1252
  // Multi-language tables (Struct, Impl, Trait, Macro, etc.) — no isExported
1231
1253
  const descPart = properties.description
1232
- ? `, description: ${escapeValue(properties.description)}`
1254
+ ? `, description: ${formatCypherValue(properties.description)}`
1233
1255
  : '';
1234
- query = `CREATE (n:${t} {id: ${escapeValue(properties.id)}, name: ${escapeValue(properties.name)}, filePath: ${escapeValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, content: ${escapeValue(properties.content || '')}${descPart}})`;
1256
+ query = `CREATE (n:${t} {id: ${formatCypherValue(properties.id)}, name: ${formatCypherValue(properties.name)}, filePath: ${formatCypherValue(properties.filePath)}, startLine: ${properties.startLine || 0}, endLine: ${properties.endLine || 0}, content: ${formatCypherValue(properties.content || '')}${descPart}})`;
1235
1257
  }
1236
1258
  // Use per-query connection if dbPath provided (avoids lock conflicts)
1237
1259
  if (targetDbPath) {
@@ -1266,14 +1288,10 @@ export const insertNodeToLbug = async (label, properties, dbPath) => {
1266
1288
  export const batchInsertNodesToLbug = async (nodes, dbPath) => {
1267
1289
  if (nodes.length === 0)
1268
1290
  return { inserted: 0, failed: 0 };
1269
- const escapeValue = (v) => {
1270
- if (v === null || v === undefined)
1271
- return 'NULL';
1272
- if (typeof v === 'number')
1273
- return String(v);
1274
- // Escape backslashes first (for Windows paths), then single quotes, then newlines
1275
- return `'${String(v).replace(/\\/g, '\\\\').replace(/'/g, "''").replace(/\n/g, '\\n').replace(/\r/g, '\\r')}'`;
1276
- };
1291
+ // Values go through the module-scope formatCypherValue — the old local
1292
+ // closure used `''` doubling, which LadybugDB rejects; the per-node catch
1293
+ // below counted every quoted value as a silent `failed` (#2409 escaping
1294
+ // sweep, tri-review 4669518496 P2-2).
1277
1295
  // Open a single connection for all inserts
1278
1296
  const tempHandle = await openLbugConnection(lbug, dbPath);
1279
1297
  const tempConn = tempHandle.conn;
@@ -1286,39 +1304,39 @@ export const batchInsertNodesToLbug = async (nodes, dbPath) => {
1286
1304
  // Use MERGE instead of CREATE for upsert behavior (handles duplicates gracefully)
1287
1305
  const t = escapeTableName(label);
1288
1306
  if (label === 'File') {
1289
- query = `MERGE (n:File {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}, n.content = ${escapeValue(properties.content || '')}`;
1307
+ query = `MERGE (n:File {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.content = ${formatCypherValue(properties.content || '')}`;
1290
1308
  }
1291
1309
  else if (label === 'Folder') {
1292
- query = `MERGE (n:Folder {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}`;
1310
+ query = `MERGE (n:Folder {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}`;
1293
1311
  }
1294
1312
  else if (label === 'Section') {
1295
1313
  const descPart = properties.description
1296
- ? `, n.description = ${escapeValue(properties.description)}`
1314
+ ? `, n.description = ${formatCypherValue(properties.description)}`
1297
1315
  : '';
1298
- query = `MERGE (n:Section {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.level = ${properties.level || 1}, n.content = ${escapeValue(properties.content || '')}${descPart}`;
1316
+ query = `MERGE (n:Section {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.level = ${properties.level || 1}, n.content = ${formatCypherValue(properties.content || '')}${descPart}`;
1299
1317
  }
1300
1318
  else if (label === 'BasicBlock') {
1301
1319
  // Taint/PDG substrate (issue #2080) — no name column. `calleeIds`
1302
1320
  // (#2227) is the sound resolved-id parallel to the `callees` set.
1303
- query = `MERGE (n:BasicBlock {id: ${escapeValue(properties.id)}}) SET n.filePath = ${escapeValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.text = ${escapeValue(properties.text || '')}, n.callees = ${escapeValue(properties.callees || '')}, n.calleeIds = ${escapeValue(properties.calleeIds || '')}`;
1321
+ query = `MERGE (n:BasicBlock {id: ${formatCypherValue(properties.id)}}) SET n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.text = ${formatCypherValue(properties.text || '')}, n.callees = ${formatCypherValue(properties.callees || '')}, n.calleeIds = ${formatCypherValue(properties.calleeIds || '')}`;
1304
1322
  }
1305
1323
  else if (TABLES_WITH_EXPORTED.has(label)) {
1306
1324
  const descPart = properties.description
1307
- ? `, n.description = ${escapeValue(properties.description)}`
1325
+ ? `, n.description = ${formatCypherValue(properties.description)}`
1308
1326
  : '';
1309
- query = `MERGE (n:${t} {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.isExported = ${!!properties.isExported}, n.content = ${escapeValue(properties.content || '')}${descPart}`;
1327
+ query = `MERGE (n:${t} {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.isExported = ${!!properties.isExported}, n.content = ${formatCypherValue(properties.content || '')}${descPart}`;
1310
1328
  }
1311
1329
  else if (label === 'Property') {
1312
1330
  const descPart = properties.description
1313
- ? `, n.description = ${escapeValue(properties.description)}`
1331
+ ? `, n.description = ${formatCypherValue(properties.description)}`
1314
1332
  : '';
1315
- query = `MERGE (n:${t} {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.content = ${escapeValue(properties.content || '')}${descPart}, n.declaredType = ${escapeValue(properties.declaredType || '')}`;
1333
+ query = `MERGE (n:${t} {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.content = ${formatCypherValue(properties.content || '')}${descPart}, n.declaredType = ${formatCypherValue(properties.declaredType || '')}`;
1316
1334
  }
1317
1335
  else {
1318
1336
  const descPart = properties.description
1319
- ? `, n.description = ${escapeValue(properties.description)}`
1337
+ ? `, n.description = ${formatCypherValue(properties.description)}`
1320
1338
  : '';
1321
- query = `MERGE (n:${t} {id: ${escapeValue(properties.id)}}) SET n.name = ${escapeValue(properties.name)}, n.filePath = ${escapeValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.content = ${escapeValue(properties.content || '')}${descPart}`;
1339
+ query = `MERGE (n:${t} {id: ${formatCypherValue(properties.id)}}) SET n.name = ${formatCypherValue(properties.name)}, n.filePath = ${formatCypherValue(properties.filePath)}, n.startLine = ${properties.startLine || 0}, n.endLine = ${properties.endLine || 0}, n.content = ${formatCypherValue(properties.content || '')}${descPart}`;
1322
1340
  }
1323
1341
  await queryAndDrain(tempConn, query);
1324
1342
  inserted++;
@@ -1749,7 +1767,140 @@ export const closeLbug = async () => {
1749
1767
  vectorIndexEnsured = false;
1750
1768
  ensuredFTSIndexes.clear();
1751
1769
  };
1770
+ /**
1771
+ * Thrown by {@link wipeLbugDbFiles} when a data-bearing member of the
1772
+ * LadybugDB file family is still present after the bounded
1773
+ * remove-and-verify retries (#2409, tri-review 4669518496 P2-4), and by
1774
+ * run-analyze's dirty-recovery block when the crashed run's sidecars can
1775
+ * neither be parked nor removed (this shipping review, FIX 1 — same lock
1776
+ * class, same remediation, and the CLI already renders this type).
1777
+ *
1778
+ * Classify by TYPE (`err instanceof LbugWipeError`) — the repo norm from
1779
+ * #2385 — never by message text. The MESSAGE is nonetheless fully
1780
+ * self-contained (headline + blocked paths + remediation) because
1781
+ * `gitnexus serve` forwards only `err.message` over worker IPC
1782
+ * (analyze-worker-core.ts), so the serve surface has nothing but this
1783
+ * string to show the user. The holder framing deliberately covers the
1784
+ * own-process case (FIX 2, finder A): the blocking handle is often a
1785
+ * lingering one from THIS process's just-closed DB or a transient AV scan
1786
+ * — not necessarily another process — so an immediate re-run often
1787
+ * succeeds.
1788
+ */
1789
+ export class LbugWipeError extends Error {
1790
+ /** Paths still present (or unverifiable) after all retries. */
1791
+ survivors;
1792
+ constructor(survivors, options) {
1793
+ super(`${options?.headline ??
1794
+ `Failed to remove the LadybugDB index files — still present after ` +
1795
+ `${HANDLE_RELEASE_PROBE_ATTEMPTS} attempts:`}\n` +
1796
+ survivors.map((p) => ` - ${p}`).join('\n') +
1797
+ `\nThe blocking handle may be another process, a lingering handle from this ` +
1798
+ `process's just-closed database, or an antivirus scan — an immediate re-run ` +
1799
+ `often succeeds. If it persists, ${lbugLockRemediation('re-run the analyze')}.`);
1800
+ this.name = 'LbugWipeError';
1801
+ this.survivors = survivors;
1802
+ }
1803
+ }
1804
+ /**
1805
+ * Remove the LadybugDB file family and VERIFY each member is really gone.
1806
+ *
1807
+ * Owns the canonical 4-file family list — `<lbugPath>`, `.wal`, `.shadow`,
1808
+ * `.lock` — so run-analyze's two wipe sites (full rebuild + the #2409
1809
+ * escalation valve) can never drift apart. `.shadow` is included because a
1810
+ * checkpoint-in-flight crash leaves a shadow sidecar, and a stale shadow next
1811
+ * to a freshly created DB file is replay poison on the next open (#2409).
1812
+ *
1813
+ * Verification contract (tri-review 4669518496 P2-4 — the old inline loops
1814
+ * swallowed rm failures and let `initLbug` reopen a still-populated DB the
1815
+ * run believed it wiped): after `fs.rm({ recursive, force })`, each path is
1816
+ * probed and counts as GONE only when the probe rejects with **ENOENT**. A
1817
+ * resolving probe, or a rejection in the EPERM/EBUSY/EACCES class (Windows
1818
+ * delete-pending / handle-release lag — see HANDLE_RELEASE_LOCK_CODES in
1819
+ * lbug-config.ts), or any other code means the path is not verifiably gone:
1820
+ * it is retried on the shared handle-release budget
1821
+ * (HANDLE_RELEASE_PROBE_ATTEMPTS × linear HANDLE_RELEASE_PROBE_DELAY_MS,
1822
+ * lbug-config.ts — the previous private mirror constants were
1823
+ * documentation-coupled copies) and then handled by CLASS (this shipping
1824
+ * review, FIX 2):
1825
+ *
1826
+ * - DATA-BEARING members (`<lbugPath>`, `.wal`, `.shadow`) — a survivor
1827
+ * means the reopen would resurrect rows this run believes wiped: throw
1828
+ * a typed {@link LbugWipeError}.
1829
+ * - `.lock` — contentless: `initLbug` recreates it, and a genuinely held
1830
+ * lock surfaces as initLbug's own lock-busy classification (a better
1831
+ * error than this one). A `.lock`-only survivor (an AV-held
1832
+ * delete-pending handle outlasting the budget previously failed a
1833
+ * perfectly sound rebuild) logs a warning and CONTINUES.
1834
+ *
1835
+ * Linux unlinked-but-open (name gone, holder keeps the old inode) probes
1836
+ * ENOENT and is accepted by design — both production wipe sites run after a
1837
+ * real `closeLbug()`.
1838
+ *
1839
+ * Deliberately OUT of this contract: `cleanupOldKuzuFiles`
1840
+ * (repo-manager.ts) sweeps the LEGACY kuzu-era file family during storage
1841
+ * migration — different family, best-effort by design; and
1842
+ * `sweepStaleSidecars` (lbug-config.ts) is a test-fixture-gated open-retry
1843
+ * fallback that must never delete production files. Neither wipes the live
1844
+ * DB the run is about to recreate, so neither needs (or may share) the
1845
+ * loud-failure contract here.
1846
+ */
1847
+ export const wipeLbugDbFiles = async (lbugPath) => {
1848
+ const lockPath = `${lbugPath}.lock`;
1849
+ const family = [lbugPath, `${lbugPath}.wal`, `${lbugPath}.shadow`, lockPath];
1850
+ let survivors = [];
1851
+ for (let attempt = 1; attempt <= HANDLE_RELEASE_PROBE_ATTEMPTS; attempt++) {
1852
+ survivors = [];
1853
+ for (const f of family) {
1854
+ try {
1855
+ await fs.rm(f, { recursive: true, force: true });
1856
+ }
1857
+ catch {
1858
+ // `force: true` swallows ENOENT, so a rejection is a real failure —
1859
+ // but the ENOENT-probe below stays authoritative either way (another
1860
+ // process may have removed the path between the rm and the probe).
1861
+ }
1862
+ const gone = await fs.access(f).then(() => false, // still present
1863
+ (err) => err?.code === 'ENOENT');
1864
+ if (!gone)
1865
+ survivors.push(f);
1866
+ }
1867
+ if (survivors.length === 0)
1868
+ return;
1869
+ if (attempt < HANDLE_RELEASE_PROBE_ATTEMPTS) {
1870
+ await sleep(HANDLE_RELEASE_PROBE_DELAY_MS * attempt);
1871
+ }
1872
+ }
1873
+ // Class split (FIX 2): the contentless `.lock` never fails the wipe.
1874
+ const dataSurvivors = survivors.filter((f) => f !== lockPath);
1875
+ if (survivors.includes(lockPath)) {
1876
+ logger.warn(`GitNexus: ${lockPath} is still present after the wipe retries — continuing: the ` +
1877
+ 'lock file is contentless and initLbug recreates it; a genuinely held lock will ' +
1878
+ "surface as the reopen's own lock-busy error.");
1879
+ }
1880
+ if (dataSurvivors.length > 0) {
1881
+ throw new LbugWipeError(dataSurvivors);
1882
+ }
1883
+ };
1752
1884
  export const isLbugReady = () => conn !== null && db !== null;
1885
+ /**
1886
+ * Multi-label alternation over exactly the labels that can own embedding
1887
+ * rows (embedding-pipeline.ts queries EMBEDDABLE_LABELS and nothing else),
1888
+ * reserved keywords backtick-escaped via {@link escapeTableName}. Probed on
1889
+ * @ladybugdb/core 0.18.0 (this shipping review, FIX 4): the full 19-label
1890
+ * alternation parses, executes, and deletes exactly the joined rows —
1891
+ * replacing the unlabeled `MATCH (n)` that scanned EVERY node table per
1892
+ * chunk (BasicBlock-dominated under `--pdg`) when only embeddable labels
1893
+ * can match an embedding row.
1894
+ */
1895
+ const embeddableLabelMatch = () => EMBEDDABLE_LABELS.map((l) => escapeTableName(l)).join('|');
1896
+ // LADYBUGDB-CONTRACT: matches @ladybugdb/core ^0.18.0 native binder text,
1897
+ // probe-recorded: `Binder exception: Table CodeEmbedding does not exist.`
1898
+ // When bumping LadybugDB, re-validate — `git grep "LADYBUGDB-CONTRACT"`
1899
+ // enumerates every version-coupled spot.
1900
+ const isMissingEmbeddingTableError = (err) => {
1901
+ const msg = err instanceof Error ? err.message : String(err);
1902
+ return msg.includes(`Table ${EMBEDDING_TABLE_NAME} does not exist`);
1903
+ };
1753
1904
  /**
1754
1905
  * Delete all nodes (and their relationships) for a specific file from LadybugDB
1755
1906
  * @param filePath - The file path to delete nodes for
@@ -1772,7 +1923,34 @@ export const deleteNodesForFile = async (filePath, dbPath) => {
1772
1923
  }
1773
1924
  try {
1774
1925
  let deletedNodes = 0;
1775
- const escapedPath = filePath.replace(/'/g, "''");
1926
+ const escapedPath = escapeCypherString(filePath);
1927
+ // Delete the file's embedding rows FIRST, while their owning nodes are
1928
+ // still present: node ids are label-first — generateId = `${label}:${name}`
1929
+ // (src/lib/utils.ts) with qualified names that embed the file path — so
1930
+ // the old `e.nodeId STARTS WITH '<filePath>'` shape never matched a row
1931
+ // (tri-review 4669518496 P2-1). Join through the nodes on exact id
1932
+ // equality instead, scoped to the embeddable labels (FIX 4 — see
1933
+ // embeddableLabelMatch); ordering is load-bearing — after the DETACH
1934
+ // DELETE loop below the join would match nothing.
1935
+ try {
1936
+ await queryAndDrain(targetConn, `MATCH (n:${embeddableLabelMatch()}) WHERE n.filePath = '${escapedPath}' ` +
1937
+ `MATCH (e:${EMBEDDING_TABLE_NAME}) WHERE e.nodeId = n.id DELETE e`);
1938
+ }
1939
+ catch (err) {
1940
+ // Deliberately legacy-permissive (pinned contract:
1941
+ // lbug-conn-serialization U5 and lbug-core-adapter expect this variant
1942
+ // to resolve `{deletedNodes: 0}` even on a bogus dbPath): the singular
1943
+ // variant swallows per-statement failures wholesale — its per-table
1944
+ // loop below does the same — so a partial rethrow here would be
1945
+ // incoherent with the rest of the function. The STRICT
1946
+ // rethrow-except-missing-table policy lives in deleteNodesForFiles,
1947
+ // the #2409 incremental writeback path (FIX 4). The one case worth a
1948
+ // diagnostic is the missing embedding table.
1949
+ if (isMissingEmbeddingTableError(err)) {
1950
+ logger.warn({ err }, `deleteNodesForFile: ${EMBEDDING_TABLE_NAME} table does not exist — ` +
1951
+ 'skipping embedding-row deletes for this DB.');
1952
+ }
1953
+ }
1776
1954
  // Delete nodes from each table that has filePath
1777
1955
  // DETACH DELETE removes the node and all its relationships
1778
1956
  for (const tableName of NODE_TABLES) {
@@ -1802,13 +1980,6 @@ export const deleteNodesForFile = async (filePath, dbPath) => {
1802
1980
  // Some tables may not support this query, skip
1803
1981
  }
1804
1982
  }
1805
- // Also delete any embeddings for nodes in this file
1806
- try {
1807
- await queryAndDrain(targetConn, `MATCH (e:${EMBEDDING_TABLE_NAME}) WHERE e.nodeId STARTS WITH '${escapedPath}' DELETE e`);
1808
- }
1809
- catch {
1810
- // Embedding table may not exist or nodeId format may differ
1811
- }
1812
1983
  return { deletedNodes };
1813
1984
  }
1814
1985
  finally {
@@ -1817,6 +1988,91 @@ export const deleteNodesForFile = async (filePath, dbPath) => {
1817
1988
  await closeLbugConnection(tempHandle);
1818
1989
  }
1819
1990
  };
1991
+ /**
1992
+ * Chunk size for {@link deleteNodesForFiles}. 200 paths keeps each
1993
+ * statement ~13KB (well inside parser limits) while a ~700-file write set
1994
+ * still collapses from ~13,000 statements to 124: 31 statements per chunk
1995
+ * (1 CodeEmbedding join-delete + 30 filePath-bearing node tables — the
1996
+ * 32-table NODE_TABLES roster minus Community/Process) × 4 chunks. The
1997
+ * original "~40" claim under-counted the per-chunk statement fan-out
1998
+ * (tri-review 4669518496 accuracy sweep).
1999
+ */
2000
+ export const DELETE_FILES_CHUNK_SIZE = 200;
2001
+ /**
2002
+ * Batched variant of {@link deleteNodesForFile} for the incremental
2003
+ * writeback (#2409). One `DETACH DELETE … WHERE n.filePath IN […]` per
2004
+ * node table per chunk of paths, instead of a count + delete per table
2005
+ * per FILE. The per-file loop issued ~13,000 single-row write
2006
+ * transactions on a ~700-file write set — a WAL-append storm that made
2007
+ * the incremental path slower than a full rebuild and is the write
2008
+ * pattern behind the native mid-writeback deaths reported in #2409.
2009
+ *
2010
+ * NO general error swallowing: a zero-match chunk is a no-op success by
2011
+ * construction (every node table except Community/Process has a filePath
2012
+ * column), so anything thrown here is a real engine failure the caller
2013
+ * must see — silently skipping was exactly how #2409 hid its root cause.
2014
+ * The single tolerated exception (FIX 4) is the missing-embedding-table
2015
+ * binder error on the embedding join-delete: a DB created without
2016
+ * EMBEDDING_SCHEMA cannot own embedding rows, so skipping that one
2017
+ * statement is sound, while failing would brick every incremental run on
2018
+ * such a DB until `--force`. Statement count per chunk is unchanged by the
2019
+ * multi-label join: 1 embedding join-delete + 30 node-table deletes = 31
2020
+ * (the rejected per-label fallback shape would have been 19 + 30 = 49).
2021
+ * Singleton-connection only: the analyze writeback owns the write lock,
2022
+ * and `queryAndDrain` routes through `withConnLock` for it (the WAL
2023
+ * checkpoint driver is live during this).
2024
+ */
2025
+ export const deleteNodesForFiles = async (filePaths, options = {}) => {
2026
+ if (!conn) {
2027
+ throw new Error('LadybugDB not initialized. Call initLbug first.');
2028
+ }
2029
+ const targetConn = conn;
2030
+ let warnedMissingEmbeddingTable = false;
2031
+ for (let i = 0; i < filePaths.length; i += DELETE_FILES_CHUNK_SIZE) {
2032
+ const chunk = filePaths.slice(i, i + DELETE_FILES_CHUNK_SIZE);
2033
+ const listLiteral = `[${chunk.map((p) => `'${escapeCypherString(p)}'`).join(', ')}]`;
2034
+ // Embedding rows key on their OWNING NODE's id: generateId builds
2035
+ // label-first ids — `${label}:${name}` (src/lib/utils.ts) with qualified
2036
+ // names that embed the file path (e.g. `Function:src/f.ts:fn0:1`) — so
2037
+ // the previous bare-path `e.nodeId STARTS WITH '<filePath>'` OR-chain
2038
+ // could never match anything (tri-review 4669518496 P2-1: the embedding
2039
+ // delete was a no-op). Join through the nodes instead: one multi-label
2040
+ // MATCH over exactly the embeddable labels (FIX 4, probe-proven on
2041
+ // 0.18.0 — see embeddableLabelMatch; the old unlabeled `MATCH (n)`
2042
+ // scanned every node table per chunk, BasicBlock-dominated under
2043
+ // `--pdg`, when only embeddable labels can own rows), and
2044
+ // `e.nodeId = n.id` equality is exact — no `File:a.ts` / `File:a.tsx`
2045
+ // prefix collisions. ORDER IS LOAD-BEARING: this must run BEFORE the
2046
+ // DETACH DELETE loop below — once the nodes are gone the join matches
2047
+ // nothing (empirically verified against @ladybugdb/core 0.18.0).
2048
+ try {
2049
+ await queryAndDrain(targetConn, `MATCH (n:${embeddableLabelMatch()}) WHERE n.filePath IN ${listLiteral} ` +
2050
+ `MATCH (e:${EMBEDDING_TABLE_NAME}) WHERE e.nodeId = n.id DELETE e`);
2051
+ }
2052
+ catch (err) {
2053
+ // Tolerate exactly the missing-embedding-table binder error: a
2054
+ // build-variant DB without EMBEDDING_SCHEMA would otherwise brick
2055
+ // every incremental run until `--force` (FIX 4). The no-swallow
2056
+ // policy stays for every real failure — anything else rethrows.
2057
+ if (!isMissingEmbeddingTableError(err))
2058
+ throw err;
2059
+ if (!warnedMissingEmbeddingTable) {
2060
+ warnedMissingEmbeddingTable = true;
2061
+ logger.warn({ err }, `deleteNodesForFiles: ${EMBEDDING_TABLE_NAME} table does not exist — ` +
2062
+ 'skipping embedding-row deletes for this writeback.');
2063
+ }
2064
+ }
2065
+ for (const tableName of NODE_TABLES) {
2066
+ // Community/Process are graph-wide (no filePath); the orchestrator
2067
+ // drops them wholesale via deleteAllCommunitiesAndProcesses.
2068
+ if (tableName === 'Community' || tableName === 'Process')
2069
+ continue;
2070
+ const tn = escapeTableName(tableName);
2071
+ await queryAndDrain(targetConn, `MATCH (n:${tn}) WHERE n.filePath IN ${listLiteral} DETACH DELETE n`);
2072
+ }
2073
+ options.onChunk?.(Math.min(i + DELETE_FILES_CHUNK_SIZE, filePaths.length), filePaths.length);
2074
+ }
2075
+ };
1820
2076
  export const getEmbeddingTableName = () => EMBEDDING_TABLE_NAME;
1821
2077
  /**
1822
2078
  * Return the distinct repo-relative paths of files that import
@@ -1838,7 +2094,7 @@ export const queryImporters = async (targetFilePath) => {
1838
2094
  if (!c) {
1839
2095
  throw new Error('LadybugDB not initialized. Call initLbug first.');
1840
2096
  }
1841
- const escaped = targetFilePath.replace(/'/g, "''");
2097
+ const escaped = escapeCypherString(targetFilePath);
1842
2098
  const cypher = `
1843
2099
  MATCH (a)-[r:${REL_TABLE_NAME}]->(b)
1844
2100
  WHERE r.type = 'IMPORTS' AND b.filePath = '${escaped}'
@@ -1870,6 +2126,70 @@ export const queryImporters = async (targetFilePath) => {
1870
2126
  }
1871
2127
  });
1872
2128
  };
2129
+ /**
2130
+ * Batched variant of {@link queryImporters} for the incremental importer
2131
+ * BFS (#2409): distinct importers of ANY of the target paths, one query per
2132
+ * chunk per BFS depth instead of one query per frontier FILE (a ~700-file
2133
+ * frontier was ~700 sequential round-trips, each taking the connection lock
2134
+ * against the live WAL checkpoint driver — ~5.6s of the writeback measured).
2135
+ *
2136
+ * Same contract as the singular form: reads the pre-pipeline DB state and
2137
+ * swallows per-chunk query failures into a smaller result (correctness
2138
+ * degrades on that branch — under-expansion means possibly-stale edges —
2139
+ * but the DB stays writable and the writeback proceeds). Unlike the singular
2140
+ * form the degradation is not silent (tri-review 4669518496 P2-5): every
2141
+ * dropped chunk is logged and reported through `options.onChunkFailure`, so
2142
+ * the orchestrator can count it into the #2410 crash diagnostics
2143
+ * (`incrementalInProgress.droppedImporterChunks`).
2144
+ */
2145
+ export const queryImportersBatch = async (targetFilePaths, options = {}) => {
2146
+ const c = conn;
2147
+ if (!c) {
2148
+ throw new Error('LadybugDB not initialized. Call initLbug first.');
2149
+ }
2150
+ const importers = new Set();
2151
+ for (let i = 0; i < targetFilePaths.length; i += DELETE_FILES_CHUNK_SIZE) {
2152
+ // `i` only ever advances in whole chunk strides, so this is exact.
2153
+ const chunkIndex = i / DELETE_FILES_CHUNK_SIZE;
2154
+ const chunk = targetFilePaths.slice(i, i + DELETE_FILES_CHUNK_SIZE);
2155
+ const listLiteral = `[${chunk.map((p) => `'${escapeCypherString(p)}'`).join(', ')}]`;
2156
+ const cypher = `
2157
+ MATCH (a)-[r:${REL_TABLE_NAME}]->(b)
2158
+ WHERE r.type = 'IMPORTS' AND b.filePath IN ${listLiteral}
2159
+ RETURN DISTINCT a.filePath AS importer
2160
+ `;
2161
+ await withConnLock(async () => {
2162
+ let queryResult;
2163
+ try {
2164
+ queryResult = await c.query(cypher);
2165
+ const result = Array.isArray(queryResult) ? queryResult[0] : queryResult;
2166
+ const rows = await result.getAll();
2167
+ for (const row of rows) {
2168
+ const v = row.importer;
2169
+ if (typeof v === 'string' && v.length > 0)
2170
+ importers.add(v);
2171
+ }
2172
+ }
2173
+ catch (err) {
2174
+ // Degrade-don't-fail, mirroring queryImporters — but LOUDLY
2175
+ // (tri-review 4669518496 P2-5): a dropped chunk means every importer
2176
+ // it would have surfaced keeps possibly-stale edges this run, and the
2177
+ // old bare `catch {}` left no trace of that anywhere. pino idiom:
2178
+ // `err` key — `error` serializes to `{}`.
2179
+ logger.warn({ err }, `Incremental importer BFS: dropped chunk ${chunkIndex} (${chunk.length} target path(s)) — ` +
2180
+ 'importer expansion degrades for this run; affected importers may keep stale edges until the next full rebuild.');
2181
+ options.onChunkFailure?.(chunkIndex, chunk.length, err);
2182
+ }
2183
+ finally {
2184
+ if (queryResult)
2185
+ await closeQueryResults(queryResult);
2186
+ }
2187
+ });
2188
+ }
2189
+ // Cypher without ORDER BY is unordered — sort so downstream chunking and
2190
+ // logs are stable run-to-run (matches diffFileHashes' sorted outputs).
2191
+ return [...importers].sort();
2192
+ };
1873
2193
  /**
1874
2194
  * Drop every Community and Process node (and their MEMBER_OF /
1875
2195
  * STEP_IN_PROCESS edges via DETACH DELETE). Used at the start of an