gitnexus 1.4.8 → 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 (211) hide show
  1. package/README.md +7 -0
  2. package/dist/cli/index-repo.d.ts +15 -0
  3. package/dist/cli/index-repo.js +115 -0
  4. package/dist/cli/index.js +11 -2
  5. package/dist/cli/setup.js +12 -9
  6. package/dist/cli/wiki.d.ts +4 -0
  7. package/dist/cli/wiki.js +174 -53
  8. package/dist/config/supported-languages.d.ts +7 -5
  9. package/dist/config/supported-languages.js +6 -4
  10. package/dist/core/graph/graph.js +9 -1
  11. package/dist/core/graph/types.d.ts +10 -2
  12. package/dist/core/ingestion/call-processor.d.ts +18 -1
  13. package/dist/core/ingestion/call-processor.js +297 -38
  14. package/dist/core/ingestion/call-routing.d.ts +3 -18
  15. package/dist/core/ingestion/call-routing.js +0 -19
  16. package/dist/core/ingestion/cobol/cobol-copy-expander.d.ts +57 -0
  17. package/dist/core/ingestion/cobol/cobol-copy-expander.js +385 -0
  18. package/dist/core/ingestion/cobol/cobol-preprocessor.d.ts +210 -0
  19. package/dist/core/ingestion/cobol/cobol-preprocessor.js +1509 -0
  20. package/dist/core/ingestion/cobol/jcl-parser.d.ts +68 -0
  21. package/dist/core/ingestion/cobol/jcl-parser.js +217 -0
  22. package/dist/core/ingestion/cobol/jcl-processor.d.ts +33 -0
  23. package/dist/core/ingestion/cobol/jcl-processor.js +229 -0
  24. package/dist/core/ingestion/cobol-processor.d.ts +54 -0
  25. package/dist/core/ingestion/cobol-processor.js +1186 -0
  26. package/dist/core/ingestion/entry-point-scoring.d.ts +17 -0
  27. package/dist/core/ingestion/entry-point-scoring.js +18 -4
  28. package/dist/core/ingestion/export-detection.d.ts +47 -8
  29. package/dist/core/ingestion/export-detection.js +29 -50
  30. package/dist/core/ingestion/field-extractor.d.ts +29 -0
  31. package/dist/core/ingestion/field-extractor.js +25 -0
  32. package/dist/core/ingestion/field-extractors/configs/c-cpp.d.ts +3 -0
  33. package/dist/core/ingestion/field-extractors/configs/c-cpp.js +108 -0
  34. package/dist/core/ingestion/field-extractors/configs/csharp.d.ts +8 -0
  35. package/dist/core/ingestion/field-extractors/configs/csharp.js +73 -0
  36. package/dist/core/ingestion/field-extractors/configs/dart.d.ts +8 -0
  37. package/dist/core/ingestion/field-extractors/configs/dart.js +76 -0
  38. package/dist/core/ingestion/field-extractors/configs/go.d.ts +11 -0
  39. package/dist/core/ingestion/field-extractors/configs/go.js +64 -0
  40. package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +44 -0
  41. package/dist/core/ingestion/field-extractors/configs/helpers.js +134 -0
  42. package/dist/core/ingestion/field-extractors/configs/jvm.d.ts +3 -0
  43. package/dist/core/ingestion/field-extractors/configs/jvm.js +118 -0
  44. package/dist/core/ingestion/field-extractors/configs/php.d.ts +8 -0
  45. package/dist/core/ingestion/field-extractors/configs/php.js +67 -0
  46. package/dist/core/ingestion/field-extractors/configs/python.d.ts +12 -0
  47. package/dist/core/ingestion/field-extractors/configs/python.js +91 -0
  48. package/dist/core/ingestion/field-extractors/configs/ruby.d.ts +16 -0
  49. package/dist/core/ingestion/field-extractors/configs/ruby.js +75 -0
  50. package/dist/core/ingestion/field-extractors/configs/rust.d.ts +9 -0
  51. package/dist/core/ingestion/field-extractors/configs/rust.js +55 -0
  52. package/dist/core/ingestion/field-extractors/configs/swift.d.ts +8 -0
  53. package/dist/core/ingestion/field-extractors/configs/swift.js +63 -0
  54. package/dist/core/ingestion/field-extractors/configs/typescript-javascript.d.ts +3 -0
  55. package/dist/core/ingestion/field-extractors/configs/typescript-javascript.js +60 -0
  56. package/dist/core/ingestion/field-extractors/generic.d.ts +46 -0
  57. package/dist/core/ingestion/field-extractors/generic.js +111 -0
  58. package/dist/core/ingestion/field-extractors/typescript.d.ts +77 -0
  59. package/dist/core/ingestion/field-extractors/typescript.js +291 -0
  60. package/dist/core/ingestion/field-types.d.ts +59 -0
  61. package/dist/core/ingestion/field-types.js +2 -0
  62. package/dist/core/ingestion/framework-detection.d.ts +87 -0
  63. package/dist/core/ingestion/framework-detection.js +65 -2
  64. package/dist/core/ingestion/heritage-processor.js +15 -17
  65. package/dist/core/ingestion/import-processor.d.ts +9 -10
  66. package/dist/core/ingestion/import-processor.js +59 -14
  67. package/dist/core/ingestion/{resolvers → import-resolvers}/csharp.d.ts +6 -9
  68. package/dist/core/ingestion/{resolvers → import-resolvers}/csharp.js +20 -2
  69. package/dist/core/ingestion/import-resolvers/dart.d.ts +7 -0
  70. package/dist/core/ingestion/import-resolvers/dart.js +44 -0
  71. package/dist/core/ingestion/{resolvers → import-resolvers}/go.d.ts +4 -5
  72. package/dist/core/ingestion/{resolvers → import-resolvers}/go.js +17 -0
  73. package/dist/core/ingestion/{resolvers → import-resolvers}/jvm.d.ts +9 -1
  74. package/dist/core/ingestion/{resolvers → import-resolvers}/jvm.js +56 -0
  75. package/dist/core/ingestion/{resolvers → import-resolvers}/php.d.ts +6 -10
  76. package/dist/core/ingestion/{resolvers → import-resolvers}/php.js +7 -2
  77. package/dist/core/ingestion/{resolvers → import-resolvers}/python.d.ts +9 -3
  78. package/dist/core/ingestion/{resolvers → import-resolvers}/python.js +35 -3
  79. package/dist/core/ingestion/{resolvers → import-resolvers}/ruby.d.ts +5 -2
  80. package/dist/core/ingestion/{resolvers → import-resolvers}/ruby.js +7 -2
  81. package/dist/core/ingestion/{resolvers → import-resolvers}/rust.d.ts +5 -2
  82. package/dist/core/ingestion/{resolvers → import-resolvers}/rust.js +41 -2
  83. package/dist/core/ingestion/{resolvers → import-resolvers}/standard.d.ts +15 -7
  84. package/dist/core/ingestion/{resolvers → import-resolvers}/standard.js +22 -3
  85. package/dist/core/ingestion/import-resolvers/swift.d.ts +7 -0
  86. package/dist/core/ingestion/import-resolvers/swift.js +23 -0
  87. package/dist/core/ingestion/import-resolvers/types.d.ts +44 -0
  88. package/dist/core/ingestion/import-resolvers/types.js +6 -0
  89. package/dist/core/ingestion/{resolvers → import-resolvers}/utils.d.ts +0 -3
  90. package/dist/core/ingestion/{resolvers → import-resolvers}/utils.js +0 -9
  91. package/dist/core/ingestion/language-config.d.ts +4 -1
  92. package/dist/core/ingestion/language-provider.d.ts +121 -0
  93. package/dist/core/ingestion/language-provider.js +24 -0
  94. package/dist/core/ingestion/languages/c-cpp.d.ts +12 -0
  95. package/dist/core/ingestion/languages/c-cpp.js +71 -0
  96. package/dist/core/ingestion/languages/cobol.d.ts +1 -0
  97. package/dist/core/ingestion/languages/cobol.js +26 -0
  98. package/dist/core/ingestion/languages/csharp.d.ts +8 -0
  99. package/dist/core/ingestion/languages/csharp.js +49 -0
  100. package/dist/core/ingestion/languages/dart.d.ts +12 -0
  101. package/dist/core/ingestion/languages/dart.js +58 -0
  102. package/dist/core/ingestion/languages/go.d.ts +11 -0
  103. package/dist/core/ingestion/languages/go.js +28 -0
  104. package/dist/core/ingestion/languages/index.d.ts +38 -0
  105. package/dist/core/ingestion/languages/index.js +63 -0
  106. package/dist/core/ingestion/languages/java.d.ts +9 -0
  107. package/dist/core/ingestion/languages/java.js +29 -0
  108. package/dist/core/ingestion/languages/kotlin.d.ts +9 -0
  109. package/dist/core/ingestion/languages/kotlin.js +53 -0
  110. package/dist/core/ingestion/languages/php.d.ts +8 -0
  111. package/dist/core/ingestion/languages/php.js +145 -0
  112. package/dist/core/ingestion/languages/python.d.ts +12 -0
  113. package/dist/core/ingestion/languages/python.js +39 -0
  114. package/dist/core/ingestion/languages/ruby.d.ts +9 -0
  115. package/dist/core/ingestion/languages/ruby.js +44 -0
  116. package/dist/core/ingestion/languages/rust.d.ts +12 -0
  117. package/dist/core/ingestion/languages/rust.js +44 -0
  118. package/dist/core/ingestion/languages/swift.d.ts +12 -0
  119. package/dist/core/ingestion/languages/swift.js +133 -0
  120. package/dist/core/ingestion/languages/typescript.d.ts +10 -0
  121. package/dist/core/ingestion/languages/typescript.js +60 -0
  122. package/dist/core/ingestion/mro-processor.js +14 -15
  123. package/dist/core/ingestion/{named-binding-extraction.d.ts → named-binding-processor.d.ts} +0 -9
  124. package/dist/core/ingestion/named-binding-processor.js +42 -0
  125. package/dist/core/ingestion/named-bindings/csharp.d.ts +3 -0
  126. package/dist/core/ingestion/named-bindings/csharp.js +37 -0
  127. package/dist/core/ingestion/named-bindings/java.d.ts +3 -0
  128. package/dist/core/ingestion/named-bindings/java.js +29 -0
  129. package/dist/core/ingestion/named-bindings/kotlin.d.ts +3 -0
  130. package/dist/core/ingestion/named-bindings/kotlin.js +36 -0
  131. package/dist/core/ingestion/named-bindings/php.d.ts +3 -0
  132. package/dist/core/ingestion/named-bindings/php.js +61 -0
  133. package/dist/core/ingestion/named-bindings/python.d.ts +3 -0
  134. package/dist/core/ingestion/named-bindings/python.js +49 -0
  135. package/dist/core/ingestion/named-bindings/rust.d.ts +3 -0
  136. package/dist/core/ingestion/named-bindings/rust.js +64 -0
  137. package/dist/core/ingestion/named-bindings/types.d.ts +16 -0
  138. package/dist/core/ingestion/named-bindings/types.js +6 -0
  139. package/dist/core/ingestion/named-bindings/typescript.d.ts +3 -0
  140. package/dist/core/ingestion/named-bindings/typescript.js +58 -0
  141. package/dist/core/ingestion/parsing-processor.d.ts +5 -1
  142. package/dist/core/ingestion/parsing-processor.js +115 -16
  143. package/dist/core/ingestion/pipeline.js +925 -424
  144. package/dist/core/ingestion/resolution-context.js +1 -1
  145. package/dist/core/ingestion/route-extractors/expo.d.ts +1 -0
  146. package/dist/core/ingestion/route-extractors/expo.js +36 -0
  147. package/dist/core/ingestion/route-extractors/middleware.d.ts +47 -0
  148. package/dist/core/ingestion/route-extractors/middleware.js +143 -0
  149. package/dist/core/ingestion/route-extractors/nextjs.d.ts +3 -0
  150. package/dist/core/ingestion/route-extractors/nextjs.js +76 -0
  151. package/dist/core/ingestion/route-extractors/php.d.ts +7 -0
  152. package/dist/core/ingestion/route-extractors/php.js +21 -0
  153. package/dist/core/ingestion/route-extractors/response-shapes.d.ts +20 -0
  154. package/dist/core/ingestion/route-extractors/response-shapes.js +290 -0
  155. package/dist/core/ingestion/tree-sitter-queries.d.ts +8 -7
  156. package/dist/core/ingestion/tree-sitter-queries.js +231 -9
  157. package/dist/core/ingestion/type-env.d.ts +14 -17
  158. package/dist/core/ingestion/type-env.js +66 -14
  159. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +1 -1
  160. package/dist/core/ingestion/type-extractors/csharp.js +1 -1
  161. package/dist/core/ingestion/type-extractors/dart.d.ts +15 -0
  162. package/dist/core/ingestion/type-extractors/dart.js +371 -0
  163. package/dist/core/ingestion/type-extractors/jvm.js +1 -1
  164. package/dist/core/ingestion/type-extractors/shared.d.ts +1 -13
  165. package/dist/core/ingestion/type-extractors/shared.js +9 -102
  166. package/dist/core/ingestion/type-extractors/swift.js +334 -4
  167. package/dist/core/ingestion/type-extractors/types.d.ts +3 -1
  168. package/dist/core/ingestion/{ast-helpers.d.ts → utils/ast-helpers.d.ts} +16 -13
  169. package/dist/core/ingestion/{ast-helpers.js → utils/ast-helpers.js} +111 -32
  170. package/dist/core/ingestion/{call-analysis.js → utils/call-analysis.js} +37 -0
  171. package/dist/core/ingestion/utils/event-loop.d.ts +5 -0
  172. package/dist/core/ingestion/utils/event-loop.js +5 -0
  173. package/dist/core/ingestion/utils/language-detection.d.ts +9 -0
  174. package/dist/core/ingestion/utils/language-detection.js +70 -0
  175. package/dist/core/ingestion/utils/verbose.d.ts +1 -0
  176. package/dist/core/ingestion/utils/verbose.js +7 -0
  177. package/dist/core/ingestion/workers/parse-worker.d.ts +43 -2
  178. package/dist/core/ingestion/workers/parse-worker.js +361 -150
  179. package/dist/core/lbug/csv-generator.js +34 -1
  180. package/dist/core/lbug/lbug-adapter.js +6 -0
  181. package/dist/core/lbug/schema.d.ts +5 -3
  182. package/dist/core/lbug/schema.js +39 -2
  183. package/dist/core/tree-sitter/parser-loader.js +7 -1
  184. package/dist/core/wiki/cursor-client.d.ts +31 -0
  185. package/dist/core/wiki/cursor-client.js +127 -0
  186. package/dist/core/wiki/generator.d.ts +28 -9
  187. package/dist/core/wiki/generator.js +115 -18
  188. package/dist/core/wiki/graph-queries.d.ts +4 -0
  189. package/dist/core/wiki/graph-queries.js +7 -1
  190. package/dist/core/wiki/llm-client.d.ts +2 -0
  191. package/dist/core/wiki/llm-client.js +8 -4
  192. package/dist/core/wiki/prompts.d.ts +3 -3
  193. package/dist/core/wiki/prompts.js +6 -0
  194. package/dist/mcp/core/lbug-adapter.d.ts +5 -0
  195. package/dist/mcp/core/lbug-adapter.js +11 -1
  196. package/dist/mcp/local/local-backend.d.ts +16 -5
  197. package/dist/mcp/local/local-backend.js +711 -74
  198. package/dist/mcp/tools.js +71 -2
  199. package/dist/storage/repo-manager.d.ts +3 -0
  200. package/package.json +17 -16
  201. package/dist/core/ingestion/import-resolution.d.ts +0 -101
  202. package/dist/core/ingestion/import-resolution.js +0 -251
  203. package/dist/core/ingestion/named-binding-extraction.js +0 -373
  204. package/dist/core/ingestion/resolvers/index.d.ts +0 -18
  205. package/dist/core/ingestion/resolvers/index.js +0 -13
  206. package/dist/core/ingestion/type-extractors/index.d.ts +0 -22
  207. package/dist/core/ingestion/type-extractors/index.js +0 -31
  208. package/dist/core/ingestion/utils.d.ts +0 -20
  209. package/dist/core/ingestion/utils.js +0 -242
  210. package/scripts/patch-tree-sitter-swift.cjs +0 -74
  211. /package/dist/core/ingestion/{call-analysis.d.ts → utils/call-analysis.d.ts} +0 -0
