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,568 @@
1
+ """
2
+ DuplicateScanner — function-level similarity detection.
3
+
4
+ Scans for duplicated code blocks at the function/method level:
5
+ - Identifies functions with high similarity (>85%)
6
+ - Finds duplicated logic that should be extracted to shared functions
7
+ - Reports duplicate code across files
8
+ - Provides similarity scores and locations
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import ast as py_ast
14
+ import logging
15
+ import re
16
+ from typing import Any
17
+
18
+ from ..brain.languages import (
19
+ DEFAULT_IGNORE_DIRS,
20
+ TREE_SITTER_LANGS,
21
+ Lang,
22
+ detect_language,
23
+ get_parser,
24
+ parse_source,
25
+ )
26
+ from .base import (
27
+ AgentGroup,
28
+ AgentInput,
29
+ AgentResult,
30
+ AgentStatus,
31
+ BaseAgent,
32
+ Severity,
33
+ make_finding,
34
+ register,
35
+ safe_rglob,
36
+ )
37
+
38
+ _log = logging.getLogger("patchi.agents.duplicate_scanner")
39
+
40
+
41
+ def _ts_node_text(node: Any, buf: bytes) -> str:
42
+ try:
43
+ if hasattr(node, "start_byte") and hasattr(node, "end_byte"):
44
+ return buf[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
45
+ except Exception as e:
46
+ _log.debug("_ts_node_text failed: %s", e)
47
+ return ""
48
+
49
+
50
+ def _ts_child_by_field(node: Any, field: str) -> Any | None:
51
+ try:
52
+ return node.child_by_field_name(field) if hasattr(node, "child_by_field_name") else None
53
+ except Exception as e:
54
+ _log.debug("_ts_child_by_field failed: %s", e)
55
+ return None
56
+
57
+
58
+ def _ts_children(node: Any) -> list[Any]:
59
+ try:
60
+ return list(node.children) if hasattr(node, "children") else []
61
+ except Exception as e:
62
+ _log.debug("_ts_children failed: %s", e)
63
+ return []
64
+
65
+
66
+ def _ts_node_type(node: Any) -> str:
67
+ try:
68
+ return node.type if hasattr(node, "type") else ""
69
+ except Exception as e:
70
+ _log.debug("_ts_node_type failed: %s", e)
71
+ return ""
72
+
73
+
74
+ @register
75
+ class DuplicateScanner(BaseAgent):
76
+ """Scanner for duplicated code blocks."""
77
+
78
+ group = AgentGroup.SCANNER
79
+ name = "DuplicateScanner"
80
+ description = "Repeated logic blocks that should be extracted to shared functions"
81
+
82
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
83
+ """Scan for duplicated code blocks."""
84
+ findings = []
85
+
86
+ all_source_files = self._find_source_files(inp)
87
+
88
+ all_functions = []
89
+ for file_path in all_source_files:
90
+ abs_path = inp.root / file_path
91
+ if not abs_path.exists():
92
+ continue
93
+
94
+ try:
95
+ content = abs_path.read_text(encoding="utf-8")
96
+ lang = detect_language(abs_path)
97
+
98
+ if lang == Lang.PYTHON:
99
+ functions = self._extract_python_functions(file_path, content)
100
+ elif lang in [Lang.JAVASCRIPT, Lang.TYPESCRIPT]:
101
+ functions = self._extract_javascript_functions(file_path, content)
102
+ elif lang in TREE_SITTER_LANGS:
103
+ functions = self._extract_functions_treesitter(file_path, content, lang)
104
+ else:
105
+ functions = self._extract_functions_basic(file_path, content)
106
+
107
+ all_functions.extend(functions)
108
+ except Exception as e:
109
+ findings.append(
110
+ make_finding(
111
+ severity=Severity.LOW,
112
+ file=file_path,
113
+ line_start=0,
114
+ title="Duplicate scanner file read error",
115
+ description=f"Could not analyze {file_path} for duplicates: {str(e)}",
116
+ evidence=str(e),
117
+ )
118
+ )
119
+
120
+ duplicate_pairs = self._find_duplicate_functions(all_functions)
121
+
122
+ for func1, func2, similarity in duplicate_pairs:
123
+ findings.append(
124
+ make_finding(
125
+ severity=Severity.MEDIUM,
126
+ file=func1["file"],
127
+ line_start=func1["line"],
128
+ title=f"DUPLICATE CODE: {similarity:.1f}% similar to {func2['file']}:{func2['line']}",
129
+ description=f"Function '{func1['name']}' is {similarity:.1f}% similar to '{func2['name']}' in {func2['file']}",
130
+ evidence=f"Similarity: {similarity:.1f}%\nLocation: {func2['file']}:{func2['line']}\nFunction: {func2['name']}",
131
+ )
132
+ )
133
+
134
+ # Add summary
135
+ findings.append(
136
+ make_finding(
137
+ severity=Severity.INFO,
138
+ file="__summary__",
139
+ line_start=0,
140
+ title=f"Duplicates Found: {len(duplicate_pairs)} pairs",
141
+ description=f"Found {len(duplicate_pairs)} pairs of duplicate functions",
142
+ evidence=f"From {len(all_functions)} total functions analyzed",
143
+ )
144
+ )
145
+
146
+ result.status = AgentStatus.SUCCEEDED
147
+ result.findings = findings
148
+ result.data.update(
149
+ {
150
+ "duplicate_pairs_found": len(duplicate_pairs),
151
+ "duplicate_pairs": duplicate_pairs,
152
+ "total_functions_analyzed": len(all_functions),
153
+ "functions_scanned": len(all_functions),
154
+ "needs_ai": True, # Refactoring suggestions require AI
155
+ }
156
+ )
157
+ return
158
+
159
+ def _find_source_files(self, inp: AgentInput) -> list[str]:
160
+ """Find all source files to analyze."""
161
+ source_extensions = {
162
+ ".py",
163
+ ".js",
164
+ ".jsx",
165
+ ".ts",
166
+ ".tsx",
167
+ ".java",
168
+ ".php",
169
+ ".rb",
170
+ ".go",
171
+ ".rs",
172
+ ".cpp",
173
+ ".cxx",
174
+ ".cc",
175
+ ".c",
176
+ ".h",
177
+ ".hpp",
178
+ ".cs",
179
+ }
180
+
181
+ files = []
182
+ for ext in source_extensions:
183
+ for file_path in safe_rglob(inp.root, f"*{ext}"):
184
+ if file_path.is_file():
185
+ rel_path = file_path.relative_to(inp.root).as_posix()
186
+ if not self._should_skip_file(rel_path, inp):
187
+ files.append(rel_path)
188
+ return files
189
+
190
+ def _should_skip_file(self, file_path: str, inp: AgentInput) -> bool:
191
+ """Check if file should be skipped based on restrictions."""
192
+ # Skip ignored directories (node_modules, .venv, etc.)
193
+ from pathlib import PurePosixPath
194
+
195
+ if any(p in DEFAULT_IGNORE_DIRS for p in PurePosixPath(file_path).parts):
196
+ return True
197
+
198
+ from pathlib import PurePosixPath
199
+
200
+ # Check restrictions
201
+ restrictions = inp.config.get("restrictions", [])
202
+ for r in restrictions:
203
+ if r.get("enabled", True):
204
+ path = r["path"]
205
+ if file_path.startswith(path) or PurePosixPath(file_path).match(path):
206
+ if r["type"] == "NO_TOUCH":
207
+ return True
208
+ elif r["type"] == "SCAN_ONLY" and self.__class__.__name__ == "FixAgent":
209
+ return True
210
+ return False
211
+
212
+ def _extract_python_functions(self, file_path: str, content: str) -> list[dict]:
213
+ """Extract functions from Python code."""
214
+ try:
215
+ tree = py_ast.parse(content)
216
+ except SyntaxError:
217
+ return []
218
+
219
+ functions = []
220
+
221
+ for node in py_ast.walk(tree):
222
+ if isinstance(node, (py_ast.FunctionDef, py_ast.AsyncFunctionDef)):
223
+ # Get function body
224
+ body_lines = []
225
+ for item in node.body:
226
+ start_line = item.lineno - 1 # Convert to 0-indexed
227
+ end_line = getattr(item, "end_lineno", start_line)
228
+ if end_line <= len(content.splitlines()):
229
+ body_lines.extend(content.splitlines()[start_line:end_line])
230
+
231
+ functions.append(
232
+ {
233
+ "file": file_path,
234
+ "name": node.name,
235
+ "line": node.lineno,
236
+ "body": "\n".join(body_lines),
237
+ "normalized_body": self._normalize_code("\n".join(body_lines)),
238
+ }
239
+ )
240
+
241
+ return functions
242
+
243
+ def _extract_javascript_functions(self, file_path: str, content: str) -> list[dict]:
244
+ """Extract functions from JavaScript/TypeScript code."""
245
+ functions = []
246
+
247
+ # Pattern for function declarations
248
+ func_pattern = r"(?:function\s+)?(\w+)\s*\([^)]*\)\s*\{([^}]*(?:\{[^}]*}[^}]*)*?)\}"
249
+ matches = re.finditer(func_pattern, content, re.DOTALL)
250
+
251
+ for match in matches:
252
+ name = match.group(1)
253
+ body = match.group(2)
254
+ line_start = content[: match.start()].count("\n") + 1
255
+
256
+ functions.append(
257
+ {
258
+ "file": file_path,
259
+ "name": name,
260
+ "line": line_start,
261
+ "body": body,
262
+ "normalized_body": self._normalize_code(body),
263
+ }
264
+ )
265
+
266
+ # Pattern for arrow functions
267
+ arrow_pattern = (
268
+ r"(const|let|var)\s+(\w+)\s*=\s*\([^)]*\)\s*=>\s*\{([^}]*(?:\{[^}]*}[^}]*)*?)\};?"
269
+ )
270
+ arrow_matches = re.finditer(arrow_pattern, content, re.DOTALL)
271
+
272
+ for match in arrow_matches:
273
+ name = match.group(2)
274
+ body = match.group(3)
275
+ line_start = content[: match.start()].count("\n") + 1
276
+
277
+ functions.append(
278
+ {
279
+ "file": file_path,
280
+ "name": name,
281
+ "line": line_start,
282
+ "body": body,
283
+ "normalized_body": self._normalize_code(body),
284
+ }
285
+ )
286
+
287
+ return functions
288
+
289
+ def _extract_functions_treesitter(self, file_path: str, content: str, lang: Lang) -> list[dict]:
290
+ try:
291
+ # Svelte: extract <script> content and parse as JavaScript
292
+ if lang == Lang.SVELTE:
293
+ return self._extract_svelte_functions(file_path, content)
294
+ parser = get_parser(lang)
295
+ if parser is None:
296
+ return self._extract_functions_basic(file_path, content)
297
+ tree = parse_source(lang, content)
298
+ functions = []
299
+ self._walk_tree_for_functions(tree.root_node, file_path, content, functions, lang)
300
+ return functions
301
+ except Exception as e:
302
+ _log.warning("DuplicateScanner._extract_functions_treesitter failed: %s", e)
303
+ return self._extract_functions_basic(file_path, content)
304
+
305
+ def _extract_svelte_functions(self, file_path: str, content: str) -> list[dict]:
306
+ """Extract functions from a Svelte file by parsing its <script> block as JavaScript."""
307
+ import re
308
+
309
+ m = re.search(r"<script[^>]*>(.*?)</script>", content, re.DOTALL)
310
+ if not m:
311
+ return self._extract_functions_basic(file_path, content)
312
+ script_src = m.group(1).strip()
313
+ if not script_src:
314
+ return []
315
+ js_parser = get_parser(Lang.JAVASCRIPT)
316
+ if js_parser is None:
317
+ return self._extract_functions_basic(file_path, content)
318
+ tree = parse_source(Lang.JAVASCRIPT, script_src)
319
+ functions = []
320
+ self._walk_tree_for_functions(
321
+ tree.root_node, file_path, script_src, functions, Lang.JAVASCRIPT
322
+ )
323
+ return functions
324
+
325
+ def _walk_tree_for_functions(
326
+ self, node, file_path: str, content: str, functions: list[dict], lang: Lang
327
+ ):
328
+ nt = _ts_node_type(node)
329
+ buf = bytes(content, "utf-8")
330
+
331
+ # Language-specific function-like node types
332
+ func_types: set[str] = set()
333
+ match lang:
334
+ case Lang.RUST:
335
+ func_types = {"function_item"}
336
+ case Lang.JAVA:
337
+ func_types = {"method_declaration", "constructor_declaration"}
338
+ case Lang.GO:
339
+ func_types = {"function_declaration", "method_declaration"}
340
+ case Lang.C | Lang.CPP:
341
+ func_types = {"function_definition"}
342
+ case Lang.SWIFT:
343
+ func_types = {
344
+ "function_declaration",
345
+ "initializer_declaration",
346
+ "deinitializer_declaration",
347
+ "subscript_declaration",
348
+ }
349
+ case Lang.RUBY:
350
+ func_types = {"method", "singleton_method"}
351
+ case Lang.PHP:
352
+ func_types = {"function_definition", "method_declaration"}
353
+ case Lang.C_SHARP:
354
+ func_types = {"method_declaration", "constructor_declaration"}
355
+ case Lang.KOTLIN:
356
+ func_types = {"function_declaration", "method_declaration"}
357
+ case Lang.DART:
358
+ func_types = {"function_declaration", "method_declaration"}
359
+ case Lang.JAVASCRIPT | Lang.TYPESCRIPT | Lang.SVELTE:
360
+ func_types = {"function_declaration", "method_definition", "arrow_function"}
361
+ case _:
362
+ func_types = set()
363
+
364
+ if nt in func_types:
365
+ if lang == Lang.C or lang == Lang.CPP:
366
+ name = _ts_node_text(self._find_c_identifier(node), buf)
367
+ body = _ts_child_by_field(node, "body")
368
+ elif lang == Lang.GO and nt == "method_declaration":
369
+ name = _ts_node_text(_ts_child_by_field(node, "name"), buf)
370
+ body = _ts_child_by_field(node, "body")
371
+ elif lang == Lang.RUBY:
372
+ name = _ts_node_text(_ts_child_by_field(node, "name"), buf)
373
+ body = next(
374
+ (c for c in _ts_children(node) if _ts_node_type(c) == "body_statement"), None
375
+ )
376
+ else:
377
+ name = _ts_node_text(_ts_child_by_field(node, "name"), buf)
378
+ body = _ts_child_by_field(node, "body")
379
+
380
+ body_text = _ts_node_text(body, buf) if body else ""
381
+ line_start = (node.start_point.row + 1) if hasattr(node, "start_point") else 0
382
+
383
+ functions.append(
384
+ {
385
+ "file": file_path,
386
+ "name": name or f"<anonymous>:{line_start}",
387
+ "line": line_start,
388
+ "body": body_text,
389
+ "normalized_body": self._normalize_code(body_text),
390
+ }
391
+ )
392
+
393
+ for child in _ts_children(node):
394
+ self._walk_tree_for_functions(child, file_path, content, functions, lang)
395
+
396
+ def _find_c_identifier(self, func_def_node):
397
+ declarator = _ts_child_by_field(func_def_node, "declarator")
398
+ if declarator is None:
399
+ return func_def_node
400
+ for candidate in [
401
+ _ts_child_by_field(declarator, "declarator"),
402
+ _ts_child_by_field(declarator, "name"),
403
+ ]:
404
+ if candidate is None:
405
+ continue
406
+ ct = _ts_node_type(candidate)
407
+ if ct == "identifier":
408
+ return candidate
409
+ if ct in (
410
+ "function_declarator",
411
+ "pointer_declarator",
412
+ "array_declarator",
413
+ "initializer_pair",
414
+ ):
415
+ found = self._find_c_identifier(candidate)
416
+ if found is not None and _ts_node_type(found) == "identifier":
417
+ return found
418
+ return None
419
+
420
+ def _extract_functions_basic(self, file_path: str, content: str) -> list[dict]:
421
+ """Basic function extraction for unsupported languages."""
422
+ # Very basic approach - look for common function patterns
423
+ import re
424
+
425
+ functions = []
426
+
427
+ # Generic function pattern
428
+ func_pattern = r"(?:function|def|fun|func)\s+(\w+)([^{]*)\{([^}]*(?:\{[^}]*}[^}]*)*?)\}"
429
+ matches = re.finditer(func_pattern, content, re.DOTALL)
430
+
431
+ for match in matches:
432
+ name = match.group(1)
433
+ body = match.group(3)
434
+ line_start = content[: match.start()].count("\n") + 1
435
+
436
+ functions.append(
437
+ {
438
+ "file": file_path,
439
+ "name": name,
440
+ "line": line_start,
441
+ "body": body,
442
+ "normalized_body": self._normalize_code(body),
443
+ }
444
+ )
445
+
446
+ return functions
447
+
448
+ def _normalize_code(self, code: str) -> str:
449
+ """Normalize code to remove formatting differences for comparison."""
450
+ # Remove extra whitespace
451
+ normalized = re.sub(r"\s+", " ", code)
452
+ # Remove comments (basic approach)
453
+ normalized = re.sub(r"//.*", "", normalized) # Single-line comments
454
+ normalized = re.sub(r"/\*.*?\*/", "", normalized, flags=re.DOTALL) # Multi-line comments
455
+ # Remove strings (basic approach)
456
+ normalized = re.sub(r'"([^"]|"")*"', '""', normalized) # Double-quoted strings
457
+ normalized = re.sub(r"'([^']|'')*'", "''", normalized) # Single-quoted strings
458
+
459
+ return normalized.strip()
460
+
461
+ def _find_duplicate_functions(self, functions: list[dict]) -> list[tuple]:
462
+ """Find pairs of functions that are highly similar (>85% Jaccard).
463
+
464
+ Candidate generation replaces the former O(n²) all-pairs scan:
465
+ * tokenize each normalized body once and tally global token frequency
466
+ * index every function under its ``int(0.26·|S|)+1`` globally-rarest
467
+ tokens (capped at the set size; floor+1 ≥ 0.26·|S| keeps the recall
468
+ bound below, and ties are broken by token string for determinism)
469
+ * any pair with Jaccard > 0.85 shares its rarest *shared* token,
470
+ which is provably a key for BOTH functions (sizes must be within
471
+ 1.176× and the intersection exceeds 91.9% of the smaller set, so
472
+ the number of tokens strictly rarer than it is < 0.081·|S| in the
473
+ smaller set and < 0.257·|S| in the larger — both < 0.26·|S|+1),
474
+ so every true duplicate is generated as a candidate — zero false
475
+ negatives
476
+ * candidates are verified with the exact same `_calculate_similarity`
477
+ and 0.85 threshold, and results are re-sorted to the original
478
+ (i, j) enumeration order — output is identical, but sub-quadratic.
479
+
480
+ Tradeoff: a common token (e.g. a frequent literal) can be the "rarest"
481
+ token of a tiny function, so one bucket may gather O(m) small functions
482
+ and verify O(m²) candidates. That is wasted-but-correct work; the
483
+ size-ratio pre-filter prunes the worst of it. Output is unaffected.
484
+ """
485
+ n = len(functions)
486
+ if n < 2:
487
+ return []
488
+
489
+ # 1. Tokenize each body once + global token frequencies.
490
+ uniq_sets: list[set[str]] = []
491
+ freq: dict[str, int] = {}
492
+ for f in functions:
493
+ us = set(f["normalized_body"].split())
494
+ uniq_sets.append(us)
495
+ for t in us:
496
+ freq[t] = freq.get(t, 0) + 1
497
+
498
+ # 2. Index each function under its rarest tokens (guaranteed recall).
499
+ buckets: dict[str, list[int]] = {}
500
+ for i, us in enumerate(uniq_sets):
501
+ if not us:
502
+ continue
503
+ k = min(len(us), int(0.26 * len(us)) + 1)
504
+ # Secondary key (token string) makes tie-breaking deterministic
505
+ # across processes; the recall bound only needs the strict-ranker.
506
+ rarest = sorted(us, key=lambda t: (freq[t], t))[:k]
507
+ for t in rarest:
508
+ buckets.setdefault(t, []).append(i)
509
+
510
+ # 3. Collect candidate pairs (deduped across shared buckets).
511
+ seen: set[tuple[int, int]] = set()
512
+ candidates: list[tuple[int, int]] = []
513
+ for idxs in buckets.values():
514
+ m = len(idxs)
515
+ if m < 2:
516
+ continue
517
+ for a in range(m - 1):
518
+ i = idxs[a]
519
+ for b in range(a + 1, m):
520
+ j = idxs[b]
521
+ key = (i, j) if i < j else (j, i)
522
+ if key not in seen:
523
+ seen.add(key)
524
+ candidates.append(key)
525
+
526
+ # 4. Verify candidates with the exact same metric + threshold.
527
+ results: list[tuple[int, int, float]] = []
528
+ for i, j in candidates:
529
+ si, sj = uniq_sets[i], uniq_sets[j]
530
+ ni, nj = len(si), len(sj)
531
+ if ni == 0 or nj == 0:
532
+ continue
533
+ # Jaccard > 0.85 forces the two set sizes within 1.176× — cheap
534
+ # pre-filter that never excludes a true positive.
535
+ if max(ni, nj) * 100 > min(ni, nj) * 118:
536
+ continue
537
+ similarity = self._calculate_similarity(
538
+ functions[i]["normalized_body"], functions[j]["normalized_body"]
539
+ )
540
+ if similarity > 0.85:
541
+ results.append((i, j, similarity))
542
+
543
+ # 5. Restore the original (i, j) enumeration order.
544
+ results.sort()
545
+ return [(functions[i], functions[j], sim * 100) for i, j, sim in results]
546
+
547
+ def _calculate_similarity(self, code1: str, code2: str) -> float:
548
+ """Calculate similarity between two code blocks using a simple algorithm."""
549
+ if not code1 or not code2:
550
+ return 0.0
551
+
552
+ # If they're exactly equal, return 1.0
553
+ if code1 == code2:
554
+ return 1.0
555
+
556
+ # Simple similarity calculation based on common substrings
557
+ # This is a simplified version - in practice, you might want to use
558
+ # more sophisticated algorithms like Levenshtein distance or AST comparison
559
+ set1 = set(code1.split())
560
+ set2 = set(code2.split())
561
+
562
+ intersection = set1.intersection(set2)
563
+ union = set1.union(set2)
564
+
565
+ if len(union) == 0:
566
+ return 0.0
567
+
568
+ return len(intersection) / len(union)