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,139 @@
1
+ """
2
+ AST utilities — function/method call finding across languages.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ from typing import Any
9
+
10
+ from patchi.core.brain.languages import Lang, get_parser
11
+
12
+ from .config import CALL_NODE_TYPES
13
+ from .helpers import _leaf_name, child_by_field, children, node_text
14
+ from .scan import scan_python
15
+
16
+ _log = logging.getLogger("patchi.brain.calls")
17
+
18
+
19
+ def find_calls(content: str, lang: Lang, names: set[str]) -> list[dict]:
20
+ """
21
+ Find all function/method calls in source whose name matches one of `names`.
22
+
23
+ Returns list of dicts with keys: name, line, col, full_text.
24
+
25
+ Python is served from the shared single-pass AST scan (one parse + walk
26
+ per file per process, cached) — repeated calls with different sink sets
27
+ no longer re-parse the file.
28
+ """
29
+ if lang == Lang.PYTHON:
30
+ return _find_calls_python(content, names)
31
+ parser = get_parser(lang)
32
+ if not parser:
33
+ return []
34
+ try:
35
+ tree = parser.parse(content.encode("utf-8"))
36
+ except Exception as e:
37
+ _log.warning("find_calls failed: %s", e)
38
+ return []
39
+ call_types = CALL_NODE_TYPES.get(lang, set())
40
+ results: list[dict] = []
41
+ _walk_calls(tree.root_node, call_types, names, results)
42
+ return results
43
+
44
+
45
+ def _find_calls_python(content: str, names: set[str]) -> list[dict]:
46
+ """Filter the single-pass scan for matching call names."""
47
+ from pathlib import Path as _Path
48
+
49
+ scan = scan_python(content, file_path=_Path("<ast_utils>"))
50
+ results: list[dict] = []
51
+ for call in scan.get("calls", []):
52
+ fn = call.get("function", call.get("name", ""))
53
+ leaf = _leaf_name(fn)
54
+ if fn in names or leaf in names:
55
+ # Compute full text from source lines
56
+ line_no = call.get("line", 0)
57
+ full_text = ""
58
+ if content and line_no > 0:
59
+ src_lines = content.splitlines()
60
+ if 0 < line_no <= len(src_lines):
61
+ full_text = src_lines[line_no - 1]
62
+ results.append(
63
+ {
64
+ "name": fn,
65
+ "line": line_no,
66
+ "col": call.get("col", 0),
67
+ "full_text": full_text,
68
+ }
69
+ )
70
+ return results
71
+
72
+
73
+ def _extract_call_name(node: Any) -> str:
74
+ fn = child_by_field(node, "function") or child_by_field(node, "name")
75
+ if fn is not None:
76
+ return node_text(fn).split("(")[0].strip()
77
+ # Fallback: join all identifier/simple children before argument_list
78
+ parts: list[str] = []
79
+ for child in children(node):
80
+ if child.type in ("argument_list", "arguments", "value_arguments", "argument_part"):
81
+ break
82
+ txt = node_text(child)
83
+ if txt and txt not in ("(", ")", ".", "::"):
84
+ parts.append(txt)
85
+ if parts:
86
+ return ".".join(parts)
87
+ return ""
88
+
89
+
90
+ def _walk_calls(node: Any, call_types: set[str], names: set[str], results: list[dict]) -> None:
91
+ try:
92
+ ntype = node.type if hasattr(node, "type") else ""
93
+ except Exception as e:
94
+ _log.warning("_walk_calls failed: %s", e)
95
+ return
96
+
97
+ if ntype in call_types:
98
+ fn = _extract_call_name(node)
99
+ if fn:
100
+ leaf = _leaf_name(fn)
101
+ if fn in names or leaf in names:
102
+ try:
103
+ line = node.start_point[0] + 1 if node.start_point else 0
104
+ col = node.start_point[1] if node.start_point else 0
105
+ except Exception as e:
106
+ _log.warning("_walk_calls failed: %s", e)
107
+ line, col = 0, 0
108
+ results.append(
109
+ {
110
+ "name": fn,
111
+ "line": line,
112
+ "col": col,
113
+ "full_text": node_text(node),
114
+ }
115
+ )
116
+
117
+ # For Ruby: also check call method name (avoid duplicate of existing match)
118
+ if ntype == "call" and not any(r["name"] == fn for r in results):
119
+ method = child_by_field(node, "method")
120
+ if method:
121
+ fn_name = node_text(method)
122
+ leaf = _leaf_name(fn_name)
123
+ if fn_name in names or leaf in names:
124
+ try:
125
+ line = node.start_point[0] + 1 if node.start_point else 0
126
+ except Exception as e:
127
+ _log.warning("_walk_calls failed: %s", e)
128
+ line = 0
129
+ results.append(
130
+ {
131
+ "name": fn_name,
132
+ "line": line,
133
+ "col": 0,
134
+ "full_text": node_text(node),
135
+ }
136
+ )
137
+
138
+ for child in children(node):
139
+ _walk_calls(child, call_types, names, results)
@@ -0,0 +1,132 @@
1
+ """
2
+ AST utilities — per-language node type configs and common sink sets.
3
+
4
+ Centralized configuration used by security agents, type checkers, and route
5
+ detectors when walking tree-sitter parse trees or the Python `ast` module.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from patchi.core.brain.languages import Lang
11
+
12
+ # ── Per-language node type config ─────────────────────────────────────────────
13
+
14
+ # Languages in this codebase use tree-sitter (except Python which uses ast)
15
+ CALL_NODE_TYPES: dict[Lang, set[str]] = {
16
+ Lang.PYTHON: set(), # handled by ast
17
+ Lang.JAVASCRIPT: {"call_expression"},
18
+ Lang.TYPESCRIPT: {"call_expression"},
19
+ Lang.RUST: {"call_expression"},
20
+ Lang.JAVA: {"method_invocation"},
21
+ Lang.GO: {"call_expression"},
22
+ Lang.C: {"call_expression"},
23
+ Lang.CPP: {"call_expression"},
24
+ Lang.SWIFT: {"function_call_expression"},
25
+ Lang.RUBY: {"call"},
26
+ Lang.PHP: {"function_call_expression", "scoped_call_expression"},
27
+ Lang.C_SHARP: {"invocation_expression"},
28
+ Lang.KOTLIN: {"call_expression"},
29
+ Lang.DART: {"function_expression"},
30
+ }
31
+
32
+ IMPORT_NODE_TYPES: dict[Lang, set[str]] = {
33
+ Lang.PYTHON: set(), # handled by ast
34
+ Lang.JAVASCRIPT: {"import_statement", "import_require_clause"},
35
+ Lang.TYPESCRIPT: {"import_statement", "import_require_clause"},
36
+ Lang.RUST: {"use_declaration"},
37
+ Lang.JAVA: {"import_declaration"},
38
+ Lang.GO: {"import_declaration"},
39
+ Lang.C: {"preproc_include"},
40
+ Lang.CPP: {"preproc_include"},
41
+ Lang.SWIFT: {"import_declaration"},
42
+ Lang.RUBY: {"call"}, # require/include are calls with specific names
43
+ Lang.PHP: {"namespace_use_declaration", "use_declaration"},
44
+ Lang.C_SHARP: {"using_directive"},
45
+ Lang.KOTLIN: {"import"},
46
+ Lang.DART: {"import"},
47
+ }
48
+
49
+ DECORATOR_NODE_TYPES: dict[Lang, set[str]] = {
50
+ Lang.PYTHON: set(), # handled by ast
51
+ Lang.JAVA: {"marker_annotation", "annotation"},
52
+ Lang.C_SHARP: {"attribute"},
53
+ Lang.KOTLIN: {"annotation"},
54
+ Lang.PHP: {"attribute_group"},
55
+ }
56
+
57
+ # ── Common function names used across agents ─────────────────────────────────
58
+
59
+ EXEC_SINKS = {
60
+ "execute",
61
+ "exec",
62
+ "eval",
63
+ "execScript",
64
+ "Runtime.getRuntime().exec",
65
+ "subprocess.call",
66
+ "subprocess.Popen",
67
+ "subprocess.run",
68
+ "subprocess.check_call",
69
+ "subprocess.check_output",
70
+ "os.system",
71
+ "os.popen",
72
+ "ProcessBuilder",
73
+ "shell_exec",
74
+ "system",
75
+ "passthru",
76
+ "exec_",
77
+ "child_process.exec",
78
+ "child_process.execSync",
79
+ "child_process.spawnSync",
80
+ "child_process.spawn",
81
+ "child_process.execFile",
82
+ "execSync",
83
+ "spawnSync",
84
+ }
85
+
86
+ SQL_SINKS = {
87
+ "execute",
88
+ "execute_query",
89
+ "executeQuery",
90
+ "executeUpdate",
91
+ "exec",
92
+ "query",
93
+ "raw_query",
94
+ "prepare",
95
+ "preparedStatement",
96
+ "cursor.execute",
97
+ "cursor.executemany",
98
+ "db.execute",
99
+ "session.execute",
100
+ "EntityManager.createQuery",
101
+ "entityManager.createQuery",
102
+ "JdbcTemplate.query",
103
+ "jdbcTemplate.query",
104
+ }
105
+
106
+ HTTP_CLIENTS = {
107
+ "requests.get",
108
+ "requests.post",
109
+ "requests.put",
110
+ "requests.delete",
111
+ "requests.patch",
112
+ "httpx.get",
113
+ "httpx.post",
114
+ "httpx.put",
115
+ "httpx.delete",
116
+ "httpx.patch",
117
+ "aiohttp.ClientSession.get",
118
+ "aiohttp.ClientSession.post",
119
+ "urllib.request.urlopen",
120
+ "urllib.request.Request",
121
+ "urllib3.request",
122
+ "curl",
123
+ "wget",
124
+ "fetch",
125
+ "axios.get",
126
+ "axios.post",
127
+ "axios.put",
128
+ "axios.delete",
129
+ "got",
130
+ "node-fetch",
131
+ "superagent",
132
+ }
@@ -0,0 +1,74 @@
1
+ """
2
+ AST utilities — control flow and data flow helpers.
3
+
4
+ Provides generic building blocks used by taint tracking:
5
+ - tracking which variables are declared in which scope
6
+ - following a variable's aliases (a = b; c = a; use(c))
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any
12
+
13
+ from patchi.core.brain.languages import Lang
14
+
15
+ from .helpers import children
16
+
17
+ # Tree-sitter node types that open a new scope / block
18
+ _SCOPE_NODE_TYPES = {
19
+ "block",
20
+ "function_declaration",
21
+ "function_item",
22
+ "method_declaration",
23
+ "function_definition",
24
+ "lambda_expression",
25
+ "arrow_function",
26
+ "class_declaration",
27
+ "class_definition",
28
+ "do_statement",
29
+ "while_statement",
30
+ "for_statement",
31
+ "for_in_statement",
32
+ "if_statement",
33
+ "compound_statement",
34
+ "method_definition",
35
+ }
36
+
37
+
38
+ def named_children_of_type(node: Any, type_name: str) -> list[Any]:
39
+ """Return immediate children whose type matches `type_name`."""
40
+ out = []
41
+ for child in children(node):
42
+ if getattr(child, "type", "") == type_name:
43
+ out.append(child)
44
+ return out
45
+
46
+
47
+ def walk_blocks(node: Any, lang: Lang | None = None) -> Any:
48
+ """Yield every node in a tree (generic pre-order walk)."""
49
+ yield node
50
+ for child in children(node):
51
+ yield from walk_blocks(child, lang)
52
+
53
+
54
+ def collect_call_arguments(call_node: Any) -> list[Any]:
55
+ """Return argument child nodes for a tree-sitter call/invocation node."""
56
+ args = (
57
+ child_by_field_any(call_node, "arguments")
58
+ or child_by_field_any(call_node, "argument_list")
59
+ or child_by_field_any(call_node, "value_arguments")
60
+ or child_by_field_any(call_node, "argument_part")
61
+ )
62
+ if args is None:
63
+ return []
64
+ return [c for c in children(args) if c.type not in ("(", ")", ",", "[", "]", "{", "}")]
65
+
66
+
67
+ def child_by_field_any(node: Any, *fields: str) -> Any | None:
68
+ """child_by_field_name for the first matching field name."""
69
+ for f in fields:
70
+ if hasattr(node, "child_by_field_name"):
71
+ res = node.child_by_field_name(f)
72
+ if res is not None:
73
+ return res
74
+ return None
@@ -0,0 +1,205 @@
1
+ """
2
+ AST utilities — intra-file dead-symbol detection across languages.
3
+
4
+ Finds top-level (file-scope) functions / classes / methods / constants that are
5
+ never referenced anywhere else in the same file. Cross-file references are the
6
+ job of the import graph (file-level dead detection); this complements it with a
7
+ symbol-level signal that works for every tree-sitter language without external
8
+ tooling (vulture, ts-prune, etc.).
9
+
10
+ Conservative by design — only flags file-scope symbols that:
11
+ - are not exported (no `export` / `pub` modifier),
12
+ - are not entry-point names (main/app/index/...),
13
+ - have a length >= 3 (avoids false positives on trivial names),
14
+ - never appear as an identifier/reference on any line other than their own.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import ast as _py_ast
19
+ import logging
20
+
21
+ from patchi.core.brain.languages import Lang, get_parser
22
+
23
+ from .helpers import child_by_field, node_text
24
+
25
+ # Node types that introduce a named symbol at file scope.
26
+ _DEF_NODE_TYPES = {
27
+ "function_definition",
28
+ "function_declaration",
29
+ "function_item",
30
+ "function_method_definition",
31
+ "method_definition",
32
+ "method_declaration",
33
+ "method",
34
+ "class_definition",
35
+ "class_declaration",
36
+ "class_item",
37
+ "decorated_definition",
38
+ "lexical_declaration",
39
+ "variable_declaration",
40
+ "const_declaration",
41
+ "abstract_method_signature",
42
+ }
43
+
44
+ # Containers whose direct children may be top-level definitions.
45
+ _ROOT_CONTAINERS = {
46
+ "program",
47
+ "source_file",
48
+ "module",
49
+ "translation_unit",
50
+ "compilation_unit",
51
+ "block",
52
+ "text",
53
+ "namespace_definition",
54
+ }
55
+
56
+ # Node types whose text counts as a reference to a name.
57
+ _REF_NODE_TYPES = {
58
+ "identifier",
59
+ "name",
60
+ "property_identifier",
61
+ "field_identifier",
62
+ "type_identifier",
63
+ "shorthand_property_identifier",
64
+ "shorthand_property_identifier_pattern",
65
+ }
66
+
67
+ _ENTRY_NAMES = frozenset(
68
+ {
69
+ "main",
70
+ "app",
71
+ "index",
72
+ "run",
73
+ "start",
74
+ "server",
75
+ "init",
76
+ "__init__",
77
+ "setup",
78
+ "handler",
79
+ }
80
+ )
81
+
82
+
83
+ _log = logging.getLogger("patchi.brain.dead_symbols")
84
+
85
+
86
+ def find_dead_symbols(content: str, lang: Lang, *, min_name_len: int = 3) -> list[dict]:
87
+ """
88
+ Return a list of intra-file dead symbols.
89
+
90
+ Each entry: {name, line, kind, file_scope}.
91
+ `file_scope` is True for module/file-level symbols (safe to report as dead),
92
+ False for nested symbols (report with care).
93
+ """
94
+ if lang == Lang.PYTHON:
95
+ return _find_dead_python(content, min_name_len)
96
+ parser = get_parser(lang)
97
+ if not parser:
98
+ return []
99
+ try:
100
+ tree = parser.parse(content.encode("utf-8"))
101
+ except Exception as e:
102
+ _log.warning("find_dead_symbols failed: %s", e)
103
+ return []
104
+
105
+ refs = _collect_refs(tree.root_node)
106
+ dead: list[dict] = []
107
+ _walk_defs(tree.root_node, refs, dead, min_name_len)
108
+ return dead
109
+
110
+
111
+ # ── Python ─────────────────────────────────────────────────────────────────────
112
+
113
+
114
+ def _find_dead_python(content: str, min_name_len: int) -> list[dict]:
115
+ try:
116
+ tree = _py_ast.parse(content)
117
+ except SyntaxError:
118
+ return []
119
+
120
+ def_lines: dict[str, int] = {}
121
+ for node in tree.body:
122
+ if isinstance(node, (_py_ast.FunctionDef, _py_ast.AsyncFunctionDef, _py_ast.ClassDef)):
123
+ if len(node.name) >= min_name_len and node.name not in _ENTRY_NAMES:
124
+ def_lines[node.name] = node.lineno
125
+
126
+ if not def_lines:
127
+ return []
128
+
129
+ ref_lines: dict[str, set[int]] = {}
130
+ for node in _py_ast.walk(tree):
131
+ if isinstance(node, _py_ast.Name):
132
+ ref_lines.setdefault(node.id, set()).add(getattr(node, "lineno", 0))
133
+
134
+ dead: list[dict] = []
135
+ for name, def_line in def_lines.items():
136
+ occurrences = ref_lines.get(name, set())
137
+ used = any(ln != def_line for ln in occurrences)
138
+ if not used:
139
+ dead.append(
140
+ {"name": name, "line": def_line, "kind": "function_or_class", "file_scope": True}
141
+ )
142
+ return dead
143
+
144
+
145
+ # ── Tree-sitter (all other languages) ─────────────────────────────────────────
146
+
147
+
148
+ def _collect_refs(node) -> dict[str, set[int]]:
149
+ """Map every identifier-like text to the set of lines it appears on."""
150
+ refs: dict[str, set[int]] = {}
151
+ stack = [node]
152
+ while stack:
153
+ cur = stack.pop()
154
+ if cur.type in _REF_NODE_TYPES:
155
+ text = cur.text.decode("utf-8", errors="replace")
156
+ if text:
157
+ refs.setdefault(text, set()).add(cur.start_point[0] + 1)
158
+ stack.extend(cur.children)
159
+ return refs
160
+
161
+
162
+ def _def_name(node) -> str | None:
163
+ nf = child_by_field(node, "name")
164
+ if nf is not None:
165
+ return node_text(nf).strip()
166
+ # variable / const declarations carry a declarator with a `name` field
167
+ if node.type in ("lexical_declaration", "variable_declaration", "const_declaration"):
168
+ for child in node.children:
169
+ if child.type in ("variable_declarator", "declarator", "pair"):
170
+ name_node = child_by_field(child, "name")
171
+ if name_node is not None:
172
+ return node_text(name_node).strip()
173
+ return None
174
+
175
+
176
+ def _is_exported(line_text: str) -> bool:
177
+ stripped = line_text.lstrip()
178
+ return stripped.startswith("export") or " pub " in line_text or stripped.startswith("pub ")
179
+
180
+
181
+ def _walk_defs(node, refs: dict[str, set[int]], dead: list[dict], min_name_len: int) -> None:
182
+ stack = [node]
183
+ while stack:
184
+ cur = stack.pop()
185
+ if cur.type in _DEF_NODE_TYPES:
186
+ name = _def_name(cur)
187
+ if name and len(name) >= min_name_len and name not in _ENTRY_NAMES:
188
+ line_no = cur.start_point[0] + 1
189
+ line_text = cur.text.decode("utf-8", errors="replace")
190
+ if _is_exported(line_text):
191
+ stack.extend(cur.children)
192
+ continue
193
+ occurrences = refs.get(name, set())
194
+ used = any(ln != line_no for ln in occurrences)
195
+ parent = cur.parent
196
+ file_scope = parent is not None and parent.type in _ROOT_CONTAINERS
197
+ if not used and file_scope:
198
+ kind = "method" if cur.type == "method_definition" else cur.type
199
+ dead.append({"name": name, "line": line_no, "kind": kind, "file_scope": True})
200
+ stack.extend(cur.children)
201
+ else:
202
+ stack.extend(cur.children)
203
+
204
+
205
+ __all__ = ["find_dead_symbols"]
@@ -0,0 +1,108 @@
1
+ """
2
+ AST utilities — decorator/annotation finding across languages.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import ast as _py_ast
8
+ import logging
9
+ from typing import Any
10
+
11
+ from patchi.core.brain.languages import Lang, get_parser
12
+
13
+ from .config import DECORATOR_NODE_TYPES
14
+ from .helpers import child_by_field, children, node_text
15
+
16
+ _log = logging.getLogger("patchi.brain.decorators")
17
+
18
+
19
+ def find_decorators(content: str, lang: Lang, names: set[str]) -> list[dict]:
20
+ """
21
+ Find all decorators/annotations matching one of `names`.
22
+ Returns list of dicts with keys: name, line, full_text.
23
+ """
24
+ if lang == Lang.PYTHON:
25
+ return _find_decorators_python(content, names)
26
+ parser = get_parser(lang)
27
+ if not parser:
28
+ return []
29
+ try:
30
+ tree = parser.parse(content.encode("utf-8"))
31
+ except Exception as e:
32
+ _log.warning("find_decorators failed: %s", e)
33
+ return []
34
+ dec_types = DECORATOR_NODE_TYPES.get(lang, set())
35
+ results: list[dict] = []
36
+ _walk_decorators(tree.root_node, dec_types, names, results)
37
+ return results
38
+
39
+
40
+ def _find_decorators_python(content: str, names: set[str]) -> list[dict]:
41
+ results: list[dict] = []
42
+ try:
43
+ tree = _py_ast.parse(content)
44
+ except SyntaxError:
45
+ return results
46
+ for node in _py_ast.walk(tree):
47
+ if isinstance(node, (_py_ast.FunctionDef, _py_ast.AsyncFunctionDef, _py_ast.ClassDef)):
48
+ for dec in node.decorator_list:
49
+ dec_name = _py_decorator_name(dec)
50
+ if dec_name in names:
51
+ results.append(
52
+ {
53
+ "name": dec_name,
54
+ "line": getattr(dec, "lineno", getattr(node, "lineno", 0)),
55
+ "full_text": _ast_text(dec, content) if content else "",
56
+ }
57
+ )
58
+ return results
59
+
60
+
61
+ def _ast_text(node: _py_ast.AST, source: str) -> str:
62
+ try:
63
+ lines = source.splitlines()
64
+ start = getattr(node, "lineno", 1) - 1
65
+ end = getattr(node, "end_lineno", start + 1) - 1
66
+ return "\n".join(lines[start : end + 1])
67
+ except Exception as e:
68
+ _log.debug("_ast_text failed: %s", e)
69
+ return ""
70
+
71
+
72
+ def _py_decorator_name(node: _py_ast.AST) -> str:
73
+ if isinstance(node, _py_ast.Name):
74
+ return node.id
75
+ if isinstance(node, _py_ast.Attribute):
76
+ return node.attr
77
+ if isinstance(node, _py_ast.Call):
78
+ return _py_decorator_name(node.func)
79
+ return ""
80
+
81
+
82
+ def _walk_decorators(node: Any, dec_types: set[str], names: set[str], results: list[dict]) -> None:
83
+ try:
84
+ ntype = node.type if hasattr(node, "type") else ""
85
+ except Exception as e:
86
+ _log.warning("_walk_decorators failed: %s", e)
87
+ return
88
+
89
+ if ntype in dec_types:
90
+ name_node = child_by_field(node, "name") or child_by_field(node, "type")
91
+ if name_node:
92
+ dec_name = node_text(name_node)
93
+ if any(n in dec_name.lower() for n in names):
94
+ try:
95
+ line = node.start_point[0] + 1 if node.start_point else 0
96
+ except Exception as e:
97
+ _log.warning("_walk_decorators failed: %s", e)
98
+ line = 0
99
+ results.append(
100
+ {
101
+ "name": dec_name,
102
+ "line": line,
103
+ "full_text": node_text(node),
104
+ }
105
+ )
106
+
107
+ for child in children(node):
108
+ _walk_decorators(child, dec_types, names, results)