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,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Assurance package — provable properties over the scanned codebase.
|
|
3
|
+
|
|
4
|
+
The question this package exists to answer (UNIFIED_UPGRADE_PLAN.md):
|
|
5
|
+
|
|
6
|
+
"What should be impossible, and can I prove it isn't?"
|
|
7
|
+
|
|
8
|
+
Structure:
|
|
9
|
+
graph.py — AssuranceGraph: claims, evidence, verdicts, persistence
|
|
10
|
+
invariants.py — invariant types + static verification engine
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from patchi.core.assurance.graph import (
|
|
14
|
+
AssuranceGraph,
|
|
15
|
+
Claim,
|
|
16
|
+
Evidence,
|
|
17
|
+
Verdict,
|
|
18
|
+
)
|
|
19
|
+
from patchi.core.assurance.invariants import (
|
|
20
|
+
Invariant,
|
|
21
|
+
InvariantType,
|
|
22
|
+
verify_invariants,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"AssuranceGraph",
|
|
27
|
+
"Claim",
|
|
28
|
+
"Evidence",
|
|
29
|
+
"Verdict",
|
|
30
|
+
"Invariant",
|
|
31
|
+
"InvariantType",
|
|
32
|
+
"verify_invariants",
|
|
33
|
+
]
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Build an AssuranceGraph from brain scan data.
|
|
2
|
+
|
|
3
|
+
Called after every ``p scan`` to populate the graph with claims derived from
|
|
4
|
+
detected routes, import graph, auth patterns, and domain knowledge.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .graph import AssuranceGraph, Evidence
|
|
14
|
+
|
|
15
|
+
_log = logging.getLogger("patchi.core.assurance.builder")
|
|
16
|
+
|
|
17
|
+
ASSURANCE_FILE = ".patchi/assurance_graph.json"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_assurance_graph(
|
|
21
|
+
root: Path,
|
|
22
|
+
brain_data: dict[str, Any] | None = None,
|
|
23
|
+
route_map: list[dict] | None = None,
|
|
24
|
+
import_graph_data: dict | None = None,
|
|
25
|
+
) -> AssuranceGraph:
|
|
26
|
+
"""Build an AssuranceGraph from brain scan data.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
root : Path
|
|
31
|
+
Project root.
|
|
32
|
+
brain_data : dict
|
|
33
|
+
Brain report summary (from ``report.summary_dict()``).
|
|
34
|
+
route_map : list[dict]
|
|
35
|
+
Detected routes (from ``RouteMapper``).
|
|
36
|
+
import_graph_data : dict
|
|
37
|
+
Import graph as dict (from ``ImportGraph.to_dict()``).
|
|
38
|
+
"""
|
|
39
|
+
graph = AssuranceGraph()
|
|
40
|
+
brain = brain_data or {}
|
|
41
|
+
routes = route_map or []
|
|
42
|
+
imp = import_graph_data or {}
|
|
43
|
+
|
|
44
|
+
# ── Components (frameworks detected) ──────────────────────────────────
|
|
45
|
+
frameworks = brain.get("detected_frameworks", [])
|
|
46
|
+
if not frameworks and brain.get("stack"):
|
|
47
|
+
frameworks = [brain["stack"].get("framework", "unknown")]
|
|
48
|
+
for fw in frameworks:
|
|
49
|
+
if fw and fw != "unknown":
|
|
50
|
+
_add_framework_claim(graph, fw)
|
|
51
|
+
|
|
52
|
+
# ── Actors (from auth patterns) ──────────────────────────────────────
|
|
53
|
+
auth_patterns = brain.get("auth_patterns", brain.get("project_context", {}).get("auth", {}))
|
|
54
|
+
actors = _extract_actors(auth_patterns)
|
|
55
|
+
for actor in actors:
|
|
56
|
+
_add_actor_claim(graph, actor)
|
|
57
|
+
|
|
58
|
+
# ── Assets (endpoints from route mapper) ─────────────────────────────
|
|
59
|
+
for route in routes:
|
|
60
|
+
path = route.get("path", route.get("route", ""))
|
|
61
|
+
methods = route.get("methods", route.get("http_methods", ["GET"]))
|
|
62
|
+
auth_required = route.get("auth_required", route.get("requires_auth", False))
|
|
63
|
+
if path:
|
|
64
|
+
_add_endpoint_claim(graph, path, methods, auth_required)
|
|
65
|
+
|
|
66
|
+
# ── Data flows (from import graph) ───────────────────────────────────
|
|
67
|
+
edges = imp.get("edges", [])
|
|
68
|
+
for edge in edges[:100]: # cap for performance
|
|
69
|
+
src = edge.get("source", edge.get("from", ""))
|
|
70
|
+
dst = edge.get("target", edge.get("to", ""))
|
|
71
|
+
if src and dst:
|
|
72
|
+
_add_data_flow_claim(graph, src, dst)
|
|
73
|
+
|
|
74
|
+
# ── Dependency health ────────────────────────────────────────────────
|
|
75
|
+
vulns = brain.get("dependency_vulnerabilities", [])
|
|
76
|
+
for v in vulns[:20]:
|
|
77
|
+
pkg = v.get("package", v.get("name", ""))
|
|
78
|
+
cve = v.get("cve", v.get("id", ""))
|
|
79
|
+
if pkg:
|
|
80
|
+
_add_dependency_claim(graph, pkg, cve)
|
|
81
|
+
|
|
82
|
+
# ── State machine (from session/auth analysis) ───────────────────────
|
|
83
|
+
_add_state_claims(graph, auth_patterns)
|
|
84
|
+
|
|
85
|
+
return graph
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _add_framework_claim(graph: AssuranceGraph, framework: str) -> None:
|
|
89
|
+
graph.upsert_claim(
|
|
90
|
+
f"framework-{framework}",
|
|
91
|
+
f"Project uses {framework}",
|
|
92
|
+
domain="architecture",
|
|
93
|
+
severity_if_disproved="info",
|
|
94
|
+
)
|
|
95
|
+
graph.attach_evidence(
|
|
96
|
+
f"framework-{framework}",
|
|
97
|
+
Evidence(source="brain_scan", detail=f"Detected framework: {framework}", supports=True),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _add_actor_claim(graph: AssuranceGraph, actor: str) -> None:
|
|
102
|
+
graph.upsert_claim(
|
|
103
|
+
f"actor-{actor}",
|
|
104
|
+
f"System has actor role: {actor}",
|
|
105
|
+
domain="auth",
|
|
106
|
+
severity_if_disproved="medium",
|
|
107
|
+
)
|
|
108
|
+
graph.attach_evidence(
|
|
109
|
+
f"actor-{actor}",
|
|
110
|
+
Evidence(source="brain_scan", detail=f"Actor detected: {actor}", supports=True),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _add_endpoint_claim(
|
|
115
|
+
graph: AssuranceGraph, path: str, methods: list, auth_required: bool
|
|
116
|
+
) -> None:
|
|
117
|
+
claim_id = f"endpoint-{path.replace('/', '_').strip('_')}"
|
|
118
|
+
graph.upsert_claim(
|
|
119
|
+
claim_id,
|
|
120
|
+
f"Endpoint {path} requires authentication",
|
|
121
|
+
domain="auth-endpoint",
|
|
122
|
+
severity_if_disproved="high" if auth_required else "medium",
|
|
123
|
+
)
|
|
124
|
+
# If we know auth is required, that's evidence supporting the claim
|
|
125
|
+
# that it *should* require auth (we're asserting it as a property)
|
|
126
|
+
graph.attach_evidence(
|
|
127
|
+
claim_id,
|
|
128
|
+
Evidence(
|
|
129
|
+
source="route_mapper",
|
|
130
|
+
detail=f"Route {path} ({', '.join(methods)}) auth_required={auth_required}",
|
|
131
|
+
supports=True,
|
|
132
|
+
artifact={"path": path, "methods": methods, "auth_required": auth_required},
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _add_data_flow_claim(graph: AssuranceGraph, source: str, sink: str) -> None:
|
|
138
|
+
claim_id = f"flow-{source.replace('.', '_')}-{sink.replace('.', '_')}"
|
|
139
|
+
graph.upsert_claim(
|
|
140
|
+
claim_id,
|
|
141
|
+
f"Data flow from {source} to {sink} is properly secured",
|
|
142
|
+
domain="data-flow",
|
|
143
|
+
severity_if_disproved="medium",
|
|
144
|
+
)
|
|
145
|
+
graph.attach_evidence(
|
|
146
|
+
claim_id,
|
|
147
|
+
Evidence(
|
|
148
|
+
source="import_graph",
|
|
149
|
+
detail=f"Import edge: {source} → {sink}",
|
|
150
|
+
supports=True,
|
|
151
|
+
artifact={"source": source, "sink": sink},
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _add_dependency_claim(graph: AssuranceGraph, pkg: str, cve: str) -> None:
|
|
157
|
+
claim_id = f"dep-{pkg}"
|
|
158
|
+
graph.upsert_claim(
|
|
159
|
+
claim_id,
|
|
160
|
+
f"Dependency {pkg} has no known vulnerabilities",
|
|
161
|
+
domain="dependency",
|
|
162
|
+
severity_if_disproved="high",
|
|
163
|
+
)
|
|
164
|
+
if cve:
|
|
165
|
+
# CVE found = refutes the claim (it IS vulnerable)
|
|
166
|
+
graph.attach_evidence(
|
|
167
|
+
claim_id,
|
|
168
|
+
Evidence(
|
|
169
|
+
source="dependency_scan",
|
|
170
|
+
detail=f"Vulnerability {cve} found in {pkg}",
|
|
171
|
+
supports=False,
|
|
172
|
+
artifact={"package": pkg, "cve": cve},
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _extract_actors(auth_patterns: Any) -> list[str]:
|
|
178
|
+
"""Extract actor roles from auth pattern data."""
|
|
179
|
+
actors = ["anonymous", "authenticated"]
|
|
180
|
+
if isinstance(auth_patterns, dict):
|
|
181
|
+
if auth_patterns.get("has_admin"):
|
|
182
|
+
actors.append("admin")
|
|
183
|
+
if auth_patterns.get("has_roles"):
|
|
184
|
+
actors.extend(["moderator", "service"])
|
|
185
|
+
if auth_patterns.get("has_api_key"):
|
|
186
|
+
actors.append("api_client")
|
|
187
|
+
return actors
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _add_state_claims(graph: AssuranceGraph, auth_patterns: Any) -> None:
|
|
191
|
+
"""Add state machine claims from auth analysis."""
|
|
192
|
+
states = ["anonymous", "authenticated"]
|
|
193
|
+
if isinstance(auth_patterns, dict) and auth_patterns.get("has_admin"):
|
|
194
|
+
states.append("admin")
|
|
195
|
+
|
|
196
|
+
for state in states:
|
|
197
|
+
claim_id = f"state-{state}"
|
|
198
|
+
graph.upsert_claim(
|
|
199
|
+
claim_id,
|
|
200
|
+
f"State '{state}' transitions are properly guarded",
|
|
201
|
+
domain="state-machine",
|
|
202
|
+
severity_if_disproved="high",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Login transition
|
|
206
|
+
graph.upsert_claim(
|
|
207
|
+
"state-login-transition",
|
|
208
|
+
"Login transition regenerates session ID",
|
|
209
|
+
domain="state-machine",
|
|
210
|
+
severity_if_disproved="critical",
|
|
211
|
+
)
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AssuranceGraph — a directed graph of claims backed by evidence.
|
|
3
|
+
|
|
4
|
+
Every security/quality property Patchi asserts becomes a Claim:
|
|
5
|
+
|
|
6
|
+
property ("all state-changing routes require auth")
|
|
7
|
+
<- Evidence (route scan: 4 routes, 1 unguarded)
|
|
8
|
+
<- Verdict (proved / not-proved / disproved + timestamp)
|
|
9
|
+
|
|
10
|
+
The graph persists to ``.patchi/assurance.json`` so campaigns accumulate
|
|
11
|
+
across runs instead of evaporating with each scan. Honesty rule (2.1-1):
|
|
12
|
+
a claim without evidence is NEVER reported as proved, and a disproved
|
|
13
|
+
claim keeps its repair history — the system records what it tried, not
|
|
14
|
+
just what it wanted to be true.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from datetime import UTC, datetime
|
|
22
|
+
from enum import StrEnum
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
_log = logging.getLogger("patchi.core.assurance.graph")
|
|
27
|
+
|
|
28
|
+
ASSURANCE_FILE = ".patchi/assurance.json"
|
|
29
|
+
COVERAGE_HISTORY_FILE = ".patchi/coverage_history.json"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Verdict(StrEnum):
|
|
33
|
+
"""Outcome of verifying a claim. No evidence => UNPROVEN, never 'proved'."""
|
|
34
|
+
|
|
35
|
+
PROVED = "proved"
|
|
36
|
+
NOT_PROVED = "not_proved" # couldn't establish (insufficient evidence)
|
|
37
|
+
DISPROVED = "disproved" # positive counter-evidence found
|
|
38
|
+
UNPROVEN = "unproven" # never checked
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Evidence:
|
|
43
|
+
"""One piece of supporting/refuting observation for a claim."""
|
|
44
|
+
|
|
45
|
+
source: str # what produced it: "intent_scan", "secret_scan", ...
|
|
46
|
+
detail: str # human-readable observation
|
|
47
|
+
supports: bool # True = supports claim; False = refutes
|
|
48
|
+
timestamp: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
|
|
49
|
+
artifact: dict = field(default_factory=dict) # machine-checkable payload
|
|
50
|
+
|
|
51
|
+
def to_dict(self) -> dict:
|
|
52
|
+
return {
|
|
53
|
+
"source": self.source,
|
|
54
|
+
"detail": self.detail,
|
|
55
|
+
"supports": self.supports,
|
|
56
|
+
"timestamp": self.timestamp,
|
|
57
|
+
"artifact": self.artifact,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_dict(cls, d: dict) -> Evidence:
|
|
62
|
+
return cls(
|
|
63
|
+
source=d.get("source", "?"),
|
|
64
|
+
detail=d.get("detail", ""),
|
|
65
|
+
supports=bool(d.get("supports", False)),
|
|
66
|
+
timestamp=d.get("timestamp", ""),
|
|
67
|
+
artifact=d.get("artifact", {}),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class Claim:
|
|
73
|
+
"""A property asserted about the project, with its evidence trail."""
|
|
74
|
+
|
|
75
|
+
id: str # stable slug, e.g. "auth-all-writes"
|
|
76
|
+
statement: str # human-readable property
|
|
77
|
+
domain: str # owning domain tag, e.g. "auth-session"
|
|
78
|
+
severity_if_disproved: str = "high"
|
|
79
|
+
verdict: Verdict = Verdict.UNPROVEN
|
|
80
|
+
evidence: list[Evidence] = field(default_factory=list)
|
|
81
|
+
repairs: list[dict] = field(default_factory=list) # adversarial-repair chain
|
|
82
|
+
updated_at: str = ""
|
|
83
|
+
|
|
84
|
+
def add_evidence(self, ev: Evidence) -> None:
|
|
85
|
+
self.evidence.append(ev)
|
|
86
|
+
self.updated_at = ev.timestamp
|
|
87
|
+
# Verdict recomputation: ANY refuting evidence wins (honesty-first);
|
|
88
|
+
# otherwise proved only if >=1 supporting evidence exists.
|
|
89
|
+
refuting = [e for e in self.evidence if not e.supports]
|
|
90
|
+
supporting = [e for e in self.evidence if e.supports]
|
|
91
|
+
if refuting:
|
|
92
|
+
self.verdict = Verdict.DISPROVED
|
|
93
|
+
elif supporting:
|
|
94
|
+
self.verdict = Verdict.PROVED
|
|
95
|
+
else:
|
|
96
|
+
self.verdict = Verdict.NOT_PROVED
|
|
97
|
+
|
|
98
|
+
def record_repair(self, description: str, outcome: str) -> None:
|
|
99
|
+
"""Record one loop of the adversarial repair chain."""
|
|
100
|
+
self.repairs.append(
|
|
101
|
+
{
|
|
102
|
+
"at": datetime.now(UTC).isoformat(),
|
|
103
|
+
"description": description,
|
|
104
|
+
"outcome": outcome,
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
# A repair attempt resets the verdict until re-verified.
|
|
108
|
+
self.verdict = Verdict.NOT_PROVED
|
|
109
|
+
|
|
110
|
+
def to_dict(self) -> dict:
|
|
111
|
+
return {
|
|
112
|
+
"id": self.id,
|
|
113
|
+
"statement": self.statement,
|
|
114
|
+
"domain": self.domain,
|
|
115
|
+
"severity_if_disproved": self.severity_if_disproved,
|
|
116
|
+
"verdict": self.verdict.value,
|
|
117
|
+
"evidence": [e.to_dict() for e in self.evidence],
|
|
118
|
+
"repairs": self.repairs,
|
|
119
|
+
"updated_at": self.updated_at,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def from_dict(cls, d: dict) -> Claim:
|
|
124
|
+
c = cls(
|
|
125
|
+
id=d.get("id", "?"),
|
|
126
|
+
statement=d.get("statement", ""),
|
|
127
|
+
domain=d.get("domain", "general"),
|
|
128
|
+
severity_if_disproved=d.get("severity_if_disproved", "high"),
|
|
129
|
+
verdict=Verdict(d.get("verdict", "unproven")),
|
|
130
|
+
updated_at=d.get("updated_at", ""),
|
|
131
|
+
)
|
|
132
|
+
c.evidence = [Evidence.from_dict(e) for e in d.get("evidence", [])]
|
|
133
|
+
c.repairs = list(d.get("repairs", []))
|
|
134
|
+
return c
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class AssuranceGraph:
|
|
139
|
+
"""The persistent collection of claims for one project."""
|
|
140
|
+
|
|
141
|
+
claims: dict[str, Claim] = field(default_factory=dict)
|
|
142
|
+
|
|
143
|
+
def upsert_claim(
|
|
144
|
+
self,
|
|
145
|
+
claim_id: str,
|
|
146
|
+
statement: str,
|
|
147
|
+
domain: str = "general",
|
|
148
|
+
severity_if_disproved: str = "high",
|
|
149
|
+
) -> Claim:
|
|
150
|
+
"""Get-or-create a claim; preserves existing evidence across runs."""
|
|
151
|
+
if claim_id not in self.claims:
|
|
152
|
+
self.claims[claim_id] = Claim(
|
|
153
|
+
id=claim_id,
|
|
154
|
+
statement=statement,
|
|
155
|
+
domain=domain,
|
|
156
|
+
severity_if_disproved=severity_if_disproved,
|
|
157
|
+
)
|
|
158
|
+
return self.claims[claim_id]
|
|
159
|
+
|
|
160
|
+
def attach_evidence(self, claim_id: str, evidence: Evidence) -> Claim:
|
|
161
|
+
claim = self.claims[claim_id] # KeyError = programming error upstream
|
|
162
|
+
claim.add_evidence(evidence)
|
|
163
|
+
return claim
|
|
164
|
+
|
|
165
|
+
# ── Reporting ───────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
def coverage(self) -> dict[str, Any]:
|
|
168
|
+
"""Assurance status summary. Never says 'secure' — says what's proved."""
|
|
169
|
+
total = len(self.claims)
|
|
170
|
+
by_verdict: dict[str, int] = {}
|
|
171
|
+
by_domain: dict[str, dict[str, int]] = {}
|
|
172
|
+
for c in self.claims.values():
|
|
173
|
+
by_verdict[c.verdict.value] = by_verdict.get(c.verdict.value, 0) + 1
|
|
174
|
+
dom = by_domain.setdefault(c.domain, {"proved": 0, "total": 0})
|
|
175
|
+
dom["total"] += 1
|
|
176
|
+
if c.verdict == Verdict.PROVED:
|
|
177
|
+
dom["proved"] += 1
|
|
178
|
+
|
|
179
|
+
disproved = [c for c in self.claims.values() if c.verdict == Verdict.DISPROVED]
|
|
180
|
+
return {
|
|
181
|
+
"claims_total": total,
|
|
182
|
+
"by_verdict": by_verdict,
|
|
183
|
+
"by_domain": by_domain,
|
|
184
|
+
"disproved_claims": [c.to_dict() for c in disproved],
|
|
185
|
+
"statement": (
|
|
186
|
+
f"{by_verdict.get('proved', 0)}/{total} properties proved; "
|
|
187
|
+
f"{len(disproved)} violated"
|
|
188
|
+
if total
|
|
189
|
+
else "no properties established yet"
|
|
190
|
+
),
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# ── Diff ─────────────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
def diff(self, other: AssuranceGraph) -> dict[str, Any]:
|
|
196
|
+
"""Compute difference between two assurance graphs.
|
|
197
|
+
|
|
198
|
+
Returns a dict with:
|
|
199
|
+
- added: claims in ``other`` but not in ``self``
|
|
200
|
+
- removed: claims in ``self`` but not in ``other``
|
|
201
|
+
- changed: claims present in both but with different verdicts or evidence
|
|
202
|
+
- unchanged: claim IDs present in both with same verdict and evidence count
|
|
203
|
+
"""
|
|
204
|
+
self_ids = set(self.claims.keys())
|
|
205
|
+
other_ids = set(other.claims.keys())
|
|
206
|
+
|
|
207
|
+
added_ids = other_ids - self_ids
|
|
208
|
+
removed_ids = self_ids - other_ids
|
|
209
|
+
common_ids = self_ids & other_ids
|
|
210
|
+
|
|
211
|
+
added = {cid: other.claims[cid].to_dict() for cid in added_ids}
|
|
212
|
+
removed = {cid: self.claims[cid].to_dict() for cid in removed_ids}
|
|
213
|
+
|
|
214
|
+
changed = {}
|
|
215
|
+
unchanged = []
|
|
216
|
+
for cid in common_ids:
|
|
217
|
+
c1 = self.claims[cid]
|
|
218
|
+
c2 = other.claims[cid]
|
|
219
|
+
if c1.verdict != c2.verdict or len(c1.evidence) != len(c2.evidence):
|
|
220
|
+
changed[cid] = {
|
|
221
|
+
"self": c1.to_dict(),
|
|
222
|
+
"other": c2.to_dict(),
|
|
223
|
+
}
|
|
224
|
+
else:
|
|
225
|
+
unchanged.append(cid)
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
"added": added,
|
|
229
|
+
"removed": removed,
|
|
230
|
+
"changed": changed,
|
|
231
|
+
"unchanged": unchanged,
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
# ── Persistence ─────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
def save(self, root: Path) -> Path:
|
|
237
|
+
path = root / ASSURANCE_FILE
|
|
238
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
239
|
+
payload = {
|
|
240
|
+
"version": 1,
|
|
241
|
+
"claims": [c.to_dict() for c in self.claims.values()],
|
|
242
|
+
}
|
|
243
|
+
tmp = path.with_suffix(".json.tmp")
|
|
244
|
+
tmp.write_text(json_dumps(payload), encoding="utf-8")
|
|
245
|
+
tmp.replace(path)
|
|
246
|
+
# Record coverage snapshot for trend line
|
|
247
|
+
self._record_coverage_snapshot(root)
|
|
248
|
+
return path
|
|
249
|
+
|
|
250
|
+
@classmethod
|
|
251
|
+
def load(cls, root: Path) -> AssuranceGraph:
|
|
252
|
+
path = root / ASSURANCE_FILE
|
|
253
|
+
g = cls()
|
|
254
|
+
if not path.is_file():
|
|
255
|
+
return g
|
|
256
|
+
try:
|
|
257
|
+
import json
|
|
258
|
+
|
|
259
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
260
|
+
for cd in data.get("claims", []):
|
|
261
|
+
try:
|
|
262
|
+
c = Claim.from_dict(cd)
|
|
263
|
+
g.claims[c.id] = c
|
|
264
|
+
except Exception as e: # noqa: BLE001 — skip corrupt entries
|
|
265
|
+
_log.warning("skipping corrupt assurance claim: %s", e)
|
|
266
|
+
except Exception as e: # noqa: BLE001 — corrupt file = fresh graph
|
|
267
|
+
_log.warning("failed to load assurance graph: %s", e)
|
|
268
|
+
return g
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _record_coverage_snapshot(self, root: Path) -> None:
|
|
272
|
+
"""Append a coverage snapshot to the history file for trend analysis."""
|
|
273
|
+
try:
|
|
274
|
+
import json
|
|
275
|
+
|
|
276
|
+
history_path = root / COVERAGE_HISTORY_FILE
|
|
277
|
+
history_path.parent.mkdir(parents=True, exist_ok=True)
|
|
278
|
+
|
|
279
|
+
# Load existing history
|
|
280
|
+
history: list[dict] = []
|
|
281
|
+
if history_path.is_file():
|
|
282
|
+
try:
|
|
283
|
+
history = json.loads(history_path.read_text(encoding="utf-8"))
|
|
284
|
+
except Exception:
|
|
285
|
+
history = []
|
|
286
|
+
|
|
287
|
+
# Compute current snapshot
|
|
288
|
+
cov = self.coverage()
|
|
289
|
+
snapshot = {
|
|
290
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
291
|
+
"overall_pct": round(
|
|
292
|
+
cov["by_verdict"].get("proved", 0) / cov["claims_total"] * 100
|
|
293
|
+
if cov["claims_total"]
|
|
294
|
+
else 0
|
|
295
|
+
),
|
|
296
|
+
"claims_total": cov["claims_total"],
|
|
297
|
+
"proved": cov["by_verdict"].get("proved", 0),
|
|
298
|
+
"disproved": cov["by_verdict"].get("disproved", 0),
|
|
299
|
+
"unproven": cov["by_verdict"].get("unproven", 0),
|
|
300
|
+
"not_proved": cov["by_verdict"].get("not_proved", 0),
|
|
301
|
+
"by_domain": {},
|
|
302
|
+
}
|
|
303
|
+
# Per-domain breakdown
|
|
304
|
+
for dom, counts in cov.get("by_domain", {}).items():
|
|
305
|
+
dom_total = counts.get("total", 0)
|
|
306
|
+
dom_proved = counts.get("proved", 0)
|
|
307
|
+
snapshot["by_domain"][dom] = {
|
|
308
|
+
"proved": dom_proved,
|
|
309
|
+
"total": dom_total,
|
|
310
|
+
"pct": round(dom_proved / dom_total * 100) if dom_total else 0,
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
history.append(snapshot)
|
|
314
|
+
|
|
315
|
+
# Keep last 500 snapshots to avoid unbounded growth
|
|
316
|
+
if len(history) > 500:
|
|
317
|
+
history = history[-500:]
|
|
318
|
+
|
|
319
|
+
tmp = history_path.with_suffix(".json.tmp")
|
|
320
|
+
tmp.write_text(json_dumps(history), encoding="utf-8")
|
|
321
|
+
tmp.replace(history_path)
|
|
322
|
+
except Exception as e: # noqa: BLE001 — best-effort
|
|
323
|
+
_log.debug("coverage snapshot failed: %s", e)
|
|
324
|
+
|
|
325
|
+
@staticmethod
|
|
326
|
+
def load_coverage_history(root: Path) -> list[dict]:
|
|
327
|
+
"""Load the coverage history for trend analysis."""
|
|
328
|
+
import json
|
|
329
|
+
|
|
330
|
+
history_path = root / COVERAGE_HISTORY_FILE
|
|
331
|
+
if not history_path.is_file():
|
|
332
|
+
return []
|
|
333
|
+
try:
|
|
334
|
+
return json.loads(history_path.read_text(encoding="utf-8"))
|
|
335
|
+
except Exception:
|
|
336
|
+
return []
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def json_dumps(payload: Any) -> str:
|
|
340
|
+
import json
|
|
341
|
+
|
|
342
|
+
return json.dumps(payload, indent=2, default=str)
|