gitnexus 1.4.8 → 1.4.10

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 +14 -14
  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
@@ -1,4 +1,4 @@
1
- export type NodeLabel = 'Project' | 'Package' | 'Module' | 'Folder' | 'File' | 'Class' | 'Function' | 'Method' | 'Variable' | 'Interface' | 'Enum' | 'Decorator' | 'Import' | 'Type' | 'CodeElement' | 'Community' | 'Process' | 'Struct' | 'Macro' | 'Typedef' | 'Union' | 'Namespace' | 'Trait' | 'Impl' | 'TypeAlias' | 'Const' | 'Static' | 'Property' | 'Record' | 'Delegate' | 'Annotation' | 'Constructor' | 'Template' | 'Section';
1
+ export type NodeLabel = 'Project' | 'Package' | 'Module' | 'Folder' | 'File' | 'Class' | 'Function' | 'Method' | 'Variable' | 'Interface' | 'Enum' | 'Decorator' | 'Import' | 'Type' | 'CodeElement' | 'Community' | 'Process' | 'Struct' | 'Macro' | 'Typedef' | 'Union' | 'Namespace' | 'Trait' | 'Impl' | 'TypeAlias' | 'Const' | 'Static' | 'Property' | 'Record' | 'Delegate' | 'Annotation' | 'Constructor' | 'Template' | 'Section' | 'Route' | 'Tool';
2
2
  import { SupportedLanguages } from '../../config/supported-languages.js';
