gitnexus 1.4.7 → 1.4.9

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 (242) hide show
  1. package/README.md +29 -1
  2. package/dist/cli/ai-context.d.ts +1 -1
  3. package/dist/cli/ai-context.js +1 -1
  4. package/dist/cli/analyze.d.ts +2 -0
  5. package/dist/cli/analyze.js +54 -21
  6. package/dist/cli/index-repo.d.ts +15 -0
  7. package/dist/cli/index-repo.js +115 -0
  8. package/dist/cli/index.js +13 -3
  9. package/dist/cli/setup.js +90 -10
  10. package/dist/cli/wiki.d.ts +4 -0
  11. package/dist/cli/wiki.js +174 -53
  12. package/dist/config/supported-languages.d.ts +33 -1
  13. package/dist/config/supported-languages.js +32 -0
  14. package/dist/core/embeddings/embedder.d.ts +6 -1
  15. package/dist/core/embeddings/embedder.js +65 -5
  16. package/dist/core/embeddings/embedding-pipeline.js +11 -9
  17. package/dist/core/embeddings/http-client.d.ts +31 -0
  18. package/dist/core/embeddings/http-client.js +179 -0
  19. package/dist/core/embeddings/index.d.ts +1 -0
  20. package/dist/core/embeddings/index.js +1 -0
  21. package/dist/core/embeddings/types.d.ts +1 -1
  22. package/dist/core/graph/graph.js +9 -1
  23. package/dist/core/graph/types.d.ts +11 -2
  24. package/dist/core/ingestion/call-processor.d.ts +66 -2
  25. package/dist/core/ingestion/call-processor.js +650 -30
  26. package/dist/core/ingestion/call-routing.d.ts +9 -18
  27. package/dist/core/ingestion/call-routing.js +0 -19
  28. package/dist/core/ingestion/cobol/cobol-copy-expander.d.ts +57 -0
  29. package/dist/core/ingestion/cobol/cobol-copy-expander.js +385 -0
  30. package/dist/core/ingestion/cobol/cobol-preprocessor.d.ts +210 -0
  31. package/dist/core/ingestion/cobol/cobol-preprocessor.js +1509 -0
  32. package/dist/core/ingestion/cobol/jcl-parser.d.ts +68 -0
  33. package/dist/core/ingestion/cobol/jcl-parser.js +217 -0
  34. package/dist/core/ingestion/cobol/jcl-processor.d.ts +33 -0
  35. package/dist/core/ingestion/cobol/jcl-processor.js +229 -0
  36. package/dist/core/ingestion/cobol-processor.d.ts +54 -0
  37. package/dist/core/ingestion/cobol-processor.js +1186 -0
  38. package/dist/core/ingestion/entry-point-scoring.d.ts +17 -0
  39. package/dist/core/ingestion/entry-point-scoring.js +52 -28
  40. package/dist/core/ingestion/export-detection.d.ts +47 -8
  41. package/dist/core/ingestion/export-detection.js +29 -50
  42. package/dist/core/ingestion/field-extractor.d.ts +29 -0
  43. package/dist/core/ingestion/field-extractor.js +25 -0
  44. package/dist/core/ingestion/field-extractors/configs/c-cpp.d.ts +3 -0
  45. package/dist/core/ingestion/field-extractors/configs/c-cpp.js +108 -0
  46. package/dist/core/ingestion/field-extractors/configs/csharp.d.ts +8 -0
  47. package/dist/core/ingestion/field-extractors/configs/csharp.js +73 -0
  48. package/dist/core/ingestion/field-extractors/configs/dart.d.ts +8 -0
  49. package/dist/core/ingestion/field-extractors/configs/dart.js +76 -0
  50. package/dist/core/ingestion/field-extractors/configs/go.d.ts +11 -0
  51. package/dist/core/ingestion/field-extractors/configs/go.js +64 -0
  52. package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +44 -0
  53. package/dist/core/ingestion/field-extractors/configs/helpers.js +134 -0
  54. package/dist/core/ingestion/field-extractors/configs/jvm.d.ts +3 -0
  55. package/dist/core/ingestion/field-extractors/configs/jvm.js +118 -0
  56. package/dist/core/ingestion/field-extractors/configs/php.d.ts +8 -0
  57. package/dist/core/ingestion/field-extractors/configs/php.js +67 -0
  58. package/dist/core/ingestion/field-extractors/configs/python.d.ts +12 -0
  59. package/dist/core/ingestion/field-extractors/configs/python.js +91 -0
  60. package/dist/core/ingestion/field-extractors/configs/ruby.d.ts +16 -0
  61. package/dist/core/ingestion/field-extractors/configs/ruby.js +75 -0
  62. package/dist/core/ingestion/field-extractors/configs/rust.d.ts +9 -0
  63. package/dist/core/ingestion/field-extractors/configs/rust.js +55 -0
  64. package/dist/core/ingestion/field-extractors/configs/swift.d.ts +8 -0
  65. package/dist/core/ingestion/field-extractors/configs/swift.js +63 -0
  66. package/dist/core/ingestion/field-extractors/configs/typescript-javascript.d.ts +3 -0
  67. package/dist/core/ingestion/field-extractors/configs/typescript-javascript.js +60 -0
  68. package/dist/core/ingestion/field-extractors/generic.d.ts +46 -0
  69. package/dist/core/ingestion/field-extractors/generic.js +111 -0
  70. package/dist/core/ingestion/field-extractors/typescript.d.ts +77 -0
  71. package/dist/core/ingestion/field-extractors/typescript.js +291 -0
  72. package/dist/core/ingestion/field-types.d.ts +59 -0
  73. package/dist/core/ingestion/field-types.js +2 -0
  74. package/dist/core/ingestion/framework-detection.d.ts +97 -2
  75. package/dist/core/ingestion/framework-detection.js +114 -14
  76. package/dist/core/ingestion/heritage-processor.js +62 -66
  77. package/dist/core/ingestion/import-processor.d.ts +9 -10
  78. package/dist/core/ingestion/import-processor.js +150 -196
  79. package/dist/core/ingestion/{resolvers → import-resolvers}/csharp.d.ts +6 -9
  80. package/dist/core/ingestion/{resolvers → import-resolvers}/csharp.js +20 -2
  81. package/dist/core/ingestion/import-resolvers/dart.d.ts +7 -0
  82. package/dist/core/ingestion/import-resolvers/dart.js +44 -0
  83. package/dist/core/ingestion/{resolvers → import-resolvers}/go.d.ts +4 -5
  84. package/dist/core/ingestion/{resolvers → import-resolvers}/go.js +17 -0
  85. package/dist/core/ingestion/{resolvers → import-resolvers}/jvm.d.ts +10 -1
  86. package/dist/core/ingestion/import-resolvers/jvm.js +159 -0
  87. package/dist/core/ingestion/import-resolvers/php.d.ts +25 -0
  88. package/dist/core/ingestion/import-resolvers/php.js +80 -0
  89. package/dist/core/ingestion/{resolvers → import-resolvers}/python.d.ts +9 -3
  90. package/dist/core/ingestion/{resolvers → import-resolvers}/python.js +35 -3
  91. package/dist/core/ingestion/{resolvers → import-resolvers}/ruby.d.ts +5 -2
  92. package/dist/core/ingestion/{resolvers → import-resolvers}/ruby.js +7 -2
  93. package/dist/core/ingestion/{resolvers → import-resolvers}/rust.d.ts +5 -2
  94. package/dist/core/ingestion/{resolvers → import-resolvers}/rust.js +41 -2
  95. package/dist/core/ingestion/{resolvers → import-resolvers}/standard.d.ts +15 -7
  96. package/dist/core/ingestion/{resolvers → import-resolvers}/standard.js +22 -3
  97. package/dist/core/ingestion/import-resolvers/swift.d.ts +7 -0
  98. package/dist/core/ingestion/import-resolvers/swift.js +23 -0
  99. package/dist/core/ingestion/import-resolvers/types.d.ts +44 -0
  100. package/dist/core/ingestion/import-resolvers/types.js +6 -0
  101. package/dist/core/ingestion/{resolvers → import-resolvers}/utils.d.ts +2 -0
  102. package/dist/core/ingestion/{resolvers → import-resolvers}/utils.js +7 -0
  103. package/dist/core/ingestion/language-config.d.ts +6 -0
  104. package/dist/core/ingestion/language-config.js +13 -0
  105. package/dist/core/ingestion/language-provider.d.ts +121 -0
  106. package/dist/core/ingestion/language-provider.js +24 -0
  107. package/dist/core/ingestion/languages/c-cpp.d.ts +12 -0
  108. package/dist/core/ingestion/languages/c-cpp.js +71 -0
  109. package/dist/core/ingestion/languages/cobol.d.ts +1 -0
  110. package/dist/core/ingestion/languages/cobol.js +26 -0
  111. package/dist/core/ingestion/languages/csharp.d.ts +8 -0
  112. package/dist/core/ingestion/languages/csharp.js +49 -0
  113. package/dist/core/ingestion/languages/dart.d.ts +12 -0
  114. package/dist/core/ingestion/languages/dart.js +58 -0
  115. package/dist/core/ingestion/languages/go.d.ts +11 -0
  116. package/dist/core/ingestion/languages/go.js +28 -0
  117. package/dist/core/ingestion/languages/index.d.ts +38 -0
  118. package/dist/core/ingestion/languages/index.js +63 -0
  119. package/dist/core/ingestion/languages/java.d.ts +9 -0
  120. package/dist/core/ingestion/languages/java.js +29 -0
  121. package/dist/core/ingestion/languages/kotlin.d.ts +9 -0
  122. package/dist/core/ingestion/languages/kotlin.js +53 -0
  123. package/dist/core/ingestion/languages/php.d.ts +8 -0
  124. package/dist/core/ingestion/languages/php.js +145 -0
  125. package/dist/core/ingestion/languages/python.d.ts +12 -0
  126. package/dist/core/ingestion/languages/python.js +39 -0
  127. package/dist/core/ingestion/languages/ruby.d.ts +9 -0
  128. package/dist/core/ingestion/languages/ruby.js +44 -0
  129. package/dist/core/ingestion/languages/rust.d.ts +12 -0
  130. package/dist/core/ingestion/languages/rust.js +44 -0
  131. package/dist/core/ingestion/languages/swift.d.ts +12 -0
  132. package/dist/core/ingestion/languages/swift.js +133 -0
  133. package/dist/core/ingestion/languages/typescript.d.ts +10 -0
  134. package/dist/core/ingestion/languages/typescript.js +60 -0
  135. package/dist/core/ingestion/markdown-processor.d.ts +17 -0
  136. package/dist/core/ingestion/markdown-processor.js +124 -0
  137. package/dist/core/ingestion/mro-processor.js +22 -18
  138. package/dist/core/ingestion/named-binding-processor.d.ts +18 -0
  139. package/dist/core/ingestion/named-binding-processor.js +42 -0
  140. package/dist/core/ingestion/named-bindings/csharp.d.ts +3 -0
  141. package/dist/core/ingestion/named-bindings/csharp.js +37 -0
  142. package/dist/core/ingestion/named-bindings/java.d.ts +3 -0
  143. package/dist/core/ingestion/named-bindings/java.js +29 -0
  144. package/dist/core/ingestion/named-bindings/kotlin.d.ts +3 -0
  145. package/dist/core/ingestion/named-bindings/kotlin.js +36 -0
  146. package/dist/core/ingestion/named-bindings/php.d.ts +3 -0
  147. package/dist/core/ingestion/named-bindings/php.js +61 -0
  148. package/dist/core/ingestion/named-bindings/python.d.ts +3 -0
  149. package/dist/core/ingestion/named-bindings/python.js +49 -0
  150. package/dist/core/ingestion/named-bindings/rust.d.ts +3 -0
  151. package/dist/core/ingestion/named-bindings/rust.js +64 -0
  152. package/dist/core/ingestion/named-bindings/types.d.ts +16 -0
  153. package/dist/core/ingestion/named-bindings/types.js +6 -0
  154. package/dist/core/ingestion/named-bindings/typescript.d.ts +3 -0
  155. package/dist/core/ingestion/named-bindings/typescript.js +58 -0
  156. package/dist/core/ingestion/parsing-processor.d.ts +6 -2
  157. package/dist/core/ingestion/parsing-processor.js +125 -85
  158. package/dist/core/ingestion/pipeline.d.ts +10 -0
  159. package/dist/core/ingestion/pipeline.js +1235 -317
  160. package/dist/core/ingestion/resolution-context.d.ts +5 -0
  161. package/dist/core/ingestion/resolution-context.js +8 -5
  162. package/dist/core/ingestion/route-extractors/expo.d.ts +1 -0
  163. package/dist/core/ingestion/route-extractors/expo.js +36 -0
  164. package/dist/core/ingestion/route-extractors/middleware.d.ts +47 -0
  165. package/dist/core/ingestion/route-extractors/middleware.js +143 -0
  166. package/dist/core/ingestion/route-extractors/nextjs.d.ts +3 -0
  167. package/dist/core/ingestion/route-extractors/nextjs.js +76 -0
  168. package/dist/core/ingestion/route-extractors/php.d.ts +7 -0
  169. package/dist/core/ingestion/route-extractors/php.js +21 -0
  170. package/dist/core/ingestion/route-extractors/response-shapes.d.ts +20 -0
  171. package/dist/core/ingestion/route-extractors/response-shapes.js +290 -0
  172. package/dist/core/ingestion/symbol-table.d.ts +16 -0
  173. package/dist/core/ingestion/symbol-table.js +20 -6
  174. package/dist/core/ingestion/tree-sitter-queries.d.ts +10 -9
  175. package/dist/core/ingestion/tree-sitter-queries.js +274 -11
  176. package/dist/core/ingestion/type-env.d.ts +42 -18
  177. package/dist/core/ingestion/type-env.js +481 -106
  178. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +5 -0
  179. package/dist/core/ingestion/type-extractors/c-cpp.js +119 -0
  180. package/dist/core/ingestion/type-extractors/csharp.js +149 -16
  181. package/dist/core/ingestion/type-extractors/dart.d.ts +15 -0
  182. package/dist/core/ingestion/type-extractors/dart.js +371 -0
  183. package/dist/core/ingestion/type-extractors/jvm.js +169 -66
  184. package/dist/core/ingestion/type-extractors/rust.js +35 -1
  185. package/dist/core/ingestion/type-extractors/shared.d.ts +1 -15
  186. package/dist/core/ingestion/type-extractors/shared.js +14 -112
  187. package/dist/core/ingestion/type-extractors/swift.js +338 -7
  188. package/dist/core/ingestion/type-extractors/types.d.ts +40 -8
  189. package/dist/core/ingestion/type-extractors/typescript.js +141 -9
  190. package/dist/core/ingestion/utils/ast-helpers.d.ts +83 -0
  191. package/dist/core/ingestion/utils/ast-helpers.js +817 -0
  192. package/dist/core/ingestion/utils/call-analysis.d.ts +73 -0
  193. package/dist/core/ingestion/utils/call-analysis.js +527 -0
  194. package/dist/core/ingestion/utils/event-loop.d.ts +5 -0
  195. package/dist/core/ingestion/utils/event-loop.js +5 -0
  196. package/dist/core/ingestion/utils/language-detection.d.ts +9 -0
  197. package/dist/core/ingestion/utils/language-detection.js +70 -0
  198. package/dist/core/ingestion/utils/verbose.d.ts +1 -0
  199. package/dist/core/ingestion/utils/verbose.js +7 -0
  200. package/dist/core/ingestion/workers/parse-worker.d.ts +55 -5
  201. package/dist/core/ingestion/workers/parse-worker.js +415 -225
  202. package/dist/core/lbug/csv-generator.js +51 -1
  203. package/dist/core/lbug/lbug-adapter.d.ts +10 -0
  204. package/dist/core/lbug/lbug-adapter.js +75 -4
  205. package/dist/core/lbug/schema.d.ts +8 -4
  206. package/dist/core/lbug/schema.js +65 -4
  207. package/dist/core/tree-sitter/parser-loader.js +7 -1
  208. package/dist/core/wiki/cursor-client.d.ts +31 -0
  209. package/dist/core/wiki/cursor-client.js +127 -0
  210. package/dist/core/wiki/generator.d.ts +28 -9
  211. package/dist/core/wiki/generator.js +115 -18
  212. package/dist/core/wiki/graph-queries.d.ts +4 -0
  213. package/dist/core/wiki/graph-queries.js +7 -1
  214. package/dist/core/wiki/llm-client.d.ts +2 -0
  215. package/dist/core/wiki/llm-client.js +8 -4
  216. package/dist/core/wiki/prompts.d.ts +3 -3
  217. package/dist/core/wiki/prompts.js +6 -0
  218. package/dist/mcp/core/embedder.js +11 -3
  219. package/dist/mcp/core/lbug-adapter.d.ts +5 -0
  220. package/dist/mcp/core/lbug-adapter.js +23 -2
  221. package/dist/mcp/local/local-backend.d.ts +38 -5
  222. package/dist/mcp/local/local-backend.js +804 -63
  223. package/dist/mcp/resources.js +2 -0
  224. package/dist/mcp/tools.js +73 -4
  225. package/dist/server/api.d.ts +19 -1
  226. package/dist/server/api.js +66 -6
  227. package/dist/storage/git.d.ts +12 -0
  228. package/dist/storage/git.js +21 -0
  229. package/dist/storage/repo-manager.d.ts +3 -0
  230. package/package.json +25 -16
  231. package/dist/core/ingestion/named-binding-extraction.d.ts +0 -61
  232. package/dist/core/ingestion/named-binding-extraction.js +0 -363
  233. package/dist/core/ingestion/resolvers/index.d.ts +0 -18
  234. package/dist/core/ingestion/resolvers/index.js +0 -13
  235. package/dist/core/ingestion/resolvers/jvm.js +0 -87
  236. package/dist/core/ingestion/resolvers/php.d.ts +0 -15
  237. package/dist/core/ingestion/resolvers/php.js +0 -35
  238. package/dist/core/ingestion/type-extractors/index.d.ts +0 -22
  239. package/dist/core/ingestion/type-extractors/index.js +0 -31
  240. package/dist/core/ingestion/utils.d.ts +0 -138
  241. package/dist/core/ingestion/utils.js +0 -1290
  242. package/scripts/patch-tree-sitter-swift.cjs +0 -74
