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,195 @@
1
+ """
2
+ Baseline locking for Patchi.
3
+
4
+ Snapshots the current finding state and compares against previous baselines.
5
+ Enables "only fail on new issues" workflow for CI/CD.
6
+
7
+ Baseline is stored in `.patchi/memory/baseline.json`.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import json
14
+ import logging
15
+ from dataclasses import dataclass, field
16
+ from datetime import UTC, datetime
17
+ from pathlib import Path
18
+
19
+ from patchi.core.agents.base import Finding, Severity
20
+
21
+ _log = logging.getLogger("patchi.brain.baseline")
22
+
23
+
24
+ @dataclass
25
+ class BaselineSnapshot:
26
+ """A snapshot of findings at a point in time."""
27
+
28
+ created_at: str = ""
29
+ total_findings: int = 0
30
+ by_severity: dict[str, int] = field(default_factory=dict)
31
+ by_agent: dict[str, int] = field(default_factory=dict)
32
+ finding_hashes: set[str] = field(default_factory=set)
33
+ file_finding_counts: dict[str, int] = field(default_factory=dict)
34
+
35
+ def to_dict(self) -> dict:
36
+ return {
37
+ "created_at": self.created_at,
38
+ "total_findings": self.total_findings,
39
+ "by_severity": self.by_severity,
40
+ "by_agent": self.by_agent,
41
+ "finding_hashes": sorted(self.finding_hashes),
42
+ "file_finding_counts": self.file_finding_counts,
43
+ }
44
+
45
+ @classmethod
46
+ def from_dict(cls, data: dict) -> BaselineSnapshot:
47
+ return cls(
48
+ created_at=data.get("created_at", ""),
49
+ total_findings=data.get("total_findings", 0),
50
+ by_severity=data.get("by_severity", {}),
51
+ by_agent=data.get("by_agent", {}),
52
+ finding_hashes=set(data.get("finding_hashes", [])),
53
+ file_finding_counts=data.get("file_finding_counts", {}),
54
+ )
55
+
56
+
57
+ @dataclass
58
+ class BaselineDiff:
59
+ """Difference between current scan and baseline."""
60
+
61
+ new_findings: list[Finding] = field(default_factory=list)
62
+ resolved_findings: list[dict] = field(default_factory=list)
63
+ total_baseline: int = 0
64
+ total_current: int = 0
65
+ new_count: int = 0
66
+ resolved_count: int = 0
67
+ has_regression: bool = False
68
+ summary: str = ""
69
+
70
+
71
+ def _finding_hash(finding: Finding) -> str:
72
+ """Deterministic hash for a finding — used to detect new vs known issues."""
73
+ raw = f"{finding.file}:{finding.line}:{finding.type}:{finding.severity.value}:{finding.message}"
74
+ return hashlib.sha256(raw.encode()).hexdigest()[:16]
75
+
76
+
77
+ def _finding_dict_hash(finding: dict) -> str:
78
+ raw = f"{finding.get('file', '')}:{finding.get('line', 0)}:{finding.get('type', '')}:{finding.get('severity', '')}:{finding.get('message', '')}"
79
+ return hashlib.sha256(raw.encode()).hexdigest()[:16]
80
+
81
+
82
+ def _baseline_path(root: Path) -> Path:
83
+ return root / ".patchi" / "memory" / "baseline.json"
84
+
85
+
86
+ def save_baseline(findings: list[Finding], root: Path) -> BaselineSnapshot:
87
+ """Snapshot current findings as the new baseline."""
88
+ snapshot = BaselineSnapshot(
89
+ created_at=datetime.now(UTC).isoformat(),
90
+ total_findings=len(findings),
91
+ by_severity={s.value: 0 for s in Severity},
92
+ by_agent={},
93
+ finding_hashes=set(),
94
+ file_finding_counts={},
95
+ )
96
+
97
+ for f in findings:
98
+ snapshot.by_severity[f.severity.value] = snapshot.by_severity.get(f.severity.value, 0) + 1
99
+ snapshot.by_agent[f.agent] = snapshot.by_agent.get(f.agent, 0) + 1
100
+ snapshot.finding_hashes.add(_finding_hash(f))
101
+ snapshot.file_finding_counts[f.file] = snapshot.file_finding_counts.get(f.file, 0) + 1
102
+
103
+ path = _baseline_path(root)
104
+ path.parent.mkdir(parents=True, exist_ok=True)
105
+ path.write_text(json.dumps(snapshot.to_dict(), indent=2), encoding="utf-8")
106
+ return snapshot
107
+
108
+
109
+ def load_baseline(root: Path) -> BaselineSnapshot | None:
110
+ """Load the saved baseline, if any."""
111
+ path = _baseline_path(root)
112
+ if not path.exists():
113
+ return None
114
+ try:
115
+ data = json.loads(path.read_text(encoding="utf-8"))
116
+ return BaselineSnapshot.from_dict(data)
117
+ except Exception as e:
118
+ _log.warning("load_baseline failed: %s", e)
119
+ return None
120
+
121
+
122
+ def diff_baseline(current_findings: list[dict | Finding], root: Path) -> BaselineDiff | None:
123
+ """
124
+ Compare current findings against baseline.
125
+ Returns None if no baseline exists.
126
+
127
+ Accepts both Finding objects and dicts (from JSON output).
128
+ """
129
+ baseline = load_baseline(root)
130
+ if baseline is None:
131
+ return None
132
+
133
+ # Convert all to Finding objects for uniform handling
134
+ normalized: list[Finding] = []
135
+ for item in current_findings:
136
+ if isinstance(item, Finding):
137
+ normalized.append(item)
138
+ elif isinstance(item, dict):
139
+ normalized.append(
140
+ Finding(
141
+ agent=item.get("agent", ""),
142
+ type=item.get("type", item.get("finding_type", "")),
143
+ severity=Severity(item.get("severity", "info")),
144
+ file=item.get("file", ""),
145
+ line=item.get("line", 0),
146
+ message=item.get("message", ""),
147
+ )
148
+ )
149
+
150
+ current_hashes = {_finding_hash(f) for f in normalized}
151
+
152
+ new_findings = [f for f in normalized if _finding_hash(f) not in baseline.finding_hashes]
153
+ resolved_hashes = baseline.finding_hashes - current_hashes
154
+
155
+ diff = BaselineDiff(
156
+ new_findings=new_findings,
157
+ resolved_findings=[{"hash": h} for h in resolved_hashes],
158
+ total_baseline=baseline.total_findings,
159
+ total_current=len(normalized),
160
+ new_count=len(new_findings),
161
+ resolved_count=len(resolved_hashes),
162
+ has_regression=len(new_findings) > 0,
163
+ )
164
+
165
+ # Build summary
166
+ parts = []
167
+ if diff.new_count > 0:
168
+ parts.append(f"{diff.new_count} new finding(s)")
169
+ if diff.resolved_count > 0:
170
+ parts.append(f"{diff.resolved_count} resolved")
171
+ parts.append(f"baseline: {diff.total_baseline}")
172
+ parts.append(f"current: {diff.total_current}")
173
+ diff.summary = ", ".join(parts)
174
+
175
+ return diff
176
+
177
+
178
+ def update_baseline(findings: list[Finding | dict], root: Path) -> BaselineSnapshot:
179
+ """Convenience: load current findings, diff against baseline, save new baseline."""
180
+ normalized: list[Finding] = []
181
+ for item in findings:
182
+ if isinstance(item, Finding):
183
+ normalized.append(item)
184
+ elif isinstance(item, dict):
185
+ normalized.append(
186
+ Finding(
187
+ agent=item.get("agent", ""),
188
+ type=item.get("type", item.get("finding_type", "")),
189
+ severity=Severity(item.get("severity", "info")),
190
+ file=item.get("file", ""),
191
+ line=item.get("line", 0),
192
+ message=item.get("message", ""),
193
+ )
194
+ )
195
+ return save_baseline(normalized, root)
@@ -0,0 +1,312 @@
1
+ """
2
+ Blast radius calculation utilities.
3
+
4
+ Two levels:
5
+ - File-level (v1, original): Uses ImportGraph. BFS on reverse edges.
6
+ - Symbol-level (v2): Uses SymbolGraph. Weighted by test coverage + criticality.
7
+ Both directions (downstream dependents + upstream dependencies).
8
+ Damage-vs-repair: structural diff before/after a change.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from collections import deque
14
+ from dataclasses import dataclass, field
15
+ from typing import Any
16
+
17
+ from patchi.core.brain.import_graph import ImportGraph
18
+
19
+ # ── File-level (v1) ────────────────────────────────────────────────────────────
20
+
21
+
22
+ @dataclass
23
+ class BlastRadius:
24
+ """Information about the blast radius of a file."""
25
+
26
+ target: str
27
+ direct_dependents: list[str]
28
+ all_dependents: list[str]
29
+ risk_level: str # 'low', 'medium', 'high'
30
+
31
+ @property
32
+ def total_affected(self) -> int:
33
+ return len(self.all_dependents)
34
+
35
+
36
+ def calculate_blast_radius(target: str, graph: ImportGraph) -> BlastRadius:
37
+ """BFS on reverse graph to find all transitive dependents."""
38
+ direct = sorted(graph.reverse.get(target, set()))
39
+ all_affected: set[str] = set()
40
+ queue = deque(direct)
41
+ while queue:
42
+ node = queue.popleft()
43
+ if node == target or node in all_affected:
44
+ continue
45
+ all_affected.add(node)
46
+ for parent in graph.reverse.get(node, set()):
47
+ if parent not in all_affected:
48
+ queue.append(parent)
49
+
50
+ count = len(all_affected)
51
+ risk = "low" if count == 0 else ("medium" if count <= 5 else "high")
52
+
53
+ return BlastRadius(
54
+ target=target,
55
+ direct_dependents=direct,
56
+ all_dependents=sorted(all_affected),
57
+ risk_level=risk,
58
+ )
59
+
60
+
61
+ def build_blast_radius_map(graph: ImportGraph) -> dict[str, BlastRadius]:
62
+ return {node: calculate_blast_radius(node, graph) for node in graph.nodes}
63
+
64
+
65
+ # ── Symbol-level (v2) ──────────────────────────────────────────────────────────
66
+
67
+
68
+ @dataclass
69
+ class SymbolBlastResult:
70
+ """Symbol-level blast radius with weighted scoring."""
71
+
72
+ symbol_name: str
73
+ file: str
74
+ line: int
75
+ # Downstream: what depends on this symbol
76
+ downstream_direct: list[dict] = field(default_factory=list)
77
+ downstream_all: list[dict] = field(default_factory=list)
78
+ downstream_count: int = 0
79
+ # Upstream: what this symbol depends on
80
+ upstream_direct: list[dict] = field(default_factory=list)
81
+ upstream_count: int = 0
82
+ # Weighted scores
83
+ test_coverage_score: float = 1.0 # 0-1, higher = better tested
84
+ criticality_score: float = 0.0 # 0-1, higher = more critical
85
+ risk_score: float = 0.0 # weighted combination
86
+ risk_level: str = "low"
87
+ # Damage vs repair
88
+ structural_change_count: int = 0
89
+
90
+ @property
91
+ def total_affected(self) -> int:
92
+ return self.downstream_count
93
+
94
+ def to_dict(self) -> dict:
95
+ return {
96
+ "symbol": self.symbol_name,
97
+ "file": self.file,
98
+ "line": self.line,
99
+ "downstream": {
100
+ "direct": len(self.downstream_direct),
101
+ "all": self.downstream_count,
102
+ "samples": self.downstream_direct[:5],
103
+ },
104
+ "upstream": {
105
+ "direct": len(self.upstream_direct),
106
+ "samples": self.upstream_direct[:5],
107
+ },
108
+ "test_coverage_score": round(self.test_coverage_score, 3),
109
+ "criticality_score": round(self.criticality_score, 3),
110
+ "risk_score": round(self.risk_score, 3),
111
+ "risk_level": self.risk_level,
112
+ "structural_changes": self.structural_change_count,
113
+ }
114
+
115
+
116
+ def calculate_symbol_blast_radius(
117
+ symbol_name: str,
118
+ file: str,
119
+ symbol_graph: Any,
120
+ test_coverage: dict[str, float] | None = None,
121
+ criticality_tags: dict[str, float] | None = None,
122
+ max_depth: int = 5,
123
+ ) -> SymbolBlastResult:
124
+ """Compute symbol-level blast radius with weighted scoring.
125
+
126
+ Args:
127
+ symbol_name: Name of the symbol to analyze.
128
+ file: File path where the symbol is defined.
129
+ symbol_graph: SymbolGraph instance.
130
+ test_coverage: Dict of {file_path: coverage_ratio 0-1}.
131
+ criticality_tags: Dict of {symbol_name: criticality_score 0-1}.
132
+ max_depth: Max transitive depth.
133
+ """
134
+ test_coverage = test_coverage or {}
135
+ criticality_tags = criticality_tags or {}
136
+
137
+ target = symbol_graph.get_symbol(symbol_name, file)
138
+ if not target:
139
+ return SymbolBlastResult(
140
+ symbol_name=symbol_name,
141
+ file=file,
142
+ line=0,
143
+ risk_level="unknown",
144
+ risk_score=0.0,
145
+ )
146
+
147
+ # Downstream: dependents
148
+ downstream_set: dict[str, dict] = {}
149
+ queue: list[tuple[str, str, int]] = [
150
+ (s.name, s.file, 1) for s in symbol_graph.get_dependents(symbol_name, file)
151
+ ]
152
+ visited: set[int] = {target.id}
153
+ direct_downstream: list[dict] = []
154
+
155
+ while queue:
156
+ name, fpath, depth = queue.pop(0)
157
+ if depth > max_depth:
158
+ continue
159
+ sym = symbol_graph.get_symbol(name, fpath)
160
+ if not sym or sym.id in visited:
161
+ continue
162
+ visited.add(sym.id)
163
+ entry = {"name": name, "file": fpath, "depth": depth}
164
+ downstream_set[f"{name}:{fpath}"] = entry
165
+ if depth == 1:
166
+ direct_downstream.append(entry)
167
+ # Recurse into their dependents
168
+ for dep in symbol_graph.get_dependents(name, fpath):
169
+ if dep.id not in visited:
170
+ queue.append((dep.name, dep.file, depth + 1))
171
+
172
+ # Upstream: dependencies
173
+ upstream_list: list[dict] = []
174
+ for dep in symbol_graph.get_dependencies(target.id):
175
+ upstream_list.append({"name": dep.name, "file": dep.file})
176
+
177
+ # File-level test coverage weight
178
+ file_coverage = test_coverage.get(file, 0.5)
179
+ downstream_file_coverage = 0.0
180
+ if downstream_set:
181
+ downstream_files = {v["file"] for v in downstream_set.values()}
182
+ covered = sum(test_coverage.get(f, 0.0) for f in downstream_files)
183
+ downstream_file_coverage = covered / len(downstream_files) if downstream_files else 0.0
184
+
185
+ test_coverage_score = (file_coverage + downstream_file_coverage) / 2.0
186
+
187
+ # Criticality: higher for exported/route/core symbols
188
+ criticality = criticality_tags.get(symbol_name, 0.3)
189
+ if target.kind == "route":
190
+ criticality = max(criticality, 0.8)
191
+ elif target.is_exported:
192
+ criticality = max(criticality, 0.5)
193
+ criticality_score = min(criticality, 1.0)
194
+
195
+ # Risk score: more downstream = higher risk, less test coverage = higher risk
196
+ downstream_risk = min(len(downstream_set) / 50.0, 1.0)
197
+ coverage_risk = 1.0 - test_coverage_score
198
+ risk_score = downstream_risk * 0.4 + coverage_risk * 0.3 + criticality_score * 0.3
199
+ risk_score = min(risk_score, 1.0)
200
+
201
+ risk_level = "low"
202
+ if risk_score > 0.3:
203
+ risk_level = "medium"
204
+ if risk_score > 0.6:
205
+ risk_level = "high"
206
+ if risk_score > 0.85:
207
+ risk_level = "critical"
208
+
209
+ return SymbolBlastResult(
210
+ symbol_name=symbol_name,
211
+ file=file,
212
+ line=target.line,
213
+ downstream_direct=direct_downstream,
214
+ downstream_all=list(downstream_set.values()),
215
+ downstream_count=len(downstream_set),
216
+ upstream_direct=upstream_list,
217
+ upstream_count=len(upstream_list),
218
+ test_coverage_score=test_coverage_score,
219
+ criticality_score=criticality_score,
220
+ risk_score=risk_score,
221
+ risk_level=risk_level,
222
+ )
223
+
224
+
225
+ def calculate_file_symbol_blast_radius(
226
+ file_path: str,
227
+ symbol_graph: Any,
228
+ test_coverage: dict[str, float] | None = None,
229
+ criticality_tags: dict[str, float] | None = None,
230
+ ) -> list[SymbolBlastResult]:
231
+ """Compute symbol-level blast radius for all symbols in a file."""
232
+ symbols = symbol_graph.get_symbols_in_file(file_path)
233
+ return [
234
+ calculate_symbol_blast_radius(s.name, s.file, symbol_graph, test_coverage, criticality_tags)
235
+ for s in symbols
236
+ ]
237
+
238
+
239
+ @dataclass
240
+ class GraphDiffImpact:
241
+ """Structural impact analysis from a graph diff."""
242
+
243
+ file: str
244
+ symbols_added: int = 0
245
+ symbols_removed: int = 0
246
+ symbols_modified: int = 0
247
+ downstream_impact: int = 0
248
+ upstream_impact: int = 0
249
+ risk_level: str = "low"
250
+
251
+ @property
252
+ def total_changes(self) -> int:
253
+ return self.symbols_added + self.symbols_removed + self.symbols_modified
254
+
255
+ def to_dict(self) -> dict:
256
+ return {
257
+ "file": self.file,
258
+ "symbols_added": self.symbols_added,
259
+ "symbols_removed": self.symbols_removed,
260
+ "symbols_modified": self.symbols_modified,
261
+ "downstream_impact": self.downstream_impact,
262
+ "upstream_impact": self.upstream_impact,
263
+ "risk_level": self.risk_level,
264
+ }
265
+
266
+
267
+ def analyze_graph_diff(
268
+ diff: Any,
269
+ symbol_graph: Any,
270
+ ) -> list[GraphDiffImpact]:
271
+ """Analyze structural impact of a graph diff (damage vs repair).
272
+
273
+ For each file with structural changes, compute downstream/upstream impact.
274
+ """
275
+ if not diff or not diff.has_changes:
276
+ return []
277
+
278
+ affected_files: dict[str, GraphDiffImpact] = {}
279
+
280
+ for sym in diff.added:
281
+ if sym.file not in affected_files:
282
+ affected_files[sym.file] = GraphDiffImpact(file=sym.file)
283
+ affected_files[sym.file].symbols_added += 1
284
+
285
+ for sym in diff.removed:
286
+ if sym.file not in affected_files:
287
+ affected_files[sym.file] = GraphDiffImpact(file=sym.file)
288
+ affected_files[sym.file].symbols_removed += 1
289
+
290
+ for sym in diff.modified:
291
+ if sym.file not in affected_files:
292
+ affected_files[sym.file] = GraphDiffImpact(file=sym.file)
293
+ affected_files[sym.file].symbols_modified += 1
294
+
295
+ for file, impact in affected_files.items():
296
+ symbols = symbol_graph.get_symbols_in_file(file)
297
+ for s in symbols:
298
+ result = calculate_symbol_blast_radius(s.name, s.file, symbol_graph)
299
+ impact.downstream_impact = max(impact.downstream_impact, result.downstream_count)
300
+ impact.upstream_impact = max(impact.upstream_impact, result.upstream_count)
301
+
302
+ total = impact.total_changes
303
+ downstream = impact.downstream_impact
304
+ impact.risk_level = "low"
305
+ if (total >= 3 and downstream >= 5) or (total >= 1 and downstream >= 20):
306
+ impact.risk_level = "critical"
307
+ elif (total >= 2 and downstream >= 3) or downstream >= 10:
308
+ impact.risk_level = "high"
309
+ elif total >= 1 and downstream >= 1:
310
+ impact.risk_level = "medium"
311
+
312
+ return list(affected_files.values())
@@ -0,0 +1,144 @@
1
+ """
2
+ Body Tags — understander-first tagging for Patchi.
3
+
4
+ Generates .patchi/memory/body_tags.json on every Brain.scan from
5
+ deterministic signals already computed: import_graph fan-in, blast_radius,
6
+ layered_brain level, route hit, stack framework, file size.
7
+
8
+ Roles map to human anatomy (Body skill):
9
+ brain = orchestrators, contracts, memory, config
10
+ muscle = agents, scanners, security, testing, fix
11
+ bone = data types, schemas, constants, models
12
+ blood = wiring (import_graph hubs, routing)
13
+ skin = web/api/cli surface
14
+ nerve = watchers, interceptors, guards, queue
15
+
16
+ Criticality: critical|high|medium|low based on fan-in + dependents + route adjacency.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import logging
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ _log = logging.getLogger("patchi.brain.body_tags")
27
+
28
+ BODY_TAGS_FILE = ".patchi/memory/body_tags.json"
29
+
30
+
31
+ def _classify_role(path: str, layers: dict, is_route_file: bool, is_hub: bool) -> tuple[str, str]:
32
+ p = path.lower()
33
+ if "brain" in p or "memory" in p or "config" in p or "contract" in p:
34
+ return "brain", "orchestrator"
35
+ if "web/routes" in p or "web/api" in p or "cli/commands" in p:
36
+ return "skin", "surface"
37
+ if "security" in p or "agents" in p or "testing" in p or "fix/" in p:
38
+ return "muscle", "scanner" if "security" in p or "agents" in p else "fixer"
39
+ if "constants" in p or "scanner" in p or "file_corpus" in p or "import_graph" in p:
40
+ return "bone", "structure"
41
+ if is_hub or "import_graph" in p or "route_mapper" in p or "blast_radius" in p:
42
+ return "blood", "wiring"
43
+ if "watch" in p or "interceptor" in p or "guard" in p or "queue" in p:
44
+ return "nerve", "guard"
45
+ # fallback via layered brain
46
+ for lname, lyr in layers.items():
47
+ if path in getattr(lyr, "files", []):
48
+ if lyr.level == 1:
49
+ return "bone", "module"
50
+ unicode = "skin" if "web" in lname or "cli" in lname else "muscle"
51
+ return unicode, lname
52
+ return "muscle", "general"
53
+
54
+
55
+ def build_body_tags(
56
+ file_infos: list[Any],
57
+ graph: Any,
58
+ layers: dict,
59
+ routes: list[Any],
60
+ blast_map: dict[str, Any] | None = None,
61
+ ) -> dict[str, dict[str, Any]]:
62
+ # fan-in via graph.reverse, hub threshold
63
+ reverse = getattr(graph, "reverse", {}) if graph else {}
64
+ fan_in_counts = {fp: len(reverse.get(fp, [])) for fp in [fi.path for fi in file_infos]}
65
+ hub_threshold = 5 # top hubs
66
+ sorted_fan = sorted(fan_in_counts.items(), key=lambda x: -x[1])
67
+ hubs = {fp for fp, _ in sorted_fan[:20] if _ >= hub_threshold}
68
+ # route adjacency
69
+ _route_paths = {getattr(r, "path", "") for r in (routes or [])}
70
+ route_files = set()
71
+ for fi in file_infos:
72
+ low = fi.path.lower()
73
+ if any(seg in low for seg in ["route", "web", "api", "handler"]):
74
+ route_files.add(fi.path)
75
+
76
+ tags: dict[str, dict[str, Any]] = {}
77
+ for fi in file_infos:
78
+ path = fi.path
79
+ fan_in = fan_in_counts.get(path, 0)
80
+ is_hub = path in hubs
81
+ is_route = path in route_files
82
+ role, system = _classify_role(path, layers, is_route, is_hub)
83
+ # criticality
84
+ br = blast_map.get(path) if blast_map and path in blast_map else None
85
+ all_deps = getattr(br, "all_dependents", None) if br is not None else None
86
+ if all_deps is None:
87
+ all_deps = getattr(br, "dependents", []) if br is not None else []
88
+ dep_count = len(all_deps) if isinstance(all_deps, (list, set, tuple)) else 0
89
+ if fan_in >= 10 or dep_count >= 15 or path.lower() in ("patchi/core/brain/brain.py", "patchi/core/memory.py"):
90
+ crit = "critical"
91
+ elif fan_in >= 5 or is_hub or is_route or "core/brain" in path:
92
+ crit = "high"
93
+ elif fan_in >= 2 or "core/" in path:
94
+ crit = "medium"
95
+ else:
96
+ crit = "low"
97
+ # layer name
98
+ layer_name = ""
99
+ for lname, lyr in layers.items():
100
+ if path in getattr(lyr, "files", []):
101
+ layer_name = lname
102
+ break
103
+ # score for understander ranking
104
+ score = fan_in * 10 + dep_count * 3 + (20 if crit == "critical" else 10 if crit == "high" else 0) + (5 if is_route else 0)
105
+ tags[path] = {
106
+ "role": role,
107
+ "system": system,
108
+ "layer": layer_name,
109
+ "criticality": crit,
110
+ "fan_in": fan_in,
111
+ "score": score,
112
+ "is_hub": is_hub,
113
+ "is_route_file": is_route,
114
+ }
115
+ return tags
116
+
117
+
118
+ def save_body_tags(tags: dict[str, dict], root: Path) -> Path:
119
+ out = root / BODY_TAGS_FILE
120
+ out.parent.mkdir(parents=True, exist_ok=True)
121
+ # atomic write
122
+ tmp = out.with_suffix(".tmp")
123
+ payload = {"version": 1, "count": len(tags), "tags": tags}
124
+ tmp.write_text(json.dumps(payload, indent=2), encoding="utf-8")
125
+ # replace dance for win32
126
+ import sys as _sys
127
+
128
+ if _sys.platform == "win32" and out.exists():
129
+ out.unlink()
130
+ tmp.replace(out)
131
+ _log.info("body_tags: wrote %d tags to %s", len(tags), out)
132
+ return out
133
+
134
+
135
+ def load_body_tags(root: Path) -> dict[str, dict]:
136
+ p = root / BODY_TAGS_FILE
137
+ if not p.exists():
138
+ return {}
139
+ try:
140
+ data: Any = json.loads(p.read_text(encoding="utf-8"))
141
+ return data.get("tags", {}) if isinstance(data, dict) else {}
142
+ except Exception as exc: # noqa: BLE001
143
+ _log.debug("body_tags load failed: %s", exc)
144
+ return {}