@@ -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,21 +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, getLabelFromCaptures, 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 { namedBindingExtractors, preprocessImportPath } from '../import-resolution.js';
38
- import { callRouters } from '../call-routing.js';
39
- import { extractPropertyDeclaredType } from '../type-extractors/shared.js';
43
+ import { preprocessImportPath } from '../import-processor.js';
44
+ import { CLASS_CONTAINER_TYPES } from '../utils/ast-helpers.js';
40
45
  // ============================================================================
41
46
  // Worker-local parser + language map
42
47
  // ============================================================================
@@ -55,6 +60,7 @@ const languageMap = {
55
60
  ...(Kotlin ? { [SupportedLanguages.Kotlin]: Kotlin } : {}),
56
61
  [SupportedLanguages.PHP]: PHP.php_only,
57
62
  [SupportedLanguages.Ruby]: Ruby,
63
+ ...(Dart ? { [SupportedLanguages.Dart]: Dart } : {}),
58
64
  ...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}),
59
65
  };
60
66
  /**
@@ -78,24 +84,130 @@ const setLanguage = (language, filePath) => {
78
84
  throw new Error(`Unsupported language: ${language}`);
79
85
  parser.setLanguage(lang);
80
86
  };
81
- // isNodeExported imported from ../export-detection.js (shared module)
82
87
  // ============================================================================
83
- // 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(); };
84
97
  // ============================================================================
85
- /** Walk up AST to find enclosing function, return its generateId or null for top-level */
86
- const findEnclosingFunctionId = (node, filePath) => {
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).
100
+ // ============================================================================
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;
87
154
  let current = node.parent;
