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,33 @@
1
+ """
2
+ Assurance package — provable properties over the scanned codebase.
3
+
4
+ The question this package exists to answer (UNIFIED_UPGRADE_PLAN.md):
5
+
6
+ "What should be impossible, and can I prove it isn't?"
7
+
8
+ Structure:
9
+ graph.py — AssuranceGraph: claims, evidence, verdicts, persistence
10
+ invariants.py — invariant types + static verification engine
11
+ """
12
+
13
+ from patchi.core.assurance.graph import (
14
+ AssuranceGraph,
15
+ Claim,
16
+ Evidence,
17
+ Verdict,
18
+ )
19
+ from patchi.core.assurance.invariants import (
20
+ Invariant,
21
+ InvariantType,
22
+ verify_invariants,
23
+ )
24
+
25
+ __all__ = [
26
+ "AssuranceGraph",
27
+ "Claim",
28
+ "Evidence",
29
+ "Verdict",
30
+ "Invariant",
31
+ "InvariantType",
32
+ "verify_invariants",
33
+ ]
@@ -0,0 +1,211 @@
1
+ """Build an AssuranceGraph from brain scan data.
2
+
3
+ Called after every ``p scan`` to populate the graph with claims derived from
4
+ detected routes, import graph, auth patterns, and domain knowledge.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from .graph import AssuranceGraph, Evidence
14
+
15
+ _log = logging.getLogger("patchi.core.assurance.builder")
16
+
17
+ ASSURANCE_FILE = ".patchi/assurance_graph.json"
18
+
19
+
20
+ def build_assurance_graph(
21
+ root: Path,
22
+ brain_data: dict[str, Any] | None = None,
23
+ route_map: list[dict] | None = None,
24
+ import_graph_data: dict | None = None,
25
+ ) -> AssuranceGraph:
26
+ """Build an AssuranceGraph from brain scan data.
27
+
28
+ Parameters
29
+ ----------
30
+ root : Path
31
+ Project root.
32
+ brain_data : dict
33
+ Brain report summary (from ``report.summary_dict()``).
34
+ route_map : list[dict]
35
+ Detected routes (from ``RouteMapper``).
36
+ import_graph_data : dict
37
+ Import graph as dict (from ``ImportGraph.to_dict()``).
38
+ """
39
+ graph = AssuranceGraph()
40
+ brain = brain_data or {}
41
+ routes = route_map or []
42
+ imp = import_graph_data or {}
43
+
44
+ # ── Components (frameworks detected) ──────────────────────────────────
45
+ frameworks = brain.get("detected_frameworks", [])
46
+ if not frameworks and brain.get("stack"):
47
+ frameworks = [brain["stack"].get("framework", "unknown")]
48
+ for fw in frameworks:
49
+ if fw and fw != "unknown":
50
+ _add_framework_claim(graph, fw)
51
+
52
+ # ── Actors (from auth patterns) ──────────────────────────────────────
53
+ auth_patterns = brain.get("auth_patterns", brain.get("project_context", {}).get("auth", {}))
54
+ actors = _extract_actors(auth_patterns)
55
+ for actor in actors:
56
+ _add_actor_claim(graph, actor)
57
+
58
+ # ── Assets (endpoints from route mapper) ─────────────────────────────
59
+ for route in routes:
60
+ path = route.get("path", route.get("route", ""))
61
+ methods = route.get("methods", route.get("http_methods", ["GET"]))
62
+ auth_required = route.get("auth_required", route.get("requires_auth", False))
63
+ if path:
64
+ _add_endpoint_claim(graph, path, methods, auth_required)
65
+
66
+ # ── Data flows (from import graph) ───────────────────────────────────
67
+ edges = imp.get("edges", [])
68
+ for edge in edges[:100]: # cap for performance
69
+ src = edge.get("source", edge.get("from", ""))
70
+ dst = edge.get("target", edge.get("to", ""))
71
+ if src and dst:
72
+ _add_data_flow_claim(graph, src, dst)
73
+
74
+ # ── Dependency health ────────────────────────────────────────────────
75
+ vulns = brain.get("dependency_vulnerabilities", [])
76
+ for v in vulns[:20]:
77
+ pkg = v.get("package", v.get("name", ""))
78
+ cve = v.get("cve", v.get("id", ""))
79
+ if pkg:
80
+ _add_dependency_claim(graph, pkg, cve)
81
+
82
+ # ── State machine (from session/auth analysis) ───────────────────────
83
+ _add_state_claims(graph, auth_patterns)
84
+
85
+ return graph
86
+
87
+
88
+ def _add_framework_claim(graph: AssuranceGraph, framework: str) -> None:
89
+ graph.upsert_claim(
90
+ f"framework-{framework}",
91
+ f"Project uses {framework}",
92
+ domain="architecture",
93
+ severity_if_disproved="info",
94
+ )
95
+ graph.attach_evidence(
96
+ f"framework-{framework}",
97
+ Evidence(source="brain_scan", detail=f"Detected framework: {framework}", supports=True),
98
+ )
99
+
100
+
101
+ def _add_actor_claim(graph: AssuranceGraph, actor: str) -> None:
102
+ graph.upsert_claim(
103
+ f"actor-{actor}",
104
+ f"System has actor role: {actor}",
105
+ domain="auth",
106
+ severity_if_disproved="medium",
107
+ )
108
+ graph.attach_evidence(
109
+ f"actor-{actor}",
110
+ Evidence(source="brain_scan", detail=f"Actor detected: {actor}", supports=True),
111
+ )
112
+
113
+
114
+ def _add_endpoint_claim(
115
+ graph: AssuranceGraph, path: str, methods: list, auth_required: bool
116
+ ) -> None:
117
+ claim_id = f"endpoint-{path.replace('/', '_').strip('_')}"
118
+ graph.upsert_claim(
119
+ claim_id,
120
+ f"Endpoint {path} requires authentication",
121
+ domain="auth-endpoint",
122
+ severity_if_disproved="high" if auth_required else "medium",
123
+ )
124
+ # If we know auth is required, that's evidence supporting the claim
125
+ # that it *should* require auth (we're asserting it as a property)
126
+ graph.attach_evidence(
127
+ claim_id,
128
+ Evidence(
129
+ source="route_mapper",
130
+ detail=f"Route {path} ({', '.join(methods)}) auth_required={auth_required}",
131
+ supports=True,
132
+ artifact={"path": path, "methods": methods, "auth_required": auth_required},
133
+ ),
134
+ )
135
+
136
+
137
+ def _add_data_flow_claim(graph: AssuranceGraph, source: str, sink: str) -> None:
138
+ claim_id = f"flow-{source.replace('.', '_')}-{sink.replace('.', '_')}"
139
+ graph.upsert_claim(
140
+ claim_id,
141
+ f"Data flow from {source} to {sink} is properly secured",
142
+ domain="data-flow",
143
+ severity_if_disproved="medium",
144
+ )
145
+ graph.attach_evidence(
146
+ claim_id,
147
+ Evidence(
148
+ source="import_graph",
149
+ detail=f"Import edge: {source} → {sink}",
150
+ supports=True,
151
+ artifact={"source": source, "sink": sink},
152
+ ),
153
+ )
154
+
155
+
156
+ def _add_dependency_claim(graph: AssuranceGraph, pkg: str, cve: str) -> None:
157
+ claim_id = f"dep-{pkg}"
158
+ graph.upsert_claim(
159
+ claim_id,
160
+ f"Dependency {pkg} has no known vulnerabilities",
161
+ domain="dependency",
162
+ severity_if_disproved="high",
163
+ )
164
+ if cve:
165
+ # CVE found = refutes the claim (it IS vulnerable)
166
+ graph.attach_evidence(
167
+ claim_id,
168
+ Evidence(
169
+ source="dependency_scan",
170
+ detail=f"Vulnerability {cve} found in {pkg}",
171
+ supports=False,
172
+ artifact={"package": pkg, "cve": cve},
173
+ ),
174
+ )
175
+
176
+
177
+ def _extract_actors(auth_patterns: Any) -> list[str]:
178
+ """Extract actor roles from auth pattern data."""
179
+ actors = ["anonymous", "authenticated"]
180
+ if isinstance(auth_patterns, dict):
181
+ if auth_patterns.get("has_admin"):
182
+ actors.append("admin")
183
+ if auth_patterns.get("has_roles"):
184
+ actors.extend(["moderator", "service"])
185
+ if auth_patterns.get("has_api_key"):
186
+ actors.append("api_client")
187
+ return actors
188
+
189
+
190
+ def _add_state_claims(graph: AssuranceGraph, auth_patterns: Any) -> None:
191
+ """Add state machine claims from auth analysis."""
192
+ states = ["anonymous", "authenticated"]
193
+ if isinstance(auth_patterns, dict) and auth_patterns.get("has_admin"):
194
+ states.append("admin")
195
+
196
+ for state in states:
197
+ claim_id = f"state-{state}"
198
+ graph.upsert_claim(
199
+ claim_id,
200
+ f"State '{state}' transitions are properly guarded",
201
+ domain="state-machine",
202
+ severity_if_disproved="high",
203
+ )
204
+
205
+ # Login transition
206
+ graph.upsert_claim(
207
+ "state-login-transition",
208
+ "Login transition regenerates session ID",
209
+ domain="state-machine",
210
+ severity_if_disproved="critical",
211
+ )
@@ -0,0 +1,342 @@
1
+ """
2
+ AssuranceGraph — a directed graph of claims backed by evidence.
3
+
4
+ Every security/quality property Patchi asserts becomes a Claim:
5
+
6
+ property ("all state-changing routes require auth")
7
+ <- Evidence (route scan: 4 routes, 1 unguarded)
8
+ <- Verdict (proved / not-proved / disproved + timestamp)
9
+
10
+ The graph persists to ``.patchi/assurance.json`` so campaigns accumulate
11
+ across runs instead of evaporating with each scan. Honesty rule (2.1-1):
12
+ a claim without evidence is NEVER reported as proved, and a disproved
13
+ claim keeps its repair history — the system records what it tried, not
14
+ just what it wanted to be true.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ from dataclasses import dataclass, field
21
+ from datetime import UTC, datetime
22
+ from enum import StrEnum
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ _log = logging.getLogger("patchi.core.assurance.graph")
27
+
28
+ ASSURANCE_FILE = ".patchi/assurance.json"
29
+ COVERAGE_HISTORY_FILE = ".patchi/coverage_history.json"
30
+
31
+
32
+ class Verdict(StrEnum):
33
+ """Outcome of verifying a claim. No evidence => UNPROVEN, never 'proved'."""
34
+
35
+ PROVED = "proved"
36
+ NOT_PROVED = "not_proved" # couldn't establish (insufficient evidence)
37
+ DISPROVED = "disproved" # positive counter-evidence found
38
+ UNPROVEN = "unproven" # never checked
39
+
40
+
41
+ @dataclass
42
+ class Evidence:
43
+ """One piece of supporting/refuting observation for a claim."""
44
+
45
+ source: str # what produced it: "intent_scan", "secret_scan", ...
46
+ detail: str # human-readable observation
47
+ supports: bool # True = supports claim; False = refutes
48
+ timestamp: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
49
+ artifact: dict = field(default_factory=dict) # machine-checkable payload
50
+
51
+ def to_dict(self) -> dict:
52
+ return {
53
+ "source": self.source,
54
+ "detail": self.detail,
55
+ "supports": self.supports,
56
+ "timestamp": self.timestamp,
57
+ "artifact": self.artifact,
58
+ }
59
+
60
+ @classmethod
61
+ def from_dict(cls, d: dict) -> Evidence:
62
+ return cls(
63
+ source=d.get("source", "?"),
64
+ detail=d.get("detail", ""),
65
+ supports=bool(d.get("supports", False)),
66
+ timestamp=d.get("timestamp", ""),
67
+ artifact=d.get("artifact", {}),
68
+ )
69
+
70
+
71
+ @dataclass
72
+ class Claim:
73
+ """A property asserted about the project, with its evidence trail."""
74
+
75
+ id: str # stable slug, e.g. "auth-all-writes"
76
+ statement: str # human-readable property
77
+ domain: str # owning domain tag, e.g. "auth-session"
78
+ severity_if_disproved: str = "high"
79
+ verdict: Verdict = Verdict.UNPROVEN
80
+ evidence: list[Evidence] = field(default_factory=list)
81
+ repairs: list[dict] = field(default_factory=list) # adversarial-repair chain
82
+ updated_at: str = ""
83
+
84
+ def add_evidence(self, ev: Evidence) -> None:
85
+ self.evidence.append(ev)
86
+ self.updated_at = ev.timestamp
87
+ # Verdict recomputation: ANY refuting evidence wins (honesty-first);
88
+ # otherwise proved only if >=1 supporting evidence exists.
89
+ refuting = [e for e in self.evidence if not e.supports]
90
+ supporting = [e for e in self.evidence if e.supports]
91
+ if refuting:
92
+ self.verdict = Verdict.DISPROVED
93
+ elif supporting:
94
+ self.verdict = Verdict.PROVED
95
+ else:
96
+ self.verdict = Verdict.NOT_PROVED
97
+
98
+ def record_repair(self, description: str, outcome: str) -> None:
99
+ """Record one loop of the adversarial repair chain."""
100
+ self.repairs.append(
101
+ {
102
+ "at": datetime.now(UTC).isoformat(),
103
+ "description": description,
104
+ "outcome": outcome,
105
+ }
106
+ )
107
+ # A repair attempt resets the verdict until re-verified.
108
+ self.verdict = Verdict.NOT_PROVED
109
+
110
+ def to_dict(self) -> dict:
111
+ return {
112
+ "id": self.id,
113
+ "statement": self.statement,
114
+ "domain": self.domain,
115
+ "severity_if_disproved": self.severity_if_disproved,
116
+ "verdict": self.verdict.value,
117
+ "evidence": [e.to_dict() for e in self.evidence],
118
+ "repairs": self.repairs,
119
+ "updated_at": self.updated_at,
120
+ }
121
+
122
+ @classmethod
123
+ def from_dict(cls, d: dict) -> Claim:
124
+ c = cls(
125
+ id=d.get("id", "?"),
126
+ statement=d.get("statement", ""),
127
+ domain=d.get("domain", "general"),
128
+ severity_if_disproved=d.get("severity_if_disproved", "high"),
129
+ verdict=Verdict(d.get("verdict", "unproven")),
130
+ updated_at=d.get("updated_at", ""),
131
+ )
132
+ c.evidence = [Evidence.from_dict(e) for e in d.get("evidence", [])]
133
+ c.repairs = list(d.get("repairs", []))
134
+ return c
135
+
136
+
137
+ @dataclass
138
+ class AssuranceGraph:
139
+ """The persistent collection of claims for one project."""
140
+
141
+ claims: dict[str, Claim] = field(default_factory=dict)
142
+
143
+ def upsert_claim(
144
+ self,
145
+ claim_id: str,
146
+ statement: str,
147
+ domain: str = "general",
148
+ severity_if_disproved: str = "high",
149
+ ) -> Claim:
150
+ """Get-or-create a claim; preserves existing evidence across runs."""
151
+ if claim_id not in self.claims:
152
+ self.claims[claim_id] = Claim(
153
+ id=claim_id,
154
+ statement=statement,
155
+ domain=domain,
156
+ severity_if_disproved=severity_if_disproved,
157
+ )
158
+ return self.claims[claim_id]
159
+
160
+ def attach_evidence(self, claim_id: str, evidence: Evidence) -> Claim:
161
+ claim = self.claims[claim_id] # KeyError = programming error upstream
162
+ claim.add_evidence(evidence)
163
+ return claim
164
+
165
+ # ── Reporting ───────────────────────────────────────────────────────────
166
+
167
+ def coverage(self) -> dict[str, Any]:
168
+ """Assurance status summary. Never says 'secure' — says what's proved."""
169
+ total = len(self.claims)
170
+ by_verdict: dict[str, int] = {}
171
+ by_domain: dict[str, dict[str, int]] = {}
172
+ for c in self.claims.values():
173
+ by_verdict[c.verdict.value] = by_verdict.get(c.verdict.value, 0) + 1
174
+ dom = by_domain.setdefault(c.domain, {"proved": 0, "total": 0})
175
+ dom["total"] += 1
176
+ if c.verdict == Verdict.PROVED:
177
+ dom["proved"] += 1
178
+
179
+ disproved = [c for c in self.claims.values() if c.verdict == Verdict.DISPROVED]
180
+ return {
181
+ "claims_total": total,
182
+ "by_verdict": by_verdict,
183
+ "by_domain": by_domain,
184
+ "disproved_claims": [c.to_dict() for c in disproved],
185
+ "statement": (
186
+ f"{by_verdict.get('proved', 0)}/{total} properties proved; "
187
+ f"{len(disproved)} violated"
188
+ if total
189
+ else "no properties established yet"
190
+ ),
191
+ }
192
+
193
+ # ── Diff ─────────────────────────────────────────────────────────────────
194
+
195
+ def diff(self, other: AssuranceGraph) -> dict[str, Any]:
196
+ """Compute difference between two assurance graphs.
197
+
198
+ Returns a dict with:
199
+ - added: claims in ``other`` but not in ``self``
200
+ - removed: claims in ``self`` but not in ``other``
201
+ - changed: claims present in both but with different verdicts or evidence
202
+ - unchanged: claim IDs present in both with same verdict and evidence count
203
+ """
204
+ self_ids = set(self.claims.keys())
205
+ other_ids = set(other.claims.keys())
206
+
207
+ added_ids = other_ids - self_ids
208
+ removed_ids = self_ids - other_ids
209
+ common_ids = self_ids & other_ids
210
+
211
+ added = {cid: other.claims[cid].to_dict() for cid in added_ids}
212
+ removed = {cid: self.claims[cid].to_dict() for cid in removed_ids}
213
+
214
+ changed = {}
215
+ unchanged = []
216
+ for cid in common_ids:
217
+ c1 = self.claims[cid]
218
+ c2 = other.claims[cid]
219
+ if c1.verdict != c2.verdict or len(c1.evidence) != len(c2.evidence):
220
+ changed[cid] = {
221
+ "self": c1.to_dict(),
222
+ "other": c2.to_dict(),
223
+ }
224
+ else:
225
+ unchanged.append(cid)
226
+
227
+ return {
228
+ "added": added,
229
+ "removed": removed,
230
+ "changed": changed,
231
+ "unchanged": unchanged,
232
+ }
233
+
234
+ # ── Persistence ─────────────────────────────────────────────────────────
235
+
236
+ def save(self, root: Path) -> Path:
237
+ path = root / ASSURANCE_FILE
238
+ path.parent.mkdir(parents=True, exist_ok=True)
239
+ payload = {
240
+ "version": 1,
241
+ "claims": [c.to_dict() for c in self.claims.values()],
242
+ }
243
+ tmp = path.with_suffix(".json.tmp")
244
+ tmp.write_text(json_dumps(payload), encoding="utf-8")
245
+ tmp.replace(path)
246
+ # Record coverage snapshot for trend line
247
+ self._record_coverage_snapshot(root)
248
+ return path
249
+
250
+ @classmethod
251
+ def load(cls, root: Path) -> AssuranceGraph:
252
+ path = root / ASSURANCE_FILE
253
+ g = cls()
254
+ if not path.is_file():
255
+ return g
256
+ try:
257
+ import json
258
+
259
+ data = json.loads(path.read_text(encoding="utf-8"))
260
+ for cd in data.get("claims", []):
261
+ try:
262
+ c = Claim.from_dict(cd)
263
+ g.claims[c.id] = c
264
+ except Exception as e: # noqa: BLE001 — skip corrupt entries
265
+ _log.warning("skipping corrupt assurance claim: %s", e)
266
+ except Exception as e: # noqa: BLE001 — corrupt file = fresh graph
267
+ _log.warning("failed to load assurance graph: %s", e)
268
+ return g
269
+
270
+
271
+ def _record_coverage_snapshot(self, root: Path) -> None:
272
+ """Append a coverage snapshot to the history file for trend analysis."""
273
+ try:
274
+ import json
275
+
276
+ history_path = root / COVERAGE_HISTORY_FILE
277
+ history_path.parent.mkdir(parents=True, exist_ok=True)
278
+
279
+ # Load existing history
280
+ history: list[dict] = []
281
+ if history_path.is_file():
282
+ try:
283
+ history = json.loads(history_path.read_text(encoding="utf-8"))
284
+ except Exception:
285
+ history = []
286
+
287
+ # Compute current snapshot
288
+ cov = self.coverage()
289
+ snapshot = {
290
+ "timestamp": datetime.now(UTC).isoformat(),
291
+ "overall_pct": round(
292
+ cov["by_verdict"].get("proved", 0) / cov["claims_total"] * 100
293
+ if cov["claims_total"]
294
+ else 0
295
+ ),
296
+ "claims_total": cov["claims_total"],
297
+ "proved": cov["by_verdict"].get("proved", 0),
298
+ "disproved": cov["by_verdict"].get("disproved", 0),
299
+ "unproven": cov["by_verdict"].get("unproven", 0),
300
+ "not_proved": cov["by_verdict"].get("not_proved", 0),
301
+ "by_domain": {},
302
+ }
303
+ # Per-domain breakdown
304
+ for dom, counts in cov.get("by_domain", {}).items():
305
+ dom_total = counts.get("total", 0)
306
+ dom_proved = counts.get("proved", 0)
307
+ snapshot["by_domain"][dom] = {
308
+ "proved": dom_proved,
309
+ "total": dom_total,
310
+ "pct": round(dom_proved / dom_total * 100) if dom_total else 0,
311
+ }
312
+
313
+ history.append(snapshot)
314
+
315
+ # Keep last 500 snapshots to avoid unbounded growth
316
+ if len(history) > 500:
317
+ history = history[-500:]
318
+
319
+ tmp = history_path.with_suffix(".json.tmp")
320
+ tmp.write_text(json_dumps(history), encoding="utf-8")
321
+ tmp.replace(history_path)
322
+ except Exception as e: # noqa: BLE001 — best-effort
323
+ _log.debug("coverage snapshot failed: %s", e)
324
+
325
+ @staticmethod
326
+ def load_coverage_history(root: Path) -> list[dict]:
327
+ """Load the coverage history for trend analysis."""
328
+ import json
329
+
330
+ history_path = root / COVERAGE_HISTORY_FILE
331
+ if not history_path.is_file():
332
+ return []
333
+ try:
334
+ return json.loads(history_path.read_text(encoding="utf-8"))
335
+ except Exception:
336
+ return []
337
+
338
+
339
+ def json_dumps(payload: Any) -> str:
340
+ import json
341
+
342
+ return json.dumps(payload, indent=2, default=str)