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,419 @@
1
+ """
2
+ Layered Brain — the "complex brain system" for Patchi.
3
+
4
+ Breaks a codebase into persistent **layers** so the agent can retrieve context
5
+ for one part without re-reading the whole repository:
6
+
7
+ Layer 4: PROJECT → purpose, stack, conventions
8
+ Layer 3: FLOWS → critical paths (contract flows)
9
+ Layer 2: SUBSYSTEMS → "auth", "data", "api", "ui" (semantic groups)
10
+ Layer 1: MODULES → directory-level units (cached summaries)
11
+ Layer 0: FILES → FileInfo (handled elsewhere)
12
+
13
+ Each layer carries a cached heuristic summary, its inter-layer dependencies
14
+ (derived from the import graph), its public API, and a validity hash. When a
15
+ single file changes, only its module summary is recomputed and the change
16
+ propagates up one level — sibling layers are never touched.
17
+
18
+ This module is **additive**: it builds on the existing FileInfo / ImportGraph /
19
+ RouteInfo structures and serialises to `.patchi/memory/layers.json`. No rewrite
20
+ of the scan pipeline is required.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import hashlib
26
+ import re
27
+ from dataclasses import dataclass, field
28
+ from typing import TYPE_CHECKING
29
+
30
+ if TYPE_CHECKING:
31
+ from patchi.core.brain.framework import StackInfo
32
+ from patchi.core.brain.import_graph import ImportGraph
33
+ from patchi.core.brain.route_mapper import RouteInfo
34
+ from patchi.core.brain.scanner import FileInfo
35
+
36
+
37
+ # ── Subsystem classification ──────────────────────────────────────────────────
38
+
39
+ # Maps a keyword (matched against module path) to a semantic subsystem name.
40
+ _SUBSYSTEM_RULES: list[tuple[str, str]] = [
41
+ (r"auth|login|session|security|oauth|jwt|token|passwd|permission", "auth"),
42
+ (r"api|route|router|controller|endpoint|view|handler|resource", "api"),
43
+ (r"model|db|data|repo|repository|orm|migration|schema|query|sql", "data"),
44
+ (r"test|spec|__tests__|e2e|fixture", "tests"),
45
+ (r"ui|component|page|frontend|web|dom|css|react|vue|svelte", "ui"),
46
+ (r"agent|scanner|fix|brain|analy|guard|detect", "agents"),
47
+ (r"core|util|common|lib|shared|helper|internal", "core"),
48
+ (r"config|deploy|infra|ci|docker|k8s|pipeline|script", "infra"),
49
+ ]
50
+
51
+
52
+ def _classify_subsystem(module_name: str) -> str:
53
+ """Map a module path to a semantic subsystem name."""
54
+ low = module_name.lower()
55
+ for pattern, name in _SUBSYSTEM_RULES:
56
+ if re.search(pattern, low):
57
+ return name
58
+ # Fall back to the top-level directory name
59
+ return module_name.split("/")[0] or "root"
60
+
61
+
62
+ # ── Layer dataclass ───────────────────────────────────────────────────────────
63
+
64
+
65
+ @dataclass
66
+ class Layer:
67
+ """A single layer in the Layered Brain."""
68
+
69
+ name: str
70
+ level: int # 1=module, 2=subsystem, 4=project
71
+ path: str = "" # relative path (modules) or logical name (subsystems/project)
72
+ language: str = "" # primary language of the layer
73
+ summary: str = "" # cached heuristic description
74
+ purpose: str = "" # one-line purpose (heuristic)
75
+ files: list[str] = field(default_factory=list) # child file paths
76
+ child_layers: list[str] = field(default_factory=list) # child layer names
77
+ parent_layer: str | None = None
78
+ depends_on: list[str] = field(default_factory=list) # layer names this imports
79
+ dependents: list[str] = field(default_factory=list) # layer names importing this
80
+ public_api: list[str] = field(default_factory=list) # exported symbols
81
+ validity_hash: str = "" # hash of public_api + summary + files
82
+ stale: bool = False # True when a parent/child change invalidated this layer
83
+
84
+ def compute_hash(self) -> str:
85
+ payload = (
86
+ self.name
87
+ + "|"
88
+ + self.summary
89
+ + "|"
90
+ + ",".join(sorted(self.public_api))
91
+ + "|"
92
+ + ",".join(sorted(self.files))
93
+ )
94
+ return hashlib.sha1(payload.encode("utf-8")).hexdigest()[:12]
95
+
96
+ def to_dict(self) -> dict:
97
+ return {
98
+ "name": self.name,
99
+ "level": self.level,
100
+ "path": self.path,
101
+ "language": self.language,
102
+ "summary": self.summary,
103
+ "purpose": self.purpose,
104
+ "files": self.files,
105
+ "child_layers": self.child_layers,
106
+ "parent_layer": self.parent_layer,
107
+ "depends_on": self.depends_on,
108
+ "dependents": self.dependents,
109
+ "public_api": self.public_api,
110
+ "validity_hash": self.validity_hash,
111
+ "stale": self.stale,
112
+ }
113
+
114
+ @classmethod
115
+ def from_dict(cls, d: dict) -> Layer:
116
+ return cls(
117
+ name=d["name"],
118
+ level=d["level"],
119
+ path=d.get("path", ""),
120
+ language=d.get("language", ""),
121
+ summary=d.get("summary", ""),
122
+ purpose=d.get("purpose", ""),
123
+ files=d.get("files", []),
124
+ child_layers=d.get("child_layers", []),
125
+ parent_layer=d.get("parent_layer"),
126
+ depends_on=d.get("depends_on", []),
127
+ dependents=d.get("dependents", []),
128
+ public_api=d.get("public_api", []),
129
+ validity_hash=d.get("validity_hash", ""),
130
+ stale=d.get("stale", False),
131
+ )
132
+
133
+
134
+ # ── Helpers ───────────────────────────────────────────────────────────────────
135
+
136
+
137
+ def _module_of(file_path: str) -> str:
138
+ """Return the module (parent directory) for a file path."""
139
+ parts = file_path.split("/")
140
+ if len(parts) <= 1:
141
+ return file_path # root-level file is its own module
142
+ return "/".join(parts[:-1])
143
+
144
+
145
+ def _lang_value(lang) -> str:
146
+ if hasattr(lang, "value"):
147
+ return lang.value
148
+ return str(lang)
149
+
150
+
151
+ def _module_summary(
152
+ module_name: str,
153
+ files: list[str],
154
+ fi_map: dict[str, FileInfo],
155
+ routes: list[RouteInfo],
156
+ ) -> tuple[str, str, list[str]]:
157
+ """Produce (summary, purpose, public_api) for a module."""
158
+ langs: dict[str, int] = {}
159
+ func_count = 0
160
+ class_count = 0
161
+ public_api: list[str] = []
162
+ for fp in files:
163
+ fi = fi_map.get(fp)
164
+ if not fi:
165
+ continue
166
+ lv = _lang_value(fi.language)
167
+ langs[lv] = langs.get(lv, 0) + 1
168
+ func_count += len(fi.functions)
169
+ class_count += len(fi.classes)
170
+ for fn in fi.functions:
171
+ public_api.append(fn.name)
172
+ for cl in fi.classes:
173
+ public_api.append(cl.name)
174
+
175
+ module_routes = [r for r in routes if _module_of(r.file) == module_name]
176
+ route_bits: list[str] = []
177
+ for r in module_routes[:6]:
178
+ method = r.method if hasattr(r, "method") else r.get("method", "GET")
179
+ path = r.path if hasattr(r, "path") else r.get("path", "/")
180
+ route_bits.append(f"{method} {path}")
181
+ route_summary = (" Routes: " + ", ".join(route_bits)) if route_bits else ""
182
+
183
+ lang_str = ", ".join(f"{k}×{v}" for k, v in langs.items()) or "unknown"
184
+ summary = (
185
+ f"Module '{module_name}' — {len(files)} file(s) ({lang_str}), "
186
+ f"{func_count} function(s), {class_count} class(es).{route_summary}"
187
+ )
188
+ purpose = f"Contains {func_count} function(s) and {class_count} class(es) under {module_name}."
189
+ return summary, purpose, public_api
190
+
191
+
192
+ # ── Builder ───────────────────────────────────────────────────────────────────
193
+
194
+
195
+ def build_layers(
196
+ file_infos: list[FileInfo],
197
+ graph: ImportGraph | None = None,
198
+ routes: list[RouteInfo] | None = None,
199
+ stack: StackInfo | None = None,
200
+ ) -> dict[str, Layer]:
201
+ """Build the full layered brain from scan results.
202
+
203
+ Returns a dict keyed by layer name. Includes module (L1), subsystem (L2),
204
+ and project (L4) layers. Layer 0 (individual files) is intentionally not
205
+ stored — it is the FileInfo cache already maintained by the scanner.
206
+ """
207
+ routes = routes or []
208
+ fi_map = {fi.path: fi for fi in file_infos}
209
+
210
+ # ── Level 1: modules (one per directory) ──────────────────────────────────
211
+ modules: dict[str, list[str]] = {}
212
+ for fi in file_infos:
213
+ mod = _module_of(fi.path)
214
+ modules.setdefault(mod, []).append(fi.path)
215
+
216
+ layers: dict[str, Layer] = {}
217
+ for mod_name, files in modules.items():
218
+ summary, purpose, public_api = _module_summary(mod_name, files, fi_map, routes)
219
+ layer = Layer(
220
+ name=mod_name,
221
+ level=1,
222
+ path=mod_name,
223
+ language=_dominant_language(files, fi_map),
224
+ summary=summary,
225
+ purpose=purpose,
226
+ files=sorted(files),
227
+ public_api=sorted(set(public_api)),
228
+ )
229
+ layer.validity_hash = layer.compute_hash()
230
+ layers[mod_name] = layer
231
+
232
+ # ── Level 2: subsystems (cluster modules by semantic role) ─────────────────
233
+ subsystem_modules: dict[str, list[str]] = {}
234
+ module_to_subsystem: dict[str, str] = {}
235
+ for mod_name in modules:
236
+ sub = _classify_subsystem(mod_name)
237
+ subsystem_modules.setdefault(sub, []).append(mod_name)
238
+ module_to_subsystem[mod_name] = sub
239
+
240
+ for sub_name, mods in subsystem_modules.items():
241
+ sub_files: list[str] = []
242
+ sub_api: list[str] = []
243
+ for m in mods:
244
+ sub_files.extend(layers[m].files)
245
+ sub_api.extend(layers[m].public_api)
246
+ sub_summary = (
247
+ f"Subsystem '{sub_name}' — {len(mods)} module(s), "
248
+ f"{len(sub_files)} file(s). Modules: {', '.join(mods[:8])}."
249
+ )
250
+ sub_layer = Layer(
251
+ name=sub_name,
252
+ level=2,
253
+ path=sub_name,
254
+ language=_dominant_language(sub_files, fi_map),
255
+ summary=sub_summary,
256
+ purpose=f"Groups {len(mods)} module(s) under the '{sub_name}' concern.",
257
+ files=sorted(set(sub_files)),
258
+ child_layers=sorted(mods),
259
+ public_api=sorted(set(sub_api)),
260
+ )
261
+ sub_layer.validity_hash = sub_layer.compute_hash()
262
+ layers[sub_name] = sub_layer
263
+ for m in mods:
264
+ layers[m].parent_layer = sub_name
265
+
266
+ # ── Inter-layer dependencies (from import graph) ───────────────────────────
267
+ if graph is not None:
268
+ _populate_dependencies(layers, graph, module_to_subsystem)
269
+
270
+ # ── Level 4: project ───────────────────────────────────────────────────────
271
+ fw_names = (
272
+ ", ".join(f.name for f in stack.frameworks[:5]) if stack and stack.frameworks else "unknown"
273
+ )
274
+ runtime = stack.runtime if stack else "unknown"
275
+ project_summary = (
276
+ f"Project — {len(file_infos)} file(s) across {len(modules)} module(s) "
277
+ f"and {len(subsystem_modules)} subsystem(s). "
278
+ f"Stack: {fw_names} (runtime: {runtime})."
279
+ )
280
+ project_layer = Layer(
281
+ name="__project__",
282
+ level=4,
283
+ path="",
284
+ language=runtime,
285
+ summary=project_summary,
286
+ purpose="The whole project — top of the layer hierarchy.",
287
+ files=sorted(fi.path for fi in file_infos),
288
+ child_layers=sorted(subsystem_modules.keys()),
289
+ public_api=sorted({a for lay in layers.values() for a in lay.public_api}),
290
+ )
291
+ project_layer.validity_hash = project_layer.compute_hash()
292
+ layers["__project__"] = project_layer
293
+ for sub in subsystem_modules:
294
+ layers[sub].parent_layer = "__project__"
295
+
296
+ return layers
297
+
298
+
299
+ def _dominant_language(files: list[str], fi_map: dict[str, FileInfo]) -> str:
300
+ counts: dict[str, int] = {}
301
+ for fp in files:
302
+ fi = fi_map.get(fp)
303
+ if fi:
304
+ lv = _lang_value(fi.language)
305
+ counts[lv] = counts.get(lv, 0) + 1
306
+ if not counts:
307
+ return ""
308
+ return max(counts, key=counts.get)
309
+
310
+
311
+ def _populate_dependencies(
312
+ layers: dict[str, Layer],
313
+ graph: ImportGraph,
314
+ module_to_subsystem: dict[str, str],
315
+ ) -> None:
316
+ """Derive module- and subsystem-level dependency edges from the graph."""
317
+ edges: dict[str, set[str]] = {}
318
+ raw_edges: dict[str, set[str]] = {}
319
+ if hasattr(graph, "edges"):
320
+ raw_edges = graph.edges
321
+ elif isinstance(graph, dict) and "edges" in graph:
322
+ raw_edges = graph["edges"]
323
+
324
+ for src_file, targets in raw_edges.items():
325
+ src_mod = _module_of(src_file)
326
+ for tgt in targets:
327
+ tgt_mod = _module_of(tgt) if isinstance(tgt, str) else str(tgt)
328
+ if src_mod == tgt_mod:
329
+ continue
330
+ edges.setdefault(src_mod, set()).add(tgt_mod)
331
+
332
+ # Module-level edges
333
+ for src_mod, tgts in edges.items():
334
+ if src_mod not in layers:
335
+ continue
336
+ for tgt_mod in tgts:
337
+ if tgt_mod in layers:
338
+ if tgt_mod not in layers[src_mod].depends_on:
339
+ layers[src_mod].depends_on.append(tgt_mod)
340
+ if src_mod not in layers[tgt_mod].dependents:
341
+ layers[tgt_mod].dependents.append(src_mod)
342
+
343
+ # Subsystem-level edges (cluster module edges)
344
+ sub_edges: dict[str, set[str]] = {}
345
+ for src_mod, tgts in edges.items():
346
+ src_sub = module_to_subsystem.get(src_mod)
347
+ if not src_sub:
348
+ continue
349
+ for tgt_mod in tgts:
350
+ tgt_sub = module_to_subsystem.get(tgt_mod)
351
+ if not tgt_sub or tgt_sub == src_sub:
352
+ continue
353
+ sub_edges.setdefault(src_sub, set()).add(tgt_sub)
354
+
355
+ for src_sub, tgts in sub_edges.items():
356
+ if src_sub not in layers:
357
+ continue
358
+ for tgt_sub in sorted(tgts):
359
+ if tgt_sub in layers:
360
+ if tgt_sub not in layers[src_sub].depends_on:
361
+ layers[src_sub].depends_on.append(tgt_sub)
362
+ if src_sub not in layers[tgt_sub].dependents:
363
+ layers[tgt_sub].dependents.append(src_sub)
364
+
365
+
366
+ # ── Serialisation helpers ─────────────────────────────────────────────────────
367
+
368
+
369
+ def layers_to_dict(layers: dict[str, Layer]) -> dict:
370
+ return {
371
+ "version": 1,
372
+ "layer_count": len(layers),
373
+ "layers": {name: layer.to_dict() for name, layer in layers.items()},
374
+ }
375
+
376
+
377
+ def layers_from_dict(data: dict) -> dict[str, Layer]:
378
+ return {name: Layer.from_dict(d) for name, d in data.get("layers", {}).items()}
379
+
380
+
381
+ # ── Query helpers (used by the reasoning engine in later phases) ───────────────
382
+
383
+
384
+ def get_layer_context(layers: dict[str, Layer], layer_name: str, max_depth: int = 1) -> dict:
385
+ """Return a layer plus its direct dependency summaries for fast context."""
386
+ if layer_name not in layers:
387
+ return {}
388
+ out: dict[str, str] = {}
389
+ seen: set[str] = set()
390
+
391
+ def collect(name: str, depth: int) -> None:
392
+ if name in seen or depth > max_depth:
393
+ return
394
+ seen.add(name)
395
+ layer = layers.get(name)
396
+ if not layer:
397
+ return
398
+ out[name] = layer.summary
399
+ if depth < max_depth:
400
+ for dep in layer.depends_on:
401
+ collect(dep, depth + 1)
402
+
403
+ collect(layer_name, 0)
404
+ return out
405
+
406
+
407
+ def find_layers_for_file(layers: dict[str, Layer], file_path: str) -> list[str]:
408
+ """Return the names of layers that contain a given file."""
409
+ return [name for name, layer in layers.items() if file_path in layer.files]
410
+
411
+
412
+ def detect_stale_layers(old_layers: dict[str, Layer], new_layers: dict[str, Layer]) -> list[str]:
413
+ """Compare two layer sets and return names whose hash changed."""
414
+ stale: list[str] = []
415
+ for name, layer in new_layers.items():
416
+ old = old_layers.get(name)
417
+ if old is None or old.validity_hash != layer.validity_hash:
418
+ stale.append(name)
419
+ return stale
@@ -0,0 +1,229 @@
1
+ """
2
+ Learning Brain — tracks user accept/reject patterns to improve future suggestions.
3
+
4
+ Maintains a Bayesian-style classifier that learns:
5
+ - Which finding types the user always accepts → suggest more aggressively
6
+ - Which finding types the user always rejects → stop suggesting
7
+ - Which fix agents produce patches the user trusts → prioritize those
8
+
9
+ All data stored in .patchi/learning.json. Zero external dependencies.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import logging
15
+ import time
16
+ from pathlib import Path
17
+
18
+ _LEARNING_FILE = ".patchi/learning.json"
19
+
20
+ # Minimum samples before we trust the pattern
21
+ _MIN_SAMPLES = 3
22
+
23
+ # Decay factor: older decisions matter less (per day)
24
+ _DECAY_HALF_LIFE_DAYS = 30
25
+
26
+
27
+ _log = logging.getLogger("patchi.brain.learning")
28
+
29
+
30
+ def _load(root: Path) -> dict:
31
+ path = root / _LEARNING_FILE
32
+ if not path.exists():
33
+ return {"acceptances": {}, "rejections": {}, "agent_trust": {}}
34
+ try:
35
+ return json.loads(path.read_text(encoding="utf-8"))
36
+ except Exception as e:
37
+ _log.warning("_load failed: %s", e)
38
+ return {"acceptances": {}, "rejections": {}, "agent_trust": {}}
39
+
40
+
41
+ def _save(root: Path, data: dict) -> None:
42
+ path = root / _LEARNING_FILE
43
+ path.parent.mkdir(parents=True, exist_ok=True)
44
+ tmp = path.with_suffix(".tmp")
45
+ tmp.write_text(json.dumps(data, indent=2), encoding="utf-8")
46
+ tmp.replace(path)
47
+
48
+
49
+ def record_acceptance(finding_type: str, agent_name: str, root: Path) -> None:
50
+ """Record that the user accepted a fix for this finding type."""
51
+ data = _load(root)
52
+ now = time.time()
53
+
54
+ acceptances = data.setdefault("acceptances", {})
55
+ acceptances.setdefault(finding_type, []).append(now)
56
+ # Keep last 100 entries per type
57
+ acceptances[finding_type] = acceptances[finding_type][-100:]
58
+
59
+ trust = data.setdefault("agent_trust", {})
60
+ trust.setdefault(agent_name, {"accepts": 0, "rejects": 0})
61
+ trust[agent_name]["accepts"] += 1
62
+
63
+ _save(root, data)
64
+
65
+
66
+ def record_rejection(finding_type: str, agent_name: str, root: Path) -> None:
67
+ """Record that the user rejected a fix for this finding type."""
68
+ data = _load(root)
69
+ now = time.time()
70
+
71
+ rejections = data.setdefault("rejections", {})
72
+ rejections.setdefault(finding_type, []).append(now)
73
+ rejections[finding_type] = rejections[finding_type][-100:]
74
+
75
+ trust = data.setdefault("agent_trust", {})
76
+ trust.setdefault(agent_name, {"accepts": 0, "rejects": 0})
77
+ trust[agent_name]["rejects"] += 1
78
+
79
+ _save(root, data)
80
+
81
+
82
+ def should_suggest(finding_type: str, root: Path) -> bool:
83
+ """
84
+ Should we suggest a fix for this finding type?
85
+ Returns False if the user has rejected this type 3+ times more than they accepted it.
86
+ """
87
+ data = _load(root)
88
+ acceptances = data.get("acceptances", {}).get(finding_type, [])
89
+ rejections = data.get("rejections", {}).get(finding_type, [])
90
+
91
+ total = len(acceptances) + len(rejections)
92
+ if total < _MIN_SAMPLES:
93
+ return True # Not enough data yet
94
+
95
+ # Weighted by recency
96
+ now = time.time()
97
+ accept_score = _recency_weighted(acceptances, now)
98
+ reject_score = _recency_weighted(rejections, now)
99
+
100
+ # If rejections outweigh acceptances 3:1, stop suggesting
101
+ if reject_score > accept_score * 3:
102
+ return False
103
+
104
+ return True
105
+
106
+
107
+ def get_agent_trust(agent_name: str, root: Path) -> float:
108
+ """
109
+ Get trust score for an agent (0.0 to 1.0).
110
+ 1.0 = user always accepts, 0.0 = user always rejects.
111
+ """
112
+ data = _load(root)
113
+ trust = data.get("agent_trust", {}).get(agent_name, {"accepts": 0, "rejects": 0})
114
+ total = trust["accepts"] + trust["rejects"]
115
+ if total < _MIN_SAMPLES:
116
+ return 0.5 # Neutral until we have data
117
+ return trust["accepts"] / total
118
+
119
+
120
+ def get_summary(root: Path) -> dict:
121
+ """Return a summary of learned patterns."""
122
+ data = _load(root)
123
+ time.time()
124
+
125
+ summary = {
126
+ "acceptances": {},
127
+ "rejections": {},
128
+ "agent_trust": {},
129
+ "skip_types": [],
130
+ }
131
+
132
+ for ftype, timestamps in data.get("acceptances", {}).items():
133
+ summary["acceptances"][ftype] = len(timestamps)
134
+
135
+ for ftype, timestamps in data.get("rejections", {}).items():
136
+ summary["rejections"][ftype] = len(timestamps)
137
+
138
+ for agent, scores in data.get("agent_trust", {}).items():
139
+ total = scores["accepts"] + scores["rejects"]
140
+ if total >= _MIN_SAMPLES:
141
+ summary["agent_trust"][agent] = round(scores["accepts"] / total, 2)
142
+
143
+ # Find types that should be skipped
144
+ for ftype in set(
145
+ list(data.get("acceptances", {}).keys()) + list(data.get("rejections", {}).keys())
146
+ ):
147
+ if not should_suggest(ftype, root):
148
+ summary["skip_types"].append(ftype)
149
+
150
+ return summary
151
+
152
+
153
+ # ── Fix-pattern memory ─────────────────────────────────────────────────────────
154
+
155
+
156
+ def record_fix_pattern(
157
+ finding_type: str,
158
+ file_pattern: str,
159
+ fix_strategy: str,
160
+ fix_summary: str,
161
+ root: Path,
162
+ ) -> None:
163
+ """Record a successful fix pattern so it can be reused for similar findings."""
164
+ data = _load(root)
165
+ patterns = data.setdefault("fix_patterns", [])
166
+ patterns.append(
167
+ {
168
+ "finding_type": finding_type,
169
+ "file_pattern": file_pattern,
170
+ "fix_strategy": fix_strategy,
171
+ "fix_summary": fix_summary,
172
+ "timestamp": time.time(),
173
+ }
174
+ )
175
+ # Keep last 500 patterns to avoid unbounded growth
176
+ data["fix_patterns"] = patterns[-500:]
177
+ _save(root, data)
178
+
179
+
180
+ def get_fix_pattern(finding_type: str, file_path: str, root: Path) -> dict | None:
181
+ """Find the best matching fix pattern for a given finding type + file path.
182
+
183
+ Matches by finding_type first, then by file extension pattern.
184
+ Returns the most recent match, or None.
185
+ """
186
+ data = _load(root)
187
+ patterns = data.get("fix_patterns", [])
188
+ if not patterns:
189
+ return None
190
+
191
+ ext = Path(file_path).suffix if file_path else ""
192
+ # Score each pattern: exact type match + optional ext match
193
+ scored: list[tuple[float, dict]] = []
194
+ for p in patterns:
195
+ score = 0.0
196
+ if p.get("finding_type") == finding_type:
197
+ score += 2.0
198
+ if ext and p.get("file_pattern", "").endswith(ext):
199
+ score += 1.0
200
+ if score > 0:
201
+ # Recency bonus: patterns applied within last 7 days get +1
202
+ age = time.time() - p.get("timestamp", 0)
203
+ if age < 604800:
204
+ score += 1.0
205
+ scored.append((score, p))
206
+
207
+ if not scored:
208
+ return None
209
+ scored.sort(key=lambda x: -x[0])
210
+ return scored[0][1]
211
+
212
+
213
+ def list_fix_patterns(root: Path, limit: int = 20) -> list[dict]:
214
+ """Return recent fix patterns for display."""
215
+ data = _load(root)
216
+ patterns = data.get("fix_patterns", [])
217
+ return patterns[-limit:]
218
+
219
+
220
+ def _recency_weighted(timestamps: list[float], now: float) -> float:
221
+ """Weight timestamps by recency using exponential decay."""
222
+ import math
223
+
224
+ score = 0.0
225
+ for ts in timestamps:
226
+ days_ago = (now - ts) / 86400
227
+ weight = math.exp(-0.693 * days_ago / _DECAY_HALF_LIFE_DAYS)
228
+ score += weight
229
+ return score