88
155
  while (current) {
89
156
  if (FUNCTION_NODE_TYPES.has(current.type)) {
90
157
  const { funcName, label } = extractFunctionName(current);
91
158
  if (funcName) {
92
- 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;
93
186
  }
94
187
  }
95
188
  current = current.parent;
96
189
  }
190
+ functionIdCache.set(node, null);
97
191
  return null;
98
192
  };
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;
210
+ };
99
211
  // Label detection moved to shared getLabelFromCaptures in utils.ts
100
212
  // DEFINITION_CAPTURE_KEYS and getDefinitionNodeFromCaptures imported from ../utils.js
101
213
  // ============================================================================
@@ -111,6 +223,10 @@ const processBatch = (files, onProgress) => {
111
223
  assignments: [],
112
224
  heritage: [],
113
225
  routes: [],
226
+ fetchCalls: [],
227
+ decoratorRoutes: [],
228
+ toolDefs: [],
229
+ ormQueries: [],
114
230
  constructorBindings: [],
115
231
  typeEnvBindings: [],
116
232
  skippedLanguages: {},
@@ -140,7 +256,8 @@ const processBatch = (files, onProgress) => {
140
256
  }
141
257
  } : undefined;
142
258
  for (const [language, langFiles] of byLanguage) {
143
- const queryString = LANGUAGE_QUERIES[language];
259
+ const provider = getProvider(language);
260
+ const queryString = provider.treeSitterQueries;
144
261
  if (!queryString)
145
262
  continue;
146
263
  // Track if we need to handle tsx separately
@@ -192,114 +309,23 @@ const processBatch = (files, onProgress) => {
192
309
  }
193
310
  return result;
194
311
  };
