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,574 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Patch applier for Patchi.
|
|
3
|
+
|
|
4
|
+
Applies a patch to disk:
|
|
5
|
+
1. Take snapshot of all files to be changed (via core.snapshot)
|
|
6
|
+
2. Write each FileChange.proposed to disk
|
|
7
|
+
3. Run relevant tests (scoped to changed files)
|
|
8
|
+
4. If tests fail → automatic rollback via snapshot
|
|
9
|
+
5. Update patch state and save to memory
|
|
10
|
+
|
|
11
|
+
Fix agents always run sequentially (one at a time — spec says non-negotiable).
|
|
12
|
+
The applier is synchronous and blocking. The coordinator calls it in a queue loop.
|
|
13
|
+
|
|
14
|
+
Test runner strategy (uses subprocess; dedicated test agents run full coverage):
|
|
15
|
+
Python → pytest [changed file path] --tb=short -q
|
|
16
|
+
Node/TS → npm test / jest [changed file pattern] --passWithNoTests
|
|
17
|
+
No tests → skip test phase, warn user
|
|
18
|
+
|
|
19
|
+
Auto-rollback fires when:
|
|
20
|
+
- Any test command exits with a non-zero code
|
|
21
|
+
- Disk write fails midway (partial write safety)
|
|
22
|
+
- Timeout exceeded (60s default)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import logging
|
|
29
|
+
import subprocess
|
|
30
|
+
import time
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
from datetime import UTC
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
from patchi.core import memory as mem
|
|
36
|
+
from patchi.core import snapshot as snap
|
|
37
|
+
from patchi.core.atomic import atomic_write_text
|
|
38
|
+
from patchi.core.fix.patch import Patch, PatchState
|
|
39
|
+
|
|
40
|
+
_log = logging.getLogger("patchi.fix.applier")
|
|
41
|
+
|
|
42
|
+
# ── Apply result ───────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class ApplyResult:
|
|
47
|
+
patch_id: str
|
|
48
|
+
success: bool
|
|
49
|
+
rolled_back: bool = False
|
|
50
|
+
snapshot_id: str = ""
|
|
51
|
+
test_passed: bool | None = None # None = no tests found
|
|
52
|
+
test_output: str = ""
|
|
53
|
+
lint_passed: bool | None = None # None = no linter found
|
|
54
|
+
lint_output: str = ""
|
|
55
|
+
error: str = ""
|
|
56
|
+
duration_ms: int = 0
|
|
57
|
+
verify_status: str = "unverified" # M-16: passed/failed/unverified
|
|
58
|
+
verify_remaining: int = 0
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> dict:
|
|
61
|
+
return {
|
|
62
|
+
"patch_id": self.patch_id,
|
|
63
|
+
"success": self.success,
|
|
64
|
+
"rolled_back": self.rolled_back,
|
|
65
|
+
"snapshot_id": self.snapshot_id,
|
|
66
|
+
"test_passed": self.test_passed,
|
|
67
|
+
"test_output": self.test_output[:2000], # cap output stored
|
|
68
|
+
"error": self.error,
|
|
69
|
+
"duration_ms": self.duration_ms,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ── Applier ────────────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class PatchApplier:
|
|
77
|
+
"""
|
|
78
|
+
Applies a patch to disk with snapshot + auto-rollback.
|
|
79
|
+
|
|
80
|
+
Usage:
|
|
81
|
+
applier = PatchApplier(project_root)
|
|
82
|
+
result = applier.apply(patch)
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
TEST_TIMEOUT = 60 # seconds
|
|
86
|
+
|
|
87
|
+
def __init__(self, root: Path):
|
|
88
|
+
self.root = root
|
|
89
|
+
|
|
90
|
+
def apply(self, patch: Patch) -> ApplyResult:
|
|
91
|
+
"""Apply a patch. Returns ApplyResult. Never raises."""
|
|
92
|
+
t0 = time.monotonic()
|
|
93
|
+
|
|
94
|
+
# ── Policy gate check (WIRE-03) ────────────────────────────────────────
|
|
95
|
+
try:
|
|
96
|
+
from patchi.core.constants import RiskLevel
|
|
97
|
+
from patchi.core.security.governance import patchi_action_log, patchi_policy_gate
|
|
98
|
+
|
|
99
|
+
risk_sev = RiskLevel.from_score(patch.risk_score).value
|
|
100
|
+
for change in patch.changes:
|
|
101
|
+
allowed, reason = patchi_policy_gate(
|
|
102
|
+
self.root, "patch_apply", change.path, risk_sev
|
|
103
|
+
)
|
|
104
|
+
if not allowed:
|
|
105
|
+
patchi_action_log(
|
|
106
|
+
self.root, "patch_blocked", change.path, detail=reason, status="blocked"
|
|
107
|
+
)
|
|
108
|
+
return ApplyResult(
|
|
109
|
+
patch_id=patch.id,
|
|
110
|
+
success=False,
|
|
111
|
+
error=f"Policy gate blocked: {reason}",
|
|
112
|
+
duration_ms=_ms(t0),
|
|
113
|
+
)
|
|
114
|
+
except ImportError:
|
|
115
|
+
pass # governance module unavailable — proceed
|
|
116
|
+
|
|
117
|
+
# ── 1. Snapshot ────────────────────────────────────────────────────────
|
|
118
|
+
try:
|
|
119
|
+
snapshot_id = snap.create(patch.affected_paths, patch.id, self.root)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
return ApplyResult(
|
|
122
|
+
patch_id=patch.id,
|
|
123
|
+
success=False,
|
|
124
|
+
error=f"Snapshot failed: {e}",
|
|
125
|
+
duration_ms=_ms(t0),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# ── 2. Write files ─────────────────────────────────────────────────────
|
|
129
|
+
written: list[str] = []
|
|
130
|
+
try:
|
|
131
|
+
for change in patch.changes:
|
|
132
|
+
# Policy gate check before writing (WIRE-03)
|
|
133
|
+
try:
|
|
134
|
+
from patchi.core.security.governance import (
|
|
135
|
+
patchi_action_log,
|
|
136
|
+
patchi_policy_gate,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
severity_str = (
|
|
140
|
+
"high"
|
|
141
|
+
if patch.risk_score >= 61
|
|
142
|
+
else "medium"
|
|
143
|
+
if patch.risk_score >= 31
|
|
144
|
+
else "low"
|
|
145
|
+
)
|
|
146
|
+
allowed, reason = patchi_policy_gate(
|
|
147
|
+
self.root, "patch_apply", change.path, severity_str
|
|
148
|
+
)
|
|
149
|
+
if not allowed:
|
|
150
|
+
snap.restore(snapshot_id, self.root)
|
|
151
|
+
return ApplyResult(
|
|
152
|
+
patch_id=patch.id,
|
|
153
|
+
success=False,
|
|
154
|
+
rolled_back=True,
|
|
155
|
+
snapshot_id=snapshot_id,
|
|
156
|
+
error=f"Policy gate blocked: {reason}",
|
|
157
|
+
duration_ms=_ms(t0),
|
|
158
|
+
)
|
|
159
|
+
patchi_action_log(
|
|
160
|
+
self.root,
|
|
161
|
+
"patch_apply",
|
|
162
|
+
change.path,
|
|
163
|
+
agent="PatchApplier",
|
|
164
|
+
detail=f"patch_id={patch.id}",
|
|
165
|
+
)
|
|
166
|
+
except ImportError:
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
# Secrets gate check (NEW-02) — block patches introducing secrets
|
|
170
|
+
try:
|
|
171
|
+
from patchi.core.security.secrets_guard import gate_check_proposed_code
|
|
172
|
+
|
|
173
|
+
if change.proposed:
|
|
174
|
+
safe, secret_findings = gate_check_proposed_code(
|
|
175
|
+
change.proposed, change.path
|
|
176
|
+
)
|
|
177
|
+
if not safe:
|
|
178
|
+
snap.restore(snapshot_id, self.root)
|
|
179
|
+
return ApplyResult(
|
|
180
|
+
patch_id=patch.id,
|
|
181
|
+
success=False,
|
|
182
|
+
rolled_back=True,
|
|
183
|
+
snapshot_id=snapshot_id,
|
|
184
|
+
error=f"Secrets gate blocked: proposed code in {change.path} introduces secrets",
|
|
185
|
+
duration_ms=_ms(t0),
|
|
186
|
+
)
|
|
187
|
+
except ImportError:
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
abs_path = self.root / change.path
|
|
191
|
+
if change.proposed == "":
|
|
192
|
+
# Empty proposed = deletion (DeadCodeRemover uses this)
|
|
193
|
+
if abs_path.exists():
|
|
194
|
+
abs_path.unlink()
|
|
195
|
+
else:
|
|
196
|
+
atomic_write_text(abs_path, change.proposed)
|
|
197
|
+
written.append(change.path)
|
|
198
|
+
except Exception as e:
|
|
199
|
+
# Partial write — rollback immediately
|
|
200
|
+
rollback_ok = True
|
|
201
|
+
try:
|
|
202
|
+
snap.restore(snapshot_id, self.root)
|
|
203
|
+
except Exception as rb_err:
|
|
204
|
+
rollback_ok = False
|
|
205
|
+
_log.warning(
|
|
206
|
+
"Rollback after failed write also failed (snapshot=%s): %s",
|
|
207
|
+
snapshot_id,
|
|
208
|
+
rb_err,
|
|
209
|
+
)
|
|
210
|
+
return ApplyResult(
|
|
211
|
+
patch_id=patch.id,
|
|
212
|
+
success=False,
|
|
213
|
+
rolled_back=rollback_ok,
|
|
214
|
+
snapshot_id=snapshot_id,
|
|
215
|
+
error=(
|
|
216
|
+
f"Write failed after {len(written)} file(s): {e}"
|
|
217
|
+
+ (
|
|
218
|
+
""
|
|
219
|
+
if rollback_ok
|
|
220
|
+
else " (rollback ALSO failed — files may be in a partial state!)"
|
|
221
|
+
)
|
|
222
|
+
),
|
|
223
|
+
duration_ms=_ms(t0),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# ── 3. Run tests ───────────────────────────────────────────────────────
|
|
227
|
+
test_result = self._run_tests(patch.affected_paths)
|
|
228
|
+
|
|
229
|
+
# ── 3b. Run linter ────────────────────────────────────────────────────
|
|
230
|
+
lint_result = self._run_linter(patch.affected_paths)
|
|
231
|
+
|
|
232
|
+
# ── 4. Rollback if tests or linter failed ─────────────────────────────
|
|
233
|
+
failed = False
|
|
234
|
+
failure_reason = ""
|
|
235
|
+
if test_result["passed"] is False:
|
|
236
|
+
failed = True
|
|
237
|
+
failure_reason = "Tests failed"
|
|
238
|
+
elif lint_result["passed"] is False:
|
|
239
|
+
failed = True
|
|
240
|
+
failure_reason = "Linter errors introduced"
|
|
241
|
+
|
|
242
|
+
if failed:
|
|
243
|
+
try:
|
|
244
|
+
snap.restore(snapshot_id, self.root)
|
|
245
|
+
rolled_back = True
|
|
246
|
+
except Exception as rb_err:
|
|
247
|
+
rolled_back = False
|
|
248
|
+
test_result["rollback_error"] = str(rb_err)
|
|
249
|
+
|
|
250
|
+
# Update patch state
|
|
251
|
+
self._update_patch_state(patch.id, PatchState.FAILED, test_result, snapshot_id)
|
|
252
|
+
|
|
253
|
+
return ApplyResult(
|
|
254
|
+
patch_id=patch.id,
|
|
255
|
+
success=False,
|
|
256
|
+
rolled_back=rolled_back,
|
|
257
|
+
snapshot_id=snapshot_id,
|
|
258
|
+
test_passed=test_result.get("passed"),
|
|
259
|
+
lint_passed=lint_result.get("passed"),
|
|
260
|
+
test_output=test_result.get("output", ""),
|
|
261
|
+
lint_output=lint_result.get("output", ""),
|
|
262
|
+
error=f"{failure_reason}. {'Rolled back.' if rolled_back else 'Rollback also failed!'}",
|
|
263
|
+
duration_ms=_ms(t0),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# ── 5. Verify fix (M-16: detect-fix-verify loop) ──────────────────────
|
|
267
|
+
verify_result = self._verify_fix(patch)
|
|
268
|
+
|
|
269
|
+
# ── 6. Success ─────────────────────────────────────────────────────────
|
|
270
|
+
self._update_patch_state(patch.id, PatchState.APPLIED, test_result, snapshot_id)
|
|
271
|
+
|
|
272
|
+
# Audit log for successful apply (WIRE-04)
|
|
273
|
+
try:
|
|
274
|
+
from patchi.core.security.governance import patchi_action_log
|
|
275
|
+
|
|
276
|
+
targets = ", ".join(c.path for c in patch.changes[:5])
|
|
277
|
+
patchi_action_log(self.root, "patch_applied", targets, detail=patch.id, status="ok")
|
|
278
|
+
except Exception as e:
|
|
279
|
+
_log.warning("Failed to write audit log entry for patch %s: %s", patch.id, e)
|
|
280
|
+
|
|
281
|
+
# Mark brain stale (files changed)
|
|
282
|
+
try:
|
|
283
|
+
mem.mark_brain_stale(patch.affected_paths, self.root)
|
|
284
|
+
except Exception as e:
|
|
285
|
+
_log.warning("Failed to mark brain stale for patch %s: %s", patch.id, e)
|
|
286
|
+
|
|
287
|
+
result = ApplyResult(
|
|
288
|
+
patch_id=patch.id,
|
|
289
|
+
success=True,
|
|
290
|
+
rolled_back=False,
|
|
291
|
+
snapshot_id=snapshot_id,
|
|
292
|
+
test_passed=test_result.get("passed"),
|
|
293
|
+
test_output=test_result.get("output", ""),
|
|
294
|
+
duration_ms=_ms(t0),
|
|
295
|
+
)
|
|
296
|
+
result.verify_status = verify_result.get("status", "unverified")
|
|
297
|
+
result.verify_remaining = verify_result.get("remaining", 0)
|
|
298
|
+
return result
|
|
299
|
+
|
|
300
|
+
def rollback(self, patch_id: str, snapshot_id: str) -> ApplyResult:
|
|
301
|
+
"""
|
|
302
|
+
Manually rollback a previously applied patch.
|
|
303
|
+
Used by `p undo [id]` and `p rollback [id]`.
|
|
304
|
+
"""
|
|
305
|
+
t0 = time.monotonic()
|
|
306
|
+
try:
|
|
307
|
+
restored = snap.restore(snapshot_id, self.root)
|
|
308
|
+
self._update_patch_state(patch_id, PatchState.ROLLED_BACK, {}, snapshot_id)
|
|
309
|
+
mem.mark_brain_stale(restored, self.root)
|
|
310
|
+
return ApplyResult(
|
|
311
|
+
patch_id=patch_id,
|
|
312
|
+
success=True,
|
|
313
|
+
rolled_back=True,
|
|
314
|
+
snapshot_id=snapshot_id,
|
|
315
|
+
duration_ms=_ms(t0),
|
|
316
|
+
)
|
|
317
|
+
except Exception as e:
|
|
318
|
+
return ApplyResult(
|
|
319
|
+
patch_id=patch_id,
|
|
320
|
+
success=False,
|
|
321
|
+
error=f"Rollback failed: {e}",
|
|
322
|
+
duration_ms=_ms(t0),
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# ── Verify fix (MISS-15: detect-fix-verify loop) ───────────────────────────
|
|
326
|
+
|
|
327
|
+
def _verify_fix(self, patch: Patch) -> dict:
|
|
328
|
+
"""Re-scan changed files to confirm the fix resolved the finding.
|
|
329
|
+
|
|
330
|
+
Uses the original scanner agent (if known) for precise verification.
|
|
331
|
+
Falls back to FileScanner complexity check for generic patches.
|
|
332
|
+
"""
|
|
333
|
+
try:
|
|
334
|
+
from patchi.core.agents.base import get_agent
|
|
335
|
+
from patchi.core.brain.scanner import FileScanner
|
|
336
|
+
|
|
337
|
+
# If we know which scanner agent found the issue, re-run it
|
|
338
|
+
scanner_agent = patch.scanner_agent or patch.source_finding.get("agent", "")
|
|
339
|
+
finding_type = patch.finding_id or patch.source_finding.get("type", "")
|
|
340
|
+
finding_file = patch.source_finding.get("file", "")
|
|
341
|
+
finding_line = patch.source_finding.get("line", 0)
|
|
342
|
+
|
|
343
|
+
if scanner_agent and finding_type:
|
|
344
|
+
agent_cls = get_agent(scanner_agent)
|
|
345
|
+
if agent_cls:
|
|
346
|
+
from patchi.core.agents.base import AgentInput
|
|
347
|
+
|
|
348
|
+
inp = AgentInput(
|
|
349
|
+
root=self.root, scope=patch.affected_paths, brain={}, config={}
|
|
350
|
+
)
|
|
351
|
+
result = agent_cls().run(inp)
|
|
352
|
+
# Check if the same finding type still exists on the same file
|
|
353
|
+
still_present = any(
|
|
354
|
+
f.type == finding_type
|
|
355
|
+
and f.file == finding_file
|
|
356
|
+
and (finding_line == 0 or abs(f.line - finding_line) <= 3)
|
|
357
|
+
for f in result.findings
|
|
358
|
+
)
|
|
359
|
+
if not still_present:
|
|
360
|
+
return {"status": "passed", "remaining": 0}
|
|
361
|
+
patch.verify_retries = max(patch.verify_retries - 1, 0)
|
|
362
|
+
return {
|
|
363
|
+
"status": "failed",
|
|
364
|
+
"remaining": 1,
|
|
365
|
+
"can_retry": patch.verify_retries > 0,
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
# Fallback: generic complexity check
|
|
369
|
+
scanner = FileScanner(self.root)
|
|
370
|
+
remaining = 0
|
|
371
|
+
for change in patch.changes:
|
|
372
|
+
abs_path = self.root / change.path
|
|
373
|
+
if not abs_path.exists():
|
|
374
|
+
continue
|
|
375
|
+
info = scanner.scan_file(abs_path)
|
|
376
|
+
if info and info.complexity and info.complexity > 10:
|
|
377
|
+
remaining += 1
|
|
378
|
+
if remaining == 0:
|
|
379
|
+
return {"status": "passed", "remaining": 0}
|
|
380
|
+
return {"status": "failed", "remaining": remaining}
|
|
381
|
+
except Exception as e:
|
|
382
|
+
_log.warning("Fix verification failed for patch %s: %s", patch.id, e)
|
|
383
|
+
return {"status": "unverified", "remaining": -1}
|
|
384
|
+
|
|
385
|
+
# ── Test runner ────────────────────────────────────────────────────────────
|
|
386
|
+
|
|
387
|
+
def _run_tests(self, changed_paths: list[str]) -> dict:
|
|
388
|
+
"""
|
|
389
|
+
Run relevant tests for the changed files.
|
|
390
|
+
Returns {passed: bool|None, output: str, runner: str}
|
|
391
|
+
"""
|
|
392
|
+
runner, cmd = self._detect_test_runner(changed_paths)
|
|
393
|
+
if runner is None:
|
|
394
|
+
return {"passed": None, "output": "No test runner detected.", "runner": None}
|
|
395
|
+
|
|
396
|
+
try:
|
|
397
|
+
proc = subprocess.run(
|
|
398
|
+
cmd,
|
|
399
|
+
capture_output=True,
|
|
400
|
+
text=True,
|
|
401
|
+
cwd=str(self.root),
|
|
402
|
+
timeout=self.TEST_TIMEOUT,
|
|
403
|
+
)
|
|
404
|
+
passed = proc.returncode == 0
|
|
405
|
+
output = (proc.stdout + proc.stderr)[:3000]
|
|
406
|
+
return {"passed": passed, "output": output, "runner": runner}
|
|
407
|
+
except subprocess.TimeoutExpired:
|
|
408
|
+
return {"passed": False, "output": "Tests timed out (60s).", "runner": runner}
|
|
409
|
+
except Exception as e:
|
|
410
|
+
return {"passed": None, "output": str(e), "runner": runner}
|
|
411
|
+
|
|
412
|
+
def _detect_test_runner(self, changed_paths: list[str]) -> tuple[str | None, list[str]]:
|
|
413
|
+
"""
|
|
414
|
+
Detect the test runner for the changed files.
|
|
415
|
+
Returns (runner_name, command_list) or (None, []) if none found.
|
|
416
|
+
"""
|
|
417
|
+
import shutil
|
|
418
|
+
|
|
419
|
+
# Python: pytest — only if matching test files exist
|
|
420
|
+
py_files = [p for p in changed_paths if p.endswith(".py")]
|
|
421
|
+
if py_files and shutil.which("pytest"):
|
|
422
|
+
test_files = self._find_python_tests(py_files)
|
|
423
|
+
if test_files:
|
|
424
|
+
return "pytest", ["pytest", "--tb=short", "-q"] + test_files
|
|
425
|
+
|
|
426
|
+
# Node/TypeScript: jest or npm test
|
|
427
|
+
js_files = [p for p in changed_paths if p.endswith((".js", ".ts", ".jsx", ".tsx"))]
|
|
428
|
+
if js_files:
|
|
429
|
+
if shutil.which("jest") and (self.root / "package.json").exists():
|
|
430
|
+
try:
|
|
431
|
+
pkg = json.loads((self.root / "package.json").read_text())
|
|
432
|
+
if "jest" in str(pkg.get("devDependencies", {})):
|
|
433
|
+
pattern = "|".join(Path(p).stem for p in js_files[:3])
|
|
434
|
+
return "jest", [
|
|
435
|
+
"jest",
|
|
436
|
+
"--testPathPattern",
|
|
437
|
+
pattern or ".",
|
|
438
|
+
"--passWithNoTests",
|
|
439
|
+
]
|
|
440
|
+
except Exception as e:
|
|
441
|
+
_log.debug("Could not parse package.json for jest detection: %s", e)
|
|
442
|
+
if (self.root / "package.json").exists() and shutil.which("npm"):
|
|
443
|
+
try:
|
|
444
|
+
pkg = json.loads((self.root / "package.json").read_text())
|
|
445
|
+
if "test" in pkg.get("scripts", {}):
|
|
446
|
+
return "npm", ["npm", "test", "--", "--passWithNoTests"]
|
|
447
|
+
except Exception as e:
|
|
448
|
+
_log.debug("Could not parse package.json for npm test detection: %s", e)
|
|
449
|
+
|
|
450
|
+
return None, []
|
|
451
|
+
|
|
452
|
+
def _find_python_tests(self, source_paths: list[str]) -> list[str]:
|
|
453
|
+
"""Find pytest test files corresponding to source files."""
|
|
454
|
+
test_files: list[str] = []
|
|
455
|
+
for src in source_paths:
|
|
456
|
+
from pathlib import PurePosixPath
|
|
457
|
+
|
|
458
|
+
stem = PurePosixPath(src).stem
|
|
459
|
+
candidates = [
|
|
460
|
+
f"tests/test_{stem}.py",
|
|
461
|
+
f"tests/{stem}_test.py",
|
|
462
|
+
f"test_{stem}.py",
|
|
463
|
+
f"{PurePosixPath(src).parent}/test_{stem}.py",
|
|
464
|
+
f"{PurePosixPath(src).parent}/tests/test_{stem}.py",
|
|
465
|
+
]
|
|
466
|
+
for c in candidates:
|
|
467
|
+
if (self.root / c).exists():
|
|
468
|
+
test_files.append(c)
|
|
469
|
+
return test_files
|
|
470
|
+
|
|
471
|
+
LINT_TIMEOUT = 30
|
|
472
|
+
|
|
473
|
+
def _run_linter(self, changed_paths: list[str]) -> dict:
|
|
474
|
+
"""
|
|
475
|
+
Run linter on changed files to catch introduced errors.
|
|
476
|
+
Returns {passed: bool|None, output: str, linter: str}
|
|
477
|
+
"""
|
|
478
|
+
import shutil
|
|
479
|
+
|
|
480
|
+
py_files = [p for p in changed_paths if p.endswith(".py")]
|
|
481
|
+
if py_files and shutil.which("ruff"):
|
|
482
|
+
try:
|
|
483
|
+
proc = subprocess.run(
|
|
484
|
+
["ruff", "check", "--select=E,F,W"] + py_files,
|
|
485
|
+
capture_output=True,
|
|
486
|
+
text=True,
|
|
487
|
+
cwd=str(self.root),
|
|
488
|
+
timeout=self.LINT_TIMEOUT,
|
|
489
|
+
)
|
|
490
|
+
passed = proc.returncode == 0
|
|
491
|
+
output = (proc.stdout + proc.stderr)[:2000]
|
|
492
|
+
return {"passed": passed, "output": output, "linter": "ruff"}
|
|
493
|
+
except subprocess.TimeoutExpired:
|
|
494
|
+
return {"passed": None, "output": "Linter timed out.", "linter": "ruff"}
|
|
495
|
+
except Exception as e:
|
|
496
|
+
return {"passed": None, "output": str(e), "linter": "ruff"}
|
|
497
|
+
|
|
498
|
+
js_files = [p for p in changed_paths if p.endswith((".js", ".ts", ".jsx", ".tsx"))]
|
|
499
|
+
if js_files and shutil.which("npx"):
|
|
500
|
+
try:
|
|
501
|
+
proc = subprocess.run(
|
|
502
|
+
["npx", "--yes", "eslint"] + js_files[:5],
|
|
503
|
+
capture_output=True,
|
|
504
|
+
text=True,
|
|
505
|
+
cwd=str(self.root),
|
|
506
|
+
timeout=self.LINT_TIMEOUT,
|
|
507
|
+
)
|
|
508
|
+
passed = proc.returncode == 0
|
|
509
|
+
output = (proc.stdout + proc.stderr)[:2000]
|
|
510
|
+
return {"passed": passed, "output": output, "linter": "eslint"}
|
|
511
|
+
except subprocess.TimeoutExpired:
|
|
512
|
+
return {"passed": None, "output": "Linter timed out.", "linter": "eslint"}
|
|
513
|
+
except Exception as e:
|
|
514
|
+
return {"passed": None, "output": str(e), "linter": "eslint"}
|
|
515
|
+
|
|
516
|
+
return {"passed": None, "output": "No linter available.", "linter": None}
|
|
517
|
+
|
|
518
|
+
# ── State updates ──────────────────────────────────────────────────────────
|
|
519
|
+
|
|
520
|
+
def _update_patch_state(
|
|
521
|
+
self,
|
|
522
|
+
patch_id: str,
|
|
523
|
+
state: PatchState,
|
|
524
|
+
test_result: dict,
|
|
525
|
+
snapshot_id: str,
|
|
526
|
+
) -> None:
|
|
527
|
+
"""Update patch state in memory."""
|
|
528
|
+
try:
|
|
529
|
+
from datetime import datetime
|
|
530
|
+
|
|
531
|
+
from patchi.core import memory as mem
|
|
532
|
+
|
|
533
|
+
patches = mem.list_patches(self.root)
|
|
534
|
+
for patch in patches:
|
|
535
|
+
if patch.get("id") == patch_id:
|
|
536
|
+
patch["state"] = state.value
|
|
537
|
+
patch["snapshot_id"] = snapshot_id
|
|
538
|
+
patch["test_result"] = test_result
|
|
539
|
+
now = datetime.now(UTC).isoformat()
|
|
540
|
+
if state == PatchState.APPLIED:
|
|
541
|
+
patch["applied_at"] = now
|
|
542
|
+
elif state == PatchState.FAILED:
|
|
543
|
+
patch["rolled_back_at"] = now
|
|
544
|
+
elif state == PatchState.ROLLED_BACK:
|
|
545
|
+
patch["rolled_back_at"] = now
|
|
546
|
+
break
|
|
547
|
+
|
|
548
|
+
# Save back — need to write to memory file
|
|
549
|
+
import json
|
|
550
|
+
|
|
551
|
+
from patchi.core.constants import MEMORY_FILES, MemoryCategory
|
|
552
|
+
|
|
553
|
+
mem_path = self.root / MEMORY_FILES[MemoryCategory.PATCHES]
|
|
554
|
+
mem_path.parent.mkdir(parents=True, exist_ok=True)
|
|
555
|
+
tmp_path = mem_path.with_suffix(".tmp")
|
|
556
|
+
with tmp_path.open("w", encoding="utf-8") as f:
|
|
557
|
+
json.dump(patches, f, indent=2)
|
|
558
|
+
# Atomic replace
|
|
559
|
+
import sys
|
|
560
|
+
|
|
561
|
+
if sys.platform == "win32":
|
|
562
|
+
# Windows doesn't support atomic rename over existing file
|
|
563
|
+
if mem_path.exists():
|
|
564
|
+
mem_path.unlink()
|
|
565
|
+
tmp_path.rename(mem_path)
|
|
566
|
+
except Exception as e:
|
|
567
|
+
_log.warning("Failed to persist patch state for %s (state=%s): %s", patch_id, state, e)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
# ── Helpers ────────────────────────────────────────────────────────────────────
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _ms(t0: float) -> int:
|
|
574
|
+
return int((time.monotonic() - t0) * 1000)
|