3
3
  export type NodeProperties = {
4
4
  name: string;
@@ -25,8 +25,15 @@ export type NodeProperties = {
25
25
  parameterCount?: number;
26
26
  level?: number;
27
27
  returnType?: string;
28
+ declaredType?: string;
29
+ visibility?: string;
30
+ isStatic?: boolean;
31
+ isReadonly?: boolean;
32
+ responseKeys?: string[];
33
+ errorKeys?: string[];
34
+ middleware?: string[];
28
35
  };
29
- export type RelationshipType = 'CONTAINS' | 'CALLS' | 'INHERITS' | 'OVERRIDES' | 'IMPORTS' | 'USES' | 'DEFINES' | 'DECORATES' | 'IMPLEMENTS' | 'EXTENDS' | 'HAS_METHOD' | 'HAS_PROPERTY' | 'ACCESSES' | 'MEMBER_OF' | 'STEP_IN_PROCESS';
36
+ export type RelationshipType = 'CONTAINS' | 'CALLS' | 'INHERITS' | 'OVERRIDES' | 'IMPORTS' | 'USES' | 'DEFINES' | 'DECORATES' | 'IMPLEMENTS' | 'EXTENDS' | 'HAS_METHOD' | 'HAS_PROPERTY' | 'ACCESSES' | 'MEMBER_OF' | 'STEP_IN_PROCESS' | 'HANDLES_ROUTE' | 'FETCHES' | 'HANDLES_TOOL' | 'ENTRY_POINT_OF' | 'WRAPS' | 'QUERIES';
30
37
  export interface GraphNode {
31
38
  id: string;
32
39
  label: NodeLabel;
@@ -64,4 +71,5 @@ export interface KnowledgeGraph {
64
71
  addRelationship: (relationship: GraphRelationship) => void;
65
72
  removeNode: (nodeId: string) => boolean;
66
73
  removeNodesByFile: (filePath: string) => number;
74
+ removeRelationship: (relationshipId: string) => boolean;
67
75
  }
@@ -2,7 +2,7 @@ import { KnowledgeGraph } from '../graph/types.js';
2
2
  import { ASTCache } from './ast-cache.js';
3
3
  import type { SymbolTable } from './symbol-table.js';
4
4
  import type { ResolutionContext } from './resolution-context.js';
5
- import type { ExtractedCall, ExtractedAssignment, ExtractedHeritage, ExtractedRoute, FileConstructorBindings } from './workers/parse-worker.js';
5
+ import type { ExtractedCall, ExtractedAssignment, ExtractedHeritage, ExtractedRoute, ExtractedFetchCall, FileConstructorBindings } from './workers/parse-worker.js';
6
6
  /** Per-file resolved type bindings for exported symbols.
7
7
  * Populated during call processing, consumed by Phase 14 re-resolution pass. */
8
8
  export type ExportedTypeMap = Map<string, Map<string, string>>;
@@ -68,3 +68,20 @@ export declare const processAssignmentsFromExtracted: (graph: KnowledgeGraph, as
68
68
  * Resolve pre-extracted Laravel routes to CALLS edges from route files to controller methods.
69
69
  */
70
70
  export declare const processRoutesFromExtracted: (graph: KnowledgeGraph, extractedRoutes: ExtractedRoute[], ctx: ResolutionContext, onProgress?: (current: number, total: number) => void) => Promise<void>;
71
+ export declare const extractConsumerAccessedKeys: (content: string) => string[];
72
+ /**
73
+ * Create FETCHES edges from extracted fetch() calls to matching Route nodes.
74
+ * When consumerContents is provided, extracts property access patterns from
75
+ * consumer files and encodes them in the edge reason field.
76
+ */
77
+ export declare const processNextjsFetchRoutes: (graph: KnowledgeGraph, fetchCalls: ExtractedFetchCall[], routeRegistry: Map<string, string>, // routeURL → handlerFilePath
78
+ consumerContents?: Map<string, string>) => void;
79
+ /**
80
+ * Extract fetch() calls from source files (sequential path).
81
+ * Workers handle this via tree-sitter captures in parse-worker; this function
82
+ * provides the same extraction for the sequential fallback path.
83
+ */
84
+ export declare const extractFetchCallsFromFiles: (files: {
85
+ path: string;
86
+ content: string;
87
+ }[], astCache: ASTCache) => Promise<ExtractedFetchCall[]>;
@@ -1,14 +1,17 @@
1
1
  import Parser from 'tree-sitter';
2
2
  import { TIER_CONFIDENCE } from './resolution-context.js';
3
3
  import { isLanguageAvailable, loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
4
- import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
4
+ import { getProvider } from './languages/index.js';
5
5
  import { generateId } from '../../lib/utils.js';
6
- import { getLanguageFromFilename, isVerboseIngestionEnabled, yieldToEventLoop, FUNCTION_NODE_TYPES, extractFunctionName, isBuiltInOrNoise, countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, findEnclosingClassId, extractMixedChain, } from './utils.js';
6
+ import { getLanguageFromFilename } from './utils/language-detection.js';
7
+ import { isVerboseIngestionEnabled } from './utils/verbose.js';
8
+ import { yieldToEventLoop } from './utils/event-loop.js';
9
+ import { FUNCTION_NODE_TYPES, extractFunctionName, findEnclosingClassId } from './utils/ast-helpers.js';
10
+ import { countCallArguments, inferCallForm, extractReceiverName, extractReceiverNode, extractMixedChain, } from './utils/call-analysis.js';
7
11
  import { buildTypeEnv, isSubclassOf } from './type-env.js';
8
12
  import { getTreeSitterBufferSize } from './constants.js';
9
- import { callRouters } from './call-routing.js';
13
+ import { normalizeFetchURL, routeMatches } from './route-extractors/nextjs.js';
10
14
  import { extractReturnTypeName, stripNullable } from './type-extractors/shared.js';
11
- import { typeConfigs } from './type-extractors/index.js';
12
15
  const MAX_EXPORTS_PER_FILE = 500;
13
16
  const MAX_TYPE_NAME_LENGTH = 256;
14
17
  /** Build a map of imported callee names → return types for cross-file call-result binding.
@@ -48,7 +51,7 @@ export function buildImportedRawReturnTypes(filePath, namedImportMap, symbolTabl
48
51
  /** Collect resolved type bindings for exported file-scope symbols.
49
52
  * Uses graph node isExported flag — does NOT require isExported on SymbolDefinition. */
50
53
  function collectExportedBindings(typeEnv, filePath, symbolTable, graph) {
51
- const fileScope = typeEnv.env.get('');
54
+ const fileScope = typeEnv.fileScope();
52
55
  if (!fileScope || fileScope.size === 0)
53
56
  return null;
54
57
  const exported = new Map();
@@ -147,7 +150,7 @@ const TYPE_PRESERVING_METHODS = new Set([
147
150
  * Walk up the AST from a node to find the enclosing function/method.
148
151
  * Returns null if the call is at module/file level (top-level code).
149
152
  */
150
- const findEnclosingFunction = (node, filePath, ctx) => {
153
+ const findEnclosingFunction = (node, filePath, ctx, provider) => {
151
154
  let current = node.parent;
152
155
  while (current) {
153
156
  if (FUNCTION_NODE_TYPES.has(current.type)) {
@@ -157,7 +160,33 @@ const findEnclosingFunction = (node, filePath, ctx) => {
157
160
  if (resolved?.tier === 'same-file' && resolved.candidates.length > 0) {
158
161
  return resolved.candidates[0].nodeId;
159
162
  }
160
- return generateId(label, `${filePath}:${funcName}`);
163
+ // Apply labelOverride so label matches the definition phase (single source of truth).
164
+ let finalLabel = label;
165
+ if (provider.labelOverride) {
166
+ const override = provider.labelOverride(current, label);
167
+ if (override !== null)
168
+ finalLabel = override;
169
+ }
170
+ return generateId(finalLabel, `${filePath}:${funcName}`);
171
+ }
172
+ }
173
+ // Language-specific enclosing function resolution (e.g., Dart where
174
+ // function_body is a sibling of function_signature, not a child).
175
+ if (provider.enclosingFunctionFinder) {
176
+ const customResult = provider.enclosingFunctionFinder(current);
177
+ if (customResult) {
178
+ // Try SymbolTable first (same pattern as the FUNCTION_NODE_TYPES branch above).
179
+ const resolved = ctx.resolve(customResult.funcName, filePath);
180
+ if (resolved?.tier === 'same-file' && resolved.candidates.length > 0) {
181
+ return resolved.candidates[0].nodeId;
182
+ }
183
+ let finalLabel = customResult.label;
184
+ if (provider.labelOverride) {
185
+ const override = provider.labelOverride(current.previousSibling, finalLabel);
186
+ if (override !== null)
187
+ finalLabel = override;
188
+ }
189
+ return generateId(finalLabel, `${filePath}:${customResult.funcName}`);
161
190
  }
162
191
  }
163
192
  current = current.parent;
@@ -246,7 +275,8 @@ importedRawReturnTypesMap) => {
246
275
  }
247
276
  continue;
248
277
  }
249
- const queryStr = LANGUAGE_QUERIES[language];
278
+ const provider = getProvider(language);
279
+ const queryStr = provider.treeSitterQueries;
250
280
  if (!queryStr)
251
281
  continue;
252
282
  await loadLanguage(language, file.path);
@@ -271,7 +301,6 @@ importedRawReturnTypesMap) => {
271
301
  console.warn(`Query error for ${file.path}:`, queryError);
272
302
  continue;
273
303
  }
274
- const lang = getLanguageFromFilename(file.path);
275
304
  // Pre-pass: extract heritage from query matches to build parentMap for buildTypeEnv.
276
305
  // Heritage-processor runs in PARALLEL, so graph edges don't exist when buildTypeEnv runs.
277
306
  const fileParentMap = new Map();
@@ -318,18 +347,19 @@ importedRawReturnTypesMap) => {
318
347
  const importedBindings = importedBindingsMap?.get(file.path);
319
348
  const importedReturnTypes = importedReturnTypesMap?.get(file.path);
320
349
  const importedRawReturnTypes = importedRawReturnTypesMap?.get(file.path);
321
- const typeEnv = lang ? buildTypeEnv(tree, lang, { symbolTable: ctx.symbols, parentMap, importedBindings, importedReturnTypes, importedRawReturnTypes }) : null;
350
+ const typeEnv = buildTypeEnv(tree, language, { symbolTable: ctx.symbols, parentMap, importedBindings, importedReturnTypes, importedRawReturnTypes, enclosingFunctionFinder: provider?.enclosingFunctionFinder });
322
351
  if (typeEnv && exportedTypeMap) {
323
352
  const fileExports = collectExportedBindings(typeEnv, file.path, ctx.symbols, graph);
324
353
  if (fileExports)
325
354
  exportedTypeMap.set(file.path, fileExports);
326
355
  }
327
- const callRouter = callRouters[language];
328
- const verifiedReceivers = typeEnv && typeEnv.constructorBindings.length > 0
356
+ const callRouter = provider.callRouter;
357
+ const verifiedReceivers = typeEnv.constructorBindings.length > 0
329
358
  ? verifyConstructorBindings(typeEnv.constructorBindings, file.path, ctx)
330
359
  : new Map();
331
360
  const receiverIndex = buildReceiverTypeIndex(verifiedReceivers);
332
361
  ctx.enableCache(file.path);
362
+ const widenCache = new Map();
333
363
  matches.forEach(match => {
334
364
  const captureMap = {};
335
365
  match.captures.forEach(c => captureMap[c.name] = c.node);
@@ -345,7 +375,7 @@ importedRawReturnTypesMap) => {
345
375
  }
346
376
  // Fall back to verified constructor bindings (mirrors CALLS resolution tier 2)
347
377
  if (!receiverTypeName && receiverText && receiverIndex.size > 0) {
348
- const enclosing = findEnclosingFunction(captureMap['assignment'], file.path, ctx);
378
+ const enclosing = findEnclosingFunction(captureMap['assignment'], file.path, ctx, provider);
349
379
  const funcName = enclosing ? extractFuncNameFromSourceId(enclosing) : '';
350
380
  receiverTypeName = lookupReceiverType(receiverIndex, funcName, receiverText);
351
381
  }
@@ -357,7 +387,7 @@ importedRawReturnTypesMap) => {
357
387
  }
358
388
  }
359
389
  if (receiverTypeName) {
360
- const enclosing = findEnclosingFunction(captureMap['assignment'], file.path, ctx);
390
+ const enclosing = findEnclosingFunction(captureMap['assignment'], file.path, ctx, provider);
361
391
  const srcId = enclosing || generateId('File', file.path);
362
392
  // Defer resolution: Ruby attr_accessor properties are registered during
363
393
  // this same loop, so cross-file lookups fail if the declaring file hasn't
@@ -375,7 +405,7 @@ importedRawReturnTypesMap) => {
375
405
  if (!nameNode)
376
406
  return;
377
407
  const calledName = nameNode.text;
378
- const routed = callRouter(calledName, captureMap['call']);
408
+ const routed = callRouter?.(calledName, captureMap['call']);
379
409
  if (routed) {
380
410
  switch (routed.kind) {
381
411
  case 'skip':
@@ -429,7 +459,7 @@ importedRawReturnTypesMap) => {
429
459
  break;
430
460
  }
431
461
  }
432
- if (isBuiltInOrNoise(calledName))
462
+ if (provider.isBuiltInName(calledName))
433
463
  return;
434
464
  const callNode = captureMap['call'];
435
465
  const callForm = inferCallForm(callNode, nameNode);
@@ -475,7 +505,7 @@ importedRawReturnTypesMap) => {
475
505
  }
476
506
  // Fall back to verified constructor bindings for return type inference
477
507
  if (!receiverTypeName && receiverName && receiverIndex.size > 0) {
478
- const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx);
508
+ const enclosingFunc = findEnclosingFunction(callNode, file.path, ctx, provider);
479
509
  const funcName = enclosingFunc ? extractFuncNameFromSourceId(enclosingFunc) : '';
480
510
  receiverTypeName = lookupReceiverType(receiverIndex, funcName, receiverName);
481
511
  }
@@ -489,7 +519,7 @@ importedRawReturnTypesMap) => {
489
519
  }
490
520
  }
491
521
  // Hoist sourceId so it's available for ACCESSES edge emission during chain walk.
492
- const enclosingFuncId = findEnclosingFunction(callNode, file.path, ctx);
522
+ const enclosingFuncId = findEnclosingFunction(callNode, file.path, ctx, provider);
493
523
  const sourceId = enclosingFuncId || generateId('File', file.path);
494
524
  // Fall back to mixed chain resolution when the receiver is a complex expression
495
525
  // (field chain, call chain, or interleaved — e.g. user.address.city.save() or
@@ -520,7 +550,7 @@ importedRawReturnTypesMap) => {
520
550
  }
521
551
  // Build overload hints for languages with inferLiteralType (Java/Kotlin/C#/C++).
522
552
  // Only used when multiple candidates survive arity filtering — ~1-3% of calls.
523
- const langConfig = lang ? typeConfigs[lang] : undefined;
553
+ const langConfig = provider.typeConfig;
524
554
  const hints = langConfig?.inferLiteralType
525
555
  ? { callNode, inferLiteralType: langConfig.inferLiteralType }
526
556
  : undefined;
@@ -530,7 +560,7 @@ importedRawReturnTypesMap) => {
530
560
  callForm,
531
561
  receiverTypeName,
532
562
  receiverName,
533
- }, file.path, ctx, hints);
563
+ }, file.path, ctx, hints, widenCache);
534
564
  if (!resolved)
