okstra 0.108.0 → 0.110.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 (648) hide show
  1. package/README.kr.md +1 -1
  2. package/README.md +1 -1
  3. package/docs/kr/architecture.md +3 -3
  4. package/docs/kr/cli.md +2 -2
  5. package/docs/project-structure-overview.md +3 -3
  6. package/package.json +1 -1
  7. package/runtime/BUILD.json +2 -2
  8. package/runtime/prompts/profiles/_common-contract.md +1 -1
  9. package/runtime/python/okstra_ctl/graphify_cmd.py +225 -0
  10. package/runtime/python/okstra_ctl/resolve_task_key.py +15 -9
  11. package/runtime/python/okstra_ctl/worktree.py +14 -13
  12. package/runtime/python/okstra_project/__init__.py +2 -0
  13. package/runtime/python/okstra_project/state.py +36 -0
  14. package/runtime/python/okstra_vendor/__init__.py +41 -0
  15. package/runtime/python/okstra_vendor/graphify/.vendored-version +1 -0
  16. package/runtime/python/okstra_vendor/graphify/__init__.py +28 -0
  17. package/runtime/python/okstra_vendor/graphify/__main__.py +1371 -0
  18. package/runtime/python/okstra_vendor/graphify/analyze.py +540 -0
  19. package/runtime/python/okstra_vendor/graphify/benchmark.py +129 -0
  20. package/runtime/python/okstra_vendor/graphify/build.py +107 -0
  21. package/runtime/python/okstra_vendor/graphify/cache.py +169 -0
  22. package/runtime/python/okstra_vendor/graphify/cluster.py +137 -0
  23. package/runtime/python/okstra_vendor/graphify/detect.py +510 -0
  24. package/runtime/python/okstra_vendor/graphify/export.py +1014 -0
  25. package/runtime/python/okstra_vendor/graphify/extract.py +3277 -0
  26. package/runtime/python/okstra_vendor/graphify/hooks.py +220 -0
  27. package/runtime/python/okstra_vendor/graphify/ingest.py +297 -0
  28. package/runtime/python/okstra_vendor/graphify/manifest.py +4 -0
  29. package/runtime/python/okstra_vendor/graphify/report.py +175 -0
  30. package/runtime/python/okstra_vendor/graphify/security.py +203 -0
  31. package/runtime/python/okstra_vendor/graphify/serve.py +373 -0
  32. package/runtime/python/okstra_vendor/graphify/skill-aider.md +1184 -0
  33. package/runtime/python/okstra_vendor/graphify/skill-claw.md +1184 -0
  34. package/runtime/python/okstra_vendor/graphify/skill-codex.md +1242 -0
  35. package/runtime/python/okstra_vendor/graphify/skill-copilot.md +1268 -0
  36. package/runtime/python/okstra_vendor/graphify/skill-droid.md +1239 -0
  37. package/runtime/python/okstra_vendor/graphify/skill-kiro.md +1183 -0
  38. package/runtime/python/okstra_vendor/graphify/skill-opencode.md +1238 -0
  39. package/runtime/python/okstra_vendor/graphify/skill-trae.md +1208 -0
  40. package/runtime/python/okstra_vendor/graphify/skill-vscode.md +253 -0
  41. package/runtime/python/okstra_vendor/graphify/skill-windows.md +1245 -0
  42. package/runtime/python/okstra_vendor/graphify/skill.md +1319 -0
  43. package/runtime/python/okstra_vendor/graphify/transcribe.py +182 -0
  44. package/runtime/python/okstra_vendor/graphify/validate.py +72 -0
  45. package/runtime/python/okstra_vendor/graphify/watch.py +188 -0
  46. package/runtime/python/okstra_vendor/graphify/wiki.py +214 -0
  47. package/runtime/python/okstra_vendor/networkx/__init__.py +62 -0
  48. package/runtime/python/okstra_vendor/networkx/algorithms/__init__.py +134 -0
  49. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/__init__.py +26 -0
  50. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/clique.py +259 -0
  51. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/clustering_coefficient.py +71 -0
  52. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/connectivity.py +412 -0
  53. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/density.py +396 -0
  54. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/distance_measures.py +150 -0
  55. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/dominating_set.py +149 -0
  56. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/kcomponents.py +369 -0
  57. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/matching.py +44 -0
  58. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/maxcut.py +143 -0
  59. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/ramsey.py +53 -0
  60. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/steinertree.py +265 -0
  61. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/__init__.py +0 -0
  62. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_approx_clust_coeff.py +41 -0
  63. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_clique.py +112 -0
  64. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_connectivity.py +199 -0
  65. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_density.py +146 -0
  66. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_distance_measures.py +59 -0
  67. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_dominating_set.py +78 -0
  68. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_kcomponents.py +303 -0
  69. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_matching.py +8 -0
  70. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_maxcut.py +94 -0
  71. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_ramsey.py +31 -0
  72. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_steinertree.py +306 -0
  73. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_traveling_salesman.py +1014 -0
  74. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_treewidth.py +274 -0
  75. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/tests/test_vertex_cover.py +68 -0
  76. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/traveling_salesman.py +1508 -0
  77. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/treewidth.py +255 -0
  78. package/runtime/python/okstra_vendor/networkx/algorithms/approximation/vertex_cover.py +83 -0
  79. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/__init__.py +5 -0
  80. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/connectivity.py +122 -0
  81. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/correlation.py +302 -0
  82. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/mixing.py +255 -0
  83. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/neighbor_degree.py +160 -0
  84. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/pairs.py +127 -0
  85. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/tests/__init__.py +0 -0
  86. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/tests/base_test.py +81 -0
  87. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/tests/test_connectivity.py +143 -0
  88. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/tests/test_correlation.py +122 -0
  89. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/tests/test_mixing.py +174 -0
  90. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/tests/test_neighbor_degree.py +107 -0
  91. package/runtime/python/okstra_vendor/networkx/algorithms/assortativity/tests/test_pairs.py +87 -0
  92. package/runtime/python/okstra_vendor/networkx/algorithms/asteroidal.py +164 -0
  93. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/__init__.py +88 -0
  94. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/basic.py +322 -0
  95. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/centrality.py +290 -0
  96. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/cluster.py +289 -0
  97. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/covering.py +57 -0
  98. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/edgelist.py +360 -0
  99. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/extendability.py +105 -0
  100. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/generators.py +603 -0
  101. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/link_analysis.py +316 -0
  102. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/matching.py +590 -0
  103. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/matrix.py +232 -0
  104. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/projection.py +526 -0
  105. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/redundancy.py +112 -0
  106. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/spectral.py +69 -0
  107. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/__init__.py +0 -0
  108. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_basic.py +125 -0
  109. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_centrality.py +192 -0
  110. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_cluster.py +84 -0
  111. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_covering.py +33 -0
  112. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_edgelist.py +240 -0
  113. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_extendability.py +334 -0
  114. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_generators.py +407 -0
  115. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_link_analysis.py +218 -0
  116. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_matching.py +327 -0
  117. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_matrix.py +138 -0
  118. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_project.py +409 -0
  119. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_redundancy.py +35 -0
  120. package/runtime/python/okstra_vendor/networkx/algorithms/bipartite/tests/test_spectral_bipartivity.py +80 -0
  121. package/runtime/python/okstra_vendor/networkx/algorithms/boundary.py +168 -0
  122. package/runtime/python/okstra_vendor/networkx/algorithms/bridges.py +205 -0
  123. package/runtime/python/okstra_vendor/networkx/algorithms/broadcasting.py +164 -0
  124. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/__init__.py +20 -0
  125. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/betweenness.py +591 -0
  126. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/betweenness_subset.py +236 -0
  127. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/closeness.py +282 -0
  128. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/current_flow_betweenness.py +364 -0
  129. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/current_flow_betweenness_subset.py +227 -0
  130. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/current_flow_closeness.py +96 -0
  131. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/degree_alg.py +150 -0
  132. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/dispersion.py +107 -0
  133. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/eigenvector.py +357 -0
  134. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/flow_matrix.py +130 -0
  135. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/group.py +787 -0
  136. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/harmonic.py +88 -0
  137. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/katz.py +331 -0
  138. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/laplacian.py +150 -0
  139. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/load.py +200 -0
  140. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/percolation.py +128 -0
  141. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/reaching.py +209 -0
  142. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/second_order.py +141 -0
  143. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/subgraph_alg.py +361 -0
  144. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/__init__.py +0 -0
  145. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_betweenness_centrality.py +923 -0
  146. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_betweenness_centrality_subset.py +354 -0
  147. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_closeness_centrality.py +274 -0
  148. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_current_flow_betweenness_centrality.py +259 -0
  149. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_current_flow_betweenness_centrality_subset.py +147 -0
  150. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_current_flow_closeness.py +43 -0
  151. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_degree_centrality.py +144 -0
  152. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_dispersion.py +73 -0
  153. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_eigenvector_centrality.py +186 -0
  154. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_group.py +277 -0
  155. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_harmonic_centrality.py +122 -0
  156. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_katz_centrality.py +345 -0
  157. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_laplacian_centrality.py +220 -0
  158. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_load_centrality.py +344 -0
  159. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_percolation_centrality.py +87 -0
  160. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_reaching.py +140 -0
  161. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_second_order_centrality.py +82 -0
  162. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_subgraph.py +110 -0
  163. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_trophic.py +302 -0
  164. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/tests/test_voterank.py +64 -0
  165. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/trophic.py +181 -0
  166. package/runtime/python/okstra_vendor/networkx/algorithms/centrality/voterank_alg.py +95 -0
  167. package/runtime/python/okstra_vendor/networkx/algorithms/chains.py +172 -0
  168. package/runtime/python/okstra_vendor/networkx/algorithms/chordal.py +443 -0
  169. package/runtime/python/okstra_vendor/networkx/algorithms/clique.py +818 -0
  170. package/runtime/python/okstra_vendor/networkx/algorithms/cluster.py +732 -0
  171. package/runtime/python/okstra_vendor/networkx/algorithms/coloring/__init__.py +4 -0
  172. package/runtime/python/okstra_vendor/networkx/algorithms/coloring/equitable_coloring.py +505 -0
  173. package/runtime/python/okstra_vendor/networkx/algorithms/coloring/greedy_coloring.py +565 -0
  174. package/runtime/python/okstra_vendor/networkx/algorithms/coloring/tests/__init__.py +0 -0
  175. package/runtime/python/okstra_vendor/networkx/algorithms/coloring/tests/test_coloring.py +863 -0
  176. package/runtime/python/okstra_vendor/networkx/algorithms/communicability_alg.py +163 -0
  177. package/runtime/python/okstra_vendor/networkx/algorithms/community/__init__.py +28 -0
  178. package/runtime/python/okstra_vendor/networkx/algorithms/community/asyn_fluid.py +153 -0
  179. package/runtime/python/okstra_vendor/networkx/algorithms/community/bipartitions.py +354 -0
  180. package/runtime/python/okstra_vendor/networkx/algorithms/community/centrality.py +171 -0
  181. package/runtime/python/okstra_vendor/networkx/algorithms/community/community_utils.py +30 -0
  182. package/runtime/python/okstra_vendor/networkx/algorithms/community/divisive.py +216 -0
  183. package/runtime/python/okstra_vendor/networkx/algorithms/community/kclique.py +79 -0
  184. package/runtime/python/okstra_vendor/networkx/algorithms/community/label_propagation.py +338 -0
  185. package/runtime/python/okstra_vendor/networkx/algorithms/community/leiden.py +162 -0
  186. package/runtime/python/okstra_vendor/networkx/algorithms/community/local.py +220 -0
  187. package/runtime/python/okstra_vendor/networkx/algorithms/community/louvain.py +384 -0
  188. package/runtime/python/okstra_vendor/networkx/algorithms/community/lukes.py +227 -0
  189. package/runtime/python/okstra_vendor/networkx/algorithms/community/modularity_max.py +452 -0
  190. package/runtime/python/okstra_vendor/networkx/algorithms/community/quality.py +347 -0
  191. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/__init__.py +0 -0
  192. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_asyn_fluid.py +147 -0
  193. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_bipartitions.py +157 -0
  194. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_centrality.py +85 -0
  195. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_divisive.py +106 -0
  196. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_kclique.py +91 -0
  197. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_label_propagation.py +241 -0
  198. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_leiden.py +138 -0
  199. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_local.py +76 -0
  200. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_louvain.py +264 -0
  201. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_lukes.py +152 -0
  202. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_modularity_max.py +340 -0
  203. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_quality.py +139 -0
  204. package/runtime/python/okstra_vendor/networkx/algorithms/community/tests/test_utils.py +26 -0
  205. package/runtime/python/okstra_vendor/networkx/algorithms/components/__init__.py +6 -0
  206. package/runtime/python/okstra_vendor/networkx/algorithms/components/attracting.py +115 -0
  207. package/runtime/python/okstra_vendor/networkx/algorithms/components/biconnected.py +394 -0
  208. package/runtime/python/okstra_vendor/networkx/algorithms/components/connected.py +282 -0
  209. package/runtime/python/okstra_vendor/networkx/algorithms/components/semiconnected.py +71 -0
  210. package/runtime/python/okstra_vendor/networkx/algorithms/components/strongly_connected.py +359 -0
  211. package/runtime/python/okstra_vendor/networkx/algorithms/components/tests/__init__.py +0 -0
  212. package/runtime/python/okstra_vendor/networkx/algorithms/components/tests/test_attracting.py +70 -0
  213. package/runtime/python/okstra_vendor/networkx/algorithms/components/tests/test_biconnected.py +248 -0
  214. package/runtime/python/okstra_vendor/networkx/algorithms/components/tests/test_connected.py +138 -0
  215. package/runtime/python/okstra_vendor/networkx/algorithms/components/tests/test_semiconnected.py +55 -0
  216. package/runtime/python/okstra_vendor/networkx/algorithms/components/tests/test_strongly_connected.py +193 -0
  217. package/runtime/python/okstra_vendor/networkx/algorithms/components/tests/test_weakly_connected.py +96 -0
  218. package/runtime/python/okstra_vendor/networkx/algorithms/components/weakly_connected.py +196 -0
  219. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/__init__.py +11 -0
  220. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/connectivity.py +811 -0
  221. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/cuts.py +616 -0
  222. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/disjoint_paths.py +408 -0
  223. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/edge_augmentation.py +1270 -0
  224. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/edge_kcomponents.py +592 -0
  225. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/kcomponents.py +220 -0
  226. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/kcutsets.py +235 -0
  227. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/stoerwagner.py +152 -0
  228. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/__init__.py +0 -0
  229. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/test_connectivity.py +421 -0
  230. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/test_cuts.py +309 -0
  231. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/test_disjoint_paths.py +249 -0
  232. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/test_edge_augmentation.py +502 -0
  233. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/test_edge_kcomponents.py +488 -0
  234. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/test_kcomponents.py +323 -0
  235. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/test_kcutsets.py +280 -0
  236. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/tests/test_stoer_wagner.py +102 -0
  237. package/runtime/python/okstra_vendor/networkx/algorithms/connectivity/utils.py +88 -0
  238. package/runtime/python/okstra_vendor/networkx/algorithms/core.py +588 -0
  239. package/runtime/python/okstra_vendor/networkx/algorithms/covering.py +142 -0
  240. package/runtime/python/okstra_vendor/networkx/algorithms/cuts.py +416 -0
  241. package/runtime/python/okstra_vendor/networkx/algorithms/cycles.py +1234 -0
  242. package/runtime/python/okstra_vendor/networkx/algorithms/d_separation.py +677 -0
  243. package/runtime/python/okstra_vendor/networkx/algorithms/dag.py +1392 -0
  244. package/runtime/python/okstra_vendor/networkx/algorithms/distance_measures.py +1095 -0
  245. package/runtime/python/okstra_vendor/networkx/algorithms/distance_regular.py +272 -0
  246. package/runtime/python/okstra_vendor/networkx/algorithms/dominance.py +142 -0
  247. package/runtime/python/okstra_vendor/networkx/algorithms/dominating.py +268 -0
  248. package/runtime/python/okstra_vendor/networkx/algorithms/efficiency_measures.py +167 -0
  249. package/runtime/python/okstra_vendor/networkx/algorithms/euler.py +470 -0
  250. package/runtime/python/okstra_vendor/networkx/algorithms/flow/__init__.py +11 -0
  251. package/runtime/python/okstra_vendor/networkx/algorithms/flow/boykovkolmogorov.py +370 -0
  252. package/runtime/python/okstra_vendor/networkx/algorithms/flow/capacityscaling.py +407 -0
  253. package/runtime/python/okstra_vendor/networkx/algorithms/flow/dinitz_alg.py +238 -0
  254. package/runtime/python/okstra_vendor/networkx/algorithms/flow/edmondskarp.py +241 -0
  255. package/runtime/python/okstra_vendor/networkx/algorithms/flow/gomory_hu.py +178 -0
  256. package/runtime/python/okstra_vendor/networkx/algorithms/flow/maxflow.py +611 -0
  257. package/runtime/python/okstra_vendor/networkx/algorithms/flow/mincost.py +356 -0
  258. package/runtime/python/okstra_vendor/networkx/algorithms/flow/networksimplex.py +662 -0
  259. package/runtime/python/okstra_vendor/networkx/algorithms/flow/preflowpush.py +425 -0
  260. package/runtime/python/okstra_vendor/networkx/algorithms/flow/shortestaugmentingpath.py +300 -0
  261. package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/__init__.py +0 -0
  262. package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/gl1.gpickle.bz2 +0 -0
  263. package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/gw1.gpickle.bz2 +0 -0
  264. package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/netgen-2.gpickle.bz2 +0 -0
  265. package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/test_gomory_hu.py +128 -0
  266. package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/test_maxflow.py +573 -0
  267. package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/test_maxflow_large_graph.py +155 -0
  268. package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/test_mincost.py +475 -0
  269. package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/test_networksimplex.py +481 -0
  270. package/runtime/python/okstra_vendor/networkx/algorithms/flow/tests/wlm3.gpickle.bz2 +0 -0
  271. package/runtime/python/okstra_vendor/networkx/algorithms/flow/utils.py +194 -0
  272. package/runtime/python/okstra_vendor/networkx/algorithms/graph_hashing.py +435 -0
  273. package/runtime/python/okstra_vendor/networkx/algorithms/graphical.py +483 -0
  274. package/runtime/python/okstra_vendor/networkx/algorithms/hierarchy.py +57 -0
  275. package/runtime/python/okstra_vendor/networkx/algorithms/hybrid.py +196 -0
  276. package/runtime/python/okstra_vendor/networkx/algorithms/isolate.py +107 -0
  277. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/__init__.py +7 -0
  278. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/ismags.py +1306 -0
  279. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/isomorph.py +336 -0
  280. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/isomorphvf2.py +1262 -0
  281. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/matchhelpers.py +352 -0
  282. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/temporalisomorphvf2.py +308 -0
  283. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/__init__.py +0 -0
  284. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/iso_r01_s80.A99 +0 -0
  285. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/iso_r01_s80.B99 +0 -0
  286. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/si2_b06_m200.A99 +0 -0
  287. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/si2_b06_m200.B99 +0 -0
  288. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_ismags.py +719 -0
  289. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_isomorphism.py +103 -0
  290. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_isomorphvf2.py +490 -0
  291. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_match_helpers.py +64 -0
  292. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_temporalisomorphvf2.py +212 -0
  293. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_tree_isomorphism.py +202 -0
  294. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_vf2pp.py +1655 -0
  295. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_vf2pp_helpers.py +3118 -0
  296. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tests/test_vf2userfunc.py +196 -0
  297. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/tree_isomorphism.py +264 -0
  298. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/vf2pp.py +1102 -0
  299. package/runtime/python/okstra_vendor/networkx/algorithms/isomorphism/vf2userfunc.py +192 -0
  300. package/runtime/python/okstra_vendor/networkx/algorithms/link_analysis/__init__.py +2 -0
  301. package/runtime/python/okstra_vendor/networkx/algorithms/link_analysis/hits_alg.py +337 -0
  302. package/runtime/python/okstra_vendor/networkx/algorithms/link_analysis/pagerank_alg.py +498 -0
  303. package/runtime/python/okstra_vendor/networkx/algorithms/link_analysis/tests/__init__.py +0 -0
  304. package/runtime/python/okstra_vendor/networkx/algorithms/link_analysis/tests/test_hits.py +77 -0
  305. package/runtime/python/okstra_vendor/networkx/algorithms/link_analysis/tests/test_pagerank.py +213 -0
  306. package/runtime/python/okstra_vendor/networkx/algorithms/link_prediction.py +687 -0
  307. package/runtime/python/okstra_vendor/networkx/algorithms/lowest_common_ancestors.py +280 -0
  308. package/runtime/python/okstra_vendor/networkx/algorithms/matching.py +1148 -0
  309. package/runtime/python/okstra_vendor/networkx/algorithms/minors/__init__.py +27 -0
  310. package/runtime/python/okstra_vendor/networkx/algorithms/minors/contraction.py +738 -0
  311. package/runtime/python/okstra_vendor/networkx/algorithms/minors/tests/test_contraction.py +544 -0
  312. package/runtime/python/okstra_vendor/networkx/algorithms/mis.py +78 -0
  313. package/runtime/python/okstra_vendor/networkx/algorithms/moral.py +59 -0
  314. package/runtime/python/okstra_vendor/networkx/algorithms/node_classification.py +219 -0
  315. package/runtime/python/okstra_vendor/networkx/algorithms/non_randomness.py +155 -0
  316. package/runtime/python/okstra_vendor/networkx/algorithms/operators/__init__.py +4 -0
  317. package/runtime/python/okstra_vendor/networkx/algorithms/operators/all.py +324 -0
  318. package/runtime/python/okstra_vendor/networkx/algorithms/operators/binary.py +468 -0
  319. package/runtime/python/okstra_vendor/networkx/algorithms/operators/product.py +633 -0
  320. package/runtime/python/okstra_vendor/networkx/algorithms/operators/tests/__init__.py +0 -0
  321. package/runtime/python/okstra_vendor/networkx/algorithms/operators/tests/test_all.py +328 -0
  322. package/runtime/python/okstra_vendor/networkx/algorithms/operators/tests/test_binary.py +451 -0
  323. package/runtime/python/okstra_vendor/networkx/algorithms/operators/tests/test_product.py +491 -0
  324. package/runtime/python/okstra_vendor/networkx/algorithms/operators/tests/test_unary.py +55 -0
  325. package/runtime/python/okstra_vendor/networkx/algorithms/operators/unary.py +77 -0
  326. package/runtime/python/okstra_vendor/networkx/algorithms/perfect_graph.py +73 -0
  327. package/runtime/python/okstra_vendor/networkx/algorithms/planar_drawing.py +464 -0
  328. package/runtime/python/okstra_vendor/networkx/algorithms/planarity.py +1463 -0
  329. package/runtime/python/okstra_vendor/networkx/algorithms/polynomials.py +306 -0
  330. package/runtime/python/okstra_vendor/networkx/algorithms/reciprocity.py +98 -0
  331. package/runtime/python/okstra_vendor/networkx/algorithms/regular.py +167 -0
  332. package/runtime/python/okstra_vendor/networkx/algorithms/richclub.py +138 -0
  333. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/__init__.py +5 -0
  334. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/astar.py +239 -0
  335. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/dense.py +264 -0
  336. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/generic.py +716 -0
  337. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/tests/__init__.py +0 -0
  338. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/tests/test_astar.py +254 -0
  339. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/tests/test_dense.py +212 -0
  340. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/tests/test_dense_numpy.py +88 -0
  341. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/tests/test_generic.py +511 -0
  342. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/tests/test_unweighted.py +149 -0
  343. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/tests/test_weighted.py +983 -0
  344. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/unweighted.py +625 -0
  345. package/runtime/python/okstra_vendor/networkx/algorithms/shortest_paths/weighted.py +2542 -0
  346. package/runtime/python/okstra_vendor/networkx/algorithms/similarity.py +2107 -0
  347. package/runtime/python/okstra_vendor/networkx/algorithms/simple_paths.py +966 -0
  348. package/runtime/python/okstra_vendor/networkx/algorithms/smallworld.py +404 -0
  349. package/runtime/python/okstra_vendor/networkx/algorithms/smetric.py +30 -0
  350. package/runtime/python/okstra_vendor/networkx/algorithms/sparsifiers.py +296 -0
  351. package/runtime/python/okstra_vendor/networkx/algorithms/structuralholes.py +374 -0
  352. package/runtime/python/okstra_vendor/networkx/algorithms/summarization.py +564 -0
  353. package/runtime/python/okstra_vendor/networkx/algorithms/swap.py +406 -0
  354. package/runtime/python/okstra_vendor/networkx/algorithms/tests/__init__.py +0 -0
  355. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_asteroidal.py +23 -0
  356. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_boundary.py +154 -0
  357. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_bridges.py +144 -0
  358. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_broadcasting.py +109 -0
  359. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_chains.py +136 -0
  360. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_chordal.py +129 -0
  361. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_clique.py +300 -0
  362. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_cluster.py +678 -0
  363. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_communicability.py +80 -0
  364. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_core.py +266 -0
  365. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_covering.py +85 -0
  366. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_cuts.py +171 -0
  367. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_cycles.py +984 -0
  368. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_d_separation.py +340 -0
  369. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_dag.py +835 -0
  370. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_distance_measures.py +831 -0
  371. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_distance_regular.py +85 -0
  372. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_dominance.py +299 -0
  373. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_dominating.py +115 -0
  374. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_efficiency.py +58 -0
  375. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_euler.py +314 -0
  376. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_graph_hashing.py +872 -0
  377. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_graphical.py +163 -0
  378. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_hierarchy.py +46 -0
  379. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_hybrid.py +24 -0
  380. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_isolate.py +26 -0
  381. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_link_prediction.py +615 -0
  382. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_lowest_common_ancestors.py +459 -0
  383. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_matching.py +556 -0
  384. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_max_weight_clique.py +179 -0
  385. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_mis.py +62 -0
  386. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_moral.py +15 -0
  387. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_node_classification.py +140 -0
  388. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_non_randomness.py +60 -0
  389. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_perfect_graph.py +27 -0
  390. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_planar_drawing.py +274 -0
  391. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_planarity.py +556 -0
  392. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_polynomials.py +57 -0
  393. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_reciprocity.py +37 -0
  394. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_regular.py +88 -0
  395. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_richclub.py +149 -0
  396. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_similarity.py +1158 -0
  397. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_simple_paths.py +803 -0
  398. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_smallworld.py +76 -0
  399. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_smetric.py +8 -0
  400. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_sparsifiers.py +138 -0
  401. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_structuralholes.py +191 -0
  402. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_summarization.py +642 -0
  403. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_swap.py +179 -0
  404. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_threshold.py +270 -0
  405. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_time_dependent.py +431 -0
  406. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_tournament.py +161 -0
  407. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_triads.py +248 -0
  408. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_vitality.py +41 -0
  409. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_voronoi.py +103 -0
  410. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_walks.py +54 -0
  411. package/runtime/python/okstra_vendor/networkx/algorithms/tests/test_wiener.py +157 -0
  412. package/runtime/python/okstra_vendor/networkx/algorithms/threshold.py +981 -0
  413. package/runtime/python/okstra_vendor/networkx/algorithms/time_dependent.py +142 -0
  414. package/runtime/python/okstra_vendor/networkx/algorithms/tournament.py +406 -0
  415. package/runtime/python/okstra_vendor/networkx/algorithms/traversal/__init__.py +5 -0
  416. package/runtime/python/okstra_vendor/networkx/algorithms/traversal/beamsearch.py +90 -0
  417. package/runtime/python/okstra_vendor/networkx/algorithms/traversal/breadth_first_search.py +576 -0
  418. package/runtime/python/okstra_vendor/networkx/algorithms/traversal/depth_first_search.py +529 -0
  419. package/runtime/python/okstra_vendor/networkx/algorithms/traversal/edgebfs.py +185 -0
  420. package/runtime/python/okstra_vendor/networkx/algorithms/traversal/edgedfs.py +182 -0
  421. package/runtime/python/okstra_vendor/networkx/algorithms/traversal/tests/__init__.py +0 -0
  422. package/runtime/python/okstra_vendor/networkx/algorithms/traversal/tests/test_beamsearch.py +25 -0
  423. package/runtime/python/okstra_vendor/networkx/algorithms/traversal/tests/test_bfs.py +203 -0
  424. package/runtime/python/okstra_vendor/networkx/algorithms/traversal/tests/test_dfs.py +307 -0
  425. package/runtime/python/okstra_vendor/networkx/algorithms/traversal/tests/test_edgebfs.py +147 -0
  426. package/runtime/python/okstra_vendor/networkx/algorithms/traversal/tests/test_edgedfs.py +131 -0
  427. package/runtime/python/okstra_vendor/networkx/algorithms/tree/__init__.py +7 -0
  428. package/runtime/python/okstra_vendor/networkx/algorithms/tree/branchings.py +1042 -0
  429. package/runtime/python/okstra_vendor/networkx/algorithms/tree/coding.py +413 -0
  430. package/runtime/python/okstra_vendor/networkx/algorithms/tree/decomposition.py +88 -0
  431. package/runtime/python/okstra_vendor/networkx/algorithms/tree/distance_measures.py +219 -0
  432. package/runtime/python/okstra_vendor/networkx/algorithms/tree/mst.py +1281 -0
  433. package/runtime/python/okstra_vendor/networkx/algorithms/tree/operations.py +106 -0
  434. package/runtime/python/okstra_vendor/networkx/algorithms/tree/recognition.py +273 -0
  435. package/runtime/python/okstra_vendor/networkx/algorithms/tree/tests/__init__.py +0 -0
  436. package/runtime/python/okstra_vendor/networkx/algorithms/tree/tests/test_branchings.py +624 -0
  437. package/runtime/python/okstra_vendor/networkx/algorithms/tree/tests/test_coding.py +114 -0
  438. package/runtime/python/okstra_vendor/networkx/algorithms/tree/tests/test_decomposition.py +79 -0
  439. package/runtime/python/okstra_vendor/networkx/algorithms/tree/tests/test_distance_measures.py +99 -0
  440. package/runtime/python/okstra_vendor/networkx/algorithms/tree/tests/test_mst.py +934 -0
  441. package/runtime/python/okstra_vendor/networkx/algorithms/tree/tests/test_operations.py +53 -0
  442. package/runtime/python/okstra_vendor/networkx/algorithms/tree/tests/test_recognition.py +174 -0
  443. package/runtime/python/okstra_vendor/networkx/algorithms/triads.py +500 -0
  444. package/runtime/python/okstra_vendor/networkx/algorithms/vitality.py +76 -0
  445. package/runtime/python/okstra_vendor/networkx/algorithms/voronoi.py +86 -0
  446. package/runtime/python/okstra_vendor/networkx/algorithms/walks.py +77 -0
  447. package/runtime/python/okstra_vendor/networkx/algorithms/wiener.py +278 -0
  448. package/runtime/python/okstra_vendor/networkx/classes/__init__.py +13 -0
  449. package/runtime/python/okstra_vendor/networkx/classes/coreviews.py +435 -0
  450. package/runtime/python/okstra_vendor/networkx/classes/digraph.py +1363 -0
  451. package/runtime/python/okstra_vendor/networkx/classes/filters.py +95 -0
  452. package/runtime/python/okstra_vendor/networkx/classes/function.py +1549 -0
  453. package/runtime/python/okstra_vendor/networkx/classes/graph.py +2082 -0
  454. package/runtime/python/okstra_vendor/networkx/classes/graphviews.py +269 -0
  455. package/runtime/python/okstra_vendor/networkx/classes/multidigraph.py +977 -0
  456. package/runtime/python/okstra_vendor/networkx/classes/multigraph.py +1294 -0
  457. package/runtime/python/okstra_vendor/networkx/classes/reportviews.py +1447 -0
  458. package/runtime/python/okstra_vendor/networkx/classes/tests/__init__.py +0 -0
  459. package/runtime/python/okstra_vendor/networkx/classes/tests/dispatch_interface.py +192 -0
  460. package/runtime/python/okstra_vendor/networkx/classes/tests/historical_tests.py +476 -0
  461. package/runtime/python/okstra_vendor/networkx/classes/tests/test_coreviews.py +362 -0
  462. package/runtime/python/okstra_vendor/networkx/classes/tests/test_digraph.py +331 -0
  463. package/runtime/python/okstra_vendor/networkx/classes/tests/test_digraph_historical.py +110 -0
  464. package/runtime/python/okstra_vendor/networkx/classes/tests/test_filters.py +177 -0
  465. package/runtime/python/okstra_vendor/networkx/classes/tests/test_function.py +1045 -0
  466. package/runtime/python/okstra_vendor/networkx/classes/tests/test_graph.py +950 -0
  467. package/runtime/python/okstra_vendor/networkx/classes/tests/test_graph_historical.py +12 -0
  468. package/runtime/python/okstra_vendor/networkx/classes/tests/test_graphviews.py +349 -0
  469. package/runtime/python/okstra_vendor/networkx/classes/tests/test_multidigraph.py +459 -0
  470. package/runtime/python/okstra_vendor/networkx/classes/tests/test_multigraph.py +528 -0
  471. package/runtime/python/okstra_vendor/networkx/classes/tests/test_reportviews.py +1421 -0
  472. package/runtime/python/okstra_vendor/networkx/classes/tests/test_special.py +131 -0
  473. package/runtime/python/okstra_vendor/networkx/classes/tests/test_subgraphviews.py +371 -0
  474. package/runtime/python/okstra_vendor/networkx/conftest.py +261 -0
  475. package/runtime/python/okstra_vendor/networkx/convert.py +502 -0
  476. package/runtime/python/okstra_vendor/networkx/convert_matrix.py +1314 -0
  477. package/runtime/python/okstra_vendor/networkx/drawing/__init__.py +7 -0
  478. package/runtime/python/okstra_vendor/networkx/drawing/layout.py +2036 -0
  479. package/runtime/python/okstra_vendor/networkx/drawing/nx_agraph.py +470 -0
  480. package/runtime/python/okstra_vendor/networkx/drawing/nx_latex.py +570 -0
  481. package/runtime/python/okstra_vendor/networkx/drawing/nx_pydot.py +361 -0
  482. package/runtime/python/okstra_vendor/networkx/drawing/nx_pylab.py +2978 -0
  483. package/runtime/python/okstra_vendor/networkx/drawing/tests/__init__.py +0 -0
  484. package/runtime/python/okstra_vendor/networkx/drawing/tests/baseline/test_display_complex.png +0 -0
  485. package/runtime/python/okstra_vendor/networkx/drawing/tests/baseline/test_display_empty_graph.png +0 -0
  486. package/runtime/python/okstra_vendor/networkx/drawing/tests/baseline/test_display_house_with_colors.png +0 -0
  487. package/runtime/python/okstra_vendor/networkx/drawing/tests/baseline/test_display_labels_and_colors.png +0 -0
  488. package/runtime/python/okstra_vendor/networkx/drawing/tests/baseline/test_display_shortest_path.png +0 -0
  489. package/runtime/python/okstra_vendor/networkx/drawing/tests/baseline/test_house_with_colors.png +0 -0
  490. package/runtime/python/okstra_vendor/networkx/drawing/tests/test_agraph.py +237 -0
  491. package/runtime/python/okstra_vendor/networkx/drawing/tests/test_image_comparison_pylab_mpl.py +229 -0
  492. package/runtime/python/okstra_vendor/networkx/drawing/tests/test_latex.py +285 -0
  493. package/runtime/python/okstra_vendor/networkx/drawing/tests/test_layout.py +631 -0
  494. package/runtime/python/okstra_vendor/networkx/drawing/tests/test_pydot.py +146 -0
  495. package/runtime/python/okstra_vendor/networkx/drawing/tests/test_pylab.py +1582 -0
  496. package/runtime/python/okstra_vendor/networkx/exception.py +131 -0
  497. package/runtime/python/okstra_vendor/networkx/generators/__init__.py +34 -0
  498. package/runtime/python/okstra_vendor/networkx/generators/atlas.dat.gz +0 -0
  499. package/runtime/python/okstra_vendor/networkx/generators/atlas.py +227 -0
  500. package/runtime/python/okstra_vendor/networkx/generators/classic.py +1091 -0
  501. package/runtime/python/okstra_vendor/networkx/generators/cographs.py +68 -0
  502. package/runtime/python/okstra_vendor/networkx/generators/community.py +1070 -0
  503. package/runtime/python/okstra_vendor/networkx/generators/degree_seq.py +886 -0
  504. package/runtime/python/okstra_vendor/networkx/generators/directed.py +572 -0
  505. package/runtime/python/okstra_vendor/networkx/generators/duplication.py +174 -0
  506. package/runtime/python/okstra_vendor/networkx/generators/ego.py +66 -0
  507. package/runtime/python/okstra_vendor/networkx/generators/expanders.py +499 -0
  508. package/runtime/python/okstra_vendor/networkx/generators/geometric.py +1037 -0
  509. package/runtime/python/okstra_vendor/networkx/generators/harary_graph.py +163 -0
  510. package/runtime/python/okstra_vendor/networkx/generators/internet_as_graphs.py +443 -0
  511. package/runtime/python/okstra_vendor/networkx/generators/intersection.py +125 -0
  512. package/runtime/python/okstra_vendor/networkx/generators/interval_graph.py +70 -0
  513. package/runtime/python/okstra_vendor/networkx/generators/joint_degree_seq.py +664 -0
  514. package/runtime/python/okstra_vendor/networkx/generators/lattice.py +405 -0
  515. package/runtime/python/okstra_vendor/networkx/generators/line.py +501 -0
  516. package/runtime/python/okstra_vendor/networkx/generators/mycielski.py +110 -0
  517. package/runtime/python/okstra_vendor/networkx/generators/nonisomorphic_trees.py +259 -0
  518. package/runtime/python/okstra_vendor/networkx/generators/random_clustered.py +117 -0
  519. package/runtime/python/okstra_vendor/networkx/generators/random_graphs.py +1416 -0
  520. package/runtime/python/okstra_vendor/networkx/generators/small.py +1070 -0
  521. package/runtime/python/okstra_vendor/networkx/generators/social.py +554 -0
  522. package/runtime/python/okstra_vendor/networkx/generators/spectral_graph_forge.py +120 -0
  523. package/runtime/python/okstra_vendor/networkx/generators/stochastic.py +54 -0
  524. package/runtime/python/okstra_vendor/networkx/generators/sudoku.py +131 -0
  525. package/runtime/python/okstra_vendor/networkx/generators/tests/__init__.py +0 -0
  526. package/runtime/python/okstra_vendor/networkx/generators/tests/test_atlas.py +75 -0
  527. package/runtime/python/okstra_vendor/networkx/generators/tests/test_classic.py +642 -0
  528. package/runtime/python/okstra_vendor/networkx/generators/tests/test_cographs.py +20 -0
  529. package/runtime/python/okstra_vendor/networkx/generators/tests/test_community.py +362 -0
  530. package/runtime/python/okstra_vendor/networkx/generators/tests/test_degree_seq.py +224 -0
  531. package/runtime/python/okstra_vendor/networkx/generators/tests/test_directed.py +189 -0
  532. package/runtime/python/okstra_vendor/networkx/generators/tests/test_duplication.py +103 -0
  533. package/runtime/python/okstra_vendor/networkx/generators/tests/test_ego.py +39 -0
  534. package/runtime/python/okstra_vendor/networkx/generators/tests/test_expanders.py +182 -0
  535. package/runtime/python/okstra_vendor/networkx/generators/tests/test_geometric.py +488 -0
  536. package/runtime/python/okstra_vendor/networkx/generators/tests/test_harary_graph.py +133 -0
  537. package/runtime/python/okstra_vendor/networkx/generators/tests/test_internet_as_graphs.py +221 -0
  538. package/runtime/python/okstra_vendor/networkx/generators/tests/test_intersection.py +28 -0
  539. package/runtime/python/okstra_vendor/networkx/generators/tests/test_interval_graph.py +144 -0
  540. package/runtime/python/okstra_vendor/networkx/generators/tests/test_joint_degree_seq.py +125 -0
  541. package/runtime/python/okstra_vendor/networkx/generators/tests/test_lattice.py +264 -0
  542. package/runtime/python/okstra_vendor/networkx/generators/tests/test_line.py +316 -0
  543. package/runtime/python/okstra_vendor/networkx/generators/tests/test_mycielski.py +30 -0
  544. package/runtime/python/okstra_vendor/networkx/generators/tests/test_nonisomorphic_trees.py +82 -0
  545. package/runtime/python/okstra_vendor/networkx/generators/tests/test_random_clustered.py +33 -0
  546. package/runtime/python/okstra_vendor/networkx/generators/tests/test_random_graphs.py +495 -0
  547. package/runtime/python/okstra_vendor/networkx/generators/tests/test_small.py +220 -0
  548. package/runtime/python/okstra_vendor/networkx/generators/tests/test_spectral_graph_forge.py +49 -0
  549. package/runtime/python/okstra_vendor/networkx/generators/tests/test_stochastic.py +72 -0
  550. package/runtime/python/okstra_vendor/networkx/generators/tests/test_sudoku.py +92 -0
  551. package/runtime/python/okstra_vendor/networkx/generators/tests/test_time_series.py +64 -0
  552. package/runtime/python/okstra_vendor/networkx/generators/tests/test_trees.py +195 -0
  553. package/runtime/python/okstra_vendor/networkx/generators/tests/test_triads.py +15 -0
  554. package/runtime/python/okstra_vendor/networkx/generators/time_series.py +74 -0
  555. package/runtime/python/okstra_vendor/networkx/generators/trees.py +1070 -0
  556. package/runtime/python/okstra_vendor/networkx/generators/triads.py +94 -0
  557. package/runtime/python/okstra_vendor/networkx/lazy_imports.py +188 -0
  558. package/runtime/python/okstra_vendor/networkx/linalg/__init__.py +13 -0
  559. package/runtime/python/okstra_vendor/networkx/linalg/algebraicconnectivity.py +650 -0
  560. package/runtime/python/okstra_vendor/networkx/linalg/attrmatrix.py +466 -0
  561. package/runtime/python/okstra_vendor/networkx/linalg/bethehessianmatrix.py +77 -0
  562. package/runtime/python/okstra_vendor/networkx/linalg/graphmatrix.py +168 -0
  563. package/runtime/python/okstra_vendor/networkx/linalg/laplacianmatrix.py +512 -0
  564. package/runtime/python/okstra_vendor/networkx/linalg/modularitymatrix.py +166 -0
  565. package/runtime/python/okstra_vendor/networkx/linalg/spectrum.py +186 -0
  566. package/runtime/python/okstra_vendor/networkx/linalg/tests/__init__.py +0 -0
  567. package/runtime/python/okstra_vendor/networkx/linalg/tests/test_algebraic_connectivity.py +400 -0
  568. package/runtime/python/okstra_vendor/networkx/linalg/tests/test_attrmatrix.py +108 -0
  569. package/runtime/python/okstra_vendor/networkx/linalg/tests/test_bethehessian.py +40 -0
  570. package/runtime/python/okstra_vendor/networkx/linalg/tests/test_graphmatrix.py +275 -0
  571. package/runtime/python/okstra_vendor/networkx/linalg/tests/test_laplacian.py +334 -0
  572. package/runtime/python/okstra_vendor/networkx/linalg/tests/test_modularity.py +86 -0
  573. package/runtime/python/okstra_vendor/networkx/linalg/tests/test_spectrum.py +70 -0
  574. package/runtime/python/okstra_vendor/networkx/readwrite/__init__.py +17 -0
  575. package/runtime/python/okstra_vendor/networkx/readwrite/adjlist.py +330 -0
  576. package/runtime/python/okstra_vendor/networkx/readwrite/edgelist.py +489 -0
  577. package/runtime/python/okstra_vendor/networkx/readwrite/gexf.py +1084 -0
  578. package/runtime/python/okstra_vendor/networkx/readwrite/gml.py +879 -0
  579. package/runtime/python/okstra_vendor/networkx/readwrite/graph6.py +427 -0
  580. package/runtime/python/okstra_vendor/networkx/readwrite/graphml.py +1053 -0
  581. package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/__init__.py +19 -0
  582. package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/adjacency.py +156 -0
  583. package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/cytoscape.py +190 -0
  584. package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/node_link.py +261 -0
  585. package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/tests/__init__.py +0 -0
  586. package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/tests/test_adjacency.py +78 -0
  587. package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/tests/test_cytoscape.py +78 -0
  588. package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/tests/test_node_link.py +109 -0
  589. package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/tests/test_tree.py +48 -0
  590. package/runtime/python/okstra_vendor/networkx/readwrite/json_graph/tree.py +137 -0
  591. package/runtime/python/okstra_vendor/networkx/readwrite/leda.py +108 -0
  592. package/runtime/python/okstra_vendor/networkx/readwrite/multiline_adjlist.py +393 -0
  593. package/runtime/python/okstra_vendor/networkx/readwrite/p2g.py +113 -0
  594. package/runtime/python/okstra_vendor/networkx/readwrite/pajek.py +286 -0
  595. package/runtime/python/okstra_vendor/networkx/readwrite/sparse6.py +379 -0
  596. package/runtime/python/okstra_vendor/networkx/readwrite/tests/__init__.py +0 -0
  597. package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_adjlist.py +354 -0
  598. package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_edgelist.py +318 -0
  599. package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_gexf.py +612 -0
  600. package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_gml.py +744 -0
  601. package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_graph6.py +181 -0
  602. package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_graphml.py +1531 -0
  603. package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_leda.py +30 -0
  604. package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_p2g.py +63 -0
  605. package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_pajek.py +128 -0
  606. package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_sparse6.py +166 -0
  607. package/runtime/python/okstra_vendor/networkx/readwrite/tests/test_text.py +1742 -0
  608. package/runtime/python/okstra_vendor/networkx/readwrite/text.py +851 -0
  609. package/runtime/python/okstra_vendor/networkx/relabel.py +285 -0
  610. package/runtime/python/okstra_vendor/networkx/tests/__init__.py +0 -0
  611. package/runtime/python/okstra_vendor/networkx/tests/test_all_random_functions.py +248 -0
  612. package/runtime/python/okstra_vendor/networkx/tests/test_convert.py +321 -0
  613. package/runtime/python/okstra_vendor/networkx/tests/test_convert_numpy.py +531 -0
  614. package/runtime/python/okstra_vendor/networkx/tests/test_convert_pandas.py +349 -0
  615. package/runtime/python/okstra_vendor/networkx/tests/test_convert_scipy.py +281 -0
  616. package/runtime/python/okstra_vendor/networkx/tests/test_exceptions.py +40 -0
  617. package/runtime/python/okstra_vendor/networkx/tests/test_import.py +11 -0
  618. package/runtime/python/okstra_vendor/networkx/tests/test_lazy_imports.py +96 -0
  619. package/runtime/python/okstra_vendor/networkx/tests/test_relabel.py +349 -0
  620. package/runtime/python/okstra_vendor/networkx/tests/test_removed_functions_exception_messages.py +8 -0
  621. package/runtime/python/okstra_vendor/networkx/utils/__init__.py +8 -0
  622. package/runtime/python/okstra_vendor/networkx/utils/backends.py +2171 -0
  623. package/runtime/python/okstra_vendor/networkx/utils/configs.py +396 -0
  624. package/runtime/python/okstra_vendor/networkx/utils/decorators.py +1233 -0
  625. package/runtime/python/okstra_vendor/networkx/utils/heaps.py +338 -0
  626. package/runtime/python/okstra_vendor/networkx/utils/mapped_queue.py +297 -0
  627. package/runtime/python/okstra_vendor/networkx/utils/misc.py +703 -0
  628. package/runtime/python/okstra_vendor/networkx/utils/random_sequence.py +198 -0
  629. package/runtime/python/okstra_vendor/networkx/utils/rcm.py +159 -0
  630. package/runtime/python/okstra_vendor/networkx/utils/tests/__init__.py +0 -0
  631. package/runtime/python/okstra_vendor/networkx/utils/tests/test__init.py +11 -0
  632. package/runtime/python/okstra_vendor/networkx/utils/tests/test_backends.py +225 -0
  633. package/runtime/python/okstra_vendor/networkx/utils/tests/test_config.py +263 -0
  634. package/runtime/python/okstra_vendor/networkx/utils/tests/test_decorators.py +510 -0
  635. package/runtime/python/okstra_vendor/networkx/utils/tests/test_heaps.py +131 -0
  636. package/runtime/python/okstra_vendor/networkx/utils/tests/test_mapped_queue.py +268 -0
  637. package/runtime/python/okstra_vendor/networkx/utils/tests/test_misc.py +393 -0
  638. package/runtime/python/okstra_vendor/networkx/utils/tests/test_random_sequence.py +53 -0
  639. package/runtime/python/okstra_vendor/networkx/utils/tests/test_rcm.py +63 -0
  640. package/runtime/python/okstra_vendor/networkx/utils/tests/test_unionfind.py +55 -0
  641. package/runtime/python/okstra_vendor/networkx/utils/union_find.py +106 -0
  642. package/runtime/skills/okstra-graphify/SKILL.md +161 -0
  643. package/runtime/skills/okstra-inspect/SKILL.md +17 -9
  644. package/runtime/templates/reports/settings.template.json +4 -0
  645. package/src/cli-registry.mjs +7 -0
  646. package/src/commands/graphify.mjs +32 -0
  647. package/src/commands/lifecycle/doctor.mjs +9 -0
  648. package/src/lib/skill-catalog.mjs +1 -0
