gitnexus 1.4.10 → 1.5.0

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 (186) hide show
  1. package/README.md +6 -5
  2. package/dist/cli/ai-context.d.ts +4 -1
  3. package/dist/cli/ai-context.js +19 -11
  4. package/dist/cli/analyze.d.ts +6 -0
  5. package/dist/cli/analyze.js +105 -251
  6. package/dist/cli/eval-server.js +20 -11
  7. package/dist/cli/index-repo.js +20 -22
  8. package/dist/cli/index.js +8 -7
  9. package/dist/cli/mcp.js +1 -1
  10. package/dist/cli/serve.js +29 -1
  11. package/dist/cli/setup.js +9 -9
  12. package/dist/cli/skill-gen.js +15 -9
  13. package/dist/cli/wiki.d.ts +2 -0
  14. package/dist/cli/wiki.js +141 -26
  15. package/dist/config/ignore-service.js +102 -22
  16. package/dist/config/supported-languages.d.ts +8 -42
  17. package/dist/config/supported-languages.js +8 -43
  18. package/dist/core/augmentation/engine.js +19 -7
  19. package/dist/core/embeddings/embedder.js +19 -15
  20. package/dist/core/embeddings/embedding-pipeline.js +6 -6
  21. package/dist/core/embeddings/http-client.js +3 -3
  22. package/dist/core/embeddings/text-generator.js +9 -24
  23. package/dist/core/embeddings/types.d.ts +1 -1
  24. package/dist/core/embeddings/types.js +1 -7
  25. package/dist/core/graph/graph.js +6 -2
  26. package/dist/core/graph/types.d.ts +9 -59
  27. package/dist/core/ingestion/ast-cache.js +3 -3
  28. package/dist/core/ingestion/call-processor.d.ts +20 -2
  29. package/dist/core/ingestion/call-processor.js +347 -144
  30. package/dist/core/ingestion/call-routing.js +10 -4
  31. package/dist/core/ingestion/call-sites/extract-language-call-site.d.ts +10 -0
  32. package/dist/core/ingestion/call-sites/extract-language-call-site.js +22 -0
  33. package/dist/core/ingestion/call-sites/java.d.ts +9 -0
  34. package/dist/core/ingestion/call-sites/java.js +30 -0
  35. package/dist/core/ingestion/cluster-enricher.js +6 -8
  36. package/dist/core/ingestion/cobol/cobol-copy-expander.js +10 -3
  37. package/dist/core/ingestion/cobol/cobol-preprocessor.js +287 -81
  38. package/dist/core/ingestion/cobol/jcl-parser.js +1 -1
  39. package/dist/core/ingestion/cobol/jcl-processor.js +1 -1
  40. package/dist/core/ingestion/cobol-processor.js +102 -56
  41. package/dist/core/ingestion/community-processor.js +21 -15
  42. package/dist/core/ingestion/entry-point-scoring.d.ts +1 -1
  43. package/dist/core/ingestion/entry-point-scoring.js +5 -6
  44. package/dist/core/ingestion/export-detection.js +32 -9
  45. package/dist/core/ingestion/field-extractor.d.ts +1 -1
  46. package/dist/core/ingestion/field-extractors/configs/c-cpp.js +8 -12
  47. package/dist/core/ingestion/field-extractors/configs/csharp.js +45 -2
  48. package/dist/core/ingestion/field-extractors/configs/dart.js +5 -3
  49. package/dist/core/ingestion/field-extractors/configs/go.js +3 -7
  50. package/dist/core/ingestion/field-extractors/configs/helpers.d.ts +5 -0
  51. package/dist/core/ingestion/field-extractors/configs/helpers.js +14 -0
  52. package/dist/core/ingestion/field-extractors/configs/jvm.js +7 -7
  53. package/dist/core/ingestion/field-extractors/configs/php.js +9 -11
  54. package/dist/core/ingestion/field-extractors/configs/python.js +1 -1
  55. package/dist/core/ingestion/field-extractors/configs/ruby.js +4 -3
  56. package/dist/core/ingestion/field-extractors/configs/rust.js +2 -5
  57. package/dist/core/ingestion/field-extractors/configs/swift.js +9 -7
  58. package/dist/core/ingestion/field-extractors/configs/typescript-javascript.js +2 -6
  59. package/dist/core/ingestion/field-extractors/generic.d.ts +5 -2
  60. package/dist/core/ingestion/field-extractors/generic.js +6 -0
  61. package/dist/core/ingestion/field-extractors/typescript.d.ts +1 -1
  62. package/dist/core/ingestion/field-extractors/typescript.js +1 -1
  63. package/dist/core/ingestion/field-types.d.ts +4 -2
  64. package/dist/core/ingestion/filesystem-walker.js +3 -3
  65. package/dist/core/ingestion/framework-detection.d.ts +1 -1
  66. package/dist/core/ingestion/framework-detection.js +355 -85
  67. package/dist/core/ingestion/heritage-processor.d.ts +24 -0
  68. package/dist/core/ingestion/heritage-processor.js +99 -8
  69. package/dist/core/ingestion/import-processor.js +44 -15
  70. package/dist/core/ingestion/import-resolvers/csharp.js +7 -3
  71. package/dist/core/ingestion/import-resolvers/dart.js +1 -1
  72. package/dist/core/ingestion/import-resolvers/go.js +4 -2
  73. package/dist/core/ingestion/import-resolvers/jvm.js +4 -4
  74. package/dist/core/ingestion/import-resolvers/php.js +4 -4
  75. package/dist/core/ingestion/import-resolvers/python.js +1 -1
  76. package/dist/core/ingestion/import-resolvers/rust.js +9 -3
  77. package/dist/core/ingestion/import-resolvers/standard.d.ts +1 -1
  78. package/dist/core/ingestion/import-resolvers/standard.js +6 -5
  79. package/dist/core/ingestion/import-resolvers/swift.js +2 -1
  80. package/dist/core/ingestion/import-resolvers/utils.js +26 -7
  81. package/dist/core/ingestion/language-config.js +5 -4
  82. package/dist/core/ingestion/language-provider.d.ts +7 -2
  83. package/dist/core/ingestion/languages/c-cpp.js +106 -21
  84. package/dist/core/ingestion/languages/cobol.js +1 -1
  85. package/dist/core/ingestion/languages/csharp.js +96 -19
  86. package/dist/core/ingestion/languages/dart.js +23 -7
  87. package/dist/core/ingestion/languages/go.js +1 -1
  88. package/dist/core/ingestion/languages/index.d.ts +1 -1
  89. package/dist/core/ingestion/languages/index.js +2 -3
  90. package/dist/core/ingestion/languages/java.js +4 -1
  91. package/dist/core/ingestion/languages/kotlin.js +60 -13
  92. package/dist/core/ingestion/languages/php.js +102 -25
  93. package/dist/core/ingestion/languages/python.js +28 -5
  94. package/dist/core/ingestion/languages/ruby.js +56 -14
  95. package/dist/core/ingestion/languages/rust.js +55 -11
  96. package/dist/core/ingestion/languages/swift.js +112 -27
  97. package/dist/core/ingestion/languages/typescript.js +95 -19
  98. package/dist/core/ingestion/markdown-processor.js +5 -5
  99. package/dist/core/ingestion/method-extractors/configs/csharp.d.ts +2 -0
  100. package/dist/core/ingestion/method-extractors/configs/csharp.js +283 -0
  101. package/dist/core/ingestion/method-extractors/configs/jvm.d.ts +3 -0
  102. package/dist/core/ingestion/method-extractors/configs/jvm.js +326 -0
  103. package/dist/core/ingestion/method-extractors/generic.d.ts +5 -0
  104. package/dist/core/ingestion/method-extractors/generic.js +137 -0
  105. package/dist/core/ingestion/method-types.d.ts +61 -0
  106. package/dist/core/ingestion/method-types.js +2 -0
  107. package/dist/core/ingestion/mro-processor.d.ts +1 -1
  108. package/dist/core/ingestion/mro-processor.js +12 -8
  109. package/dist/core/ingestion/named-binding-processor.js +2 -2
  110. package/dist/core/ingestion/named-bindings/rust.js +3 -1
  111. package/dist/core/ingestion/parsing-processor.js +74 -24
  112. package/dist/core/ingestion/pipeline.d.ts +2 -1
  113. package/dist/core/ingestion/pipeline.js +208 -102
  114. package/dist/core/ingestion/process-processor.js +12 -10
  115. package/dist/core/ingestion/resolution-context.js +3 -3
  116. package/dist/core/ingestion/route-extractors/middleware.js +31 -7
  117. package/dist/core/ingestion/route-extractors/php.js +2 -1
  118. package/dist/core/ingestion/route-extractors/response-shapes.js +8 -4
  119. package/dist/core/ingestion/structure-processor.d.ts +1 -1
  120. package/dist/core/ingestion/structure-processor.js +4 -4
  121. package/dist/core/ingestion/symbol-table.d.ts +1 -1
  122. package/dist/core/ingestion/symbol-table.js +22 -6
  123. package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -1
  124. package/dist/core/ingestion/tree-sitter-queries.js +1 -1
  125. package/dist/core/ingestion/type-env.d.ts +2 -2
  126. package/dist/core/ingestion/type-env.js +75 -50
  127. package/dist/core/ingestion/type-extractors/c-cpp.js +33 -30
  128. package/dist/core/ingestion/type-extractors/csharp.js +24 -14
  129. package/dist/core/ingestion/type-extractors/dart.js +6 -8
  130. package/dist/core/ingestion/type-extractors/go.js +7 -6
  131. package/dist/core/ingestion/type-extractors/jvm.js +10 -21
  132. package/dist/core/ingestion/type-extractors/php.js +26 -13
  133. package/dist/core/ingestion/type-extractors/python.js +11 -15
  134. package/dist/core/ingestion/type-extractors/ruby.js +8 -3
  135. package/dist/core/ingestion/type-extractors/rust.js +6 -8
  136. package/dist/core/ingestion/type-extractors/shared.js +134 -50
  137. package/dist/core/ingestion/type-extractors/swift.js +16 -13
  138. package/dist/core/ingestion/type-extractors/typescript.js +23 -15
  139. package/dist/core/ingestion/utils/ast-helpers.d.ts +8 -8
  140. package/dist/core/ingestion/utils/ast-helpers.js +72 -35
  141. package/dist/core/ingestion/utils/call-analysis.d.ts +2 -0
  142. package/dist/core/ingestion/utils/call-analysis.js +96 -49
  143. package/dist/core/ingestion/utils/event-loop.js +1 -1
  144. package/dist/core/ingestion/workers/parse-worker.d.ts +7 -2
  145. package/dist/core/ingestion/workers/parse-worker.js +364 -84
  146. package/dist/core/ingestion/workers/worker-pool.js +5 -10
  147. package/dist/core/lbug/csv-generator.js +54 -15
  148. package/dist/core/lbug/lbug-adapter.d.ts +5 -0
  149. package/dist/core/lbug/lbug-adapter.js +86 -23
  150. package/dist/core/lbug/schema.d.ts +3 -6
  151. package/dist/core/lbug/schema.js +6 -30
  152. package/dist/core/run-analyze.d.ts +49 -0
  153. package/dist/core/run-analyze.js +257 -0
  154. package/dist/core/tree-sitter/parser-loader.d.ts +1 -1
  155. package/dist/core/tree-sitter/parser-loader.js +1 -1
  156. package/dist/core/wiki/cursor-client.js +2 -7
  157. package/dist/core/wiki/generator.js +38 -23
  158. package/dist/core/wiki/graph-queries.js +10 -10
  159. package/dist/core/wiki/html-viewer.js +7 -3
  160. package/dist/core/wiki/llm-client.d.ts +23 -2
  161. package/dist/core/wiki/llm-client.js +96 -26
  162. package/dist/core/wiki/prompts.js +7 -6
  163. package/dist/mcp/core/embedder.js +1 -1
  164. package/dist/mcp/core/lbug-adapter.d.ts +4 -1
  165. package/dist/mcp/core/lbug-adapter.js +17 -7
  166. package/dist/mcp/local/local-backend.js +247 -95
  167. package/dist/mcp/resources.js +14 -6
  168. package/dist/mcp/server.js +13 -5
  169. package/dist/mcp/staleness.js +5 -1
  170. package/dist/mcp/tools.js +100 -23
  171. package/dist/server/analyze-job.d.ts +53 -0
  172. package/dist/server/analyze-job.js +146 -0
  173. package/dist/server/analyze-worker.d.ts +13 -0
  174. package/dist/server/analyze-worker.js +59 -0
  175. package/dist/server/api.js +795 -44
  176. package/dist/server/git-clone.d.ts +25 -0
  177. package/dist/server/git-clone.js +91 -0
  178. package/dist/storage/git.js +1 -3
  179. package/dist/storage/repo-manager.d.ts +5 -2
  180. package/dist/storage/repo-manager.js +4 -4
  181. package/dist/types/pipeline.d.ts +1 -21
  182. package/dist/types/pipeline.js +1 -18
  183. package/hooks/claude/gitnexus-hook.cjs +52 -22
  184. package/package.json +3 -2
  185. package/dist/core/ingestion/utils/language-detection.d.ts +0 -9
  186. package/dist/core/ingestion/utils/language-detection.js +0 -70
