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
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Classify a LadybugDB `LOAD EXTENSION` failure into one of four actionable
3
+ * classes and produce an accurate, literal-English remedy.
4
+ *
5
+ * Background (#2374): PR #2375 made the real LadybugDB LOAD error visible
6
+ * (instead of a false "not pre-installed" message). The rc.4 reproduction then
7
+ * showed the remaining defect — on Windows the extension file downloads and
8
+ * INSTALLs fine, but `LoadLibrary` fails with error 126 ("the specified module
9
+ * could not be found" / `找不到指定的模块`) because the extension dynamically
10
+ * imports OpenSSL 3 / MSVC 14 DLLs that ship nowhere. For that class, telling
11
+ * the user to reinstall/redownload is wrong — the file is fine; a *runtime
12
+ * dependency* is missing. This module decides which class an error is so each
13
+ * surface (doctor, --repair-fts, the analyze degrade warning, and
14
+ * ftsDegradedWarning) can emit the right remedy instead of a one-size-fits-all
15
+ * "reinstall over the network".
16
+ *
17
+ * `classifyExtensionLoadError` is pure string logic — no `@ladybugdb/core`
18
+ * import, no filesystem — which keeps `native-check.ts` free of a static lbug
19
+ * dependency. `diagnoseExtensionLoad` layers a LANGUAGE-INDEPENDENT structural
20
+ * check on top: it pulls the extension's file path out of lbug's own (English)
21
+ * wrapper and inspects the binary header directly (PE/ELF/Mach-O magic +
22
+ * architecture), so corrupt-vs-valid is decided by the file itself, not by the
23
+ * localized OS error tail. It reads the file (node:fs core module only, still no
24
+ * lbug) and never throws — any read failure degrades to the string classifier.
25
+ */
26
+ import { closeSync, openSync, readSync } from 'node:fs';
27
+ /** LadybugDB says the extension file was never installed. INSTALL can heal it. */
28
+ const MISSING_FILE_SIGNATURES = [
29
+ /has not been installed/i,
30
+ /not been installed/i,
31
+ ];
32
+ /**
33
+ * On-disk file corruption / wrong-platform. FORCE INSTALL re-downloads.
34
+ * Kept byte-identical to `FILE_CORRUPTION_SIGNATURES` in
35
+ * scripts/install-duckdb-extension.mjs (that `.mjs` cannot import this `.ts`;
36
+ * the duplication is deliberate — the two serve different call sites). Note
37
+ * `/not a valid/i` already covers Windows error 193 ("is not a valid Win32
38
+ * application"), so a truncated Windows download is caught here, before the
39
+ * missing-dependency branch.
40
+ */
41
+ // Exported so a parity test can assert this stays byte-identical to the copy in
42
+ // scripts/install-duckdb-extension.mjs (that `.mjs` cannot import this `.ts`), #2383 F5b.
43
+ export const FILE_CORRUPTION_SIGNATURES = [
44
+ /invalid elf/i,
45
+ /file too short/i,
46
+ /not a valid/i,
47
+ /bad magic/i,
48
+ /wrong architecture/i,
49
+ /mach-o/i,
50
+ /truncat/i,
51
+ ];
52
+ /**
53
+ * A *transitive dependency* of the extension is missing — the file loaded far
54
+ * enough to be found, but a library it needs is absent. Reinstalling the
55
+ * extension is a no-op for this class.
56
+ *
57
+ * WINDOWS CATCH-ALL GUARD (adversarial review): LadybugDB wraps *every* Windows
58
+ * load failure in `Failed to load library … which is needed by extension`, so
59
+ * that generic wrapper must NOT be sufficient — otherwise error 127 (wrong
60
+ * OpenSSL minor / unresolved procedure), 5 (AV/permission lock), and 1114
61
+ * (dependency DllMain failure) would all be mislabeled `missing_dependency` and
62
+ * told to install a runtime, the opposite of their real fix. We key strictly on
63
+ * the specific error-126 tail. Linux/macOS loaders name the missing library
64
+ * directly, so their signals are unambiguous.
65
+ *
66
+ * Localized Windows tails we do not enumerate (French, German, Japanese, …) and
67
+ * mojibake renderings of the Chinese text won't match here — but they still
68
+ * carry lbug's language-independent `Failed to load library` wrapper, so they
69
+ * are caught by the hedged fallback (LOAD_FAILURE_WRAPPER) with a non-committal
70
+ * remedy, never a wrong confident "reinstall" instruction.
71
+ */
72
+ const WINDOWS_MISSING_DEPENDENCY_SIGNATURES = [
73
+ /找不到指定的模块/,
74
+ /specified module could not be found/i,
75
+ ];
76
+ const POSIX_MISSING_DEPENDENCY_SIGNATURES = [
77
+ /cannot open shared object file/i, // Linux ld.so
78
+ /image not found/i, // macOS dyld
79
+ /library not loaded/i, // macOS dyld
80
+ ];
81
+ /**
82
+ * LadybugDB's own English wrapper for a dlopen/LoadLibrary failure
83
+ * (extension.cpp: `Failed to load library: {path} which is needed by extension:
84
+ * {name}`). It is emitted for EVERY extension load failure regardless of the OS
85
+ * display language — the only localized part is the OS-error tail after it. So
86
+ * it is the language-independent fallback signal once the specific tails miss: a
87
+ * French/German/Japanese Windows 126 has a localized tail we cannot enumerate,
88
+ * but it still carries this wrapper. See HEDGED_LOAD_FAILURE_REMEDY.
89
+ */
90
+ const LOAD_FAILURE_WRAPPER = /failed to load library/i;
91
+ const MISSING_FILE_REMEDY = 'The FTS extension is not installed. Re-run with network access and ' +
92
+ 'GITNEXUS_LBUG_EXTENSION_INSTALL=auto (or `gitnexus analyze --repair-fts`) to download it.';
93
+ const CORRUPT_FILE_REMEDY = 'The FTS extension file is present but unreadable (corrupt, truncated, or built for another ' +
94
+ 'platform). Re-download it with network access and GITNEXUS_LBUG_EXTENSION_INSTALL=auto ' +
95
+ '(`gitnexus analyze --repair-fts`).';
96
+ // Single source of truth for the VC++ runtime-install pointer, shared by the
97
+ // Windows-126 and structural missing-dependency remedies so the name/URL cannot
98
+ // drift between them (#2383 F5).
99
+ const VC_REDIST_INSTALL_HINT = 'the Microsoft Visual C++ 2015-2022 Redistributable (x64) from ' +
100
+ 'https://aka.ms/vs/17/release/vc_redist.x64.exe';
101
+ // MSVC-first per DuckDB's canonical answer for this exact error; OpenSSL second.
102
+ const WINDOWS_MISSING_DEPENDENCY_REMEDY = 'The FTS extension is present but a required runtime library is missing (Windows error 126). ' +
103
+ 'Reinstalling the extension will NOT help. Install ' +
104
+ VC_REDIST_INSTALL_HINT +
105
+ '; if the error persists, the extension also needs OpenSSL 3 ' +
106
+ '(libcrypto-3-x64.dll / libssl-3-x64.dll) on the DLL search path.';
107
+ const POSIX_MISSING_DEPENDENCY_REMEDY = 'The FTS extension is present but a shared library it depends on could not be loaded (named in ' +
108
+ 'the error above). Reinstalling the extension will NOT help — install that library or add it to ' +
109
+ 'your loader search path.';
110
+ // Language-independent fallback: we know the extension failed to load, but the
111
+ // OS-error tail is in a locale we did not enumerate, so we cannot say which class
112
+ // it is. Hedge honestly — point at the user's own localized error and give both
113
+ // branches — rather than confidently prescribing the wrong single fix. The clean
114
+ // long-term fix is upstream: have LadybugDB include the numeric GetLastError/errno
115
+ // in the message (as it already does elsewhere), so this becomes a code match.
116
+ const HEDGED_LOAD_FAILURE_REMEDY = 'The FTS extension file was found but could not be loaded — see the "Error:" text above (shown ' +
117
+ "in your system's language). Reinstalling usually will not help. If it names a missing module or " +
118
+ 'library, install the required runtime (on Windows: the Microsoft Visual C++ 2015-2022 ' +
119
+ 'Redistributable x64 and OpenSSL 3); if it names a corrupt or invalid file, run ' +
120
+ '`gitnexus analyze --repair-fts` to re-download.';
121
+ const UNKNOWN_REMEDY = 'The FTS extension failed to load for an unrecognized reason. Run `gitnexus doctor` for live ' +
122
+ 'FTS status and verify the extension file and platform.';
123
+ const matchesAny = (reason, signatures) => signatures.some((re) => re.test(reason));
124
+ /**
125
+ * Classify a collapsed LadybugDB LOAD error. Order is most-specific-first and is
126
+ * load-bearing: corrupt-file is tested before missing-dependency so a truncated
127
+ * Windows download (error 193, matched by `/not a valid/i`) routes to
128
+ * FORCE-reinstall rather than to the runtime-install remedy.
129
+ */
130
+ export function classifyExtensionLoadError(reason) {
131
+ const text = reason ?? '';
132
+ if (matchesAny(text, MISSING_FILE_SIGNATURES)) {
133
+ return { kind: 'missing_file', remedy: MISSING_FILE_REMEDY };
134
+ }
135
+ if (matchesAny(text, FILE_CORRUPTION_SIGNATURES)) {
136
+ return { kind: 'corrupt_file', remedy: CORRUPT_FILE_REMEDY };
137
+ }
138
+ if (matchesAny(text, WINDOWS_MISSING_DEPENDENCY_SIGNATURES)) {
139
+ return { kind: 'missing_dependency', remedy: WINDOWS_MISSING_DEPENDENCY_REMEDY };
140
+ }
141
+ if (matchesAny(text, POSIX_MISSING_DEPENDENCY_SIGNATURES)) {
142
+ return { kind: 'missing_dependency', remedy: POSIX_MISSING_DEPENDENCY_REMEDY };
143
+ }
144
+ // Language-independent fallback: the extension demonstrably failed to load
145
+ // (lbug's English wrapper is present) but the localized OS tail matched no
146
+ // specific class. Treat as a dependency/runtime load failure with a hedged
147
+ // remedy — strictly better than the generic `unknown` for non-English hosts,
148
+ // and it never prescribes the wrong fix.
149
+ if (LOAD_FAILURE_WRAPPER.test(text)) {
150
+ return { kind: 'missing_dependency', remedy: HEDGED_LOAD_FAILURE_REMEDY };
151
+ }
152
+ return { kind: 'unknown', remedy: UNKNOWN_REMEDY };
153
+ }
154
+ const STRUCTURAL_MISSING_DEPENDENCY_REMEDY = 'The FTS extension file is valid, so the failure is a missing or incompatible runtime dependency, ' +
155
+ 'not the extension itself — reinstalling will NOT help. On Windows, install ' +
156
+ VC_REDIST_INSTALL_HINT +
157
+ ' and ensure OpenSSL 3 is available; on Linux/macOS install the shared library named in the error above.';
158
+ /**
159
+ * Pull the extension file path out of lbug's load error. lbug's wrapper is
160
+ * English regardless of OS language — `Failed to load library: {path} which is
161
+ * needed by extension: {name}` (real lbug), or the quoted `Failed to load
162
+ * library '{path}': {reason}` variant — so the path is recoverable in any locale.
163
+ * Only paths ending in `.lbug_extension` are accepted, so a regex misfire can
164
+ * never point the inspector at an arbitrary file.
165
+ */
166
+ export function extractExtensionPath(reason) {
167
+ const text = reason ?? '';
168
+ const m = /failed to load library:?\s*['"]?(.+?\.lbug_extension)/i.exec(text);
169
+ const path = m?.[1]?.trim();
170
+ return path && path.length > 0 ? path : null;
171
+ }
172
+ /** Node `process.arch` → PE `Machine`. Undefined for arches we don't map. */
173
+ const PE_MACHINE = { x64: 0x8664, arm64: 0xaa64 };
174
+ /** Node `process.arch` → ELF `e_machine`. */
175
+ const ELF_MACHINE = { x64: 0x3e, arm64: 0xb7 };
176
+ /** Node `process.arch` → Mach-O `cputype`. */
177
+ const MACHO_CPUTYPE = { x64: 0x01000007, arm64: 0x0100000c };
178
+ function classifyPE(buf, bytesRead, arch) {
179
+ if (bytesRead < 0x40 || buf[0] !== 0x4d || buf[1] !== 0x5a)
180
+ return 'corrupt'; // 'MZ'
181
+ const peOffset = buf.readUInt32LE(0x3c);
182
+ // The PE header (e_lfanew) points beyond what we read. A large-DOS-stub VALID PE
183
+ // and a garbage e_lfanew are indistinguishable from here, so don't claim 'corrupt'
184
+ // — defer to the loader's own report (#2383 F1-secondary).
185
+ if (peOffset + 6 > bytesRead)
186
+ return 'indeterminate';
187
+ const isPE = buf[peOffset] === 0x50 &&
188
+ buf[peOffset + 1] === 0x45 &&
189
+ buf[peOffset + 2] === 0 &&
190
+ buf[peOffset + 3] === 0;
191
+ if (!isPE)
192
+ return 'corrupt';
193
+ const expected = PE_MACHINE[arch];
194
+ if (expected === undefined)
195
+ return 'valid'; // arch we don't map: don't claim corrupt
196
+ return buf.readUInt16LE(peOffset + 4) === expected ? 'valid' : 'corrupt';
197
+ }
198
+ function classifyELF(buf, bytesRead, arch) {
199
+ if (bytesRead < 20)
200
+ return 'corrupt';
201
+ if (buf[0] !== 0x7f || buf[1] !== 0x45 || buf[2] !== 0x4c || buf[3] !== 0x46)
202
+ return 'corrupt'; // 0x7F ELF
203
+ const littleEndian = buf[5] === 1; // EI_DATA
204
+ const eMachine = littleEndian ? buf.readUInt16LE(18) : buf.readUInt16BE(18);
205
+ const expected = ELF_MACHINE[arch];
206
+ if (expected === undefined)
207
+ return 'valid';
208
+ return eMachine === expected ? 'valid' : 'corrupt';
209
+ }
210
+ function classifyMachO(buf, bytesRead, arch) {
211
+ if (bytesRead < 8)
212
+ return 'corrupt';
213
+ const magicLE = buf.readUInt32LE(0);
214
+ const magicBE = buf.readUInt32BE(0);
215
+ // Universal ("fat") binary — assume it carries the host slice.
216
+ if (magicBE === 0xcafebabe || magicLE === 0xcafebabe)
217
+ return 'valid';
218
+ const thin = magicLE === 0xfeedfacf || magicLE === 0xfeedface;
219
+ const thinSwapped = magicBE === 0xfeedfacf || magicBE === 0xfeedface;
220
+ if (!thin && !thinSwapped)
221
+ return 'corrupt';
222
+ const cpuType = thin ? buf.readUInt32LE(4) : buf.readUInt32BE(4);
223
+ const expected = MACHO_CPUTYPE[arch];
224
+ if (expected === undefined)
225
+ return 'valid';
226
+ return cpuType === expected ? 'valid' : 'corrupt';
227
+ }
228
+ /**
229
+ * Decide whether a binary header is a well-formed shared library for the given
230
+ * platform + architecture — using only the file's structure, no localized text.
231
+ * Pure and injectable (platform/arch as params) so every format+arch combination
232
+ * is unit-testable regardless of the host it runs on.
233
+ */
234
+ export function classifyBinaryHeader(buf, bytesRead, platform, arch) {
235
+ if (platform === 'win32')
236
+ return classifyPE(buf, bytesRead, arch);
237
+ if (platform === 'linux')
238
+ return classifyELF(buf, bytesRead, arch);
239
+ if (platform === 'darwin')
240
+ return classifyMachO(buf, bytesRead, arch);
241
+ return 'valid'; // unknown host: never claim corrupt
242
+ }
243
+ const BINARY_HEADER_BYTES = 4096;
244
+ /**
245
+ * Best-effort language-independent inspection of the extension file. Reads the
246
+ * header and classifies it; never throws — a missing file is `absent`, an
247
+ * unreadable one is `indeterminate`.
248
+ */
249
+ export function inspectExtensionBinary(extensionPath) {
250
+ if (!extensionPath)
251
+ return 'indeterminate';
252
+ let fd;
253
+ try {
254
+ fd = openSync(extensionPath, 'r');
255
+ }
256
+ catch (err) {
257
+ return err?.code === 'ENOENT' ? 'absent' : 'indeterminate';
258
+ }
259
+ try {
260
+ const buf = Buffer.alloc(BINARY_HEADER_BYTES);
261
+ const bytesRead = readSync(fd, buf, 0, BINARY_HEADER_BYTES, 0);
262
+ return classifyBinaryHeader(buf, bytesRead, process.platform, process.arch);
263
+ }
264
+ catch {
265
+ return 'indeterminate';
266
+ }
267
+ finally {
268
+ try {
269
+ closeSync(fd);
270
+ }
271
+ catch {
272
+ /* closing the probe fd must never surface */
273
+ }
274
+ }
275
+ }
276
+ /**
277
+ * Diagnose a LadybugDB load failure, preferring a LANGUAGE-INDEPENDENT structural
278
+ * check of the extension binary over the localized error text:
279
+ * - file absent → missing_file
280
+ * - present but malformed → corrupt_file (bad magic / wrong architecture)
281
+ * - present and well-formed → missing_dependency (a valid binary the loader rejected)
282
+ * The path comes from lbug's own English wrapper, so this holds in any OS display
283
+ * language. When the file cannot be located or read, it falls back to the string
284
+ * classifier (which still carries the language-independent hedged fallback). This
285
+ * is the entry point every surface should call.
286
+ */
287
+ export function diagnoseExtensionLoad(reason) {
288
+ const text = reason ?? '';
289
+ const stringResult = classifyExtensionLoadError(text);
290
+ const fileState = inspectExtensionBinary(extractExtensionPath(text));
291
+ if (fileState === 'corrupt') {
292
+ return { kind: 'corrupt_file', remedy: CORRUPT_FILE_REMEDY };
293
+ }
294
+ if (fileState === 'valid') {
295
+ // The structural probe only inspects the first BINARY_HEADER_BYTES, so a file
296
+ // truncated AFTER its header still reads 'valid'. When the loader itself reported
297
+ // corruption (e.g. "file too short" / Windows error 193 "not a valid Win32
298
+ // application"), that whole-file verdict is stronger evidence than an intact-looking
299
+ // header — honor it and route to re-download, not a runtime-dependency install (#2383
300
+ // F1). Localized corrupt tails classify as hedged missing_dependency (not
301
+ // corrupt_file), so they still fall through to the dependency remedy below.
302
+ if (stringResult.kind === 'corrupt_file') {
303
+ return stringResult;
304
+ }
305
+ // A structurally sound binary that still failed to load ⇒ a dependency/runtime
306
+ // problem, decided WITHOUT the localized tail. Keep the string classifier's
307
+ // sharper remedy when it recognized the specific case (e.g. English 126).
308
+ const remedy = stringResult.kind === 'missing_dependency'
309
+ ? stringResult.remedy
310
+ : STRUCTURAL_MISSING_DEPENDENCY_REMEDY;
311
+ return { kind: 'missing_dependency', remedy };
312
+ }
313
+ // 'absent' or 'indeterminate' → no positive structural evidence, so defer to the
314
+ // string classifier. Note a real never-installed extension has NO path in its
315
+ // reason (lbug says "has not been installed"), so it lands here via
316
+ // 'indeterminate' and the string classifier reports missing_file correctly; a
317
+ // path that lbug named but that is now gone (stale/racy) is better judged by
318
+ // what lbug actually reported than by re-deriving from disk.
319
+ return stringResult;
320
+ }
@@ -1,3 +1,4 @@
1
+ import { type ExtensionLoadDiagnosis } from './extension-load-error.js';
1
2
  /**
2
3
  * Lifecycle policy for an optional DuckDB extension.
3
4
  *
@@ -20,6 +21,12 @@ export interface ExtensionCapability {
20
21
  loaded: boolean;
21
22
  /** Human-readable reason when `loaded` is false. */
22
23
  reason?: string;
24
+ /**
25
+ * Classified diagnosis of `reason`, computed ONCE at mark-unavailable time so
26
+ * per-request surfaces (ftsDegradedWarning on /api/search + MCP query) read the
27
+ * cached remedy instead of re-inspecting the extension file on every call (#2383 F3).
28
+ */
29
+ diagnosis?: ExtensionLoadDiagnosis;
23
30
  }
24
31
  /** Per-call overrides applied on top of `ExtensionManager` defaults. */
25
32
  export interface ExtensionEnsureOptions {
@@ -29,7 +36,7 @@ export interface ExtensionEnsureOptions {
29
36
  export interface ExtensionManagerOptions {
30
37
  policy?: ExtensionInstallPolicy;
31
38
  installTimeoutMs?: number;
32
- installExtension?: (extensionName: string, timeoutMs: number) => Promise<ExtensionInstallResult>;
39
+ installExtension?: (extensionName: string, timeoutMs: number, loadError?: string) => Promise<ExtensionInstallResult>;
33
40
  warn?: (message: string) => void;
34
41
  }
35
42
  export declare const getExtensionInstallPolicy: () => ExtensionInstallPolicy;
@@ -58,7 +65,7 @@ export declare const getExtensionInstallChildProcessArgs: (extensionName: string
58
65
  * If the child exceeds `timeoutMs` the parent kills it with SIGKILL and
59
66
  * resolves with `timedOut: true`.
60
67
  */
61
- export declare const installDuckDbExtensionOutOfProcess: (extensionName: string, timeoutMs?: number) => Promise<ExtensionInstallResult>;
68
+ export declare const installDuckDbExtensionOutOfProcess: (extensionName: string, timeoutMs?: number, loadError?: string) => Promise<ExtensionInstallResult>;
62
69
  /**
63
70
  * Centralized lifecycle manager for optional LadybugDB extensions.
64
71
  *
@@ -90,6 +97,14 @@ export declare class ExtensionManager {
90
97
  * paths are expected to degrade gracefully.
91
98
  */
92
99
  ensure(query: (sql: string) => Promise<unknown>, name: string, label: string, opts?: ExtensionEnsureOptions): Promise<boolean>;
100
+ /**
101
+ * Attempt `LOAD EXTENSION <name>`; returns `null` on success and the
102
+ * collapsed error message on failure. The message is the load-side ground
103
+ * truth — LadybugDB distinguishes a missing extension file from a present
104
+ * but unloadable one (wrong platform, truncated download, version mismatch),
105
+ * and discarding it left users staring at "not pre-installed" when the file
106
+ * existed all along (#2374).
107
+ */
93
108
  private tryLoad;
94
109
  private markLoaded;
95
110
  private markUnavailable;
@@ -1,12 +1,15 @@
1
1
  import { spawn } from 'child_process';
2
2
  import { fileURLToPath } from 'node:url';
3
3
  import { LBUG_MAX_DB_SIZE } from './lbug-config.js';
4
+ import { diagnoseExtensionLoad } from './extension-load-error.js';
4
5
  import { logger } from '../logger.js';
5
6
  const DEFAULT_EXTENSION_INSTALL_TIMEOUT_MS = 15_000;
6
7
  const EXTENSION_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/;
7
8
  const alreadyAvailable = (message) => message.includes('already loaded') ||
8
9
  message.includes('already installed') ||
9
10
  message.includes('already exists');
11
+ /** LadybugDB errors are multi-line; collapse for single-line warn/reason strings. */
12
+ const oneLine = (value) => value.replace(/\s+/g, ' ').trim();
10
13
  const resolvePolicyFromEnv = () => {
11
14
  const raw = process.env.GITNEXUS_LBUG_EXTENSION_INSTALL;
12
15
  if (raw === 'load-only' || raw === 'never' || raw === 'auto')
@@ -51,7 +54,7 @@ export const getExtensionInstallChildProcessArgs = (extensionName, maxDbSize = L
51
54
  * If the child exceeds `timeoutMs` the parent kills it with SIGKILL and
52
55
  * resolves with `timedOut: true`.
53
56
  */
54
- export const installDuckDbExtensionOutOfProcess = async (extensionName, timeoutMs = getExtensionInstallTimeoutMs()) => {
57
+ export const installDuckDbExtensionOutOfProcess = async (extensionName, timeoutMs = getExtensionInstallTimeoutMs(), loadError) => {
55
58
  if (!EXTENSION_NAME_PATTERN.test(extensionName)) {
56
59
  throw new Error(`Invalid DuckDB extension name: ${extensionName}`);
57
60
  }
@@ -60,6 +63,9 @@ export const installDuckDbExtensionOutOfProcess = async (extensionName, timeoutM
60
63
  env: {
61
64
  ...process.env,
62
65
  GITNEXUS_LBUG_EXTENSION_NAME: extensionName,
66
+ // The child picks INSTALL vs FORCE INSTALL from this LOAD error so it
67
+ // only re-downloads when the on-disk extension file is actually broken.
68
+ ...(loadError ? { GITNEXUS_LBUG_EXTENSION_LOAD_ERROR: loadError } : {}),
63
69
  },
64
70
  stdio: ['ignore', 'ignore', 'pipe'],
65
71
  windowsHide: true,
@@ -78,7 +84,7 @@ export const installDuckDbExtensionOutOfProcess = async (extensionName, timeoutM
78
84
  resolve({
79
85
  success: false,
80
86
  timedOut: true,
81
- message: `INSTALL ${extensionName} timed out after ${timeoutMs}ms`,
87
+ message: `extension install for ${extensionName} timed out after ${timeoutMs}ms`,
82
88
  });
83
89
  }, timeoutMs);
84
90
  child.on('error', (err) => {
@@ -97,8 +103,8 @@ export const installDuckDbExtensionOutOfProcess = async (extensionName, timeoutM
97
103
  success: code === 0,
98
104
  timedOut: false,
99
105
  message: code === 0
100
- ? `INSTALL ${extensionName} completed`
101
- : `INSTALL ${extensionName} failed with ${signal ?? `exit code ${code}`}${stderr ? `: ${stderr.trim()}` : ''}`,
106
+ ? `extension install for ${extensionName} completed`
107
+ : `extension install for ${extensionName} failed with ${signal ?? `exit code ${code}`}${stderr ? `: ${stderr.trim()}` : ''}`,
102
108
  });
103
109
  });
104
110
  });
@@ -152,46 +158,65 @@ export class ExtensionManager {
152
158
  this.markUnavailable(name, label, 'extension install policy is "never"', warn);
153
159
  return false;
154
160
  }
155
- if (await this.tryLoad(query, name)) {
161
+ const loadError = await this.tryLoad(query, name);
162
+ if (loadError === null) {
156
163
  this.markLoaded(name);
157
164
  return true;
158
165
  }
159
166
  if (policy === 'load-only') {
160
- this.markUnavailable(name, label, 'load-only policy: extension not pre-installed', warn);
167
+ this.markUnavailable(name, label, `load-only policy (no install attempted); LOAD ${name} failed: ${loadError}`, warn);
161
168
  return false;
162
169
  }
163
170
  let install = this.installAttempted.get(name);
164
171
  if (!install) {
165
172
  const installFn = this.options.installExtension ?? installDuckDbExtensionOutOfProcess;
166
- install = await installFn(name, timeoutMs);
173
+ // Hand the child the LOAD error so it re-downloads (FORCE) only when the
174
+ // present extension file is provably broken, not on every LOAD failure.
175
+ install = await installFn(name, timeoutMs, loadError);
167
176
  this.installAttempted.set(name, install);
168
177
  }
169
178
  if (!install.success) {
170
- this.markUnavailable(name, label, install.message, warn);
179
+ this.markUnavailable(name, label, `${install.message}; LOAD ${name} had failed: ${loadError}`, warn);
171
180
  return false;
172
181
  }
173
- if (await this.tryLoad(query, name)) {
182
+ const retryError = await this.tryLoad(query, name);
183
+ if (retryError === null) {
174
184
  this.markLoaded(name);
175
185
  return true;
176
186
  }
177
- this.markUnavailable(name, label, `LOAD ${name} failed after successful INSTALL`, warn);
187
+ this.markUnavailable(name, label, `LOAD ${name} failed after successful INSTALL: ${retryError}`, warn);
178
188
  return false;
179
189
  }
190
+ /**
191
+ * Attempt `LOAD EXTENSION <name>`; returns `null` on success and the
192
+ * collapsed error message on failure. The message is the load-side ground
193
+ * truth — LadybugDB distinguishes a missing extension file from a present
194
+ * but unloadable one (wrong platform, truncated download, version mismatch),
195
+ * and discarding it left users staring at "not pre-installed" when the file
196
+ * existed all along (#2374).
197
+ */
180
198
  async tryLoad(query, name) {
181
199
  try {
182
200
  await query(`LOAD EXTENSION ${name}`);
183
- return true;
201
+ return null;
184
202
  }
185
203
  catch (err) {
186
204
  const msg = err instanceof Error ? err.message : String(err);
187
- return alreadyAvailable(msg);
205
+ return alreadyAvailable(msg) ? null : oneLine(msg);
188
206
  }
189
207
  }
190
208
  markLoaded(name) {
191
209
  this.capabilities.set(name, { name, loaded: true });
192
210
  }
193
211
  markUnavailable(name, label, reason, warn) {
194
- this.capabilities.set(name, { name, loaded: false, reason });
212
+ // Classify once here (the single load-failure sink, run per Database not per
213
+ // request) so the hot per-request warning path does no file I/O (#2383 F3).
214
+ this.capabilities.set(name, {
215
+ name,
216
+ loaded: false,
217
+ reason,
218
+ diagnosis: diagnoseExtensionLoad(reason),
219
+ });
195
220
  const key = `${name}:${reason}`;
196
221
  if (this.warnedKeys.has(key))
197
222
  return;