535
565
  return;
536
566
  const relId = generateId('CALLS', `${sourceId}:${calledName}->${resolved.nodeId}`);
@@ -694,21 +724,20 @@ const tryOverloadDisambiguation = (candidates, hints) => {
694
724
  }
695
725
  return null;
696
726
  };
697
- /**
698
- * Resolve a function call to its target node ID using priority strategy:
699
- * A. Narrow candidates by scope tier via ctx.resolve()
700
- * B. Filter to callable symbol kinds (constructor-aware when callForm is set)
701
- * C. Apply arity filtering when parameter metadata is available
702
- * D. Apply receiver-type filtering for member calls with typed receivers
703
- * E. Apply overload disambiguation via argument literal types (when available)
704
- *
705
- * If filtering still leaves multiple candidates, refuse to emit a CALLS edge.
706
- */
707
- const resolveCallTarget = (call, currentFile, ctx, overloadHints) => {
727
+ const resolveCallTarget = (call, currentFile, ctx, overloadHints, widenCache) => {
708
728
  const tiered = ctx.resolve(call.calledName, currentFile);
709
729
  if (!tiered)
710
730
  return null;
711
731
  let filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, call.callForm);
732
+ // Swift/Kotlin: constructor calls look like free function calls (no `new` keyword).
733
+ // If free-form filtering found no callable candidates but the symbol resolves to a
734
+ // Class/Struct, retry with constructor form so CONSTRUCTOR_TARGET_TYPES applies.
735
+ if (filteredCandidates.length === 0 && call.callForm === 'free') {
736
+ const hasTypeTarget = tiered.candidates.some(c => c.type === 'Class' || c.type === 'Struct' || c.type === 'Enum');
737
+ if (hasTypeTarget) {
738
+ filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, 'constructor');
739
+ }
740
+ }
712
741
  // Module-qualified constructor pattern: e.g. Python `import models; models.User()`.