@@ -3,23 +3,23 @@ import { processStructure } from './structure-processor.js';
3
3
  import { processMarkdown } from './markdown-processor.js';
4
4
  import { processCobol, isCobolFile, isJclFile } from './cobol-processor.js';
5
5
  import { processParsing } from './parsing-processor.js';
6
- import { processImports, processImportsFromExtracted, buildImportResolutionContext } from './import-processor.js';
6
+ import { processImports, processImportsFromExtracted, buildImportResolutionContext, } from './import-processor.js';
7
7
  import { EMPTY_INDEX } from './import-resolvers/utils.js';
8
- import { processCalls, processCallsFromExtracted, processAssignmentsFromExtracted, processRoutesFromExtracted, processNextjsFetchRoutes, extractFetchCallsFromFiles, seedCrossFileReceiverTypes, buildImportedReturnTypes, buildImportedRawReturnTypes, buildExportedTypeMapFromGraph } from './call-processor.js';
8
+ import { processCalls, processCallsFromExtracted, processAssignmentsFromExtracted, processRoutesFromExtracted, processNextjsFetchRoutes, extractFetchCallsFromFiles, seedCrossFileReceiverTypes, buildImportedReturnTypes, buildImportedRawReturnTypes, buildExportedTypeMapFromGraph, buildImplementorMap, mergeImplementorMaps, } from './call-processor.js';
9
9
  import { nextjsFileToRouteURL, normalizeFetchURL } from './route-extractors/nextjs.js';
