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,153 @@
1
+ """
2
+ Ignore lists with expiry for Patchi.
3
+
4
+ Parses inline ignore comments in source code:
5
+ - // patchi-ignore: RULE_NAME YYYY-MM-DD
6
+ - # patchi-ignore: RULE_NAME YYYY-MM-DD
7
+ - /* patchi-ignore: RULE_NAME YYYY-MM-DD */
8
+ - -- patchi-ignore: RULE_NAME YYYY-MM-DD
9
+
10
+ Reports expired ignores as findings.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import re
16
+ from dataclasses import dataclass
17
+ from datetime import date, datetime
18
+ from pathlib import Path
19
+
20
+ from patchi.core.agents.base import Finding, Severity, safe_rglob
21
+
22
+ # Match: comment_start patchi-ignore: RULE_NAME YYYY-MM-DD [optional reason]
23
+ IGNORE_PATTERN = re.compile(
24
+ r"""(?P<comment>[#*\/;{]+)\s*
25
+ patchi-?ignore\s*:\s*
26
+ (?P<rule>\S+)\s*
27
+ (?P<expiry>\d{4}-\d{2}-\d{2})
28
+ (?P<reason>.*?)? # optional reason after date
29
+ (?P<close>\*\/)? # optional */ close for block comments
30
+ """,
31
+ re.IGNORECASE | re.VERBOSE,
32
+ )
33
+
34
+
35
+ _log = logging.getLogger("patchi.brain.ignore_parser")
36
+
37
+
38
+ @dataclass
39
+ class IgnoreDirective:
40
+ """A single patchi-ignore directive found in source."""
41
+
42
+ file: str
43
+ line: int
44
+ rule: str
45
+ expiry: date
46
+ reason: str = ""
47
+ expired: bool = False
48
+ raw: str = ""
49
+
50
+
51
+ def scan_ignore_directives(root: Path) -> list[IgnoreDirective]:
52
+ """
53
+ Scan all source files for patchi-ignore directives.
54
+ Returns list of all directives found, annotated with expired status.
55
+ """
56
+ directives: list[IgnoreDirective] = []
57
+ today = date.today()
58
+
59
+ source_patterns = [
60
+ "*.py",
61
+ "*.js",
62
+ "*.jsx",
63
+ "*.ts",
64
+ "*.tsx",
65
+ "*.java",
66
+ "*.go",
67
+ "*.rs",
68
+ "*.c",
69
+ "*.h",
70
+ "*.cpp",
71
+ "*.cxx",
72
+ "*.cc",
73
+ "*.hpp",
74
+ "*.rb",
75
+ "*.swift",
76
+ "*.kt",
77
+ "*.kts",
78
+ "*.yaml",
79
+ "*.yml",
80
+ "*.json",
81
+ "*.toml",
82
+ "*.html",
83
+ "*.css",
84
+ "*.scss",
85
+ ]
86
+
87
+ for pattern in source_patterns:
88
+ for file_path in safe_rglob(root, pattern):
89
+ if not file_path.is_file():
90
+ continue
91
+ try:
92
+ content = file_path.read_text(encoding="utf-8")
93
+ except Exception as e:
94
+ _log.warning("scan_ignore_directives failed: %s", e)
95
+ continue
96
+
97
+ rel_path = file_path.relative_to(root).as_posix()
98
+ lines = content.splitlines()
99
+
100
+ for line_num, line in enumerate(lines, 1):
101
+ for match in IGNORE_PATTERN.finditer(line):
102
+ try:
103
+ expiry_date = datetime.strptime(match.group("expiry"), "%Y-%m-%d").date()
104
+ except ValueError:
105
+ continue
106
+
107
+ try:
108
+ reason = match.group("reason") or ""
109
+ except IndexError:
110
+ reason = ""
111
+
112
+ directives.append(
113
+ IgnoreDirective(
114
+ file=rel_path,
115
+ line=line_num,
116
+ rule=match.group("rule").strip(),
117
+ expiry=expiry_date,
118
+ reason=reason.strip(),
119
+ expired=expiry_date < today,
120
+ raw=line.strip(),
121
+ )
122
+ )
123
+
124
+ return directives
125
+
126
+
127
+ def find_expired_ignores(root: Path) -> list[IgnoreDirective]:
128
+ """Return only expired ignore directives."""
129
+ return [d for d in scan_ignore_directives(root) if d.expired]
130
+
131
+
132
+ def findings_from_expired_ignores(root: Path) -> list[Finding]:
133
+ """Convert expired ignore directives into findings."""
134
+ findings: list[Finding] = []
135
+ for d in find_expired_ignores(root):
136
+ findings.append(
137
+ Finding(
138
+ agent="IgnoreParser",
139
+ type="expired_ignore_directive",
140
+ severity=Severity.MEDIUM,
141
+ file=d.file,
142
+ line=d.line,
143
+ message=f"Expired patchi-ignore for '{d.rule}'",
144
+ detail=(
145
+ f"The patchi-ignore directive for rule '{d.rule}' expired on {d.expiry}. "
146
+ f"Either remove the suppression or update the expiry date if the issue persists."
147
+ ),
148
+ code_snippet=d.raw,
149
+ suggestion=f"Remove the ignore or set a new expiry: patchi-ignore: {d.rule} YYYY-MM-DD",
150
+ extra={"expiry": d.expiry.isoformat(), "rule": d.rule},
151
+ )
152
+ )
153
+ return findings
@@ -0,0 +1,486 @@
1
+ """
2
+ Import graph — directed graph of file import relationships.
3
+
4
+ Builds a directed graph where edges mean "A imports B".
5
+ Uses FileInfo.imports (already parsed by scanner) — no re-parsing.
6
+
7
+ Used for:
8
+ - Dead file detection (files with no incoming edges from app code)
9
+ - Circular dependency detection
10
+ - Blast radius calculation (how many files break if X changes)
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from collections import defaultdict
16
+ from dataclasses import dataclass
17
+ from pathlib import Path
18
+ from typing import TYPE_CHECKING
19
+
20
+ if TYPE_CHECKING:
21
+ from .scanner import FileInfo
22
+
23
+
24
+ # ── Known source file extensions (for cross-language resolution) ────────
25
+
26
+ KNOWN_EXTENSIONS = frozenset(
27
+ {
28
+ ".py",
29
+ ".pyw",
30
+ ".js",
31
+ ".mjs",
32
+ ".cjs",
33
+ ".jsx",
34
+ ".ts",
35
+ ".tsx",
36
+ ".mts",
37
+ ".rs",
38
+ ".go",
39
+ ".java",
40
+ ".c",
41
+ ".h",
42
+ ".cpp",
43
+ ".cxx",
44
+ ".cc",
45
+ ".hpp",
46
+ ".swift",
47
+ ".rb",
48
+ ".php",
49
+ ".php3",
50
+ ".php4",
51
+ ".php5",
52
+ ".phtml",
53
+ ".kt",
54
+ ".scala",
55
+ ".cs",
56
+ ".dart",
57
+ ".svelte",
58
+ # Config / data languages (parsed by scanner but not TREE_SITTER_LANGS)
59
+ ".html",
60
+ ".htm",
61
+ ".jinja",
62
+ ".jinja2",
63
+ ".j2",
64
+ ".css",
65
+ ".scss",
66
+ ".sass",
67
+ ".sh",
68
+ ".bash",
69
+ ".zsh",
70
+ ".json",
71
+ ".jsonc",
72
+ ".yml",
73
+ ".yaml",
74
+ ".sql",
75
+ # Extensionless filename-based languages (Dockerfile, Makefile, etc.)
76
+ }
77
+ )
78
+
79
+ # ── Data types ─────────────────────────────────────────────────────────────────
80
+
81
+
82
+ @dataclass
83
+ class CircularDep:
84
+ """A detected circular import cycle."""
85
+
86
+ cycle: list[str] # ordered list of files in the cycle
87
+
88
+ @property
89
+ def short_label(self) -> str:
90
+ return " → ".join(self.cycle)
91
+
92
+
93
+ class ImportGraph:
94
+ """
95
+ Directed graph of import relationships.
96
+
97
+ edges[A] = {B, C} means A imports B and C
98
+ reverse[B] = {A} means A imports B (who imports B)
99
+ """
100
+
101
+ def __init__(self) -> None:
102
+ self.nodes: set[str] = set()
103
+ self.edges: dict[str, set[str]] = defaultdict(set)
104
+ self.reverse: dict[str, set[str]] = defaultdict(set)
105
+
106
+ def add_edge(self, importer: str, imported: str) -> None:
107
+ """Record that `importer` imports `imported`."""
108
+ self.nodes.add(importer)
109
+ self.nodes.add(imported)
110
+ self.edges[importer].add(imported)
111
+ self.reverse[imported].add(importer)
112
+
113
+ def to_dict(self) -> dict:
114
+ return {
115
+ "nodes": sorted(self.nodes),
116
+ "edges": {k: sorted(v) for k, v in self.edges.items()},
117
+ }
118
+
119
+ def get_file_context(self, file_path: str) -> dict:
120
+ """Return import context for a file: what it imports, what imports it."""
121
+ return {
122
+ "file": file_path,
123
+ "imports": sorted(self.edges.get(file_path, set())),
124
+ "imported_by": sorted(self.reverse.get(file_path, set())),
125
+ }
126
+
127
+ def get_transitive_dependents(self, file_path: str) -> set[str]:
128
+ """All files that directly or indirectly depend on the given file."""
129
+ visited: set[str] = set()
130
+ queue = list(self.reverse.get(file_path, set()))
131
+ while queue:
132
+ current = queue.pop(0)
133
+ if current in visited:
134
+ continue
135
+ visited.add(current)
136
+ queue.extend(self.reverse.get(current, set()) - visited)
137
+ return visited
138
+
139
+ def format_fix_context(self, file_path: str, max_imports: int = 15) -> str:
140
+ """Human-readable context block for AI fix agents."""
141
+ ctx = self.get_file_context(file_path)
142
+ lines: list[str] = []
143
+ if ctx["imports"]:
144
+ truncated = ctx["imports"][:max_imports]
145
+ lines.append(f" Imports ({len(ctx['imports'])}): " + ", ".join(truncated))
146
+ if len(ctx["imports"]) > max_imports:
147
+ lines.append(f" ... and {len(ctx['imports']) - max_imports} more")
148
+ if ctx["imported_by"]:
149
+ truncated = ctx["imported_by"][:max_imports]
150
+ lines.append(f" Imported by ({len(ctx['imported_by'])}): " + ", ".join(truncated))
151
+ if len(ctx["imported_by"]) > max_imports:
152
+ lines.append(f" ... and {len(ctx['imported_by']) - max_imports} more")
153
+ if not ctx["imports"] and not ctx["imported_by"]:
154
+ lines.append(" (no import relationships found)")
155
+ return "\n".join(lines)
156
+
157
+ def to_dot(self, max_nodes: int = 200) -> str:
158
+ """DOT/Graphviz export for visualization (§1.3.3)."""
159
+ lines = ["digraph G {", " rankdir=LR;", ' node [shape=box, style=rounded];']
160
+ nodes = sorted(self.nodes)[:max_nodes]
161
+ node_set = set(nodes)
162
+ for n in nodes:
163
+ safe = n.replace('"', '\\"')
164
+ lines.append(f' "{safe}";')
165
+ for src in nodes:
166
+ for tgt in sorted(self.edges.get(src, [])):
167
+ if tgt in node_set:
168
+ lines.append(f' "{src.replace(chr(34), chr(92)+chr(34))}" -> "{tgt.replace(chr(34), chr(92)+chr(34))}";')
169
+ lines.append("}")
170
+ return "\n".join(lines)
171
+
172
+
173
+ # ── Builder ────────────────────────────────────────────────────────────────────
174
+
175
+
176
+ def build_graph(files: list[FileInfo], root: Path) -> ImportGraph:
177
+ """
178
+ Build an import graph from a list of FileInfo objects.
179
+
180
+ Reads imports from FileInfo.imports (already parsed by scanner).
181
+ Only resolves imports to local project files; external packages are ignored.
182
+ """
183
+ graph = ImportGraph()
184
+
185
+ # Register every scanned file as a node
186
+ local_files: set[str] = set()
187
+ for fi in files:
188
+ graph.nodes.add(fi.path)
189
+ local_files.add(fi.path)
190
+
191
+ # Resolve each file's imports to local files
192
+ for fi in files:
193
+ for imp in fi.imports:
194
+ resolved = _resolve_to_local(imp.source, fi.path, local_files)
195
+ if resolved and resolved != fi.path:
196
+ graph.add_edge(fi.path, resolved)
197
+
198
+ return graph
199
+
200
+
201
+ def build_import_graph(root: Path) -> ImportGraph:
202
+ """Build an import graph by scanning root directly (no FileInfo list)."""
203
+ from .scanner import FileScanner
204
+
205
+ scanner = FileScanner(root)
206
+ files = scanner.scan()
207
+ return build_graph(files, root)
208
+
209
+
210
+ # Alias used by some older callers
211
+ build_graph_from_root = build_import_graph
212
+
213
+
214
+ # ── Cross-language import resolution ──────────────────────────────────────────
215
+
216
+
217
+ def _resolve_to_local(
218
+ imp: str,
219
+ from_file: str,
220
+ local_files: set[str],
221
+ ) -> str | None:
222
+ """
223
+ Try to resolve an import string to a local project file path.
224
+
225
+ Handles multiple import conventions across languages:
226
+ - Relative paths (.utils, ../helper, ./foo)
227
+ - Path-style imports (fmt, myproject/pkg/util)
228
+ - Dotted module names (os.path, java.util.List)
229
+ - Rust :: separators (std::collections::HashMap, crate::mod::fn)
230
+ - Simple filenames (stdio.h, myheader.h, json)
231
+ """
232
+ imp = imp.strip("\"'<>")
233
+ if not imp:
234
+ return None
235
+
236
+ source_dir = Path(from_file).parent
237
+
238
+ # Strategy 1: Relative path starting with . or /
239
+ if imp.startswith(".") or imp.startswith("/"):
240
+ return _resolve_relative(imp, source_dir, local_files)
241
+
242
+ # Strategy 2: Path-style import (contains /)
243
+ if "/" in imp:
244
+ return _resolve_path(imp, source_dir, local_files)
245
+
246
+ # Strategy 3: Rust :: separator
247
+ if "::" in imp:
248
+ path_style = imp.replace("::", "/")
249
+ candidate = _resolve_path(path_style, source_dir, local_files)
250
+ if candidate:
251
+ return candidate
252
+ for prefix in ("crate/", "self/", "super/"):
253
+ if path_style.startswith(prefix):
254
+ sub = path_style[len(prefix) :]
255
+ r = _resolve_path(sub, source_dir, local_files)
256
+ if r:
257
+ return r
258
+ return None
259
+
260
+ # Strategy 4: Dotted module (Python, Java)
261
+ if "." in imp:
262
+ return _resolve_dotted(imp, source_dir, local_files)
263
+
264
+ # Strategy 5: Bare name — try as file, then as dir/name
265
+ result = _try_extensions(imp, local_files)
266
+ if result:
267
+ return result
268
+ candidate = (source_dir / imp).as_posix()
269
+ return _try_extensions(candidate, local_files)
270
+
271
+
272
+ def _resolve_relative(imp: str, source_dir: Path, local_files: set[str]) -> str | None:
273
+ """Resolve a relative import like .utils, ../helper, /src/foo."""
274
+ if imp.startswith("."):
275
+ level = 0
276
+ while level < len(imp) and imp[level] == ".":
277
+ level += 1
278
+ remainder = imp[level:]
279
+ target_dir = source_dir
280
+ for _ in range(level - 1):
281
+ target_dir = target_dir.parent
282
+ if remainder:
283
+ candidate = (target_dir / remainder).as_posix()
284
+ else:
285
+ candidate = (target_dir / "__init__").as_posix()
286
+ return _try_extensions(candidate, local_files)
287
+
288
+ if imp.startswith("/"):
289
+ candidate = imp.lstrip("/")
290
+ return _try_extensions(candidate, local_files) if candidate else None
291
+
292
+ return None
293
+
294
+
295
+ def _resolve_path(imp: str, source_dir: Path, local_files: set[str]) -> str | None:
296
+ """Resolve a path-style import relative to source file's directory."""
297
+ # Try direct resolution relative to source directory
298
+ candidate = (source_dir / imp).as_posix()
299
+ result = _try_extensions(candidate, local_files)
300
+ if result:
301
+ return result
302
+ # For module-prefixed paths (Go, etc.), strip leading components
303
+ # e.g. "example.com/app/src/greeter" → strip "example.com" → strip "app" → "src/greeter"
304
+ parts = imp.split("/")
305
+ for start in range(1, len(parts)):
306
+ sub = "/".join(parts[start:])
307
+ # Try relative to source directory
308
+ candidate = (source_dir / sub).as_posix()
309
+ result = _try_extensions(candidate, local_files)
310
+ if result:
311
+ return result
312
+ # Try as absolute from project root
313
+ result = _try_extensions(sub, local_files)
314
+ if result:
315
+ return result
316
+ return None
317
+
318
+
319
+ def _resolve_dotted(imp: str, source_dir: Path, local_files: set[str]) -> str | None:
320
+ """Resolve a dotted module import (Python, Java)."""
321
+ # Try the original string as a path first (handles "util.h", "theme.css", "db.php")
322
+ result = _resolve_path(imp, source_dir, local_files)
323
+ if result:
324
+ return result
325
+ parts = imp.split(".")
326
+ for length in range(len(parts), 0, -1):
327
+ candidate = "/".join(parts[:length])
328
+ result = _try_extensions(candidate, local_files)
329
+ if result:
330
+ return result
331
+ return None
332
+
333
+
334
+ _JAVA_SRC_PREFIXES = ("src/main/java/", "src/test/java/", "src/main/kotlin/", "src/")
335
+
336
+ def _try_extensions(base_path: str, local_files: set[str]) -> str | None:
337
+ """Try appending each known extension and check if the file exists in local_files."""
338
+ base = base_path.lstrip("/")
339
+
340
+ # Try the path itself (handles extensionless files like Makefile, Dockerfile)
341
+ if base in local_files:
342
+ return base
343
+
344
+ # If the path already has a known extension, try it directly first
345
+ if any(base.endswith(ext) for ext in KNOWN_EXTENSIONS):
346
+ if base in local_files:
347
+ return base
348
+ # Also try under Java/Kotlin src prefixes for dotted com.example.Foo
349
+ for pref in _JAVA_SRC_PREFIXES:
350
+ if (pref + base) in local_files:
351
+ return pref + base
352
+
353
+ for ext in KNOWN_EXTENSIONS:
354
+ candidate = base + ext
355
+ if candidate in local_files:
356
+ return candidate
357
+ for pref in _JAVA_SRC_PREFIXES:
358
+ if (pref + candidate) in local_files:
359
+ return pref + candidate
360
+ init_candidate = base + "/__init__" + ext
361
+ if init_candidate in local_files:
362
+ return init_candidate
363
+ # Go/Rust directory convention: "pkg" → "pkg/pkg.go" or "pkg/mod.rs"
364
+ last = base.rsplit("/", 1)[-1]
365
+ dir_candidate = base + "/" + last + ext
366
+ if dir_candidate in local_files:
367
+ return dir_candidate
368
+ if ext == ".rs":
369
+ mod_candidate = base + "/mod.rs"
370
+ if mod_candidate in local_files:
371
+ return mod_candidate
372
+ return None
373
+
374
+
375
+ # ── Circular dependency detection ──────────────────────────────────────────────
376
+
377
+
378
+ def find_circular_dependencies(graph: ImportGraph) -> list[CircularDep]:
379
+ """
380
+ Detect circular import chains via DFS.
381
+ Returns deduplicated CircularDep objects.
382
+ """
383
+ visited: set[str] = set()
384
+ rec_stack: set[str] = set()
385
+ raw_cycles: list[tuple[str, ...]] = []
386
+
387
+ def dfs(node: str, path: list[str]) -> None:
388
+ visited.add(node)
389
+ rec_stack.add(node)
390
+ path.append(node)
391
+ for neighbour in sorted(graph.edges.get(node, set())):
392
+ if neighbour not in visited:
393
+ dfs(neighbour, path)
394
+ elif neighbour in rec_stack:
395
+ idx = path.index(neighbour)
396
+ cycle = tuple(path[idx:])
397
+ min_i = cycle.index(min(cycle))
398
+ canonical = cycle[min_i:] + cycle[:min_i]
399
+ raw_cycles.append(canonical)
400
+ path.pop()
401
+ rec_stack.discard(node)
402
+
403
+ for node in sorted(graph.nodes):
404
+ if node not in visited:
405
+ dfs(node, [])
406
+
407
+ seen: set[tuple[str, ...]] = set()
408
+ result: list[CircularDep] = []
409
+ for c in raw_cycles:
410
+ if c not in seen:
411
+ seen.add(c)
412
+ result.append(CircularDep(cycle=list(c) + [c[0]]))
413
+ return result
414
+
415
+
416
+ # ── Dead file detection ────────────────────────────────────────────────────────
417
+
418
+ _INTERNAL_SEGMENTS = frozenset(
419
+ {
420
+ "cli",
421
+ "core",
422
+ "web",
423
+ "tests",
424
+ "test",
425
+ "commands",
426
+ "notifications",
427
+ "hosted",
428
+ "docs",
429
+ "scripts",
430
+ "benchmark",
431
+ "examples",
432
+ "example",
433
+ "demo",
434
+ }
435
+ )
436
+
437
+ _ENTRY_POINT_STEMS = frozenset(
438
+ {
439
+ "main",
440
+ "app",
441
+ "index",
442
+ "__init__",
443
+ "setup",
444
+ "cli",
445
+ "server",
446
+ "api",
447
+ "manage",
448
+ "wsgi",
449
+ "asgi",
450
+ "run",
451
+ "start",
452
+ }
453
+ )
454
+
455
+
456
+ def find_dead_files(
457
+ files: list[FileInfo],
458
+ graph: ImportGraph,
459
+ ) -> list[str]:
460
+ """
461
+ Return paths of files that nothing in the project imports.
462
+
463
+ Excludes:
464
+ - Files in internal package folders (cli/, core/, web/, tests/, ...)
465
+ - Entry-point files (main.py, app.py, index.py, ...)
466
+ - Test files (test_*.py, *_test.py)
467
+ """
468
+ dead: list[str] = []
469
+ for fi in files:
470
+ path = fi.path
471
+ parts = path.lower().split("/")
472
+
473
+ if any(seg in _INTERNAL_SEGMENTS for seg in parts):
474
+ continue
475
+
476
+ stem = Path(path).stem
477
+ if stem.startswith("test_") or stem.endswith("_test"):
478
+ continue
479
+
480
+ if stem in _ENTRY_POINT_STEMS:
481
+ continue
482
+
483
+ if not graph.reverse.get(path):
484
+ dead.append(path)
485
+
486
+ return dead