patchi 0.7.5__py3-none-any.whl

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 (496) hide show
  1. patchi/__init__.py +1 -0
  2. patchi/__main__.py +13 -0
  3. patchi/cli/__init__.py +0 -0
  4. patchi/cli/command_families.py +235 -0
  5. patchi/cli/commands/__init__.py +0 -0
  6. patchi/cli/commands/access_cmd.py +65 -0
  7. patchi/cli/commands/agent_stats_cmd.py +273 -0
  8. patchi/cli/commands/agents_cmd.py +187 -0
  9. patchi/cli/commands/ai_cmd.py +308 -0
  10. patchi/cli/commands/assure_cmd.py +538 -0
  11. patchi/cli/commands/audit_cmd.py +286 -0
  12. patchi/cli/commands/auto_cmd.py +110 -0
  13. patchi/cli/commands/blame_cmd.py +53 -0
  14. patchi/cli/commands/chains_cmd.py +327 -0
  15. patchi/cli/commands/charter_cmd.py +173 -0
  16. patchi/cli/commands/chat_cmd.py +597 -0
  17. patchi/cli/commands/check_cmd.py +232 -0
  18. patchi/cli/commands/cleanup_cmd.py +328 -0
  19. patchi/cli/commands/cockpit_cmd.py +208 -0
  20. patchi/cli/commands/commands_cmd.py +160 -0
  21. patchi/cli/commands/cross_repo_cmd.py +113 -0
  22. patchi/cli/commands/deps_cmd.py +110 -0
  23. patchi/cli/commands/dev_check_cmd.py +342 -0
  24. patchi/cli/commands/dev_cmd.py +445 -0
  25. patchi/cli/commands/doctor_cmd.py +457 -0
  26. patchi/cli/commands/findings_cmd.py +187 -0
  27. patchi/cli/commands/fix_cmd.py +648 -0
  28. patchi/cli/commands/fix_review_cmd.py +273 -0
  29. patchi/cli/commands/heatmap_cmd.py +62 -0
  30. patchi/cli/commands/help_cmd.py +159 -0
  31. patchi/cli/commands/hosted_cmd.py +980 -0
  32. patchi/cli/commands/init.py +456 -0
  33. patchi/cli/commands/key_cmd.py +571 -0
  34. patchi/cli/commands/learn_cmd.py +140 -0
  35. patchi/cli/commands/link_cmd.py +104 -0
  36. patchi/cli/commands/log_cmd.py +57 -0
  37. patchi/cli/commands/memory_cmd.py +215 -0
  38. patchi/cli/commands/model_cmd.py +207 -0
  39. patchi/cli/commands/notify_cmd.py +213 -0
  40. patchi/cli/commands/notify_prompts.py +29 -0
  41. patchi/cli/commands/patch_cmd.py +231 -0
  42. patchi/cli/commands/plan_cmd.py +128 -0
  43. patchi/cli/commands/plugins_cmd.py +307 -0
  44. patchi/cli/commands/queue_cmd.py +152 -0
  45. patchi/cli/commands/quick_cmd.py +159 -0
  46. patchi/cli/commands/ready_cmd.py +292 -0
  47. patchi/cli/commands/report_cmd.py +416 -0
  48. patchi/cli/commands/restrict_cmd.py +127 -0
  49. patchi/cli/commands/review_cmd.py +225 -0
  50. patchi/cli/commands/rules_cmd.py +233 -0
  51. patchi/cli/commands/scan_cmd.py +1908 -0
  52. patchi/cli/commands/settings_cmd.py +45 -0
  53. patchi/cli/commands/status_cmd.py +679 -0
  54. patchi/cli/commands/test_cmd.py +1052 -0
  55. patchi/cli/commands/trend_cmd.py +296 -0
  56. patchi/cli/commands/undo_cmd.py +235 -0
  57. patchi/cli/commands/update_cmd.py +345 -0
  58. patchi/cli/commands/verify_cmd.py +56 -0
  59. patchi/cli/commands/vr_cmd.py +440 -0
  60. patchi/cli/commands/watch_cmd.py +304 -0
  61. patchi/cli/commands/web_cmd.py +114 -0
  62. patchi/cli/console.py +101 -0
  63. patchi/cli/display/__init__.py +1 -0
  64. patchi/cli/display/live_audit.py +269 -0
  65. patchi/cli/display/live_progress.py +169 -0
  66. patchi/cli/framework.py +241 -0
  67. patchi/cli/logo.py +128 -0
  68. patchi/cli/main.py +222 -0
  69. patchi/cli/registry.py +1171 -0
  70. patchi/cli/style.py +68 -0
  71. patchi/cli/themes.py +187 -0
  72. patchi/cli/ux.py +444 -0
  73. patchi/core/__init__.py +0 -0
  74. patchi/core/agents/__init__.py +0 -0
  75. patchi/core/agents/api_fuzzer_agent.py +141 -0
  76. patchi/core/agents/attack_agent.py +242 -0
  77. patchi/core/agents/base.py +1012 -0
  78. patchi/core/agents/bug_predictor_agent.py +58 -0
  79. patchi/core/agents/build_tool_validator.py +150 -0
  80. patchi/core/agents/cache.py +192 -0
  81. patchi/core/agents/chaos_agent.py +133 -0
  82. patchi/core/agents/cicd_generator.py +240 -0
  83. patchi/core/agents/comment_scanner.py +280 -0
  84. patchi/core/agents/console_logging_agent.py +156 -0
  85. patchi/core/agents/coordinator.py +858 -0
  86. patchi/core/agents/core_scanner.py +269 -0
  87. patchi/core/agents/coverage_prioritizer.py +181 -0
  88. patchi/core/agents/dead_code_hygiene.py +296 -0
  89. patchi/core/agents/dead_code_scanner.py +692 -0
  90. patchi/core/agents/dependency_scanner.py +725 -0
  91. patchi/core/agents/doc_claim_agent.py +573 -0
  92. patchi/core/agents/duplicate_scanner.py +568 -0
  93. patchi/core/agents/env_scanner.py +410 -0
  94. patchi/core/agents/feature_flag_agent.py +89 -0
  95. patchi/core/agents/frontend_framework_agent.py +133 -0
  96. patchi/core/agents/goroutine_agent.py +82 -0
  97. patchi/core/agents/governor.py +1972 -0
  98. patchi/core/agents/i18n_agent.py +95 -0
  99. patchi/core/agents/license_compliance.py +247 -0
  100. patchi/core/agents/linking_agent.py +251 -0
  101. patchi/core/agents/memory_profiler_agent.py +102 -0
  102. patchi/core/agents/modernization_agent.py +84 -0
  103. patchi/core/agents/mutation_agent.py +174 -0
  104. patchi/core/agents/nl_query_agent.py +74 -0
  105. patchi/core/agents/promise_rejection_agent.py +100 -0
  106. patchi/core/agents/refactoring_agent.py +286 -0
  107. patchi/core/agents/resource_leak_agent.py +91 -0
  108. patchi/core/agents/route_graph_scanner.py +951 -0
  109. patchi/core/agents/run_agent.py +106 -0
  110. patchi/core/agents/rust_unwrap_agent.py +100 -0
  111. patchi/core/agents/sbom_generator.py +284 -0
  112. patchi/core/agents/scanners.py +67 -0
  113. patchi/core/agents/side/__init__.py +1 -0
  114. patchi/core/agents/side/build_agent.py +65 -0
  115. patchi/core/agents/side/format_agent.py +50 -0
  116. patchi/core/agents/side/install_agent.py +63 -0
  117. patchi/core/agents/side_file_scanner.py +549 -0
  118. patchi/core/agents/snapshot_drift_detector.py +146 -0
  119. patchi/core/agents/spa_route_inventory.py +203 -0
  120. patchi/core/agents/test_scanner.py +442 -0
  121. patchi/core/agents/tool_health.py +109 -0
  122. patchi/core/agents/tool_runner.py +162 -0
  123. patchi/core/agents/type_scanner.py +168 -0
  124. patchi/core/agents/ui_scanner.py +558 -0
  125. patchi/core/ai/__init__.py +20 -0
  126. patchi/core/ai/agent_audit.py +153 -0
  127. patchi/core/ai/agent_profiler.py +277 -0
  128. patchi/core/ai/client.py +499 -0
  129. patchi/core/ai/cost_tracker.py +107 -0
  130. patchi/core/ai/model_router.py +318 -0
  131. patchi/core/ai/orchestrator.py +625 -0
  132. patchi/core/ai/prompts.py +681 -0
  133. patchi/core/ai/smart.py +302 -0
  134. patchi/core/ai/tool_executor.py +24 -0
  135. patchi/core/ai/tools/__init__.py +10 -0
  136. patchi/core/ai/tools/executor.py +621 -0
  137. patchi/core/ai/tools/realize.py +1160 -0
  138. patchi/core/ai/tools/registry.py +1577 -0
  139. patchi/core/assurance/__init__.py +33 -0
  140. patchi/core/assurance/builder.py +211 -0
  141. patchi/core/assurance/graph.py +342 -0
  142. patchi/core/assurance/invariants.py +168 -0
  143. patchi/core/atomic.py +61 -0
  144. patchi/core/attackers/__init__.py +17 -0
  145. patchi/core/attackers/api_abuse.py +89 -0
  146. patchi/core/attackers/auth_bypass.py +73 -0
  147. patchi/core/attackers/base.py +91 -0
  148. patchi/core/attackers/business_logic.py +88 -0
  149. patchi/core/attackers/planner.py +108 -0
  150. patchi/core/attackers/priv_escalation.py +88 -0
  151. patchi/core/attackers/recon.py +87 -0
  152. patchi/core/brain/__init__.py +0 -0
  153. patchi/core/brain/ast_utils/__init__.py +81 -0
  154. patchi/core/brain/ast_utils/assignments.py +219 -0
  155. patchi/core/brain/ast_utils/calls.py +139 -0
  156. patchi/core/brain/ast_utils/config.py +132 -0
  157. patchi/core/brain/ast_utils/control_flow.py +74 -0
  158. patchi/core/brain/ast_utils/dead_symbols.py +205 -0
  159. patchi/core/brain/ast_utils/decorators.py +108 -0
  160. patchi/core/brain/ast_utils/helpers.py +144 -0
  161. patchi/core/brain/ast_utils/imports.py +169 -0
  162. patchi/core/brain/ast_utils/scan.py +143 -0
  163. patchi/core/brain/ast_utils/taint.py +310 -0
  164. patchi/core/brain/audit.py +379 -0
  165. patchi/core/brain/baseline.py +195 -0
  166. patchi/core/brain/blast_radius.py +312 -0
  167. patchi/core/brain/body_tags.py +144 -0
  168. patchi/core/brain/brain.py +1161 -0
  169. patchi/core/brain/brain_context.py +393 -0
  170. patchi/core/brain/brain_watcher.py +340 -0
  171. patchi/core/brain/charter.py +473 -0
  172. patchi/core/brain/classifier.py +214 -0
  173. patchi/core/brain/cockpit.py +350 -0
  174. patchi/core/brain/code_query.py +710 -0
  175. patchi/core/brain/contract.py +889 -0
  176. patchi/core/brain/contract_diff.py +155 -0
  177. patchi/core/brain/council.py +800 -0
  178. patchi/core/brain/cross_repo.py +104 -0
  179. patchi/core/brain/dead_code_tools.py +157 -0
  180. patchi/core/brain/doc_validator.py +197 -0
  181. patchi/core/brain/domain_activator.py +2183 -0
  182. patchi/core/brain/enriched_context.py +220 -0
  183. patchi/core/brain/file_corpus.py +220 -0
  184. patchi/core/brain/framework.py +953 -0
  185. patchi/core/brain/freshness.py +273 -0
  186. patchi/core/brain/git_aware.py +206 -0
  187. patchi/core/brain/heatmap.py +104 -0
  188. patchi/core/brain/ignore_parser.py +153 -0
  189. patchi/core/brain/import_graph.py +486 -0
  190. patchi/core/brain/languages.py +397 -0
  191. patchi/core/brain/layered_brain.py +419 -0
  192. patchi/core/brain/learning.py +229 -0
  193. patchi/core/brain/orphaned_endpoints.py +313 -0
  194. patchi/core/brain/personas/__init__.py +648 -0
  195. patchi/core/brain/personas/bad_user.py +165 -0
  196. patchi/core/brain/personas/base.py +349 -0
  197. patchi/core/brain/proactive.py +807 -0
  198. patchi/core/brain/project_context.py +121 -0
  199. patchi/core/brain/project_reader.py +521 -0
  200. patchi/core/brain/reasoning.py +269 -0
  201. patchi/core/brain/route_detector/__init__.py +15 -0
  202. patchi/core/brain/route_detector/base.py +31 -0
  203. patchi/core/brain/route_detector/csharp.py +209 -0
  204. patchi/core/brain/route_detector/file_based.py +81 -0
  205. patchi/core/brain/route_detector/go.py +152 -0
  206. patchi/core/brain/route_detector/java.py +195 -0
  207. patchi/core/brain/route_detector/javascript.py +197 -0
  208. patchi/core/brain/route_detector/php.py +158 -0
  209. patchi/core/brain/route_detector/python.py +193 -0
  210. patchi/core/brain/route_detector/registry.py +25 -0
  211. patchi/core/brain/route_detector/ruby.py +134 -0
  212. patchi/core/brain/route_detector/rust.py +216 -0
  213. patchi/core/brain/route_detector/swift.py +118 -0
  214. patchi/core/brain/route_mapper.py +265 -0
  215. patchi/core/brain/scanner.py +2136 -0
  216. patchi/core/brain/secrets.py +280 -0
  217. patchi/core/brain/symbol_graph.py +1955 -0
  218. patchi/core/brain/team.py +28 -0
  219. patchi/core/brain/trace_log.py +132 -0
  220. patchi/core/brain/type_checker/__init__.py +65 -0
  221. patchi/core/brain/type_checker/base.py +60 -0
  222. patchi/core/brain/type_checker/csharp.py +59 -0
  223. patchi/core/brain/type_checker/dart.py +59 -0
  224. patchi/core/brain/type_checker/go.py +81 -0
  225. patchi/core/brain/type_checker/java.py +75 -0
  226. patchi/core/brain/type_checker/kotlin.py +75 -0
  227. patchi/core/brain/type_checker/php.py +88 -0
  228. patchi/core/brain/type_checker/python.py +87 -0
  229. patchi/core/brain/type_checker/rust.py +81 -0
  230. patchi/core/brain/type_checker/swift.py +59 -0
  231. patchi/core/brain/type_checker/typescript.py +298 -0
  232. patchi/core/brain/understander.py +130 -0
  233. patchi/core/brain/verify.py +379 -0
  234. patchi/core/campaigns/__init__.py +14 -0
  235. patchi/core/campaigns/base.py +62 -0
  236. patchi/core/campaigns/data_flow_abuse.py +153 -0
  237. patchi/core/campaigns/orchestrator.py +140 -0
  238. patchi/core/campaigns/state_transitions.py +178 -0
  239. patchi/core/ci_bundle.py +104 -0
  240. patchi/core/config.py +311 -0
  241. patchi/core/constants.py +245 -0
  242. patchi/core/debug/__init__.py +25 -0
  243. patchi/core/debug/adapters/__init__.py +11 -0
  244. patchi/core/debug/adapters/codelldb.py +379 -0
  245. patchi/core/debug/adapters/node.py +222 -0
  246. patchi/core/debug/adapters/powershell.py +210 -0
  247. patchi/core/debug/adapters/python.py +244 -0
  248. patchi/core/debug/capture.py +210 -0
  249. patchi/core/debug/dap_client.py +268 -0
  250. patchi/core/detector/__init__.py +0 -0
  251. patchi/core/detector/audit.py +194 -0
  252. patchi/core/detector/bus.py +226 -0
  253. patchi/core/detector/dispatcher.py +275 -0
  254. patchi/core/detector/event.py +254 -0
  255. patchi/core/detector/sigma_engine.py +378 -0
  256. patchi/core/detector/triage.py +289 -0
  257. patchi/core/export/sarif.py +113 -0
  258. patchi/core/fix/__init__.py +17 -0
  259. patchi/core/fix/applier.py +574 -0
  260. patchi/core/fix/base.py +510 -0
  261. patchi/core/fix/code_fixer.py +169 -0
  262. patchi/core/fix/dead_code_remover.py +131 -0
  263. patchi/core/fix/fix_agents.py +775 -0
  264. patchi/core/fix/patch.py +444 -0
  265. patchi/core/fix/risk_gate.py +383 -0
  266. patchi/core/fix/security_fixer.py +93 -0
  267. patchi/core/fix/verify_loop.py +204 -0
  268. patchi/core/fuzz/__init__.py +8 -0
  269. patchi/core/fuzz/corpus.py +137 -0
  270. patchi/core/fuzz/input_fuzzer.py +256 -0
  271. patchi/core/fuzz/sequence_fuzzer.py +162 -0
  272. patchi/core/fuzz/state_fuzzer.py +236 -0
  273. patchi/core/health.py +407 -0
  274. patchi/core/hosted/__init__.py +1 -0
  275. patchi/core/hosted/anomaly.py +298 -0
  276. patchi/core/hosted/audit_log.py +102 -0
  277. patchi/core/hosted/compliance_report.py +148 -0
  278. patchi/core/hosted/ip_reputation.py +169 -0
  279. patchi/core/hosted/log_parsers.py +251 -0
  280. patchi/core/hosted/tokens.py +127 -0
  281. patchi/core/hosted/watchlist.py +164 -0
  282. patchi/core/hosted/webhooks.py +156 -0
  283. patchi/core/memory.py +437 -0
  284. patchi/core/notifications/__init__.py +18 -0
  285. patchi/core/notifications/channels.py +192 -0
  286. patchi/core/notifications/digest.py +141 -0
  287. patchi/core/notifications/escalation.py +158 -0
  288. patchi/core/notifications/notifier.py +182 -0
  289. patchi/core/notifications/quiet_hours.py +69 -0
  290. patchi/core/plugins/__init__.py +48 -0
  291. patchi/core/plugins/analyzer.py +326 -0
  292. patchi/core/plugins/analyzers/__init__.py +6 -0
  293. patchi/core/plugins/analyzers/dead_code_analyzer.py +120 -0
  294. patchi/core/plugins/analyzers/secret_scanner.py +158 -0
  295. patchi/core/plugins/analyzers/todo_scanner.py +115 -0
  296. patchi/core/plugins/registry.py +335 -0
  297. patchi/core/queue.py +375 -0
  298. patchi/core/reliability/__init__.py +13 -0
  299. patchi/core/reliability/chaos.py +163 -0
  300. patchi/core/reliability/fault_injector.py +172 -0
  301. patchi/core/reliability/idempotency.py +128 -0
  302. patchi/core/reliability/recovery.py +147 -0
  303. patchi/core/runtime/__init__.py +5 -0
  304. patchi/core/runtime/tracer.py +230 -0
  305. patchi/core/scan_bus.py +156 -0
  306. patchi/core/security/__init__.py +1 -0
  307. patchi/core/security/ai_validator.py +462 -0
  308. patchi/core/security/app_mapper.py +216 -0
  309. patchi/core/security/app_profile.py +464 -0
  310. patchi/core/security/attack_feedback.py +141 -0
  311. patchi/core/security/attack_scenarios/__init__.py +1 -0
  312. patchi/core/security/attack_tree.py +176 -0
  313. patchi/core/security/auth_audit_agent.py +344 -0
  314. patchi/core/security/authz_agent.py +602 -0
  315. patchi/core/security/auto_fix_proactive.py +620 -0
  316. patchi/core/security/auto_fixer.py +660 -0
  317. patchi/core/security/auto_ticket.py +53 -0
  318. patchi/core/security/bandit_agent.py +152 -0
  319. patchi/core/security/blast_radius.py +226 -0
  320. patchi/core/security/browser_tester.py +333 -0
  321. patchi/core/security/business_logic_agent.py +263 -0
  322. patchi/core/security/catch_block_auditor.py +395 -0
  323. patchi/core/security/cdn_cache_agent.py +357 -0
  324. patchi/core/security/chain_analyzer.py +321 -0
  325. patchi/core/security/chain_to_assurance.py +168 -0
  326. patchi/core/security/charter.py +510 -0
  327. patchi/core/security/cloud_waf_detector.py +295 -0
  328. patchi/core/security/codeql_agent.py +233 -0
  329. patchi/core/security/compliance_agent.py +348 -0
  330. patchi/core/security/confidence_gate.py +423 -0
  331. patchi/core/security/container_scanner.py +282 -0
  332. patchi/core/security/crypto_agent.py +378 -0
  333. patchi/core/security/cve_monitor.py +262 -0
  334. patchi/core/security/dast_agent.py +865 -0
  335. patchi/core/security/defectdojo.py +238 -0
  336. patchi/core/security/defenders/__init__.py +75 -0
  337. patchi/core/security/defenders/auth_middleware.py +115 -0
  338. patchi/core/security/defenders/base.py +97 -0
  339. patchi/core/security/defenders/block_ip.py +71 -0
  340. patchi/core/security/defenders/block_ws_origin.py +36 -0
  341. patchi/core/security/defenders/crypto_fix.py +118 -0
  342. patchi/core/security/defenders/enforce_rate_limit.py +108 -0
  343. patchi/core/security/defenders/fix_code.py +93 -0
  344. patchi/core/security/defenders/invalidate_session.py +40 -0
  345. patchi/core/security/defenders/patch_config.py +70 -0
  346. patchi/core/security/defenders/rotate_secret.py +54 -0
  347. patchi/core/security/defenders/suspend_account.py +41 -0
  348. patchi/core/security/defenders/update_dependency.py +160 -0
  349. patchi/core/security/defense_layer.py +217 -0
  350. patchi/core/security/dependency_vulnerability_agent.py +616 -0
  351. patchi/core/security/detection_pipeline.py +289 -0
  352. patchi/core/security/dns_security_agent.py +497 -0
  353. patchi/core/security/domain_activator_v2.py +913 -0
  354. patchi/core/security/domain_loader.py +1004 -0
  355. patchi/core/security/domains/__init__.py +0 -0
  356. patchi/core/security/email_authentication_agent.py +417 -0
  357. patchi/core/security/env_var_validator.py +304 -0
  358. patchi/core/security/evidence.py +100 -0
  359. patchi/core/security/falco_runtime_agent.py +387 -0
  360. patchi/core/security/fix-playbooks/__init__.py +0 -0
  361. patchi/core/security/gated_finding.py +85 -0
  362. patchi/core/security/git_diff_activator.py +367 -0
  363. patchi/core/security/governance.py +208 -0
  364. patchi/core/security/gradual_gate.py +34 -0
  365. patchi/core/security/history.py +315 -0
  366. patchi/core/security/iac_scanner.py +250 -0
  367. patchi/core/security/ignore_expiry.py +111 -0
  368. patchi/core/security/ignore_learner.py +416 -0
  369. patchi/core/security/injection_agent.py +319 -0
  370. patchi/core/security/insecure_randomness_agent.py +288 -0
  371. patchi/core/security/intent_analyzer.py +326 -0
  372. patchi/core/security/jwt_agent.py +453 -0
  373. patchi/core/security/kubernetes_agent.py +638 -0
  374. patchi/core/security/layer2_orchestrator.py +341 -0
  375. patchi/core/security/llm_security_agent.py +252 -0
  376. patchi/core/security/misconfig_agent.py +600 -0
  377. patchi/core/security/mobile_security_agent.py +474 -0
  378. patchi/core/security/network_agent.py +376 -0
  379. patchi/core/security/noise_filter.py +274 -0
  380. patchi/core/security/orchestrator.py +313 -0
  381. patchi/core/security/pattern_context.py +125 -0
  382. patchi/core/security/pattern_loader.py +101 -0
  383. patchi/core/security/pentest/__init__.py +4 -0
  384. patchi/core/security/pentest/base.py +58 -0
  385. patchi/core/security/pentest/dalfox_adapter.py +40 -0
  386. patchi/core/security/pentest/ffuf_adapter.py +48 -0
  387. patchi/core/security/pentest/nuclei_adapter.py +50 -0
  388. patchi/core/security/pentest/registry.py +80 -0
  389. patchi/core/security/pentest/shannon_adapter.py +107 -0
  390. patchi/core/security/pentest/sqlmap_adapter.py +56 -0
  391. patchi/core/security/pentest/zap_adapter.py +52 -0
  392. patchi/core/security/plan_auditor.py +276 -0
  393. patchi/core/security/policy_engine.py +272 -0
  394. patchi/core/security/prechecks.py +172 -0
  395. patchi/core/security/precommit_hook.py +247 -0
  396. patchi/core/security/privacy_agent.py +361 -0
  397. patchi/core/security/push_notification_agent.py +417 -0
  398. patchi/core/security/pysa_agent.py +143 -0
  399. patchi/core/security/reasoning.py +442 -0
  400. patchi/core/security/red_team_agent.py +184 -0
  401. patchi/core/security/red_team_engine.py +1142 -0
  402. patchi/core/security/remediation.py +360 -0
  403. patchi/core/security/request_interceptor.py +294 -0
  404. patchi/core/security/runtime_validator.py +245 -0
  405. patchi/core/security/saml_sso_agent.py +401 -0
  406. patchi/core/security/sast_agent.py +213 -0
  407. patchi/core/security/sast_gate.py +95 -0
  408. patchi/core/security/scheduler.py +287 -0
  409. patchi/core/security/secrets_runtime_agent.py +476 -0
  410. patchi/core/security/security_agents.py +175 -0
  411. patchi/core/security/security_config.py +551 -0
  412. patchi/core/security/security_probe.py +762 -0
  413. patchi/core/security/security_taint.py +1005 -0
  414. patchi/core/security/sensitive_data_agent.py +271 -0
  415. patchi/core/security/service_mesh_agent.py +412 -0
  416. patchi/core/security/session_management_agent.py +245 -0
  417. patchi/core/security/ssrf_agent.py +422 -0
  418. patchi/core/security/supply_chain.py +449 -0
  419. patchi/core/security/threat_model_generator.py +378 -0
  420. patchi/core/security/threat_model_updater.py +196 -0
  421. patchi/core/security/tool_adapters.py +135 -0
  422. patchi/core/security/tool_verify.py +173 -0
  423. patchi/core/security/websocket_security_agent.py +188 -0
  424. patchi/core/snapshot.py +242 -0
  425. patchi/core/tenant.py +386 -0
  426. patchi/core/testing/__init__.py +17 -0
  427. patchi/core/testing/_browser.py +147 -0
  428. patchi/core/testing/ads_agent.py +141 -0
  429. patchi/core/testing/api_contract_agent.py +450 -0
  430. patchi/core/testing/app_discovery_agent.py +641 -0
  431. patchi/core/testing/app_launcher.py +184 -0
  432. patchi/core/testing/browser_test_agent.py +516 -0
  433. patchi/core/testing/e2e_flow_agent.py +354 -0
  434. patchi/core/testing/flake_detector_agent.py +292 -0
  435. patchi/core/testing/gate.py +31 -0
  436. patchi/core/testing/live_test_runner.py +304 -0
  437. patchi/core/testing/live_v2/__init__.py +1 -0
  438. patchi/core/testing/live_v2/browser_pool.py +349 -0
  439. patchi/core/testing/live_v2/browser_test_runner.py +313 -0
  440. patchi/core/testing/live_v2/runner.py +626 -0
  441. patchi/core/testing/live_v2/screenshot_manager.py +474 -0
  442. patchi/core/testing/live_v2/stress_orchestrator.py +523 -0
  443. patchi/core/testing/live_v2/video_recorder.py +303 -0
  444. patchi/core/testing/regression_agent.py +302 -0
  445. patchi/core/testing/security_test_agent.py +253 -0
  446. patchi/core/testing/stress_test_agent.py +203 -0
  447. patchi/core/testing/test_agents.py +50 -0
  448. patchi/core/testing/test_config.py +194 -0
  449. patchi/core/testing/ui_accessibility_agent.py +324 -0
  450. patchi/core/testing/ui_button_agent.py +294 -0
  451. patchi/core/testing/ui_layout_agent.py +330 -0
  452. patchi/core/testing/unit_test_agent.py +1149 -0
  453. patchi/core/testing/visual_regression_agent.py +260 -0
  454. patchi/web/__init__.py +1 -0
  455. patchi/web/api/__init__.py +1 -0
  456. patchi/web/api/brain_map.py +114 -0
  457. patchi/web/api/charts.py +47 -0
  458. patchi/web/api/chat.py +224 -0
  459. patchi/web/api/cicd.py +576 -0
  460. patchi/web/api/dast_evidence.py +49 -0
  461. patchi/web/api/dev_check.py +320 -0
  462. patchi/web/api/fix.py +134 -0
  463. patchi/web/api/guard.py +65 -0
  464. patchi/web/api/hosted.py +128 -0
  465. patchi/web/api/hosted_v2.py +234 -0
  466. patchi/web/api/live_testing.py +699 -0
  467. patchi/web/api/scan.py +582 -0
  468. patchi/web/api/smart.py +111 -0
  469. patchi/web/api/tenant.py +117 -0
  470. patchi/web/api/tools.py +85 -0
  471. patchi/web/api_legacy.py +2401 -0
  472. patchi/web/app.py +310 -0
  473. patchi/web/events.py +483 -0
  474. patchi/web/routes/__init__.py +1 -0
  475. patchi/web/routes/assurance.py +541 -0
  476. patchi/web/routes/brain.py +36 -0
  477. patchi/web/routes/charter.py +227 -0
  478. patchi/web/routes/chat.py +23 -0
  479. patchi/web/routes/dashboard.py +558 -0
  480. patchi/web/routes/dashboard_v2.py +402 -0
  481. patchi/web/routes/findings.py +330 -0
  482. patchi/web/routes/guard.py +35 -0
  483. patchi/web/routes/history.py +126 -0
  484. patchi/web/routes/landing.py +52 -0
  485. patchi/web/routes/live_testing.py +24 -0
  486. patchi/web/routes/review.py +57 -0
  487. patchi/web/routes/self_improvement.py +110 -0
  488. patchi/web/routes/settings.py +49 -0
  489. patchi/web/routes/tokens.py +29 -0
  490. patchi/web/spawn.py +139 -0
  491. patchi/web/ws.py +141 -0
  492. patchi-0.7.5.dist-info/METADATA +315 -0
  493. patchi-0.7.5.dist-info/RECORD +496 -0
  494. patchi-0.7.5.dist-info/WHEEL +5 -0
  495. patchi-0.7.5.dist-info/licenses/LICENSE +60 -0
  496. patchi-0.7.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1955 @@