10
10
  import { expoFileToRouteURL } from './route-extractors/expo.js';
11
11
  import { phpFileToRouteURL } from './route-extractors/php.js';
12
- import { extractResponseShapes, extractPHPResponseShapes } from './route-extractors/response-shapes.js';
13
- import { extractMiddlewareChain, extractNextjsMiddlewareConfig, compileMatcher, compiledMatcherMatchesRoute } from './route-extractors/middleware.js';
12
+ import { extractResponseShapes, extractPHPResponseShapes, } from './route-extractors/response-shapes.js';
13
+ import { extractMiddlewareChain, extractNextjsMiddlewareConfig, compileMatcher, compiledMatcherMatchesRoute, } from './route-extractors/middleware.js';
14
14
  import { generateId } from '../../lib/utils.js';
15
- import { processHeritage, processHeritageFromExtracted } from './heritage-processor.js';
15
+ import { processHeritage, processHeritageFromExtracted, extractExtractedHeritageFromFiles, } from './heritage-processor.js';
16
16
  import { computeMRO } from './mro-processor.js';
17
17
  import { processCommunities } from './community-processor.js';
18
18
  import { processProcesses } from './process-processor.js';
19
19
  import { createResolutionContext } from './resolution-context.js';
20
20
  import { createASTCache } from './ast-cache.js';
21
+ import { getLanguageFromFilename } from 'gitnexus-shared';
21
22
  import { walkRepositoryPaths, readFileContents } from './filesystem-walker.js';
22
- import { getLanguageFromFilename } from './utils/language-detection.js';
23
23
  import { isLanguageAvailable } from '../tree-sitter/parser-loader.js';
24
24
  import { providers, getProviderForFile } from './languages/index.js';
25
25
  import { createWorkerPool } from './workers/worker-pool.js';
@@ -57,9 +57,7 @@ export function topologicalLevelSort(importMap) {
57
57
  }
58
58
  // BFS from zero-in-degree nodes, grouping by level
59
59
  const levels = [];
