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,156 @@
1
+ """
2
+ Hosted Webhooks — Outbound alert forwarding for Patchi hosted mode.
3
+
4
+ Stores webhook subscriptions in .patchi/hosted/webhooks.json and delivers
5
+ finding/anomaly alerts via signed POST requests. Delivery failures are
6
+ logged to the audit log; repeated failures auto-disable a webhook.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import hmac
13
+ import json
14
+ import logging
15
+ import time
16
+ import uuid
17
+ from pathlib import Path
18
+
19
+ _log = logging.getLogger("patchi.hosted.webhooks")
20
+
21
+ MAX_FAILURES = 5 # consecutive failures before auto-disable
22
+
23
+
24
+ def _path(root: Path) -> Path:
25
+ return root / ".patchi" / "hosted" / "webhooks.json"
26
+
27
+
28
+ def _load(root: Path) -> list[dict]:
29
+ p = _path(root)
30
+ if not p.exists():
31
+ return []
32
+ try:
33
+ return json.loads(p.read_text(encoding="utf-8"))
34
+ except Exception as e:
35
+ _log.warning("webhook load failed: %s", e)
36
+ return []
37
+
38
+
39
+ def _save(root: Path, hooks: list[dict]) -> None:
40
+ p = _path(root)
41
+ p.parent.mkdir(parents=True, exist_ok=True)
42
+ tmp = p.with_suffix(".tmp")
43
+ tmp.write_text(json.dumps(hooks, indent=2), encoding="utf-8")
44
+ tmp.replace(p)
45
+
46
+
47
+ def list_webhooks(root: Path) -> list[dict]:
48
+ """All webhooks with secret redacted."""
49
+ out = []
50
+ for h in _load(root):
51
+ out.append(
52
+ {k: v for k, v in h.items() if k != "secret"} | {"has_secret": bool(h.get("secret"))}
53
+ )
54
+ return out
55
+
56
+
57
+ def add_webhook(
58
+ root: Path, url: str, events: list[str] | None = None, name: str = "", secret: str = ""
59
+ ) -> dict:
60
+ """Register a webhook. Returns its record (with id)."""
61
+ hooks = _load(root)
62
+ record = {
63
+ "id": uuid.uuid4().hex[:10],
64
+ "url": url,
65
+ "events": events or ["finding", "anomaly", "scan_completed"],
66
+ "name": name or url[:40],
67
+ "secret": secret,
68
+ "enabled": True,
69
+ "consecutive_failures": 0,
70
+ "last_delivery": None,
71
+ "created_at": time.time(),
72
+ }
73
+ hooks.append(record)
74
+ _save(root, hooks)
75
+ return {k: v for k, v in record.items() if k != "secret"}
76
+
77
+
78
+ def remove_webhook(root: Path, hook_id: str) -> bool:
79
+ hooks = _load(root)
80
+ before = len(hooks)
81
+ hooks = [h for h in hooks if h.get("id") != hook_id]
82
+ if len(hooks) < before:
83
+ _save(root, hooks)
84
+ return True
85
+ return False
86
+
87
+
88
+ def _sign(payload_bytes: bytes, secret: str) -> str:
89
+ return hmac.new(secret.encode("utf-8"), payload_bytes, hashlib.sha256).hexdigest()
90
+
91
+
92
+ def dispatch_event(root: Path, event: str, data: dict) -> int:
93
+ """Fan an event out to matching enabled webhooks. Returns delivery count.
94
+
95
+ Never raises — delivery issues are logged and counted per-hook.
96
+ """
97
+ delivered = 0
98
+ hooks = _load(root)
99
+ dirty = False
100
+
101
+ for h in hooks:
102
+ if not h.get("enabled"):
103
+ continue
104
+ if event not in h.get("events", []) and "*" not in h.get("events", []):
105
+ continue
106
+
107
+ body = json.dumps({"event": event, "data": data, "ts": time.time()}).encode("utf-8")
108
+ headers = {"Content-Type": "application/json"}
109
+ if h.get("secret"):
110
+ headers["X-Patchi-Signature"] = _sign(body, h["secret"])
111
+
112
+ ok = False
113
+ try:
114
+ import urllib.request
115
+
116
+ req = urllib.request.Request(h["url"], data=body, headers=headers, method="POST")
117
+ with urllib.request.urlopen(req, timeout=10) as resp:
118
+ ok = 200 <= resp.status < 300
119
+ except Exception as e:
120
+ _log.debug("webhook %s delivery failed: %s", h["id"], e)
121
+
122
+ if ok:
123
+ h["consecutive_failures"] = 0
124
+ h["last_delivery"] = time.time()
125
+ delivered += 1
126
+ else:
127
+ h["consecutive_failures"] = h.get("consecutive_failures", 0) + 1
128
+ if h["consecutive_failures"] >= MAX_FAILURES:
129
+ h["enabled"] = False
130
+ try:
131
+ from patchi.core.hosted.audit_log import write as audit_write
132
+
133
+ audit_write(
134
+ root,
135
+ "webhook.disabled",
136
+ data={
137
+ "id": h["id"],
138
+ "reason": f"{MAX_FAILURES} consecutive delivery failures",
139
+ },
140
+ )
141
+ except Exception as _exc:
142
+ _log.warning('dispatch_event failed: %s', _exc)
143
+ dirty = True
144
+
145
+ if dirty:
146
+ _save(root, hooks)
147
+
148
+ try:
149
+ from patchi.core.hosted.audit_log import write as audit_write
150
+
151
+ if delivered:
152
+ audit_write(root, "webhook.delivered", data={"event": event, "count": delivered})
153
+ except Exception as _exc:
154
+ _log.warning('dispatch_event failed: %s', _exc)
155
+
156
+ return delivered
patchi/core/memory.py ADDED
@@ -0,0 +1,437 @@
1
+ """
2
+ Memory system for Patchi.
3
+
4
+ All persistent data outside config lives here. Organised JSON files,
5
+ one per category. Human-readable. Viewable and deletable from CLI and web UI.
6
+
7
+ Rules:
8
+ - Every write is atomic (write to temp, rename to target).
9
+ - Deleting any category triggers a rescan of that area on next run.
10
+ - Callers never read the JSON files directly — always go through these functions.
11
+ """
12
+
13
+ import json
14
+
15
+ # ── Low-level read/write ───────────────────────────────────────────────────────
16
+ import logging
17
+ import threading
18
+ import time
19
+ import uuid
20
+ from datetime import UTC
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ from patchi.core.atomic import atomic_replace
25
+ from patchi.core.config import require_project_root
26
+ from patchi.core.constants import (
27
+ MEMORY_FILES,
28
+ MemoryCategory,
29
+ )
30
+
31
+ _log = logging.getLogger("patchi.core.memory")
32
+
33
+ # Serializes read-modify-write ops (e.g. save_scan_result) that the Coordinator
34
+ # calls from parallel agent threads — without it, concurrent writers race on
35
+ # the same .tmp file (PermissionError/FileNotFoundError on Windows).
36
+ _WRITE_LOCK = threading.RLock()
37
+
38
+
39
+ def _mem_path(category: MemoryCategory, root: Path) -> Path:
40
+ return root / MEMORY_FILES[category]
41
+
42
+
43
+ def _read(category: MemoryCategory, root: Path) -> Any:
44
+ path = _mem_path(category, root)
45
+ if not path.exists():
46
+ return [] if category in _list_categories() else {}
47
+ with path.open("r", encoding="utf-8") as f:
48
+ return json.load(f)
49
+
50
+
51
+ def _write(category: MemoryCategory, data: Any, root: Path) -> None:
52
+ with _WRITE_LOCK:
53
+ path = _mem_path(category, root)
54
+ path.parent.mkdir(parents=True, exist_ok=True)
55
+ tmp = path.with_suffix(".tmp")
56
+ with tmp.open("w", encoding="utf-8") as f:
57
+ json.dump(data, f, indent=2)
58
+ atomic_replace(tmp, path)
59
+
60
+
61
+ def _update(category: MemoryCategory, fn: Any, root: Path) -> Any:
62
+ """Thread-safe read-modify-write: lock, read, mutate via fn, write back."""
63
+ r = _root(root)
64
+ with _WRITE_LOCK:
65
+ data = _read(category, r)
66
+ result = fn(data)
67
+ _write(category, result, r)
68
+ return result
69
+
70
+
71
+ def _list_categories() -> set[MemoryCategory]:
72
+ return {
73
+ MemoryCategory.PATCHES,
74
+ MemoryCategory.FAILED,
75
+ MemoryCategory.ISSUES,
76
+ MemoryCategory.RESTRICTIONS,
77
+ MemoryCategory.TOKENS,
78
+ }
79
+
80
+
81
+ def _root(root: Path | None) -> Path:
82
+ return root or require_project_root()
83
+
84
+
85
+ # ── Generic read / delete ──────────────────────────────────────────────────────
86
+
87
+
88
+ def read(category: MemoryCategory, root: Path | None = None) -> Any:
89
+ """Return all memory data for a category."""
90
+ return _read(category, _root(root))
91
+
92
+
93
+ def delete(category: MemoryCategory, root: Path | None = None) -> None:
94
+ """
95
+ Clear all memory for a category.
96
+ Resets to empty list or dict (not deleting the file itself,
97
+ so Patchi knows to re-scan on next run).
98
+ """
99
+ r = _root(root)
100
+ empty: Any = [] if category in _list_categories() else {}
101
+ _write(category, empty, r)
102
+
103
+
104
+ def delete_all(root: Path | None = None) -> None:
105
+ """Wipe everything. Called after user types CONFIRM."""
106
+ r = _root(root)
107
+ for category in MemoryCategory:
108
+ delete(category, r)
109
+
110
+
111
+ def summary(root: Path | None = None) -> dict[str, Any]:
112
+ """High-level counts per category — used by `p memory` and Overview panel."""
113
+ r = _root(root)
114
+ out: dict[str, Any] = {}
115
+ for category in MemoryCategory:
116
+ data = _read(category, r)
117
+ if isinstance(data, list):
118
+ out[category.value] = {"count": len(data)}
119
+ elif isinstance(data, dict):
120
+ out[category.value] = {"keys": list(data.keys())}
121
+ else:
122
+ out[category.value] = {}
123
+ return out
124
+
125
+
126
+ # ── Brain ──────────────────────────────────────────────────────────────────────
127
+
128
+
129
+ def get_brain(root: Path | None = None) -> dict:
130
+ return _read(MemoryCategory.BRAIN, _root(root))
131
+
132
+
133
+ def save_brain(brain: dict, root: Path | None = None) -> None:
134
+ _write(MemoryCategory.BRAIN, brain, _root(root))
135
+
136
+
137
+ # ── Layered brain ─────────────────────────────────────────────────────────────
138
+
139
+
140
+ def get_layers(root: Path | None = None) -> dict:
141
+ return _read(MemoryCategory.LAYERS, _root(root))
142
+
143
+
144
+ def save_layers(layers: dict, root: Path | None = None) -> None:
145
+ _write(MemoryCategory.LAYERS, layers, _root(root))
146
+
147
+
148
+ # ── Project charter (guard rails) ─────────────────────────────────────────────
149
+
150
+
151
+ def get_charter(root: Path | None = None) -> dict:
152
+ return _read(MemoryCategory.CHARTER, _root(root))
153
+
154
+
155
+ def save_charter(charter: dict, root: Path | None = None) -> None:
156
+ _write(MemoryCategory.CHARTER, charter, _root(root))
157
+
158
+
159
+ def mark_brain_stale(
160
+ changed_files: list[str],
161
+ root: Path | None = None,
162
+ stale_layers: list[str] | None = None,
163
+ ) -> None:
164
+ r = _root(root)
165
+ brain = _read(MemoryCategory.BRAIN, r)
166
+ brain["stale"] = True
167
+ brain["stale_reason"] = f"{len(changed_files)} file(s) changed"
168
+ brain["changed_files"] = changed_files
169
+ if stale_layers is not None:
170
+ brain["stale_layers"] = sorted(stale_layers)
171
+ _write(MemoryCategory.BRAIN, brain, r)
172
+
173
+
174
+ # ── Patches ────────────────────────────────────────────────────────────────────
175
+
176
+
177
+ def list_patches(root: Path | None = None) -> list[dict]:
178
+ return _read(MemoryCategory.PATCHES, _root(root))
179
+
180
+
181
+ def save_patch(patch: dict, root: Path | None = None) -> str:
182
+ """Append a patch record. Returns the patch ID."""
183
+ r = _root(root)
184
+ patches = _read(MemoryCategory.PATCHES, r)
185
+ if "id" not in patch:
186
+ patch["id"] = str(uuid.uuid4())[:8]
187
+ patch.setdefault("timestamp", _now())
188
+ patches.append(patch)
189
+ _write(MemoryCategory.PATCHES, patches, r)
190
+ return patch["id"]
191
+
192
+
193
+ def get_patch(patch_id: str, root: Path | None = None) -> dict | None:
194
+ for p in list_patches(root):
195
+ if p.get("id") == patch_id:
196
+ return p
197
+ return None
198
+
199
+
200
+ def delete_patches(root: Path | None = None) -> None:
201
+ delete(MemoryCategory.PATCHES, root)
202
+
203
+
204
+ # ── Failed patches ─────────────────────────────────────────────────────────────
205
+
206
+
207
+ def list_failed(root: Path | None = None) -> list[dict]:
208
+ return _read(MemoryCategory.FAILED, _root(root))
209
+
210
+
211
+ def save_failed(patch_id: str, reason: str, diff: str = "", root: Path | None = None) -> None:
212
+ r = _root(root)
213
+ failed = _read(MemoryCategory.FAILED, r)
214
+ failed.append(
215
+ {
216
+ "id": patch_id,
217
+ "reason": reason,
218
+ "diff": diff,
219
+ "timestamp": _now(),
220
+ }
221
+ )
222
+ _write(MemoryCategory.FAILED, failed, r)
223
+
224
+
225
+ def delete_failed(root: Path | None = None) -> None:
226
+ delete(MemoryCategory.FAILED, root)
227
+
228
+
229
+ # ── Scan results ───────────────────────────────────────────────────────────────
230
+
231
+
232
+ def get_scan_results(root: Path | None = None) -> dict:
233
+ return _read(MemoryCategory.SCANS, _root(root))
234
+
235
+
236
+ def save_scan_result(scanner_name: str, result: dict, root: Path | None = None) -> None:
237
+ def _mutate(scans: dict) -> dict:
238
+ scans[scanner_name] = {**result, "timestamp": _now()}
239
+ return scans
240
+
241
+ _update(MemoryCategory.SCANS, _mutate, root)
242
+
243
+
244
+ # ── Scan history ──────────────────────────────────────────────────────────────
245
+ SCAN_HISTORY_FILE = ".patchi/scan_history.json"
246
+ SCAN_HISTORY_MAX = 50 # keep last 50 scans
247
+
248
+
249
+ def get_scan_history(root: Path | None = None) -> list[dict]:
250
+ """Return the list of past scan summaries, newest first."""
251
+ r = _root(root)
252
+ path = r / SCAN_HISTORY_FILE
253
+ if not path.exists():
254
+ return []
255
+ try:
256
+ with path.open("r", encoding="utf-8") as f:
257
+ data = json.load(f)
258
+ return data if isinstance(data, list) else []
259
+ except Exception:
260
+ return []
261
+
262
+
263
+ def record_scan(summary: dict, root: Path | None = None) -> None:
264
+ """Append a scan summary to history. Keeps only the last SCAN_HISTORY_MAX entries."""
265
+ r = _root(root)
266
+ path = r / SCAN_HISTORY_FILE
267
+ with _WRITE_LOCK:
268
+ r.mkdir(parents=True, exist_ok=True)
269
+ history = []
270
+ if path.exists():
271
+ try:
272
+ with path.open("r", encoding="utf-8") as f:
273
+ history = json.load(f)
274
+ except Exception:
275
+ history = []
276
+ if not isinstance(history, list):
277
+ history = []
278
+ history.insert(0, summary)
279
+ history = history[:SCAN_HISTORY_MAX]
280
+ tmp = path.with_suffix(".tmp")
281
+ with tmp.open("w", encoding="utf-8") as f:
282
+ json.dump(history, f, indent=2, default=str)
283
+ atomic_replace(tmp, path)
284
+
285
+
286
+ # ── Known issues ───────────────────────────────────────────────────────────────
287
+
288
+
289
+ def list_issues(root: Path | None = None) -> list[dict]:
290
+ return _read(MemoryCategory.ISSUES, _root(root))
291
+
292
+
293
+ def save_issue(issue: dict, root: Path | None = None) -> None:
294
+ r = _root(root)
295
+ issues = _read(MemoryCategory.ISSUES, r)
296
+ issue.setdefault("id", str(uuid.uuid4())[:8])
297
+ issue.setdefault("timestamp", _now())
298
+ issues.append(issue)
299
+ _write(MemoryCategory.ISSUES, issues, r)
300
+
301
+
302
+ def resolve_issue(issue_id: str, root: Path | None = None) -> bool:
303
+ r = _root(root)
304
+ issues = _read(MemoryCategory.ISSUES, r)
305
+ before = len(issues)
306
+ issues = [i for i in issues if i.get("id") != issue_id]
307
+ _write(MemoryCategory.ISSUES, issues, r)
308
+ return len(issues) < before
309
+
310
+
311
+ def delete_issues(root: Path | None = None) -> None:
312
+ delete(MemoryCategory.ISSUES, root)
313
+
314
+
315
+ # ── Dev tokens (names only, not values) ───────────────────────────────────────
316
+
317
+
318
+ def list_tokens(root: Path | None = None) -> list[dict]:
319
+ return _read(MemoryCategory.TOKENS, _root(root))
320
+
321
+
322
+ def save_token(name: str, env_var: str, root: Path | None = None) -> None:
323
+ r = _root(root)
324
+ tokens = _read(MemoryCategory.TOKENS, r)
325
+ if not any(t["name"] == name for t in tokens):
326
+ tokens.append({"name": name, "env_var": env_var, "added": _now()})
327
+ _write(MemoryCategory.TOKENS, tokens, r)
328
+
329
+
330
+ def remove_token(name: str, root: Path | None = None) -> bool:
331
+ r = _root(root)
332
+ tokens = _read(MemoryCategory.TOKENS, r)
333
+ before = len(tokens)
334
+ tokens = [t for t in tokens if t["name"] != name]
335
+ _write(MemoryCategory.TOKENS, tokens, r)
336
+ return len(tokens) < before
337
+
338
+
339
+ # ── Helpers ────────────────────────────────────────────────────────────────────
340
+
341
+
342
+ def _now() -> str:
343
+ from datetime import datetime
344
+
345
+ return datetime.now(UTC).isoformat()
346
+
347
+
348
+ def clear_all(root: "Path | None" = None) -> None:
349
+ """Wipe every memory category. Irreversible."""
350
+ try:
351
+ r = root or require_project_root()
352
+ mem_dir = r / _PATCHI_DIR
353
+ for f in mem_dir.glob("*.json"):
354
+ if f.name != "config.json":
355
+ tmp = f.with_suffix(".tmp")
356
+ with tmp.open("w", encoding="utf-8") as fh:
357
+ json.dump({}, fh)
358
+ atomic_replace(tmp, f)
359
+ except Exception as e:
360
+ _log.warning("clear_all failed: %s", e)
361
+
362
+
363
+ # ── Health score history ───────────────────────────────────────────────────────
364
+
365
+ _PATCHI_DIR = ".patchi"
366
+
367
+
368
+ def log_health_score(score: int, finding_count: int, root: "Path | None" = None) -> None:
369
+ """Append a health score data point to history.json after every scan."""
370
+ r = root or require_project_root()
371
+ path = r / _PATCHI_DIR / "history.json"
372
+ try:
373
+ history: list[dict] = json.loads(path.read_text(encoding="utf-8")) if path.exists() else []
374
+ except Exception as e:
375
+ _log.warning("log_health_score failed: %s", e)
376
+ history = []
377
+
378
+ history.append(
379
+ {
380
+ "ts": int(time.time()),
381
+ "health_score": score,
382
+ "finding_count": finding_count,
383
+ "type": "scan",
384
+ }
385
+ )
386
+ # Keep last 500 entries
387
+ history = history[-500:]
388
+ # Atomic write: tmp + rename
389
+ tmp = path.with_suffix(".tmp")
390
+ tmp.write_text(json.dumps(history, indent=2), encoding="utf-8")
391
+ atomic_replace(tmp, path)
392
+
393
+
394
+ def get_health_history(root: "Path | None" = None) -> list[dict]:
395
+ """Return all health score history entries."""
396
+ r = root or require_project_root()
397
+ path = r / _PATCHI_DIR / "history.json"
398
+ try:
399
+ return json.loads(path.read_text(encoding="utf-8")) if path.exists() else []
400
+ except Exception as e:
401
+ _log.warning("get_health_history failed: %s", e)
402
+ return []
403
+
404
+
405
+ # ── Rejection learning ─────────────────────────────────────────────────────────
406
+
407
+
408
+ def record_rejection(finding_type: str, root: "Path | None" = None) -> int:
409
+ """
410
+ Track how many times the user has rejected a finding type.
411
+ Returns the new rejection count for that type.
412
+ """
413
+ r = root or require_project_root()
414
+ path = r / _PATCHI_DIR / "rejection_counts.json"
415
+ try:
416
+ counts: dict = json.loads(path.read_text(encoding="utf-8")) if path.exists() else {}
417
+ except Exception as e:
418
+ _log.warning("record_rejection failed: %s", e)
419
+ counts = {}
420
+
421
+ counts[finding_type] = counts.get(finding_type, 0) + 1
422
+ # Atomic write: tmp + rename
423
+ tmp = path.with_suffix(".tmp")
424
+ tmp.write_text(json.dumps(counts, indent=2), encoding="utf-8")
425
+ atomic_replace(tmp, path)
426
+ return counts[finding_type]
427
+
428
+
429
+ def get_rejection_counts(root: "Path | None" = None) -> dict[str, int]:
430
+ """Return all rejection counts by finding type."""
431
+ r = root or require_project_root()
432
+ path = r / _PATCHI_DIR / "rejection_counts.json"
433
+ try:
434
+ return json.loads(path.read_text(encoding="utf-8")) if path.exists() else {}
435
+ except Exception as e:
436
+ _log.warning("get_rejection_counts failed: %s", e)
437
+ return {}
@@ -0,0 +1,18 @@
1
+ """Patchi notification system — public API."""
2
+
3
+ from patchi.core.notifications.channels import ChannelConfig, ChannelType, load_channels
4
+ from patchi.core.notifications.digest import DigestQueue
5
+ from patchi.core.notifications.escalation import EscalationTracker
6
+ from patchi.core.notifications.notifier import Notifier
7
+ from patchi.core.notifications.quiet_hours import is_quiet_now, should_suppress
8
+
9
+ __all__ = [
10
+ "Notifier",
11
+ "ChannelConfig",
12
+ "ChannelType",
13
+ "load_channels",
14
+ "DigestQueue",
15
+ "EscalationTracker",
16
+ "should_suppress",
17
+ "is_quiet_now",
18
+ ]