1
+ """
2
+ Symbol Graph — function/class/route-level dependency graph.
3
+
4
+ Parallel to ImportGraph (file-level). Uses tree-sitter for accurate
5
+ AST-based symbol extraction. Backed by SQLite for incremental updates.
6
+
7
+ SymbolNode types:
8
+ - function, async_function, method, async_method
9
+ - class
10
+ - route (web route handler)
11
+ - variable (module-level exported)
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import hashlib
16
+ import logging
17
+ import re
18
+ import sqlite3
19
+ import threading
20
+ import time
21
+ from dataclasses import dataclass, field
22
+ from enum import StrEnum
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ from patchi.core.brain.languages import Lang, detect_language, get_parser
27
+ from patchi.core.brain.scanner import FileInfo
28
+
29
+ # ── Constants ───────────────────────────────────────────────────────────────────
30
+
31
+ SYMBOL_GRAPH_DB = "symbol_graph.db"
32
+
33
+ # Serializes ensure_built()'s check-then-build so concurrent callers cannot
34
+ # race full rebuilds (DELETE + INSERT) on the same SQLite file.
35
+ _BUILD_LOCK = threading.Lock()
36
+
37
+ SCHEMA_SQL = """
38
+ CREATE TABLE IF NOT EXISTS meta (
39
+ key TEXT PRIMARY KEY,
40
+ value TEXT
41
+ );
42
+
43
+ CREATE TABLE IF NOT EXISTS symbols (
44
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
45
+ name TEXT NOT NULL,
46
+ kind TEXT NOT NULL,
47
+ file TEXT NOT NULL,
48
+ line INTEGER NOT NULL,
49
+ end_line INTEGER NOT NULL,
50
+ parent_id INTEGER,
51
+ docstring TEXT,
52
+ language TEXT NOT NULL,
53
+ hash TEXT NOT NULL,
54
+ decorators TEXT DEFAULT '',
55
+ params TEXT DEFAULT '',
56
+ is_exported INTEGER DEFAULT 0,
57
+ UNIQUE(name, file, line)
58
+ );
59
+
60
+ CREATE TABLE IF NOT EXISTS edges (
61
+ source_id INTEGER NOT NULL,
62
+ target_id INTEGER NOT NULL,
63
+ kind TEXT NOT NULL DEFAULT 'calls',
64
+ PRIMARY KEY (source_id, target_id, kind),
65
+ FOREIGN KEY (source_id) REFERENCES symbols(id),
66
+ FOREIGN KEY (target_id) REFERENCES symbols(id)
67
+ );
68
+
69
+ CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file);
70
+ CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind);
71
+ CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
72
+ CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id);
73
+ """
74
+
75
+ _RUST_ROUTE_ATTR_RE = re.compile(
76
+ r"#\[\s*(get|post|put|delete|patch|head|options|connect|trace)\s*\(\s*\"([^\"]+)\"\s*\)",
77
+ re.IGNORECASE,
78
+ )
79
+ _SVELTE_SCRIPT_RE = re.compile(r"<script[^>]*>", re.IGNORECASE)
80
+
81
+
82
+ # ── Symbol kind enum ────────────────────────────────────────────────────────────
83
+
84
+
85
+ _log = logging.getLogger("patchi.brain.symbol_graph")
86
+
87
+
88
+ class SymbolKind(StrEnum):
89
+ FUNCTION = "function"
90
+ ASYNC_FUNCTION = "async_function"
91
+ METHOD = "method"
92
+ ASYNC_METHOD = "async_method"
93
+ CLASS = "class"
94
+ ROUTE = "route"
95
+ VARIABLE = "variable"
96
+
97
+
98
+ # ── Data types ──────────────────────────────────────────────────────────────────
99
+
100
+
101
+ @dataclass
102
+ class SymbolNode:
103
+ id: int = 0
104
+ name: str = ""
105
+ kind: str = ""
106
+ file: str = ""
107
+ line: int = 0
108
+ end_line: int = 0
109
+ parent_id: int | None = None
110
+ docstring: str = ""
111
+ language: str = ""
112
+ hash: str = ""
113
+ decorators: list[str] = field(default_factory=list)
114
+ params: list[str] = field(default_factory=list)
115
+ is_exported: bool = False
116
+
117
+ def to_dict(self) -> dict:
118
+ return {
119
+ "id": self.id,
120
+ "name": self.name,
121
+ "kind": self.kind,
122
+ "file": self.file,
123
+ "line": self.line,
124
+ "end_line": self.end_line,
125
+ "parent_id": self.parent_id,
126
+ "language": self.language,
127
+ "decorators": self.decorators,
128
+ "params": self.params,
129
+ "is_exported": self.is_exported,
130
+ }
131
+
132
+ @property
133
+ def qualified_name(self) -> str:
134
+ """Dotted module path + symbol name, e.g. 'src.main.handler'."""
135
+ if self.file and self.file != ".":
136
+ mod = self.file[:-3] if self.file.endswith(".py") else self.file
137
+ mod = mod.replace("/", ".").replace("\\", ".")
138
+ return f"{mod}.{self.name}"
139
+ return self.name
140
+
141
+
142
+ @dataclass
143
+ class GraphDiff:
144
+ added: list[SymbolNode] = field(default_factory=list)
145
+ removed: list[SymbolNode] = field(default_factory=list)
146
+ modified: list[SymbolNode] = field(default_factory=list)
147
+
148
+ @property
149
+ def has_changes(self) -> bool:
150
+ return bool(self.added or self.removed or self.modified)
151
+
152
+ def summary(self) -> str:
153
+ parts = []
154
+ if self.added:
155
+ parts.append(f"+{len(self.added)} added")
156
+ if self.removed:
157
+ parts.append(f"-{len(self.removed)} removed")
158
+ if self.modified:
159
+ parts.append(f"~{len(self.modified)} modified")
160
+ return ", ".join(parts) if parts else "no changes"
161
+
162
+
163
+ # ── SymbolGraph ─────────────────────────────────────────────────────────────────
164
+
165
+
166
+ class SymbolGraph:
167
+ """Symbol-level dependency graph backed by SQLite."""
168
+
169
+ def __init__(self, root: Path):
170
+ self.root = root
171
+ self.db_path = root / ".patchi" / SYMBOL_GRAPH_DB
172
+ self._conn: sqlite3.Connection | None = None
173
+ self._init_db()
174
+
175
+ def _init_db(self) -> None:
176
+ self.db_path.parent.mkdir(parents=True, exist_ok=True)
177
+ conn = self._get_conn()
178
+ conn.executescript(SCHEMA_SQL)
179
+
180
+ def _get_conn(self) -> sqlite3.Connection:
181
+ if self._conn is None:
182
+ self._conn = sqlite3.connect(str(self.db_path))
183
+ self._conn.execute("PRAGMA journal_mode=WAL")
184
+ self._conn.execute("PRAGMA synchronous=NORMAL")
185
+ return self._conn
186
+
187
+ def close(self) -> None:
188
+ if self._conn is not None:
189
+ self._conn.close()
190
+ self._conn = None
191
+
192
+ # ── Public API ──────────────────────────────────────────────────────────
193
+
194
+ def build_from_files(self, files: list[FileInfo]) -> int:
195
+ """Full rebuild from a list of FileInfo objects. Returns symbol count."""
196
+ conn = self._get_conn()
197
+ conn.execute("DELETE FROM edges")
198
+ conn.execute("DELETE FROM symbols")
199
+ conn.execute("DELETE FROM meta")
200
+
201
+ count = 0
202
+ for fi in files:
203
+ symbols = self._extract_symbols(fi)
204
+ for sym in symbols:
205
+ self._insert_symbol(conn, sym, fi.path)
206
+ count += 1
207
+
208
+ self._build_edges(conn, files)
209
+ conn.execute(
210
+ "INSERT OR REPLACE INTO meta (key, value) VALUES (?, ?)",
211
+ ("last_build", str(time.time())),
212
+ )
213
+ conn.commit()
214
+ return count
215
+
216
+ def build_from_root(self) -> int:
217
+ """Build by scanning root directory directly."""
218
+ from patchi.core.brain.scanner import FileScanner
219
+
220
+ scanner = FileScanner(self.root)
221
+ files = scanner.scan()
222
+ return self.build_from_files(files)
223
+
224
+ def patch(self, changed_files: list[Path]) -> GraphDiff:
225
+ """Incremental update: re-parse only changed files. Returns diff."""
226
+ diff = GraphDiff()
227
+ conn = self._get_conn()
228
+
229
+ for file_path in changed_files:
230
+ try:
231
+ rel = file_path.relative_to(self.root).as_posix()
232
+ except ValueError:
233
+ continue
234
+
235
+ lang = detect_language(file_path)
236
+ if lang == Lang.UNKNOWN:
237
+ continue
238
+
239
+ old_symbols = self._get_symbols_in_file(conn, rel)
240
+ for sym in old_symbols:
241
+ diff.removed.append(sym)
242
+ conn.execute("DELETE FROM symbols WHERE file = ?", (rel,))
243
+
244
+ content = file_path.read_text(encoding="utf-8", errors="ignore")
245
+ fi = FileInfo(
246
+ path=rel,
247
+ language=lang,
248
+ size_bytes=len(content.encode("utf-8")),
249
+ lines=content.count("\n") + 1,
250
+ )
251
+
252
+ new_symbols = self._extract_symbols(fi)
253
+ for sym in new_symbols:
254
+ self._insert_symbol(conn, sym, rel)
255
+ old = next(
256
+ (s for s in old_symbols if s.name == sym.name and s.line == sym.line), None
257
+ )
258
+ if old and old.hash != sym.hash:
259
+ diff.modified.append(sym)
260
+ elif not old:
261
+ diff.added.append(sym)
262
+
263
+ for sym in list(diff.removed):
264
+ still_exists = any(s.name == sym.name and s.line == sym.line for s in new_symbols)
265
+ if still_exists:
266
+ diff.removed.remove(sym)
267
+
268
+ conn.commit()
269
+ return diff
270
+
271
+ def get_symbol(
272
+ self, name: str, file: str | None = None, line: int | None = None
273
+ ) -> SymbolNode | None:
274
+ """Look up a symbol by name (and optionally file+line)."""
275
+ conn = self._get_conn()
276
+ if file and line:
277
+ cur = conn.execute(
278
+ "SELECT * FROM symbols WHERE name = ? AND file = ? AND line = ?",
279
+ (name, file, line),
280
+ )
281
+ elif file:
282
+ cur = conn.execute("SELECT * FROM symbols WHERE name = ? AND file = ?", (name, file))
283
+ else:
284
+ cur = conn.execute("SELECT * FROM symbols WHERE name = ? LIMIT 1", (name,))
285
+ row = cur.fetchone()
286
+ return self._row_to_symbol(row) if row else None
287
+
288
+ def get_symbols_in_file(self, file: str) -> list[SymbolNode]:
289
+ """All symbols defined in a file."""
290
+ return self._get_symbols_in_file(self._get_conn(), file)
291
+
292
+ def get_symbols_by_kind(self, kind: SymbolKind | str) -> list[SymbolNode]:
293
+ """All symbols of a given kind."""
294
+ conn = self._get_conn()
295
+ cur = conn.execute(
296
+ "SELECT * FROM symbols WHERE kind = ? ORDER BY file, line",
297
+ (kind.value if isinstance(kind, SymbolKind) else kind,),
298
+ )
299
+ return [self._row_to_symbol(r) for r in cur.fetchall()]
300
+
301
+ def get_all_symbols(self) -> list[SymbolNode]:
302
+ """Every symbol in the graph, ordered by file and line."""
303
+ conn = self._get_conn()
304
+ cur = conn.execute("SELECT * FROM symbols ORDER BY file, line")
305
+ return [self._row_to_symbol(r) for r in cur.fetchall()]
306
+
307
+ def ensure_built(self) -> int:
308
+ """Build the graph from root if it is empty. Returns symbol count.
309
+
310
+ Idempotent — a populated DB is left untouched, so this is safe to call
311
+ before every query site (e.g. governor neighborhood builders) without
312
+ re-scanning the whole project on each invocation.
313
+
314
+ Thread-safe: a module-level lock serializes the check-then-build, so
315
+ two concurrent governor paths can never both see count()==0 and race
316
+ DELETE/INSERT on the same SQLite file.
317
+ """
318
+ count = self.count()
319
+ if count > 0:
320
+ return count
321
+ with _BUILD_LOCK:
322
+ # Re-check inside the lock — another caller may have built it.
323
+ count = self.count()
324
+ if count > 0:
325
+ return count
326
+ return self.build_from_root()
327
+
328
+ def get_dependents(self, symbol_name: str, file: str | None = None) -> list[SymbolNode]:
329
+ """Symbols that reference (depend on) the given symbol."""
330
+ conn = self._get_conn()
331
+ if file:
332
+ cur = conn.execute(
333
+ """SELECT s.* FROM symbols s
334
+ JOIN edges e ON s.id = e.source_id
335
+ JOIN symbols t ON t.id = e.target_id
336
+ WHERE t.name = ? AND t.file = ?""",
337
+ (symbol_name, file),
338
+ )
339
+ else:
340
+ cur = conn.execute(
341
+ """SELECT s.* FROM symbols s
342
+ JOIN edges e ON s.id = e.source_id
343
+ JOIN symbols t ON t.id = e.target_id
344
+ WHERE t.name = ?""",
345
+ (symbol_name,),
346
+ )
347
+ return [self._row_to_symbol(r) for r in cur.fetchall()]
348
+
349
+ def get_dependencies(self, symbol_id: int) -> list[SymbolNode]:
350
+ """Symbols that the given symbol references."""
351
+ conn = self._get_conn()
352
+ cur = conn.execute(
353
+ """SELECT s.* FROM symbols s
354
+ JOIN edges e ON s.id = e.target_id
355
+ WHERE e.source_id = ?""",
356
+ (symbol_id,),
357
+ )
358
+ return [self._row_to_symbol(r) for r in cur.fetchall()]
359
+
360
+ def get_transitive_dependents(
361
+ self, symbol_name: str, file: str, max_depth: int = 5
362
+ ) -> list[SymbolNode]:
363
+ """All symbols that transitively depend on the given symbol."""
364
+ visited: set[int] = set()
365
+ result: list[SymbolNode] = []
366
+ queue: list[tuple[int, int]] = []
367
+ symbol = self.get_symbol(symbol_name, file)
368
+ if not symbol:
369
+ return []
370
+ queue.extend((d.id, 1) for d in self.get_dependents(symbol_name, file))
371
+
372
+ while queue:
373
+ sid, depth = queue.pop(0)
374
+ if sid in visited or depth > max_depth:
375
+ continue
376
+ visited.add(sid)
377
+ s = self._get_symbol_by_id(sid)
378
+ if s:
379
+ result.append(s)
380
+ queue.extend((d.id, depth + 1) for d in self.get_dependents(s.name, s.file))
381
+
382
+ return result
383
+
384
+ def get_route_endpoints(self) -> list[SymbolNode]:
385
+ """All route handler symbols."""
386
+ return self.get_symbols_by_kind(SymbolKind.ROUTE)
387
+
388
+ def count(self) -> int:
389
+ conn = self._get_conn()
390
+ cur = conn.execute("SELECT COUNT(*) FROM symbols")
391
+ return cur.fetchone()[0]
392
+
393
+ def export_json(self) -> dict:
394
+ """Export entire graph as JSON (for web UI / CLI display)."""
395
+ conn = self._get_conn()
396
+ symbols = [
397
+ self._row_to_symbol(r).to_dict()
398
+ for r in conn.execute("SELECT * FROM symbols ORDER BY file, line").fetchall()
399
+ ]
400
+ edges = [
401
+ {"source": r[0], "target": r[1], "kind": r[2]}
402
+ for r in conn.execute("SELECT * FROM edges").fetchall()
403
+ ]
404
+ meta = dict(conn.execute("SELECT key, value FROM meta").fetchall())
405
+ return {"symbols": symbols, "edges": edges, "meta": meta, "count": len(symbols)}
406
+
407
+ # ── Internal: Extraction ───────────────────────────────────────────────
408
+
409
+ def _extract_symbols(self, fi: FileInfo) -> list[SymbolNode]:
410
+ """Extract SymbolNodes from a single file using tree-sitter or regex."""
411
+ lang = fi.language
412
+ abs_path = self.root / fi.path
413
+ if not abs_path.exists():
414
+ return []
415
+
416
+ # Read as bytes for tree-sitter (byte offsets), decode to str for ast fallback
417
+ raw_bytes = abs_path.read_bytes()
418
+ content_str = raw_bytes.decode("utf-8", errors="replace")
419
+
420
+ if lang == Lang.PYTHON:
421
+ return self._extract_python(raw_bytes, content_str, fi.path)
422
+ elif lang in (Lang.JAVASCRIPT, Lang.TYPESCRIPT):
423
+ return self._extract_js_ts(raw_bytes, content_str, fi.path, lang)
424
+ elif lang == Lang.RUST:
425
+ return self._extract_rust(raw_bytes, content_str, fi.path)
426
+ elif lang == Lang.SVELTE:
427
+ return self._extract_svelte(raw_bytes, content_str, fi.path)
428
+ elif lang == Lang.JAVA:
429
+ return self._extract_java(raw_bytes, content_str, fi.path)
430
+ elif lang == Lang.GO:
431
+ return self._extract_go(raw_bytes, content_str, fi.path)
432
+ elif lang in (Lang.C, Lang.CPP):
433
+ return self._extract_c_cpp(raw_bytes, content_str, fi.path, lang)
434
+ elif lang == Lang.SWIFT:
435
+ return self._extract_swift(raw_bytes, content_str, fi.path)
436
+ elif lang == Lang.RUBY:
437
+ return self._extract_ruby(raw_bytes, content_str, fi.path)
438
+ elif lang == Lang.PHP:
439
+ return self._extract_generic_ts(
440
+ raw_bytes,
441
+ content_str,
442
+ fi.path,
443
+ lang,
444
+ function_types={"function_definition"},
445
+ method_types={"method_declaration"},
446
+ class_types={"class_declaration"},
447
+ )
448
+ elif lang == Lang.C_SHARP:
449
+ return self._extract_generic_ts(
450
+ raw_bytes,
451
+ content_str,
452
+ fi.path,
453
+ lang,
454
+ method_types={"method_declaration"},
455
+ class_types={"class_declaration"},
456
+ )
457
+ elif lang == Lang.KOTLIN:
458
+ return self._extract_generic_ts(
459
+ raw_bytes,
460
+ content_str,
461
+ fi.path,
462
+ lang,
463
+ function_types={"function_declaration"},
464
+ method_types={"function_declaration"},
465
+ class_types={"class_declaration"},
466
+ )
467
+ elif lang == Lang.DART:
468
+ return self._extract_generic_ts(
469
+ raw_bytes,
470
+ content_str,
471
+ fi.path,
472
+ lang,
473
+ function_types={"function_declaration"},
474
+ method_types={"method_declaration"},
475
+ class_types={"class_definition"},
476
+ )
477
+ elif lang == Lang.BASH:
478
+ return self._extract_generic_ts(
479
+ raw_bytes,
480
+ content_str,
481
+ fi.path,
482
+ lang,
483
+ function_types={"function_definition"},
484
+ )
485
+ elif lang in (Lang.CSS, Lang.SQL):
486
+ return []
487
+ else:
488
+ return []
489
+
490
+ def _extract_python(self, raw_bytes: bytes, content_str: str, file: str) -> list[SymbolNode]:
491
+ """Python symbols via tree-sitter or ast."""
492
+ symbols: list[SymbolNode] = []
493
+
494
+ parser = get_parser(Lang.PYTHON)
495
+ if parser:
496
+ try:
497
+ tree = parser.parse(raw_bytes)
498
+ self._walk_python(tree.root_node, raw_bytes, content_str, file, symbols, None)
499
+ if symbols:
500
+ return symbols
501
+ except Exception as e:
502
+ _log.warning("SymbolGraph._extract_python failed: %s", e)
503
+
504
+ return self._extract_python_ast(content_str, file)
505
+
506
+ def _extract_python_ast(self, content: str, file: str) -> list[SymbolNode]:
507
+ """Fallback Python symbol extraction using stdlib ast."""
508
+ import ast
509
+
510
+ symbols: list[SymbolNode] = []
511
+ try:
512
+ tree = ast.parse(content)
513
+ except SyntaxError:
514
+ return symbols
515
+
516
+ lines = content.split("\n")
517
+
518
+ for node in ast.walk(tree):
519
+ if isinstance(node, ast.FunctionDef):
520
+ end = getattr(node, "end_lineno", node.lineno) or node.lineno
521
+ sym = SymbolNode(
522
+ name=node.name,
523
+ kind=SymbolKind.ASYNC_FUNCTION
524
+ if isinstance(node, ast.AsyncFunctionDef)
525
+ else SymbolKind.FUNCTION,
526
+ file=file,
527
+ line=node.lineno,
528
+ end_line=end,
529
+ docstring=ast.get_docstring(node) or "",
530
+ language="python",
531
+ decorators=[
532
+ d.id if isinstance(d, ast.Name) else ""
533
+ for d in node.decorator_list
534
+ if isinstance(d, ast.Name)
535
+ ],
536
+ params=[a.arg for a in node.args.args] if hasattr(node.args, "args") else [],
537
+ is_exported=not node.name.startswith("_") or file == "__init__.py",
538
+ hash=_content_hash(lines[node.lineno - 1] if node.lineno <= len(lines) else ""),
539
+ )
540
+ symbols.append(sym)
541
+
542
+ elif isinstance(node, ast.ClassDef):
543
+ end = getattr(node, "end_lineno", node.lineno) or node.lineno
544
+ sym = SymbolNode(
545
+ name=node.name,
546
+ kind=SymbolKind.CLASS,
547
+ file=file,
548
+ line=node.lineno,
549
+ end_line=end,
550
+ docstring=ast.get_docstring(node) or "",
551
+ language="python",
552
+ is_exported=True,
553
+ hash=_content_hash(lines[node.lineno - 1] if node.lineno <= len(lines) else ""),
554
+ )
555
+ symbols.append(sym)
556
+
557
+ return symbols
558
+
559
+ def _walk_python(
560
+ self,
561
+ node: Any,
562
+ raw_bytes: bytes,
563
+ content_str: str,
564
+ file: str,
565
+ symbols: list[SymbolNode],
566
+ parent_id: int | None,
567
+ ) -> None:
568
+ """Walk Python tree-sitter CST for function/class/route symbols."""
569
+ buf = raw_bytes # byte buffer for tree-sitter byte-offset lookups
570
+ try:
571
+ node_type = node.type if hasattr(node, "type") else ""
572
+ except Exception as e:
573
+ _log.warning("SymbolGraph._walk_python failed: %s", e)
574
+ return
575
+
576
+ try:
577
+ start_line = node.start_point[0] + 1 if node.start_point else 0
578
+ end_line = node.end_point[0] + 1 if node.end_point else 0
579
+ except Exception as e:
580
+ _log.warning("SymbolGraph._walk_python failed: %s", e)
581
+ return
582
+
583
+ if node_type in ("function_definition", "async_function_definition"):
584
+ name_node = self._child_by_field(node, "name")
585
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
586
+ decorators = self._extract_decorators_buf(node, buf)
587
+ params = self._extract_params_buf(node, buf)
588
+ docstring = self._extract_docstring_buf(node, buf)
589
+ kind = (
590
+ SymbolKind.ASYNC_FUNCTION
591
+ if node_type == "async_function_definition"
592
+ else SymbolKind.FUNCTION
593
+ )
594
+
595
+ is_route = any(d for d in decorators if "route" in d or "app." in d or "router." in d)
596
+ sym_kind = SymbolKind.ROUTE if is_route else kind
597
+
598
+ sym = SymbolNode(
599
+ name=name,
600
+ kind=sym_kind,
601
+ file=file,
602
+ line=start_line,
603
+ end_line=end_line,
604
+ parent_id=parent_id,
605
+ docstring=docstring,
606
+ language="python",
607
+ decorators=decorators,
608
+ params=params,
609
+ is_exported=not name.startswith("_") or file == "__init__.py",
610
+ hash=_node_hash_buf(node, buf),
611
+ )
612
+ symbols.append(sym)
613
+
614
+ for child in self._children(node):
615
+ self._walk_python(child, buf, content_str, file, symbols, None)
616
+
617
+ elif node_type == "decorated_definition":
618
+ decorators = self._extract_decorators_buf(node, buf)
619
+ for child in self._children(node):
620
+ child_type = getattr(child, "type", "") if hasattr(child, "type") else ""
621
+ if child_type in (
622
+ "function_definition",
623
+ "async_function_definition",
624
+ "class_definition",
625
+ ):
626
+ self._walk_python(child, buf, content_str, file, symbols, parent_id)
627
+ if symbols:
628
+ symbols[-1].decorators = decorators
629
+ else:
630
+ self._walk_python(child, buf, content_str, file, symbols, parent_id)
631
+
632
+ elif node_type == "class_definition":
633
+ name_node = self._child_by_field(node, "name")
634
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
635
+ docstring = self._extract_docstring_buf(node, buf)
636
+ sym = SymbolNode(
637
+ name=name,
638
+ kind=SymbolKind.CLASS,
639
+ file=file,
640
+ line=start_line,
641
+ end_line=end_line,
642
+ parent_id=parent_id,
643
+ docstring=docstring,
644
+ language="python",
645
+ is_exported=True,
646
+ hash=_node_hash_buf(node, buf),
647
+ )
648
+ symbols.append(sym)
649
+
650
+ body = self._child_by_field(node, "body")
651
+ if body:
652
+ for child in self._children(body):
653
+ self._walk_python(child, buf, content_str, file, symbols, None)
654
+
655
+ else:
656
+ for child in self._children(node):
657
+ self._walk_python(child, buf, content_str, file, symbols, parent_id)
658
+
659
+ def _extract_js_ts(
660
+ self, raw_bytes: bytes, content_str: str, file: str, lang: Lang
661
+ ) -> list[SymbolNode]:
662
+ """JS/TS symbols via tree-sitter."""
663
+ symbols: list[SymbolNode] = []
664
+ parser = get_parser(lang)
665
+ if not parser:
666
+ return symbols
667
+
668
+ try:
669
+ tree = parser.parse(raw_bytes)
670
+ self._walk_js_ts(tree.root_node, raw_bytes, content_str, file, symbols, None)
671
+ except Exception as e:
672
+ _log.warning("SymbolGraph._extract_js_ts failed: %s", e)
673
+
674
+ return symbols
675
+
676
+ def _walk_js_ts(
677
+ self,
678
+ node: Any,
679
+ raw_bytes: bytes,
680
+ content_str: str,
681
+ file: str,
682
+ symbols: list[SymbolNode],
683
+ parent_id: int | None,
684
+ ) -> None:
685
+ buf = raw_bytes
686
+ try:
687
+ node_type = node.type if hasattr(node, "type") else ""
688
+ except Exception as e:
689
+ _log.warning("SymbolGraph._walk_js_ts failed: %s", e)
690
+ return
691
+
692
+ try:
693
+ start_line = node.start_point[0] + 1 if node.start_point else 0
694
+ end_line = node.end_point[0] + 1 if node.end_point else 0
695
+ except Exception as e:
696
+ _log.warning("SymbolGraph._walk_js_ts failed: %s", e)
697
+ return
698
+
699
+ func_types = {
700
+ "function_declaration",
701
+ "function_definition",
702
+ "method_definition",
703
+ "arrow_function",
704
+ "generator_function_declaration",
705
+ }
706
+ class_types = {"class_declaration", "class_definition"}
707
+ export_types = {"export_statement", "export_specifier"}
708
+
709
+ if node_type in func_types:
710
+ name_node = self._child_by_field(node, "name")
711
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
712
+ decorators = self._extract_decorators_buf(node, buf)
713
+ params = self._extract_params_buf(node, buf)
714
+ kind = (
715
+ SymbolKind.ASYNC_FUNCTION if node_type == "arrow_function" else SymbolKind.FUNCTION
716
+ )
717
+
718
+ is_route = any(d for d in decorators if "route" in d or "app." in d or "router." in d)
719
+ sym_kind = SymbolKind.ROUTE if is_route else kind
720
+
721
+ sym = SymbolNode(
722
+ name=name,
723
+ kind=sym_kind,
724
+ file=file,
725
+ line=start_line,
726
+ end_line=end_line,
727
+ parent_id=parent_id,
728
+ language="typescript" if file.endswith((".ts", ".tsx")) else "javascript",
729
+ decorators=decorators,
730
+ params=params,
731
+ is_exported=False,
732
+ hash=_node_hash_buf(node, buf),
733
+ )
734
+ symbols.append(sym)
735
+ for child in self._children(node):
736
+ self._walk_js_ts(child, buf, content_str, file, symbols, None)
737
+
738
+ elif node_type in class_types:
739
+ name_node = self._child_by_field(node, "name")
740
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
741
+ sym = SymbolNode(
742
+ name=name,
743
+ kind=SymbolKind.CLASS,
744
+ file=file,
745
+ line=start_line,
746
+ end_line=end_line,
747
+ parent_id=parent_id,
748
+ language="typescript" if file.endswith((".ts", ".tsx")) else "javascript",
749
+ is_exported=False,
750
+ hash=_node_hash_buf(node, buf),
751
+ )
752
+ symbols.append(sym)
753
+ body = self._child_by_field(node, "body")
754
+ if body:
755
+ for child in self._children(body):
756
+ self._walk_js_ts(child, buf, content_str, file, symbols, None)
757
+
758
+ elif node_type in export_types:
759
+ for child in self._children(node):
760
+ self._walk_js_ts(child, buf, content_str, file, symbols, parent_id)
761
+
762
+ else:
763
+ for child in self._children(node):
764
+ self._walk_js_ts(child, buf, content_str, file, symbols, parent_id)
765
+
766
+ def _extract_rust(self, raw_bytes: bytes, content_str: str, file: str) -> list[SymbolNode]:
767
+ symbols: list[SymbolNode] = []
768
+ parser = get_parser(Lang.RUST)
769
+ if not parser:
770
+ return symbols
771
+ try:
772
+ tree = parser.parse(raw_bytes)
773
+ self._walk_rust(tree.root_node, raw_bytes, content_str, file, symbols, None)
774
+ except Exception as e:
775
+ _log.warning("SymbolGraph._extract_rust failed: %s", e)
776
+ return symbols
777
+
778
+ def _walk_rust(
779
+ self,
780
+ node: Any,
781
+ raw_bytes: bytes,
782
+ content_str: str,
783
+ file: str,
784
+ symbols: list[SymbolNode],
785
+ parent_id: int | None,
786
+ ) -> None:
787
+ buf = raw_bytes
788
+ try:
789
+ node_type = node.type if hasattr(node, "type") else ""
790
+ except Exception as e:
791
+ _log.warning("SymbolGraph._walk_rust failed: %s", e)
792
+ return
793
+
794
+ try:
795
+ start_line = node.start_point[0] + 1 if node.start_point else 0
796
+ end_line = node.end_point[0] + 1 if node.end_point else 0
797
+ except Exception as e:
798
+ _log.warning("SymbolGraph._walk_rust failed: %s", e)
799
+ return
800
+
801
+ # Track route attribute macros on adjacent items
802
+ pending_route: tuple[str, str] | None = None
803
+ if node_type == "attribute_item":
804
+ attr_text = self._node_text_buf(node, buf)
805
+ rm = _RUST_ROUTE_ATTR_RE.search(attr_text)
806
+ if rm:
807
+ pending_route = (rm.group(1).upper(), rm.group(2))
808
+ for child in self._children(node):
809
+ self._walk_rust(child, buf, content_str, file, symbols, parent_id)
810
+ return
811
+
812
+ if node_type == "function_item":
813
+ name_node = self._child_by_field(node, "name")
814
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
815
+ params = self._extract_params_buf(node, buf)
816
+
817
+ is_route = pending_route is not None
818
+ kind = SymbolKind.ROUTE if is_route else SymbolKind.FUNCTION
819
+
820
+ sym = SymbolNode(
821
+ name=name,
822
+ kind=kind,
823
+ file=file,
824
+ line=start_line,
825
+ end_line=end_line,
826
+ parent_id=parent_id,
827
+ language="rust",
828
+ params=params,
829
+ hash=_node_hash_buf(node, buf),
830
+ )
831
+ if is_route and pending_route:
832
+ sym.decorators = [f'#[{pending_route[0].lower()}("{pending_route[1]}")]']
833
+ symbols.append(sym)
834
+
835
+ elif node_type == "struct_item":
836
+ name_node = self._child_by_field(node, "name")
837
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
838
+ sym = SymbolNode(
839
+ name=name,
840
+ kind=SymbolKind.CLASS,
841
+ file=file,
842
+ line=start_line,
843
+ end_line=end_line,
844
+ parent_id=parent_id,
845
+ language="rust",
846
+ hash=_node_hash_buf(node, buf),
847
+ )
848
+ symbols.append(sym)
849
+
850
+ elif node_type == "enum_item":
851
+ name_node = self._child_by_field(node, "name")
852
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
853
+ sym = SymbolNode(
854
+ name=name,
855
+ kind=SymbolKind.CLASS,
856
+ file=file,
857
+ line=start_line,
858
+ end_line=end_line,
859
+ parent_id=parent_id,
860
+ language="rust",
861
+ hash=_node_hash_buf(node, buf),
862
+ )
863
+ symbols.append(sym)
864
+
865
+ elif node_type == "impl_item":
866
+ type_node = self._child_by_field(node, "type")
867
+ trait_node = self._child_by_field(node, "trait")
868
+ name = ""
869
+ if trait_node:
870
+ name = (
871
+ f"impl {self._node_text_buf(trait_node, buf)} for {self._node_text_buf(type_node, buf)}"
872
+ if type_node
873
+ else f"impl {self._node_text_buf(trait_node, buf)}"
874
+ )
875
+ elif type_node:
876
+ name = f"impl {self._node_text_buf(type_node, buf)}"
877
+ if name:
878
+ sym = SymbolNode(
879
+ name=name,
880
+ kind=SymbolKind.CLASS,
881
+ file=file,
882
+ line=start_line,
883
+ end_line=end_line,
884
+ parent_id=parent_id,
885
+ language="rust",
886
+ hash=_node_hash_buf(node, buf),
887
+ )
888
+ symbols.append(sym)
889
+ body = self._child_by_field(node, "body")
890
+ if body:
891
+ for child in self._children(body):
892
+ self._walk_rust(child, buf, content_str, file, symbols, None)
893
+
894
+ elif node_type == "trait_item":
895
+ name_node = self._child_by_field(node, "name")
896
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
897
+ sym = SymbolNode(
898
+ name=f"trait {name}",
899
+ kind=SymbolKind.CLASS,
900
+ file=file,
901
+ line=start_line,
902
+ end_line=end_line,
903
+ parent_id=parent_id,
904
+ language="rust",
905
+ hash=_node_hash_buf(node, buf),
906
+ )
907
+ symbols.append(sym)
908
+
909
+ else:
910
+ for child in self._children(node):
911
+ self._walk_rust(child, buf, content_str, file, symbols, parent_id)
912
+
913
+ def _extract_svelte(self, raw_bytes: bytes, content_str: str, file: str) -> list[SymbolNode]:
914
+ symbols: list[SymbolNode] = []
915
+ parser = get_parser(Lang.SVELTE)
916
+ if not parser:
917
+ return symbols
918
+ try:
919
+ tree = parser.parse(raw_bytes)
920
+ self._walk_svelte(tree.root_node, raw_bytes, content_str, file, symbols, None)
921
+ except Exception as e:
922
+ _log.warning("SymbolGraph._extract_svelte failed: %s", e)
923
+ return symbols
924
+
925
+ def _walk_svelte(
926
+ self,
927
+ node: Any,
928
+ raw_bytes: bytes,
929
+ content_str: str,
930
+ file: str,
931
+ symbols: list[SymbolNode],
932
+ parent_id: int | None,
933
+ ) -> None:
934
+ buf = raw_bytes
935
+ try:
936
+ node_type = node.type if hasattr(node, "type") else ""
937
+ except Exception as e:
938
+ _log.warning("SymbolGraph._walk_svelte failed: %s", e)
939
+ return
940
+
941
+ try:
942
+ start_line = node.start_point[0] + 1 if node.start_point else 0
943
+ end_line = node.end_point[0] + 1 if node.end_point else 0
944
+ except Exception as e:
945
+ _log.warning("SymbolGraph._walk_svelte failed: %s", e)
946
+ return
947
+
948
+ # Svelte <script> tag contains JavaScript/TypeScript — reuse JS walker
949
+ if node_type == "script_element":
950
+ raw_tag = self._node_text_buf(node, buf)
951
+ _SVELTE_SCRIPT_RE.match(raw_tag)
952
+ has_ts = (
953
+ "ts" in raw_tag[:80] or 'lang="ts"' in raw_tag[:80] or "lang='ts'" in raw_tag[:80]
954
+ )
955
+ js_lang = Lang.TYPESCRIPT if has_ts else Lang.JAVASCRIPT
956
+ js_parser = get_parser(js_lang)
957
+ if js_parser:
958
+ inner_text = raw_tag
959
+ inner_bytes = inner_text.encode("utf-8")
960
+ try:
961
+ js_tree = js_parser.parse(inner_bytes)
962
+ self._walk_js_ts(
963
+ js_tree.root_node, inner_bytes, inner_text, file, symbols, parent_id
964
+ )
965
+ except Exception as e:
966
+ _log.warning("SymbolGraph._walk_svelte failed: %s", e)
967
+ return
968
+
969
+ # Component tags (capitalized) → custom components referenced
970
+ if node_type == "element":
971
+ tag_text = (
972
+ self._node_text_buf(node, buf).split()[0] if self._node_text_buf(node, buf) else ""
973
+ )
974
+ if tag_text and tag_text[0].isupper():
975
+ sym = SymbolNode(
976
+ name=tag_text,
977
+ kind=SymbolKind.CLASS,
978
+ file=file,
979
+ line=start_line,
980
+ end_line=end_line,
981
+ parent_id=parent_id,
982
+ language="svelte",
983
+ hash=_node_hash_buf(node, buf),
984
+ )
985
+ symbols.append(sym)
986
+
987
+ for child in self._children(node):
988
+ self._walk_svelte(child, buf, content_str, file, symbols, parent_id)
989
+
990
+ # ── Java extractor ─────────────────────────────────────────────────────
991
+
992
+ def _extract_java(self, raw_bytes: bytes, content_str: str, file: str) -> list[SymbolNode]:
993
+ symbols: list[SymbolNode] = []
994
+ parser = get_parser(Lang.JAVA)
995
+ if not parser:
996
+ return symbols
997
+ try:
998
+ tree = parser.parse(raw_bytes)
999
+ self._walk_java(tree.root_node, raw_bytes, content_str, file, symbols, None)
1000
+ except Exception as e:
1001
+ _log.warning("SymbolGraph._extract_java failed: %s", e)
1002
+ return symbols
1003
+
1004
+ def _walk_java(
1005
+ self,
1006
+ node: Any,
1007
+ raw_bytes: bytes,
1008
+ content_str: str,
1009
+ file: str,
1010
+ symbols: list[SymbolNode],
1011
+ parent_id: int | None,
1012
+ ) -> None:
1013
+ buf = raw_bytes
1014
+ try:
1015
+ node_type = node.type if hasattr(node, "type") else ""
1016
+ except Exception as e:
1017
+ _log.warning("SymbolGraph._walk_java failed: %s", e)
1018
+ return
1019
+ try:
1020
+ start_line = node.start_point[0] + 1 if node.start_point else 0
1021
+ end_line = node.end_point[0] + 1 if node.end_point else 0
1022
+ except Exception as e:
1023
+ _log.warning("SymbolGraph._walk_java failed: %s", e)
1024
+ return
1025
+
1026
+ if node_type == "class_declaration":
1027
+ name_node = self._child_by_field(node, "name")
1028
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
1029
+ sym = SymbolNode(
1030
+ name=name,
1031
+ kind=SymbolKind.CLASS,
1032
+ file=file,
1033
+ line=start_line,
1034
+ end_line=end_line,
1035
+ parent_id=parent_id,
1036
+ language="java",
1037
+ hash=_node_hash_buf(node, buf),
1038
+ )
1039
+ symbols.append(sym)
1040
+ body = self._child_by_field(node, "body")
1041
+ if body:
1042
+ for child in self._children(body):
1043
+ self._walk_java(child, buf, content_str, file, symbols, None)
1044
+
1045
+ elif node_type == "interface_declaration":
1046
+ name_node = self._child_by_field(node, "name")
1047
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
1048
+ sym = SymbolNode(
1049
+ name=f"interface {name}",
1050
+ kind=SymbolKind.CLASS,
1051
+ file=file,
1052
+ line=start_line,
1053
+ end_line=end_line,
1054
+ parent_id=parent_id,
1055
+ language="java",
1056
+ hash=_node_hash_buf(node, buf),
1057
+ )
1058
+ symbols.append(sym)
1059
+
1060
+ elif node_type == "annotation_type_declaration":
1061
+ name_node = self._child_by_field(node, "name")
1062
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
1063
+ sym = SymbolNode(
1064
+ name=f"@interface {name}",
1065
+ kind=SymbolKind.CLASS,
1066
+ file=file,
1067
+ line=start_line,
1068
+ end_line=end_line,
1069
+ parent_id=parent_id,
1070
+ language="java",
1071
+ hash=_node_hash_buf(node, buf),
1072
+ )
1073
+ symbols.append(sym)
1074
+
1075
+ elif node_type == "method_declaration":
1076
+ name_node = self._child_by_field(node, "name")
1077
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
1078
+ decorators = self._extract_decorators_buf(node, buf)
1079
+ is_route = any(
1080
+ "GetMapping" in d
1081
+ or "PostMapping" in d
1082
+ or "RequestMapping" in d
1083
+ or "PutMapping" in d
1084
+ or "DeleteMapping" in d
1085
+ or "PatchMapping" in d
1086
+ for d in decorators
1087
+ )
1088
+ kind = SymbolKind.ROUTE if is_route else SymbolKind.FUNCTION
1089
+ sym = SymbolNode(
1090
+ name=name,
1091
+ kind=kind,
1092
+ file=file,
1093
+ line=start_line,
1094
+ end_line=end_line,
1095
+ parent_id=parent_id,
1096
+ language="java",
1097
+ decorators=decorators,
1098
+ hash=_node_hash_buf(node, buf),
1099
+ )
1100
+ symbols.append(sym)
1101
+ for child in self._children(node):
1102
+ self._walk_java(child, buf, content_str, file, symbols, None)
1103
+
1104
+ elif node_type == "enum_declaration":
1105
+ name_node = self._child_by_field(node, "name")
1106
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
1107
+ sym = SymbolNode(
1108
+ name=f"enum {name}",
1109
+ kind=SymbolKind.CLASS,
1110
+ file=file,
1111
+ line=start_line,
1112
+ end_line=end_line,
1113
+ parent_id=parent_id,
1114
+ language="java",
1115
+ hash=_node_hash_buf(node, buf),
1116
+ )
1117
+ symbols.append(sym)
1118
+
1119
+ elif node_type == "record_declaration":
1120
+ name_node = self._child_by_field(node, "name")
1121
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
1122
+ sym = SymbolNode(
1123
+ name=f"record {name}",
1124
+ kind=SymbolKind.CLASS,
1125
+ file=file,
1126
+ line=start_line,
1127
+ end_line=end_line,
1128
+ parent_id=parent_id,
1129
+ language="java",
1130
+ hash=_node_hash_buf(node, buf),
1131
+ )
1132
+ symbols.append(sym)
1133
+
1134
+ else:
1135
+ for child in self._children(node):
1136
+ self._walk_java(child, buf, content_str, file, symbols, parent_id)
1137
+
1138
+ # ── Go extractor ───────────────────────────────────────────────────────
1139
+
1140
+ def _extract_go(self, raw_bytes: bytes, content_str: str, file: str) -> list[SymbolNode]:
1141
+ symbols: list[SymbolNode] = []
1142
+ parser = get_parser(Lang.GO)
1143
+ if not parser:
1144
+ return symbols
1145
+ try:
1146
+ tree = parser.parse(raw_bytes)
1147
+ self._walk_go(tree.root_node, raw_bytes, content_str, file, symbols, None)
1148
+ except Exception as e:
1149
+ _log.warning("SymbolGraph._extract_go failed: %s", e)
1150
+ return symbols
1151
+
1152
+ def _walk_go(
1153
+ self,
1154
+ node: Any,
1155
+ raw_bytes: bytes,
1156
+ content_str: str,
1157
+ file: str,
1158
+ symbols: list[SymbolNode],
1159
+ parent_id: int | None,
1160
+ ) -> None:
1161
+ buf = raw_bytes
1162
+ try:
1163
+ node_type = node.type if hasattr(node, "type") else ""
1164
+ except Exception as e:
1165
+ _log.warning("SymbolGraph._walk_go failed: %s", e)
1166
+ return
1167
+ try:
1168
+ start_line = node.start_point[0] + 1 if node.start_point else 0
1169
+ end_line = node.end_point[0] + 1 if node.end_point else 0
1170
+ except Exception as e:
1171
+ _log.warning("SymbolGraph._walk_go failed: %s", e)
1172
+ return
1173
+
1174
+ if node_type == "function_declaration":
1175
+ name_node = self._child_by_field(node, "name")
1176
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
1177
+ sym = SymbolNode(
1178
+ name=name,
1179
+ kind=SymbolKind.FUNCTION,
1180
+ file=file,
1181
+ line=start_line,
1182
+ end_line=end_line,
1183
+ parent_id=parent_id,
1184
+ language="go",
1185
+ hash=_node_hash_buf(node, buf),
1186
+ )
1187
+ symbols.append(sym)
1188
+
1189
+ elif node_type == "method_declaration":
1190
+ name_node = self._child_by_field(node, "name")
1191
+ receiver_node = self._child_by_field(node, "receiver")
1192
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
1193
+ if receiver_node:
1194
+ recv_text = self._node_text_buf(receiver_node, buf)
1195
+ name = f"({recv_text}).{name}"
1196
+ sym = SymbolNode(
1197
+ name=name,
1198
+ kind=SymbolKind.METHOD,
1199
+ file=file,
1200
+ line=start_line,
1201
+ end_line=end_line,
1202
+ parent_id=parent_id,
1203
+ language="go",
1204
+ hash=_node_hash_buf(node, buf),
1205
+ )
1206
+ symbols.append(sym)
1207
+
1208
+ elif node_type == "type_declaration":
1209
+ for child in self._children(node):
1210
+ self._walk_go(child, buf, content_str, file, symbols, parent_id)
1211
+
1212
+ elif node_type == "type_spec":
1213
+ name_node = self._child_by_field(node, "name")
1214
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
1215
+ type_node = self._child_by_field(node, "type")
1216
+ sub_kind = SymbolKind.CLASS
1217
+ if type_node:
1218
+ ttype = getattr(type_node, "type", "")
1219
+ if ttype == "struct_type":
1220
+ sub_kind = SymbolKind.CLASS
1221
+ elif ttype == "interface_type":
1222
+ name = f"interface {name}"
1223
+ sym = SymbolNode(
1224
+ name=name,
1225
+ kind=sub_kind,
1226
+ file=file,
1227
+ line=start_line,
1228
+ end_line=end_line,
1229
+ parent_id=parent_id,
1230
+ language="go",
1231
+ hash=_node_hash_buf(node, buf),
1232
+ )
1233
+ symbols.append(sym)
1234
+ if type_node:
1235
+ for child in self._children(type_node):
1236
+ self._walk_go(child, buf, content_str, file, symbols, None)
1237
+
1238
+ else:
1239
+ for child in self._children(node):
1240
+ self._walk_go(child, buf, content_str, file, symbols, parent_id)
1241
+
1242
+ # ── C/C++ extractor ────────────────────────────────────────────────────
1243
+
1244
+ def _extract_c_cpp(
1245
+ self, raw_bytes: bytes, content_str: str, file: str, lang: Lang
1246
+ ) -> list[SymbolNode]:
1247
+ symbols: list[SymbolNode] = []
1248
+ parser = get_parser(lang)
1249
+ if not parser:
1250
+ return symbols
1251
+ try:
1252
+ tree = parser.parse(raw_bytes)
1253
+ self._walk_c_cpp(tree.root_node, raw_bytes, content_str, file, symbols, None, lang)
1254
+ except Exception as e:
1255
+ _log.warning("SymbolGraph._extract_c_cpp failed: %s", e)
1256
+ return symbols
1257
+
1258
+ def _walk_c_cpp(
1259
+ self,
1260
+ node: Any,
1261
+ raw_bytes: bytes,
1262
+ content_str: str,
1263
+ file: str,
1264
+ symbols: list[SymbolNode],
1265
+ parent_id: int | None,
1266
+ lang: Lang,
1267
+ ) -> None:
1268
+ buf = raw_bytes
1269
+ try:
1270
+ node_type = node.type if hasattr(node, "type") else ""
1271
+ except Exception as e:
1272
+ _log.warning("SymbolGraph._walk_c_cpp failed: %s", e)
1273
+ return
1274
+ try:
1275
+ start_line = node.start_point[0] + 1 if node.start_point else 0
1276
+ end_line = node.end_point[0] + 1 if node.end_point else 0
1277
+ except Exception as e:
1278
+ _log.warning("SymbolGraph._walk_c_cpp failed: %s", e)
1279
+ return
1280
+
1281
+ lang_str = "c" if lang == Lang.C else "cpp"
1282
+
1283
+ if node_type == "function_definition":
1284
+ decl = self._child_by_field(node, "declarator")
1285
+ name = ""
1286
+ if decl:
1287
+ name_node = (
1288
+ self._child_by_field(decl, "declarator")
1289
+ if hasattr(decl, "child_by_field_name")
1290
+ else None
1291
+ )
1292
+ if not name_node:
1293
+ name_node = self._child_by_field(decl, "name")
1294
+ if name_node:
1295
+ name = self._node_text_buf(name_node, buf)
1296
+ if not name:
1297
+ name = self._node_text_buf(node, buf)[:30]
1298
+ sym = SymbolNode(
1299
+ name=name,
1300
+ kind=SymbolKind.FUNCTION,
1301
+ file=file,
1302
+ line=start_line,
1303
+ end_line=end_line,
1304
+ parent_id=parent_id,
1305
+ language=lang_str,
1306
+ hash=_node_hash_buf(node, buf),
1307
+ )
1308
+ symbols.append(sym)
1309
+
1310
+ elif node_type == "struct_specifier" and lang == Lang.CPP:
1311
+ name_node = self._child_by_field(node, "name")
1312
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
1313
+ sym = SymbolNode(
1314
+ name=name,
1315
+ kind=SymbolKind.CLASS,
1316
+ file=file,
1317
+ line=start_line,
1318
+ end_line=end_line,
1319
+ parent_id=parent_id,
1320
+ language=lang_str,
1321
+ hash=_node_hash_buf(node, buf),
1322
+ )
1323
+ symbols.append(sym)
1324
+
1325
+ elif node_type == "class_specifier":
1326
+ name_node = self._child_by_field(node, "name")
1327
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
1328
+ sym = SymbolNode(
1329
+ name=name,
1330
+ kind=SymbolKind.CLASS,
1331
+ file=file,
1332
+ line=start_line,
1333
+ end_line=end_line,
1334
+ parent_id=parent_id,
1335
+ language=lang_str,
1336
+ hash=_node_hash_buf(node, buf),
1337
+ )
1338
+ symbols.append(sym)
1339
+ body = self._child_by_field(node, "body")
1340
+ if body:
1341
+ for child in self._children(body):
1342
+ self._walk_c_cpp(child, buf, content_str, file, symbols, None, lang)
1343
+
1344
+ elif node_type == "namespace_definition":
1345
+ name_node = self._child_by_field(node, "name")
1346
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
1347
+ sym = SymbolNode(
1348
+ name=f"namespace {name}",
1349
+ kind=SymbolKind.CLASS,
1350
+ file=file,
1351
+ line=start_line,
1352
+ end_line=end_line,
1353
+ parent_id=parent_id,
1354
+ language=lang_str,
1355
+ hash=_node_hash_buf(node, buf),
1356
+ )
1357
+ symbols.append(sym)
1358
+
1359
+ else:
1360
+ for child in self._children(node):
1361
+ self._walk_c_cpp(child, buf, content_str, file, symbols, parent_id, lang)
1362
+
1363
+ def _extract_swift(self, raw_bytes: bytes, content_str: str, file: str) -> list[SymbolNode]:
1364
+ symbols: list[SymbolNode] = []
1365
+ parser = get_parser(Lang.SWIFT)
1366
+ if not parser:
1367
+ return symbols
1368
+ try:
1369
+ tree = parser.parse(raw_bytes)
1370
+ self._walk_swift(tree.root_node, raw_bytes, content_str, file, symbols, None)
1371
+ except Exception as e:
1372
+ _log.warning("SymbolGraph._extract_swift failed: %s", e)
1373
+ return symbols
1374
+
1375
+ def _walk_swift(
1376
+ self,
1377
+ node: Any,
1378
+ raw_bytes: bytes,
1379
+ content_str: str,
1380
+ file: str,
1381
+ symbols: list[SymbolNode],
1382
+ parent_id: int | None,
1383
+ ) -> None:
1384
+ buf = raw_bytes
1385
+ try:
1386
+ node_type = node.type if hasattr(node, "type") else ""
1387
+ except Exception as e:
1388
+ _log.warning("SymbolGraph._walk_swift failed: %s", e)
1389
+ return
1390
+ try:
1391
+ start_line = node.start_point[0] + 1 if node.start_point else 0
1392
+ end_line = node.end_point[0] + 1 if node.end_point else 0
1393
+ except Exception as e:
1394
+ _log.warning("SymbolGraph._walk_swift failed: %s", e)
1395
+ return
1396
+
1397
+ if node_type == "import_declaration":
1398
+ # Skip — imports are handled by scanner, not needed as symbols
1399
+ pass
1400
+
1401
+ elif node_type == "class_declaration":
1402
+ name_node = self._child_by_field(node, "name")
1403
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
1404
+ sym = SymbolNode(
1405
+ name=name,
1406
+ kind=SymbolKind.CLASS,
1407
+ file=file,
1408
+ line=start_line,
1409
+ end_line=end_line,
1410
+ parent_id=parent_id,
1411
+ language="swift",
1412
+ hash=_node_hash_buf(node, buf),
1413
+ )
1414
+ symbols.append(sym)
1415
+ body = self._child_by_field(node, "body")
1416
+ if body:
1417
+ for child in self._children(body):
1418
+ self._walk_swift(child, buf, content_str, file, symbols, sym.id)
1419
+
1420
+ elif node_type == "function_declaration":
1421
+ name_node = self._child_by_field(node, "name")
1422
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
1423
+ sym = SymbolNode(
1424
+ name=name,
1425
+ kind=SymbolKind.FUNCTION,
1426
+ file=file,
1427
+ line=start_line,
1428
+ end_line=end_line,
1429
+ parent_id=parent_id,
1430
+ language="swift",
1431
+ hash=_node_hash_buf(node, buf),
1432
+ )
1433
+ symbols.append(sym)
1434
+ for child in self._children(node):
1435
+ self._walk_swift(child, buf, content_str, file, symbols, sym.id)
1436
+
1437
+ elif node_type == "protocol_declaration":
1438
+ name_node = self._child_by_field(node, "name")
1439
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
1440
+ symbols.append(
1441
+ SymbolNode(
1442
+ name=f"protocol {name}",
1443
+ kind=SymbolKind.CLASS,
1444
+ file=file,
1445
+ line=start_line,
1446
+ end_line=end_line,
1447
+ parent_id=parent_id,
1448
+ language="swift",
1449
+ hash=_node_hash_buf(node, buf),
1450
+ )
1451
+ )
1452
+
1453
+ else:
1454
+ for child in self._children(node):
1455
+ self._walk_swift(child, buf, content_str, file, symbols, parent_id)
1456
+
1457
+ # ── Ruby extractor ───────────────────────────────────────────────────────
1458
+
1459
+ def _extract_ruby(self, raw_bytes: bytes, content_str: str, file: str) -> list[SymbolNode]:
1460
+ symbols: list[SymbolNode] = []
1461
+ parser = get_parser(Lang.RUBY)
1462
+ if not parser:
1463
+ return symbols
1464
+ try:
1465
+ tree = parser.parse(raw_bytes)
1466
+ self._walk_ruby(tree.root_node, raw_bytes, content_str, file, symbols, None)
1467
+ except Exception as e:
1468
+ _log.warning("SymbolGraph._extract_ruby failed: %s", e)
1469
+ return symbols
1470
+
1471
+ def _walk_ruby(
1472
+ self,
1473
+ node: Any,
1474
+ raw_bytes: bytes,
1475
+ content_str: str,
1476
+ file: str,
1477
+ symbols: list[SymbolNode],
1478
+ parent_id: int | None,
1479
+ ) -> None:
1480
+ buf = raw_bytes
1481
+ try:
1482
+ node_type = node.type if hasattr(node, "type") else ""
1483
+ except Exception as e:
1484
+ _log.warning("SymbolGraph._walk_ruby failed: %s", e)
1485
+ return
1486
+ try:
1487
+ start_line = node.start_point[0] + 1 if node.start_point else 0
1488
+ end_line = node.end_point[0] + 1 if node.end_point else 0
1489
+ except Exception as e:
1490
+ _log.warning("SymbolGraph._walk_ruby failed: %s", e)
1491
+ return
1492
+
1493
+ if node_type == "call":
1494
+ name = ""
1495
+ for c in self._children(node):
1496
+ if c.type == "identifier":
1497
+ name = self._node_text_buf(c, buf)
1498
+ break
1499
+ if name in ("require", "require_relative", "load", "include", "extend", "prepend"):
1500
+ arg = ""
1501
+ for c in self._children(node):
1502
+ if c.type == "argument_list":
1503
+ for a in self._children(c):
1504
+ if a.type == "string":
1505
+ arg = self._node_text_buf(a, buf).strip("\"'")
1506
+ break
1507
+ if arg:
1508
+ # Skip — imports handled by scanner
1509
+ pass
1510
+
1511
+ elif node_type == "method":
1512
+ name_node = None
1513
+ for c in self._children(node):
1514
+ if c.type == "identifier":
1515
+ name_node = c
1516
+ break
1517
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
1518
+ sym = SymbolNode(
1519
+ name=name,
1520
+ kind=SymbolKind.FUNCTION,
1521
+ file=file,
1522
+ line=start_line,
1523
+ end_line=end_line,
1524
+ parent_id=parent_id,
1525
+ language="ruby",
1526
+ hash=_node_hash_buf(node, buf),
1527
+ )
1528
+ symbols.append(sym)
1529
+ body = None
1530
+ for c in self._children(node):
1531
+ if c.type == "body_statement":
1532
+ body = c
1533
+ break
1534
+ if body:
1535
+ for child in self._children(body):
1536
+ self._walk_ruby(child, buf, content_str, file, symbols, sym.id)
1537
+
1538
+ elif node_type == "class":
1539
+ name_node = None
1540
+ for c in self._children(node):
1541
+ if c.type == "constant":
1542
+ name_node = c
1543
+ break
1544
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
1545
+ sym = SymbolNode(
1546
+ name=name,
1547
+ kind=SymbolKind.CLASS,
1548
+ file=file,
1549
+ line=start_line,
1550
+ end_line=end_line,
1551
+ parent_id=parent_id,
1552
+ language="ruby",
1553
+ hash=_node_hash_buf(node, buf),
1554
+ )
1555
+ symbols.append(sym)
1556
+ for c in self._children(node):
1557
+ if c.type == "body_statement":
1558
+ for child in self._children(c):
1559
+ self._walk_ruby(child, buf, content_str, file, symbols, sym.id)
1560
+
1561
+ elif node_type in ("module", "singleton_class"):
1562
+ name_node = None
1563
+ for c in self._children(node):
1564
+ if c.type == "constant":
1565
+ name_node = c
1566
+ break
1567
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
1568
+ sym = SymbolNode(
1569
+ name=f"module {name}" if node_type == "module" else name,
1570
+ kind=SymbolKind.CLASS,
1571
+ file=file,
1572
+ line=start_line,
1573
+ end_line=end_line,
1574
+ parent_id=parent_id,
1575
+ language="ruby",
1576
+ hash=_node_hash_buf(node, buf),
1577
+ )
1578
+ symbols.append(sym)
1579
+ for c in self._children(node):
1580
+ if c.type == "body_statement":
1581
+ for child in self._children(c):
1582
+ self._walk_ruby(child, buf, content_str, file, symbols, sym.id)
1583
+
1584
+ else:
1585
+ for child in self._children(node):
1586
+ self._walk_ruby(child, buf, content_str, file, symbols, parent_id)
1587
+
1588
+ def _extract_generic_ts(
1589
+ self,
1590
+ raw_bytes: bytes,
1591
+ content_str: str,
1592
+ file: str,
1593
+ lang: Lang,
1594
+ function_types: set[str] | None = None,
1595
+ method_types: set[str] | None = None,
1596
+ class_types: set[str] | None = None,
1597
+ ) -> list[SymbolNode]:
1598
+ """Generic tree-sitter symbol extractor for languages without custom walkers."""
1599
+ symbols: list[SymbolNode] = []
1600
+ parser = get_parser(lang)
1601
+ if not parser:
1602
+ return []
1603
+ try:
1604
+ tree = parser.parse(raw_bytes)
1605
+ self._walk_generic(
1606
+ tree.root_node,
1607
+ raw_bytes,
1608
+ file,
1609
+ lang,
1610
+ symbols,
1611
+ None,
1612
+ function_types or set(),
1613
+ method_types or set(),
1614
+ class_types or set(),
1615
+ )
1616
+ except Exception as e:
1617
+ _log.warning("SymbolGraph._extract_generic_ts failed: %s", e)
1618
+ return symbols
1619
+
1620
+ def _walk_generic(
1621
+ self,
1622
+ node: Any,
1623
+ buf: bytes,
1624
+ file: str,
1625
+ lang: Lang,
1626
+ symbols: list[SymbolNode],
1627
+ parent_id: int | None,
1628
+ function_types: set[str],
1629
+ method_types: set[str],
1630
+ class_types: set[str],
1631
+ ) -> None:
1632
+ try:
1633
+ node_type = node.type if hasattr(node, "type") else ""
1634
+ except Exception as e:
1635
+ _log.warning("SymbolGraph._walk_generic failed: %s", e)
1636
+ return
1637
+ try:
1638
+ start_line = node.start_point[0] + 1 if node.start_point else 0
1639
+ end_line = node.end_point[0] + 1 if node.end_point else 0
1640
+ except Exception as e:
1641
+ _log.warning("SymbolGraph._walk_generic failed: %s", e)
1642
+ return
1643
+
1644
+ kind = None
1645
+ if node_type in function_types:
1646
+ kind = SymbolKind.FUNCTION
1647
+ name_node = self._child_by_field(node, "name")
1648
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
1649
+ elif node_type in method_types:
1650
+ kind = SymbolKind.FUNCTION
1651
+ name_node = self._child_by_field(node, "name")
1652
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
1653
+ elif node_type in class_types:
1654
+ kind = SymbolKind.CLASS
1655
+ name_node = self._child_by_field(node, "name")
1656
+ name = self._node_text_buf(name_node, buf) if name_node else "anon"
1657
+
1658
+ if kind:
1659
+ lang_str = lang.value if hasattr(lang, "value") else str(lang)
1660
+ sym = SymbolNode(
1661
+ name=name,
1662
+ kind=kind,
1663
+ file=file,
1664
+ line=start_line,
1665
+ end_line=end_line,
1666
+ parent_id=parent_id,
1667
+ language=lang_str,
1668
+ hash=_node_hash_buf(node, buf),
1669
+ )
1670
+ symbols.append(sym)
1671
+ for child in self._children(node):
1672
+ self._walk_generic(
1673
+ child,
1674
+ buf,
1675
+ file,
1676
+ lang,
1677
+ symbols,
1678
+ sym.id,
1679
+ function_types,
1680
+ method_types,
1681
+ class_types,
1682
+ )
1683
+ return
1684
+
1685
+ for child in self._children(node):
1686
+ self._walk_generic(
1687
+ child,
1688
+ buf,
1689
+ file,
1690
+ lang,
1691
+ symbols,
1692
+ parent_id,
1693
+ function_types,
1694
+ method_types,
1695
+ class_types,
1696
+ )
1697
+
1698
+ # ── Internal: Tree-sitter helpers ──────────────────────────────────────
1699
+
1700
+ def _children(self, node: Any) -> list[Any]:
1701
+ try:
1702
+ return list(node.children) if hasattr(node, "children") else []
1703
+ except Exception as e:
1704
+ _log.warning("SymbolGraph._children failed: %s", e)
1705
+ return []
1706
+
1707
+ def _child_by_field(self, node: Any, field_name: str) -> Any | None:
1708
+ try:
1709
+ return (
1710
+ node.child_by_field_name(field_name)
1711
+ if hasattr(node, "child_by_field_name")
1712
+ else None
1713
+ )
1714
+ except Exception as e:
1715
+ _log.warning("SymbolGraph._child_by_field failed: %s", e)
1716
+ return None
1717
+
1718
+ def _node_text_buf(self, node: Any, buf: bytes) -> str:
1719
+ """Extract text from a tree-sitter node using the raw byte buffer."""
1720
+ try:
1721
+ if hasattr(node, "start_byte") and hasattr(node, "end_byte"):
1722
+ return buf[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
1723
+ except Exception as e:
1724
+ _log.warning("SymbolGraph._node_text_buf failed: %s", e)
1725
+ return ""
1726
+
1727
+ def _extract_decorators_buf(self, node: Any, buf: bytes) -> list[str]:
1728
+ decorators: list[str] = []
1729
+ decorator = self._child_by_field(node, "decorator")
1730
+ if decorator:
1731
+ text = self._node_text_buf(decorator, buf)
1732
+ if text:
1733
+ decorators.append(text.strip())
1734
+ for child in self._children(node):
1735
+ try:
1736
+ if getattr(child, "type", "") == "decorator":
1737
+ text = self._node_text_buf(child, buf)
1738
+ if text:
1739
+ decorators.append(text.strip())
1740
+ except Exception as e:
1741
+ _log.warning("SymbolGraph._extract_decorators_buf failed: %s", e)
1742
+ return decorators
1743
+
1744
+ def _extract_params_buf(self, node: Any, buf: bytes) -> list[str]:
1745
+ params_node = self._child_by_field(node, "parameters")
1746
+ if not params_node:
1747
+ return []
1748
+ text = self._node_text_buf(params_node, buf)
1749
+ if not text:
1750
+ return []
1751
+ import re
1752
+
1753
+ return [
1754
+ p.strip()
1755
+ for p in re.split(r"[,:]", text.strip("()"))
1756
+ if p.strip() and not p.strip().startswith("*")
1757
+ ]
1758
+
1759
+ def _extract_docstring_buf(self, node: Any, buf: bytes) -> str:
1760
+ body = self._child_by_field(node, "body")
1761
+ if not body:
1762
+ return ""
1763
+ children = self._children(body)
1764
+ if not children:
1765
+ return ""
1766
+ first = children[0]
1767
+ try:
1768
+ if hasattr(first, "type") and first.type in ("expression_statement", "string"):
1769
+ return self._node_text_buf(first, buf)[:200]
1770
+ except Exception as e:
1771
+ _log.warning("SymbolGraph._extract_docstring_buf failed: %s", e)
1772
+ return ""
1773
+
1774
+ # ── Internal: SQLite helpers ───────────────────────────────────────────
1775
+
1776
+ def _insert_symbol(self, conn: sqlite3.Connection, sym: SymbolNode, file: str) -> None:
1777
+ content_hash = (
1778
+ sym.hash or hashlib.md5(f"{sym.name}{sym.line}{sym.kind}".encode()).hexdigest()[:12]
1779
+ )
1780
+ conn.execute(
1781
+ """INSERT OR IGNORE INTO symbols
1782
+ (name, kind, file, line, end_line, parent_id, docstring, language, hash, decorators, params, is_exported)
1783
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
1784
+ (
1785
+ sym.name,
1786
+ sym.kind,
1787
+ file,
1788
+ sym.line,
1789
+ sym.end_line,
1790
+ sym.parent_id,
1791
+ sym.docstring,
1792
+ sym.language,
1793
+ content_hash,
1794
+ ",".join(sym.decorators),
1795
+ ",".join(sym.params),
1796
+ 1 if sym.is_exported else 0,
1797
+ ),
1798
+ )
1799
+
1800
+ def _get_symbols_in_file(self, conn: sqlite3.Connection, file: str) -> list[SymbolNode]:
1801
+ cur = conn.execute("SELECT * FROM symbols WHERE file = ? ORDER BY line", (file,))
1802
+ return [self._row_to_symbol(r) for r in cur.fetchall()]
1803
+
1804
+ def _get_symbol_by_id(self, sid: int) -> SymbolNode | None:
1805
+ conn = self._get_conn()
1806
+ cur = conn.execute("SELECT * FROM symbols WHERE id = ?", (sid,))
1807
+ row = cur.fetchone()
1808
+ return self._row_to_symbol(row) if row else None
1809
+
1810
+ def _row_to_symbol(self, row: sqlite3.Row) -> SymbolNode:
1811
+ return SymbolNode(
1812
+ id=row[0],
1813
+ name=row[1],
1814
+ kind=row[2],
1815
+ file=row[3],
1816
+ line=row[4],
1817
+ end_line=row[5],
1818
+ parent_id=row[6],
1819
+ docstring=row[7] or "",
1820
+ language=row[8],
1821
+ hash=row[9] or "",
1822
+ decorators=row[10].split(",") if row[10] else [],
1823
+ params=row[11].split(",") if row[11] else [],
1824
+ is_exported=bool(row[12]),
1825
+ )
1826
+
1827
+ def _build_edges(self, conn: sqlite3.Connection, files: list[FileInfo]) -> None:
1828
+ """Build symbol-level call/reference edges by scanning ASTs."""
1829
+ for fi in files:
1830
+ abs_path = self.root / fi.path
1831
+ if not abs_path.exists():
1832
+ continue
1833
+ content = abs_path.read_text(encoding="utf-8", errors="ignore")
1834
+ file_symbols = self._get_symbols_in_file(conn, fi.path)
1835
+ if not file_symbols:
1836
+ continue
1837
+
1838
+ refs = self._find_references(content, fi.language)
1839
+ for sym in file_symbols:
1840
+ for ref_name in refs:
1841
+ target = self._find_local_symbol(conn, ref_name, fi.path, sym.line)
1842
+ if target and target.id != sym.id:
1843
+ try:
1844
+ conn.execute(
1845
+ "INSERT OR IGNORE INTO edges (source_id, target_id, kind) VALUES (?, ?, ?)",
1846
+ (sym.id, target.id, "calls"),
1847
+ )
1848
+ except Exception as e:
1849
+ _log.warning("SymbolGraph._build_edges failed: %s", e)
1850
+
1851
+ def _find_references(self, content: str, lang: Lang) -> set[str]:
1852
+ """Extract referenced names from source content using AST."""
1853
+ import ast
1854
+
1855
+ refs: set[str] = set()
1856
+ if lang == Lang.PYTHON:
1857
+ try:
1858
+ tree = ast.parse(content)
1859
+ for node in ast.walk(tree):
1860
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
1861
+ refs.add(node.func.id)
1862
+ elif isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute):
1863
+ refs.add(node.func.attr)
1864
+ elif isinstance(node, ast.Name):
1865
+ refs.add(node.id)
1866
+ except SyntaxError:
1867
+ pass
1868
+ else:
1869
+ parser = get_parser(lang)
1870
+ if parser:
1871
+ try:
1872
+ tree = parser.parse(content.encode("utf-8"))
1873
+ self._walk_calls(tree.root_node, refs)
1874
+ except Exception as e:
1875
+ _log.warning("SymbolGraph._find_references failed: %s", e)
1876
+
1877
+ return refs
1878
+
1879
+ def _walk_calls(self, node: Any, refs: set[str]) -> None:
1880
+ """Walk a tree-sitter AST extracting call target names."""
1881
+ try:
1882
+ ntype = node.type if hasattr(node, "type") else ""
1883
+ except Exception as e:
1884
+ _log.warning("SymbolGraph._walk_calls failed: %s", e)
1885
+ return
1886
+
1887
+ # Common call-like node types across grammars
1888
+ if ntype in (
1889
+ "call_expression",
1890
+ "method_invocation",
1891
+ "function_call_expression",
1892
+ "invocation_expression",
1893
+ ):
1894
+ fn = self._child_by_field(node, "function") or self._child_by_field(node, "name")
1895
+ if fn:
1896
+ try:
1897
+ text = fn.text if hasattr(fn, "text") else b""
1898
+ if isinstance(text, bytes):
1899
+ text = text.decode("utf-8", errors="replace")
1900
+ name = str(text).split(".")[-1].split("::")[-1]
1901
+ if name and name.isidentifier():
1902
+ refs.add(name)
1903
+ except Exception as e:
1904
+ _log.warning("SymbolGraph._walk_calls failed: %s", e)
1905
+
1906
+ # Ruby: call node has method field
1907
+ elif ntype == "call":
1908
+ fn = self._child_by_field(node, "method")
1909
+ if fn:
1910
+ try:
1911
+ text = fn.text if hasattr(fn, "text") else b""
1912
+ if isinstance(text, bytes):
1913
+ text = text.decode("utf-8", errors="replace")
1914
+ name = str(text)
1915
+ if name and name.isidentifier():
1916
+ refs.add(name)
1917
+ except Exception as e:
1918
+ _log.warning("SymbolGraph._walk_calls failed: %s", e)
1919
+
1920
+ for child in node.children if hasattr(node, "children") else []:
1921
+ self._walk_calls(child, refs)
1922
+
1923
+ def _find_local_symbol(
1924
+ self, conn: sqlite3.Connection, name: str, file: str, near_line: int
1925
+ ) -> SymbolNode | None:
1926
+ """Find a symbol by name in the same file, preferring the one closest to near_line."""
1927
+ cur = conn.execute(
1928
+ "SELECT * FROM symbols WHERE file = ? AND name = ? ORDER BY ABS(line - ?) LIMIT 1",
1929
+ (file, name, near_line),
1930
+ )
1931
+ row = cur.fetchone()
1932
+ return self._row_to_symbol(row) if row else None
1933
+
1934
+ def __enter__(self) -> SymbolGraph:
1935
+ return self
1936
+
1937
+ def __exit__(self, *args: Any) -> None:
1938
+ self.close()
1939
+
1940
+
1941
+ # ── Utility ─────────────────────────────────────────────────────────────────────
1942
+
1943
+
1944
+ def _content_hash(content: str) -> str:
1945
+ return hashlib.md5(content.encode("utf-8")).hexdigest()[:12]
1946
+
1947
+
1948
+ def _node_hash_buf(node: Any, buf: bytes) -> str:
1949
+ """Hash the node's source content from a raw byte buffer."""
1950
+ try:
1951
+ if hasattr(node, "start_byte") and hasattr(node, "end_byte"):
1952
+ return hashlib.md5(buf[node.start_byte : node.end_byte]).hexdigest()[:12]
1953
+ except Exception as e:
1954
+ _log.debug("_node_hash_buf failed: %s", e)
1955
+ return hashlib.md5(str(id(node)).encode()).hexdigest()[:12]