60
- let currentLevel = [...inDegree.entries()]
61
- .filter(([, d]) => d === 0)
62
- .map(([f]) => f);
60
+ let currentLevel = [...inDegree.entries()].filter(([, d]) => d === 0).map(([f]) => f);
63
61
  while (currentLevel.length > 0) {
64
62
  levels.push(currentLevel);
65
63
  const nextLevel = [];
@@ -74,9 +72,7 @@ export function topologicalLevelSort(importMap) {
74
72
  currentLevel = nextLevel;
75
73
  }
76
74
  // Files still with positive in-degree are in cycles — add as final group
77
- const cycleFiles = [...inDegree.entries()]
78
- .filter(([, d]) => d > 0)
79
- .map(([f]) => f);
75
+ const cycleFiles = [...inDegree.entries()].filter(([, d]) => d > 0).map(([f]) => f);
80
76
  if (cycleFiles.length > 0) {
81
77
  levels.push(cycleFiles);
82
78
  }
@@ -97,15 +93,30 @@ const MAX_CROSS_FILE_REPROCESS = 2000;
97
93
  * Excludes Method, Property, Constructor (accessed via receiver, not directly imported),
98
94
  * and structural labels (File, Folder, Package, Module, Project, etc.). */
99
95
  const IMPORTABLE_SYMBOL_LABELS = new Set([
100
- 'Function', 'Class', 'Interface', 'Struct', 'Enum', 'Trait',
101
- 'TypeAlias', 'Const', 'Static', 'Record', 'Union', 'Typedef', 'Macro',
96
+ 'Function',
97
+ 'Class',
98
+ 'Interface',
99
+ 'Struct',
100
+ 'Enum',
101
+ 'Trait',
102
+ 'TypeAlias',
103
+ 'Const',
104
+ 'Static',
105
+ 'Record',
106
+ 'Union',
107
+ 'Typedef',
108
+ 'Macro',
102
109
  ]);
103
110
  /** Max synthetic bindings per importing file — prevents memory bloat for
104
111
  * C/C++ files that include many large headers. */
105
112
  const MAX_SYNTHETIC_BINDINGS_PER_FILE = 1000;
106
113
  /** Pre-computed language sets derived from providers at module load. */
107
- const WILDCARD_LANGUAGES = new Set(Object.values(providers).filter(p => p.importSemantics === 'wildcard').map(p => p.id));
108
- const SYNTHESIS_LANGUAGES = new Set(Object.values(providers).filter(p => p.importSemantics !== 'named').map(p => p.id));
114
+ const WILDCARD_LANGUAGES = new Set(Object.values(providers)
115
+ .filter((p) => p.importSemantics === 'wildcard')
116
+ .map((p) => p.id));
117
+ const SYNTHESIS_LANGUAGES = new Set(Object.values(providers)
118
+ .filter((p) => p.importSemantics !== 'named')
119
+ .map((p) => p.id));
109
120
  /** Check if a language uses wildcard (whole-module) import semantics.
110
121
  * Derived from LanguageProvider.importSemantics — no hardcoded set needed. */
111
122
  function isWildcardImportLanguage(lang) {
@@ -124,7 +135,7 @@ function needsSynthesis(lang) {
124
135
  function synthesizeWildcardImportBindings(graph, ctx) {
125
136
  // Pre-compute exported symbols per file from graph (single pass)
126
137
  const exportedSymbolsByFile = new Map();
127
- graph.forEachNode(node => {
138
+ graph.forEachNode((node) => {
128
139
  if (!node.properties?.isExported)
129
140
  return;
130
141
  if (!IMPORTABLE_SYMBOL_LABELS.has(node.label))
@@ -147,7 +158,7 @@ function synthesizeWildcardImportBindings(graph, ctx) {
147
158
  // Collect graph-level IMPORTS edges for wildcard languages missing from ctx.importMap.
148
159
  const FILE_PREFIX = 'File:';
149
160
  const graphImports = new Map();
150
- graph.forEachRelationship(rel => {
161
+ graph.forEachRelationship((rel) => {
151
162
  if (rel.type !== 'IMPORTS')
152
163
  return;
153
164
  if (!rel.sourceId.startsWith(FILE_PREFIX) || !rel.targetId.startsWith(FILE_PREFIX))
@@ -292,7 +303,7 @@ async function runCrossFileBindingPropagation(graph, ctx, exportedTypeMap, allPa
292
303
  });
293
304
  let crossFileResolved = 0;
294
305
  const crossFileStart = Date.now();
295
- let astCache = createASTCache(AST_CACHE_CAP);
306
+ const astCache = createASTCache(AST_CACHE_CAP);
296
307
  for (const level of levels) {
297
308
  const levelCandidates = [];
298
309
  for (const filePath of level) {
@@ -323,7 +334,7 @@ async function runCrossFileBindingPropagation(graph, ctx, exportedTypeMap, allPa
323
334
  }
324
335
  if (levelCandidates.length === 0)
325
336
  continue;
326
- const levelPaths = levelCandidates.map(c => c.filePath);
337
+ const levelPaths = levelCandidates.map((c) => c.filePath);
327
338
  const contentMap = await readFileContents(repoPath, levelPaths);
328
339
  for (const { filePath, seeded, importedReturns, importedRawReturns } of levelCandidates) {
329
340
  const content = contentMap.get(filePath);
@@ -396,7 +407,7 @@ async function runScanAndStructure(repoPath, graph, onProgress) {
396
407
  message: 'Analyzing project structure...',
397
408
  stats: { filesProcessed: 0, totalFiles, nodesCreated: graph.nodeCount },
398
409
  });
399
- const allPaths = scannedFiles.map(f => f.path);
410
+ const allPaths = scannedFiles.map((f) => f.path);
400
411
  processStructure(graph, allPaths);
401
412
  onProgress({
402
413
  phase: 'structure',
@@ -412,12 +423,12 @@ async function runScanAndStructure(repoPath, graph, onProgress) {
412
423
  // To add a new language: create a new processor file, import it here,
413
424
  // and add a filter-read-call-log block following the pattern below.
414
425
  // ── Phase 2.5: Markdown processing (headings + cross-links) ────────
415
- const mdScanned = scannedFiles.filter(f => f.path.endsWith('.md') || f.path.endsWith('.mdx'));
426
+ const mdScanned = scannedFiles.filter((f) => f.path.endsWith('.md') || f.path.endsWith('.mdx'));
416
427
  if (mdScanned.length > 0) {
417
- const mdContents = await readFileContents(repoPath, mdScanned.map(f => f.path));
428
+ const mdContents = await readFileContents(repoPath, mdScanned.map((f) => f.path));
418
429
  const mdFiles = mdScanned
419
- .filter(f => mdContents.has(f.path))
420
- .map(f => ({ path: f.path, content: mdContents.get(f.path) }));
430
+ .filter((f) => mdContents.has(f.path))
431
+ .map((f) => ({ path: f.path, content: mdContents.get(f.path) }));
421
432
  const allPathSet = new Set(allPaths);
422
433
  const mdResult = processMarkdown(graph, mdFiles, allPathSet);
423
434
  if (isDev) {
@@ -425,17 +436,19 @@ async function runScanAndStructure(repoPath, graph, onProgress) {
425
436
  }
426
437
  }
427
438
  // ── Phase 2.6: COBOL processing (regex extraction, no tree-sitter) ──
428
- const cobolScanned = scannedFiles.filter(f => isCobolFile(f.path) || isJclFile(f.path));
439
+ const cobolScanned = scannedFiles.filter((f) => isCobolFile(f.path) || isJclFile(f.path));
429
440
  if (cobolScanned.length > 0) {
430
- const cobolContents = await readFileContents(repoPath, cobolScanned.map(f => f.path));
441
+ const cobolContents = await readFileContents(repoPath, cobolScanned.map((f) => f.path));
431
442
  const cobolFiles = cobolScanned
432
- .filter(f => cobolContents.has(f.path))
433
- .map(f => ({ path: f.path, content: cobolContents.get(f.path) }));
443
+ .filter((f) => cobolContents.has(f.path))
444
+ .map((f) => ({ path: f.path, content: cobolContents.get(f.path) }));
434
445
  const allPathSet = new Set(allPaths);
435
446
  const cobolResult = processCobol(graph, cobolFiles, allPathSet);
436
447
  if (isDev) {
437
448
  console.log(` COBOL: ${cobolResult.programs} programs, ${cobolResult.paragraphs} paragraphs, ${cobolResult.sections} sections from ${cobolFiles.length} files`);
438
- if (cobolResult.execSqlBlocks > 0 || cobolResult.execCicsBlocks > 0 || cobolResult.entryPoints > 0) {
449
+ if (cobolResult.execSqlBlocks > 0 ||
450
+ cobolResult.execCicsBlocks > 0 ||
451
+ cobolResult.entryPoints > 0) {
439
452
  console.log(` COBOL enriched: ${cobolResult.execSqlBlocks} SQL blocks, ${cobolResult.execCicsBlocks} CICS blocks, ${cobolResult.entryPoints} entry points, ${cobolResult.moves} moves, ${cobolResult.fileDeclarations} file declarations`);
440
453
  }
441
454
  if (cobolResult.jclJobs > 0) {
@@ -452,7 +465,8 @@ async function runScanAndStructure(repoPath, graph, onProgress) {
452
465
  * 1. Parse via worker pool (or sequential fallback)
453
466
  * 2. Resolve imports from extracted data
454
467
  * 3. Synthesize wildcard import bindings (Go/Ruby/C++/Swift/Python)
455
- * 4. Resolve calls, heritage, routes concurrently (Promise.all)
468
+ * 4. Resolve heritage + routes per chunk; defer worker CALLS until all chunks
469
+ * have contributed heritage so interface-dispatch implementor map is complete
456
470
  * 5. Collect TypeEnv bindings for cross-file propagation
457
471
  *
458
472
  * State accumulated across chunks: symbolTable, importMap, namedImportMap,
@@ -462,10 +476,13 @@ async function runScanAndStructure(repoPath, graph, onProgress) {
462
476
  * @reads allPaths (from scan phase)
463
477
  * @writes graph (Symbol nodes, IMPORTS/CALLS/EXTENDS/IMPLEMENTS/ACCESSES edges)
464
478
  * @writes ctx.symbolTable, ctx.importMap, ctx.namedImportMap, ctx.moduleAliasMap
479
+ *
480
+ * Follow-up from PR review: MethodExtractor (FieldExtractor parity) and optional
481
+ * METHOD_IMPLEMENTS graph edges to make dispatch queryable without an in-memory map.
465
482
  */
466
483
  async function runChunkedParseAndResolve(graph, ctx, scannedFiles, allPaths, totalFiles, repoPath, pipelineStart, onProgress) {
467
484
  const symbolTable = ctx.symbols;
468
- const parseableScanned = scannedFiles.filter(f => {
485
+ const parseableScanned = scannedFiles.filter((f) => {
469
486
  const lang = getLanguageFromFilename(f.path);
470
487
  return lang && isLanguageAvailable(lang);
471
488
  });
@@ -547,14 +564,14 @@ async function runChunkedParseAndResolve(graph, ctx, scannedFiles, allPaths, tot
547
564
  // Build import resolution context once — suffix index, file lists, resolve cache.
548
565
  // Reused across all chunks to avoid rebuilding O(files × path_depth) structures.
549
566
  const importCtx = buildImportResolutionContext(allPaths);
550
- const allPathObjects = allPaths.map(p => ({ path: p }));
551
- // Single-pass: parse + resolve imports/calls/heritage per chunk.
552
- // Calls/heritage use the symbol table built so far (symbols from earlier chunks
553
- // are already registered). This trades ~5% cross-chunk resolution accuracy for
554
- // 200-400MB less memory critical for Linux-kernel-scale repos.
567
+ const allPathObjects = allPaths.map((p) => ({ path: p }));
568
+ // Worker path: parse + imports + heritage per chunk; buffer extracted calls and
569
+ // run processCallsFromExtracted once after all chunks so interface-dispatch uses a
570
+ // complete implementor map (heritage from every chunk). Costs peak RAM for buffered
571
+ // call rows vs streaming resolution per chunk.
555
572
  const sequentialChunkPaths = [];
556
573
  // Pre-compute which chunks need synthesis — O(1) lookup per chunk.
557
- const chunkNeedsSynthesis = chunks.map(paths => paths.some(p => {
574
+ const chunkNeedsSynthesis = chunks.map((paths) => paths.some((p) => {
558
575
  const lang = getLanguageFromFilename(p);
559
576
  return lang != null && needsSynthesis(lang);
560
577
  }));
@@ -571,27 +588,35 @@ async function runChunkedParseAndResolve(graph, ctx, scannedFiles, allPaths, tot
571
588
  // Accumulate MCP/RPC tool definitions (@mcp.tool(), @app.tool(), etc.)
572
589
  const allToolDefs = [];
573
590
  const allORMQueries = [];
591
+ const deferredWorkerCalls = [];
592
+ const deferredWorkerHeritage = [];
593
+ const deferredConstructorBindings = [];
594
+ const deferredAssignments = [];
574
595
  try {
575
596
  for (let chunkIdx = 0; chunkIdx < numChunks; chunkIdx++) {
576
597
  const chunkPaths = chunks[chunkIdx];
577
598
  // Read content for this chunk only
578
599
  const chunkContents = await readFileContents(repoPath, chunkPaths);
579
600
  const chunkFiles = chunkPaths
580
- .filter(p => chunkContents.has(p))
581
- .map(p => ({ path: p, content: chunkContents.get(p) }));
601
+ .filter((p) => chunkContents.has(p))
602
+ .map((p) => ({ path: p, content: chunkContents.get(p) }));
582
603
  // Parse this chunk (workers or sequential fallback)
583
604
  const chunkWorkerData = await processParsing(graph, chunkFiles, symbolTable, astCache, (current, _total, filePath) => {
584
605
  const globalCurrent = filesParsedSoFar + current;
585
- const parsingProgress = 20 + ((globalCurrent / totalParseable) * 62);
606
+ const parsingProgress = 20 + (globalCurrent / totalParseable) * 62;
586
607
  onProgress({
587
608
  phase: 'parsing',
588
609
  percent: Math.round(parsingProgress),
589
610
  message: `Parsing chunk ${chunkIdx + 1}/${numChunks}...`,
590
611
  detail: filePath,
591
- stats: { filesProcessed: globalCurrent, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
612
+ stats: {
613
+ filesProcessed: globalCurrent,
614
+ totalFiles: totalParseable,
615
+ nodesCreated: graph.nodeCount,
616
+ },
592
617
  });
593
618
  }, workerPool);
594
- const chunkBasePercent = 20 + ((filesParsedSoFar / totalParseable) * 62);
619
+ const chunkBasePercent = 20 + (filesParsedSoFar / totalParseable) * 62;
595
620
  if (chunkWorkerData) {
596
621
  // Imports
597
622
  await processImportsFromExtracted(graph, allPathObjects, chunkWorkerData.imports, ctx, (current, total) => {
@@ -600,7 +625,11 @@ async function runChunkedParseAndResolve(graph, ctx, scannedFiles, allPaths, tot
600
625
  percent: Math.round(chunkBasePercent),
601
626
  message: `Resolving imports (chunk ${chunkIdx + 1}/${numChunks})...`,
602
627
  detail: `${current}/${total} files`,
603
- stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
628
+ stats: {
629
+ filesProcessed: filesParsedSoFar,
630
+ totalFiles: totalParseable,
631
+ nodesCreated: graph.nodeCount,
632
+ },
604
633
  });
605
634
  }, repoPath, importCtx);
606
635
  // ── Wildcard-import synthesis (Ruby / C/C++ / Swift / Go) + Python module aliases ─
@@ -620,26 +649,26 @@ async function runChunkedParseAndResolve(graph, ctx, scannedFiles, allPaths, tot
620
649
  console.log(`🔗 E1: Seeded ${enrichedCount} cross-file receiver types (chunk ${chunkIdx + 1})`);
621
650
  }
622
651
  }
623
- // Calls + Heritage + Routes — resolve in parallel (no shared mutable state between them)
624
- // This is safe because each writes disjoint relationship types into idempotent id-keyed Maps,
625
- // and the single-threaded event loop prevents races between synchronous addRelationship calls.
652
+ deferredWorkerCalls.push(...chunkWorkerData.calls);
653
+ deferredWorkerHeritage.push(...chunkWorkerData.heritage);
654
+ deferredConstructorBindings.push(...chunkWorkerData.constructorBindings);
655
+ if (chunkWorkerData.assignments?.length) {
656
+ deferredAssignments.push(...chunkWorkerData.assignments);
657
+ }
658
+ // Heritage + Routes — calls deferred until all chunks have contributed heritage
659
+ // (complete implementor map for interface dispatch).
626
660
  await Promise.all([
627
- processCallsFromExtracted(graph, chunkWorkerData.calls, ctx, (current, total) => {
628
- onProgress({
629
- phase: 'parsing',
630
- percent: Math.round(chunkBasePercent),
631
- message: `Resolving calls (chunk ${chunkIdx + 1}/${numChunks})...`,
632
- detail: `${current}/${total} files`,
633
- stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
634
- });
635
- }, chunkWorkerData.constructorBindings),
636
661
  processHeritageFromExtracted(graph, chunkWorkerData.heritage, ctx, (current, total) => {
637
662
  onProgress({
638
663
  phase: 'parsing',
639
664
  percent: Math.round(chunkBasePercent),
640
665
  message: `Resolving heritage (chunk ${chunkIdx + 1}/${numChunks})...`,
641
666
  detail: `${current}/${total} records`,
642
- stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
667
+ stats: {
668
+ filesProcessed: filesParsedSoFar,
669
+ totalFiles: totalParseable,
670
+ nodesCreated: graph.nodeCount,
671
+ },
643
672
  });
644
673
  }),
645
674
  processRoutesFromExtracted(graph, chunkWorkerData.routes ?? [], ctx, (current, total) => {
@@ -648,14 +677,14 @@ async function runChunkedParseAndResolve(graph, ctx, scannedFiles, allPaths, tot
648
677
  percent: Math.round(chunkBasePercent),
649
678
  message: `Resolving routes (chunk ${chunkIdx + 1}/${numChunks})...`,
650
679
  detail: `${current}/${total} routes`,
651
- stats: { filesProcessed: filesParsedSoFar, totalFiles: totalParseable, nodesCreated: graph.nodeCount },
680
+ stats: {
681
+ filesProcessed: filesParsedSoFar,
682
+ totalFiles: totalParseable,
683
+ nodesCreated: graph.nodeCount,
684
+ },
652
685
  });
653
686
  }),
654
687
  ]);
655
- // Process field write assignments (synchronous, runs after calls resolve)
656
- if (chunkWorkerData.assignments?.length) {
657
- processAssignmentsFromExtracted(graph, chunkWorkerData.assignments, ctx, chunkWorkerData.constructorBindings);
658
- }
659
688
  // Collect TypeEnv file-scope bindings for exported type enrichment
660
689
  if (chunkWorkerData.typeEnvBindings?.length) {
661
690
  workerTypeEnvBindings.push(...chunkWorkerData.typeEnvBindings);
@@ -686,6 +715,28 @@ async function runChunkedParseAndResolve(graph, ctx, scannedFiles, allPaths, tot
686
715
  astCache.clear();
687
716
  // chunkContents + chunkFiles + chunkWorkerData go out of scope → GC reclaims
688
717
  }
718
+ // Complete implementor map from all worker heritage, then resolve CALLS once (interface dispatch).
719
+ const fullWorkerImplementorMap = deferredWorkerHeritage.length > 0
720
+ ? buildImplementorMap(deferredWorkerHeritage, ctx)
721
+ : new Map();
722
+ if (deferredWorkerCalls.length > 0) {
723
+ await processCallsFromExtracted(graph, deferredWorkerCalls, ctx, (current, total) => {
724
+ onProgress({
725
+ phase: 'parsing',
726
+ percent: 82,
727
+ message: 'Resolving calls (all chunks)...',
728
+ detail: `${current}/${total} files`,
729
+ stats: {
730
+ filesProcessed: filesParsedSoFar,
731
+ totalFiles: totalParseable,
732
+ nodesCreated: graph.nodeCount,
733
+ },
734
+ });
735
+ }, deferredConstructorBindings.length > 0 ? deferredConstructorBindings : undefined, fullWorkerImplementorMap);
736
+ }
737
+ if (deferredAssignments.length > 0) {
738
+ processAssignmentsFromExtracted(graph, deferredAssignments, ctx, deferredConstructorBindings.length > 0 ? deferredConstructorBindings : undefined);
739
+ }
689
740
  }
690
741
  finally {
691
742
  await workerPool?.terminate();
@@ -695,13 +746,18 @@ async function runChunkedParseAndResolve(graph, ctx, scannedFiles, allPaths, tot
695
746
  // before any call resolution — same rationale as the worker-path inline synthesis.
696
747
  if (sequentialChunkPaths.length > 0)
697
748
  synthesizeWildcardImportBindings(graph, ctx);
749
+ // Merge implementor-map deltas per chunk (O(heritage per chunk)), not O(|edges|) graph scans
750
+ // per chunk — mirrors worker-path deferred heritage without re-iterating all relationships.
751
+ const sequentialImplementorMap = new Map();
698
752
  for (const chunkPaths of sequentialChunkPaths) {
699
753
  const chunkContents = await readFileContents(repoPath, chunkPaths);
700
754
  const chunkFiles = chunkPaths
701
- .filter(p => chunkContents.has(p))
702
- .map(p => ({ path: p, content: chunkContents.get(p) }));
755
+ .filter((p) => chunkContents.has(p))
756
+ .map((p) => ({ path: p, content: chunkContents.get(p) }));
703
757
  astCache = createASTCache(chunkFiles.length);
704
- const rubyHeritage = await processCalls(graph, chunkFiles, astCache, ctx, undefined, exportedTypeMap);
758
+ const sequentialHeritage = await extractExtractedHeritageFromFiles(chunkFiles, astCache);
759
+ mergeImplementorMaps(sequentialImplementorMap, buildImplementorMap(sequentialHeritage, ctx));
760
+ const rubyHeritage = await processCalls(graph, chunkFiles, astCache, ctx, undefined, exportedTypeMap, undefined, undefined, undefined, sequentialImplementorMap);
705
761
  await processHeritage(graph, chunkFiles, astCache, ctx);
706
762
  if (rubyHeritage.length > 0) {
707
763
  await processHeritageFromExtracted(graph, rubyHeritage, ctx);
@@ -769,7 +825,14 @@ async function runChunkedParseAndResolve(graph, ctx, scannedFiles, allPaths, tot
769
825
  importCtx.resolveCache.clear();
770
826
  importCtx.index = EMPTY_INDEX; // Release suffix index memory (~30MB for large repos)
771
827
  importCtx.normalizedFileList = [];
772
- return { exportedTypeMap, allFetchCalls, allExtractedRoutes, allDecoratorRoutes, allToolDefs, allORMQueries };
828
+ return {
829
+ exportedTypeMap,
830
+ allFetchCalls,
831
+ allExtractedRoutes,
832
+ allDecoratorRoutes,
833
+ allToolDefs,
834
+ allORMQueries,
835
+ };
773
836
  }
774
837
  /**
775
838
  * Post-parse graph analysis: MRO, community detection, process extraction.
@@ -797,7 +860,7 @@ async function runGraphAnalysisPhases(graph, totalFiles, onProgress, routeRegist
797
860
  stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
798
861
  });
799
862
  const communityResult = await processCommunities(graph, (message, progress) => {
800
- const communityProgress = 82 + (progress * 0.10);
863
+ const communityProgress = 82 + progress * 0.1;
801
864
  onProgress({
802
865
  phase: 'communities',
803
866
  percent: Math.round(communityProgress),
@@ -808,7 +871,7 @@ async function runGraphAnalysisPhases(graph, totalFiles, onProgress, routeRegist
808
871
  if (isDev) {
809
872
  console.log(`🏘️ Community detection: ${communityResult.stats.totalCommunities} communities found (modularity: ${communityResult.stats.modularity.toFixed(3)})`);
810
873
  }
811
- communityResult.communities.forEach(comm => {
874
+ communityResult.communities.forEach((comm) => {
812
875
  graph.addNode({
813
876
  id: comm.id,
814
877
  label: 'Community',
@@ -818,10 +881,10 @@ async function runGraphAnalysisPhases(graph, totalFiles, onProgress, routeRegist
818
881
  heuristicLabel: comm.heuristicLabel,
819
882
  cohesion: comm.cohesion,
820
883
  symbolCount: comm.symbolCount,
821
- }
884
+ },
822
885
  });
823
886
  });
824
- communityResult.memberships.forEach(membership => {
887
+ communityResult.memberships.forEach((membership) => {
825
888
  graph.addRelationship({
826
889
  id: `${membership.nodeId}_member_of_${membership.communityId}`,
827
890
  type: 'MEMBER_OF',
@@ -839,11 +902,13 @@ async function runGraphAnalysisPhases(graph, totalFiles, onProgress, routeRegist
839
902
  stats: { filesProcessed: totalFiles, totalFiles, nodesCreated: graph.nodeCount },
840
903
  });
841
904
  let symbolCount = 0;
842
- graph.forEachNode(n => { if (n.label !== 'File')
843
- symbolCount++; });
905
+ graph.forEachNode((n) => {
906
+ if (n.label !== 'File')
907
+ symbolCount++;
908
+ });
844
909
  const dynamicMaxProcesses = Math.max(20, Math.min(300, Math.round(symbolCount / 10)));
845
910
  const processResult = await processProcesses(graph, communityResult.memberships, (message, progress) => {
846
- const processProgress = 94 + (progress * 0.05);
911
+ const processProgress = 94 + progress * 0.05;
847
912
  onProgress({
848
913
  phase: 'processes',
849
914
  percent: Math.round(processProgress),
@@ -854,7 +919,7 @@ async function runGraphAnalysisPhases(graph, totalFiles, onProgress, routeRegist
854
919
  if (isDev) {
855
920
  console.log(`🔄 Process detection: ${processResult.stats.totalProcesses} processes found (${processResult.stats.crossCommunityCount} cross-community)`);
856
921
  }
857
- processResult.processes.forEach(proc => {
922
+ processResult.processes.forEach((proc) => {
858
923
  graph.addNode({
859
924
  id: proc.id,
860
925
  label: 'Process',
@@ -867,10 +932,10 @@ async function runGraphAnalysisPhases(graph, totalFiles, onProgress, routeRegist
867
932
  communities: proc.communities,
868
933
  entryPointId: proc.entryPointId,
869
934
  terminalId: proc.terminalId,
870
- }
935
+ },
871
936
  });
872
937
  });
873
- processResult.steps.forEach(step => {
938
+ processResult.steps.forEach((step) => {
874
939
  graph.addRelationship({
875
940
  id: `${step.nodeId}_step_${step.step}_${step.processId}`,
876
941
  type: 'STEP_IN_PROCESS',
@@ -962,7 +1027,7 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
962
1027
  // Phase 1+2: Scan paths, build structure, process markdown
963
1028
  const { scannedFiles, allPaths, totalFiles } = await runScanAndStructure(repoPath, graph, onProgress);
964
1029
  // Phase 3+4: Chunked parse + resolve (imports, calls, heritage, routes)
965
- const { exportedTypeMap, allFetchCalls, allExtractedRoutes, allDecoratorRoutes, allToolDefs, allORMQueries } = await runChunkedParseAndResolve(graph, ctx, scannedFiles, allPaths, totalFiles, repoPath, pipelineStart, onProgress);
1030
+ const { exportedTypeMap, allFetchCalls, allExtractedRoutes, allDecoratorRoutes, allToolDefs, allORMQueries, } = await runChunkedParseAndResolve(graph, ctx, scannedFiles, allPaths, totalFiles, repoPath, pipelineStart, onProgress);
966
1031
  const routeRegistry = new Map();
967
1032
  // Detect Expo Router app/ roots vs Next.js app/ roots (monorepo-safe).
968
1033
  const expoAppRoots = new Set();
@@ -1009,7 +1074,7 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
1009
1074
  }
1010
1075
  }
1011
1076
  }
1012
- const ensureSlash = (path) => path.startsWith('/') ? path : '/' + path;
1077
+ const ensureSlash = (path) => (path.startsWith('/') ? path : '/' + path);
1013
1078
  let duplicateRoutes = 0;
1014
1079
  const addRoute = (url, entry) => {
1015
1080
  if (routeRegistry.has(url)) {
@@ -1021,20 +1086,28 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
1021
1086
  for (const route of allExtractedRoutes) {
1022
1087
  if (!route.routePath)
1023
1088
  continue;
1024
- addRoute(ensureSlash(route.routePath), { filePath: route.filePath, source: 'framework-route' });
1089
+ addRoute(ensureSlash(route.routePath), {
1090
+ filePath: route.filePath,
1091
+ source: 'framework-route',
1092
+ });
1025
1093
  }
1026
1094
  for (const dr of allDecoratorRoutes) {
1027
- addRoute(ensureSlash(dr.routePath), { filePath: dr.filePath, source: `decorator-${dr.decoratorName}` });
1095
+ addRoute(ensureSlash(dr.routePath), {
1096
+ filePath: dr.filePath,
1097
+ source: `decorator-${dr.decoratorName}`,
1098
+ });
1028
1099
  }
1029
1100
  let handlerContents;
1030
1101
  if (routeRegistry.size > 0) {
1031
- const handlerPaths = [...routeRegistry.values()].map(e => e.filePath);
1102
+ const handlerPaths = [...routeRegistry.values()].map((e) => e.filePath);
1032
1103
  handlerContents = await readFileContents(repoPath, handlerPaths);
1033
1104
  for (const [routeURL, entry] of routeRegistry) {
1034
1105
  const { filePath: handlerPath, source: routeSource } = entry;
1035
1106
  const content = handlerContents.get(handlerPath);
1036
1107
  const { responseKeys, errorKeys } = content
1037
- ? (handlerPath.endsWith(".php") ? extractPHPResponseShapes(content) : extractResponseShapes(content))
1108
+ ? handlerPath.endsWith('.php')
1109
+ ? extractPHPResponseShapes(content)
1110
+ : extractResponseShapes(content)
1038
1111
  : { responseKeys: undefined, errorKeys: undefined };
1039
1112
  const mwResult = content ? extractMiddlewareChain(content) : undefined;
1040
1113
  const middleware = mwResult?.chain;
@@ -1066,23 +1139,29 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
1066
1139
  }
1067
1140
  // ── Phase 3.5b: Link Next.js project-level middleware.ts to routes ──
1068
1141
  if (routeRegistry.size > 0) {
1069
- const middlewareCandidates = allPaths.filter(p => p === 'middleware.ts' || p === 'middleware.js' || p === 'middleware.tsx' || p === 'middleware.jsx' ||
1070
- p === 'src/middleware.ts' || p === 'src/middleware.js' || p === 'src/middleware.tsx' || p === 'src/middleware.jsx');
1142
+ const middlewareCandidates = allPaths.filter((p) => p === 'middleware.ts' ||
1143
+ p === 'middleware.js' ||
1144
+ p === 'middleware.tsx' ||
1145
+ p === 'middleware.jsx' ||
1146
+ p === 'src/middleware.ts' ||
1147
+ p === 'src/middleware.js' ||
1148
+ p === 'src/middleware.tsx' ||
1149
+ p === 'src/middleware.jsx');
1071
1150
  if (middlewareCandidates.length > 0) {
1072
1151
  const mwContents = await readFileContents(repoPath, middlewareCandidates);
1073
1152
  for (const [mwPath, mwContent] of mwContents) {
1074
1153
  const config = extractNextjsMiddlewareConfig(mwContent);
1075
1154
  if (!config)
1076
1155
  continue;
1077
- const mwLabel = config.wrappedFunctions.length > 0
1078
- ? config.wrappedFunctions
1079
- : [config.exportedName];
1156
+ const mwLabel = config.wrappedFunctions.length > 0 ? config.wrappedFunctions : [config.exportedName];
1080
1157
  // Pre-compile matchers once per middleware file
1081
- const compiled = config.matchers.map(compileMatcher).filter((m) => m !== null);
1158
+ const compiled = config.matchers
1159
+ .map(compileMatcher)
1160
+ .filter((m) => m !== null);
1082
1161
  let linkedCount = 0;
1083
1162
  for (const [routeURL] of routeRegistry) {
1084
1163
  const matches = compiled.length === 0 ||
1085
- compiled.some(cm => compiledMatcherMatchesRoute(cm, routeURL));
1164
+ compiled.some((cm) => compiledMatcherMatchesRoute(cm, routeURL));
1086
1165
  if (!matches)
1087
1166
  continue;
1088
1167
  const routeNodeId = generateId('Route', routeURL);
@@ -1091,7 +1170,10 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
1091
1170
  continue;
1092
1171
  const currentMw = existing.properties.middleware ?? [];
1093
1172
  // Prepend project-level middleware (runs before handler-level wrappers)
1094
- existing.properties.middleware = [...mwLabel, ...currentMw.filter(m => !mwLabel.includes(m))];
1173
+ existing.properties.middleware = [
1174
+ ...mwLabel,
1175
+ ...currentMw.filter((m) => !mwLabel.includes(m)),
1176
+ ];
1095
1177
  linkedCount++;
1096
1178
  }
1097
1179
  if (isDev && linkedCount > 0) {
@@ -1103,8 +1185,11 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
1103
1185
  // Scan HTML/PHP/template files for <form action="/path"> and AJAX url patterns
1104
1186
  // Scan HTML/template files for <form action="/path"> and AJAX url patterns
1105
1187
  // Skip .php — already parsed by tree-sitter with http_client/fetch queries
1106
- const htmlCandidates = allPaths.filter(p => p.endsWith('.html') || p.endsWith('.htm') ||
1107
- p.endsWith('.ejs') || p.endsWith('.hbs') || p.endsWith('.blade.php'));
1188
+ const htmlCandidates = allPaths.filter((p) => p.endsWith('.html') ||
1189
+ p.endsWith('.htm') ||
1190
+ p.endsWith('.ejs') ||
1191
+ p.endsWith('.hbs') ||
1192
+ p.endsWith('.blade.php'));
1108
1193
  if (htmlCandidates.length > 0 && routeRegistry.size > 0) {
1109
1194
  const htmlContents = await readFileContents(repoPath, htmlCandidates);
1110
1195
  const htmlPatterns = [/action=["']([^"']+)["']/g, /url:\s*["']([^"']+)["']/g];
@@ -1123,8 +1208,10 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
1123
1208
  }
1124
1209
  // ── Phase 3.5c: Extract Expo Router navigation patterns ──
1125
1210
  if (expoAppPaths.size > 0 && routeRegistry.size > 0) {
1126
- const unreadExpoPaths = [...expoAppPaths].filter(p => !handlerContents?.has(p));
1127
- const extraContents = unreadExpoPaths.length > 0 ? await readFileContents(repoPath, unreadExpoPaths) : new Map();
1211
+ const unreadExpoPaths = [...expoAppPaths].filter((p) => !handlerContents?.has(p));
1212
+ const extraContents = unreadExpoPaths.length > 0
1213
+ ? await readFileContents(repoPath, unreadExpoPaths)
1214
+ : new Map();
1128
1215
  const allExpoContents = new Map([...(handlerContents ?? new Map()), ...extraContents]);
1129
1216
  for (const [filePath, content] of allExpoContents) {
1130
1217
  if (!expoAppPaths.has(filePath))
@@ -1146,7 +1233,7 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
1146
1233
  for (const [url, entry] of routeRegistry)
1147
1234
  routeURLToFile.set(url, entry.filePath);
1148
1235
  // Read consumer file contents so we can extract property access patterns
1149
- const consumerPaths = [...new Set(allFetchCalls.map(c => c.filePath))];
1236
+ const consumerPaths = [...new Set(allFetchCalls.map((c) => c.filePath))];
1150
1237
  const consumerContents = await readFileContents(repoPath, consumerPaths);
1151
1238
  processNextjsFetchRoutes(graph, allFetchCalls, routeURLToFile, consumerContents);
1152
1239
  if (isDev) {
@@ -1163,8 +1250,11 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
1163
1250
  toolDefs.push({ name: td.toolName, filePath: td.filePath, description: td.description });
1164
1251
  }
1165
1252
  // TS tool definition arrays — require inputSchema nearby to distinguish from config objects
1166
- const toolCandidatePaths = allPaths.filter(p => (p.endsWith('.ts') || p.endsWith('.js')) && p.toLowerCase().includes('tool')
1167
- && !p.includes('node_modules') && !p.includes('test') && !p.includes('__'));
1253
+ const toolCandidatePaths = allPaths.filter((p) => (p.endsWith('.ts') || p.endsWith('.js')) &&
1254
+ p.toLowerCase().includes('tool') &&
1255
+ !p.includes('node_modules') &&
1256
+ !p.includes('test') &&
1257
+ !p.includes('__'));
1168
1258
  if (toolCandidatePaths.length > 0) {
1169
1259
  const toolContents = await readFileContents(repoPath, toolCandidatePaths);
1170
1260
  for (const [filePath, content] of toolContents) {
@@ -1178,7 +1268,11 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
1178
1268
  if (seenToolNames.has(name))
1179
1269
  continue;
1180
1270
  seenToolNames.add(name);
1181
- toolDefs.push({ name, filePath, description: match[2].slice(0, 200).replace(/\n/g, ' ').trim() });
1271
+ toolDefs.push({
1272
+ name,
1273
+ filePath,
1274
+ description: match[2].slice(0, 200).replace(/\n/g, ' ').trim(),
1275
+ });
1182
1276
  }
1183
1277
  }
1184
1278
  }
@@ -1228,7 +1322,7 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
1228
1322
  stats: {
1229
1323
  filesProcessed: totalFiles,
1230
1324
  totalFiles,
1231
- nodesCreated: graph.nodeCount
1325
+ nodesCreated: graph.nodeCount,
1232
1326
  },
1233
1327
  });
1234
1328
  return { graph, repoPath, totalFileCount: totalFiles, communityResult, processResult };
@@ -1253,14 +1347,26 @@ function extractORMQueriesInline(filePath, content, out) {
1253
1347
  const model = m[1];
1254
1348
  if (model.startsWith('$'))
1255
1349
  continue;
1256
- out.push({ filePath, orm: 'prisma', model, method: m[2], lineNumber: content.substring(0, m.index).split('\n').length - 1 });
1350
+ out.push({
1351
+ filePath,
1352
+ orm: 'prisma',
1353
+ model,
1354
+ method: m[2],
1355
+ lineNumber: content.substring(0, m.index).split('\n').length - 1,
1356
+ });
1257
1357
  }
1258
1358
  }
1259
1359
  if (hasSupabase) {
1260
1360
  SUPABASE_QUERY_RE.lastIndex = 0;
1261
1361
  let m;
1262
1362
  while ((m = SUPABASE_QUERY_RE.exec(content)) !== null) {
1263
- out.push({ filePath, orm: 'supabase', model: m[1], method: m[2], lineNumber: content.substring(0, m.index).split('\n').length - 1 });
1363
+ out.push({
1364
+ filePath,
1365
+ orm: 'supabase',
1366
+ model: m[1],
1367
+ method: m[2],
1368
+ lineNumber: content.substring(0, m.index).split('\n').length - 1,
1369
+ });
1264
1370
  }
1265
1371
  }
1266
1372
  }
@@ -1280,7 +1386,7 @@ function processORMQueries(graph, queries, isDev) {
1280
1386
  generateId('Interface', `${q.model}`),
1281
1387
  generateId('CodeElement', `${q.model}`),
1282
1388
  ];
1283
- const existing = candidateIds.find(id => graph.getNode(id));
1389
+ const existing = candidateIds.find((id) => graph.getNode(id));
1284
1390
  if (existing) {
1285
1391
  modelNodeId = existing;
1286
1392
  }