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,648 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`p fix` — Propose and apply fixes for all detected issues.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
p fix — fix entire project
|
|
6
|
+
p fix src/auth — fix specific area
|
|
7
|
+
p fix --dry-run — preview what would be fixed, touch nothing
|
|
8
|
+
p fix --safe-all — apply every non-blocked patch, not just AUTO
|
|
9
|
+
|
|
10
|
+
Flow:
|
|
11
|
+
1. Check contract_locked (hard block if not confirmed)
|
|
12
|
+
2. Load findings from last scan (from memory)
|
|
13
|
+
3. Run relevant fix agents (sequentially — spec mandates non-parallel)
|
|
14
|
+
4. Collect proposed patches
|
|
15
|
+
5. For each patch: run through risk gate
|
|
16
|
+
- BLOCK → skip, explain why
|
|
17
|
+
- AUTO → apply immediately (AUTO/AUTOPILOT mode, low risk)
|
|
18
|
+
- REQUIRE_REVIEW → queue for `p review`
|
|
19
|
+
6. Show summary: applied / queued / blocked
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from rich.panel import Panel
|
|
27
|
+
from rich.table import Table
|
|
28
|
+
from rich.text import Text
|
|
29
|
+
|
|
30
|
+
from patchi.cli.console import con
|
|
31
|
+
from patchi.cli.display.live_progress import LiveProgress
|
|
32
|
+
from patchi.core import memory as mem
|
|
33
|
+
from patchi.core.config import require_project_root
|
|
34
|
+
from patchi.core.fix.applier import PatchApplier
|
|
35
|
+
from patchi.core.fix.patch import Patch, PatchState
|
|
36
|
+
from patchi.core.fix.risk_gate import RiskGate
|
|
37
|
+
from patchi.core.fix.verify_loop import run_verify_loop, should_flag_for_review
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def run(
|
|
41
|
+
area: str | None = None,
|
|
42
|
+
dry_run: bool = False,
|
|
43
|
+
preview: bool = False,
|
|
44
|
+
safe_all: bool = False,
|
|
45
|
+
root: Path | None = None,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Entry point for `p fix [area]`."""
|
|
48
|
+
try:
|
|
49
|
+
r = root or require_project_root()
|
|
50
|
+
except RuntimeError as e:
|
|
51
|
+
con.print(f"[red]{e}[/red]")
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
# ── 1. Contract lock check ─────────────────────────────────────────────────
|
|
55
|
+
brain = mem.get_brain(r)
|
|
56
|
+
if not brain.get("contract_locked"):
|
|
57
|
+
con.print()
|
|
58
|
+
con.print(
|
|
59
|
+
Panel(
|
|
60
|
+
"[bold yellow]App contract not yet formed.[/bold yellow]\n\n"
|
|
61
|
+
"[dim]Run [bold]p scan[/bold] first to understand your project,\n"
|
|
62
|
+
"then [bold]p fix[/bold] will work automatically.[/dim]\n",
|
|
63
|
+
border_style="yellow",
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
con.print()
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
# ── 2. Load findings from last scan ───────────────────────────────────────
|
|
70
|
+
scan_results = mem.get_scan_results(r)
|
|
71
|
+
all_findings = _collect_findings(scan_results, area)
|
|
72
|
+
|
|
73
|
+
# Annotate findings with framework info for multi-framework awareness
|
|
74
|
+
_annotate_findings_with_framework(all_findings, brain)
|
|
75
|
+
|
|
76
|
+
if not all_findings:
|
|
77
|
+
con.print()
|
|
78
|
+
con.print("[dim]No findings from last scan. Run [bold]p scan[/bold] first.[/dim]")
|
|
79
|
+
con.print()
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
fixable = [f for f in all_findings if f.get("fix_agent")]
|
|
83
|
+
if not fixable:
|
|
84
|
+
con.print()
|
|
85
|
+
con.print("[#4ADE80]✓[/#4ADE80] [dim]No fixable issues found.[/dim]")
|
|
86
|
+
con.print()
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
con.print()
|
|
90
|
+
area_label = f" [dim]→ {area}[/dim]" if area else ""
|
|
91
|
+
con.print(
|
|
92
|
+
f"[bold #C8621A]Fixing{area_label}[/bold #C8621A] "
|
|
93
|
+
f"[dim]{len(fixable)} fixable finding(s)[/dim]"
|
|
94
|
+
)
|
|
95
|
+
con.print()
|
|
96
|
+
|
|
97
|
+
if dry_run or preview:
|
|
98
|
+
_show_dry_run(fixable, r, preview=preview)
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
# ── 3. Run fix agents sequentially ────────────────────────────────────────
|
|
102
|
+
from patchi.cli.ux import format_step, status_icon, status_style
|
|
103
|
+
|
|
104
|
+
fix_phases = [
|
|
105
|
+
"Finding analysis",
|
|
106
|
+
"Fix agent execution",
|
|
107
|
+
"Risk gate evaluation",
|
|
108
|
+
"Patch application",
|
|
109
|
+
]
|
|
110
|
+
total_steps = len(fix_phases)
|
|
111
|
+
current_step = 0
|
|
112
|
+
|
|
113
|
+
def show_fix_step(step_name: str, status: str = "running"):
|
|
114
|
+
nonlocal current_step
|
|
115
|
+
current_step += 1
|
|
116
|
+
icon = status_icon(status)
|
|
117
|
+
style = status_style(status)
|
|
118
|
+
con.print(format_step(current_step, total_steps, f"[{style}]{icon} {step_name}[/{style}]"))
|
|
119
|
+
|
|
120
|
+
show_fix_step("Finding analysis", "done")
|
|
121
|
+
show_fix_step("Fix agent execution")
|
|
122
|
+
|
|
123
|
+
patches = _run_fix_agents(fixable, r)
|
|
124
|
+
|
|
125
|
+
if not patches:
|
|
126
|
+
show_fix_step("Fix agent execution", "failed")
|
|
127
|
+
con.print("[dim]Fix agents produced no patches.[/dim]")
|
|
128
|
+
con.print(
|
|
129
|
+
"[dim]This may be because no AI key is configured. "
|
|
130
|
+
"Run [bold]p key add[/bold] to add one.[/dim]"
|
|
131
|
+
)
|
|
132
|
+
con.print()
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
show_fix_step("Fix agent execution", "done")
|
|
136
|
+
show_fix_step("Risk gate evaluation")
|
|
137
|
+
|
|
138
|
+
# ── 4. Risk gate + apply/queue ────────────────────────────────────────────
|
|
139
|
+
gate = RiskGate(r)
|
|
140
|
+
applier = PatchApplier(r)
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
from patchi.core import config as cfg
|
|
144
|
+
|
|
145
|
+
config = cfg.load(r)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
con.print(f"[dim]Config load warning: {e}[/dim]")
|
|
148
|
+
config = {}
|
|
149
|
+
brain = mem.get_brain(r)
|
|
150
|
+
|
|
151
|
+
# Sort patches by blast radius (smallest first) to minimize risk
|
|
152
|
+
patches.sort(key=lambda p: p.blast_radius or 0)
|
|
153
|
+
|
|
154
|
+
applied: list[Patch] = []
|
|
155
|
+
queued: list[Patch] = []
|
|
156
|
+
blocked: list[tuple[Patch, str]] = []
|
|
157
|
+
|
|
158
|
+
for patch in patches:
|
|
159
|
+
# Test-weakening guard: a fix that only changes test files must go to
|
|
160
|
+
# human review, never auto-apply — flag it before the gate so even
|
|
161
|
+
# AUTOPILOT mode queues it.
|
|
162
|
+
if should_flag_for_review(patch):
|
|
163
|
+
patch.requires_review = True
|
|
164
|
+
|
|
165
|
+
gate_result = gate.evaluate(patch)
|
|
166
|
+
|
|
167
|
+
if gate_result.is_blocked:
|
|
168
|
+
blocked.append((patch, gate_result.reason))
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
# --safe-all: apply everything the gate didn't block, except
|
|
172
|
+
# test-weakening edits (requires_review set above) which still queue.
|
|
173
|
+
if gate_result.is_auto or (
|
|
174
|
+
safe_all and gate_result.needs_review and not patch.requires_review
|
|
175
|
+
):
|
|
176
|
+
if (patch.source_finding or {}).get("type") == "test_failure":
|
|
177
|
+
# fix → verify → retry loop: apply, re-run the failing test,
|
|
178
|
+
# feed the failure back (up to 2 retries), never weaken tests.
|
|
179
|
+
outcome = run_verify_loop(
|
|
180
|
+
patch,
|
|
181
|
+
root=r,
|
|
182
|
+
config=config,
|
|
183
|
+
brain=brain,
|
|
184
|
+
applier=applier,
|
|
185
|
+
log=lambda msg: con.print(f"[dim]{msg}[/dim]"),
|
|
186
|
+
)
|
|
187
|
+
if outcome.review_required:
|
|
188
|
+
patch.state = PatchState.PENDING
|
|
189
|
+
_save_patch(outcome.patch or patch, r)
|
|
190
|
+
queued.append(outcome.patch or patch)
|
|
191
|
+
con.print(
|
|
192
|
+
f"[yellow]⚠[/yellow] Patch {patch.id} only changes test file(s) — "
|
|
193
|
+
"queued for human review (test-weakening edits are never "
|
|
194
|
+
"auto-applied)."
|
|
195
|
+
)
|
|
196
|
+
elif outcome.applied:
|
|
197
|
+
applied.append(outcome.patch or patch)
|
|
198
|
+
if outcome.verified:
|
|
199
|
+
con.print(
|
|
200
|
+
f"[#4ADE80]✓[/#4ADE80] Patch {patch.id} verified — failing test passes."
|
|
201
|
+
)
|
|
202
|
+
else:
|
|
203
|
+
con.print(
|
|
204
|
+
f"[yellow]⚠[/yellow] Patch {patch.id} applied but could not "
|
|
205
|
+
f"be re-verified: {outcome.reason}"
|
|
206
|
+
)
|
|
207
|
+
else:
|
|
208
|
+
con.print(
|
|
209
|
+
f"[yellow]⚠[/yellow] Patch {patch.id} not applied after "
|
|
210
|
+
f"{outcome.retries_used} retr{'y' if outcome.retries_used == 1 else 'ies'} — "
|
|
211
|
+
f"rolled back. {outcome.reason}"
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
# Save patch to memory first
|
|
215
|
+
_save_patch(patch, r)
|
|
216
|
+
result = applier.apply(patch)
|
|
217
|
+
if result.success:
|
|
218
|
+
applied.append(patch)
|
|
219
|
+
else:
|
|
220
|
+
con.print(
|
|
221
|
+
f"[yellow]⚠[/yellow] Patch {patch.id} applied but tests failed — "
|
|
222
|
+
f"rolled back. {result.error}"
|
|
223
|
+
)
|
|
224
|
+
else:
|
|
225
|
+
# Queue for review
|
|
226
|
+
patch.state = PatchState.PENDING
|
|
227
|
+
_save_patch(patch, r)
|
|
228
|
+
queued.append(patch)
|
|
229
|
+
|
|
230
|
+
# ── 5. Record fix patterns for learning ───────────────────────────────────
|
|
231
|
+
try:
|
|
232
|
+
from patchi.core.brain.learning import record_fix_pattern
|
|
233
|
+
|
|
234
|
+
for patch in applied:
|
|
235
|
+
if patch.findings:
|
|
236
|
+
finding_type = patch.findings[0].get("type", "unknown")
|
|
237
|
+
file_pattern = patch.findings[0].get("file", "")
|
|
238
|
+
record_fix_pattern(
|
|
239
|
+
finding_type=finding_type,
|
|
240
|
+
file_pattern=file_pattern,
|
|
241
|
+
fix_strategy=patch.strategy,
|
|
242
|
+
fix_summary=patch.message[:200],
|
|
243
|
+
root=r,
|
|
244
|
+
)
|
|
245
|
+
except Exception as e:
|
|
246
|
+
con.print(f"[dim]Could not record fix pattern: {e}[/dim]")
|
|
247
|
+
|
|
248
|
+
show_fix_step("Risk gate evaluation", "done")
|
|
249
|
+
show_fix_step("Patch application", "done")
|
|
250
|
+
|
|
251
|
+
# ── 6. Summary ────────────────────────────────────────────────────────────
|
|
252
|
+
_show_summary(applied, queued, blocked)
|
|
253
|
+
|
|
254
|
+
# ── 7. Interactive review prompt ──────────────────────────────────────────
|
|
255
|
+
if queued and not dry_run:
|
|
256
|
+
from patchi.cli.ux import confirm
|
|
257
|
+
con.print()
|
|
258
|
+
if confirm("Review queued patches interactively?", default=False):
|
|
259
|
+
from patchi.cli.commands.fix_review_cmd import run as review_run
|
|
260
|
+
review_run(root=r)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# ── Fix agents runner ──────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _run_fix_agents(findings: list[dict], root: Path) -> list[Patch]:
|
|
267
|
+
"""Run relevant fix agents sequentially. Returns all proposed patches."""
|
|
268
|
+
import patchi.core.fix.fix_agents # noqa: F401 — trigger registration
|
|
269
|
+
from patchi.core import config as cfg
|
|
270
|
+
from patchi.core.agents.base import AgentGroup, AgentInput, list_agents
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
config = cfg.load(root)
|
|
274
|
+
except Exception as e:
|
|
275
|
+
con.print(f"[dim]Config load warning: {e}[/dim]")
|
|
276
|
+
config = {}
|
|
277
|
+
|
|
278
|
+
brain = mem.get_brain(root)
|
|
279
|
+
|
|
280
|
+
# Load brain report to include blast radius information
|
|
281
|
+
brain_report = None
|
|
282
|
+
try:
|
|
283
|
+
brain_data = mem.get_brain(root)
|
|
284
|
+
brain_report = type(
|
|
285
|
+
"BrainReport",
|
|
286
|
+
(),
|
|
287
|
+
{
|
|
288
|
+
"blast_radius_map": brain_data.get("blast_radius_map", {}),
|
|
289
|
+
"routes": brain_data.get("routes", []),
|
|
290
|
+
"file_infos": brain_data.get("file_infos", []),
|
|
291
|
+
},
|
|
292
|
+
)()
|
|
293
|
+
except Exception as e:
|
|
294
|
+
con.print(f"[dim]Could not build brain report: {e}[/dim]")
|
|
295
|
+
brain_report = None
|
|
296
|
+
|
|
297
|
+
# Build import graph for fix context
|
|
298
|
+
import_graph = None
|
|
299
|
+
try:
|
|
300
|
+
from patchi.core.brain.import_graph import build_import_graph
|
|
301
|
+
|
|
302
|
+
import_graph = build_import_graph(root)
|
|
303
|
+
# Enrich each finding with import graph context
|
|
304
|
+
for f in findings:
|
|
305
|
+
fpath = f.get("file", "")
|
|
306
|
+
if fpath and import_graph and fpath in import_graph.nodes:
|
|
307
|
+
f["import_context"] = import_graph.format_fix_context(fpath)
|
|
308
|
+
f["transitive_dependents"] = list(import_graph.get_transitive_dependents(fpath))
|
|
309
|
+
except Exception as e:
|
|
310
|
+
con.print(f"[dim]Import graph enrichment error: {e}[/dim]")
|
|
311
|
+
|
|
312
|
+
inp = AgentInput(
|
|
313
|
+
root=root,
|
|
314
|
+
scope=[],
|
|
315
|
+
brain=brain,
|
|
316
|
+
config=config,
|
|
317
|
+
extra={"findings": findings, "brain_report": brain_report}
|
|
318
|
+
if brain_report
|
|
319
|
+
else {"findings": findings},
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
fix_agent_classes = list_agents(AgentGroup.FIX)
|
|
323
|
+
all_patches: list[Patch] = []
|
|
324
|
+
|
|
325
|
+
lp = LiveProgress(con, title="Fix")
|
|
326
|
+
lp.start()
|
|
327
|
+
for agent_cls in fix_agent_classes:
|
|
328
|
+
# Only run agents that have relevant findings
|
|
329
|
+
relevant = [f for f in findings if f.get("fix_agent") == agent_cls.name]
|
|
330
|
+
if not relevant and agent_cls.name not in ("EnvFixer",):
|
|
331
|
+
continue
|
|
332
|
+
|
|
333
|
+
short_name = agent_cls.name.replace("Agent", "").replace("Fixer", "")
|
|
334
|
+
lp.set_progress(len(all_patches) + 1, len(fix_agent_classes), short_name)
|
|
335
|
+
lp.log(f" {short_name}…")
|
|
336
|
+
lp.update()
|
|
337
|
+
|
|
338
|
+
def ai_progress(msg: str):
|
|
339
|
+
lp.log(f" [dim]{msg}[/dim]", "dim")
|
|
340
|
+
lp.update()
|
|
341
|
+
|
|
342
|
+
inp_with_cb = AgentInput(
|
|
343
|
+
root=inp.root,
|
|
344
|
+
scope=inp.scope,
|
|
345
|
+
brain=inp.brain,
|
|
346
|
+
config=inp.config,
|
|
347
|
+
extra=inp.extra,
|
|
348
|
+
on_message=lambda n, msg, s: (lp.log(f" {msg}", s), lp.update()),
|
|
349
|
+
on_ai_progress=ai_progress,
|
|
350
|
+
)
|
|
351
|
+
result = agent_cls().run(inp_with_cb)
|
|
352
|
+
patch_count = len(result.data.get("patches", []))
|
|
353
|
+
lp.log(f" [{patch_count}] patches from {short_name}", "")
|
|
354
|
+
|
|
355
|
+
# Extract patches from result
|
|
356
|
+
for patch_dict in result.data.get("patches", []):
|
|
357
|
+
try:
|
|
358
|
+
patch = Patch.from_dict(patch_dict)
|
|
359
|
+
all_patches.append(patch)
|
|
360
|
+
except Exception as e:
|
|
361
|
+
lp.log(f" [dim]Skipping invalid patch: {e}[/dim]", "dim")
|
|
362
|
+
|
|
363
|
+
lp.update()
|
|
364
|
+
|
|
365
|
+
lp.stop(summary=f"{len(all_patches)} patch(es) from {len(fix_agent_classes)} agent(s)")
|
|
366
|
+
return all_patches
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
# ── Dry run display ────────────────────────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _show_dry_run(findings: list[dict], root: Path, preview: bool = False) -> None:
|
|
373
|
+
"""Show what would be fixed without doing anything.
|
|
374
|
+
|
|
375
|
+
If preview=True, also show the actual before/after code diff for each patch.
|
|
376
|
+
"""
|
|
377
|
+
table = Table(show_header=True, header_style="bold #C8621A", box=None, pad_edge=False)
|
|
378
|
+
table.add_column("Fix Agent", style="bold #F2EDD6", width=20)
|
|
379
|
+
table.add_column("File", style="dim", width=34)
|
|
380
|
+
table.add_column("Issue", width=40)
|
|
381
|
+
table.add_column("Sev", width=8)
|
|
382
|
+
|
|
383
|
+
sev_colors = {
|
|
384
|
+
"critical": "#FF4D6D",
|
|
385
|
+
"high": "#FF8C42",
|
|
386
|
+
"medium": "#FACC15",
|
|
387
|
+
"low": "#4ADE80",
|
|
388
|
+
"info": "dim",
|
|
389
|
+
}
|
|
390
|
+
_SEV_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
|
|
391
|
+
|
|
392
|
+
for f in sorted(findings, key=lambda x: _SEV_ORDER.get(x.get("severity", "info"), 99)):
|
|
393
|
+
sev = f.get("severity", "info")
|
|
394
|
+
table.add_row(
|
|
395
|
+
f.get("fix_agent", "?"),
|
|
396
|
+
f.get("file", "?")[:34],
|
|
397
|
+
f.get("message", "?")[:40],
|
|
398
|
+
Text(sev, style=sev_colors.get(sev, "white")),
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
con.print(
|
|
402
|
+
Panel(
|
|
403
|
+
table,
|
|
404
|
+
title="[bold #C8621A]--dry-run: Would fix[/bold #C8621A]",
|
|
405
|
+
border_style="#2A3D28",
|
|
406
|
+
)
|
|
407
|
+
)
|
|
408
|
+
con.print()
|
|
409
|
+
con.print(f"[dim]{len(findings)} finding(s) would be sent to fix agents.[/dim]")
|
|
410
|
+
|
|
411
|
+
# Run fix agents to generate proposed patches (without applying)
|
|
412
|
+
patches = _run_fix_agents(findings, root)
|
|
413
|
+
|
|
414
|
+
if patches:
|
|
415
|
+
gate = RiskGate(root)
|
|
416
|
+
con.print()
|
|
417
|
+
risk_table = Table(show_header=True, header_style="bold #C8621A", box=None, pad_edge=False)
|
|
418
|
+
risk_table.add_column("File", style="dim", width=34)
|
|
419
|
+
risk_table.add_column("Risk", justify="right", width=8)
|
|
420
|
+
risk_table.add_column("Confidence", justify="right", width=10)
|
|
421
|
+
risk_table.add_column("Action", width=16)
|
|
422
|
+
risk_table.add_column("Description", width=40)
|
|
423
|
+
|
|
424
|
+
files_changed: set[str] = set()
|
|
425
|
+
for patch in sorted(patches, key=lambda p: p.blast_radius or 0):
|
|
426
|
+
gate_result = gate.evaluate(patch)
|
|
427
|
+
action = (
|
|
428
|
+
"BLOCKED"
|
|
429
|
+
if gate_result.is_blocked
|
|
430
|
+
else ("AUTO-APPLY" if gate_result.is_auto else "QUEUE")
|
|
431
|
+
)
|
|
432
|
+
action_color = (
|
|
433
|
+
"#FF4D6D"
|
|
434
|
+
if gate_result.is_blocked
|
|
435
|
+
else ("#4ADE80" if gate_result.is_auto else "#FACC15")
|
|
436
|
+
)
|
|
437
|
+
for fp in patch.affected_paths:
|
|
438
|
+
files_changed.add(fp)
|
|
439
|
+
file_label = patch.affected_paths[0] if patch.affected_paths else "(multiple)"
|
|
440
|
+
risk_table.add_row(
|
|
441
|
+
file_label[:34],
|
|
442
|
+
f"{patch.risk_score:.0f}",
|
|
443
|
+
f"{patch.confidence:.0f}%",
|
|
444
|
+
Text(action, style=action_color),
|
|
445
|
+
(patch.description or "")[:40],
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
con.print(
|
|
449
|
+
Panel(
|
|
450
|
+
risk_table,
|
|
451
|
+
title="[bold #C8621A]--dry-run: Proposed patches[/bold #C8621A]",
|
|
452
|
+
border_style="#2A3D28",
|
|
453
|
+
)
|
|
454
|
+
)
|
|
455
|
+
con.print()
|
|
456
|
+
con.print(
|
|
457
|
+
f"[dim]{len(patches)} patch(es) across {len(files_changed)} file(s) would be generated.[/dim]"
|
|
458
|
+
)
|
|
459
|
+
else:
|
|
460
|
+
con.print("[dim]No patches would be generated by fix agents.[/dim]")
|
|
461
|
+
|
|
462
|
+
# --preview: show before/after code diffs
|
|
463
|
+
if preview and patches:
|
|
464
|
+
_show_preview_diffs(patches, root)
|
|
465
|
+
|
|
466
|
+
hint = "Remove --preview or --dry-run to apply fixes."
|
|
467
|
+
con.print(f"[dim]{hint}[/dim]")
|
|
468
|
+
con.print()
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
# ── Preview Diffs ──────────────────────────────────────────────────────────────
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _show_preview_diffs(patches: list[Patch], root: Path) -> None:
|
|
475
|
+
"""Show before/after code diffs for each proposed patch."""
|
|
476
|
+
con.print()
|
|
477
|
+
con.print("[bold #C8621A]── Preview: Before / After ──[/bold #C8621A]")
|
|
478
|
+
con.print()
|
|
479
|
+
|
|
480
|
+
for i, patch in enumerate(patches, 1):
|
|
481
|
+
file_label = patch.affected_paths[0] if patch.affected_paths else "(unknown)"
|
|
482
|
+
con.print(
|
|
483
|
+
f"[bold]#{i} {file_label}[/bold] "
|
|
484
|
+
f"[dim]{patch.description[:60] if patch.description else ''}[/dim]"
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
# Try to read the current file content and show relevant lines
|
|
488
|
+
for change in (patch.changes or []):
|
|
489
|
+
file_path = change.get("path", "")
|
|
490
|
+
if not file_path:
|
|
491
|
+
continue
|
|
492
|
+
|
|
493
|
+
full_path = root / file_path
|
|
494
|
+
if not full_path.is_file():
|
|
495
|
+
continue
|
|
496
|
+
|
|
497
|
+
# Get line range from the change
|
|
498
|
+
start_line = change.get("line_start", change.get("line", 0))
|
|
499
|
+
end_line = change.get("line_end", start_line)
|
|
500
|
+
new_content = change.get("content", change.get("new_content", ""))
|
|
501
|
+
|
|
502
|
+
if not new_content and not start_line:
|
|
503
|
+
continue
|
|
504
|
+
|
|
505
|
+
try:
|
|
506
|
+
lines = full_path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
507
|
+
except Exception:
|
|
508
|
+
continue
|
|
509
|
+
|
|
510
|
+
# Show context: 3 lines before, the changed area, 3 lines after
|
|
511
|
+
ctx_start = max(0, (start_line or 1) - 4)
|
|
512
|
+
ctx_end = min(len(lines), (end_line or start_line or len(lines)) + 3)
|
|
513
|
+
|
|
514
|
+
if start_line and end_line:
|
|
515
|
+
# Show the diff
|
|
516
|
+
con.print(f" [dim]Lines {start_line}-{end_line}:[/dim]")
|
|
517
|
+
for ln in range(ctx_start, ctx_end):
|
|
518
|
+
line_num = ln + 1
|
|
519
|
+
marker = "-" if start_line <= line_num <= end_line else " "
|
|
520
|
+
color = "red" if marker == "-" else "dim"
|
|
521
|
+
con.print(f" [{color}]{marker} {line_num:4d} │ {lines[ln][:80]}[/{color}]")
|
|
522
|
+
|
|
523
|
+
if new_content:
|
|
524
|
+
con.print(" [green]+ After:[/green]")
|
|
525
|
+
for new_line in new_content.splitlines()[:10]:
|
|
526
|
+
con.print(f" [green]+ │ {new_line[:80]}[/green]")
|
|
527
|
+
elif new_content:
|
|
528
|
+
# New file or block
|
|
529
|
+
con.print(" [green]+ New content:[/green]")
|
|
530
|
+
for new_line in new_content.splitlines()[:15]:
|
|
531
|
+
con.print(f" [green]+ │ {new_line[:80]}[/green]")
|
|
532
|
+
|
|
533
|
+
con.print()
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
# ── Summary ────────────────────────────────────────────────────────────────────
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _show_summary(
|
|
540
|
+
applied: list[Patch],
|
|
541
|
+
queued: list[Patch],
|
|
542
|
+
blocked: list[tuple[Patch, str]],
|
|
543
|
+
) -> None:
|
|
544
|
+
con.print()
|
|
545
|
+
|
|
546
|
+
if applied:
|
|
547
|
+
con.print(
|
|
548
|
+
f"[#4ADE80]✓ Applied:[/#4ADE80] {len(applied)} fix(es) passed tests and are live."
|
|
549
|
+
)
|
|
550
|
+
for p in applied:
|
|
551
|
+
con.print(f" [dim]● {p.id}[/dim] {p.description[:60]}")
|
|
552
|
+
|
|
553
|
+
if queued:
|
|
554
|
+
con.print()
|
|
555
|
+
con.print(
|
|
556
|
+
f"[yellow]● Queued for review:[/yellow] {len(queued)} fix(es) need your approval."
|
|
557
|
+
)
|
|
558
|
+
con.print(" [dim]Run [bold]p review[/bold] to see diffs and approve or reject.[/dim]")
|
|
559
|
+
|
|
560
|
+
if blocked:
|
|
561
|
+
con.print()
|
|
562
|
+
con.print(f"[red]✗ Blocked:[/red] {len(blocked)} fix(es) could not proceed.")
|
|
563
|
+
for _, reason in blocked[:3]:
|
|
564
|
+
con.print(f" [dim]{reason[:80]}[/dim]")
|
|
565
|
+
|
|
566
|
+
if not applied and not queued and not blocked:
|
|
567
|
+
con.print("[dim]No patches were generated.[/dim]")
|
|
568
|
+
|
|
569
|
+
con.print()
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
# ── Multi-framework annotation ─────────────────────────────────────────────────
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _build_file_framework_map(brain: dict) -> dict[str, str]:
|
|
576
|
+
"""Build a file path → framework name map from brain routes and framework list.
|
|
577
|
+
|
|
578
|
+
Routes carry a ``framework`` field (set by RouteMapper). For files that
|
|
579
|
+
appear in no route we fall back to the first detected framework for that
|
|
580
|
+
file's language.
|
|
581
|
+
"""
|
|
582
|
+
mapping: dict[str, str] = {}
|
|
583
|
+
|
|
584
|
+
# 1. Route-based mapping (most precise — per-file per-route)
|
|
585
|
+
for route in brain.get("routes", []):
|
|
586
|
+
if isinstance(route, dict):
|
|
587
|
+
fw = route.get("framework", "") or ""
|
|
588
|
+
fp = route.get("file", "") or ""
|
|
589
|
+
else:
|
|
590
|
+
fw = getattr(route, "framework", "") or ""
|
|
591
|
+
fp = getattr(route, "file", "") or ""
|
|
592
|
+
if fw and fp:
|
|
593
|
+
mapping[fp] = fw
|
|
594
|
+
|
|
595
|
+
# 2. Language-based fallback for files without routes
|
|
596
|
+
detected_frameworks = brain.get("frameworks", [])
|
|
597
|
+
if not mapping and detected_frameworks:
|
|
598
|
+
first_fw = ""
|
|
599
|
+
if isinstance(detected_frameworks[0], dict):
|
|
600
|
+
first_fw = detected_frameworks[0].get("name", "")
|
|
601
|
+
elif isinstance(detected_frameworks[0], str):
|
|
602
|
+
first_fw = detected_frameworks[0]
|
|
603
|
+
if first_fw:
|
|
604
|
+
for fi in brain.get("file_infos", []):
|
|
605
|
+
fp = (
|
|
606
|
+
fi.path
|
|
607
|
+
if hasattr(fi, "path")
|
|
608
|
+
else (fi.get("path") if isinstance(fi, dict) else "")
|
|
609
|
+
)
|
|
610
|
+
if fp and fp not in mapping:
|
|
611
|
+
mapping[fp] = first_fw
|
|
612
|
+
|
|
613
|
+
return mapping
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _annotate_findings_with_framework(findings: list[dict], brain: dict) -> None:
|
|
617
|
+
"""Annotate each finding with the ``framework`` key so fix agents can
|
|
618
|
+
produce framework-aware prompts."""
|
|
619
|
+
fw_map = _build_file_framework_map(brain)
|
|
620
|
+
for f in findings:
|
|
621
|
+
fp = f.get("file", "")
|
|
622
|
+
if fp in fw_map:
|
|
623
|
+
f["framework"] = fw_map[fp]
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
# ── Helpers ────────────────────────────────────────────────────────────────────
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _collect_findings(scan_results: dict, area: str | None) -> list[dict]:
|
|
630
|
+
"""Collect all findings from the last agent scan results."""
|
|
631
|
+
all_findings: list[dict] = []
|
|
632
|
+
for agent_name, data in scan_results.items():
|
|
633
|
+
if agent_name == "Brain":
|
|
634
|
+
continue
|
|
635
|
+
findings = data.get("findings", [])
|
|
636
|
+
for f in findings:
|
|
637
|
+
if area and not f.get("file", "").startswith(area):
|
|
638
|
+
continue
|
|
639
|
+
all_findings.append(f)
|
|
640
|
+
return all_findings
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _save_patch(patch: Patch, root: Path) -> None:
|
|
644
|
+
"""Save a patch to memory."""
|
|
645
|
+
try:
|
|
646
|
+
mem.save_patch(patch.to_dict(), root)
|
|
647
|
+
except Exception as e:
|
|
648
|
+
con.print(f"[dim]Could not save patch {patch.id}: {e}[/dim]")
|