@@ -13,7 +13,7 @@ import PHP from 'tree-sitter-php';
13
13
  import Ruby from 'tree-sitter-ruby';
14
14
  import { createRequire } from 'node:module';
15
15
  import { SupportedLanguages } from '../../../config/supported-languages.js';
16
- import { LANGUAGE_QUERIES } from '../tree-sitter-queries.js';
16
+ import { getProvider } from '../languages/index.js';
17
17
  import { getTreeSitterBufferSize, TREE_SITTER_MAX_BUFFER } from '../constants.js';
18
18
  // tree-sitter-swift is an optionalDependency — may not be installed
19
19
  const _require = createRequire(import.meta.url);
@@ -22,22 +22,26 @@ try {
22
22
  Swift = _require('tree-sitter-swift');
23
23
  }
24
24
  catch { }
25
+ // tree-sitter-dart is an optionalDependency — may not be installed
26
+ let Dart = null;
27
+ try {
28
+ Dart = _require('tree-sitter-dart');
29
+ }
30
+ catch { }
25
31
  // tree-sitter-kotlin is an optionalDependency — may not be installed
26
32
  let Kotlin = null;
27
33
  try {
28
34
  Kotlin = _require('tree-sitter-kotlin');
29
35
  }
30
36
  catch { }
31
- import { getLanguageFromFilename, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, getDefinitionNodeFromCaptures, findEnclosingClassId, extractMethodSignature, countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, extractMixedChain, } from '../utils.js';
37
+ import { getLanguageFromFilename } from '../utils/language-detection.js';
38
+ import { FUNCTION_NODE_TYPES, extractFunctionName, getDefinitionNodeFromCaptures, findEnclosingClassId, getLabelFromCaptures, extractMethodSignature, findDescendant, extractStringContent, } from '../utils/ast-helpers.js';
39
+ import { countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, extractMixedChain, } from '../utils/call-analysis.js';
32
40
  import { buildTypeEnv } from '../type-env.js';