713
742
  // The attribute access gives callForm='member', but the callee may be a Class — a valid
714
743
  // constructor target. Re-try with constructor-form filtering so that `module.ClassName()`
@@ -716,17 +745,35 @@ const resolveCallTarget = (call, currentFile, ctx, overloadHints) => {
716
745
  if (filteredCandidates.length === 0 && call.callForm === 'member') {
717
746
  filteredCandidates = filterCallableCandidates(tiered.candidates, call.argCount, 'constructor');
718
747
  }
719
- // Module-alias disambiguation: Python `import auth; auth.User()` — when both models.py and
720
- // auth.py export User, receiverName='auth' selects auth.py via moduleAliasMap.
721
- // Runs when multiple candidates survive filtering and the receiver is a known module alias.
722
- if (filteredCandidates.length > 1 && call.callForm === 'member' && call.receiverName) {
748
+ // Module-alias disambiguation: Python `import auth; auth.User()` — receiverName='auth'
749
+ // selects auth.py via moduleAliasMap. Runs for ALL member calls with a known module alias,
750
+ // not just ambiguous ones same-file tier may shadow the correct cross-module target when
751
+ // the caller defines a function with the same name as the callee (Issue #417).
752
+ if (call.callForm === 'member' && call.receiverName) {
723
753
  const aliasMap = ctx.moduleAliasMap?.get(currentFile);
724
754
  if (aliasMap) {
725
755
  const moduleFile = aliasMap.get(call.receiverName);
726
756
  if (moduleFile) {
727
757
  const aliasFiltered = filteredCandidates.filter(c => c.filePath === moduleFile);
728
- if (aliasFiltered.length > 0)
758
+ if (aliasFiltered.length > 0) {
729
759
  filteredCandidates = aliasFiltered;
760
+ }
761
+ else {
762
+ // Same-file tier returned a local match, but the alias points elsewhere.
763
+ // Widen to global candidates and filter to the aliased module's file.
764
+ // Use per-file widenCache to avoid repeated lookupFuzzy for the same
765
+ // calledName+moduleFile from multiple call sites in the same file.
766
+ const cacheKey = `${call.calledName}\0${moduleFile}`;
767
+ let fuzzyDefs = widenCache?.get(cacheKey);
768
+ if (!fuzzyDefs) {
769
+ fuzzyDefs = ctx.symbols.lookupFuzzy(call.calledName);
770
+ widenCache?.set(cacheKey, fuzzyDefs);
771
+ }
772
+ const widened = filterCallableCandidates(fuzzyDefs, call.argCount, call.callForm)
773
+ .filter(c => c.filePath === moduleFile);
774
+ if (widened.length > 0)
775
+ filteredCandidates = widened;
776
+ }
730
777
  }
731
778
  }
732
779
  }
@@ -780,8 +827,20 @@ const resolveCallTarget = (call, currentFile, ctx, overloadHints) => {
780
827
  if (disambiguated)
781
828
  return toResolveResult(disambiguated, tiered.tier);
782
829
  }
783
- if (filteredCandidates.length !== 1)
830
+ if (filteredCandidates.length !== 1) {
831
+ // Deduplicate: Swift extensions create multiple Class nodes with the same name.
832
+ // When all candidates share the same type and differ only by file (extension vs
833
+ // primary definition), they represent the same symbol. Prefer the primary
834
+ // definition (shortest file path: Product.swift over ProductExtension.swift).
835
+ if (filteredCandidates.length > 1) {
836
+ const allSameType = filteredCandidates.every(c => c.type === filteredCandidates[0].type);
837
+ if (allSameType && (filteredCandidates[0].type === 'Class' || filteredCandidates[0].type === 'Struct')) {
838
+ const sorted = [...filteredCandidates].sort((a, b) => a.filePath.length - b.filePath.length);
839
+ return toResolveResult(sorted[0], tiered.tier);
840
+ }
841
+ }
784
842
  return null;
843
+ }
785
844
  return toResolveResult(filteredCandidates[0], tiered.tier);
786
845
  };
787
846
  // ── Scope key helpers ────────────────────────────────────────────────────
@@ -997,6 +1056,7 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
997
1056
  await yieldToEventLoop();
998
1057
  }
