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,172 @@
1
+ """FaultInjector — identify fault injection points in code.
2
+
3
+ Scans for patterns that should handle failures gracefully but don't
4
+ (network calls without try/except, DB writes without rollback, file I/O
5
+ without close/finally, etc.).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+
14
+ from patchi.core.assurance.graph import AssuranceGraph
15
+
16
+ _log = logging.getLogger("patchi.reliability.fault_injection")
17
+
18
+
19
+ @dataclass
20
+ class FaultInjectionPoint:
21
+ """A location in code where fault injection would be valuable."""
22
+
23
+ file: str
24
+ line: int
25
+ fault_type: str # "network", "disk", "database", "memory", "cpu"
26
+ pattern: str # what was detected
27
+ recovery_strategy: str # what should be there
28
+ severity: str # "high", "medium", "low"
29
+
30
+ def to_dict(self) -> dict:
31
+ return {
32
+ "file": self.file,
33
+ "line": self.line,
34
+ "fault_type": self.fault_type,
35
+ "pattern": self.pattern,
36
+ "recovery_strategy": self.recovery_strategy,
37
+ "severity": self.severity,
38
+ }
39
+
40
+
41
+ # Patterns that indicate missing fault handling
42
+ _FAULT_PATTERNS: list[tuple[str, str, str, str, str]] = [
43
+ # (regex pattern, fault_type, description, recovery, severity)
44
+ (
45
+ r"\.read\(|\.write\(",
46
+ "disk",
47
+ "File I/O without try/except",
48
+ "Add try/except with IOError handling",
49
+ "medium",
50
+ ),
51
+ (
52
+ r"requests\.(get|post|put|delete)\(",
53
+ "network",
54
+ "HTTP request without timeout/retry",
55
+ "Add timeout and retry with backoff",
56
+ "high",
57
+ ),
58
+ (
59
+ r"subprocess\.run\(|subprocess\.Popen\(",
60
+ "network",
61
+ "Subprocess without error handling",
62
+ "Add try/except and check returncode",
63
+ "medium",
64
+ ),
65
+ (
66
+ r"\.execute\(|\.commit\(",
67
+ "database",
68
+ "DB operation without rollback",
69
+ "Add try/except with rollback in finally",
70
+ "high",
71
+ ),
72
+ (
73
+ r"json\.loads\(",
74
+ "disk",
75
+ "JSON parse without try/except",
76
+ "Add try/except for JSONDecodeError",
77
+ "medium",
78
+ ),
79
+ (
80
+ r"open\(",
81
+ "disk",
82
+ "File open without context manager",
83
+ "Use with-statement for guaranteed close",
84
+ "medium",
85
+ ),
86
+ (
87
+ r"pickle\.loads?\(",
88
+ "memory",
89
+ "Unsafe deserialization",
90
+ "Use safe deserialization or json",
91
+ "high",
92
+ ),
93
+ (
94
+ r"eval\(|exec\(",
95
+ "memory",
96
+ "Dynamic code execution",
97
+ "Replace with safe alternatives",
98
+ "high",
99
+ ),
100
+ ]
101
+
102
+
103
+ class FaultInjector:
104
+ """Analyze code for missing fault handling."""
105
+
106
+ def __init__(self, root: Path, graph: AssuranceGraph | None = None):
107
+ self._root = root
108
+ self._graph = graph
109
+
110
+ def scan_file(self, file_path: Path) -> list[FaultInjectionPoint]:
111
+ """Scan a single file for fault injection points."""
112
+ results: list[FaultInjectionPoint] = []
113
+ try:
114
+ content = file_path.read_text(encoding="utf-8", errors="ignore")
115
+ except OSError:
116
+ return results
117
+
118
+ lines = content.splitlines()
119
+ rel_path = (
120
+ str(file_path.relative_to(self._root))
121
+ if self._root in file_path.parents
122
+ else str(file_path)
123
+ )
124
+
125
+ for line_num, line in enumerate(lines, 1):
126
+ stripped = line.strip()
127
+ if stripped.startswith("#") or stripped.startswith("//"):
128
+ continue
129
+
130
+ for pattern, fault_type, description, recovery, severity in _FAULT_PATTERNS:
131
+ import re
132
+
133
+ if re.search(pattern, line):
134
+ # Check if already has try/except nearby
135
+ if not self._has_error_handling(lines, line_num - 1):
136
+ results.append(
137
+ FaultInjectionPoint(
138
+ file=rel_path,
139
+ line=line_num,
140
+ fault_type=fault_type,
141
+ pattern=description,
142
+ recovery_strategy=recovery,
143
+ severity=severity,
144
+ )
145
+ )
146
+ break # one finding per line
147
+
148
+ return results
149
+
150
+ def scan_project(self, max_files: int = 200) -> list[FaultInjectionPoint]:
151
+ """Scan the project for fault injection points."""
152
+ results: list[FaultInjectionPoint] = []
153
+ count = 0
154
+
155
+ for py_file in sorted(self._root.rglob("*.py")):
156
+ if count >= max_files:
157
+ break
158
+ if ".patchi" in str(py_file) or "__pycache__" in str(py_file):
159
+ continue
160
+ if ".venv" in str(py_file) or "node_modules" in str(py_file):
161
+ continue
162
+ results.extend(self.scan_file(py_file))
163
+ count += 1
164
+
165
+ return results
166
+
167
+ def _has_error_handling(self, lines: list[str], line_idx: int) -> bool:
168
+ """Check if a line is inside a try/except block (±5 lines)."""
169
+ start = max(0, line_idx - 5)
170
+ end = min(len(lines), line_idx + 6)
171
+ block = "\n".join(lines[start:end])
172
+ return "try:" in block and ("except" in block or "finally:" in block)
@@ -0,0 +1,128 @@
1
+ """IdempotencyAnalyzer — find operations that should be idempotent but aren't.
2
+
3
+ Detects side effects that happen on every call (file writes without checks,
4
+ DB inserts without upserts, network calls without deduplication).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import re
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+
14
+ _log = logging.getLogger("patchi.reliability.idempotency")
15
+
16
+
17
+ @dataclass
18
+ class IdempotencyIssue:
19
+ """An operation that should be idempotent but has side effects."""
20
+
21
+ file: str
22
+ line: int
23
+ issue_type: str # "append_without_check", "insert_without_upsert", "write_without_guard"
24
+ description: str
25
+ fix_suggestion: str
26
+
27
+ def to_dict(self) -> dict:
28
+ return {
29
+ "file": self.file,
30
+ "line": self.line,
31
+ "issue_type": self.issue_type,
32
+ "description": self.description,
33
+ "fix_suggestion": self.fix_suggestion,
34
+ }
35
+
36
+
37
+ # Patterns indicating non-idempotent operations
38
+ _IDEMPOTENCY_PATTERNS: list[tuple[str, str, str, str]] = [
39
+ (
40
+ r"\.append\(",
41
+ "append_without_check",
42
+ "List append without dedup check",
43
+ "Check if item exists before appending",
44
+ ),
45
+ (
46
+ r"conn\.execute\(['\"]INSERT",
47
+ "insert_without_upsert",
48
+ "SQL INSERT without upsert",
49
+ "Use INSERT OR REPLACE or check existence first",
50
+ ),
51
+ (
52
+ r"\.write_text\(",
53
+ "write_without_guard",
54
+ "File write without content comparison",
55
+ "Compare content before writing to avoid unnecessary I/O",
56
+ ),
57
+ (
58
+ r"json\.dumps.*\.write_text\(",
59
+ "write_without_guard",
60
+ "JSON write without read-compare",
61
+ "Read existing content and compare before writing",
62
+ ),
63
+ (
64
+ r"\.add\(",
65
+ "set_add_without_check",
66
+ "Set add without membership check",
67
+ "Use set.add() (already idempotent) or document non-idempotent semantics",
68
+ ),
69
+ ]
70
+
71
+
72
+ class IdempotencyAnalyzer:
73
+ """Analyze code for idempotency issues."""
74
+
75
+ def __init__(self, root: Path):
76
+ self._root = root
77
+
78
+ def scan_file(self, file_path: Path) -> list[IdempotencyIssue]:
79
+ """Scan a single file for idempotency issues."""
80
+ results: list[IdempotencyIssue] = []
81
+ try:
82
+ content = file_path.read_text(encoding="utf-8", errors="ignore")
83
+ except OSError:
84
+ return results
85
+
86
+ lines = content.splitlines()
87
+ rel_path = (
88
+ str(file_path.relative_to(self._root))
89
+ if self._root in file_path.parents
90
+ else str(file_path)
91
+ )
92
+
93
+ for line_num, line in enumerate(lines, 1):
94
+ stripped = line.strip()
95
+ if stripped.startswith("#") or stripped.startswith("//"):
96
+ continue
97
+
98
+ for pattern, issue_type, description, fix in _IDEMPOTENCY_PATTERNS:
99
+ if re.search(pattern, line):
100
+ results.append(
101
+ IdempotencyIssue(
102
+ file=rel_path,
103
+ line=line_num,
104
+ issue_type=issue_type,
105
+ description=description,
106
+ fix_suggestion=fix,
107
+ )
108
+ )
109
+ break # one per line
110
+
111
+ return results
112
+
113
+ def scan_project(self, max_files: int = 200) -> list[IdempotencyIssue]:
114
+ """Scan the project for idempotency issues."""
115
+ results: list[IdempotencyIssue] = []
116
+ count = 0
117
+
118
+ for py_file in sorted(self._root.rglob("*.py")):
119
+ if count >= max_files:
120
+ break
121
+ if ".patchi" in str(py_file) or "__pycache__" in str(py_file):
122
+ continue
123
+ if ".venv" in str(py_file) or "node_modules" in str(py_file):
124
+ continue
125
+ results.extend(self.scan_file(py_file))
126
+ count += 1
127
+
128
+ return results
@@ -0,0 +1,147 @@
1
+ """RecoveryAnalyzer — analyze data flows for recovery patterns.
2
+
3
+ Checks that critical data paths have circuit breakers, retry logic,
4
+ and graceful degradation.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import re
11
+ from dataclasses import dataclass
12
+ from pathlib import Path
13
+
14
+ from patchi.core.assurance.graph import AssuranceGraph
15
+
16
+ _log = logging.getLogger("patchi.reliability.recovery")
17
+
18
+
19
+ @dataclass
20
+ class RecoveryGap:
21
+ """A data flow missing a recovery pattern."""
22
+
23
+ file: str
24
+ flow: str # "source → sink"
25
+ missing_pattern: str # "circuit_breaker", "retry", "fallback", "timeout"
26
+ severity: str
27
+ recommendation: str
28
+
29
+ def to_dict(self) -> dict:
30
+ return {
31
+ "file": self.file,
32
+ "flow": self.flow,
33
+ "missing_pattern": self.missing_pattern,
34
+ "severity": self.severity,
35
+ "recommendation": self.recommendation,
36
+ }
37
+
38
+
39
+ class RecoveryAnalyzer:
40
+ """Analyze data flows for missing recovery patterns."""
41
+
42
+ def __init__(self, root: Path, graph: AssuranceGraph | None = None):
43
+ self._root = root
44
+ self._graph = graph
45
+
46
+ def analyze(self, max_gaps: int = 30) -> list[RecoveryGap]:
47
+ """Analyze the project for missing recovery patterns."""
48
+ gaps: list[RecoveryGap] = []
49
+
50
+ # Check data flows from the graph
51
+ if self._graph:
52
+ gaps.extend(self._graph_flow_analysis())
53
+
54
+ # Check common patterns in code
55
+ gaps.extend(self._code_pattern_analysis())
56
+
57
+ return gaps[:max_gaps]
58
+
59
+ def _graph_flow_analysis(self) -> list[RecoveryGap]:
60
+ """Analyze data flows from the assurance graph for recovery gaps."""
61
+ gaps: list[RecoveryGap] = []
62
+
63
+ for claim in self._graph.claims.values():
64
+ for ev in claim.evidence:
65
+ if not ev.supports:
66
+ continue
67
+ artifact = ev.artifact
68
+ source = artifact.get("source", "")
69
+ sink = artifact.get("sink", "")
70
+
71
+ if not source or not sink:
72
+ continue
73
+
74
+ # External sinks need circuit breakers
75
+ if any(kw in sink.lower() for kw in ["api", "http", "network", "remote"]):
76
+ gaps.append(
77
+ RecoveryGap(
78
+ file=source,
79
+ flow=f"{source} → {sink}",
80
+ missing_pattern="circuit_breaker",
81
+ severity="high",
82
+ recommendation=f"Add circuit breaker between {source} and external {sink}",
83
+ )
84
+ )
85
+
86
+ # Database sinks need retry with backoff
87
+ if any(kw in sink.lower() for kw in ["db", "database", "sql", "redis"]):
88
+ gaps.append(
89
+ RecoveryGap(
90
+ file=source,
91
+ flow=f"{source} → {sink}",
92
+ missing_pattern="retry",
93
+ severity="medium",
94
+ recommendation=f"Add retry with exponential backoff for {sink} operations",
95
+ )
96
+ )
97
+
98
+ return gaps
99
+
100
+ def _code_pattern_analysis(self) -> list[RecoveryGap]:
101
+ """Scan code for missing recovery patterns."""
102
+ gaps: list[RecoveryGap] = []
103
+
104
+ for py_file in sorted(self._root.rglob("*.py")):
105
+ if ".patchi" in str(py_file) or "__pycache__" in str(py_file):
106
+ continue
107
+ if ".venv" in str(py_file):
108
+ continue
109
+
110
+ try:
111
+ content = py_file.read_text(encoding="utf-8", errors="ignore")
112
+ except OSError:
113
+ continue
114
+
115
+ rel = (
116
+ str(py_file.relative_to(self._root))
117
+ if self._root in py_file.parents
118
+ else str(py_file)
119
+ )
120
+
121
+ # Check for network calls without retry
122
+ if re.search(r"requests\.(get|post|put|delete)\(", content):
123
+ if "retry" not in content.lower() and "tenacity" not in content.lower():
124
+ gaps.append(
125
+ RecoveryGap(
126
+ file=rel,
127
+ flow="HTTP request",
128
+ missing_pattern="retry",
129
+ severity="medium",
130
+ recommendation="Add retry with backoff for HTTP requests",
131
+ )
132
+ )
133
+
134
+ # Check for DB operations without rollback
135
+ if re.search(r"\.execute\(|\.commit\(", content):
136
+ if "rollback" not in content.lower():
137
+ gaps.append(
138
+ RecoveryGap(
139
+ file=rel,
140
+ flow="Database operation",
141
+ missing_pattern="rollback",
142
+ severity="high",
143
+ recommendation="Add try/except with rollback for database operations",
144
+ )
145
+ )
146
+
147
+ return gaps
@@ -0,0 +1,5 @@
1
+ """Runtime tracer package (E-3)."""
2
+
3
+ from patchi.core.runtime.tracer import TraceReport, trace_file
4
+
5
+ __all__ = ["TraceReport", "trace_file"]
@@ -0,0 +1,230 @@
1
+ """
2
+ Runtime Tracer — AST-guided execution tracing for Python target code.
3
+
4
+ Runs a Python file in a fresh subprocess under a sys.settrace-based tracer
5
+ that records:
6
+ - every function call/return (name, file, line, duration)
7
+ - raised exceptions with full argument snapshot
8
+ - total wall time
9
+
10
+ Bounded by design: wall-clock timeout kills the child; trace buffer is
11
+ capped; output is a single JSON blob on a pipe — never interleaved with
12
+ program stdout.
13
+
14
+ This closes the gap between "scanner found it" and "reproduced it":
15
+ RuntimeCrashScanner findings can be fed here for a reproduction trace.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import logging
22
+ import subprocess
23
+ import sys
24
+ import tempfile
25
+ from dataclasses import dataclass, field
26
+ from pathlib import Path
27
+
28
+ _log = logging.getLogger("patchi.core.runtime.tracer")
29
+
30
+ # The child-side tracer source. Injected as a file so we don't fight
31
+ # shell quoting; kept as one string so it stays reviewable.
32
+ _CHILD_TRACER = r"""
33
+ import json
34
+ import sys
35
+ import time
36
+ import tracemalloc
37
+
38
+ MAX_CALLS = int(sys.argv[1]) if len(sys.argv) > 1 else 5000
39
+
40
+ calls = [] # completed call records
41
+ exceptions = [] # captured exceptions
42
+ order = [0]
43
+ tracemalloc.start()
44
+
45
+ def _tracer(frame, event, arg):
46
+ if len(calls) >= MAX_CALLS and event != "exception":
47
+ return None
48
+ co = frame.f_code
49
+ rec = {
50
+ "fn": co.co_name,
51
+ "file": co.co_filename,
52
+ "line": co.co_firstlineno,
53
+ }
54
+ if event == "call":
55
+ order[0] += 1
56
+ rec["seq"] = order[0]
57
+ rec["event"] = "call"
58
+ calls.append(rec)
59
+ return _tracer
60
+ if event == "return":
61
+ rec["event"] = "return"
62
+ calls.append(rec)
63
+ return None
64
+ if event == "exception":
65
+ exc_type, exc_val, _ = arg
66
+ tb = exc_val.__traceback__
67
+ exceptions.append({
68
+ "type": exc_type.__name__,
69
+ "message": str(exc_val)[:300],
70
+ "file": tb.tb_frame.f_code.co_filename if tb else co.co_filename,
71
+ "line": tb.tb_lineno if tb else 0,
72
+ "locals": {
73
+ k: repr(v)[:120]
74
+ for k, v in list(frame.f_locals.items())[:15]
75
+ if not k.startswith("__")
76
+ },
77
+ })
78
+ return _tracer
79
+ return None
80
+
81
+
82
+ def main():
83
+ target = sys.argv[2]
84
+ out_path = sys.argv[3]
85
+ sys.argv = [target] + sys.argv[4:]
86
+ sys.settrace(_tracer)
87
+ t0 = time.perf_counter()
88
+ exit_code = 0
89
+ try:
90
+ with open(target, encoding="utf-8") as fh:
91
+ code = compile(fh.read(), target, "exec")
92
+ g = {"__name__": "__main__", "__file__": target}
93
+ exec(code, g)
94
+ except SystemExit as e:
95
+ exit_code = e.code if isinstance(e.code, int) else 0
96
+ except BaseException as e:
97
+ exit_code = 1
98
+ exceptions.append({
99
+ "type": type(e).__name__,
100
+ "message": str(e)[:300],
101
+ "file": target,
102
+ "line": getattr(getattr(e, "__traceback__", None), "tb_lineno", 0),
103
+ "locals": {},
104
+ "fatal": True,
105
+ })
106
+ finally:
107
+ sys.settrace(None)
108
+ duration = time.perf_counter() - t0
109
+ current, peak = tracemalloc.get_traced_memory()
110
+ out = {
111
+ "exit_code": exit_code,
112
+ "duration_s": round(duration, 4),
113
+ "peak_memory_bytes": peak,
114
+ "call_count": len(calls),
115
+ "truncated": len(calls) >= MAX_CALLS,
116
+ "calls": calls[-200:], # tail: most recent activity
117
+ "exceptions": exceptions,
118
+ }
119
+ # Dedicated file, NOT stdout: the traced program owns stdout.
120
+ with open(out_path, "w", encoding="utf-8") as fh:
121
+ json.dump(out, fh, default=str)
122
+
123
+ main()
124
+ """
125
+
126
+
127
+ @dataclass
128
+ class TraceReport:
129
+ """Structured result of one traced run."""
130
+
131
+ exit_code: int = -1
132
+ duration_s: float = 0.0
133
+ peak_memory_bytes: int = 0
134
+ call_count: int = 0
135
+ truncated: bool = False
136
+ calls: list[dict] = field(default_factory=list)
137
+ exceptions: list[dict] = field(default_factory=list)
138
+ error: str = ""
139
+
140
+ @property
141
+ def crashed(self) -> bool:
142
+ return bool(self.exceptions) or self.exit_code not in (0, None)
143
+
144
+ def hot_functions(self, top_n: int = 10) -> list[tuple[str, int]]:
145
+ """Most-called functions — N+1 query / loop-hotspot candidates."""
146
+ counts: dict[str, int] = {}
147
+ for c in self.calls:
148
+ if c.get("event") == "call":
149
+ key = f"{c['fn']} ({Path(c['file']).name}:{c['line']})"
150
+ counts[key] = counts.get(key, 0) + 1
151
+ return sorted(counts.items(), key=lambda kv: -kv[1])[:top_n]
152
+
153
+ def to_dict(self) -> dict:
154
+ return {
155
+ "exit_code": self.exit_code,
156
+ "duration_s": self.duration_s,
157
+ "peak_memory_bytes": self.peak_memory_bytes,
158
+ "call_count": self.call_count,
159
+ "truncated": self.truncated,
160
+ "crashed": self.crashed,
161
+ "hot_functions": [{"fn": fn, "calls": n} for fn, n in self.hot_functions()],
162
+ "exceptions": self.exceptions,
163
+ "error": self.error,
164
+ }
165
+
166
+
167
+ def trace_file(
168
+ script: str | Path,
169
+ timeout: float = 60.0,
170
+ max_calls: int = 5000,
171
+ args: list[str] | None = None,
172
+ ) -> TraceReport:
173
+ """
174
+ Run *script* under the tracer; returns a TraceReport.
175
+
176
+ Never raises for program behavior — crashes are captured data. Raises
177
+ only for infrastructure failures (missing file, bad extension).
178
+ """
179
+ path = Path(script).resolve()
180
+ if path.suffix.lower() != ".py":
181
+ raise ValueError(f"tracer supports .py only, got {path.suffix}")
182
+ if not path.is_file():
183
+ raise FileNotFoundError(str(path))
184
+
185
+ with tempfile.TemporaryDirectory() as td:
186
+ tracer_path = Path(td) / "_patchi_tracer.py"
187
+ tracer_path.write_text(_CHILD_TRACER, encoding="utf-8")
188
+ out_path = Path(td) / "trace_out.json"
189
+
190
+ cmd = [
191
+ sys.executable,
192
+ str(tracer_path),
193
+ str(max_calls),
194
+ str(path),
195
+ str(out_path),
196
+ *(args or []),
197
+ ]
198
+ try:
199
+ proc = subprocess.run(
200
+ cmd,
201
+ capture_output=True,
202
+ timeout=timeout,
203
+ cwd=str(path.parent),
204
+ )
205
+ except subprocess.TimeoutExpired:
206
+ return TraceReport(error=f"timed out after {timeout}s")
207
+
208
+ if not out_path.is_file():
209
+ return TraceReport(
210
+ error=f"tracer failed rc={proc.returncode}: {proc.stderr.decode(errors='replace')[:300]}"
211
+ )
212
+
213
+ try:
214
+ raw = json.loads(out_path.read_text(encoding="utf-8", errors="replace"))
215
+ except (json.JSONDecodeError, UnicodeDecodeError) as e:
216
+ return TraceReport(error=f"unparseable trace output: {e}")
217
+
218
+ report = TraceReport(
219
+ exit_code=raw.get("exit_code", -1),
220
+ duration_s=raw.get("duration_s", 0.0),
221
+ peak_memory_bytes=raw.get("peak_memory_bytes", 0),
222
+ call_count=raw.get("call_count", 0),
223
+ truncated=raw.get("truncated", False),
224
+ calls=raw.get("calls", []),
225
+ exceptions=raw.get("exceptions", []),
226
+ )
227
+ # A fatal exception means exit handler captured it but exit stayed 1.
228
+ if any(x.get("fatal") for x in report.exceptions):
229
+ report.exit_code = 1
230
+ return report