195
- // ============================================================================
196
- // PHP Eloquent metadata extraction
197
- // ============================================================================
198
- /** Eloquent model properties whose array values are worth indexing */
199
- const ELOQUENT_ARRAY_PROPS = new Set(['fillable', 'casts', 'hidden', 'guarded', 'with', 'appends']);
200
- /** Eloquent relationship method names */
201
- const ELOQUENT_RELATIONS = new Set([
202
- 'hasMany', 'hasOne', 'belongsTo', 'belongsToMany',
203
- 'morphTo', 'morphMany', 'morphOne', 'morphToMany', 'morphedByMany',
204
- 'hasManyThrough', 'hasOneThrough',
205
- ]);
206
- function findDescendant(node, type) {
207
- if (node.type === type)
208
- return node;
209
- for (const child of (node.children ?? [])) {
210
- const found = findDescendant(child, type);
211
- if (found)
212
- return found;
213
- }
214
- return null;
215
- }
216
- function extractStringContent(node) {
217
- if (!node)
218
- return null;
219
- const content = node.children?.find((c) => c.type === 'string_content');
220
- if (content)
221
- return content.text;
222
- if (node.type === 'string_content')
223
- return node.text;
224
- return null;
225
- }
226
- /**
227
- * For a PHP property_declaration node, extract array values as a description string.
228
- * Returns null if not an Eloquent model property or no array values found.
229
- */
230
- function extractPhpPropertyDescription(propName, propDeclNode) {
231
- if (!ELOQUENT_ARRAY_PROPS.has(propName))
232
- return null;
233
- const arrayNode = findDescendant(propDeclNode, 'array_creation_expression');
234
- if (!arrayNode)
235
- return null;
236
- const items = [];
237
- for (const child of (arrayNode.children ?? [])) {
238
- if (child.type !== 'array_element_initializer')
239
- continue;
240
- const children = child.children ?? [];
241
- const arrowIdx = children.findIndex((c) => c.type === '=>');
242
- if (arrowIdx !== -1) {
243
- // key => value pair (used in $casts)
244
- const key = extractStringContent(children[arrowIdx - 1]);
245
- const val = extractStringContent(children[arrowIdx + 1]);
246
- if (key && val)
247
- items.push(`${key}:${val}`);
248
- }
249
- else {
250
- // Simple value (used in $fillable, $hidden, etc.)
251
- const val = extractStringContent(children[0]);
252
- if (val)
253
- items.push(val);
254
- }
255
- }
256
- return items.length > 0 ? items.join(', ') : null;
257
- }
258
- /**
259
- * For a PHP method_declaration node, detect if it defines an Eloquent relationship.
260
- * Returns description like "hasMany(Post)" or null.
261
- */
262
- function extractEloquentRelationDescription(methodNode) {
263
- function findRelationCall(node) {
264
- if (node.type === 'member_call_expression') {
265
- const children = node.children ?? [];
266
- const objectNode = children.find((c) => c.type === 'variable_name' && c.text === '$this');
267
- const nameNode = children.find((c) => c.type === 'name');
268
- if (objectNode && nameNode && ELOQUENT_RELATIONS.has(nameNode.text))
269
- return node;
270
- }
271
- for (const child of (node.children ?? [])) {
272
- const found = findRelationCall(child);
273
- if (found)
274
- return found;
275
- }
276
- return null;
277
- }
278
- const callNode = findRelationCall(methodNode);
279
- if (!callNode)
280
- return null;
281
- const relType = callNode.children?.find((c) => c.type === 'name')?.text;
282
- const argsNode = callNode.children?.find((c) => c.type === 'arguments');
283
- let targetModel = null;
284
- if (argsNode) {
285
- const firstArg = argsNode.children?.find((c) => c.type === 'argument');
286
- if (firstArg) {
287
- const classConstant = firstArg.children?.find((c) => c.type === 'class_constant_access_expression');
288
- if (classConstant) {
289
- targetModel = classConstant.children?.find((c) => c.type === 'name')?.text ?? null;
290
- }
291
- }
292
- }
293
- if (relType && targetModel)
294
- return `${relType}(${targetModel})`;
295
- if (relType)
296
- return relType;
297
- return null;
298
- }
299
312
  const ROUTE_HTTP_METHODS = new Set([
300
313
  'get', 'post', 'put', 'patch', 'delete', 'options', 'any', 'match',
301
314
  ]);
