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,168 @@
1
+ """
2
+ Chain-to-Assurance Bridge — feeds exploit chains and intent gaps into
3
+ the assurance graph as disproved claims with evidence trails.
4
+
5
+ Each multi-step chain becomes a claim like "auth bypass leads to data
6
+ exfiltration via 4-step chain" with refuting evidence from each step.
7
+ Each intent gap becomes a claim like "state-changing route lacks auth"
8
+ with refuting evidence from the route analysis.
9
+
10
+ This lets the assurance page show cross-file attack paths alongside
11
+ the invariant-based claims.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import logging
18
+ import time
19
+ from pathlib import Path
20
+
21
+ _log = logging.getLogger("patchi.security.chain_to_assurance")
22
+
23
+
24
+ def feed_chains_to_graph(root: Path) -> int:
25
+ """Load persisted chain/intent data and upsert into the assurance graph.
26
+
27
+ Returns the number of claims added/updated.
28
+ """
29
+ ci_path = root / ".patchi" / "chain_intent.json"
30
+ if not ci_path.is_file():
31
+ return 0
32
+
33
+ try:
34
+ ci_data = json.loads(ci_path.read_text(encoding="utf-8"))
35
+ except Exception as exc:
36
+ _log.debug("Failed to load chain_intent.json: %s", exc)
37
+ return 0
38
+
39
+ chains = ci_data.get("chains", [])
40
+ intent = ci_data.get("intent_report")
41
+ if not chains and not intent:
42
+ return 0
43
+
44
+ try:
45
+ from patchi.core.assurance.graph import AssuranceGraph, Evidence
46
+ except ImportError as exc:
47
+ _log.debug("Assurance graph not available: %s", exc)
48
+ return 0
49
+
50
+ graph = AssuranceGraph.load(root)
51
+ count = 0
52
+
53
+ # ── Chains → Claims ──────────────────────────────────────────────────
54
+ for i, chain in enumerate(chains):
55
+ severity = chain.get("severity", "medium")
56
+ score = chain.get("score", 0)
57
+ steps = chain.get("steps", [])
58
+ narrative = chain.get("narrative", "")
59
+
60
+ if len(steps) < 2:
61
+ continue
62
+
63
+ claim_id = f"chain-{i + 1}-{'-'.join(s.get('type', 'unknown')[:10] for s in steps[:3])}"
64
+ entry_type = steps[0].get("type", "unknown")
65
+ impact_type = steps[-1].get("type", "unknown")
66
+
67
+ statement = (
68
+ f"Attack chain: {entry_type} → {impact_type} "
69
+ f"({len(steps)} steps, severity={severity}, score={score:.0f})"
70
+ )
71
+
72
+ graph.upsert_claim(
73
+ claim_id=claim_id,
74
+ statement=statement,
75
+ domain="exploit-chain",
76
+ severity_if_disproved=severity,
77
+ )
78
+
79
+ # Each step is refuting evidence (the chain proves the path exists)
80
+ for j, step in enumerate(steps):
81
+ # Attach remediation suggestion
82
+ step_type = step.get("type", "")
83
+ try:
84
+ from patchi.core.security.remediation import get_remediation
85
+
86
+ rem = get_remediation(step_type)
87
+ fix_hint = rem.action if rem else ""
88
+ except ImportError:
89
+ fix_hint = ""
90
+
91
+ ev = Evidence(
92
+ source=f"chain_analyzer.step{j + 1}",
93
+ detail=f"[{step.get('role', '?')}] {step.get('type', '?')} @ {step.get('file', '?')}:{step.get('line', '?')}"
94
+ + (f" → FIX: {fix_hint}" if fix_hint else ""),
95
+ supports=False, # refuting = vulnerability confirmed
96
+ timestamp=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
97
+ artifact={
98
+ "step": j + 1,
99
+ "role": step.get("role", ""),
100
+ "agent": step.get("agent", ""),
101
+ "remediation": fix_hint,
102
+ },
103
+ )
104
+ graph.attach_evidence(claim_id, ev)
105
+ count += 1
106
+
107
+ # Narrative evidence
108
+ if narrative:
109
+ ev = Evidence(
110
+ source="chain_analyzer.narrative",
111
+ detail=narrative[:500],
112
+ supports=False,
113
+ timestamp=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
114
+ )
115
+ graph.attach_evidence(claim_id, ev)
116
+ count += 1
117
+
118
+ # ── Intent Gaps → Claims ─────────────────────────────────────────────
119
+ if intent:
120
+ gap_categories = [
121
+ ("unauthenticated_state_changing", "State-changing routes lack authentication", "high"),
122
+ ("admin_without_strict_guard", "Admin routes have no strict auth guard", "high"),
123
+ ("unprotected_among_protected", "Routes unprotected among protected peers", "medium"),
124
+ ]
125
+
126
+ for cat_key, description, sev in gap_categories:
127
+ routes = intent.get(cat_key, [])
128
+ if not routes:
129
+ continue
130
+
131
+ claim_id = f"intent-gap-{cat_key}"
132
+ statement = f"{description} ({len(routes)} routes)"
133
+
134
+ graph.upsert_claim(
135
+ claim_id=claim_id,
136
+ statement=statement,
137
+ domain="intent-gap",
138
+ severity_if_disproved=sev,
139
+ )
140
+
141
+ for r in routes:
142
+ method = r.get("method", "?")
143
+ path = r.get("path", "?")
144
+ file_ = r.get("file", "?")
145
+ line = r.get("line", "?")
146
+ has_guard = r.get("has_auth_guard", False)
147
+
148
+ ev = Evidence(
149
+ source="intent_analyzer",
150
+ detail=f"{method} {path} @ {file_}:{line} (guard={'yes' if has_guard else 'no'})",
151
+ supports=False,
152
+ timestamp=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
153
+ artifact={"route": path, "method": method, "has_guard": has_guard},
154
+ )
155
+ graph.attach_evidence(claim_id, ev)
156
+ count += 1
157
+
158
+ # Save
159
+ if count > 0:
160
+ graph.save(root)
161
+ _log.info(
162
+ "Fed %d evidence items into assurance graph (chains=%d, intent gaps=%d)",
163
+ count,
164
+ len(chains),
165
+ len([c for c in gap_categories if intent.get(c[0])]) if intent else 0,
166
+ )
167
+
168
+ return count
@@ -0,0 +1,510 @@
1
+ """
2
+ Charter — Project Guard Rails.
3
+
4
+ Lets the user declare what the project *should be* via structured rules:
5
+
6
+ p charter set "Frontend must not import backend"
7
+ p charter set "All files must have headers"
8
+ p charter set "No hardcoded secrets"
9
+
10
+ Rules are stored in ``.patchi/memory/charter.json`` with explicit fields
11
+ (no regex/heuristics — structured matching only).
12
+
13
+ Three enforcement points:
14
+ 1. Pre-commit (humans): checks changed files against charter
15
+ 2. Watch mode (realtime): flags violations on file save
16
+ 3. Governor (agents): charter rules become escalation triggers
17
+
18
+ Rule types:
19
+ boundary — forbidden import edges (from -> to)
20
+ convention — file/line limits, naming, test requirements
21
+ security — forbidden patterns (secrets, eval, pickle)
22
+ stack — allowed languages, frameworks
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import json
28
+ import logging
29
+ from dataclasses import dataclass, field
30
+ from enum import StrEnum
31
+ from pathlib import Path
32
+ from typing import Any
33
+
34
+ from patchi.core.config import MEMORY_DIR
35
+
36
+ _log = logging.getLogger("patchi.charter")
37
+
38
+ CHARTER_FILE = "charter.json"
39
+
40
+
41
+ class RuleType(StrEnum):
42
+ STACK = "stack"
43
+ BOUNDARY = "boundary"
44
+ CONVENTION = "convention"
45
+ SECURITY = "security"
46
+
47
+
48
+ class Severity(StrEnum):
49
+ CRITICAL = "critical"
50
+ HIGH = "high"
51
+ MEDIUM = "medium"
52
+ LOW = "low"
53
+
54
+
55
+ @dataclass
56
+ class CharterRule:
57
+ """A single project guard-rail rule with explicit fields."""
58
+
59
+ id: str
60
+ type: RuleType
61
+ description: str
62
+ severity: Severity = Severity.MEDIUM
63
+ enabled: bool = True
64
+
65
+ # Boundary fields
66
+ from_pattern: str = "" # source subsystem/path prefix
67
+ to_pattern: str = "" # target subsystem/path prefix
68
+
69
+ # Convention fields
70
+ max_value: int = 0 # max lines/size
71
+ metric: str = "" # "lines", "file_size", "functions"
72
+ scope: str = "" # "functions", "classes", "all_files"
73
+
74
+ # Security fields
75
+ pattern_type: str = "" # "forbidden_keywords", "forbidden_imports"
76
+ keywords: list[str] = field(default_factory=list)
77
+
78
+ # Stack fields
79
+ allowed_languages: list[str] = field(default_factory=list)
80
+ allowed_frameworks: list[str] = field(default_factory=list)
81
+
82
+ # Generic metadata
83
+ metadata: dict[str, Any] = field(default_factory=dict)
84
+
85
+ def to_dict(self) -> dict[str, Any]:
86
+ d = {
87
+ "id": self.id,
88
+ "type": self.type.value,
89
+ "description": self.description,
90
+ "severity": self.severity.value,
91
+ "enabled": self.enabled,
92
+ }
93
+ if self.from_pattern:
94
+ d["from_pattern"] = self.from_pattern
95
+ if self.to_pattern:
96
+ d["to_pattern"] = self.to_pattern
97
+ if self.max_value:
98
+ d["max_value"] = self.max_value
99
+ if self.metric:
100
+ d["metric"] = self.metric
101
+ if self.scope:
102
+ d["scope"] = self.scope
103
+ if self.keywords:
104
+ d["keywords"] = self.keywords
105
+ if self.allowed_languages:
106
+ d["allowed_languages"] = self.allowed_languages
107
+ if self.allowed_frameworks:
108
+ d["allowed_frameworks"] = self.allowed_frameworks
109
+ if self.metadata:
110
+ d["metadata"] = self.metadata
111
+ return d
112
+
113
+ @classmethod
114
+ def from_dict(cls, d: dict[str, Any]) -> CharterRule:
115
+ return cls(
116
+ id=d["id"],
117
+ type=RuleType(d["type"]),
118
+ description=d.get("description", ""),
119
+ severity=Severity(d.get("severity", "medium")),
120
+ enabled=d.get("enabled", True),
121
+ from_pattern=d.get("from_pattern", ""),
122
+ to_pattern=d.get("to_pattern", ""),
123
+ max_value=d.get("max_value", 0),
124
+ metric=d.get("metric", ""),
125
+ scope=d.get("scope", ""),
126
+ keywords=d.get("keywords", []),
127
+ allowed_languages=d.get("allowed_languages", []),
128
+ allowed_frameworks=d.get("allowed_frameworks", []),
129
+ metadata=d.get("metadata", {}),
130
+ )
131
+
132
+
133
+ @dataclass
134
+ class CharterViolation:
135
+ """A detected violation of a charter rule."""
136
+
137
+ rule_id: str
138
+ rule_description: str
139
+ severity: str
140
+ file_path: str = ""
141
+ line: int = 0
142
+ message: str = ""
143
+ suggestion: str = ""
144
+
145
+ def to_dict(self) -> dict:
146
+ return {
147
+ "rule_id": self.rule_id,
148
+ "rule_description": self.rule_description,
149
+ "severity": self.severity,
150
+ "file_path": self.file_path,
151
+ "line": self.line,
152
+ "message": self.message,
153
+ "suggestion": self.suggestion,
154
+ }
155
+
156
+
157
+ @dataclass
158
+ class Charter:
159
+ """The full project charter — a collection of guard-rail rules."""
160
+
161
+ text: str = ""
162
+ rules: list[CharterRule] = field(default_factory=list)
163
+ created_at: str = ""
164
+ updated_at: str = ""
165
+
166
+ def to_dict(self) -> dict:
167
+ return {
168
+ "text": self.text,
169
+ "rules": [r.to_dict() for r in self.rules],
170
+ "created_at": self.created_at,
171
+ "updated_at": self.updated_at,
172
+ }
173
+
174
+ @classmethod
175
+ def from_dict(cls, d: dict) -> Charter:
176
+ return cls(
177
+ text=d.get("text", ""),
178
+ rules=[CharterRule.from_dict(r) for r in d.get("rules", [])],
179
+ created_at=d.get("created_at", ""),
180
+ updated_at=d.get("updated_at", ""),
181
+ )
182
+
183
+ def add_rule(self, text: str) -> CharterRule | None:
184
+ """Add a rule from natural language text using structured parsing."""
185
+ rules = parse_nl_to_rules(text)
186
+ if rules:
187
+ self.rules.extend(rules)
188
+ return rules[0]
189
+ return None
190
+
191
+ @classmethod
192
+ def load(cls, root: Path | None = None) -> Charter:
193
+ """Load charter from disk, or return empty."""
194
+ return load_charter(root)
195
+
196
+
197
+ # ── Structured NL Parser (no regex) ─────────────────────────────────────────
198
+
199
+
200
+ def _classify_text(text: str) -> RuleType:
201
+ """Classify natural language text into a rule type using keyword matching."""
202
+ lower = text.lower()
203
+
204
+ # Boundary: must/must not/cannot import, never import
205
+ boundary_words = ["must not import", "cannot import", "never import",
206
+ "should not import", "must not use", "cannot use"]
207
+ if any(w in lower for w in boundary_words):
208
+ return RuleType.BOUNDARY
209
+
210
+ # Convention: limits, naming, tests
211
+ convention_words = ["under", "below", "less than", "max", "maximum",
212
+ "must have", "requires tests", "need tests",
213
+ "all files must", "every file", "snake_case", "camelCase"]
214
+ if any(w in lower for w in convention_words):
215
+ return RuleType.CONVENTION
216
+
217
+ # Security: secrets, eval, pickle
218
+ security_words = ["no secrets", "no hardcoded", "no eval", "no pickle",
219
+ "no marshal", "no debug", "use https", "parameterized"]
220
+ if any(w in lower for w in security_words):
221
+ return RuleType.SECURITY
222
+
223
+ # Stack: languages, frameworks
224
+ stack_words = ["only python", "only javascript", "use go", "use rust",
225
+ "frameworks allowed", "languages allowed", "stack is"]
226
+ if any(w in lower for w in stack_words):
227
+ return RuleType.STACK
228
+
229
+ return RuleType.CONVENTION # default
230
+
231
+
232
+ def _parse_boundary(text: str) -> CharterRule:
233
+ """Parse a boundary rule from text."""
234
+ lower = text.lower()
235
+
236
+ # Extract source and target from "X must not import Y"
237
+ from_pattern = ""
238
+ to_pattern = ""
239
+
240
+ # Try to find the pattern: "X must not import Y" or "X cannot import Y"
241
+ for sep in ["must not import", "cannot import", "never import",
242
+ "should not import", "must not use", "cannot use"]:
243
+ if sep in lower:
244
+ parts = lower.split(sep)
245
+ if len(parts) == 2:
246
+ from_pattern = parts[0].strip().rstrip()
247
+ to_pattern = parts[1].strip().lstrip()
248
+ break
249
+
250
+ # Clean up extracted patterns
251
+ # Remove leading articles/determiners
252
+ for prefix in ["the ", "a ", "an "]:
253
+ if from_pattern.startswith(prefix):
254
+ from_pattern = from_pattern[len(prefix):]
255
+ if to_pattern.startswith(prefix):
256
+ to_pattern = to_pattern[len(prefix):]
257
+
258
+ return CharterRule(
259
+ id="b-001",
260
+ type=RuleType.BOUNDARY,
261
+ description=text,
262
+ severity=Severity.HIGH,
263
+ from_pattern=from_pattern,
264
+ to_pattern=to_pattern,
265
+ )
266
+
267
+
268
+ def _parse_convention(text: str) -> CharterRule:
269
+ """Parse a convention rule from text."""
270
+ lower = text.lower()
271
+
272
+ # Extract numeric limits
273
+ max_value = 0
274
+ metric = ""
275
+ scope = ""
276
+
277
+ # Try "under N lines" pattern
278
+ if "under" in lower or "below" in lower or "less than" in lower:
279
+ for word in lower.split():
280
+ if word.isdigit():
281
+ max_value = int(word)
282
+ break
283
+ if "lines" in lower:
284
+ metric = "lines"
285
+ # Extract scope
286
+ for s in ["file", "files", "function", "functions",
287
+ "class", "classes", "service", "services"]:
288
+ if s in lower:
289
+ scope = s + "s" if not s.endswith("s") else s
290
+ break
291
+
292
+ # Try "must have tests" pattern
293
+ if "must have" in lower or "need" in lower or "require" in lower:
294
+ if "test" in lower:
295
+ scope = "tests_required"
296
+
297
+ # Try naming conventions
298
+ if "snake_case" in lower:
299
+ scope = "snake_case"
300
+ elif "camelCase" in lower:
301
+ scope = "camel_case"
302
+ elif "PascalCase" in lower:
303
+ scope = "pascal_case"
304
+
305
+ # Try "all files must have X" pattern
306
+ if "all files must" in lower or "every file" in lower:
307
+ if "header" in lower:
308
+ scope = "headers_required"
309
+ elif "docstring" in lower:
310
+ scope = "docstrings_required"
311
+
312
+ return CharterRule(
313
+ id="c-001",
314
+ type=RuleType.CONVENTION,
315
+ description=text,
316
+ severity=Severity.MEDIUM,
317
+ max_value=max_value,
318
+ metric=metric,
319
+ scope=scope,
320
+ )
321
+
322
+
323
+ def _parse_security(text: str) -> CharterRule:
324
+ """Parse a security rule from text."""
325
+ lower = text.lower()
326
+
327
+ # Extract forbidden keywords
328
+ keywords = []
329
+ if "hardcoded" in lower or "hard coded" in lower:
330
+ keywords.extend(["password", "secret", "api_key", "token"])
331
+ if "eval" in lower:
332
+ keywords.append("eval")
333
+ if "pickle" in lower:
334
+ keywords.append("pickle")
335
+ if "marshal" in lower:
336
+ keywords.append("marshal")
337
+ if "debug" in lower:
338
+ keywords.append("debug")
339
+ if "http" in lower and "https" not in lower:
340
+ keywords.append("http://")
341
+ if "parameterized" in lower:
342
+ keywords.append("raw_sql")
343
+
344
+ return CharterRule(
345
+ id="s-001",
346
+ type=RuleType.SECURITY,
347
+ description=text,
348
+ severity=Severity.HIGH if keywords else Severity.MEDIUM,
349
+ pattern_type="forbidden_keywords",
350
+ keywords=keywords,
351
+ )
352
+
353
+
354
+ def _parse_stack(text: str) -> CharterRule:
355
+ """Parse a stack rule from text."""
356
+ lower = text.lower()
357
+
358
+ # Extract allowed languages
359
+ allowed_languages = []
360
+ lang_keywords = {
361
+ "python": "python", "javascript": "javascript", "typescript": "typescript",
362
+ "go": "go", "rust": "rust", "java": "java", "c#": "csharp",
363
+ }
364
+ for kw, lang in lang_keywords.items():
365
+ if kw in lower:
366
+ allowed_languages.append(lang)
367
+
368
+ return CharterRule(
369
+ id="t-001",
370
+ type=RuleType.STACK,
371
+ description=text,
372
+ severity=Severity.MEDIUM,
373
+ allowed_languages=allowed_languages,
374
+ )
375
+
376
+
377
+ def parse_nl_to_rules(text: str) -> list[CharterRule]:
378
+ """Parse natural language into structured rules (no regex, no heuristics).
379
+
380
+ Uses keyword matching to classify text into rule types, then extracts
381
+ explicit fields for each rule type.
382
+ """
383
+ rules = []
384
+ rule_type = _classify_text(text)
385
+
386
+ if rule_type == RuleType.BOUNDARY:
387
+ rules.append(_parse_boundary(text))
388
+ elif rule_type == RuleType.CONVENTION:
389
+ rules.append(_parse_convention(text))
390
+ elif rule_type == RuleType.SECURITY:
391
+ rules.append(_parse_security(text))
392
+ elif rule_type == RuleType.STACK:
393
+ rules.append(_parse_stack(text))
394
+
395
+ return rules
396
+
397
+
398
+ # ── Rule Checking (structured matching) ──────────────────────────────────────
399
+
400
+
401
+ def check_boundary_violations(
402
+ charter: Charter,
403
+ import_edges: list[tuple[str, str]],
404
+ ) -> list[CharterViolation]:
405
+ """Check if import edges violate boundary rules using structured matching."""
406
+ violations: list[CharterViolation] = []
407
+ boundary_rules = [r for r in charter.rules
408
+ if r.type == RuleType.BOUNDARY and r.enabled]
409
+
410
+ for rule in boundary_rules:
411
+ from_pat = rule.from_pattern.lower()
412
+ to_pat = rule.to_pattern.lower()
413
+
414
+ if not from_pat or not to_pat:
415
+ continue
416
+
417
+ for src, dst in import_edges:
418
+ # Check if source matches from_pattern and target matches to_pattern
419
+ src_lower = src.lower()
420
+ dst_lower = dst.lower()
421
+
422
+ # Match using prefix matching (structured, not regex)
423
+ if from_pat in src_lower and to_pat in dst_lower:
424
+ violations.append(CharterViolation(
425
+ rule_id=rule.id,
426
+ rule_description=rule.description,
427
+ severity=rule.severity.value,
428
+ file_path=src,
429
+ message=f"'{src}' imports '{dst}' — violates boundary rule",
430
+ suggestion=f"Remove import of {to_pat} from {from_pat}",
431
+ ))
432
+
433
+ return violations
434
+
435
+
436
+ def check_convention_violations(
437
+ charter: Charter,
438
+ file_infos: list[Any],
439
+ ) -> list[CharterViolation]:
440
+ """Check files against convention rules using structured matching."""
441
+ violations: list[CharterViolation] = []
442
+ conv_rules = [r for r in charter.rules
443
+ if r.type == RuleType.CONVENTION and r.enabled]
444
+
445
+ for rule in conv_rules:
446
+ if rule.max_value > 0 and rule.metric == "lines":
447
+ for fi in file_infos:
448
+ path = getattr(fi, "path", "")
449
+ if path.endswith((".py", ".ts", ".js", ".go", ".java", ".rs")):
450
+ line_count = getattr(fi, "line_count", 0)
451
+ if line_count and line_count > rule.max_value:
452
+ violations.append(CharterViolation(
453
+ rule_id=rule.id,
454
+ rule_description=rule.description,
455
+ severity=rule.severity.value,
456
+ file_path=path,
457
+ message=f"{path} is {line_count} lines (max {rule.max_value})",
458
+ suggestion=f"Split into modules under {rule.max_value} lines",
459
+ ))
460
+
461
+ return violations
462
+
463
+
464
+ def check_all_violations(
465
+ charter: Charter,
466
+ import_edges: list[tuple[str, str]] | None = None,
467
+ file_infos: list[Any] | None = None,
468
+ ) -> list[CharterViolation]:
469
+ """Run all charter checks and return combined violations."""
470
+ violations: list[CharterViolation] = []
471
+
472
+ if import_edges:
473
+ violations.extend(check_boundary_violations(charter, import_edges))
474
+
475
+ if file_infos:
476
+ violations.extend(check_convention_violations(charter, file_infos))
477
+
478
+ return violations
479
+
480
+
481
+ # ── Persistence ───────────────────────────────────────────────────────────────
482
+
483
+
484
+ def _charter_path(root: Path | None = None) -> Path:
485
+ r = root or Path.cwd()
486
+ return r / MEMORY_DIR / CHARTER_FILE
487
+
488
+
489
+ def load_charter(root: Path | None = None) -> Charter:
490
+ """Load charter from disk, or return empty."""
491
+ path = _charter_path(root)
492
+ if not path.exists():
493
+ return Charter()
494
+ try:
495
+ data = json.loads(path.read_text(encoding="utf-8"))
496
+ return Charter.from_dict(data)
497
+ except Exception:
498
+ _log.warning("Failed to load charter from %s", path)
499
+ return Charter()
500
+
501
+
502
+ def save_charter(charter: Charter, root: Path | None = None) -> Path:
503
+ """Save charter to disk."""
504
+ path = _charter_path(root)
505
+ path.parent.mkdir(parents=True, exist_ok=True)
506
+ path.write_text(
507
+ json.dumps(charter.to_dict(), indent=2, ensure_ascii=False),
508
+ encoding="utf-8",
509
+ )
510
+ return path