openlore 2.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 (634) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +268 -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 +143 -0
  6. package/dist/api/analyze.js.map +1 -0
  7. package/dist/api/audit.d.ts +10 -0
  8. package/dist/api/audit.d.ts.map +1 -0
  9. package/dist/api/audit.js +117 -0
  10. package/dist/api/audit.js.map +1 -0
  11. package/dist/api/decisions.d.ts +55 -0
  12. package/dist/api/decisions.d.ts.map +1 -0
  13. package/dist/api/decisions.js +157 -0
  14. package/dist/api/decisions.js.map +1 -0
  15. package/dist/api/drift.d.ts +21 -0
  16. package/dist/api/drift.d.ts.map +1 -0
  17. package/dist/api/drift.js +152 -0
  18. package/dist/api/drift.js.map +1 -0
  19. package/dist/api/generate.d.ts +18 -0
  20. package/dist/api/generate.d.ts.map +1 -0
  21. package/dist/api/generate.js +259 -0
  22. package/dist/api/generate.js.map +1 -0
  23. package/dist/api/index.d.ts +41 -0
  24. package/dist/api/index.d.ts.map +1 -0
  25. package/dist/api/index.js +34 -0
  26. package/dist/api/index.js.map +1 -0
  27. package/dist/api/init.d.ts +18 -0
  28. package/dist/api/init.d.ts.map +1 -0
  29. package/dist/api/init.js +83 -0
  30. package/dist/api/init.js.map +1 -0
  31. package/dist/api/run.d.ts +19 -0
  32. package/dist/api/run.d.ts.map +1 -0
  33. package/dist/api/run.js +312 -0
  34. package/dist/api/run.js.map +1 -0
  35. package/dist/api/specs.d.ts +49 -0
  36. package/dist/api/specs.d.ts.map +1 -0
  37. package/dist/api/specs.js +137 -0
  38. package/dist/api/specs.js.map +1 -0
  39. package/dist/api/types.d.ts +201 -0
  40. package/dist/api/types.d.ts.map +1 -0
  41. package/dist/api/types.js +9 -0
  42. package/dist/api/types.js.map +1 -0
  43. package/dist/api/verify.d.ts +20 -0
  44. package/dist/api/verify.d.ts.map +1 -0
  45. package/dist/api/verify.js +117 -0
  46. package/dist/api/verify.js.map +1 -0
  47. package/dist/cli/commands/analyze.d.ts +30 -0
  48. package/dist/cli/commands/analyze.d.ts.map +1 -0
  49. package/dist/cli/commands/analyze.js +683 -0
  50. package/dist/cli/commands/analyze.js.map +1 -0
  51. package/dist/cli/commands/audit.d.ts +9 -0
  52. package/dist/cli/commands/audit.d.ts.map +1 -0
  53. package/dist/cli/commands/audit.js +98 -0
  54. package/dist/cli/commands/audit.js.map +1 -0
  55. package/dist/cli/commands/decisions.d.ts +16 -0
  56. package/dist/cli/commands/decisions.d.ts.map +1 -0
  57. package/dist/cli/commands/decisions.js +864 -0
  58. package/dist/cli/commands/decisions.js.map +1 -0
  59. package/dist/cli/commands/digest.d.ts +9 -0
  60. package/dist/cli/commands/digest.d.ts.map +1 -0
  61. package/dist/cli/commands/digest.js +61 -0
  62. package/dist/cli/commands/digest.js.map +1 -0
  63. package/dist/cli/commands/doctor.d.ts +9 -0
  64. package/dist/cli/commands/doctor.d.ts.map +1 -0
  65. package/dist/cli/commands/doctor.js +398 -0
  66. package/dist/cli/commands/doctor.js.map +1 -0
  67. package/dist/cli/commands/drift.d.ts +9 -0
  68. package/dist/cli/commands/drift.d.ts.map +1 -0
  69. package/dist/cli/commands/drift.js +550 -0
  70. package/dist/cli/commands/drift.js.map +1 -0
  71. package/dist/cli/commands/generate.d.ts +9 -0
  72. package/dist/cli/commands/generate.d.ts.map +1 -0
  73. package/dist/cli/commands/generate.js +565 -0
  74. package/dist/cli/commands/generate.js.map +1 -0
  75. package/dist/cli/commands/init.d.ts +9 -0
  76. package/dist/cli/commands/init.d.ts.map +1 -0
  77. package/dist/cli/commands/init.js +173 -0
  78. package/dist/cli/commands/init.js.map +1 -0
  79. package/dist/cli/commands/mcp.d.ts +2235 -0
  80. package/dist/cli/commands/mcp.d.ts.map +1 -0
  81. package/dist/cli/commands/mcp.js +1384 -0
  82. package/dist/cli/commands/mcp.js.map +1 -0
  83. package/dist/cli/commands/refresh-stories.d.ts +10 -0
  84. package/dist/cli/commands/refresh-stories.d.ts.map +1 -0
  85. package/dist/cli/commands/refresh-stories.js +314 -0
  86. package/dist/cli/commands/refresh-stories.js.map +1 -0
  87. package/dist/cli/commands/run.d.ts +9 -0
  88. package/dist/cli/commands/run.d.ts.map +1 -0
  89. package/dist/cli/commands/run.js +459 -0
  90. package/dist/cli/commands/run.js.map +1 -0
  91. package/dist/cli/commands/setup.d.ts +19 -0
  92. package/dist/cli/commands/setup.d.ts.map +1 -0
  93. package/dist/cli/commands/setup.js +355 -0
  94. package/dist/cli/commands/setup.js.map +1 -0
  95. package/dist/cli/commands/test.d.ts +22 -0
  96. package/dist/cli/commands/test.d.ts.map +1 -0
  97. package/dist/cli/commands/test.js +180 -0
  98. package/dist/cli/commands/test.js.map +1 -0
  99. package/dist/cli/commands/verify.d.ts +9 -0
  100. package/dist/cli/commands/verify.d.ts.map +1 -0
  101. package/dist/cli/commands/verify.js +383 -0
  102. package/dist/cli/commands/verify.js.map +1 -0
  103. package/dist/cli/commands/view.d.ts +13 -0
  104. package/dist/cli/commands/view.d.ts.map +1 -0
  105. package/dist/cli/commands/view.js +547 -0
  106. package/dist/cli/commands/view.js.map +1 -0
  107. package/dist/cli/index.d.ts +9 -0
  108. package/dist/cli/index.d.ts.map +1 -0
  109. package/dist/cli/index.js +118 -0
  110. package/dist/cli/index.js.map +1 -0
  111. package/dist/cli/tui-approval.d.ts +11 -0
  112. package/dist/cli/tui-approval.d.ts.map +1 -0
  113. package/dist/cli/tui-approval.js +129 -0
  114. package/dist/cli/tui-approval.js.map +1 -0
  115. package/dist/constants.d.ts +314 -0
  116. package/dist/constants.d.ts.map +1 -0
  117. package/dist/constants.js +382 -0
  118. package/dist/constants.js.map +1 -0
  119. package/dist/core/analyzer/ai-config-generator.d.ts +54 -0
  120. package/dist/core/analyzer/ai-config-generator.d.ts.map +1 -0
  121. package/dist/core/analyzer/ai-config-generator.js +98 -0
  122. package/dist/core/analyzer/ai-config-generator.js.map +1 -0
  123. package/dist/core/analyzer/architecture-writer.d.ts +67 -0
  124. package/dist/core/analyzer/architecture-writer.d.ts.map +1 -0
  125. package/dist/core/analyzer/architecture-writer.js +209 -0
  126. package/dist/core/analyzer/architecture-writer.js.map +1 -0
  127. package/dist/core/analyzer/artifact-generator.d.ts +261 -0
  128. package/dist/core/analyzer/artifact-generator.d.ts.map +1 -0
  129. package/dist/core/analyzer/artifact-generator.js +909 -0
  130. package/dist/core/analyzer/artifact-generator.js.map +1 -0
  131. package/dist/core/analyzer/ast-chunker.d.ts +24 -0
  132. package/dist/core/analyzer/ast-chunker.d.ts.map +1 -0
  133. package/dist/core/analyzer/ast-chunker.js +198 -0
  134. package/dist/core/analyzer/ast-chunker.js.map +1 -0
  135. package/dist/core/analyzer/call-graph.d.ts +162 -0
  136. package/dist/core/analyzer/call-graph.d.ts.map +1 -0
  137. package/dist/core/analyzer/call-graph.js +2040 -0
  138. package/dist/core/analyzer/call-graph.js.map +1 -0
  139. package/dist/core/analyzer/code-shaper.d.ts +33 -0
  140. package/dist/core/analyzer/code-shaper.d.ts.map +1 -0
  141. package/dist/core/analyzer/code-shaper.js +154 -0
  142. package/dist/core/analyzer/code-shaper.js.map +1 -0
  143. package/dist/core/analyzer/codebase-digest.d.ts +40 -0
  144. package/dist/core/analyzer/codebase-digest.d.ts.map +1 -0
  145. package/dist/core/analyzer/codebase-digest.js +195 -0
  146. package/dist/core/analyzer/codebase-digest.js.map +1 -0
  147. package/dist/core/analyzer/cpp-header-resolver.d.ts +30 -0
  148. package/dist/core/analyzer/cpp-header-resolver.d.ts.map +1 -0
  149. package/dist/core/analyzer/cpp-header-resolver.js +71 -0
  150. package/dist/core/analyzer/cpp-header-resolver.js.map +1 -0
  151. package/dist/core/analyzer/dependency-graph.d.ts +230 -0
  152. package/dist/core/analyzer/dependency-graph.d.ts.map +1 -0
  153. package/dist/core/analyzer/dependency-graph.js +752 -0
  154. package/dist/core/analyzer/dependency-graph.js.map +1 -0
  155. package/dist/core/analyzer/duplicate-detector.d.ts +52 -0
  156. package/dist/core/analyzer/duplicate-detector.d.ts.map +1 -0
  157. package/dist/core/analyzer/duplicate-detector.js +289 -0
  158. package/dist/core/analyzer/duplicate-detector.js.map +1 -0
  159. package/dist/core/analyzer/embedding-service.d.ts +56 -0
  160. package/dist/core/analyzer/embedding-service.d.ts.map +1 -0
  161. package/dist/core/analyzer/embedding-service.js +118 -0
  162. package/dist/core/analyzer/embedding-service.js.map +1 -0
  163. package/dist/core/analyzer/env-extractor.d.ts +33 -0
  164. package/dist/core/analyzer/env-extractor.d.ts.map +1 -0
  165. package/dist/core/analyzer/env-extractor.js +196 -0
  166. package/dist/core/analyzer/env-extractor.js.map +1 -0
  167. package/dist/core/analyzer/external-packages.d.ts +20 -0
  168. package/dist/core/analyzer/external-packages.d.ts.map +1 -0
  169. package/dist/core/analyzer/external-packages.js +175 -0
  170. package/dist/core/analyzer/external-packages.js.map +1 -0
  171. package/dist/core/analyzer/file-walker.d.ts +78 -0
  172. package/dist/core/analyzer/file-walker.d.ts.map +1 -0
  173. package/dist/core/analyzer/file-walker.js +532 -0
  174. package/dist/core/analyzer/file-walker.js.map +1 -0
  175. package/dist/core/analyzer/function-registry-trie.d.ts +21 -0
  176. package/dist/core/analyzer/function-registry-trie.d.ts.map +1 -0
  177. package/dist/core/analyzer/function-registry-trie.js +39 -0
  178. package/dist/core/analyzer/function-registry-trie.js.map +1 -0
  179. package/dist/core/analyzer/http-route-parser.d.ts +152 -0
  180. package/dist/core/analyzer/http-route-parser.d.ts.map +1 -0
  181. package/dist/core/analyzer/http-route-parser.js +971 -0
  182. package/dist/core/analyzer/http-route-parser.js.map +1 -0
  183. package/dist/core/analyzer/import-parser.d.ts +100 -0
  184. package/dist/core/analyzer/import-parser.d.ts.map +1 -0
  185. package/dist/core/analyzer/import-parser.js +952 -0
  186. package/dist/core/analyzer/import-parser.js.map +1 -0
  187. package/dist/core/analyzer/import-resolver-bridge.d.ts +25 -0
  188. package/dist/core/analyzer/import-resolver-bridge.d.ts.map +1 -0
  189. package/dist/core/analyzer/import-resolver-bridge.js +99 -0
  190. package/dist/core/analyzer/import-resolver-bridge.js.map +1 -0
  191. package/dist/core/analyzer/index.d.ts +10 -0
  192. package/dist/core/analyzer/index.d.ts.map +1 -0
  193. package/dist/core/analyzer/index.js +10 -0
  194. package/dist/core/analyzer/index.js.map +1 -0
  195. package/dist/core/analyzer/middleware-extractor.d.ts +29 -0
  196. package/dist/core/analyzer/middleware-extractor.d.ts.map +1 -0
  197. package/dist/core/analyzer/middleware-extractor.js +195 -0
  198. package/dist/core/analyzer/middleware-extractor.js.map +1 -0
  199. package/dist/core/analyzer/refactor-analyzer.d.ts +83 -0
  200. package/dist/core/analyzer/refactor-analyzer.d.ts.map +1 -0
  201. package/dist/core/analyzer/refactor-analyzer.js +351 -0
  202. package/dist/core/analyzer/refactor-analyzer.js.map +1 -0
  203. package/dist/core/analyzer/repository-mapper.d.ts +150 -0
  204. package/dist/core/analyzer/repository-mapper.d.ts.map +1 -0
  205. package/dist/core/analyzer/repository-mapper.js +740 -0
  206. package/dist/core/analyzer/repository-mapper.js.map +1 -0
  207. package/dist/core/analyzer/schema-extractor.d.ts +41 -0
  208. package/dist/core/analyzer/schema-extractor.d.ts.map +1 -0
  209. package/dist/core/analyzer/schema-extractor.js +229 -0
  210. package/dist/core/analyzer/schema-extractor.js.map +1 -0
  211. package/dist/core/analyzer/signature-extractor.d.ts +31 -0
  212. package/dist/core/analyzer/signature-extractor.d.ts.map +1 -0
  213. package/dist/core/analyzer/signature-extractor.js +675 -0
  214. package/dist/core/analyzer/signature-extractor.js.map +1 -0
  215. package/dist/core/analyzer/significance-scorer.d.ts +79 -0
  216. package/dist/core/analyzer/significance-scorer.d.ts.map +1 -0
  217. package/dist/core/analyzer/significance-scorer.js +407 -0
  218. package/dist/core/analyzer/significance-scorer.js.map +1 -0
  219. package/dist/core/analyzer/spec-snapshot-generator.d.ts +17 -0
  220. package/dist/core/analyzer/spec-snapshot-generator.d.ts.map +1 -0
  221. package/dist/core/analyzer/spec-snapshot-generator.js +201 -0
  222. package/dist/core/analyzer/spec-snapshot-generator.js.map +1 -0
  223. package/dist/core/analyzer/spec-vector-index.d.ts +68 -0
  224. package/dist/core/analyzer/spec-vector-index.d.ts.map +1 -0
  225. package/dist/core/analyzer/spec-vector-index.js +340 -0
  226. package/dist/core/analyzer/spec-vector-index.js.map +1 -0
  227. package/dist/core/analyzer/subgraph-extractor.d.ts +51 -0
  228. package/dist/core/analyzer/subgraph-extractor.d.ts.map +1 -0
  229. package/dist/core/analyzer/subgraph-extractor.js +147 -0
  230. package/dist/core/analyzer/subgraph-extractor.js.map +1 -0
  231. package/dist/core/analyzer/type-inference-engine.d.ts +23 -0
  232. package/dist/core/analyzer/type-inference-engine.d.ts.map +1 -0
  233. package/dist/core/analyzer/type-inference-engine.js +130 -0
  234. package/dist/core/analyzer/type-inference-engine.js.map +1 -0
  235. package/dist/core/analyzer/ui-component-extractor.d.ts +43 -0
  236. package/dist/core/analyzer/ui-component-extractor.d.ts.map +1 -0
  237. package/dist/core/analyzer/ui-component-extractor.js +245 -0
  238. package/dist/core/analyzer/ui-component-extractor.js.map +1 -0
  239. package/dist/core/analyzer/unified-search.d.ts +116 -0
  240. package/dist/core/analyzer/unified-search.d.ts.map +1 -0
  241. package/dist/core/analyzer/unified-search.js +231 -0
  242. package/dist/core/analyzer/unified-search.js.map +1 -0
  243. package/dist/core/analyzer/vector-index.d.ts +92 -0
  244. package/dist/core/analyzer/vector-index.d.ts.map +1 -0
  245. package/dist/core/analyzer/vector-index.js +451 -0
  246. package/dist/core/analyzer/vector-index.js.map +1 -0
  247. package/dist/core/decisions/consolidator.d.ts +14 -0
  248. package/dist/core/decisions/consolidator.d.ts.map +1 -0
  249. package/dist/core/decisions/consolidator.js +169 -0
  250. package/dist/core/decisions/consolidator.js.map +1 -0
  251. package/dist/core/decisions/extractor.d.ts +26 -0
  252. package/dist/core/decisions/extractor.d.ts.map +1 -0
  253. package/dist/core/decisions/extractor.js +156 -0
  254. package/dist/core/decisions/extractor.js.map +1 -0
  255. package/dist/core/decisions/index.d.ts +19 -0
  256. package/dist/core/decisions/index.d.ts.map +1 -0
  257. package/dist/core/decisions/index.js +16 -0
  258. package/dist/core/decisions/index.js.map +1 -0
  259. package/dist/core/decisions/store.d.ts +36 -0
  260. package/dist/core/decisions/store.d.ts.map +1 -0
  261. package/dist/core/decisions/store.js +109 -0
  262. package/dist/core/decisions/store.js.map +1 -0
  263. package/dist/core/decisions/syncer.d.ts +27 -0
  264. package/dist/core/decisions/syncer.d.ts.map +1 -0
  265. package/dist/core/decisions/syncer.js +214 -0
  266. package/dist/core/decisions/syncer.js.map +1 -0
  267. package/dist/core/decisions/verifier.d.ts +20 -0
  268. package/dist/core/decisions/verifier.d.ts.map +1 -0
  269. package/dist/core/decisions/verifier.js +115 -0
  270. package/dist/core/decisions/verifier.js.map +1 -0
  271. package/dist/core/digest/digest-generator.d.ts +29 -0
  272. package/dist/core/digest/digest-generator.d.ts.map +1 -0
  273. package/dist/core/digest/digest-generator.js +181 -0
  274. package/dist/core/digest/digest-generator.js.map +1 -0
  275. package/dist/core/drift/drift-detector.d.ts +102 -0
  276. package/dist/core/drift/drift-detector.d.ts.map +1 -0
  277. package/dist/core/drift/drift-detector.js +598 -0
  278. package/dist/core/drift/drift-detector.js.map +1 -0
  279. package/dist/core/drift/git-diff.d.ts +60 -0
  280. package/dist/core/drift/git-diff.d.ts.map +1 -0
  281. package/dist/core/drift/git-diff.js +383 -0
  282. package/dist/core/drift/git-diff.js.map +1 -0
  283. package/dist/core/drift/index.d.ts +12 -0
  284. package/dist/core/drift/index.d.ts.map +1 -0
  285. package/dist/core/drift/index.js +9 -0
  286. package/dist/core/drift/index.js.map +1 -0
  287. package/dist/core/drift/spec-mapper.d.ts +73 -0
  288. package/dist/core/drift/spec-mapper.d.ts.map +1 -0
  289. package/dist/core/drift/spec-mapper.js +353 -0
  290. package/dist/core/drift/spec-mapper.js.map +1 -0
  291. package/dist/core/drift/test-suggester.d.ts +18 -0
  292. package/dist/core/drift/test-suggester.d.ts.map +1 -0
  293. package/dist/core/drift/test-suggester.js +107 -0
  294. package/dist/core/drift/test-suggester.js.map +1 -0
  295. package/dist/core/generator/adr-generator.d.ts +32 -0
  296. package/dist/core/generator/adr-generator.d.ts.map +1 -0
  297. package/dist/core/generator/adr-generator.js +192 -0
  298. package/dist/core/generator/adr-generator.js.map +1 -0
  299. package/dist/core/generator/index.d.ts +9 -0
  300. package/dist/core/generator/index.d.ts.map +1 -0
  301. package/dist/core/generator/index.js +12 -0
  302. package/dist/core/generator/index.js.map +1 -0
  303. package/dist/core/generator/mapping-generator.d.ts +54 -0
  304. package/dist/core/generator/mapping-generator.d.ts.map +1 -0
  305. package/dist/core/generator/mapping-generator.js +240 -0
  306. package/dist/core/generator/mapping-generator.js.map +1 -0
  307. package/dist/core/generator/openspec-compat.d.ts +160 -0
  308. package/dist/core/generator/openspec-compat.d.ts.map +1 -0
  309. package/dist/core/generator/openspec-compat.js +524 -0
  310. package/dist/core/generator/openspec-compat.js.map +1 -0
  311. package/dist/core/generator/openspec-format-generator.d.ts +131 -0
  312. package/dist/core/generator/openspec-format-generator.d.ts.map +1 -0
  313. package/dist/core/generator/openspec-format-generator.js +963 -0
  314. package/dist/core/generator/openspec-format-generator.js.map +1 -0
  315. package/dist/core/generator/openspec-writer.d.ts +130 -0
  316. package/dist/core/generator/openspec-writer.d.ts.map +1 -0
  317. package/dist/core/generator/openspec-writer.js +404 -0
  318. package/dist/core/generator/openspec-writer.js.map +1 -0
  319. package/dist/core/generator/prompts.d.ts +35 -0
  320. package/dist/core/generator/prompts.d.ts.map +1 -0
  321. package/dist/core/generator/prompts.js +212 -0
  322. package/dist/core/generator/prompts.js.map +1 -0
  323. package/dist/core/generator/rag-manifest-generator.d.ts +37 -0
  324. package/dist/core/generator/rag-manifest-generator.d.ts.map +1 -0
  325. package/dist/core/generator/rag-manifest-generator.js +134 -0
  326. package/dist/core/generator/rag-manifest-generator.js.map +1 -0
  327. package/dist/core/generator/schemas.d.ts +365 -0
  328. package/dist/core/generator/schemas.d.ts.map +1 -0
  329. package/dist/core/generator/schemas.js +190 -0
  330. package/dist/core/generator/schemas.js.map +1 -0
  331. package/dist/core/generator/spec-pipeline.d.ts +123 -0
  332. package/dist/core/generator/spec-pipeline.d.ts.map +1 -0
  333. package/dist/core/generator/spec-pipeline.js +699 -0
  334. package/dist/core/generator/spec-pipeline.js.map +1 -0
  335. package/dist/core/generator/stages/stage1-survey.d.ts +19 -0
  336. package/dist/core/generator/stages/stage1-survey.d.ts.map +1 -0
  337. package/dist/core/generator/stages/stage1-survey.js +171 -0
  338. package/dist/core/generator/stages/stage1-survey.js.map +1 -0
  339. package/dist/core/generator/stages/stage2-entities.d.ts +11 -0
  340. package/dist/core/generator/stages/stage2-entities.d.ts.map +1 -0
  341. package/dist/core/generator/stages/stage2-entities.js +74 -0
  342. package/dist/core/generator/stages/stage2-entities.js.map +1 -0
  343. package/dist/core/generator/stages/stage3-services.d.ts +11 -0
  344. package/dist/core/generator/stages/stage3-services.d.ts.map +1 -0
  345. package/dist/core/generator/stages/stage3-services.js +85 -0
  346. package/dist/core/generator/stages/stage3-services.js.map +1 -0
  347. package/dist/core/generator/stages/stage4-api.d.ts +11 -0
  348. package/dist/core/generator/stages/stage4-api.d.ts.map +1 -0
  349. package/dist/core/generator/stages/stage4-api.js +72 -0
  350. package/dist/core/generator/stages/stage4-api.js.map +1 -0
  351. package/dist/core/generator/stages/stage5-architecture.d.ts +11 -0
  352. package/dist/core/generator/stages/stage5-architecture.d.ts.map +1 -0
  353. package/dist/core/generator/stages/stage5-architecture.js +75 -0
  354. package/dist/core/generator/stages/stage5-architecture.js.map +1 -0
  355. package/dist/core/generator/stages/stage6-adr.d.ts +8 -0
  356. package/dist/core/generator/stages/stage6-adr.d.ts.map +1 -0
  357. package/dist/core/generator/stages/stage6-adr.js +47 -0
  358. package/dist/core/generator/stages/stage6-adr.js.map +1 -0
  359. package/dist/core/services/chat-agent.d.ts +50 -0
  360. package/dist/core/services/chat-agent.d.ts.map +1 -0
  361. package/dist/core/services/chat-agent.js +369 -0
  362. package/dist/core/services/chat-agent.js.map +1 -0
  363. package/dist/core/services/chat-tools.d.ts +32 -0
  364. package/dist/core/services/chat-tools.d.ts.map +1 -0
  365. package/dist/core/services/chat-tools.js +494 -0
  366. package/dist/core/services/chat-tools.js.map +1 -0
  367. package/dist/core/services/config-manager.d.ts +61 -0
  368. package/dist/core/services/config-manager.d.ts.map +1 -0
  369. package/dist/core/services/config-manager.js +149 -0
  370. package/dist/core/services/config-manager.js.map +1 -0
  371. package/dist/core/services/edge-store.d.ts +57 -0
  372. package/dist/core/services/edge-store.d.ts.map +1 -0
  373. package/dist/core/services/edge-store.js +419 -0
  374. package/dist/core/services/edge-store.js.map +1 -0
  375. package/dist/core/services/gitignore-manager.d.ts +29 -0
  376. package/dist/core/services/gitignore-manager.d.ts.map +1 -0
  377. package/dist/core/services/gitignore-manager.js +95 -0
  378. package/dist/core/services/gitignore-manager.js.map +1 -0
  379. package/dist/core/services/index.d.ts +8 -0
  380. package/dist/core/services/index.d.ts.map +1 -0
  381. package/dist/core/services/index.js +8 -0
  382. package/dist/core/services/index.js.map +1 -0
  383. package/dist/core/services/llm-service.d.ts +379 -0
  384. package/dist/core/services/llm-service.d.ts.map +1 -0
  385. package/dist/core/services/llm-service.js +1553 -0
  386. package/dist/core/services/llm-service.js.map +1 -0
  387. package/dist/core/services/mcp-handlers/analysis.d.ts +127 -0
  388. package/dist/core/services/mcp-handlers/analysis.d.ts.map +1 -0
  389. package/dist/core/services/mcp-handlers/analysis.js +1185 -0
  390. package/dist/core/services/mcp-handlers/analysis.js.map +1 -0
  391. package/dist/core/services/mcp-handlers/change.d.ts +14 -0
  392. package/dist/core/services/mcp-handlers/change.d.ts.map +1 -0
  393. package/dist/core/services/mcp-handlers/change.js +416 -0
  394. package/dist/core/services/mcp-handlers/change.js.map +1 -0
  395. package/dist/core/services/mcp-handlers/decisions.d.ts +16 -0
  396. package/dist/core/services/mcp-handlers/decisions.d.ts.map +1 -0
  397. package/dist/core/services/mcp-handlers/decisions.js +239 -0
  398. package/dist/core/services/mcp-handlers/decisions.js.map +1 -0
  399. package/dist/core/services/mcp-handlers/graph.d.ts +94 -0
  400. package/dist/core/services/mcp-handlers/graph.d.ts.map +1 -0
  401. package/dist/core/services/mcp-handlers/graph.js +693 -0
  402. package/dist/core/services/mcp-handlers/graph.js.map +1 -0
  403. package/dist/core/services/mcp-handlers/orient.d.ts +17 -0
  404. package/dist/core/services/mcp-handlers/orient.d.ts.map +1 -0
  405. package/dist/core/services/mcp-handlers/orient.js +357 -0
  406. package/dist/core/services/mcp-handlers/orient.js.map +1 -0
  407. package/dist/core/services/mcp-handlers/semantic.d.ts +66 -0
  408. package/dist/core/services/mcp-handlers/semantic.d.ts.map +1 -0
  409. package/dist/core/services/mcp-handlers/semantic.js +432 -0
  410. package/dist/core/services/mcp-handlers/semantic.js.map +1 -0
  411. package/dist/core/services/mcp-handlers/utils.d.ts +85 -0
  412. package/dist/core/services/mcp-handlers/utils.d.ts.map +1 -0
  413. package/dist/core/services/mcp-handlers/utils.js +262 -0
  414. package/dist/core/services/mcp-handlers/utils.js.map +1 -0
  415. package/dist/core/services/mcp-watcher.d.ts +41 -0
  416. package/dist/core/services/mcp-watcher.d.ts.map +1 -0
  417. package/dist/core/services/mcp-watcher.js +254 -0
  418. package/dist/core/services/mcp-watcher.js.map +1 -0
  419. package/dist/core/services/project-detector.d.ts +32 -0
  420. package/dist/core/services/project-detector.d.ts.map +1 -0
  421. package/dist/core/services/project-detector.js +100 -0
  422. package/dist/core/services/project-detector.js.map +1 -0
  423. package/dist/core/test-generator/coverage-analyzer.d.ts +27 -0
  424. package/dist/core/test-generator/coverage-analyzer.d.ts.map +1 -0
  425. package/dist/core/test-generator/coverage-analyzer.js +285 -0
  426. package/dist/core/test-generator/coverage-analyzer.js.map +1 -0
  427. package/dist/core/test-generator/framework-detector.d.ts +17 -0
  428. package/dist/core/test-generator/framework-detector.d.ts.map +1 -0
  429. package/dist/core/test-generator/framework-detector.js +65 -0
  430. package/dist/core/test-generator/framework-detector.js.map +1 -0
  431. package/dist/core/test-generator/index.d.ts +14 -0
  432. package/dist/core/test-generator/index.d.ts.map +1 -0
  433. package/dist/core/test-generator/index.js +11 -0
  434. package/dist/core/test-generator/index.js.map +1 -0
  435. package/dist/core/test-generator/renderers/catch2.d.ts +8 -0
  436. package/dist/core/test-generator/renderers/catch2.d.ts.map +1 -0
  437. package/dist/core/test-generator/renderers/catch2.js +47 -0
  438. package/dist/core/test-generator/renderers/catch2.js.map +1 -0
  439. package/dist/core/test-generator/renderers/gtest.d.ts +8 -0
  440. package/dist/core/test-generator/renderers/gtest.d.ts.map +1 -0
  441. package/dist/core/test-generator/renderers/gtest.js +45 -0
  442. package/dist/core/test-generator/renderers/gtest.js.map +1 -0
  443. package/dist/core/test-generator/renderers/index.d.ts +20 -0
  444. package/dist/core/test-generator/renderers/index.d.ts.map +1 -0
  445. package/dist/core/test-generator/renderers/index.js +35 -0
  446. package/dist/core/test-generator/renderers/index.js.map +1 -0
  447. package/dist/core/test-generator/renderers/playwright.d.ts +8 -0
  448. package/dist/core/test-generator/renderers/playwright.d.ts.map +1 -0
  449. package/dist/core/test-generator/renderers/playwright.js +44 -0
  450. package/dist/core/test-generator/renderers/playwright.js.map +1 -0
  451. package/dist/core/test-generator/renderers/pytest.d.ts +8 -0
  452. package/dist/core/test-generator/renderers/pytest.d.ts.map +1 -0
  453. package/dist/core/test-generator/renderers/pytest.js +44 -0
  454. package/dist/core/test-generator/renderers/pytest.js.map +1 -0
  455. package/dist/core/test-generator/renderers/shared.d.ts +21 -0
  456. package/dist/core/test-generator/renderers/shared.d.ts.map +1 -0
  457. package/dist/core/test-generator/renderers/shared.js +56 -0
  458. package/dist/core/test-generator/renderers/shared.js.map +1 -0
  459. package/dist/core/test-generator/renderers/vitest.d.ts +8 -0
  460. package/dist/core/test-generator/renderers/vitest.d.ts.map +1 -0
  461. package/dist/core/test-generator/renderers/vitest.js +52 -0
  462. package/dist/core/test-generator/renderers/vitest.js.map +1 -0
  463. package/dist/core/test-generator/scenario-parser.d.ts +33 -0
  464. package/dist/core/test-generator/scenario-parser.d.ts.map +1 -0
  465. package/dist/core/test-generator/scenario-parser.js +244 -0
  466. package/dist/core/test-generator/scenario-parser.js.map +1 -0
  467. package/dist/core/test-generator/test-generator.d.ts +30 -0
  468. package/dist/core/test-generator/test-generator.d.ts.map +1 -0
  469. package/dist/core/test-generator/test-generator.js +174 -0
  470. package/dist/core/test-generator/test-generator.js.map +1 -0
  471. package/dist/core/test-generator/test-writer.d.ts +25 -0
  472. package/dist/core/test-generator/test-writer.d.ts.map +1 -0
  473. package/dist/core/test-generator/test-writer.js +128 -0
  474. package/dist/core/test-generator/test-writer.js.map +1 -0
  475. package/dist/core/test-generator/then-matchers.d.ts +35 -0
  476. package/dist/core/test-generator/then-matchers.d.ts.map +1 -0
  477. package/dist/core/test-generator/then-matchers.js +211 -0
  478. package/dist/core/test-generator/then-matchers.js.map +1 -0
  479. package/dist/core/verifier/index.d.ts +5 -0
  480. package/dist/core/verifier/index.d.ts.map +1 -0
  481. package/dist/core/verifier/index.js +5 -0
  482. package/dist/core/verifier/index.js.map +1 -0
  483. package/dist/core/verifier/verification-engine.d.ts +293 -0
  484. package/dist/core/verifier/verification-engine.d.ts.map +1 -0
  485. package/dist/core/verifier/verification-engine.js +919 -0
  486. package/dist/core/verifier/verification-engine.js.map +1 -0
  487. package/dist/types/index.d.ts +368 -0
  488. package/dist/types/index.d.ts.map +1 -0
  489. package/dist/types/index.js +5 -0
  490. package/dist/types/index.js.map +1 -0
  491. package/dist/types/pipeline.d.ts +167 -0
  492. package/dist/types/pipeline.d.ts.map +1 -0
  493. package/dist/types/pipeline.js +5 -0
  494. package/dist/types/pipeline.js.map +1 -0
  495. package/dist/types/test-generator.d.ts +103 -0
  496. package/dist/types/test-generator.d.ts.map +1 -0
  497. package/dist/types/test-generator.js +17 -0
  498. package/dist/types/test-generator.js.map +1 -0
  499. package/dist/utils/command-helpers.d.ts +68 -0
  500. package/dist/utils/command-helpers.d.ts.map +1 -0
  501. package/dist/utils/command-helpers.js +150 -0
  502. package/dist/utils/command-helpers.js.map +1 -0
  503. package/dist/utils/errors.d.ts +51 -0
  504. package/dist/utils/errors.d.ts.map +1 -0
  505. package/dist/utils/errors.js +129 -0
  506. package/dist/utils/errors.js.map +1 -0
  507. package/dist/utils/logger.d.ts +149 -0
  508. package/dist/utils/logger.d.ts.map +1 -0
  509. package/dist/utils/logger.js +342 -0
  510. package/dist/utils/logger.js.map +1 -0
  511. package/dist/utils/misc.d.ts +10 -0
  512. package/dist/utils/misc.d.ts.map +1 -0
  513. package/dist/utils/misc.js +21 -0
  514. package/dist/utils/misc.js.map +1 -0
  515. package/dist/utils/progress.d.ts +142 -0
  516. package/dist/utils/progress.d.ts.map +1 -0
  517. package/dist/utils/progress.js +283 -0
  518. package/dist/utils/progress.js.map +1 -0
  519. package/dist/utils/prompts.d.ts +53 -0
  520. package/dist/utils/prompts.d.ts.map +1 -0
  521. package/dist/utils/prompts.js +199 -0
  522. package/dist/utils/prompts.js.map +1 -0
  523. package/dist/utils/shutdown.d.ts +89 -0
  524. package/dist/utils/shutdown.d.ts.map +1 -0
  525. package/dist/utils/shutdown.js +238 -0
  526. package/dist/utils/shutdown.js.map +1 -0
  527. package/examples/bmad/README.md +113 -0
  528. package/examples/bmad/agents/architect.md +226 -0
  529. package/examples/bmad/agents/dev-brownfield.md +69 -0
  530. package/examples/bmad/setup/architect.customize.yaml +14 -0
  531. package/examples/bmad/tasks/implement-story.md +254 -0
  532. package/examples/bmad/tasks/onboarding.md +169 -0
  533. package/examples/bmad/tasks/refactor.md +178 -0
  534. package/examples/bmad/tasks/sprint-planning.md +168 -0
  535. package/examples/bmad/templates/story.md +108 -0
  536. package/examples/cline-workflows/openlore-analyze-codebase.md +101 -0
  537. package/examples/cline-workflows/openlore-check-spec-drift.md +102 -0
  538. package/examples/cline-workflows/openlore-execute-refactor.md +212 -0
  539. package/examples/cline-workflows/openlore-implement-feature.md +266 -0
  540. package/examples/cline-workflows/openlore-plan-refactor.md +279 -0
  541. package/examples/cline-workflows/openlore-refactor-codebase.md +16 -0
  542. package/examples/cline-workflows/openlore-write-tests.md +177 -0
  543. package/examples/drift-demo/openspec/config.yaml +14 -0
  544. package/examples/drift-demo/openspec/specs/architecture/spec.md +30 -0
  545. package/examples/drift-demo/openspec/specs/auth/spec.md +71 -0
  546. package/examples/drift-demo/openspec/specs/database/spec.md +33 -0
  547. package/examples/drift-demo/openspec/specs/overview/spec.md +20 -0
  548. package/examples/drift-demo/openspec/specs/projects/spec.md +55 -0
  549. package/examples/drift-demo/openspec/specs/tasks/spec.md +78 -0
  550. package/examples/drift-demo/package.json +21 -0
  551. package/examples/drift-demo/src/auth/auth-middleware.ts +30 -0
  552. package/examples/drift-demo/src/auth/auth-routes.ts +29 -0
  553. package/examples/drift-demo/src/auth/auth-service.ts +45 -0
  554. package/examples/drift-demo/src/database/connection.ts +27 -0
  555. package/examples/drift-demo/src/index.ts +16 -0
  556. package/examples/drift-demo/src/projects/project-model.ts +15 -0
  557. package/examples/drift-demo/src/projects/project-service.ts +34 -0
  558. package/examples/drift-demo/src/tasks/task-model.ts +37 -0
  559. package/examples/drift-demo/src/tasks/task-routes.ts +53 -0
  560. package/examples/drift-demo/src/tasks/task-service.ts +60 -0
  561. package/examples/drift-demo/src/utils/validation.ts +11 -0
  562. package/examples/drift-demo/tests/auth.test.ts +4 -0
  563. package/examples/drift-demo/tests/tasks.test.ts +4 -0
  564. package/examples/drift-demo/tsconfig.json +10 -0
  565. package/examples/drift-test/run-drift-test.sh +1087 -0
  566. package/examples/gsd/README.md +119 -0
  567. package/examples/gsd/commands/gsd/openlore-drift.md +111 -0
  568. package/examples/gsd/commands/gsd/openlore-orient.md +191 -0
  569. package/examples/mistral-vibe/README.md +101 -0
  570. package/examples/mistral-vibe/antipatterns-template.md +18 -0
  571. package/examples/mistral-vibe/skills/openlore-analyze-codebase/SKILL.md +124 -0
  572. package/examples/mistral-vibe/skills/openlore-brainstorm/SKILL.md +379 -0
  573. package/examples/mistral-vibe/skills/openlore-debug/SKILL.md +330 -0
  574. package/examples/mistral-vibe/skills/openlore-execute-refactor/SKILL.md +291 -0
  575. package/examples/mistral-vibe/skills/openlore-generate/SKILL.md +245 -0
  576. package/examples/mistral-vibe/skills/openlore-implement-story/SKILL.md +326 -0
  577. package/examples/mistral-vibe/skills/openlore-plan-refactor/SKILL.md +365 -0
  578. package/examples/mistral-vibe/skills/openlore-review-changes/SKILL.md +128 -0
  579. package/examples/mistral-vibe/skills/openlore-write-tests/SKILL.md +261 -0
  580. package/examples/opencode/agent-guard.ts +170 -0
  581. package/examples/opencode/plugins/anti-laziness.ts +202 -0
  582. package/examples/opencode/plugins/lib/openlore-context-injector-helpers.ts +116 -0
  583. package/examples/opencode/plugins/lib/openlore-decision-extractor-helpers.ts +65 -0
  584. package/examples/opencode/plugins/openlore-context-injector.test.ts +211 -0
  585. package/examples/opencode/plugins/openlore-context-injector.ts +165 -0
  586. package/examples/opencode/plugins/openlore-decision-extractor.test.ts +131 -0
  587. package/examples/opencode/plugins/openlore-decision-extractor.ts +322 -0
  588. package/examples/opencode/plugins/openlore-enforcer.ts +227 -0
  589. package/examples/opencode/prompts/sisyphus-sdd.md +150 -0
  590. package/examples/opencode-skills/openlore-analyze-codebase/SKILL.md +101 -0
  591. package/examples/opencode-skills/openlore-brainstorm/SKILL.md +354 -0
  592. package/examples/opencode-skills/openlore-debug/SKILL.md +291 -0
  593. package/examples/opencode-skills/openlore-execute-refactor/SKILL.md +241 -0
  594. package/examples/opencode-skills/openlore-generate/SKILL.md +236 -0
  595. package/examples/opencode-skills/openlore-implement-story/SKILL.md +251 -0
  596. package/examples/opencode-skills/openlore-plan-refactor/SKILL.md +298 -0
  597. package/examples/opencode-skills/openlore-review-changes/SKILL.md +134 -0
  598. package/examples/opencode-skills/openlore-write-tests/SKILL.md +230 -0
  599. package/examples/openspec-analysis/README.md +59 -0
  600. package/examples/openspec-analysis/SUMMARY.md +72 -0
  601. package/examples/openspec-analysis/config.json +16 -0
  602. package/examples/openspec-analysis/dependencies.mermaid +35 -0
  603. package/examples/openspec-analysis/dependency-graph.json +12116 -0
  604. package/examples/openspec-analysis/llm-context.json +119 -0
  605. package/examples/openspec-analysis/repo-structure.json +871 -0
  606. package/examples/openspec-cli/README.md +67 -0
  607. package/examples/openspec-cli/openspec/config.yaml +26 -0
  608. package/examples/openspec-cli/openspec/specs/architecture/spec.md +178 -0
  609. package/examples/openspec-cli/openspec/specs/artifact-graph/spec.md +143 -0
  610. package/examples/openspec-cli/openspec/specs/cli/spec.md +138 -0
  611. package/examples/openspec-cli/openspec/specs/overview/spec.md +60 -0
  612. package/examples/openspec-cli/openspec/specs/parsing/spec.md +123 -0
  613. package/examples/openspec-cli/openspec/specs/validation/spec.md +108 -0
  614. package/examples/spec-kit/README.md +104 -0
  615. package/examples/spec-kit/commands/drift.md +87 -0
  616. package/examples/spec-kit/commands/orient.md +138 -0
  617. package/examples/spec-kit/extension.yml +54 -0
  618. package/package.json +125 -0
  619. package/src/viewer/InteractiveGraphViewer.jsx +1600 -0
  620. package/src/viewer/app/index.html +17 -0
  621. package/src/viewer/app/main.jsx +13 -0
  622. package/src/viewer/components/ArchitectureView.jsx +177 -0
  623. package/src/viewer/components/ChatPanel.jsx +450 -0
  624. package/src/viewer/components/ClassGraph.jsx +782 -0
  625. package/src/viewer/components/ClusterGraph.jsx +469 -0
  626. package/src/viewer/components/FilterBar.jsx +179 -0
  627. package/src/viewer/components/FlatGraph.jsx +282 -0
  628. package/src/viewer/components/MicroComponents.jsx +85 -0
  629. package/src/viewer/hooks/usePanZoom.js +79 -0
  630. package/src/viewer/utils/constants.js +64 -0
  631. package/src/viewer/utils/graph-helpers.js +303 -0
  632. package/src/viewer/utils/graph-helpers.test.ts +39 -0
  633. package/src/viewer/utils/themes.js +206 -0
  634. package/stubs/tree-sitter-cli-stub/package.json +6 -0