33
- import { isNodeExported } from '../export-detection.js';
34
41
  import { detectFrameworkFromAST } from '../framework-detection.js';
35
- import { typeConfigs } from '../type-extractors/index.js';
36
42
  import { generateId } from '../../../lib/utils.js';
37
- import { extractNamedBindings } from '../named-binding-extraction.js';
38
- import { appendKotlinWildcard } from '../resolvers/index.js';
39
- import { callRouters } from '../call-routing.js';
40
- import { extractPropertyDeclaredType } from '../type-extractors/shared.js';
43
+ import { preprocessImportPath } from '../import-processor.js';
44
+ import { CLASS_CONTAINER_TYPES } from '../utils/ast-helpers.js';
41
45
  // ============================================================================
42
46
  // Worker-local parser + language map
43
47
  // ============================================================================
@@ -56,6 +60,7 @@ const languageMap = {
56
60
  ...(Kotlin ? { [SupportedLanguages.Kotlin]: Kotlin } : {}),
57
61
  [SupportedLanguages.PHP]: PHP.php_only,
58
62
  [SupportedLanguages.Ruby]: Ruby,
63
+ ...(Dart ? { [SupportedLanguages.Dart]: Dart } : {}),
59
64
  ...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}),
60
65
  };
61
66
  /**
@@ -79,79 +84,131 @@ const setLanguage = (language, filePath) => {
79
84
  throw new Error(`Unsupported language: ${language}`);
80
85
  parser.setLanguage(lang);
81
86
  };
82
- // isNodeExported imported from ../export-detection.js (shared module)
83
87
  // ============================================================================
84
- // Enclosing function detection (for call extraction)
88
+ // Per-file O(1) memoization avoids repeated parent-chain walks per symbol.
89
+ // Three bare Maps cleared at file boundaries. Map.get() returns undefined for
90
+ // missing keys, so `cached !== undefined` distinguishes "not computed" from
91
+ // a stored null (enclosing class/function not found = top-level).
92
+ // ============================================================================
93
+ const classIdCache = new Map();
94
+ const functionIdCache = new Map();
95
+ const exportCache = new Map();
96
+ const clearCaches = () => { classIdCache.clear(); functionIdCache.clear(); exportCache.clear(); fieldInfoCache.clear(); };
97
+ // ============================================================================
98
+ // FieldExtractor cache — extract field metadata once per class, reuse for each property.
99
+ // Keyed by class node startIndex (unique per AST node within a file).
85
100
  // ============================================================================
86
- /** Walk up AST to find enclosing function, return its generateId or null for top-level */
87
- const findEnclosingFunctionId = (node, filePath) => {
101
+ const fieldInfoCache = new Map();
102
+ /**
103
+ * Walk up from a definition node to find the nearest enclosing class/struct/interface
104
+ * AST node. Returns the SyntaxNode itself (not an ID) for passing to FieldExtractor.
105
+ */
106
+ function findEnclosingClassNode(node) {
107
+ let current = node.parent;
108
+ while (current) {
109
+ if (CLASS_CONTAINER_TYPES.has(current.type)) {
110
+ return current;
111
+ }
112
+ current = current.parent;
113
+ }
114
+ return null;
115
+ }
116
+ /**
117
+ * Minimal no-op SymbolTable stub for FieldExtractorContext in the worker.
118
+ * Field extraction only uses symbolTable.lookupExactAll for optional type resolution —
119
+ * returning [] causes the extractor to use the raw type string, which is fine for us.
120
+ */
121
+ const NOOP_SYMBOL_TABLE = {
122
+ lookupExactAll: () => [],
123
+ lookupExact: () => undefined,
124
+ lookupExactFull: () => undefined,
125
+ };
126
+ /**
127
+ * Get (or extract and cache) field info for a class node.
128
+ * Returns a name→FieldInfo map, or undefined if the provider has no field extractor
129
+ * or the class yielded no fields.
130
+ */
131
+ function getFieldInfo(classNode, provider, context) {
132
+ if (!provider.fieldExtractor)
133
+ return undefined;
134
+ const cacheKey = classNode.startIndex;
135
+ let cached = fieldInfoCache.get(cacheKey);
136
+ if (cached)
137
+ return cached;
138
+ const result = provider.fieldExtractor.extract(classNode, context);
139
+ if (!result?.fields?.length)
140
+ return undefined;
141
+ cached = new Map();
142
+ for (const field of result.fields) {
143
+ cached.set(field.name, field);
144
+ }
145
+ fieldInfoCache.set(cacheKey, cached);
146
+ return cached;
147
+ }
148
+ /** Walk up AST to find enclosing function, return its generateId or null for top-level.
149
+ * Applies provider.labelOverride so the label matches the definition phase (single source of truth). */
150
+ const findEnclosingFunctionId = (node, filePath, provider) => {
151
+ const cached = functionIdCache.get(node);
152
+ if (cached !== undefined)
153
+ return cached;
88
154
  let current = node.parent;
89
155
  while (current) {
90
156
  if (FUNCTION_NODE_TYPES.has(current.type)) {
91
157
  const { funcName, label } = extractFunctionName(current);
92
158
  if (funcName) {
93
- return generateId(label, `${filePath}:${funcName}`);
159
+ // Apply labelOverride so label matches definition phase (e.g., Kotlin Function→Method).
160
+ // null means "skip as definition" — keep original label for scope identification.
161
+ let finalLabel = label;
162
+ if (provider.labelOverride) {
163
+ const override = provider.labelOverride(current, label);
164
+ if (override !== null)
165
+ finalLabel = override;
166
+ }
167
+ const result = generateId(finalLabel, `${filePath}:${funcName}`);
168
+ functionIdCache.set(node, result);
169
+ return result;
170
+ }
171
+ }
172
+ // Language-specific enclosing function resolution (e.g., Dart where
173
+ // function_body is a sibling of function_signature, not a child).
174
+ if (provider.enclosingFunctionFinder) {
175
+ const customResult = provider.enclosingFunctionFinder(current);
176
+ if (customResult) {
177
+ let finalLabel = customResult.label;
178
+ if (provider.labelOverride) {
179
+ const override = provider.labelOverride(current.previousSibling, finalLabel);
180
+ if (override !== null)
181
+ finalLabel = override;
182
+ }
183
+ const result = generateId(finalLabel, `${filePath}:${customResult.funcName}`);
184
+ functionIdCache.set(node, result);
185
+ return result;
94
186
  }
95
187
  }
96
188
  current = current.parent;
97
189
  }
190
+ functionIdCache.set(node, null);
98
191
  return null;
99
192
  };
100
- // ============================================================================
101
- // Label detection from capture map
102
- // ============================================================================
103
- const getLabelFromCaptures = (captureMap) => {
104
- // Skip imports (handled separately) and calls
105
- if (captureMap['import'] || captureMap['call'])
106
- return null;
107
- if (!captureMap['name'])
108
- return null;
109
- if (captureMap['definition.function'])
110
- return 'Function';
111
- if (captureMap['definition.class'])
112
- return 'Class';
113
- if (captureMap['definition.interface'])
114
- return 'Interface';
115
- if (captureMap['definition.method'])
116
- return 'Method';
117
- if (captureMap['definition.struct'])
118
- return 'Struct';
119
- if (captureMap['definition.enum'])
120
- return 'Enum';
121
- if (captureMap['definition.namespace'])
122
- return 'Namespace';
123
- if (captureMap['definition.module'])
124
- return 'Module';
125
- if (captureMap['definition.trait'])
126
- return 'Trait';
127
- if (captureMap['definition.impl'])
128
- return 'Impl';
129
- if (captureMap['definition.type'])
130
- return 'TypeAlias';
131
- if (captureMap['definition.const'])
132
- return 'Const';
133
- if (captureMap['definition.static'])
134
- return 'Static';
135
- if (captureMap['definition.typedef'])
136
- return 'Typedef';
137
- if (captureMap['definition.macro'])
138
- return 'Macro';
139
- if (captureMap['definition.union'])
140
- return 'Union';
141
- if (captureMap['definition.property'])
142
- return 'Property';
143
- if (captureMap['definition.record'])
144
- return 'Record';
145
- if (captureMap['definition.delegate'])
146
- return 'Delegate';
147
- if (captureMap['definition.annotation'])
148
- return 'Annotation';
149
- if (captureMap['definition.constructor'])
150
- return 'Constructor';
151
- if (captureMap['definition.template'])
152
- return 'Template';
153
- return 'CodeElement';
193
+ /** Cached wrapper for findEnclosingClassId — avoids repeated parent walks. */
194
+ const cachedFindEnclosingClassId = (node, filePath) => {
195
+ const cached = classIdCache.get(node);
196
+ if (cached !== undefined)
197
+ return cached;
198
+ const result = findEnclosingClassId(node, filePath);
199
+ classIdCache.set(node, result);
200
+ return result;
201
+ };
202
+ /** Cached wrapper for export checking — avoids repeated parent walks per symbol. */
203
+ const cachedExportCheck = (checker, node, name) => {
204
+ const cached = exportCache.get(node);
205
+ if (cached !== undefined)
206
+ return cached;
207
+ const result = checker(node, name);
208
+ exportCache.set(node, result);
209
+ return result;
154
210
  };
211
+ // Label detection moved to shared getLabelFromCaptures in utils.ts
155
212
  // DEFINITION_CAPTURE_KEYS and getDefinitionNodeFromCaptures imported from ../utils.js
156
213
  // ============================================================================
157
214
  // Process a batch of files
@@ -166,7 +223,12 @@ const processBatch = (files, onProgress) => {
166
223
  assignments: [],
167
224
  heritage: [],
168
225
  routes: [],
226
+ fetchCalls: [],
227
+ decoratorRoutes: [],
228
+ toolDefs: [],
229
+ ormQueries: [],
169
230
  constructorBindings: [],
231
+ typeEnvBindings: [],
170
232
  skippedLanguages: {},
171
233
  fileCount: 0,
172
234
  };
@@ -194,7 +256,8 @@ const processBatch = (files, onProgress) => {
194
256
  }
195
257
  } : undefined;
196
258
  for (const [language, langFiles] of byLanguage) {
197
- const queryString = LANGUAGE_QUERIES[language];
259
+ const provider = getProvider(language);
260
+ const queryString = provider.treeSitterQueries;
198
261
  if (!queryString)
199
262
  continue;
200
263
  // Track if we need to handle tsx separately
@@ -246,114 +309,23 @@ const processBatch = (files, onProgress) => {
246
309
  }
247
310
  return result;
248
311
  };
249
- // ============================================================================
250
- // PHP Eloquent metadata extraction
251
- // ============================================================================
252
- /** Eloquent model properties whose array values are worth indexing */
253
- const ELOQUENT_ARRAY_PROPS = new Set(['fillable', 'casts', 'hidden', 'guarded', 'with', 'appends']);
254
- /** Eloquent relationship method names */
255
- const ELOQUENT_RELATIONS = new Set([
256
- 'hasMany', 'hasOne', 'belongsTo', 'belongsToMany',
257
- 'morphTo', 'morphMany', 'morphOne', 'morphToMany', 'morphedByMany',
258
- 'hasManyThrough', 'hasOneThrough',
259
- ]);
260
- function findDescendant(node, type) {
261
- if (node.type === type)
262
- return node;
263
- for (const child of (node.children ?? [])) {
264
- const found = findDescendant(child, type);
265
- if (found)
266
- return found;
267
- }
268
- return null;
269
- }
270
- function extractStringContent(node) {
271
- if (!node)
272
- return null;
273
- const content = node.children?.find((c) => c.type === 'string_content');
274
- if (content)
275
- return content.text;
276
- if (node.type === 'string_content')
277
- return node.text;
278
- return null;
279
- }
280
- /**
281
- * For a PHP property_declaration node, extract array values as a description string.
282
- * Returns null if not an Eloquent model property or no array values found.
283
- */
284
- function extractPhpPropertyDescription(propName, propDeclNode) {
285
- if (!ELOQUENT_ARRAY_PROPS.has(propName))
286
- return null;
287
- const arrayNode = findDescendant(propDeclNode, 'array_creation_expression');
288
- if (!arrayNode)
289
- return null;
290
- const items = [];
291
- for (const child of (arrayNode.children ?? [])) {
292
- if (child.type !== 'array_element_initializer')
293
- continue;
294
- const children = child.children ?? [];
295
- const arrowIdx = children.findIndex((c) => c.type === '=>');
296
- if (arrowIdx !== -1) {
297
- // key => value pair (used in $casts)
298
- const key = extractStringContent(children[arrowIdx - 1]);
299
- const val = extractStringContent(children[arrowIdx + 1]);
300
- if (key && val)
301
- items.push(`${key}:${val}`);
302
- }
303
- else {
304
- // Simple value (used in $fillable, $hidden, etc.)
305
- const val = extractStringContent(children[0]);
306
- if (val)
307
- items.push(val);
308
- }
309
- }
310
- return items.length > 0 ? items.join(', ') : null;
311
- }
312
- /**
313
- * For a PHP method_declaration node, detect if it defines an Eloquent relationship.
314
- * Returns description like "hasMany(Post)" or null.
315
- */
316
- function extractEloquentRelationDescription(methodNode) {
317
- function findRelationCall(node) {
318
- if (node.type === 'member_call_expression') {
319
- const children = node.children ?? [];
320
- const objectNode = children.find((c) => c.type === 'variable_name' && c.text === '$this');
321
- const nameNode = children.find((c) => c.type === 'name');
322
- if (objectNode && nameNode && ELOQUENT_RELATIONS.has(nameNode.text))
323
- return node;
324
- }
325
- for (const child of (node.children ?? [])) {
326
- const found = findRelationCall(child);
327
- if (found)
328
- return found;
329
- }
330
- return null;
331
- }
332
- const callNode = findRelationCall(methodNode);
333
- if (!callNode)
334
- return null;
335
- const relType = callNode.children?.find((c) => c.type === 'name')?.text;
336
- const argsNode = callNode.children?.find((c) => c.type === 'arguments');
337
- let targetModel = null;
338
- if (argsNode) {
339
- const firstArg = argsNode.children?.find((c) => c.type === 'argument');
340
- if (firstArg) {
341
- const classConstant = firstArg.children?.find((c) => c.type === 'class_constant_access_expression');
342
- if (classConstant) {
343
- targetModel = classConstant.children?.find((c) => c.type === 'name')?.text ?? null;
344
- }
345
- }
346
- }
347
- if (relType && targetModel)
348
- return `${relType}(${targetModel})`;
349
- if (relType)
350
- return relType;
351
- return null;
352
- }
353
312
  const ROUTE_HTTP_METHODS = new Set([
354
313
  'get', 'post', 'put', 'patch', 'delete', 'options', 'any', 'match',
355
314
  ]);
356
315
  const ROUTE_RESOURCE_METHODS = new Set(['resource', 'apiResource']);
316
+ // Express/Hono method names that register routes
317
+ const EXPRESS_ROUTE_METHODS = new Set(['get', 'post', 'put', 'delete', 'patch', 'all', 'use', 'route']);
318
+ // HTTP client methods that are ONLY used by clients, not Express route registration.
319
+ // Methods like get/post/put/delete/patch overlap with Express — those are captured by
320
+ // the express_route handler as route definitions, not consumers. The fetch() global
321
+ // function is captured separately by the route.fetch query.
322
+ const HTTP_CLIENT_ONLY_METHODS = new Set(['head', 'options', 'request', 'ajax']);
323
+ // Decorator names that indicate HTTP route handlers (NestJS, Flask, FastAPI, Spring)
324
+ const ROUTE_DECORATOR_NAMES = new Set([
325
+ 'Get', 'Post', 'Put', 'Delete', 'Patch', 'Route',
326
+ 'get', 'post', 'put', 'delete', 'patch', 'route',
327
+ 'RequestMapping', 'GetMapping', 'PostMapping', 'PutMapping', 'DeleteMapping',
328
+ ]);
357
329
  const RESOURCE_ACTIONS = ['index', 'create', 'store', 'show', 'edit', 'update', 'destroy'];
358
330
  const API_RESOURCE_ACTIONS = ['index', 'store', 'show', 'update', 'destroy'];
359
331
  /** Check if node is a scoped_call_expression with object 'Route' */
@@ -697,6 +669,50 @@ function extractLaravelRoutes(tree, filePath) {
697
669
  walk(tree.rootNode, []);
698
670
  return routes;
699
671
  }
672
+ // ============================================================================
673
+ // ORM Query Detection (Prisma + Supabase)
674
+ // ============================================================================
675
+ const PRISMA_QUERY_RE = /\bprisma\.(\w+)\.(findMany|findFirst|findUnique|findUniqueOrThrow|findFirstOrThrow|create|createMany|update|updateMany|delete|deleteMany|upsert|count|aggregate|groupBy)\s*\(/g;
676
+ const SUPABASE_QUERY_RE = /\bsupabase\.from\s*\(\s*['"](\w+)['"]\s*\)\s*\.(select|insert|update|delete|upsert)\s*\(/g;
677
+ /**
678
+ * Extract ORM query calls from file content via regex.
679
+ * Appends results to the provided array (avoids allocation when no matches).
680
+ */
681
+ export function extractORMQueries(filePath, content, out) {
682
+ const hasPrisma = content.includes('prisma.');
683
+ const hasSupabase = content.includes('supabase.from');
684
+ if (!hasPrisma && !hasSupabase)
685
+ return;
686
+ if (hasPrisma) {
687
+ PRISMA_QUERY_RE.lastIndex = 0;
688
+ let m;
689
+ while ((m = PRISMA_QUERY_RE.exec(content)) !== null) {
690
+ const model = m[1];
691
+ if (model.startsWith('$'))
692
+ continue;
693
+ out.push({
694
+ filePath,
695
+ orm: 'prisma',
696
+ model,
697
+ method: m[2],
698
+ lineNumber: content.substring(0, m.index).split('\n').length - 1,
699
+ });
700
+ }
701
+ }
702
+ if (hasSupabase) {
703
+ SUPABASE_QUERY_RE.lastIndex = 0;
704
+ let m;
705
+ while ((m = SUPABASE_QUERY_RE.exec(content)) !== null) {
706
+ out.push({
707
+ filePath,
708
+ orm: 'supabase',
709
+ model: m[1],
710
+ method: m[2],
711
+ lineNumber: content.substring(0, m.index).split('\n').length - 1,
712
+ });
713
+ }
714
+ }
715
+ }
700
716
  const processFileGroup = (files, language, queryString, result, onFileProcessed) => {
701
717
  let query;
702
718
  try {
@@ -717,6 +733,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
717
733
  // Skip files larger than the max tree-sitter buffer (32 MB)
718
734
  if (file.content.length > TREE_SITTER_MAX_BUFFER)
719
735
  continue;
736
+ clearCaches(); // Reset memoization before each new file
720
737
  let tree;
721
738
  try {
722
739
  tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
@@ -727,13 +744,6 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
727
744
  }
728
745
  result.fileCount++;
729
746
  onFileProcessed?.();
730
- // Build per-file type environment + constructor bindings in a single AST walk.
731
- // Constructor bindings are verified against the SymbolTable in processCallsFromExtracted.
732
- const typeEnv = buildTypeEnv(tree, language);
733
- const callRouter = callRouters[language];
734
- if (typeEnv.constructorBindings.length > 0) {
735
- result.constructorBindings.push({ filePath: file.path, bindings: [...typeEnv.constructorBindings] });
736
- }
737
747
  let matches;
738
748
  try {
739
749
  matches = query.matches(tree.rootNode);
@@ -742,6 +752,54 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
742
752
  console.warn(`Query execution failed for ${file.path}: ${err instanceof Error ? err.message : String(err)}`);
743
753
  continue;
744
754
  }
755
+ // Pre-pass: extract heritage from query matches to build parentMap for buildTypeEnv.
756
+ // Heritage edges (EXTENDS/IMPLEMENTS) are created by heritage-processor which runs
757
+ // in PARALLEL with call-processor, so the graph edges don't exist when buildTypeEnv
758
+ // runs. This pre-pass makes parent class information available for type resolution.
759
+ const fileParentMap = new Map();
760
+ for (const match of matches) {
761
+ const captureMap = {};
762
+ for (const c of match.captures) {
763
+ captureMap[c.name] = c.node;
764
+ }
765
+ if (captureMap['heritage.class'] && captureMap['heritage.extends']) {
766
+ const className = captureMap['heritage.class'].text;
767
+ const parentName = captureMap['heritage.extends'].text;
768
+ // Skip Go named fields (only anonymous fields are struct embedding)
769
+ const extendsNode = captureMap['heritage.extends'];
770
+ const fieldDecl = extendsNode.parent;
771
+ if (fieldDecl?.type === 'field_declaration' && fieldDecl.childForFieldName('name'))
772
+ continue;
773
+ let parents = fileParentMap.get(className);
774
+ if (!parents) {
775
+ parents = [];
776
+ fileParentMap.set(className, parents);
777
+ }
778
+ if (!parents.includes(parentName))
779
+ parents.push(parentName);
780
+ }
781
+ }
782
+ // Build per-file type environment + constructor bindings in a single AST walk.
783
+ // Constructor bindings are verified against the SymbolTable in processCallsFromExtracted.
784
+ const parentMap = fileParentMap;
785
+ const provider = getProvider(language);
786
+ const typeEnv = buildTypeEnv(tree, language, { parentMap, enclosingFunctionFinder: provider?.enclosingFunctionFinder });
787
+ const callRouter = provider.callRouter;
788
+ if (typeEnv.constructorBindings.length > 0) {
789
+ result.constructorBindings.push({ filePath: file.path, bindings: [...typeEnv.constructorBindings] });
790
+ }
791
+ // Extract file-scope bindings for ExportedTypeMap (closes worker/sequential quality gap).
792
+ // Sequential path uses collectExportedBindings(typeEnv) directly; worker path serializes
793
+ // these bindings so the main thread can merge them into ExportedTypeMap.
794
+ const fileScope = typeEnv.fileScope();
795
+ if (fileScope.size > 0) {
796
+ const bindings = [];
797
+ for (const [name, type] of fileScope)
798
+ bindings.push([name, type]);
799
+ result.typeEnvBindings.push({ filePath: file.path, bindings });
800
+ }
801
+ // Per-file map: decorator end-line → decorator info, for associating with definitions
802
+ const fileDecorators = new Map();
745
803
  for (const match of matches) {
746
804
  const captureMap = {};
747
805
  for (const c of match.captures) {
@@ -749,10 +807,11 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
749
807
  }
750
808
  // Extract import paths before skipping
751
809
  if (captureMap['import'] && captureMap['import.source']) {
752
- const rawImportPath = language === SupportedLanguages.Kotlin
753
- ? appendKotlinWildcard(captureMap['import.source'].text.replace(/['"<>]/g, ''), captureMap['import'])
754
- : captureMap['import.source'].text.replace(/['"<>]/g, '');
755
- const namedBindings = extractNamedBindings(captureMap['import'], language);
810
+ const rawImportPath = preprocessImportPath(captureMap['import.source'].text, captureMap['import'], provider);
811
+ if (!rawImportPath)
812
+ continue;
813
+ const extractor = provider.namedBindingExtractor;
814
+ const namedBindings = extractor ? extractor(captureMap['import']) : undefined;
756
815
  result.imports.push({
757
816
  filePath: file.path,
758
817
  rawImportPath,
@@ -766,7 +825,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
766
825
  const receiverText = captureMap['assignment.receiver'].text;
767
826
  const propertyName = captureMap['assignment.property'].text;
768
827
  if (receiverText && propertyName) {
769
- const srcId = findEnclosingFunctionId(captureMap['assignment'], file.path)
828
+ const srcId = findEnclosingFunctionId(captureMap['assignment'], file.path, provider)
770
829
  || generateId('File', file.path);
771
830
  let receiverTypeName;
772
831
  if (typeEnv) {
@@ -783,13 +842,82 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
783
842
  if (!captureMap['call'])
784
843
  continue;
785
844
  }
845
+ // Store decorator metadata for later association with definitions
846
+ if (captureMap['decorator'] && captureMap['decorator.name']) {
847
+ const decoratorName = captureMap['decorator.name'].text;
848
+ const decoratorArg = captureMap['decorator.arg']?.text;
849
+ const decoratorNode = captureMap['decorator'];
850
+ // Store by the decorator's end line — the definition follows immediately after
851
+ fileDecorators.set(decoratorNode.endPosition.row, { name: decoratorName, arg: decoratorArg });
852
+ if (ROUTE_DECORATOR_NAMES.has(decoratorName)) {
853
+ const routePath = decoratorArg || '';
854
+ const method = decoratorName.replace('Mapping', '').toUpperCase();
855
+ const httpMethod = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(method) ? method : 'GET';
856
+ result.decoratorRoutes.push({
857
+ filePath: file.path,
858
+ routePath,
859
+ httpMethod,
860
+ decoratorName,
861
+ lineNumber: decoratorNode.startPosition.row,
862
+ });
863
+ }
864
+ // MCP/RPC tool detection: @mcp.tool(), @app.tool(), @server.tool()
865
+ if (decoratorName === 'tool') {
866
+ // Re-store with isTool flag for the definition handler
867
+ fileDecorators.set(decoratorNode.endPosition.row, { name: decoratorName, arg: decoratorArg, isTool: true });
868
+ }
869
+ continue;
870
+ }
871
+ // Extract HTTP consumer URLs: fetch(), axios.get(), $.get(), requests.get(), etc.
872
+ if (captureMap['route.fetch']) {
873
+ const urlNode = captureMap['route.url'] ?? captureMap['route.template_url'];
874
+ if (urlNode) {
875
+ result.fetchCalls.push({
876
+ filePath: file.path,
877
+ fetchURL: urlNode.text,
878
+ lineNumber: captureMap['route.fetch'].startPosition.row,
879
+ });
880
+ }
881
+ continue;
882
+ }
883
+ // HTTP client calls: axios.get('/path'), $.post('/path'), requests.get('/path')
884
+ // Skip methods also in EXPRESS_ROUTE_METHODS to avoid double-registering Express
885
+ // routes as both route definitions AND consumers (both queries match same AST node)
886
+ if (captureMap['http_client'] && captureMap['http_client.url']) {
887
+ const method = captureMap['http_client.method']?.text;
888
+ const url = captureMap['http_client.url'].text;
889
+ if (method && HTTP_CLIENT_ONLY_METHODS.has(method) && url.startsWith('/')) {
890
+ result.fetchCalls.push({
891
+ filePath: file.path,
892
+ fetchURL: url,
893
+ lineNumber: captureMap['http_client'].startPosition.row,
894
+ });
895
+ }
896
+ continue;
897
+ }
898
+ // Express/Hono route registration: app.get('/path', handler)
899
+ if (captureMap['express_route'] && captureMap['express_route.method'] && captureMap['express_route.path']) {
900
+ const method = captureMap['express_route.method'].text;
901
+ const routePath = captureMap['express_route.path'].text;
902
+ if (EXPRESS_ROUTE_METHODS.has(method) && routePath.startsWith('/')) {
903
+ const httpMethod = method === 'all' || method === 'use' || method === 'route' ? 'GET' : method.toUpperCase();
904
+ result.decoratorRoutes.push({
905
+ filePath: file.path,
906
+ routePath,
907
+ httpMethod,
908
+ decoratorName: `express.${method}`,
909
+ lineNumber: captureMap['express_route'].startPosition.row,
910
+ });
911
+ }
912
+ continue;
913
+ }
786
914
  // Extract call sites
787
915
  if (captureMap['call']) {
788
916
  const callNameNode = captureMap['call.name'];
789
917
  if (callNameNode) {
790
918
  const calledName = callNameNode.text;
791
919
  // Dispatch: route language-specific calls (heritage, properties, imports)
792
- const routed = callRouter(calledName, captureMap['call']);
920
+ const routed = callRouter?.(calledName, captureMap['call']);
793
921
  if (routed) {
794
922
  if (routed.kind === 'skip')
795
923
  continue;
@@ -813,8 +941,19 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
813
941
  continue;
814
942
  }
815
943
  if (routed.kind === 'properties') {
816
- const propEnclosingClassId = findEnclosingClassId(captureMap['call'], file.path);
944
+ const propEnclosingClassId = cachedFindEnclosingClassId(captureMap['call'], file.path);
945
+ // Enrich routed properties with FieldExtractor metadata
946
+ let routedFieldMap;
947
+ if (provider.fieldExtractor && typeEnv) {
948
+ const classNode = findEnclosingClassNode(captureMap['call']);
949
+ if (classNode) {
950
+ routedFieldMap = getFieldInfo(classNode, provider, {
951
+ typeEnv, symbolTable: NOOP_SYMBOL_TABLE, filePath: file.path, language,
952
+ });
953
+ }
954
+ }
817
955
  for (const item of routed.items) {
956
+ const routedFieldInfo = routedFieldMap?.get(item.propName);
818
957
  const nodeId = generateId('Property', `${file.path}:${item.propName}`);
819
958
  result.nodes.push({
820
959
  id: nodeId,
@@ -827,6 +966,10 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
827
966
  language,
828
967
  isExported: true,
829
968
  description: item.accessorType,
969
+ ...(item.declaredType ? { declaredType: item.declaredType } : routedFieldInfo?.type ? { declaredType: routedFieldInfo.type } : {}),
970
+ ...(routedFieldInfo?.visibility !== undefined ? { visibility: routedFieldInfo.visibility } : {}),
971
+ ...(routedFieldInfo?.isStatic !== undefined ? { isStatic: routedFieldInfo.isStatic } : {}),
972
+ ...(routedFieldInfo?.isReadonly !== undefined ? { isReadonly: routedFieldInfo.isReadonly } : {}),
830
973
  },
831
974
  });
832
975
  result.symbols.push({
@@ -835,7 +978,10 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
835
978
  nodeId,
836
979
  type: 'Property',
837
980
  ...(propEnclosingClassId ? { ownerId: propEnclosingClassId } : {}),
838
- ...(item.declaredType ? { declaredType: item.declaredType } : {}),
981
+ ...(item.declaredType ? { declaredType: item.declaredType } : routedFieldInfo?.type ? { declaredType: routedFieldInfo.type } : {}),
982
+ ...(routedFieldInfo?.visibility !== undefined ? { visibility: routedFieldInfo.visibility } : {}),
983
+ ...(routedFieldInfo?.isStatic !== undefined ? { isStatic: routedFieldInfo.isStatic } : {}),
984
+ ...(routedFieldInfo?.isReadonly !== undefined ? { isReadonly: routedFieldInfo.isReadonly } : {}),
839
985
  });
840
986
  const fileId = generateId('File', file.path);
841
987
  const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
@@ -862,9 +1008,9 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
862
1008
  }
863
1009
  // kind === 'call' — fall through to normal call processing below
864
1010
  }
865
- if (!isBuiltInOrNoise(calledName)) {
1011
+ if (!provider.isBuiltInName(calledName)) {
866
1012
  const callNode = captureMap['call'];
867
- const sourceId = findEnclosingFunctionId(callNode, file.path)
1013
+ const sourceId = findEnclosingFunctionId(callNode, file.path, provider)
868
1014
  || generateId('File', file.path);
869
1015
  const callForm = inferCallForm(callNode, callNameNode);
870
1016
  let receiverName = callForm === 'member' ? extractReceiverName(callNameNode) : undefined;
@@ -941,24 +1087,9 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
941
1087
  continue;
942
1088
  }
943
1089
  }
944
- const nodeLabel = getLabelFromCaptures(captureMap);
1090
+ const nodeLabel = getLabelFromCaptures(captureMap, provider);
945
1091
  if (!nodeLabel)
946
1092
  continue;
947
- // C/C++: @definition.function is broad and also matches inline class methods (inside
948
- // a class/struct body). Those are already captured by @definition.method, so skip
949
- // the duplicate Function entry to prevent double-indexing in globalIndex.
950
- if ((language === SupportedLanguages.CPlusPlus || language === SupportedLanguages.C) &&
951
- nodeLabel === 'Function') {
952
- let ancestor = captureMap['definition.function']?.parent;
953
- while (ancestor) {
954
- if (ancestor.type === 'class_specifier' || ancestor.type === 'struct_specifier') {
955
- break; // inside a class body — duplicate of @definition.method
956
- }
957
- ancestor = ancestor.parent;
958
- }
959
- if (ancestor)
960
- continue; // found a class/struct ancestor → skip
961
- }
962
1093
  const nameNode = captureMap['name'];
963
1094
  // Synthesize name for constructors without explicit @name capture (e.g. Swift init)
964
1095
  if (!nameNode && nodeLabel !== 'Constructor')
@@ -967,29 +1098,57 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
967
1098
  const definitionNode = getDefinitionNodeFromCaptures(captureMap);
968
1099
  const startLine = definitionNode ? definitionNode.startPosition.row : (nameNode ? nameNode.startPosition.row : 0);
969
1100
  const nodeId = generateId(nodeLabel, `${file.path}:${nodeName}`);
970
- let description;
971
- if (language === SupportedLanguages.PHP) {
972
- if (nodeLabel === 'Property' && captureMap['definition.property']) {
973
- description = extractPhpPropertyDescription(nodeName, captureMap['definition.property']) ?? undefined;
974
- }
975
- else if (nodeLabel === 'Method' && captureMap['definition.method']) {
976
- description = extractEloquentRelationDescription(captureMap['definition.method']) ?? undefined;
977
- }
978
- }
979
- const frameworkHint = definitionNode
1101
+ const description = provider.descriptionExtractor?.(nodeLabel, nodeName, captureMap);
1102
+ let frameworkHint = definitionNode
980
1103
  ? detectFrameworkFromAST(language, (definitionNode.text || '').slice(0, 300))
981
1104
  : null;
1105
+ // Decorators appear on lines immediately before their definition; allow up to
1106
+ // MAX_DECORATOR_SCAN_LINES gap for blank lines / multi-line decorator stacks.
1107
+ const MAX_DECORATOR_SCAN_LINES = 5;
1108
+ if (definitionNode) {
1109
+ const defStartLine = definitionNode.startPosition.row;
1110
+ for (let checkLine = defStartLine - 1; checkLine >= Math.max(0, defStartLine - MAX_DECORATOR_SCAN_LINES); checkLine--) {
1111
+ const dec = fileDecorators.get(checkLine);
1112
+ if (dec) {
1113
+ // Use first (closest) decorator found for framework hint
1114
+ if (!frameworkHint) {
1115
+ frameworkHint = {
1116
+ framework: 'decorator',
1117
+ entryPointMultiplier: 1.2,
1118
+ reason: `@${dec.name}${dec.arg ? `("${dec.arg}")` : ''}`,
1119
+ };
1120
+ }
1121
+ // Emit tool definition if this is a @tool decorator
1122
+ if (dec.isTool) {
1123
+ result.toolDefs.push({
1124
+ filePath: file.path,
1125
+ toolName: nodeName,
1126
+ description: dec.arg || '',
1127
+ lineNumber: definitionNode.startPosition.row,
1128
+ });
1129
+ }
1130
+ fileDecorators.delete(checkLine);
1131
+ }
1132
+ }
1133
+ }
982
1134
  let parameterCount;
1135
+ let requiredParameterCount;
1136
+ let parameterTypes;
983
1137
  let returnType;
984
1138
  let declaredType;
1139
+ let visibility;
1140
+ let isStatic;
1141
+ let isReadonly;
985
1142
  if (nodeLabel === 'Function' || nodeLabel === 'Method' || nodeLabel === 'Constructor') {
986
1143
  const sig = extractMethodSignature(definitionNode);
987
1144
  parameterCount = sig.parameterCount;
1145
+ requiredParameterCount = sig.requiredParameterCount;
1146
+ parameterTypes = sig.parameterTypes;
988
1147
  returnType = sig.returnType;
989
1148
  // Language-specific return type fallback (e.g. Ruby YARD @return [Type])
990
1149
  // Also upgrades uninformative AST types like PHP `array` with PHPDoc `@return User[]`
991
1150
  if ((!returnType || returnType === 'array' || returnType === 'iterable') && definitionNode) {
992
- const tc = typeConfigs[language];
1151
+ const tc = provider.typeConfig;
993
1152
  if (tc?.extractReturnType) {
994
1153
  const docReturn = tc.extractReturnType(definitionNode);
995
1154
  if (docReturn)
@@ -998,9 +1157,22 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
998
1157
  }
999
1158
  }
1000
1159
  else if (nodeLabel === 'Property' && definitionNode) {
1001
- // Extract the declared type for property/field nodes.
1002
- // Walk the definition node for type annotation children.
1003
- declaredType = extractPropertyDeclaredType(definitionNode);
1160
+ // FieldExtractor is the single source of truth when available
1161
+ if (provider.fieldExtractor && typeEnv) {
1162
+ const classNode = findEnclosingClassNode(definitionNode);
1163
+ if (classNode) {
1164
+ const fieldMap = getFieldInfo(classNode, provider, {
1165
+ typeEnv, symbolTable: NOOP_SYMBOL_TABLE, filePath: file.path, language,
1166
+ });
1167
+ const info = fieldMap?.get(nodeName);
1168
+ if (info) {
1169
+ declaredType = info.type ?? undefined;
1170
+ visibility = info.visibility;
1171
+ isStatic = info.isStatic;
1172
+ isReadonly = info.isReadonly;
1173
+ }
1174
+ }
1175
+ }
1004
1176
  }
1005
1177
  result.nodes.push({
1006
1178
  id: nodeId,
@@ -1011,29 +1183,40 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1011
1183
  startLine: definitionNode ? definitionNode.startPosition.row : startLine,
1012
1184
  endLine: definitionNode ? definitionNode.endPosition.row : startLine,
1013
1185
  language: language,
1014
- isExported: isNodeExported(nameNode || definitionNode, nodeName, language),
1186
+ isExported: cachedExportCheck(provider.exportChecker, nameNode || definitionNode, nodeName),
1015
1187
  ...(frameworkHint ? {
1016
1188
  astFrameworkMultiplier: frameworkHint.entryPointMultiplier,
1017
1189
  astFrameworkReason: frameworkHint.reason,
1018
1190
  } : {}),
1019
1191
  ...(description !== undefined ? { description } : {}),
1020
1192
  ...(parameterCount !== undefined ? { parameterCount } : {}),
1193
+ ...(requiredParameterCount !== undefined ? { requiredParameterCount } : {}),
1194
+ ...(parameterTypes !== undefined ? { parameterTypes } : {}),
1021
1195
  ...(returnType !== undefined ? { returnType } : {}),
1196
+ ...(declaredType !== undefined ? { declaredType } : {}),
1197
+ ...(visibility !== undefined ? { visibility } : {}),
1198
+ ...(isStatic !== undefined ? { isStatic } : {}),
1199
+ ...(isReadonly !== undefined ? { isReadonly } : {}),
1022
1200
  },
1023
1201
  });
1024
1202
  // Compute enclosing class for Method/Constructor/Property/Function — used for both ownerId and HAS_METHOD
1025
1203
  // Function is included because Kotlin/Rust/Python capture class methods as Function nodes
1026
1204
  const needsOwner = nodeLabel === 'Method' || nodeLabel === 'Constructor' || nodeLabel === 'Property' || nodeLabel === 'Function';
1027
- const enclosingClassId = needsOwner ? findEnclosingClassId(nameNode || definitionNode, file.path) : null;
1205
+ const enclosingClassId = needsOwner ? cachedFindEnclosingClassId(nameNode || definitionNode, file.path) : null;
1028
1206
  result.symbols.push({
1029
1207
  filePath: file.path,
1030
1208
  name: nodeName,
1031
1209
  nodeId,
1032
1210
  type: nodeLabel,
1033
1211
  ...(parameterCount !== undefined ? { parameterCount } : {}),
1212
+ ...(requiredParameterCount !== undefined ? { requiredParameterCount } : {}),
1213
+ ...(parameterTypes !== undefined ? { parameterTypes } : {}),
1034
1214
  ...(returnType !== undefined ? { returnType } : {}),
1035
1215
  ...(declaredType !== undefined ? { declaredType } : {}),
1036
1216
  ...(enclosingClassId ? { ownerId: enclosingClassId } : {}),
1217
+ ...(visibility !== undefined ? { visibility } : {}),
1218
+ ...(isStatic !== undefined ? { isStatic } : {}),
1219
+ ...(isReadonly !== undefined ? { isReadonly } : {}),
1037
1220
  });
1038
1221
  const fileId = generateId('File', file.path);
1039
1222
  const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
@@ -1058,11 +1241,13 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1058
1241
  });
1059
1242
  }
1060
1243
  }
1061
- // Extract Laravel routes from route files via procedural AST walk
1062
- if (language === SupportedLanguages.PHP && (file.path.includes('/routes/') || file.path.startsWith('routes/')) && file.path.endsWith('.php')) {
1244
+ // Extract framework routes via provider detection (e.g., Laravel routes.php)
1245
+ if (provider.isRouteFile?.(file.path)) {
1063
1246
  const extractedRoutes = extractLaravelRoutes(tree, file.path);
1064
1247
  result.routes.push(...extractedRoutes);
1065
1248
  }
1249
+ // Extract ORM queries (Prisma, Supabase)
1250
+ extractORMQueries(file.path, file.content, result.ormQueries);
1066
1251
  }
1067
1252
  };
1068
1253
  // ============================================================================
@@ -1071,7 +1256,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1071
1256
  /** Accumulated result across sub-batches */
1072
1257
  let accumulated = {
1073
1258
  nodes: [], relationships: [], symbols: [],
1074
- imports: [], calls: [], assignments: [], heritage: [], routes: [], constructorBindings: [], skippedLanguages: {}, fileCount: 0,
1259
+ imports: [], calls: [], assignments: [], heritage: [], routes: [], fetchCalls: [], decoratorRoutes: [], toolDefs: [], ormQueries: [], constructorBindings: [], typeEnvBindings: [], skippedLanguages: {}, fileCount: 0,
1075
1260
  };
1076
1261
  let cumulativeProcessed = 0;
1077
1262
  const mergeResult = (target, src) => {
@@ -1083,7 +1268,12 @@ const mergeResult = (target, src) => {
1083
1268
  target.assignments.push(...src.assignments);
1084
1269
  target.heritage.push(...src.heritage);
1085
1270
  target.routes.push(...src.routes);
1271
+ target.fetchCalls.push(...src.fetchCalls);
1272
+ target.decoratorRoutes.push(...src.decoratorRoutes);
1273
+ target.toolDefs.push(...src.toolDefs);
1274
+ target.ormQueries.push(...src.ormQueries);
1086
1275
  target.constructorBindings.push(...src.constructorBindings);
1276
+ target.typeEnvBindings.push(...src.typeEnvBindings);
1087
1277
  for (const [lang, count] of Object.entries(src.skippedLanguages)) {
1088
1278
  target.skippedLanguages[lang] = (target.skippedLanguages[lang] || 0) + count;
1089
1279
  }
@@ -1106,7 +1296,7 @@ parentPort.on('message', (msg) => {
1106
1296
  if (msg && msg.type === 'flush') {
1107
1297
  parentPort.postMessage({ type: 'result', data: accumulated });
1108
1298
  // Reset for potential reuse
1109
- accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], assignments: [], heritage: [], routes: [], constructorBindings: [], skippedLanguages: {}, fileCount: 0 };
1299
+ accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], assignments: [], heritage: [], routes: [], fetchCalls: [], decoratorRoutes: [], toolDefs: [], ormQueries: [], constructorBindings: [], typeEnvBindings: [], skippedLanguages: {}, fileCount: 0 };
1110
1300
  cumulativeProcessed = 0;
1111
1301
  return;
1112
1302
  }