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,75 @@
1
+ /**
2
+ * Python binding for the language-agnostic constant resolver (#2391).
3
+ *
4
+ * Supplies the two Python-specific pieces the shared fold in
5
+ * `constant-resolver.ts` needs — {@link resolvePythonImport} (import-specifier →
6
+ * file, honoring leading-dot relative imports and `.py` module files) and
7
+ * {@link extractPythonModuleConstants} (tree → {@link ModuleConstants}) — plus
8
+ * pre-bound {@link resolveConstant}/{@link resolveOperands} wrappers so Python
9
+ * callers stay language-oblivious. The reusable fold, the cycle guard, and the
10
+ * depth cap all live in the agnostic core; a JVM/other language binding reuses
11
+ * that core with its own `ImportResolver` + extractor.
12
+ *
13
+ * Keying (KTD4): the repo map is keyed by unique POSIX file path, NOT the
14
+ * dot-stripped module basename. `from .constants import X`,
15
+ * `from ..pkg.constants import X`, and `from constants import X` all collapse to
16
+ * the basename `constants` — a ubiquitous filename — so basename keying would
17
+ * resolve one package's routes to another's literal (a confidently WRONG path,
18
+ * worse than an unresolved one). A relative import is therefore resolved against
19
+ * the importing file's package directory (walk up one level per leading dot); an
20
+ * absolute import is matched by unique path suffix and returns `null` (skip
21
+ * floor) when ambiguous.
22
+ */
23
+ import { type SyntaxNode } from '../utils/ast-helpers.js';
24
+ import type Parser from 'tree-sitter';
25
+ import { type ImportResolver, type ModuleConstants, type Operand, type RepoConstants } from './constant-resolver.js';
26
+ export type { ImportBinding, ModuleConstants, Operand, RepoConstants, } from './constant-resolver.js';
27
+ /**
28
+ * The Python {@link ImportResolver}: map an import specifier to the unique file
29
+ * key it refers to, or `null` when it cannot be pinned to exactly one file (KTD4).
30
+ *
31
+ * Relative imports (`.constants`, `..pkg.mod`) resolve against the importing
32
+ * file's directory — one level up per leading dot beyond the first — and must
33
+ * hit an existing file key exactly. Absolute imports (`api.constants`) are
34
+ * matched by unique path suffix; a suffix shared by 2+ files is ambiguous and
35
+ * returns `null` rather than an arbitrary winner.
36
+ */
37
+ export declare const resolvePythonImport: ImportResolver;
38
+ /**
39
+ * Resolve a single named Python constant referenced in `fileKey` to its literal
40
+ * value, or `null`. Python-bound wrapper over the agnostic fold.
41
+ */
42
+ export declare function resolveConstant(fileKey: string, name: string, repo: RepoConstants): string | null;
43
+ /**
44
+ * Resolve an inline Python operand list (an unnamed `+`-expression at a decorator
45
+ * argument, e.g. `@router.get(API_V1 + "/widgets")`) against `fileKey`.
46
+ * Python-bound wrapper over the agnostic fold.
47
+ */
48
+ export declare function resolveOperands(fileKey: string, operands: readonly Operand[], repo: RepoConstants): string | null;
49
+ /**
50
+ * Parse a Python right-hand side into an operand list, or `null` when it is not a
51
+ * foldable string expression. Handles a bare string literal, a bare identifier
52
+ * (`X = Y`), and left-associative `+` chains of the two (`A + "/b" + C`).
53
+ * Everything else — numbers, calls, attribute access (`settings.X`), f-strings,
54
+ * conditional expressions (`x if c else y`), `concatenated_string` adjacency, and
55
+ * non-`+` operators — returns `null`, which makes the constant unresolvable
56
+ * (→ skip floor), never a wrong value.
57
+ */
58
+ export declare function parseConstOperands(node: SyntaxNode | null | undefined, depth?: number): Operand[] | null;
59
+ /**
60
+ * Extract the module-level string constants and `from … import …` bindings of
61
+ * one parsed Python file into the {@link ModuleConstants} shape the resolver
62
+ * consumes. Only top-level (`module`-direct) statements are walked — function-
63
+ * and class-local names never become route path constants and must not leak in.
64
+ *
65
+ * Assignment semantics are last-wins in source order (matches Python): a rebind
66
+ * to a non-string (`X = "/a"; X = build()`) drops `X` to unresolvable rather than
67
+ * keeping the stale literal; `X += "/b"` folds onto the prior representation.
68
+ *
69
+ * Assignment RHS references are SNAPSHOTTED at the assignment line (`snapshot`),
70
+ * not resolved lazily against a name's final binding — so `ROUTE = BASE; BASE +=
71
+ * "/v1"` leaves `ROUTE` at BASE's value AT the `ROUTE =` line, never the mutated
72
+ * one. Without this, an aliased-then-rebound constant resolved to a confidently
73
+ * wrong path (#2393).
74
+ */
75
+ export declare function extractPythonModuleConstants(tree: Parser.Tree): ModuleConstants;
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Python binding for the language-agnostic constant resolver (#2391).
3
+ *
4
+ * Supplies the two Python-specific pieces the shared fold in
5
+ * `constant-resolver.ts` needs — {@link resolvePythonImport} (import-specifier →
6
+ * file, honoring leading-dot relative imports and `.py` module files) and
7
+ * {@link extractPythonModuleConstants} (tree → {@link ModuleConstants}) — plus
8
+ * pre-bound {@link resolveConstant}/{@link resolveOperands} wrappers so Python
9
+ * callers stay language-oblivious. The reusable fold, the cycle guard, and the
10
+ * depth cap all live in the agnostic core; a JVM/other language binding reuses
11
+ * that core with its own `ImportResolver` + extractor.
12
+ *
13
+ * Keying (KTD4): the repo map is keyed by unique POSIX file path, NOT the
14
+ * dot-stripped module basename. `from .constants import X`,
15
+ * `from ..pkg.constants import X`, and `from constants import X` all collapse to
16
+ * the basename `constants` — a ubiquitous filename — so basename keying would
17
+ * resolve one package's routes to another's literal (a confidently WRONG path,
18
+ * worse than an unresolved one). A relative import is therefore resolved against
19
+ * the importing file's package directory (walk up one level per leading dot); an
20
+ * absolute import is matched by unique path suffix and returns `null` (skip
21
+ * floor) when ambiguous.
22
+ */
23
+ import { extractStringContent } from '../utils/ast-helpers.js';
24
+ import { resolveConstant as foldConstant, resolveOperands as foldOperands, } from './constant-resolver.js';
25
+ function dirOf(fileKey) {
26
+ const slash = fileKey.lastIndexOf('/');
27
+ return slash >= 0 ? fileKey.slice(0, slash) : '';
28
+ }
29
+ /** Collapse `a/b/../c` and `./` segments in a POSIX-ish path. */
30
+ function normalizePosix(path) {
31
+ const out = [];
32
+ for (const seg of path.split('/')) {
33
+ if (seg === '' || seg === '.')
34
+ continue;
35
+ if (seg === '..') {
36
+ if (out.length > 0 && out[out.length - 1] !== '..')
37
+ out.pop();
38
+ else
39
+ out.push('..');
40
+ }
41
+ else {
42
+ out.push(seg);
43
+ }
44
+ }
45
+ return out.join('/');
46
+ }
47
+ /**
48
+ * The Python {@link ImportResolver}: map an import specifier to the unique file
49
+ * key it refers to, or `null` when it cannot be pinned to exactly one file (KTD4).
50
+ *
51
+ * Relative imports (`.constants`, `..pkg.mod`) resolve against the importing
52
+ * file's directory — one level up per leading dot beyond the first — and must
53
+ * hit an existing file key exactly. Absolute imports (`api.constants`) are
54
+ * matched by unique path suffix; a suffix shared by 2+ files is ambiguous and
55
+ * returns `null` rather than an arbitrary winner.
56
+ */
57
+ export const resolvePythonImport = (importingFileKey, moduleSpec, repoKeys) => {
58
+ const dots = moduleSpec.length - moduleSpec.replace(/^\.+/, '').length;
59
+ const bare = moduleSpec.slice(dots);
60
+ const modPath = bare.replace(/\./g, '/');
61
+ if (dots > 0) {
62
+ // 1 dot = current package (the importing file's dir); each extra dot walks
63
+ // up one more level. If the walk would climb ABOVE the repo root (more extra
64
+ // dots than the importing file has directory levels), the import escapes the
65
+ // tree → null, rather than clamping to an unrelated root-level `<name>.py`.
66
+ const dir = dirOf(importingFileKey);
67
+ const depth = dir === '' ? 0 : dir.split('/').length;
68
+ const walk = dots - 1;
69
+ if (walk > depth)
70
+ return null;
71
+ let base = dir;
72
+ for (let i = 0; i < walk; i++)
73
+ base = dirOf(base);
74
+ // `from . import X` / `from .. import X` (no module after the dots): the
75
+ // module IS the package, whose file is `<base>/__init__.py`, not a sibling
76
+ // `<base>.py`.
77
+ const candidate = modPath === ''
78
+ ? base === ''
79
+ ? '__init__.py'
80
+ : `${base}/__init__.py`
81
+ : normalizePosix(`${base}/${modPath}`) + '.py';
82
+ return repoKeys.has(candidate) ? candidate : null;
83
+ }
84
+ // Absolute: match by unique path suffix. `api.constants` -> `api/constants.py`.
85
+ const suffix = `${modPath}.py`;
86
+ let hit = null;
87
+ for (const key of repoKeys) {
88
+ if (key === suffix || key.endsWith(`/${suffix}`)) {
89
+ if (hit !== null)
90
+ return null; // ambiguous — refuse to guess
91
+ hit = key;
92
+ }
93
+ }
94
+ return hit;
95
+ };
96
+ /**
97
+ * Resolve a single named Python constant referenced in `fileKey` to its literal
98
+ * value, or `null`. Python-bound wrapper over the agnostic fold.
99
+ */
100
+ export function resolveConstant(fileKey, name, repo) {
101
+ return foldConstant(fileKey, name, repo, resolvePythonImport);
102
+ }
103
+ /**
104
+ * Resolve an inline Python operand list (an unnamed `+`-expression at a decorator
105
+ * argument, e.g. `@router.get(API_V1 + "/widgets")`) against `fileKey`.
106
+ * Python-bound wrapper over the agnostic fold.
107
+ */
108
+ export function resolveOperands(fileKey, operands, repo) {
109
+ return foldOperands(fileKey, operands, repo, resolvePythonImport);
110
+ }
111
+ /**
112
+ * Parse a Python right-hand side into an operand list, or `null` when it is not a
113
+ * foldable string expression. Handles a bare string literal, a bare identifier
114
+ * (`X = Y`), and left-associative `+` chains of the two (`A + "/b" + C`).
115
+ * Everything else — numbers, calls, attribute access (`settings.X`), f-strings,
116
+ * conditional expressions (`x if c else y`), `concatenated_string` adjacency, and
117
+ * non-`+` operators — returns `null`, which makes the constant unresolvable
118
+ * (→ skip floor), never a wrong value.
119
+ */
120
+ export function parseConstOperands(node, depth = 0) {
121
+ if (!node)
122
+ return null;
123
+ // Defense-in-depth: bound the recursion so an adversarial deep `+`-chain floors
124
+ // to null (skip) rather than risking a stack overflow. 64 is far beyond any real
125
+ // route-path constant chain; tree-sitter caps expression nesting well below the
126
+ // JS stack limit today, so this is a belt-and-suspenders guard, not a reachable
127
+ // crash. Mirrors the fold engine's MAX_RESOLVE_DEPTH.
128
+ if (depth > 64)
129
+ return null;
130
+ if (node.type === 'string') {
131
+ const value = extractStringContent(node);
132
+ return value === null ? null : [{ kind: 'literal', value }];
133
+ }
134
+ if (node.type === 'identifier') {
135
+ return [{ kind: 'ref', name: node.text }];
136
+ }
137
+ if (node.type === 'binary_operator') {
138
+ const isPlus = (node.children ?? []).some((c) => c.type === '+');
139
+ if (!isPlus)
140
+ return null;
141
+ const left = parseConstOperands(node.childForFieldName('left'), depth + 1);
142
+ const right = parseConstOperands(node.childForFieldName('right'), depth + 1);
143
+ if (left === null || right === null)
144
+ return null;
145
+ return [...left, ...right];
146
+ }
147
+ return null;
148
+ }
149
+ /**
150
+ * Extract the module-level string constants and `from … import …` bindings of
151
+ * one parsed Python file into the {@link ModuleConstants} shape the resolver
152
+ * consumes. Only top-level (`module`-direct) statements are walked — function-
153
+ * and class-local names never become route path constants and must not leak in.
154
+ *
155
+ * Assignment semantics are last-wins in source order (matches Python): a rebind
156
+ * to a non-string (`X = "/a"; X = build()`) drops `X` to unresolvable rather than
157
+ * keeping the stale literal; `X += "/b"` folds onto the prior representation.
158
+ *
159
+ * Assignment RHS references are SNAPSHOTTED at the assignment line (`snapshot`),
160
+ * not resolved lazily against a name's final binding — so `ROUTE = BASE; BASE +=
161
+ * "/v1"` leaves `ROUTE` at BASE's value AT the `ROUTE =` line, never the mutated
162
+ * one. Without this, an aliased-then-rebound constant resolved to a confidently
163
+ * wrong path (#2393).
164
+ */
165
+ export function extractPythonModuleConstants(tree) {
166
+ const literals = new Map();
167
+ const exprs = new Map();
168
+ const imports = new Map();
169
+ // Monotonic counter for synthetic import-alias keys (see the `+=`-on-import
170
+ // case in the augmented-assignment branch below). Per-file, so keys are unique
171
+ // within this file's ModuleConstants.
172
+ let importAliasSeq = 0;
173
+ // The three maps are ONE logical namespace keyed by local name: a write to any
174
+ // one clears the other two, so last-binding-in-source-order wins (matches
175
+ // Python) and a name never carries a stale binding from a different map (#2391,
176
+ // #2393). Without this, `from .c import X; X = <dynamic>` would keep the stale
177
+ // import and resolve a confidently WRONG path instead of dropping.
178
+ // Apply an assignment result, honoring last-wins: clear any prior binding for
179
+ // `name` (including a shadowed import), then set the new one (a `null` rep
180
+ // leaves it cleared = unresolvable).
181
+ const setName = (name, ops) => {
182
+ literals.delete(name);
183
+ exprs.delete(name);
184
+ imports.delete(name);
185
+ if (ops === null)
186
+ return;
187
+ if (ops.length === 1 && ops[0].kind === 'literal')
188
+ literals.set(name, ops[0].value);
189
+ else
190
+ exprs.set(name, ops);
191
+ };
192
+ // Bind an import for `localName`, clearing any prior local literal/expr of the
193
+ // same name (an import shadows an earlier assignment, and vice versa).
194
+ const bindImport = (localName, binding) => {
195
+ literals.delete(localName);
196
+ exprs.delete(localName);
197
+ imports.set(localName, binding);
198
+ };
199
+ // Freeze a name's CURRENT binding into a stable operand list that is immune to
200
+ // any LATER rebind of `name`: a literal value, a copy of the current expr (whose
201
+ // refs are themselves already frozen, see `snapshot`), or an import preserved
202
+ // under a synthetic `$imp$N` key (`$` can never appear in a Python identifier, so
203
+ // it cannot collide with a real name). Returns null when `name` is not yet bound
204
+ // (a forward reference — left lazy).
205
+ const freeze = (name) => {
206
+ const lit = literals.get(name);
207
+ if (lit !== undefined)
208
+ return [{ kind: 'literal', value: lit }];
209
+ const ex = exprs.get(name);
210
+ if (ex !== undefined)
211
+ return [...ex];
212
+ const imp = imports.get(name);
213
+ if (imp !== undefined) {
214
+ const aliasKey = `$imp$${importAliasSeq++}`;
215
+ imports.set(aliasKey, imp);
216
+ return [{ kind: 'ref', name: aliasKey }];
217
+ }
218
+ return null;
219
+ };
220
+ // Snapshot an assignment RHS: replace each ref to an ALREADY-BOUND name with that
221
+ // name's frozen value, so a later rebind of that name does not retroactively
222
+ // change this binding — Python assigns by value at this source line, so
223
+ // `ROUTE = BASE; BASE += "/v1"` must leave ROUTE at BASE's value AT the `ROUTE =`
224
+ // line, never the mutated one (#2393). Unbound refs (forward references) stay
225
+ // lazy. Because every assignment snapshots, stored exprs only ever contain
226
+ // literals, frozen `$imp$N` refs, or lazy forward refs — never a live mutable ref.
227
+ const snapshot = (ops) => {
228
+ if (ops === null)
229
+ return null;
230
+ const out = [];
231
+ for (const op of ops) {
232
+ if (op.kind === 'literal') {
233
+ out.push(op);
234
+ continue;
235
+ }
236
+ const frozen = freeze(op.name);
237
+ if (frozen === null)
238
+ out.push(op);
239
+ else
240
+ out.push(...frozen);
241
+ }
242
+ return out;
243
+ };
244
+ const handleImport = (node) => {
245
+ const moduleNode = node.childForFieldName('module_name');
246
+ const moduleSpec = moduleNode?.text;
247
+ if (!moduleSpec)
248
+ return;
249
+ for (let i = 0; i < node.namedChildCount; i++) {
250
+ const child = node.namedChild(i);
251
+ if (!child || child.id === moduleNode?.id)
252
+ continue;
253
+ if (child.type === 'dotted_name') {
254
+ bindImport(child.text, { module: moduleSpec, originalName: child.text });
255
+ }
256
+ else if (child.type === 'aliased_import') {
257
+ const nameNode = child.childForFieldName('name');
258
+ const aliasNode = child.childForFieldName('alias');
259
+ if (nameNode && aliasNode) {
260
+ bindImport(aliasNode.text, { module: moduleSpec, originalName: nameNode.text });
261
+ }
262
+ }
263
+ }
264
+ };
265
+ for (let i = 0; i < tree.rootNode.namedChildCount; i++) {
266
+ const stmt = tree.rootNode.namedChild(i);
267
+ if (!stmt)
268
+ continue;
269
+ if (stmt.type === 'import_from_statement') {
270
+ handleImport(stmt);
271
+ continue;
272
+ }
273
+ if (stmt.type !== 'expression_statement')
274
+ continue;
275
+ const inner = stmt.namedChild(0);
276
+ if (!inner)
277
+ continue;
278
+ if (inner.type === 'assignment') {
279
+ const left = inner.childForFieldName('left');
280
+ if (left?.type !== 'identifier')
281
+ continue; // only bare-name module constants
282
+ setName(left.text, snapshot(parseConstOperands(inner.childForFieldName('right'))));
283
+ }
284
+ else if (inner.type === 'augmented_assignment') {
285
+ const left = inner.childForFieldName('left');
286
+ if (left?.type !== 'identifier')
287
+ continue;
288
+ const name = left.text;
289
+ const isPlusEq = inner.childForFieldName('operator')?.text === '+=';
290
+ // `X += rhs` folds onto X's CURRENT frozen value (`freeze` handles a local
291
+ // literal/expr and an imported base via the `$imp$N` alias). Both sides are
292
+ // snapshotted so a later rebind cannot retroactively change this binding.
293
+ const prior = freeze(name);
294
+ const rhs = snapshot(parseConstOperands(inner.childForFieldName('right')));
295
+ setName(name, isPlusEq && prior && rhs ? [...prior, ...rhs] : null);
296
+ }
297
+ }
298
+ return { literals, exprs, imports };
299
+ }
@@ -1,6 +1,6 @@
1
1
  export declare const TYPESCRIPT_QUERIES: string;
2
2
  export declare const JAVASCRIPT_QUERIES: string;
3
- export declare const PYTHON_QUERIES = "\n(class_definition\n name: (identifier) @name) @definition.class\n\n(function_definition\n name: (identifier) @name) @definition.function\n\n(import_statement\n name: (dotted_name) @import.source) @import\n\n; import numpy as np \u2192 aliased_import captures the module name so the\n; import path is resolved and named-binding extraction stores \"np\" \u2192 \"numpy\".\n(import_statement\n name: (aliased_import\n name: (dotted_name) @import.source)) @import\n\n(import_from_statement\n module_name: (dotted_name) @import.source) @import\n\n(import_from_statement\n module_name: (relative_import) @import.source) @import\n\n(call\n function: (identifier) @call.name) @call\n\n(call\n function: (attribute\n attribute: (identifier) @call.name)) @call\n\n; Class attribute type annotations \u2014 PEP 526: address: Address or address: Address = Address()\n; Both bare annotations (address: Address) and annotated assignments (name: str = \"test\")\n; are parsed as (assignment left: ... type: ...) in tree-sitter-python.\n(expression_statement\n (assignment\n left: (identifier) @name\n type: (type)) @definition.property)\n\n; Plain variable assignments without type annotation: x = 5, MAX_SIZE = 100\n; Overlap with @definition.property (typed) is handled by parse-worker dedup.\n(expression_statement\n (assignment\n left: (identifier) @name)) @definition.variable\n\n; Write access: obj.field = value\n(assignment\n left: (attribute\n object: (_) @assignment.receiver\n attribute: (identifier) @assignment.property)\n right: (_)) @assignment\n\n; Write access: obj.field += value (compound assignment)\n(augmented_assignment\n left: (attribute\n object: (_) @assignment.receiver\n attribute: (identifier) @assignment.property)\n right: (_)) @assignment\n\n; Python HTTP clients: requests.get('/path'), httpx.post('/path'), session.get('/path')\n(call\n function: (attribute\n attribute: (identifier) @http_client.method)\n arguments: (argument_list\n (string (string_content) @http_client.url))) @http_client\n\n; Python decorators: @app.route, @router.get, etc.\n(decorator\n (call\n function: (attribute\n object: (identifier) @decorator.receiver\n attribute: (identifier) @decorator.name)\n arguments: (argument_list\n (string (string_content) @decorator.arg)?))) @decorator\n";
3
+ export declare const PYTHON_QUERIES = "\n(class_definition\n name: (identifier) @name) @definition.class\n\n(function_definition\n name: (identifier) @name) @definition.function\n\n(import_statement\n name: (dotted_name) @import.source) @import\n\n; import numpy as np \u2192 aliased_import captures the module name so the\n; import path is resolved and named-binding extraction stores \"np\" \u2192 \"numpy\".\n(import_statement\n name: (aliased_import\n name: (dotted_name) @import.source)) @import\n\n(import_from_statement\n module_name: (dotted_name) @import.source) @import\n\n(import_from_statement\n module_name: (relative_import) @import.source) @import\n\n(call\n function: (identifier) @call.name) @call\n\n(call\n function: (attribute\n attribute: (identifier) @call.name)) @call\n\n; Class attribute type annotations \u2014 PEP 526: address: Address or address: Address = Address()\n; Both bare annotations (address: Address) and annotated assignments (name: str = \"test\")\n; are parsed as (assignment left: ... type: ...) in tree-sitter-python.\n(expression_statement\n (assignment\n left: (identifier) @name\n type: (type)) @definition.property)\n\n; Plain variable assignments without type annotation: x = 5, MAX_SIZE = 100\n; Overlap with @definition.property (typed) is handled by parse-worker dedup.\n(expression_statement\n (assignment\n left: (identifier) @name)) @definition.variable\n\n; Write access: obj.field = value\n(assignment\n left: (attribute\n object: (_) @assignment.receiver\n attribute: (identifier) @assignment.property)\n right: (_)) @assignment\n\n; Write access: obj.field += value (compound assignment)\n(augmented_assignment\n left: (attribute\n object: (_) @assignment.receiver\n attribute: (identifier) @assignment.property)\n right: (_)) @assignment\n\n; Python HTTP clients: requests.get('/path'), httpx.post('/path'), session.get('/path')\n(call\n function: (attribute\n attribute: (identifier) @http_client.method)\n arguments: (argument_list\n (string (string_content) @http_client.url))) @http_client\n\n; Python decorators: @app.route, @router.get, etc.\n; The first positional argument is captured three ways (#2391): a string literal\n; path via @decorator.arg (quote-free, the fast path); a bare constant name or a\n; plus-concatenation via @decorator.arg_expr (resolved cross-file by the constant\n; resolver). The anchored optional alternation pins to the FIRST arg and stays\n; optional, so no-arg decorators (@app.tool(), etc.) and non-path first args still\n; match.\n(decorator\n (call\n function: (attribute\n object: (identifier) @decorator.receiver\n attribute: (identifier) @decorator.name)\n arguments: (argument_list\n .\n [\n (string (string_content)? @decorator.arg) @decorator.arg_str\n (identifier) @decorator.arg_expr\n (binary_operator) @decorator.arg_expr\n ]?))) @decorator\n";
4
4
  export declare const JAVA_QUERIES = "\n; Classes, Interfaces, Enums, Annotations\n(class_declaration name: (identifier) @name) @definition.class\n(interface_declaration name: (identifier) @name) @definition.interface\n(enum_declaration name: (identifier) @name) @definition.enum\n(annotation_type_declaration name: (identifier) @name) @definition.annotation\n\n; Methods & Constructors\n(method_declaration name: (identifier) @name) @definition.method\n(constructor_declaration name: (identifier) @name) @definition.constructor\n\n; Fields \u2014 typed field declarations inside class bodies\n(field_declaration\n declarator: (variable_declarator\n name: (identifier) @name)) @definition.property\n\n; Imports - capture any import declaration child as source\n(import_declaration (_) @import.source) @import\n\n; Calls\n(method_invocation name: (identifier) @call.name) @call\n(method_invocation object: (_) name: (identifier) @call.name) @call\n(method_reference) @call\n\n; Constructor calls: new Foo()\n(object_creation_expression type: (type_identifier) @call.name) @call\n\n; Local variable declarations inside method bodies\n(local_variable_declaration\n declarator: (variable_declarator\n name: (identifier) @name)) @definition.variable\n\n; Write access: obj.field = value\n(assignment_expression\n left: (field_access\n object: (_) @assignment.receiver\n field: (identifier) @assignment.property)\n right: (_)) @assignment\n";
5
5
  export declare const C_QUERIES = "\n; Functions (direct declarator)\n(function_definition declarator: (function_declarator declarator: (identifier) @name)) @definition.function\n(declaration declarator: (function_declarator declarator: (identifier) @name)) @definition.function\n\n; Functions returning pointers (pointer_declarator wraps function_declarator)\n(function_definition declarator: (pointer_declarator declarator: (function_declarator declarator: (identifier) @name))) @definition.function\n(declaration declarator: (pointer_declarator declarator: (function_declarator declarator: (identifier) @name))) @definition.function\n\n; Functions returning double pointers (nested pointer_declarator)\n(function_definition declarator: (pointer_declarator declarator: (pointer_declarator declarator: (function_declarator declarator: (identifier) @name)))) @definition.function\n\n; Structs, Unions, Enums, Typedefs\n(struct_specifier name: (type_identifier) @name) @definition.struct\n(type_definition\n type: (struct_specifier\n body: (field_declaration_list))\n declarator: (type_identifier) @name) @definition.struct\n(union_specifier name: (type_identifier) @name) @definition.union\n(enum_specifier name: (type_identifier) @name) @definition.enum\n(type_definition\n type: (enum_specifier\n body: (enumerator_list))\n declarator: (type_identifier) @name) @definition.enum\n(enumerator name: (identifier) @name) @definition.const\n(type_definition declarator: (type_identifier) @name) @definition.typedef\n\n; Macros\n(preproc_function_def name: (identifier) @name) @definition.macro\n(preproc_def name: (identifier) @name) @definition.macro\n\n; Includes\n(preproc_include path: (_) @import.source) @import\n\n; Calls\n(call_expression function: (identifier) @call.name) @call\n(call_expression function: (field_expression field: (field_identifier) @call.name)) @call\n\n; Variable declarations: int x = 5; or int x;\n(declaration\n declarator: (init_declarator\n declarator: (identifier) @name)) @definition.variable\n";
6
6
  export declare const GO_QUERIES = "\n; Functions & Methods\n(function_declaration name: (identifier) @name) @definition.function\n(method_declaration name: (field_identifier) @name) @definition.method\n(method_elem name: (field_identifier) @name) @definition.method\n\n; Types\n(type_declaration (type_spec name: (type_identifier) @name type: (struct_type))) @definition.struct\n(type_declaration (type_spec name: (type_identifier) @name type: (interface_type))) @definition.interface\n\n; Imports\n(import_declaration (import_spec path: (interpreted_string_literal) @import.source)) @import\n(import_declaration (import_spec_list (import_spec path: (interpreted_string_literal) @import.source))) @import\n\n; Struct fields \u2014 named field declarations inside struct types\n(field_declaration_list\n (field_declaration\n name: (field_identifier) @name) @definition.property)\n\n; Calls\n(call_expression function: (identifier) @call.name) @call\n(call_expression function: (selector_expression field: (field_identifier) @call.name)) @call\n\n; Const/var declarations\n(const_declaration (const_spec (identifier) @name)) @definition.const\n(var_declaration (var_spec (identifier) @name)) @definition.variable\n(var_declaration (var_spec_list (var_spec (identifier) @name))) @definition.variable\n\n; Short variable declaration: x := 5\n(short_var_declaration left: (expression_list (identifier) @name)) @definition.variable\n\n; Struct literal construction: User{Name: \"Alice\"}\n(composite_literal type: (type_identifier) @call.name) @call\n\n; Write access: obj.field = value\n(assignment_statement\n left: (expression_list\n (selector_expression\n operand: (_) @assignment.receiver\n field: (field_identifier) @assignment.property))\n right: (_)) @assignment\n\n; Write access: obj.field++ / obj.field--\n(inc_statement\n (selector_expression\n operand: (_) @assignment.receiver\n field: (field_identifier) @assignment.property)) @assignment\n(dec_statement\n (selector_expression\n operand: (_) @assignment.receiver\n field: (field_identifier) @assignment.property)) @assignment\n";
@@ -717,13 +717,24 @@ export const PYTHON_QUERIES = `
717
717
  (string (string_content) @http_client.url))) @http_client
718
718
 
719
719
  ; Python decorators: @app.route, @router.get, etc.
720
+ ; The first positional argument is captured three ways (#2391): a string literal
721
+ ; path via @decorator.arg (quote-free, the fast path); a bare constant name or a
722
+ ; plus-concatenation via @decorator.arg_expr (resolved cross-file by the constant
723
+ ; resolver). The anchored optional alternation pins to the FIRST arg and stays
724
+ ; optional, so no-arg decorators (@app.tool(), etc.) and non-path first args still
725
+ ; match.
720
726
  (decorator
721
727
  (call
722
728
  function: (attribute
723
729
  object: (identifier) @decorator.receiver
724
730
  attribute: (identifier) @decorator.name)
725
731
  arguments: (argument_list
726
- (string (string_content) @decorator.arg)?))) @decorator
732
+ .
733
+ [
734
+ (string (string_content)? @decorator.arg) @decorator.arg_str
735
+ (identifier) @decorator.arg_expr
736
+ (binary_operator) @decorator.arg_expr
737
+ ]?))) @decorator
727
738
  `;
728
739
  // Java queries - works with tree-sitter-java
729
740
  export const JAVA_QUERIES = `
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Convert a 1-based source line number to the 0-based convention used by
3
+ * GraphNode `startLine`/`endLine`.
4
+ *
5
+ * The graph layer stores line numbers 0-based (tree-sitter `startPosition.row`),
6
+ * and this is load-bearing: the taint/PDG/CFG join and the MCP consumers all add
7
+ * `+ 1` to recover 1-based (see `summary-harvest-driver.ts` — "Function/Method
8
+ * node startLine is 0-based"). Most emitters get 0-based for free from
9
+ * tree-sitter. The exceptions are the regex-based COBOL/JCL processors (their
10
+ * parsers use `lineNum = i + 1`) and the scope-capture path (`Capture` ranges
11
+ * are 1-based per RFC §2.1). Those must convert to 0-based when they build a
12
+ * graph node, or the exact-content slice in `csv-generator.ts` drops the
13
+ * symbol's declaration line (#2379) and reported line numbers are off (#2377).
14
+ *
15
+ * Apply this ONLY at the graph-node `startLine:`/`endLine:` assignment. The
16
+ * parser-internal 1-based values (`.line`, `prog.startLine`) stay 1-based —
17
+ * they feed `L${line}` node/edge IDs and line-range containment checks that
18
+ * must not shift. The clamp guards degenerate inputs (line 0 / empty files).
19
+ */
20
+ export declare const toZeroBasedLine: (oneBasedLine: number) => number;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Convert a 1-based source line number to the 0-based convention used by
3
+ * GraphNode `startLine`/`endLine`.
4
+ *
5
+ * The graph layer stores line numbers 0-based (tree-sitter `startPosition.row`),
6
+ * and this is load-bearing: the taint/PDG/CFG join and the MCP consumers all add
7
+ * `+ 1` to recover 1-based (see `summary-harvest-driver.ts` — "Function/Method
8
+ * node startLine is 0-based"). Most emitters get 0-based for free from
9
+ * tree-sitter. The exceptions are the regex-based COBOL/JCL processors (their
10
+ * parsers use `lineNum = i + 1`) and the scope-capture path (`Capture` ranges
11
+ * are 1-based per RFC §2.1). Those must convert to 0-based when they build a
12
+ * graph node, or the exact-content slice in `csv-generator.ts` drops the
13
+ * symbol's declaration line (#2379) and reported line numbers are off (#2377).
14
+ *
15
+ * Apply this ONLY at the graph-node `startLine:`/`endLine:` assignment. The
16
+ * parser-internal 1-based values (`.line`, `prog.startLine`) stay 1-based —
17
+ * they feed `L${line}` node/edge IDs and line-range containment checks that
18
+ * must not shift. The clamp guards degenerate inputs (line 0 / empty files).
19
+ */
20
+ export const toZeroBasedLine = (oneBasedLine) => Math.max(0, oneBasedLine - 1);
@@ -0,0 +1,21 @@
1
+ import type { NodeLabel } from '../../../_shared/index.js';
2
+ /**
3
+ * Graph-node labels that represent a resolvable code symbol — a definition with
4
+ * its own source span (function, type, member, module-like container).
5
+ *
6
+ * These get EXACT source-span content in the FTS index: `csv-generator.ts`
7
+ * slices exactly `[startLine, endLine]` for them (no ±2 padding), while every
8
+ * other label keeps the context window. That exactness depends on the 0-based
9
+ * `startLine`/`endLine` invariant enforced by `line-base.ts` — the slice is only
10
+ * correct because all emitters store 0-based lines. Keep the two together.
11
+ *
12
+ * Single source of truth so the set can't silently drift the way the inline copy
13
+ * did in #2379.
14
+ *
15
+ * NOTE: `group/extractors/manifest-extractor.ts`'s `CUSTOM_CONTRACT_RESOLVE_QUERY`
16
+ * carries a near-identical hand-list that is intentionally a SUBSET — it excludes
17
+ * `Namespace`, `Variable`, `Module`. Unifying the two needs a contract-resolution
18
+ * behavior check (would widen which nodes resolve as contract symbols), so it is
19
+ * deliberately left separate for now.
20
+ */
21
+ export declare const SYMBOL_NODE_LABELS: ReadonlySet<NodeLabel>;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Graph-node labels that represent a resolvable code symbol — a definition with
3
+ * its own source span (function, type, member, module-like container).
4
+ *
5
+ * These get EXACT source-span content in the FTS index: `csv-generator.ts`
6
+ * slices exactly `[startLine, endLine]` for them (no ±2 padding), while every
7
+ * other label keeps the context window. That exactness depends on the 0-based
8
+ * `startLine`/`endLine` invariant enforced by `line-base.ts` — the slice is only
9
+ * correct because all emitters store 0-based lines. Keep the two together.
10
+ *
11
+ * Single source of truth so the set can't silently drift the way the inline copy
12
+ * did in #2379.
13
+ *
14
+ * NOTE: `group/extractors/manifest-extractor.ts`'s `CUSTOM_CONTRACT_RESOLVE_QUERY`
15
+ * carries a near-identical hand-list that is intentionally a SUBSET — it excludes
16
+ * `Namespace`, `Variable`, `Module`. Unifying the two needs a contract-resolution
17
+ * behavior check (would widen which nodes resolve as contract symbols), so it is
18
+ * deliberately left separate for now.
19
+ */
20
+ export const SYMBOL_NODE_LABELS = new Set([
21
+ 'Function',
22
+ 'Method',
23
+ 'Class',
24
+ 'Interface',
25
+ 'CodeElement',
26
+ 'Struct',
27
+ 'Enum',
28
+ 'Macro',
29
+ 'Typedef',
30
+ 'Union',
31
+ 'Namespace',
32
+ 'Trait',
33
+ 'Impl',
34
+ 'TypeAlias',
35
+ 'Const',
36
+ 'Static',
37
+ 'Variable',
38
+ 'Property',
39
+ 'Record',
40
+ 'Delegate',
41
+ 'Annotation',
42
+ 'Constructor',
43
+ 'Template',
44
+ 'Module',
45
+ ]);
@@ -113,6 +113,21 @@ export interface ExtractedDecoratorRoute {
113
113
  * participate in `include_router(prefix=...)` joining.
114
114
  */
115
115
  decoratorReceiver?: string;
116
+ /**
117
+ * Raw text of a non-literal decorator path argument (`#2391`), e.g.
118
+ * `API_V1_WIDGETS_GET` or `API_V1 + "/widgets"`. Present only when the
119
+ * decorator's first argument was NOT a string literal, in which case
120
+ * `routePath` is empty and parse-impl resolves the constant cross-file (or
121
+ * drops the route on failure). Absent for ordinary string-literal routes.
122
+ */
123
+ routePathExpr?: string;
124
+ /**
125
+ * Parsed operand list for {@link routePathExpr} — an identifier reference or a
126
+ * `+`-concatenation, in the {@link Operand} shape the constant resolver folds.
127
+ * `undefined` when the expression was not a foldable string form (e.g. an
128
+ * attribute access), in which case the route is dropped at resolution.
129
+ */
130
+ routePathOperands?: Operand[];
116
131
  /**
117
132
  * FastAPI `app.include_router(prefix='/x')` prefix that applies to
118
133
  * this route. Filled by parse-impl after cross-file aggregation; the
@@ -131,6 +146,17 @@ export interface ExtractedDecoratorRoute {
131
146
  */
132
147
  handlerName?: string;
133
148
  }
