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.
- patchi/__init__.py +1 -0
- patchi/__main__.py +13 -0
- patchi/cli/__init__.py +0 -0
- patchi/cli/command_families.py +235 -0
- patchi/cli/commands/__init__.py +0 -0
- patchi/cli/commands/access_cmd.py +65 -0
- patchi/cli/commands/agent_stats_cmd.py +273 -0
- patchi/cli/commands/agents_cmd.py +187 -0
- patchi/cli/commands/ai_cmd.py +308 -0
- patchi/cli/commands/assure_cmd.py +538 -0
- patchi/cli/commands/audit_cmd.py +286 -0
- patchi/cli/commands/auto_cmd.py +110 -0
- patchi/cli/commands/blame_cmd.py +53 -0
- patchi/cli/commands/chains_cmd.py +327 -0
- patchi/cli/commands/charter_cmd.py +173 -0
- patchi/cli/commands/chat_cmd.py +597 -0
- patchi/cli/commands/check_cmd.py +232 -0
- patchi/cli/commands/cleanup_cmd.py +328 -0
- patchi/cli/commands/cockpit_cmd.py +208 -0
- patchi/cli/commands/commands_cmd.py +160 -0
- patchi/cli/commands/cross_repo_cmd.py +113 -0
- patchi/cli/commands/deps_cmd.py +110 -0
- patchi/cli/commands/dev_check_cmd.py +342 -0
- patchi/cli/commands/dev_cmd.py +445 -0
- patchi/cli/commands/doctor_cmd.py +457 -0
- patchi/cli/commands/findings_cmd.py +187 -0
- patchi/cli/commands/fix_cmd.py +648 -0
- patchi/cli/commands/fix_review_cmd.py +273 -0
- patchi/cli/commands/heatmap_cmd.py +62 -0
- patchi/cli/commands/help_cmd.py +159 -0
- patchi/cli/commands/hosted_cmd.py +980 -0
- patchi/cli/commands/init.py +456 -0
- patchi/cli/commands/key_cmd.py +571 -0
- patchi/cli/commands/learn_cmd.py +140 -0
- patchi/cli/commands/link_cmd.py +104 -0
- patchi/cli/commands/log_cmd.py +57 -0
- patchi/cli/commands/memory_cmd.py +215 -0
- patchi/cli/commands/model_cmd.py +207 -0
- patchi/cli/commands/notify_cmd.py +213 -0
- patchi/cli/commands/notify_prompts.py +29 -0
- patchi/cli/commands/patch_cmd.py +231 -0
- patchi/cli/commands/plan_cmd.py +128 -0
- patchi/cli/commands/plugins_cmd.py +307 -0
- patchi/cli/commands/queue_cmd.py +152 -0
- patchi/cli/commands/quick_cmd.py +159 -0
- patchi/cli/commands/ready_cmd.py +292 -0
- patchi/cli/commands/report_cmd.py +416 -0
- patchi/cli/commands/restrict_cmd.py +127 -0
- patchi/cli/commands/review_cmd.py +225 -0
- patchi/cli/commands/rules_cmd.py +233 -0
- patchi/cli/commands/scan_cmd.py +1908 -0
- patchi/cli/commands/settings_cmd.py +45 -0
- patchi/cli/commands/status_cmd.py +679 -0
- patchi/cli/commands/test_cmd.py +1052 -0
- patchi/cli/commands/trend_cmd.py +296 -0
- patchi/cli/commands/undo_cmd.py +235 -0
- patchi/cli/commands/update_cmd.py +345 -0
- patchi/cli/commands/verify_cmd.py +56 -0
- patchi/cli/commands/vr_cmd.py +440 -0
- patchi/cli/commands/watch_cmd.py +304 -0
- patchi/cli/commands/web_cmd.py +114 -0
- patchi/cli/console.py +101 -0
- patchi/cli/display/__init__.py +1 -0
- patchi/cli/display/live_audit.py +269 -0
- patchi/cli/display/live_progress.py +169 -0
- patchi/cli/framework.py +241 -0
- patchi/cli/logo.py +128 -0
- patchi/cli/main.py +222 -0
- patchi/cli/registry.py +1171 -0
- patchi/cli/style.py +68 -0
- patchi/cli/themes.py +187 -0
- patchi/cli/ux.py +444 -0
- patchi/core/__init__.py +0 -0
- patchi/core/agents/__init__.py +0 -0
- patchi/core/agents/api_fuzzer_agent.py +141 -0
- patchi/core/agents/attack_agent.py +242 -0
- patchi/core/agents/base.py +1012 -0
- patchi/core/agents/bug_predictor_agent.py +58 -0
- patchi/core/agents/build_tool_validator.py +150 -0
- patchi/core/agents/cache.py +192 -0
- patchi/core/agents/chaos_agent.py +133 -0
- patchi/core/agents/cicd_generator.py +240 -0
- patchi/core/agents/comment_scanner.py +280 -0
- patchi/core/agents/console_logging_agent.py +156 -0
- patchi/core/agents/coordinator.py +858 -0
- patchi/core/agents/core_scanner.py +269 -0
- patchi/core/agents/coverage_prioritizer.py +181 -0
- patchi/core/agents/dead_code_hygiene.py +296 -0
- patchi/core/agents/dead_code_scanner.py +692 -0
- patchi/core/agents/dependency_scanner.py +725 -0
- patchi/core/agents/doc_claim_agent.py +573 -0
- patchi/core/agents/duplicate_scanner.py +568 -0
- patchi/core/agents/env_scanner.py +410 -0
- patchi/core/agents/feature_flag_agent.py +89 -0
- patchi/core/agents/frontend_framework_agent.py +133 -0
- patchi/core/agents/goroutine_agent.py +82 -0
- patchi/core/agents/governor.py +1972 -0
- patchi/core/agents/i18n_agent.py +95 -0
- patchi/core/agents/license_compliance.py +247 -0
- patchi/core/agents/linking_agent.py +251 -0
- patchi/core/agents/memory_profiler_agent.py +102 -0
- patchi/core/agents/modernization_agent.py +84 -0
- patchi/core/agents/mutation_agent.py +174 -0
- patchi/core/agents/nl_query_agent.py +74 -0
- patchi/core/agents/promise_rejection_agent.py +100 -0
- patchi/core/agents/refactoring_agent.py +286 -0
- patchi/core/agents/resource_leak_agent.py +91 -0
- patchi/core/agents/route_graph_scanner.py +951 -0
- patchi/core/agents/run_agent.py +106 -0
- patchi/core/agents/rust_unwrap_agent.py +100 -0
- patchi/core/agents/sbom_generator.py +284 -0
- patchi/core/agents/scanners.py +67 -0
- patchi/core/agents/side/__init__.py +1 -0
- patchi/core/agents/side/build_agent.py +65 -0
- patchi/core/agents/side/format_agent.py +50 -0
- patchi/core/agents/side/install_agent.py +63 -0
- patchi/core/agents/side_file_scanner.py +549 -0
- patchi/core/agents/snapshot_drift_detector.py +146 -0
- patchi/core/agents/spa_route_inventory.py +203 -0
- patchi/core/agents/test_scanner.py +442 -0
- patchi/core/agents/tool_health.py +109 -0
- patchi/core/agents/tool_runner.py +162 -0
- patchi/core/agents/type_scanner.py +168 -0
- patchi/core/agents/ui_scanner.py +558 -0
- patchi/core/ai/__init__.py +20 -0
- patchi/core/ai/agent_audit.py +153 -0
- patchi/core/ai/agent_profiler.py +277 -0
- patchi/core/ai/client.py +499 -0
- patchi/core/ai/cost_tracker.py +107 -0
- patchi/core/ai/model_router.py +318 -0
- patchi/core/ai/orchestrator.py +625 -0
- patchi/core/ai/prompts.py +681 -0
- patchi/core/ai/smart.py +302 -0
- patchi/core/ai/tool_executor.py +24 -0
- patchi/core/ai/tools/__init__.py +10 -0
- patchi/core/ai/tools/executor.py +621 -0
- patchi/core/ai/tools/realize.py +1160 -0
- patchi/core/ai/tools/registry.py +1577 -0
- patchi/core/assurance/__init__.py +33 -0
- patchi/core/assurance/builder.py +211 -0
- patchi/core/assurance/graph.py +342 -0
- patchi/core/assurance/invariants.py +168 -0
- patchi/core/atomic.py +61 -0
- patchi/core/attackers/__init__.py +17 -0
- patchi/core/attackers/api_abuse.py +89 -0
- patchi/core/attackers/auth_bypass.py +73 -0
- patchi/core/attackers/base.py +91 -0
- patchi/core/attackers/business_logic.py +88 -0
- patchi/core/attackers/planner.py +108 -0
- patchi/core/attackers/priv_escalation.py +88 -0
- patchi/core/attackers/recon.py +87 -0
- patchi/core/brain/__init__.py +0 -0
- patchi/core/brain/ast_utils/__init__.py +81 -0
- patchi/core/brain/ast_utils/assignments.py +219 -0
- patchi/core/brain/ast_utils/calls.py +139 -0
- patchi/core/brain/ast_utils/config.py +132 -0
- patchi/core/brain/ast_utils/control_flow.py +74 -0
- patchi/core/brain/ast_utils/dead_symbols.py +205 -0
- patchi/core/brain/ast_utils/decorators.py +108 -0
- patchi/core/brain/ast_utils/helpers.py +144 -0
- patchi/core/brain/ast_utils/imports.py +169 -0
- patchi/core/brain/ast_utils/scan.py +143 -0
- patchi/core/brain/ast_utils/taint.py +310 -0
- patchi/core/brain/audit.py +379 -0
- patchi/core/brain/baseline.py +195 -0
- patchi/core/brain/blast_radius.py +312 -0
- patchi/core/brain/body_tags.py +144 -0
- patchi/core/brain/brain.py +1161 -0
- patchi/core/brain/brain_context.py +393 -0
- patchi/core/brain/brain_watcher.py +340 -0
- patchi/core/brain/charter.py +473 -0
- patchi/core/brain/classifier.py +214 -0
- patchi/core/brain/cockpit.py +350 -0
- patchi/core/brain/code_query.py +710 -0
- patchi/core/brain/contract.py +889 -0
- patchi/core/brain/contract_diff.py +155 -0
- patchi/core/brain/council.py +800 -0
- patchi/core/brain/cross_repo.py +104 -0
- patchi/core/brain/dead_code_tools.py +157 -0
- patchi/core/brain/doc_validator.py +197 -0
- patchi/core/brain/domain_activator.py +2183 -0
- patchi/core/brain/enriched_context.py +220 -0
- patchi/core/brain/file_corpus.py +220 -0
- patchi/core/brain/framework.py +953 -0
- patchi/core/brain/freshness.py +273 -0
- patchi/core/brain/git_aware.py +206 -0
- patchi/core/brain/heatmap.py +104 -0
- patchi/core/brain/ignore_parser.py +153 -0
- patchi/core/brain/import_graph.py +486 -0
- patchi/core/brain/languages.py +397 -0
- patchi/core/brain/layered_brain.py +419 -0
- patchi/core/brain/learning.py +229 -0
- patchi/core/brain/orphaned_endpoints.py +313 -0
- patchi/core/brain/personas/__init__.py +648 -0
- patchi/core/brain/personas/bad_user.py +165 -0
- patchi/core/brain/personas/base.py +349 -0
- patchi/core/brain/proactive.py +807 -0
- patchi/core/brain/project_context.py +121 -0
- patchi/core/brain/project_reader.py +521 -0
- patchi/core/brain/reasoning.py +269 -0
- patchi/core/brain/route_detector/__init__.py +15 -0
- patchi/core/brain/route_detector/base.py +31 -0
- patchi/core/brain/route_detector/csharp.py +209 -0
- patchi/core/brain/route_detector/file_based.py +81 -0
- patchi/core/brain/route_detector/go.py +152 -0
- patchi/core/brain/route_detector/java.py +195 -0
- patchi/core/brain/route_detector/javascript.py +197 -0
- patchi/core/brain/route_detector/php.py +158 -0
- patchi/core/brain/route_detector/python.py +193 -0
- patchi/core/brain/route_detector/registry.py +25 -0
- patchi/core/brain/route_detector/ruby.py +134 -0
- patchi/core/brain/route_detector/rust.py +216 -0
- patchi/core/brain/route_detector/swift.py +118 -0
- patchi/core/brain/route_mapper.py +265 -0
- patchi/core/brain/scanner.py +2136 -0
- patchi/core/brain/secrets.py +280 -0
- patchi/core/brain/symbol_graph.py +1955 -0
- patchi/core/brain/team.py +28 -0
- patchi/core/brain/trace_log.py +132 -0
- patchi/core/brain/type_checker/__init__.py +65 -0
- patchi/core/brain/type_checker/base.py +60 -0
- patchi/core/brain/type_checker/csharp.py +59 -0
- patchi/core/brain/type_checker/dart.py +59 -0
- patchi/core/brain/type_checker/go.py +81 -0
- patchi/core/brain/type_checker/java.py +75 -0
- patchi/core/brain/type_checker/kotlin.py +75 -0
- patchi/core/brain/type_checker/php.py +88 -0
- patchi/core/brain/type_checker/python.py +87 -0
- patchi/core/brain/type_checker/rust.py +81 -0
- patchi/core/brain/type_checker/swift.py +59 -0
- patchi/core/brain/type_checker/typescript.py +298 -0
- patchi/core/brain/understander.py +130 -0
- patchi/core/brain/verify.py +379 -0
- patchi/core/campaigns/__init__.py +14 -0
- patchi/core/campaigns/base.py +62 -0
- patchi/core/campaigns/data_flow_abuse.py +153 -0
- patchi/core/campaigns/orchestrator.py +140 -0
- patchi/core/campaigns/state_transitions.py +178 -0
- patchi/core/ci_bundle.py +104 -0
- patchi/core/config.py +311 -0
- patchi/core/constants.py +245 -0
- patchi/core/debug/__init__.py +25 -0
- patchi/core/debug/adapters/__init__.py +11 -0
- patchi/core/debug/adapters/codelldb.py +379 -0
- patchi/core/debug/adapters/node.py +222 -0
- patchi/core/debug/adapters/powershell.py +210 -0
- patchi/core/debug/adapters/python.py +244 -0
- patchi/core/debug/capture.py +210 -0
- patchi/core/debug/dap_client.py +268 -0
- patchi/core/detector/__init__.py +0 -0
- patchi/core/detector/audit.py +194 -0
- patchi/core/detector/bus.py +226 -0
- patchi/core/detector/dispatcher.py +275 -0
- patchi/core/detector/event.py +254 -0
- patchi/core/detector/sigma_engine.py +378 -0
- patchi/core/detector/triage.py +289 -0
- patchi/core/export/sarif.py +113 -0
- patchi/core/fix/__init__.py +17 -0
- patchi/core/fix/applier.py +574 -0
- patchi/core/fix/base.py +510 -0
- patchi/core/fix/code_fixer.py +169 -0
- patchi/core/fix/dead_code_remover.py +131 -0
- patchi/core/fix/fix_agents.py +775 -0
- patchi/core/fix/patch.py +444 -0
- patchi/core/fix/risk_gate.py +383 -0
- patchi/core/fix/security_fixer.py +93 -0
- patchi/core/fix/verify_loop.py +204 -0
- patchi/core/fuzz/__init__.py +8 -0
- patchi/core/fuzz/corpus.py +137 -0
- patchi/core/fuzz/input_fuzzer.py +256 -0
- patchi/core/fuzz/sequence_fuzzer.py +162 -0
- patchi/core/fuzz/state_fuzzer.py +236 -0
- patchi/core/health.py +407 -0
- patchi/core/hosted/__init__.py +1 -0
- patchi/core/hosted/anomaly.py +298 -0
- patchi/core/hosted/audit_log.py +102 -0
- patchi/core/hosted/compliance_report.py +148 -0
- patchi/core/hosted/ip_reputation.py +169 -0
- patchi/core/hosted/log_parsers.py +251 -0
- patchi/core/hosted/tokens.py +127 -0
- patchi/core/hosted/watchlist.py +164 -0
- patchi/core/hosted/webhooks.py +156 -0
- patchi/core/memory.py +437 -0
- patchi/core/notifications/__init__.py +18 -0
- patchi/core/notifications/channels.py +192 -0
- patchi/core/notifications/digest.py +141 -0
- patchi/core/notifications/escalation.py +158 -0
- patchi/core/notifications/notifier.py +182 -0
- patchi/core/notifications/quiet_hours.py +69 -0
- patchi/core/plugins/__init__.py +48 -0
- patchi/core/plugins/analyzer.py +326 -0
- patchi/core/plugins/analyzers/__init__.py +6 -0
- patchi/core/plugins/analyzers/dead_code_analyzer.py +120 -0
- patchi/core/plugins/analyzers/secret_scanner.py +158 -0
- patchi/core/plugins/analyzers/todo_scanner.py +115 -0
- patchi/core/plugins/registry.py +335 -0
- patchi/core/queue.py +375 -0
- patchi/core/reliability/__init__.py +13 -0
- patchi/core/reliability/chaos.py +163 -0
- patchi/core/reliability/fault_injector.py +172 -0
- patchi/core/reliability/idempotency.py +128 -0
- patchi/core/reliability/recovery.py +147 -0
- patchi/core/runtime/__init__.py +5 -0
- patchi/core/runtime/tracer.py +230 -0
- patchi/core/scan_bus.py +156 -0
- patchi/core/security/__init__.py +1 -0
- patchi/core/security/ai_validator.py +462 -0
- patchi/core/security/app_mapper.py +216 -0
- patchi/core/security/app_profile.py +464 -0
- patchi/core/security/attack_feedback.py +141 -0
- patchi/core/security/attack_scenarios/__init__.py +1 -0
- patchi/core/security/attack_tree.py +176 -0
- patchi/core/security/auth_audit_agent.py +344 -0
- patchi/core/security/authz_agent.py +602 -0
- patchi/core/security/auto_fix_proactive.py +620 -0
- patchi/core/security/auto_fixer.py +660 -0
- patchi/core/security/auto_ticket.py +53 -0
- patchi/core/security/bandit_agent.py +152 -0
- patchi/core/security/blast_radius.py +226 -0
- patchi/core/security/browser_tester.py +333 -0
- patchi/core/security/business_logic_agent.py +263 -0
- patchi/core/security/catch_block_auditor.py +395 -0
- patchi/core/security/cdn_cache_agent.py +357 -0
- patchi/core/security/chain_analyzer.py +321 -0
- patchi/core/security/chain_to_assurance.py +168 -0
- patchi/core/security/charter.py +510 -0
- patchi/core/security/cloud_waf_detector.py +295 -0
- patchi/core/security/codeql_agent.py +233 -0
- patchi/core/security/compliance_agent.py +348 -0
- patchi/core/security/confidence_gate.py +423 -0
- patchi/core/security/container_scanner.py +282 -0
- patchi/core/security/crypto_agent.py +378 -0
- patchi/core/security/cve_monitor.py +262 -0
- patchi/core/security/dast_agent.py +865 -0
- patchi/core/security/defectdojo.py +238 -0
- patchi/core/security/defenders/__init__.py +75 -0
- patchi/core/security/defenders/auth_middleware.py +115 -0
- patchi/core/security/defenders/base.py +97 -0
- patchi/core/security/defenders/block_ip.py +71 -0
- patchi/core/security/defenders/block_ws_origin.py +36 -0
- patchi/core/security/defenders/crypto_fix.py +118 -0
- patchi/core/security/defenders/enforce_rate_limit.py +108 -0
- patchi/core/security/defenders/fix_code.py +93 -0
- patchi/core/security/defenders/invalidate_session.py +40 -0
- patchi/core/security/defenders/patch_config.py +70 -0
- patchi/core/security/defenders/rotate_secret.py +54 -0
- patchi/core/security/defenders/suspend_account.py +41 -0
- patchi/core/security/defenders/update_dependency.py +160 -0
- patchi/core/security/defense_layer.py +217 -0
- patchi/core/security/dependency_vulnerability_agent.py +616 -0
- patchi/core/security/detection_pipeline.py +289 -0
- patchi/core/security/dns_security_agent.py +497 -0
- patchi/core/security/domain_activator_v2.py +913 -0
- patchi/core/security/domain_loader.py +1004 -0
- patchi/core/security/domains/__init__.py +0 -0
- patchi/core/security/email_authentication_agent.py +417 -0
- patchi/core/security/env_var_validator.py +304 -0
- patchi/core/security/evidence.py +100 -0
- patchi/core/security/falco_runtime_agent.py +387 -0
- patchi/core/security/fix-playbooks/__init__.py +0 -0
- patchi/core/security/gated_finding.py +85 -0
- patchi/core/security/git_diff_activator.py +367 -0
- patchi/core/security/governance.py +208 -0
- patchi/core/security/gradual_gate.py +34 -0
- patchi/core/security/history.py +315 -0
- patchi/core/security/iac_scanner.py +250 -0
- patchi/core/security/ignore_expiry.py +111 -0
- patchi/core/security/ignore_learner.py +416 -0
- patchi/core/security/injection_agent.py +319 -0
- patchi/core/security/insecure_randomness_agent.py +288 -0
- patchi/core/security/intent_analyzer.py +326 -0
- patchi/core/security/jwt_agent.py +453 -0
- patchi/core/security/kubernetes_agent.py +638 -0
- patchi/core/security/layer2_orchestrator.py +341 -0
- patchi/core/security/llm_security_agent.py +252 -0
- patchi/core/security/misconfig_agent.py +600 -0
- patchi/core/security/mobile_security_agent.py +474 -0
- patchi/core/security/network_agent.py +376 -0
- patchi/core/security/noise_filter.py +274 -0
- patchi/core/security/orchestrator.py +313 -0
- patchi/core/security/pattern_context.py +125 -0
- patchi/core/security/pattern_loader.py +101 -0
- patchi/core/security/pentest/__init__.py +4 -0
- patchi/core/security/pentest/base.py +58 -0
- patchi/core/security/pentest/dalfox_adapter.py +40 -0
- patchi/core/security/pentest/ffuf_adapter.py +48 -0
- patchi/core/security/pentest/nuclei_adapter.py +50 -0
- patchi/core/security/pentest/registry.py +80 -0
- patchi/core/security/pentest/shannon_adapter.py +107 -0
- patchi/core/security/pentest/sqlmap_adapter.py +56 -0
- patchi/core/security/pentest/zap_adapter.py +52 -0
- patchi/core/security/plan_auditor.py +276 -0
- patchi/core/security/policy_engine.py +272 -0
- patchi/core/security/prechecks.py +172 -0
- patchi/core/security/precommit_hook.py +247 -0
- patchi/core/security/privacy_agent.py +361 -0
- patchi/core/security/push_notification_agent.py +417 -0
- patchi/core/security/pysa_agent.py +143 -0
- patchi/core/security/reasoning.py +442 -0
- patchi/core/security/red_team_agent.py +184 -0
- patchi/core/security/red_team_engine.py +1142 -0
- patchi/core/security/remediation.py +360 -0
- patchi/core/security/request_interceptor.py +294 -0
- patchi/core/security/runtime_validator.py +245 -0
- patchi/core/security/saml_sso_agent.py +401 -0
- patchi/core/security/sast_agent.py +213 -0
- patchi/core/security/sast_gate.py +95 -0
- patchi/core/security/scheduler.py +287 -0
- patchi/core/security/secrets_runtime_agent.py +476 -0
- patchi/core/security/security_agents.py +175 -0
- patchi/core/security/security_config.py +551 -0
- patchi/core/security/security_probe.py +762 -0
- patchi/core/security/security_taint.py +1005 -0
- patchi/core/security/sensitive_data_agent.py +271 -0
- patchi/core/security/service_mesh_agent.py +412 -0
- patchi/core/security/session_management_agent.py +245 -0
- patchi/core/security/ssrf_agent.py +422 -0
- patchi/core/security/supply_chain.py +449 -0
- patchi/core/security/threat_model_generator.py +378 -0
- patchi/core/security/threat_model_updater.py +196 -0
- patchi/core/security/tool_adapters.py +135 -0
- patchi/core/security/tool_verify.py +173 -0
- patchi/core/security/websocket_security_agent.py +188 -0
- patchi/core/snapshot.py +242 -0
- patchi/core/tenant.py +386 -0
- patchi/core/testing/__init__.py +17 -0
- patchi/core/testing/_browser.py +147 -0
- patchi/core/testing/ads_agent.py +141 -0
- patchi/core/testing/api_contract_agent.py +450 -0
- patchi/core/testing/app_discovery_agent.py +641 -0
- patchi/core/testing/app_launcher.py +184 -0
- patchi/core/testing/browser_test_agent.py +516 -0
- patchi/core/testing/e2e_flow_agent.py +354 -0
- patchi/core/testing/flake_detector_agent.py +292 -0
- patchi/core/testing/gate.py +31 -0
- patchi/core/testing/live_test_runner.py +304 -0
- patchi/core/testing/live_v2/__init__.py +1 -0
- patchi/core/testing/live_v2/browser_pool.py +349 -0
- patchi/core/testing/live_v2/browser_test_runner.py +313 -0
- patchi/core/testing/live_v2/runner.py +626 -0
- patchi/core/testing/live_v2/screenshot_manager.py +474 -0
- patchi/core/testing/live_v2/stress_orchestrator.py +523 -0
- patchi/core/testing/live_v2/video_recorder.py +303 -0
- patchi/core/testing/regression_agent.py +302 -0
- patchi/core/testing/security_test_agent.py +253 -0
- patchi/core/testing/stress_test_agent.py +203 -0
- patchi/core/testing/test_agents.py +50 -0
- patchi/core/testing/test_config.py +194 -0
- patchi/core/testing/ui_accessibility_agent.py +324 -0
- patchi/core/testing/ui_button_agent.py +294 -0
- patchi/core/testing/ui_layout_agent.py +330 -0
- patchi/core/testing/unit_test_agent.py +1149 -0
- patchi/core/testing/visual_regression_agent.py +260 -0
- patchi/web/__init__.py +1 -0
- patchi/web/api/__init__.py +1 -0
- patchi/web/api/brain_map.py +114 -0
- patchi/web/api/charts.py +47 -0
- patchi/web/api/chat.py +224 -0
- patchi/web/api/cicd.py +576 -0
- patchi/web/api/dast_evidence.py +49 -0
- patchi/web/api/dev_check.py +320 -0
- patchi/web/api/fix.py +134 -0
- patchi/web/api/guard.py +65 -0
- patchi/web/api/hosted.py +128 -0
- patchi/web/api/hosted_v2.py +234 -0
- patchi/web/api/live_testing.py +699 -0
- patchi/web/api/scan.py +582 -0
- patchi/web/api/smart.py +111 -0
- patchi/web/api/tenant.py +117 -0
- patchi/web/api/tools.py +85 -0
- patchi/web/api_legacy.py +2401 -0
- patchi/web/app.py +310 -0
- patchi/web/events.py +483 -0
- patchi/web/routes/__init__.py +1 -0
- patchi/web/routes/assurance.py +541 -0
- patchi/web/routes/brain.py +36 -0
- patchi/web/routes/charter.py +227 -0
- patchi/web/routes/chat.py +23 -0
- patchi/web/routes/dashboard.py +558 -0
- patchi/web/routes/dashboard_v2.py +402 -0
- patchi/web/routes/findings.py +330 -0
- patchi/web/routes/guard.py +35 -0
- patchi/web/routes/history.py +126 -0
- patchi/web/routes/landing.py +52 -0
- patchi/web/routes/live_testing.py +24 -0
- patchi/web/routes/review.py +57 -0
- patchi/web/routes/self_improvement.py +110 -0
- patchi/web/routes/settings.py +49 -0
- patchi/web/routes/tokens.py +29 -0
- patchi/web/spawn.py +139 -0
- patchi/web/ws.py +141 -0
- patchi-0.7.5.dist-info/METADATA +315 -0
- patchi-0.7.5.dist-info/RECORD +496 -0
- patchi-0.7.5.dist-info/WHEEL +5 -0
- patchi-0.7.5.dist-info/licenses/LICENSE +60 -0
- 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)
|