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,172 @@
|
|
|
1
|
+
"""FaultInjector — identify fault injection points in code.
|
|
2
|
+
|
|
3
|
+
Scans for patterns that should handle failures gracefully but don't
|
|
4
|
+
(network calls without try/except, DB writes without rollback, file I/O
|
|
5
|
+
without close/finally, etc.).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from patchi.core.assurance.graph import AssuranceGraph
|
|
15
|
+
|
|
16
|
+
_log = logging.getLogger("patchi.reliability.fault_injection")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class FaultInjectionPoint:
|
|
21
|
+
"""A location in code where fault injection would be valuable."""
|
|
22
|
+
|
|
23
|
+
file: str
|
|
24
|
+
line: int
|
|
25
|
+
fault_type: str # "network", "disk", "database", "memory", "cpu"
|
|
26
|
+
pattern: str # what was detected
|
|
27
|
+
recovery_strategy: str # what should be there
|
|
28
|
+
severity: str # "high", "medium", "low"
|
|
29
|
+
|
|
30
|
+
def to_dict(self) -> dict:
|
|
31
|
+
return {
|
|
32
|
+
"file": self.file,
|
|
33
|
+
"line": self.line,
|
|
34
|
+
"fault_type": self.fault_type,
|
|
35
|
+
"pattern": self.pattern,
|
|
36
|
+
"recovery_strategy": self.recovery_strategy,
|
|
37
|
+
"severity": self.severity,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Patterns that indicate missing fault handling
|
|
42
|
+
_FAULT_PATTERNS: list[tuple[str, str, str, str, str]] = [
|
|
43
|
+
# (regex pattern, fault_type, description, recovery, severity)
|
|
44
|
+
(
|
|
45
|
+
r"\.read\(|\.write\(",
|
|
46
|
+
"disk",
|
|
47
|
+
"File I/O without try/except",
|
|
48
|
+
"Add try/except with IOError handling",
|
|
49
|
+
"medium",
|
|
50
|
+
),
|
|
51
|
+
(
|
|
52
|
+
r"requests\.(get|post|put|delete)\(",
|
|
53
|
+
"network",
|
|
54
|
+
"HTTP request without timeout/retry",
|
|
55
|
+
"Add timeout and retry with backoff",
|
|
56
|
+
"high",
|
|
57
|
+
),
|
|
58
|
+
(
|
|
59
|
+
r"subprocess\.run\(|subprocess\.Popen\(",
|
|
60
|
+
"network",
|
|
61
|
+
"Subprocess without error handling",
|
|
62
|
+
"Add try/except and check returncode",
|
|
63
|
+
"medium",
|
|
64
|
+
),
|
|
65
|
+
(
|
|
66
|
+
r"\.execute\(|\.commit\(",
|
|
67
|
+
"database",
|
|
68
|
+
"DB operation without rollback",
|
|
69
|
+
"Add try/except with rollback in finally",
|
|
70
|
+
"high",
|
|
71
|
+
),
|
|
72
|
+
(
|
|
73
|
+
r"json\.loads\(",
|
|
74
|
+
"disk",
|
|
75
|
+
"JSON parse without try/except",
|
|
76
|
+
"Add try/except for JSONDecodeError",
|
|
77
|
+
"medium",
|
|
78
|
+
),
|
|
79
|
+
(
|
|
80
|
+
r"open\(",
|
|
81
|
+
"disk",
|
|
82
|
+
"File open without context manager",
|
|
83
|
+
"Use with-statement for guaranteed close",
|
|
84
|
+
"medium",
|
|
85
|
+
),
|
|
86
|
+
(
|
|
87
|
+
r"pickle\.loads?\(",
|
|
88
|
+
"memory",
|
|
89
|
+
"Unsafe deserialization",
|
|
90
|
+
"Use safe deserialization or json",
|
|
91
|
+
"high",
|
|
92
|
+
),
|
|
93
|
+
(
|
|
94
|
+
r"eval\(|exec\(",
|
|
95
|
+
"memory",
|
|
96
|
+
"Dynamic code execution",
|
|
97
|
+
"Replace with safe alternatives",
|
|
98
|
+
"high",
|
|
99
|
+
),
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class FaultInjector:
|
|
104
|
+
"""Analyze code for missing fault handling."""
|
|
105
|
+
|
|
106
|
+
def __init__(self, root: Path, graph: AssuranceGraph | None = None):
|
|
107
|
+
self._root = root
|
|
108
|
+
self._graph = graph
|
|
109
|
+
|
|
110
|
+
def scan_file(self, file_path: Path) -> list[FaultInjectionPoint]:
|
|
111
|
+
"""Scan a single file for fault injection points."""
|
|
112
|
+
results: list[FaultInjectionPoint] = []
|
|
113
|
+
try:
|
|
114
|
+
content = file_path.read_text(encoding="utf-8", errors="ignore")
|
|
115
|
+
except OSError:
|
|
116
|
+
return results
|
|
117
|
+
|
|
118
|
+
lines = content.splitlines()
|
|
119
|
+
rel_path = (
|
|
120
|
+
str(file_path.relative_to(self._root))
|
|
121
|
+
if self._root in file_path.parents
|
|
122
|
+
else str(file_path)
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
for line_num, line in enumerate(lines, 1):
|
|
126
|
+
stripped = line.strip()
|
|
127
|
+
if stripped.startswith("#") or stripped.startswith("//"):
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
for pattern, fault_type, description, recovery, severity in _FAULT_PATTERNS:
|
|
131
|
+
import re
|
|
132
|
+
|
|
133
|
+
if re.search(pattern, line):
|
|
134
|
+
# Check if already has try/except nearby
|
|
135
|
+
if not self._has_error_handling(lines, line_num - 1):
|
|
136
|
+
results.append(
|
|
137
|
+
FaultInjectionPoint(
|
|
138
|
+
file=rel_path,
|
|
139
|
+
line=line_num,
|
|
140
|
+
fault_type=fault_type,
|
|
141
|
+
pattern=description,
|
|
142
|
+
recovery_strategy=recovery,
|
|
143
|
+
severity=severity,
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
break # one finding per line
|
|
147
|
+
|
|
148
|
+
return results
|
|
149
|
+
|
|
150
|
+
def scan_project(self, max_files: int = 200) -> list[FaultInjectionPoint]:
|
|
151
|
+
"""Scan the project for fault injection points."""
|
|
152
|
+
results: list[FaultInjectionPoint] = []
|
|
153
|
+
count = 0
|
|
154
|
+
|
|
155
|
+
for py_file in sorted(self._root.rglob("*.py")):
|
|
156
|
+
if count >= max_files:
|
|
157
|
+
break
|
|
158
|
+
if ".patchi" in str(py_file) or "__pycache__" in str(py_file):
|
|
159
|
+
continue
|
|
160
|
+
if ".venv" in str(py_file) or "node_modules" in str(py_file):
|
|
161
|
+
continue
|
|
162
|
+
results.extend(self.scan_file(py_file))
|
|
163
|
+
count += 1
|
|
164
|
+
|
|
165
|
+
return results
|
|
166
|
+
|
|
167
|
+
def _has_error_handling(self, lines: list[str], line_idx: int) -> bool:
|
|
168
|
+
"""Check if a line is inside a try/except block (±5 lines)."""
|
|
169
|
+
start = max(0, line_idx - 5)
|
|
170
|
+
end = min(len(lines), line_idx + 6)
|
|
171
|
+
block = "\n".join(lines[start:end])
|
|
172
|
+
return "try:" in block and ("except" in block or "finally:" in block)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""IdempotencyAnalyzer — find operations that should be idempotent but aren't.
|
|
2
|
+
|
|
3
|
+
Detects side effects that happen on every call (file writes without checks,
|
|
4
|
+
DB inserts without upserts, network calls without deduplication).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
_log = logging.getLogger("patchi.reliability.idempotency")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class IdempotencyIssue:
|
|
19
|
+
"""An operation that should be idempotent but has side effects."""
|
|
20
|
+
|
|
21
|
+
file: str
|
|
22
|
+
line: int
|
|
23
|
+
issue_type: str # "append_without_check", "insert_without_upsert", "write_without_guard"
|
|
24
|
+
description: str
|
|
25
|
+
fix_suggestion: str
|
|
26
|
+
|
|
27
|
+
def to_dict(self) -> dict:
|
|
28
|
+
return {
|
|
29
|
+
"file": self.file,
|
|
30
|
+
"line": self.line,
|
|
31
|
+
"issue_type": self.issue_type,
|
|
32
|
+
"description": self.description,
|
|
33
|
+
"fix_suggestion": self.fix_suggestion,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Patterns indicating non-idempotent operations
|
|
38
|
+
_IDEMPOTENCY_PATTERNS: list[tuple[str, str, str, str]] = [
|
|
39
|
+
(
|
|
40
|
+
r"\.append\(",
|
|
41
|
+
"append_without_check",
|
|
42
|
+
"List append without dedup check",
|
|
43
|
+
"Check if item exists before appending",
|
|
44
|
+
),
|
|
45
|
+
(
|
|
46
|
+
r"conn\.execute\(['\"]INSERT",
|
|
47
|
+
"insert_without_upsert",
|
|
48
|
+
"SQL INSERT without upsert",
|
|
49
|
+
"Use INSERT OR REPLACE or check existence first",
|
|
50
|
+
),
|
|
51
|
+
(
|
|
52
|
+
r"\.write_text\(",
|
|
53
|
+
"write_without_guard",
|
|
54
|
+
"File write without content comparison",
|
|
55
|
+
"Compare content before writing to avoid unnecessary I/O",
|
|
56
|
+
),
|
|
57
|
+
(
|
|
58
|
+
r"json\.dumps.*\.write_text\(",
|
|
59
|
+
"write_without_guard",
|
|
60
|
+
"JSON write without read-compare",
|
|
61
|
+
"Read existing content and compare before writing",
|
|
62
|
+
),
|
|
63
|
+
(
|
|
64
|
+
r"\.add\(",
|
|
65
|
+
"set_add_without_check",
|
|
66
|
+
"Set add without membership check",
|
|
67
|
+
"Use set.add() (already idempotent) or document non-idempotent semantics",
|
|
68
|
+
),
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class IdempotencyAnalyzer:
|
|
73
|
+
"""Analyze code for idempotency issues."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, root: Path):
|
|
76
|
+
self._root = root
|
|
77
|
+
|
|
78
|
+
def scan_file(self, file_path: Path) -> list[IdempotencyIssue]:
|
|
79
|
+
"""Scan a single file for idempotency issues."""
|
|
80
|
+
results: list[IdempotencyIssue] = []
|
|
81
|
+
try:
|
|
82
|
+
content = file_path.read_text(encoding="utf-8", errors="ignore")
|
|
83
|
+
except OSError:
|
|
84
|
+
return results
|
|
85
|
+
|
|
86
|
+
lines = content.splitlines()
|
|
87
|
+
rel_path = (
|
|
88
|
+
str(file_path.relative_to(self._root))
|
|
89
|
+
if self._root in file_path.parents
|
|
90
|
+
else str(file_path)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
for line_num, line in enumerate(lines, 1):
|
|
94
|
+
stripped = line.strip()
|
|
95
|
+
if stripped.startswith("#") or stripped.startswith("//"):
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
for pattern, issue_type, description, fix in _IDEMPOTENCY_PATTERNS:
|
|
99
|
+
if re.search(pattern, line):
|
|
100
|
+
results.append(
|
|
101
|
+
IdempotencyIssue(
|
|
102
|
+
file=rel_path,
|
|
103
|
+
line=line_num,
|
|
104
|
+
issue_type=issue_type,
|
|
105
|
+
description=description,
|
|
106
|
+
fix_suggestion=fix,
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
break # one per line
|
|
110
|
+
|
|
111
|
+
return results
|
|
112
|
+
|
|
113
|
+
def scan_project(self, max_files: int = 200) -> list[IdempotencyIssue]:
|
|
114
|
+
"""Scan the project for idempotency issues."""
|
|
115
|
+
results: list[IdempotencyIssue] = []
|
|
116
|
+
count = 0
|
|
117
|
+
|
|
118
|
+
for py_file in sorted(self._root.rglob("*.py")):
|
|
119
|
+
if count >= max_files:
|
|
120
|
+
break
|
|
121
|
+
if ".patchi" in str(py_file) or "__pycache__" in str(py_file):
|
|
122
|
+
continue
|
|
123
|
+
if ".venv" in str(py_file) or "node_modules" in str(py_file):
|
|
124
|
+
continue
|
|
125
|
+
results.extend(self.scan_file(py_file))
|
|
126
|
+
count += 1
|
|
127
|
+
|
|
128
|
+
return results
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""RecoveryAnalyzer — analyze data flows for recovery patterns.
|
|
2
|
+
|
|
3
|
+
Checks that critical data paths have circuit breakers, retry logic,
|
|
4
|
+
and graceful degradation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from patchi.core.assurance.graph import AssuranceGraph
|
|
15
|
+
|
|
16
|
+
_log = logging.getLogger("patchi.reliability.recovery")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class RecoveryGap:
|
|
21
|
+
"""A data flow missing a recovery pattern."""
|
|
22
|
+
|
|
23
|
+
file: str
|
|
24
|
+
flow: str # "source → sink"
|
|
25
|
+
missing_pattern: str # "circuit_breaker", "retry", "fallback", "timeout"
|
|
26
|
+
severity: str
|
|
27
|
+
recommendation: str
|
|
28
|
+
|
|
29
|
+
def to_dict(self) -> dict:
|
|
30
|
+
return {
|
|
31
|
+
"file": self.file,
|
|
32
|
+
"flow": self.flow,
|
|
33
|
+
"missing_pattern": self.missing_pattern,
|
|
34
|
+
"severity": self.severity,
|
|
35
|
+
"recommendation": self.recommendation,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class RecoveryAnalyzer:
|
|
40
|
+
"""Analyze data flows for missing recovery patterns."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, root: Path, graph: AssuranceGraph | None = None):
|
|
43
|
+
self._root = root
|
|
44
|
+
self._graph = graph
|
|
45
|
+
|
|
46
|
+
def analyze(self, max_gaps: int = 30) -> list[RecoveryGap]:
|
|
47
|
+
"""Analyze the project for missing recovery patterns."""
|
|
48
|
+
gaps: list[RecoveryGap] = []
|
|
49
|
+
|
|
50
|
+
# Check data flows from the graph
|
|
51
|
+
if self._graph:
|
|
52
|
+
gaps.extend(self._graph_flow_analysis())
|
|
53
|
+
|
|
54
|
+
# Check common patterns in code
|
|
55
|
+
gaps.extend(self._code_pattern_analysis())
|
|
56
|
+
|
|
57
|
+
return gaps[:max_gaps]
|
|
58
|
+
|
|
59
|
+
def _graph_flow_analysis(self) -> list[RecoveryGap]:
|
|
60
|
+
"""Analyze data flows from the assurance graph for recovery gaps."""
|
|
61
|
+
gaps: list[RecoveryGap] = []
|
|
62
|
+
|
|
63
|
+
for claim in self._graph.claims.values():
|
|
64
|
+
for ev in claim.evidence:
|
|
65
|
+
if not ev.supports:
|
|
66
|
+
continue
|
|
67
|
+
artifact = ev.artifact
|
|
68
|
+
source = artifact.get("source", "")
|
|
69
|
+
sink = artifact.get("sink", "")
|
|
70
|
+
|
|
71
|
+
if not source or not sink:
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
# External sinks need circuit breakers
|
|
75
|
+
if any(kw in sink.lower() for kw in ["api", "http", "network", "remote"]):
|
|
76
|
+
gaps.append(
|
|
77
|
+
RecoveryGap(
|
|
78
|
+
file=source,
|
|
79
|
+
flow=f"{source} → {sink}",
|
|
80
|
+
missing_pattern="circuit_breaker",
|
|
81
|
+
severity="high",
|
|
82
|
+
recommendation=f"Add circuit breaker between {source} and external {sink}",
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Database sinks need retry with backoff
|
|
87
|
+
if any(kw in sink.lower() for kw in ["db", "database", "sql", "redis"]):
|
|
88
|
+
gaps.append(
|
|
89
|
+
RecoveryGap(
|
|
90
|
+
file=source,
|
|
91
|
+
flow=f"{source} → {sink}",
|
|
92
|
+
missing_pattern="retry",
|
|
93
|
+
severity="medium",
|
|
94
|
+
recommendation=f"Add retry with exponential backoff for {sink} operations",
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return gaps
|
|
99
|
+
|
|
100
|
+
def _code_pattern_analysis(self) -> list[RecoveryGap]:
|
|
101
|
+
"""Scan code for missing recovery patterns."""
|
|
102
|
+
gaps: list[RecoveryGap] = []
|
|
103
|
+
|
|
104
|
+
for py_file in sorted(self._root.rglob("*.py")):
|
|
105
|
+
if ".patchi" in str(py_file) or "__pycache__" in str(py_file):
|
|
106
|
+
continue
|
|
107
|
+
if ".venv" in str(py_file):
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
content = py_file.read_text(encoding="utf-8", errors="ignore")
|
|
112
|
+
except OSError:
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
rel = (
|
|
116
|
+
str(py_file.relative_to(self._root))
|
|
117
|
+
if self._root in py_file.parents
|
|
118
|
+
else str(py_file)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Check for network calls without retry
|
|
122
|
+
if re.search(r"requests\.(get|post|put|delete)\(", content):
|
|
123
|
+
if "retry" not in content.lower() and "tenacity" not in content.lower():
|
|
124
|
+
gaps.append(
|
|
125
|
+
RecoveryGap(
|
|
126
|
+
file=rel,
|
|
127
|
+
flow="HTTP request",
|
|
128
|
+
missing_pattern="retry",
|
|
129
|
+
severity="medium",
|
|
130
|
+
recommendation="Add retry with backoff for HTTP requests",
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Check for DB operations without rollback
|
|
135
|
+
if re.search(r"\.execute\(|\.commit\(", content):
|
|
136
|
+
if "rollback" not in content.lower():
|
|
137
|
+
gaps.append(
|
|
138
|
+
RecoveryGap(
|
|
139
|
+
file=rel,
|
|
140
|
+
flow="Database operation",
|
|
141
|
+
missing_pattern="rollback",
|
|
142
|
+
severity="high",
|
|
143
|
+
recommendation="Add try/except with rollback for database operations",
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return gaps
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Runtime Tracer — AST-guided execution tracing for Python target code.
|
|
3
|
+
|
|
4
|
+
Runs a Python file in a fresh subprocess under a sys.settrace-based tracer
|
|
5
|
+
that records:
|
|
6
|
+
- every function call/return (name, file, line, duration)
|
|
7
|
+
- raised exceptions with full argument snapshot
|
|
8
|
+
- total wall time
|
|
9
|
+
|
|
10
|
+
Bounded by design: wall-clock timeout kills the child; trace buffer is
|
|
11
|
+
capped; output is a single JSON blob on a pipe — never interleaved with
|
|
12
|
+
program stdout.
|
|
13
|
+
|
|
14
|
+
This closes the gap between "scanner found it" and "reproduced it":
|
|
15
|
+
RuntimeCrashScanner findings can be fed here for a reproduction trace.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import logging
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
import tempfile
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
_log = logging.getLogger("patchi.core.runtime.tracer")
|
|
29
|
+
|
|
30
|
+
# The child-side tracer source. Injected as a file so we don't fight
|
|
31
|
+
# shell quoting; kept as one string so it stays reviewable.
|
|
32
|
+
_CHILD_TRACER = r"""
|
|
33
|
+
import json
|
|
34
|
+
import sys
|
|
35
|
+
import time
|
|
36
|
+
import tracemalloc
|
|
37
|
+
|
|
38
|
+
MAX_CALLS = int(sys.argv[1]) if len(sys.argv) > 1 else 5000
|
|
39
|
+
|
|
40
|
+
calls = [] # completed call records
|
|
41
|
+
exceptions = [] # captured exceptions
|
|
42
|
+
order = [0]
|
|
43
|
+
tracemalloc.start()
|
|
44
|
+
|
|
45
|
+
def _tracer(frame, event, arg):
|
|
46
|
+
if len(calls) >= MAX_CALLS and event != "exception":
|
|
47
|
+
return None
|
|
48
|
+
co = frame.f_code
|
|
49
|
+
rec = {
|
|
50
|
+
"fn": co.co_name,
|
|
51
|
+
"file": co.co_filename,
|
|
52
|
+
"line": co.co_firstlineno,
|
|
53
|
+
}
|
|
54
|
+
if event == "call":
|
|
55
|
+
order[0] += 1
|
|
56
|
+
rec["seq"] = order[0]
|
|
57
|
+
rec["event"] = "call"
|
|
58
|
+
calls.append(rec)
|
|
59
|
+
return _tracer
|
|
60
|
+
if event == "return":
|
|
61
|
+
rec["event"] = "return"
|
|
62
|
+
calls.append(rec)
|
|
63
|
+
return None
|
|
64
|
+
if event == "exception":
|
|
65
|
+
exc_type, exc_val, _ = arg
|
|
66
|
+
tb = exc_val.__traceback__
|
|
67
|
+
exceptions.append({
|
|
68
|
+
"type": exc_type.__name__,
|
|
69
|
+
"message": str(exc_val)[:300],
|
|
70
|
+
"file": tb.tb_frame.f_code.co_filename if tb else co.co_filename,
|
|
71
|
+
"line": tb.tb_lineno if tb else 0,
|
|
72
|
+
"locals": {
|
|
73
|
+
k: repr(v)[:120]
|
|
74
|
+
for k, v in list(frame.f_locals.items())[:15]
|
|
75
|
+
if not k.startswith("__")
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
return _tracer
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def main():
|
|
83
|
+
target = sys.argv[2]
|
|
84
|
+
out_path = sys.argv[3]
|
|
85
|
+
sys.argv = [target] + sys.argv[4:]
|
|
86
|
+
sys.settrace(_tracer)
|
|
87
|
+
t0 = time.perf_counter()
|
|
88
|
+
exit_code = 0
|
|
89
|
+
try:
|
|
90
|
+
with open(target, encoding="utf-8") as fh:
|
|
91
|
+
code = compile(fh.read(), target, "exec")
|
|
92
|
+
g = {"__name__": "__main__", "__file__": target}
|
|
93
|
+
exec(code, g)
|
|
94
|
+
except SystemExit as e:
|
|
95
|
+
exit_code = e.code if isinstance(e.code, int) else 0
|
|
96
|
+
except BaseException as e:
|
|
97
|
+
exit_code = 1
|
|
98
|
+
exceptions.append({
|
|
99
|
+
"type": type(e).__name__,
|
|
100
|
+
"message": str(e)[:300],
|
|
101
|
+
"file": target,
|
|
102
|
+
"line": getattr(getattr(e, "__traceback__", None), "tb_lineno", 0),
|
|
103
|
+
"locals": {},
|
|
104
|
+
"fatal": True,
|
|
105
|
+
})
|
|
106
|
+
finally:
|
|
107
|
+
sys.settrace(None)
|
|
108
|
+
duration = time.perf_counter() - t0
|
|
109
|
+
current, peak = tracemalloc.get_traced_memory()
|
|
110
|
+
out = {
|
|
111
|
+
"exit_code": exit_code,
|
|
112
|
+
"duration_s": round(duration, 4),
|
|
113
|
+
"peak_memory_bytes": peak,
|
|
114
|
+
"call_count": len(calls),
|
|
115
|
+
"truncated": len(calls) >= MAX_CALLS,
|
|
116
|
+
"calls": calls[-200:], # tail: most recent activity
|
|
117
|
+
"exceptions": exceptions,
|
|
118
|
+
}
|
|
119
|
+
# Dedicated file, NOT stdout: the traced program owns stdout.
|
|
120
|
+
with open(out_path, "w", encoding="utf-8") as fh:
|
|
121
|
+
json.dump(out, fh, default=str)
|
|
122
|
+
|
|
123
|
+
main()
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass
|
|
128
|
+
class TraceReport:
|
|
129
|
+
"""Structured result of one traced run."""
|
|
130
|
+
|
|
131
|
+
exit_code: int = -1
|
|
132
|
+
duration_s: float = 0.0
|
|
133
|
+
peak_memory_bytes: int = 0
|
|
134
|
+
call_count: int = 0
|
|
135
|
+
truncated: bool = False
|
|
136
|
+
calls: list[dict] = field(default_factory=list)
|
|
137
|
+
exceptions: list[dict] = field(default_factory=list)
|
|
138
|
+
error: str = ""
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def crashed(self) -> bool:
|
|
142
|
+
return bool(self.exceptions) or self.exit_code not in (0, None)
|
|
143
|
+
|
|
144
|
+
def hot_functions(self, top_n: int = 10) -> list[tuple[str, int]]:
|
|
145
|
+
"""Most-called functions — N+1 query / loop-hotspot candidates."""
|
|
146
|
+
counts: dict[str, int] = {}
|
|
147
|
+
for c in self.calls:
|
|
148
|
+
if c.get("event") == "call":
|
|
149
|
+
key = f"{c['fn']} ({Path(c['file']).name}:{c['line']})"
|
|
150
|
+
counts[key] = counts.get(key, 0) + 1
|
|
151
|
+
return sorted(counts.items(), key=lambda kv: -kv[1])[:top_n]
|
|
152
|
+
|
|
153
|
+
def to_dict(self) -> dict:
|
|
154
|
+
return {
|
|
155
|
+
"exit_code": self.exit_code,
|
|
156
|
+
"duration_s": self.duration_s,
|
|
157
|
+
"peak_memory_bytes": self.peak_memory_bytes,
|
|
158
|
+
"call_count": self.call_count,
|
|
159
|
+
"truncated": self.truncated,
|
|
160
|
+
"crashed": self.crashed,
|
|
161
|
+
"hot_functions": [{"fn": fn, "calls": n} for fn, n in self.hot_functions()],
|
|
162
|
+
"exceptions": self.exceptions,
|
|
163
|
+
"error": self.error,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def trace_file(
|
|
168
|
+
script: str | Path,
|
|
169
|
+
timeout: float = 60.0,
|
|
170
|
+
max_calls: int = 5000,
|
|
171
|
+
args: list[str] | None = None,
|
|
172
|
+
) -> TraceReport:
|
|
173
|
+
"""
|
|
174
|
+
Run *script* under the tracer; returns a TraceReport.
|
|
175
|
+
|
|
176
|
+
Never raises for program behavior — crashes are captured data. Raises
|
|
177
|
+
only for infrastructure failures (missing file, bad extension).
|
|
178
|
+
"""
|
|
179
|
+
path = Path(script).resolve()
|
|
180
|
+
if path.suffix.lower() != ".py":
|
|
181
|
+
raise ValueError(f"tracer supports .py only, got {path.suffix}")
|
|
182
|
+
if not path.is_file():
|
|
183
|
+
raise FileNotFoundError(str(path))
|
|
184
|
+
|
|
185
|
+
with tempfile.TemporaryDirectory() as td:
|
|
186
|
+
tracer_path = Path(td) / "_patchi_tracer.py"
|
|
187
|
+
tracer_path.write_text(_CHILD_TRACER, encoding="utf-8")
|
|
188
|
+
out_path = Path(td) / "trace_out.json"
|
|
189
|
+
|
|
190
|
+
cmd = [
|
|
191
|
+
sys.executable,
|
|
192
|
+
str(tracer_path),
|
|
193
|
+
str(max_calls),
|
|
194
|
+
str(path),
|
|
195
|
+
str(out_path),
|
|
196
|
+
*(args or []),
|
|
197
|
+
]
|
|
198
|
+
try:
|
|
199
|
+
proc = subprocess.run(
|
|
200
|
+
cmd,
|
|
201
|
+
capture_output=True,
|
|
202
|
+
timeout=timeout,
|
|
203
|
+
cwd=str(path.parent),
|
|
204
|
+
)
|
|
205
|
+
except subprocess.TimeoutExpired:
|
|
206
|
+
return TraceReport(error=f"timed out after {timeout}s")
|
|
207
|
+
|
|
208
|
+
if not out_path.is_file():
|
|
209
|
+
return TraceReport(
|
|
210
|
+
error=f"tracer failed rc={proc.returncode}: {proc.stderr.decode(errors='replace')[:300]}"
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
raw = json.loads(out_path.read_text(encoding="utf-8", errors="replace"))
|
|
215
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
|
216
|
+
return TraceReport(error=f"unparseable trace output: {e}")
|
|
217
|
+
|
|
218
|
+
report = TraceReport(
|
|
219
|
+
exit_code=raw.get("exit_code", -1),
|
|
220
|
+
duration_s=raw.get("duration_s", 0.0),
|
|
221
|
+
peak_memory_bytes=raw.get("peak_memory_bytes", 0),
|
|
222
|
+
call_count=raw.get("call_count", 0),
|
|
223
|
+
truncated=raw.get("truncated", False),
|
|
224
|
+
calls=raw.get("calls", []),
|
|
225
|
+
exceptions=raw.get("exceptions", []),
|
|
226
|
+
)
|
|
227
|
+
# A fatal exception means exit handler captured it but exit stayed 1.
|
|
228
|
+
if any(x.get("fatal") for x in report.exceptions):
|
|
229
|
+
report.exit_code = 1
|
|
230
|
+
return report
|