@@ -0,0 +1,2040 @@
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, Swift — 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 { dirname, resolve as resolvePath } from 'node:path';
15
+ import Parser from 'tree-sitter';
16
+ import { FunctionRegistryTrie } from './function-registry-trie.js';
17
+ import { inferTypesFromSource, resolveViaTypeInference } from './type-inference-engine.js';
18
+ import { extractAllHttpEdges } from './http-route-parser.js';
19
+ // ============================================================================
20
+ // CONSTANTS
21
+ // ============================================================================
22
+ const HUB_THRESHOLD = 5;
23
+ /** Common builtins and stdlib names to ignore as call targets (across all languages) */
24
+ const IGNORED_CALLEES = new Set([
25
+ // Python builtins
26
+ 'print', 'len', 'range', 'str', 'int', 'float', 'list', 'dict', 'set', 'tuple',
27
+ 'bool', 'type', 'isinstance', 'issubclass', 'hasattr', 'getattr', 'setattr',
28
+ 'enumerate', 'zip', 'map', 'filter', 'sorted', 'reversed', 'sum', 'min', 'max',
29
+ 'open', 'input', 'format', 'repr', 'id', 'hash', 'abs', 'round', 'pow',
30
+ 'super', 'object', 'property', 'staticmethod', 'classmethod',
31
+ // JS/TS common
32
+ 'console', 'log', 'error', 'warn', 'JSON', 'parse', 'stringify',
33
+ 'Promise', 'resolve', 'reject', 'then', 'catch', 'finally',
34
+ 'Array', 'Object', 'String', 'Number', 'Boolean', 'Math', 'Date',
35
+ 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
36
+ 'require', 'import', 'exports',
37
+ // Python control flow (used like functions sometimes)
38
+ 'assert', 'raise', 'return', 'yield', 'await', 'pass', 'del',
39
+ // Node.js common
40
+ 'readFile', 'writeFile', 'mkdir', 'join', 'resolve', 'basename', 'dirname',
41
+ 'existsSync', 'readFileSync', 'writeFileSync',
42
+ // Go builtins
43
+ 'make', 'new', 'append', 'copy', 'delete', 'close', 'panic', 'recover',
44
+ 'println', 'printf', 'sprintf', 'errorf', 'fprintf',
45
+ // Rust macros / common stdlib
46
+ 'println', 'eprintln', 'format', 'vec', 'assert', 'unwrap', 'expect',
47
+ 'ok', 'err', 'some', 'none',
48
+ // Ruby builtins
49
+ 'puts', 'print', 'p', 'raise', 'require', 'require_relative', 'include',
50
+ 'extend', 'attr_accessor', 'attr_reader', 'attr_writer',
51
+ // Java common
52
+ 'toString', 'equals', 'hashCode', 'getClass', 'println', 'printf',
53
+ // Swift stdlib / builtins
54
+ 'print', 'debugPrint', 'dump', 'fatalError', 'precondition', 'preconditionFailure',
55
+ 'assert', 'assertionFailure', 'withUnsafePointer', 'withUnsafeMutablePointer',
56
+ 'DispatchQueue', 'main', 'async', 'sync', 'append', 'remove', 'insert', 'contains',
57
+ 'map', 'filter', 'reduce', 'forEach', 'compactMap', 'flatMap', 'sorted', 'first', 'last',
58
+ // C++ stdlib / builtins
59
+ 'cout', 'cin', 'cerr', 'endl', 'malloc', 'free', 'memcpy', 'memset', 'memcmp',
60
+ 'strlen', 'strcpy', 'strcat', 'strcmp', 'sprintf', 'snprintf', 'fprintf',
61
+ 'push_back', 'pop_back', 'emplace_back', 'begin', 'end', 'size', 'empty',
62
+ 'find', 'insert', 'erase', 'at', 'front', 'back', 'clear', 'reserve', 'resize',
63
+ 'make_shared', 'make_unique', 'move', 'forward', 'swap',
64
+ 'static_cast', 'dynamic_cast', 'reinterpret_cast', 'const_cast',
65
+ ]);
66
+ /** Returns true if the name should be skipped as a call target. */
67
+ function isIgnoredCallee(name) {
68
+ if (IGNORED_CALLEES.has(name))
69
+ return true;
70
+ // ALL_CAPS names (3+ chars) are almost certainly C/C++ macros, not functions
71
+ if (/^[A-Z][A-Z0-9_]{2,}$/.test(name))
72
+ return true;
73
+ return false;
74
+ }
75
+ // ============================================================================
76
+ // PARSER SINGLETONS (lazy init)
77
+ // ============================================================================
78
+ let _tsParser;
79
+ let _pyParser;
80
+ let _goParser;
81
+ let _rustParser;
82
+ let _rubyParser;
83
+ let _javaParser;
84
+ let _cppParser;
85
+ let _swiftParser;
86
+ let _TsLanguage;
87
+ let _PyLanguage;
88
+ let _GoLanguage;
89
+ let _RustLanguage;
90
+ let _RubyLanguage;
91
+ let _JavaLanguage;
92
+ let _CppLanguage;
93
+ let _SwiftLanguage;
94
+ async function getTSParser() {
95
+ if (!_tsParser) {
96
+ const tsModule = await import('tree-sitter-typescript');
97
+ _TsLanguage = tsModule.default.typescript;
98
+ _tsParser = new Parser();
99
+ _tsParser.setLanguage(_TsLanguage);
100
+ }
101
+ return { parser: _tsParser, lang: _TsLanguage };
102
+ }
103
+ async function getPyParser() {
104
+ if (!_pyParser) {
105
+ const pyModule = await import('tree-sitter-python');
106
+ _PyLanguage = pyModule.default;
107
+ _pyParser = new Parser();
108
+ _pyParser.setLanguage(_PyLanguage);
109
+ }
110
+ return { parser: _pyParser, lang: _PyLanguage };
111
+ }
112
+ async function getGoParser() {
113
+ if (!_goParser) {
114
+ const goModule = await import('tree-sitter-go');
115
+ _GoLanguage = goModule.default;
116
+ _goParser = new Parser();
117
+ _goParser.setLanguage(_GoLanguage);
118
+ }
119
+ return { parser: _goParser, lang: _GoLanguage };
120
+ }
121
+ async function getRustParser() {
122
+ if (!_rustParser) {
123
+ const rustModule = await import('tree-sitter-rust');
124
+ _RustLanguage = rustModule.default;
125
+ _rustParser = new Parser();
126
+ _rustParser.setLanguage(_RustLanguage);
127
+ }
128
+ return { parser: _rustParser, lang: _RustLanguage };
129
+ }
130
+ async function getRubyParser() {
131
+ if (!_rubyParser) {
132
+ const rubyModule = await import('tree-sitter-ruby');
133
+ _RubyLanguage = rubyModule.default;
134
+ _rubyParser = new Parser();
135
+ _rubyParser.setLanguage(_RubyLanguage);
136
+ }
137
+ return { parser: _rubyParser, lang: _RubyLanguage };
138
+ }
139
+ async function getJavaParser() {
140
+ if (!_javaParser) {
141
+ const javaModule = await import('tree-sitter-java');
142
+ _JavaLanguage = javaModule.default;
143
+ _javaParser = new Parser();
144
+ _javaParser.setLanguage(_JavaLanguage);
145
+ }
146
+ return { parser: _javaParser, lang: _JavaLanguage };
147
+ }
148
+ async function getCppParser() {
149
+ if (!_cppParser) {
150
+ const cppModule = await import('tree-sitter-cpp');
151
+ _CppLanguage = cppModule.default;
152
+ _cppParser = new Parser();
153
+ _cppParser.setLanguage(_CppLanguage);
154
+ }
155
+ return { parser: _cppParser, lang: _CppLanguage };
156
+ }
157
+ async function getSwiftParser() {
158
+ if (!_swiftParser) {
159
+ const swiftModule = await import('tree-sitter-swift');
160
+ _SwiftLanguage = swiftModule.default;
161
+ _swiftParser = new Parser();
162
+ _swiftParser.setLanguage(_SwiftLanguage);
163
+ }
164
+ return { parser: _swiftParser, lang: _SwiftLanguage };
165
+ }
166
+ // ============================================================================
167
+ // ATTRIBUTION HELPER
168
+ // ============================================================================
169
+ /**
170
+ * Given a list of function nodes (with startIndex/endIndex) and a call position,
171
+ * find the narrowest enclosing function node.
172
+ */
173
+ function findEnclosingFunction(nodes, callPos) {
174
+ let best;
175
+ let bestSize = Infinity;
176
+ for (const n of nodes) {
177
+ if (n.startIndex <= callPos && callPos < n.endIndex) {
178
+ const size = n.endIndex - n.startIndex;
179
+ if (size < bestSize) {
180
+ bestSize = size;
181
+ best = n;
182
+ }
183
+ }
184
+ }
185
+ return best;
186
+ }
187
+ // ============================================================================
188
+ // DOCSTRING / SIGNATURE EXTRACTION HELPERS
189
+ // ============================================================================
190
+ /**
191
+ * Scan backward from `startIndex` in `source` to find the doc comment
192
+ * immediately preceding the function declaration. Skip blank lines.
193
+ *
194
+ * For Python, docstrings are INSIDE the function body — scan forward from
195
+ * `startIndex` past the `def name(...):` colon to find the triple-quoted string.
196
+ *
197
+ * Returns the first meaningful (non-empty, non-decorator) line of the comment.
198
+ */
199
+ function extractDocstringBefore(source, startIndex, language) {
200
+ // ── Python: scan forward past the colon into the function body ──────────
201
+ if (language === 'Python') {
202
+ // Find the colon that ends the `def` line
203
+ let i = startIndex;
204
+ while (i < source.length && source[i] !== ':')
205
+ i++;
206
+ // Skip past the colon
207
+ i++;
208
+ // Skip whitespace / newline
209
+ while (i < source.length && (source[i] === ' ' || source[i] === '\t' || source[i] === '\n' || source[i] === '\r'))
210
+ i++;
211
+ // Check for triple-quoted docstring
212
+ const tripleDouble = source.startsWith('"""', i);
213
+ const tripleSingle = source.startsWith("'''", i);
214
+ if (tripleDouble || tripleSingle) {
215
+ const quote = tripleDouble ? '"""' : "'''";
216
+ const bodyStart = i + 3;
217
+ const closeIdx = source.indexOf(quote, bodyStart);
218
+ if (closeIdx === -1)
219
+ return undefined;
220
+ const inner = source.slice(bodyStart, closeIdx);
221
+ const firstLine = inner.split('\n').map(l => l.trim()).find(l => l.length > 0);
222
+ return firstLine ?? undefined;
223
+ }
224
+ return undefined;
225
+ }
226
+ // ── All other languages: scan backward from startIndex ─────────────────
227
+ // Move to the character just before startIndex
228
+ let pos = startIndex - 1;
229
+ // Skip trailing whitespace / newlines before the declaration
230
+ while (pos >= 0 && (source[pos] === ' ' || source[pos] === '\t' || source[pos] === '\n' || source[pos] === '\r')) {
231
+ pos--;
232
+ }
233
+ if (pos < 0)
234
+ return undefined;
235
+ // ── TypeScript / JavaScript / Java / C++: JSDoc block /** ... */ ────────
236
+ if (language === 'TypeScript' || language === 'JavaScript' ||
237
+ language === 'Java' || language === 'C++') {
238
+ // Expect closing */ of a JSDoc block
239
+ if (source[pos] === '/' && pos > 0 && source[pos - 1] === '*') {
240
+ const closePos = pos - 1; // points at '*' of closing '*/'
241
+ // Find opening /**
242
+ const openIdx = source.lastIndexOf('/**', closePos);
243
+ if (openIdx === -1)
244
+ return undefined;
245
+ const inner = source.slice(openIdx + 3, closePos - 0);
246
+ // Remove leading * on each line, find first non-empty, non-@ line
247
+ const firstLine = inner
248
+ .split('\n')
249
+ .map(l => l.replace(/^\s*\*\s?/, '').trim())
250
+ .find(l => l.length > 0 && !l.startsWith('@'));
251
+ return firstLine ?? undefined;
252
+ }
253
+ return undefined;
254
+ }
255
+ // ── Go: // comment lines immediately before ──────────────────────────────
256
+ if (language === 'Go') {
257
+ const lines = [];
258
+ // Walk backward line by line
259
+ let lineEnd = pos;
260
+ while (lineEnd >= 0) {
261
+ // Find start of this line
262
+ let lineStart = lineEnd;
263
+ while (lineStart > 0 && source[lineStart - 1] !== '\n')
264
+ lineStart--;
265
+ const line = source.slice(lineStart, lineEnd + 1).trimEnd();
266
+ const trimmed = line.trim();
267
+ if (trimmed.startsWith('//')) {
268
+ lines.unshift(trimmed.slice(2).trim());
269
+ lineEnd = lineStart - 1;
270
+ // Skip over the newline
271
+ while (lineEnd >= 0 && (source[lineEnd] === '\n' || source[lineEnd] === '\r'))
272
+ lineEnd--;
273
+ }
274
+ else {
275
+ break;
276
+ }
277
+ }
278
+ return lines.find(l => l.length > 0) ?? undefined;
279
+ }
280
+ // ── Rust / Swift: /// doc comment lines immediately before ─────────────
281
+ if (language === 'Rust' || language === 'Swift') {
282
+ const lines = [];
283
+ let lineEnd = pos;
284
+ while (lineEnd >= 0) {
285
+ let lineStart = lineEnd;
286
+ while (lineStart > 0 && source[lineStart - 1] !== '\n')
287
+ lineStart--;
288
+ const line = source.slice(lineStart, lineEnd + 1).trimEnd();
289
+ const trimmed = line.trim();
290
+ if (trimmed.startsWith('///')) {
291
+ lines.unshift(trimmed.slice(3).trim());
292
+ lineEnd = lineStart - 1;
293
+ while (lineEnd >= 0 && (source[lineEnd] === '\n' || source[lineEnd] === '\r'))
294
+ lineEnd--;
295
+ }
296
+ else {
297
+ break;
298
+ }
299
+ }
300
+ return lines.find(l => l.length > 0) ?? undefined;
301
+ }
302
+ // ── Ruby: # comment lines immediately before ─────────────────────────────
303
+ if (language === 'Ruby') {
304
+ const lines = [];
305
+ let lineEnd = pos;
306
+ while (lineEnd >= 0) {
307
+ let lineStart = lineEnd;
308
+ while (lineStart > 0 && source[lineStart - 1] !== '\n')
309
+ lineStart--;
310
+ const line = source.slice(lineStart, lineEnd + 1).trimEnd();
311
+ const trimmed = line.trim();
312
+ if (trimmed.startsWith('#')) {
313
+ lines.unshift(trimmed.slice(1).trim());
314
+ lineEnd = lineStart - 1;
315
+ while (lineEnd >= 0 && (source[lineEnd] === '\n' || source[lineEnd] === '\r'))
316
+ lineEnd--;
317
+ }
318
+ else {
319
+ break;
320
+ }
321
+ }
322
+ return lines.find(l => l.length > 0) ?? undefined;
323
+ }
324
+ return undefined;
325
+ }
326
+ /**
327
+ * Extract the function declaration (signature without body) from
328
+ * `source.slice(startIndex, endIndex)`.
329
+ *
330
+ * Strategy:
331
+ * - TS/JS/Java/C++/Go/Rust/Ruby: take everything up to the first `{` at depth 0
332
+ * - Python: take everything up to the first `:` that ends the `def` line
333
+ *
334
+ * Whitespace is normalized (multiple spaces/newlines → single space).
335
+ * Limited to 300 characters max.
336
+ */
337
+ function extractDeclaration(source, startIndex, endIndex, language) {
338
+ const slice = source.slice(startIndex, Math.min(endIndex, startIndex + 1500));
339
+ let decl;
340
+ if (language === 'Python') {
341
+ // Take up to (not including) the first `:` that ends the def line
342
+ // We scan for `:` while tracking parenthesis depth to avoid matching
343
+ // colons inside type annotations (e.g., def f(x: int) -> dict[str, int]:)
344
+ let depth = 0;
345
+ let end = -1;
346
+ for (let i = 0; i < slice.length; i++) {
347
+ const ch = slice[i];
348
+ if (ch === '(' || ch === '[' || ch === '{')
349
+ depth++;
350
+ else if (ch === ')' || ch === ']' || ch === '}')
351
+ depth--;
352
+ else if (ch === ':' && depth === 0) {
353
+ end = i;
354
+ break;
355
+ }
356
+ }
357
+ decl = end !== -1 ? slice.slice(0, end) : slice.slice(0, 300);
358
+ }
359
+ else {
360
+ // Find first `{` at brace depth 0
361
+ let depth = 0;
362
+ let end = -1;
363
+ for (let i = 0; i < slice.length; i++) {
364
+ const ch = slice[i];
365
+ if (ch === '{') {
366
+ if (depth === 0) {
367
+ end = i;
368
+ break;
369
+ }
370
+ depth++;
371
+ }
372
+ else if (ch === '}') {
373
+ depth--;
374
+ }
375
+ }
376
+ decl = end !== -1 ? slice.slice(0, end) : slice.slice(0, 300);
377
+ }
378
+ // Normalize whitespace
379
+ return decl.replace(/\s+/g, ' ').trim().slice(0, 300);
380
+ }
381
+ // ============================================================================
382
+ // TYPESCRIPT EXTRACTOR
383
+ // ============================================================================
384
+ const TS_FN_QUERY = `
385
+ (function_declaration
386
+ name: (identifier) @fn.name) @fn.node
387
+
388
+ (export_statement
389
+ declaration: (function_declaration
390
+ name: (identifier) @fn.name)) @fn.node
391
+
392
+ (method_definition
393
+ name: (property_identifier) @fn.name) @fn.node
394
+
395
+ (lexical_declaration
396
+ (variable_declarator
397
+ name: (identifier) @fn.name
398
+ value: [(arrow_function) (function_expression)] @fn.value)) @fn.node
399
+ `;
400
+ const TS_CALL_QUERY = `
401
+ (call_expression
402
+ function: [(identifier) @call.name
403
+ (member_expression
404
+ object: (identifier) @call.object
405
+ property: (property_identifier) @call.name)]) @call.node
406
+ `;
407
+ async function extractTSGraph(filePath, content) {
408
+ const { parser, lang } = await getTSParser();
409
+ const tree = parser.parse(content);
410
+ const fnQuery = new Parser.Query(lang, TS_FN_QUERY);
411
+ const callQuery = new Parser.Query(lang, TS_CALL_QUERY);
412
+ // --- Extract function nodes ---
413
+ const nodes = [];
414
+ const fnMatches = fnQuery.matches(tree.rootNode);
415
+ for (const match of fnMatches) {
416
+ const nameCapture = match.captures.find(c => c.name === 'fn.name');
417
+ const nodeCapture = match.captures.find(c => c.name === 'fn.node');
418
+ if (!nameCapture || !nodeCapture)
419
+ continue;
420
+ const name = nameCapture.node.text;
421
+ const fnNode = nodeCapture.node;
422
+ // Find enclosing class (walk up — skip class_body, its children are methods not the name)
423
+ let className;
424
+ let cursor = fnNode.parent;
425
+ while (cursor) {
426
+ if (cursor.type === 'class_declaration') {
427
+ const classNameNode = cursor.children.find(c => c.type === 'type_identifier' || c.type === 'identifier');
428
+ if (classNameNode)
429
+ className = classNameNode.text;
430
+ break;
431
+ }
432
+ cursor = cursor.parent;
433
+ }
434
+ // Detect async (method_definition has 'async' as first named child keyword)
435
+ const isAsync = fnNode.children.some(c => c.type === 'async') ||
436
+ fnNode.text.startsWith('async ');
437
+ const id = className
438
+ ? `${filePath}::${className}.${name}`
439
+ : `${filePath}::${name}`;
440
+ nodes.push({
441
+ id,
442
+ name,
443
+ filePath,
444
+ className,
445
+ isAsync,
446
+ language: 'TypeScript',
447
+ startIndex: fnNode.startIndex,
448
+ endIndex: fnNode.endIndex,
449
+ fanIn: 0,
450
+ fanOut: 0,
451
+ docstring: extractDocstringBefore(content, fnNode.startIndex, 'TypeScript'),
452
+ signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'TypeScript'),
453
+ });
454
+ }
455
+ // --- Extract calls ---
456
+ const rawEdges = [];
457
+ const callMatches = callQuery.matches(tree.rootNode);
458
+ for (const match of callMatches) {
459
+ const nameCapture = match.captures.find(c => c.name === 'call.name');
460
+ const nodeCapture = match.captures.find(c => c.name === 'call.node');
461
+ const objectCapture = match.captures.find(c => c.name === 'call.object');
462
+ if (!nameCapture || !nodeCapture)
463
+ continue;
464
+ const calleeName = nameCapture.node.text;
465
+ if (isIgnoredCallee(calleeName))
466
+ continue;
467
+ const callPos = nodeCapture.node.startIndex;
468
+ const caller = findEnclosingFunction(nodes, callPos);
469
+ if (!caller)
470
+ continue;
471
+ // Detect call type from AST parent context
472
+ let callType = objectCapture ? 'method' : 'direct';
473
+ const parentType = nodeCapture.node.parent?.type;
474
+ if (parentType === 'await_expression')
475
+ callType = 'awaited';
476
+ else if (parentType === 'new_expression')
477
+ callType = 'constructor';
478
+ rawEdges.push({
479
+ callerId: caller.id,
480
+ calleeName,
481
+ line: nodeCapture.node.startPosition.row + 1,
482
+ calleeObject: objectCapture?.node.text,
483
+ callType,
484
+ });
485
+ }
486
+ return { nodes, rawEdges };
487
+ }
488
+ // ============================================================================
489
+ // PYTHON EXTRACTOR
490
+ // ============================================================================
491
+ const PY_FN_QUERY = `
492
+ (function_definition
493
+ name: (identifier) @fn.name) @fn.node
494
+
495
+ (decorated_definition
496
+ (function_definition
497
+ name: (identifier) @fn.name)) @fn.node
498
+ `;
499
+ /**
500
+ * Direct function calls: foo(), bar(x)
501
+ * We keep this separate from attribute calls so we can filter attribute calls
502
+ * by object name (only self/cls are resolved to internal functions).
503
+ */
504
+ const PY_DIRECT_CALL_QUERY = `
505
+ (call
506
+ function: (identifier) @call.name) @call.node
507
+ `;
508
+ /**
509
+ * Method calls on an object: obj.method()
510
+ * We capture the object name so we can restrict resolution to self/cls.
511
+ * Calls like redis.get(), dict.get(), os.environ.get() are NOT resolved —
512
+ * only self.method() and cls.method() are tracked as internal edges.
513
+ */
514
+ const PY_METHOD_CALL_QUERY = `
515
+ (call
516
+ function: (attribute
517
+ object: (identifier) @call.object
518
+ attribute: (identifier) @call.name)) @call.node
519
+ `;
520
+ async function extractPyGraph(filePath, content) {
521
+ const { parser, lang } = await getPyParser();
522
+ const tree = parser.parse(content);
523
+ const fnQuery = new Parser.Query(lang, PY_FN_QUERY);
524
+ // --- Extract function nodes ---
525
+ const nodes = [];
526
+ const seen = new Set(); // avoid duplicates from decorated_definition + function_definition
527
+ const fnMatches = fnQuery.matches(tree.rootNode);
528
+ for (const match of fnMatches) {
529
+ const nameCapture = match.captures.find(c => c.name === 'fn.name');
530
+ const nodeCapture = match.captures.find(c => c.name === 'fn.node');
531
+ if (!nameCapture || !nodeCapture)
532
+ continue;
533
+ const name = nameCapture.node.text;
534
+ const fnNode = nodeCapture.node;
535
+ // Deduplicate by name node position (decorated_definition wraps the function_definition)
536
+ if (seen.has(nameCapture.node.startIndex))
537
+ continue;
538
+ seen.add(nameCapture.node.startIndex);
539
+ // Find enclosing class
540
+ let className;
541
+ let cursor = fnNode.parent;
542
+ while (cursor) {
543
+ if (cursor.type === 'class_definition') {
544
+ const classNameNode = cursor.children.find(c => c.type === 'identifier');
545
+ if (classNameNode)
546
+ className = classNameNode.text;
547
+ break;
548
+ }
549
+ cursor = cursor.parent;
550
+ }
551
+ // Skip private methods (underscore prefix) unless they're __init__ or there are very few nodes
552
+ if (name.startsWith('_') && name !== '__init__')
553
+ continue;
554
+ const isAsync = fnNode.text.startsWith('async ') ||
555
+ (fnNode.type === 'function_definition' && fnNode.children[0]?.text === 'async');
556
+ const id = className
557
+ ? `${filePath}::${className}.${name}`
558
+ : `${filePath}::${name}`;
559
+ nodes.push({
560
+ id,
561
+ name,
562
+ filePath,
563
+ className,
564
+ isAsync,
565
+ language: 'Python',
566
+ startIndex: fnNode.startIndex,
567
+ endIndex: fnNode.endIndex,
568
+ fanIn: 0,
569
+ fanOut: 0,
570
+ docstring: extractDocstringBefore(content, fnNode.startIndex, 'Python'),
571
+ signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'Python'),
572
+ });
573
+ }
574
+ // --- Extract calls ---
575
+ const rawEdges = [];
576
+ const directCallQuery = new Parser.Query(lang, PY_DIRECT_CALL_QUERY);
577
+ const methodCallQuery = new Parser.Query(lang, PY_METHOD_CALL_QUERY);
578
+ // Direct calls: foo(), bar(x) — resolve across all files
579
+ for (const match of directCallQuery.matches(tree.rootNode)) {
580
+ const nameCapture = match.captures.find(c => c.name === 'call.name');
581
+ const nodeCapture = match.captures.find(c => c.name === 'call.node');
582
+ if (!nameCapture || !nodeCapture)
583
+ continue;
584
+ const calleeName = nameCapture.node.text;
585
+ if (isIgnoredCallee(calleeName))
586
+ continue;
587
+ const callPos = nodeCapture.node.startIndex;
588
+ const caller = findEnclosingFunction(nodes, callPos);
589
+ if (!caller)
590
+ continue;
591
+ // In Python tree-sitter, `await expr` wraps the call: parent type is 'await'
592
+ const callType = nodeCapture.node.parent?.type === 'await' ? 'awaited' : 'direct';
593
+ rawEdges.push({
594
+ callerId: caller.id,
595
+ calleeName,
596
+ line: nodeCapture.node.startPosition.row + 1,
597
+ callType,
598
+ });
599
+ }
600
+ // Method calls: obj.method() — capture receiver for type-inference-based resolution
601
+ for (const match of methodCallQuery.matches(tree.rootNode)) {
602
+ const objectCapture = match.captures.find(c => c.name === 'call.object');
603
+ const nameCapture = match.captures.find(c => c.name === 'call.name');
604
+ const nodeCapture = match.captures.find(c => c.name === 'call.node');
605
+ if (!objectCapture || !nameCapture || !nodeCapture)
606
+ continue;
607
+ const calleeName = nameCapture.node.text;
608
+ if (isIgnoredCallee(calleeName))
609
+ continue;
610
+ const callPos = nodeCapture.node.startIndex;
611
+ const caller = findEnclosingFunction(nodes, callPos);
612
+ if (!caller)
613
+ continue;
614
+ const methodCallType = nodeCapture.node.parent?.type === 'await' ? 'awaited' : 'method';
615
+ rawEdges.push({
616
+ callerId: caller.id,
617
+ calleeName,
618
+ line: nodeCapture.node.startPosition.row + 1,
619
+ calleeObject: objectCapture.node.text,
620
+ callType: methodCallType,
621
+ });
622
+ }
623
+ return { nodes, rawEdges };
624
+ }
625
+ // ============================================================================
626
+ // GO EXTRACTOR
627
+ // ============================================================================
628
+ const GO_FN_QUERY = `
629
+ (function_declaration
630
+ name: (identifier) @fn.name) @fn.node
631
+
632
+ (method_declaration
633
+ name: (field_identifier) @fn.name) @fn.node
634
+ `;
635
+ const GO_CALL_QUERY = `
636
+ (call_expression
637
+ function: (identifier) @call.name) @call.node
638
+
639
+ (call_expression
640
+ function: (selector_expression
641
+ operand: (identifier) @call.object
642
+ field: (field_identifier) @call.name)) @call.node
643
+ `;
644
+ async function extractGoGraph(filePath, content) {
645
+ const { parser, lang } = await getGoParser();
646
+ const tree = parser.parse(content);
647
+ const fnQuery = new Parser.Query(lang, GO_FN_QUERY);
648
+ const callQuery = new Parser.Query(lang, GO_CALL_QUERY);
649
+ const nodes = [];
650
+ for (const match of fnQuery.matches(tree.rootNode)) {
651
+ const nameCapture = match.captures.find(c => c.name === 'fn.name');
652
+ const nodeCapture = match.captures.find(c => c.name === 'fn.node');
653
+ if (!nameCapture || !nodeCapture)
654
+ continue;
655
+ const name = nameCapture.node.text;
656
+ const fnNode = nodeCapture.node;
657
+ // Receiver type for method_declaration → use as className
658
+ let className;
659
+ if (fnNode.type === 'method_declaration') {
660
+ const receiver = fnNode.children.find(c => c.type === 'parameter_list');
661
+ if (receiver) {
662
+ // Extract type name from receiver: (r *MyStruct) → MyStruct
663
+ const typeNode = receiver.descendantsOfType('type_identifier')[0]
664
+ ?? receiver.descendantsOfType('pointer_type')[0];
665
+ if (typeNode)
666
+ className = typeNode.text.replace(/^\*/, '');
667
+ }
668
+ }
669
+ const id = className ? `${filePath}::${className}.${name}` : `${filePath}::${name}`;
670
+ nodes.push({
671
+ id, name, filePath, className,
672
+ isAsync: false, // Go has goroutines, not async/await
673
+ language: 'Go',
674
+ startIndex: fnNode.startIndex,
675
+ endIndex: fnNode.endIndex,
676
+ fanIn: 0, fanOut: 0,
677
+ docstring: extractDocstringBefore(content, fnNode.startIndex, 'Go'),
678
+ signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'Go'),
679
+ });
680
+ }
681
+ const rawEdges = [];
682
+ for (const match of callQuery.matches(tree.rootNode)) {
683
+ const nameCapture = match.captures.find(c => c.name === 'call.name');
684
+ const nodeCapture = match.captures.find(c => c.name === 'call.node');
685
+ const objectCapture = match.captures.find(c => c.name === 'call.object');
686
+ if (!nameCapture || !nodeCapture)
687
+ continue;
688
+ const calleeName = nameCapture.node.text;
689
+ if (isIgnoredCallee(calleeName))
690
+ continue;
691
+ const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
692
+ if (!caller)
693
+ continue;
694
+ rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject: objectCapture?.node.text });
695
+ }
696
+ return { nodes, rawEdges };
697
+ }
698
+ // ============================================================================
699
+ // RUST EXTRACTOR
700
+ // ============================================================================
701
+ const RUST_FN_QUERY = `
702
+ (function_item
703
+ name: (identifier) @fn.name) @fn.node
704
+ `;
705
+ const RUST_CALL_QUERY = `
706
+ (call_expression
707
+ function: (identifier) @call.name) @call.node
708
+
709
+ (call_expression
710
+ function: (field_expression
711
+ value: (identifier) @call.object
712
+ field: (field_identifier) @call.name)) @call.node
713
+ `;
714
+ async function extractRustGraph(filePath, content) {
715
+ const { parser, lang } = await getRustParser();
716
+ const tree = parser.parse(content);
717
+ const fnQuery = new Parser.Query(lang, RUST_FN_QUERY);
718
+ const callQuery = new Parser.Query(lang, RUST_CALL_QUERY);
719
+ const nodes = [];
720
+ for (const match of fnQuery.matches(tree.rootNode)) {
721
+ const nameCapture = match.captures.find(c => c.name === 'fn.name');
722
+ const nodeCapture = match.captures.find(c => c.name === 'fn.node');
723
+ if (!nameCapture || !nodeCapture)
724
+ continue;
725
+ const name = nameCapture.node.text;
726
+ const fnNode = nodeCapture.node;
727
+ // Find enclosing impl block → use as className
728
+ let className;
729
+ let cursor = fnNode.parent;
730
+ while (cursor) {
731
+ if (cursor.type === 'impl_item') {
732
+ const typeNode = cursor.children.find(c => c.type === 'type_identifier');
733
+ if (typeNode)
734
+ className = typeNode.text;
735
+ break;
736
+ }
737
+ cursor = cursor.parent;
738
+ }
739
+ // Rust: async keyword lives inside a function_modifiers child
740
+ const isAsync = fnNode.children.some(c => c.type === 'function_modifiers' && c.text.includes('async'));
741
+ const id = className ? `${filePath}::${className}.${name}` : `${filePath}::${name}`;
742
+ nodes.push({
743
+ id, name, filePath, className,
744
+ isAsync,
745
+ language: 'Rust',
746
+ startIndex: fnNode.startIndex,
747
+ endIndex: fnNode.endIndex,
748
+ fanIn: 0, fanOut: 0,
749
+ docstring: extractDocstringBefore(content, fnNode.startIndex, 'Rust'),
750
+ signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'Rust'),
751
+ });
752
+ }
753
+ const rawEdges = [];
754
+ for (const match of callQuery.matches(tree.rootNode)) {
755
+ const nameCapture = match.captures.find(c => c.name === 'call.name');
756
+ const nodeCapture = match.captures.find(c => c.name === 'call.node');
757
+ const objectCapture = match.captures.find(c => c.name === 'call.object');
758
+ if (!nameCapture || !nodeCapture)
759
+ continue;
760
+ const calleeName = nameCapture.node.text;
761
+ if (isIgnoredCallee(calleeName))
762
+ continue;
763
+ const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
764
+ if (!caller)
765
+ continue;
766
+ rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject: objectCapture?.node.text });
767
+ }
768
+ return { nodes, rawEdges };
769
+ }
770
+ // ============================================================================
771
+ // RUBY EXTRACTOR
772
+ // ============================================================================
773
+ const RUBY_FN_QUERY = `
774
+ (method
775
+ name: (identifier) @fn.name) @fn.node
776
+
777
+ (singleton_method
778
+ name: (identifier) @fn.name) @fn.node
779
+ `;
780
+ // Explicit calls: fn(), obj.method()
781
+ const RUBY_CALL_QUERY = `
782
+ (call
783
+ receiver: (identifier) @call.object
784
+ method: (identifier) @call.name) @call.node
785
+
786
+ (call
787
+ method: (identifier) @call.name) @call.node
788
+ `;
789
+ // Bareword calls: Ruby allows calling methods without parentheses.
790
+ // An identifier at statement level inside a body_statement is almost always
791
+ // a method call (variable usage appears in assignments/expressions, not alone).
792
+ const RUBY_BAREWORD_QUERY = `
793
+ (body_statement
794
+ (identifier) @call.name)
795
+ `;
796
+ async function extractRubyGraph(filePath, content) {
797
+ const { parser, lang } = await getRubyParser();
798
+ const tree = parser.parse(content);
799
+ const fnQuery = new Parser.Query(lang, RUBY_FN_QUERY);
800
+ const callQuery = new Parser.Query(lang, RUBY_CALL_QUERY);
801
+ const barewordQuery = new Parser.Query(lang, RUBY_BAREWORD_QUERY);
802
+ const nodes = [];
803
+ for (const match of fnQuery.matches(tree.rootNode)) {
804
+ const nameCapture = match.captures.find(c => c.name === 'fn.name');
805
+ const nodeCapture = match.captures.find(c => c.name === 'fn.node');
806
+ if (!nameCapture || !nodeCapture)
807
+ continue;
808
+ const name = nameCapture.node.text;
809
+ const fnNode = nodeCapture.node;
810
+ // Find enclosing class/module
811
+ let className;
812
+ let cursor = fnNode.parent;
813
+ while (cursor) {
814
+ if (cursor.type === 'class' || cursor.type === 'module') {
815
+ const nameNode = cursor.children.find(c => c.type === 'constant' || c.type === 'scope_resolution');
816
+ if (nameNode)
817
+ className = nameNode.text;
818
+ break;
819
+ }
820
+ cursor = cursor.parent;
821
+ }
822
+ const id = className ? `${filePath}::${className}.${name}` : `${filePath}::${name}`;
823
+ nodes.push({
824
+ id, name, filePath, className,
825
+ isAsync: false,
826
+ language: 'Ruby',
827
+ startIndex: fnNode.startIndex,
828
+ endIndex: fnNode.endIndex,
829
+ fanIn: 0, fanOut: 0,
830
+ docstring: extractDocstringBefore(content, fnNode.startIndex, 'Ruby'),
831
+ signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'Ruby'),
832
+ });
833
+ }
834
+ const rawEdges = [];
835
+ // Explicit calls: fn(), obj.method()
836
+ for (const match of callQuery.matches(tree.rootNode)) {
837
+ const nameCapture = match.captures.find(c => c.name === 'call.name');
838
+ const nodeCapture = match.captures.find(c => c.name === 'call.node');
839
+ const objectCapture = match.captures.find(c => c.name === 'call.object');
840
+ if (!nameCapture || !nodeCapture)
841
+ continue;
842
+ const calleeName = nameCapture.node.text;
843
+ if (isIgnoredCallee(calleeName))
844
+ continue;
845
+ const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
846
+ if (!caller)
847
+ continue;
848
+ rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject: objectCapture?.node.text });
849
+ }
850
+ // Bareword calls: identifier at statement level, no parens
851
+ for (const match of barewordQuery.matches(tree.rootNode)) {
852
+ const nameCapture = match.captures.find(c => c.name === 'call.name');
853
+ if (!nameCapture)
854
+ continue;
855
+ const calleeName = nameCapture.node.text;
856
+ if (isIgnoredCallee(calleeName))
857
+ continue;
858
+ const caller = findEnclosingFunction(nodes, nameCapture.node.startIndex);
859
+ if (!caller)
860
+ continue;
861
+ rawEdges.push({ callerId: caller.id, calleeName, line: nameCapture.node.startPosition.row + 1 });
862
+ }
863
+ return { nodes, rawEdges };
864
+ }
865
+ // ============================================================================
866
+ // JAVA EXTRACTOR
867
+ // ============================================================================
868
+ const JAVA_FN_QUERY = `
869
+ (method_declaration
870
+ name: (identifier) @fn.name) @fn.node
871
+
872
+ (constructor_declaration
873
+ name: (identifier) @fn.name) @fn.node
874
+ `;
875
+ const JAVA_CALL_QUERY = `
876
+ (method_invocation
877
+ object: (identifier) @call.object
878
+ name: (identifier) @call.name) @call.node
879
+
880
+ (method_invocation
881
+ name: (identifier) @call.name) @call.node
882
+ `;
883
+ async function extractJavaGraph(filePath, content) {
884
+ const { parser, lang } = await getJavaParser();
885
+ const tree = parser.parse(content);
886
+ const fnQuery = new Parser.Query(lang, JAVA_FN_QUERY);
887
+ const callQuery = new Parser.Query(lang, JAVA_CALL_QUERY);
888
+ const nodes = [];
889
+ for (const match of fnQuery.matches(tree.rootNode)) {
890
+ const nameCapture = match.captures.find(c => c.name === 'fn.name');
891
+ const nodeCapture = match.captures.find(c => c.name === 'fn.node');
892
+ if (!nameCapture || !nodeCapture)
893
+ continue;
894
+ const name = nameCapture.node.text;
895
+ const fnNode = nodeCapture.node;
896
+ // Find enclosing class/interface/enum
897
+ let className;
898
+ let cursor = fnNode.parent;
899
+ while (cursor) {
900
+ if (cursor.type === 'class_declaration' || cursor.type === 'interface_declaration' || cursor.type === 'enum_declaration') {
901
+ const nameNode = cursor.children.find(c => c.type === 'identifier');
902
+ if (nameNode)
903
+ className = nameNode.text;
904
+ break;
905
+ }
906
+ cursor = cursor.parent;
907
+ }
908
+ const isAsync = false; // Java uses Future/CompletableFuture, not async keyword
909
+ const id = className ? `${filePath}::${className}.${name}` : `${filePath}::${name}`;
910
+ nodes.push({
911
+ id, name, filePath, className,
912
+ isAsync,
913
+ language: 'Java',
914
+ startIndex: fnNode.startIndex,
915
+ endIndex: fnNode.endIndex,
916
+ fanIn: 0, fanOut: 0,
917
+ docstring: extractDocstringBefore(content, fnNode.startIndex, 'Java'),
918
+ signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'Java'),
919
+ });
920
+ }
921
+ const rawEdges = [];
922
+ for (const match of callQuery.matches(tree.rootNode)) {
923
+ const nameCapture = match.captures.find(c => c.name === 'call.name');
924
+ const nodeCapture = match.captures.find(c => c.name === 'call.node');
925
+ const objectCapture = match.captures.find(c => c.name === 'call.object');
926
+ if (!nameCapture || !nodeCapture)
927
+ continue;
928
+ const calleeName = nameCapture.node.text;
929
+ if (isIgnoredCallee(calleeName))
930
+ continue;
931
+ const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
932
+ if (!caller)
933
+ continue;
934
+ rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject: objectCapture?.node.text });
935
+ }
936
+ return { nodes, rawEdges };
937
+ }
938
+ // ============================================================================
939
+ // C++ EXTRACTOR
940
+ // ============================================================================
941
+ /**
942
+ * Safely run a tree-sitter query, returning [] if the S-expression is invalid
943
+ * for the grammar. C++ grammar has many edge cases (templates, operators,
944
+ * pointer declarators) that can make certain queries fail.
945
+ */
946
+ function safeQuery(lang, queryStr, root) {
947
+ try {
948
+ const q = new Parser.Query(lang, queryStr);
949
+ return q.matches(root);
950
+ }
951
+ catch {
952
+ return [];
953
+ }
954
+ }
955
+ /** Free functions and inline class methods with a simple identifier name */
956
+ const CPP_FN_BASIC_QUERY = `
957
+ (function_definition
958
+ declarator: (function_declarator
959
+ declarator: (identifier) @fn.name)) @fn.node
960
+
961
+ (function_definition
962
+ declarator: (function_declarator
963
+ declarator: (field_identifier) @fn.name)) @fn.node
964
+ `;
965
+ /** Out-of-class definitions: void Foo::bar() {} */
966
+ const CPP_FN_QUALIFIED_QUERY = `
967
+ (function_definition
968
+ declarator: (function_declarator
969
+ declarator: (qualified_identifier
970
+ name: (identifier) @fn.name))) @fn.node
971
+ `;
972
+ /** Plain function calls: foo() */
973
+ const CPP_CALL_DIRECT_QUERY = `
974
+ (call_expression
975
+ function: (identifier) @call.name) @call.node
976
+ `;
977
+ /** Member calls: obj.method() and ptr->method() — captures receiver */
978
+ const CPP_CALL_MEMBER_QUERY = `
979
+ (call_expression
980
+ function: (field_expression
981
+ argument: (identifier) @call.object
982
+ field: (field_identifier) @call.name)) @call.node
983
+ `;
984
+ async function extractCppGraph(filePath, content) {
985
+ const { parser, lang } = await getCppParser();
986
+ const tree = parser.parse(content);
987
+ const nodes = [];
988
+ const seen = new Set(); // deduplicate by name-node start position
989
+ for (const queryStr of [CPP_FN_BASIC_QUERY, CPP_FN_QUALIFIED_QUERY]) {
990
+ for (const match of safeQuery(lang, queryStr, tree.rootNode)) {
991
+ const nameCapture = match.captures.find(c => c.name === 'fn.name');
992
+ const nodeCapture = match.captures.find(c => c.name === 'fn.node');
993
+ if (!nameCapture || !nodeCapture)
994
+ continue;
995
+ if (seen.has(nameCapture.node.startIndex))
996
+ continue;
997
+ seen.add(nameCapture.node.startIndex);
998
+ const name = nameCapture.node.text;
999
+ // Skip ALL_CAPS names — these are almost certainly macros, not functions
1000
+ if (/^[A-Z][A-Z0-9_]{2,}$/.test(name))
1001
+ continue;
1002
+ const fnNode = nodeCapture.node;
1003
+ // Find enclosing class (inline method defined inside class body)
1004
+ let className;
1005
+ let cursor = fnNode.parent;
1006
+ while (cursor) {
1007
+ if (cursor.type === 'class_specifier' || cursor.type === 'struct_specifier') {
1008
+ const nameNode = cursor.children.find(c => c.type === 'type_identifier');
1009
+ if (nameNode)
1010
+ className = nameNode.text;
1011
+ break;
1012
+ }
1013
+ cursor = cursor.parent;
1014
+ }
1015
+ // For out-of-class: void Foo::bar() — extract class from qualified_identifier scope
1016
+ if (!className) {
1017
+ const fnDeclarator = fnNode.children.find(c => c.type === 'function_declarator');
1018
+ if (fnDeclarator) {
1019
+ const qualNode = fnDeclarator.children.find(c => c.type === 'qualified_identifier');
1020
+ if (qualNode) {
1021
+ const scopeNode = qualNode.children.find(c => c.type === 'namespace_identifier' || c.type === 'type_identifier');
1022
+ if (scopeNode)
1023
+ className = scopeNode.text;
1024
+ }
1025
+ }
1026
+ }
1027
+ const id = className ? `${filePath}::${className}.${name}` : `${filePath}::${name}`;
1028
+ nodes.push({
1029
+ id, name, filePath, className,
1030
+ isAsync: false, // C++ has no async keyword at language level
1031
+ language: 'C++',
1032
+ startIndex: fnNode.startIndex,
1033
+ endIndex: fnNode.endIndex,
1034
+ fanIn: 0, fanOut: 0,
1035
+ docstring: extractDocstringBefore(content, fnNode.startIndex, 'C++'),
1036
+ signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'C++'),
1037
+ });
1038
+ }
1039
+ }
1040
+ const rawEdges = [];
1041
+ // Plain calls: foo()
1042
+ for (const match of safeQuery(lang, CPP_CALL_DIRECT_QUERY, tree.rootNode)) {
1043
+ const nameCapture = match.captures.find(c => c.name === 'call.name');
1044
+ const nodeCapture = match.captures.find(c => c.name === 'call.node');
1045
+ if (!nameCapture || !nodeCapture)
1046
+ continue;
1047
+ const calleeName = nameCapture.node.text;
1048
+ if (isIgnoredCallee(calleeName))
1049
+ continue;
1050
+ const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
1051
+ if (!caller)
1052
+ continue;
1053
+ rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1 });
1054
+ }
1055
+ // Member calls: obj.method() / ptr->method()
1056
+ for (const match of safeQuery(lang, CPP_CALL_MEMBER_QUERY, tree.rootNode)) {
1057
+ const nameCapture = match.captures.find(c => c.name === 'call.name');
1058
+ const nodeCapture = match.captures.find(c => c.name === 'call.node');
1059
+ const objectCapture = match.captures.find(c => c.name === 'call.object');
1060
+ if (!nameCapture || !nodeCapture)
1061
+ continue;
1062
+ const calleeName = nameCapture.node.text;
1063
+ if (isIgnoredCallee(calleeName))
1064
+ continue;
1065
+ const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
1066
+ if (!caller)
1067
+ continue;
1068
+ rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject: objectCapture?.node.text });
1069
+ }
1070
+ return { nodes, rawEdges };
1071
+ }
1072
+ // ============================================================================
1073
+ // SWIFT EXTRACTOR
1074
+ // ============================================================================
1075
+ // function_declaration covers free functions and methods inside class_body
1076
+ const SWIFT_FN_QUERY = `
1077
+ (function_declaration
1078
+ name: (simple_identifier) @fn.name) @fn.node
1079
+
1080
+ (init_declaration) @fn.node
1081
+ `;
1082
+ // Direct calls: foo()
1083
+ const SWIFT_CALL_DIRECT_QUERY = `
1084
+ (call_expression
1085
+ (simple_identifier) @call.name) @call.node
1086
+ `;
1087
+ // Method calls: obj.method() / self.method()
1088
+ const SWIFT_CALL_NAV_QUERY = `
1089
+ (call_expression
1090
+ (navigation_expression
1091
+ (navigation_suffix
1092
+ (simple_identifier) @call.name))) @call.node
1093
+ `;
1094
+ async function extractSwiftGraph(filePath, content) {
1095
+ const { parser, lang } = await getSwiftParser();
1096
+ const tree = parser.parse(content);
1097
+ const fnQuery = new Parser.Query(lang, SWIFT_FN_QUERY);
1098
+ const directCallQuery = new Parser.Query(lang, SWIFT_CALL_DIRECT_QUERY);
1099
+ const navCallQuery = new Parser.Query(lang, SWIFT_CALL_NAV_QUERY);
1100
+ const nodes = [];
1101
+ for (const match of fnQuery.matches(tree.rootNode)) {
1102
+ const nameCapture = match.captures.find(c => c.name === 'fn.name');
1103
+ const nodeCapture = match.captures.find(c => c.name === 'fn.node');
1104
+ if (!nodeCapture)
1105
+ continue;
1106
+ const fnNode = nodeCapture.node;
1107
+ const name = nameCapture?.node.text ?? 'init';
1108
+ // Find enclosing class/struct/actor/enum/extension (all are class_declaration in this grammar)
1109
+ let className;
1110
+ let cursor = fnNode.parent;
1111
+ while (cursor) {
1112
+ if (cursor.type === 'class_declaration') {
1113
+ const nameNode = cursor.children.find(c => c.type === 'type_identifier');
1114
+ if (nameNode)
1115
+ className = nameNode.text;
1116
+ break;
1117
+ }
1118
+ cursor = cursor.parent;
1119
+ }
1120
+ const isAsync = content.slice(fnNode.startIndex, fnNode.endIndex).includes(' async ');
1121
+ const id = className ? `${filePath}::${className}.${name}` : `${filePath}::${name}`;
1122
+ nodes.push({
1123
+ id, name, filePath, className,
1124
+ isAsync,
1125
+ language: 'Swift',
1126
+ startIndex: fnNode.startIndex,
1127
+ endIndex: fnNode.endIndex,
1128
+ fanIn: 0, fanOut: 0,
1129
+ docstring: extractDocstringBefore(content, fnNode.startIndex, 'Swift'),
1130
+ signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, 'Swift'),
1131
+ });
1132
+ }
1133
+ const rawEdges = [];
1134
+ // Direct calls: foo()
1135
+ for (const match of directCallQuery.matches(tree.rootNode)) {
1136
+ const nameCapture = match.captures.find(c => c.name === 'call.name');
1137
+ const nodeCapture = match.captures.find(c => c.name === 'call.node');
1138
+ if (!nameCapture || !nodeCapture)
1139
+ continue;
1140
+ const calleeName = nameCapture.node.text;
1141
+ if (isIgnoredCallee(calleeName))
1142
+ continue;
1143
+ const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
1144
+ if (!caller)
1145
+ continue;
1146
+ rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1 });
1147
+ }
1148
+ // Method calls: obj.method() / self.method()
1149
+ for (const match of navCallQuery.matches(tree.rootNode)) {
1150
+ const nameCapture = match.captures.find(c => c.name === 'call.name');
1151
+ const nodeCapture = match.captures.find(c => c.name === 'call.node');
1152
+ if (!nameCapture || !nodeCapture)
1153
+ continue;
1154
+ const calleeName = nameCapture.node.text;
1155
+ if (isIgnoredCallee(calleeName))
1156
+ continue;
1157
+ const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
1158
+ if (!caller)
1159
+ continue;
1160
+ // Extract the receiver object (first child of navigation_expression)
1161
+ const navExpr = nodeCapture.node.firstChild;
1162
+ const objText = navExpr?.firstChild?.type === 'self_expression'
1163
+ ? 'self'
1164
+ : navExpr?.firstChild?.text;
1165
+ rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject: objText });
1166
+ }
1167
+ return { nodes, rawEdges };
1168
+ }
1169
+ // ============================================================================
1170
+ // CLASS HIERARCHY EXTRACTION
1171
+ // ============================================================================
1172
+ /**
1173
+ * Extract parent class / interface relationships from source files using
1174
+ * tree-sitter. Returns a map from `filePath::ClassName` → relationship info.
1175
+ * Uses safeQuery so any query that doesn't match a grammar version is silently
1176
+ * skipped rather than crashing.
1177
+ */
1178
+ async function extractClassRelationships(files) {
1179
+ const out = new Map();
1180
+ // Helper to merge into map keyed by `filePath::ClassName`
1181
+ function merge(filePath, className, parents, ifaces) {
1182
+ const key = `${filePath}::${className}`;
1183
+ const existing = out.get(key) ?? { parentClasses: [], interfaces: [] };
1184
+ for (const p of parents)
1185
+ if (!existing.parentClasses.includes(p))
1186
+ existing.parentClasses.push(p);
1187
+ for (const i of ifaces)
1188
+ if (!existing.interfaces.includes(i))
1189
+ existing.interfaces.push(i);
1190
+ out.set(key, existing);
1191
+ }
1192
+ for (const file of files) {
1193
+ try {
1194
+ if (file.language === 'TypeScript' || file.language === 'JavaScript') {
1195
+ const { parser, lang } = await getTSParser();
1196
+ const tree = parser.parse(file.content);
1197
+ // class Foo extends Bar implements Baz, Qux
1198
+ const EXTENDS_Q = `
1199
+ (class_declaration
1200
+ name: (type_identifier) @cls
1201
+ (class_heritage (extends_clause value: (identifier) @parent)))`;
1202
+ const IMPLEMENTS_Q = `
1203
+ (class_declaration
1204
+ name: (type_identifier) @cls
1205
+ (class_heritage (implements_clause (type_identifier) @iface)))`;
1206
+ for (const m of safeQuery(lang, EXTENDS_Q, tree.rootNode)) {
1207
+ const cls = m.captures.find(c => c.name === 'cls')?.node.text;
1208
+ const parent = m.captures.find(c => c.name === 'parent')?.node.text;
1209
+ if (cls && parent)
1210
+ merge(file.path, cls, [parent], []);
1211
+ }
1212
+ for (const m of safeQuery(lang, IMPLEMENTS_Q, tree.rootNode)) {
1213
+ const cls = m.captures.find(c => c.name === 'cls')?.node.text;
1214
+ const iface = m.captures.find(c => c.name === 'iface')?.node.text;
1215
+ if (cls && iface)
1216
+ merge(file.path, cls, [], [iface]);
1217
+ }
1218
+ }
1219
+ else if (file.language === 'Python') {
1220
+ const { parser, lang } = await getPyParser();
1221
+ const tree = parser.parse(file.content);
1222
+ // class Foo(Bar, Baz):
1223
+ const Q = `
1224
+ (class_definition
1225
+ name: (identifier) @cls
1226
+ superclasses: (argument_list (identifier) @parent))`;
1227
+ for (const m of safeQuery(lang, Q, tree.rootNode)) {
1228
+ const cls = m.captures.find(c => c.name === 'cls')?.node.text;
1229
+ const parent = m.captures.find(c => c.name === 'parent')?.node.text;
1230
+ if (cls && parent && parent !== 'object')
1231
+ merge(file.path, cls, [parent], []);
1232
+ }
1233
+ }
1234
+ else if (file.language === 'Java') {
1235
+ const { parser, lang } = await getJavaParser();
1236
+ const tree = parser.parse(file.content);
1237
+ const EXTENDS_Q = `
1238
+ (class_declaration
1239
+ name: (identifier) @cls
1240
+ (superclass (type_identifier) @parent))`;
1241
+ const IMPLEMENTS_Q = `
1242
+ (class_declaration
1243
+ name: (identifier) @cls
1244
+ (super_interfaces (type_list (type_identifier) @iface)))`;
1245
+ for (const m of safeQuery(lang, EXTENDS_Q, tree.rootNode)) {
1246
+ const cls = m.captures.find(c => c.name === 'cls')?.node.text;
1247
+ const parent = m.captures.find(c => c.name === 'parent')?.node.text;
1248
+ if (cls && parent)
1249
+ merge(file.path, cls, [parent], []);
1250
+ }
1251
+ for (const m of safeQuery(lang, IMPLEMENTS_Q, tree.rootNode)) {
1252
+ const cls = m.captures.find(c => c.name === 'cls')?.node.text;
1253
+ const iface = m.captures.find(c => c.name === 'iface')?.node.text;
1254
+ if (cls && iface)
1255
+ merge(file.path, cls, [], [iface]);
1256
+ }
1257
+ }
1258
+ else if (file.language === 'C++') {
1259
+ const { parser, lang } = await getCppParser();
1260
+ const tree = parser.parse(file.content);
1261
+ // class Foo : public Bar
1262
+ const Q = `
1263
+ (class_specifier
1264
+ name: (type_identifier) @cls
1265
+ (base_class_clause (type_identifier) @parent))`;
1266
+ for (const m of safeQuery(lang, Q, tree.rootNode)) {
1267
+ const cls = m.captures.find(c => c.name === 'cls')?.node.text;
1268
+ const parent = m.captures.find(c => c.name === 'parent')?.node.text;
1269
+ if (cls && parent)
1270
+ merge(file.path, cls, [parent], []);
1271
+ }
1272
+ }
1273
+ else if (file.language === 'Ruby') {
1274
+ const { parser, lang } = await getRubyParser();
1275
+ const tree = parser.parse(file.content);
1276
+ // class Foo < Bar
1277
+ const Q = `
1278
+ (class
1279
+ name: (constant) @cls
1280
+ superclass: (superclass (constant) @parent))`;
1281
+ for (const m of safeQuery(lang, Q, tree.rootNode)) {
1282
+ const cls = m.captures.find(c => c.name === 'cls')?.node.text;
1283
+ const parent = m.captures.find(c => c.name === 'parent')?.node.text;
1284
+ if (cls && parent)
1285
+ merge(file.path, cls, [parent], []);
1286
+ }
1287
+ }
1288
+ else if (file.language === 'Go') {
1289
+ // Go has no inheritance but has struct embedding; treat as 'embeds' edges
1290
+ const { parser, lang } = await getGoParser();
1291
+ const tree = parser.parse(file.content);
1292
+ // Anonymous (embedded) field in a struct: type Foo struct { Bar }
1293
+ const Q = `
1294
+ (type_declaration
1295
+ (type_spec
1296
+ name: (type_identifier) @cls
1297
+ type: (struct_type
1298
+ (field_declaration_list
1299
+ (field_declaration
1300
+ type: (type_identifier) @embedded)))))`;
1301
+ for (const m of safeQuery(lang, Q, tree.rootNode)) {
1302
+ const cls = m.captures.find(c => c.name === 'cls')?.node.text;
1303
+ const embedded = m.captures.find(c => c.name === 'embedded')?.node.text;
1304
+ if (cls && embedded) {
1305
+ const key = `${file.path}::${cls}`;
1306
+ const existing = out.get(key) ?? { parentClasses: [], interfaces: [] };
1307
+ // Store Go embeds as parentClasses (will be tagged as 'embeds' when building edges)
1308
+ if (!existing.parentClasses.includes(embedded))
1309
+ existing.parentClasses.push(embedded);
1310
+ out.set(key, existing);
1311
+ }
1312
+ }
1313
+ }
1314
+ // Rust: trait impls are structural but less like OOP inheritance; skip for now
1315
+ }
1316
+ catch {
1317
+ // Best-effort; skip unparseable files
1318
+ }
1319
+ }
1320
+ return out;
1321
+ }
1322
+ /**
1323
+ * Build ClassNode[] from the set of extracted FunctionNodes (which carry
1324
+ * `className`), enriched with inheritance data from `extractClassRelationships`.
1325
+ *
1326
+ * Functions without a className are grouped by file into synthetic module nodes
1327
+ * (e.g. `[call-graph]`) so every function appears in the class graph, not just
1328
+ * class methods. This is essential for codebases that use mostly module-level
1329
+ * exports rather than OOP classes.
1330
+ */
1331
+ function buildClassNodes(allNodes, relationships) {
1332
+ // Group FunctionNodes by (filePath, className).
1333
+ // Free functions use a synthetic "[basename]" module name keyed by filePath alone.
1334
+ const groups = new Map();
1335
+ for (const fn of allNodes.values()) {
1336
+ let key;
1337
+ let name;
1338
+ let isModule;
1339
+ if (fn.className) {
1340
+ key = `${fn.filePath}::${fn.className}`;
1341
+ name = fn.className;
1342
+ isModule = false;
1343
+ }
1344
+ else {
1345
+ // Synthetic module node — one per file
1346
+ key = fn.filePath;
1347
+ const base = fn.filePath.split('/').pop() ?? fn.filePath;
1348
+ name = '[' + base.replace(/\.[^.]+$/, '') + ']';
1349
+ isModule = true;
1350
+ }
1351
+ if (!groups.has(key)) {
1352
+ groups.set(key, { name, filePath: fn.filePath, language: fn.language, isModule, methods: [] });
1353
+ }
1354
+ groups.get(key).methods.push(fn);
1355
+ }
1356
+ // Build ClassNode[]
1357
+ const classMap = new Map();
1358
+ for (const [id, g] of groups) {
1359
+ const rel = relationships.get(id) ?? { parentClasses: [], interfaces: [] };
1360
+ const cls = {
1361
+ id,
1362
+ name: g.name,
1363
+ filePath: g.filePath,
1364
+ language: g.language,
1365
+ parentClasses: rel.parentClasses,
1366
+ interfaces: rel.interfaces,
1367
+ methodIds: g.methods.map(m => m.id),
1368
+ fanIn: g.methods.reduce((s, m) => s + m.fanIn, 0),
1369
+ fanOut: g.methods.reduce((s, m) => s + m.fanOut, 0),
1370
+ isModule: g.isModule,
1371
+ };
1372
+ classMap.set(id, cls);
1373
+ }
1374
+ // Build InheritanceEdge[] — only when both parent and child are in our graph
1375
+ // Parent lookup: match by class name across all ClassNodes (first match wins)
1376
+ const byName = new Map();
1377
+ for (const cls of classMap.values()) {
1378
+ if (!byName.has(cls.name))
1379
+ byName.set(cls.name, cls);
1380
+ }
1381
+ const inheritanceEdges = [];
1382
+ const seenEdges = new Set();
1383
+ for (const cls of classMap.values()) {
1384
+ for (const parentName of cls.parentClasses) {
1385
+ const parent = byName.get(parentName);
1386
+ if (!parent)
1387
+ continue;
1388
+ const edgeId = `${parent.id}->${cls.id}`;
1389
+ if (seenEdges.has(edgeId))
1390
+ continue;
1391
+ seenEdges.add(edgeId);
1392
+ // Go embedding vs OOP inheritance
1393
+ const kind = cls.language === 'Go' ? 'embeds' : 'extends';
1394
+ inheritanceEdges.push({ id: edgeId, parentId: parent.id, childId: cls.id, kind });
1395
+ }
1396
+ for (const ifaceName of cls.interfaces) {
1397
+ const parent = byName.get(ifaceName);
1398
+ if (!parent)
1399
+ continue;
1400
+ const edgeId = `${parent.id}->${cls.id}`;
1401
+ if (seenEdges.has(edgeId))
1402
+ continue;
1403
+ seenEdges.add(edgeId);
1404
+ inheritanceEdges.push({ id: edgeId, parentId: parent.id, childId: cls.id, kind: 'implements' });
1405
+ }
1406
+ }
1407
+ // OVERRIDES edges: child defines method with same name as parent — language-agnostic
1408
+ const methodNameSet = new Map();
1409
+ for (const [id, cls] of classMap) {
1410
+ const names = new Set();
1411
+ for (const memberId of cls.methodIds) {
1412
+ const fn = allNodes.get(memberId);
1413
+ if (fn && !fn.isExternal)
1414
+ names.add(fn.name);
1415
+ }
1416
+ methodNameSet.set(id, names);
1417
+ }
1418
+ const extendsEdges = inheritanceEdges.filter(e => e.kind === 'extends');
1419
+ for (const edge of extendsEdges) {
1420
+ const childNames = methodNameSet.get(edge.childId);
1421
+ const parentNames = methodNameSet.get(edge.parentId);
1422
+ if (!childNames || !parentNames)
1423
+ continue;
1424
+ if (![...childNames].some(n => parentNames.has(n)))
1425
+ continue;
1426
+ const overrideId = `${edge.parentId}=>${edge.childId}:overrides`;
1427
+ if (seenEdges.has(overrideId))
1428
+ continue;
1429
+ seenEdges.add(overrideId);
1430
+ inheritanceEdges.push({ id: overrideId, parentId: edge.parentId, childId: edge.childId, kind: 'overrides' });
1431
+ }
1432
+ return { classes: Array.from(classMap.values()), inheritanceEdges };
1433
+ }
1434
+ // ============================================================================
1435
+ // EXTERNAL NODE HELPER
1436
+ // ============================================================================
1437
+ const TEST_FILE_PATTERNS = [
1438
+ /\.test\.[tj]sx?$/, /\.spec\.[tj]sx?$/,
1439
+ /_test\.py$/, /test_[^/]+\.py$/,
1440
+ /_spec\.rb$/, /_test\.go$/, /[A-Z][^/]*Test\.java$/,
1441
+ ];
1442
+ function isTestFile(filePath) {
1443
+ return TEST_FILE_PATTERNS.some(p => p.test(filePath));
1444
+ }
1445
+ const EXTERNAL_HTTP_RE = /^(fetch|axios|got|superagent|node-fetch|ky|request|https?|xmlhttprequest|grpc|undici|requests|aiohttp|httpx|urllib|urllib2|urllib3|curl|curleasy|pycurl|http|httpclient|httpurlconnection|reqwest|hyper|ureq|isahc|surf|net|faraday|httparty|rest|typhoeus|excon|okhttp|retrofit|feign|resttemplate|webclient|urlsession|alamofire|moya)$/;
1446
+ const EXTERNAL_DB_RE = /^(pg|mysql|mysql2|sqlite|sqlite3|redis|ioredis|mongoose|mongo|mongodb|prisma|knex|sequelize|typeorm|drizzle|cassandra|dynamodb|firestore|supabase|neo4j|influxdb|clickhouse|kysely|psycopg2|psycopg|sqlalchemy|pymysql|asyncpg|motor|aiomysql|tortoise|sql|gorm|sqlx|pgx|bun|diesel|seaorm|rusqlite|activerecord|sequel|jdbc|hibernate|jpa|entitymanager|datasource|jdbctemplate|r2dbc|coredata|grdb|realm)$/;
1447
+ const EXTERNAL_FS_RE = /^(fs|fsp|readfile|writefile|readdir|stat|mkdir|unlink|rename|copyfile|createreadstream|createwritestream|open|fopen|fread|fwrite|fclose|remove|ifstream|ofstream|fstream|os|path|file)$/;
1448
+ const EXTERNAL_STDLIB_BASES = new Set([
1449
+ // JavaScript / Node.js
1450
+ 'array', 'object', 'string', 'number', 'math', 'json', 'date', 'regexp',
1451
+ 'promise', 'map', 'set', 'weakmap', 'weakset', 'symbol', 'reflect', 'proxy',
1452
+ 'console', 'error', 'buffer', 'process', 'int8array', 'uint8array',
1453
+ // Python
1454
+ 'os', 'sys', 're', 'io', 'abc', 'ast', 'csv', 'copy', 'enum', 'glob',
1455
+ 'gzip', 'hmac', 'html', 'http', 'logging', 'operator', 'pathlib', 'pickle',
1456
+ 'pprint', 'queue', 'random', 'shutil', 'signal', 'socket', 'ssl', 'struct',
1457
+ 'subprocess', 'tempfile', 'threading', 'time', 'traceback', 'typing', 'uuid',
1458
+ 'warnings', 'collections', 'functools', 'itertools', 'contextlib',
1459
+ 'dataclasses', 'unittest', 'hashlib', 'base64', 'binascii', 'codecs',
1460
+ 'inspect', 'importlib', 'weakref', 'gc', 'platform', 'shlex', 'textwrap',
1461
+ // C / C++
1462
+ 'std', 'printf', 'fprintf', 'sprintf', 'snprintf', 'scanf', 'malloc',
1463
+ 'calloc', 'realloc', 'free', 'memcpy', 'memmove', 'memset', 'memcmp',
1464
+ 'strlen', 'strcpy', 'strncpy', 'strcat', 'strcmp', 'strncmp', 'strstr',
1465
+ 'assert', 'abort', 'exit', 'atexit',
1466
+ // Go
1467
+ 'fmt', 'log', 'sort', 'sync', 'atomic', 'bytes', 'errors', 'context',
1468
+ 'reflect', 'runtime', 'bufio', 'unicode', 'strings', 'strconv', 'math',
1469
+ 'rand', 'time', 'flag', 'testing',
1470
+ // Rust
1471
+ 'vec', 'option', 'result', 'iter', 'collections', 'thread', 'env',
1472
+ 'cell', 'rc', 'arc', 'mutex', 'rwlock', 'channel', 'mpsc',
1473
+ // Ruby
1474
+ 'integer', 'float', 'numeric', 'enumerable', 'comparable', 'kernel',
1475
+ 'module', 'class', 'basicobject', 'nilclass', 'trueclass', 'falseclass',
1476
+ 'symbol', 'regexp', 'range', 'proc', 'method', 'encoding',
1477
+ // Java
1478
+ 'system', 'integer', 'long', 'double', 'boolean', 'character',
1479
+ 'list', 'arraylist', 'linkedlist', 'hashmap', 'treemap', 'hashset', 'treeset',
1480
+ 'optional', 'stream', 'arrays', 'collections', 'objects', 'math',
1481
+ 'thread', 'runnable', 'exception', 'runtimeexception', 'illegalargumentexception',
1482
+ 'stringbuilder', 'stringbuffer', 'scanner',
1483
+ // Swift
1484
+ 'int', 'double', 'bool', 'dictionary', 'swift', 'foundation',
1485
+ 'dispatchqueue', 'notificationcenter', 'nsstring', 'nsarray', 'nsdictionary',
1486
+ ]);
1487
+ const EXTERNAL_NOISE_RECEIVERS = new Set([
1488
+ 'response', 'body', 't', 'err', 'error', 'buf', 'str', 'res', 'req', 'data', 'result',
1489
+ ]);
1490
+ function classifyExternal(name) {
1491
+ const base = name.split('.')[0].toLowerCase().replace(/[^a-z0-9]/g, '');
1492
+ if (EXTERNAL_HTTP_RE.test(base))
1493
+ return 'http';
1494
+ if (EXTERNAL_DB_RE.test(base))
1495
+ return 'database';
1496
+ if (EXTERNAL_FS_RE.test(base))
1497
+ return 'filesystem';
1498
+ if (EXTERNAL_STDLIB_BASES.has(base))
1499
+ return 'stdlib';
1500
+ if (name.includes('.') && EXTERNAL_NOISE_RECEIVERS.has(name.split('.')[0].toLowerCase()))
1501
+ return 'stdlib';
1502
+ return 'unknown';
1503
+ }
1504
+ function getOrCreateExternalNode(name, nodes) {
1505
+ const id = `external::${name}`;
1506
+ if (!nodes.has(id)) {
1507
+ nodes.set(id, {
1508
+ id, name, filePath: 'external', isExternal: true,
1509
+ externalKind: classifyExternal(name),
1510
+ isAsync: false, language: 'external',
1511
+ startIndex: 0, endIndex: 0, fanIn: 0, fanOut: 0,
1512
+ });
1513
+ }
1514
+ return nodes.get(id);
1515
+ }
1516
+ // ============================================================================
1517
+ // CYCLOMATIC COMPLEXITY
1518
+ // ============================================================================
1519
+ const CC_PATTERN_PYTHON = /\bif\s|\belif\s|\bwhile\s|\bfor\s|\bexcept\b|\band\s|\bor\s/g;
1520
+ const CC_PATTERN_DEFAULT = /\bif\s*\(|\bwhile\s*\(|\bfor\s*[(]|\bdo\s*[{]|\bcase\s+|\bcatch\s*\(|&&|\|\|/g;
1521
+ /**
1522
+ * McCabe cyclomatic complexity via regex over function body.
1523
+ * CC = 1 + decision points (if, while, for, case, catch, &&, ||).
1524
+ * Approximate (regex, not AST), suitable for triage/ranking.
1525
+ */
1526
+ export function computeCyclomaticComplexity(body, language) {
1527
+ const source = language === 'Python' ? CC_PATTERN_PYTHON.source : CC_PATTERN_DEFAULT.source;
1528
+ return 1 + (body.match(new RegExp(source, 'g'))?.length ?? 0);
1529
+ }
1530
+ // ============================================================================
1531
+ // CALL GRAPH BUILDER
1532
+ // ============================================================================
1533
+ export class CallGraphBuilder {
1534
+ /**
1535
+ * Build a call graph from a list of source files.
1536
+ *
1537
+ * @param files Source files with path, content, and language
1538
+ * @param layers Optional layer map { layerName: [path prefix, ...] }
1539
+ * @param importMap Optional per-file import map (from ImportResolverBridge)
1540
+ */
1541
+ async build(files, layers, importMap) {
1542
+ const allNodes = new Map();
1543
+ const allRawEdges = [];
1544
+ // Pass 1: Extract nodes and raw edges from each file
1545
+ for (const file of files) {
1546
+ try {
1547
+ let result;
1548
+ if (file.language === 'Python') {
1549
+ result = await extractPyGraph(file.path, file.content);
1550
+ }
1551
+ else if (file.language === 'TypeScript' || file.language === 'JavaScript') {
1552
+ result = await extractTSGraph(file.path, file.content);
1553
+ }
1554
+ else if (file.language === 'Go') {
1555
+ result = await extractGoGraph(file.path, file.content);
1556
+ }
1557
+ else if (file.language === 'Rust') {
1558
+ result = await extractRustGraph(file.path, file.content);
1559
+ }
1560
+ else if (file.language === 'Ruby') {
1561
+ result = await extractRubyGraph(file.path, file.content);
1562
+ }
1563
+ else if (file.language === 'Java') {
1564
+ result = await extractJavaGraph(file.path, file.content);
1565
+ }
1566
+ else if (file.language === 'C++') {
1567
+ result = await extractCppGraph(file.path, file.content);
1568
+ }
1569
+ else if (file.language === 'Swift') {
1570
+ result = await extractSwiftGraph(file.path, file.content);
1571
+ }
1572
+ else {
1573
+ continue;
1574
+ }
1575
+ // Compute startLine (1-based) from byte offset — cheap, done once at build time
1576
+ const lineOffsets = [0];
1577
+ for (let i = 0; i < file.content.length; i++) {
1578
+ if (file.content[i] === '\n')
1579
+ lineOffsets.push(i + 1);
1580
+ }
1581
+ const byteToLine = (offset) => {
1582
+ let lo = 0, hi = lineOffsets.length - 1;
1583
+ while (lo < hi) {
1584
+ const mid = (lo + hi + 1) >> 1;
1585
+ if (lineOffsets[mid] <= offset)
1586
+ lo = mid;
1587
+ else
1588
+ hi = mid - 1;
1589
+ }
1590
+ return lo + 1;
1591
+ };
1592
+ for (const node of result.nodes) {
1593
+ node.startLine = byteToLine(node.startIndex);
1594
+ node.endLine = byteToLine(node.endIndex);
1595
+ allNodes.set(node.id, node);
1596
+ }
1597
+ allRawEdges.push(...result.rawEdges);
1598
+ }
1599
+ catch (error) {
1600
+ // Skip files that fail to parse (syntax errors, encoding issues, etc.)
1601
+ if (process.env.DEBUG) {
1602
+ console.debug(`[call-graph] Failed to parse ${file.path}: ${error.message}`);
1603
+ }
1604
+ }
1605
+ }
1606
+ // Pass 2: Resolve raw edges — multi-strategy resolution
1607
+ const trie = new FunctionRegistryTrie();
1608
+ for (const node of allNodes.values())
1609
+ trie.insert(node);
1610
+ // Build per-function-body content slices for type inference (keyed by functionId)
1611
+ const fileContents = new Map();
1612
+ for (const file of files)
1613
+ fileContents.set(file.path, file.content);
1614
+ const edges = [];
1615
+ for (const raw of allRawEdges) {
1616
+ const callerNode = allNodes.get(raw.callerId);
1617
+ if (!callerNode)
1618
+ continue;
1619
+ let calleeNode;
1620
+ let confidence = 'name_only';
1621
+ // Strategy 1 — self/cls intra-class (Python self.*, cls.* or same-class method)
1622
+ if (raw.calleeObject === 'self' || raw.calleeObject === 'cls') {
1623
+ if (callerNode.className) {
1624
+ const candidates = trie.findByQualifiedName(callerNode.className, raw.calleeName);
1625
+ if (candidates.length > 0) {
1626
+ calleeNode = candidates[0];
1627
+ confidence = 'self_cls';
1628
+ }
1629
+ }
1630
+ }
1631
+ // Strategy 1b — Swift/C++ type-name resolution (capitalized receiver = type/class reference)
1632
+ // In Swift and C++, there are no intra-module imports, so cross-file calls appear as
1633
+ // TypeName.method() or TypeName::method(). A capitalized receiver with no same-file
1634
+ // class of that name is a reliable signal for a cross-file type reference.
1635
+ if (!calleeNode && raw.calleeObject && (callerNode.language === 'Swift' || callerNode.language === 'C++')) {
1636
+ const ch = raw.calleeObject.charCodeAt(0);
1637
+ const isCapitalized = ch >= 65 && ch <= 90; // A-Z
1638
+ if (isCapitalized) {
1639
+ const candidates = trie.findByQualifiedName(raw.calleeObject, raw.calleeName);
1640
+ if (candidates.length > 0) {
1641
+ calleeNode = candidates[0];
1642
+ confidence = 'type_name';
1643
+ }
1644
+ }
1645
+ }
1646
+ // Strategy 2 — type inference on receiver variable
1647
+ if (!calleeNode && raw.calleeObject) {
1648
+ const fileContent = fileContents.get(callerNode.filePath);
1649
+ if (fileContent) {
1650
+ const bodySlice = fileContent.slice(callerNode.startIndex, callerNode.endIndex);
1651
+ const inferredTypes = inferTypesFromSource(bodySlice, callerNode.language);
1652
+ const resolved = resolveViaTypeInference(raw.calleeObject, raw.calleeName, inferredTypes, trie);
1653
+ if (resolved) {
1654
+ calleeNode = resolved;
1655
+ confidence = 'type_inference';
1656
+ }
1657
+ }
1658
+ }
1659
+ // Strategy 3 — import resolution (TS/JS/Python/Go/Rust/Ruby/Java)
1660
+ if (!calleeNode && importMap) {
1661
+ const importedFile = importMap.get(callerNode.filePath)?.get(raw.calleeName)
1662
+ ?? (raw.calleeObject ? importMap.get(callerNode.filePath)?.get(raw.calleeObject) : undefined);
1663
+ if (importedFile) {
1664
+ const candidates = trie.findBySimpleName(raw.calleeName).filter(n => n.filePath.startsWith(importedFile));
1665
+ if (candidates.length > 0) {
1666
+ calleeNode = candidates[0];
1667
+ confidence = 'import';
1668
+ }
1669
+ }
1670
+ }
1671
+ // Strategy 4 — same-file preference (only for calls without a typed receiver)
1672
+ // When a receiver is explicitly present but unresolvable (e.g. redis_client.get()),
1673
+ // skip name_only fallback to avoid false-positive edges.
1674
+ if (!calleeNode && !raw.calleeObject) {
1675
+ const candidates = trie.findBySimpleName(raw.calleeName);
1676
+ if (candidates.length === 0) {
1677
+ // Unresolved bare call — create a synthetic external leaf node
1678
+ calleeNode = getOrCreateExternalNode(raw.calleeName, allNodes);
1679
+ confidence = 'external';
1680
+ }
1681
+ else {
1682
+ const sameFile = candidates.find(c => c.filePath === callerNode.filePath);
1683
+ if (sameFile) {
1684
+ calleeNode = sameFile;
1685
+ confidence = 'same_file';
1686
+ }
1687
+ else {
1688
+ calleeNode = candidates[0];
1689
+ confidence = 'name_only';
1690
+ }
1691
+ }
1692
+ }
1693
+ if (!calleeNode) {
1694
+ // Unresolved receiver-based call (e.g. redis_client.get()) — synthetic external node
1695
+ const label = raw.calleeObject
1696
+ ? `${raw.calleeObject}.${raw.calleeName}`
1697
+ : raw.calleeName;
1698
+ calleeNode = getOrCreateExternalNode(label, allNodes);
1699
+ confidence = 'external';
1700
+ }
1701
+ const callType = raw.callType
1702
+ ?? (raw.calleeObject ? 'method' : 'direct');
1703
+ edges.push({
1704
+ callerId: raw.callerId,
1705
+ calleeId: calleeNode.id,
1706
+ calleeName: raw.calleeName,
1707
+ line: raw.line,
1708
+ confidence,
1709
+ kind: 'calls',
1710
+ callType,
1711
+ });
1712
+ }
1713
+ // Pass 2b: HTTP cross-language edges (JS/TS caller → Python handler)
1714
+ try {
1715
+ const filePaths = files.map(f => f.path);
1716
+ const { edges: httpEdges } = await extractAllHttpEdges(filePaths);
1717
+ for (const he of httpEdges) {
1718
+ // Find callee: handler function by name in handlerFile
1719
+ const calleeNode = trie.findBySimpleName(he.route.handlerName)
1720
+ .find(n => n.filePath === he.handlerFile);
1721
+ if (!calleeNode)
1722
+ continue;
1723
+ // Find caller: any function in callerFile that encloses the HTTP call's line
1724
+ const callerContent = fileContents.get(he.callerFile);
1725
+ const callerNode = callerContent
1726
+ ? (() => {
1727
+ let offset = 0;
1728
+ const lines = callerContent.split('\n');
1729
+ for (let i = 0; i < he.call.line - 1 && i < lines.length; i++) {
1730
+ offset += lines[i].length + 1;
1731
+ }
1732
+ const candidates = Array.from(allNodes.values())
1733
+ .filter(n => n.filePath === he.callerFile);
1734
+ return findEnclosingFunction(candidates, offset);
1735
+ })()
1736
+ : undefined;
1737
+ if (!callerNode)
1738
+ continue;
1739
+ edges.push({
1740
+ callerId: callerNode.id,
1741
+ calleeId: calleeNode.id,
1742
+ calleeName: he.route.handlerName,
1743
+ line: he.call.line,
1744
+ confidence: 'http_endpoint',
1745
+ kind: 'calls',
1746
+ callType: 'direct',
1747
+ });
1748
+ }
1749
+ }
1750
+ catch {
1751
+ // HTTP edge extraction is best-effort; don't fail the whole build
1752
+ }
1753
+ // Pass 3: Calculate fanIn / fanOut (count unique caller→callee pairs, not call sites)
1754
+ const seenPairs = new Set();
1755
+ for (const edge of edges) {
1756
+ const pairKey = `${edge.callerId}\0${edge.calleeId}`;
1757
+ if (seenPairs.has(pairKey))
1758
+ continue;
1759
+ seenPairs.add(pairKey);
1760
+ const caller = allNodes.get(edge.callerId);
1761
+ const callee = allNodes.get(edge.calleeId);
1762
+ if (caller)
1763
+ caller.fanOut++;
1764
+ if (callee)
1765
+ callee.fanIn++;
1766
+ }
1767
+ // Pass 4 (prep): Mark test-file nodes before tested_by derivation
1768
+ const nodes = Array.from(allNodes.values());
1769
+ for (const n of nodes) {
1770
+ if (!n.isExternal && isTestFile(n.filePath))
1771
+ n.isTest = true;
1772
+ }
1773
+ // Pass 3b: Derive tested_by edges — reverse edges from production fn ← test fn
1774
+ // Source 1: call edges where the caller is a test function
1775
+ const callsEdges = edges.filter(e => !e.kind || e.kind === 'calls');
1776
+ const testedByPairs = new Set(); // deduplicate across sources
1777
+ for (const edge of callsEdges) {
1778
+ const caller = allNodes.get(edge.callerId);
1779
+ if (!caller || !isTestFile(caller.filePath))
1780
+ continue;
1781
+ const callee = allNodes.get(edge.calleeId);
1782
+ // Only emit tested_by when the production fn is internal (not external, not a test helper)
1783
+ if (!callee || callee.isExternal || callee.isTest)
1784
+ continue;
1785
+ const pairKey = `${edge.calleeId}\0${caller.filePath}`;
1786
+ if (testedByPairs.has(pairKey))
1787
+ continue;
1788
+ testedByPairs.add(pairKey);
1789
+ edges.push({
1790
+ kind: 'tested_by',
1791
+ callerId: edge.calleeId,
1792
+ calleeId: edge.callerId,
1793
+ calleeName: caller.name,
1794
+ confidence: edge.confidence,
1795
+ callType: undefined,
1796
+ });
1797
+ }
1798
+ // Source 2: import-based — every name imported by a test file from a production file.
1799
+ // Catches mocked functions that are imported but never directly called in the test.
1800
+ // Build a lightweight import map from file content (only test files, TS/JS).
1801
+ const allFilePaths = files.map(f => f.path);
1802
+ const NAMED_IMPORT_RE = /^\s*import\s+(?:type\s+)?\{([^}]+)\}\s+from\s+['"](\.[^'"]+)['"]/gm;
1803
+ const DEFAULT_IMPORT_RE = /^\s*import\s+(?:type\s+)?(\w+)\s+from\s+['"](\.[^'"]+)['"]/gm;
1804
+ for (const file of files) {
1805
+ if (!isTestFile(file.path))
1806
+ continue;
1807
+ if (file.language !== 'TypeScript' && file.language !== 'JavaScript')
1808
+ continue;
1809
+ const dir = dirname(file.path);
1810
+ const resolveSource = (rel) => {
1811
+ // Strip .js extension: TS ESM imports use './foo.js' to refer to './foo.ts'
1812
+ const stripped = rel.replace(/\.js$/, '');
1813
+ const base = resolvePath(dir, stripped);
1814
+ return allFilePaths.find(p => p === base || p === `${base}.ts` || p === `${base}.tsx` ||
1815
+ p === `${base}.js` || p === `${base}.jsx` || p === `${base}/index.ts`);
1816
+ };
1817
+ const testLabel = file.path.split('/').pop().replace(/\.[tj]sx?$/, '');
1818
+ const emitEdge = (name, sourceFile) => {
1819
+ const candidates = trie.findBySimpleName(name)
1820
+ .filter(n => n.filePath === sourceFile && !n.isTest && !n.isExternal);
1821
+ for (const callee of candidates) {
1822
+ const pairKey = `${callee.id}\0${file.path}`;
1823
+ if (testedByPairs.has(pairKey))
1824
+ continue;
1825
+ testedByPairs.add(pairKey);
1826
+ edges.push({
1827
+ kind: 'tested_by',
1828
+ callerId: callee.id,
1829
+ calleeId: `${file.path}::*`,
1830
+ calleeName: testLabel,
1831
+ confidence: 'import',
1832
+ callType: undefined,
1833
+ });
1834
+ }
1835
+ };
1836
+ // Named imports: import { foo, bar as baz } from './module'
1837
+ for (const m of file.content.matchAll(NAMED_IMPORT_RE)) {
1838
+ const sourceFile = resolveSource(m[2]);
1839
+ if (!sourceFile)
1840
+ continue;
1841
+ for (const part of m[1].split(',')) {
1842
+ const name = (part.match(/\bas\s+(\w+)/) ?? part.match(/(\w+)/))?.[1]?.trim();
1843
+ if (name)
1844
+ emitEdge(name, sourceFile);
1845
+ }
1846
+ }
1847
+ // Default imports: import foo from './module'
1848
+ for (const m of file.content.matchAll(DEFAULT_IMPORT_RE)) {
1849
+ const sourceFile = resolveSource(m[2]);
1850
+ if (!sourceFile)
1851
+ continue;
1852
+ emitEdge(m[1], sourceFile);
1853
+ }
1854
+ }
1855
+ // Also apply caller-provided importMap if present (cross-language coverage)
1856
+ if (importMap) {
1857
+ for (const [testFilePath, imports] of importMap) {
1858
+ if (!isTestFile(testFilePath))
1859
+ continue;
1860
+ for (const [importedName, sourceFile] of imports) {
1861
+ const candidates = trie.findBySimpleName(importedName)
1862
+ .filter(n => n.filePath === sourceFile && !n.isTest && !n.isExternal);
1863
+ for (const callee of candidates) {
1864
+ const pairKey = `${callee.id}\0${testFilePath}`;
1865
+ if (testedByPairs.has(pairKey))
1866
+ continue;
1867
+ testedByPairs.add(pairKey);
1868
+ edges.push({
1869
+ kind: 'tested_by',
1870
+ callerId: callee.id,
1871
+ calleeId: `${testFilePath}::*`,
1872
+ calleeName: testFilePath.split('/').pop().replace(/\.[tj]sx?$/, ''),
1873
+ confidence: 'import',
1874
+ callType: undefined,
1875
+ });
1876
+ }
1877
+ }
1878
+ }
1879
+ }
1880
+ // Pass 4: Derive hub functions, entry points, layer violations
1881
+ // External and test nodes are excluded from structural stats
1882
+ const internalNodes = nodes.filter(n => !n.isExternal && !n.isTest);
1883
+ const hubFunctions = internalNodes
1884
+ .filter(n => n.fanIn >= HUB_THRESHOLD)
1885
+ .sort((a, b) => b.fanIn - a.fanIn);
1886
+ const calledIds = new Set(edges.map(e => e.calleeId));
1887
+ const entryPoints = internalNodes
1888
+ .filter(n => !calledIds.has(n.id))
1889
+ .sort((a, b) => b.fanOut - a.fanOut);
1890
+ const layerViolations = layers
1891
+ ? this.detectLayerViolations(edges, allNodes, layers)
1892
+ : [];
1893
+ const totalFanIn = internalNodes.reduce((s, n) => s + n.fanIn, 0);
1894
+ const totalFanOut = internalNodes.reduce((s, n) => s + n.fanOut, 0);
1895
+ // Pass 5: Label-propagation community detection (internal non-test nodes only)
1896
+ // Each node starts with its own label; iteratively adopts the most common neighbor label.
1897
+ // Converges in ~10 passes for typical codebases. External/test nodes get no community.
1898
+ {
1899
+ const callsEdgesOnly = edges.filter(e => !e.kind || e.kind === 'calls');
1900
+ const label = new Map();
1901
+ for (const n of internalNodes)
1902
+ label.set(n.id, n.id);
1903
+ // Build adjacency for internal nodes (bidirectional — community ignores direction)
1904
+ const neighbors = new Map();
1905
+ for (const n of internalNodes)
1906
+ neighbors.set(n.id, []);
1907
+ for (const e of callsEdgesOnly) {
1908
+ if (label.has(e.callerId) && label.has(e.calleeId)) {
1909
+ neighbors.get(e.callerId).push(e.calleeId);
1910
+ neighbors.get(e.calleeId).push(e.callerId);
1911
+ }
1912
+ }
1913
+ for (let iter = 0; iter < 15; iter++) {
1914
+ let changed = false;
1915
+ // Deterministic order each iteration (sorted) avoids oscillation
1916
+ const order = [...internalNodes].sort((a, b) => a.id < b.id ? -1 : 1);
1917
+ for (const n of order) {
1918
+ const nbrs = neighbors.get(n.id);
1919
+ if (nbrs.length === 0)
1920
+ continue;
1921
+ const counts = new Map();
1922
+ for (const nbId of nbrs) {
1923
+ const l = label.get(nbId) ?? nbId;
1924
+ counts.set(l, (counts.get(l) ?? 0) + 1);
1925
+ }
1926
+ let best = label.get(n.id);
1927
+ let bestCnt = 0;
1928
+ for (const [l, c] of counts) {
1929
+ if (c > bestCnt || (c === bestCnt && l < best)) {
1930
+ best = l;
1931
+ bestCnt = c;
1932
+ }
1933
+ }
1934
+ if (best !== label.get(n.id)) {
1935
+ label.set(n.id, best);
1936
+ changed = true;
1937
+ }
1938
+ }
1939
+ if (!changed)
1940
+ break;
1941
+ }
1942
+ // Name each community by its highest-fanIn member
1943
+ const communityMembers = new Map();
1944
+ for (const n of internalNodes) {
1945
+ const l = label.get(n.id);
1946
+ if (!communityMembers.has(l))
1947
+ communityMembers.set(l, []);
1948
+ communityMembers.get(l).push(n);
1949
+ }
1950
+ for (const members of communityMembers.values()) {
1951
+ const hub = members.slice().sort((a, b) => b.fanIn - a.fanIn)[0];
1952
+ const communityLabel = hub.name;
1953
+ for (const n of members) {
1954
+ n.communityId = label.get(n.id);
1955
+ n.communityLabel = communityLabel;
1956
+ }
1957
+ }
1958
+ }
1959
+ // Pass 6: Cyclomatic complexity — regex over body slice for each internal node
1960
+ for (const node of allNodes.values()) {
1961
+ if (node.isExternal || node.startIndex === undefined || node.endIndex === undefined)
1962
+ continue;
1963
+ const content = fileContents.get(node.filePath);
1964
+ if (!content)
1965
+ continue;
1966
+ node.cyclomaticComplexity = computeCyclomaticComplexity(content.slice(node.startIndex, node.endIndex), node.language);
1967
+ }
1968
+ // Pass 7: Build class hierarchy (inheritance + grouping)
1969
+ const relationships = await extractClassRelationships(files);
1970
+ const { classes, inheritanceEdges } = buildClassNodes(allNodes, relationships);
1971
+ return {
1972
+ nodes: allNodes,
1973
+ edges,
1974
+ classes,
1975
+ inheritanceEdges,
1976
+ hubFunctions,
1977
+ entryPoints,
1978
+ layerViolations,
1979
+ stats: {
1980
+ totalNodes: internalNodes.length,
1981
+ totalEdges: edges.filter(e => !e.kind || e.kind === 'calls').length,
1982
+ avgFanIn: internalNodes.length > 0 ? totalFanIn / internalNodes.length : 0,
1983
+ avgFanOut: internalNodes.length > 0 ? totalFanOut / internalNodes.length : 0,
1984
+ },
1985
+ };
1986
+ }
1987
+ detectLayerViolations(edges, nodes, layers) {
1988
+ // Build ordered layer list (index 0 = top layer, higher index = lower layer)
1989
+ const layerOrder = Object.keys(layers);
1990
+ const getLayer = (filePath) => {
1991
+ for (const [layerName, prefixes] of Object.entries(layers)) {
1992
+ if (prefixes.some(p => filePath.includes(p)))
1993
+ return layerName;
1994
+ }
1995
+ return undefined;
1996
+ };
1997
+ const violations = [];
1998
+ for (const edge of edges) {
1999
+ const caller = nodes.get(edge.callerId);
2000
+ const callee = nodes.get(edge.calleeId);
2001
+ if (!caller || !callee)
2002
+ continue;
2003
+ const callerLayer = getLayer(caller.filePath);
2004
+ const calleeLayer = getLayer(callee.filePath);
2005
+ if (!callerLayer || !calleeLayer || callerLayer === calleeLayer)
2006
+ continue;
2007
+ const callerIdx = layerOrder.indexOf(callerLayer);
2008
+ const calleeIdx = layerOrder.indexOf(calleeLayer);
2009
+ if (callerIdx === -1 || calleeIdx === -1)
2010
+ continue;
2011
+ if (callerIdx > calleeIdx) {
2012
+ // Lower layer calling upper layer — violation
2013
+ violations.push({
2014
+ callerId: edge.callerId,
2015
+ calleeId: edge.calleeId,
2016
+ callerLayer,
2017
+ calleeLayer,
2018
+ reason: `${callerLayer} calls ${calleeLayer} (${caller.name} → ${callee.name})`,
2019
+ });
2020
+ }
2021
+ }
2022
+ return violations;
2023
+ }
2024
+ }
2025
+ // ============================================================================
2026
+ // SERIALIZATION HELPER
2027
+ // ============================================================================
2028
+ export function serializeCallGraph(result) {
2029
+ return {
2030
+ nodes: Array.from(result.nodes.values()),
2031
+ edges: result.edges,
2032
+ classes: result.classes,
2033
+ inheritanceEdges: result.inheritanceEdges,
2034
+ hubFunctions: result.hubFunctions,
2035
+ entryPoints: result.entryPoints,
2036
+ layerViolations: result.layerViolations,
2037
+ stats: result.stats,
2038
+ };
2039
+ }
2040
+ //# sourceMappingURL=call-graph.js.map