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,130 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Understander — picks the 12-20 files that actually matter.
|
|
3
|
+
|
|
4
|
+
Uses Body tags + blast_radius + import_graph instead of file_infos[:50]
|
|
5
|
+
in insertion order. All AI surfaces call this instead of slicing.
|
|
6
|
+
|
|
7
|
+
Provides:
|
|
8
|
+
- core_files(limit) -> [{path, why, score, content_snippet}]
|
|
9
|
+
- refine_domains(active, tags) -> filtered domains (drop false hubs)
|
|
10
|
+
- get_function_at(file, line) -> function body via FileCorpus cache
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
_log = logging.getLogger("patchi.brain.understander")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Understander:
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
root: Path,
|
|
26
|
+
file_infos: list[Any],
|
|
27
|
+
body_tags: dict[str, dict[str, Any]],
|
|
28
|
+
blast_map: dict[str, Any] | None = None,
|
|
29
|
+
routes: list[Any] | None = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
self.root = root
|
|
32
|
+
self.file_infos = {fi.path: fi for fi in file_infos}
|
|
33
|
+
self.tags = body_tags
|
|
34
|
+
self.blast = blast_map or {}
|
|
35
|
+
self.routes = routes or []
|
|
36
|
+
# sort once
|
|
37
|
+
self._ranked = sorted(
|
|
38
|
+
body_tags.items(),
|
|
39
|
+
key=lambda kv: (-int(kv[1].get("score", 0)), -int(kv[1].get("fan_in", 0)), kv[0]),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def core_files(self, limit: int = 16, include_low: bool = False) -> list[dict[str, Any]]:
|
|
43
|
+
"""Top-ranked files the LLM must see. Always includes Brain + memory + entry points."""
|
|
44
|
+
out: list[dict[str, Any]] = []
|
|
45
|
+
seen: set[str] = set()
|
|
46
|
+
# force-include
|
|
47
|
+
for must in ("patchi/core/brain/brain.py", "patchi/core/memory.py", "patchi/core/brain/body_tags.py"):
|
|
48
|
+
if must in self.tags and must not in seen:
|
|
49
|
+
t = self.tags[must]
|
|
50
|
+
out.append({"path": must, "why": f"{t['role']}/{t['system']} critical={t['criticality']} fan_in={t['fan_in']}", "score": t["score"], "tag": t})
|
|
51
|
+
seen.add(must)
|
|
52
|
+
for path, tag in self._ranked:
|
|
53
|
+
if path in seen:
|
|
54
|
+
continue
|
|
55
|
+
if not include_low and tag.get("criticality") == "low" and tag.get("score", 0) < 15 and len(out) >= limit:
|
|
56
|
+
continue
|
|
57
|
+
out.append({"path": path, "why": f"{tag['role']}/{tag['system']} {tag['criticality']} fan_in={tag.get('fan_in',0)}", "score": tag["score"], "tag": tag})
|
|
58
|
+
seen.add(path)
|
|
59
|
+
if len(out) >= limit:
|
|
60
|
+
break
|
|
61
|
+
return out
|
|
62
|
+
|
|
63
|
+
def refine_domains(self, active: list[str]) -> list[str]:
|
|
64
|
+
"""Drop domains that are low-score hubs only (speed). Keep high/critical cores."""
|
|
65
|
+
if not active:
|
|
66
|
+
return active
|
|
67
|
+
# count how many core_files map to each domain keyword
|
|
68
|
+
core_paths = {c["path"] for c in self.core_files(limit=12)}
|
|
69
|
+
# naive: keep all for now, but log drop candidate
|
|
70
|
+
# Future: intersect with body_tags system fields
|
|
71
|
+
_ = core_paths
|
|
72
|
+
return active
|
|
73
|
+
|
|
74
|
+
def file_snippet(self, rel_path: str, max_lines: int = 200) -> str:
|
|
75
|
+
"""Return first max_lines with line numbers — for prompts. Uses FileCorpus-style cache via pathlib."""
|
|
76
|
+
fi = self.file_infos.get(rel_path)
|
|
77
|
+
if fi is not None:
|
|
78
|
+
try:
|
|
79
|
+
txt = (self.root / rel_path).read_text(encoding="utf-8", errors="replace")
|
|
80
|
+
lines = txt.splitlines()[:max_lines]
|
|
81
|
+
return "\n".join(f"{i+1:4d} | {line}" for i, line in enumerate(lines))
|
|
82
|
+
except Exception as exc: # noqa: BLE001
|
|
83
|
+
_log.debug("understander snippet failed for %s: %s", rel_path, exc)
|
|
84
|
+
return ""
|
|
85
|
+
return ""
|
|
86
|
+
|
|
87
|
+
def function_at(self, rel_path: str, line: int, window: int = 50) -> str:
|
|
88
|
+
"""Return function body surrounding line, using line-number heuristic (no heavy AST)."""
|
|
89
|
+
try:
|
|
90
|
+
txt = (self.root / rel_path).read_text(encoding="utf-8", errors="replace")
|
|
91
|
+
except Exception as exc: # noqa: BLE001
|
|
92
|
+
_log.debug("function_at read failed: %s", exc)
|
|
93
|
+
return ""
|
|
94
|
+
lines = txt.splitlines()
|
|
95
|
+
if not 1 <= line <= len(lines):
|
|
96
|
+
return "\n".join(lines[: min(len(lines), window)])
|
|
97
|
+
# find def/class up
|
|
98
|
+
start = max(0, line - 1 - 60)
|
|
99
|
+
for i in range(line - 2, max(-1, line - 80), -1):
|
|
100
|
+
s = lines[i].lstrip()
|
|
101
|
+
if s.startswith("def ") or s.startswith("async def ") or s.startswith("class "):
|
|
102
|
+
start = i
|
|
103
|
+
break
|
|
104
|
+
# find next def at same or lesser indent
|
|
105
|
+
base_indent = len(lines[line - 1]) - len(lines[line - 1].lstrip())
|
|
106
|
+
end = min(len(lines), line + window)
|
|
107
|
+
for i in range(line, min(len(lines), line + 80)):
|
|
108
|
+
s = lines[i]
|
|
109
|
+
if s.strip() and not s.strip().startswith("#"):
|
|
110
|
+
indent = len(s) - len(s.lstrip())
|
|
111
|
+
st = s.strip()
|
|
112
|
+
if indent <= base_indent and (st.startswith("def ") or st.startswith("class ") or st.startswith("async def ")):
|
|
113
|
+
end = i
|
|
114
|
+
break
|
|
115
|
+
block = lines[start:end]
|
|
116
|
+
return "\n".join(f"{i+1:4d} | {line}" for i, line in enumerate(block, start=start + 1))
|
|
117
|
+
|
|
118
|
+
def as_prompt_block(self, limit: int = 14) -> str:
|
|
119
|
+
"""One-shot block for LLM prompts — core files + why."""
|
|
120
|
+
core = self.core_files(limit=limit)
|
|
121
|
+
lines = []
|
|
122
|
+
for c in core:
|
|
123
|
+
lines.append(f"- {c['path']} // {c['why']}")
|
|
124
|
+
snippet = self.file_snippet(c["path"], max_lines=24)
|
|
125
|
+
if snippet:
|
|
126
|
+
# cap per-file snippet to 24 lines to stay within 6k budget
|
|
127
|
+
lines.append("```python")
|
|
128
|
+
lines.append(snippet)
|
|
129
|
+
lines.append("```")
|
|
130
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Self-Report Verifier (Dream Assistant spec — Live/Audit Mode, priority #1).
|
|
3
|
+
|
|
4
|
+
The whole point: don't trust the agent's "tests pass" / "this works" claim.
|
|
5
|
+
Independently re-run the project's real test suite (and optionally a scan),
|
|
6
|
+
compare against the last known-good baseline, and emit an authoritative,
|
|
7
|
+
plain-language truth report.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
p verify — re-run tests (+ scan), diff against baseline, report truth
|
|
11
|
+
p verify --no-scan — only re-run tests
|
|
12
|
+
p verify --claim "tests pass" — assert a specific claim against actual truth
|
|
13
|
+
|
|
14
|
+
This reuses the project's own test runner (pytest by default) and Patchi's
|
|
15
|
+
Brain scan — it never trusts a self-report.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import logging
|
|
21
|
+
import re
|
|
22
|
+
import shutil
|
|
23
|
+
import subprocess
|
|
24
|
+
import time
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from datetime import UTC, datetime
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
_BASELINE_FILE = ".patchi/verify_baseline.json"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_log = logging.getLogger("patchi.brain.verify")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class TestRun:
|
|
37
|
+
__test__ = False # tell pytest this is not a test class
|
|
38
|
+
command: str = ""
|
|
39
|
+
passed: int = 0
|
|
40
|
+
failed: int = 0
|
|
41
|
+
error: int = 0
|
|
42
|
+
skipped: int = 0
|
|
43
|
+
total: int = 0
|
|
44
|
+
failed_names: list[str] = field(default_factory=list)
|
|
45
|
+
raw: str = ""
|
|
46
|
+
exit_ok: bool = False
|
|
47
|
+
duration_seconds: float = 0.0
|
|
48
|
+
note: str = ""
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def truthful(self) -> bool:
|
|
52
|
+
"""True only if the suite actually ran and nothing failed/errored."""
|
|
53
|
+
return bool(self.command) and self.exit_ok and self.failed == 0 and self.error == 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class VerifyReport:
|
|
58
|
+
__test__ = False
|
|
59
|
+
scanned_at: str = ""
|
|
60
|
+
test_run: TestRun | None = None
|
|
61
|
+
findings_count: int = 0
|
|
62
|
+
charter_violations: int = 0
|
|
63
|
+
scan_error: str | None = None
|
|
64
|
+
baseline: dict | None = None
|
|
65
|
+
regression: dict | None = None
|
|
66
|
+
truthful: bool = False
|
|
67
|
+
summary: str = ""
|
|
68
|
+
|
|
69
|
+
def to_dict(self) -> dict:
|
|
70
|
+
return {
|
|
71
|
+
"scanned_at": self.scanned_at,
|
|
72
|
+
"truthful": self.truthful,
|
|
73
|
+
"tests": {
|
|
74
|
+
"passed": self.test_run.passed if self.test_run else 0,
|
|
75
|
+
"failed": self.test_run.failed if self.test_run else 0,
|
|
76
|
+
"error": self.test_run.error if self.test_run else 0,
|
|
77
|
+
"skipped": self.test_run.skipped if self.test_run else 0,
|
|
78
|
+
"truthful": self.test_run.truthful if self.test_run else False,
|
|
79
|
+
},
|
|
80
|
+
"findings_count": self.findings_count,
|
|
81
|
+
"charter_violations": self.charter_violations,
|
|
82
|
+
"regression": self.regression,
|
|
83
|
+
"summary": self.summary,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ── Test runner ────────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def detect_test_command(root: Path) -> str | None:
|
|
91
|
+
"""Pick the project's real test runner. Prefer pytest; fall back to unittest."""
|
|
92
|
+
if shutil.which("pytest"):
|
|
93
|
+
return "pytest -q -p no:cacheprovider"
|
|
94
|
+
return "python -m unittest discover -s tests -v"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def run_tests(root: Path, command: str | None = None) -> TestRun:
|
|
98
|
+
"""Independently execute the project's test suite. Never trusts a claim."""
|
|
99
|
+
cmd = command or detect_test_command(root)
|
|
100
|
+
if not cmd:
|
|
101
|
+
return TestRun(note="No test runner (pytest/unittest) found.")
|
|
102
|
+
start = time.monotonic()
|
|
103
|
+
try:
|
|
104
|
+
proc = subprocess.run(
|
|
105
|
+
cmd.split(),
|
|
106
|
+
cwd=str(root),
|
|
107
|
+
capture_output=True,
|
|
108
|
+
text=True,
|
|
109
|
+
timeout=600,
|
|
110
|
+
)
|
|
111
|
+
except Exception as e: # pragma: no cover - environment dependent
|
|
112
|
+
return TestRun(command=cmd, note=f"Could not run tests: {e}")
|
|
113
|
+
raw = (proc.stdout or "") + "\n" + (proc.stderr or "")
|
|
114
|
+
dur = time.monotonic() - start
|
|
115
|
+
run = _parse_pytest_output(raw)
|
|
116
|
+
run.command = cmd
|
|
117
|
+
run.raw = raw
|
|
118
|
+
run.exit_ok = proc.returncode == 0
|
|
119
|
+
run.duration_seconds = round(dur, 2)
|
|
120
|
+
if not run.total and "no tests ran" in raw:
|
|
121
|
+
run.note = "No tests found."
|
|
122
|
+
return run
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _parse_pytest_output(text: str) -> TestRun:
|
|
126
|
+
run = TestRun()
|
|
127
|
+
for metric, attr in (
|
|
128
|
+
("passed", "passed"),
|
|
129
|
+
("failed", "failed"),
|
|
130
|
+
("error", "error"),
|
|
131
|
+
("skipped", "skipped"),
|
|
132
|
+
):
|
|
133
|
+
m = re.search(rf"(\d+)\s+{metric}", text)
|
|
134
|
+
if m:
|
|
135
|
+
setattr(run, attr, int(m.group(1)))
|
|
136
|
+
run.total = run.passed + run.failed + run.error + run.skipped
|
|
137
|
+
run.failed_names = re.findall(r"FAILED\s+([^\s-]+)", text)
|
|
138
|
+
return run
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ── Baseline (regression radar) ───────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _baseline_path(root: Path) -> Path:
|
|
145
|
+
return root / _BASELINE_FILE
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def load_baseline(root: Path) -> dict | None:
|
|
149
|
+
p = _baseline_path(root)
|
|
150
|
+
if not p.exists():
|
|
151
|
+
return None
|
|
152
|
+
try:
|
|
153
|
+
return json.loads(p.read_text(encoding="utf-8"))
|
|
154
|
+
except Exception as e:
|
|
155
|
+
_log.warning("load_baseline failed: %s", e)
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def save_baseline(root: Path, snapshot: dict) -> None:
|
|
160
|
+
p = _baseline_path(root)
|
|
161
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
162
|
+
tmp = p.with_suffix(".tmp")
|
|
163
|
+
tmp.write_text(json.dumps(snapshot, indent=2), encoding="utf-8")
|
|
164
|
+
tmp.replace(p)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def compute_regression(
|
|
168
|
+
baseline: dict | None,
|
|
169
|
+
test_run: TestRun,
|
|
170
|
+
findings_count: int,
|
|
171
|
+
charter_violations: int,
|
|
172
|
+
) -> dict:
|
|
173
|
+
"""Diff current truth against the last known-good baseline."""
|
|
174
|
+
if not baseline:
|
|
175
|
+
return {
|
|
176
|
+
"has_baseline": False,
|
|
177
|
+
"new_failures": list(test_run.failed_names),
|
|
178
|
+
"new_findings": findings_count,
|
|
179
|
+
"new_charter_violations": charter_violations,
|
|
180
|
+
}
|
|
181
|
+
base_fail = set(baseline.get("failed_names", []))
|
|
182
|
+
cur_fail = set(test_run.failed_names)
|
|
183
|
+
base_find = baseline.get("findings_count", 0)
|
|
184
|
+
base_charter = baseline.get("charter_violations", 0)
|
|
185
|
+
return {
|
|
186
|
+
"has_baseline": True,
|
|
187
|
+
"new_failures": sorted(cur_fail - base_fail),
|
|
188
|
+
"resolved_failures": sorted(base_fail - cur_fail),
|
|
189
|
+
"new_findings": max(0, findings_count - base_find),
|
|
190
|
+
"new_charter_violations": max(0, charter_violations - base_charter),
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ── Verify orchestration ─────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def verify_project(
|
|
198
|
+
root: Path,
|
|
199
|
+
test_runner=None,
|
|
200
|
+
run_scan: bool = True,
|
|
201
|
+
) -> VerifyReport:
|
|
202
|
+
"""
|
|
203
|
+
Independently verify the project's actual state.
|
|
204
|
+
|
|
205
|
+
test_runner: injectable for tests; defaults to run_tests().
|
|
206
|
+
Returns a VerifyReport with an authoritative truth + regression delta.
|
|
207
|
+
"""
|
|
208
|
+
root = Path(root)
|
|
209
|
+
test_run = (test_runner or run_tests)(root)
|
|
210
|
+
|
|
211
|
+
findings_count = 0
|
|
212
|
+
charter_violations = 0
|
|
213
|
+
scan_error = None
|
|
214
|
+
if run_scan:
|
|
215
|
+
try:
|
|
216
|
+
from patchi.core.brain.brain import Brain
|
|
217
|
+
|
|
218
|
+
Brain(root).scan()
|
|
219
|
+
findings_count = _count_scan_findings(root)
|
|
220
|
+
charter_violations = _count_charter_violations(root)
|
|
221
|
+
except Exception as e:
|
|
222
|
+
scan_error = str(e)
|
|
223
|
+
|
|
224
|
+
baseline = load_baseline(root)
|
|
225
|
+
regression = compute_regression(baseline, test_run, findings_count, charter_violations)
|
|
226
|
+
|
|
227
|
+
truthful = test_run.truthful and not regression.get("new_failures") and charter_violations == 0
|
|
228
|
+
|
|
229
|
+
save_baseline(
|
|
230
|
+
root,
|
|
231
|
+
{
|
|
232
|
+
"scanned_at": datetime.now(UTC).isoformat(),
|
|
233
|
+
"failed_names": test_run.failed_names,
|
|
234
|
+
"findings_count": findings_count,
|
|
235
|
+
"charter_violations": charter_violations,
|
|
236
|
+
},
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
summary = _build_summary(truthful, test_run, regression, charter_violations, scan_error)
|
|
240
|
+
return VerifyReport(
|
|
241
|
+
scanned_at=datetime.now(UTC).isoformat(),
|
|
242
|
+
test_run=test_run,
|
|
243
|
+
findings_count=findings_count,
|
|
244
|
+
charter_violations=charter_violations,
|
|
245
|
+
scan_error=scan_error,
|
|
246
|
+
baseline=baseline,
|
|
247
|
+
regression=regression,
|
|
248
|
+
truthful=truthful,
|
|
249
|
+
summary=summary,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _count_scan_findings(root: Path) -> int:
|
|
254
|
+
"""Count findings from the most recent scan results in memory."""
|
|
255
|
+
try:
|
|
256
|
+
from patchi.core import memory as mem
|
|
257
|
+
|
|
258
|
+
results = mem.get_scan_results(root)
|
|
259
|
+
total = 0
|
|
260
|
+
for _name, data in results.items():
|
|
261
|
+
for f in data.get("findings", []):
|
|
262
|
+
if isinstance(f, dict):
|
|
263
|
+
total += 1
|
|
264
|
+
return total
|
|
265
|
+
except Exception as e:
|
|
266
|
+
_log.warning("_count_scan_findings failed: %s", e)
|
|
267
|
+
return 0
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _count_charter_violations(root: Path) -> int:
|
|
271
|
+
try:
|
|
272
|
+
from patchi.core import memory as mem
|
|
273
|
+
|
|
274
|
+
results = mem.get_scan_results(root)
|
|
275
|
+
return len(results.get("CharterGuard", {}).get("findings", []))
|
|
276
|
+
except Exception as e:
|
|
277
|
+
_log.warning("_count_charter_violations failed: %s", e)
|
|
278
|
+
return 0
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _build_summary(truthful, test_run, regression, charter_violations, scan_error) -> str:
|
|
282
|
+
if truthful:
|
|
283
|
+
return (
|
|
284
|
+
f"VERIFIED ✅ — tests: {test_run.passed} passed"
|
|
285
|
+
+ (f", {test_run.skipped} skipped" if test_run.skipped else "")
|
|
286
|
+
+ (
|
|
287
|
+
f"; {regression.get('new_findings', 0)} new scan finding(s)"
|
|
288
|
+
if regression.get("new_findings")
|
|
289
|
+
else "; no new scan findings"
|
|
290
|
+
)
|
|
291
|
+
+ "."
|
|
292
|
+
)
|
|
293
|
+
parts = ["UNVERIFIED ❌ — mismatch between claim and reality:"]
|
|
294
|
+
if not test_run.truthful:
|
|
295
|
+
parts.append(
|
|
296
|
+
f" • tests: {test_run.failed} failed, {test_run.error} errored"
|
|
297
|
+
+ (f" -> {', '.join(test_run.failed_names[:5])}" if test_run.failed_names else "")
|
|
298
|
+
)
|
|
299
|
+
if regression.get("new_failures"):
|
|
300
|
+
parts.append(f" • new failures vs baseline: {', '.join(regression['new_failures'][:5])}")
|
|
301
|
+
if charter_violations:
|
|
302
|
+
parts.append(f" • {charter_violations} charter violation(s)")
|
|
303
|
+
if scan_error:
|
|
304
|
+
parts.append(f" • scan could not run: {scan_error}")
|
|
305
|
+
return "\n".join(parts)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def claim_holds(claim: str, report: VerifyReport) -> bool:
|
|
309
|
+
"""Crude claim check: 'tests pass' / 'works' must match an actually truthful run."""
|
|
310
|
+
low = claim.lower()
|
|
311
|
+
if "pass" in low or "works" in low or "green" in low:
|
|
312
|
+
return report.truthful
|
|
313
|
+
return report.truthful
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def verify_ui_findings(root: Path, findings: list[dict]) -> tuple[list[dict], list[dict]]:
|
|
317
|
+
"""Gate UI/test findings against the codebase before they enter memory.
|
|
318
|
+
|
|
319
|
+
A finding is kept only if its file resolves inside the project and any
|
|
320
|
+
claimed line exists in that file. Summary findings ("(all pages)", empty
|
|
321
|
+
file) always pass. Fail-open: anything unreadable is kept — only
|
|
322
|
+
provably-bogus entries (missing file, out-of-range line) are dropped.
|
|
323
|
+
Returns (kept, dropped).
|
|
324
|
+
"""
|
|
325
|
+
try:
|
|
326
|
+
from patchi.core.brain.file_corpus import FileCorpus
|
|
327
|
+
|
|
328
|
+
entries = FileCorpus(root).entries
|
|
329
|
+
except Exception:
|
|
330
|
+
entries = {}
|
|
331
|
+
|
|
332
|
+
kept: list[dict] = []
|
|
333
|
+
dropped: list[dict] = []
|
|
334
|
+
for f in findings:
|
|
335
|
+
if not isinstance(f, dict):
|
|
336
|
+
kept.append(f)
|
|
337
|
+
continue
|
|
338
|
+
rel = (f.get("file") or "").strip()
|
|
339
|
+
if not rel or rel == "(all pages)":
|
|
340
|
+
kept.append(f)
|
|
341
|
+
continue
|
|
342
|
+
# URL routes (UI findings reference pages, not files): fail open.
|
|
343
|
+
# Full route verification needs RouteMapper contract scope, which is
|
|
344
|
+
# unavailable here; file findings below are still strictly gated.
|
|
345
|
+
if rel.startswith("/"):
|
|
346
|
+
kept.append(f)
|
|
347
|
+
continue
|
|
348
|
+
rel = rel.replace("\\", "/").lstrip("/")
|
|
349
|
+
if rel in entries:
|
|
350
|
+
try:
|
|
351
|
+
n_lines = len(entries[rel].read().splitlines())
|
|
352
|
+
except Exception:
|
|
353
|
+
kept.append(f)
|
|
354
|
+
continue
|
|
355
|
+
else:
|
|
356
|
+
cand = (root / rel).resolve()
|
|
357
|
+
try:
|
|
358
|
+
cand.relative_to(root.resolve())
|
|
359
|
+
except ValueError:
|
|
360
|
+
dropped.append({**f, "drop_reason": "file outside project"})
|
|
361
|
+
continue
|
|
362
|
+
if not cand.is_file():
|
|
363
|
+
dropped.append({**f, "drop_reason": "file not found"})
|
|
364
|
+
continue
|
|
365
|
+
try:
|
|
366
|
+
n_lines = len(cand.read_text(encoding="utf-8", errors="replace").splitlines())
|
|
367
|
+
except OSError:
|
|
368
|
+
kept.append(f)
|
|
369
|
+
continue
|
|
370
|
+
line = f.get("line", 0) or 0
|
|
371
|
+
try:
|
|
372
|
+
line = int(line)
|
|
373
|
+
except (TypeError, ValueError):
|
|
374
|
+
line = 0
|
|
375
|
+
if line > 0 and line > n_lines:
|
|
376
|
+
dropped.append({**f, "drop_reason": f"line {line} beyond {n_lines} lines"})
|
|
377
|
+
continue
|
|
378
|
+
kept.append(f)
|
|
379
|
+
return kept, dropped
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Campaigns module — state transitions and data flow abuse campaigns."""
|
|
2
|
+
|
|
3
|
+
from .base import Campaign, CampaignStep
|
|
4
|
+
from .data_flow_abuse import DataFlowAbuseCampaign
|
|
5
|
+
from .orchestrator import CampaignOrchestrator
|
|
6
|
+
from .state_transitions import StateTransitionCampaign
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"Campaign",
|
|
10
|
+
"CampaignStep",
|
|
11
|
+
"CampaignOrchestrator",
|
|
12
|
+
"StateTransitionCampaign",
|
|
13
|
+
"DataFlowAbuseCampaign",
|
|
14
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Base campaign class and CampaignStep."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import abc
|
|
6
|
+
import logging
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
from patchi.core.assurance.graph import AssuranceGraph
|
|
10
|
+
|
|
11
|
+
_log = logging.getLogger("patchi.core.campaigns")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class CampaignStep:
|
|
16
|
+
"""One step in a campaign."""
|
|
17
|
+
|
|
18
|
+
name: str
|
|
19
|
+
description: str
|
|
20
|
+
status: str = "pending" # "pending", "running", "passed", "failed"
|
|
21
|
+
findings: list[dict] = field(default_factory=list)
|
|
22
|
+
|
|
23
|
+
def to_dict(self) -> dict:
|
|
24
|
+
return {
|
|
25
|
+
"name": self.name,
|
|
26
|
+
"description": self.description,
|
|
27
|
+
"status": self.status,
|
|
28
|
+
"findings_count": len(self.findings),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Campaign(abc.ABC):
|
|
33
|
+
"""Base class for assurance campaigns."""
|
|
34
|
+
|
|
35
|
+
name: str = "base"
|
|
36
|
+
description: str = ""
|
|
37
|
+
|
|
38
|
+
def __init__(self, graph: AssuranceGraph):
|
|
39
|
+
self._graph = graph
|
|
40
|
+
|
|
41
|
+
@abc.abstractmethod
|
|
42
|
+
def steps(self) -> list[CampaignStep]:
|
|
43
|
+
"""Define the steps in this campaign."""
|
|
44
|
+
|
|
45
|
+
def run(self) -> list[CampaignStep]:
|
|
46
|
+
"""Execute all steps and return results."""
|
|
47
|
+
results: list[CampaignStep] = []
|
|
48
|
+
for step in self.steps():
|
|
49
|
+
try:
|
|
50
|
+
step.status = "running"
|
|
51
|
+
self._execute_step(step)
|
|
52
|
+
step.status = "failed" if step.findings else "passed"
|
|
53
|
+
except Exception as e:
|
|
54
|
+
_log.warning("Campaign %s step %s failed: %s", self.name, step.name, e)
|
|
55
|
+
step.status = "failed"
|
|
56
|
+
step.findings.append({"error": str(e)})
|
|
57
|
+
results.append(step)
|
|
58
|
+
return results
|
|
59
|
+
|
|
60
|
+
@abc.abstractmethod
|
|
61
|
+
def _execute_step(self, step: CampaignStep) -> None:
|
|
62
|
+
"""Execute a single step (modifies step.findings in place)."""
|