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,692 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DeadCodeScanner — vulture + import-graph dual-signal dead code detection.
|
|
3
|
+
|
|
4
|
+
Strategy (per MASTER_REBUILD_BRIEF FIX 2C):
|
|
5
|
+
- Import graph detects file-level dead code (files with no importers).
|
|
6
|
+
- vulture (when installed) detects symbol-level dead code at confidence >= 90.
|
|
7
|
+
- Symbol findings are only reported when BOTH graph AND vulture agree.
|
|
8
|
+
- File-level dead code is always detected via graph alone.
|
|
9
|
+
- Patchi's own infrastructure folders are never flagged.
|
|
10
|
+
|
|
11
|
+
No keyword grep. No hardcoded summaries. Every scan derives from real code.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from ..brain.ast_utils import find_dead_symbols
|
|
22
|
+
from ..brain.import_graph import ImportGraph, build_import_graph, find_dead_files
|
|
23
|
+
from ..brain.languages import EXTENSION_MAP, Lang
|
|
24
|
+
from .base import (
|
|
25
|
+
AgentGroup,
|
|
26
|
+
AgentInput,
|
|
27
|
+
AgentResult,
|
|
28
|
+
BaseAgent,
|
|
29
|
+
Severity,
|
|
30
|
+
make_finding,
|
|
31
|
+
register,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Folders that are intentional entry points or infrastructure — never flag them
|
|
35
|
+
_SKIP_SEGMENTS = frozenset(
|
|
36
|
+
{
|
|
37
|
+
"cli",
|
|
38
|
+
"core",
|
|
39
|
+
"web",
|
|
40
|
+
"tests",
|
|
41
|
+
"test",
|
|
42
|
+
"commands",
|
|
43
|
+
"notifications",
|
|
44
|
+
"hosted",
|
|
45
|
+
"docs",
|
|
46
|
+
"scripts",
|
|
47
|
+
"benchmark",
|
|
48
|
+
"examples",
|
|
49
|
+
"example",
|
|
50
|
+
"demo",
|
|
51
|
+
"__pycache__",
|
|
52
|
+
".patchi",
|
|
53
|
+
"node_modules",
|
|
54
|
+
"dist",
|
|
55
|
+
"build",
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
_log = logging.getLogger("patchi.agents.dead_code_scanner")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _run_sglyon_deadcode(root: Path) -> list[dict] | None:
|
|
64
|
+
"""Run sglyon/deadcode if installed; parses JSON output."""
|
|
65
|
+
import shutil
|
|
66
|
+
import subprocess
|
|
67
|
+
import json
|
|
68
|
+
|
|
69
|
+
# Try binary first, then python -m deadcode
|
|
70
|
+
candidates = []
|
|
71
|
+
if shutil.which("deadcode"):
|
|
72
|
+
candidates.append(["deadcode", "--format", "json", str(root)])
|
|
73
|
+
# sglyon/deadcode also provides `deadcode` entrypoint via pip; fallback to module
|
|
74
|
+
candidates.append([shutil.which("python") or "python", "-m", "deadcode", "--format", "json", str(root)])
|
|
75
|
+
for cmd in candidates:
|
|
76
|
+
if not cmd[0] or (cmd[0] not in ("python", "python3") and not shutil.which(cmd[0])):
|
|
77
|
+
continue
|
|
78
|
+
try:
|
|
79
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=90)
|
|
80
|
+
if proc.returncode not in (0, 1):
|
|
81
|
+
continue
|
|
82
|
+
raw = proc.stdout.strip() or proc.stderr.strip()
|
|
83
|
+
if not raw:
|
|
84
|
+
continue
|
|
85
|
+
data = json.loads(raw)
|
|
86
|
+
# sglyon/deadcode JSON: [{file, line, symbol, kind}]
|
|
87
|
+
out: list[dict] = []
|
|
88
|
+
for item in data if isinstance(data, list) else data.get("results", []):
|
|
89
|
+
f = item.get("file") or item.get("filename") or ""
|
|
90
|
+
try:
|
|
91
|
+
rel = Path(f).relative_to(root).as_posix() if f else ""
|
|
92
|
+
except ValueError:
|
|
93
|
+
rel = f
|
|
94
|
+
if _should_skip(rel):
|
|
95
|
+
continue
|
|
96
|
+
out.append(
|
|
97
|
+
{
|
|
98
|
+
"file": rel or f,
|
|
99
|
+
"line": int(item.get("line", 0) or 0),
|
|
100
|
+
"name": item.get("symbol") or item.get("name", ""),
|
|
101
|
+
"type": item.get("kind") or item.get("type", "deadcode"),
|
|
102
|
+
"message": f"sglyon/deadcode: {item.get('kind','unused')} {item.get('symbol','')}",
|
|
103
|
+
"confidence": 90,
|
|
104
|
+
"code": "",
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
if out:
|
|
108
|
+
return out
|
|
109
|
+
except (subprocess.TimeoutExpired, json.JSONDecodeError, FileNotFoundError, OSError) as exc:
|
|
110
|
+
_log.debug("sglyon deadcode %s failed: %s", cmd[0], exc)
|
|
111
|
+
continue
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@register
|
|
116
|
+
class DeadCodeScanner(BaseAgent):
|
|
117
|
+
"""Dead code detection via vulture + import graph dual-signal."""
|
|
118
|
+
|
|
119
|
+
group = AgentGroup.SCANNER
|
|
120
|
+
name = "DeadCodeScanner"
|
|
121
|
+
timeout = 120
|
|
122
|
+
|
|
123
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
124
|
+
# 1. Try sglyon/deadcode first (unified JSON for Python/JS/TS/Go/Elixir)
|
|
125
|
+
sg = _run_sglyon_deadcode(inp.root)
|
|
126
|
+
if sg:
|
|
127
|
+
for item in sg:
|
|
128
|
+
result.findings.append(
|
|
129
|
+
make_finding(
|
|
130
|
+
severity=Severity.LOW,
|
|
131
|
+
file=item.get("file", ""),
|
|
132
|
+
line_start=item.get("line", 0),
|
|
133
|
+
title=f"Dead code (sglyon): {item.get('name', '')}",
|
|
134
|
+
description=item.get("message", ""),
|
|
135
|
+
evidence=item.get("code", ""),
|
|
136
|
+
finding_type="dead_code",
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
result.data["orchestrator"] = "sglyon/deadcode"
|
|
140
|
+
result.data["sglyon_count"] = len(sg)
|
|
141
|
+
result.status = AgentStatus.SUCCEEDED
|
|
142
|
+
result.files_scanned = len({f.file for f in result.findings}) if result.findings else 0
|
|
143
|
+
result.data["total_findings"] = len(result.findings)
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
# 2. Fallback: existing dual-signal (import graph + vulture + per-language tools)
|
|
147
|
+
# Build import graph — always available, no external tool needed
|
|
148
|
+
try:
|
|
149
|
+
graph = build_import_graph(inp.root)
|
|
150
|
+
except Exception as exc:
|
|
151
|
+
result.add_error(f"import graph failed: {exc}")
|
|
152
|
+
graph = ImportGraph()
|
|
153
|
+
|
|
154
|
+
# File-level dead code from graph alone
|
|
155
|
+
try:
|
|
156
|
+
from ..brain.scanner import FileScanner
|
|
157
|
+
|
|
158
|
+
scanner = FileScanner(inp.root, ignore_paths=inp.config.get("ignore_paths", []))
|
|
159
|
+
all_files = scanner.scan()
|
|
160
|
+
graph_dead_files = set(find_dead_files(all_files, graph))
|
|
161
|
+
except Exception as e:
|
|
162
|
+
_log.warning("DeadCodeScanner._run failed: %s", e)
|
|
163
|
+
all_files = []
|
|
164
|
+
graph_dead_files = set()
|
|
165
|
+
|
|
166
|
+
# Run vulture for symbol-level analysis (optional)
|
|
167
|
+
vulture_findings = _run_vulture(inp.root)
|
|
168
|
+
|
|
169
|
+
# Run per-language dead code tools (ts-prune, cargo deadlinks, etc.)
|
|
170
|
+
tool_findings = _run_all_tools(inp.root)
|
|
171
|
+
vulture_dead_files: set[str] = set()
|
|
172
|
+
if vulture_findings is not None:
|
|
173
|
+
for vf in vulture_findings:
|
|
174
|
+
if vf.get("confidence", 0) >= 90:
|
|
175
|
+
vulture_dead_files.add(vf["file"])
|
|
176
|
+
|
|
177
|
+
confirmed_dead: list[str] = []
|
|
178
|
+
uncertain: list[str] = []
|
|
179
|
+
|
|
180
|
+
# File-level findings
|
|
181
|
+
for rel_path in sorted(graph_dead_files):
|
|
182
|
+
if _should_skip(rel_path):
|
|
183
|
+
continue
|
|
184
|
+
is_vulture_confirmed = vulture_findings is not None and rel_path in vulture_dead_files
|
|
185
|
+
if is_vulture_confirmed:
|
|
186
|
+
confirmed_dead.append(rel_path)
|
|
187
|
+
result.findings.append(
|
|
188
|
+
make_finding(
|
|
189
|
+
severity=Severity.MEDIUM,
|
|
190
|
+
file=rel_path,
|
|
191
|
+
line_start=0,
|
|
192
|
+
title=f"Confirmed dead file: {rel_path}",
|
|
193
|
+
description="Not imported anywhere and vulture confirms no live symbols.",
|
|
194
|
+
finding_type="confirmed_dead",
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
else:
|
|
198
|
+
uncertain.append(rel_path)
|
|
199
|
+
result.findings.append(
|
|
200
|
+
make_finding(
|
|
201
|
+
severity=Severity.LOW,
|
|
202
|
+
file=rel_path,
|
|
203
|
+
line_start=0,
|
|
204
|
+
title=f"Possibly unused file: {rel_path}",
|
|
205
|
+
description="Not imported anywhere in the project.",
|
|
206
|
+
finding_type="uncertain_dead",
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Symbol-level findings from vulture (only when file is also graph-dead)
|
|
211
|
+
if vulture_findings:
|
|
212
|
+
for vf in vulture_findings:
|
|
213
|
+
rel_path = vf.get("file", "")
|
|
214
|
+
if _should_skip(rel_path):
|
|
215
|
+
continue
|
|
216
|
+
if vf.get("confidence", 0) < 90:
|
|
217
|
+
continue
|
|
218
|
+
# Only report symbol if the file is already in our dead set
|
|
219
|
+
if rel_path not in graph_dead_files:
|
|
220
|
+
continue
|
|
221
|
+
if rel_path in confirmed_dead or rel_path in uncertain:
|
|
222
|
+
continue # already reported at file level
|
|
223
|
+
result.findings.append(
|
|
224
|
+
make_finding(
|
|
225
|
+
severity=Severity.MEDIUM,
|
|
226
|
+
file=rel_path,
|
|
227
|
+
line_start=vf.get("line", 0),
|
|
228
|
+
title=f"Unused {vf.get('type', 'symbol')}: {vf.get('name', '')}",
|
|
229
|
+
description=f"vulture ({vf.get('confidence')}%): {vf.get('message', '')}",
|
|
230
|
+
evidence=vf.get("code", ""),
|
|
231
|
+
finding_type="dead_code",
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Per-language tool findings (not graph-cross-referenced)
|
|
236
|
+
for tf in tool_findings:
|
|
237
|
+
if not tf.get("file") or _should_skip(tf["file"]):
|
|
238
|
+
continue
|
|
239
|
+
result.findings.append(
|
|
240
|
+
make_finding(
|
|
241
|
+
severity=Severity.LOW,
|
|
242
|
+
file=tf["file"],
|
|
243
|
+
line_start=tf.get("line", 0),
|
|
244
|
+
title=f"Dead code ({tf.get('type', 'unknown')}): {tf.get('name', '')}",
|
|
245
|
+
description=tf.get("message", ""),
|
|
246
|
+
evidence=tf.get("code", ""),
|
|
247
|
+
finding_type="dead_code",
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
result.data["confirmed_dead"] = confirmed_dead
|
|
252
|
+
result.data["uncertain"] = uncertain
|
|
253
|
+
result.data["vulture_available"] = vulture_findings is not None
|
|
254
|
+
result.data["tool_findings_count"] = len(tool_findings)
|
|
255
|
+
result.files_scanned = len(graph.nodes)
|
|
256
|
+
|
|
257
|
+
# ── AST-based intra-file dead symbol detection (all parser langs) ──────
|
|
258
|
+
# Complements vulture (Python) and external per-language tools. Symbols
|
|
259
|
+
# for graph-dead files get a line-level dead-code signal even when no
|
|
260
|
+
# external toolchain is installed (multi-language parity).
|
|
261
|
+
ast_symbol_count = 0
|
|
262
|
+
for rel_path in sorted(graph_dead_files):
|
|
263
|
+
if _should_skip(rel_path):
|
|
264
|
+
continue
|
|
265
|
+
ext = Path(rel_path).suffix.lower()
|
|
266
|
+
lang = EXTENSION_MAP.get(ext)
|
|
267
|
+
if lang is None:
|
|
268
|
+
continue
|
|
269
|
+
if lang == Lang.PYTHON and vulture_findings is not None:
|
|
270
|
+
continue # vulture covers Python more precisely
|
|
271
|
+
fp = inp.root / rel_path
|
|
272
|
+
try:
|
|
273
|
+
content = fp.read_text(encoding="utf-8", errors="replace")
|
|
274
|
+
except OSError:
|
|
275
|
+
continue
|
|
276
|
+
for sym in find_dead_symbols(content, lang):
|
|
277
|
+
ast_symbol_count += 1
|
|
278
|
+
result.findings.append(
|
|
279
|
+
make_finding(
|
|
280
|
+
severity=Severity.LOW,
|
|
281
|
+
file=rel_path,
|
|
282
|
+
line_start=sym["line"],
|
|
283
|
+
title=f"Unused {sym['kind']}: {sym['name']}",
|
|
284
|
+
description=(
|
|
285
|
+
f"Symbol '{sym['name']}' is never referenced "
|
|
286
|
+
f"within {rel_path} (intra-file dead code)."
|
|
287
|
+
),
|
|
288
|
+
finding_type="dead_code",
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
result.data["ast_symbol_findings"] = ast_symbol_count
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# ── Per-language dead code tool helpers ─────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _detect_project_langs(root: Path) -> set[str]:
|
|
298
|
+
"""Detect which languages the project uses by looking for manifest files."""
|
|
299
|
+
langs: set[str] = set()
|
|
300
|
+
if (root / "Cargo.toml").exists():
|
|
301
|
+
langs.add("rust")
|
|
302
|
+
if (root / "go.mod").exists():
|
|
303
|
+
langs.add("go")
|
|
304
|
+
if (root / "Package.swift").exists():
|
|
305
|
+
langs.add("swift")
|
|
306
|
+
if (root / "composer.json").exists():
|
|
307
|
+
langs.add("php")
|
|
308
|
+
if (root / "Gemfile").exists():
|
|
309
|
+
langs.add("ruby")
|
|
310
|
+
if (root / "pom.xml").exists():
|
|
311
|
+
langs.add("java")
|
|
312
|
+
if (root / "build.gradle").exists() or (root / "build.gradle.kts").exists():
|
|
313
|
+
langs.add("java")
|
|
314
|
+
if (root / "package.json").exists():
|
|
315
|
+
langs.add("javascript")
|
|
316
|
+
if (
|
|
317
|
+
(root / "requirements.txt").exists()
|
|
318
|
+
or (root / "setup.py").exists()
|
|
319
|
+
or (root / "pyproject.toml").exists()
|
|
320
|
+
):
|
|
321
|
+
langs.add("python")
|
|
322
|
+
if list(root.glob("*.csproj")):
|
|
323
|
+
langs.add("csharp")
|
|
324
|
+
return langs
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _run_tool(
|
|
328
|
+
cmd: list[str],
|
|
329
|
+
root: Path,
|
|
330
|
+
timeout: int = 90,
|
|
331
|
+
) -> subprocess.CompletedProcess | None:
|
|
332
|
+
if not shutil.which(cmd[0]):
|
|
333
|
+
return None
|
|
334
|
+
try:
|
|
335
|
+
return subprocess.run(
|
|
336
|
+
cmd,
|
|
337
|
+
capture_output=True,
|
|
338
|
+
text=True,
|
|
339
|
+
timeout=timeout,
|
|
340
|
+
)
|
|
341
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _run_vulture(root: Path) -> list[dict] | None:
|
|
346
|
+
"""Run vulture for Python. Returns None if not installed."""
|
|
347
|
+
proc = _run_tool(["vulture", str(root), "--min-confidence", "90", "--json"], root)
|
|
348
|
+
if proc is None or proc.returncode not in (0, 1):
|
|
349
|
+
return None
|
|
350
|
+
raw = proc.stdout.strip()
|
|
351
|
+
if not raw:
|
|
352
|
+
return []
|
|
353
|
+
try:
|
|
354
|
+
data = json.loads(raw)
|
|
355
|
+
except json.JSONDecodeError:
|
|
356
|
+
return []
|
|
357
|
+
findings: list[dict] = []
|
|
358
|
+
for item in data:
|
|
359
|
+
abs_path = item.get("filename", "")
|
|
360
|
+
try:
|
|
361
|
+
rel = Path(abs_path).relative_to(root).as_posix()
|
|
362
|
+
except ValueError:
|
|
363
|
+
rel = abs_path
|
|
364
|
+
findings.append(
|
|
365
|
+
{
|
|
366
|
+
"file": rel,
|
|
367
|
+
"line": item.get("first_lineno", 0),
|
|
368
|
+
"name": item.get("name", ""),
|
|
369
|
+
"type": item.get("type", ""),
|
|
370
|
+
"message": item.get("message", ""),
|
|
371
|
+
"confidence": item.get("confidence", 0),
|
|
372
|
+
"code": item.get("code", ""),
|
|
373
|
+
}
|
|
374
|
+
)
|
|
375
|
+
return findings
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _run_ts_prune(root: Path) -> list[dict] | None:
|
|
379
|
+
"""
|
|
380
|
+
Run ts-prune for TypeScript unused exports.
|
|
381
|
+
npm install -g ts-prune or npx ts-prune
|
|
382
|
+
"""
|
|
383
|
+
proc = _run_tool(["npx", "--yes", "ts-prune", "--project", str(root / "tsconfig.json")], root)
|
|
384
|
+
if proc is None:
|
|
385
|
+
return None
|
|
386
|
+
findings: list[dict] = []
|
|
387
|
+
for line in proc.stdout.strip().splitlines():
|
|
388
|
+
if " - " not in line:
|
|
389
|
+
continue
|
|
390
|
+
parts = line.split(" - ", 1)
|
|
391
|
+
loc = parts[0].strip()
|
|
392
|
+
symbol = parts[1].strip() if len(parts) > 1 else ""
|
|
393
|
+
if ":" in loc:
|
|
394
|
+
file_part, line_part = loc.rsplit(":", 1)
|
|
395
|
+
line_num = int(line_part) if line_part.isdigit() else 0
|
|
396
|
+
else:
|
|
397
|
+
file_part = loc
|
|
398
|
+
line_num = 0
|
|
399
|
+
try:
|
|
400
|
+
rel = Path(file_part).as_posix()
|
|
401
|
+
except ValueError:
|
|
402
|
+
rel = file_part
|
|
403
|
+
findings.append(
|
|
404
|
+
{
|
|
405
|
+
"file": rel,
|
|
406
|
+
"line": line_num,
|
|
407
|
+
"name": symbol,
|
|
408
|
+
"type": "export",
|
|
409
|
+
"message": f"Unused export: {symbol}",
|
|
410
|
+
"confidence": 95,
|
|
411
|
+
"code": "",
|
|
412
|
+
}
|
|
413
|
+
)
|
|
414
|
+
return findings
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _run_cargo_dead_code(root: Path) -> list[dict] | None:
|
|
418
|
+
"""Run cargo udeps for Rust unused dependencies."""
|
|
419
|
+
findings: list[dict] = []
|
|
420
|
+
proc = _run_tool(["cargo", "udeps"], root, timeout=120)
|
|
421
|
+
if proc is not None:
|
|
422
|
+
for line in proc.stderr.strip().splitlines():
|
|
423
|
+
if "unused" in line.lower():
|
|
424
|
+
findings.append(
|
|
425
|
+
{
|
|
426
|
+
"file": "Cargo.toml",
|
|
427
|
+
"line": 0,
|
|
428
|
+
"name": line.strip(),
|
|
429
|
+
"type": "unused_dependency",
|
|
430
|
+
"message": line.strip(),
|
|
431
|
+
"confidence": 90,
|
|
432
|
+
"code": "",
|
|
433
|
+
}
|
|
434
|
+
)
|
|
435
|
+
return findings or None
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _run_go_dead_code(root: Path) -> list[dict] | None:
|
|
439
|
+
"""Run go vet + staticcheck for Go dead code."""
|
|
440
|
+
findings: list[dict] = []
|
|
441
|
+
# go vet -unusedresult removed in Go 1.19+; use go vet ./... with check for unused diagnostics
|
|
442
|
+
proc = _run_tool(["go", "vet", "./..."], root, timeout=120)
|
|
443
|
+
if proc is not None:
|
|
444
|
+
for line in proc.stderr.strip().splitlines():
|
|
445
|
+
if "unused" in line.lower() or "dead" in line.lower():
|
|
446
|
+
findings.append(
|
|
447
|
+
{
|
|
448
|
+
"file": "",
|
|
449
|
+
"line": 0,
|
|
450
|
+
"name": line.strip(),
|
|
451
|
+
"type": "go_dead_code",
|
|
452
|
+
"message": line.strip(),
|
|
453
|
+
"confidence": 85,
|
|
454
|
+
"code": "",
|
|
455
|
+
}
|
|
456
|
+
)
|
|
457
|
+
# golangci-lint with unused linter
|
|
458
|
+
proc = _run_tool(
|
|
459
|
+
["golangci-lint", "run", "--disable-all", "--enable=unused", "--timeout=2m"],
|
|
460
|
+
root,
|
|
461
|
+
timeout=180,
|
|
462
|
+
)
|
|
463
|
+
if proc is not None:
|
|
464
|
+
for line in proc.stdout.strip().splitlines():
|
|
465
|
+
if "unused" in line.lower():
|
|
466
|
+
findings.append(
|
|
467
|
+
{
|
|
468
|
+
"file": "",
|
|
469
|
+
"line": 0,
|
|
470
|
+
"name": line.strip(),
|
|
471
|
+
"type": "go_dead_code",
|
|
472
|
+
"message": line.strip(),
|
|
473
|
+
"confidence": 90,
|
|
474
|
+
"code": "",
|
|
475
|
+
}
|
|
476
|
+
)
|
|
477
|
+
return findings or None
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _run_debride(root: Path) -> list[dict] | None:
|
|
481
|
+
"""Run debride for Ruby dead code."""
|
|
482
|
+
proc = _run_tool(["debride", "."], root, timeout=120)
|
|
483
|
+
if proc is None:
|
|
484
|
+
return None
|
|
485
|
+
findings: list[dict] = []
|
|
486
|
+
for line in proc.stdout.strip().splitlines():
|
|
487
|
+
if ":" not in line:
|
|
488
|
+
continue
|
|
489
|
+
parts = line.split(":", 2)
|
|
490
|
+
if len(parts) < 2:
|
|
491
|
+
continue
|
|
492
|
+
file_part, line_part = parts[0], parts[1]
|
|
493
|
+
rest = parts[2] if len(parts) > 2 else ""
|
|
494
|
+
line_num = int(line_part) if line_part.isdigit() else 0
|
|
495
|
+
try:
|
|
496
|
+
rel = Path(file_part).as_posix()
|
|
497
|
+
except ValueError:
|
|
498
|
+
rel = file_part
|
|
499
|
+
findings.append(
|
|
500
|
+
{
|
|
501
|
+
"file": rel,
|
|
502
|
+
"line": line_num,
|
|
503
|
+
"name": rest.strip(),
|
|
504
|
+
"type": "ruby_dead_code",
|
|
505
|
+
"message": rest.strip(),
|
|
506
|
+
"confidence": 90,
|
|
507
|
+
"code": "",
|
|
508
|
+
}
|
|
509
|
+
)
|
|
510
|
+
return findings or None
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _run_composer_unused(root: Path) -> list[dict] | None:
|
|
514
|
+
"""Run composer-unused for PHP unused dependencies."""
|
|
515
|
+
proc = _run_tool(["composer", "unused"], root, timeout=120)
|
|
516
|
+
if proc is None:
|
|
517
|
+
return None
|
|
518
|
+
findings: list[dict] = []
|
|
519
|
+
for line in proc.stdout.strip().splitlines():
|
|
520
|
+
if "unused" in line.lower():
|
|
521
|
+
findings.append(
|
|
522
|
+
{
|
|
523
|
+
"file": "composer.json",
|
|
524
|
+
"line": 0,
|
|
525
|
+
"name": line.strip(),
|
|
526
|
+
"type": "unused_dependency",
|
|
527
|
+
"message": line.strip(),
|
|
528
|
+
"confidence": 85,
|
|
529
|
+
"code": "",
|
|
530
|
+
}
|
|
531
|
+
)
|
|
532
|
+
return findings or None
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _run_dotnet_dead_code(root: Path) -> list[dict] | None:
|
|
536
|
+
"""Run dotnet tool for C# dead code analysis."""
|
|
537
|
+
csproj_files = list(root.glob("*.csproj"))
|
|
538
|
+
if not csproj_files:
|
|
539
|
+
csproj_files = list(root.glob("**/*.csproj"))
|
|
540
|
+
if not csproj_files:
|
|
541
|
+
return None
|
|
542
|
+
proc = _run_tool(
|
|
543
|
+
["dotnet", "list", str(csproj_files[0]), "package", "--vulnerable"], root, timeout=120
|
|
544
|
+
)
|
|
545
|
+
if proc is None:
|
|
546
|
+
return None
|
|
547
|
+
findings: list[dict] = []
|
|
548
|
+
for line in proc.stdout.strip().splitlines():
|
|
549
|
+
if "unused" in line.lower() or "not used" in line.lower():
|
|
550
|
+
findings.append(
|
|
551
|
+
{
|
|
552
|
+
"file": csproj_files[0].relative_to(root).as_posix(),
|
|
553
|
+
"line": 0,
|
|
554
|
+
"name": line.strip(),
|
|
555
|
+
"type": "unused_dependency",
|
|
556
|
+
"message": line.strip(),
|
|
557
|
+
"confidence": 80,
|
|
558
|
+
"code": "",
|
|
559
|
+
}
|
|
560
|
+
)
|
|
561
|
+
return findings or None
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def _run_jdeps(root: Path) -> list[dict] | None:
|
|
565
|
+
"""Run jdeps for Java unused dependencies."""
|
|
566
|
+
proc = _run_tool(["jdeps", "-summary", "-recursive", "."], root, timeout=120)
|
|
567
|
+
if proc is None:
|
|
568
|
+
# Try mvn dependency:analyze as an alternative
|
|
569
|
+
proc = _run_tool(["mvn", "dependency:analyze", "-DignoreNonCompile"], root, timeout=180)
|
|
570
|
+
if proc is None:
|
|
571
|
+
return None
|
|
572
|
+
findings: list[dict] = []
|
|
573
|
+
for line in proc.stdout.strip().splitlines():
|
|
574
|
+
if "Unused" in line or "unused" in line:
|
|
575
|
+
findings.append(
|
|
576
|
+
{
|
|
577
|
+
"file": "pom.xml",
|
|
578
|
+
"line": 0,
|
|
579
|
+
"name": line.strip(),
|
|
580
|
+
"type": "unused_dependency",
|
|
581
|
+
"message": line.strip(),
|
|
582
|
+
"confidence": 85,
|
|
583
|
+
"code": "",
|
|
584
|
+
}
|
|
585
|
+
)
|
|
586
|
+
return findings or None
|
|
587
|
+
findings = []
|
|
588
|
+
for line in proc.stdout.strip().splitlines():
|
|
589
|
+
if "->" not in line:
|
|
590
|
+
continue
|
|
591
|
+
parts = line.split("->")
|
|
592
|
+
if len(parts) >= 2:
|
|
593
|
+
source = parts[0].strip()
|
|
594
|
+
findings.append(
|
|
595
|
+
{
|
|
596
|
+
"file": source if source else "",
|
|
597
|
+
"line": 0,
|
|
598
|
+
"name": parts[1].strip(),
|
|
599
|
+
"type": "java_dependency",
|
|
600
|
+
"message": line.strip(),
|
|
601
|
+
"confidence": 80,
|
|
602
|
+
"code": "",
|
|
603
|
+
}
|
|
604
|
+
)
|
|
605
|
+
return findings or None
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def _run_swift_dead_code(root: Path) -> list[dict] | None:
|
|
609
|
+
"""Run swiftc dead code analysis for Swift."""
|
|
610
|
+
proc = _run_tool(["swiftc", "-typecheck", "-unused-imports"], root, timeout=120)
|
|
611
|
+
if proc is None:
|
|
612
|
+
return None
|
|
613
|
+
findings: list[dict] = []
|
|
614
|
+
for line in proc.stderr.strip().splitlines():
|
|
615
|
+
if "unused" in line.lower():
|
|
616
|
+
findings.append(
|
|
617
|
+
{
|
|
618
|
+
"file": "",
|
|
619
|
+
"line": 0,
|
|
620
|
+
"name": line.strip(),
|
|
621
|
+
"type": "swift_unused",
|
|
622
|
+
"message": line.strip(),
|
|
623
|
+
"confidence": 85,
|
|
624
|
+
"code": "",
|
|
625
|
+
}
|
|
626
|
+
)
|
|
627
|
+
return findings or None
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def _run_clang_tidy(root: Path) -> list[dict] | None:
|
|
631
|
+
"""Run clang-tidy for C/C++ dead code detection."""
|
|
632
|
+
proc = _run_tool(
|
|
633
|
+
["clang-tidy", "--checks=-*,misc-unused-*", "--list-checks"], root, timeout=120
|
|
634
|
+
)
|
|
635
|
+
if proc is None:
|
|
636
|
+
return None
|
|
637
|
+
findings: list[dict] = []
|
|
638
|
+
for line in proc.stdout.strip().splitlines():
|
|
639
|
+
if "unused" in line.lower():
|
|
640
|
+
findings.append(
|
|
641
|
+
{
|
|
642
|
+
"file": "",
|
|
643
|
+
"line": 0,
|
|
644
|
+
"name": line.strip(),
|
|
645
|
+
"type": "c_cpp_unused",
|
|
646
|
+
"message": line.strip(),
|
|
647
|
+
"confidence": 80,
|
|
648
|
+
"code": "",
|
|
649
|
+
}
|
|
650
|
+
)
|
|
651
|
+
return findings or None
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
TOOL_DISPATCH: dict[str, list[tuple[str, str, str]]] = {
|
|
655
|
+
# Note: python/vulture is NOT listed — it's called directly in _run() for graph cross-referencing
|
|
656
|
+
"javascript": [("ts-prune", "unused exports", "ts_prune")],
|
|
657
|
+
"rust": [("cargo deadlinks/udeps", "dead code/deps", "cargo_dead_code")],
|
|
658
|
+
"go": [("go vet/golangci-lint", "dead code", "go_dead_code")],
|
|
659
|
+
"ruby": [("debride", "dead methods", "debride")],
|
|
660
|
+
"php": [("composer-unused", "unused deps", "composer_unused")],
|
|
661
|
+
"csharp": [("dotnet list package", "unused deps", "dotnet_dead_code")],
|
|
662
|
+
"java": [("jdeps/mvn dependency:analyze", "unused deps", "jdeps")],
|
|
663
|
+
"swift": [("swiftc -unused-imports", "unused imports", "swift_dead_code")],
|
|
664
|
+
"c": [("clang-tidy", "unused declarations", "clang_tidy")],
|
|
665
|
+
"cpp": [("clang-tidy", "unused declarations", "clang_tidy")],
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _run_all_tools(root: Path) -> list[dict]:
|
|
670
|
+
"""Run all available dead code tools for the project's detected languages."""
|
|
671
|
+
all_findings: list[dict] = []
|
|
672
|
+
langs = _detect_project_langs(root)
|
|
673
|
+
for lang in langs:
|
|
674
|
+
_, _, func_suffix = (TOOL_DISPATCH.get(lang) or [("", "", "")])[0]
|
|
675
|
+
if not func_suffix:
|
|
676
|
+
continue
|
|
677
|
+
func_name = f"_run_{func_suffix}"
|
|
678
|
+
func = globals().get(func_name)
|
|
679
|
+
if func is None:
|
|
680
|
+
continue
|
|
681
|
+
try:
|
|
682
|
+
findings = func(root)
|
|
683
|
+
if findings:
|
|
684
|
+
all_findings.extend(findings)
|
|
685
|
+
except Exception as e:
|
|
686
|
+
_log.warning("_run_all_tools failed: %s", e)
|
|
687
|
+
return all_findings
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _should_skip(rel_path: str) -> bool:
|
|
691
|
+
parts = rel_path.lower().split("/")
|
|
692
|
+
return any(seg in _SKIP_SEGMENTS for seg in parts)
|