spec-gen-cli 1.0.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 (303) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1078 -0
  3. package/dist/api/analyze.d.ts +17 -0
  4. package/dist/api/analyze.d.ts.map +1 -0
  5. package/dist/api/analyze.js +109 -0
  6. package/dist/api/analyze.js.map +1 -0
  7. package/dist/api/drift.d.ts +21 -0
  8. package/dist/api/drift.d.ts.map +1 -0
  9. package/dist/api/drift.js +145 -0
  10. package/dist/api/drift.js.map +1 -0
  11. package/dist/api/generate.d.ts +18 -0
  12. package/dist/api/generate.d.ts.map +1 -0
  13. package/dist/api/generate.js +251 -0
  14. package/dist/api/generate.js.map +1 -0
  15. package/dist/api/index.d.ts +39 -0
  16. package/dist/api/index.d.ts.map +1 -0
  17. package/dist/api/index.js +32 -0
  18. package/dist/api/index.js.map +1 -0
  19. package/dist/api/init.d.ts +18 -0
  20. package/dist/api/init.d.ts.map +1 -0
  21. package/dist/api/init.js +82 -0
  22. package/dist/api/init.js.map +1 -0
  23. package/dist/api/run.d.ts +19 -0
  24. package/dist/api/run.d.ts.map +1 -0
  25. package/dist/api/run.js +291 -0
  26. package/dist/api/run.js.map +1 -0
  27. package/dist/api/specs.d.ts +49 -0
  28. package/dist/api/specs.d.ts.map +1 -0
  29. package/dist/api/specs.js +136 -0
  30. package/dist/api/specs.js.map +1 -0
  31. package/dist/api/types.d.ts +176 -0
  32. package/dist/api/types.d.ts.map +1 -0
  33. package/dist/api/types.js +9 -0
  34. package/dist/api/types.js.map +1 -0
  35. package/dist/api/verify.d.ts +20 -0
  36. package/dist/api/verify.d.ts.map +1 -0
  37. package/dist/api/verify.js +117 -0
  38. package/dist/api/verify.js.map +1 -0
  39. package/dist/cli/commands/analyze.d.ts +27 -0
  40. package/dist/cli/commands/analyze.d.ts.map +1 -0
  41. package/dist/cli/commands/analyze.js +485 -0
  42. package/dist/cli/commands/analyze.js.map +1 -0
  43. package/dist/cli/commands/drift.d.ts +9 -0
  44. package/dist/cli/commands/drift.d.ts.map +1 -0
  45. package/dist/cli/commands/drift.js +540 -0
  46. package/dist/cli/commands/drift.js.map +1 -0
  47. package/dist/cli/commands/generate.d.ts +9 -0
  48. package/dist/cli/commands/generate.d.ts.map +1 -0
  49. package/dist/cli/commands/generate.js +633 -0
  50. package/dist/cli/commands/generate.js.map +1 -0
  51. package/dist/cli/commands/init.d.ts +9 -0
  52. package/dist/cli/commands/init.d.ts.map +1 -0
  53. package/dist/cli/commands/init.js +171 -0
  54. package/dist/cli/commands/init.js.map +1 -0
  55. package/dist/cli/commands/mcp.d.ts +638 -0
  56. package/dist/cli/commands/mcp.d.ts.map +1 -0
  57. package/dist/cli/commands/mcp.js +574 -0
  58. package/dist/cli/commands/mcp.js.map +1 -0
  59. package/dist/cli/commands/run.d.ts +24 -0
  60. package/dist/cli/commands/run.d.ts.map +1 -0
  61. package/dist/cli/commands/run.js +546 -0
  62. package/dist/cli/commands/run.js.map +1 -0
  63. package/dist/cli/commands/verify.d.ts +9 -0
  64. package/dist/cli/commands/verify.d.ts.map +1 -0
  65. package/dist/cli/commands/verify.js +417 -0
  66. package/dist/cli/commands/verify.js.map +1 -0
  67. package/dist/cli/commands/view.d.ts +9 -0
  68. package/dist/cli/commands/view.d.ts.map +1 -0
  69. package/dist/cli/commands/view.js +511 -0
  70. package/dist/cli/commands/view.js.map +1 -0
  71. package/dist/cli/index.d.ts +9 -0
  72. package/dist/cli/index.d.ts.map +1 -0
  73. package/dist/cli/index.js +83 -0
  74. package/dist/cli/index.js.map +1 -0
  75. package/dist/core/analyzer/architecture-writer.d.ts +67 -0
  76. package/dist/core/analyzer/architecture-writer.d.ts.map +1 -0
  77. package/dist/core/analyzer/architecture-writer.js +209 -0
  78. package/dist/core/analyzer/architecture-writer.js.map +1 -0
  79. package/dist/core/analyzer/artifact-generator.d.ts +222 -0
  80. package/dist/core/analyzer/artifact-generator.d.ts.map +1 -0
  81. package/dist/core/analyzer/artifact-generator.js +726 -0
  82. package/dist/core/analyzer/artifact-generator.js.map +1 -0
  83. package/dist/core/analyzer/call-graph.d.ts +83 -0
  84. package/dist/core/analyzer/call-graph.d.ts.map +1 -0
  85. package/dist/core/analyzer/call-graph.js +827 -0
  86. package/dist/core/analyzer/call-graph.js.map +1 -0
  87. package/dist/core/analyzer/code-shaper.d.ts +33 -0
  88. package/dist/core/analyzer/code-shaper.d.ts.map +1 -0
  89. package/dist/core/analyzer/code-shaper.js +149 -0
  90. package/dist/core/analyzer/code-shaper.js.map +1 -0
  91. package/dist/core/analyzer/dependency-graph.d.ts +179 -0
  92. package/dist/core/analyzer/dependency-graph.d.ts.map +1 -0
  93. package/dist/core/analyzer/dependency-graph.js +574 -0
  94. package/dist/core/analyzer/dependency-graph.js.map +1 -0
  95. package/dist/core/analyzer/duplicate-detector.d.ts +52 -0
  96. package/dist/core/analyzer/duplicate-detector.d.ts.map +1 -0
  97. package/dist/core/analyzer/duplicate-detector.js +279 -0
  98. package/dist/core/analyzer/duplicate-detector.js.map +1 -0
  99. package/dist/core/analyzer/embedding-service.d.ts +50 -0
  100. package/dist/core/analyzer/embedding-service.d.ts.map +1 -0
  101. package/dist/core/analyzer/embedding-service.js +104 -0
  102. package/dist/core/analyzer/embedding-service.js.map +1 -0
  103. package/dist/core/analyzer/file-walker.d.ts +78 -0
  104. package/dist/core/analyzer/file-walker.d.ts.map +1 -0
  105. package/dist/core/analyzer/file-walker.js +531 -0
  106. package/dist/core/analyzer/file-walker.js.map +1 -0
  107. package/dist/core/analyzer/import-parser.d.ts +91 -0
  108. package/dist/core/analyzer/import-parser.d.ts.map +1 -0
  109. package/dist/core/analyzer/import-parser.js +720 -0
  110. package/dist/core/analyzer/import-parser.js.map +1 -0
  111. package/dist/core/analyzer/index.d.ts +10 -0
  112. package/dist/core/analyzer/index.d.ts.map +1 -0
  113. package/dist/core/analyzer/index.js +10 -0
  114. package/dist/core/analyzer/index.js.map +1 -0
  115. package/dist/core/analyzer/refactor-analyzer.d.ts +80 -0
  116. package/dist/core/analyzer/refactor-analyzer.d.ts.map +1 -0
  117. package/dist/core/analyzer/refactor-analyzer.js +339 -0
  118. package/dist/core/analyzer/refactor-analyzer.js.map +1 -0
  119. package/dist/core/analyzer/repository-mapper.d.ts +150 -0
  120. package/dist/core/analyzer/repository-mapper.d.ts.map +1 -0
  121. package/dist/core/analyzer/repository-mapper.js +731 -0
  122. package/dist/core/analyzer/repository-mapper.js.map +1 -0
  123. package/dist/core/analyzer/signature-extractor.d.ts +31 -0
  124. package/dist/core/analyzer/signature-extractor.d.ts.map +1 -0
  125. package/dist/core/analyzer/signature-extractor.js +387 -0
  126. package/dist/core/analyzer/signature-extractor.js.map +1 -0
  127. package/dist/core/analyzer/significance-scorer.d.ts +79 -0
  128. package/dist/core/analyzer/significance-scorer.d.ts.map +1 -0
  129. package/dist/core/analyzer/significance-scorer.js +407 -0
  130. package/dist/core/analyzer/significance-scorer.js.map +1 -0
  131. package/dist/core/analyzer/subgraph-extractor.d.ts +43 -0
  132. package/dist/core/analyzer/subgraph-extractor.d.ts.map +1 -0
  133. package/dist/core/analyzer/subgraph-extractor.js +129 -0
  134. package/dist/core/analyzer/subgraph-extractor.js.map +1 -0
  135. package/dist/core/analyzer/vector-index.d.ts +63 -0
  136. package/dist/core/analyzer/vector-index.d.ts.map +1 -0
  137. package/dist/core/analyzer/vector-index.js +169 -0
  138. package/dist/core/analyzer/vector-index.js.map +1 -0
  139. package/dist/core/drift/drift-detector.d.ts +102 -0
  140. package/dist/core/drift/drift-detector.d.ts.map +1 -0
  141. package/dist/core/drift/drift-detector.js +597 -0
  142. package/dist/core/drift/drift-detector.js.map +1 -0
  143. package/dist/core/drift/git-diff.d.ts +55 -0
  144. package/dist/core/drift/git-diff.d.ts.map +1 -0
  145. package/dist/core/drift/git-diff.js +356 -0
  146. package/dist/core/drift/git-diff.js.map +1 -0
  147. package/dist/core/drift/index.d.ts +12 -0
  148. package/dist/core/drift/index.d.ts.map +1 -0
  149. package/dist/core/drift/index.js +9 -0
  150. package/dist/core/drift/index.js.map +1 -0
  151. package/dist/core/drift/spec-mapper.d.ts +73 -0
  152. package/dist/core/drift/spec-mapper.d.ts.map +1 -0
  153. package/dist/core/drift/spec-mapper.js +353 -0
  154. package/dist/core/drift/spec-mapper.js.map +1 -0
  155. package/dist/core/generator/adr-generator.d.ts +32 -0
  156. package/dist/core/generator/adr-generator.d.ts.map +1 -0
  157. package/dist/core/generator/adr-generator.js +192 -0
  158. package/dist/core/generator/adr-generator.js.map +1 -0
  159. package/dist/core/generator/index.d.ts +9 -0
  160. package/dist/core/generator/index.d.ts.map +1 -0
  161. package/dist/core/generator/index.js +12 -0
  162. package/dist/core/generator/index.js.map +1 -0
  163. package/dist/core/generator/mapping-generator.d.ts +54 -0
  164. package/dist/core/generator/mapping-generator.d.ts.map +1 -0
  165. package/dist/core/generator/mapping-generator.js +239 -0
  166. package/dist/core/generator/mapping-generator.js.map +1 -0
  167. package/dist/core/generator/openspec-compat.d.ts +160 -0
  168. package/dist/core/generator/openspec-compat.d.ts.map +1 -0
  169. package/dist/core/generator/openspec-compat.js +523 -0
  170. package/dist/core/generator/openspec-compat.js.map +1 -0
  171. package/dist/core/generator/openspec-format-generator.d.ts +111 -0
  172. package/dist/core/generator/openspec-format-generator.d.ts.map +1 -0
  173. package/dist/core/generator/openspec-format-generator.js +817 -0
  174. package/dist/core/generator/openspec-format-generator.js.map +1 -0
  175. package/dist/core/generator/openspec-writer.d.ts +131 -0
  176. package/dist/core/generator/openspec-writer.d.ts.map +1 -0
  177. package/dist/core/generator/openspec-writer.js +379 -0
  178. package/dist/core/generator/openspec-writer.js.map +1 -0
  179. package/dist/core/generator/prompts.d.ts +35 -0
  180. package/dist/core/generator/prompts.d.ts.map +1 -0
  181. package/dist/core/generator/prompts.js +212 -0
  182. package/dist/core/generator/prompts.js.map +1 -0
  183. package/dist/core/generator/spec-pipeline.d.ts +94 -0
  184. package/dist/core/generator/spec-pipeline.d.ts.map +1 -0
  185. package/dist/core/generator/spec-pipeline.js +474 -0
  186. package/dist/core/generator/spec-pipeline.js.map +1 -0
  187. package/dist/core/generator/stages/stage1-survey.d.ts +19 -0
  188. package/dist/core/generator/stages/stage1-survey.d.ts.map +1 -0
  189. package/dist/core/generator/stages/stage1-survey.js +105 -0
  190. package/dist/core/generator/stages/stage1-survey.js.map +1 -0
  191. package/dist/core/generator/stages/stage2-entities.d.ts +11 -0
  192. package/dist/core/generator/stages/stage2-entities.d.ts.map +1 -0
  193. package/dist/core/generator/stages/stage2-entities.js +67 -0
  194. package/dist/core/generator/stages/stage2-entities.js.map +1 -0
  195. package/dist/core/generator/stages/stage3-services.d.ts +11 -0
  196. package/dist/core/generator/stages/stage3-services.d.ts.map +1 -0
  197. package/dist/core/generator/stages/stage3-services.js +75 -0
  198. package/dist/core/generator/stages/stage3-services.js.map +1 -0
  199. package/dist/core/generator/stages/stage4-api.d.ts +11 -0
  200. package/dist/core/generator/stages/stage4-api.d.ts.map +1 -0
  201. package/dist/core/generator/stages/stage4-api.js +65 -0
  202. package/dist/core/generator/stages/stage4-api.js.map +1 -0
  203. package/dist/core/generator/stages/stage5-architecture.d.ts +10 -0
  204. package/dist/core/generator/stages/stage5-architecture.d.ts.map +1 -0
  205. package/dist/core/generator/stages/stage5-architecture.js +62 -0
  206. package/dist/core/generator/stages/stage5-architecture.js.map +1 -0
  207. package/dist/core/generator/stages/stage6-adr.d.ts +8 -0
  208. package/dist/core/generator/stages/stage6-adr.d.ts.map +1 -0
  209. package/dist/core/generator/stages/stage6-adr.js +41 -0
  210. package/dist/core/generator/stages/stage6-adr.js.map +1 -0
  211. package/dist/core/services/chat-agent.d.ts +45 -0
  212. package/dist/core/services/chat-agent.d.ts.map +1 -0
  213. package/dist/core/services/chat-agent.js +310 -0
  214. package/dist/core/services/chat-agent.js.map +1 -0
  215. package/dist/core/services/chat-tools.d.ts +32 -0
  216. package/dist/core/services/chat-tools.d.ts.map +1 -0
  217. package/dist/core/services/chat-tools.js +270 -0
  218. package/dist/core/services/chat-tools.js.map +1 -0
  219. package/dist/core/services/config-manager.d.ts +61 -0
  220. package/dist/core/services/config-manager.d.ts.map +1 -0
  221. package/dist/core/services/config-manager.js +143 -0
  222. package/dist/core/services/config-manager.js.map +1 -0
  223. package/dist/core/services/gitignore-manager.d.ts +29 -0
  224. package/dist/core/services/gitignore-manager.d.ts.map +1 -0
  225. package/dist/core/services/gitignore-manager.js +106 -0
  226. package/dist/core/services/gitignore-manager.js.map +1 -0
  227. package/dist/core/services/index.d.ts +8 -0
  228. package/dist/core/services/index.d.ts.map +1 -0
  229. package/dist/core/services/index.js +8 -0
  230. package/dist/core/services/index.js.map +1 -0
  231. package/dist/core/services/llm-service.d.ts +336 -0
  232. package/dist/core/services/llm-service.d.ts.map +1 -0
  233. package/dist/core/services/llm-service.js +1155 -0
  234. package/dist/core/services/llm-service.js.map +1 -0
  235. package/dist/core/services/mcp-handlers/analysis.d.ts +42 -0
  236. package/dist/core/services/mcp-handlers/analysis.d.ts.map +1 -0
  237. package/dist/core/services/mcp-handlers/analysis.js +300 -0
  238. package/dist/core/services/mcp-handlers/analysis.js.map +1 -0
  239. package/dist/core/services/mcp-handlers/graph.d.ts +65 -0
  240. package/dist/core/services/mcp-handlers/graph.d.ts.map +1 -0
  241. package/dist/core/services/mcp-handlers/graph.js +509 -0
  242. package/dist/core/services/mcp-handlers/graph.js.map +1 -0
  243. package/dist/core/services/mcp-handlers/semantic.d.ts +38 -0
  244. package/dist/core/services/mcp-handlers/semantic.d.ts.map +1 -0
  245. package/dist/core/services/mcp-handlers/semantic.js +172 -0
  246. package/dist/core/services/mcp-handlers/semantic.js.map +1 -0
  247. package/dist/core/services/mcp-handlers/utils.d.ts +21 -0
  248. package/dist/core/services/mcp-handlers/utils.d.ts.map +1 -0
  249. package/dist/core/services/mcp-handlers/utils.js +62 -0
  250. package/dist/core/services/mcp-handlers/utils.js.map +1 -0
  251. package/dist/core/services/project-detector.d.ts +32 -0
  252. package/dist/core/services/project-detector.d.ts.map +1 -0
  253. package/dist/core/services/project-detector.js +111 -0
  254. package/dist/core/services/project-detector.js.map +1 -0
  255. package/dist/core/verifier/index.d.ts +5 -0
  256. package/dist/core/verifier/index.d.ts.map +1 -0
  257. package/dist/core/verifier/index.js +5 -0
  258. package/dist/core/verifier/index.js.map +1 -0
  259. package/dist/core/verifier/verification-engine.d.ts +226 -0
  260. package/dist/core/verifier/verification-engine.d.ts.map +1 -0
  261. package/dist/core/verifier/verification-engine.js +681 -0
  262. package/dist/core/verifier/verification-engine.js.map +1 -0
  263. package/dist/types/index.d.ts +252 -0
  264. package/dist/types/index.d.ts.map +1 -0
  265. package/dist/types/index.js +5 -0
  266. package/dist/types/index.js.map +1 -0
  267. package/dist/types/pipeline.d.ts +148 -0
  268. package/dist/types/pipeline.d.ts.map +1 -0
  269. package/dist/types/pipeline.js +5 -0
  270. package/dist/types/pipeline.js.map +1 -0
  271. package/dist/utils/errors.d.ts +51 -0
  272. package/dist/utils/errors.d.ts.map +1 -0
  273. package/dist/utils/errors.js +128 -0
  274. package/dist/utils/errors.js.map +1 -0
  275. package/dist/utils/logger.d.ts +149 -0
  276. package/dist/utils/logger.d.ts.map +1 -0
  277. package/dist/utils/logger.js +331 -0
  278. package/dist/utils/logger.js.map +1 -0
  279. package/dist/utils/progress.d.ts +142 -0
  280. package/dist/utils/progress.d.ts.map +1 -0
  281. package/dist/utils/progress.js +280 -0
  282. package/dist/utils/progress.js.map +1 -0
  283. package/dist/utils/prompts.d.ts +53 -0
  284. package/dist/utils/prompts.d.ts.map +1 -0
  285. package/dist/utils/prompts.js +199 -0
  286. package/dist/utils/prompts.js.map +1 -0
  287. package/dist/utils/shutdown.d.ts +89 -0
  288. package/dist/utils/shutdown.d.ts.map +1 -0
  289. package/dist/utils/shutdown.js +237 -0
  290. package/dist/utils/shutdown.js.map +1 -0
  291. package/package.json +114 -0
  292. package/src/viewer/InteractiveGraphViewer.jsx +1486 -0
  293. package/src/viewer/app/index.html +17 -0
  294. package/src/viewer/app/main.jsx +13 -0
  295. package/src/viewer/components/ArchitectureView.jsx +177 -0
  296. package/src/viewer/components/ChatPanel.jsx +448 -0
  297. package/src/viewer/components/ClusterGraph.jsx +441 -0
  298. package/src/viewer/components/FilterBar.jsx +179 -0
  299. package/src/viewer/components/FlatGraph.jsx +275 -0
  300. package/src/viewer/components/MicroComponents.jsx +83 -0
  301. package/src/viewer/hooks/usePanZoom.js +79 -0
  302. package/src/viewer/utils/constants.js +47 -0
  303. package/src/viewer/utils/graph-helpers.js +291 -0