999
1058
  ctx.enableCache(filePath);
1059
+ const widenCache = new Map();
1000
1060
  const receiverMap = fileReceiverTypes.get(filePath);
1001
1061
  for (const call of calls) {
1002
1062
  let effectiveCall = call;
@@ -1038,7 +1098,7 @@ export const processCallsFromExtracted = async (graph, extractedCalls, ctx, onPr
1038
1098
  }
1039
1099
  }
1040
1100
  }
1041
- const resolved = resolveCallTarget(effectiveCall, effectiveCall.filePath, ctx);
1101
+ const resolved = resolveCallTarget(effectiveCall, effectiveCall.filePath, ctx, undefined, widenCache);
1042
1102
  if (!resolved)
1043
1103
  continue;
1044
1104
  const relId = generateId('CALLS', `${effectiveCall.sourceId}:${effectiveCall.calledName}->${resolved.nodeId}`);
@@ -1152,3 +1212,202 @@ export const processRoutesFromExtracted = async (graph, extractedRoutes, ctx, on
1152
1212
  }
1153
1213
  onProgress?.(extractedRoutes.length, extractedRoutes.length);
1154
1214
  };
1215
+ /**
1216
+ * Extract property access keys from a consumer file's source code near fetch calls.
1217
+ *
1218
+ * Looks for three patterns after a fetch/response variable assignment:
1219
+ * 1. Destructuring: `const { data, pagination } = await res.json()`
1220
+ * 2. Property access: `response.data`, `result.items`
1221
+ * 3. Optional chaining: `data?.key1?.key2`
1222
+ *
1223
+ * Returns deduplicated top-level property names accessed on the response.
1224
+ *
1225
+ * NOTE: This scans the entire file content, not just code near a specific fetch call.
1226
+ * If a file has multiple fetch calls to different routes, all accessed keys are
1227
+ * attributed to each fetch. This is an acceptable tradeoff for regex-based extraction.
1228
+ */
1229
+ /** Common method names on response/data objects that are NOT property accesses */
1230
+ // Properties/methods to ignore when extracting consumer accessed keys from `data.X` patterns.
1231
+ // Avoids false positives from Fetch API, Array, Object, Promise, and DOM access on variables
1232
+ // that happen to share names with response variables (data, result, response, etc.).
1233
+ const RESPONSE_ACCESS_BLOCKLIST = new Set([
1234
+ // Fetch/Response API
1235
+ 'json', 'text', 'blob', 'arrayBuffer', 'formData', 'ok', 'status', 'headers', 'clone',
1236
+ // Promise
1237
+ 'then', 'catch', 'finally',
1238
+ // Array
1239
+ 'map', 'filter', 'forEach', 'reduce', 'find', 'some', 'every',
1240
+ 'push', 'pop', 'shift', 'unshift', 'splice', 'slice', 'concat', 'join',
1241
+ 'sort', 'reverse', 'includes', 'indexOf',
1242
+ // Object
1243
+ 'length', 'toString', 'valueOf', 'keys', 'values', 'entries',
1244
+ // DOM methods — file-download patterns often reuse `data`/`response` variable names
1245
+ 'appendChild', 'removeChild', 'insertBefore', 'replaceChild', 'replaceChildren',
1246
+ 'createElement', 'getElementById', 'querySelector', 'querySelectorAll',
1247
+ 'setAttribute', 'getAttribute', 'removeAttribute', 'hasAttribute',
1248
+ 'addEventListener', 'removeEventListener', 'dispatchEvent',
1249
+ 'classList', 'className',
1250
+ 'parentNode', 'parentElement', 'childNodes', 'children',
1251
+ 'nextSibling', 'previousSibling', 'firstChild', 'lastChild',
1252
+ 'click', 'focus', 'blur', 'submit', 'reset',
1253
+ 'innerHTML', 'outerHTML', 'textContent', 'innerText',
1254
+ ]);
1255
+ export const extractConsumerAccessedKeys = (content) => {
1256
+ const keys = new Set();
1257
+ // Pattern 1: Destructuring from .json() — const { key1, key2 } = await res.json()
1258
+ // Also matches: const { key1, key2 } = await (await fetch(...)).json()
1259
+ const destructurePattern = /(?:const|let|var)\s+\{([^}]+)\}\s*=\s*(?:await\s+)?(?:\w+\.json\s*\(\)|(?:await\s+)?(?:fetch|axios|got)\s*\([^)]*\)(?:\.then\s*\([^)]*\))?(?:\.json\s*\(\))?)/g;
1260
+ let match;
1261
+ while ((match = destructurePattern.exec(content)) !== null) {
1262
+ const destructuredBody = match[1];
1263
+ // Extract identifiers from destructuring, handling renamed bindings (key: alias)
1264
+ const keyPattern = /(\w+)\s*(?::\s*\w+)?/g;
1265
+ let keyMatch;
1266
+ while ((keyMatch = keyPattern.exec(destructuredBody)) !== null) {
1267
+ keys.add(keyMatch[1]);
1268
+ }
1269
+ }
1270
+ // Pattern 2: Destructuring from a data/result/response/json variable
1271
+ // e.g., const { items, total } = data; or const { error } = result;
1272
+ const dataVarDestructure = /(?:const|let|var)\s+\{([^}]+)\}\s*=\s*(?:data|result|response|json|body|res)\b/g;
1273
+ while ((match = dataVarDestructure.exec(content)) !== null) {
1274
+ const destructuredBody = match[1];
1275
+ const keyPattern = /(\w+)\s*(?::\s*\w+)?/g;
1276
+ let keyMatch;
1277
+ while ((keyMatch = keyPattern.exec(destructuredBody)) !== null) {
1278
+ keys.add(keyMatch[1]);
1279
+ }
1280
+ }
1281
+ // Pattern 3: Property access on common response variable names
1282
+ // Matches: data.key, response.key, result.key, json.key, body.key
1283
+ // Also matches optional chaining: data?.key
1284
+ const propAccessPattern = /\b(?:data|response|result|json|body|res)\s*(?:\?\.|\.)(\w+)/g;
1285
+ while ((match = propAccessPattern.exec(content)) !== null) {
1286
+ const key = match[1];
1287
+ // Skip common method calls that aren't property accesses
1288
+ if (!RESPONSE_ACCESS_BLOCKLIST.has(key)) {
1289
+ keys.add(key);
1290
+ }
1291
+ }
1292
+ return [...keys];
1293
+ };
1294
+ /**
1295
+ * Create FETCHES edges from extracted fetch() calls to matching Route nodes.
1296
+ * When consumerContents is provided, extracts property access patterns from
1297
+ * consumer files and encodes them in the edge reason field.
1298
+ */
1299
+ export const processNextjsFetchRoutes = (graph, fetchCalls, routeRegistry, // routeURL → handlerFilePath
1300
+ consumerContents) => {
1301
+ // Pre-count how many routes each consumer file matches (for confidence attribution)
1302
+ const routeCountByFile = new Map();
1303
+ for (const call of fetchCalls) {
1304
+ const normalized = normalizeFetchURL(call.fetchURL);
1305
+ if (!normalized)
1306
+ continue;
1307
+ for (const [routeURL] of routeRegistry) {
1308
+ if (routeMatches(normalized, routeURL)) {
1309
+ routeCountByFile.set(call.filePath, (routeCountByFile.get(call.filePath) ?? 0) + 1);
1310
+ break;
1311
+ }
1312
+ }
1313
+ }
1314
+ for (const call of fetchCalls) {
1315
+ const normalized = normalizeFetchURL(call.fetchURL);
1316
+ if (!normalized)
1317
+ continue;
1318
+ for (const [routeURL] of routeRegistry) {
1319
+ if (routeMatches(normalized, routeURL)) {
1320
+ const sourceId = generateId('File', call.filePath);
1321
+ const routeNodeId = generateId('Route', routeURL);
1322
+ // Extract consumer accessed keys if file content is available
1323
+ let reason = 'fetch-url-match';
1324
+ if (consumerContents) {
1325
+ const content = consumerContents.get(call.filePath);
1326
+ if (content) {
1327
+ const accessedKeys = extractConsumerAccessedKeys(content);
1328
+ if (accessedKeys.length > 0) {
1329
+ reason = `fetch-url-match|keys:${accessedKeys.join(',')}`;
1330
+ }
1331
+ }
1332
+ }
1333
+ // Encode multi-fetch count so downstream can set confidence
1334
+ const fetchCount = routeCountByFile.get(call.filePath) ?? 1;
1335
+ if (fetchCount > 1) {
1336
+ reason = `${reason}|fetches:${fetchCount}`;
1337
+ }
1338
+ graph.addRelationship({
1339
+ id: generateId('FETCHES', `${sourceId}->${routeNodeId}`),
1340
+ sourceId,
1341
+ targetId: routeNodeId,
1342
+ type: 'FETCHES',
1343
+ confidence: 0.9,
1344
+ reason,
1345
+ });
1346
+ break;
1347
+ }
1348
+ }
1349
+ }
1350
+ };
1351
+ /**
1352
+ * Extract fetch() calls from source files (sequential path).
1353
+ * Workers handle this via tree-sitter captures in parse-worker; this function
1354
+ * provides the same extraction for the sequential fallback path.
1355
+ */
1356
+ export const extractFetchCallsFromFiles = async (files, astCache) => {
1357
+ const parser = await loadParser();
1358
+ const result = [];
1359
+ for (const file of files) {
1360
+ const language = getLanguageFromFilename(file.path);
1361
+ if (!language)
1362
+ continue;
1363
+ if (!isLanguageAvailable(language))
1364
+ continue;
1365
+ const provider = getProvider(language);
1366
+ const queryStr = provider.treeSitterQueries;
1367
+ if (!queryStr)
1368
+ continue;
1369
+ await loadLanguage(language, file.path);
1370
+ let tree = astCache.get(file.path);
1371
+ if (!tree) {
1372
+ try {
1373
+ tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
1374
+ }
1375
+ catch {
1376
+ continue;
1377
+ }
1378
+ astCache.set(file.path, tree);
1379
+ }
1380
+ let matches;
1381
+ try {
1382
+ const lang = parser.getLanguage();
1383
+ const query = new Parser.Query(lang, queryStr);
1384
+ matches = query.matches(tree.rootNode);
1385
+ }
1386
+ catch {
1387
+ continue;
1388
+ }
1389
+ for (const match of matches) {
1390
+ const captureMap = {};
1391
+ match.captures.forEach(c => captureMap[c.name] = c.node);
1392
+ if (captureMap['route.fetch']) {
1393
+ const urlNode = captureMap['route.url'] ?? captureMap['route.template_url'];
1394
+ if (urlNode) {
1395
+ result.push({
1396
+ filePath: file.path,
1397
+ fetchURL: urlNode.text,
1398
+ lineNumber: captureMap['route.fetch'].startPosition.row,
1399
+ });
1400
+ }
1401
+ }
1402
+ else if (captureMap['http_client'] && captureMap['http_client.url']) {
1403
+ const method = captureMap['http_client.method']?.text;
1404
+ const url = captureMap['http_client.url'].text;
1405
+ const HTTP_CLIENT_ONLY = new Set(['head', 'options', 'request', 'ajax']);
1406
+ if (method && HTTP_CLIENT_ONLY.has(method) && url.startsWith('/')) {
1407
+ result.push({ filePath: file.path, fetchURL: url, lineNumber: captureMap['http_client'].startPosition.row });
1408
+ }
1409
+ }
1410
+ }
1411
+ }
1412
+ return result;
1413
+ };
@@ -10,6 +10,7 @@
10
10
  * two packages have separate build targets (Node native vs WASM/browser).