302
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
+ ]);
303
329
  const RESOURCE_ACTIONS = ['index', 'create', 'store', 'show', 'edit', 'update', 'destroy'];
304
330
  const API_RESOURCE_ACTIONS = ['index', 'store', 'show', 'update', 'destroy'];
305
331
  /** Check if node is a scoped_call_expression with object 'Route' */
@@ -643,6 +669,50 @@ function extractLaravelRoutes(tree, filePath) {
643
669
  walk(tree.rootNode, []);
644
670
  return routes;
645
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
+ }
646
716
  const processFileGroup = (files, language, queryString, result, onFileProcessed) => {
647
717
  let query;
648
718
  try {
@@ -663,6 +733,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
663
733
  // Skip files larger than the max tree-sitter buffer (32 MB)
664
734
  if (file.content.length > TREE_SITTER_MAX_BUFFER)
665
735
  continue;
736
+ clearCaches(); // Reset memoization before each new file
666
737
  let tree;
667
738
  try {
668
739
  tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
@@ -711,21 +782,24 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
711
782
  // Build per-file type environment + constructor bindings in a single AST walk.
712
783
  // Constructor bindings are verified against the SymbolTable in processCallsFromExtracted.
713
784
  const parentMap = fileParentMap;
714
- const typeEnv = buildTypeEnv(tree, language, { parentMap });
715
- const callRouter = callRouters[language];
785
+ const provider = getProvider(language);
786
+ const typeEnv = buildTypeEnv(tree, language, { parentMap, enclosingFunctionFinder: provider?.enclosingFunctionFinder });
787
+ const callRouter = provider.callRouter;
716
788
  if (typeEnv.constructorBindings.length > 0) {
717
789
  result.constructorBindings.push({ filePath: file.path, bindings: [...typeEnv.constructorBindings] });
718
790
  }
719
791
  // Extract file-scope bindings for ExportedTypeMap (closes worker/sequential quality gap).
720
792
  // Sequential path uses collectExportedBindings(typeEnv) directly; worker path serializes
721
793
  // these bindings so the main thread can merge them into ExportedTypeMap.
722
- const fileScope = typeEnv.env.get('');
723
- if (fileScope && fileScope.size > 0) {
794
+ const fileScope = typeEnv.fileScope();
795
+ if (fileScope.size > 0) {
724
796
  const bindings = [];
725
797
  for (const [name, type] of fileScope)
726
798
  bindings.push([name, type]);
727
799
  result.typeEnvBindings.push({ filePath: file.path, bindings });
728
800
  }
801
+ // Per-file map: decorator end-line → decorator info, for associating with definitions
802
+ const fileDecorators = new Map();
729
803
  for (const match of matches) {
730
804
  const captureMap = {};
731
805
  for (const c of match.captures) {
@@ -733,10 +807,10 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
733
807
  }
734
808
  // Extract import paths before skipping
735
809
  if (captureMap['import'] && captureMap['import.source']) {
736
- const rawImportPath = preprocessImportPath(captureMap['import.source'].text, captureMap['import'], language);
810
+ const rawImportPath = preprocessImportPath(captureMap['import.source'].text, captureMap['import'], provider);
737
811
  if (!rawImportPath)
738
812
  continue;
739
- const extractor = namedBindingExtractors[language];
813
+ const extractor = provider.namedBindingExtractor;
740
814
  const namedBindings = extractor ? extractor(captureMap['import']) : undefined;
741
815
  result.imports.push({
742
816
  filePath: file.path,
@@ -751,7 +825,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
751
825
  const receiverText = captureMap['assignment.receiver'].text;
752
826
  const propertyName = captureMap['assignment.property'].text;
753
827
  if (receiverText && propertyName) {
754
- const srcId = findEnclosingFunctionId(captureMap['assignment'], file.path)
828
+ const srcId = findEnclosingFunctionId(captureMap['assignment'], file.path, provider)
755
829
  || generateId('File', file.path);
756
830
  let receiverTypeName;
757
831
  if (typeEnv) {
@@ -768,13 +842,82 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
768
842
  if (!captureMap['call'])
769
843
  continue;
770
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
+ }
771
914
  // Extract call sites
772
915
  if (captureMap['call']) {
773
916
  const callNameNode = captureMap['call.name'];
774
917
  if (callNameNode) {
775
918
  const calledName = callNameNode.text;
776
919
  // Dispatch: route language-specific calls (heritage, properties, imports)
777
- const routed = callRouter(calledName, captureMap['call']);
920
+ const routed = callRouter?.(calledName, captureMap['call']);
778
921
  if (routed) {
779
922
  if (routed.kind === 'skip')
780
923
  continue;
@@ -798,8 +941,19 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
798
941
  continue;
799
942
  }
800
943
  if (routed.kind === 'properties') {
801
- 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
+ }
802
955
  for (const item of routed.items) {
956
+ const routedFieldInfo = routedFieldMap?.get(item.propName);
803
957
  const nodeId = generateId('Property', `${file.path}:${item.propName}`);
804
958
  result.nodes.push({
805
959
  id: nodeId,
@@ -812,6 +966,10 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
812
966
  language,
813
967
  isExported: true,
814
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 } : {}),
815
973
  },
816
974
  });
817
975
  result.symbols.push({
@@ -820,7 +978,10 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
820
978
  nodeId,
821
979
  type: 'Property',
822
980
  ...(propEnclosingClassId ? { ownerId: propEnclosingClassId } : {}),
823
- ...(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 } : {}),
824
985
  });
825
986
  const fileId = generateId('File', file.path);
826
987
  const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
@@ -847,9 +1008,9 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
847
1008
  }
848
1009
  // kind === 'call' — fall through to normal call processing below
849
1010
  }
850
- if (!isBuiltInOrNoise(calledName)) {
1011
+ if (!provider.isBuiltInName(calledName)) {
851
1012
  const callNode = captureMap['call'];
852
- const sourceId = findEnclosingFunctionId(callNode, file.path)
1013
+ const sourceId = findEnclosingFunctionId(callNode, file.path, provider)
853
1014
  || generateId('File', file.path);
854
1015
  const callForm = inferCallForm(callNode, callNameNode);
855
1016
  let receiverName = callForm === 'member' ? extractReceiverName(callNameNode) : undefined;
@@ -926,7 +1087,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
926
1087
  continue;
927
1088
  }
928
1089
  }
929
- const nodeLabel = getLabelFromCaptures(captureMap, language);
1090
+ const nodeLabel = getLabelFromCaptures(captureMap, provider);
930
1091
  if (!nodeLabel)
931
1092
  continue;
932
1093
  const nameNode = captureMap['name'];
@@ -937,23 +1098,47 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
937
1098
  const definitionNode = getDefinitionNodeFromCaptures(captureMap);
938
1099
  const startLine = definitionNode ? definitionNode.startPosition.row : (nameNode ? nameNode.startPosition.row : 0);
939
1100
  const nodeId = generateId(nodeLabel, `${file.path}:${nodeName}`);
940
- let description;
941
- if (language === SupportedLanguages.PHP) {
942
- if (nodeLabel === 'Property' && captureMap['definition.property']) {
943
- description = extractPhpPropertyDescription(nodeName, captureMap['definition.property']) ?? undefined;
944
- }
945
- else if (nodeLabel === 'Method' && captureMap['definition.method']) {
946
- description = extractEloquentRelationDescription(captureMap['definition.method']) ?? undefined;
947
- }
948
- }
949
- const frameworkHint = definitionNode
1101
+ const description = provider.descriptionExtractor?.(nodeLabel, nodeName, captureMap);
1102
+ let frameworkHint = definitionNode
950
1103
  ? detectFrameworkFromAST(language, (definitionNode.text || '').slice(0, 300))
951
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
+ }
952
1134
  let parameterCount;
953
1135
  let requiredParameterCount;
954
1136
  let parameterTypes;
955
1137
  let returnType;
956
1138
  let declaredType;
1139
+ let visibility;
1140
+ let isStatic;
1141
+ let isReadonly;
957
1142
  if (nodeLabel === 'Function' || nodeLabel === 'Method' || nodeLabel === 'Constructor') {
958
1143
  const sig = extractMethodSignature(definitionNode);
959
1144
  parameterCount = sig.parameterCount;
@@ -963,7 +1148,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
963
1148
  // Language-specific return type fallback (e.g. Ruby YARD @return [Type])
964
1149
  // Also upgrades uninformative AST types like PHP `array` with PHPDoc `@return User[]`
965
1150
  if ((!returnType || returnType === 'array' || returnType === 'iterable') && definitionNode) {
966
- const tc = typeConfigs[language];
1151
+ const tc = provider.typeConfig;
967
1152
  if (tc?.extractReturnType) {
968
1153
  const docReturn = tc.extractReturnType(definitionNode);
969
1154
  if (docReturn)
@@ -972,9 +1157,22 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
972
1157
  }
973
1158
  }
974
1159
  else if (nodeLabel === 'Property' && definitionNode) {
975
- // Extract the declared type for property/field nodes.
976
- // Walk the definition node for type annotation children.
977
- 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
+ }
978
1176
  }
979
1177
  result.nodes.push({
980
1178
  id: nodeId,
@@ -985,7 +1183,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
985
1183
  startLine: definitionNode ? definitionNode.startPosition.row : startLine,
986
1184
  endLine: definitionNode ? definitionNode.endPosition.row : startLine,
987
1185
  language: language,
988
- isExported: isNodeExported(nameNode || definitionNode, nodeName, language),
1186
+ isExported: cachedExportCheck(provider.exportChecker, nameNode || definitionNode, nodeName),
989
1187
  ...(frameworkHint ? {
990
1188
  astFrameworkMultiplier: frameworkHint.entryPointMultiplier,
991
1189
  astFrameworkReason: frameworkHint.reason,
@@ -995,12 +1193,16 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
995
1193
  ...(requiredParameterCount !== undefined ? { requiredParameterCount } : {}),
996
1194
  ...(parameterTypes !== undefined ? { parameterTypes } : {}),
997
1195
  ...(returnType !== undefined ? { returnType } : {}),
1196
+ ...(declaredType !== undefined ? { declaredType } : {}),
1197
+ ...(visibility !== undefined ? { visibility } : {}),
1198
+ ...(isStatic !== undefined ? { isStatic } : {}),
1199
+ ...(isReadonly !== undefined ? { isReadonly } : {}),
998
1200
  },
999
1201
  });
1000
1202
  // Compute enclosing class for Method/Constructor/Property/Function — used for both ownerId and HAS_METHOD
1001
1203
  // Function is included because Kotlin/Rust/Python capture class methods as Function nodes
1002
1204
  const needsOwner = nodeLabel === 'Method' || nodeLabel === 'Constructor' || nodeLabel === 'Property' || nodeLabel === 'Function';
1003
- const enclosingClassId = needsOwner ? findEnclosingClassId(nameNode || definitionNode, file.path) : null;
1205
+ const enclosingClassId = needsOwner ? cachedFindEnclosingClassId(nameNode || definitionNode, file.path) : null;
1004
1206
  result.symbols.push({
1005
1207
  filePath: file.path,
1006
1208
  name: nodeName,
@@ -1012,6 +1214,9 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1012
1214
  ...(returnType !== undefined ? { returnType } : {}),
1013
1215
  ...(declaredType !== undefined ? { declaredType } : {}),
1014
1216
  ...(enclosingClassId ? { ownerId: enclosingClassId } : {}),
1217
+ ...(visibility !== undefined ? { visibility } : {}),
1218
+ ...(isStatic !== undefined ? { isStatic } : {}),
1219
+ ...(isReadonly !== undefined ? { isReadonly } : {}),
1015
1220
  });
1016
1221
  const fileId = generateId('File', file.path);
1017
1222
  const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
@@ -1036,11 +1241,13 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1036
1241
  });
1037
1242
  }
1038
1243
  }
1039
- // Extract Laravel routes from route files via procedural AST walk
1040
- 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)) {
1041
1246
  const extractedRoutes = extractLaravelRoutes(tree, file.path);
1042
1247
  result.routes.push(...extractedRoutes);
1043
1248
  }