149
+ /**
150
+ * One Python file's module-level string constants (#2391), used by parse-impl to
151
+ * resolve non-literal decorator route paths cross-file. `constants` is the
152
+ * `Map`-based {@link ModuleConstants} shape — it survives the worker
153
+ * `postMessage` boundary (structured clone) and the parse cache
154
+ * (`mapReplacer`/`mapReviver`) without conversion.
155
+ */
156
+ export interface ExtractedModuleConstants {
157
+ filePath: string;
158
+ constants: ModuleConstants;
159
+ }
134
160
  export interface ExtractedToolDef {
135
161
  filePath: string;
136
162
  toolName: string;
@@ -211,6 +237,13 @@ export interface ParseWorkerResult {
211
237
  * predate the field; consumers must guard with `if (… ?? [])`).
212
238
  */
213
239
  routerModuleAliases?: ExtractedRouterModuleAlias[];
240
+ /**
241
+ * Per-file Python module-level string constants (#2391). parse-impl aggregates
242
+ * these into a repo-wide, file-path-keyed map and resolves each decorator
243
+ * route's non-literal path expression against it. Optional for cache backward
244
+ * compatibility (older entries predate the field; consumers guard with `?? []`).
245
+ */
246
+ moduleConstants?: ExtractedModuleConstants[];
214
247
  toolDefs: ExtractedToolDef[];
215
248
  ormQueries: ExtractedORMQuery[];
216
249
  constructorBindings: FileConstructorBindings[];
@@ -256,3 +289,4 @@ export interface ParseWorkerInput {
256
289
  * Appends results to the provided array (avoids allocation when no matches).
257
290
  */
258
291
  export declare function extractORMQueries(filePath: string, content: string, out: ExtractedORMQuery[]): void;
292
+ import { type ModuleConstants, type Operand } from '../route-extractors/python-const-resolver.js';