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