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,269 @@
1
+ """
2
+ Reasoning Engine — Phase 3 of the Patchi super-agent.
3
+
4
+ Sits *above* the Layered Brain and turns it into answers. Given a change-set, a
5
+ file, or a natural-language question, it reasons over the cached layer summaries
6
+ (never re-reading raw source) and produces structured, human-readable output:
7
+
8
+ - impact_analysis(changed_files) → blast radius across layers
9
+ - explain(target) → a layer's summary + its dependency context
10
+ - why(path) → why a file matters (dependents, purpose)
11
+ - ask(question) → NL answer built from layer summaries
12
+
13
+ Everything is offline-safe (pure heuristics over the Layered Brain). If an LLM
14
+ is configured, :meth:`ReasoningEngine.ask` can optionally enrich the answer.
15
+
16
+ This module is additive: it only reads the Layered Brain persisted at
17
+ ``.patchi/memory/layers.json`` and existing ``layered_brain`` helpers.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ import re
23
+ from dataclasses import dataclass, field
24
+ from pathlib import Path
25
+ from typing import TYPE_CHECKING
26
+
27
+ from patchi.core.brain.brain_watcher import ChangeSet, affected_layers
28
+ from patchi.core.brain.layered_brain import (
29
+ find_layers_for_file,
30
+ get_layer_context,
31
+ layers_from_dict,
32
+ )
33
+
34
+ _log = logging.getLogger("patchi.brain.reasoning")
35
+
36
+ if TYPE_CHECKING:
37
+ from patchi.core.brain.layered_brain import Layer
38
+
39
+
40
+ @dataclass
41
+ class ImpactAnalysis:
42
+ """Structured result of reasoning about a change-set."""
43
+
44
+ changed_files: list[str] = field(default_factory=list)
45
+ affected_layers: list[str] = field(default_factory=list) # directly touched
46
+ impacted_layers: list[str] = field(default_factory=list) # blast radius (dependents)
47
+ summary: str = ""
48
+
49
+ def to_dict(self) -> dict:
50
+ return {
51
+ "changed_files": self.changed_files,
52
+ "affected_layers": self.affected_layers,
53
+ "impacted_layers": self.impacted_layers,
54
+ "summary": self.summary,
55
+ }
56
+
57
+
58
+ class ReasoningEngine:
59
+ """Answers questions using the cached Layered Brain (no raw-source reads)."""
60
+
61
+ def __init__(self, root: Path):
62
+ self.root = Path(root)
63
+ self.layers: dict[str, Layer] = self._load_layers()
64
+
65
+ def _load_layers(self) -> dict[str, Layer]:
66
+ from patchi.core import memory as mem
67
+
68
+ data = mem.get_layers(self.root) or {}
69
+ if not data.get("layers"):
70
+ return {}
71
+ return layers_from_dict(data)
72
+
73
+ # ── Impact analysis (blast radius) ──────────────────────────────────────────
74
+
75
+ def impact_analysis(self, changed_files: list[str]) -> ImpactAnalysis:
76
+ if not self.layers:
77
+ return ImpactAnalysis(
78
+ changed_files=changed_files,
79
+ summary="No layered brain available. Run `p scan` first to build it.",
80
+ )
81
+
82
+ changes = ChangeSet(changed=set(changed_files))
83
+ affected = affected_layers(changes, self.layers)
84
+
85
+ # Blast radius: transitively follow dependents of every affected layer.
86
+ impacted: set[str] = set()
87
+ stack = list(affected)
88
+ while stack:
89
+ cur = stack.pop()
90
+ layer = self.layers.get(cur)
91
+ if not layer:
92
+ continue
93
+ for dep in layer.dependents:
94
+ if dep in self.layers and dep not in impacted:
95
+ impacted.add(dep)
96
+ stack.append(dep)
97
+
98
+ blast = impacted - affected
99
+ summary = self._impact_summary(changed_files, affected, blast)
100
+ return ImpactAnalysis(
101
+ changed_files=changed_files,
102
+ affected_layers=sorted(affected),
103
+ impacted_layers=sorted(blast),
104
+ summary=summary,
105
+ )
106
+
107
+ def _impact_summary(self, changed, affected, blast) -> str:
108
+ if not changed:
109
+ return "No changes provided."
110
+ parts = [f"{len(changed)} file(s) changed"]
111
+ if affected:
112
+ parts.append(
113
+ f"{len(affected)} layer(s) directly affected: {', '.join(sorted(affected))}"
114
+ )
115
+ if blast:
116
+ parts.append(
117
+ f"{len(blast)} downstream layer(s) in the blast radius: {', '.join(sorted(blast))}"
118
+ )
119
+ else:
120
+ parts.append("no downstream layers depend on the changed code")
121
+ return "; ".join(parts) + "."
122
+
123
+ # ── Explain a layer / file ───────────────────────────────────────────────────
124
+
125
+ def explain(self, target: str) -> dict:
126
+ """Return a layer's summary plus its direct dependency context.
127
+
128
+ ``target`` may be a file path or a layer name.
129
+ """
130
+ if not self.layers:
131
+ return {"error": "No layered brain available. Run `p scan` first."}
132
+
133
+ # Resolve to a layer name.
134
+ if target in self.layers:
135
+ layer_name = target
136
+ else:
137
+ matches = find_layers_for_file(self.layers, target)
138
+ if not matches:
139
+ return {"error": f"No layer found for '{target}'."}
140
+ layer_name = matches[0]
141
+
142
+ layer = self.layers[layer_name]
143
+ context = get_layer_context(self.layers, layer_name, max_depth=1)
144
+ return {
145
+ "layer": layer_name,
146
+ "level": layer.level,
147
+ "summary": layer.summary,
148
+ "purpose": layer.purpose,
149
+ "depends_on": layer.depends_on,
150
+ "dependents": layer.dependents,
151
+ "context": {k: v for k, v in context.items() if k != layer_name},
152
+ }
153
+
154
+ # ── Why does a file matter ───────────────────────────────────────────────────
155
+
156
+ def why(self, path: str) -> dict:
157
+ """Explain why a file matters: its layer, dependents, and purpose."""
158
+ if not self.layers:
159
+ return {"error": "No layered brain available. Run `p scan` first."}
160
+
161
+ matches = find_layers_for_file(self.layers, path)
162
+ if not matches:
163
+ return {"error": f"'{path}' is not part of any known layer."}
164
+
165
+ layer_name = matches[0]
166
+ layer = self.layers[layer_name]
167
+ dependents = layer.dependents
168
+ importance = "critical" if dependents else ("shared" if len(layer.files) > 1 else "leaf")
169
+ return {
170
+ "file": path,
171
+ "layer": layer_name,
172
+ "layer_summary": layer.summary,
173
+ "purpose": layer.purpose,
174
+ "depended_on_by": dependents,
175
+ "importance": importance,
176
+ "files_in_layer": len(layer.files),
177
+ }
178
+
179
+ # ── Natural-language问答 (TF-IDF over layers + body_tags, LLM optional) ─
180
+
181
+ def ask(self, question: str, use_ai: bool = False, ask_ai: bool | None = None, **_) -> str:
182
+ """Answer a question from layer summaries. TF-IDF ranked, optionally LLM-synthesized.
183
+
184
+ Spec L3: RAG over cached layers[].summary built at scan time (layers.json rag_index),
185
+ query via cosine, ask_ai=True wraps RAG context + call_ai. use_ai is alias for backward compat.
186
+ """
187
+ if ask_ai is not None:
188
+ use_ai = bool(ask_ai)
189
+ if not self.layers:
190
+ return (
191
+ "I don't have a brain-map of this project yet. "
192
+ "Run `p scan` to build the layered brain, then ask again."
193
+ )
194
+
195
+ q = question.lower()
196
+ tokens = {t for t in re.findall(r"[a-z0-9_]+", q) if len(t) > 2}
197
+ if not tokens:
198
+ return "Could you rephrase? Try naming a subsystem (auth, api, data, ui…)."
199
+
200
+ # Try understander/body_tags boost if available
201
+ body_boost: dict[str, float] = {}
202
+ try:
203
+ from patchi.core.brain.body_tags import load_body_tags
204
+
205
+ tags = load_body_tags(self.root)
206
+ for _path, tag in tags.items():
207
+ layer_hint = tag.get("layer", "")
208
+ if layer_hint:
209
+ body_boost[layer_hint] = max(body_boost.get(layer_hint, 0), float(tag.get("score", 0)) / 100.0)
210
+ except Exception:
211
+ body_boost = {}
212
+
213
+ # TF-IDF-ish: term freq * inverse doc freq approximation
214
+ doc_freq: dict[str, int] = {}
215
+ for t in tokens:
216
+ df = sum(1 for n, layer in self.layers.items() if t in f"{n} {layer.summary} {layer.purpose}".lower())
217
+ doc_freq[t] = max(1, df)
218
+ import math
219
+
220
+ scored: list[tuple[float, str, Layer]] = []
221
+ for name, layer in self.layers.items():
222
+ if layer.level == 4:
223
+ continue
224
+ text = f"{name} {layer.summary} {layer.purpose}".lower()
225
+ tfidf = 0.0
226
+ for t in tokens:
227
+ if t in text:
228
+ tf = text.count(t)
229
+ idf = math.log(len(self.layers) / doc_freq[t] + 1)
230
+ tfidf += tf * idf
231
+ tfidf += body_boost.get(name, 0.0)
232
+ if tfidf > 0:
233
+ scored.append((tfidf, name, layer))
234
+
235
+ if not scored:
236
+ return (
237
+ "I couldn't match that to any layer I know. "
238
+ "Known layers: "
239
+ + ", ".join(sorted(n for n in self.layers if n != "__project__"))
240
+ + "."
241
+ )
242
+
243
+ scored.sort(key=lambda x: x[0], reverse=True)
244
+ top = scored[:3]
245
+ if use_ai:
246
+ try:
247
+ import os
248
+
249
+ if not os.environ.get("PATCHI_OFFLINE"):
250
+ from patchi.core import config as cfg
251
+ from patchi.core.ai.client import call_ai
252
+
253
+ cfgd = cfg.load(self.root) if hasattr(cfg, "load") else {}
254
+ ctx = "\n".join(f"[{n}] {lay.summary} — {lay.purpose}" for _, n, lay in top)
255
+ ans = call_ai(cfgd, "You are Patchi understander. Answer from layered brain context.", f"Question: {question}\n\nContext:\n{ctx}", max_tokens=400)
256
+ if ans:
257
+ return ans
258
+ except Exception as _exc:
259
+ _log.debug("reasoning AI synthesis fallback: %s", _exc)
260
+ lines = [f"Based on the layered brain, here's what I know about '{question.strip()}':", ""]
261
+ for _, name, layer in top:
262
+ lines.append(f"[bold]{name}[/bold] ({_level_name(layer.level)}): {layer.summary}")
263
+ lines.append("")
264
+ lines.append("[dim]Answers are derived from cached layer summaries + body_tags ranking.[/dim]")
265
+ return "\n".join(lines)
266
+
267
+
268
+ def _level_name(level: int) -> str:
269
+ return {1: "module", 2: "subsystem", 4: "project"}.get(level, f"L{level}")
@@ -0,0 +1,15 @@
1
+ from patchi.core.brain.route_detector.base import BaseRouteDetector
2
+ from patchi.core.brain.route_detector.registry import (
3
+ _DETECTOR_REGISTRY,
4
+ detect_routes,
5
+ get_detector,
6
+ register_detector,
7
+ )
8
+
9
+ __all__ = [
10
+ "BaseRouteDetector",
11
+ "detect_routes",
12
+ "get_detector",
13
+ "register_detector",
14
+ "_DETECTOR_REGISTRY",
15
+ ]
@@ -0,0 +1,31 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+
4
+ class BaseRouteDetector(ABC):
5
+ @abstractmethod
6
+ def detect(self, content: str, file_path: str) -> list[dict]: ...
7
+
8
+ def _make_route(
9
+ self,
10
+ method: str,
11
+ path: str,
12
+ handler: str,
13
+ file_path: str,
14
+ line: int,
15
+ framework: str = "",
16
+ middleware: list[str] | None = None,
17
+ auth_required: bool | None = None,
18
+ ) -> dict:
19
+ return {
20
+ "method": method,
21
+ "path": path,
22
+ "handler": handler,
23
+ "file": file_path,
24
+ "line": line,
25
+ "middleware": middleware or [],
26
+ "auth_required": auth_required,
27
+ "framework": framework,
28
+ }
29
+
30
+ def _path_from_parts(self, parts: list[str]) -> str:
31
+ return "/" + "/".join(parts).replace("[", ":").replace("]", "")
@@ -0,0 +1,209 @@
1
+ """
2
+ C# route detector — ASP.NET Core attribute-based routing.
3
+
4
+ Node types below (attribute, attribute_list, attribute_argument_list,
5
+ attribute_argument, method_declaration, class_declaration) were verified
6
+ directly against the installed tree-sitter-c-sharp grammar, not assumed from
7
+ generic documentation — including that both `method_declaration` and
8
+ `attribute` expose a reliable `name` field via child_by_field_name, which is
9
+ what this walker relies on instead of guessing child position.
10
+ """
11
+
12
+ import logging
13
+ import re
14
+
15
+ from patchi.core.brain.route_detector.base import BaseRouteDetector
16
+ from patchi.core.brain.route_detector.registry import register_detector
17
+
18
+ _log = logging.getLogger("patchi.brain.csharp")
19
+
20
+ _HTTP_ATTRS = {
21
+ "httpget": "GET",
22
+ "httppost": "POST",
23
+ "httpput": "PUT",
24
+ "httpdelete": "DELETE",
25
+ "httppatch": "PATCH",
26
+ "httphead": "HEAD",
27
+ "httpoptions": "OPTIONS",
28
+ }
29
+
30
+
31
+ @register_detector("c_sharp")
32
+ class CSharpRouteDetector(BaseRouteDetector):
33
+ def detect(self, content: str, file_path: str) -> list[dict]:
34
+ routes = self._detect_ts(content, file_path)
35
+ if routes:
36
+ return routes
37
+ return self._detect_fallback(content, file_path)
38
+
39
+ def _detect_ts(self, content: str, file_path: str) -> list[dict] | None:
40
+ try:
41
+ from patchi.core.brain.languages import Lang, get_parser
42
+
43
+ parser = get_parser(Lang.C_SHARP)
44
+ if parser is None:
45
+ return None
46
+ tree = parser.parse(content.encode("utf-8"))
47
+ except Exception as e:
48
+ _log.warning("CSharpRouteDetector._detect_ts failed: %s", e)
49
+ return None
50
+
51
+ routes: list[dict] = []
52
+ self._walk(tree.root_node, content, file_path, routes)
53
+ return routes
54
+
55
+ def _walk(self, node: object, content: str, file_path: str, routes: list[dict]) -> None:
56
+ ntype = getattr(node, "type", "")
57
+ if ntype == "method_declaration":
58
+ self._check_method(node, content, file_path, routes)
59
+ for child in getattr(node, "children", []):
60
+ self._walk(child, content, file_path, routes)
61
+
62
+ def _check_method(
63
+ self, method_node: object, content: str, file_path: str, routes: list[dict]
64
+ ) -> None:
65
+ http_method = None
66
+ route_suffix = ""
67
+ method_auth = False
68
+
69
+ for child in getattr(method_node, "children", []):
70
+ if child.type != "attribute_list":
71
+ continue
72
+ for attr in getattr(child, "children", []):
73
+ if attr.type != "attribute":
74
+ continue
75
+ name = self._attr_name(attr, content).lower()
76
+ if name in _HTTP_ATTRS:
77
+ http_method = _HTTP_ATTRS[name]
78
+ route_suffix = self._attr_string_arg(attr, content)
79
+ elif name == "route" and http_method is None:
80
+ http_method = "ANY"
81
+ route_suffix = self._attr_string_arg(attr, content)
82
+ elif name in ("authorize",):
83
+ method_auth = True
84
+
85
+ if http_method is None:
86
+ return
87
+
88
+ class_prefix, class_auth = self._enclosing_class_context(method_node, content)
89
+ path = self._combine_path(class_prefix, route_suffix)
90
+
91
+ name_node = (
92
+ method_node.child_by_field_name("name")
93
+ if hasattr(method_node, "child_by_field_name")
94
+ else None
95
+ )
96
+ handler = self._node_text(name_node, content) if name_node else ""
97
+ line = getattr(method_node, "start_point", (0, 0))[0] + 1
98
+
99
+ auth = method_auth or class_auth
100
+ routes.append(
101
+ self._make_route(
102
+ http_method,
103
+ path,
104
+ handler,
105
+ file_path,
106
+ line,
107
+ framework="ASP.NET Core",
108
+ auth_required=auth if auth else None,
109
+ )
110
+ )
111
+
112
+ def _enclosing_class_context(self, node: object, content: str) -> tuple[str, bool]:
113
+ """Walk up to the enclosing class_declaration for its [Route] prefix and [Authorize]."""
114
+ cur = node
115
+ for _ in range(10):
116
+ cur = getattr(cur, "parent", None)
117
+ if cur is None:
118
+ break
119
+ if getattr(cur, "type", "") == "class_declaration":
120
+ prefix = ""
121
+ auth = False
122
+ for child in getattr(cur, "children", []):
123
+ if child.type != "attribute_list":
124
+ continue
125
+ for attr in getattr(child, "children", []):
126
+ if attr.type != "attribute":
127
+ continue
128
+ name = self._attr_name(attr, content).lower()
129
+ if name == "route":
130
+ prefix = self._attr_string_arg(attr, content)
131
+ elif name == "authorize":
132
+ auth = True
133
+ return prefix, auth
134
+ return "", False
135
+
136
+ def _combine_path(self, prefix: str, suffix: str) -> str:
137
+ # [controller] is ASP.NET's token substitution for the class name minus
138
+ # "Controller" -- approximated here rather than resolved exactly, since
139
+ # getting it exact requires the class name, which is a reasonable
140
+ # follow-up refinement rather than a blocker for a first working version.
141
+ parts = [p.strip("/") for p in (prefix, suffix) if p]
142
+ return "/" + "/".join(parts) if parts else ""
143
+
144
+ def _attr_name(self, attr_node: object, content: str) -> str:
145
+ name_node = (
146
+ attr_node.child_by_field_name("name")
147
+ if hasattr(attr_node, "child_by_field_name")
148
+ else None
149
+ )
150
+ return self._node_text(name_node, content) if name_node else ""
151
+
152
+ def _attr_string_arg(self, attr_node: object, content: str) -> str:
153
+ for child in getattr(attr_node, "children", []):
154
+ if child.type != "attribute_argument_list":
155
+ continue
156
+ for arg in getattr(child, "children", []):
157
+ if arg.type == "attribute_argument":
158
+ for lit in getattr(arg, "children", []):
159
+ if lit.type == "string_literal":
160
+ return self._node_text(lit, content).strip('"')
161
+ return ""
162
+
163
+ def _node_text(self, node: object, content: str) -> str:
164
+ try:
165
+ return content[node.start_byte : node.end_byte]
166
+ except Exception as e:
167
+ _log.debug("CSharpRouteDetector._node_text failed: %s", e)
168
+ return ""
169
+
170
+ def _detect_fallback(self, content: str, file_path: str) -> list[dict]:
171
+ routes: list[dict] = []
172
+ verb_names = "|".join(_HTTP_ATTRS)
173
+ pat = re.compile(rf'\[({verb_names}|Route)(?:\("([^"]*)"\))?\]', re.IGNORECASE)
174
+ auth_pat = re.compile(r"\[Authorize\]", re.IGNORECASE)
175
+
176
+ lines = content.splitlines()
177
+ for i, line in enumerate(lines, 1):
178
+ m = pat.search(line)
179
+ if not m:
180
+ continue
181
+ verb_raw = m.group(1).lower()
182
+ method = _HTTP_ATTRS.get(verb_raw, "ANY")
183
+ path = "/" + (m.group(2) or "").strip("/")
184
+
185
+ handler = ""
186
+ for j in range(i, min(i + 5, len(lines) + 1)):
187
+ hm = re.search(
188
+ r"\b(?:public|private|protected)\s+\w+(?:<[\w,\s]+>)?\s+(\w+)\s*\(",
189
+ lines[j - 1],
190
+ )
191
+ if hm:
192
+ handler = hm.group(1)
193
+ break
194
+
195
+ ctx = "\n".join(lines[max(0, i - 5) : i])
196
+ auth = bool(auth_pat.search(ctx))
197
+
198
+ routes.append(
199
+ self._make_route(
200
+ method,
201
+ path,
202
+ handler,
203
+ file_path,
204
+ i,
205
+ framework="ASP.NET Core",
206
+ auth_required=auth if auth else None,
207
+ )
208
+ )
209
+ return routes
@@ -0,0 +1,81 @@
1
+ import re
2
+ from pathlib import PurePosixPath
3
+
4
+ from patchi.core.brain.route_detector.base import BaseRouteDetector
5
+
6
+
7
+ class NextJsRouteDetector(BaseRouteDetector):
8
+ def detect(self, content: str, file_path: str) -> list[dict]:
9
+ routes: list[dict] = []
10
+ p = PurePosixPath(file_path)
11
+ parts = p.parts
12
+
13
+ if "pages" in parts and "api" in parts:
14
+ idx = parts.index("pages")
15
+ path_parts = parts[idx + 1 :]
16
+ url_path = "/" + "/".join(path_parts).replace("[", ":").replace("]", "")
17
+ url_path = re.sub(r"\.(?:js|ts|jsx|tsx)$", "", url_path)
18
+ if url_path.endswith("/index"):
19
+ url_path = url_path[:-6] or "/"
20
+ routes.append(
21
+ self._make_route("ANY", url_path, p.stem, file_path, 1, framework="Next.js")
22
+ )
23
+
24
+ elif "app" in parts and p.stem in ("route", "page"):
25
+ idx = parts.index("app")
26
+ path_parts = [x for x in parts[idx + 1 : -1] if not x.startswith("(")]
27
+ url_path = "/" + "/".join(path_parts).replace("[", ":").replace("]", "")
28
+ if not url_path:
29
+ url_path = "/"
30
+ routes.append(
31
+ self._make_route(
32
+ "ANY" if p.stem == "route" else "GET",
33
+ url_path,
34
+ p.stem,
35
+ file_path,
36
+ 1,
37
+ framework="Next.js",
38
+ )
39
+ )
40
+
41
+ return routes
42
+
43
+
44
+ class NuxtRouteDetector(BaseRouteDetector):
45
+ def detect(self, content: str, file_path: str) -> list[dict]:
46
+ routes: list[dict] = []
47
+ p = PurePosixPath(file_path)
48
+ if "pages" not in p.parts:
49
+ return routes
50
+
51
+ idx = p.parts.index("pages")
52
+ path_parts = list(p.parts[idx + 1 :])
53
+ stem = PurePosixPath(path_parts[-1]).stem if path_parts else ""
54
+ path_parts[-1] = stem
55
+ url_path = "/" + "/".join(path_parts).replace("[", ":").replace("]", "")
56
+ if url_path.endswith("/index"):
57
+ url_path = url_path[:-6] or "/"
58
+ routes.append(self._make_route("GET", url_path, stem, file_path, 1, framework="Nuxt"))
59
+ return routes
60
+
61
+
62
+ class SvelteKitRouteDetector(BaseRouteDetector):
63
+ def detect(self, content: str, file_path: str) -> list[dict]:
64
+ routes: list[dict] = []
65
+ p = PurePosixPath(file_path)
66
+ if "routes" not in p.parts:
67
+ return routes
68
+
69
+ idx = p.parts.index("routes")
70
+ path_parts = [x for x in p.parts[idx + 1 : -1] if not x.startswith("(")]
71
+ url_path = "/" + "/".join(path_parts).replace("[", ":").replace("]", "")
72
+ if not url_path:
73
+ url_path = "/"
74
+
75
+ method = (
76
+ "ANY" if (p.name.endswith("+server.ts") or p.name.endswith("+server.js")) else "GET"
77
+ )
78
+ routes.append(
79
+ self._make_route(method, url_path, p.stem, file_path, 1, framework="SvelteKit")
80
+ )
81
+ return routes