1249
+ // Extract ORM queries (Prisma, Supabase)
1250
+ extractORMQueries(file.path, file.content, result.ormQueries);
1044
1251
  }
1045
1252
  };
1046
1253
  // ============================================================================
@@ -1049,7 +1256,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
1049
1256
  /** Accumulated result across sub-batches */
1050
1257
  let accumulated = {
1051
1258
  nodes: [], relationships: [], symbols: [],
1052
- imports: [], calls: [], assignments: [], heritage: [], routes: [], constructorBindings: [], typeEnvBindings: [], skippedLanguages: {}, fileCount: 0,
1259
+ imports: [], calls: [], assignments: [], heritage: [], routes: [], fetchCalls: [], decoratorRoutes: [], toolDefs: [], ormQueries: [], constructorBindings: [], typeEnvBindings: [], skippedLanguages: {}, fileCount: 0,
1053
1260
  };
1054
1261
  let cumulativeProcessed = 0;
1055
1262
  const mergeResult = (target, src) => {
@@ -1061,6 +1268,10 @@ const mergeResult = (target, src) => {
1061
1268
  target.assignments.push(...src.assignments);
1062
1269
  target.heritage.push(...src.heritage);
1063
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);
1064
1275
  target.constructorBindings.push(...src.constructorBindings);
1065
1276
  target.typeEnvBindings.push(...src.typeEnvBindings);
1066
1277
  for (const [lang, count] of Object.entries(src.skippedLanguages)) {
@@ -1085,7 +1296,7 @@ parentPort.on('message', (msg) => {
1085
1296
  if (msg && msg.type === 'flush') {
1086
1297
  parentPort.postMessage({ type: 'result', data: accumulated });
1087
1298
  // Reset for potential reuse
1088
- accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], assignments: [], heritage: [], routes: [], constructorBindings: [], typeEnvBindings: [], skippedLanguages: {}, fileCount: 0 };
1299
+ accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], assignments: [], heritage: [], routes: [], fetchCalls: [], decoratorRoutes: [], toolDefs: [], ormQueries: [], constructorBindings: [], typeEnvBindings: [], skippedLanguages: {}, fileCount: 0 };
1089
1300
  cumulativeProcessed = 0;
1090
1301
  return;
1091
1302
  }