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,807 @@
1
+ """
2
+ Proactive Agent — Phase 4 of the Patchi super-agent.
3
+
4
+ Turns a change-set into *safe, mechanical* fixes and applies them behind a flag.
5
+ This is the "proactive" half of Pillar 3's event table: when a file is saved,
6
+ Patchi proposes (and, with ``--apply``, performs) fixes such as:
7
+
8
+ - missing_import → a name is used but never imported/defined; add the import
9
+ - unused_import → an imported name is never referenced; drop the line
10
+ - dead_code → a top-level def is referenced nowhere; flag for removal
11
+ - signature_callers→ a function's signature changed; list/update its callers
12
+ - charter_violation→ the change breaks a guard-rail boundary
13
+ - format → run the project formatter (ruff/black) if available
14
+
15
+ Detection is offline (AST for Python, textual heuristics otherwise) and reads
16
+ only the Layered Brain / import graph — no raw re-scan of the whole repo.
17
+ Applying is always opt-in (``--apply`` for safe fixes, ``--unsafe`` for all).
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import ast
22
+ import logging
23
+ import re
24
+ import shutil
25
+ import subprocess
26
+ from dataclasses import dataclass, field
27
+ from pathlib import Path
28
+ from typing import TYPE_CHECKING
29
+
30
+ from patchi.core.brain import learning
31
+ from patchi.core.brain.layered_brain import _classify_subsystem, _module_of
32
+
33
+ if TYPE_CHECKING:
34
+ from patchi.core.brain.import_graph import ImportGraph
35
+ from patchi.core.brain.scanner import FileInfo
36
+
37
+
38
+ # fix_type → (human label, is_safe_to_auto_apply)
39
+ _FIX_META: dict[str, tuple[str, bool]] = {
40
+ "missing_import": ("Add missing import", True),
41
+ "unused_import": ("Remove unused import", True),
42
+ "dead_code": ("Remove dead code", True),
43
+ "format": ("Format file", True),
44
+ "signature_callers": ("Update callers after signature change", False),
45
+ "charter_violation": ("Charter violation in change", False),
46
+ }
47
+
48
+
49
+ _log = logging.getLogger("patchi.brain.proactive")
50
+
51
+
52
+ @dataclass
53
+ class ProposedFix:
54
+ """A single safe/proposed fix for one changed file."""
55
+
56
+ fix_type: str
57
+ file: str
58
+ description: str
59
+ safe: bool
60
+ name: str = "" # symbol involved (function/class/import name)
61
+ suggested: str = "" # e.g. the import line to add, or replacement text
62
+ callers: list[str] = field(default_factory=list)
63
+ line: int = 0 # line of the symbol (for UI/debug)
64
+
65
+ def to_dict(self) -> dict:
66
+ return {
67
+ "fix_type": self.fix_type,
68
+ "file": self.file,
69
+ "description": self.description,
70
+ "safe": self.safe,
71
+ "name": self.name,
72
+ "suggested": self.suggested,
73
+ "callers": self.callers,
74
+ "line": self.line,
75
+ }
76
+
77
+
78
+ def _word_in_source(name: str, src: str) -> bool:
79
+ return bool(re.search(rf"(?<![\w.]){re.escape(name)}(?![\w])", src))
80
+
81
+
82
+ class ProactiveAgent:
83
+ """Proposes (and optionally applies) safe fixes for a change-set."""
84
+
85
+ def __init__(self, root: Path):
86
+ self.root = Path(root)
87
+
88
+ # ── Analysis ────────────────────────────────────────────────────────────────
89
+
90
+ def analyze_change(
91
+ self,
92
+ changed_files: list[str],
93
+ file_infos: list[FileInfo],
94
+ graph: ImportGraph | None = None,
95
+ charter: object | None = None,
96
+ include_format: bool = True,
97
+ ) -> list[ProposedFix]:
98
+ fi_map = {fi.path: fi for fi in file_infos}
99
+ public_api: dict[str, str] = {}
100
+ for fi in file_infos:
101
+ for fn in fi.functions:
102
+ public_api.setdefault(fn.name, fi.path)
103
+ for cl in fi.classes:
104
+ public_api.setdefault(cl.name, fi.path)
105
+
106
+ # Shared read cache for this whole call. Without it, dead-code checking
107
+ # re-reads every other file from disk for *every candidate symbol in
108
+ # every file* — O(files^2 * symbols_per_file) disk reads. On a
109
+ # whole-project scan (hundreds of files) that's well over a million
110
+ # redundant reads and can make the call take minutes or effectively hang.
111
+ # File contents don't change during this synchronous scan, so caching
112
+ # is safe.
113
+ src_cache: dict[str, str] = {}
114
+
115
+ def _cached_read(path: str) -> str | None:
116
+ if path not in src_cache:
117
+ src = self._read(path)
118
+ if src is None:
119
+ return None
120
+ src_cache[path] = src
121
+ return src_cache[path]
122
+
123
+ fixes: list[ProposedFix] = []
124
+ for path in changed_files:
125
+ fi = fi_map.get(path)
126
+ if not fi:
127
+ continue
128
+ src = _cached_read(path)
129
+ if src is None:
130
+ continue
131
+
132
+ if fi.language == "python":
133
+ fixes += self._check_imports(path, src, fi, public_api)
134
+ fixes += self._check_dead_code(
135
+ path, src, fi, file_infos, fi_map, src_cache=src_cache
136
+ )
137
+ fixes += self._check_signature(path, src, fi)
138
+ if charter is not None:
139
+ fixes += self._check_charter(path, fi, graph, charter)
140
+ if include_format and self._formatter():
141
+ fixes.append(
142
+ ProposedFix(
143
+ "format",
144
+ path,
145
+ "File can be auto-formatted",
146
+ True,
147
+ )
148
+ )
149
+ return fixes
150
+
151
+ # ── Import analysis (Python AST) ──────────────────────────────────────────────
152
+
153
+ def _check_imports(
154
+ self, path: str, src: str, fi: FileInfo, public_api: dict[str, str]
155
+ ) -> list[ProposedFix]:
156
+ try:
157
+ tree = ast.parse(src)
158
+ except SyntaxError:
159
+ return []
160
+
161
+ imported_names: set[str] = set()
162
+ import_lines: dict[str, int] = {}
163
+ for node in ast.walk(tree):
164
+ if isinstance(node, ast.Import):
165
+ for alias in node.names:
166
+ imported_names.add(alias.asname or alias.name.split(".")[0])
167
+ import_lines[alias.asname or alias.name.split(".")[0]] = node.lineno
168
+ elif isinstance(node, ast.ImportFrom):
169
+ for alias in node.names:
170
+ imported_names.add(alias.asname or alias.name)
171
+ import_lines[alias.asname or alias.name] = node.lineno
172
+
173
+ defined_names: set[str] = {fn.name for fn in fi.functions} | {cl.name for cl in fi.classes}
174
+
175
+ used_names: set[str] = set()
176
+ for node in ast.walk(tree):
177
+ if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Load):
178
+ used_names.add(node.id)
179
+ elif isinstance(node, ast.Attribute) and isinstance(node.ctx, ast.Load):
180
+ used_names.add(node.attr)
181
+
182
+ fixes: list[ProposedFix] = []
183
+ # Missing imports
184
+ for name in sorted(used_names - imported_names - defined_names):
185
+ definer = public_api.get(name)
186
+ if definer and definer != path:
187
+ module = definer.rsplit(".py", 1)[0].replace("/", ".")
188
+ fixes.append(
189
+ ProposedFix(
190
+ "missing_import",
191
+ path,
192
+ f"'{name}' is used but not imported; define it in '{definer}'",
193
+ True,
194
+ name=name,
195
+ suggested=f"from {module} import {name}",
196
+ )
197
+ )
198
+ # Unused imports
199
+ for name in sorted(imported_names - used_names - defined_names):
200
+ fixes.append(
201
+ ProposedFix(
202
+ "unused_import",
203
+ path,
204
+ f"Import '{name}' is never referenced",
205
+ True,
206
+ name=name,
207
+ line=import_lines.get(name, 0),
208
+ )
209
+ )
210
+ return fixes
211
+
212
+ # ── Dead code (referenced nowhere in the project) ─────────────────────────────
213
+
214
+ def _check_dead_code(
215
+ self,
216
+ path: str,
217
+ src: str,
218
+ fi: FileInfo,
219
+ file_infos: list[FileInfo],
220
+ fi_map: dict[str, FileInfo],
221
+ src_cache: dict[str, str] | None = None,
222
+ ) -> list[ProposedFix]:
223
+ fixes: list[ProposedFix] = []
224
+ # Map name -> def line span so we can exclude the definition itself
225
+ # when searching for *references* within the same file.
226
+ span: dict[str, tuple[int, int]] = {}
227
+ for f in fi.functions:
228
+ span[f.name] = (getattr(f, "line", 1), getattr(f, "end_line", getattr(f, "line", 1)))
229
+ for c in fi.classes:
230
+ span[c.name] = (getattr(c, "line", 1), getattr(c, "end_line", getattr(c, "line", 1)))
231
+ candidates = list(span)
232
+ # Skip likely entry points (invoked externally, not within the project).
233
+ entry_names = {"main", "run", "__main__"}
234
+ candidates = [n for n in candidates if n not in entry_names and not n.startswith("__")]
235
+
236
+ # Read every other file's source exactly once for this whole method call
237
+ # (shared across all candidate symbols), instead of once per symbol.
238
+ # Uses the caller-provided cache when available so it's shared across
239
+ # the *entire* analyze_change() call too, not just this one file.
240
+ cache = src_cache if src_cache is not None else {}
241
+ other_sources: dict[str, str] = {}
242
+ for other in file_infos:
243
+ if other.path in cache:
244
+ other_sources[other.path] = cache[other.path]
245
+ continue
246
+ src_o = self._read(other.path)
247
+ if src_o is None:
248
+ continue
249
+ cache[other.path] = src_o
250
+ other_sources[other.path] = src_o
251
+
252
+ for name in candidates:
253
+ # Search every file; for the defining file, blank out the def lines
254
+ # so the definition does not count as a reference.
255
+ referenced = False
256
+ for other_path, src_o in other_sources.items():
257
+ if other_path == path:
258
+ lines = src_o.splitlines()
259
+ lo, hi = span.get(name, (0, 0))
260
+ for ln in range(lo - 1, hi):
261
+ if 0 <= ln < len(lines):
262
+ lines[ln] = ""
263
+ src_o = "\n".join(lines)
264
+ if _word_in_source(name, src_o):
265
+ referenced = True
266
+ break
267
+ if not referenced:
268
+ # Find the def line for context.
269
+ line = 0
270
+ for fn in fi.functions:
271
+ if fn.name == name:
272
+ line = getattr(fn, "line", 0)
273
+ for cl in fi.classes:
274
+ if cl.name == name:
275
+ line = getattr(cl, "line", 0)
276
+ fixes.append(
277
+ ProposedFix(
278
+ "dead_code",
279
+ path,
280
+ f"'{name}' is defined but referenced nowhere in the project",
281
+ True,
282
+ name=name,
283
+ line=line,
284
+ )
285
+ )
286
+ return fixes
287
+
288
+ # ── Signature change → caller update ───────────────────────────────────────────
289
+
290
+ def _check_signature(self, path: str, src: str, fi: FileInfo) -> list[ProposedFix]:
291
+ try:
292
+ tree = ast.parse(src)
293
+ except SyntaxError:
294
+ return []
295
+
296
+ current: dict[str, list[str]] = {}
297
+ for node in tree.body:
298
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
299
+ args = node.args
300
+ params = [a.arg for a in args.args]
301
+ params += [a.arg for a in args.kwonlyargs]
302
+ if args.vararg:
303
+ params.append("*" + args.vararg.arg)
304
+ if args.kwarg:
305
+ params.append("**" + args.kwarg.arg)
306
+ current[node.name] = params
307
+
308
+ baseline = self._load_baseline().get(path, {})
309
+ if not baseline:
310
+ self._store_baseline({path: current})
311
+ return []
312
+
313
+ fixes: list[ProposedFix] = []
314
+ for name, params in current.items():
315
+ if name in baseline and baseline[name] != params:
316
+ callers = self._find_callers(path, name)
317
+ fixes.append(
318
+ ProposedFix(
319
+ "signature_callers",
320
+ path,
321
+ f"'{name}' signature changed ({baseline[name]} → {params}); "
322
+ f"review {len(callers)} caller(s)",
323
+ False,
324
+ name=name,
325
+ callers=callers,
326
+ )
327
+ )
328
+ # Update baseline for next run.
329
+ self._store_baseline({path: current})
330
+ return fixes
331
+
332
+ def _find_callers(self, path: str, name: str) -> list[str]:
333
+ """Files (other than path) that contain a call site '<name>('."""
334
+ callers: list[str] = []
335
+ # Use the import graph to narrow the search to importers.
336
+ from patchi.core.brain.import_graph import build_import_graph
337
+
338
+ try:
339
+ graph = build_import_graph(self.root)
340
+ candidates = set(graph.reverse.get(path, set()))
341
+ candidates |= graph.get_transitive_dependents(path)
342
+ except Exception as e:
343
+ _log.warning("ProactiveAgent._find_callers failed: %s", e)
344
+ candidates = set()
345
+ # Fall back to a whole-project textual scan if no graph.
346
+ if not candidates:
347
+ from patchi.core.brain.scanner import FileScanner
348
+
349
+ try:
350
+ candidates = {fi.path for fi in FileScanner(self.root).scan()}
351
+ except Exception as e:
352
+ _log.warning("ProactiveAgent._find_callers failed: %s", e)
353
+ candidates = set()
354
+ for c in candidates:
355
+ if c == path:
356
+ continue
357
+ if re.search(rf"(?<![\w.]){re.escape(name)}\s*\(", self._read(c) or ""):
358
+ callers.append(c)
359
+ return sorted(callers)
360
+
361
+ # ── Charter check on the changed file ──────────────────────────────────────────
362
+
363
+ def _check_charter(
364
+ self, path: str, fi: FileInfo, graph: ImportGraph | None, charter: object
365
+ ) -> list[ProposedFix]:
366
+ from patchi.core.brain.charter import _matches_boundary
367
+
368
+ boundaries = getattr(charter, "boundaries", [])
369
+ if not boundaries:
370
+ return []
371
+ sub = _classify_subsystem(_module_of(path))
372
+ # Subsystems this file imports.
373
+ imported_subs: set[str] = set()
374
+ if graph is not None:
375
+ for tgt in graph.edges.get(path, set()):
376
+ imported_subs.add(_classify_subsystem(_module_of(tgt)))
377
+ fixes: list[ProposedFix] = []
378
+ for b in boundaries:
379
+ for imp in sorted(imported_subs):
380
+ if _matches_boundary(sub, imp, b):
381
+ fixes.append(
382
+ ProposedFix(
383
+ "charter_violation",
384
+ path,
385
+ f"Change makes '{sub}' import '{imp}', violating charter "
386
+ f"'{b.get('from')} must not import {b.get('to')}'",
387
+ False,
388
+ name=f"{sub}→{imp}",
389
+ )
390
+ )
391
+ return fixes
392
+
393
+ # ── Application ────────────────────────────────────────────────────────────────
394
+
395
+ @staticmethod
396
+ def apply(fix: ProposedFix, root: Path, apply_unsafe: bool = False) -> bool:
397
+ """Apply a single fix. Returns True if applied. Safe fixes only unless apply_unsafe."""
398
+ if not fix.safe and not apply_unsafe:
399
+ return False
400
+ path = Path(root) / fix.file
401
+ if not path.exists():
402
+ return False
403
+
404
+ if fix.fix_type == "missing_import":
405
+ return _apply_missing_import(path, fix.suggested)
406
+ if fix.fix_type == "unused_import":
407
+ return _apply_unused_import(path, fix.name, fix.line)
408
+ if fix.fix_type == "dead_code":
409
+ return _apply_dead_code(path, fix.name, fix.line)
410
+ if fix.fix_type == "format":
411
+ return _apply_format(path)
412
+ # signature_callers / charter_violation are never auto-applied.
413
+ return False
414
+
415
+ # ── Helpers ─────────────────────────────────────────────────────────────────────
416
+
417
+ def _read(self, path: str) -> str | None:
418
+ try:
419
+ return (self.root / path).read_text(encoding="utf-8")
420
+ except Exception as e:
421
+ _log.warning("ProactiveAgent._read failed: %s", e)
422
+ return None
423
+
424
+ def _formatter(self) -> str | None:
425
+ for tool in ("ruff", "black"):
426
+ if shutil.which(tool):
427
+ return tool
428
+ return None
429
+
430
+ def _baseline_path(self) -> Path:
431
+ return self.root / ".patchi" / "memory" / "proactive_signatures.json"
432
+
433
+ def _load_baseline(self) -> dict:
434
+ import json
435
+
436
+ p = self._baseline_path()
437
+ if p.exists():
438
+ try:
439
+ return json.loads(p.read_text(encoding="utf-8"))
440
+ except Exception as e:
441
+ _log.warning("ProactiveAgent._load_baseline failed: %s", e)
442
+ return {}
443
+ return {}
444
+
445
+ def _store_baseline(self, update: dict) -> None:
446
+ import json
447
+
448
+ p = self._baseline_path()
449
+ p.parent.mkdir(parents=True, exist_ok=True)
450
+ data = self._load_baseline()
451
+ data.update(update)
452
+ p.write_text(json.dumps(data, indent=2), encoding="utf-8")
453
+
454
+
455
+ # ── Low-level text edits ─────────────────────────────────────────────────────────
456
+
457
+
458
+ def _write_guarded(path: Path, text: str) -> bool:
459
+ """Write file content, refusing to wipe a file to empty/whitespace.
460
+
461
+ Guards the auto-apply path so a 'safe' fix can never destroy a file's
462
+ contents (e.g. removing the only top-level definition as 'dead code').
463
+ """
464
+ if not text.strip():
465
+ return False
466
+ path.write_text(text, encoding="utf-8")
467
+ return True
468
+
469
+
470
+ def _apply_missing_import(path: Path, import_line: str) -> bool:
471
+ lines = path.read_text(encoding="utf-8").splitlines()
472
+ # Insert after the last existing import, else at the top (after docstring).
473
+ insert_at = 0
474
+ last_import = -1
475
+ for i, line in enumerate(lines):
476
+ stripped = line.lstrip()
477
+ if stripped.startswith(("import ", "from ")):
478
+ last_import = i
479
+ elif stripped and not stripped.startswith("#"):
480
+ break
481
+ insert_at = last_import + 1 if last_import >= 0 else 0
482
+ if any(ln.strip() == import_line for ln in lines):
483
+ return False # already present
484
+ lines.insert(insert_at, import_line)
485
+ return _write_guarded(path, "\n".join(lines) + "\n")
486
+
487
+
488
+ def _apply_unused_import(path: Path, name: str, line: int) -> bool:
489
+ lines = path.read_text(encoding="utf-8").splitlines()
490
+ target = line - 1 if line > 0 else None
491
+ if target is not None and 0 <= target < len(lines) and name in lines[target]:
492
+ del lines[target]
493
+ return _write_guarded(path, "\n".join(lines) + "\n")
494
+ # Fallback: remove first import line containing the name.
495
+ for i, line in enumerate(lines):
496
+ if re.search(
497
+ rf"(^|\b)(import\s+.*\b{re.escape(name)}\b|from\s+\S+\s+import\s+.*\b{re.escape(name)}\b)",
498
+ line,
499
+ ):
500
+ del lines[i]
501
+ return _write_guarded(path, "\n".join(lines) + "\n")
502
+ return False
503
+
504
+
505
+ def _apply_dead_code(path: Path, name: str, line: int) -> bool:
506
+ """Remove the top-level def block for ``name`` (best-effort, safe-only behind flag)."""
507
+ src = path.read_text(encoding="utf-8")
508
+ try:
509
+ tree = ast.parse(src)
510
+ except SyntaxError:
511
+ return False
512
+ target = None
513
+ for node in tree.body:
514
+ if (
515
+ isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))
516
+ and node.name == name
517
+ ):
518
+ target = node
519
+ break
520
+ if target is None:
521
+ return False
522
+ start = target.lineno - 1
523
+ end = target.end_lineno # 1-based inclusive
524
+ lines = src.splitlines()
525
+ # Safety: never remove the only top-level definition in a file.
526
+ top_level_defs = [
527
+ n for n in tree.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))
528
+ ]
529
+ if len(top_level_defs) <= 1:
530
+ return False
531
+ del lines[start:end]
532
+ # Drop a trailing blank line left behind.
533
+ while start < len(lines) and lines[start].strip() == "":
534
+ del lines[start]
535
+ return _write_guarded(path, "\n".join(lines) + "\n")
536
+
537
+
538
+ def _apply_format(path: Path) -> bool:
539
+ tool = shutil.which("ruff") or shutil.which("black")
540
+ if not tool:
541
+ return False
542
+ try:
543
+ subprocess.run([tool, "format", str(path)], check=False, capture_output=True, timeout=30)
544
+ return True
545
+ except Exception as e:
546
+ _log.warning("_apply_format failed: %s", e)
547
+ return False
548
+
549
+
550
+ def run_proactive(
551
+ root: Path,
552
+ files: list[str],
553
+ apply: bool = False,
554
+ unsafe: bool = False,
555
+ include_format: bool = True,
556
+ ) -> dict:
557
+ """
558
+ High-level entry point used by `p auto` and watch mode.
559
+
560
+ Scans project context, proposes proactive fixes for the given changed files,
561
+ optionally applies the safe ones, and honours learned rejections
562
+ (see patchi.core.brain.learning — the Learning Brain).
563
+
564
+ Returns a structured result:
565
+ {
566
+ "fixes": [ProposedFix] not suppressed,
567
+ "applied": [ProposedFix] actually applied,
568
+ "escalated": [ProposedFix] unsafe (need human/Governor review),
569
+ "skipped": [ProposedFix] safe but not applied (apply=False),
570
+ "suppressed": [ProposedFix] hidden because the user rejected them,
571
+ }
572
+ """
573
+ from patchi.core.brain.charter import load_charter
574
+ from patchi.core.brain.import_graph import build_import_graph
575
+ from patchi.core.brain.scanner import FileScanner
576
+
577
+ scanner = FileScanner(root)
578
+ file_infos = scanner.scan()
579
+ graph = build_import_graph(root)
580
+ charter = load_charter(root)
581
+ agent = ProactiveAgent(root)
582
+ all_fixes = agent.analyze_change(
583
+ files, file_infos, graph, charter, include_format=include_format
584
+ )
585
+
586
+ # Phase 5 — Learning Brain: don't propose fix types the user rejects.
587
+ suppressed = [f for f in all_fixes if not learning.should_suggest(f.fix_type, root)]
588
+ active = [f for f in all_fixes if f not in suppressed]
589
+
590
+ applied, escalated, skipped = [], [], []
591
+ for f in active:
592
+ if apply and ProactiveAgent.apply(f, root, apply_unsafe=unsafe):
593
+ applied.append(f)
594
+ learning.record_acceptance(f.fix_type, "ProactiveAgent", root)
595
+ elif not f.safe:
596
+ escalated.append(f)
597
+ else:
598
+ skipped.append(f)
599
+
600
+ # Record to fix history when fixes are applied
601
+ if applied:
602
+ _record_fix_history(root, applied, skipped)
603
+
604
+ return {
605
+ "fixes": active,
606
+ "applied": applied,
607
+ "escalated": escalated,
608
+ "skipped": skipped,
609
+ "suppressed": suppressed,
610
+ }
611
+
612
+
613
+ def _record_fix_history(root: Path, applied: list[ProposedFix], skipped: list[ProposedFix]) -> None:
614
+ """Record a fix operation to the history file for the charter page."""
615
+ if not applied and not skipped:
616
+ return
617
+
618
+ import json
619
+ from datetime import UTC, datetime
620
+
621
+ history_path = root / ".patchi" / "memory" / "fix_history.json"
622
+ history_path.parent.mkdir(parents=True, exist_ok=True)
623
+
624
+ # Load existing history
625
+ history = []
626
+ if history_path.exists():
627
+ try:
628
+ history = json.loads(history_path.read_text(encoding="utf-8"))
629
+ except Exception:
630
+ history = []
631
+
632
+ # Record this operation
633
+ entry = {
634
+ "id": datetime.now(UTC).strftime("%Y%m%d-%H%M%S-%f"),
635
+ "timestamp": datetime.now(UTC).isoformat(),
636
+ "applied": [f.to_dict() for f in applied],
637
+ "skipped": [f.to_dict() for f in skipped],
638
+ "files_changed": list({f.file for f in applied}),
639
+ "count": len(applied),
640
+ }
641
+
642
+ history.append(entry)
643
+
644
+ # Keep last 50 entries
645
+ history = history[-50:]
646
+
647
+ history_path.write_text(json.dumps(history, indent=2, default=str), encoding="utf-8")
648
+
649
+
650
+ def load_fix_history(root: Path, limit: int = 20) -> list[dict]:
651
+ """Load fix history for the charter page."""
652
+ import json
653
+
654
+ history_path = root / ".patchi" / "memory" / "fix_history.json"
655
+ if not history_path.exists():
656
+ return []
657
+
658
+ try:
659
+ history = json.loads(history_path.read_text(encoding="utf-8"))
660
+ return list(reversed(history[-limit:])) # newest first
661
+ except Exception:
662
+ return []
663
+
664
+
665
+ def revert_fix(root: Path, fix_id: str) -> dict:
666
+ """Revert a fix operation by restoring files from git."""
667
+ import json
668
+ import subprocess
669
+
670
+ history_path = root / ".patchi" / "memory" / "fix_history.json"
671
+ if not history_path.exists():
672
+ return {"success": False, "error": "No history found"}
673
+
674
+ try:
675
+ history = json.loads(history_path.read_text(encoding="utf-8"))
676
+ except Exception:
677
+ return {"success": False, "error": "Failed to read history"}
678
+
679
+ # Find the entry
680
+ entry = None
681
+ for h in history:
682
+ if h.get("id") == fix_id:
683
+ entry = h
684
+ break
685
+
686
+ if not entry:
687
+ return {"success": False, "error": f"Fix {fix_id} not found"}
688
+
689
+ # Restore files from git
690
+ reverted = []
691
+ failed = []
692
+ for file_info in entry.get("applied", []):
693
+ file_path = file_info.get("file", "")
694
+ if not file_path:
695
+ continue
696
+ try:
697
+ root / file_path
698
+ result = subprocess.run(
699
+ ["git", "checkout", "HEAD", "--", file_path],
700
+ cwd=str(root),
701
+ capture_output=True,
702
+ text=True,
703
+ timeout=10,
704
+ )
705
+ if result.returncode == 0:
706
+ reverted.append(file_path)
707
+ else:
708
+ failed.append(file_path)
709
+ except Exception:
710
+ failed.append(file_path)
711
+
712
+ # Remove from history
713
+ history = [h for h in history if h.get("id") != fix_id]
714
+ history_path.write_text(json.dumps(history, indent=2, default=str), encoding="utf-8")
715
+
716
+ return {
717
+ "success": len(failed) == 0,
718
+ "reverted": reverted,
719
+ "failed": failed,
720
+ "count": len(reverted),
721
+ }
722
+
723
+
724
+ def escalate_to_governor(root: Path, fix: ProposedFix) -> None:
725
+ """
726
+ Surface an unsafe fix (e.g. charter violation) to human review by recording
727
+ it as an open issue. This is the lightweight Governor escalation sink — the
728
+ issue shows up in the unified findings/issues list for both humans and ants.
729
+ """
730
+ from patchi.core import memory as mem
731
+
732
+ mem.save_issue(
733
+ {
734
+ "source": "ProactiveAgent",
735
+ "fix_type": fix.fix_type,
736
+ "file": fix.file,
737
+ "name": fix.name,
738
+ "description": fix.description,
739
+ "severity": "high" if fix.fix_type == "charter_violation" else "medium",
740
+ },
741
+ root,
742
+ )
743
+
744
+
745
+ # ── Prioritized Fix List (Dream Assistant spec) ──────────────────────────────────
746
+
747
+ # Lower number = higher priority (fix first). Charter/contract breaches and
748
+ # breaking changes rank above cosmetic cleanups.
749
+ _FIX_PRIORITY: dict[str, int] = {
750
+ "charter_violation": 1,
751
+ "signature_callers": 2,
752
+ "missing_import": 3,
753
+ "dead_code": 4,
754
+ "unused_import": 5,
755
+ "format": 6,
756
+ }
757
+
758
+
759
+ def rank_fixes(fixes: list[ProposedFix]) -> list[ProposedFix]:
760
+ """Return fixes sorted by priority (high→low), then safe-before-unsafe."""
761
+ return sorted(
762
+ fixes,
763
+ key=lambda f: (_FIX_PRIORITY.get(f.fix_type, 9), 0 if f.safe else 1, f.file, f.name),
764
+ )
765
+
766
+
767
+ def build_fix_list(
768
+ root: Path,
769
+ area: str | None = None,
770
+ include_format: bool = False,
771
+ include_missing_import: bool = False,
772
+ ) -> list[ProposedFix]:
773
+ """
774
+ Propose fixes across the whole project (or an area) and return them ranked
775
+ — the standalone "Prioritized Fix List" view.
776
+
777
+ ``missing_import`` is excluded by default: across a whole repo every
778
+ cross-module symbol reads as "used but not imported in this file", which is
779
+ noise here (it is meaningful only for *changed* files in ``run_proactive``).
780
+ """
781
+ from patchi.core.brain.charter import load_charter
782
+ from patchi.core.brain.import_graph import build_import_graph
783
+ from patchi.core.brain.scanner import FileScanner
784
+
785
+ root = Path(root)
786
+ scanner = FileScanner(root)
787
+ file_infos = scanner.scan()
788
+ graph = build_import_graph(root)
789
+ charter = load_charter(root)
790
+ agent = ProactiveAgent(root)
791
+
792
+ if area:
793
+ area_rel = str(area).rstrip("/\\")
794
+ files = [
795
+ fi.path
796
+ for fi in file_infos
797
+ if fi.path == area_rel or fi.path.startswith(area_rel + "/")
798
+ ]
799
+ else:
800
+ files = [fi.path for fi in file_infos]
801
+
802
+ fixes = agent.analyze_change(files, file_infos, graph, charter, include_format=include_format)
803
+ # Honour learned rejections (Phase 5) for the list too.
804
+ visible = [f for f in fixes if learning.should_suggest(f.fix_type, root)]
805
+ if not include_missing_import:
806
+ visible = [f for f in visible if f.fix_type != "missing_import"]
807
+ return rank_fixes(visible)