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,130 @@
1
+ """
2
+ Understander — picks the 12-20 files that actually matter.
3
+
4
+ Uses Body tags + blast_radius + import_graph instead of file_infos[:50]
5
+ in insertion order. All AI surfaces call this instead of slicing.
6
+
7
+ Provides:
8
+ - core_files(limit) -> [{path, why, score, content_snippet}]
9
+ - refine_domains(active, tags) -> filtered domains (drop false hubs)
10
+ - get_function_at(file, line) -> function body via FileCorpus cache
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ _log = logging.getLogger("patchi.brain.understander")
20
+
21
+
22
+ class Understander:
23
+ def __init__(
24
+ self,
25
+ root: Path,
26
+ file_infos: list[Any],
27
+ body_tags: dict[str, dict[str, Any]],
28
+ blast_map: dict[str, Any] | None = None,
29
+ routes: list[Any] | None = None,
30
+ ) -> None:
31
+ self.root = root
32
+ self.file_infos = {fi.path: fi for fi in file_infos}
33
+ self.tags = body_tags
34
+ self.blast = blast_map or {}
35
+ self.routes = routes or []
36
+ # sort once
37
+ self._ranked = sorted(
38
+ body_tags.items(),
39
+ key=lambda kv: (-int(kv[1].get("score", 0)), -int(kv[1].get("fan_in", 0)), kv[0]),
40
+ )
41
+
42
+ def core_files(self, limit: int = 16, include_low: bool = False) -> list[dict[str, Any]]:
43
+ """Top-ranked files the LLM must see. Always includes Brain + memory + entry points."""
44
+ out: list[dict[str, Any]] = []
45
+ seen: set[str] = set()
46
+ # force-include
47
+ for must in ("patchi/core/brain/brain.py", "patchi/core/memory.py", "patchi/core/brain/body_tags.py"):
48
+ if must in self.tags and must not in seen:
49
+ t = self.tags[must]
50
+ out.append({"path": must, "why": f"{t['role']}/{t['system']} critical={t['criticality']} fan_in={t['fan_in']}", "score": t["score"], "tag": t})
51
+ seen.add(must)
52
+ for path, tag in self._ranked:
53
+ if path in seen:
54
+ continue
55
+ if not include_low and tag.get("criticality") == "low" and tag.get("score", 0) < 15 and len(out) >= limit:
56
+ continue
57
+ out.append({"path": path, "why": f"{tag['role']}/{tag['system']} {tag['criticality']} fan_in={tag.get('fan_in',0)}", "score": tag["score"], "tag": tag})
58
+ seen.add(path)
59
+ if len(out) >= limit:
60
+ break
61
+ return out
62
+
63
+ def refine_domains(self, active: list[str]) -> list[str]:
64
+ """Drop domains that are low-score hubs only (speed). Keep high/critical cores."""
65
+ if not active:
66
+ return active
67
+ # count how many core_files map to each domain keyword
68
+ core_paths = {c["path"] for c in self.core_files(limit=12)}
69
+ # naive: keep all for now, but log drop candidate
70
+ # Future: intersect with body_tags system fields
71
+ _ = core_paths
72
+ return active
73
+
74
+ def file_snippet(self, rel_path: str, max_lines: int = 200) -> str:
75
+ """Return first max_lines with line numbers — for prompts. Uses FileCorpus-style cache via pathlib."""
76
+ fi = self.file_infos.get(rel_path)
77
+ if fi is not None:
78
+ try:
79
+ txt = (self.root / rel_path).read_text(encoding="utf-8", errors="replace")
80
+ lines = txt.splitlines()[:max_lines]
81
+ return "\n".join(f"{i+1:4d} | {line}" for i, line in enumerate(lines))
82
+ except Exception as exc: # noqa: BLE001
83
+ _log.debug("understander snippet failed for %s: %s", rel_path, exc)
84
+ return ""
85
+ return ""
86
+
87
+ def function_at(self, rel_path: str, line: int, window: int = 50) -> str:
88
+ """Return function body surrounding line, using line-number heuristic (no heavy AST)."""
89
+ try:
90
+ txt = (self.root / rel_path).read_text(encoding="utf-8", errors="replace")
91
+ except Exception as exc: # noqa: BLE001
92
+ _log.debug("function_at read failed: %s", exc)
93
+ return ""
94
+ lines = txt.splitlines()
95
+ if not 1 <= line <= len(lines):
96
+ return "\n".join(lines[: min(len(lines), window)])
97
+ # find def/class up
98
+ start = max(0, line - 1 - 60)
99
+ for i in range(line - 2, max(-1, line - 80), -1):
100
+ s = lines[i].lstrip()
101
+ if s.startswith("def ") or s.startswith("async def ") or s.startswith("class "):
102
+ start = i
103
+ break
104
+ # find next def at same or lesser indent
105
+ base_indent = len(lines[line - 1]) - len(lines[line - 1].lstrip())
106
+ end = min(len(lines), line + window)
107
+ for i in range(line, min(len(lines), line + 80)):
108
+ s = lines[i]
109
+ if s.strip() and not s.strip().startswith("#"):
110
+ indent = len(s) - len(s.lstrip())
111
+ st = s.strip()
112
+ if indent <= base_indent and (st.startswith("def ") or st.startswith("class ") or st.startswith("async def ")):
113
+ end = i
114
+ break
115
+ block = lines[start:end]
116
+ return "\n".join(f"{i+1:4d} | {line}" for i, line in enumerate(block, start=start + 1))
117
+
118
+ def as_prompt_block(self, limit: int = 14) -> str:
119
+ """One-shot block for LLM prompts — core files + why."""
120
+ core = self.core_files(limit=limit)
121
+ lines = []
122
+ for c in core:
123
+ lines.append(f"- {c['path']} // {c['why']}")
124
+ snippet = self.file_snippet(c["path"], max_lines=24)
125
+ if snippet:
126
+ # cap per-file snippet to 24 lines to stay within 6k budget
127
+ lines.append("```python")
128
+ lines.append(snippet)
129
+ lines.append("```")
130
+ return "\n".join(lines)
@@ -0,0 +1,379 @@
1
+ """
2
+ Self-Report Verifier (Dream Assistant spec — Live/Audit Mode, priority #1).
3
+
4
+ The whole point: don't trust the agent's "tests pass" / "this works" claim.
5
+ Independently re-run the project's real test suite (and optionally a scan),
6
+ compare against the last known-good baseline, and emit an authoritative,
7
+ plain-language truth report.
8
+
9
+ Usage:
10
+ p verify — re-run tests (+ scan), diff against baseline, report truth
11
+ p verify --no-scan — only re-run tests
12
+ p verify --claim "tests pass" — assert a specific claim against actual truth
13
+
14
+ This reuses the project's own test runner (pytest by default) and Patchi's
15
+ Brain scan — it never trusts a self-report.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import logging
21
+ import re
22
+ import shutil
23
+ import subprocess
24
+ import time
25
+ from dataclasses import dataclass, field
26
+ from datetime import UTC, datetime
27
+ from pathlib import Path
28
+
29
+ _BASELINE_FILE = ".patchi/verify_baseline.json"
30
+
31
+
32
+ _log = logging.getLogger("patchi.brain.verify")
33
+
34
+
35
+ @dataclass
36
+ class TestRun:
37
+ __test__ = False # tell pytest this is not a test class
38
+ command: str = ""
39
+ passed: int = 0
40
+ failed: int = 0
41
+ error: int = 0
42
+ skipped: int = 0
43
+ total: int = 0
44
+ failed_names: list[str] = field(default_factory=list)
45
+ raw: str = ""
46
+ exit_ok: bool = False
47
+ duration_seconds: float = 0.0
48
+ note: str = ""
49
+
50
+ @property
51
+ def truthful(self) -> bool:
52
+ """True only if the suite actually ran and nothing failed/errored."""
53
+ return bool(self.command) and self.exit_ok and self.failed == 0 and self.error == 0
54
+
55
+
56
+ @dataclass
57
+ class VerifyReport:
58
+ __test__ = False
59
+ scanned_at: str = ""
60
+ test_run: TestRun | None = None
61
+ findings_count: int = 0
62
+ charter_violations: int = 0
63
+ scan_error: str | None = None
64
+ baseline: dict | None = None
65
+ regression: dict | None = None
66
+ truthful: bool = False
67
+ summary: str = ""
68
+
69
+ def to_dict(self) -> dict:
70
+ return {
71
+ "scanned_at": self.scanned_at,
72
+ "truthful": self.truthful,
73
+ "tests": {
74
+ "passed": self.test_run.passed if self.test_run else 0,
75
+ "failed": self.test_run.failed if self.test_run else 0,
76
+ "error": self.test_run.error if self.test_run else 0,
77
+ "skipped": self.test_run.skipped if self.test_run else 0,
78
+ "truthful": self.test_run.truthful if self.test_run else False,
79
+ },
80
+ "findings_count": self.findings_count,
81
+ "charter_violations": self.charter_violations,
82
+ "regression": self.regression,
83
+ "summary": self.summary,
84
+ }
85
+
86
+
87
+ # ── Test runner ────────────────────────────────────────────────────────────────
88
+
89
+
90
+ def detect_test_command(root: Path) -> str | None:
91
+ """Pick the project's real test runner. Prefer pytest; fall back to unittest."""
92
+ if shutil.which("pytest"):
93
+ return "pytest -q -p no:cacheprovider"
94
+ return "python -m unittest discover -s tests -v"
95
+
96
+
97
+ def run_tests(root: Path, command: str | None = None) -> TestRun:
98
+ """Independently execute the project's test suite. Never trusts a claim."""
99
+ cmd = command or detect_test_command(root)
100
+ if not cmd:
101
+ return TestRun(note="No test runner (pytest/unittest) found.")
102
+ start = time.monotonic()
103
+ try:
104
+ proc = subprocess.run(
105
+ cmd.split(),
106
+ cwd=str(root),
107
+ capture_output=True,
108
+ text=True,
109
+ timeout=600,
110
+ )
111
+ except Exception as e: # pragma: no cover - environment dependent
112
+ return TestRun(command=cmd, note=f"Could not run tests: {e}")
113
+ raw = (proc.stdout or "") + "\n" + (proc.stderr or "")
114
+ dur = time.monotonic() - start
115
+ run = _parse_pytest_output(raw)
116
+ run.command = cmd
117
+ run.raw = raw
118
+ run.exit_ok = proc.returncode == 0
119
+ run.duration_seconds = round(dur, 2)
120
+ if not run.total and "no tests ran" in raw:
121
+ run.note = "No tests found."
122
+ return run
123
+
124
+
125
+ def _parse_pytest_output(text: str) -> TestRun:
126
+ run = TestRun()
127
+ for metric, attr in (
128
+ ("passed", "passed"),
129
+ ("failed", "failed"),
130
+ ("error", "error"),
131
+ ("skipped", "skipped"),
132
+ ):
133
+ m = re.search(rf"(\d+)\s+{metric}", text)
134
+ if m:
135
+ setattr(run, attr, int(m.group(1)))
136
+ run.total = run.passed + run.failed + run.error + run.skipped
137
+ run.failed_names = re.findall(r"FAILED\s+([^\s-]+)", text)
138
+ return run
139
+
140
+
141
+ # ── Baseline (regression radar) ───────────────────────────────────────────────────
142
+
143
+
144
+ def _baseline_path(root: Path) -> Path:
145
+ return root / _BASELINE_FILE
146
+
147
+
148
+ def load_baseline(root: Path) -> dict | None:
149
+ p = _baseline_path(root)
150
+ if not p.exists():
151
+ return None
152
+ try:
153
+ return json.loads(p.read_text(encoding="utf-8"))
154
+ except Exception as e:
155
+ _log.warning("load_baseline failed: %s", e)
156
+ return None
157
+
158
+
159
+ def save_baseline(root: Path, snapshot: dict) -> None:
160
+ p = _baseline_path(root)
161
+ p.parent.mkdir(parents=True, exist_ok=True)
162
+ tmp = p.with_suffix(".tmp")
163
+ tmp.write_text(json.dumps(snapshot, indent=2), encoding="utf-8")
164
+ tmp.replace(p)
165
+
166
+
167
+ def compute_regression(
168
+ baseline: dict | None,
169
+ test_run: TestRun,
170
+ findings_count: int,
171
+ charter_violations: int,
172
+ ) -> dict:
173
+ """Diff current truth against the last known-good baseline."""
174
+ if not baseline:
175
+ return {
176
+ "has_baseline": False,
177
+ "new_failures": list(test_run.failed_names),
178
+ "new_findings": findings_count,
179
+ "new_charter_violations": charter_violations,
180
+ }
181
+ base_fail = set(baseline.get("failed_names", []))
182
+ cur_fail = set(test_run.failed_names)
183
+ base_find = baseline.get("findings_count", 0)
184
+ base_charter = baseline.get("charter_violations", 0)
185
+ return {
186
+ "has_baseline": True,
187
+ "new_failures": sorted(cur_fail - base_fail),
188
+ "resolved_failures": sorted(base_fail - cur_fail),
189
+ "new_findings": max(0, findings_count - base_find),
190
+ "new_charter_violations": max(0, charter_violations - base_charter),
191
+ }
192
+
193
+
194
+ # ── Verify orchestration ─────────────────────────────────────────────────────────
195
+
196
+
197
+ def verify_project(
198
+ root: Path,
199
+ test_runner=None,
200
+ run_scan: bool = True,
201
+ ) -> VerifyReport:
202
+ """
203
+ Independently verify the project's actual state.
204
+
205
+ test_runner: injectable for tests; defaults to run_tests().
206
+ Returns a VerifyReport with an authoritative truth + regression delta.
207
+ """
208
+ root = Path(root)
209
+ test_run = (test_runner or run_tests)(root)
210
+
211
+ findings_count = 0
212
+ charter_violations = 0
213
+ scan_error = None
214
+ if run_scan:
215
+ try:
216
+ from patchi.core.brain.brain import Brain
217
+
218
+ Brain(root).scan()
219
+ findings_count = _count_scan_findings(root)
220
+ charter_violations = _count_charter_violations(root)
221
+ except Exception as e:
222
+ scan_error = str(e)
223
+
224
+ baseline = load_baseline(root)
225
+ regression = compute_regression(baseline, test_run, findings_count, charter_violations)
226
+
227
+ truthful = test_run.truthful and not regression.get("new_failures") and charter_violations == 0
228
+
229
+ save_baseline(
230
+ root,
231
+ {
232
+ "scanned_at": datetime.now(UTC).isoformat(),
233
+ "failed_names": test_run.failed_names,
234
+ "findings_count": findings_count,
235
+ "charter_violations": charter_violations,
236
+ },
237
+ )
238
+
239
+ summary = _build_summary(truthful, test_run, regression, charter_violations, scan_error)
240
+ return VerifyReport(
241
+ scanned_at=datetime.now(UTC).isoformat(),
242
+ test_run=test_run,
243
+ findings_count=findings_count,
244
+ charter_violations=charter_violations,
245
+ scan_error=scan_error,
246
+ baseline=baseline,
247
+ regression=regression,
248
+ truthful=truthful,
249
+ summary=summary,
250
+ )
251
+
252
+
253
+ def _count_scan_findings(root: Path) -> int:
254
+ """Count findings from the most recent scan results in memory."""
255
+ try:
256
+ from patchi.core import memory as mem
257
+
258
+ results = mem.get_scan_results(root)
259
+ total = 0
260
+ for _name, data in results.items():
261
+ for f in data.get("findings", []):
262
+ if isinstance(f, dict):
263
+ total += 1
264
+ return total
265
+ except Exception as e:
266
+ _log.warning("_count_scan_findings failed: %s", e)
267
+ return 0
268
+
269
+
270
+ def _count_charter_violations(root: Path) -> int:
271
+ try:
272
+ from patchi.core import memory as mem
273
+
274
+ results = mem.get_scan_results(root)
275
+ return len(results.get("CharterGuard", {}).get("findings", []))
276
+ except Exception as e:
277
+ _log.warning("_count_charter_violations failed: %s", e)
278
+ return 0
279
+
280
+
281
+ def _build_summary(truthful, test_run, regression, charter_violations, scan_error) -> str:
282
+ if truthful:
283
+ return (
284
+ f"VERIFIED ✅ — tests: {test_run.passed} passed"
285
+ + (f", {test_run.skipped} skipped" if test_run.skipped else "")
286
+ + (
287
+ f"; {regression.get('new_findings', 0)} new scan finding(s)"
288
+ if regression.get("new_findings")
289
+ else "; no new scan findings"
290
+ )
291
+ + "."
292
+ )
293
+ parts = ["UNVERIFIED ❌ — mismatch between claim and reality:"]
294
+ if not test_run.truthful:
295
+ parts.append(
296
+ f" • tests: {test_run.failed} failed, {test_run.error} errored"
297
+ + (f" -> {', '.join(test_run.failed_names[:5])}" if test_run.failed_names else "")
298
+ )
299
+ if regression.get("new_failures"):
300
+ parts.append(f" • new failures vs baseline: {', '.join(regression['new_failures'][:5])}")
301
+ if charter_violations:
302
+ parts.append(f" • {charter_violations} charter violation(s)")
303
+ if scan_error:
304
+ parts.append(f" • scan could not run: {scan_error}")
305
+ return "\n".join(parts)
306
+
307
+
308
+ def claim_holds(claim: str, report: VerifyReport) -> bool:
309
+ """Crude claim check: 'tests pass' / 'works' must match an actually truthful run."""
310
+ low = claim.lower()
311
+ if "pass" in low or "works" in low or "green" in low:
312
+ return report.truthful
313
+ return report.truthful
314
+
315
+
316
+ def verify_ui_findings(root: Path, findings: list[dict]) -> tuple[list[dict], list[dict]]:
317
+ """Gate UI/test findings against the codebase before they enter memory.
318
+
319
+ A finding is kept only if its file resolves inside the project and any
320
+ claimed line exists in that file. Summary findings ("(all pages)", empty
321
+ file) always pass. Fail-open: anything unreadable is kept — only
322
+ provably-bogus entries (missing file, out-of-range line) are dropped.
323
+ Returns (kept, dropped).
324
+ """
325
+ try:
326
+ from patchi.core.brain.file_corpus import FileCorpus
327
+
328
+ entries = FileCorpus(root).entries
329
+ except Exception:
330
+ entries = {}
331
+
332
+ kept: list[dict] = []
333
+ dropped: list[dict] = []
334
+ for f in findings:
335
+ if not isinstance(f, dict):
336
+ kept.append(f)
337
+ continue
338
+ rel = (f.get("file") or "").strip()
339
+ if not rel or rel == "(all pages)":
340
+ kept.append(f)
341
+ continue
342
+ # URL routes (UI findings reference pages, not files): fail open.
343
+ # Full route verification needs RouteMapper contract scope, which is
344
+ # unavailable here; file findings below are still strictly gated.
345
+ if rel.startswith("/"):
346
+ kept.append(f)
347
+ continue
348
+ rel = rel.replace("\\", "/").lstrip("/")
349
+ if rel in entries:
350
+ try:
351
+ n_lines = len(entries[rel].read().splitlines())
352
+ except Exception:
353
+ kept.append(f)
354
+ continue
355
+ else:
356
+ cand = (root / rel).resolve()
357
+ try:
358
+ cand.relative_to(root.resolve())
359
+ except ValueError:
360
+ dropped.append({**f, "drop_reason": "file outside project"})
361
+ continue
362
+ if not cand.is_file():
363
+ dropped.append({**f, "drop_reason": "file not found"})
364
+ continue
365
+ try:
366
+ n_lines = len(cand.read_text(encoding="utf-8", errors="replace").splitlines())
367
+ except OSError:
368
+ kept.append(f)
369
+ continue
370
+ line = f.get("line", 0) or 0
371
+ try:
372
+ line = int(line)
373
+ except (TypeError, ValueError):
374
+ line = 0
375
+ if line > 0 and line > n_lines:
376
+ dropped.append({**f, "drop_reason": f"line {line} beyond {n_lines} lines"})
377
+ continue
378
+ kept.append(f)
379
+ return kept, dropped
@@ -0,0 +1,14 @@
1
+ """Campaigns module — state transitions and data flow abuse campaigns."""
2
+
3
+ from .base import Campaign, CampaignStep
4
+ from .data_flow_abuse import DataFlowAbuseCampaign
5
+ from .orchestrator import CampaignOrchestrator
6
+ from .state_transitions import StateTransitionCampaign
7
+
8
+ __all__ = [
9
+ "Campaign",
10
+ "CampaignStep",
11
+ "CampaignOrchestrator",
12
+ "StateTransitionCampaign",
13
+ "DataFlowAbuseCampaign",
14
+ ]
@@ -0,0 +1,62 @@
1
+ """Base campaign class and CampaignStep."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import abc
6
+ import logging
7
+ from dataclasses import dataclass, field
8
+
9
+ from patchi.core.assurance.graph import AssuranceGraph
10
+
11
+ _log = logging.getLogger("patchi.core.campaigns")
12
+
13
+
14
+ @dataclass
15
+ class CampaignStep:
16
+ """One step in a campaign."""
17
+
18
+ name: str
19
+ description: str
20
+ status: str = "pending" # "pending", "running", "passed", "failed"
21
+ findings: list[dict] = field(default_factory=list)
22
+
23
+ def to_dict(self) -> dict:
24
+ return {
25
+ "name": self.name,
26
+ "description": self.description,
27
+ "status": self.status,
28
+ "findings_count": len(self.findings),
29
+ }
30
+
31
+
32
+ class Campaign(abc.ABC):
33
+ """Base class for assurance campaigns."""
34
+
35
+ name: str = "base"
36
+ description: str = ""
37
+
38
+ def __init__(self, graph: AssuranceGraph):
39
+ self._graph = graph
40
+
41
+ @abc.abstractmethod
42
+ def steps(self) -> list[CampaignStep]:
43
+ """Define the steps in this campaign."""
44
+
45
+ def run(self) -> list[CampaignStep]:
46
+ """Execute all steps and return results."""
47
+ results: list[CampaignStep] = []
48
+ for step in self.steps():
49
+ try:
50
+ step.status = "running"
51
+ self._execute_step(step)
52
+ step.status = "failed" if step.findings else "passed"
53
+ except Exception as e:
54
+ _log.warning("Campaign %s step %s failed: %s", self.name, step.name, e)
55
+ step.status = "failed"
56
+ step.findings.append({"error": str(e)})
57
+ results.append(step)
58
+ return results
59
+
60
+ @abc.abstractmethod
61
+ def _execute_step(self, step: CampaignStep) -> None:
62
+ """Execute a single step (modifies step.findings in place)."""