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
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
+ import { HANDLE_RELEASE_PROBE_ATTEMPTS, HANDLE_RELEASE_PROBE_DELAY_MS, sleep, } from './lbug-config.js';
3
4
  export const TINY_ORPHAN_WAL_BYTES = 4 * 1024;
4
5
  /**
5
6
  * Counter-based warn anti-spam (PR #1747 review, Finding 6).
@@ -87,16 +88,47 @@ const warnOnce = (logger, key, message) => {
87
88
  logger.warn(`${message} (${ordinal(next)} occurrence of this condition)`);
88
89
  };
89
90
  // LADYBUGDB-CONTRACT: matches @ladybugdb/core ^0.18.0 native error text.
90
- // When bumping LadybugDB, re-validate this regex against the new error format
91
+ // When bumping LadybugDB, re-validate this against the new error format
91
92
  // — `git grep "LADYBUGDB-CONTRACT"` enumerates every version-coupled spot.
92
- // Verified by upstream source/changelog diff only — forcing a genuine
93
- // `.shadow`-missing state via a live crash to trigger this error is not
94
- // reliably reproducible (a SIGKILL at the exact moment `.shadow` exists on
95
- // disk still recovers via `.wal.checkpoint` alone), so this matcher does not
96
- // have live-trigger test coverage.
93
+ //
94
+ // Two native formats reach here for a genuinely-missing shadow sidecar:
95
+ // POSIX: `Cannot open file <path>.shadow: No such file or directory`
96
+ // Windows: `Cannot open file. path: <path>.shadow - Error 2: <system text>`
97
+ // Windows OS text is localized on non-English installs (issue #2382 was filed
98
+ // from a non-English Windows), so we key on the locale-invariant Win32 code
99
+ // (2 = ERROR_FILE_NOT_FOUND), NOT the English phrase. The code is matched only
100
+ // in the reason AFTER the LAST `.shadow` token (the real failing sidecar; the
101
+ // reason text never contains `.shadow`), so a repo *path* containing e.g.
102
+ // `\error 2\` — even under a `.shadow`-suffixed parent directory — cannot trip
103
+ // it. Deliberate exclusions:
104
+ // - `Error 3` (ERROR_PATH_NOT_FOUND): the #1811 non-ASCII path-garble
105
+ // artifact (see lbug-config.ts) where the shadow is PRESENT on disk;
106
+ // treating it as missing would quarantine a live WAL — data loss.
107
+ // - `Error 5` / `Error 32` / POSIX `Permission denied`: present-but-locked;
108
+ // handled as permission/lock classes, must not quarantine.
109
+ // The quarantine path adds a present-shadow disk check as a belt (see
110
+ // refuseLargeWalQuarantine in lbug-adapter.ts).
111
+ //
112
+ // The Windows branch is derived from the issue #2382 reported string, not a
113
+ // self-produced live crash; unit/consumer tests inject that same string, so
114
+ // GREEN TESTS DO NOT PROVE the byte-exact 0.18.0 Windows format — confirm
115
+ // against a real Windows run before closing #2382.
97
116
  export const isMissingShadowSidecarError = (err) => {
98
117
  const msg = err instanceof Error ? err.message : String(err);
99
- return /Cannot open file .*\.shadow: No such file or directory/i.test(msg);
118
+ if (!/cannot open file/i.test(msg))
119
+ return false;
120
+ // Anchor on the LAST `.shadow`, not the first: LadybugDB names the failing
121
+ // sidecar as the final `.shadow` token and its reason text (POSIX
122
+ // `: No such file or directory` / Windows ` - Error N: ...`) never contains
123
+ // `.shadow`. Slicing from the last match isolates the true reason, so an
124
+ // earlier `.shadow`-suffixed path segment (e.g. a `branch=subdir` directory
125
+ // like `snap.shadow\`) can't shift the anchor and let a path-embedded
126
+ // `error 2` be read as the Win32 code (issue #2382 review, Finding A).
127
+ const lastShadow = [...msg.matchAll(/\.shadow\b/gi)].at(-1);
128
+ if (lastShadow?.index === undefined)
129
+ return false;
130
+ const reason = msg.slice(lastShadow.index);
131
+ return /no such file or directory/i.test(reason) || /\berror\s+2\b/i.test(reason);
100
132
  };
101
133
  // LADYBUGDB-CONTRACT: matches @ladybugdb/core ^0.18.0 native error text.
102
134
  // When bumping LadybugDB, re-validate this regex against the new error format
@@ -114,11 +146,44 @@ export const shadowSidecarRecoveryMessage = (dbPath, err) => {
114
146
  'Rebuild the index with `gitnexus analyze --force <repo-path> --index-only` and restart `gitnexus serve`.' +
115
147
  `\n Original error: ${msg.slice(0, 200)}`);
116
148
  };
149
+ /**
150
+ * Actionable message for the case where LadybugDB reports a "missing shadow"
151
+ * but `inspectLbugSidecars` finds the `.shadow` PRESENT on disk — the open
152
+ * failed on path reachability or a lock, not a genuinely-missing sidecar (issue
153
+ * #2382 review, S2). Unlike `shadowSidecarRecoveryMessage` it does NOT tell the
154
+ * operator to rebuild the index (the remedy is fixing the lock/path). Keeps the
155
+ * `Original error:` tail so downstream `isMissingShadowSidecarError` recognition
156
+ * still matches the wrapped error.
157
+ */
158
+ export const presentShadowUnreachableMessage = (dbPath, err) => {
159
+ const msg = err instanceof Error ? err.message : String(err);
160
+ return (`LadybugDB checkpoint sidecar is present but unreachable for ${dbPath}. ` +
161
+ 'The .shadow file is on disk, so the open likely failed on path reachability or a file lock ' +
162
+ '(antivirus, another process holding a handle, or a non-ASCII path) rather than a missing sidecar. ' +
163
+ 'Check filesystem access and locks; only run `gitnexus analyze --force <repo-path> --index-only` ' +
164
+ 'if the index is genuinely broken.' +
165
+ `\n Original error: ${msg.slice(0, 200)}`);
166
+ };
117
167
  const PERMISSION_RENAME_CODES = new Set(['EACCES', 'EPERM', 'EBUSY']);
