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,473 @@
1
+ """
2
+ Project Charter — the Guard Rails pillar of the Patchi super-agent.
3
+
4
+ The user declares what the project *should be* in natural language:
5
+
6
+ p charter "This is a Flask+React monorepo. Frontend must never import
7
+ backend DB modules. All API routes need tests. No hardcoded secrets.
8
+ Services under 80 lines."
9
+
10
+ This is parsed (heuristically now, LLM-assisted later) into a structured
11
+ :class:`Charter` with four rule families:
12
+
13
+ - stack → expected languages / frameworks (drift if a new one appears)
14
+ - boundaries → forbidden import edges between layers (architecture drift)
15
+ - conventions → naming, max file/function size, required test coverage
16
+ - security → no hardcoded secrets, no pickle/eval, parameterized queries
17
+
18
+ The charter is stored in `.patchi/memory/charter.json` and checked on every
19
+ scan. Violations are surfaced as ``charter-drift`` findings so that **both
20
+ humans (via ``p scan``) and agents (via the Governor) stay in context**.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import json
25
+ import logging
26
+ import re
27
+ from dataclasses import dataclass, field
28
+ from pathlib import Path
29
+
30
+ # ── Known frameworks / languages for stack detection ──────────────────────────
31
+
32
+ _KNOWN_FRAMEWORKS = [
33
+ "flask",
34
+ "django",
35
+ "fastapi",
36
+ "starlette",
37
+ "tornado",
38
+ "sanic",
39
+ "litestar",
40
+ "express",
41
+ "fastify",
42
+ "koa",
43
+ "nestjs",
44
+ "next.js",
45
+ "nuxt",
46
+ "sveltekit",
47
+ "remix",
48
+ "astro",
49
+ "react",
50
+ "vue",
51
+ "angular",
52
+ "svelte",
53
+ "solid.js",
54
+ "qwik",
55
+ "spring",
56
+ "quarkus",
57
+ "micronaut",
58
+ "laravel",
59
+ "symfony",
60
+ "rails",
61
+ "sinatra",
62
+ "gin",
63
+ "echo",
64
+ "fiber",
65
+ "chi",
66
+ "actix",
67
+ "axum",
68
+ "rocket",
69
+ "tauri",
70
+ "warp",
71
+ "hono",
72
+ "trpc",
73
+ "vapor",
74
+ "phoenix",
75
+ ]
76
+
77
+ _KNOWN_LANGUAGES = [
78
+ "python",
79
+ "javascript",
80
+ "typescript",
81
+ "go",
82
+ "golang",
83
+ "rust",
84
+ "java",
85
+ "php",
86
+ "ruby",
87
+ "swift",
88
+ "kotlin",
89
+ "scala",
90
+ "c#",
91
+ "csharp",
92
+ "c++",
93
+ "dart",
94
+ ]
95
+
96
+ # Role words → subsystem layer names they map to
97
+ _ROLE_MAP: dict[str, set[str]] = {
98
+ "frontend": {"ui"},
99
+ "ui": {"ui"},
100
+ "client": {"ui"},
101
+ "backend": {"api", "data", "core", "agents"},
102
+ "server": {"api", "core"},
103
+ "database": {"data"},
104
+ "db": {"data"},
105
+ "auth": {"auth"},
106
+ "api": {"api"},
107
+ "data": {"data"},
108
+ "core": {"core"},
109
+ }
110
+
111
+
112
+ # ── Dataclasses ────────────────────────────────────────────────────────────────
113
+
114
+
115
+ _log = logging.getLogger("patchi.brain.charter")
116
+
117
+
118
+ @dataclass
119
+ class Charter:
120
+ """Structured project guard rails parsed from natural language."""
121
+
122
+ raw_text: str = ""
123
+ stack: dict = field(default_factory=lambda: {"languages": [], "frameworks": []})
124
+ boundaries: list[dict] = field(default_factory=list) # [{"from": str, "to": str}]
125
+ conventions: dict = field(default_factory=dict) # max_file_lines, require_tests_for_routes, ...
126
+ security: list[str] = field(default_factory=list) # rule names
127
+ notes: list[str] = field(default_factory=list)
128
+
129
+ def to_dict(self) -> dict:
130
+ return {
131
+ "raw_text": self.raw_text,
132
+ "stack": self.stack,
133
+ "boundaries": self.boundaries,
134
+ "conventions": self.conventions,
135
+ "security": self.security,
136
+ "notes": self.notes,
137
+ }
138
+
139
+ @classmethod
140
+ def from_dict(cls, d: dict) -> Charter:
141
+ # Handle old format (boundaries/conventions/security)
142
+ if "rules" in d:
143
+ # Convert new security.charter format to brain.charter format
144
+ boundaries = []
145
+ conventions = {}
146
+ security = []
147
+ stack = {"languages": [], "frameworks": []}
148
+ for rule in d["rules"]:
149
+ if rule.get("type") == "boundary":
150
+ boundaries.append({
151
+ "from": rule.get("from_pattern", ""),
152
+ "to": rule.get("to_pattern", ""),
153
+ })
154
+ elif rule.get("type") == "convention":
155
+ if rule.get("max_value"):
156
+ conventions["max_file_lines"] = rule["max_value"]
157
+ elif rule.get("type") == "security":
158
+ security.extend(rule.get("keywords", []))
159
+ elif rule.get("type") == "stack":
160
+ stack["languages"].extend(rule.get("allowed_languages", []))
161
+ stack["frameworks"].extend(rule.get("allowed_frameworks", []))
162
+ return cls(
163
+ raw_text=d.get("text", ""),
164
+ stack=stack,
165
+ boundaries=boundaries,
166
+ conventions=conventions,
167
+ security=security,
168
+ )
169
+ # Old format
170
+ return cls(
171
+ raw_text=d.get("raw_text", ""),
172
+ stack=d.get("stack", {"languages": [], "frameworks": []}),
173
+ boundaries=d.get("boundaries", []),
174
+ conventions=d.get("conventions", {}),
175
+ security=d.get("security", []),
176
+ notes=d.get("notes", []),
177
+ )
178
+
179
+
180
+ @dataclass
181
+ class CharterViolation:
182
+ """A single guard-rail breach detected during a scan."""
183
+
184
+ rule: str # which charter rule was broken
185
+ severity: str # "high" | "medium" | "low"
186
+ message: str
187
+ file: str = "" # offending file (if known)
188
+ layer: str = "" # offending layer (if known)
189
+
190
+ def to_finding(self) -> dict:
191
+ return {
192
+ "agent": "CharterGuard",
193
+ "type": "charter-drift",
194
+ "severity": self.severity,
195
+ "file": self.file,
196
+ "message": self.message,
197
+ "rule": self.rule,
198
+ "layer": self.layer,
199
+ }
200
+
201
+
202
+ # ── Parsing ───────────────────────────────────────────────────────────────────
203
+
204
+
205
+ def parse_charter(text: str, config: dict | None = None) -> Charter:
206
+ """Parse a natural-language charter into a structured Charter.
207
+
208
+ Uses keyword matching (no regex, no heuristics) to extract structured rules.
209
+ """
210
+ text_lower = text.lower()
211
+ charter = Charter(raw_text=text)
212
+
213
+ # ── Stack: frameworks + languages ──
214
+ for fw in _KNOWN_FRAMEWORKS:
215
+ if " " + fw + " " in text_lower or text_lower.startswith(fw + " ") or text_lower.endswith(" " + fw):
216
+ if fw not in charter.stack["frameworks"]:
217
+ charter.stack["frameworks"].append(fw)
218
+ for lang in _KNOWN_LANGUAGES:
219
+ if " " + lang + " " in text_lower or text_lower.startswith(lang + " ") or text_lower.endswith(" " + lang):
220
+ norm = "golang" if lang == "go" else ("csharp" if lang in ("c#",) else lang)
221
+ if norm not in charter.stack["languages"]:
222
+ charter.stack["languages"].append(norm)
223
+
224
+ # ── Boundaries: "X must not import Y" ──
225
+ boundary_words = ["must not import", "cannot import", "never import",
226
+ "should not import", "must not use", "cannot use"]
227
+ for sep in boundary_words:
228
+ if sep in text_lower:
229
+ parts = text_lower.split(sep)
230
+ if len(parts) == 2:
231
+ from_pat = parts[0].strip().rstrip()
232
+ to_pat = parts[1].strip().lstrip()
233
+ # Remove leading articles
234
+ for prefix in ["the ", "a ", "an "]:
235
+ if from_pat.startswith(prefix):
236
+ from_pat = from_pat[len(prefix):]
237
+ if to_pat.startswith(prefix):
238
+ to_pat = to_pat[len(prefix):]
239
+ charter.boundaries.append({"from": from_pat, "to": to_pat})
240
+
241
+ # ── Conventions: sizes + test coverage ──
242
+ if "under" in text_lower or "below" in text_lower or "less than" in text_lower:
243
+ for word in text_lower.split():
244
+ if word.isdigit() and "lines" in text_lower:
245
+ charter.conventions["max_file_lines"] = int(word)
246
+ break
247
+
248
+ if any(phrase in text_lower for phrase in ["all routes need", "all routes must", "routes require tests"]):
249
+ charter.conventions["require_tests_for_routes"] = True
250
+ if any(phrase in text_lower for phrase in ["every function", "all modules", "all services"]):
251
+ if "test" in text_lower:
252
+ charter.conventions["require_tests_for_routes"] = True
253
+
254
+ # ── Security rules ──
255
+ if "hardcoded" in text_lower or "hard coded" in text_lower or "secrets" in text_lower:
256
+ charter.security.append("no_hardcoded_secrets")
257
+ if "pickle" in text_lower:
258
+ charter.security.append("no_pickle")
259
+ if "eval" in text_lower:
260
+ charter.security.append("no_eval")
261
+ if "parameterized" in text_lower:
262
+ charter.security.append("parameterized_queries")
263
+
264
+ return charter
265
+
266
+ def _split_role_phrase(phrase: str) -> tuple[str, str]:
267
+ """Split a 'X importing Y' phrase into (from, to) role tokens."""
268
+ m = re.search(r"([\w./]+)\s+importing\s+([\w./]+)", phrase)
269
+ if m:
270
+ return m.group(1), m.group(2)
271
+ return phrase, ""
272
+
273
+
274
+ def parse_charter_with_ai(text: str, config: dict) -> Charter | None:
275
+ """Optional LLM-backed parser. Returns None if AI unavailable or it fails."""
276
+ ai_config = config.get("ai", {})
277
+ has_ai = bool(ai_config.get("keys") or ai_config.get("local_model_name"))
278
+ if not has_ai:
279
+ return None
280
+ try:
281
+ from patchi.core.ai.client import call_ai
282
+ except Exception as e:
283
+ _log.warning("parse_charter_with_ai failed: %s", e)
284
+ return None
285
+
286
+ prompt = (
287
+ "Convert the following project charter into JSON with keys: "
288
+ "frameworks (list), languages (list), boundaries (list of {from,to}), "
289
+ "conventions (dict), security (list of rule strings). "
290
+ "Return ONLY JSON.\n\n" + text
291
+ )
292
+ try:
293
+ resp = call_ai(
294
+ config, "You are a config parser. Output only JSON.", prompt, max_tokens=1024
295
+ )
296
+ except Exception as e:
297
+ _log.warning("parse_charter_with_ai failed: %s", e)
298
+ return None
299
+ if not resp:
300
+ return None
301
+ # strip fences
302
+ resp = resp.strip()
303
+ if resp.startswith("```"):
304
+ resp = resp.split("```")[1]
305
+ if resp.startswith("json"):
306
+ resp = resp[4:]
307
+ try:
308
+ data = json.loads(resp)
309
+ except (json.JSONDecodeError, ValueError):
310
+ return None
311
+
312
+ charter = Charter(raw_text=text)
313
+ charter.stack = data.get("stack", {"languages": [], "frameworks": []})
314
+ charter.boundaries = data.get("boundaries", [])
315
+ charter.conventions = data.get("conventions", {})
316
+ charter.security = data.get("security", [])
317
+ return charter
318
+
319
+
320
+ # ── Resolution helpers ────────────────────────────────────────────────────────
321
+
322
+
323
+ def _resolve_role(token: str) -> set[str]:
324
+ """Map a role/framework/layer token to the subsystem layer names it covers."""
325
+ token_l = token.lower().strip()
326
+ if token_l in _ROLE_MAP:
327
+ return _ROLE_MAP[token_l]
328
+ # Direct subsystem name match
329
+ if token_l in {"auth", "api", "data", "ui", "core", "agents", "tests", "infra"}:
330
+ return {token_l}
331
+ # Framework → its typical subsystem
332
+ fw_to_sub = {
333
+ "react": "ui",
334
+ "vue": "ui",
335
+ "angular": "ui",
336
+ "svelte": "ui",
337
+ "next.js": "ui",
338
+ "nuxt": "ui",
339
+ "sveltekit": "ui",
340
+ "flask": "api",
341
+ "django": "api",
342
+ "fastapi": "api",
343
+ "express": "api",
344
+ "spring": "api",
345
+ "laravel": "api",
346
+ "rails": "api",
347
+ }
348
+ if token_l in fw_to_sub:
349
+ return {fw_to_sub[token_l]}
350
+ return set()
351
+
352
+
353
+ def _matches_boundary(edge_from: str, edge_to: str, boundary: dict) -> bool:
354
+ """Does a layer dependency edge violate a forbidden boundary?"""
355
+ from_set = _resolve_role(boundary.get("from", ""))
356
+ to_set = _resolve_role(boundary.get("to", ""))
357
+ if not from_set or not to_set:
358
+ return False
359
+ # Direct name match OR role-resolved match
360
+ direct = (
361
+ boundary.get("from", "").lower().strip() in edge_from.lower()
362
+ and boundary.get("to", "").lower().strip() in edge_to.lower()
363
+ )
364
+ role = (bool(from_set & {edge_from} or any(f in edge_from.lower() for f in from_set))) and (
365
+ bool(to_set & {edge_to} or any(t in edge_to.lower() for t in to_set))
366
+ )
367
+ return direct or role
368
+
369
+
370
+ # ── Checking ──────────────────────────────────────────────────────────────────
371
+
372
+
373
+ def check_charter(
374
+ charter: Charter,
375
+ layers: dict,
376
+ detected_frameworks: list[str] | None = None,
377
+ routes: list | None = None,
378
+ file_infos: list | None = None,
379
+ ) -> list[CharterViolation]:
380
+ """Evaluate a charter against the current codebase state.
381
+
382
+ Returns a list of :class:`CharterViolation` (empty if fully compliant).
383
+ """
384
+ violations: list[CharterViolation] = []
385
+ if not charter or not layers:
386
+ return violations
387
+
388
+ # ── Boundary checks (subsystem-level dependency edges) ─────────────────────
389
+ subsystem_layers = {n: lay for n, lay in layers.items() if lay.get("level") == 2}
390
+ for name, layer in subsystem_layers.items():
391
+ for dep in layer.get("depends_on", []):
392
+ for b in charter.boundaries:
393
+ if _matches_boundary(name, dep, b):
394
+ violations.append(
395
+ CharterViolation(
396
+ rule=f"boundary:{b.get('from')}→{b.get('to')}",
397
+ severity="high",
398
+ message=(
399
+ f"Architecture drift: subsystem '{name}' imports "
400
+ f"'{dep}', which violates the charter rule "
401
+ f"'{b.get('from')} must not import {b.get('to')}'."
402
+ ),
403
+ layer=name,
404
+ )
405
+ )
406
+
407
+ # ── Stack drift ───────────────────────────────────────────────────────────
408
+ if detected_frameworks and charter.stack.get("frameworks"):
409
+ expected = {f.lower() for f in charter.stack["frameworks"]}
410
+ actual = {f.lower() for f in detected_frameworks}
411
+ unexpected = actual - expected
412
+ # Only flag clearly-new frameworks, not the same framework under another name
413
+ if unexpected:
414
+ violations.append(
415
+ CharterViolation(
416
+ rule="stack-drift",
417
+ severity="medium",
418
+ message=(
419
+ f"Stack drift: project uses frameworks not declared in the "
420
+ f"charter: {', '.join(sorted(unexpected))}. Charter expected: "
421
+ f"{', '.join(sorted(expected))}."
422
+ ),
423
+ )
424
+ )
425
+
426
+ # ── Convention: require tests for routes ───────────────────────────────────
427
+ if charter.conventions.get("require_tests_for_routes") and routes:
428
+ route_files = {
429
+ r.get("file", "") if isinstance(r, dict) else getattr(r, "file", "") for r in routes
430
+ }
431
+ test_files = {
432
+ fi.path
433
+ for fi in (file_infos or [])
434
+ if fi.path
435
+ and (
436
+ "test" in fi.path.lower()
437
+ or fi.path.endswith("_test.py")
438
+ or fi.path.endswith(".test.js")
439
+ or fi.path.endswith(".test.ts")
440
+ )
441
+ }
442
+ untested = [rf for rf in route_files if rf and rf not in test_files]
443
+ if untested:
444
+ violations.append(
445
+ CharterViolation(
446
+ rule="require_tests_for_routes",
447
+ severity="low",
448
+ message=(
449
+ f"Convention drift: {len(untested)} route file(s) have no "
450
+ f"test coverage (charter requires tests for all API routes)."
451
+ ),
452
+ )
453
+ )
454
+
455
+ return violations
456
+
457
+
458
+ # ── Persistence helpers ───────────────────────────────────────────────────────
459
+
460
+
461
+ def save_charter(charter: Charter, root: Path) -> None:
462
+ from patchi.core import memory as mem
463
+
464
+ mem.save_charter(charter.to_dict(), root)
465
+
466
+
467
+ def load_charter(root: Path) -> Charter | None:
468
+ from patchi.core import memory as mem
469
+
470
+ data = mem.get_charter(root)
471
+ if not data:
472
+ return None
473
+ return Charter.from_dict(data)
@@ -0,0 +1,214 @@
1
+ """
2
+ File classifier — assigns a purpose label to every file in the project.
3
+
4
+ Philosophy (per supplementary spec §2 offline/AI split):
5
+ 80% of files can be classified from their AST and path alone — no AI cost.
6
+ Only genuinely ambiguous files get an AI call.
7
+
8
+ Offline classification uses:
9
+ - File path / name patterns (test_, __init__, cli/, routes/, etc.)
10
+ - Export type (what functions/classes the file defines)
11
+ - Import patterns (what the file imports)
12
+ - File size and structure
13
+
14
+ AI classification (fallback only):
15
+ - Called only when offline pass returns label="unknown"
16
+ - Returns a plain-English one-liner describing the file's purpose
17
+ - Result cached in brain.json so the call only happens once per file
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import ast
22
+ import logging
23
+ import re
24
+ from pathlib import Path
25
+
26
+ # ── Offline label lookup ───────────────────────────────────────────────────────
27
+
28
+ _PATH_RULES: list[tuple[str, str]] = [
29
+ # Path segment → label
30
+ (r"test[_/]|[_/]test\.py$|tests?/", "test"),
31
+ (r"__init__\.py$", "package init"),
32
+ (r"cli[/\\]|commands?[/\\]", "CLI command"),
33
+ (r"routes?[/\\]|router|handlers?[/\\]", "route handler"),
34
+ (r"middleware[/\\]|middleware\.py", "middleware"),
35
+ (r"models?[/\\]|schemas?[/\\]", "data model / schema"),
36
+ (r"migrations?[/\\]|alembic[/\\]", "database migration"),
37
+ (r"config[/\\]|settings[/\\]|conf\.py", "configuration"),
38
+ (r"constants?\.py$|enums?\.py$", "constants / enums"),
39
+ (r"utils?[/\\]|helpers?[/\\]", "utility functions"),
40
+ (r"static[/\\]|assets?[/\\]", "static asset"),
41
+ (r"templates?[/\\]|views?[/\\]", "template / view"),
42
+ (r"security[/\\]|auth[/\\]", "auth / security"),
43
+ (r"core[/\\]agents?[/\\]|core[/\\]fix[/\\]|core[/\\]security[/\\]", "agent / scanner"),
44
+ (r"core[/\\]|engine[/\\]", "core engine"),
45
+ (r"web[/\\]|server\.py$|api\.py$", "web server / API"),
46
+ (r"agents?[/\\]|scanners?[/\\]", "agent / scanner"),
47
+ (r"brain[/\\]", "brain module"),
48
+ (r"fix[/\\]|fixer[/\\]", "fix agent"),
49
+ (r"notify[/\\]|notification", "notification"),
50
+ (r"queue\.py$|queue[/\\]", "queue system"),
51
+ (r"memory\.py$|memory[/\\]", "memory store"),
52
+ (r"health\.py$", "health checker"),
53
+ (r"setup\.py$|pyproject\.toml$", "package setup"),
54
+ (r"Makefile$|Dockerfile", "build / deploy"),
55
+ (r"\.env$|\.env\.", "environment config"),
56
+ (r"README|CHANGELOG|LICENSE|AGENTS", "documentation"),
57
+ (r"requirements.*\.txt$", "dependency list"),
58
+ ]
59
+
60
+ _IMPORT_SIGNALS: dict[str, str] = {
61
+ "fastapi": "FastAPI route handler",
62
+ "flask": "Flask route handler",
63
+ "django": "Django view/model",
64
+ "sqlalchemy": "database model/query",
65
+ "pytest": "test file",
66
+ "unittest": "test file",
67
+ "click": "CLI command",
68
+ "typer": "CLI command",
69
+ "pydantic": "data schema / model",
70
+ "celery": "background task",
71
+ "redis": "cache / queue integration",
72
+ "httpx": "HTTP client",
73
+ "aiohttp": "async HTTP",
74
+ "playwright": "browser automation",
75
+ "selenium": "browser automation",
76
+ "torch": "ML / AI model",
77
+ "tensorflow": "ML / AI model",
78
+ "sklearn": "ML / AI model",
79
+ "logging": "utility / logging",
80
+ "argparse": "CLI entry point",
81
+ "rich": "CLI output formatting",
82
+ }
83
+
84
+
85
+ _log = logging.getLogger("patchi.brain.classifier")
86
+
87
+
88
+ def classify_file(rel_path: str, abs_path: Path) -> str:
89
+ """
90
+ Return a short plain-English purpose label for a file.
91
+ Uses path patterns and AST — no AI call.
92
+ """
93
+ path_str = rel_path.replace("\\", "/").lower()
94
+
95
+ # 1. Path-based rules — fastest, covers the majority
96
+ for pattern, label in _PATH_RULES:
97
+ if re.search(pattern, path_str):
98
+ return label
99
+
100
+ # 2. Extension quick exits
101
+ ext = abs_path.suffix.lower()
102
+ if ext in {".json", ".yaml", ".yml", ".toml", ".ini", ".cfg"}:
103
+ return "configuration / data file"
104
+ if ext in {".md", ".rst", ".txt"}:
105
+ return "documentation"
106
+ if ext in {".html", ".jinja", ".j2"}:
107
+ return "HTML template"
108
+ if ext in {".css", ".scss", ".less"}:
109
+ return "stylesheet"
110
+ if ext in {".js", ".mjs"} and "test" not in path_str:
111
+ return "JavaScript module"
112
+ if ext in {".ts", ".tsx"} and "test" not in path_str:
113
+ return "TypeScript module"
114
+
115
+ if ext != ".py":
116
+ return "source file"
117
+
118
+ # 3. AST — read imports and top-level definitions
119
+ try:
120
+ src = abs_path.read_text(encoding="utf-8", errors="ignore")
121
+ tree = ast.parse(src)
122
+ except (SyntaxError, OSError):
123
+ return "Python module"
124
+
125
+ imports: list[str] = []
126
+ top_fns: list[str] = []
127
+ top_cls: list[str] = []
128
+
129
+ for node in ast.walk(tree):
130
+ if isinstance(node, ast.Import):
131
+ for a in node.names:
132
+ imports.append(a.name.split(".")[0].lower())
133
+ elif isinstance(node, ast.ImportFrom) and node.module:
134
+ imports.append(node.module.split(".")[0].lower())
135
+ elif isinstance(node, ast.FunctionDef):
136
+ if node.col_offset == 0:
137
+ top_fns.append(node.name)
138
+ elif isinstance(node, ast.ClassDef):
139
+ if node.col_offset == 0:
140
+ top_cls.append(node.name)
141
+
142
+ # 4. Import signal match
143
+ for imp in imports:
144
+ if imp in _IMPORT_SIGNALS:
145
+ return _IMPORT_SIGNALS[imp]
146
+
147
+ # 5. Top-level structure heuristics
148
+ fn_names = " ".join(top_fns).lower()
149
+ cl_names = " ".join(top_cls).lower()
150
+ all_names = fn_names + " " + cl_names
151
+
152
+ if re.search(r"\btest_\w+|\b\w+_test\b", fn_names):
153
+ return "test file"
154
+ if re.search(r"\bget_|post_|put_|delete_|patch_|handle_", fn_names):
155
+ return "request handler"
156
+ if re.search(r"\bscanner\b|\bscanner\b|\bdetect\b|\bcheck\b|\baudit\b", cl_names.lower()):
157
+ return "scanner / detector"
158
+ if re.search(r"\bagent\b|\bworker\b|\bcoordinator\b", all_names):
159
+ return "agent / worker"
160
+ if re.search(r"\bmodel\b|\bschema\b|\bentity\b", all_names):
161
+ return "data model"
162
+ if re.search(r"\bconfig\b|\bsetting\b|\boption\b", all_names):
163
+ return "configuration"
164
+ if re.search(r"\bcommand\b|\bcli\b|\bmain\b", fn_names) and "__main__" in src:
165
+ return "CLI entry point"
166
+ if re.search(r"\bcreate_app\b|\bapp = \b", src):
167
+ return "application factory"
168
+
169
+ return "Python module"
170
+
171
+
172
+ def batch_classify(
173
+ files: list, # list[FileInfo]
174
+ root: Path,
175
+ ai_config: dict | None = None,
176
+ max_ai_calls: int = 20,
177
+ ) -> dict[str, str]:
178
+ """
179
+ Classify all files. Returns {rel_path: purpose_label}.
180
+ AI is called only for truly unknown files, capped at max_ai_calls.
181
+ """
182
+ result: dict[str, str] = {}
183
+ needs_ai: list[str] = []
184
+
185
+ for fi in files:
186
+ abs_path = root / fi.path
187
+ label = classify_file(fi.path, abs_path)
188
+ if label == "Python module" and ai_config:
189
+ needs_ai.append(fi.path)
190
+ else:
191
+ result[fi.path] = label
192
+
193
+ # AI pass — only for genuinely ambiguous Python files
194
+ if ai_config and needs_ai:
195
+ from patchi.core.fix.base import _call_ai
196
+
197
+ for rel_path in needs_ai[:max_ai_calls]:
198
+ abs_path = root / rel_path
199
+ try:
200
+ snippet = (root / rel_path).read_text(encoding="utf-8", errors="ignore")[:600]
201
+ prompt = (
202
+ f"What does this Python file do? Answer in one sentence, plain English, "
203
+ f"no code. File: {rel_path}\n\n{snippet}"
204
+ )
205
+ label = _call_ai(prompt, ai_config, max_tokens=60).strip()
206
+ result[rel_path] = label or "Python module"
207
+ except Exception as e:
208
+ _log.warning("batch_classify failed: %s", e)
209
+ result[rel_path] = "Python module"
210
+ # remaining uncalled files
211
+ for rel_path in needs_ai[max_ai_calls:]:
212
+ result[rel_path] = "Python module"
213
+
214
+ return result