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
@@ -12,6 +12,27 @@ import { markCppInlineNamespaceRange } from './inline-namespaces.js';
12
12
  import { extractCppTemplateConstraints } from './constraint-extractor.js';
13
13
  import { captureCppMemberLookupFacts } from './member-lookup.js';
14
14
  import { CPP_BRACED_INIT_TYPE_PREFIX } from './conversion-rank.js';
15
+ import { logger } from '../../../logger.js';
16
+ /**
17
+ * Per-file wall-clock budget for the capture-emit loop (#2432). A worker
18
+ * thread stuck in this loop cannot be terminated safely (terminating a
19
+ * thread mid-N-API call aborts the whole process with Napi::Error), so the
20
+ * loop must bound itself: on breach we return the captures accumulated so
21
+ * far with a warning — degraded coverage for one file, never a crash or a
22
+ * thrown error (a throw here would make the language-group catch drop every
23
+ * remaining file in the batch).
24
+ *
25
+ * `GITNEXUS_CPP_CAPTURE_BUDGET_MS`: unset/invalid/negative → 20000; explicit
26
+ * 0 → expires immediately (deterministic test hook).
27
+ */
28
+ const CPP_CAPTURE_BUDGET_DEFAULT_MS = 20_000;
29
+ function cppCaptureBudgetMs() {
30
+ const raw = process.env.GITNEXUS_CPP_CAPTURE_BUDGET_MS;
31
+ if (raw === undefined || raw === '')
32
+ return CPP_CAPTURE_BUDGET_DEFAULT_MS;
33
+ const parsed = Number(raw);
34
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : CPP_CAPTURE_BUDGET_DEFAULT_MS;
35
+ }
15
36
  export function emitCppScopeCaptures(sourceText, filePath, cachedTree) {
16
37
  let tree = cachedTree;
17
38
  if (tree === undefined) {
@@ -21,10 +42,27 @@ export function emitCppScopeCaptures(sourceText, filePath, cachedTree) {
21
42
  }
22
43
  const rawMatches = getCppScopeQuery().matches(tree.rootNode);
23
44
  const out = [];
45
+ // #2432: reset the per-file lookup index. The identifier-argument type
46
+ // lookups below used to re-walk the AST per identifier (full-tree DFS in
47
+ // isKnownEnumName, per-scope declaration scans) — O(calls × args × treeSize)
48
+ // per file, 151s on a 194KB file that parses in 46ms. The index makes each
49
+ // lookup O(1) after a single lazily-built pass.
50
+ resetCppFileLookupIndex();
24
51
  // Track ranges where typedef-struct/enum was captured as its concrete type
25
52
  // so we can suppress the duplicate @declaration.typedef match.
26
53
  const concreteTypedefRanges = new Set();
54
+ // #2432: per-file deadline for the loop below (see cppCaptureBudgetMs).
55
+ // Checked every 64 matches — post-index a single iteration is microseconds,
56
+ // so the check granularity costs nothing and bounds the drift past the
57
+ // deadline to well under a second.
58
+ const budgetMs = cppCaptureBudgetMs();
59
+ const deadline = Date.now() + budgetMs;
60
+ let matchIndex = 0;
27
61
  for (const m of rawMatches) {
62
+ if ((matchIndex++ & 63) === 0 && Date.now() >= deadline) {
63
+ logger.warn({ filePath, budgetMs, processedMatches: matchIndex - 1, totalMatches: rawMatches.length }, `C++ capture extraction exceeded its ${budgetMs}ms budget for ${filePath}; returning partial captures for this file (#2432).`);
64
+ break;
65
+ }
28
66
  const grouped = {};
29
67
  // Parallel tag -> captured SyntaxNode map. The tree-sitter query already
30
68
  // hands us each matched node as `c.node`, so anchors resolve via a
@@ -960,6 +998,47 @@ function inferCppBracedInitType(node) {
960
998
  ? `${CPP_BRACED_INIT_TYPE_PREFIX}${first}:${elementTypes.length}`
961
999
  : `${CPP_BRACED_INIT_TYPE_PREFIX}unknown:${elementTypes.length}`;
962
1000
  }
1001
+ let fileLookupIndex = {
1002
+ enumNames: null,
1003
+ scopeDecls: new Map(),
1004
+ fnParams: new Map(),
1005
+ };
1006
+ function resetCppFileLookupIndex() {
1007
+ fileLookupIndex = { enumNames: null, scopeDecls: new Map(), fnParams: new Map() };
1008
+ }
1009
+ /**
1010
+ * First-declaration-wins map of the scope's `declaration` children that
1011
+ * carry a concrete (non-placeholder) type and a nameable declarator —
1012
+ * exactly the entries the replaced per-identifier scans could match.
1013
+ */
1014
+ function scopeDeclarationsFor(scope) {
1015
+ const cached = fileLookupIndex.scopeDecls.get(scope.id);
1016
+ if (cached !== undefined)
1017
+ return cached;
1018
+ const decls = new Map();
1019
+ for (let i = 0; i < scope.childCount; i++) {
1020
+ const stmt = scope.child(i);
1021
+ if (stmt === null || stmt.type !== 'declaration')
1022
+ continue;
1023
+ const typeNode = stmt.childForFieldName('type');
1024
+ if (typeNode === null)
1025
+ continue;
1026
+ if (typeNode.type === 'placeholder_type_specifier')
1027
+ continue;
1028
+ const declarator = stmt.childForFieldName('declarator');
1029
+ if (declarator === null)
1030
+ continue;
1031
+ const nameChild = declaredNameNode(declarator);
1032
+ if (nameChild === null)
1033
+ continue;
1034
+ const name = extractDeclaratorLeafName(nameChild);
1035
+ if (name === '' || decls.has(name))
1036
+ continue;
1037
+ decls.set(name, stmt);
1038
+ }
1039
+ fileLookupIndex.scopeDecls.set(scope.id, decls);
1040
+ return decls;
1041
+ }
963
1042
  /**
964
1043
  * Look up the declared type of a variable by scanning sibling declarations
965
1044
  * in the enclosing compound_statement (function body). Handles:
@@ -991,27 +1070,14 @@ function lookupDeclaredTypeForIdentifier(identNode) {
991
1070
  const paramType = lookupFunctionParameterType(scope, varName);
992
1071
  if (paramType !== '')
993
1072
  return paramType;
994
- // Scan declarations in the scope for a matching variable name
995
- for (let i = 0; i < scope.childCount; i++) {
996
- const stmt = scope.child(i);
997
- if (stmt === null || stmt.type !== 'declaration')
998
- continue;
999
- const typeNode = stmt.childForFieldName('type');
1000
- if (typeNode === null)
1001
- continue;
1002
- // Skip auto/placeholder types — those need chain-follow, not literal
1003
- if (typeNode.type === 'placeholder_type_specifier')
1004
- continue;
1005
- // Check init_declarator children for the variable name
1006
- const declarator = stmt.childForFieldName('declarator');
1007
- if (declarator === null)
1008
- continue;
1009
- const nameChild = declaredNameNode(declarator);
1010
- if (nameChild !== null && extractDeclaratorLeafName(nameChild) === varName) {
1011
- return normalizeCppTypeText(typeNode.text);
1012
- }
1013
- }
1014
- return '';
1073
+ // Indexed scope-declaration lookup (#2432; was a per-identifier scan).
1074
+ const stmt = scopeDeclarationsFor(scope).get(varName);
1075
+ if (stmt === undefined)
1076
+ return '';
1077
+ const typeNode = stmt.childForFieldName('type');
1078
+ if (typeNode === null)
1079
+ return '';
1080
+ return normalizeCppTypeText(typeNode.text);
1015
1081
  }
1016
1082
  function lookupDeclaredTypeClassForIdentifier(identNode) {
1017
1083
  const varName = identNode.text;
@@ -1026,28 +1092,20 @@ function lookupDeclaredTypeClassForIdentifier(identNode) {
1026
1092
  const paramTypeClass = lookupFunctionParameterTypeClass(scope, varName, identNode);
1027
1093
  if (paramTypeClass !== undefined)
1028
1094
  return paramTypeClass;
1029
- for (let i = 0; i < scope.childCount; i++) {
1030
- const stmt = scope.child(i);
1031
- if (stmt === null || stmt.type !== 'declaration')
1032
- continue;
1033
- const typeNode = stmt.childForFieldName('type');
1034
- if (typeNode === null)
1035
- continue;
1036
- if (typeNode.type === 'placeholder_type_specifier')
1037
- continue;
1038
- const declarator = stmt.childForFieldName('declarator');
1039
- if (declarator === null)
1040
- continue;
1041
- const nameChild = declaredNameNode(declarator);
1042
- if (nameChild === null || extractDeclaratorLeafName(nameChild) !== varName)
1043
- continue;
1044
- const typeClass = classifyCppParameterType(typeNode.text, nameChild.text, stmt.text.replace(/;\s*$/, ''));
1045
- if (isKnownEnumName(identNode, typeClass.base)) {
1046
- return { ...typeClass, base: `enum:${typeClass.base}` };
1047
- }
1048
- return typeClass;
1095
+ // Indexed scope-declaration lookup (#2432; was a per-identifier scan).
1096
+ const stmt = scopeDeclarationsFor(scope).get(varName);
1097
+ if (stmt === undefined)
1098
+ return unknownTypeClass('unknown');
1099
+ const typeNode = stmt.childForFieldName('type');
1100
+ const declarator = stmt.childForFieldName('declarator');
1101
+ const nameChild = declarator !== null ? declaredNameNode(declarator) : null;
1102
+ if (typeNode === null || nameChild === null)
1103
+ return unknownTypeClass('unknown');
1104
+ const typeClass = classifyCppParameterType(typeNode.text, nameChild.text, stmt.text.replace(/;\s*$/, ''));
1105
+ if (isKnownEnumName(identNode, typeClass.base)) {
1106
+ return { ...typeClass, base: `enum:${typeClass.base}` };
1049
1107
  }
1050
- return unknownTypeClass('unknown');
1108
+ return typeClass;
1051
1109
  }
1052
1110
  function lookupFunctionParameterType(scope, varName) {
1053
1111
  const param = findEnclosingFunctionParameter(scope, varName);
@@ -1078,27 +1136,45 @@ function findEnclosingFunctionParameter(scope, varName) {
1078
1136
  let node = scope.parent;
1079
1137
  while (node !== null) {
1080
1138
  if (node.type === 'function_definition' || node.type === 'function_declarator') {
1081
- const fnDecl = node.type === 'function_declarator'
1082
- ? node
1083
- : findFirstDescendantOfType(node, 'function_declarator');
1084
- const params = fnDecl?.childForFieldName('parameters') ?? null;
1085
- if (params !== null) {
1086
- for (let i = 0; i < params.namedChildCount; i++) {
1087
- const param = params.namedChild(i);
1088
- if (param === null || param.type !== 'parameter_declaration')
1089
- continue;
1090
- const declarator = param.childForFieldName('declarator');
1091
- if (declarator !== null && extractDeclaratorLeafName(declarator) === varName) {
1092
- return param;
1093
- }
1094
- }
1095
- }
1096
- return null;
1139
+ return enclosingFunctionParametersFor(node)?.get(varName) ?? null;
1097
1140
  }
1098
1141
  node = node.parent;
1099
1142
  }
1100
1143
  return null;
1101
1144
  }
1145
+ /**
1146
+ * First-wins map of a function's `parameter_declaration`s by declarator
1147
+ * leaf name (#2432; was a per-identifier scan). `null` when the function
1148
+ * has no parameter list — the caller returns null without walking further
1149
+ * up, preserving the replaced scan's early-return.
1150
+ */
1151
+ function enclosingFunctionParametersFor(fnNode) {
1152
+ const cached = fileLookupIndex.fnParams.get(fnNode.id);
1153
+ if (cached !== undefined)
1154
+ return cached;
1155
+ const fnDecl = fnNode.type === 'function_declarator'
1156
+ ? fnNode
1157
+ : findFirstDescendantOfType(fnNode, 'function_declarator');
1158
+ const params = fnDecl?.childForFieldName('parameters') ?? null;
1159
+ let index = null;
1160
+ if (params !== null) {
1161
+ index = new Map();
1162
+ for (let i = 0; i < params.namedChildCount; i++) {
1163
+ const param = params.namedChild(i);
1164
+ if (param === null || param.type !== 'parameter_declaration')
1165
+ continue;
1166
+ const declarator = param.childForFieldName('declarator');
1167
+ if (declarator === null)
1168
+ continue;
1169
+ const name = extractDeclaratorLeafName(declarator);
1170
+ if (name === '' || index.has(name))
1171
+ continue;
1172
+ index.set(name, param);
1173
+ }
1174
+ }
1175
+ fileLookupIndex.fnParams.set(fnNode.id, index);
1176
+ return index;
1177
+ }
1102
1178
  function declaredNameNode(declarator) {
1103
1179
  if (declarator.type !== 'init_declarator')
1104
1180
  return declarator;
@@ -1126,24 +1202,31 @@ function normalizeCppTypeText(text) {
1126
1202
  function isKnownEnumName(node, typeName) {
1127
1203
  if (typeName === '' || typeName === 'unknown')
1128
1204
  return false;
1129
- let root = node;
1130
- while (root.parent !== null)
1131
- root = root.parent;
1132
- const stack = [root];
1133
- while (stack.length > 0) {
1134
- const cur = stack.pop();
1135
- if (cur.type === 'enum_specifier') {
1136
- const name = cur.childForFieldName('name');
1137
- if (name?.text === typeName)
1138
- return true;
1139
- }
1140
- for (let i = 0; i < cur.childCount; i++) {
1141
- const child = cur.child(i);
1142
- if (child !== null)
1143
- stack.push(child);
1205
+ // One full-tree DFS per FILE (lazy), not per identifier argument — the
1206
+ // per-identifier walk here was the dominant cost of #2432 (87s of a 151s
1207
+ // extraction on a file that parses in 46ms).
1208
+ if (fileLookupIndex.enumNames === null) {
1209
+ let root = node;
1210
+ while (root.parent !== null)
1211
+ root = root.parent;
1212
+ const names = new Set();
1213
+ const stack = [root];
1214
+ while (stack.length > 0) {
1215
+ const cur = stack.pop();
1216
+ if (cur.type === 'enum_specifier') {
1217
+ const name = cur.childForFieldName('name');
1218
+ if (name !== null)
1219
+ names.add(name.text);
1220
+ }
1221
+ for (let i = 0; i < cur.childCount; i++) {
1222
+ const child = cur.child(i);
1223
+ if (child !== null)
1224
+ stack.push(child);
1225
+ }
1144
1226
  }
1227
+ fileLookupIndex.enumNames = names;
1145
1228
  }
1146
- return false;
1229
+ return fileLookupIndex.enumNames.has(typeName);
1147
1230
  }
1148
1231
  /**
1149
1232
  * Detect whether a `namespace_definition` AST node is inline.
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import path from 'node:path';
9
9
  import { generateId } from '../../lib/utils.js';
10
+ import { toZeroBasedLine } from './utils/line-base.js';
10
11
  const HEADING_RE = /^(#{1,6})\s+(.+)$/;
11
12
  const LINK_RE = /\[([^\]]*)\]\(([^)]+)\)/g;
12
13
  const MD_EXTENSIONS = new Set(['.md', '.mdx']);
@@ -59,8 +60,8 @@ export const processMarkdown = (graph, files, allPathSet) => {
59
60
  properties: {
60
61
  name: heading,
61
62
  filePath: file.path,
62
- startLine: lineNum,
63
- endLine,
63
+ startLine: toZeroBasedLine(lineNum),
64
+ endLine: toZeroBasedLine(endLine),
64
65
  level,
65
66
  description: `h${level}`,
66
67
  },
@@ -3,7 +3,7 @@ import type { SymbolTableWriter } from './model/index.js';
3
3
  import { type ExportedTypeMap } from './call-processor.js';
4
4
  import type { ParsedFile } from '../../_shared/index.js';
5
5
  import { WorkerPool } from './workers/worker-pool.js';
6
- import type { ParseWorkerResult, ExtractedRoute, ExtractedFetchCall, ExtractedDecoratorRoute, ExtractedToolDef, FileScopeBindings, ExtractedORMQuery, FetchWrapperDef } from './workers/parse-worker.js';
6
+ import type { ParseWorkerResult, ExtractedRoute, ExtractedFetchCall, ExtractedDecoratorRoute, ExtractedModuleConstants, ExtractedToolDef, FileScopeBindings, ExtractedORMQuery, FetchWrapperDef } from './workers/parse-worker.js';
7
7
  import type { ExtractedRouterConstructorPrefix, ExtractedRouterImport, ExtractedRouterInclude, ExtractedRouterModuleAlias } from './route-extractors/fastapi-router-bindings.js';
8
8
  import type { SharedSpringType } from './route-extractors/spring-shared.js';
9
9
  export type FileProgressCallback = (current: number, total: number, filePath: string) => void;
@@ -16,6 +16,8 @@ export interface WorkerExtractedData {
16
16
  routerImports: ExtractedRouterImport[];
17
17
  routerConstructorPrefixes: ExtractedRouterConstructorPrefix[];
18
18
  routerModuleAliases: ExtractedRouterModuleAlias[];
19
+ /** Per-file Python module constants for cross-file route-path resolution (#2391). */
20
+ moduleConstants: ExtractedModuleConstants[];
19
21
  toolDefs: ExtractedToolDef[];
20
22
  ormQueries: ExtractedORMQuery[];
21
23
  /** Project-wide Spring class/interface views for the #2288 inheritance pass. */
@@ -24,6 +24,7 @@ export const mergeChunkResults = (graph, symbolTable, chunkResults, exportedType
24
24
  const allRouterImports = [];
25
25
  const allRouterConstructorPrefixes = [];
26
26
  const allRouterModuleAliases = [];
27
+ const allModuleConstants = [];
27
28
  const allSpringTypes = [];
28
29
  const allToolDefs = [];
29
30
  const allORMQueries = [];
@@ -76,6 +77,8 @@ export const mergeChunkResults = (graph, symbolTable, chunkResults, exportedType
76
77
  }
77
78
  for (const item of result.routerModuleAliases ?? [])
78
79
  allRouterModuleAliases.push(item);
80
+ for (const item of result.moduleConstants ?? [])
81
+ allModuleConstants.push(item);
79
82
  for (const item of result.springTypes ?? [])
80
83
  allSpringTypes.push(item);
81
84
  for (const item of result.toolDefs)
@@ -99,6 +102,7 @@ export const mergeChunkResults = (graph, symbolTable, chunkResults, exportedType
99
102
  routerImports: allRouterImports,
100
103
  routerConstructorPrefixes: allRouterConstructorPrefixes,
101
104
  routerModuleAliases: allRouterModuleAliases,
105
+ moduleConstants: allModuleConstants,
102
106
  toolDefs: allToolDefs,
103
107
  ormQueries: allORMQueries,
104
108
  springTypes: allSpringTypes,
@@ -28,6 +28,7 @@ import { parseSourceSafe } from '../../tree-sitter/safe-parse.js';
28
28
  import { getProvider, providers } from '../languages/index.js';
29
29
  import { createWorkerPool, workerPoolDisabledByEnv, resolveAutoPoolSize, WorkerPoolInitializationError, WorkerPoolDisabledError, } from '../workers/worker-pool.js';
30
30
  import { normalizeExtractedRoutePath } from '../route-extractors/route-path.js';
31
+ import { resolveOperands, } from '../route-extractors/python-const-resolver.js';
31
32
  import { resolveInheritedSpringRoutes, } from '../route-extractors/spring-shared.js';
32
33
  import fs from 'node:fs';
33
34
  import path from 'node:path';
@@ -464,6 +465,9 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
464
465
  const allRouterImports = [];
465
466
  const allRouterConstructorPrefixes = [];
466
467
  const allRouterModuleAliases = [];
468
+ // Per-file Python module constants (#2391); resolved into decorator route paths
469
+ // below, after cross-file aggregation, alongside the include_router prefix pass.
470
+ const allModuleConstants = [];
467
471
  const allSpringTypes = [];
468
472
  const allToolDefs = [];
469
473
  const allORMQueries = [];
@@ -610,6 +614,10 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
610
614
  for (const item of chunkWorkerData.routerModuleAliases)
611
615
  allRouterModuleAliases.push(item);
612
616
  }
617
+ if (chunkWorkerData.moduleConstants?.length) {
618
+ for (const item of chunkWorkerData.moduleConstants)
619
+ allModuleConstants.push(item);
620
+ }
613
621
  if (chunkWorkerData.springTypes?.length) {
614
622
  for (const item of chunkWorkerData.springTypes)
615
623
  allSpringTypes.push(item);
@@ -925,6 +933,40 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
925
933
  }
926
934
  // FastAPI router-prefix resolution (cross-file).
927
935
  //
936
+ // #2391: resolve non-literal FastAPI decorator route paths (imported/composed
937
+ // string constants) BEFORE the include_router/APIRouter prefix pass below, so a
938
+ // resolved path is then prefix-joined like any literal path. Each such route
939
+ // carries `routePathExpr`/`routePathOperands` and an empty `routePath`; we fold
940
+ // the operands against the repo-wide, file-path-keyed constant map. On failure
941
+ // we DROP the route (KTD5 skip floor) rather than emit a phantom `POST /`.
942
+ if (allDecoratorRoutes.some((dr) => dr.routePathExpr !== undefined)) {
943
+ const repoConstants = new Map();
944
+ for (const { filePath, constants } of allModuleConstants) {
945
+ repoConstants.set(filePath, constants);
946
+ }
947
+ const resolvedRoutes = [];
948
+ let skipped = 0;
949
+ for (const dr of allDecoratorRoutes) {
950
+ if (dr.routePathExpr === undefined) {
951
+ resolvedRoutes.push(dr);
952
+ continue;
953
+ }
954
+ const value = dr.routePathOperands
955
+ ? resolveOperands(dr.filePath, dr.routePathOperands, repoConstants)
956
+ : null;
957
+ if (value === null) {
958
+ skipped++;
959
+ continue;
960
+ }
961
+ resolvedRoutes.push({ ...dr, routePath: value });
962
+ }
963
+ allDecoratorRoutes.length = 0;
964
+ for (const dr of resolvedRoutes)
965
+ allDecoratorRoutes.push(dr);
966
+ if (isDev && skipped > 0) {
967
+ logger.info(` 🧩 Resolved composed route constants; ${skipped} unresolved route(s) skipped`);
968
+ }
969
+ }
928
970
  // Workers emit two kinds of records per Python file:
929
971
  // • `routerIncludes` — every `app.include_router(<routerExpr>, prefix='/x')`
930
972
  // site, where `routerExpr` is either `<module>.router` (Shape A) or a
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Language-agnostic string-constant folding for route-path resolution (#2391).
3
+ *
4
+ * Route decorators/annotations frequently build their path from a constant rather
5
+ * than a string literal — `@router.post(API_V1_WIDGETS_GET)` (Python),
6
+ * `@GetMapping(PathConstants.WIDGETS)` (Spring), and the Kotlin/C# equivalents are
7
+ * the same shape. This module folds such a constant — or an inline
8
+ * `+`-concatenation — to its literal value, following `+` operands and import
9
+ * chains across a repo-wide, file-keyed constant map.
10
+ *
11
+ * The FOLD is language-neutral: it walks {@link Operand} lists and
12
+ * {@link ModuleConstants} that ANY language's extractor can produce, and defers
13
+ * the one language-specific decision — mapping an import specifier to the file it
14
+ * refers to — to a caller-supplied {@link ImportResolver}. A language binding
15
+ * (e.g. `python-const-resolver.ts`) provides that resolver plus a tree →
16
+ * {@link ModuleConstants} extractor and, if wanted, thin pre-bound wrappers.
17
+ *
18
+ * This mirrors how `route-path.ts` (URL normalization) and `spring-shared.ts`
19
+ * (annotation primitives) are shared across the ingestion and group layers and
20
+ * across languages: the reusable core lives in one place; per-language semantics
21
+ * plug in. It deliberately does NOT reuse `ScopeResolver` (which resolves symbol
22
+ * IDENTITIES, not literal string VALUES) or the `--pdg` `REACHING_DEF` layer
23
+ * (intra-procedural, function-local, def→use reachability — not module-level
24
+ * cross-file value folding).
25
+ */
26
+ /**
27
+ * One term of a constant's right-hand side. A `+`-concatenation
28
+ * (`A + "/b" + C`) becomes an ordered `Operand[]`; a bare literal is a
29
+ * single-element list.
30
+ */
31
+ export type Operand = {
32
+ readonly kind: 'literal';
33
+ readonly value: string;
34
+ } | {
35
+ readonly kind: 'ref';
36
+ readonly name: string;
37
+ };
38
+ /**
39
+ * A `from <module> import <name> [as <local>]` (or the language's equivalent)
40
+ * binding. `module` is the import specifier as written (e.g. `.constants`,
41
+ * `..pkg.constants`, `api.constants`) so the {@link ImportResolver} can apply
42
+ * language-specific rules; `originalName` is the exported name in the target
43
+ * module (pre-alias). The map key is the local (in-file) name.
44
+ */
45
+ export interface ImportBinding {
46
+ readonly module: string;
47
+ readonly originalName: string;
48
+ }
49
+ /**
50
+ * String-valued module-level constants of one source file. `literals` are
51
+ * fully-resolved (`X = "/a"`); `exprs` are unresolved operand lists
52
+ * (`X = A + "/b"`); `imports` maps a local name to the module it was imported
53
+ * from. All string keys are the in-file (local) names.
54
+ */
55
+ export interface ModuleConstants {
56
+ readonly literals: Map<string, string>;
57
+ readonly exprs: Map<string, readonly Operand[]>;
58
+ readonly imports: Map<string, ImportBinding>;
59
+ }
60
+ /** Repo-wide map: unique file key (e.g. `app/constants.py`) → that file's
61
+ * {@link ModuleConstants}. */
62
+ export type RepoConstants = ReadonlyMap<string, ModuleConstants>;
63
+ /**
64
+ * Resolve an import specifier (as written) from `importingFileKey` to the unique
65
+ * repo file key it refers to, or `null` when it cannot be pinned to exactly one
66
+ * file. This is the sole language-specific dependency of the fold: Python uses
67
+ * leading-dot relative imports + `.py`-suffix rules; a JVM binding would use
68
+ * package/classpath rules. Returning `null` on ambiguity keeps the fold honest —
69
+ * an unresolvable or ambiguous import floors to skip, never a wrong path.
70
+ */
71
+ export type ImportResolver = (importingFileKey: string, moduleSpec: string, repoKeys: ReadonlySet<string>) => string | null;
72
+ /**
73
+ * Resolve a single named constant referenced in `fileKey` to its literal string
74
+ * value, folding `+` concatenation and following import chains via
75
+ * `resolveImport`, or `null` when it cannot be fully folded.
76
+ */
77
+ export declare function resolveConstant(fileKey: string, name: string, repo: RepoConstants, resolveImport: ImportResolver): string | null;
78
+ /**
79
+ * Resolve an inline operand list (an unnamed `+`-expression captured directly at
80
+ * a decorator/annotation argument, e.g. `@router.get(API_V1 + "/widgets")`)
81
+ * against `fileKey`.
82
+ */
83
+ export declare function resolveOperands(fileKey: string, operands: readonly Operand[], repo: RepoConstants, resolveImport: ImportResolver): string | null;
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Language-agnostic string-constant folding for route-path resolution (#2391).
3
+ *
4
+ * Route decorators/annotations frequently build their path from a constant rather
5
+ * than a string literal — `@router.post(API_V1_WIDGETS_GET)` (Python),
6
+ * `@GetMapping(PathConstants.WIDGETS)` (Spring), and the Kotlin/C# equivalents are
7
+ * the same shape. This module folds such a constant — or an inline
8
+ * `+`-concatenation — to its literal value, following `+` operands and import
9
+ * chains across a repo-wide, file-keyed constant map.
10
+ *
11
+ * The FOLD is language-neutral: it walks {@link Operand} lists and
12
+ * {@link ModuleConstants} that ANY language's extractor can produce, and defers
13
+ * the one language-specific decision — mapping an import specifier to the file it
14
+ * refers to — to a caller-supplied {@link ImportResolver}. A language binding
15
+ * (e.g. `python-const-resolver.ts`) provides that resolver plus a tree →
16
+ * {@link ModuleConstants} extractor and, if wanted, thin pre-bound wrappers.
17
+ *
18
+ * This mirrors how `route-path.ts` (URL normalization) and `spring-shared.ts`
19
+ * (annotation primitives) are shared across the ingestion and group layers and
20
+ * across languages: the reusable core lives in one place; per-language semantics
21
+ * plug in. It deliberately does NOT reuse `ScopeResolver` (which resolves symbol
22
+ * IDENTITIES, not literal string VALUES) or the `--pdg` `REACHING_DEF` layer
23
+ * (intra-procedural, function-local, def→use reachability — not module-level
24
+ * cross-file value folding).
25
+ */
26
+ /** Depth ceiling for the import/constant chase. A heuristic bound, not a proven
27
+ * one; overrun floors to `null` (skip), never a wrong value. */
28
+ const MAX_RESOLVE_DEPTH = 8;
29
+ /** Max length of a folded path. Real route paths are well under this; a fold that
30
+ * exceeds it is a pathological self-multiplying concat (`X = A + A; A = B + B; …`)
31
+ * whose true value is genuinely huge — building it risks a `RangeError`/heap OOM,
32
+ * so we floor to `null` (skip) instead (#2393). The depth cap bounds recursion but
33
+ * NOT output size, which grows multiplicatively; this bounds the output. */
34
+ const MAX_FOLD_LENGTH = 8192;
35
+ /**
36
+ * Fold an operand list to its concatenated literal, or `null` if any operand is
37
+ * unresolvable (an unknown name, a non-string term, a cycle, or a depth overrun).
38
+ */
39
+ function foldExpr(fileKey, operands, state, depth) {
40
+ if (depth > MAX_RESOLVE_DEPTH)
41
+ return null;
42
+ let out = '';
43
+ for (const op of operands) {
44
+ if (op.kind === 'literal') {
45
+ out += op.value;
46
+ }
47
+ else {
48
+ const resolved = foldName(fileKey, op.name, state, depth + 1);
49
+ if (resolved === null)
50
+ return null;
51
+ out += resolved;
52
+ }
53
+ if (out.length > MAX_FOLD_LENGTH)
54
+ return null; // pathological self-multiplying concat → drop
55
+ }
56
+ return out;
57
+ }
58
+ function foldName(fileKey, name, state, depth) {
59
+ if (depth > MAX_RESOLVE_DEPTH)
60
+ return null;
61
+ const guard = `${fileKey}::${name}`;
62
+ // Memoize successful folds. `visited` (below) is the ACTIVE resolution stack
63
+ // for cycle detection — popped on unwind so `A + A` / diamonds fold instead of
64
+ // false-cycling (#2393) — but popping it alone reintroduces recomputation: a
65
+ // wide shared-descendant DAG re-folds each child once per reference, O(fanout^depth),
66
+ // which can exhaust the heap. The never-popped `memo` caps that at O(nodes): a
67
+ // name resolved on one branch is returned directly on the next. Only SUCCESSES
68
+ // are cached — a `null` may be transient (a name that is a cycle on the current
69
+ // branch can resolve on another), so caching it would be unsound.
70
+ const memoized = state.memo.get(guard);
71
+ if (memoized !== undefined)
72
+ return memoized;
73
+ if (state.visited.has(guard))
74
+ return null; // cycle: `name` is on the active stack
75
+ state.visited.add(guard);
76
+ try {
77
+ const result = computeFold(fileKey, name, state, depth);
78
+ if (result !== null)
79
+ state.memo.set(guard, result);
80
+ return result;
81
+ }
82
+ finally {
83
+ state.visited.delete(guard);
84
+ }
85
+ }
86
+ /** The literal/expr/import resolution for one name. Cycle guard + memo live in
87
+ * {@link foldName}; this is the pure lookup body. */
88
+ function computeFold(fileKey, name, state, depth) {
89
+ const mc = state.repo.get(fileKey);
90
+ if (!mc)
91
+ return null;
92
+ const literal = mc.literals.get(name);
93
+ if (literal !== undefined)
94
+ return literal;
95
+ const expr = mc.exprs.get(name);
96
+ if (expr !== undefined)
97
+ return foldExpr(fileKey, expr, state, depth + 1);
98
+ const imp = mc.imports.get(name);
99
+ if (imp !== undefined) {
100
+ const targetKey = state.resolveImport(fileKey, imp.module, state.repoKeys);
101
+ if (targetKey === null)
102
+ return null;
103
+ return foldName(targetKey, imp.originalName, state, depth + 1);
104
+ }
105
+ return null;
106
+ }
107
+ function newState(repo, resolveImport) {
108
+ return {
109
+ repo,
110
+ repoKeys: new Set(repo.keys()),
111
+ resolveImport,
112
+ visited: new Set(),
113
+ memo: new Map(),
114
+ };
115
+ }
116
+ /**
117
+ * Resolve a single named constant referenced in `fileKey` to its literal string
118
+ * value, folding `+` concatenation and following import chains via
119
+ * `resolveImport`, or `null` when it cannot be fully folded.
120
+ */
121
+ export function resolveConstant(fileKey, name, repo, resolveImport) {
122
+ return foldName(fileKey, name, newState(repo, resolveImport), 0);
123
+ }
124
+ /**
125
+ * Resolve an inline operand list (an unnamed `+`-expression captured directly at
126
+ * a decorator/annotation argument, e.g. `@router.get(API_V1 + "/widgets")`)
127
+ * against `fileKey`.
128
+ */
129
+ export function resolveOperands(fileKey, operands, repo, resolveImport) {
130
+ return foldExpr(fileKey, operands, newState(repo, resolveImport), 0);
131
+ }