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,236 @@
1
+ """StateFuzzer — explore application state machine transitions.
2
+
3
+ Given an AssuranceGraph with states and transitions, generates test
4
+ sequences that attempt illegal state transitions, replay attacks,
5
+ and concurrent state mutations.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+
12
+ from patchi.core.assurance.graph import AssuranceGraph
13
+
14
+
15
+ @dataclass
16
+ class StateTransition:
17
+ """A single state transition."""
18
+
19
+ from_state: str
20
+ to_state: str
21
+ action: str # trigger/action name
22
+
23
+ def to_dict(self) -> dict:
24
+ return {
25
+ "from": self.from_state,
26
+ "to": self.to_state,
27
+ "action": self.action,
28
+ }
29
+
30
+
31
+ @dataclass
32
+ class FuzzPath:
33
+ """A path through the state machine for testing."""
34
+
35
+ label: str
36
+ transitions: list[StateTransition]
37
+ strategy: str # "illegal", "replay", "boundary", "concurrent"
38
+ description: str = ""
39
+
40
+ def to_dict(self) -> dict:
41
+ return {
42
+ "label": self.label,
43
+ "transitions": [t.to_dict() for t in self.transitions],
44
+ "strategy": self.strategy,
45
+ "description": self.description,
46
+ }
47
+
48
+
49
+ class StateFuzzer:
50
+ """Generate state-machine fuzz paths from an AssuranceGraph."""
51
+
52
+ def __init__(self, graph: AssuranceGraph | None = None):
53
+ self._graph = graph
54
+ self._transitions: list[StateTransition] = []
55
+ self._states: set[str] = set()
56
+ self._legal_map: dict[str, list[str]] = {}
57
+
58
+ def load_graph(self, graph: AssuranceGraph) -> None:
59
+ """Load transitions from an assurance graph's data."""
60
+ self._graph = graph
61
+ self._extract_transitions()
62
+
63
+ def _extract_transitions(self) -> None:
64
+ """Extract state transitions from the graph."""
65
+ if self._graph is None:
66
+ return
67
+
68
+ self._transitions = []
69
+ self._states = set()
70
+ self._legal_map = {}
71
+
72
+ for claim in self._graph.claims.values():
73
+ # Extract states and transitions from claim evidence
74
+ for ev in claim.evidence:
75
+ artifact = ev.artifact
76
+ if "from_state" in artifact and "to_state" in artifact:
77
+ t = StateTransition(
78
+ from_state=artifact["from_state"],
79
+ to_state=artifact["to_state"],
80
+ action=artifact.get("action", "unknown"),
81
+ )
82
+ self._transitions.append(t)
83
+ self._states.add(t.from_state)
84
+ self._states.add(t.to_state)
85
+ self._legal_map.setdefault(t.from_state, []).append(t.to_state)
86
+
87
+ def load_from_dict(self, data: dict) -> None:
88
+ """Load transitions from a dict (e.g. from brain data)."""
89
+ self._transitions = []
90
+ self._states = set()
91
+ self._legal_map = {}
92
+
93
+ for t_data in data.get("transitions", []):
94
+ t = StateTransition(
95
+ from_state=t_data["from_state"],
96
+ to_state=t_data["to_state"],
97
+ action=t_data.get("action", "unknown"),
98
+ )
99
+ self._transitions.append(t)
100
+ self._states.add(t.from_state)
101
+ self._states.add(t.to_state)
102
+ self._legal_map.setdefault(t.from_state, []).append(t.to_state)
103
+
104
+ def generate(self, max_paths: int = 30) -> list[FuzzPath]:
105
+ """Generate fuzz paths through the state machine."""
106
+ results: list[FuzzPath] = []
107
+
108
+ if not self._states:
109
+ return results
110
+
111
+ # 1. Illegal transitions (try going to every state from every state)
112
+ results.extend(self._illegal_transitions())
113
+
114
+ # 2. Replay attack (repeat same transition)
115
+ results.extend(self._replay_attacks())
116
+
117
+ # 3. Boundary paths (start→end shortest paths, then extend)
118
+ results.extend(self._boundary_paths())
119
+
120
+ # 4. Missing guards (states with many outgoing transitions)
121
+ results.extend(self._unguarded_transitions())
122
+
123
+ return results[:max_paths]
124
+
125
+ def _illegal_transitions(self) -> list[FuzzPath]:
126
+ """Try transitions that should not be legal."""
127
+ results: list[FuzzPath] = []
128
+ for from_s in self._states:
129
+ legal_targets = set(self._legal_map.get(from_s, []))
130
+ for to_s in self._states:
131
+ if to_s != from_s and to_s not in legal_targets:
132
+ results.append(
133
+ FuzzPath(
134
+ label=f"illegal_{from_s}_to_{to_s}",
135
+ transitions=[StateTransition(from_s, to_s, "test_illegal")],
136
+ strategy="illegal",
137
+ description=f"Attempt illegal transition: {from_s} → {to_s}",
138
+ )
139
+ )
140
+ return results
141
+
142
+ def _replay_attacks(self) -> list[FuzzPath]:
143
+ """Replay the same transition multiple times."""
144
+ results: list[FuzzPath] = []
145
+ for t in self._transitions:
146
+ results.append(
147
+ FuzzPath(
148
+ label=f"replay_{t.from_state}_{t.to_state}_x3",
149
+ transitions=[t, t, t],
150
+ strategy="replay",
151
+ description=f"Replay {t.action} ({t.from_state}→{t.to_state}) 3 times",
152
+ )
153
+ )
154
+ return results
155
+
156
+ def _boundary_paths(self) -> list[FuzzPath]:
157
+ """Find longest paths through the state machine."""
158
+ results: list[FuzzPath] = []
159
+ if not self._transitions:
160
+ return results
161
+
162
+ # Find start states (no incoming transitions)
163
+ all_targets = {t.to_state for t in self._transitions}
164
+ start_states = self._states - all_targets
165
+ if not start_states:
166
+ start_states = {self._transitions[0].from_state}
167
+
168
+ # DFS to find paths
169
+ for start in start_states:
170
+ paths = self._dfs_paths(start, max_depth=6, max_results=5)
171
+ for path in paths:
172
+ results.append(
173
+ FuzzPath(
174
+ label=f"boundary_path_{'_'.join(t.from_state for t in path)}",
175
+ transitions=path,
176
+ strategy="boundary",
177
+ description=f"Path: {' → '.join(t.from_state for t in path)} → {path[-1].to_state}",
178
+ )
179
+ )
180
+ return results
181
+
182
+ def _dfs_paths(
183
+ self, start: str, max_depth: int = 6, max_results: int = 5
184
+ ) -> list[list[StateTransition]]:
185
+ """DFS to find paths through the state machine."""
186
+ results: list[list[StateTransition]] = []
187
+ stack: list[tuple[str, list[StateTransition], set[str]]] = [(start, [], {start})]
188
+
189
+ while stack and len(results) < max_results:
190
+ state, path, visited = stack.pop()
191
+ targets = self._legal_map.get(state, [])
192
+ for target in targets:
193
+ if target in visited:
194
+ continue
195
+ transition = next(
196
+ (
197
+ t
198
+ for t in self._transitions
199
+ if t.from_state == state and t.to_state == target
200
+ ),
201
+ StateTransition(state, target, "unknown"),
202
+ )
203
+ new_path = path + [transition]
204
+ if len(new_path) >= max_depth:
205
+ results.append(new_path)
206
+ else:
207
+ stack.append((target, new_path, visited | {target}))
208
+
209
+ return results
210
+
211
+ def _unguarded_transitions(self) -> list[FuzzPath]:
212
+ """Find states with many outgoing transitions (potential missing guards)."""
213
+ results: list[FuzzPath] = []
214
+ for state, targets in self._legal_map.items():
215
+ if len(targets) >= 3:
216
+ # Try all outgoing transitions in sequence
217
+ transitions = [
218
+ next(
219
+ (
220
+ t
221
+ for t in self._transitions
222
+ if t.from_state == state and t.to_state == tgt
223
+ ),
224
+ StateTransition(state, tgt, "unknown"),
225
+ )
226
+ for tgt in targets
227
+ ]
228
+ results.append(
229
+ FuzzPath(
230
+ label=f"unguarded_{state}",
231
+ transitions=transitions,
232
+ strategy="boundary",
233
+ description=f"State '{state}' has {len(targets)} outgoing transitions — test all",
234
+ )
235
+ )
236
+ return results
patchi/core/health.py ADDED
@@ -0,0 +1,407 @@
1
+ """
2
+ Patchi Health Score — single 0–100 number computed from scan data.
3
+
4
+ Formula (Section 13 of spec):
5
+ Security posture 35% — findings severity weighted, secret count, vuln deps
6
+ Test coverage 25% — % of source files with a test counterpart
7
+ Dead code ratio 20% — % of files that are confirmed dead
8
+ Dependency health 10% — % of deps with no known CVE
9
+ App contract 10% — % of contract flows confirmed (0 if no contract yet)
10
+
11
+ Score interpretation:
12
+ 90–100 Excellent (green)
13
+ 70–89 Good (yellow-green)
14
+ 50–69 Fair (yellow)
15
+ 30–49 Poor (orange)
16
+ 0–29 Critical (red)
17
+
18
+ The score is computed from brain memory + agent scan results.
19
+ It is saved back to brain memory after every scan so `p status` can display it.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import logging
25
+ import threading
26
+ import time
27
+ from dataclasses import dataclass
28
+ from pathlib import Path
29
+
30
+ from patchi.core import memory as mem
31
+
32
+ _log = logging.getLogger("patchi.core.health")
33
+
34
+ # In-process cache: recompute at most every N seconds so dashboard requests
35
+ # stay fast. Scan completion bumps the timestamp so fresh data shows quickly.
36
+ _HEALTH_TTL = 30.0
37
+ _health_cache: dict = {}
38
+ _health_lock = threading.Lock()
39
+
40
+
41
+ def invalidate_cache() -> None:
42
+ """Force next compute() to re-run (call after a scan completes)."""
43
+ with _health_lock:
44
+ _health_cache.clear()
45
+
46
+
47
+ @dataclass
48
+ class HealthScore:
49
+ total: int # 0–100
50
+ security: float # component raw score 0–100
51
+ test_coverage: float
52
+ dead_code: float
53
+ dependency: float
54
+ contract: float
55
+ grade: str # A / B / C / D / F
56
+ color: str # rich color string
57
+ breakdown: dict # per-component detail
58
+
59
+ def to_dict(self) -> dict:
60
+ return {
61
+ "total": self.total,
62
+ "grade": self.grade,
63
+ "color": self.color,
64
+ "components": {
65
+ "security": round(self.security, 1),
66
+ "test_coverage": round(self.test_coverage, 1),
67
+ "dead_code": round(self.dead_code, 1),
68
+ "dependency": round(self.dependency, 1),
69
+ "contract": round(self.contract, 1),
70
+ },
71
+ "breakdown": self.breakdown,
72
+ }
73
+
74
+
75
+ # ── Weights ────────────────────────────────────────────────────────────────────
76
+
77
+ _WEIGHTS = {
78
+ "security": 0.35,
79
+ "test_coverage": 0.25,
80
+ "dead_code": 0.20,
81
+ "dependency": 0.10,
82
+ "contract": 0.10,
83
+ }
84
+
85
+
86
+ # ── Main compute function ──────────────────────────────────────────────────────
87
+
88
+
89
+ def compute(root: Path | None = None) -> HealthScore:
90
+ """
91
+ Compute the health score from brain memory and agent scan results.
92
+ Returns a HealthScore with all components filled.
93
+ Results are cached in-process for _HEALTH_TTL seconds — the dashboard
94
+ calls this on every request, and a full recompute is expensive.
95
+ """
96
+ key = str(root) if root is not None else ""
97
+ now = time.time()
98
+ with _health_lock:
99
+ cached = _health_cache.get(key)
100
+ if cached and (now - cached[0]) < _HEALTH_TTL:
101
+ return cached[1]
102
+
103
+ brain = mem.get_brain(root)
104
+ scans = mem.get_scan_results(root)
105
+ patches = mem.list_patches(root)
106
+
107
+ security = _compute_security(brain, scans)
108
+ test_coverage_pct = _compute_test_coverage_pct(root, brain)
109
+ test_coverage = _compute_test_coverage(brain, scans, test_coverage_pct)
110
+ dead_code = _compute_dead_code(brain, scans)
111
+ dependency = _compute_dependency(brain, scans)
112
+ contract = _compute_contract(brain)
113
+
114
+ raw = (
115
+ security * _WEIGHTS["security"]
116
+ + test_coverage * _WEIGHTS["test_coverage"]
117
+ + dead_code * _WEIGHTS["dead_code"]
118
+ + dependency * _WEIGHTS["dependency"]
119
+ + contract * _WEIGHTS["contract"]
120
+ )
121
+
122
+ total = max(0, min(100, round(raw)))
123
+ grade, color = _grade(total)
124
+
125
+ score = HealthScore(
126
+ total=total,
127
+ security=security,
128
+ test_coverage=test_coverage,
129
+ dead_code=dead_code,
130
+ dependency=dependency,
131
+ contract=contract,
132
+ grade=grade,
133
+ color=color,
134
+ breakdown=_build_breakdown(brain, scans, patches, test_coverage_pct),
135
+ )
136
+
137
+ # Save to brain memory so `p status` can display it without rescanning
138
+ try:
139
+ brain["health_score"] = score.to_dict()
140
+ brain["test_coverage_pct"] = test_coverage_pct
141
+ mem.save_brain(brain, root)
142
+ except Exception as e:
143
+ _log.warning("Failed to save health score to brain: %s", e)
144
+
145
+ # Persist health score to scan history for trend charts (WIRE-07)
146
+ if root is not None:
147
+ try:
148
+ from patchi.core.security.history import patchi_record_scan
149
+
150
+ patchi_record_scan(root, tool="health_compute", findings=[], health_score=total)
151
+ except Exception as e:
152
+ _log.warning("Failed to record health score to history: %s", e)
153
+
154
+ with _health_lock:
155
+ _health_cache[key] = (time.time(), score)
156
+
157
+ return score
158
+
159
+
160
+ # ── Component computers ────────────────────────────────────────────────────────
161
+
162
+
163
+ def _compute_security(brain: dict, scans: dict) -> float:
164
+ """
165
+ Security score (0-100).
166
+ Deductions:
167
+ CRITICAL finding -20 (capped at -60 total)
168
+ HIGH finding -10 (capped at -30 total)
169
+ MEDIUM finding -3
170
+ Hardcoded secret -25 (per secret, heavy)
171
+ Unprotected route -8 (per route)
172
+ Injection vuln -15 (per finding, capped at -30)
173
+ Crypto issue -5 (per finding, capped at -15)
174
+ AuthZ issue -10 (per finding, capped at -20)
175
+ """
176
+ score = 100.0
177
+ env_data = scans.get("EnvScanner", {})
178
+ route_data = scans.get("RouteGraphScanner", {})
179
+
180
+ secret_count = env_data.get("finding_count", 0)
181
+ score -= min(secret_count * 25, 60)
182
+
183
+ unprotected = route_data.get("finding_count", 0)
184
+ score -= min(unprotected * 8, 32)
185
+
186
+ circular = len(brain.get("circular_deps", []))
187
+ score -= min(circular * 2, 10)
188
+
189
+ security_agents = [
190
+ "TaintAnalyzer",
191
+ "SecretScanner",
192
+ "ConfigAuditAgent",
193
+ "HeaderAuditAgent",
194
+ "RateLimitAuditor",
195
+ "CORSAuditor",
196
+ "DependencyCVEChecker",
197
+ "MisconfigAgent",
198
+ "JWTSecurityAgent",
199
+ "SensitiveDataAgent",
200
+ "AuthenticationAuditAgent",
201
+ "SSRFProtectionAgent",
202
+ "InjectionAgent",
203
+ "AuthZAgent",
204
+ "CryptoAgent",
205
+ "NetworkAgent",
206
+ "PrivacyAgent",
207
+ "DependencyVulnerabilityAgent",
208
+ "ComplianceAgent",
209
+ ]
210
+
211
+ crit_deductions = 0
212
+ high_deductions = 0
213
+ med_deductions = 0
214
+
215
+ for agent_name in security_agents:
216
+ agent_data = scans.get(agent_name, {})
217
+ for f in agent_data.get("findings", []):
218
+ sev = f.get("severity", "info")
219
+ if sev == "critical":
220
+ crit_deductions += 20
221
+ elif sev == "high":
222
+ high_deductions += 10
223
+ elif sev == "medium":
224
+ med_deductions += 3
225
+
226
+ score -= min(crit_deductions, 60)
227
+ score -= min(high_deductions, 30)
228
+ score -= med_deductions
229
+
230
+ return max(0.0, score)
231
+
232
+
233
+ def _compute_test_coverage(brain: dict, scans: dict, real_pct: float = 0.0) -> float:
234
+ """Test coverage score from TestScanner coverage_pct, with real % as fallback."""
235
+ test_data = scans.get("TestScanner", {})
236
+ pct = test_data.get("coverage_pct")
237
+ if pct is not None:
238
+ return float(pct)
239
+ if real_pct > 0:
240
+ return real_pct
241
+ return 50.0
242
+
243
+
244
+ def _compute_dead_code(brain: dict, scans: dict) -> float:
245
+ """
246
+ Dead code score (0-100).
247
+ 100 = no dead code. Deduction per dead file.
248
+ """
249
+ dead_data = scans.get("DeadCodeScanner", {})
250
+ file_count = brain.get("file_count", 1) or 1
251
+
252
+ confirmed = len(dead_data.get("confirmed_dead", []))
253
+ broken = len(dead_data.get("broken_import", []))
254
+ total_dead = confirmed + (broken * 0.5)
255
+
256
+ dead_ratio = min(total_dead / file_count, 1.0)
257
+ score = 100 - (dead_ratio * 100)
258
+ return max(0.0, min(100.0, score))
259
+
260
+
261
+ def _compute_dependency(brain: dict, scans: dict) -> float:
262
+ """
263
+ Dependency health: 100 = all clean. Score drops with ratio of vulnerable deps.
264
+ """
265
+ dep_data = scans.get("DependencyScanner", {})
266
+ total = dep_data.get("total_deps", 0)
267
+ vulnerable = len(dep_data.get("vulnerable", []))
268
+
269
+ if total == 0:
270
+ return 80.0
271
+
272
+ ratio = vulnerable / total
273
+ score = 100 - (ratio * 150)
274
+ return max(0.0, min(100.0, score))
275
+
276
+
277
+ def _compute_contract(brain: dict) -> float:
278
+ """
279
+ Contract score: 100 = all flows confirmed. 0 = no contract established yet.
280
+ Partial: (confirmed / inferred) * 100
281
+ """
282
+ inferred = brain.get("inferred_flows", [])
283
+ confirmed = brain.get("confirmed_flows", [])
284
+
285
+ if not inferred:
286
+ return 50.0
287
+
288
+ if not confirmed:
289
+ return 20.0
290
+
291
+ ratio = len(confirmed) / len(inferred)
292
+ return min(100.0, ratio * 100)
293
+
294
+
295
+ # ── Test coverage percentage ─────────────────────────────────────────────────
296
+
297
+
298
+ def _compute_test_coverage_pct(root: Path | None, brain: dict) -> float:
299
+ """Count source files with test counterparts and return percentage.
300
+
301
+ Same semantics as the original implementation (a source file is covered
302
+ only by a test co-located in its own directory or a sibling tests/
303
+ directory) but implemented as set lookups instead of per-file stat
304
+ probes — that probe storm made this the dashboard's #1 bottleneck (~3.8s).
305
+ """
306
+ if root is None:
307
+ return 0.0
308
+
309
+ file_count = brain.get("file_count", 0)
310
+ if file_count == 0:
311
+ return 0.0
312
+
313
+ source_extensions = {".py", ".js", ".ts", ".jsx", ".tsx", ".rb", ".go", ".rs", ".java"}
314
+ test_patterns = ("test_", "_test.", ".test.", ".spec.", "_spec.")
315
+ test_dirs = ("tests", "test", "__tests__", "spec")
316
+
317
+ try:
318
+ from patchi.core.agents.base import safe_rglob
319
+
320
+ # Pass 1 — walk the tree once, collecting existing test names and
321
+ # all source files. No per-file stat() calls.
322
+ src_by_dir: dict = {}
323
+ test_names_by_dir: dict = {}
324
+ for ext in source_extensions:
325
+ for f in safe_rglob(root, f"*{ext}"):
326
+ rel = f.relative_to(root).as_posix()
327
+ parts = Path(rel).parts
328
+ fname = f.name.lower()
329
+
330
+ in_test_dir = any(p.lower() in test_dirs for p in parts)
331
+ is_test_file = any(pat in fname for pat in test_patterns)
332
+ dir_key = str(f.parent)
333
+
334
+ if in_test_dir or is_test_file:
335
+ test_names_by_dir.setdefault(dir_key, set()).add(fname)
336
+ else:
337
+ src_by_dir.setdefault(dir_key, []).append(f)
338
+
339
+ source_files = 0
340
+ files_with_tests = 0
341
+
342
+ for dir_key, files in src_by_dir.items():
343
+ own_tests = test_names_by_dir.get(dir_key, set())
344
+ parent = Path(dir_key)
345
+ sibling_tests = set()
346
+ for td in test_dirs:
347
+ sibling_tests |= test_names_by_dir.get(str(parent / td), set())
348
+
349
+ for f in files:
350
+ source_files += 1
351
+ stem = f.stem
352
+ candidates = {
353
+ f"test_{stem}{ext}".lower()
354
+ for ext in source_extensions
355
+ } | {
356
+ f"{stem}_test{ext}".lower()
357
+ for ext in source_extensions
358
+ } | {
359
+ f"{stem}.test{ext}".lower()
360
+ for ext in source_extensions
361
+ } | {
362
+ f"{stem}.spec{ext}".lower()
363
+ for ext in source_extensions
364
+ }
365
+ if candidates & own_tests or candidates & sibling_tests:
366
+ files_with_tests += 1
367
+
368
+ if source_files == 0:
369
+ return 0.0
370
+
371
+ return round((files_with_tests / source_files) * 100, 1)
372
+ except Exception as e:
373
+ _log.warning("_compute_test_coverage_pct failed: %s", e)
374
+ return 0.0
375
+
376
+
377
+ # ── Grade + color ──────────────────────────────────────────────────────────────
378
+
379
+
380
+ def _grade(score: int) -> tuple[str, str]:
381
+ if score >= 90:
382
+ return "A", "#4ADE80"
383
+ if score >= 70:
384
+ return "B", "#86EFAC"
385
+ if score >= 50:
386
+ return "C", "#FACC15"
387
+ if score >= 30:
388
+ return "D", "#FB923C"
389
+ return "F", "#FF4D6D"
390
+
391
+
392
+ # ── Breakdown detail ───────────────────────────────────────────────────────────
393
+
394
+
395
+ def _build_breakdown(
396
+ brain: dict, scans: dict, patches: list, test_coverage_pct: float = 0.0
397
+ ) -> dict:
398
+ return {
399
+ "file_count": brain.get("file_count", 0),
400
+ "route_count": brain.get("route_count", 0),
401
+ "framework": brain.get("framework", "Unknown"),
402
+ "circular_deps": len(brain.get("circular_deps", [])),
403
+ "patches_applied": len(patches),
404
+ "last_scan": brain.get("last_scan", ""),
405
+ "agents_run": list(scans.keys()),
406
+ "test_coverage_pct": test_coverage_pct,
407
+ }
@@ -0,0 +1 @@
1
+ """Patchi hosted mode — live application monitoring and guard."""