@@ -0,0 +1,827 @@
1
+ /**
2
+ * Call Graph Analyzer
3
+ *
4
+ * Performs static analysis of function calls across source files using tree-sitter.
5
+ * Supports TypeScript/JavaScript, Python, Go, Rust, Ruby, Java — no LLM, pure AST.
6
+ *
7
+ * Produces:
8
+ * - FunctionNode[] — all identified functions/methods
9
+ * - CallEdge[] — resolved function→function call relationships
10
+ * - Hub functions — high-fanIn nodes (called by many others)
11
+ * - Entry points — functions with no internal callers
12
+ * - Layer violations — cross-layer calls in the wrong direction
13
+ */
14
+ import Parser from 'tree-sitter';
15
+ // ============================================================================
16
+ // CONSTANTS
17
+ // ============================================================================
18
+ const HUB_THRESHOLD = 5;
19
+ /** Common builtins and stdlib names to ignore as call targets (across all languages) */
20
+ const IGNORED_CALLEES = new Set([
21
+ // Python builtins
22
+ 'print', 'len', 'range', 'str', 'int', 'float', 'list', 'dict', 'set', 'tuple',
23
+ 'bool', 'type', 'isinstance', 'issubclass', 'hasattr', 'getattr', 'setattr',
24
+ 'enumerate', 'zip', 'map', 'filter', 'sorted', 'reversed', 'sum', 'min', 'max',
25
+ 'open', 'input', 'format', 'repr', 'id', 'hash', 'abs', 'round', 'pow',
26
+ 'super', 'object', 'property', 'staticmethod', 'classmethod',
27
+ // JS/TS common
28
+ 'console', 'log', 'error', 'warn', 'JSON', 'parse', 'stringify',
29
+ 'Promise', 'resolve', 'reject', 'then', 'catch', 'finally',
30
+ 'Array', 'Object', 'String', 'Number', 'Boolean', 'Math', 'Date',
31
+ 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
32
+ 'require', 'import', 'exports',
33
+ // Python control flow (used like functions sometimes)
34
+ 'assert', 'raise', 'return', 'yield', 'await', 'pass', 'del',
35
+ // Node.js common
36
+ 'readFile', 'writeFile', 'mkdir', 'join', 'resolve', 'basename', 'dirname',
37
+ 'existsSync', 'readFileSync', 'writeFileSync',
38
+ // Go builtins
39
+ 'make', 'new', 'append', 'copy', 'delete', 'close', 'panic', 'recover',
40
+ 'println', 'printf', 'sprintf', 'errorf', 'fprintf',
41
+ // Rust macros / common stdlib
42
+ 'println', 'eprintln', 'format', 'vec', 'assert', 'unwrap', 'expect',
43
+ 'ok', 'err', 'some', 'none',
44
+ // Ruby builtins
45
+ 'puts', 'print', 'p', 'raise', 'require', 'require_relative', 'include',
46
+ 'extend', 'attr_accessor', 'attr_reader', 'attr_writer',
47
+ // Java common
48
+ 'toString', 'equals', 'hashCode', 'getClass', 'println', 'printf',
49
+ ]);
50
+ // ============================================================================
51
+ // PARSER SINGLETONS (lazy init)
52
+ // ============================================================================
53
+ let _tsParser;
54
+ let _pyParser;
55
+ let _goParser;
56
+ let _rustParser;
57
+ let _rubyParser;
58
+ let _javaParser;
59
+ let _TsLanguage;
60
+ let _PyLanguage;
61
+ let _GoLanguage;
62
+ let _RustLanguage;
63
+ let _RubyLanguage;
64
+ let _JavaLanguage;
65
+ async function getTSParser() {
66
+ if (!_tsParser) {
67
+ const tsModule = await import('tree-sitter-typescript');
68
+ _TsLanguage = tsModule.default.typescript;
69
+ _tsParser = new Parser();
70
+ _tsParser.setLanguage(_TsLanguage);
71
+ }
72
+ return { parser: _tsParser, lang: _TsLanguage };
73
+ }
74
+ async function getPyParser() {
75
+ if (!_pyParser) {
76
+ const pyModule = await import('tree-sitter-python');
77
+ _PyLanguage = pyModule.default;
78
+ _pyParser = new Parser();
79
+ _pyParser.setLanguage(_PyLanguage);
80
+ }
81
+ return { parser: _pyParser, lang: _PyLanguage };
82
+ }
83
+ async function getGoParser() {
84
+ if (!_goParser) {
85
+ const goModule = await import('tree-sitter-go');
86
+ _GoLanguage = goModule.default;
87
+ _goParser = new Parser();
88
+ _goParser.setLanguage(_GoLanguage);
89
+ }
90
+ return { parser: _goParser, lang: _GoLanguage };
91
+ }
92
+ async function getRustParser() {
93
+ if (!_rustParser) {
94
+ const rustModule = await import('tree-sitter-rust');
95
+ _RustLanguage = rustModule.default;
96
+ _rustParser = new Parser();
97
+ _rustParser.setLanguage(_RustLanguage);
98
+ }
99
+ return { parser: _rustParser, lang: _RustLanguage };
100
+ }
101
+ async function getRubyParser() {
102
+ if (!_rubyParser) {
103
+ const rubyModule = await import('tree-sitter-ruby');
104
+ _RubyLanguage = rubyModule.default;
105
+ _rubyParser = new Parser();
106
+ _rubyParser.setLanguage(_RubyLanguage);
107
+ }
108
+ return { parser: _rubyParser, lang: _RubyLanguage };
109
+ }
110
+ async function getJavaParser() {
111
+ if (!_javaParser) {
112
+ const javaModule = await import('tree-sitter-java');
113
+ _JavaLanguage = javaModule.default;
114
+ _javaParser = new Parser();
115
+ _javaParser.setLanguage(_JavaLanguage);
116
+ }
117
+ return { parser: _javaParser, lang: _JavaLanguage };
118
+ }
119
+ // ============================================================================
120
+ // ATTRIBUTION HELPER
121
+ // ============================================================================
122
+ /**
123
+ * Given a list of function nodes (with startIndex/endIndex) and a call position,
124
+ * find the narrowest enclosing function node.
125
+ */
126
+ function findEnclosingFunction(nodes, callPos) {
127
+ let best;
128
+ let bestSize = Infinity;
129
+ for (const n of nodes) {
130
+ if (n.startIndex <= callPos && callPos < n.endIndex) {
131
+ const size = n.endIndex - n.startIndex;
132
+ if (size < bestSize) {
133
+ bestSize = size;
134
+ best = n;
135
+ }
136
+ }
137
+ }
138
+ return best;
139
+ }
140
+ // ============================================================================
141
+ // TYPESCRIPT EXTRACTOR
142
+ // ============================================================================
143
+ const TS_FN_QUERY = `
144
+ (function_declaration
145
+ name: (identifier) @fn.name) @fn.node
146
+
147
+ (export_statement
148
+ declaration: (function_declaration
149
+ name: (identifier) @fn.name)) @fn.node
150
+
151
+ (method_definition
152
+ name: (property_identifier) @fn.name) @fn.node
153
+
154
+ (lexical_declaration
155
+ (variable_declarator
156
+ name: (identifier) @fn.name
157
+ value: [(arrow_function) (function_expression)] @fn.value)) @fn.node
158
+ `;
159
+ const TS_CALL_QUERY = `
160
+ (call_expression
161
+ function: [(identifier) @call.name
162
+ (member_expression
163
+ property: (property_identifier) @call.name)]) @call.node
164
+ `;
165
+ async function extractTSGraph(filePath, content) {
166
+ const { parser, lang } = await getTSParser();
167
+ const tree = parser.parse(content);
168
+ const fnQuery = new Parser.Query(lang, TS_FN_QUERY);
169
+ const callQuery = new Parser.Query(lang, TS_CALL_QUERY);
170
+ // --- Extract function nodes ---
171
+ const nodes = [];
172
+ const fnMatches = fnQuery.matches(tree.rootNode);
173
+ for (const match of fnMatches) {
174
+ const nameCapture = match.captures.find(c => c.name === 'fn.name');
175
+ const nodeCapture = match.captures.find(c => c.name === 'fn.node');
176
+ if (!nameCapture || !nodeCapture)
177
+ continue;
178
+ const name = nameCapture.node.text;
179
+ const fnNode = nodeCapture.node;
180
+ // Find enclosing class (walk up — skip class_body, its children are methods not the name)
181
+ let className;
182
+ let cursor = fnNode.parent;
183
+ while (cursor) {
184
+ if (cursor.type === 'class_declaration') {
185
+ const classNameNode = cursor.children.find(c => c.type === 'type_identifier' || c.type === 'identifier');
186
+ if (classNameNode)
187
+ className = classNameNode.text;
188
+ break;
189
+ }
190
+ cursor = cursor.parent;
191
+ }
192
+ // Detect async (method_definition has 'async' as first named child keyword)
193
+ const isAsync = fnNode.children.some(c => c.type === 'async') ||
194
+ fnNode.text.startsWith('async ');
195
+ const id = className
196
+ ? `${filePath}::${className}.${name}`
197
+ : `${filePath}::${name}`;
198
+ nodes.push({
199
+ id,
200
+ name,
201
+ filePath,
202
+ className,
203
+ isAsync,
204
+ language: 'TypeScript',
205
+ startIndex: fnNode.startIndex,
206
+ endIndex: fnNode.endIndex,
207
+ fanIn: 0,
208
+ fanOut: 0,
209
+ });
210
+ }
211
+ // --- Extract calls ---
212
+ const rawEdges = [];
213
+ const callMatches = callQuery.matches(tree.rootNode);
214
+ for (const match of callMatches) {
215
+ const nameCapture = match.captures.find(c => c.name === 'call.name');
216
+ const nodeCapture = match.captures.find(c => c.name === 'call.node');
217
+ if (!nameCapture || !nodeCapture)
218
+ continue;
219
+ const calleeName = nameCapture.node.text;
220
+ if (IGNORED_CALLEES.has(calleeName))
221
+ continue;
222
+ const callPos = nodeCapture.node.startIndex;
223
+ const caller = findEnclosingFunction(nodes, callPos);
224
+ if (!caller)
225
+ continue;
226
+ rawEdges.push({
227
+ callerId: caller.id,
228
+ calleeName,
229
+ line: nodeCapture.node.startPosition.row + 1,
230
+ });
231
+ }
232
+ return { nodes, rawEdges };
233
+ }
234
+ // ============================================================================
235
+ // PYTHON EXTRACTOR
236
+ // ============================================================================
237
+ const PY_FN_QUERY = `
238
+ (function_definition
239
+ name: (identifier) @fn.name) @fn.node
240
+
241
+ (decorated_definition
242
+ (function_definition
243
+ name: (identifier) @fn.name)) @fn.node
244
+ `;
245
+ /**
246
+ * Direct function calls: foo(), bar(x)
247
+ * We keep this separate from attribute calls so we can filter attribute calls
248
+ * by object name (only self/cls are resolved to internal functions).
249
+ */
250
+ const PY_DIRECT_CALL_QUERY = `
251
+ (call
252
+ function: (identifier) @call.name) @call.node
253
+ `;
254
+ /**
255
+ * Method calls on an object: obj.method()
256
+ * We capture the object name so we can restrict resolution to self/cls.
257
+ * Calls like redis.get(), dict.get(), os.environ.get() are NOT resolved —
258
+ * only self.method() and cls.method() are tracked as internal edges.
259
+ */
260
+ const PY_METHOD_CALL_QUERY = `
261
+ (call
262
+ function: (attribute
263
+ object: (identifier) @call.object
264
+ attribute: (identifier) @call.name)) @call.node
265
+ `;
266
+ async function extractPyGraph(filePath, content) {
267
+ const { parser, lang } = await getPyParser();
268
+ const tree = parser.parse(content);
269
+ const fnQuery = new Parser.Query(lang, PY_FN_QUERY);
270
+ // --- Extract function nodes ---
271
+ const nodes = [];
272
+ const seen = new Set(); // avoid duplicates from decorated_definition + function_definition
273
+ const fnMatches = fnQuery.matches(tree.rootNode);
274
+ for (const match of fnMatches) {
275
+ const nameCapture = match.captures.find(c => c.name === 'fn.name');
276
+ const nodeCapture = match.captures.find(c => c.name === 'fn.node');
277
+ if (!nameCapture || !nodeCapture)
278
+ continue;
279
+ const name = nameCapture.node.text;
280
+ const fnNode = nodeCapture.node;
281
+ // Deduplicate by name node position (decorated_definition wraps the function_definition)
282
+ if (seen.has(nameCapture.node.startIndex))
283
+ continue;
284
+ seen.add(nameCapture.node.startIndex);
285
+ // Find enclosing class
286
+ let className;
287
+ let cursor = fnNode.parent;
288
+ while (cursor) {
289
+ if (cursor.type === 'class_definition') {
290
+ const classNameNode = cursor.children.find(c => c.type === 'identifier');
291
+ if (classNameNode)
292
+ className = classNameNode.text;
293
+ break;
294
+ }
295
+ cursor = cursor.parent;
296
+ }
297
+ // Skip private methods (underscore prefix) unless they're __init__ or there are very few nodes
298
+ if (name.startsWith('_') && name !== '__init__')
299
+ continue;
300
+ const isAsync = fnNode.text.startsWith('async ') ||
301
+ (fnNode.type === 'function_definition' && fnNode.children[0]?.text === 'async');
302
+ const id = className
303
+ ? `${filePath}::${className}.${name}`
304
+ : `${filePath}::${name}`;
305
+ nodes.push({
306
+ id,
307
+ name,
308
+ filePath,
309
+ className,
310
+ isAsync,
311
+ language: 'Python',
312
+ startIndex: fnNode.startIndex,
313
+ endIndex: fnNode.endIndex,
314
+ fanIn: 0,
315
+ fanOut: 0,
316
+ });
317
+ }
318
+ // --- Extract calls ---
319
+ const rawEdges = [];
320
+ const directCallQuery = new Parser.Query(lang, PY_DIRECT_CALL_QUERY);
321
+ const methodCallQuery = new Parser.Query(lang, PY_METHOD_CALL_QUERY);
322
+ // Direct calls: foo(), bar(x) — resolve across all files
323
+ for (const match of directCallQuery.matches(tree.rootNode)) {
324
+ const nameCapture = match.captures.find(c => c.name === 'call.name');
325
+ const nodeCapture = match.captures.find(c => c.name === 'call.node');
326
+ if (!nameCapture || !nodeCapture)
327
+ continue;
328
+ const calleeName = nameCapture.node.text;
329
+ if (IGNORED_CALLEES.has(calleeName))
330
+ continue;
331
+ const callPos = nodeCapture.node.startIndex;
332
+ const caller = findEnclosingFunction(nodes, callPos);
333
+ if (!caller)
334
+ continue;
335
+ rawEdges.push({
336
+ callerId: caller.id,
337
+ calleeName,
338
+ line: nodeCapture.node.startPosition.row + 1,
339
+ });
340
+ }
341
+ // Method calls: obj.method() — only resolve self.* and cls.* (internal object methods)
342
+ for (const match of methodCallQuery.matches(tree.rootNode)) {
343
+ const objectCapture = match.captures.find(c => c.name === 'call.object');
344
+ const nameCapture = match.captures.find(c => c.name === 'call.name');
345
+ const nodeCapture = match.captures.find(c => c.name === 'call.node');
346
+ if (!objectCapture || !nameCapture || !nodeCapture)
347
+ continue;
348
+ const objectName = objectCapture.node.text;
349
+ // Only track self.method() and cls.method() — external objects like
350
+ // redis.get(), dict.get(), os.path.join() would create massive false positives
351
+ if (objectName !== 'self' && objectName !== 'cls')
352
+ continue;
353
+ const calleeName = nameCapture.node.text;
354
+ if (IGNORED_CALLEES.has(calleeName))
355
+ continue;
356
+ const callPos = nodeCapture.node.startIndex;
357
+ const caller = findEnclosingFunction(nodes, callPos);
358
+ if (!caller)
359
+ continue;
360
+ rawEdges.push({
361
+ callerId: caller.id,
362
+ calleeName,
363
+ line: nodeCapture.node.startPosition.row + 1,
364
+ });
365
+ }
366
+ return { nodes, rawEdges };
367
+ }
368
+ // ============================================================================
369
+ // GO EXTRACTOR
370
+ // ============================================================================
371
+ const GO_FN_QUERY = `
372
+ (function_declaration
373
+ name: (identifier) @fn.name) @fn.node
374
+
375
+ (method_declaration
376
+ name: (field_identifier) @fn.name) @fn.node
377
+ `;
378
+ const GO_CALL_QUERY = `
379
+ (call_expression
380
+ function: (identifier) @call.name) @call.node
381
+
382
+ (call_expression
383
+ function: (selector_expression
384
+ field: (field_identifier) @call.name)) @call.node
385
+ `;
386
+ async function extractGoGraph(filePath, content) {
387
+ const { parser, lang } = await getGoParser();
388
+ const tree = parser.parse(content);
389
+ const fnQuery = new Parser.Query(lang, GO_FN_QUERY);
390
+ const callQuery = new Parser.Query(lang, GO_CALL_QUERY);
391
+ const nodes = [];
392
+ for (const match of fnQuery.matches(tree.rootNode)) {
393
+ const nameCapture = match.captures.find(c => c.name === 'fn.name');
394
+ const nodeCapture = match.captures.find(c => c.name === 'fn.node');
395
+ if (!nameCapture || !nodeCapture)
396
+ continue;
397
+ const name = nameCapture.node.text;
398
+ const fnNode = nodeCapture.node;
399
+ // Receiver type for method_declaration → use as className
400
+ let className;
401
+ if (fnNode.type === 'method_declaration') {
402
+ const receiver = fnNode.children.find(c => c.type === 'parameter_list');
403
+ if (receiver) {
404
+ // Extract type name from receiver: (r *MyStruct) → MyStruct
405
+ const typeNode = receiver.descendantsOfType('type_identifier')[0]
406
+ ?? receiver.descendantsOfType('pointer_type')[0];
407
+ if (typeNode)
408
+ className = typeNode.text.replace(/^\*/, '');
409
+ }
410
+ }
411
+ const id = className ? `${filePath}::${className}.${name}` : `${filePath}::${name}`;
412
+ nodes.push({
413
+ id, name, filePath, className,
414
+ isAsync: false, // Go has goroutines, not async/await
415
+ language: 'Go',
416
+ startIndex: fnNode.startIndex,
417
+ endIndex: fnNode.endIndex,
418
+ fanIn: 0, fanOut: 0,
419
+ });
420
+ }
421
+ const rawEdges = [];
422
+ for (const match of callQuery.matches(tree.rootNode)) {
423
+ const nameCapture = match.captures.find(c => c.name === 'call.name');
424
+ const nodeCapture = match.captures.find(c => c.name === 'call.node');
425
+ if (!nameCapture || !nodeCapture)
426
+ continue;
427
+ const calleeName = nameCapture.node.text;
428
+ if (IGNORED_CALLEES.has(calleeName))
429
+ continue;
430
+ const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
431
+ if (!caller)
432
+ continue;
433
+ rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1 });
434
+ }
435
+ return { nodes, rawEdges };
436
+ }
437
+ // ============================================================================
438
+ // RUST EXTRACTOR
439
+ // ============================================================================
440
+ const RUST_FN_QUERY = `
441
+ (function_item
442
+ name: (identifier) @fn.name) @fn.node
443
+ `;
444
+ const RUST_CALL_QUERY = `
445
+ (call_expression
446
+ function: (identifier) @call.name) @call.node
447
+
448
+ (call_expression
449
+ function: (field_expression
450
+ field: (field_identifier) @call.name)) @call.node
451
+ `;
452
+ async function extractRustGraph(filePath, content) {
453
+ const { parser, lang } = await getRustParser();
454
+ const tree = parser.parse(content);
455
+ const fnQuery = new Parser.Query(lang, RUST_FN_QUERY);
456
+ const callQuery = new Parser.Query(lang, RUST_CALL_QUERY);
457
+ const nodes = [];
458
+ for (const match of fnQuery.matches(tree.rootNode)) {
459
+ const nameCapture = match.captures.find(c => c.name === 'fn.name');
460
+ const nodeCapture = match.captures.find(c => c.name === 'fn.node');
461
+ if (!nameCapture || !nodeCapture)
462
+ continue;
463
+ const name = nameCapture.node.text;
464
+ const fnNode = nodeCapture.node;
465
+ // Find enclosing impl block → use as className
466
+ let className;
467
+ let cursor = fnNode.parent;
468
+ while (cursor) {
469
+ if (cursor.type === 'impl_item') {
470
+ const typeNode = cursor.children.find(c => c.type === 'type_identifier');
471
+ if (typeNode)
472
+ className = typeNode.text;
473
+ break;
474
+ }
475
+ cursor = cursor.parent;
476
+ }
477
+ // Rust: async keyword lives inside a function_modifiers child
478
+ const isAsync = fnNode.children.some(c => c.type === 'function_modifiers' && c.text.includes('async'));
479
+ const id = className ? `${filePath}::${className}.${name}` : `${filePath}::${name}`;
480
+ nodes.push({
481
+ id, name, filePath, className,
482
+ isAsync,
483
+ language: 'Rust',
484
+ startIndex: fnNode.startIndex,
485
+ endIndex: fnNode.endIndex,
486
+ fanIn: 0, fanOut: 0,
487
+ });
488
+ }
489
+ const rawEdges = [];
490
+ for (const match of callQuery.matches(tree.rootNode)) {
491
+ const nameCapture = match.captures.find(c => c.name === 'call.name');
492
+ const nodeCapture = match.captures.find(c => c.name === 'call.node');
493
+ if (!nameCapture || !nodeCapture)
494
+ continue;
495
+ const calleeName = nameCapture.node.text;
496
+ if (IGNORED_CALLEES.has(calleeName))
497
+ continue;
498
+ const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
499
+ if (!caller)
500
+ continue;
501
+ rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1 });
502
+ }
503
+ return { nodes, rawEdges };
504
+ }
505
+ // ============================================================================
506
+ // RUBY EXTRACTOR
507
+ // ============================================================================
508
+ const RUBY_FN_QUERY = `
509
+ (method
510
+ name: (identifier) @fn.name) @fn.node
511
+
512
+ (singleton_method
513
+ name: (identifier) @fn.name) @fn.node
514
+ `;
515
+ // Explicit calls: fetch(), obj.method()
516
+ const RUBY_CALL_QUERY = `
517
+ (call
518
+ method: (identifier) @call.name) @call.node
519
+ `;
520
+ // Bareword calls: Ruby allows calling methods without parentheses.
521
+ // An identifier at statement level inside a body_statement is almost always
522
+ // a method call (variable usage appears in assignments/expressions, not alone).
523
+ const RUBY_BAREWORD_QUERY = `
524
+ (body_statement
525
+ (identifier) @call.name)
526
+ `;
527
+ async function extractRubyGraph(filePath, content) {
528
+ const { parser, lang } = await getRubyParser();
529
+ const tree = parser.parse(content);
530
+ const fnQuery = new Parser.Query(lang, RUBY_FN_QUERY);
531
+ const callQuery = new Parser.Query(lang, RUBY_CALL_QUERY);
532
+ const barewordQuery = new Parser.Query(lang, RUBY_BAREWORD_QUERY);
533
+ const nodes = [];
534
+ for (const match of fnQuery.matches(tree.rootNode)) {
535
+ const nameCapture = match.captures.find(c => c.name === 'fn.name');
536
+ const nodeCapture = match.captures.find(c => c.name === 'fn.node');
537
+ if (!nameCapture || !nodeCapture)
538
+ continue;
539
+ const name = nameCapture.node.text;
540
+ const fnNode = nodeCapture.node;
541
+ // Find enclosing class/module
542
+ let className;
543
+ let cursor = fnNode.parent;
544
+ while (cursor) {
545
+ if (cursor.type === 'class' || cursor.type === 'module') {
546
+ const nameNode = cursor.children.find(c => c.type === 'constant' || c.type === 'scope_resolution');
547
+ if (nameNode)
548
+ className = nameNode.text;
549
+ break;
550
+ }
551
+ cursor = cursor.parent;
552
+ }
553
+ const id = className ? `${filePath}::${className}.${name}` : `${filePath}::${name}`;
554
+ nodes.push({
555
+ id, name, filePath, className,
556
+ isAsync: false,
557
+ language: 'Ruby',
558
+ startIndex: fnNode.startIndex,
559
+ endIndex: fnNode.endIndex,
560
+ fanIn: 0, fanOut: 0,
561
+ });
562
+ }
563
+ const rawEdges = [];
564
+ // Explicit calls: fetch(), obj.method()
565
+ for (const match of callQuery.matches(tree.rootNode)) {
566
+ const nameCapture = match.captures.find(c => c.name === 'call.name');
567
+ const nodeCapture = match.captures.find(c => c.name === 'call.node');
568
+ if (!nameCapture || !nodeCapture)
569
+ continue;
570
+ const calleeName = nameCapture.node.text;
571
+ if (IGNORED_CALLEES.has(calleeName))
572
+ continue;
573
+ const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
574
+ if (!caller)
575
+ continue;
576
+ rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1 });
577
+ }
578
+ // Bareword calls: fetch (no parens) — identifier at statement level
579
+ for (const match of barewordQuery.matches(tree.rootNode)) {
580
+ const nameCapture = match.captures.find(c => c.name === 'call.name');
581
+ if (!nameCapture)
582
+ continue;
583
+ const calleeName = nameCapture.node.text;
584
+ if (IGNORED_CALLEES.has(calleeName))
585
+ continue;
586
+ const caller = findEnclosingFunction(nodes, nameCapture.node.startIndex);
587
+ if (!caller)
588
+ continue;
589
+ rawEdges.push({ callerId: caller.id, calleeName, line: nameCapture.node.startPosition.row + 1 });
590
+ }
591
+ return { nodes, rawEdges };
592
+ }
593
+ // ============================================================================
594
+ // JAVA EXTRACTOR
595
+ // ============================================================================
596
+ const JAVA_FN_QUERY = `
597
+ (method_declaration
598
+ name: (identifier) @fn.name) @fn.node
599
+
600
+ (constructor_declaration
601
+ name: (identifier) @fn.name) @fn.node
602
+ `;
603
+ const JAVA_CALL_QUERY = `
604
+ (method_invocation
605
+ name: (identifier) @call.name) @call.node
606
+ `;
607
+ async function extractJavaGraph(filePath, content) {
608
+ const { parser, lang } = await getJavaParser();
609
+ const tree = parser.parse(content);
610
+ const fnQuery = new Parser.Query(lang, JAVA_FN_QUERY);
611
+ const callQuery = new Parser.Query(lang, JAVA_CALL_QUERY);
612
+ const nodes = [];
613
+ for (const match of fnQuery.matches(tree.rootNode)) {
614
+ const nameCapture = match.captures.find(c => c.name === 'fn.name');
615
+ const nodeCapture = match.captures.find(c => c.name === 'fn.node');
616
+ if (!nameCapture || !nodeCapture)
617
+ continue;
618
+ const name = nameCapture.node.text;
619
+ const fnNode = nodeCapture.node;
620
+ // Find enclosing class/interface/enum
621
+ let className;
622
+ let cursor = fnNode.parent;
623
+ while (cursor) {
624
+ if (cursor.type === 'class_declaration' || cursor.type === 'interface_declaration' || cursor.type === 'enum_declaration') {
625
+ const nameNode = cursor.children.find(c => c.type === 'identifier');
626
+ if (nameNode)
627
+ className = nameNode.text;
628
+ break;
629
+ }
630
+ cursor = cursor.parent;
631
+ }
632
+ const isAsync = false; // Java uses Future/CompletableFuture, not async keyword
633
+ const id = className ? `${filePath}::${className}.${name}` : `${filePath}::${name}`;
634
+ nodes.push({
635
+ id, name, filePath, className,
636
+ isAsync,
637
+ language: 'Java',
638
+ startIndex: fnNode.startIndex,
639
+ endIndex: fnNode.endIndex,
640
+ fanIn: 0, fanOut: 0,
641
+ });
642
+ }
643
+ const rawEdges = [];
644
+ for (const match of callQuery.matches(tree.rootNode)) {
645
+ const nameCapture = match.captures.find(c => c.name === 'call.name');
646
+ const nodeCapture = match.captures.find(c => c.name === 'call.node');
647
+ if (!nameCapture || !nodeCapture)
648
+ continue;
649
+ const calleeName = nameCapture.node.text;
650
+ if (IGNORED_CALLEES.has(calleeName))
651
+ continue;
652
+ const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
653
+ if (!caller)
654
+ continue;
655
+ rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1 });
656
+ }
657
+ return { nodes, rawEdges };
658
+ }
659
+ // ============================================================================
660
+ // CALL GRAPH BUILDER
661
+ // ============================================================================
662
+ export class CallGraphBuilder {
663
+ /**
664
+ * Build a call graph from a list of source files.
665
+ *
666
+ * @param files Source files with path, content, and language
667
+ * @param layers Optional layer map { layerName: [path prefix, ...] }
668
+ * e.g. { api: ['routes/', 'controllers/'], storage: ['models/'] }
669
+ */
670
+ async build(files, layers) {
671
+ const allNodes = new Map();
672
+ const allRawEdges = [];
673
+ // Pass 1: Extract nodes and raw edges from each file
674
+ for (const file of files) {
675
+ try {
676
+ let result;
677
+ if (file.language === 'Python') {
678
+ result = await extractPyGraph(file.path, file.content);
679
+ }
680
+ else if (file.language === 'TypeScript' || file.language === 'JavaScript') {
681
+ result = await extractTSGraph(file.path, file.content);
682
+ }
683
+ else if (file.language === 'Go') {
684
+ result = await extractGoGraph(file.path, file.content);
685
+ }
686
+ else if (file.language === 'Rust') {
687
+ result = await extractRustGraph(file.path, file.content);
688
+ }
689
+ else if (file.language === 'Ruby') {
690
+ result = await extractRubyGraph(file.path, file.content);
691
+ }
692
+ else if (file.language === 'Java') {
693
+ result = await extractJavaGraph(file.path, file.content);
694
+ }
695
+ else {
696
+ continue;
697
+ }
698
+ for (const node of result.nodes) {
699
+ allNodes.set(node.id, node);
700
+ }
701
+ allRawEdges.push(...result.rawEdges);
702
+ }
703
+ catch {
704
+ // Skip files that fail to parse (syntax errors, encoding issues, etc.)
705
+ }
706
+ }
707
+ // Pass 2: Resolve raw edges — find callee FunctionNode by name
708
+ const nodesByName = new Map();
709
+ for (const node of allNodes.values()) {
710
+ const list = nodesByName.get(node.name) ?? [];
711
+ list.push(node);
712
+ nodesByName.set(node.name, list);
713
+ }
714
+ const edges = [];
715
+ for (const raw of allRawEdges) {
716
+ const candidates = nodesByName.get(raw.calleeName);
717
+ if (!candidates || candidates.length === 0)
718
+ continue; // external call
719
+ let calleeNode;
720
+ if (candidates.length === 1) {
721
+ calleeNode = candidates[0];
722
+ }
723
+ else {
724
+ // Prefer same file as caller
725
+ const callerNode = allNodes.get(raw.callerId);
726
+ const sameFile = candidates.find(c => c.filePath === callerNode?.filePath);
727
+ calleeNode = sameFile ?? candidates[0];
728
+ }
729
+ edges.push({
730
+ callerId: raw.callerId,
731
+ calleeId: calleeNode.id,
732
+ calleeName: raw.calleeName,
733
+ line: raw.line,
734
+ });
735
+ }
736
+ // Pass 3: Calculate fanIn / fanOut (count unique caller→callee pairs, not call sites)
737
+ const seenPairs = new Set();
738
+ for (const edge of edges) {
739
+ const pairKey = `${edge.callerId}\0${edge.calleeId}`;
740
+ if (seenPairs.has(pairKey))
741
+ continue;
742
+ seenPairs.add(pairKey);
743
+ const caller = allNodes.get(edge.callerId);
744
+ const callee = allNodes.get(edge.calleeId);
745
+ if (caller)
746
+ caller.fanOut++;
747
+ if (callee)
748
+ callee.fanIn++;
749
+ }
750
+ // Pass 4: Derive hub functions, entry points, layer violations
751
+ const nodes = Array.from(allNodes.values());
752
+ const hubFunctions = nodes
753
+ .filter(n => n.fanIn >= HUB_THRESHOLD)
754
+ .sort((a, b) => b.fanIn - a.fanIn);
755
+ const calledIds = new Set(edges.map(e => e.calleeId));
756
+ const entryPoints = nodes
757
+ .filter(n => !calledIds.has(n.id))
758
+ .sort((a, b) => b.fanOut - a.fanOut);
759
+ const layerViolations = layers
760
+ ? this.detectLayerViolations(edges, allNodes, layers)
761
+ : [];
762
+ const totalFanIn = nodes.reduce((s, n) => s + n.fanIn, 0);
763
+ const totalFanOut = nodes.reduce((s, n) => s + n.fanOut, 0);
764
+ return {
765
+ nodes: allNodes,
766
+ edges,
767
+ hubFunctions,
768
+ entryPoints,
769
+ layerViolations,
770
+ stats: {
771
+ totalNodes: nodes.length,
772
+ totalEdges: edges.length,
773
+ avgFanIn: nodes.length > 0 ? totalFanIn / nodes.length : 0,
774
+ avgFanOut: nodes.length > 0 ? totalFanOut / nodes.length : 0,
775
+ },
776
+ };
777
+ }
778
+ detectLayerViolations(edges, nodes, layers) {
779
+ // Build ordered layer list (index 0 = top layer, higher index = lower layer)
780
+ const layerOrder = Object.keys(layers);
781
+ const getLayer = (filePath) => {
782
+ for (const [layerName, prefixes] of Object.entries(layers)) {
783
+ if (prefixes.some(p => filePath.includes(p)))
784
+ return layerName;
785
+ }
786
+ return undefined;
787
+ };
788
+ const violations = [];
789
+ for (const edge of edges) {
790
+ const caller = nodes.get(edge.callerId);
791
+ const callee = nodes.get(edge.calleeId);
792
+ if (!caller || !callee)
793
+ continue;
794
+ const callerLayer = getLayer(caller.filePath);
795
+ const calleeLayer = getLayer(callee.filePath);
796
+ if (!callerLayer || !calleeLayer || callerLayer === calleeLayer)
797
+ continue;
798
+ const callerIdx = layerOrder.indexOf(callerLayer);
799
+ const calleeIdx = layerOrder.indexOf(calleeLayer);
800
+ if (callerIdx > calleeIdx) {
801
+ // Lower layer calling upper layer — violation
802
+ violations.push({
803
+ callerId: edge.callerId,
804
+ calleeId: edge.calleeId,
805
+ callerLayer,
806
+ calleeLayer,
807
+ reason: `${callerLayer} calls ${calleeLayer} (${caller.name} → ${callee.name})`,
808
+ });
809
+ }
810
+ }
811
+ return violations;
812
+ }
813
+ }
814
+ // ============================================================================
815
+ // SERIALIZATION HELPER
816
+ // ============================================================================
817
+ export function serializeCallGraph(result) {
818
+ return {
819
+ nodes: Array.from(result.nodes.values()),
820
+ edges: result.edges,
821
+ hubFunctions: result.hubFunctions,
822
+ entryPoints: result.entryPoints,
823
+ layerViolations: result.layerViolations,
824
+ stats: result.stats,
825
+ };
826
+ }
827
+ //# sourceMappingURL=call-graph.js.map