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,121 @@
1
+ """
2
+ Project context for context-aware agent gating.
3
+
4
+ Central place to determine whether a security agent is relevant
5
+ to the project being scanned, based on its inferred domain and purpose.
6
+
7
+ Usage:
8
+ from patchi.core.brain.project_context import agent_is_relevant
9
+ if not agent_is_relevant(inp):
10
+ self.skip(result, "Not relevant for this project type")
11
+ return result
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ _WEB_DOMAINS = {"web-app", "web", "e-commerce", "saas", "social", "cms", "portal"}
17
+ _FINANCE_DOMAINS = {"finance", "banking", "fintech", "payments"}
18
+ _HEALTHCARE_DOMAINS = {"healthcare", "health", "medical", "hipaa"}
19
+ _MOBILE_DOMAINS = {"mobile-app", "android", "ios"}
20
+ _DATA_DOMAINS = {"data-pipeline", "etl", "analytics", "data"}
21
+
22
+ # Domain IDs from the security taxonomy that are web-related
23
+ _WEB_DOMAIN_IDS = {"web-frontend", "auth-session"}
24
+
25
+ # Agents that should only run when the project is a user-facing web app
26
+ _WEB_ONLY_AGENTS = {
27
+ "AuthenticationAuditAgent",
28
+ "AuthZAgent",
29
+ "CORS",
30
+ "HeaderAuditAgent",
31
+ "RateLimitAuditor",
32
+ }
33
+
34
+ # Agents that should skip industry-specific rules unless domain matches
35
+ _INDUSTRY_AGENTS = {
36
+ "PolicyEngine",
37
+ "ComplianceAgent",
38
+ "PrivacyAgent",
39
+ }
40
+
41
+ # Agents that are universal (run on any project)
42
+ _UNIVERSAL_AGENTS = {
43
+ "SecretScanner",
44
+ "EnvScanner",
45
+ "DependencyScanner",
46
+ "CVEMonitorAgent",
47
+ "SupplyChainAgent",
48
+ "PreCheckAgent",
49
+ "IaCScanner",
50
+ "ContainerScanner",
51
+ "SSRFProtection",
52
+ "InjectionAgent",
53
+ "CryptoAgent",
54
+ "NetworkAgent",
55
+ "JWTSecurityAgent",
56
+ "SensitiveDataAgent",
57
+ "MisconfigAgent",
58
+ "TaintAnalyzer",
59
+ "RedTeamAgent",
60
+ }
61
+
62
+
63
+ def agent_is_relevant(
64
+ domain: str,
65
+ purpose: str,
66
+ agent_name: str,
67
+ active_domains: list[str] | None = None,
68
+ ) -> tuple[bool, str]:
69
+ """
70
+ Check whether a security agent is relevant for a project.
71
+
72
+ Uses the active security domain IDs from the Brain's context phase
73
+ when available (preferred), falling back to domain/purpose string matching.
74
+
75
+ Returns (relevant, reason) where reason is empty if relevant, or explains the skip.
76
+ """
77
+ domain_lower = domain.lower()
78
+ purpose_lower = purpose.lower()
79
+ active = active_domains or []
80
+
81
+ # Universal agents always run
82
+ if agent_name in _UNIVERSAL_AGENTS:
83
+ return True, ""
84
+
85
+ # Web-only agents skip if no web-related domain is activated
86
+ if agent_name in _WEB_ONLY_AGENTS:
87
+ is_web = bool(set(active) & _WEB_DOMAIN_IDS)
88
+ if not is_web:
89
+ is_web = bool(any(d in domain_lower for d in _WEB_DOMAINS) or "web" in purpose_lower)
90
+ if not is_web:
91
+ return False, f"Project domain is '{domain}' — auth checks not applicable"
92
+
93
+ # Industry-specific agents skip unless their domain matches
94
+ if agent_name in _INDUSTRY_AGENTS:
95
+ if not domain_lower and not active:
96
+ return True, ""
97
+ is_finance = any(d in domain_lower for d in _FINANCE_DOMAINS)
98
+ is_health = any(d in domain_lower for d in _HEALTHCARE_DOMAINS)
99
+ if agent_name == "PolicyEngine":
100
+ return True, ""
101
+ if agent_name == "PrivacyAgent":
102
+ if not is_finance and not is_health:
103
+ _has_pii_signals = any(
104
+ w in purpose_lower for w in ("pii", "gdpr", "ccpa", "privacy", "personal")
105
+ )
106
+ if not _has_pii_signals:
107
+ return False, f"Project domain is '{domain}' — privacy rules not applicable"
108
+
109
+ return True, ""
110
+
111
+
112
+ def domain_has_auth(domain: str, routes: list[str]) -> bool:
113
+ """Quick check if a project likely has authentication based on routes."""
114
+ if domain in _WEB_DOMAINS:
115
+ return True
116
+ auth_keywords = {"login", "signin", "auth", "register", "token"}
117
+ for route in routes:
118
+ for kw in auth_keywords:
119
+ if kw in route.lower():
120
+ return True
121
+ return False
@@ -0,0 +1,521 @@
1
+ """
2
+ Smart Project Reader — understands what the project is by reading its files.
3
+
4
+ Extracts:
5
+ - Project name and description from README, package.json, pyproject.toml
6
+ - Tech stack from dependencies
7
+ - Entry points and main modules
8
+ - Project structure and architecture patterns
9
+ - Critical paths that must never break
10
+
11
+ This feeds into the ContractBuilder to make the app contract
12
+ actually understand the project, not just pattern-match routes.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import logging
19
+ from dataclasses import dataclass, field
20
+ from pathlib import Path
21
+
22
+ _log = logging.getLogger("patchi.brain.project_reader")
23
+
24
+
25
+ @dataclass
26
+ class ProjectInsight:
27
+ """What the project reader understands about the project."""
28
+ name: str = ""
29
+ description: str = ""
30
+ tech_stack: list[str] = field(default_factory=list)
31
+ entry_points: list[str] = field(default_factory=list)
32
+ critical_dirs: list[str] = field(default_factory=list)
33
+ project_type: str = "" # "web-app", "cli", "library", "api", "desktop", etc.
34
+ framework: str = ""
35
+ language: str = ""
36
+ dependencies: list[str] = field(default_factory=list)
37
+ dev_dependencies: list[str] = field(default_factory=list)
38
+ scripts: dict[str, str] = field(default_factory=dict)
39
+ readme_summary: str = ""
40
+ architecture_notes: str = ""
41
+
42
+ def to_dict(self) -> dict:
43
+ return {
44
+ "name": self.name,
45
+ "description": self.description,
46
+ "tech_stack": self.tech_stack,
47
+ "entry_points": self.entry_points,
48
+ "critical_dirs": self.critical_dirs,
49
+ "project_type": self.project_type,
50
+ "framework": self.framework,
51
+ "language": self.language,
52
+ "dependencies": self.dependencies[:20],
53
+ "readme_summary": self.readme_summary[:500],
54
+ }
55
+
56
+
57
+ def read_project_insight(root: Path) -> ProjectInsight:
58
+ """Read the project and build a comprehensive understanding."""
59
+ insight = ProjectInsight()
60
+
61
+ # Try multiple sources in order of reliability
62
+ _read_readme(root, insight)
63
+ _read_package_json(root, insight)
64
+ _read_pyproject_toml(root, insight)
65
+ _read_setup_cfg(root, insight)
66
+ _read_cargo_toml(root, insight)
67
+ _read_go_mod(root, insight)
68
+ _read_package_json_ts(root, insight)
69
+
70
+ # Infer project type from structure
71
+ _infer_project_type(root, insight)
72
+
73
+ # Find entry points
74
+ _find_entry_points(root, insight)
75
+
76
+ # Find critical directories
77
+ _find_critical_dirs(root, insight)
78
+
79
+ return insight
80
+
81
+
82
+ def _read_readme(root: Path, insight: ProjectInsight) -> None:
83
+ """Extract project info from README files."""
84
+ for name in ["README.md", "README.rst", "README.txt", "README", "readme.md"]:
85
+ readme_path = root / name
86
+ if readme_path.is_file():
87
+ try:
88
+ content = readme_path.read_text(encoding="utf-8", errors="replace")
89
+ lines = content.strip().splitlines()
90
+
91
+ # Extract title (first # heading or first non-empty line)
92
+ for line in lines:
93
+ stripped = line.strip()
94
+ if stripped.startswith("# "):
95
+ insight.name = stripped[2:].strip()
96
+ break
97
+ elif stripped and not insight.name:
98
+ # First non-empty line as fallback
99
+ insight.name = stripped[:80]
100
+ break
101
+
102
+ # Extract description (first paragraph after title)
103
+ found_title = False
104
+ desc_lines = []
105
+ for line in lines:
106
+ stripped = line.strip()
107
+ if stripped.startswith("# "):
108
+ found_title = True
109
+ continue
110
+ if found_title:
111
+ if stripped == "":
112
+ if desc_lines:
113
+ break
114
+ continue
115
+ if stripped.startswith("#"):
116
+ break
117
+ desc_lines.append(stripped)
118
+
119
+ if desc_lines:
120
+ insight.description = " ".join(desc_lines)[:300]
121
+
122
+ # Extract summary for brain context
123
+ insight.readme_summary = "\n".join(lines[:50])[:500]
124
+ break
125
+ except Exception as e:
126
+ _log.debug("Failed to read %s: %s", name, e)
127
+
128
+
129
+ def _read_package_json(root: Path, insight: ProjectInsight) -> None:
130
+ """Extract project info from package.json."""
131
+ pkg_path = root / "package.json"
132
+ if not pkg_path.is_file():
133
+ return
134
+
135
+ try:
136
+ data = json.loads(pkg_path.read_text(encoding="utf-8"))
137
+ if not insight.name:
138
+ insight.name = data.get("name", "")
139
+ if not insight.description:
140
+ insight.description = data.get("description", "")
141
+
142
+ # Dependencies
143
+ deps = data.get("dependencies", {})
144
+ dev_deps = data.get("devDependencies", {})
145
+ insight.dependencies = list(deps.keys())
146
+ insight.dev_dependencies = list(dev_deps.keys())
147
+
148
+ # Tech stack from dependencies
149
+ _infer_tech_stack_from_deps(insight, deps, dev_deps)
150
+
151
+ # Scripts
152
+ insight.scripts = data.get("scripts", {})
153
+
154
+ # Entry points
155
+ main = data.get("main", "")
156
+ if main:
157
+ insight.entry_points.append(main)
158
+
159
+ bin_entry = data.get("bin", {})
160
+ if isinstance(bin_entry, str):
161
+ insight.entry_points.append(bin_entry)
162
+ elif isinstance(bin_entry, dict):
163
+ insight.entry_points.extend(bin_entry.values())
164
+
165
+ except Exception as e:
166
+ _log.debug("Failed to read package.json: %s", e)
167
+
168
+
169
+ def _read_pyproject_toml(root: Path, insight: ProjectInsight) -> None:
170
+ """Extract project info from pyproject.toml."""
171
+ toml_path = root / "pyproject.toml"
172
+ if not toml_path.is_file():
173
+ return
174
+
175
+ try:
176
+ import tomllib
177
+ except ImportError:
178
+ # Python < 3.11 fallback
179
+ try:
180
+ import tomli as tomllib
181
+ except ImportError:
182
+ return
183
+
184
+ try:
185
+ data = tomllib.loads(toml_path.read_text(encoding="utf-8"))
186
+
187
+ # Project metadata
188
+ project = data.get("project", {})
189
+ if not insight.name:
190
+ insight.name = project.get("name", "")
191
+ if not insight.description:
192
+ insight.description = project.get("description", "")
193
+
194
+ insight.language = "python"
195
+
196
+ # Dependencies
197
+ deps = project.get("dependencies", [])
198
+ insight.dependencies = [_dep_name(d) for d in deps]
199
+
200
+ optional = project.get("optional-dependencies", {})
201
+ for group_deps in optional.values():
202
+ insight.dev_dependencies.extend([_dep_name(d) for d in group_deps])
203
+
204
+ # Scripts / entry points
205
+ scripts = data.get("project", {}).get("scripts", {})
206
+ if scripts:
207
+ insight.scripts.update(scripts)
208
+ insight.entry_points.extend(scripts.values())
209
+
210
+ # Build system hints
211
+ build = data.get("build-system", {})
212
+ build_backend = build.get("build-backend", "")
213
+ if "setuptools" in build_backend:
214
+ insight.tech_stack.append("setuptools")
215
+ elif "poetry" in build_backend:
216
+ insight.tech_stack.append("poetry")
217
+ elif "hatchling" in build_backend:
218
+ insight.tech_stack.append("hatchling")
219
+
220
+ # Tool configs
221
+ if "ruff" in data:
222
+ insight.dev_dependencies.append("ruff")
223
+ if "pytest" in data:
224
+ insight.dev_dependencies.append("pytest")
225
+ if "mypy" in data:
226
+ insight.dev_dependencies.append("mypy")
227
+
228
+ # Infer tech stack
229
+ _infer_tech_stack_from_deps(
230
+ insight,
231
+ dict.fromkeys(insight.dependencies, ""),
232
+ dict.fromkeys(insight.dev_dependencies, ""),
233
+ )
234
+
235
+ except Exception as e:
236
+ _log.debug("Failed to read pyproject.toml: %s", e)
237
+
238
+
239
+ def _read_setup_cfg(root: Path, insight: ProjectInsight) -> None:
240
+ """Extract from setup.cfg."""
241
+ setup_path = root / "setup.cfg"
242
+ if not setup_path.is_file():
243
+ return
244
+
245
+ try:
246
+ import configparser
247
+ cfg = configparser.ConfigParser()
248
+ cfg.read(str(setup_path))
249
+
250
+ if not insight.name:
251
+ insight.name = cfg.get("metadata", "name", fallback="")
252
+ if not insight.description:
253
+ insight.description = cfg.get("metadata", "description", fallback="")
254
+ insight.language = "python"
255
+ except Exception as _exc:
256
+ _log.debug('_read_setup_cfg skipped: %s', _exc)
257
+
258
+
259
+ def _read_cargo_toml(root: Path, insight: ProjectInsight) -> None:
260
+ """Extract from Cargo.toml (Rust projects)."""
261
+ cargo_path = root / "Cargo.toml"
262
+ if not cargo_path.is_file():
263
+ return
264
+
265
+ try:
266
+ import tomllib
267
+ except ImportError:
268
+ try:
269
+ import tomli as tomllib
270
+ except ImportError:
271
+ return
272
+
273
+ try:
274
+ data = tomllib.loads(cargo_path.read_text(encoding="utf-8"))
275
+ pkg = data.get("package", {})
276
+ insight.name = pkg.get("name", "")
277
+ insight.description = pkg.get("description", "")
278
+ insight.language = "rust"
279
+ insight.tech_stack.append("cargo")
280
+
281
+ deps = data.get("dependencies", {})
282
+ insight.dependencies = list(deps.keys())
283
+ except Exception as _exc:
284
+ _log.debug('_read_cargo_toml skipped: %s', _exc)
285
+
286
+
287
+ def _read_go_mod(root: Path, insight: ProjectInsight) -> None:
288
+ """Extract from go.mod (Go projects)."""
289
+ go_mod = root / "go.mod"
290
+ if not go_mod.is_file():
291
+ return
292
+
293
+ try:
294
+ content = go_mod.read_text(encoding="utf-8")
295
+ for line in content.splitlines():
296
+ if line.startswith("module "):
297
+ insight.name = line.split()[-1].split("/")[-1]
298
+ insight.language = "go"
299
+ break
300
+ except Exception as _exc:
301
+ _log.debug('_read_go_mod skipped: %s', _exc)
302
+
303
+
304
+ def _read_package_json_ts(root: Path, insight: ProjectInsight) -> None:
305
+ """Read tsconfig.json for TypeScript projects."""
306
+ tsconfig = root / "tsconfig.json"
307
+ if not tsconfig.is_file():
308
+ return
309
+
310
+ try:
311
+ data = json.loads(tsconfig.read_text(encoding="utf-8"))
312
+ if "compilerOptions" in data:
313
+ insight.tech_stack.append("typescript")
314
+ if insight.language == "":
315
+ insight.language = "typescript"
316
+ except Exception as _exc:
317
+ _log.debug('_read_package_json_ts skipped: %s', _exc)
318
+
319
+
320
+ def _infer_tech_stack_from_deps(
321
+ insight: ProjectInsight,
322
+ deps: dict,
323
+ dev_deps: dict,
324
+ ) -> None:
325
+ """Infer tech stack from dependency names."""
326
+ all_deps = set(list(deps.keys()) + list(dev_deps.keys()))
327
+
328
+ framework_hints = {
329
+ "fastapi": ("FastAPI", "web-app", "python"),
330
+ "flask": ("Flask", "web-app", "python"),
331
+ "django": ("Django", "web-app", "python"),
332
+ "starlette": ("Starlette", "web-app", "python"),
333
+ "uvicorn": ("uvicorn", "web-app", "python"),
334
+ "react": ("React", "web-app", "javascript"),
335
+ "vue": ("Vue", "web-app", "javascript"),
336
+ "angular": ("Angular", "web-app", "javascript"),
337
+ "next": ("Next.js", "web-app", "javascript"),
338
+ "svelte": ("Svelte", "web-app", "javascript"),
339
+ "express": ("Express", "api", "javascript"),
340
+ "koa": ("Koa", "api", "javascript"),
341
+ "hono": ("Hono", "api", "javascript"),
342
+ "gin": ("Gin", "api", "go"),
343
+ "fiber": ("Fiber", "api", "go"),
344
+ "actix": ("Actix", "api", "rust"),
345
+ "axum": ("Axum", "api", "rust"),
346
+ "tauri": ("Tauri", "desktop", "rust"),
347
+ "electron": ("Electron", "desktop", "javascript"),
348
+ }
349
+
350
+ for dep_name, (fw, ptype, lang) in framework_hints.items():
351
+ if dep_name in all_deps:
352
+ if fw not in insight.tech_stack:
353
+ insight.tech_stack.append(fw)
354
+ if not insight.project_type:
355
+ insight.project_type = ptype
356
+ if not insight.language:
357
+ insight.language = lang
358
+ insight.framework = fw
359
+
360
+ # Dev tool hints
361
+ dev_tools = {
362
+ "pytest": "pytest",
363
+ "jest": "jest",
364
+ "vitest": "vitest",
365
+ "mocha": "mocha",
366
+ "eslint": "eslint",
367
+ "prettier": "prettier",
368
+ "ruff": "ruff",
369
+ "mypy": "mypy",
370
+ "typescript": "typescript",
371
+ "playwright": "playwright",
372
+ "cypress": "cypress",
373
+ }
374
+ for tool_name, tool_label in dev_tools.items():
375
+ if tool_name in all_deps and tool_label not in insight.tech_stack:
376
+ insight.tech_stack.append(tool_label)
377
+
378
+
379
+ def _infer_project_type(root: Path, insight: ProjectInsight) -> None:
380
+ """Infer project type from structure if not already known."""
381
+ if insight.project_type:
382
+ return
383
+
384
+ indicators = {
385
+ "web-app": [
386
+ "templates/", "static/", "public/", "pages/", "components/",
387
+ "app.py", "main.py", "server.py", "index.html",
388
+ ],
389
+ "api": [
390
+ "routes/", "handlers/", "controllers/", "endpoints/",
391
+ "api/", "openapi", "swagger",
392
+ ],
393
+ "cli": [
394
+ "cli/", "commands/", "cli.py", "main.py", "__main__.py",
395
+ ],
396
+ "library": [
397
+ "src/", "lib/", "__init__.py",
398
+ ],
399
+ "desktop": [
400
+ "electron/", "tauri/", "src-tauri/", "main.js",
401
+ ],
402
+ }
403
+
404
+ scores: dict[str, int] = {}
405
+ for ptype, indicators_list in indicators.items():
406
+ score = 0
407
+ for indicator in indicators_list:
408
+ if (root / indicator).exists():
409
+ score += 1
410
+ if score:
411
+ scores[ptype] = score
412
+
413
+ if scores:
414
+ insight.project_type = max(scores, key=scores.get)
415
+ else:
416
+ insight.project_type = "unknown"
417
+
418
+
419
+ def _find_entry_points(root: Path, insight: ProjectInsight) -> None:
420
+ """Find main entry points of the project."""
421
+ # Check pyproject.toml scripts
422
+ if insight.scripts:
423
+ for name, cmd in insight.scripts.items():
424
+ if "python" in cmd or "patchi" in cmd:
425
+ insight.entry_points.append(f"pyproject:{name}")
426
+
427
+ # Check common entry points
428
+ candidates = [
429
+ "main.py", "app.py", "server.py", "index.py",
430
+ "main.js", "index.js", "app.js", "server.js",
431
+ "main.ts", "index.ts", "app.ts",
432
+ "src/main.rs", "src/lib.rs",
433
+ "main.go",
434
+ ]
435
+
436
+ for candidate in candidates:
437
+ path = root / candidate
438
+ if path.exists() and candidate not in insight.entry_points:
439
+ insight.entry_points.append(candidate)
440
+
441
+ # Check for __main__.py (Python package entry point)
442
+ for pkg_dir in ["patchi", "src"]:
443
+ main_file = root / pkg_dir / "__main__.py"
444
+ if main_file.exists():
445
+ insight.entry_points.append(f"{pkg_dir}/__main__.py")
446
+
447
+ # Check for CLI entry points in setup.py/setup.cfg
448
+ for setup_file in ["setup.py", "setup.cfg"]:
449
+ setup_path = root / setup_file
450
+ if setup_path.is_file():
451
+ try:
452
+ content = setup_path.read_text(encoding="utf-8", errors="replace")
453
+ if "console_scripts" in content or "entry_points" in content:
454
+ insight.entry_points.append(setup_file)
455
+ except Exception as _exc:
456
+ _log.debug('_find_entry_points skipped: %s', _exc)
457
+
458
+
459
+ def _find_critical_dirs(root: Path, insight: ProjectInsight) -> None:
460
+ """Find directories that are critical to the project."""
461
+ # Skip common non-critical directories
462
+ skip_dirs = {
463
+ ".git", ".venv", "venv", "env", "__pycache__", "node_modules",
464
+ ".mypy_cache", ".pytest_cache", ".ruff_cache", "dist", "build",
465
+ ".eggs", "*.egg-info", ".tox", ".nox", "htmlcov",
466
+ }
467
+
468
+ # Patterns that indicate critical directories
469
+ critical_patterns = {
470
+ "auth": ["auth", "authentication", "authorization", "login", "session"],
471
+ "api": ["api", "routes", "handlers", "controllers", "endpoints"],
472
+ "models": ["models", "schemas", "entities", "types"],
473
+ "services": ["services", "business", "domain", "core"],
474
+ "database": ["db", "database", "migrations", "models"],
475
+ "config": ["config", "settings", "env"],
476
+ "cli": ["cli", "commands"],
477
+ "web": ["web", "templates", "static", "views"],
478
+ "core": ["core", "lib", "src"],
479
+ "tests": ["tests", "test", "spec"],
480
+ }
481
+
482
+ for dirpath in [d for d in root.iterdir() if d.is_dir()]:
483
+ if dirpath.name.startswith(".") or dirpath.name in skip_dirs:
484
+ continue
485
+ # Skip directories with only __pycache__ or hidden files
486
+ try:
487
+ contents = list(dirpath.iterdir())
488
+ real_contents = [c for c in contents if not c.name.startswith(".") and c.name != "__pycache__"]
489
+ if not real_contents:
490
+ continue
491
+ except PermissionError:
492
+ continue
493
+
494
+ dir_lower = dirpath.name.lower()
495
+ matched = False
496
+ for _category, patterns in critical_patterns.items():
497
+ if any(p in dir_lower for p in patterns):
498
+ if dirpath.name not in insight.critical_dirs:
499
+ insight.critical_dirs.append(dirpath.name)
500
+ matched = True
501
+ break
502
+
503
+ # If no pattern matched, check if it's a source code directory
504
+ if not matched:
505
+ # Check if it has Python/JS/TS files
506
+ has_source = False
507
+ try:
508
+ for f in dirpath.iterdir():
509
+ if f.suffix in (".py", ".js", ".ts", ".tsx", ".jsx", ".go", ".rs"):
510
+ has_source = True
511
+ break
512
+ except PermissionError:
513
+ pass
514
+ if has_source and dirpath.name not in insight.critical_dirs:
515
+ insight.critical_dirs.append(dirpath.name)
516
+
517
+
518
+ def _dep_name(dep_string: str) -> str:
519
+ """Extract package name from a dependency string like 'requests>=2.0'."""
520
+ name = dep_string.split(">")[0].split("<")[0].split("=")[0].split("!")[0].split("[")[0].strip()
521
+ return name.lower().replace("-", "_")