118
168
  export const isPermissionRenameError = (err) => {
119
169
  const code = err?.code;
120
170
  return typeof code === 'string' && PERMISSION_RENAME_CODES.has(code);
121
171
  };
172
+ /**
173
+ * Canonical remediation guidance for the LadybugDB file-lock class
174
+ * (EBUSY/EPERM/EACCES — an MCP/serve process holding the index, or an
175
+ * antivirus scan). One exported producer (this shipping review, FIX 7):
176
+ * the dirty-recovery park warning below, `LbugWipeError`'s message builder
177
+ * (lbug-adapter.ts) and {@link renameFailureMessage} previously carried
178
+ * three divergent hand-written copies of the same advice.
179
+ *
180
+ * `rerun` names the command to retry once the lock clears — the analyze
181
+ * wipe/park surfaces re-run the analyze; the read-path quarantine surface
182
+ * re-runs whatever command failed. No trailing period: callers own the
183
+ * sentence end.
184
+ */
185
+ export const lbugLockRemediation = (rerun = 're-run `gitnexus analyze`') => 'stop any GitNexus MCP or serve process using this repository, add an antivirus ' +
186
+ `exclusion for the GitNexus storage directory, then ${rerun}`;
122
187
  /**
123
188
  * Classify a failure surfaced by quarantine rename into an actionable user-facing
124
189
  * message.
@@ -140,10 +205,10 @@ export const renameFailureMessage = (dbPath, err) => {
140
205
  const msg = err instanceof Error ? err.message : String(err);
141
206
  return (`GitNexus could not move the LadybugDB WAL sidecar at ${dbPath}.wal because of a ` +
142
207
  `filesystem permission or file-lock error (${code}). ` +
143
- 'Check filesystem ACLs, antivirus exclusions for the index directory, and ' +
144
- 'whether another process holds an open handle on the file. ' +
145
- 'The index does not need to be rebuilt — re-running the failing command after ' +
146
- 'resolving the lock or permission should succeed.' +
208
+ 'The index does not need to be rebuilt ' +
209
+ // Shared remediation copy (FIX 7); this surface serves read paths too,
210
+ // so the re-run target is the failing command, not the analyze.
211
+ `${lbugLockRemediation('re-run the failing command once the lock or permission is resolved')}.` +
147
212
  `\n Original error: ${msg.slice(0, 200)}`);
148
213
  }
149
214
  return shadowSidecarRecoveryMessage(dbPath, err);
@@ -165,6 +230,41 @@ export async function inspectLbugSidecars(dbPath) {
165
230
  }
166
231
  return { kind: 'clean', dbPath };
167
232
  }
233
+ /**
234
+ * Reject the WAL-quarantine path when discarding the WAL would be unsafe or
235
+ * wrong. Shared by every reactive missing-shadow recovery consumer — serve (via
236
+ * lbug-adapter's `refuseLargeWalQuarantine`) and the MCP/wiki/augmentation pool
237
+ * (via pool-adapter's `tryQuarantineForMissingShadow`) — so the quarantine
238
+ * safety policy has a single source of truth (issue #2382 review, Finding B).
239
+ *
240
+ * 1. `wal-with-shadow` — the `.shadow` sidecar is PRESENT on disk. A
241
+ * "missing shadow" error alongside a present shadow means the open failed
242
+ * on path reachability or a lock (the #1811 non-ASCII path-garble on
243
+ * Windows), not a genuinely-missing shadow; quarantining would move a live
244
+ * WAL sitting next to its shadow — data loss.
245
+ * 2. `orphan-wal` — the orphan WAL is too large to safely discard
246
+ * (>TINY_ORPHAN_WAL_BYTES); preserve the uncheckpointed pages for explicit
247
+ * operator recovery.
248
+ *
249
+ * Throws `shadowSidecarRecoveryMessage` in either case. Returns silently only
250
+ * when the shadow is absent AND the WAL is absent or tiny — the states where
251
+ * the existing recovery path is safe to proceed. `mode` is a label used only in
252
+ * the warning text (e.g. 'read-only', 'writable', 'pool read-only recovery').
253
+ */
254
+ export const guardWalQuarantine = async (dbPath, mode, triggeringErr, logger) => {
255
+ const state = await inspectLbugSidecars(dbPath);
256
+ if (state.kind === 'wal-with-shadow') {
257
+ warnOnce(logger, `${dbPath}:present-shadow-refuse:${mode}`, `GitNexus: refusing to quarantine WAL at ${dbPath}.wal during ${mode} recovery — ` +
258
+ 'the .shadow sidecar is present on disk, so the open likely failed on path reachability or a lock ' +
259
+ 'rather than a missing shadow. Run `gitnexus analyze --force <repo-path> --index-only` if the index is genuinely broken.');
260
+ throw new Error(presentShadowUnreachableMessage(dbPath, triggeringErr));
261
+ }
262
+ if (state.kind === 'orphan-wal') {
263
+ warnOnce(logger, `${dbPath}:large-wal-refuse:${mode}`, `GitNexus: refusing to quarantine large WAL (${state.walBytes} bytes) at ${dbPath}.wal during ${mode} recovery; ` +
264
+ 'manual recovery required — run `gitnexus analyze --force <repo-path> --index-only`.');
265
+ throw new Error(shadowSidecarRecoveryMessage(dbPath, triggeringErr));
266
+ }
267
+ };
168
268
  export async function quarantineWalForMissingShadow(dbPath, options) {
169
269
  const walPath = `${dbPath}.wal`;
170
270
  const quarantinePath = `${walPath}.missing-shadow.${Date.now()}-${Math.random()
@@ -254,6 +354,227 @@ export async function finalizeLbugSidecarsAfterClose(dbPath, options) {
254
354
  'keeping it for recovery. If this repeats, run `gitnexus analyze --force --index-only` or the sidecar repair command.');
255
355
  }
256
356
  }
357
+ /**
358
+ * Corrected parking-failure warning (tri-review 4669518496 P2-3). The old
359
+ * text promised "the rebuild will wipe it in place instead" — false: the
360
+ * recovery run's pre-wipe DB open would replay the poisoned WAL and die
361
+ * before any wipe could happen. Mirrors {@link renameFailureMessage}'s
362
+ * EBUSY/EPERM framing via the shared {@link lbugLockRemediation} copy
363
+ * (FIX 7): the problem is environmental (file lock, AV), not data
364
+ * integrity — fix the lock and re-run.
365
+ */
366
+ const sidecarParkRefusedWarning = (from, err) => `Warning: could not park or remove ${path.basename(from)} before the recovery rebuild ` +
367
+ `(${err instanceof Error ? err.message : String(err)}). Another process likely holds an ` +
368
+ `open handle on it — ${lbugLockRemediation()}.`;
369
+ /**
370
+ * The sidecar family parked by {@link quarantineSidecarsForDirtyRecovery}
371
+ * and enumerated by {@link listParkedDirtyRecoverySidecars} — one shared
372
+ * roster so the park and clean surfaces cannot drift apart (tri-review
373
+ * 4669518496 P2-7).
374
+ */
375
+ const DIRTY_RECOVERY_SIDECAR_SUFFIXES = ['.wal', '.shadow'];
376
+ /**
377
+ * Every fixed name the dirty-recovery park can leave beside `dbPath`: the
378
+ * two `.dirty-recovery` destinations PLUS their `.next` probe residues
379
+ * (this shipping review, FIX 5 — the residue used to be invisible to every
380
+ * cleanup surface while the docs said "remove manually"). Single roster
381
+ * authority for {@link listParkedDirtyRecoverySidecars}.
382
+ */
383
+ const dirtyRecoveryParkedNames = (dbPath) => DIRTY_RECOVERY_SIDECAR_SUFFIXES.flatMap((suffix) => [
384
+ `${dbPath}${suffix}.dirty-recovery`,
385
+ `${dbPath}${suffix}.dirty-recovery.next`,
386
+ ]);
387
+ /**
388
+ * Move the WAL/shadow sidecars aside before a dirty-flag recovery rebuild
389
+ * (#2409 defect 2).
390
+ *
391
+ * When `incrementalInProgress` forces a full rebuild, the previous run
392
+ * died mid-writeback — its WAL can be poisoned in a way that natively
393
+ * kills the process on replay. The recovery run used to open the DB
394
+ * BEFORE the rebuild wipe (the embedding-cache preservation open), replay
395
+ * the poisoned WAL, and die on the spot — so recovery never happened and
396
+ * only a manual rename-aside of the index dir escaped the loop. The
397
+ * rebuild discards every pending WAL byte anyway (the DB files are wiped),
398
+ * so parking the sidecars first costs nothing and makes every subsequent
399
+ * open replay-free.
400
+ *
401
+ * Renamed when possible, so the bytes stay available for post-mortem
402
+ * debugging — and, like {@link quarantineWalForMissingShadow}'s quarantine
403
+ * files, the parked copies are surfaced and removable by
404
+ * `gitnexus clean --lbug-sidecars` (tri-review 4669518496 P2-7; before
405
+ * that, this comment claimed a "same philosophy" parity while the
406
+ * dirty-recovery files were invisible to every cleanup surface). Real
407
+ * lifecycle: the destinations are FIXED names — no timestamp, see
408
+ * {@link listParkedDirtyRecoverySidecars} — so each new crash overwrites
409
+ * the previous parked copy, capping accumulation at one file per sidecar;
410
+ * remove them via `clean --lbug-sidecars` or manually once their
411
+ * post-mortem value has passed.
412
+ *
413
+ * Escalation ladder per suffix (this shipping review, FIX 1 — replacing
414
+ * the drop-shape design, whose park had ZERO retry while the wipe path
415
+ * retried the very same lock class):
416
+ *
417
+ * 1. `rename(from, to)` retried over the shared handle-release budget
418
+ * (HANDLE_RELEASE_PROBE_ATTEMPTS × linear HANDLE_RELEASE_PROBE_DELAY_MS,
419
+ * lbug-config.ts) — a transient AV/handle-lag EBUSY must not cost the
420
+ * run anything.
421
+ * 2. Structural confirm probe: a bare "does `to` exist?" check cannot
422
+ * discriminate a Windows rename-onto-existing collision from a locked
423
+ * source that happens to have a leftover parked copy. Renaming the
424
+ * source to the collision-free `${to}.next` can — success proves the
425
+ * failure was the collision, so the stale copy is replaced (newest
426
+ * forensics win). The crash window between the `rm(to)` and the final
427
+ * promote rename strands the bytes at `.next` — acceptable: `.next`
428
+ * residues are enumerated by the dirty-recovery lister and removed by
429
+ * `clean --lbug-sidecars` (FIX 5). Never pre-delete the previous
430
+ * crash's parked copy on the bet that a rename will then succeed
431
+ * (tri-review 4669518496 P2-3: the old rm-first shape destroyed the
432
+ * prior forensics exactly when the source was locked and nothing
433
+ * replaced them).
434
+ * 3. rm-fallback: the source itself is locked for RENAME, but Windows
435
+ * lets some holders' files be unlink-marked — retry
436
+ * `rm(from, {force:true})` over the same budget and require the file
437
+ * verifiably GONE. Success eliminates the replay risk at the cost of
438
+ * the post-mortem forensics (logged exactly so).
439
+ * 4. Report in `failed` with the corrected lock guidance — the caller
440
+ * must abort (run-analyze throws a LbugWipeError in seconds instead
441
+ * of running the whole pipeline and dying at the wipe on the same
442
+ * handle).
443
+ *
444
+ * Per-suffix isolation: a `.wal` failure never skips the `.shadow`
445
+ * attempt.
446
+ *
447
+ * INVARIANT: after this function returns, either no original sidecar
448
+ * remains adjacent to the DB — every entry is in `moved` or `removed`, so
449
+ * every subsequent open this run performs is replay-free — or the entry is
450
+ * in `failed` and the caller MUST abort before any DB open.
451
+ *
452
+ * @returns `moved` — destination paths now holding the parked bytes;
453
+ * `removed` — source sidecars whose bytes are GONE (forensics lost, replay
454
+ * risk eliminated); `failed` — source sidecars still in place: a
455
+ * possibly-poisoned sidecar sits next to the DB and any pre-wipe open
456
+ * would replay it and die (there is no "wipe it in place" fallback).
457
+ */
458
+ export async function quarantineSidecarsForDirtyRecovery(dbPath, log) {
459
+ const moved = [];
460
+ const removed = [];
461
+ const failed = [];
462
+ for (const suffix of DIRTY_RECOVERY_SIDECAR_SUFFIXES) {
463
+ const from = `${dbPath}${suffix}`;
464
+ const to = `${from}.dirty-recovery`;
465
+ try {
466
+ if (!(await statIfExists(from)))
467
+ continue;
468
+ }
469
+ catch (err) {
470
+ // Non-ENOENT stat failure (EPERM/EBUSY class — statIfExists swallows
471
+ // ENOENT itself): assume the sidecar exists and is unreachable; the
472
+ // caller must fail safe.
473
+ failed.push(from);
474
+ log(sidecarParkRefusedWarning(from, err));
475
+ continue;
476
+ }
477
+ // 1. Rename, retried over the shared handle-release budget for the
478
+ // transient lock class only (EACCES/EPERM/EBUSY — an AV scan or
479
+ // handle-release lag clears within it; a structural failure like
480
+ // EEXIST goes straight to the confirm probe).
481
+ let outcome = 'rename-failed';
482
+ let renameErr;
483
+ for (let attempt = 1; attempt <= HANDLE_RELEASE_PROBE_ATTEMPTS; attempt++) {
484
+ try {
485
+ await fs.rename(from, to);
486
+ outcome = 'moved';
487
+ break;
488
+ }
489
+ catch (err) {
490
+ if (missing(err)) {
491
+ outcome = 'raced'; // source raced away between stat and rename
492
+ break;
493
+ }
494
+ renameErr = err;
495
+ if (!isPermissionRenameError(err) || attempt === HANDLE_RELEASE_PROBE_ATTEMPTS)
496
+ break;
497
+ await sleep(HANDLE_RELEASE_PROBE_DELAY_MS * attempt);
498
+ }
499
+ }
500
+ if (outcome === 'moved') {
501
+ moved.push(to);
502
+ continue;
503
+ }
504
+ if (outcome === 'raced')
505
+ continue;
506
+ // 2. Structural confirm probe (see TSDoc step 2).
507
+ const probe = `${to}.next`;
508
+ let probeLanded = false;
509
+ try {
510
+ await fs.rename(from, probe);
511
+ probeLanded = true;
512
+ }
513
+ catch (probeErr) {
514
+ if (missing(probeErr))
515
+ continue; // source raced away mid-probe
516
+ // Source locked for rename — fall through to the rm-fallback below.
517
+ }
518
+ if (probeLanded) {
519
+ try {
520
+ // True collision — replace the stale parked copy: newest forensics win.
521
+ await fs.rm(to, { force: true });
522
+ await fs.rename(probe, to);
523
+ moved.push(to);
524
+ }
525
+ catch {
526
+ // Double failure: the stale copy is itself locked/undeletable. The
527
+ // interrupted run's sidecar is already out of the replay path at the
528
+ // probe name, so the recovery open stays safe — keep both files.
529
+ moved.push(probe);
530
+ log(`Warning: parked ${path.basename(from)} as ${path.basename(probe)} — the stale ` +
531
+ `${path.basename(to)} from an earlier crash is locked and could not be replaced.`);
532
+ }
533
+ continue;
534
+ }
535
+ // 3. rm-fallback, retried over the same budget. `force: true` swallows
536
+ // ENOENT, so a resolving rm proves nothing on Windows (delete-pending
537
+ // keeps the name visible) — only a verifiably-absent file counts.
538
+ let removedOk = false;
539
+ for (let attempt = 1; attempt <= HANDLE_RELEASE_PROBE_ATTEMPTS; attempt++) {
540
+ try {
541
+ await fs.rm(from, { force: true });
542
+ }
543
+ catch {
544
+ /* verified below — the absence probe is authoritative */
545
+ }
546
+ let stillPresent = true;
547
+ try {
548
+ stillPresent = (await statIfExists(from)) !== null;
549
+ }
550
+ catch {
551
+ // Non-ENOENT stat failure: not verifiably gone — keep retrying.
552
+ }
553
+ if (!stillPresent) {
554
+ removedOk = true;
555
+ break;
556
+ }
557
+ if (attempt < HANDLE_RELEASE_PROBE_ATTEMPTS) {
558
+ await sleep(HANDLE_RELEASE_PROBE_DELAY_MS * attempt);
559
+ }
560
+ }
561
+ if (removedOk) {
562
+ removed.push(from);
563
+ log(`Removed ${path.basename(from)} from the interrupted run — it could not be parked ` +
564
+ 'aside (rename locked), so its bytes were deleted instead: post-mortem forensics ' +
565
+ 'are lost, but the replay risk is eliminated and recovery can proceed.');
566
+ continue;
567
+ }
568
+ // 4. Everything failed — the poisoned bytes still sit next to the DB.
569
+ failed.push(from);
570
+ log(sidecarParkRefusedWarning(from, renameErr));
571
+ }
572
+ if (moved.length > 0) {
573
+ log(`Parked ${moved.map((p) => path.basename(p)).join(', ')} from the interrupted run ` +
574
+ 'so the recovery rebuild opens without replaying it.');
575
+ }
576
+ return { moved, removed, failed };
577
+ }
257
578
  export async function listQuarantinedMissingShadowWals(dbPath) {
258
579
  const dir = path.dirname(dbPath);
259
580
  const base = path.basename(dbPath);
@@ -271,14 +592,95 @@ export async function listQuarantinedMissingShadowWals(dbPath) {
271
592
  .map((entry) => path.join(dir, entry))
272
593
  .sort();
273
594
  }
274
- export async function cleanQuarantinedMissingShadowWals(dbPath) {
275
- const files = await listQuarantinedMissingShadowWals(dbPath);
595
+ /**
596
+ * Shared unlink walker for the parked-sidecar cleaners (this shipping
597
+ * review, FIX 5). Per-file error policy: ENOENT is skipped silently (a
598
+ * list→delete race means the file is already gone — the desired state);
599
+ * EBUSY/EPERM/anything else lands in `failed` and the walk CONTINUES —
600
+ * the old per-family cleaners threw on the first locked file, crashing
601
+ * the whole clean mid-command after a partial deletion.
602
+ */
603
+ const unlinkParkedFiles = async (files) => {
276
604
  const deleted = [];
605
+ const failed = [];
277
606
  for (const file of files) {
278
- await fs.unlink(file);
279
- deleted.push(file);
607
+ try {
608
+ await fs.unlink(file);
609
+ deleted.push(file);
610
+ }
611
+ catch (err) {
612
+ if (missing(err))
613
+ continue;
614
+ failed.push(file);
615
+ }
616
+ }
617
+ return { deleted, failed };
618
+ };
619
+ /**
620
+ * Delete the missing-shadow WAL quarantines for `dbPath` and return the
621
+ * deleted paths. Locked files are skipped, not thrown (FIX 5) — user-facing
622
+ * surfaces should call {@link cleanParkedLbugSidecars}, which also REPORTS
623
+ * the skipped files.
624
+ */
625
+ export async function cleanQuarantinedMissingShadowWals(dbPath) {
626
+ return (await unlinkParkedFiles(await listQuarantinedMissingShadowWals(dbPath))).deleted;
627
+ }
628
+ /**
629
+ * List the `.dirty-recovery` sidecars parked beside `dbPath` by
630
+ * {@link quarantineSidecarsForDirtyRecovery}, so `gitnexus clean
631
+ * --lbug-sidecars` can surface them next to the missing-shadow quarantines
632
+ * (tri-review 4669518496 P2-7 — they were previously invisible to every
633
+ * cleanup surface). Only fixed names can exist (see
634
+ * {@link dirtyRecoveryParkedNames}: `<dbPath>.wal.dirty-recovery`,
635
+ * `<dbPath>.shadow.dirty-recovery`, and their `.next` probe residues from a
636
+ * double park failure — enumerated since FIX 5 of this shipping review; the
637
+ * docs used to say "remove manually" while no surface even listed them), so
638
+ * this stats them directly instead of prefix-scanning the directory the way
639
+ * the timestamped missing-shadow lister must.
640
+ *
641
+ * Returns existing parked files as sorted absolute paths. Branch-scoped
642
+ * index slots (`branches/<slug>/`) are outside `clean.ts`'s flat-path
643
+ * resolution — the same documented limitation as the missing-shadow pair.
644
+ */
645
+ export async function listParkedDirtyRecoverySidecars(dbPath) {
646
+ const present = [];
647
+ for (const parked of dirtyRecoveryParkedNames(dbPath)) {
648
+ if (await statIfExists(parked))
649
+ present.push(parked);
280
650
  }
281
- return deleted;
651
+ return present.sort();
652
+ }
653
+ /**
654
+ * Delete the `.dirty-recovery` parked sidecars for `dbPath` and return the
655
+ * deleted paths. Sibling of {@link cleanQuarantinedMissingShadowWals}; same
656
+ * skip-not-throw policy (FIX 5) — user-facing surfaces should call
657
+ * {@link cleanParkedLbugSidecars}, which also reports locked files.
658
+ */
659
+ export async function cleanParkedDirtyRecoverySidecars(dbPath) {
660
+ return (await unlinkParkedFiles(await listParkedDirtyRecoverySidecars(dbPath))).deleted;
661
+ }
662
+ /**
663
+ * Aggregate roster of every parked/quarantined sidecar family beside
664
+ * `dbPath` (this shipping review, FIX 5): the timestamped missing-shadow
665
+ * WAL quarantines plus the fixed-name dirty-recovery parks (`.next`
666
+ * residues included). Single roster authority for `clean --lbug-sidecars`
667
+ * — the command previously concatenated the families inline in two places,
668
+ * which is how the `.next` residue stayed invisible.
669
+ */
670
+ export async function listParkedLbugSidecars(dbPath) {
671
+ return [
672
+ ...(await listQuarantinedMissingShadowWals(dbPath)),
673
+ ...(await listParkedDirtyRecoverySidecars(dbPath)),
674
+ ];
675
+ }
676
+ /**
677
+ * Delete every file {@link listParkedLbugSidecars} enumerates. Per-file
678
+ * error policy via {@link unlinkParkedFiles}: ENOENT skipped silently,
679
+ * locked files collected into `failed` while the rest are still deleted —
680
+ * a locked parked file must not crash the whole clean mid-command.
681
+ */
682
+ export async function cleanParkedLbugSidecars(dbPath) {
683
+ return unlinkParkedFiles(await listParkedLbugSidecars(dbPath));
282
684
  }
283
685
  export const _resetSidecarRecoveryWarningsForTest = () => {
284
686
  warnedKeyCounts.clear();
@@ -1,4 +1,6 @@
1
+ import fs from 'fs';
1
2
  import os from 'os';
3
+ import path from 'path';
2
4
  import { createRequire } from 'module';
3
5
  const require = createRequire(import.meta.url);
4
6
  const packageVersion = (name) => {
@@ -6,7 +8,31 @@ const packageVersion = (name) => {
6
8
  return require(`${name}/package.json`).version;
7
9
  }
8
10
  catch {
9
- return undefined;
11
+ // Packages whose `exports` map omits ./package.json (e.g. @ladybugdb/core)
12
+ // reject the direct require with ERR_PACKAGE_PATH_NOT_EXPORTED, which made
13
+ // doctor print "LadybugDB: unknown" on every platform (#2374). Resolve the
14
+ // entry point instead and walk up to the package's own package.json.
15
+ try {
16
+ let dir = path.dirname(require.resolve(name));
17
+ // Entry points sit at the package root or a shallow dist/ dir; a few
18
+ // hops always reach the package's own package.json.
19
+ for (let hops = 0; hops < 5; hops++) {
20
+ const candidate = path.join(dir, 'package.json');
21
+ if (fs.existsSync(candidate)) {
22
+ const pkg = JSON.parse(fs.readFileSync(candidate, 'utf8'));
23
+ if (pkg.name === name)
24
+ return pkg.version;
25
+ }
26
+ const parent = path.dirname(dir);
27
+ if (parent === dir)
28
+ break;
29
+ dir = parent;
30
+ }
31
+ return undefined;
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
10
36
  }
11
37
  };
12
38
  const gitnexusVersion = () => {