11
11
  * Keep both copies in sync until a shared package is introduced.
12
12
  */
13
+ import type { SyntaxNode } from './utils/ast-helpers.js';
13
14
  /** null = this call was not routed; fall through to default call handling */
14
15
  export type CallRoutingResult = RubyCallRouting | null;
15
16
  /**
@@ -18,23 +19,7 @@ export type CallRoutingResult = RubyCallRouting | null;
18
19
  * returns an importPath MUST validate it independently (length cap, control-char
19
20
  * rejection). See routeRubyCall for the reference implementation.
20
21
  */
21
- export type CallRouter = (calledName: string, callNode: any) => CallRoutingResult;
22
- /** Per-language call routing. noRouting = no special routing (normal call processing) */
23
- export declare const callRouters: {
24
- javascript: CallRouter;
25
- typescript: CallRouter;
26
- python: CallRouter;
27
- java: CallRouter;
28
- kotlin: CallRouter;
29
- go: CallRouter;
30
- rust: CallRouter;
31
- csharp: CallRouter;
32
- php: CallRouter;
33
- swift: CallRouter;
34
- cpp: CallRouter;
35
- c: CallRouter;
36
- ruby: typeof routeRubyCall;
37
- };
22
+ export type CallRouter = (calledName: string, callNode: SyntaxNode) => CallRoutingResult;
38
23
  export type RubyCallRouting = {
39
24
  kind: 'import';
40
25
  importPath: string;
@@ -71,4 +56,4 @@ export interface RubyPropertyItem {
71
56
  * @param callNode - The tree-sitter `call` AST node
72
57
  * @returns A discriminated union describing the call's semantic role
73
58
  */
74
- export declare function routeRubyCall(calledName: string, callNode: any): RubyCallRouting;
59
+ export declare function routeRubyCall(calledName: string, callNode: SyntaxNode): RubyCallRouting;
@@ -10,25 +10,6 @@
10
10
  * two packages have separate build targets (Node native vs WASM/browser).
11
11
  * Keep both copies in sync until a shared package is introduced.
12
12
  */
13
- import { SupportedLanguages } from '../../config/supported-languages.js';
14
- /** No-op router: returns null for every call (passthrough to normal processing) */
15
- const noRouting = () => null;
16
- /** Per-language call routing. noRouting = no special routing (normal call processing) */
17
- export const callRouters = {
18
- [SupportedLanguages.JavaScript]: noRouting,
19
- [SupportedLanguages.TypeScript]: noRouting,
20
- [SupportedLanguages.Python]: noRouting,
21
- [SupportedLanguages.Java]: noRouting,
22
- [SupportedLanguages.Kotlin]: noRouting,
23
- [SupportedLanguages.Go]: noRouting,
24
- [SupportedLanguages.Rust]: noRouting,
25
- [SupportedLanguages.CSharp]: noRouting,
26
- [SupportedLanguages.PHP]: noRouting,
27
- [SupportedLanguages.Swift]: noRouting,
28
- [SupportedLanguages.CPlusPlus]: noRouting,
29
- [SupportedLanguages.C]: noRouting,
30
- [SupportedLanguages.Ruby]: routeRubyCall,
31
- };
32
13
  // ── Pre-allocated singletons for common return values ────────────────────────
33
14
  const CALL_RESULT = { kind: 'call' };
34
15
  const SKIP_RESULT = { kind: 'skip' };