@@ -0,0 +1,3277 @@
1
+ """Deterministic structural extraction from source code using tree-sitter. Outputs nodes+edges dicts."""
2
+ from __future__ import annotations
3
+ import importlib
4
+ import json
5
+ import os
6
+ import re
7
+ import sys
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+ from typing import Callable, Any
11
+ from .cache import load_cached, save_cached
12
+
13
+
14
+ def _make_id(*parts: str) -> str:
15
+ """Build a stable node ID from one or more name parts."""
16
+ combined = "_".join(p.strip("_.") for p in parts if p)
17
+ cleaned = re.sub(r"[^a-zA-Z0-9]+", "_", combined)
18
+ return cleaned.strip("_").lower()
19
+
20
+
21
+ # ── LanguageConfig dataclass ─────────────────────────────────────────────────
22
+
23
+ @dataclass
24
+ class LanguageConfig:
25
+ ts_module: str # e.g. "tree_sitter_python"
26
+ ts_language_fn: str = "language" # attr to call: e.g. tslang.language()
27
+
28
+ class_types: frozenset = frozenset()
29
+ function_types: frozenset = frozenset()
30
+ import_types: frozenset = frozenset()
31
+ call_types: frozenset = frozenset()
32
+ static_prop_types: frozenset = frozenset()
33
+ helper_fn_names: frozenset = frozenset()
34
+ container_bind_methods: frozenset = frozenset()
35
+ event_listener_properties: frozenset = frozenset()
36
+
37
+ # Name extraction
38
+ name_field: str = "name"
39
+ name_fallback_child_types: tuple = ()
40
+
41
+ # Body detection
42
+ body_field: str = "body"
43
+ body_fallback_child_types: tuple = () # e.g. ("declaration_list", "compound_statement")
44
+
45
+ # Call name extraction
46
+ call_function_field: str = "function" # field on call node for callee
47
+ call_accessor_node_types: frozenset = frozenset() # member/attribute nodes
48
+ call_accessor_field: str = "attribute" # field on accessor for method name
49
+
50
+ # Stop recursion at these types in walk_calls
51
+ function_boundary_types: frozenset = frozenset()
52
+
53
+ # Import handler: called for import nodes instead of generic handling
54
+ import_handler: Callable | None = None
55
+
56
+ # Optional custom name resolver for functions (C, C++ declarator unwrapping)
57
+ resolve_function_name_fn: Callable | None = None
58
+
59
+ # Extra label formatting for functions: if True, functions get "name()" label
60
+ function_label_parens: bool = True
61
+
62
+ # Extra walk hook called after generic dispatch (for JS arrow functions, C# namespaces, etc.)
63
+ extra_walk_fn: Callable | None = None
64
+
65
+
66
+ # ── Generic helpers ───────────────────────────────────────────────────────────
67
+
68
+ def _read_text(node, source: bytes) -> str:
69
+ return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
70
+
71
+
72
+ def _resolve_name(node, source: bytes, config: LanguageConfig) -> str | None:
73
+ """Get the name from a node using config.name_field, falling back to child types."""
74
+ if config.resolve_function_name_fn is not None:
75
+ # For C/C++ where the name is inside a declarator
76
+ return None # caller handles this separately
77
+ n = node.child_by_field_name(config.name_field)
78
+ if n:
79
+ return _read_text(n, source)
80
+ for child in node.children:
81
+ if child.type in config.name_fallback_child_types:
82
+ return _read_text(child, source)
83
+ return None
84
+
85
+
86
+ def _find_body(node, config: LanguageConfig):
87
+ """Find the body node using config.body_field, falling back to child types."""
88
+ b = node.child_by_field_name(config.body_field)
89
+ if b:
90
+ return b
91
+ for child in node.children:
92
+ if child.type in config.body_fallback_child_types:
93
+ return child
94
+ return None
95
+
96
+
97
+ # ── Import handlers ───────────────────────────────────────────────────────────
98
+
99
+ def _import_python(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None:
100
+ t = node.type
101
+ if t == "import_statement":
102
+ for child in node.children:
103
+ if child.type in ("dotted_name", "aliased_import"):
104
+ raw = _read_text(child, source)
105
+ module_name = raw.split(" as ")[0].strip().lstrip(".")
106
+ tgt_nid = _make_id(module_name)
107
+ edges.append({
108
+ "source": file_nid,
109
+ "target": tgt_nid,
110
+ "relation": "imports",
111
+ "confidence": "EXTRACTED",
112
+ "source_file": str_path,
113
+ "source_location": f"L{node.start_point[0] + 1}",
114
+ "weight": 1.0,
115
+ })
116
+ elif t == "import_from_statement":
117
+ module_node = node.child_by_field_name("module_name")
118
+ if module_node:
119
+ raw = _read_text(module_node, source)
120
+ if raw.startswith("."):
121
+ # Relative import - resolve to full path so IDs match file node IDs
122
+ dots = len(raw) - len(raw.lstrip("."))
123
+ module_name = raw.lstrip(".")
124
+ base = Path(str_path).parent
125
+ for _ in range(dots - 1):
126
+ base = base.parent
127
+ rel = (module_name.replace(".", "/") + ".py") if module_name else "__init__.py"
128
+ tgt_nid = _make_id(str(base / rel))
129
+ else:
130
+ tgt_nid = _make_id(raw)
131
+ edges.append({
132
+ "source": file_nid,
133
+ "target": tgt_nid,
134
+ "relation": "imports_from",
135
+ "confidence": "EXTRACTED",
136
+ "source_file": str_path,
137
+ "source_location": f"L{node.start_point[0] + 1}",
138
+ "weight": 1.0,
139
+ })
140
+
141
+
142
+ def _import_js(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None:
143
+ for child in node.children:
144
+ if child.type == "string":
145
+ raw = _read_text(child, source).strip("'\"` ")
146
+ if not raw:
147
+ break
148
+ if raw.startswith("."):
149
+ # Relative import - resolve to full path so IDs match file node IDs
150
+ # normpath removes ".." segments so the ID matches the target file's own node ID
151
+ resolved = Path(os.path.normpath(Path(str_path).parent / raw))
152
+ # TypeScript ESM: imports written as .js but actual file is .ts/.tsx
153
+ if resolved.suffix == ".js":
154
+ resolved = resolved.with_suffix(".ts")
155
+ elif resolved.suffix == ".jsx":
156
+ resolved = resolved.with_suffix(".tsx")
157
+ tgt_nid = _make_id(str(resolved))
158
+ else:
159
+ # Bare/scoped import (node_modules) - use last segment; dropped as external
160
+ module_name = raw.split("/")[-1]
161
+ if not module_name:
162
+ break
163
+ tgt_nid = _make_id(module_name)
164
+ edges.append({
165
+ "source": file_nid,
166
+ "target": tgt_nid,
167
+ "relation": "imports_from",
168
+ "confidence": "EXTRACTED",
169
+ "source_file": str_path,
170
+ "source_location": f"L{node.start_point[0] + 1}",
171
+ "weight": 1.0,
172
+ })
173
+ break
174
+
175
+
176
+ def _import_java(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None:
177
+ def _walk_scoped(n) -> str:
178
+ parts: list[str] = []
179
+ cur = n
180
+ while cur:
181
+ if cur.type == "scoped_identifier":
182
+ name_node = cur.child_by_field_name("name")
183
+ if name_node:
184
+ parts.append(_read_text(name_node, source))
185
+ cur = cur.child_by_field_name("scope")
186
+ elif cur.type == "identifier":
187
+ parts.append(_read_text(cur, source))
188
+ break
189
+ else:
190
+ break
191
+ parts.reverse()
192
+ return ".".join(parts)
193
+
194
+ for child in node.children:
195
+ if child.type in ("scoped_identifier", "identifier"):
196
+ path_str = _walk_scoped(child)
197
+ module_name = path_str.split(".")[-1].strip("*").strip(".") or (
198
+ path_str.split(".")[-2] if len(path_str.split(".")) > 1 else path_str
199
+ )
200
+ if module_name:
201
+ tgt_nid = _make_id(module_name)
202
+ edges.append({
203
+ "source": file_nid,
204
+ "target": tgt_nid,
205
+ "relation": "imports",
206
+ "confidence": "EXTRACTED",
207
+ "source_file": str_path,
208
+ "source_location": f"L{node.start_point[0] + 1}",
209
+ "weight": 1.0,
210
+ })
211
+ break
212
+
213
+
214
+ def _import_c(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None:
215
+ for child in node.children:
216
+ if child.type in ("string_literal", "system_lib_string", "string"):
217
+ raw = _read_text(child, source).strip('"<> ')
218
+ module_name = raw.split("/")[-1].split(".")[0]
219
+ if module_name:
220
+ tgt_nid = _make_id(module_name)
221
+ edges.append({
222
+ "source": file_nid,
223
+ "target": tgt_nid,
224
+ "relation": "imports",
225
+ "confidence": "EXTRACTED",
226
+ "source_file": str_path,
227
+ "source_location": f"L{node.start_point[0] + 1}",
228
+ "weight": 1.0,
229
+ })
230
+ break
231
+
232
+
233
+ def _import_csharp(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None:
234
+ for child in node.children:
235
+ if child.type in ("qualified_name", "identifier", "name_equals"):
236
+ raw = _read_text(child, source)
237
+ module_name = raw.split(".")[-1].strip()
238
+ if module_name:
239
+ tgt_nid = _make_id(module_name)
240
+ edges.append({
241
+ "source": file_nid,
242
+ "target": tgt_nid,
243
+ "relation": "imports",
244
+ "confidence": "EXTRACTED",
245
+ "source_file": str_path,
246
+ "source_location": f"L{node.start_point[0] + 1}",
247
+ "weight": 1.0,
248
+ })
249
+ break
250
+
251
+
252
+ def _import_kotlin(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None:
253
+ path_node = node.child_by_field_name("path")
254
+ if path_node:
255
+ raw = _read_text(path_node, source)
256
+ module_name = raw.split(".")[-1].strip()
257
+ if module_name:
258
+ tgt_nid = _make_id(module_name)
259
+ edges.append({
260
+ "source": file_nid,
261
+ "target": tgt_nid,
262
+ "relation": "imports",
263
+ "confidence": "EXTRACTED",
264
+ "source_file": str_path,
265
+ "source_location": f"L{node.start_point[0] + 1}",
266
+ "weight": 1.0,
267
+ })
268
+ return
269
+ # Fallback: find identifier child
270
+ for child in node.children:
271
+ if child.type == "identifier":
272
+ raw = _read_text(child, source)
273
+ tgt_nid = _make_id(raw)
274
+ edges.append({
275
+ "source": file_nid,
276
+ "target": tgt_nid,
277
+ "relation": "imports",
278
+ "confidence": "EXTRACTED",
279
+ "source_file": str_path,
280
+ "source_location": f"L{node.start_point[0] + 1}",
281
+ "weight": 1.0,
282
+ })
283
+ break
284
+
285
+
286
+ def _import_scala(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None:
287
+ for child in node.children:
288
+ if child.type in ("stable_id", "identifier"):
289
+ raw = _read_text(child, source)
290
+ module_name = raw.split(".")[-1].strip("{} ")
291
+ if module_name and module_name != "_":
292
+ tgt_nid = _make_id(module_name)
293
+ edges.append({
294
+ "source": file_nid,
295
+ "target": tgt_nid,
296
+ "relation": "imports",
297
+ "confidence": "EXTRACTED",
298
+ "source_file": str_path,
299
+ "source_location": f"L{node.start_point[0] + 1}",
300
+ "weight": 1.0,
301
+ })
302
+ break
303
+
304
+
305
+ def _import_php(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None:
306
+ for child in node.children:
307
+ if child.type in ("qualified_name", "name", "identifier"):
308
+ raw = _read_text(child, source)
309
+ module_name = raw.split("\\")[-1].strip()
310
+ if module_name:
311
+ tgt_nid = _make_id(module_name)
312
+ edges.append({
313
+ "source": file_nid,
314
+ "target": tgt_nid,
315
+ "relation": "imports",
316
+ "confidence": "EXTRACTED",
317
+ "source_file": str_path,
318
+ "source_location": f"L{node.start_point[0] + 1}",
319
+ "weight": 1.0,
320
+ })
321
+ break
322
+
323
+
324
+ # ── C/C++ function name helpers ───────────────────────────────────────────────
325
+
326
+ def _get_c_func_name(node, source: bytes) -> str | None:
327
+ """Recursively unwrap declarator to find the innermost identifier (C)."""
328
+ if node.type == "identifier":
329
+ return _read_text(node, source)
330
+ decl = node.child_by_field_name("declarator")
331
+ if decl:
332
+ return _get_c_func_name(decl, source)
333
+ for child in node.children:
334
+ if child.type == "identifier":
335
+ return _read_text(child, source)
336
+ return None
337
+
338
+
339
+ def _get_cpp_func_name(node, source: bytes) -> str | None:
340
+ """Recursively unwrap declarator to find the innermost identifier (C++)."""
341
+ if node.type == "identifier":
342
+ return _read_text(node, source)
343
+ if node.type == "qualified_identifier":
344
+ name_node = node.child_by_field_name("name")
345
+ if name_node:
346
+ return _read_text(name_node, source)
347
+ decl = node.child_by_field_name("declarator")
348
+ if decl:
349
+ return _get_cpp_func_name(decl, source)
350
+ for child in node.children:
351
+ if child.type == "identifier":
352
+ return _read_text(child, source)
353
+ return None
354
+
355
+
356
+ # ── JS/TS extra walk for arrow functions ──────────────────────────────────────
357
+
358
+ def _js_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str,
359
+ nodes: list, edges: list, seen_ids: set, function_bodies: list,
360
+ parent_class_nid: str | None, add_node_fn, add_edge_fn) -> bool:
361
+ """Handle lexical_declaration (arrow functions) for JS/TS. Returns True if handled."""
362
+ if node.type == "lexical_declaration":
363
+ for child in node.children:
364
+ if child.type == "variable_declarator":
365
+ value = child.child_by_field_name("value")
366
+ if value and value.type == "arrow_function":
367
+ name_node = child.child_by_field_name("name")
368
+ if name_node:
369
+ func_name = _read_text(name_node, source)
370
+ line = child.start_point[0] + 1
371
+ func_nid = _make_id(stem, func_name)
372
+ add_node_fn(func_nid, f"{func_name}()", line)
373
+ add_edge_fn(file_nid, func_nid, "contains", line)
374
+ body = value.child_by_field_name("body")
375
+ if body:
376
+ function_bodies.append((func_nid, body))
377
+ return True
378
+ return False
379
+
380
+
381
+ # ── C# extra walk for namespace declarations ──────────────────────────────────
382
+
383
+ def _csharp_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str,
384
+ nodes: list, edges: list, seen_ids: set, function_bodies: list,
385
+ parent_class_nid: str | None, add_node_fn, add_edge_fn,
386
+ walk_fn) -> bool:
387
+ """Handle namespace_declaration for C#. Returns True if handled."""
388
+ if node.type == "namespace_declaration":
389
+ name_node = node.child_by_field_name("name")
390
+ if name_node:
391
+ ns_name = _read_text(name_node, source)
392
+ ns_nid = _make_id(stem, ns_name)
393
+ line = node.start_point[0] + 1
394
+ add_node_fn(ns_nid, ns_name, line)
395
+ add_edge_fn(file_nid, ns_nid, "contains", line)
396
+ body = node.child_by_field_name("body")
397
+ if body:
398
+ for child in body.children:
399
+ walk_fn(child, parent_class_nid)
400
+ return True
401
+ return False
402
+
403
+
404
+ # ── Swift extra walk for enum cases ──────────────────────────────────────────
405
+
406
+ def _swift_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str,
407
+ nodes: list, edges: list, seen_ids: set, function_bodies: list,
408
+ parent_class_nid: str | None, add_node_fn, add_edge_fn) -> bool:
409
+ """Handle enum_entry for Swift. Returns True if handled."""
410
+ if node.type == "enum_entry" and parent_class_nid:
411
+ for child in node.children:
412
+ if child.type == "simple_identifier":
413
+ case_name = _read_text(child, source)
414
+ case_nid = _make_id(parent_class_nid, case_name)
415
+ line = node.start_point[0] + 1
416
+ add_node_fn(case_nid, case_name, line)
417
+ add_edge_fn(parent_class_nid, case_nid, "case_of", line)
418
+ return True
419
+ return False
420
+
421
+
422
+ # ── Language configs ──────────────────────────────────────────────────────────
423
+
424
+ _PYTHON_CONFIG = LanguageConfig(
425
+ ts_module="tree_sitter_python",
426
+ class_types=frozenset({"class_definition"}),
427
+ function_types=frozenset({"function_definition"}),
428
+ import_types=frozenset({"import_statement", "import_from_statement"}),
429
+ call_types=frozenset({"call"}),
430
+ call_function_field="function",
431
+ call_accessor_node_types=frozenset({"attribute"}),
432
+ call_accessor_field="attribute",
433
+ function_boundary_types=frozenset({"function_definition"}),
434
+ import_handler=_import_python,
435
+ )
436
+
437
+ _JS_CONFIG = LanguageConfig(
438
+ ts_module="tree_sitter_javascript",
439
+ class_types=frozenset({"class_declaration"}),
440
+ function_types=frozenset({"function_declaration", "method_definition"}),
441
+ import_types=frozenset({"import_statement"}),
442
+ call_types=frozenset({"call_expression"}),
443
+ call_function_field="function",
444
+ call_accessor_node_types=frozenset({"member_expression"}),
445
+ call_accessor_field="property",
446
+ function_boundary_types=frozenset({"function_declaration", "arrow_function", "method_definition"}),
447
+ import_handler=_import_js,
448
+ )
449
+
450
+ _TS_CONFIG = LanguageConfig(
451
+ ts_module="tree_sitter_typescript",
452
+ ts_language_fn="language_typescript",
453
+ class_types=frozenset({"class_declaration"}),
454
+ function_types=frozenset({"function_declaration", "method_definition"}),
455
+ import_types=frozenset({"import_statement"}),
456
+ call_types=frozenset({"call_expression"}),
457
+ call_function_field="function",
458
+ call_accessor_node_types=frozenset({"member_expression"}),
459
+ call_accessor_field="property",
460
+ function_boundary_types=frozenset({"function_declaration", "arrow_function", "method_definition"}),
461
+ import_handler=_import_js,
462
+ )
463
+
464
+ _JAVA_CONFIG = LanguageConfig(
465
+ ts_module="tree_sitter_java",
466
+ class_types=frozenset({"class_declaration", "interface_declaration"}),
467
+ function_types=frozenset({"method_declaration", "constructor_declaration"}),
468
+ import_types=frozenset({"import_declaration"}),
469
+ call_types=frozenset({"method_invocation"}),
470
+ call_function_field="name",
471
+ call_accessor_node_types=frozenset(),
472
+ function_boundary_types=frozenset({"method_declaration", "constructor_declaration"}),
473
+ import_handler=_import_java,
474
+ )
475
+
476
+ _C_CONFIG = LanguageConfig(
477
+ ts_module="tree_sitter_c",
478
+ class_types=frozenset(),
479
+ function_types=frozenset({"function_definition"}),
480
+ import_types=frozenset({"preproc_include"}),
481
+ call_types=frozenset({"call_expression"}),
482
+ call_function_field="function",
483
+ call_accessor_node_types=frozenset({"field_expression"}),
484
+ call_accessor_field="field",
485
+ function_boundary_types=frozenset({"function_definition"}),
486
+ import_handler=_import_c,
487
+ resolve_function_name_fn=_get_c_func_name,
488
+ )
489
+
490
+ _CPP_CONFIG = LanguageConfig(
491
+ ts_module="tree_sitter_cpp",
492
+ class_types=frozenset({"class_specifier"}),
493
+ function_types=frozenset({"function_definition"}),
494
+ import_types=frozenset({"preproc_include"}),
495
+ call_types=frozenset({"call_expression"}),
496
+ call_function_field="function",
497
+ call_accessor_node_types=frozenset({"field_expression", "qualified_identifier"}),
498
+ call_accessor_field="field",
499
+ function_boundary_types=frozenset({"function_definition"}),
500
+ import_handler=_import_c,
501
+ resolve_function_name_fn=_get_cpp_func_name,
502
+ )
503
+
504
+ _RUBY_CONFIG = LanguageConfig(
505
+ ts_module="tree_sitter_ruby",
506
+ class_types=frozenset({"class"}),
507
+ function_types=frozenset({"method", "singleton_method"}),
508
+ import_types=frozenset(),
509
+ call_types=frozenset({"call"}),
510
+ call_function_field="method",
511
+ call_accessor_node_types=frozenset(),
512
+ name_fallback_child_types=("constant", "scope_resolution", "identifier"),
513
+ body_fallback_child_types=("body_statement",),
514
+ function_boundary_types=frozenset({"method", "singleton_method"}),
515
+ )
516
+
517
+ _CSHARP_CONFIG = LanguageConfig(
518
+ ts_module="tree_sitter_c_sharp",
519
+ class_types=frozenset({"class_declaration", "interface_declaration"}),
520
+ function_types=frozenset({"method_declaration"}),
521
+ import_types=frozenset({"using_directive"}),
522
+ call_types=frozenset({"invocation_expression"}),
523
+ call_function_field="function",
524
+ call_accessor_node_types=frozenset({"member_access_expression"}),
525
+ call_accessor_field="name",
526
+ body_fallback_child_types=("declaration_list",),
527
+ function_boundary_types=frozenset({"method_declaration"}),
528
+ import_handler=_import_csharp,
529
+ )
530
+
531
+ _KOTLIN_CONFIG = LanguageConfig(
532
+ ts_module="tree_sitter_kotlin",
533
+ class_types=frozenset({"class_declaration", "object_declaration"}),
534
+ function_types=frozenset({"function_declaration"}),
535
+ import_types=frozenset({"import_header"}),
536
+ call_types=frozenset({"call_expression"}),
537
+ call_function_field="",
538
+ call_accessor_node_types=frozenset({"navigation_expression"}),
539
+ call_accessor_field="",
540
+ name_fallback_child_types=("simple_identifier",),
541
+ body_fallback_child_types=("function_body", "class_body"),
542
+ function_boundary_types=frozenset({"function_declaration"}),
543
+ import_handler=_import_kotlin,
544
+ )
545
+
546
+ _SCALA_CONFIG = LanguageConfig(
547
+ ts_module="tree_sitter_scala",
548
+ class_types=frozenset({"class_definition", "object_definition"}),
549
+ function_types=frozenset({"function_definition"}),
550
+ import_types=frozenset({"import_declaration"}),
551
+ call_types=frozenset({"call_expression"}),
552
+ call_function_field="",
553
+ call_accessor_node_types=frozenset({"field_expression"}),
554
+ call_accessor_field="field",
555
+ name_fallback_child_types=("identifier",),
556
+ body_fallback_child_types=("template_body",),
557
+ function_boundary_types=frozenset({"function_definition"}),
558
+ import_handler=_import_scala,
559
+ )
560
+
561
+ _PHP_CONFIG = LanguageConfig(
562
+ ts_module="tree_sitter_php",
563
+ ts_language_fn="language_php",
564
+ class_types=frozenset({"class_declaration"}),
565
+ function_types=frozenset({"function_definition", "method_declaration"}),
566
+ import_types=frozenset({"namespace_use_clause"}),
567
+ call_types=frozenset({"function_call_expression", "member_call_expression", "scoped_call_expression", "class_constant_access_expression"}),
568
+ static_prop_types=frozenset({"scoped_property_access_expression"}),
569
+ helper_fn_names=frozenset({"config"}),
570
+ container_bind_methods=frozenset({"bind", "singleton", "scoped", "instance"}),
571
+ event_listener_properties=frozenset({"listen", "subscribe"}),
572
+ call_function_field="function",
573
+ call_accessor_node_types=frozenset({"member_call_expression"}),
574
+ call_accessor_field="name",
575
+ name_fallback_child_types=("name",),
576
+ body_fallback_child_types=("declaration_list", "compound_statement"),
577
+ function_boundary_types=frozenset({"function_definition", "method_declaration"}),
578
+ import_handler=_import_php,
579
+ )
580
+
581
+
582
+ def _import_lua(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None:
583
+ """Extract require('module') from Lua variable_declaration nodes."""
584
+ text = _read_text(node, source)
585
+ import re
586
+ m = re.search(r"""require\s*[\('"]\s*['"]?([^'")\s]+)""", text)
587
+ if m:
588
+ module_name = m.group(1).split(".")[-1]
589
+ if module_name:
590
+ edges.append({
591
+ "source": file_nid,
592
+ "target": module_name,
593
+ "relation": "imports",
594
+ "confidence": "EXTRACTED",
595
+ "confidence_score": 1.0,
596
+ "source_file": str_path,
597
+ "source_location": str(node.start_point[0] + 1),
598
+ "weight": 1.0,
599
+ })
600
+
601
+
602
+ _LUA_CONFIG = LanguageConfig(
603
+ ts_module="tree_sitter_lua",
604
+ ts_language_fn="language",
605
+ class_types=frozenset(),
606
+ function_types=frozenset({"function_declaration"}),
607
+ import_types=frozenset({"variable_declaration"}),
608
+ call_types=frozenset({"function_call"}),
609
+ call_function_field="name",
610
+ call_accessor_node_types=frozenset({"method_index_expression"}),
611
+ call_accessor_field="name",
612
+ name_fallback_child_types=("identifier", "method_index_expression"),
613
+ body_fallback_child_types=("block",),
614
+ function_boundary_types=frozenset({"function_declaration"}),
615
+ import_handler=_import_lua,
616
+ )
617
+
618
+
619
+ def _import_swift(node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str) -> None:
620
+ for child in node.children:
621
+ if child.type == "identifier":
622
+ raw = _read_text(child, source)
623
+ tgt_nid = _make_id(raw)
624
+ edges.append({
625
+ "source": file_nid,
626
+ "target": tgt_nid,
627
+ "relation": "imports",
628
+ "confidence": "EXTRACTED",
629
+ "source_file": str_path,
630
+ "source_location": f"L{node.start_point[0] + 1}",
631
+ "weight": 1.0,
632
+ })
633
+ break
634
+
635
+
636
+ _SWIFT_CONFIG = LanguageConfig(
637
+ ts_module="tree_sitter_swift",
638
+ class_types=frozenset({"class_declaration", "protocol_declaration"}),
639
+ function_types=frozenset({"function_declaration", "init_declaration", "deinit_declaration", "subscript_declaration"}),
640
+ import_types=frozenset({"import_declaration"}),
641
+ call_types=frozenset({"call_expression"}),
642
+ call_function_field="",
643
+ call_accessor_node_types=frozenset({"navigation_expression"}),
644
+ call_accessor_field="",
645
+ name_fallback_child_types=("simple_identifier", "type_identifier", "user_type"),
646
+ body_fallback_child_types=("class_body", "protocol_body", "function_body", "enum_class_body"),
647
+ function_boundary_types=frozenset({"function_declaration", "init_declaration", "deinit_declaration", "subscript_declaration"}),
648
+ import_handler=_import_swift,
649
+ )
650
+
651
+
652
+ # ── Generic extractor ─────────────────────────────────────────────────────────
653
+
654
+ def _extract_generic(path: Path, config: LanguageConfig) -> dict:
655
+ """Generic AST extractor driven by LanguageConfig."""
656
+ try:
657
+ mod = importlib.import_module(config.ts_module)
658
+ from tree_sitter import Language, Parser
659
+ lang_fn = getattr(mod, config.ts_language_fn, None)
660
+ if lang_fn is None:
661
+ # Fallback for PHP: try "language_php" then "language"
662
+ lang_fn = getattr(mod, "language", None)
663
+ if lang_fn is None:
664
+ return {"nodes": [], "edges": [], "error": f"No language function in {config.ts_module}"}
665
+ language = Language(lang_fn())
666
+ except ImportError:
667
+ return {"nodes": [], "edges": [], "error": f"{config.ts_module} not installed"}
668
+ except Exception as e:
669
+ return {"nodes": [], "edges": [], "error": str(e)}
670
+
671
+ try:
672
+ parser = Parser(language)
673
+ source = path.read_bytes()
674
+ tree = parser.parse(source)
675
+ root = tree.root_node
676
+ except Exception as e:
677
+ return {"nodes": [], "edges": [], "error": str(e)}
678
+
679
+ stem = path.stem
680
+ str_path = str(path)
681
+ nodes: list[dict] = []
682
+ edges: list[dict] = []
683
+ seen_ids: set[str] = set()
684
+ function_bodies: list[tuple[str, object]] = []
685
+ pending_listen_edges: list[tuple[str, str, int]] = []
686
+
687
+ def add_node(nid: str, label: str, line: int) -> None:
688
+ if nid not in seen_ids:
689
+ seen_ids.add(nid)
690
+ nodes.append({
691
+ "id": nid,
692
+ "label": label,
693
+ "file_type": "code",
694
+ "source_file": str_path,
695
+ "source_location": f"L{line}",
696
+ })
697
+
698
+ def add_edge(src: str, tgt: str, relation: str, line: int,
699
+ confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
700
+ edges.append({
701
+ "source": src,
702
+ "target": tgt,
703
+ "relation": relation,
704
+ "confidence": confidence,
705
+ "source_file": str_path,
706
+ "source_location": f"L{line}",
707
+ "weight": weight,
708
+ })
709
+
710
+ file_nid = _make_id(str(path))
711
+ add_node(file_nid, path.name, 1)
712
+
713
+ def walk(node, parent_class_nid: str | None = None) -> None:
714
+ t = node.type
715
+
716
+ # Import types
717
+ if t in config.import_types:
718
+ if config.import_handler:
719
+ config.import_handler(node, source, file_nid, stem, edges, str_path)
720
+ return
721
+
722
+ # Class types
723
+ if t in config.class_types:
724
+ # Resolve class name
725
+ name_node = node.child_by_field_name(config.name_field)
726
+ if name_node is None:
727
+ for child in node.children:
728
+ if child.type in config.name_fallback_child_types:
729
+ name_node = child
730
+ break
731
+ if not name_node:
732
+ return
733
+ class_name = _read_text(name_node, source)
734
+ class_nid = _make_id(stem, class_name)
735
+ line = node.start_point[0] + 1
736
+ add_node(class_nid, class_name, line)
737
+ add_edge(file_nid, class_nid, "contains", line)
738
+
739
+ # Python-specific: inheritance
740
+ if config.ts_module == "tree_sitter_python":
741
+ args = node.child_by_field_name("superclasses")
742
+ if args:
743
+ for arg in args.children:
744
+ if arg.type == "identifier":
745
+ base = _read_text(arg, source)
746
+ base_nid = _make_id(stem, base)
747
+ if base_nid not in seen_ids:
748
+ base_nid = _make_id(base)
749
+ if base_nid not in seen_ids:
750
+ nodes.append({
751
+ "id": base_nid,
752
+ "label": base,
753
+ "file_type": "code",
754
+ "source_file": "",
755
+ "source_location": "",
756
+ })
757
+ seen_ids.add(base_nid)
758
+ add_edge(class_nid, base_nid, "inherits", line)
759
+
760
+ # Swift-specific: conformance / inheritance
761
+ if config.ts_module == "tree_sitter_swift":
762
+ for child in node.children:
763
+ if child.type == "inheritance_specifier":
764
+ for sub in child.children:
765
+ if sub.type in ("user_type", "type_identifier"):
766
+ base = _read_text(sub, source)
767
+ base_nid = _make_id(stem, base)
768
+ if base_nid not in seen_ids:
769
+ base_nid = _make_id(base)
770
+ if base_nid not in seen_ids:
771
+ nodes.append({
772
+ "id": base_nid,
773
+ "label": base,
774
+ "file_type": "code",
775
+ "source_file": "",
776
+ "source_location": "",
777
+ })
778
+ seen_ids.add(base_nid)
779
+ add_edge(class_nid, base_nid, "inherits", line)
780
+
781
+ # C#-specific: inheritance / interface implementation via base_list
782
+ if config.ts_module == "tree_sitter_c_sharp":
783
+ for child in node.children:
784
+ if child.type == "base_list":
785
+ for sub in child.children:
786
+ if sub.type in ("identifier", "generic_name"):
787
+ if sub.type == "generic_name":
788
+ name_child = sub.child_by_field_name("name")
789
+ base = _read_text(name_child, source) if name_child else _read_text(sub.children[0], source)
790
+ else:
791
+ base = _read_text(sub, source)
792
+ base_nid = _make_id(stem, base)
793
+ if base_nid not in seen_ids:
794
+ base_nid = _make_id(base)
795
+ if base_nid not in seen_ids:
796
+ nodes.append({
797
+ "id": base_nid,
798
+ "label": base,
799
+ "file_type": "code",
800
+ "source_file": "",
801
+ "source_location": "",
802
+ })
803
+ seen_ids.add(base_nid)
804
+ add_edge(class_nid, base_nid, "inherits", line)
805
+
806
+ # Find body and recurse
807
+ body = _find_body(node, config)
808
+ if body:
809
+ for child in body.children:
810
+ walk(child, parent_class_nid=class_nid)
811
+ return
812
+
813
+ # Event listener property arrays: $listen = [Event::class => [Listener::class]]
814
+ if (t == "property_declaration"
815
+ and parent_class_nid
816
+ and config.event_listener_properties):
817
+ for element in node.children:
818
+ if element.type != "property_element":
819
+ continue
820
+ prop_name: str | None = None
821
+ array_node = None
822
+ for c in element.children:
823
+ if c.type == "variable_name":
824
+ for sc in c.children:
825
+ if sc.type == "name":
826
+ prop_name = _read_text(sc, source)
827
+ break
828
+ elif c.type == "array_creation_expression":
829
+ array_node = c
830
+ if (prop_name is None
831
+ or prop_name not in config.event_listener_properties
832
+ or array_node is None):
833
+ continue
834
+ for entry in array_node.children:
835
+ if entry.type != "array_element_initializer":
836
+ continue
837
+ event_cls: str | None = None
838
+ listener_arr = None
839
+ for sub in entry.children:
840
+ if sub.type == "class_constant_access_expression" and event_cls is None:
841
+ for sc in sub.children:
842
+ if sc.is_named and sc.type in ("name", "qualified_name"):
843
+ event_cls = _read_text(sc, source)
844
+ break
845
+ elif sub.type == "array_creation_expression":
846
+ listener_arr = sub
847
+ if not event_cls or listener_arr is None:
848
+ continue
849
+ for listener_entry in listener_arr.children:
850
+ if listener_entry.type != "array_element_initializer":
851
+ continue
852
+ for item in listener_entry.children:
853
+ if item.type != "class_constant_access_expression":
854
+ continue
855
+ for sc in item.children:
856
+ if sc.is_named and sc.type in ("name", "qualified_name"):
857
+ listener_cls = _read_text(sc, source)
858
+ line_no = item.start_point[0] + 1
859
+ pending_listen_edges.append((event_cls, listener_cls, line_no))
860
+ break
861
+ break
862
+ return
863
+
864
+ # Function types
865
+ if t in config.function_types:
866
+ # Swift deinit/subscript have no name field — resolve before generic fallback
867
+ if t == "deinit_declaration":
868
+ func_name: str | None = "deinit"
869
+ elif t == "subscript_declaration":
870
+ func_name = "subscript"
871
+ elif config.resolve_function_name_fn is not None:
872
+ # C/C++ style: use declarator
873
+ declarator = node.child_by_field_name("declarator")
874
+ func_name = None
875
+ if declarator:
876
+ func_name = config.resolve_function_name_fn(declarator, source)
877
+ else:
878
+ name_node = node.child_by_field_name(config.name_field)
879
+ if name_node is None:
880
+ for child in node.children:
881
+ if child.type in config.name_fallback_child_types:
882
+ name_node = child
883
+ break
884
+ func_name = _read_text(name_node, source) if name_node else None
885
+
886
+ if not func_name:
887
+ return
888
+
889
+ line = node.start_point[0] + 1
890
+ if parent_class_nid:
891
+ func_nid = _make_id(parent_class_nid, func_name)
892
+ add_node(func_nid, f".{func_name}()", line)
893
+ add_edge(parent_class_nid, func_nid, "method", line)
894
+ else:
895
+ func_nid = _make_id(stem, func_name)
896
+ add_node(func_nid, f"{func_name}()", line)
897
+ add_edge(file_nid, func_nid, "contains", line)
898
+
899
+ body = _find_body(node, config)
900
+ if body:
901
+ function_bodies.append((func_nid, body))
902
+ return
903
+
904
+ # JS/TS arrow functions and C# namespaces — language-specific extra handling
905
+ if config.ts_module in ("tree_sitter_javascript", "tree_sitter_typescript"):
906
+ if _js_extra_walk(node, source, file_nid, stem, str_path,
907
+ nodes, edges, seen_ids, function_bodies,
908
+ parent_class_nid, add_node, add_edge):
909
+ return
910
+
911
+ if config.ts_module == "tree_sitter_c_sharp":
912
+ if _csharp_extra_walk(node, source, file_nid, stem, str_path,
913
+ nodes, edges, seen_ids, function_bodies,
914
+ parent_class_nid, add_node, add_edge, walk):
915
+ return
916
+
917
+ if config.ts_module == "tree_sitter_swift":
918
+ if _swift_extra_walk(node, source, file_nid, stem, str_path,
919
+ nodes, edges, seen_ids, function_bodies,
920
+ parent_class_nid, add_node, add_edge):
921
+ return
922
+
923
+ # Default: recurse
924
+ for child in node.children:
925
+ walk(child, parent_class_nid=None)
926
+
927
+ walk(root)
928
+
929
+ # ── Call-graph pass ───────────────────────────────────────────────────────
930
+ label_to_nid: dict[str, str] = {}
931
+ for n in nodes:
932
+ raw = n["label"]
933
+ normalised = raw.strip("()").lstrip(".")
934
+ label_to_nid[normalised.lower()] = n["id"]
935
+
936
+ seen_call_pairs: set[tuple[str, str]] = set()
937
+ seen_static_ref_pairs: set[tuple[str, str, str]] = set()
938
+ seen_helper_ref_pairs: set[tuple[str, str, str]] = set()
939
+ seen_bind_pairs: set[tuple[str, str, str]] = set()
940
+ raw_calls: list[dict] = [] # unresolved calls for cross-file resolution in extract()
941
+
942
+ def _php_class_const_scope(n) -> str | None:
943
+ scope = n.child_by_field_name("scope")
944
+ if scope is None:
945
+ for c in n.children:
946
+ if c.is_named and c.type in ("name", "qualified_name", "identifier"):
947
+ scope = c
948
+ break
949
+ if scope is None:
950
+ return None
951
+ return _read_text(scope, source)
952
+
953
+ def walk_calls(node, caller_nid: str) -> None:
954
+ if node.type in config.function_boundary_types:
955
+ return
956
+
957
+ if node.type in config.call_types:
958
+ callee_name: str | None = None
959
+
960
+ # Special handling per language
961
+ if config.ts_module == "tree_sitter_swift":
962
+ # Swift: first child may be simple_identifier or navigation_expression
963
+ first = node.children[0] if node.children else None
964
+ if first:
965
+ if first.type == "simple_identifier":
966
+ callee_name = _read_text(first, source)
967
+ elif first.type == "navigation_expression":
968
+ for child in first.children:
969
+ if child.type == "navigation_suffix":
970
+ for sc in child.children:
971
+ if sc.type == "simple_identifier":
972
+ callee_name = _read_text(sc, source)
973
+ elif config.ts_module == "tree_sitter_kotlin":
974
+ # Kotlin: first child may be simple_identifier or navigation_expression
975
+ first = node.children[0] if node.children else None
976
+ if first:
977
+ if first.type == "simple_identifier":
978
+ callee_name = _read_text(first, source)
979
+ elif first.type == "navigation_expression":
980
+ for child in reversed(first.children):
981
+ if child.type == "simple_identifier":
982
+ callee_name = _read_text(child, source)
983
+ break
984
+ elif config.ts_module == "tree_sitter_scala":
985
+ # Scala: first child
986
+ first = node.children[0] if node.children else None
987
+ if first:
988
+ if first.type == "identifier":
989
+ callee_name = _read_text(first, source)
990
+ elif first.type == "field_expression":
991
+ field = first.child_by_field_name("field")
992
+ if field:
993
+ callee_name = _read_text(field, source)
994
+ else:
995
+ for child in reversed(first.children):
996
+ if child.type == "identifier":
997
+ callee_name = _read_text(child, source)
998
+ break
999
+ elif config.ts_module == "tree_sitter_c_sharp" and node.type == "invocation_expression":
1000
+ # C#: try name field, then first named child
1001
+ name_node = node.child_by_field_name("name")
1002
+ if name_node:
1003
+ callee_name = _read_text(name_node, source)
1004
+ else:
1005
+ for child in node.children:
1006
+ if child.is_named:
1007
+ raw = _read_text(child, source)
1008
+ if "." in raw:
1009
+ callee_name = raw.split(".")[-1]
1010
+ else:
1011
+ callee_name = raw
1012
+ break
1013
+ elif config.ts_module == "tree_sitter_php":
1014
+ # PHP: distinguish call expression subtypes
1015
+ if node.type == "function_call_expression":
1016
+ func_node = node.child_by_field_name("function")
1017
+ if func_node:
1018
+ callee_name = _read_text(func_node, source)
1019
+ elif node.type == "scoped_call_expression":
1020
+ # Static method call: Helper::format() → callee = "Helper"
1021
+ scope_node = node.child_by_field_name("scope")
1022
+ if scope_node:
1023
+ callee_name = _read_text(scope_node, source)
1024
+ else:
1025
+ name_node = node.child_by_field_name("name")
1026
+ if name_node:
1027
+ callee_name = _read_text(name_node, source)
1028
+ elif config.ts_module == "tree_sitter_cpp":
1029
+ # C++: function field, then field_expression/qualified_identifier
1030
+ func_node = node.child_by_field_name(config.call_function_field) if config.call_function_field else None
1031
+ if func_node:
1032
+ if func_node.type == "identifier":
1033
+ callee_name = _read_text(func_node, source)
1034
+ elif func_node.type in ("field_expression", "qualified_identifier"):
1035
+ name = func_node.child_by_field_name("field") or func_node.child_by_field_name("name")
1036
+ if name:
1037
+ callee_name = _read_text(name, source)
1038
+ else:
1039
+ # Generic: get callee from call_function_field
1040
+ func_node = node.child_by_field_name(config.call_function_field) if config.call_function_field else None
1041
+ if func_node:
1042
+ if func_node.type == "identifier":
1043
+ callee_name = _read_text(func_node, source)
1044
+ elif func_node.type in config.call_accessor_node_types:
1045
+ if config.call_accessor_field:
1046
+ attr = func_node.child_by_field_name(config.call_accessor_field)
1047
+ if attr:
1048
+ callee_name = _read_text(attr, source)
1049
+ else:
1050
+ # Try reading the node directly (e.g. Java name field is the callee)
1051
+ callee_name = _read_text(func_node, source)
1052
+
1053
+ if callee_name:
1054
+ tgt_nid = label_to_nid.get(callee_name.lower())
1055
+ if tgt_nid and tgt_nid != caller_nid:
1056
+ pair = (caller_nid, tgt_nid)
1057
+ if pair not in seen_call_pairs:
1058
+ seen_call_pairs.add(pair)
1059
+ line = node.start_point[0] + 1
1060
+ edges.append({
1061
+ "source": caller_nid,
1062
+ "target": tgt_nid,
1063
+ "relation": "calls",
1064
+ "confidence": "EXTRACTED",
1065
+ "source_file": str_path,
1066
+ "source_location": f"L{line}",
1067
+ "weight": 1.0,
1068
+ })
1069
+ elif callee_name and not tgt_nid:
1070
+ # Callee not in this file — save for cross-file resolution in extract()
1071
+ raw_calls.append({
1072
+ "caller_nid": caller_nid,
1073
+ "callee": callee_name,
1074
+ "source_file": str_path,
1075
+ "source_location": f"L{node.start_point[0] + 1}",
1076
+ })
1077
+
1078
+ # Helper function calls: config('foo.bar') → uses_config edge to "foo"
1079
+ if (callee_name and callee_name in config.helper_fn_names):
1080
+ args_node = node.child_by_field_name("arguments")
1081
+ first_key: str | None = None
1082
+ if args_node:
1083
+ for arg in args_node.children:
1084
+ if arg.type != "argument":
1085
+ continue
1086
+ for inner in arg.children:
1087
+ if inner.type == "string":
1088
+ for sc in inner.children:
1089
+ if sc.type == "string_content":
1090
+ first_key = _read_text(sc, source)
1091
+ break
1092
+ break
1093
+ if first_key:
1094
+ break
1095
+ if first_key:
1096
+ segment = first_key.split(".")[0]
1097
+ tgt_nid = (label_to_nid.get(segment.lower())
1098
+ or label_to_nid.get(f"{segment}.php".lower()))
1099
+ if tgt_nid and tgt_nid != caller_nid:
1100
+ relation = f"uses_{callee_name}"
1101
+ pair3 = (caller_nid, tgt_nid, relation)
1102
+ if pair3 not in seen_helper_ref_pairs:
1103
+ seen_helper_ref_pairs.add(pair3)
1104
+ line = node.start_point[0] + 1
1105
+ edges.append({
1106
+ "source": caller_nid,
1107
+ "target": tgt_nid,
1108
+ "relation": relation,
1109
+ "confidence": "EXTRACTED",
1110
+ "confidence_score": 1.0,
1111
+ "source_file": str_path,
1112
+ "source_location": f"L{line}",
1113
+ "weight": 1.0,
1114
+ })
1115
+
1116
+ # Service container bindings: $this->app->bind(Foo::class, Bar::class)
1117
+ if (node.type == "member_call_expression"
1118
+ and callee_name
1119
+ and callee_name in config.container_bind_methods):
1120
+ args_node = node.child_by_field_name("arguments")
1121
+ class_args: list[str] = []
1122
+ if args_node:
1123
+ for arg in args_node.children:
1124
+ if arg.type != "argument":
1125
+ continue
1126
+ for inner in arg.children:
1127
+ if inner.type == "class_constant_access_expression":
1128
+ cls = _php_class_const_scope(inner)
1129
+ if cls:
1130
+ class_args.append(cls)
1131
+ break
1132
+ if len(class_args) >= 2:
1133
+ break
1134
+ if len(class_args) == 2:
1135
+ contract_name, impl_name = class_args
1136
+ contract_nid = label_to_nid.get(contract_name.lower())
1137
+ impl_nid = label_to_nid.get(impl_name.lower())
1138
+ if contract_nid and impl_nid and contract_nid != impl_nid:
1139
+ pair3 = (contract_nid, impl_nid, "bound_to")
1140
+ if pair3 not in seen_bind_pairs:
1141
+ seen_bind_pairs.add(pair3)
1142
+ line = node.start_point[0] + 1
1143
+ edges.append({
1144
+ "source": contract_nid,
1145
+ "target": impl_nid,
1146
+ "relation": "bound_to",
1147
+ "confidence": "EXTRACTED",
1148
+ "confidence_score": 1.0,
1149
+ "source_file": str_path,
1150
+ "source_location": f"L{line}",
1151
+ "weight": 1.0,
1152
+ })
1153
+
1154
+ # Static property access: Foo::$bar → uses_static_prop edge
1155
+ if node.type in config.static_prop_types:
1156
+ scope_node = node.child_by_field_name("scope")
1157
+ if scope_node is None:
1158
+ for child in node.children:
1159
+ if child.is_named and child.type in ("name", "qualified_name", "identifier"):
1160
+ scope_node = child
1161
+ break
1162
+ if scope_node is not None:
1163
+ class_name = _read_text(scope_node, source)
1164
+ tgt_nid = label_to_nid.get(class_name.lower())
1165
+ if tgt_nid and tgt_nid != caller_nid:
1166
+ pair3 = (caller_nid, tgt_nid, "uses_static_prop")
1167
+ if pair3 not in seen_static_ref_pairs:
1168
+ seen_static_ref_pairs.add(pair3)
1169
+ line = node.start_point[0] + 1
1170
+ edges.append({
1171
+ "source": caller_nid,
1172
+ "target": tgt_nid,
1173
+ "relation": "uses_static_prop",
1174
+ "confidence": "EXTRACTED",
1175
+ "confidence_score": 1.0,
1176
+ "source_file": str_path,
1177
+ "source_location": f"L{line}",
1178
+ "weight": 1.0,
1179
+ })
1180
+
1181
+ # PHP class constant access: Foo::BAR → references_constant edge
1182
+ if config.ts_module == "tree_sitter_php" and node.type == "class_constant_access_expression":
1183
+ class_name = _php_class_const_scope(node)
1184
+ if class_name:
1185
+ tgt_nid = label_to_nid.get(class_name.lower())
1186
+ if tgt_nid and tgt_nid != caller_nid:
1187
+ pair3 = (caller_nid, tgt_nid, "references_constant")
1188
+ if pair3 not in seen_static_ref_pairs:
1189
+ seen_static_ref_pairs.add(pair3)
1190
+ line = node.start_point[0] + 1
1191
+ edges.append({
1192
+ "source": caller_nid,
1193
+ "target": tgt_nid,
1194
+ "relation": "references_constant",
1195
+ "confidence": "EXTRACTED",
1196
+ "confidence_score": 1.0,
1197
+ "source_file": str_path,
1198
+ "source_location": f"L{line}",
1199
+ "weight": 1.0,
1200
+ })
1201
+
1202
+ for child in node.children:
1203
+ walk_calls(child, caller_nid)
1204
+
1205
+ for caller_nid, body_node in function_bodies:
1206
+ walk_calls(body_node, caller_nid)
1207
+
1208
+ # ── Event listener pass ───────────────────────────────────────────────────
1209
+ seen_listen_pairs: set[tuple[str, str]] = set()
1210
+ for event_name, listener_name, line in pending_listen_edges:
1211
+ event_nid = label_to_nid.get(event_name.lower())
1212
+ listener_nid = label_to_nid.get(listener_name.lower())
1213
+ if not event_nid or not listener_nid or event_nid == listener_nid:
1214
+ continue
1215
+ pair2 = (event_nid, listener_nid)
1216
+ if pair2 in seen_listen_pairs:
1217
+ continue
1218
+ seen_listen_pairs.add(pair2)
1219
+ edges.append({
1220
+ "source": event_nid,
1221
+ "target": listener_nid,
1222
+ "relation": "listened_by",
1223
+ "confidence": "EXTRACTED",
1224
+ "confidence_score": 1.0,
1225
+ "source_file": str_path,
1226
+ "source_location": f"L{line}",
1227
+ "weight": 1.0,
1228
+ })
1229
+
1230
+ # ── Clean edges ───────────────────────────────────────────────────────────
1231
+ valid_ids = seen_ids
1232
+ clean_edges = []
1233
+ for edge in edges:
1234
+ src, tgt = edge["source"], edge["target"]
1235
+ if src in valid_ids and (tgt in valid_ids or edge["relation"] in ("imports", "imports_from")):
1236
+ clean_edges.append(edge)
1237
+
1238
+ return {"nodes": nodes, "edges": clean_edges, "raw_calls": raw_calls}
1239
+
1240
+
1241
+ # ── Python rationale extraction ───────────────────────────────────────────────
1242
+
1243
+ _RATIONALE_PREFIXES = ("# NOTE:", "# IMPORTANT:", "# HACK:", "# WHY:", "# RATIONALE:", "# TODO:", "# FIXME:")
1244
+
1245
+
1246
+ def _extract_python_rationale(path: Path, result: dict) -> None:
1247
+ """Post-pass: extract docstrings and rationale comments from Python source.
1248
+ Mutates result in-place by appending to result['nodes'] and result['edges'].
1249
+ """
1250
+ try:
1251
+ import tree_sitter_python as tspython
1252
+ from tree_sitter import Language, Parser
1253
+ language = Language(tspython.language())
1254
+ parser = Parser(language)
1255
+ source = path.read_bytes()
1256
+ tree = parser.parse(source)
1257
+ root = tree.root_node
1258
+ except Exception:
1259
+ return
1260
+
1261
+ stem = path.stem
1262
+ str_path = str(path)
1263
+ nodes = result["nodes"]
1264
+ edges = result["edges"]
1265
+ seen_ids = {n["id"] for n in nodes}
1266
+ file_nid = _make_id(str(path))
1267
+
1268
+ def _get_docstring(body_node) -> tuple[str, int] | None:
1269
+ if not body_node:
1270
+ return None
1271
+ for child in body_node.children:
1272
+ if child.type == "expression_statement":
1273
+ for sub in child.children:
1274
+ if sub.type in ("string", "concatenated_string"):
1275
+ text = source[sub.start_byte:sub.end_byte].decode("utf-8", errors="replace")
1276
+ text = text.strip("\"'").strip('"""').strip("'''").strip()
1277
+ if len(text) > 20:
1278
+ return text, child.start_point[0] + 1
1279
+ break
1280
+ return None
1281
+
1282
+ def _add_rationale(text: str, line: int, parent_nid: str) -> None:
1283
+ label = text[:80].replace("\r\n", " ").replace("\r", " ").replace("\n", " ").strip()
1284
+ rid = _make_id(stem, "rationale", str(line))
1285
+ if rid not in seen_ids:
1286
+ seen_ids.add(rid)
1287
+ nodes.append({
1288
+ "id": rid,
1289
+ "label": label,
1290
+ "file_type": "rationale",
1291
+ "source_file": str_path,
1292
+ "source_location": f"L{line}",
1293
+ })
1294
+ edges.append({
1295
+ "source": rid,
1296
+ "target": parent_nid,
1297
+ "relation": "rationale_for",
1298
+ "confidence": "EXTRACTED",
1299
+ "source_file": str_path,
1300
+ "source_location": f"L{line}",
1301
+ "weight": 1.0,
1302
+ })
1303
+
1304
+ # Module-level docstring
1305
+ ds = _get_docstring(root)
1306
+ if ds:
1307
+ _add_rationale(ds[0], ds[1], file_nid)
1308
+
1309
+ # Class and function docstrings
1310
+ def walk_docstrings(node, parent_nid: str) -> None:
1311
+ t = node.type
1312
+ if t == "class_definition":
1313
+ name_node = node.child_by_field_name("name")
1314
+ body = node.child_by_field_name("body")
1315
+ if name_node and body:
1316
+ class_name = source[name_node.start_byte:name_node.end_byte].decode("utf-8", errors="replace")
1317
+ nid = _make_id(stem, class_name)
1318
+ ds = _get_docstring(body)
1319
+ if ds:
1320
+ _add_rationale(ds[0], ds[1], nid)
1321
+ for child in body.children:
1322
+ walk_docstrings(child, nid)
1323
+ return
1324
+ if t == "function_definition":
1325
+ name_node = node.child_by_field_name("name")
1326
+ body = node.child_by_field_name("body")
1327
+ if name_node and body:
1328
+ func_name = source[name_node.start_byte:name_node.end_byte].decode("utf-8", errors="replace")
1329
+ nid = _make_id(parent_nid, func_name) if parent_nid != file_nid else _make_id(stem, func_name)
1330
+ ds = _get_docstring(body)
1331
+ if ds:
1332
+ _add_rationale(ds[0], ds[1], nid)
1333
+ return
1334
+ for child in node.children:
1335
+ walk_docstrings(child, parent_nid)
1336
+
1337
+ walk_docstrings(root, file_nid)
1338
+
1339
+ # Rationale comments (# NOTE:, # IMPORTANT:, etc.)
1340
+ source_text = source.decode("utf-8", errors="replace")
1341
+ for lineno, line_text in enumerate(source_text.splitlines(), start=1):
1342
+ stripped = line_text.strip()
1343
+ if any(stripped.startswith(p) for p in _RATIONALE_PREFIXES):
1344
+ _add_rationale(stripped, lineno, file_nid)
1345
+
1346
+
1347
+ # ── Public API ────────────────────────────────────────────────────────────────
1348
+
1349
+ def extract_python(path: Path) -> dict:
1350
+ """Extract classes, functions, and imports from a .py file via tree-sitter AST."""
1351
+ result = _extract_generic(path, _PYTHON_CONFIG)
1352
+ if "error" not in result:
1353
+ _extract_python_rationale(path, result)
1354
+ return result
1355
+
1356
+
1357
+ def extract_js(path: Path) -> dict:
1358
+ """Extract classes, functions, arrow functions, and imports from a .js/.ts/.tsx file."""
1359
+ config = _TS_CONFIG if path.suffix in (".ts", ".tsx") else _JS_CONFIG
1360
+ return _extract_generic(path, config)
1361
+
1362
+
1363
+ def extract_java(path: Path) -> dict:
1364
+ """Extract classes, interfaces, methods, constructors, and imports from a .java file."""
1365
+ return _extract_generic(path, _JAVA_CONFIG)
1366
+
1367
+
1368
+ def extract_c(path: Path) -> dict:
1369
+ """Extract functions and includes from a .c/.h file."""
1370
+ return _extract_generic(path, _C_CONFIG)
1371
+
1372
+
1373
+ def extract_cpp(path: Path) -> dict:
1374
+ """Extract functions, classes, and includes from a .cpp/.cc/.cxx/.hpp file."""
1375
+ return _extract_generic(path, _CPP_CONFIG)
1376
+
1377
+
1378
+ def extract_ruby(path: Path) -> dict:
1379
+ """Extract classes, methods, singleton methods, and calls from a .rb file."""
1380
+ return _extract_generic(path, _RUBY_CONFIG)
1381
+
1382
+
1383
+ def extract_csharp(path: Path) -> dict:
1384
+ """Extract classes, interfaces, methods, namespaces, and usings from a .cs file."""
1385
+ return _extract_generic(path, _CSHARP_CONFIG)
1386
+
1387
+
1388
+ def extract_kotlin(path: Path) -> dict:
1389
+ """Extract classes, objects, functions, and imports from a .kt/.kts file."""
1390
+ return _extract_generic(path, _KOTLIN_CONFIG)
1391
+
1392
+
1393
+ def extract_scala(path: Path) -> dict:
1394
+ """Extract classes, objects, functions, and imports from a .scala file."""
1395
+ return _extract_generic(path, _SCALA_CONFIG)
1396
+
1397
+
1398
+ def extract_php(path: Path) -> dict:
1399
+ """Extract classes, functions, methods, namespace uses, and calls from a .php file."""
1400
+ return _extract_generic(path, _PHP_CONFIG)
1401
+
1402
+
1403
+ def extract_blade(path: Path) -> dict:
1404
+ """Extract @include, <livewire:> components, and wire:click bindings from Blade templates."""
1405
+ import re
1406
+ try:
1407
+ src = path.read_text(encoding="utf-8", errors="replace")
1408
+ except OSError:
1409
+ return {"error": f"cannot read {path}"}
1410
+
1411
+ file_nid = _make_id(str(path))
1412
+ nodes = [{"id": file_nid, "label": path.name, "file_type": "code",
1413
+ "source_file": str(path), "source_location": None}]
1414
+ edges = []
1415
+
1416
+ # @include('path.to.partial') or @include("path.to.partial")
1417
+ for m in re.finditer(r"@include\(['\"]([^'\"]+)['\"]", src):
1418
+ tgt = m.group(1).replace(".", "/")
1419
+ tgt_nid = _make_id(tgt)
1420
+ if tgt_nid not in {n["id"] for n in nodes}:
1421
+ nodes.append({"id": tgt_nid, "label": m.group(1), "file_type": "code",
1422
+ "source_file": str(path), "source_location": None})
1423
+ edges.append({"source": file_nid, "target": tgt_nid, "relation": "includes",
1424
+ "confidence": "EXTRACTED", "confidence_score": 1.0,
1425
+ "source_file": str(path), "source_location": None, "weight": 1.0})
1426
+
1427
+ # <livewire:component.name /> or <livewire:component.name>
1428
+ for m in re.finditer(r"<livewire:([\w.\-]+)", src):
1429
+ tgt_nid = _make_id(m.group(1))
1430
+ if tgt_nid not in {n["id"] for n in nodes}:
1431
+ nodes.append({"id": tgt_nid, "label": m.group(1), "file_type": "code",
1432
+ "source_file": str(path), "source_location": None})
1433
+ edges.append({"source": file_nid, "target": tgt_nid, "relation": "uses_component",
1434
+ "confidence": "EXTRACTED", "confidence_score": 1.0,
1435
+ "source_file": str(path), "source_location": None, "weight": 1.0})
1436
+
1437
+ # wire:click="methodName"
1438
+ for m in re.finditer(r'wire:click=["\']([^"\']+)["\']', src):
1439
+ tgt_nid = _make_id(m.group(1))
1440
+ if tgt_nid not in {n["id"] for n in nodes}:
1441
+ nodes.append({"id": tgt_nid, "label": m.group(1), "file_type": "code",
1442
+ "source_file": str(path), "source_location": None})
1443
+ edges.append({"source": file_nid, "target": tgt_nid, "relation": "binds_method",
1444
+ "confidence": "EXTRACTED", "confidence_score": 1.0,
1445
+ "source_file": str(path), "source_location": None, "weight": 1.0})
1446
+
1447
+ return {"nodes": nodes, "edges": edges}
1448
+
1449
+
1450
+ def extract_dart(path: Path) -> dict:
1451
+ """Extract classes, mixins, functions, imports, and calls from a .dart file using regex."""
1452
+ try:
1453
+ src = path.read_text(encoding="utf-8", errors="replace")
1454
+ except OSError:
1455
+ return {"error": f"cannot read {path}"}
1456
+
1457
+ file_nid = _make_id(str(path))
1458
+ nodes = [{"id": file_nid, "label": path.name, "file_type": "code",
1459
+ "source_file": str(path), "source_location": None}]
1460
+ edges = []
1461
+ defined: set[str] = set()
1462
+
1463
+ # Classes and mixins
1464
+ for m in re.finditer(r"^\s*(?:abstract\s+)?(?:class|mixin)\s+(\w+)", src, re.MULTILINE):
1465
+ nid = _make_id(str(path), m.group(1))
1466
+ if nid not in defined:
1467
+ nodes.append({"id": nid, "label": m.group(1), "file_type": "code",
1468
+ "source_file": str(path), "source_location": None})
1469
+ edges.append({"source": file_nid, "target": nid, "relation": "defines",
1470
+ "confidence": "EXTRACTED", "confidence_score": 1.0,
1471
+ "source_file": str(path), "source_location": None, "weight": 1.0})
1472
+ defined.add(nid)
1473
+
1474
+ # Top-level and member functions/methods
1475
+ for m in re.finditer(r"^\s*(?:static\s+|async\s+)?(?:\w+\s+)+(\w+)\s*\(", src, re.MULTILINE):
1476
+ name = m.group(1)
1477
+ if name in {"if", "for", "while", "switch", "catch", "return"}:
1478
+ continue
1479
+ nid = _make_id(str(path), name)
1480
+ if nid not in defined:
1481
+ nodes.append({"id": nid, "label": name, "file_type": "code",
1482
+ "source_file": str(path), "source_location": None})
1483
+ edges.append({"source": file_nid, "target": nid, "relation": "defines",
1484
+ "confidence": "EXTRACTED", "confidence_score": 1.0,
1485
+ "source_file": str(path), "source_location": None, "weight": 1.0})
1486
+ defined.add(nid)
1487
+
1488
+ # import 'package:...' or import '...'
1489
+ for m in re.finditer(r"""^import\s+['"]([^'"]+)['"]""", src, re.MULTILINE):
1490
+ pkg = m.group(1)
1491
+ tgt_nid = _make_id(pkg)
1492
+ if tgt_nid not in defined:
1493
+ nodes.append({"id": tgt_nid, "label": pkg, "file_type": "code",
1494
+ "source_file": str(path), "source_location": None})
1495
+ defined.add(tgt_nid)
1496
+ edges.append({"source": file_nid, "target": tgt_nid, "relation": "imports",
1497
+ "confidence": "EXTRACTED", "confidence_score": 1.0,
1498
+ "source_file": str(path), "source_location": None, "weight": 1.0})
1499
+
1500
+ return {"nodes": nodes, "edges": edges}
1501
+
1502
+
1503
+ def extract_verilog(path: Path) -> dict:
1504
+ """Extract modules, functions, tasks, package imports, and instantiations from .v/.sv files."""
1505
+ try:
1506
+ import tree_sitter_verilog as tsverilog
1507
+ from tree_sitter import Language, Parser
1508
+ except ImportError:
1509
+ return {"nodes": [], "edges": [], "error": "tree_sitter_verilog not installed"}
1510
+
1511
+ try:
1512
+ language = Language(tsverilog.language())
1513
+ parser = Parser(language)
1514
+ source = path.read_bytes()
1515
+ tree = parser.parse(source)
1516
+ root = tree.root_node
1517
+ except Exception as e:
1518
+ return {"nodes": [], "edges": [], "error": str(e)}
1519
+
1520
+ stem = path.stem
1521
+ str_path = str(path)
1522
+ nodes: list[dict] = []
1523
+ edges: list[dict] = []
1524
+ seen_ids: set[str] = set()
1525
+
1526
+ def add_node(nid: str, label: str, line: int) -> None:
1527
+ if nid not in seen_ids:
1528
+ seen_ids.add(nid)
1529
+ nodes.append({"id": nid, "label": label, "file_type": "code",
1530
+ "source_file": str_path, "source_location": f"L{line}",
1531
+ "confidence_score": 1.0})
1532
+
1533
+ def add_edge(src: str, tgt: str, relation: str, line: int,
1534
+ confidence: str = "EXTRACTED", score: float = 1.0) -> None:
1535
+ edges.append({"source": src, "target": tgt, "relation": relation,
1536
+ "confidence": confidence, "confidence_score": score,
1537
+ "source_file": str_path, "source_location": f"L{line}", "weight": 1.0})
1538
+
1539
+ file_nid = _make_id(str(path))
1540
+ add_node(file_nid, path.name, 1)
1541
+
1542
+ def walk(node, module_nid: str | None = None) -> None:
1543
+ t = node.type
1544
+
1545
+ if t == "module_declaration":
1546
+ name_node = node.child_by_field_name("name")
1547
+ if name_node:
1548
+ mod_name = _read_text(name_node, source)
1549
+ line = node.start_point[0] + 1
1550
+ nid = _make_id(stem, mod_name)
1551
+ add_node(nid, mod_name, line)
1552
+ add_edge(file_nid, nid, "defines", line)
1553
+ for child in node.children:
1554
+ walk(child, nid)
1555
+ return
1556
+
1557
+ elif t in ("function_declaration", "function_prototype"):
1558
+ name_node = node.child_by_field_name("name")
1559
+ if name_node:
1560
+ func_name = _read_text(name_node, source)
1561
+ line = node.start_point[0] + 1
1562
+ parent = module_nid or file_nid
1563
+ nid = _make_id(parent, func_name)
1564
+ add_node(nid, f"{func_name}()", line)
1565
+ add_edge(parent, nid, "contains", line)
1566
+
1567
+ elif t == "task_declaration":
1568
+ name_node = node.child_by_field_name("name")
1569
+ if name_node:
1570
+ task_name = _read_text(name_node, source)
1571
+ line = node.start_point[0] + 1
1572
+ parent = module_nid or file_nid
1573
+ nid = _make_id(parent, task_name)
1574
+ add_node(nid, task_name, line)
1575
+ add_edge(parent, nid, "contains", line)
1576
+
1577
+ elif t == "package_import_declaration":
1578
+ for child in node.children:
1579
+ if child.type == "package_import_item":
1580
+ pkg_text = _read_text(child, source)
1581
+ pkg_name = pkg_text.split("::")[0].strip()
1582
+ if pkg_name:
1583
+ line = node.start_point[0] + 1
1584
+ tgt_nid = _make_id(pkg_name)
1585
+ add_node(tgt_nid, pkg_name, line)
1586
+ src = module_nid or file_nid
1587
+ add_edge(src, tgt_nid, "imports_from", line)
1588
+
1589
+ elif t == "module_instantiation":
1590
+ # module_type instantiates another module
1591
+ type_node = node.child_by_field_name("module_type")
1592
+ if type_node and module_nid:
1593
+ inst_type = _read_text(type_node, source).strip()
1594
+ if inst_type:
1595
+ line = node.start_point[0] + 1
1596
+ tgt_nid = _make_id(inst_type)
1597
+ add_node(tgt_nid, inst_type, line)
1598
+ add_edge(module_nid, tgt_nid, "instantiates", line)
1599
+
1600
+ for child in node.children:
1601
+ walk(child, module_nid)
1602
+
1603
+ walk(root)
1604
+ return {"nodes": nodes, "edges": edges}
1605
+
1606
+
1607
+ def extract_lua(path: Path) -> dict:
1608
+ """Extract functions, methods, require() imports, and calls from a .lua file."""
1609
+ return _extract_generic(path, _LUA_CONFIG)
1610
+
1611
+
1612
+ def extract_swift(path: Path) -> dict:
1613
+ """Extract classes, structs, protocols, functions, imports, and calls from a .swift file."""
1614
+ return _extract_generic(path, _SWIFT_CONFIG)
1615
+
1616
+
1617
+ # ── Julia extractor (custom walk) ────────────────────────────────────────────
1618
+
1619
+ def extract_julia(path: Path) -> dict:
1620
+ """Extract modules, structs, functions, imports, and calls from a .jl file."""
1621
+ try:
1622
+ import tree_sitter_julia as tsjulia
1623
+ from tree_sitter import Language, Parser
1624
+ except ImportError:
1625
+ return {"nodes": [], "edges": [], "error": "tree-sitter-julia not installed"}
1626
+
1627
+ try:
1628
+ language = Language(tsjulia.language())
1629
+ parser = Parser(language)
1630
+ source = path.read_bytes()
1631
+ tree = parser.parse(source)
1632
+ root = tree.root_node
1633
+ except Exception as e:
1634
+ return {"nodes": [], "edges": [], "error": str(e)}
1635
+
1636
+ stem = path.stem
1637
+ str_path = str(path)
1638
+ nodes: list[dict] = []
1639
+ edges: list[dict] = []
1640
+ seen_ids: set[str] = set()
1641
+ function_bodies: list[tuple[str, object]] = []
1642
+
1643
+ def add_node(nid: str, label: str, line: int) -> None:
1644
+ if nid not in seen_ids:
1645
+ seen_ids.add(nid)
1646
+ nodes.append({
1647
+ "id": nid,
1648
+ "label": label,
1649
+ "file_type": "code",
1650
+ "source_file": str_path,
1651
+ "source_location": f"L{line}",
1652
+ })
1653
+
1654
+ def add_edge(src: str, tgt: str, relation: str, line: int,
1655
+ confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
1656
+ edges.append({
1657
+ "source": src,
1658
+ "target": tgt,
1659
+ "relation": relation,
1660
+ "confidence": confidence,
1661
+ "source_file": str_path,
1662
+ "source_location": f"L{line}",
1663
+ "weight": weight,
1664
+ })
1665
+
1666
+ file_nid = _make_id(str(path))
1667
+ add_node(file_nid, path.name, 1)
1668
+
1669
+ def _func_name_from_signature(sig_node) -> str | None:
1670
+ """Extract function name from a Julia signature node (call_expression > identifier)."""
1671
+ for child in sig_node.children:
1672
+ if child.type == "call_expression":
1673
+ callee = child.children[0] if child.children else None
1674
+ if callee and callee.type == "identifier":
1675
+ return _read_text(callee, source)
1676
+ return None
1677
+
1678
+ def walk_calls(body_node, func_nid: str) -> None:
1679
+ if body_node is None:
1680
+ return
1681
+ t = body_node.type
1682
+ if t in ("function_definition", "short_function_definition"):
1683
+ return
1684
+ if t == "call_expression" and body_node.children:
1685
+ callee = body_node.children[0]
1686
+ # Direct call: foo(...)
1687
+ if callee.type == "identifier":
1688
+ callee_name = _read_text(callee, source)
1689
+ target_nid = _make_id(stem, callee_name)
1690
+ add_edge(func_nid, target_nid, "calls", body_node.start_point[0] + 1,
1691
+ confidence="EXTRACTED")
1692
+ # Method call: obj.method(...)
1693
+ elif callee.type == "field_expression" and len(callee.children) >= 3:
1694
+ method_node = callee.children[-1]
1695
+ method_name = _read_text(method_node, source)
1696
+ target_nid = _make_id(stem, method_name)
1697
+ add_edge(func_nid, target_nid, "calls", body_node.start_point[0] + 1,
1698
+ confidence="EXTRACTED")
1699
+ for child in body_node.children:
1700
+ walk_calls(child, func_nid)
1701
+
1702
+ def walk(node, scope_nid: str) -> None:
1703
+ t = node.type
1704
+
1705
+ # Module
1706
+ if t == "module_definition":
1707
+ name_node = next((c for c in node.children if c.type == "identifier"), None)
1708
+ if name_node:
1709
+ mod_name = _read_text(name_node, source)
1710
+ mod_nid = _make_id(stem, mod_name)
1711
+ line = node.start_point[0] + 1
1712
+ add_node(mod_nid, mod_name, line)
1713
+ add_edge(file_nid, mod_nid, "defines", line)
1714
+ for child in node.children:
1715
+ walk(child, mod_nid)
1716
+ return
1717
+
1718
+ # Struct (struct / mutable struct — both map to struct_definition in tree-sitter-julia)
1719
+ if t == "struct_definition":
1720
+ # type_head may contain: identifier (simple) or binary_expression (Foo <: Bar)
1721
+ type_head = next((c for c in node.children if c.type == "type_head"), None)
1722
+ if type_head:
1723
+ bin_expr = next((c for c in type_head.children if c.type == "binary_expression"), None)
1724
+ if bin_expr:
1725
+ # First identifier is the struct name, last is the supertype
1726
+ identifiers = [c for c in bin_expr.children if c.type == "identifier"]
1727
+ if identifiers:
1728
+ struct_name = _read_text(identifiers[0], source)
1729
+ struct_nid = _make_id(stem, struct_name)
1730
+ line = node.start_point[0] + 1
1731
+ add_node(struct_nid, struct_name, line)
1732
+ add_edge(scope_nid, struct_nid, "defines", line)
1733
+ if len(identifiers) >= 2:
1734
+ super_name = _read_text(identifiers[-1], source)
1735
+ add_edge(struct_nid, _make_id(stem, super_name), "inherits",
1736
+ line, confidence="EXTRACTED")
1737
+ else:
1738
+ name_node = next((c for c in type_head.children if c.type == "identifier"), None)
1739
+ if name_node:
1740
+ struct_name = _read_text(name_node, source)
1741
+ struct_nid = _make_id(stem, struct_name)
1742
+ line = node.start_point[0] + 1
1743
+ add_node(struct_nid, struct_name, line)
1744
+ add_edge(scope_nid, struct_nid, "defines", line)
1745
+ return
1746
+
1747
+ # Abstract type
1748
+ if t == "abstract_definition":
1749
+ # type_head > identifier
1750
+ type_head = next((c for c in node.children if c.type == "type_head"), None)
1751
+ if type_head:
1752
+ name_node = next((c for c in type_head.children if c.type == "identifier"), None)
1753
+ if name_node:
1754
+ abs_name = _read_text(name_node, source)
1755
+ abs_nid = _make_id(stem, abs_name)
1756
+ line = node.start_point[0] + 1
1757
+ add_node(abs_nid, abs_name, line)
1758
+ add_edge(scope_nid, abs_nid, "defines", line)
1759
+ return
1760
+
1761
+ # Function: function foo(...) ... end
1762
+ if t == "function_definition":
1763
+ sig_node = next((c for c in node.children if c.type == "signature"), None)
1764
+ if sig_node:
1765
+ func_name = _func_name_from_signature(sig_node)
1766
+ if func_name:
1767
+ func_nid = _make_id(stem, func_name)
1768
+ line = node.start_point[0] + 1
1769
+ add_node(func_nid, f"{func_name}()", line)
1770
+ add_edge(scope_nid, func_nid, "defines", line)
1771
+ function_bodies.append((func_nid, node))
1772
+ return
1773
+
1774
+ # Short function: foo(x) = expr
1775
+ if t == "assignment":
1776
+ lhs = node.children[0] if node.children else None
1777
+ if lhs and lhs.type == "call_expression" and lhs.children:
1778
+ callee = lhs.children[0]
1779
+ if callee.type == "identifier":
1780
+ func_name = _read_text(callee, source)
1781
+ func_nid = _make_id(stem, func_name)
1782
+ line = node.start_point[0] + 1
1783
+ add_node(func_nid, f"{func_name}()", line)
1784
+ add_edge(scope_nid, func_nid, "defines", line)
1785
+ # Only walk the RHS (index 2 after lhs and operator) to avoid self-loops
1786
+ rhs = node.children[-1] if len(node.children) >= 3 else None
1787
+ if rhs:
1788
+ function_bodies.append((func_nid, rhs))
1789
+ return
1790
+
1791
+ # Using / Import
1792
+ if t in ("using_statement", "import_statement"):
1793
+ line = node.start_point[0] + 1
1794
+ for child in node.children:
1795
+ if child.type == "identifier":
1796
+ mod_name = _read_text(child, source)
1797
+ imp_nid = _make_id(mod_name)
1798
+ add_node(imp_nid, mod_name, line)
1799
+ add_edge(scope_nid, imp_nid, "imports", line)
1800
+ elif child.type == "selected_import":
1801
+ identifiers = [c for c in child.children if c.type == "identifier"]
1802
+ if identifiers:
1803
+ pkg_name = _read_text(identifiers[0], source)
1804
+ pkg_nid = _make_id(pkg_name)
1805
+ add_node(pkg_nid, pkg_name, line)
1806
+ add_edge(scope_nid, pkg_nid, "imports", line)
1807
+ return
1808
+
1809
+ for child in node.children:
1810
+ walk(child, scope_nid)
1811
+
1812
+ walk(root, file_nid)
1813
+
1814
+ for func_nid, body_node in function_bodies:
1815
+ # For function_definition nodes, walk children directly to avoid
1816
+ # the boundary check returning early on the top-level node itself.
1817
+ # Skip the "signature" child — it contains the function's own call_expression
1818
+ # which would create a self-loop.
1819
+ if body_node.type == "function_definition":
1820
+ for child in body_node.children:
1821
+ if child.type != "signature":
1822
+ walk_calls(child, func_nid)
1823
+ else:
1824
+ walk_calls(body_node, func_nid)
1825
+
1826
+ return {"nodes": nodes, "edges": edges}
1827
+
1828
+
1829
+ # ── Go extractor (custom walk) ────────────────────────────────────────────────
1830
+
1831
+ def extract_go(path: Path) -> dict:
1832
+ """Extract functions, methods, type declarations, and imports from a .go file."""
1833
+ try:
1834
+ import tree_sitter_go as tsgo
1835
+ from tree_sitter import Language, Parser
1836
+ except ImportError:
1837
+ return {"nodes": [], "edges": [], "error": "tree-sitter-go not installed"}
1838
+
1839
+ try:
1840
+ language = Language(tsgo.language())
1841
+ parser = Parser(language)
1842
+ source = path.read_bytes()
1843
+ tree = parser.parse(source)
1844
+ root = tree.root_node
1845
+ except Exception as e:
1846
+ return {"nodes": [], "edges": [], "error": str(e)}
1847
+
1848
+ stem = path.stem
1849
+ # Use directory name as package scope so methods on the same type across
1850
+ # multiple files in a package share one canonical type node.
1851
+ pkg_scope = path.parent.name or stem
1852
+ str_path = str(path)
1853
+ nodes: list[dict] = []
1854
+ edges: list[dict] = []
1855
+ seen_ids: set[str] = set()
1856
+ function_bodies: list[tuple[str, object]] = []
1857
+
1858
+ def add_node(nid: str, label: str, line: int) -> None:
1859
+ if nid not in seen_ids:
1860
+ seen_ids.add(nid)
1861
+ nodes.append({
1862
+ "id": nid,
1863
+ "label": label,
1864
+ "file_type": "code",
1865
+ "source_file": str_path,
1866
+ "source_location": f"L{line}",
1867
+ })
1868
+
1869
+ def add_edge(src: str, tgt: str, relation: str, line: int,
1870
+ confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
1871
+ edges.append({
1872
+ "source": src,
1873
+ "target": tgt,
1874
+ "relation": relation,
1875
+ "confidence": confidence,
1876
+ "source_file": str_path,
1877
+ "source_location": f"L{line}",
1878
+ "weight": weight,
1879
+ })
1880
+
1881
+ file_nid = _make_id(str(path))
1882
+ add_node(file_nid, path.name, 1)
1883
+
1884
+ def walk(node) -> None:
1885
+ t = node.type
1886
+
1887
+ if t == "function_declaration":
1888
+ name_node = node.child_by_field_name("name")
1889
+ if name_node:
1890
+ func_name = _read_text(name_node, source)
1891
+ line = node.start_point[0] + 1
1892
+ func_nid = _make_id(stem, func_name)
1893
+ add_node(func_nid, f"{func_name}()", line)
1894
+ add_edge(file_nid, func_nid, "contains", line)
1895
+ body = node.child_by_field_name("body")
1896
+ if body:
1897
+ function_bodies.append((func_nid, body))
1898
+ return
1899
+
1900
+ if t == "method_declaration":
1901
+ receiver = node.child_by_field_name("receiver")
1902
+ receiver_type: str | None = None
1903
+ if receiver:
1904
+ for param in receiver.children:
1905
+ if param.type == "parameter_declaration":
1906
+ type_node = param.child_by_field_name("type")
1907
+ if type_node:
1908
+ raw = _read_text(type_node, source).lstrip("*").strip()
1909
+ receiver_type = raw
1910
+ break
1911
+ name_node = node.child_by_field_name("name")
1912
+ if name_node:
1913
+ method_name = _read_text(name_node, source)
1914
+ line = node.start_point[0] + 1
1915
+ if receiver_type:
1916
+ parent_nid = _make_id(pkg_scope, receiver_type)
1917
+ add_node(parent_nid, receiver_type, line)
1918
+ method_nid = _make_id(parent_nid, method_name)
1919
+ add_node(method_nid, f".{method_name}()", line)
1920
+ add_edge(parent_nid, method_nid, "method", line)
1921
+ else:
1922
+ method_nid = _make_id(stem, method_name)
1923
+ add_node(method_nid, f"{method_name}()", line)
1924
+ add_edge(file_nid, method_nid, "contains", line)
1925
+ body = node.child_by_field_name("body")
1926
+ if body:
1927
+ function_bodies.append((method_nid, body))
1928
+ return
1929
+
1930
+ if t == "type_declaration":
1931
+ for child in node.children:
1932
+ if child.type == "type_spec":
1933
+ name_node = child.child_by_field_name("name")
1934
+ if name_node:
1935
+ type_name = _read_text(name_node, source)
1936
+ line = child.start_point[0] + 1
1937
+ type_nid = _make_id(pkg_scope, type_name)
1938
+ add_node(type_nid, type_name, line)
1939
+ add_edge(file_nid, type_nid, "contains", line)
1940
+ return
1941
+
1942
+ if t == "import_declaration":
1943
+ for child in node.children:
1944
+ if child.type == "import_spec_list":
1945
+ for spec in child.children:
1946
+ if spec.type == "import_spec":
1947
+ path_node = spec.child_by_field_name("path")
1948
+ if path_node:
1949
+ raw = _read_text(path_node, source).strip('"')
1950
+ module_name = raw.split("/")[-1]
1951
+ tgt_nid = _make_id(module_name)
1952
+ add_edge(file_nid, tgt_nid, "imports_from", spec.start_point[0] + 1)
1953
+ elif child.type == "import_spec":
1954
+ path_node = child.child_by_field_name("path")
1955
+ if path_node:
1956
+ raw = _read_text(path_node, source).strip('"')
1957
+ module_name = raw.split("/")[-1]
1958
+ tgt_nid = _make_id(module_name)
1959
+ add_edge(file_nid, tgt_nid, "imports_from", child.start_point[0] + 1)
1960
+ return
1961
+
1962
+ for child in node.children:
1963
+ walk(child)
1964
+
1965
+ walk(root)
1966
+
1967
+ label_to_nid: dict[str, str] = {}
1968
+ for n in nodes:
1969
+ raw = n["label"]
1970
+ normalised = raw.strip("()").lstrip(".")
1971
+ label_to_nid[normalised.lower()] = n["id"]
1972
+
1973
+ seen_call_pairs: set[tuple[str, str]] = set()
1974
+ raw_calls: list[dict] = []
1975
+
1976
+ def walk_calls(node, caller_nid: str) -> None:
1977
+ if node.type in ("function_declaration", "method_declaration"):
1978
+ return
1979
+ if node.type == "call_expression":
1980
+ func_node = node.child_by_field_name("function")
1981
+ callee_name: str | None = None
1982
+ if func_node:
1983
+ if func_node.type == "identifier":
1984
+ callee_name = _read_text(func_node, source)
1985
+ elif func_node.type == "selector_expression":
1986
+ field = func_node.child_by_field_name("field")
1987
+ if field:
1988
+ callee_name = _read_text(field, source)
1989
+ if callee_name:
1990
+ tgt_nid = label_to_nid.get(callee_name.lower())
1991
+ if tgt_nid and tgt_nid != caller_nid:
1992
+ pair = (caller_nid, tgt_nid)
1993
+ if pair not in seen_call_pairs:
1994
+ seen_call_pairs.add(pair)
1995
+ line = node.start_point[0] + 1
1996
+ edges.append({
1997
+ "source": caller_nid,
1998
+ "target": tgt_nid,
1999
+ "relation": "calls",
2000
+ "confidence": "EXTRACTED",
2001
+ "source_file": str_path,
2002
+ "source_location": f"L{line}",
2003
+ "weight": 1.0,
2004
+ })
2005
+ elif callee_name:
2006
+ raw_calls.append({
2007
+ "caller_nid": caller_nid,
2008
+ "callee": callee_name,
2009
+ "source_file": str_path,
2010
+ "source_location": f"L{node.start_point[0] + 1}",
2011
+ })
2012
+ for child in node.children:
2013
+ walk_calls(child, caller_nid)
2014
+
2015
+ for caller_nid, body_node in function_bodies:
2016
+ walk_calls(body_node, caller_nid)
2017
+
2018
+ valid_ids = seen_ids
2019
+ clean_edges = []
2020
+ for edge in edges:
2021
+ src, tgt = edge["source"], edge["target"]
2022
+ if src in valid_ids and (tgt in valid_ids or edge["relation"] in ("imports", "imports_from")):
2023
+ clean_edges.append(edge)
2024
+
2025
+ return {"nodes": nodes, "edges": clean_edges, "raw_calls": raw_calls}
2026
+
2027
+
2028
+ # ── Rust extractor (custom walk) ──────────────────────────────────────────────
2029
+
2030
+ def extract_rust(path: Path) -> dict:
2031
+ """Extract functions, structs, enums, traits, impl methods, and use declarations from a .rs file."""
2032
+ try:
2033
+ import tree_sitter_rust as tsrust
2034
+ from tree_sitter import Language, Parser
2035
+ except ImportError:
2036
+ return {"nodes": [], "edges": [], "error": "tree-sitter-rust not installed"}
2037
+
2038
+ try:
2039
+ language = Language(tsrust.language())
2040
+ parser = Parser(language)
2041
+ source = path.read_bytes()
2042
+ tree = parser.parse(source)
2043
+ root = tree.root_node
2044
+ except Exception as e:
2045
+ return {"nodes": [], "edges": [], "error": str(e)}
2046
+
2047
+ stem = path.stem
2048
+ str_path = str(path)
2049
+ nodes: list[dict] = []
2050
+ edges: list[dict] = []
2051
+ seen_ids: set[str] = set()
2052
+ function_bodies: list[tuple[str, object]] = []
2053
+
2054
+ def add_node(nid: str, label: str, line: int) -> None:
2055
+ if nid not in seen_ids:
2056
+ seen_ids.add(nid)
2057
+ nodes.append({
2058
+ "id": nid,
2059
+ "label": label,
2060
+ "file_type": "code",
2061
+ "source_file": str_path,
2062
+ "source_location": f"L{line}",
2063
+ })
2064
+
2065
+ def add_edge(src: str, tgt: str, relation: str, line: int,
2066
+ confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
2067
+ edges.append({
2068
+ "source": src,
2069
+ "target": tgt,
2070
+ "relation": relation,
2071
+ "confidence": confidence,
2072
+ "source_file": str_path,
2073
+ "source_location": f"L{line}",
2074
+ "weight": weight,
2075
+ })
2076
+
2077
+ file_nid = _make_id(str(path))
2078
+ add_node(file_nid, path.name, 1)
2079
+
2080
+ def walk(node, parent_impl_nid: str | None = None) -> None:
2081
+ t = node.type
2082
+
2083
+ if t == "function_item":
2084
+ name_node = node.child_by_field_name("name")
2085
+ if name_node:
2086
+ func_name = _read_text(name_node, source)
2087
+ line = node.start_point[0] + 1
2088
+ if parent_impl_nid:
2089
+ func_nid = _make_id(parent_impl_nid, func_name)
2090
+ add_node(func_nid, f".{func_name}()", line)
2091
+ add_edge(parent_impl_nid, func_nid, "method", line)
2092
+ else:
2093
+ func_nid = _make_id(stem, func_name)
2094
+ add_node(func_nid, f"{func_name}()", line)
2095
+ add_edge(file_nid, func_nid, "contains", line)
2096
+ body = node.child_by_field_name("body")
2097
+ if body:
2098
+ function_bodies.append((func_nid, body))
2099
+ return
2100
+
2101
+ if t in ("struct_item", "enum_item", "trait_item"):
2102
+ name_node = node.child_by_field_name("name")
2103
+ if name_node:
2104
+ item_name = _read_text(name_node, source)
2105
+ line = node.start_point[0] + 1
2106
+ item_nid = _make_id(stem, item_name)
2107
+ add_node(item_nid, item_name, line)
2108
+ add_edge(file_nid, item_nid, "contains", line)
2109
+ return
2110
+
2111
+ if t == "impl_item":
2112
+ type_node = node.child_by_field_name("type")
2113
+ impl_nid: str | None = None
2114
+ if type_node:
2115
+ type_name = _read_text(type_node, source).strip()
2116
+ impl_nid = _make_id(stem, type_name)
2117
+ add_node(impl_nid, type_name, node.start_point[0] + 1)
2118
+ body = node.child_by_field_name("body")
2119
+ if body:
2120
+ for child in body.children:
2121
+ walk(child, parent_impl_nid=impl_nid)
2122
+ return
2123
+
2124
+ if t == "use_declaration":
2125
+ arg = node.child_by_field_name("argument")
2126
+ if arg:
2127
+ raw = _read_text(arg, source)
2128
+ clean = raw.split("{")[0].rstrip(":").rstrip("*").rstrip(":")
2129
+ module_name = clean.split("::")[-1].strip()
2130
+ if module_name:
2131
+ tgt_nid = _make_id(module_name)
2132
+ add_edge(file_nid, tgt_nid, "imports_from", node.start_point[0] + 1)
2133
+ return
2134
+
2135
+ for child in node.children:
2136
+ walk(child, parent_impl_nid=None)
2137
+
2138
+ walk(root)
2139
+
2140
+ label_to_nid: dict[str, str] = {}
2141
+ for n in nodes:
2142
+ raw = n["label"]
2143
+ normalised = raw.strip("()").lstrip(".")
2144
+ label_to_nid[normalised.lower()] = n["id"]
2145
+
2146
+ seen_call_pairs: set[tuple[str, str]] = set()
2147
+ raw_calls: list[dict] = []
2148
+
2149
+ def walk_calls(node, caller_nid: str) -> None:
2150
+ if node.type == "function_item":
2151
+ return
2152
+ if node.type == "call_expression":
2153
+ func_node = node.child_by_field_name("function")
2154
+ callee_name: str | None = None
2155
+ if func_node:
2156
+ if func_node.type == "identifier":
2157
+ callee_name = _read_text(func_node, source)
2158
+ elif func_node.type == "field_expression":
2159
+ field = func_node.child_by_field_name("field")
2160
+ if field:
2161
+ callee_name = _read_text(field, source)
2162
+ elif func_node.type == "scoped_identifier":
2163
+ name = func_node.child_by_field_name("name")
2164
+ if name:
2165
+ callee_name = _read_text(name, source)
2166
+ if callee_name:
2167
+ tgt_nid = label_to_nid.get(callee_name.lower())
2168
+ if tgt_nid and tgt_nid != caller_nid:
2169
+ pair = (caller_nid, tgt_nid)
2170
+ if pair not in seen_call_pairs:
2171
+ seen_call_pairs.add(pair)
2172
+ line = node.start_point[0] + 1
2173
+ edges.append({
2174
+ "source": caller_nid,
2175
+ "target": tgt_nid,
2176
+ "relation": "calls",
2177
+ "confidence": "EXTRACTED",
2178
+ "source_file": str_path,
2179
+ "source_location": f"L{line}",
2180
+ "weight": 1.0,
2181
+ })
2182
+ else:
2183
+ raw_calls.append({
2184
+ "caller_nid": caller_nid,
2185
+ "callee": callee_name,
2186
+ "source_file": str_path,
2187
+ "source_location": f"L{node.start_point[0] + 1}",
2188
+ })
2189
+ for child in node.children:
2190
+ walk_calls(child, caller_nid)
2191
+
2192
+ for caller_nid, body_node in function_bodies:
2193
+ walk_calls(body_node, caller_nid)
2194
+
2195
+ valid_ids = seen_ids
2196
+ clean_edges = []
2197
+ for edge in edges:
2198
+ src, tgt = edge["source"], edge["target"]
2199
+ if src in valid_ids and (tgt in valid_ids or edge["relation"] in ("imports", "imports_from")):
2200
+ clean_edges.append(edge)
2201
+
2202
+ return {"nodes": nodes, "edges": clean_edges, "raw_calls": raw_calls}
2203
+
2204
+
2205
+ # ── Zig ───────────────────────────────────────────────────────────────────────
2206
+
2207
+ def extract_zig(path: Path) -> dict:
2208
+ """Extract functions, structs, enums, unions, and imports from a .zig file."""
2209
+ try:
2210
+ import tree_sitter_zig as tszig
2211
+ from tree_sitter import Language, Parser
2212
+ except ImportError:
2213
+ return {"nodes": [], "edges": [], "error": "tree_sitter_zig not installed"}
2214
+
2215
+ try:
2216
+ language = Language(tszig.language())
2217
+ parser = Parser(language)
2218
+ source = path.read_bytes()
2219
+ tree = parser.parse(source)
2220
+ root = tree.root_node
2221
+ except Exception as e:
2222
+ return {"nodes": [], "edges": [], "error": str(e)}
2223
+
2224
+ stem = path.stem
2225
+ str_path = str(path)
2226
+ nodes: list[dict] = []
2227
+ edges: list[dict] = []
2228
+ seen_ids: set[str] = set()
2229
+ function_bodies: list[tuple[str, Any]] = []
2230
+
2231
+ def add_node(nid: str, label: str, line: int) -> None:
2232
+ if nid not in seen_ids:
2233
+ seen_ids.add(nid)
2234
+ nodes.append({"id": nid, "label": label, "file_type": "code",
2235
+ "source_file": str_path, "source_location": f"L{line}"})
2236
+
2237
+ def add_edge(src: str, tgt: str, relation: str, line: int,
2238
+ confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
2239
+ edges.append({"source": src, "target": tgt, "relation": relation,
2240
+ "confidence": confidence, "source_file": str_path,
2241
+ "source_location": f"L{line}", "weight": weight})
2242
+
2243
+ file_nid = _make_id(str(path))
2244
+ add_node(file_nid, path.name, 1)
2245
+
2246
+ def _extract_import(node) -> None:
2247
+ for child in node.children:
2248
+ if child.type == "builtin_function":
2249
+ bi = None
2250
+ args = None
2251
+ for c in child.children:
2252
+ if c.type == "builtin_identifier":
2253
+ bi = _read_text(c, source)
2254
+ elif c.type == "arguments":
2255
+ args = c
2256
+ if bi in ("@import", "@cImport") and args:
2257
+ for arg in args.children:
2258
+ if arg.type in ("string_literal", "string"):
2259
+ raw = _read_text(arg, source).strip('"')
2260
+ module_name = raw.split("/")[-1].split(".")[0]
2261
+ if module_name:
2262
+ tgt_nid = _make_id(module_name)
2263
+ add_edge(file_nid, tgt_nid, "imports_from",
2264
+ node.start_point[0] + 1)
2265
+ return
2266
+ elif child.type == "field_expression":
2267
+ _extract_import(child)
2268
+ return
2269
+
2270
+ def walk(node, parent_struct_nid: str | None = None) -> None:
2271
+ t = node.type
2272
+
2273
+ if t == "function_declaration":
2274
+ name_node = node.child_by_field_name("name")
2275
+ if name_node:
2276
+ func_name = _read_text(name_node, source)
2277
+ line = node.start_point[0] + 1
2278
+ if parent_struct_nid:
2279
+ func_nid = _make_id(parent_struct_nid, func_name)
2280
+ add_node(func_nid, f".{func_name}()", line)
2281
+ add_edge(parent_struct_nid, func_nid, "method", line)
2282
+ else:
2283
+ func_nid = _make_id(stem, func_name)
2284
+ add_node(func_nid, f"{func_name}()", line)
2285
+ add_edge(file_nid, func_nid, "contains", line)
2286
+ body = node.child_by_field_name("body")
2287
+ if body:
2288
+ function_bodies.append((func_nid, body))
2289
+ return
2290
+
2291
+ if t == "variable_declaration":
2292
+ name_node = None
2293
+ value_node = None
2294
+ for child in node.children:
2295
+ if child.type == "identifier":
2296
+ name_node = child
2297
+ elif child.type in ("struct_declaration", "enum_declaration",
2298
+ "union_declaration", "builtin_function",
2299
+ "field_expression"):
2300
+ value_node = child
2301
+
2302
+ if value_node and value_node.type == "struct_declaration":
2303
+ if name_node:
2304
+ struct_name = _read_text(name_node, source)
2305
+ line = node.start_point[0] + 1
2306
+ struct_nid = _make_id(stem, struct_name)
2307
+ add_node(struct_nid, struct_name, line)
2308
+ add_edge(file_nid, struct_nid, "contains", line)
2309
+ for child in value_node.children:
2310
+ walk(child, parent_struct_nid=struct_nid)
2311
+ return
2312
+
2313
+ if value_node and value_node.type in ("enum_declaration", "union_declaration"):
2314
+ if name_node:
2315
+ type_name = _read_text(name_node, source)
2316
+ line = node.start_point[0] + 1
2317
+ type_nid = _make_id(stem, type_name)
2318
+ add_node(type_nid, type_name, line)
2319
+ add_edge(file_nid, type_nid, "contains", line)
2320
+ return
2321
+
2322
+ if value_node and value_node.type in ("builtin_function", "field_expression"):
2323
+ _extract_import(node)
2324
+ return
2325
+
2326
+ for child in node.children:
2327
+ walk(child, parent_struct_nid)
2328
+
2329
+ walk(root)
2330
+
2331
+ seen_call_pairs: set[tuple[str, str]] = set()
2332
+ raw_calls: list[dict] = []
2333
+
2334
+ def walk_calls(node, caller_nid: str) -> None:
2335
+ if node.type == "function_declaration":
2336
+ return
2337
+ if node.type == "call_expression":
2338
+ fn = node.child_by_field_name("function")
2339
+ if fn:
2340
+ callee = _read_text(fn, source).split(".")[-1]
2341
+ tgt_nid = next((n["id"] for n in nodes if n["label"] in
2342
+ (f"{callee}()", f".{callee}()")), None)
2343
+ if tgt_nid and tgt_nid != caller_nid:
2344
+ pair = (caller_nid, tgt_nid)
2345
+ if pair not in seen_call_pairs:
2346
+ seen_call_pairs.add(pair)
2347
+ add_edge(caller_nid, tgt_nid, "calls",
2348
+ node.start_point[0] + 1,
2349
+ confidence="EXTRACTED", weight=1.0)
2350
+ elif callee:
2351
+ raw_calls.append({
2352
+ "caller_nid": caller_nid,
2353
+ "callee": callee,
2354
+ "source_file": str_path,
2355
+ "source_location": f"L{node.start_point[0] + 1}",
2356
+ })
2357
+ for child in node.children:
2358
+ walk_calls(child, caller_nid)
2359
+
2360
+ for caller_nid, body_node in function_bodies:
2361
+ walk_calls(body_node, caller_nid)
2362
+
2363
+ clean_edges = [e for e in edges if e["source"] in seen_ids and
2364
+ (e["target"] in seen_ids or e["relation"] == "imports_from")]
2365
+ return {"nodes": nodes, "edges": clean_edges, "raw_calls": raw_calls}
2366
+
2367
+
2368
+ # ── PowerShell ────────────────────────────────────────────────────────────────
2369
+
2370
+ def extract_powershell(path: Path) -> dict:
2371
+ """Extract functions, classes, methods, and using statements from a .ps1 file."""
2372
+ try:
2373
+ import tree_sitter_powershell as tsps
2374
+ from tree_sitter import Language, Parser
2375
+ except ImportError:
2376
+ return {"nodes": [], "edges": [], "error": "tree_sitter_powershell not installed"}
2377
+
2378
+ try:
2379
+ language = Language(tsps.language())
2380
+ parser = Parser(language)
2381
+ source = path.read_bytes()
2382
+ tree = parser.parse(source)
2383
+ root = tree.root_node
2384
+ except Exception as e:
2385
+ return {"nodes": [], "edges": [], "error": str(e)}
2386
+
2387
+ stem = path.stem
2388
+ str_path = str(path)
2389
+ nodes: list[dict] = []
2390
+ edges: list[dict] = []
2391
+ seen_ids: set[str] = set()
2392
+ function_bodies: list[tuple[str, Any]] = []
2393
+
2394
+ def add_node(nid: str, label: str, line: int) -> None:
2395
+ if nid not in seen_ids:
2396
+ seen_ids.add(nid)
2397
+ nodes.append({"id": nid, "label": label, "file_type": "code",
2398
+ "source_file": str_path, "source_location": f"L{line}"})
2399
+
2400
+ def add_edge(src: str, tgt: str, relation: str, line: int,
2401
+ confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
2402
+ edges.append({"source": src, "target": tgt, "relation": relation,
2403
+ "confidence": confidence, "source_file": str_path,
2404
+ "source_location": f"L{line}", "weight": weight})
2405
+
2406
+ file_nid = _make_id(str(path))
2407
+ add_node(file_nid, path.name, 1)
2408
+
2409
+ _PS_SKIP = frozenset({
2410
+ "using", "return", "if", "else", "elseif", "foreach", "for",
2411
+ "while", "do", "switch", "try", "catch", "finally", "throw",
2412
+ "break", "continue", "exit", "param", "begin", "process", "end",
2413
+ })
2414
+
2415
+ def _find_script_block_body(node):
2416
+ for child in node.children:
2417
+ if child.type == "script_block":
2418
+ for sc in child.children:
2419
+ if sc.type == "script_block_body":
2420
+ return sc
2421
+ return child
2422
+ return None
2423
+
2424
+ def walk(node, parent_class_nid: str | None = None) -> None:
2425
+ t = node.type
2426
+
2427
+ if t == "function_statement":
2428
+ name_node = next((c for c in node.children if c.type == "function_name"), None)
2429
+ if name_node:
2430
+ func_name = _read_text(name_node, source)
2431
+ line = node.start_point[0] + 1
2432
+ func_nid = _make_id(stem, func_name)
2433
+ add_node(func_nid, f"{func_name}()", line)
2434
+ add_edge(file_nid, func_nid, "contains", line)
2435
+ body = _find_script_block_body(node)
2436
+ if body:
2437
+ function_bodies.append((func_nid, body))
2438
+ return
2439
+
2440
+ if t == "class_statement":
2441
+ name_node = next((c for c in node.children if c.type == "simple_name"), None)
2442
+ if name_node:
2443
+ class_name = _read_text(name_node, source)
2444
+ line = node.start_point[0] + 1
2445
+ class_nid = _make_id(stem, class_name)
2446
+ add_node(class_nid, class_name, line)
2447
+ add_edge(file_nid, class_nid, "contains", line)
2448
+ for child in node.children:
2449
+ walk(child, parent_class_nid=class_nid)
2450
+ return
2451
+
2452
+ if t == "class_method_definition":
2453
+ name_node = next((c for c in node.children if c.type == "simple_name"), None)
2454
+ if name_node:
2455
+ method_name = _read_text(name_node, source)
2456
+ line = node.start_point[0] + 1
2457
+ if parent_class_nid:
2458
+ method_nid = _make_id(parent_class_nid, method_name)
2459
+ add_node(method_nid, f".{method_name}()", line)
2460
+ add_edge(parent_class_nid, method_nid, "method", line)
2461
+ else:
2462
+ method_nid = _make_id(stem, method_name)
2463
+ add_node(method_nid, f"{method_name}()", line)
2464
+ add_edge(file_nid, method_nid, "contains", line)
2465
+ body = _find_script_block_body(node)
2466
+ if body:
2467
+ function_bodies.append((method_nid, body))
2468
+ return
2469
+
2470
+ if t == "command":
2471
+ cmd_name_node = next((c for c in node.children if c.type == "command_name"), None)
2472
+ if cmd_name_node:
2473
+ cmd_text = _read_text(cmd_name_node, source).lower()
2474
+ if cmd_text == "using":
2475
+ tokens = []
2476
+ for child in node.children:
2477
+ if child.type == "command_elements":
2478
+ for el in child.children:
2479
+ if el.type == "generic_token":
2480
+ tokens.append(_read_text(el, source))
2481
+ module_tokens = [t for t in tokens
2482
+ if t.lower() not in ("namespace", "module", "assembly")]
2483
+ if module_tokens:
2484
+ module_name = module_tokens[-1].split(".")[-1]
2485
+ add_edge(file_nid, _make_id(module_name), "imports_from",
2486
+ node.start_point[0] + 1)
2487
+ return
2488
+
2489
+ for child in node.children:
2490
+ walk(child, parent_class_nid)
2491
+
2492
+ walk(root)
2493
+
2494
+ label_to_nid = {n["label"].strip("()").lstrip(".").lower(): n["id"] for n in nodes}
2495
+ seen_call_pairs: set[tuple[str, str]] = set()
2496
+ raw_calls: list[dict] = []
2497
+
2498
+ def walk_calls(node, caller_nid: str) -> None:
2499
+ if node.type in ("function_statement", "class_statement"):
2500
+ return
2501
+ if node.type == "command":
2502
+ cmd_name_node = next((c for c in node.children if c.type == "command_name"), None)
2503
+ if cmd_name_node:
2504
+ cmd_text = _read_text(cmd_name_node, source)
2505
+ if cmd_text.lower() not in _PS_SKIP:
2506
+ tgt_nid = label_to_nid.get(cmd_text.lower())
2507
+ if tgt_nid and tgt_nid != caller_nid:
2508
+ pair = (caller_nid, tgt_nid)
2509
+ if pair not in seen_call_pairs:
2510
+ seen_call_pairs.add(pair)
2511
+ add_edge(caller_nid, tgt_nid, "calls",
2512
+ node.start_point[0] + 1,
2513
+ confidence="EXTRACTED", weight=1.0)
2514
+ elif cmd_text:
2515
+ raw_calls.append({
2516
+ "caller_nid": caller_nid,
2517
+ "callee": cmd_text,
2518
+ "source_file": str_path,
2519
+ "source_location": f"L{node.start_point[0] + 1}",
2520
+ })
2521
+ for child in node.children:
2522
+ walk_calls(child, caller_nid)
2523
+
2524
+ for caller_nid, body_node in function_bodies:
2525
+ walk_calls(body_node, caller_nid)
2526
+
2527
+ clean_edges = [e for e in edges if e["source"] in seen_ids and
2528
+ (e["target"] in seen_ids or e["relation"] == "imports_from")]
2529
+ return {"nodes": nodes, "edges": clean_edges, "raw_calls": raw_calls}
2530
+
2531
+
2532
+ # ── Cross-file import resolution ──────────────────────────────────────────────
2533
+
2534
+ def _resolve_cross_file_imports(
2535
+ per_file: list[dict],
2536
+ paths: list[Path],
2537
+ ) -> list[dict]:
2538
+ """
2539
+ Two-pass import resolution: turn file-level imports into class-level edges.
2540
+
2541
+ Pass 1 - build a global map: class/function name → node_id, per stem.
2542
+ Pass 2 - for each `from .module import Name`, look up Name in the global
2543
+ map and add a direct INFERRED edge from each class in the
2544
+ importing file to the imported entity.
2545
+
2546
+ This turns:
2547
+ auth.py --imports_from--> models.py (obvious, filtered out)
2548
+ Into:
2549
+ DigestAuth --uses--> Response [INFERRED] (cross-file, interesting!)
2550
+ BasicAuth --uses--> Request [INFERRED]
2551
+ """
2552
+ try:
2553
+ import tree_sitter_python as tspython
2554
+ from tree_sitter import Language, Parser
2555
+ except ImportError:
2556
+ return []
2557
+
2558
+ language = Language(tspython.language())
2559
+ parser = Parser(language)
2560
+
2561
+ # Pass 1: name → node_id across all files
2562
+ # Map: stem → {ClassName: node_id}
2563
+ stem_to_entities: dict[str, dict[str, str]] = {}
2564
+ for file_result in per_file:
2565
+ for node in file_result.get("nodes", []):
2566
+ src = node.get("source_file", "")
2567
+ if not src:
2568
+ continue
2569
+ stem = Path(src).stem
2570
+ label = node.get("label", "")
2571
+ nid = node.get("id", "")
2572
+ # Only index real classes/functions (not file nodes, not method stubs)
2573
+ if label and not label.endswith((")", ".py")) and "_" not in label[:1]:
2574
+ stem_to_entities.setdefault(stem, {})[label] = nid
2575
+
2576
+ # Pass 2: for each file, find `from .X import A, B, C` and resolve
2577
+ new_edges: list[dict] = []
2578
+ stem_to_path: dict[str, Path] = {p.stem: p for p in paths}
2579
+
2580
+ for file_result, path in zip(per_file, paths):
2581
+ stem = path.stem
2582
+ str_path = str(path)
2583
+
2584
+ # Find all classes defined in this file (the importers)
2585
+ local_classes = [
2586
+ n["id"] for n in file_result.get("nodes", [])
2587
+ if n.get("source_file") == str_path
2588
+ and not n["label"].endswith((")", ".py"))
2589
+ and n["id"] != _make_id(stem) # exclude file-level node
2590
+ ]
2591
+ if not local_classes:
2592
+ continue
2593
+
2594
+ # Parse imports from this file
2595
+ try:
2596
+ source = path.read_bytes()
2597
+ tree = parser.parse(source)
2598
+ except Exception:
2599
+ continue
2600
+
2601
+ def walk_imports(node) -> None:
2602
+ if node.type == "import_from_statement":
2603
+ # Find the module name - handles both absolute and relative imports.
2604
+ # Relative: `from .models import X` → relative_import → dotted_name
2605
+ # Absolute: `from models import X` → module_name field
2606
+ target_stem: str | None = None
2607
+ for child in node.children:
2608
+ if child.type == "relative_import":
2609
+ # Dig into relative_import → dotted_name → identifier
2610
+ for sub in child.children:
2611
+ if sub.type == "dotted_name":
2612
+ raw = source[sub.start_byte:sub.end_byte].decode("utf-8", errors="replace")
2613
+ target_stem = raw.split(".")[-1]
2614
+ break
2615
+ break
2616
+ if child.type == "dotted_name" and target_stem is None:
2617
+ raw = source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
2618
+ target_stem = raw.split(".")[-1]
2619
+
2620
+ if not target_stem or target_stem not in stem_to_entities:
2621
+ return
2622
+
2623
+ # Collect imported names: dotted_name children of import_from_statement
2624
+ # that come AFTER the 'import' keyword token.
2625
+ imported_names: list[str] = []
2626
+ past_import_kw = False
2627
+ for child in node.children:
2628
+ if child.type == "import":
2629
+ past_import_kw = True
2630
+ continue
2631
+ if not past_import_kw:
2632
+ continue
2633
+ if child.type == "dotted_name":
2634
+ imported_names.append(
2635
+ source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
2636
+ )
2637
+ elif child.type == "aliased_import":
2638
+ # `import X as Y` - take the original name
2639
+ name_node = child.child_by_field_name("name")
2640
+ if name_node:
2641
+ imported_names.append(
2642
+ source[name_node.start_byte:name_node.end_byte].decode("utf-8", errors="replace")
2643
+ )
2644
+
2645
+ line = node.start_point[0] + 1
2646
+ for name in imported_names:
2647
+ tgt_nid = stem_to_entities[target_stem].get(name)
2648
+ if tgt_nid:
2649
+ for src_class_nid in local_classes:
2650
+ new_edges.append({
2651
+ "source": src_class_nid,
2652
+ "target": tgt_nid,
2653
+ "relation": "uses",
2654
+ "confidence": "INFERRED",
2655
+ "source_file": str_path,
2656
+ "source_location": f"L{line}",
2657
+ "weight": 0.8,
2658
+ })
2659
+ for child in node.children:
2660
+ walk_imports(child)
2661
+
2662
+ walk_imports(tree.root_node)
2663
+
2664
+ return new_edges
2665
+
2666
+
2667
+ def extract_objc(path: Path) -> dict:
2668
+ """Extract interfaces, implementations, protocols, methods, and imports from .m/.mm/.h files."""
2669
+ try:
2670
+ import tree_sitter_objc as tsobjc
2671
+ from tree_sitter import Language, Parser
2672
+ except ImportError:
2673
+ return {"nodes": [], "edges": [], "error": "tree_sitter_objc not installed"}
2674
+
2675
+ try:
2676
+ language = Language(tsobjc.language())
2677
+ parser = Parser(language)
2678
+ source = path.read_bytes()
2679
+ tree = parser.parse(source)
2680
+ root = tree.root_node
2681
+ except Exception as e:
2682
+ return {"nodes": [], "edges": [], "error": str(e)}
2683
+
2684
+ stem = path.stem
2685
+ str_path = str(path)
2686
+ nodes: list[dict] = []
2687
+ edges: list[dict] = []
2688
+ seen_ids: set[str] = set()
2689
+ method_bodies: list[tuple[str, Any]] = []
2690
+
2691
+ def add_node(nid: str, label: str, line: int) -> None:
2692
+ if nid not in seen_ids:
2693
+ seen_ids.add(nid)
2694
+ nodes.append({"id": nid, "label": label, "file_type": "code",
2695
+ "source_file": str_path, "source_location": f"L{line}"})
2696
+
2697
+ def add_edge(src: str, tgt: str, relation: str, line: int,
2698
+ confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
2699
+ edges.append({"source": src, "target": tgt, "relation": relation,
2700
+ "confidence": confidence, "source_file": str_path,
2701
+ "source_location": f"L{line}", "weight": weight})
2702
+
2703
+ file_nid = _make_id(str(path))
2704
+ add_node(file_nid, path.name, 1)
2705
+
2706
+ def _read(node) -> str:
2707
+ return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
2708
+
2709
+ def _get_name(node, field: str) -> str | None:
2710
+ n = node.child_by_field_name(field)
2711
+ return _read(n) if n else None
2712
+
2713
+ def walk(node, parent_nid: str | None = None) -> None:
2714
+ t = node.type
2715
+ line = node.start_point[0] + 1
2716
+
2717
+ if t == "preproc_include":
2718
+ # #import <Foundation/Foundation.h> or #import "MyClass.h"
2719
+ for child in node.children:
2720
+ if child.type == "system_lib_string":
2721
+ raw = _read(child).strip("<>")
2722
+ module = raw.split("/")[-1].replace(".h", "")
2723
+ if module:
2724
+ tgt_nid = _make_id(module)
2725
+ add_edge(file_nid, tgt_nid, "imports", line)
2726
+ elif child.type == "string_literal":
2727
+ # recurse into string_literal to find string_content
2728
+ for sub in child.children:
2729
+ if sub.type == "string_content":
2730
+ raw = _read(sub)
2731
+ module = raw.split("/")[-1].replace(".h", "")
2732
+ if module:
2733
+ tgt_nid = _make_id(module)
2734
+ add_edge(file_nid, tgt_nid, "imports", line)
2735
+ return
2736
+
2737
+ if t == "class_interface":
2738
+ # @interface ClassName : SuperClass <Protocols>
2739
+ # children: @interface, identifier(name), ':', identifier(super), parameterized_arguments, ...
2740
+ identifiers = [c for c in node.children if c.type == "identifier"]
2741
+ if not identifiers:
2742
+ for child in node.children:
2743
+ walk(child, parent_nid)
2744
+ return
2745
+ name = _read(identifiers[0])
2746
+ cls_nid = _make_id(stem, name)
2747
+ add_node(cls_nid, name, line)
2748
+ add_edge(file_nid, cls_nid, "contains", line)
2749
+ # superclass is second identifier after ':'
2750
+ colon_seen = False
2751
+ for child in node.children:
2752
+ if child.type == ":":
2753
+ colon_seen = True
2754
+ elif colon_seen and child.type == "identifier":
2755
+ super_nid = _make_id(_read(child))
2756
+ add_edge(cls_nid, super_nid, "inherits", line)
2757
+ colon_seen = False
2758
+ elif child.type == "parameterized_arguments":
2759
+ # protocols adopted
2760
+ for sub in child.children:
2761
+ if sub.type == "type_name":
2762
+ for s in sub.children:
2763
+ if s.type == "type_identifier":
2764
+ proto_nid = _make_id(_read(s))
2765
+ add_edge(cls_nid, proto_nid, "imports", line)
2766
+ elif child.type == "method_declaration":
2767
+ walk(child, cls_nid)
2768
+ return
2769
+
2770
+ if t == "class_implementation":
2771
+ # @implementation ClassName
2772
+ name = None
2773
+ for child in node.children:
2774
+ if child.type == "identifier":
2775
+ name = _read(child)
2776
+ break
2777
+ if not name:
2778
+ for child in node.children:
2779
+ walk(child, parent_nid)
2780
+ return
2781
+ impl_nid = _make_id(stem, name)
2782
+ if impl_nid not in seen_ids:
2783
+ add_node(impl_nid, name, line)
2784
+ add_edge(file_nid, impl_nid, "contains", line)
2785
+ for child in node.children:
2786
+ if child.type == "implementation_definition":
2787
+ for sub in child.children:
2788
+ walk(sub, impl_nid)
2789
+ return
2790
+
2791
+ if t == "protocol_declaration":
2792
+ name = None
2793
+ for child in node.children:
2794
+ if child.type == "identifier":
2795
+ name = _read(child)
2796
+ break
2797
+ if name:
2798
+ proto_nid = _make_id(stem, name)
2799
+ add_node(proto_nid, f"<{name}>", line)
2800
+ add_edge(file_nid, proto_nid, "contains", line)
2801
+ for child in node.children:
2802
+ walk(child, proto_nid)
2803
+ return
2804
+
2805
+ if t in ("method_declaration", "method_definition"):
2806
+ container = parent_nid or file_nid
2807
+ # method name is the first identifier child (simple selector)
2808
+ # for compound selectors: identifier + method_parameter pairs
2809
+ parts = []
2810
+ for child in node.children:
2811
+ if child.type == "identifier":
2812
+ parts.append(_read(child))
2813
+ elif child.type == "method_parameter":
2814
+ for sub in child.children:
2815
+ if sub.type == "identifier":
2816
+ # selector keyword before ':'
2817
+ pass
2818
+ method_name = "".join(parts) if parts else None
2819
+ if method_name:
2820
+ method_nid = _make_id(container, method_name)
2821
+ add_node(method_nid, f"-{method_name}", line)
2822
+ add_edge(container, method_nid, "method", line)
2823
+ if t == "method_definition":
2824
+ method_bodies.append((method_nid, node))
2825
+ return
2826
+
2827
+ for child in node.children:
2828
+ walk(child, parent_nid)
2829
+
2830
+ walk(root)
2831
+
2832
+ # Second pass: resolve calls inside method bodies
2833
+ all_method_nids = {n["id"] for n in nodes if n["id"] != file_nid}
2834
+ seen_calls: set[tuple[str, str]] = set()
2835
+ for caller_nid, body_node in method_bodies:
2836
+ def walk_calls(n) -> None:
2837
+ if n.type == "message_expression":
2838
+ # [receiver selector]
2839
+ for child in n.children:
2840
+ if child.type in ("selector", "keyword_argument_list"):
2841
+ sel = []
2842
+ if child.type == "selector":
2843
+ sel.append(_read(child))
2844
+ else:
2845
+ for sub in child.children:
2846
+ if sub.type == "keyword_argument":
2847
+ for s in sub.children:
2848
+ if s.type == "selector":
2849
+ sel.append(_read(s))
2850
+ method_name = "".join(sel)
2851
+ for candidate in all_method_nids:
2852
+ if candidate.endswith(_make_id("", method_name).lstrip("_")):
2853
+ pair = (caller_nid, candidate)
2854
+ if pair not in seen_calls and caller_nid != candidate:
2855
+ seen_calls.add(pair)
2856
+ add_edge(caller_nid, candidate, "calls", body_node.start_point[0] + 1,
2857
+ confidence="EXTRACTED", weight=1.0)
2858
+ for child in n.children:
2859
+ walk_calls(child)
2860
+ walk_calls(body_node)
2861
+
2862
+ return {"nodes": nodes, "edges": edges, "input_tokens": 0, "output_tokens": 0}
2863
+
2864
+
2865
+ def extract_elixir(path: Path) -> dict:
2866
+ """Extract modules, functions, imports, and calls from a .ex/.exs file."""
2867
+ try:
2868
+ import tree_sitter_elixir as tselixir
2869
+ from tree_sitter import Language, Parser
2870
+ except ImportError:
2871
+ return {"nodes": [], "edges": [], "error": "tree_sitter_elixir not installed"}
2872
+
2873
+ try:
2874
+ language = Language(tselixir.language())
2875
+ parser = Parser(language)
2876
+ source = path.read_bytes()
2877
+ tree = parser.parse(source)
2878
+ root = tree.root_node
2879
+ except Exception as e:
2880
+ return {"nodes": [], "edges": [], "error": str(e)}
2881
+
2882
+ stem = path.stem
2883
+ str_path = str(path)
2884
+ nodes: list[dict] = []
2885
+ edges: list[dict] = []
2886
+ seen_ids: set[str] = set()
2887
+ function_bodies: list[tuple[str, Any]] = []
2888
+
2889
+ def add_node(nid: str, label: str, line: int) -> None:
2890
+ if nid not in seen_ids:
2891
+ seen_ids.add(nid)
2892
+ nodes.append({"id": nid, "label": label, "file_type": "code",
2893
+ "source_file": str_path, "source_location": f"L{line}"})
2894
+
2895
+ def add_edge(src: str, tgt: str, relation: str, line: int,
2896
+ confidence: str = "EXTRACTED", weight: float = 1.0) -> None:
2897
+ edges.append({"source": src, "target": tgt, "relation": relation,
2898
+ "confidence": confidence, "source_file": str_path,
2899
+ "source_location": f"L{line}", "weight": weight})
2900
+
2901
+ file_nid = _make_id(str(path))
2902
+ add_node(file_nid, path.name, 1)
2903
+
2904
+ _IMPORT_KEYWORDS = frozenset({"alias", "import", "require", "use"})
2905
+
2906
+ def _get_alias_text(node) -> str | None:
2907
+ for child in node.children:
2908
+ if child.type == "alias":
2909
+ return source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
2910
+ return None
2911
+
2912
+ def walk(node, parent_module_nid: str | None = None) -> None:
2913
+ if node.type != "call":
2914
+ for child in node.children:
2915
+ walk(child, parent_module_nid)
2916
+ return
2917
+
2918
+ identifier_node = None
2919
+ arguments_node = None
2920
+ do_block_node = None
2921
+ for child in node.children:
2922
+ if child.type == "identifier":
2923
+ identifier_node = child
2924
+ elif child.type == "arguments":
2925
+ arguments_node = child
2926
+ elif child.type == "do_block":
2927
+ do_block_node = child
2928
+
2929
+ if identifier_node is None:
2930
+ for child in node.children:
2931
+ walk(child, parent_module_nid)
2932
+ return
2933
+
2934
+ keyword = source[identifier_node.start_byte:identifier_node.end_byte].decode("utf-8", errors="replace")
2935
+ line = node.start_point[0] + 1
2936
+
2937
+ if keyword == "defmodule":
2938
+ module_name = _get_alias_text(arguments_node) if arguments_node else None
2939
+ if not module_name:
2940
+ return
2941
+ module_nid = _make_id(stem, module_name)
2942
+ add_node(module_nid, module_name, line)
2943
+ add_edge(file_nid, module_nid, "contains", line)
2944
+ if do_block_node:
2945
+ for child in do_block_node.children:
2946
+ walk(child, parent_module_nid=module_nid)
2947
+ return
2948
+
2949
+ if keyword in ("def", "defp"):
2950
+ func_name = None
2951
+ if arguments_node:
2952
+ for child in arguments_node.children:
2953
+ if child.type == "call":
2954
+ for sub in child.children:
2955
+ if sub.type == "identifier":
2956
+ func_name = source[sub.start_byte:sub.end_byte].decode("utf-8", errors="replace")
2957
+ break
2958
+ elif child.type == "identifier":
2959
+ func_name = source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
2960
+ break
2961
+ if not func_name:
2962
+ return
2963
+ container = parent_module_nid or file_nid
2964
+ func_nid = _make_id(container, func_name)
2965
+ add_node(func_nid, f"{func_name}()", line)
2966
+ if parent_module_nid:
2967
+ add_edge(parent_module_nid, func_nid, "method", line)
2968
+ else:
2969
+ add_edge(file_nid, func_nid, "contains", line)
2970
+ if do_block_node:
2971
+ function_bodies.append((func_nid, do_block_node))
2972
+ return
2973
+
2974
+ if keyword in _IMPORT_KEYWORDS and arguments_node:
2975
+ module_name = _get_alias_text(arguments_node)
2976
+ if module_name:
2977
+ tgt_nid = _make_id(module_name)
2978
+ add_edge(file_nid, tgt_nid, "imports", line)
2979
+ return
2980
+
2981
+ for child in node.children:
2982
+ walk(child, parent_module_nid)
2983
+
2984
+ walk(root)
2985
+
2986
+ label_to_nid: dict[str, str] = {}
2987
+ for n in nodes:
2988
+ normalised = n["label"].strip("()").lstrip(".")
2989
+ label_to_nid[normalised.lower()] = n["id"]
2990
+
2991
+ seen_call_pairs: set[tuple[str, str]] = set()
2992
+ raw_calls: list[dict] = []
2993
+ _SKIP_KEYWORDS = frozenset({
2994
+ "def", "defp", "defmodule", "defmacro", "defmacrop",
2995
+ "defstruct", "defprotocol", "defimpl", "defguard",
2996
+ "alias", "import", "require", "use",
2997
+ "if", "unless", "case", "cond", "with", "for",
2998
+ })
2999
+
3000
+ def walk_calls(node, caller_nid: str) -> None:
3001
+ if node.type != "call":
3002
+ for child in node.children:
3003
+ walk_calls(child, caller_nid)
3004
+ return
3005
+ for child in node.children:
3006
+ if child.type == "identifier":
3007
+ kw = source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
3008
+ if kw in _SKIP_KEYWORDS:
3009
+ for c in node.children:
3010
+ walk_calls(c, caller_nid)
3011
+ return
3012
+ break
3013
+ callee_name: str | None = None
3014
+ for child in node.children:
3015
+ if child.type == "dot":
3016
+ dot_text = source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
3017
+ parts = dot_text.rstrip(".").split(".")
3018
+ if parts:
3019
+ callee_name = parts[-1]
3020
+ break
3021
+ if child.type == "identifier":
3022
+ callee_name = source[child.start_byte:child.end_byte].decode("utf-8", errors="replace")
3023
+ break
3024
+ if callee_name:
3025
+ tgt_nid = label_to_nid.get(callee_name.lower())
3026
+ if tgt_nid and tgt_nid != caller_nid:
3027
+ pair = (caller_nid, tgt_nid)
3028
+ if pair not in seen_call_pairs:
3029
+ seen_call_pairs.add(pair)
3030
+ add_edge(caller_nid, tgt_nid, "calls",
3031
+ node.start_point[0] + 1, confidence="EXTRACTED", weight=1.0)
3032
+ else:
3033
+ raw_calls.append({
3034
+ "caller_nid": caller_nid,
3035
+ "callee": callee_name,
3036
+ "source_file": str_path,
3037
+ "source_location": f"L{node.start_point[0] + 1}",
3038
+ })
3039
+ for child in node.children:
3040
+ walk_calls(child, caller_nid)
3041
+
3042
+ for caller_nid, body in function_bodies:
3043
+ walk_calls(body, caller_nid)
3044
+
3045
+ clean_edges = [e for e in edges if e["source"] in seen_ids and
3046
+ (e["target"] in seen_ids or e["relation"] == "imports")]
3047
+ return {"nodes": nodes, "edges": clean_edges, "raw_calls": raw_calls, "input_tokens": 0, "output_tokens": 0}
3048
+
3049
+
3050
+ # ── Main extract and collect_files ────────────────────────────────────────────
3051
+
3052
+
3053
+ def _check_tree_sitter_version() -> None:
3054
+ """Raise a clear error if tree-sitter is too old for the new Language API."""
3055
+ try:
3056
+ from tree_sitter import LANGUAGE_VERSION
3057
+ except ImportError:
3058
+ raise ImportError(
3059
+ "tree-sitter is not installed. Run: pip install 'tree-sitter>=0.23.0'"
3060
+ )
3061
+ # Language API v2 starts at LANGUAGE_VERSION 14
3062
+ if LANGUAGE_VERSION < 14:
3063
+ import tree_sitter as _ts
3064
+ raise RuntimeError(
3065
+ f"tree-sitter {getattr(_ts, '__version__', 'unknown')} is too old. "
3066
+ f"graphify requires tree-sitter >= 0.23.0 (Language API v2). "
3067
+ f"Run: pip install --upgrade tree-sitter"
3068
+ )
3069
+
3070
+
3071
+ def extract(paths: list[Path], cache_root: Path | None = None) -> dict:
3072
+ """Extract AST nodes and edges from a list of code files.
3073
+
3074
+ Two-pass process:
3075
+ 1. Per-file structural extraction (classes, functions, imports)
3076
+ 2. Cross-file import resolution: turns file-level imports into
3077
+ class-level INFERRED edges (DigestAuth --uses--> Response)
3078
+
3079
+ Args:
3080
+ paths: files to extract from
3081
+ cache_root: explicit root for graphify-out/cache/ (overrides the
3082
+ inferred common path prefix). Pass Path('.') when running on a
3083
+ subdirectory so the cache stays at ./graphify-out/cache/.
3084
+ """
3085
+ _check_tree_sitter_version()
3086
+ per_file: list[dict] = []
3087
+
3088
+ # Infer a common root for cache keys
3089
+ try:
3090
+ if not paths:
3091
+ root = Path(".")
3092
+ elif len(paths) == 1:
3093
+ root = paths[0].parent
3094
+ else:
3095
+ common_len = sum(
3096
+ 1 for i in range(min(len(p.parts) for p in paths))
3097
+ if len({p.parts[i] for p in paths}) == 1
3098
+ )
3099
+ root = Path(*paths[0].parts[:common_len]) if common_len else Path(".")
3100
+ except Exception:
3101
+ root = Path(".")
3102
+
3103
+ _DISPATCH: dict[str, Any] = {
3104
+ ".py": extract_python,
3105
+ ".js": extract_js,
3106
+ ".jsx": extract_js,
3107
+ ".mjs": extract_js,
3108
+ ".ts": extract_js,
3109
+ ".tsx": extract_js,
3110
+ ".go": extract_go,
3111
+ ".rs": extract_rust,
3112
+ ".java": extract_java,
3113
+ ".c": extract_c,
3114
+ ".h": extract_c,
3115
+ ".cpp": extract_cpp,
3116
+ ".cc": extract_cpp,
3117
+ ".cxx": extract_cpp,
3118
+ ".hpp": extract_cpp,
3119
+ ".rb": extract_ruby,
3120
+ ".cs": extract_csharp,
3121
+ ".kt": extract_kotlin,
3122
+ ".kts": extract_kotlin,
3123
+ ".scala": extract_scala,
3124
+ ".php": extract_php,
3125
+ ".swift": extract_swift,
3126
+ ".lua": extract_lua,
3127
+ ".toc": extract_lua,
3128
+ ".zig": extract_zig,
3129
+ ".ps1": extract_powershell,
3130
+ ".ex": extract_elixir,
3131
+ ".exs": extract_elixir,
3132
+ ".m": extract_objc,
3133
+ ".mm": extract_objc,
3134
+ ".jl": extract_julia,
3135
+ ".vue": extract_js,
3136
+ ".svelte": extract_js,
3137
+ ".dart": extract_dart,
3138
+ ".v": extract_verilog,
3139
+ ".sv": extract_verilog,
3140
+ }
3141
+
3142
+ total = len(paths)
3143
+ _PROGRESS_INTERVAL = 100
3144
+ for i, path in enumerate(paths):
3145
+ if total >= _PROGRESS_INTERVAL and i % _PROGRESS_INTERVAL == 0 and i > 0:
3146
+ print(f" AST extraction: {i}/{total} files ({i * 100 // total}%)", flush=True)
3147
+ # .blade.php must be checked before suffix lookup since Path.suffix returns .php
3148
+ if path.name.endswith(".blade.php"):
3149
+ extractor = extract_blade
3150
+ else:
3151
+ extractor = _DISPATCH.get(path.suffix)
3152
+ if extractor is None:
3153
+ continue
3154
+ cached = load_cached(path, cache_root or root)
3155
+ if cached is not None:
3156
+ per_file.append(cached)
3157
+ continue
3158
+ result = extractor(path)
3159
+ if "error" not in result:
3160
+ save_cached(path, result, cache_root or root)
3161
+ per_file.append(result)
3162
+ if total >= _PROGRESS_INTERVAL:
3163
+ print(f" AST extraction: {total}/{total} files (100%)", flush=True)
3164
+
3165
+ all_nodes: list[dict] = []
3166
+ all_edges: list[dict] = []
3167
+ for result in per_file:
3168
+ all_nodes.extend(result.get("nodes", []))
3169
+ all_edges.extend(result.get("edges", []))
3170
+
3171
+ # Add cross-file class-level edges (Python only - uses Python parser internally)
3172
+ py_paths = [p for p in paths if p.suffix == ".py"]
3173
+ if py_paths:
3174
+ py_results = [r for r, p in zip(per_file, paths) if p.suffix == ".py"]
3175
+ try:
3176
+ cross_file_edges = _resolve_cross_file_imports(py_results, py_paths)
3177
+ all_edges.extend(cross_file_edges)
3178
+ except Exception as exc:
3179
+ import logging
3180
+ logging.getLogger(__name__).warning("Cross-file import resolution failed, skipping: %s", exc)
3181
+
3182
+ # Cross-file call resolution for all languages
3183
+ # Each extractor saved unresolved calls in raw_calls. Now that we have all
3184
+ # nodes from all files, resolve any callee that exists in another file.
3185
+ global_label_to_nid: dict[str, str] = {}
3186
+ for n in all_nodes:
3187
+ raw = n.get("label", "")
3188
+ normalised = raw.strip("()").lstrip(".")
3189
+ if normalised:
3190
+ global_label_to_nid[normalised.lower()] = n["id"]
3191
+
3192
+ existing_pairs = {(e["source"], e["target"]) for e in all_edges}
3193
+ for result in per_file:
3194
+ for rc in result.get("raw_calls", []):
3195
+ callee = rc.get("callee", "")
3196
+ if not callee:
3197
+ continue
3198
+ tgt = global_label_to_nid.get(callee.lower())
3199
+ caller = rc["caller_nid"]
3200
+ if tgt and tgt != caller and (caller, tgt) not in existing_pairs:
3201
+ existing_pairs.add((caller, tgt))
3202
+ all_edges.append({
3203
+ "source": caller,
3204
+ "target": tgt,
3205
+ "relation": "calls",
3206
+ "confidence": "INFERRED",
3207
+ "confidence_score": 0.8,
3208
+ "source_file": rc.get("source_file", ""),
3209
+ "source_location": rc.get("source_location"),
3210
+ "weight": 1.0,
3211
+ })
3212
+
3213
+ return {
3214
+ "nodes": all_nodes,
3215
+ "edges": all_edges,
3216
+ "input_tokens": 0,
3217
+ "output_tokens": 0,
3218
+ }
3219
+
3220
+
3221
+ def collect_files(target: Path, *, follow_symlinks: bool = False, root: Path | None = None) -> list[Path]:
3222
+ if target.is_file():
3223
+ return [target]
3224
+ _EXTENSIONS = {
3225
+ ".py", ".js", ".ts", ".tsx", ".go", ".rs",
3226
+ ".java", ".c", ".h", ".cpp", ".cc", ".cxx", ".hpp",
3227
+ ".rb", ".cs", ".kt", ".kts", ".scala", ".php", ".swift",
3228
+ ".lua", ".toc", ".zig", ".ps1",
3229
+ ".m", ".mm",
3230
+ }
3231
+ from graphify.detect import _load_graphifyignore, _is_ignored
3232
+ ignore_root = root if root is not None else target
3233
+ patterns = _load_graphifyignore(ignore_root)
3234
+
3235
+ def _ignored(p: Path) -> bool:
3236
+ return bool(patterns and _is_ignored(p, ignore_root, patterns))
3237
+
3238
+ if not follow_symlinks:
3239
+ results: list[Path] = []
3240
+ for ext in sorted(_EXTENSIONS):
3241
+ results.extend(
3242
+ p for p in target.rglob(f"*{ext}")
3243
+ if not any(part.startswith(".") for part in p.parts)
3244
+ and not _ignored(p)
3245
+ )
3246
+ return sorted(results)
3247
+ # Walk with symlink following + cycle detection
3248
+ results = []
3249
+ for dirpath, dirnames, filenames in os.walk(target, followlinks=True):
3250
+ if os.path.islink(dirpath):
3251
+ real = os.path.realpath(dirpath)
3252
+ parent_real = os.path.realpath(os.path.dirname(dirpath))
3253
+ if parent_real == real or parent_real.startswith(real + os.sep):
3254
+ dirnames.clear()
3255
+ continue
3256
+ dp = Path(dirpath)
3257
+ if any(part.startswith(".") for part in dp.parts):
3258
+ dirnames.clear()
3259
+ continue
3260
+ for fname in filenames:
3261
+ p = dp / fname
3262
+ if p.suffix in _EXTENSIONS and not fname.startswith(".") and not _ignored(p):
3263
+ results.append(p)
3264
+ return sorted(results)
3265
+
3266
+
3267
+ if __name__ == "__main__":
3268
+ if len(sys.argv) < 2:
3269
+ print("Usage: python -m graphify.extract <file_or_dir> ...", file=sys.stderr)
3270
+ sys.exit(1)
3271
+
3272
+ paths: list[Path] = []
3273
+ for arg in sys.argv[1:]:
3274
+ paths.extend(collect_files(Path(arg)))
3275
+
3276
+ result = extract(paths)
3277
+ print(json.dumps(result, indent=2))