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,889 @@
1
+ """
2
+ App Contract builder for Patchi's Brain.
3
+
4
+ The App Contract is the list of critical flows that must never break.
5
+ Every fix checks against this contract before applying.
6
+
7
+ The contract is built in two layers:
8
+ 1. Offline (free, no AI): Pattern-based inference from routes, file names, and AST structure.
9
+ 2. AI-powered: When AI is configured, the LLM synthesises a summary from all AST scanner
10
+ results — file purposes, routes, import graph, dead code, dependencies — producing a
11
+ richer, context-aware contract.
12
+
13
+ The confirmed contract is stored in Brain memory and never silently overwritten.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import re
20
+ from dataclasses import dataclass
21
+ from pathlib import Path
22
+ from typing import TYPE_CHECKING, Any
23
+
24
+ if TYPE_CHECKING:
25
+ from patchi.core.brain.route_mapper import RouteInfo
26
+ from patchi.core.brain.scanner import FileInfo
27
+
28
+
29
+ import logging
30
+
31
+ _log = logging.getLogger("patchi.brain.contract")
32
+
33
+
34
+ @dataclass
35
+ class ContractFlow:
36
+ id: str
37
+ name: str
38
+ description: str
39
+ routes: list[str]
40
+ files: list[str]
41
+ signals: list[str]
42
+ confirmed: bool = False
43
+ user_added: bool = False
44
+ critical: bool = True
45
+ confidence: str = "medium" # "high" | "medium" | "low"
46
+ suggested: bool = False # if True, hidden from default confirmation
47
+
48
+ def to_dict(self) -> dict:
49
+ return {
50
+ "id": self.id,
51
+ "name": self.name,
52
+ "description": self.description,
53
+ "routes": self.routes,
54
+ "files": self.files,
55
+ "signals": self.signals,
56
+ "confirmed": self.confirmed,
57
+ "user_added": self.user_added,
58
+ "critical": self.critical,
59
+ "confidence": self.confidence,
60
+ "suggested": self.suggested,
61
+ }
62
+
63
+
64
+ _ROUTE_TO_FLOW: dict[str, dict] = {
65
+ "dashboard": {
66
+ "name": "Dashboard Overview",
67
+ "desc": "Project dashboard with status and health overview.",
68
+ "signal": "dashboard",
69
+ },
70
+ "findings": {
71
+ "name": "Findings Review",
72
+ "desc": "Browse, filter, and review scan findings.",
73
+ "signal": "findings",
74
+ },
75
+ "settings": {
76
+ "name": "Settings / Configuration",
77
+ "desc": "View and update project configuration.",
78
+ "signal": "config",
79
+ },
80
+ "security": {
81
+ "name": "Security Scanning",
82
+ "desc": "Run security scans and view reports.",
83
+ "signal": "security",
84
+ },
85
+ "history": {
86
+ "name": "History & Trends",
87
+ "desc": "View scan history and health trends.",
88
+ "signal": "history",
89
+ },
90
+ "tests": {
91
+ "name": "Test Management",
92
+ "desc": "Run, view, and manage test suites.",
93
+ "signal": "testing",
94
+ },
95
+ "review": {
96
+ "name": "Review & Fixes",
97
+ "desc": "Review proposed fixes and apply or reject patches.",
98
+ "signal": "fixes",
99
+ },
100
+ "queue": {
101
+ "name": "Queue Management",
102
+ "desc": "View and manage the scan queue.",
103
+ "signal": "queue",
104
+ },
105
+ "hosted": {
106
+ "name": "Hosted Mode",
107
+ "desc": "Manage hosted mode, tokens, and IP reputation.",
108
+ "signal": "hosted",
109
+ },
110
+ "brain": {
111
+ "name": "Brain & Memory",
112
+ "desc": "View brain map, memory, import graph, and blast radius.",
113
+ "signal": "insights",
114
+ },
115
+ "agents": {
116
+ "name": "Agent Management",
117
+ "desc": "View and manage scanner agents.",
118
+ "signal": "agents",
119
+ },
120
+ "notifications": {
121
+ "name": "Notifications",
122
+ "desc": "Configure and receive notifications and alerts.",
123
+ "signal": "notifications",
124
+ },
125
+ "keys": {
126
+ "name": "Key Management",
127
+ "desc": "Manage API keys for AI providers.",
128
+ "signal": "keys",
129
+ },
130
+ }
131
+
132
+ INFERENCE_RULES: list[tuple[str, str, str, list[str], list[str], list[str]]] = [
133
+ (
134
+ "dashboard-overview",
135
+ "Dashboard Overview",
136
+ "Users can view the main dashboard with project status and health.",
137
+ [r"^/$", r"dashboard", r"status$"],
138
+ [r"dashboard", r"status"],
139
+ ["dashboard"],
140
+ ),
141
+ (
142
+ "findings-review",
143
+ "Findings Review",
144
+ "Users can browse and review scan findings.",
145
+ [r"findings", r"scan/results", r"findings-table"],
146
+ [r"finding", r"result"],
147
+ ["findings"],
148
+ ),
149
+ (
150
+ "settings-config",
151
+ "Settings / Configuration",
152
+ "Users can view and update project configuration.",
153
+ [r"settings", r"config$", r"preferences"],
154
+ [r"settings", r"config"],
155
+ ["config"],
156
+ ),
157
+ (
158
+ "security-scanning",
159
+ "Security Scanning",
160
+ "Run security scans and view reports on the project.",
161
+ [r"security", r"scan", r"guard", r"threats"],
162
+ [r"security", r"scan", r"guard"],
163
+ ["security"],
164
+ ),
165
+ (
166
+ "history-trends",
167
+ "History & Trends",
168
+ "Users can view scan history and health trends over time.",
169
+ [r"history", r"trend", r"health-breakdown"],
170
+ [r"history", r"trend"],
171
+ ["history", "analytics"],
172
+ ),
173
+ (
174
+ "test-management",
175
+ "Test Management",
176
+ "Users can run, view, and manage test suites and agents.",
177
+ [r"tests?", r"test-agents", r"test-agents/status"],
178
+ [r"test", r"unit", r"suite"],
179
+ ["testing"],
180
+ ),
181
+ (
182
+ "review-patches",
183
+ "Review & Fixes",
184
+ "Users can review proposed fixes and apply or reject patches.",
185
+ [r"review", r"fix", r"patch", r"issue"],
186
+ [r"review", r"fix", r"patch"],
187
+ ["fixes"],
188
+ ),
189
+ (
190
+ "queue-management",
191
+ "Queue Management",
192
+ "Users can view and manage the scan queue (pause/resume/clear).",
193
+ [r"queue", r"queue/pause", r"queue/resume", r"queue/clear"],
194
+ [r"queue"],
195
+ ["queue"],
196
+ ),
197
+ (
198
+ "hosted-mode",
199
+ "Hosted Mode Management",
200
+ "Users can manage hosted mode, tokens, and IP reputation.",
201
+ [r"hosted", r"tokens", r"block", r"unblock"],
202
+ [r"hosted", r"token", r"reputation"],
203
+ ["hosted"],
204
+ ),
205
+ (
206
+ "brain-insights",
207
+ "Brain & Memory",
208
+ "Users can view the brain map, memory, import graph, and blast radius.",
209
+ [r"brain", r"memory", r"blast", r"explain"],
210
+ [r"brain", r"memory", r"blast", r"import"],
211
+ ["insights"],
212
+ ),
213
+ (
214
+ "agent-management",
215
+ "Agent Management",
216
+ "Users can view and manage scanner agents and their status.",
217
+ [r"agents", r"model", r"ai"],
218
+ [r"agent", r"scanner"],
219
+ ["agents"],
220
+ ),
221
+ (
222
+ "notifications",
223
+ "Notifications",
224
+ "Users can configure and receive notifications and alerts.",
225
+ [r"notifications?", r"notify"],
226
+ [r"notify", r"notification", r"alert"],
227
+ ["notifications"],
228
+ ),
229
+ (
230
+ "key-management",
231
+ "Key Management",
232
+ "Users can manage API keys for AI providers.",
233
+ [r"keys", r"keys/add", r"keys/remove"],
234
+ [r"key", r"api.key", r"credential"],
235
+ ["keys"],
236
+ ),
237
+ ]
238
+
239
+
240
+ _KNOWN_PREFIXES = set(_ROUTE_TO_FLOW.keys())
241
+
242
+
243
+ # ── AI contract summariser ─────────────────────────────────────────────────────
244
+
245
+
246
+ def build_ai_contract_summary(
247
+ file_infos: list[FileInfo],
248
+ routes: list[RouteInfo],
249
+ dead_files: list[str],
250
+ circular_deps: list[Any],
251
+ config: dict | None = None,
252
+ ) -> str | None:
253
+ """
254
+ When AI is configured, send the full AST scanner results to the LLM
255
+ and get back a plain-English summary of the app's critical flows.
256
+
257
+ Returns the AI summary string, or None if no AI is available.
258
+ """
259
+ if not config:
260
+ return None
261
+
262
+ ai_config = config.get("ai", {})
263
+ has_keys = bool(ai_config.get("keys")) or bool(ai_config.get("local_model_name"))
264
+ if not has_keys:
265
+ return None
266
+
267
+ # Build a compact summary of what the AST scanners found
268
+ # Prefer understander-ranked core files when available (Slice 2)
269
+ use_core = False
270
+ try:
271
+
272
+ from patchi.core.brain.body_tags import load_body_tags
273
+ from patchi.core.brain.understander import Understander
274
+
275
+ _root = None
276
+ # try to infer root from first file_infos path (relative), fallback to cwd
277
+ if file_infos and len(file_infos) > 0:
278
+ # file_infos paths are relative to root; we don't have root here, use cwd probe
279
+ for cand in [Path.cwd(), Path(".")]:
280
+ if (cand / ".patchi").exists() or (cand / "pyproject.toml").exists():
281
+ _root = cand.resolve()
282
+ break
283
+ if _root is not None:
284
+ _tags = load_body_tags(_root)
285
+ if _tags:
286
+ _u = Understander(_root, file_infos, _tags, None, routes)
287
+ core = _u.core_files(limit=20)
288
+ file_summaries = []
289
+ for c in core:
290
+ fi = next((x for x in file_infos if x.path == c["path"]), None)
291
+ if fi is None:
292
+ continue
293
+ funcs = ", ".join(f.name for f in fi.functions[:5])
294
+ classes = ", ".join(c.name for c in fi.classes[:3])
295
+ parts = [fi.path + f" // {c['why']}"]
296
+ if fi.purpose:
297
+ parts.append(f"({fi.purpose})")
298
+ if funcs:
299
+ parts.append(f"fns: [{funcs}]")
300
+ if classes:
301
+ parts.append(f"cls: [{classes}]")
302
+ file_summaries.append(" ".join(parts))
303
+ use_core = bool(file_summaries)
304
+ except Exception:
305
+ use_core = False
306
+ if not use_core:
307
+ file_summaries = []
308
+ for fi in file_infos[:50]:
309
+ funcs = ", ".join(f.name for f in fi.functions[:5])
310
+ classes = ", ".join(c.name for c in fi.classes[:3])
311
+ parts = [fi.path]
312
+ if fi.purpose:
313
+ parts.append(f"({fi.purpose})")
314
+ if funcs:
315
+ parts.append(f"fns: [{funcs}]")
316
+ if classes:
317
+ parts.append(f"cls: [{classes}]")
318
+ file_summaries.append(" ".join(parts))
319
+
320
+ route_summaries = [f"{r.method} {r.path}" for r in routes[:30]]
321
+
322
+ prompt = (
323
+ "You are analysing a codebase. Below is a structured summary of:\n"
324
+ "- Every source file with its inferred purpose, functions, and classes\n"
325
+ "- Every HTTP route with its method and path\n"
326
+ f"- Dead/unreachable files: {len(dead_files)}\n"
327
+ f"- Circular dependencies: {len(circular_deps)}\n\n"
328
+ "Based on this data, identify the app's critical flows "
329
+ "(login, checkout, admin, API, data submission, etc.) that must never break.\n\n"
330
+ f"=== FILES ({len(file_infos)} total, showing up to 50) ===\n"
331
+ + "\n".join(file_summaries)
332
+ + "\n\n=== ROUTES ===\n"
333
+ + "\n".join(route_summaries)
334
+ + "\n\nRespond with a concise JSON list of critical flows. "
335
+ "Each flow must have: name, description (one sentence), "
336
+ "route_paths (list of matching routes), "
337
+ "and file_paths (list of relevant source files). "
338
+ 'Format: [{"name": "User Login", "description": "...", '
339
+ '"route_paths": [...], "file_paths": [...]}]'
340
+ )
341
+
342
+ return _call_ai_summary(prompt, config)
343
+
344
+
345
+ def _call_ai_summary(prompt: str, config: dict) -> str | None:
346
+ """Send prompt to the configured AI and return the response."""
347
+ ai_config = config.get("ai", {})
348
+
349
+ local_model = ai_config.get("local_model_name")
350
+ if local_model:
351
+ return _call_ollama(local_model, prompt)
352
+
353
+ keys = ai_config.get("keys", [])
354
+ for key_cfg in keys:
355
+ if key_cfg.get("status") == "error":
356
+ continue
357
+ import os
358
+
359
+ env_var = key_cfg.get("env_var", "")
360
+ api_key = os.environ.get(env_var, "")
361
+ if not api_key:
362
+ continue
363
+ result = _call_openai_compat(
364
+ api_key=api_key,
365
+ base_url=key_cfg.get("base_url", "https://api.openai.com/v1"),
366
+ model=key_cfg.get("model", "gpt-4o-mini"),
367
+ prompt=prompt,
368
+ )
369
+ if result:
370
+ return result
371
+ return None
372
+
373
+
374
+ def _call_ollama(model: str, prompt: str) -> str | None:
375
+ import urllib.request
376
+
377
+ from patchi.core.constants import OLLAMA_GENERATE_URL
378
+
379
+ try:
380
+ payload = json.dumps(
381
+ {
382
+ "model": model,
383
+ "prompt": prompt,
384
+ "stream": False,
385
+ }
386
+ ).encode()
387
+ req = urllib.request.Request(
388
+ OLLAMA_GENERATE_URL,
389
+ data=payload,
390
+ headers={"Content-Type": "application/json"},
391
+ method="POST",
392
+ )
393
+ with urllib.request.urlopen(req, timeout=30) as resp:
394
+ data = json.loads(resp.read())
395
+ return data.get("response", "")
396
+ except Exception as e:
397
+ _log.warning("_call_ollama failed: %s", e)
398
+ return None
399
+
400
+
401
+ def _call_openai_compat(api_key: str, base_url: str, model: str, prompt: str) -> str | None:
402
+ import urllib.request
403
+
404
+ try:
405
+ payload = json.dumps(
406
+ {
407
+ "model": model,
408
+ "max_tokens": 2000,
409
+ "messages": [{"role": "user", "content": prompt}],
410
+ }
411
+ ).encode()
412
+ headers = {
413
+ "Authorization": f"Bearer {api_key}",
414
+ "Content-Type": "application/json",
415
+ }
416
+ req = urllib.request.Request(
417
+ f"{base_url}/chat/completions",
418
+ data=payload,
419
+ headers=headers,
420
+ method="POST",
421
+ )
422
+ with urllib.request.urlopen(req, timeout=30) as resp:
423
+ data = json.loads(resp.read())
424
+ return data.get("choices", [{}])[0].get("message", {}).get("content", "")
425
+ except Exception as e:
426
+ _log.warning("_call_openai_compat failed: %s", e)
427
+ return None
428
+
429
+
430
+ def parse_ai_contract_response(response: str) -> list[dict] | None:
431
+ """Parse AI response JSON into flow dicts that confirm_flows can use."""
432
+ try:
433
+ data = json.loads(response)
434
+ if isinstance(data, list):
435
+ return data
436
+ except json.JSONDecodeError:
437
+ m = re.search(r"\[.*?\]", response, re.DOTALL)
438
+ if m:
439
+ try:
440
+ return json.loads(m.group())
441
+ except json.JSONDecodeError:
442
+ pass
443
+ return None
444
+
445
+
446
+ # ── Contract builder ───────────────────────────────────────────────────────────
447
+
448
+
449
+ class ContractBuilder:
450
+ """
451
+ Infers the App Contract from routes, AST file info, project understanding, and optionally AI.
452
+
453
+ Three inference paths:
454
+ 1. project_infer() — reads README, package.json, etc. for deep understanding
455
+ 2. offline inference() — pattern-based, free, no API calls
456
+ 3. ai_infer() — sends AST scanner summary to LLM for richer contract
457
+
458
+ Usage:
459
+ builder = ContractBuilder(routes, file_infos, root=root)
460
+ flows = builder.project_infer() # smart, reads project files
461
+ flows = builder.infer() # offline, fast
462
+ ai_flows = builder.ai_infer(config) # AI-powered summary
463
+ """
464
+
465
+ def __init__(
466
+ self,
467
+ routes: list[RouteInfo],
468
+ file_infos: list[FileInfo],
469
+ dead_files: list[str] | None = None,
470
+ circular_deps: list[Any] | None = None,
471
+ root: Path | None = None,
472
+ ):
473
+ self.routes = routes
474
+ self.file_infos = file_infos
475
+ self.dead_files = dead_files or []
476
+ self.circular_deps = circular_deps or []
477
+ self.root = root
478
+
479
+ def project_infer(self) -> list[ContractFlow]:
480
+ """
481
+ Smart inference: reads project files to understand what the project is,
482
+ then builds contract flows based on that understanding.
483
+
484
+ This is much more accurate than pattern matching because it actually
485
+ reads the README, package.json, pyproject.toml, etc.
486
+ """
487
+ if not self.root:
488
+ return self.infer()
489
+
490
+ from patchi.core.brain.project_reader import read_project_insight
491
+
492
+ insight = read_project_insight(self.root)
493
+
494
+ # Build flows based on project understanding
495
+ flows: list[ContractFlow] = []
496
+ seen_ids: set[str] = set()
497
+
498
+ # 1. Create flows from critical directories
499
+ for dir_name in insight.critical_dirs:
500
+ dir_files = [fi.path for fi in self.file_infos if dir_name.lower() in fi.path.lower()]
501
+ if dir_files:
502
+ flow_id = f"project-{dir_name}"
503
+ if flow_id not in seen_ids:
504
+ seen_ids.add(flow_id)
505
+ flows.append(ContractFlow(
506
+ id=flow_id,
507
+ name=f"{dir_name.title()} Module",
508
+ description=f"The {dir_name} directory containing critical project code.",
509
+ routes=[],
510
+ files=dir_files[:10],
511
+ signals=["project-reader", f"dir:{dir_name}"],
512
+ confirmed=False,
513
+ confidence="high",
514
+ ))
515
+
516
+ # 2. Create flows from entry points
517
+ for ep in insight.entry_points:
518
+ ep_files = [fi.path for fi in self.file_infos if ep in fi.path]
519
+ if ep_files:
520
+ flow_id = f"entry-{ep.replace('/', '-').replace('.', '-')}"
521
+ if flow_id not in seen_ids:
522
+ seen_ids.add(flow_id)
523
+ flows.append(ContractFlow(
524
+ id=flow_id,
525
+ name=f"Entry Point: {ep}",
526
+ description=f"Main entry point of the {insight.project_type or 'project'}.",
527
+ routes=[],
528
+ files=ep_files[:5],
529
+ signals=["project-reader", "entry-point"],
530
+ confirmed=False,
531
+ confidence="high",
532
+ ))
533
+
534
+ # 3. Create flows from routes (using project context)
535
+ from collections import defaultdict
536
+ clusters: dict[str, list[str]] = defaultdict(list)
537
+ for r in self.routes:
538
+ segments = [s for s in r.path.split("/") if s]
539
+ prefix = segments[0] if segments else "root"
540
+ clusters[prefix].append(r.path)
541
+
542
+ for prefix, matched_routes in clusters.items():
543
+ flow_id = f"route-{prefix}"
544
+ if flow_id in seen_ids:
545
+ continue
546
+ seen_ids.add(flow_id)
547
+
548
+ # Use project context to name the flow
549
+ meta = _ROUTE_TO_FLOW.get(prefix)
550
+ if meta:
551
+ name = meta["name"]
552
+ desc = meta["desc"]
553
+ else:
554
+ name = f"{prefix.title()} Endpoints"
555
+ desc = f"Routes under /{prefix}/"
556
+
557
+ file_hits = [fi.path for fi in self.file_infos if prefix in fi.path.lower()]
558
+ confidence = "high" if file_hits else "medium"
559
+
560
+ flows.append(ContractFlow(
561
+ id=flow_id,
562
+ name=name,
563
+ description=desc,
564
+ routes=matched_routes[:5],
565
+ files=file_hits[:10],
566
+ signals=["project-reader", "route"],
567
+ confirmed=False,
568
+ confidence=confidence,
569
+ ))
570
+
571
+ # 4. Merge with offline inference for anything we missed
572
+ if not flows:
573
+ flows = self.infer()
574
+ else:
575
+ offline = self.infer()
576
+ for f in offline:
577
+ if f.id not in seen_ids and not f.suggested:
578
+ flows.append(f)
579
+
580
+ return flows
581
+
582
+ def infer(self) -> list[ContractFlow]:
583
+ """
584
+ Infer contract flows by clustering route paths by their URL prefix.
585
+
586
+ Each route is assigned to a cluster based on its first path segment.
587
+ Clusters with at least one route produce a ContractFlow at medium confidence.
588
+ Clusters with routes AND matching file purposes produce high confidence.
589
+ Routes that don't match any known prefix produce a single "Other API" flow.
590
+
591
+ If no routes are detected, falls back to file-structure-based inference.
592
+ """
593
+ from collections import defaultdict
594
+
595
+ clusters: dict[str, list[str]] = defaultdict(list)
596
+ unmatched: list[str] = []
597
+
598
+ for r in self.routes:
599
+ path = r.path
600
+ segments = [s for s in path.split("/") if s]
601
+ prefix = segments[0] if segments else "root"
602
+ if prefix in _KNOWN_PREFIXES:
603
+ clusters[prefix].append(path)
604
+ else:
605
+ unmatched.append(path)
606
+
607
+ found: list[ContractFlow] = []
608
+
609
+ for prefix, matched_routes in sorted(clusters.items()):
610
+ meta = _ROUTE_TO_FLOW[prefix]
611
+ flow_id = prefix
612
+ name = meta["name"]
613
+ desc = meta["desc"]
614
+ signal = meta["signal"]
615
+
616
+ file_hits = [fi.path for fi in self.file_infos if prefix in fi.path.lower()]
617
+ confidence = "high" if file_hits else "medium"
618
+
619
+ found.append(
620
+ ContractFlow(
621
+ id=flow_id,
622
+ name=name,
623
+ description=desc,
624
+ routes=matched_routes[:5],
625
+ files=file_hits[:10],
626
+ signals=[signal],
627
+ confirmed=False,
628
+ user_added=False,
629
+ confidence=confidence,
630
+ suggested=False,
631
+ )
632
+ )
633
+
634
+ if unmatched:
635
+ found.append(
636
+ ContractFlow(
637
+ id="other-api",
638
+ name="Other API Endpoints",
639
+ description="Additional API endpoints that don't fit a named category.",
640
+ routes=unmatched[:10],
641
+ files=[],
642
+ signals=["api"],
643
+ confirmed=False,
644
+ user_added=False,
645
+ confidence="medium",
646
+ suggested=True,
647
+ )
648
+ )
649
+
650
+ # Fallback: if no routes detected, infer from file structure
651
+ if not found and self.file_infos:
652
+ found = self._infer_from_file_structure()
653
+
654
+ return found
655
+
656
+ def _infer_from_file_structure(self) -> list[ContractFlow]:
657
+ """
658
+ Infer contract flows from file structure when no routes are detected.
659
+ This helps with projects that don't have standard route definitions.
660
+ """
661
+ from collections import defaultdict
662
+
663
+ # Group files by directory structure
664
+ dir_groups: dict[str, list[str]] = defaultdict(list)
665
+ for fi in self.file_infos:
666
+ parts = fi.path.replace("\\", "/").split("/")
667
+ if len(parts) >= 2:
668
+ # Use first two directory levels as group key
669
+ group_key = "/".join(parts[:2])
670
+ else:
671
+ group_key = "root"
672
+ dir_groups[group_key].append(fi.path)
673
+
674
+ found: list[ContractFlow] = []
675
+
676
+ # Map common directory patterns to contract flows
677
+ dir_to_flow = {
678
+ "api": ("API Endpoints", "API route handlers and controllers."),
679
+ "routes": ("Route Handlers", "Web route handlers and controllers."),
680
+ "pages": ("Page Components", "Web page components and views."),
681
+ "components": ("UI Components", "Reusable UI components."),
682
+ "services": ("Service Layer", "Business logic and service classes."),
683
+ "models": ("Data Models", "Data models and database schemas."),
684
+ "utils": ("Utilities", "Utility functions and helpers."),
685
+ "lib": ("Library Code", "Shared library code."),
686
+ "core": ("Core Logic", "Core application logic."),
687
+ "auth": ("Authentication", "Authentication and authorization logic."),
688
+ "views": ("Views", "View templates and components."),
689
+ "controllers": ("Controllers", "Request handlers and controllers."),
690
+ "middleware": ("Middleware", "Request/response middleware."),
691
+ "tests": ("Test Suite", "Test files and test utilities."),
692
+ }
693
+
694
+ for dir_key, files in dir_groups.items():
695
+ dir_name = dir_key.split("/")[-1].lower()
696
+
697
+ # Check if this directory matches a known pattern
698
+ flow_name = None
699
+ flow_desc = None
700
+ for pattern, (name, desc) in dir_to_flow.items():
701
+ if pattern in dir_name:
702
+ flow_name = name
703
+ flow_desc = desc
704
+ break
705
+
706
+ if not flow_name:
707
+ # Use directory name as flow name
708
+ flow_name = f"{dir_name.title()} Module"
709
+ flow_desc = f"Code in the {dir_name} directory."
710
+
711
+ flow_id = dir_name.replace("/", "-")
712
+ found.append(
713
+ ContractFlow(
714
+ id=flow_id,
715
+ name=flow_name,
716
+ description=flow_desc,
717
+ routes=[],
718
+ files=files[:10],
719
+ signals=["file-structure"],
720
+ confirmed=False,
721
+ user_added=False,
722
+ confidence="low",
723
+ suggested=True,
724
+ )
725
+ )
726
+
727
+ return found[:10] # Limit to top 10 flows
728
+
729
+ def ai_infer(self, config: dict | None = None) -> list[ContractFlow] | None:
730
+ """
731
+ Use AI to generate a richer contract from AST scanner data.
732
+ Returns AI-inferred flows merged with offline pattern matches,
733
+ or None if AI is not available.
734
+ """
735
+ response = build_ai_contract_summary(
736
+ self.file_infos,
737
+ self.routes,
738
+ self.dead_files,
739
+ self.circular_deps,
740
+ config,
741
+ )
742
+ if not response:
743
+ return None
744
+
745
+ ai_flows = parse_ai_contract_response(response)
746
+ if not ai_flows:
747
+ return None
748
+
749
+ offline_flows = self.infer()
750
+ {f.id for f in offline_flows}
751
+
752
+ merged: list[ContractFlow] = []
753
+ seen_names: set[str] = set()
754
+
755
+ for i, af in enumerate(ai_flows):
756
+ name = af.get("name", f"AI Flow {i + 1}")
757
+ key = name.lower().replace(" ", "-")
758
+ if key in seen_names:
759
+ continue
760
+ seen_names.add(key)
761
+ merged.append(
762
+ ContractFlow(
763
+ id=key,
764
+ name=name,
765
+ description=af.get("description", ""),
766
+ routes=af.get("route_paths", [])[:5],
767
+ files=af.get("file_paths", [])[:10],
768
+ signals=["ai-inferred"],
769
+ confirmed=False,
770
+ user_added=False,
771
+ )
772
+ )
773
+
774
+ # Mark AI-inferred flows with high confidence
775
+ for f in merged:
776
+ if "ai-inferred" in f.signals:
777
+ f.confidence = "high"
778
+ f.suggested = False
779
+
780
+ # Merge offline-only flows that AI missed — skip suggested (low confidence)
781
+ for f in offline_flows:
782
+ if f.suggested:
783
+ continue
784
+ key = f.name.lower().replace(" ", "-")
785
+ if key not in seen_names:
786
+ merged.append(f)
787
+
788
+ return merged
789
+
790
+ def build_confirmation_message(self, flows: list[ContractFlow], all_flows: bool = False) -> str:
791
+ """
792
+ Build the plain-English message Patchi shows the user before confirmation.
793
+ Suggested (low-confidence) flows are excluded from the display unless all_flows=True.
794
+ """
795
+ visible = flows if all_flows else [f for f in flows if not f.suggested]
796
+ hidden_count = len(flows) - len(visible)
797
+
798
+ if not visible:
799
+ msg = (
800
+ "I didn't find any obvious critical flows in your project.\n"
801
+ "You can add them manually below."
802
+ )
803
+ if hidden_count:
804
+ msg += (
805
+ f"\n\n[dim]({hidden_count} low-confidence flow(s) were inferred but hidden — "
806
+ f"run with --all-flows to see them)[/dim]"
807
+ )
808
+ return msg
809
+
810
+ ai_count = sum(1 for f in visible if "ai-inferred" in f.signals)
811
+ len(visible) - ai_count
812
+
813
+ names = [f.name for f in visible]
814
+ if len(names) == 1:
815
+ flow_list = names[0]
816
+ elif len(names) == 2:
817
+ flow_list = f"{names[0]} and {names[1]}"
818
+ else:
819
+ flow_list = ", ".join(names[:-1]) + f", and {names[-1]}"
820
+
821
+ source_note = ""
822
+ if ai_count > 0:
823
+ n_files = len(self.file_infos)
824
+ n_routes = len(self.routes)
825
+ source_note = f"\n(Detected via AI analysis of {n_files} files and {n_routes} routes)"
826
+ if hidden_count:
827
+ source_note += (
828
+ f"\n({hidden_count} low-confidence flow(s) hidden — run --all-flows to see)"
829
+ )
830
+
831
+ return (
832
+ f"I think your critical flows are: {flow_list}.{source_note}\n"
833
+ f"Does that look right? I'll protect these with every fix I make."
834
+ )
835
+
836
+
837
+ def confirm_flows(
838
+ flows: list[ContractFlow],
839
+ confirmed_ids: set[str],
840
+ user_additions: list[dict] | None = None,
841
+ ) -> list[ContractFlow]:
842
+ """
843
+ Mark flows as confirmed based on user selection.
844
+ user_additions: list of {name, description, routes, files} dicts for user-added flows.
845
+ Returns the final confirmed contract.
846
+ """
847
+ result: list[ContractFlow] = []
848
+
849
+ for flow in flows:
850
+ if flow.id in confirmed_ids:
851
+ flow.confirmed = True
852
+ result.append(flow)
853
+
854
+ if user_additions:
855
+ for i, addition in enumerate(user_additions):
856
+ result.append(
857
+ ContractFlow(
858
+ id=f"user-{i}",
859
+ name=addition.get("name", f"Custom Flow {i + 1}"),
860
+ description=addition.get("description", "User-defined critical flow."),
861
+ routes=addition.get("routes", []),
862
+ files=addition.get("files", []),
863
+ signals=["user-defined"],
864
+ confirmed=True,
865
+ user_added=True,
866
+ )
867
+ )
868
+
869
+ return result
870
+
871
+
872
+ def flows_from_dict(data: list[dict]) -> list[ContractFlow]:
873
+ """Deserialize flows from stored dict (memory)."""
874
+ return [
875
+ ContractFlow(
876
+ id=d["id"],
877
+ name=d["name"],
878
+ description=d["description"],
879
+ routes=d.get("routes", []),
880
+ files=d.get("files", []),
881
+ signals=d.get("signals", []),
882
+ confirmed=d.get("confirmed", False),
883
+ user_added=d.get("user_added", False),
884
+ critical=d.get("critical", True),
885
+ confidence=d.get("confidence", "medium"),
886
+ suggested=d.get("suggested", False),
887
+ )
888
+ for d in data
889
+ ]