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,273 @@
1
+ """
2
+ Brain freshness tracker.
3
+
4
+ Determines whether the Brain's knowledge is still current by comparing
5
+ file modification times against the last scan timestamp.
6
+
7
+ Also provides the file watcher used by `p watch`.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import logging
13
+ import threading
14
+ from collections.abc import Callable
15
+ from datetime import UTC, datetime
16
+ from pathlib import Path
17
+
18
+ FRESHNESS_FILE = ".patchi/brain_freshness.json"
19
+
20
+
21
+ # ── Freshness record ───────────────────────────────────────────────────────────
22
+
23
+
24
+ _log = logging.getLogger("patchi.brain.freshness")
25
+
26
+
27
+ def save_freshness_snapshot(root: Path, file_infos_paths: list[str]) -> None:
28
+ """
29
+ Record the current mtime of every scanned file.
30
+ Called at the end of a successful brain scan.
31
+ """
32
+ snapshot: dict[str, float] = {}
33
+ for rel_path in file_infos_paths:
34
+ abs_path = root / rel_path
35
+ try:
36
+ snapshot[rel_path] = abs_path.stat().st_mtime
37
+ except OSError:
38
+ pass
39
+
40
+ freshness_path = root / FRESHNESS_FILE
41
+ freshness_path.parent.mkdir(parents=True, exist_ok=True)
42
+ with freshness_path.open("w", encoding="utf-8") as f:
43
+ json.dump(
44
+ {
45
+ "snapshot": snapshot,
46
+ "recorded_at": datetime.now(UTC).isoformat(),
47
+ },
48
+ f,
49
+ indent=2,
50
+ )
51
+
52
+
53
+ def check_freshness(root: Path) -> dict:
54
+ """
55
+ Compare current file mtimes to the saved snapshot.
56
+
57
+ Returns:
58
+ {
59
+ "is_stale": bool,
60
+ "changed_files": [list of changed rel paths],
61
+ "new_files": [list of new files not in snapshot],
62
+ "deleted_files": [list of files that no longer exist],
63
+ "last_recorded": ISO timestamp or None,
64
+ }
65
+ """
66
+ freshness_path = root / FRESHNESS_FILE
67
+ if not freshness_path.exists():
68
+ return {
69
+ "is_stale": True,
70
+ "changed_files": [],
71
+ "new_files": [],
72
+ "deleted_files": [],
73
+ "last_recorded": None,
74
+ "reason": "No scan recorded yet.",
75
+ }
76
+
77
+ try:
78
+ with freshness_path.open("r", encoding="utf-8") as f:
79
+ data = json.load(f)
80
+ except (json.JSONDecodeError, OSError):
81
+ return {
82
+ "is_stale": True,
83
+ "changed_files": [],
84
+ "new_files": [],
85
+ "deleted_files": [],
86
+ "last_recorded": None,
87
+ "reason": "Freshness file is corrupted.",
88
+ }
89
+
90
+ snapshot: dict[str, float] = data.get("snapshot", {})
91
+ last_recorded: str = data.get("recorded_at", "")
92
+
93
+ changed: list[str] = []
94
+ deleted: list[str] = []
95
+
96
+ for rel_path, old_mtime in snapshot.items():
97
+ abs_path = root / rel_path
98
+ try:
99
+ current_mtime = abs_path.stat().st_mtime
100
+ if current_mtime != old_mtime:
101
+ changed.append(rel_path)
102
+ except OSError:
103
+ deleted.append(rel_path)
104
+
105
+ # New files: scan for any source files not in snapshot
106
+ # (lightweight — just check common source roots)
107
+ new_files = _find_new_files(root, snapshot)
108
+
109
+ is_stale = bool(changed or deleted or new_files)
110
+ reason = ""
111
+ if is_stale:
112
+ parts: list[str] = []
113
+ if changed:
114
+ parts.append(f"{len(changed)} file(s) modified")
115
+ if deleted:
116
+ parts.append(f"{len(deleted)} file(s) deleted")
117
+ if new_files:
118
+ parts.append(f"{len(new_files)} new file(s) found")
119
+ reason = "; ".join(parts) + " since last scan."
120
+
121
+ return {
122
+ "is_stale": is_stale,
123
+ "changed_files": changed,
124
+ "new_files": new_files,
125
+ "deleted_files": deleted,
126
+ "last_recorded": last_recorded,
127
+ "reason": reason,
128
+ }
129
+
130
+
131
+ def _find_new_files(root: Path, snapshot: dict[str, float]) -> list[str]:
132
+ """
133
+ Quick scan for source files that exist now but weren't in the snapshot.
134
+ Checks only source directories, not node_modules etc.
135
+ """
136
+ from patchi.core.brain.languages import DEFAULT_IGNORE_DIRS, Lang, detect_language
137
+
138
+ new_files: list[str] = []
139
+ count = 0
140
+
141
+ for dirpath, dirnames, filenames in root_walk(root):
142
+ # Prune ignored dirs
143
+ dirnames[:] = [d for d in dirnames if d not in DEFAULT_IGNORE_DIRS]
144
+ for fname in filenames:
145
+ fpath = Path(dirpath) / fname
146
+ try:
147
+ rel = fpath.relative_to(root).as_posix()
148
+ except ValueError:
149
+ continue
150
+ if rel not in snapshot:
151
+ lang = detect_language(fpath)
152
+ if lang != Lang.UNKNOWN:
153
+ new_files.append(rel)
154
+ count += 1
155
+ if count > 5000: # bail early on huge projects
156
+ return new_files
157
+
158
+ return new_files
159
+
160
+
161
+ def root_walk(root: Path):
162
+ """os.walk wrapper that stays inside root."""
163
+ import os
164
+
165
+ yield from os.walk(root)
166
+
167
+
168
+ # ── File watcher (for p watch) ─────────────────────────────────────────────────
169
+
170
+
171
+ class BrainWatcher:
172
+ """
173
+ Watches the project directory for file changes using watchfiles (Rust-backed).
174
+
175
+ watchfiles is significantly faster than watchdog — it uses native OS file
176
+ events (inotify on Linux, FSEvents on macOS, ReadDirectoryChangesW on Windows)
177
+ via a Rust extension. Debouncing is built-in.
178
+
179
+ On change: marks brain stale and calls on_change callback with changed paths.
180
+
181
+ Usage:
182
+ watcher = BrainWatcher(root, on_change=lambda paths: ...)
183
+ watcher.start() # blocks until stop() is called
184
+ watcher.stop()
185
+ """
186
+
187
+ def __init__(
188
+ self,
189
+ root: Path,
190
+ on_change: Callable[[list[str]], None],
191
+ area: str | None = None,
192
+ debounce_ms: int = 500,
193
+ ):
194
+ self.root = root
195
+ self.on_change = on_change
196
+ self.area = area
197
+ self.debounce_ms = debounce_ms
198
+ self._stop_event: threading.Event | None = None
199
+ self._thread: threading.Thread | None = None
200
+
201
+ def start(self) -> None:
202
+ """Start watching in a background thread. Returns immediately."""
203
+ import threading
204
+
205
+ self._stop_event = threading.Event()
206
+ self._thread = threading.Thread(
207
+ target=self._watch_loop,
208
+ daemon=True,
209
+ name="BrainWatcher",
210
+ )
211
+ self._thread.start()
212
+
213
+ def _watch_loop(self) -> None:
214
+ """Background thread: runs watchfiles.watch() and dispatches changes."""
215
+ try:
216
+ from watchfiles import Change # noqa: F401 — used to check availability
217
+ from watchfiles import watch as wf_watch
218
+ except ImportError as e:
219
+ raise RuntimeError(
220
+ "watchfiles is required for p watch. Install it with: pip install watchfiles"
221
+ ) from e
222
+
223
+ from patchi.core.brain.languages import DEFAULT_IGNORE_DIRS, Lang, detect_language
224
+
225
+ watch_path = str(self.root / self.area) if self.area else str(self.root)
226
+
227
+ # watchfiles accepts stop_event directly — clean shutdown
228
+ for changes in wf_watch(
229
+ watch_path,
230
+ debounce=self.debounce_ms,
231
+ stop_event=self._stop_event,
232
+ ignore_permission_denied=True,
233
+ ):
234
+ if self._stop_event and self._stop_event.is_set():
235
+ break
236
+
237
+ changed_paths: list[str] = []
238
+ for _change_type, raw_path in changes:
239
+ path = Path(raw_path)
240
+
241
+ # Skip patchi internals and build artifacts
242
+ if any(part in DEFAULT_IGNORE_DIRS for part in path.parts):
243
+ continue
244
+
245
+ lang = detect_language(path)
246
+ if lang == Lang.UNKNOWN:
247
+ continue
248
+
249
+ try:
250
+ rel = path.relative_to(self.root).as_posix()
251
+ changed_paths.append(rel)
252
+ except ValueError:
253
+ continue
254
+
255
+ if changed_paths:
256
+ # Mark brain stale for these files
257
+ try:
258
+ from patchi.core import memory as mem
259
+
260
+ mem.mark_brain_stale(changed_paths, self.root)
261
+ except Exception as e:
262
+ _log.warning("BrainWatcher._watch_loop failed: %s", e)
263
+ self.on_change(changed_paths)
264
+
265
+ def stop(self) -> None:
266
+ """Signal the watcher to stop and wait for the thread to exit."""
267
+ if self._stop_event:
268
+ self._stop_event.set()
269
+ if self._thread and self._thread.is_alive():
270
+ self._thread.join(timeout=3.0)
271
+
272
+ def is_alive(self) -> bool:
273
+ return self._thread is not None and self._thread.is_alive()
@@ -0,0 +1,206 @@
1
+ """
2
+ Git-aware intelligence for Patchi.
3
+
4
+ Provides:
5
+ - Incremental scan: only scan files changed since last scan (via git diff)
6
+ - Blame integration: trace which commit introduced a finding
7
+ - Changelog generation: summarize what changed between scans
8
+
9
+ Uses subprocess to call git — no Python git library required.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import subprocess
16
+ from pathlib import Path
17
+
18
+ from patchi.core.agents.base import Finding
19
+
20
+ _log = logging.getLogger("patchi.brain.git_aware")
21
+
22
+
23
+ def _git(args: list[str], root: Path) -> str | None:
24
+ """Run a git command and return stdout, or None on failure."""
25
+ try:
26
+ result = subprocess.run(
27
+ ["git"] + args,
28
+ cwd=str(root),
29
+ capture_output=True,
30
+ text=True,
31
+ timeout=10,
32
+ )
33
+ if result.returncode == 0:
34
+ return result.stdout.strip()
35
+ return None
36
+ except Exception as e:
37
+ _log.debug("_git failed: %s", e)
38
+ return None
39
+
40
+
41
+ def is_git_repo(root: Path) -> bool:
42
+ """Check if the project is a git repository."""
43
+ return _git(["rev-parse", "--is-inside-work-tree"], root) == "true"
44
+
45
+
46
+ def get_changed_files(root: Path, since: str = "HEAD") -> list[str]:
47
+ """
48
+ Get list of files changed since a ref (commit, tag, branch).
49
+ Default: changes since last commit.
50
+ Returns relative paths.
51
+ """
52
+ output = _git(["diff", "--name-only", since], root)
53
+ if output is None:
54
+ # Try diffing against empty tree (all files)
55
+ output = _git(["diff", "--name-only", "--diff-filter=ACMR", "HEAD~1"], root)
56
+ if output is None:
57
+ return []
58
+
59
+ # Also include untracked files
60
+ untracked = _git(["ls-files", "--others", "--exclude-standard"], root)
61
+
62
+ files = [f for f in output.splitlines() if f.strip()]
63
+ if untracked:
64
+ files.extend(f for f in untracked.splitlines() if f.strip())
65
+
66
+ return list(set(files))
67
+
68
+
69
+ def get_changed_since_scan(root: Path, last_scan_time: float | None = None) -> list[str]:
70
+ """
71
+ Get files changed since the last scan timestamp.
72
+ Falls back to git log if timestamps aren't available.
73
+ """
74
+ if last_scan_time is None:
75
+ # No previous scan — return all files
76
+ return []
77
+
78
+ # Use git log to find commits since last scan
79
+ import time
80
+
81
+ since_date = time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(last_scan_time))
82
+
83
+ output = _git(["log", "--since=" + since_date, "--name-only", "--pretty=format:"], root)
84
+ if output is None:
85
+ return []
86
+
87
+ files = [f for f in output.splitlines() if f.strip()]
88
+ return list(set(files))
89
+
90
+
91
+ def blame_line(file_path: str, line: int, root: Path) -> dict | None:
92
+ """
93
+ Git blame a specific line. Returns:
94
+ {"commit": "abc123", "author": "Name", "date": "2024-01-01", "summary": "msg"}
95
+ """
96
+ output = _git(["blame", "-L", f"{line},{line}", "--porcelain", file_path], root)
97
+ if not output:
98
+ return None
99
+
100
+ result: dict[str, str] = {}
101
+ for line_text in output.splitlines():
102
+ # Porcelain format: first line is "<hash> <orig-line> <final-line> <count>"
103
+ # No "commit " prefix — the hash IS the first token on the first line.
104
+ if not result.get("commit") and line_text and not line_text.startswith("\t"):
105
+ parts = line_text.split()
106
+ if parts and len(parts[0]) == 40 and all(c in "0123456789abcdef" for c in parts[0]):
107
+ result["commit"] = parts[0]
108
+ elif line_text.startswith("author "):
109
+ result["author"] = line_text[7:]
110
+ elif line_text.startswith("author-time "):
111
+ import time
112
+
113
+ ts = int(line_text[12:])
114
+ result["date"] = time.strftime("%Y-%m-%d", time.localtime(ts))
115
+ elif line_text.startswith("summary "):
116
+ result["summary"] = line_text[8:]
117
+
118
+ return result if result.get("commit") else None
119
+
120
+
121
+ def annotate_findings_with_blame(findings: list, root: Path, max_workers: int = 4) -> int:
122
+ """
123
+ Enrich findings with git blame information.
124
+
125
+ For each finding with a file + line, runs `git blame` and stores
126
+ result in finding.extra["blame"] as {"commit", "author", "date", "summary"}.
127
+
128
+ Uses parallel processing for speed. Skips non-git repos silently.
129
+ Returns count of annotated findings.
130
+ """
131
+ if not is_git_repo(root):
132
+ return 0
133
+
134
+ annotated = 0
135
+ import concurrent.futures
136
+
137
+ def _blame_one(f: Finding) -> Finding:
138
+ if not f.file or f.line <= 0:
139
+ return f
140
+ try:
141
+ info = blame_line(f.file, f.line, root)
142
+ if info:
143
+ f.extra["blame"] = info
144
+ except Exception as e:
145
+ _log.debug("_blame_one failed: %s", e)
146
+ return f
147
+
148
+ batch = [f for f in findings if isinstance(f, Finding) and f.file and f.line > 0]
149
+ if not batch:
150
+ return 0
151
+
152
+ # Cap per-agent blame calls to avoid excessive subprocess overhead on Windows
153
+ if len(batch) > 200:
154
+ batch = batch[:200]
155
+
156
+ if len(batch) == 1 or max_workers <= 1:
157
+ for f in batch:
158
+ _blame_one(f)
159
+ if f.extra.get("blame"):
160
+ annotated += 1
161
+ else:
162
+ with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as pool:
163
+ for f in pool.map(_blame_one, batch):
164
+ if f.extra.get("blame"):
165
+ annotated += 1
166
+
167
+ return annotated
168
+
169
+
170
+ def changelog(root: Path, since: str = "HEAD~10") -> list[dict]:
171
+ """
172
+ Generate a changelog from git log.
173
+ Returns list of {"hash", "author", "date", "summary", "files"}.
174
+ """
175
+ output = _git(
176
+ ["log", since, "--pretty=format:%H|%an|%ad|%s", "--date=short"],
177
+ root,
178
+ )
179
+ if not output:
180
+ return []
181
+
182
+ entries = []
183
+ for line in output.splitlines():
184
+ if not line.strip():
185
+ continue
186
+ parts = line.split("|", 3)
187
+ if len(parts) < 4:
188
+ continue
189
+
190
+ commit_hash, author, date, summary = parts
191
+
192
+ # Get files changed in this commit
193
+ files_output = _git(["diff-tree", "--no-commit-id", "-r", "--name-only", commit_hash], root)
194
+ files = [f for f in (files_output or "").splitlines() if f.strip()]
195
+
196
+ entries.append(
197
+ {
198
+ "hash": commit_hash[:8],
199
+ "author": author,
200
+ "date": date,
201
+ "summary": summary,
202
+ "files": files[:20], # Cap at 20 files
203
+ }
204
+ )
205
+
206
+ return entries
@@ -0,0 +1,104 @@
1
+ """
2
+ Risk Heatmap §9.1.3 — file map colored by bug-prone score.
3
+
4
+ Score per file: findings * 10 + churn * 2 + (size_kb / 10) + complexity_hint.
5
+ Color intensity mapped to folder tree for web D3 treemap.
6
+
7
+ Usage:
8
+ from patchi.core.brain.heatmap import build_heatmap
9
+ data = build_heatmap(root) # {name, children, value, severity}
10
+ # CLI: p heatmap | p heatmap --json
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from collections import defaultdict
17
+ from pathlib import Path
18
+
19
+ from patchi.core import memory as mem
20
+
21
+ _log = logging.getLogger("patchi.brain.heatmap")
22
+
23
+
24
+ def _git_churn(root: Path, rel: str) -> int:
25
+ try:
26
+ import subprocess
27
+
28
+ out = subprocess.run(
29
+ ["git", "log", "--oneline", "--", rel],
30
+ capture_output=True,
31
+ text=True,
32
+ timeout=5,
33
+ cwd=str(root),
34
+ )
35
+ if out.returncode == 0:
36
+ return len([line for line in out.stdout.splitlines() if line.strip()])
37
+ except Exception as exc: # noqa: BLE001
38
+ _log.debug("churn %s failed: %s", rel, exc)
39
+ return 0
40
+
41
+
42
+ def build_heatmap(root: Path) -> dict:
43
+ scans = mem.get_scan_results(root) or {}
44
+ # findings per file
45
+ per_file: dict[str, int] = defaultdict(int)
46
+ sev_max: dict[str, str] = {}
47
+ order = {"critical": 4, "high": 3, "medium": 2, "low": 1, "info": 0}
48
+ for data in scans.values():
49
+ for f in data.get("findings", []) if isinstance(data, dict) else []:
50
+ fp = f.get("file", "")
51
+ if not fp:
52
+ continue
53
+ per_file[fp] += 1
54
+ sev = f.get("severity", "low")
55
+ if order.get(sev, 0) > order.get(sev_max.get(fp, "info"), 0):
56
+ sev_max[fp] = sev
57
+
58
+ # file sizes for complexity hint
59
+ from patchi.core.brain.file_corpus import FileCorpus
60
+
61
+ corpus = FileCorpus(root)
62
+ size_map = {e.path: e.size_bytes for e in corpus.files()}
63
+
64
+ # build tree
65
+ tree: dict = {}
66
+ for rel, cnt in per_file.items():
67
+ parts = rel.split("/")
68
+ cur = tree
69
+ for seg in parts[:-1]:
70
+ cur = cur.setdefault(seg, {})
71
+ leaf = parts[-1]
72
+ churn = _git_churn(root, rel) if cnt else 0
73
+ size_kb = size_map.get(rel, 0) / 1024
74
+ score = cnt * 10 + churn * 2 + size_kb / 10
75
+ cur[leaf] = {"_value": round(score, 1), "_severity": sev_max.get(rel, "low"), "_findings": cnt}
76
+
77
+ def to_children(d: dict, name: str = "root") -> dict:
78
+ children = []
79
+ for k, v in sorted(d.items()):
80
+ if isinstance(v, dict) and "_value" in v:
81
+ children.append({"name": k, "value": v["_value"], "severity": v["_severity"], "findings": v["_findings"]})
82
+ elif isinstance(v, dict):
83
+ sub = to_children(v, k)
84
+ if sub["children"]:
85
+ total = sum(c.get("value", 0) for c in sub["children"])
86
+ sub["value"] = round(total, 1)
87
+ children.append(sub)
88
+ return {"name": name, "children": children}
89
+
90
+ root_node = to_children(tree)
91
+ # top hotspots
92
+ flat = []
93
+
94
+ def collect(node, prefix=""):
95
+ for ch in node.get("children", []):
96
+ path = f"{prefix}/{ch['name']}" if prefix else ch["name"]
97
+ if "children" in ch:
98
+ collect(ch, path)
99
+ else:
100
+ flat.append((path, ch["value"], ch["severity"]))
101
+
102
+ collect(root_node)
103
+ flat.sort(key=lambda x: -x[1])
104
+ return {"tree": root_node, "hotspots": [{"file": p, "score": s, "severity": sev} for p, s, sev in flat[:20]], "total_files": len(per_file)}