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,442 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TestScanner — test file discovery, coverage mapping.
|
|
3
|
+
|
|
4
|
+
Scans for test files and maps which source files they cover:
|
|
5
|
+
- Jest/React (test.js, test.jsx, spec.js, spec.jsx, *.test.*, *.spec.*)
|
|
6
|
+
- PyTest/Python (test_*.py, *_test.py, test/*.py)
|
|
7
|
+
- JUnit/Java (Test*.java, *Test.java)
|
|
8
|
+
- PHPUnit/PHP (Test*.php, *Test.php)
|
|
9
|
+
- RSpec/Ruby (*_spec.rb, spec/*.rb)
|
|
10
|
+
|
|
11
|
+
Maps test → source relationships and identifies uncovered files.
|
|
12
|
+
|
|
13
|
+
Does NOT call AI.
|
|
14
|
+
Does NOT write to disk.
|
|
15
|
+
Does NOT touch the queue.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import re
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from patchi.core.brain.languages import DEFAULT_IGNORE_DIRS
|
|
25
|
+
|
|
26
|
+
from .base import (
|
|
27
|
+
AgentGroup,
|
|
28
|
+
AgentInput,
|
|
29
|
+
AgentResult,
|
|
30
|
+
BaseAgent,
|
|
31
|
+
Finding,
|
|
32
|
+
Severity,
|
|
33
|
+
make_finding,
|
|
34
|
+
register,
|
|
35
|
+
safe_rglob,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
_log = logging.getLogger("patchi.agents.test_scanner")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@register
|
|
42
|
+
class TestScanner(BaseAgent):
|
|
43
|
+
"""Scanner for test files and coverage mapping."""
|
|
44
|
+
|
|
45
|
+
group = AgentGroup.SCANNER
|
|
46
|
+
name = "TestScanner"
|
|
47
|
+
description = "Test file discovery, coverage mapping"
|
|
48
|
+
|
|
49
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
50
|
+
"""Scan for test files and map coverage."""
|
|
51
|
+
findings = []
|
|
52
|
+
|
|
53
|
+
# Define test file patterns
|
|
54
|
+
test_patterns = [
|
|
55
|
+
# JavaScript/TypeScript
|
|
56
|
+
"*test*.js",
|
|
57
|
+
"*test*.jsx",
|
|
58
|
+
"*test*.ts",
|
|
59
|
+
"*test*.tsx",
|
|
60
|
+
"*spec*.js",
|
|
61
|
+
"*spec*.jsx",
|
|
62
|
+
"*spec*.ts",
|
|
63
|
+
"*spec*.tsx",
|
|
64
|
+
"*.test.js",
|
|
65
|
+
"*.test.jsx",
|
|
66
|
+
"*.test.ts",
|
|
67
|
+
"*.test.tsx",
|
|
68
|
+
"*.spec.js",
|
|
69
|
+
"*.spec.jsx",
|
|
70
|
+
"*.spec.ts",
|
|
71
|
+
"*.spec.tsx",
|
|
72
|
+
"**/test/**",
|
|
73
|
+
"**/__tests__/**",
|
|
74
|
+
"**/tests/**",
|
|
75
|
+
# Python
|
|
76
|
+
"test_*.py",
|
|
77
|
+
"*_test.py",
|
|
78
|
+
"tests/**/*.py",
|
|
79
|
+
"**/test/**/*.py",
|
|
80
|
+
"conftest.py",
|
|
81
|
+
"pytest.ini",
|
|
82
|
+
"tox.ini",
|
|
83
|
+
# Java
|
|
84
|
+
"*Test.java",
|
|
85
|
+
"Test*.java",
|
|
86
|
+
"**/test/**/java/**",
|
|
87
|
+
"**/src/test/**",
|
|
88
|
+
# PHP
|
|
89
|
+
"*Test.php",
|
|
90
|
+
"Test*.php",
|
|
91
|
+
"*Test.php",
|
|
92
|
+
"**/tests/**",
|
|
93
|
+
# Ruby
|
|
94
|
+
"*_spec.rb",
|
|
95
|
+
"spec/**/*.rb",
|
|
96
|
+
"**/spec/**",
|
|
97
|
+
# Go
|
|
98
|
+
"*_test.go",
|
|
99
|
+
# Rust
|
|
100
|
+
"*_test.rs",
|
|
101
|
+
"tests/**/*.rs",
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
# Search for test files
|
|
105
|
+
test_files = set()
|
|
106
|
+
for pattern in test_patterns:
|
|
107
|
+
for file_path in safe_rglob(inp.root, pattern):
|
|
108
|
+
if file_path.is_file():
|
|
109
|
+
rel_path = file_path.relative_to(inp.root).as_posix()
|
|
110
|
+
if not self._should_skip_file(rel_path, inp):
|
|
111
|
+
test_files.add(rel_path)
|
|
112
|
+
|
|
113
|
+
# Map test files to source files
|
|
114
|
+
test_mappings = self._map_tests_to_source(test_files, inp)
|
|
115
|
+
|
|
116
|
+
# Count total test files
|
|
117
|
+
findings.append(
|
|
118
|
+
self._mkf(
|
|
119
|
+
severity=Severity.INFO,
|
|
120
|
+
file="__summary__",
|
|
121
|
+
line_start=0,
|
|
122
|
+
title=f"Test Files Found: {len(test_files)}",
|
|
123
|
+
description=f"Discovered {len(test_files)} test files in project",
|
|
124
|
+
evidence=f"Patterns: {len(test_patterns)} used",
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Add findings for each test file
|
|
129
|
+
for test_file in sorted(test_files):
|
|
130
|
+
findings.append(
|
|
131
|
+
self._mkf(
|
|
132
|
+
severity=Severity.INFO,
|
|
133
|
+
file=test_file,
|
|
134
|
+
line_start=0,
|
|
135
|
+
title="Test File",
|
|
136
|
+
description="Discovered test file",
|
|
137
|
+
evidence="Test file identified by pattern matching",
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Add findings for coverage mappings
|
|
142
|
+
for test_file, source_files in test_mappings.items():
|
|
143
|
+
if source_files:
|
|
144
|
+
findings.append(
|
|
145
|
+
self._mkf(
|
|
146
|
+
severity=Severity.INFO,
|
|
147
|
+
file=test_file,
|
|
148
|
+
line_start=0,
|
|
149
|
+
title="Covers Source Files",
|
|
150
|
+
description=f"Test file covers: {', '.join(sorted(source_files)[:3])}{'...' if len(source_files) > 3 else ''}",
|
|
151
|
+
evidence=f"Covers {len(source_files)} source files",
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Identify uncovered source files
|
|
156
|
+
all_source_files = self._find_all_source_files(inp)
|
|
157
|
+
covered_files = set()
|
|
158
|
+
for source_list in test_mappings.values():
|
|
159
|
+
covered_files.update(source_list)
|
|
160
|
+
|
|
161
|
+
uncovered_files = all_source_files - covered_files
|
|
162
|
+
for uncovered in sorted(uncovered_files):
|
|
163
|
+
findings.append(
|
|
164
|
+
self._mkf(
|
|
165
|
+
severity=Severity.MEDIUM,
|
|
166
|
+
file=uncovered,
|
|
167
|
+
line_start=0,
|
|
168
|
+
title="Uncovered Source File",
|
|
169
|
+
description="Source file has no corresponding test coverage",
|
|
170
|
+
evidence="Not referenced by any test file",
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Calculate coverage percentage
|
|
175
|
+
total_sources = len(all_source_files)
|
|
176
|
+
covered_count = len(covered_files)
|
|
177
|
+
coverage_pct = (covered_count / total_sources * 100) if total_sources > 0 else 0
|
|
178
|
+
|
|
179
|
+
findings.append(
|
|
180
|
+
self._mkf(
|
|
181
|
+
severity=Severity.INFO,
|
|
182
|
+
file="__coverage__",
|
|
183
|
+
line_start=0,
|
|
184
|
+
title=f"Test Coverage: {coverage_pct:.1f}%",
|
|
185
|
+
description=f"Coverage: {covered_count}/{total_sources} files covered",
|
|
186
|
+
evidence=f"{covered_count} covered, {len(uncovered_files)} uncovered",
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
for finding in findings:
|
|
191
|
+
result.add_finding(finding)
|
|
192
|
+
|
|
193
|
+
result.data["test_files"] = sorted(test_files)
|
|
194
|
+
result.data["source_files"] = sorted(all_source_files)
|
|
195
|
+
result.data["coverage_pct"] = coverage_pct
|
|
196
|
+
result.data["uncovered_files"] = sorted(uncovered_files)
|
|
197
|
+
result.data["test_files_found"] = len(test_files)
|
|
198
|
+
result.data["covered_files"] = covered_count
|
|
199
|
+
result.data["uncovered_file_count"] = len(uncovered_files)
|
|
200
|
+
result.data["needs_ai"] = False
|
|
201
|
+
result.files_scanned = len(test_files) + len(all_source_files)
|
|
202
|
+
|
|
203
|
+
def _mkf(self, *args, **kwargs) -> Finding:
|
|
204
|
+
"""Compatibility wrapper for finding construction."""
|
|
205
|
+
if args and isinstance(args[0], Severity):
|
|
206
|
+
severity = args[0]
|
|
207
|
+
file = args[1] if len(args) > 1 else ""
|
|
208
|
+
line_start = args[2] if len(args) > 2 else 0
|
|
209
|
+
title = args[3] if len(args) > 3 else ""
|
|
210
|
+
description = args[4] if len(args) > 4 else ""
|
|
211
|
+
evidence = args[5] if len(args) > 5 else ""
|
|
212
|
+
finding_type = kwargs.pop("finding_type", None) or title.lower().replace(
|
|
213
|
+
" ", "_"
|
|
214
|
+
).replace(":", "").replace("'", "").replace("-", "_")
|
|
215
|
+
return make_finding(
|
|
216
|
+
self.name,
|
|
217
|
+
finding_type,
|
|
218
|
+
severity,
|
|
219
|
+
file,
|
|
220
|
+
description,
|
|
221
|
+
line=line_start,
|
|
222
|
+
evidence=evidence,
|
|
223
|
+
**kwargs,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if "severity" in kwargs and ("file" in kwargs or "file_path" in kwargs):
|
|
227
|
+
severity = kwargs.pop("severity")
|
|
228
|
+
file = kwargs.pop("file", kwargs.pop("file_path", ""))
|
|
229
|
+
line_start = kwargs.pop("line_start", kwargs.pop("line", 0))
|
|
230
|
+
title = kwargs.pop("title", "")
|
|
231
|
+
description = kwargs.pop("description", title)
|
|
232
|
+
evidence = kwargs.pop("evidence", "")
|
|
233
|
+
finding_type = kwargs.pop("finding_type", None) or title.lower().replace(
|
|
234
|
+
" ", "_"
|
|
235
|
+
).replace(":", "").replace("'", "").replace("-", "_")
|
|
236
|
+
return make_finding(
|
|
237
|
+
self.name,
|
|
238
|
+
finding_type,
|
|
239
|
+
severity,
|
|
240
|
+
file,
|
|
241
|
+
description,
|
|
242
|
+
line=line_start,
|
|
243
|
+
evidence=evidence,
|
|
244
|
+
**kwargs,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return make_finding(*args, **kwargs)
|
|
248
|
+
|
|
249
|
+
def _should_skip_file(self, file_path: str, inp: AgentInput) -> bool:
|
|
250
|
+
"""Check if file should be skipped based on restrictions."""
|
|
251
|
+
# Skip ignored directories (node_modules, .venv, etc.)
|
|
252
|
+
from pathlib import PurePosixPath
|
|
253
|
+
|
|
254
|
+
if any(p in DEFAULT_IGNORE_DIRS for p in PurePosixPath(file_path).parts):
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
# Check restrictions
|
|
258
|
+
restrictions = inp.config.get("restrictions", [])
|
|
259
|
+
for r in restrictions:
|
|
260
|
+
if r.get("enabled", True):
|
|
261
|
+
path = r["path"]
|
|
262
|
+
if file_path.startswith(path) or PurePosixPath(file_path).match(path):
|
|
263
|
+
if r["type"] == "NO_TOUCH":
|
|
264
|
+
return True
|
|
265
|
+
elif r["type"] == "SCAN_ONLY" and self.__class__.__name__ == "FixAgent":
|
|
266
|
+
return True
|
|
267
|
+
return False
|
|
268
|
+
|
|
269
|
+
def _map_tests_to_source(self, test_files: set[str], inp: AgentInput) -> dict[str, list[str]]:
|
|
270
|
+
"""Map test files to the source files they likely cover."""
|
|
271
|
+
mappings = {}
|
|
272
|
+
|
|
273
|
+
for test_file in test_files:
|
|
274
|
+
test_path = Path(test_file)
|
|
275
|
+
test_name = test_path.stem.replace(".test", "").replace(".spec", "")
|
|
276
|
+
|
|
277
|
+
# Remove common test suffixes/prefixes
|
|
278
|
+
clean_test_name = test_name
|
|
279
|
+
for suffix in ["_test", "test_", "_spec", "spec_"]:
|
|
280
|
+
if clean_test_name.endswith(suffix):
|
|
281
|
+
clean_test_name = clean_test_name[: -len(suffix)]
|
|
282
|
+
if clean_test_name.startswith(suffix):
|
|
283
|
+
clean_test_name = clean_test_name[len(suffix) :]
|
|
284
|
+
|
|
285
|
+
# Find potential source file matches
|
|
286
|
+
potential_matches = []
|
|
287
|
+
|
|
288
|
+
# Look for source file with same name in adjacent directories
|
|
289
|
+
test_dir = test_path.parent
|
|
290
|
+
for ext in [".js", ".jsx", ".ts", ".tsx", ".py", ".java", ".php", ".rb", ".go", ".rs"]:
|
|
291
|
+
# Same directory
|
|
292
|
+
alt_name = test_dir / f"{clean_test_name}{ext}"
|
|
293
|
+
if (inp.root / alt_name).exists():
|
|
294
|
+
potential_matches.append(str(alt_name))
|
|
295
|
+
|
|
296
|
+
# Adjacent src directory
|
|
297
|
+
if "test" in str(test_dir).lower() or "spec" in str(test_dir).lower():
|
|
298
|
+
# We're in a test directory, look for source in sibling src/ directory
|
|
299
|
+
parts = test_dir.parts
|
|
300
|
+
for i, part in enumerate(parts):
|
|
301
|
+
if part in ["test", "tests", "__tests__", "spec"]:
|
|
302
|
+
# Try replacing with 'src'
|
|
303
|
+
new_parts = list(parts)
|
|
304
|
+
new_parts[i] = "src"
|
|
305
|
+
src_dir = Path(*new_parts)
|
|
306
|
+
src_file = src_dir / f"{clean_test_name}{ext}"
|
|
307
|
+
if (inp.root / src_file).exists():
|
|
308
|
+
potential_matches.append(str(src_file))
|
|
309
|
+
|
|
310
|
+
# Look for source in src directory
|
|
311
|
+
src_alt = (
|
|
312
|
+
Path("src") / test_path.relative_to(test_dir).parent / f"{clean_test_name}{ext}"
|
|
313
|
+
)
|
|
314
|
+
if (inp.root / src_alt).exists():
|
|
315
|
+
potential_matches.append(str(src_alt))
|
|
316
|
+
|
|
317
|
+
# Also look for imports/requires in the test file that might indicate covered sources
|
|
318
|
+
try:
|
|
319
|
+
test_content_path = inp.root / test_file
|
|
320
|
+
if test_content_path.exists():
|
|
321
|
+
content = test_content_path.read_text(encoding="utf-8")
|
|
322
|
+
imported_sources = self._find_imported_sources(content, test_file, inp)
|
|
323
|
+
potential_matches.extend(imported_sources)
|
|
324
|
+
except Exception as e:
|
|
325
|
+
_log.warning("TestScanner._map_tests_to_source failed: %s", e)
|
|
326
|
+
|
|
327
|
+
# Remove duplicates and filter to actual existing files
|
|
328
|
+
unique_matches = []
|
|
329
|
+
for match in potential_matches:
|
|
330
|
+
if match not in unique_matches and (inp.root / match).exists():
|
|
331
|
+
unique_matches.append(match)
|
|
332
|
+
|
|
333
|
+
mappings[test_file] = unique_matches
|
|
334
|
+
|
|
335
|
+
return mappings
|
|
336
|
+
|
|
337
|
+
def _find_imported_sources(self, content: str, test_file: str, inp: AgentInput) -> list[str]:
|
|
338
|
+
"""Find source files imported by the test file."""
|
|
339
|
+
matches = []
|
|
340
|
+
test_ext = Path(test_file).suffix
|
|
341
|
+
|
|
342
|
+
# Different import patterns based on language
|
|
343
|
+
if test_ext in [".js", ".jsx", ".ts", ".tsx"]:
|
|
344
|
+
# JavaScript/TypeScript import patterns
|
|
345
|
+
patterns = [
|
|
346
|
+
r"from\s+['\"](\.{1,2}/[^'\"]+)['\"]", # ES6 imports
|
|
347
|
+
r"import\s+['\"](\.{1,2}/[^'\"]+)['\"]", # import statements
|
|
348
|
+
r"require\s*\(\s*['\"](\.{1,2}/[^'\"]+)['\"]\s*\)", # require calls
|
|
349
|
+
r"require\.resolve\s*\(\s*['\"](\.{1,2}/[^'\"]+)['\"]\s*\)", # require.resolve
|
|
350
|
+
]
|
|
351
|
+
|
|
352
|
+
for pattern in patterns:
|
|
353
|
+
for match in re.finditer(pattern, content):
|
|
354
|
+
import_path = match.group(1)
|
|
355
|
+
# Resolve relative path
|
|
356
|
+
resolved_path = Path(test_file).parent / import_path
|
|
357
|
+
resolved_path = resolved_path.resolve().relative_to(inp.root.resolve())
|
|
358
|
+
source_path = str(resolved_path).replace("\\", "/")
|
|
359
|
+
|
|
360
|
+
# Check if it exists and is not another test file
|
|
361
|
+
if (inp.root / source_path).exists() and not self._is_test_file(source_path):
|
|
362
|
+
matches.append(source_path)
|
|
363
|
+
|
|
364
|
+
elif test_ext == ".py":
|
|
365
|
+
# Python import patterns
|
|
366
|
+
patterns = [
|
|
367
|
+
r"from\s+([a-zA-Z_][a-zA-Z0-9_.]*)\s+import", # from x import y
|
|
368
|
+
r"import\s+([a-zA-Z_][a-zA-Z0-9_.]+)", # import x
|
|
369
|
+
r"from\s+\.{1,2}/?([a-zA-Z_][a-zA-Z0-9_/]+)", # relative imports
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
for pattern in patterns:
|
|
373
|
+
for match in re.finditer(pattern, content):
|
|
374
|
+
import_module = match.group(1).replace(".", "/")
|
|
375
|
+
# Try to convert module name to file path
|
|
376
|
+
for ext in [".py", ".pyx"]:
|
|
377
|
+
module_path = f"{import_module}{ext}"
|
|
378
|
+
if (inp.root / module_path).exists() and not self._is_test_file(
|
|
379
|
+
module_path
|
|
380
|
+
):
|
|
381
|
+
matches.append(module_path)
|
|
382
|
+
|
|
383
|
+
return matches
|
|
384
|
+
|
|
385
|
+
def _is_test_file(self, file_path: str) -> bool:
|
|
386
|
+
"""Check if a file is a test file."""
|
|
387
|
+
path = Path(file_path)
|
|
388
|
+
stem = path.stem.lower()
|
|
389
|
+
parent_dirs = [part.lower() for part in path.parts]
|
|
390
|
+
|
|
391
|
+
# Check if filename contains test/spec patterns
|
|
392
|
+
test_indicators = ["test", "spec"]
|
|
393
|
+
if any(indicator in stem for indicator in test_indicators):
|
|
394
|
+
return True
|
|
395
|
+
|
|
396
|
+
# Check if file is in test directories
|
|
397
|
+
if any(test_dir in parent_dirs for test_dir in ["test", "tests", "__tests__", "spec"]):
|
|
398
|
+
return True
|
|
399
|
+
|
|
400
|
+
# Check if it's a test file by extension pattern
|
|
401
|
+
if any(
|
|
402
|
+
test_pattern in file_path.lower()
|
|
403
|
+
for test_pattern in ["test.", "spec.", ".test", ".spec"]
|
|
404
|
+
):
|
|
405
|
+
return True
|
|
406
|
+
|
|
407
|
+
return False
|
|
408
|
+
|
|
409
|
+
def _find_all_source_files(self, inp: AgentInput) -> set[str]:
|
|
410
|
+
"""Find all source files in the project (excluding test files)."""
|
|
411
|
+
source_extensions = {
|
|
412
|
+
".js",
|
|
413
|
+
".jsx",
|
|
414
|
+
".ts",
|
|
415
|
+
".tsx", # JavaScript/TypeScript
|
|
416
|
+
".py", # Python
|
|
417
|
+
".java", # Java
|
|
418
|
+
".php", # PHP
|
|
419
|
+
".rb", # Ruby
|
|
420
|
+
".go", # Go
|
|
421
|
+
".rs", # Rust
|
|
422
|
+
".cpp",
|
|
423
|
+
".cxx",
|
|
424
|
+
".cc",
|
|
425
|
+
".c",
|
|
426
|
+
".h",
|
|
427
|
+
".hpp", # C/C++
|
|
428
|
+
".cs", # C#
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
all_files = set()
|
|
432
|
+
|
|
433
|
+
for ext in source_extensions:
|
|
434
|
+
for file_path in safe_rglob(inp.root, f"*{ext}"):
|
|
435
|
+
if file_path.is_file():
|
|
436
|
+
rel_path = file_path.relative_to(inp.root).as_posix()
|
|
437
|
+
if not self._is_test_file(rel_path) and not self._should_skip_file(
|
|
438
|
+
rel_path, inp
|
|
439
|
+
):
|
|
440
|
+
all_files.add(rel_path)
|
|
441
|
+
|
|
442
|
+
return all_files
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool Health — three-state probe for external security tooling.
|
|
3
|
+
|
|
4
|
+
Reconstructed module (referenced by cli/commands/doctor_cmd.py; original
|
|
5
|
+
was lost before any commit existed). A tool that is installed but not
|
|
6
|
+
runnable must be reported as *broken*, never silently as present-and-fine
|
|
7
|
+
or as absent:
|
|
8
|
+
|
|
9
|
+
check_tool("bandit") -> {"status": "ok", "version": "1.7.5", "hint": ""}
|
|
10
|
+
{"status": "broken", "version": "", "hint": "..."}
|
|
11
|
+
{"status": "missing", "version": "", "hint": ""}
|
|
12
|
+
|
|
13
|
+
_TOOLS is the registry of known probeable tools; unknown names fall back
|
|
14
|
+
to the caller's own PATH/import check.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
import re
|
|
21
|
+
import shutil
|
|
22
|
+
import subprocess
|
|
23
|
+
|
|
24
|
+
_log = logging.getLogger("patchi.core.agents.tool_health")
|
|
25
|
+
|
|
26
|
+
_VERSION_TIMEOUT_S = 10
|
|
27
|
+
|
|
28
|
+
# Tools this module knows how to probe end-to-end.
|
|
29
|
+
_TOOLS = frozenset(
|
|
30
|
+
{
|
|
31
|
+
"bandit",
|
|
32
|
+
"semgrep",
|
|
33
|
+
"codeql",
|
|
34
|
+
"pyre",
|
|
35
|
+
"safety",
|
|
36
|
+
"pip-audit",
|
|
37
|
+
"git",
|
|
38
|
+
"node",
|
|
39
|
+
"npm",
|
|
40
|
+
"ollama",
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Per-tool run flags: some tools don't answer `--version` nicely.
|
|
45
|
+
_VERSION_ARGS: dict[str, list[str]] = {
|
|
46
|
+
"codeql": ["version"],
|
|
47
|
+
"pyre": ["--version"],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Focused hints when a present tool fails to run.
|
|
51
|
+
_BROKEN_HINTS: dict[str, str] = {
|
|
52
|
+
"codeql": "codeql binary missing — install the CodeQL CLI and add it to PATH",
|
|
53
|
+
"pyre": "pyre.bin not runnable on this platform — see pyre-check install docs",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _extract_version(output: str, tool: str) -> str:
|
|
58
|
+
"""Pull a plausible version token out of a --version blob."""
|
|
59
|
+
for line in output.splitlines():
|
|
60
|
+
line = line.strip()
|
|
61
|
+
if not line:
|
|
62
|
+
continue
|
|
63
|
+
m = re.search(r"\d+\.\d+(?:\.\d+)?[a-zA-Z0-9.\-]*", line)
|
|
64
|
+
if m:
|
|
65
|
+
return m.group(0)
|
|
66
|
+
# Fall back to first non-empty line, minus the tool name echo
|
|
67
|
+
lowered = line.lower()
|
|
68
|
+
if tool.lower() not in lowered:
|
|
69
|
+
return line[:40]
|
|
70
|
+
return ""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def check_tool(tool: str) -> dict:
|
|
74
|
+
"""Probe one external tool. Never raises."""
|
|
75
|
+
path = shutil.which(tool)
|
|
76
|
+
if path is None:
|
|
77
|
+
return {"status": "missing", "version": "", "hint": ""}
|
|
78
|
+
|
|
79
|
+
args = _VERSION_ARGS.get(tool, ["--version"])
|
|
80
|
+
try:
|
|
81
|
+
proc = subprocess.run( # noqa: S603 — fixed argv, no shell
|
|
82
|
+
[path, *args],
|
|
83
|
+
capture_output=True,
|
|
84
|
+
text=True,
|
|
85
|
+
timeout=_VERSION_TIMEOUT_S,
|
|
86
|
+
)
|
|
87
|
+
except subprocess.TimeoutExpired:
|
|
88
|
+
hint = _BROKEN_HINTS.get(tool, f"{tool} did not answer within {_VERSION_TIMEOUT_S}s")
|
|
89
|
+
return {"status": "broken", "version": "", "hint": hint}
|
|
90
|
+
except OSError as exc:
|
|
91
|
+
return {
|
|
92
|
+
"status": "broken",
|
|
93
|
+
"version": "",
|
|
94
|
+
"hint": _BROKEN_HINTS.get(tool, f"cannot execute {path}: {exc}"),
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if proc.returncode != 0:
|
|
98
|
+
hint = _BROKEN_HINTS.get(
|
|
99
|
+
tool,
|
|
100
|
+
f"{tool} exits {proc.returncode} on {args[0]} — reinstall or check its runtime deps",
|
|
101
|
+
)
|
|
102
|
+
return {"status": "broken", "version": "", "hint": hint}
|
|
103
|
+
|
|
104
|
+
blob = (proc.stdout or "") + (proc.stderr or "")
|
|
105
|
+
return {
|
|
106
|
+
"status": "ok",
|
|
107
|
+
"version": _extract_version(blob, tool),
|
|
108
|
+
"hint": "",
|
|
109
|
+
}
|