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,889 @@
|
|
|
1
|
+
"""
|
|
2
|
+
App Contract builder for Patchi's Brain.
|
|
3
|
+
|
|
4
|
+
The App Contract is the list of critical flows that must never break.
|
|
5
|
+
Every fix checks against this contract before applying.
|
|
6
|
+
|
|
7
|
+
The contract is built in two layers:
|
|
8
|
+
1. Offline (free, no AI): Pattern-based inference from routes, file names, and AST structure.
|
|
9
|
+
2. AI-powered: When AI is configured, the LLM synthesises a summary from all AST scanner
|
|
10
|
+
results — file purposes, routes, import graph, dead code, dependencies — producing a
|
|
11
|
+
richer, context-aware contract.
|
|
12
|
+
|
|
13
|
+
The confirmed contract is stored in Brain memory and never silently overwritten.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import re
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import TYPE_CHECKING, Any
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from patchi.core.brain.route_mapper import RouteInfo
|
|
26
|
+
from patchi.core.brain.scanner import FileInfo
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
import logging
|
|
30
|
+
|
|
31
|
+
_log = logging.getLogger("patchi.brain.contract")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ContractFlow:
|
|
36
|
+
id: str
|
|
37
|
+
name: str
|
|
38
|
+
description: str
|
|
39
|
+
routes: list[str]
|
|
40
|
+
files: list[str]
|
|
41
|
+
signals: list[str]
|
|
42
|
+
confirmed: bool = False
|
|
43
|
+
user_added: bool = False
|
|
44
|
+
critical: bool = True
|
|
45
|
+
confidence: str = "medium" # "high" | "medium" | "low"
|
|
46
|
+
suggested: bool = False # if True, hidden from default confirmation
|
|
47
|
+
|
|
48
|
+
def to_dict(self) -> dict:
|
|
49
|
+
return {
|
|
50
|
+
"id": self.id,
|
|
51
|
+
"name": self.name,
|
|
52
|
+
"description": self.description,
|
|
53
|
+
"routes": self.routes,
|
|
54
|
+
"files": self.files,
|
|
55
|
+
"signals": self.signals,
|
|
56
|
+
"confirmed": self.confirmed,
|
|
57
|
+
"user_added": self.user_added,
|
|
58
|
+
"critical": self.critical,
|
|
59
|
+
"confidence": self.confidence,
|
|
60
|
+
"suggested": self.suggested,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
_ROUTE_TO_FLOW: dict[str, dict] = {
|
|
65
|
+
"dashboard": {
|
|
66
|
+
"name": "Dashboard Overview",
|
|
67
|
+
"desc": "Project dashboard with status and health overview.",
|
|
68
|
+
"signal": "dashboard",
|
|
69
|
+
},
|
|
70
|
+
"findings": {
|
|
71
|
+
"name": "Findings Review",
|
|
72
|
+
"desc": "Browse, filter, and review scan findings.",
|
|
73
|
+
"signal": "findings",
|
|
74
|
+
},
|
|
75
|
+
"settings": {
|
|
76
|
+
"name": "Settings / Configuration",
|
|
77
|
+
"desc": "View and update project configuration.",
|
|
78
|
+
"signal": "config",
|
|
79
|
+
},
|
|
80
|
+
"security": {
|
|
81
|
+
"name": "Security Scanning",
|
|
82
|
+
"desc": "Run security scans and view reports.",
|
|
83
|
+
"signal": "security",
|
|
84
|
+
},
|
|
85
|
+
"history": {
|
|
86
|
+
"name": "History & Trends",
|
|
87
|
+
"desc": "View scan history and health trends.",
|
|
88
|
+
"signal": "history",
|
|
89
|
+
},
|
|
90
|
+
"tests": {
|
|
91
|
+
"name": "Test Management",
|
|
92
|
+
"desc": "Run, view, and manage test suites.",
|
|
93
|
+
"signal": "testing",
|
|
94
|
+
},
|
|
95
|
+
"review": {
|
|
96
|
+
"name": "Review & Fixes",
|
|
97
|
+
"desc": "Review proposed fixes and apply or reject patches.",
|
|
98
|
+
"signal": "fixes",
|
|
99
|
+
},
|
|
100
|
+
"queue": {
|
|
101
|
+
"name": "Queue Management",
|
|
102
|
+
"desc": "View and manage the scan queue.",
|
|
103
|
+
"signal": "queue",
|
|
104
|
+
},
|
|
105
|
+
"hosted": {
|
|
106
|
+
"name": "Hosted Mode",
|
|
107
|
+
"desc": "Manage hosted mode, tokens, and IP reputation.",
|
|
108
|
+
"signal": "hosted",
|
|
109
|
+
},
|
|
110
|
+
"brain": {
|
|
111
|
+
"name": "Brain & Memory",
|
|
112
|
+
"desc": "View brain map, memory, import graph, and blast radius.",
|
|
113
|
+
"signal": "insights",
|
|
114
|
+
},
|
|
115
|
+
"agents": {
|
|
116
|
+
"name": "Agent Management",
|
|
117
|
+
"desc": "View and manage scanner agents.",
|
|
118
|
+
"signal": "agents",
|
|
119
|
+
},
|
|
120
|
+
"notifications": {
|
|
121
|
+
"name": "Notifications",
|
|
122
|
+
"desc": "Configure and receive notifications and alerts.",
|
|
123
|
+
"signal": "notifications",
|
|
124
|
+
},
|
|
125
|
+
"keys": {
|
|
126
|
+
"name": "Key Management",
|
|
127
|
+
"desc": "Manage API keys for AI providers.",
|
|
128
|
+
"signal": "keys",
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
INFERENCE_RULES: list[tuple[str, str, str, list[str], list[str], list[str]]] = [
|
|
133
|
+
(
|
|
134
|
+
"dashboard-overview",
|
|
135
|
+
"Dashboard Overview",
|
|
136
|
+
"Users can view the main dashboard with project status and health.",
|
|
137
|
+
[r"^/$", r"dashboard", r"status$"],
|
|
138
|
+
[r"dashboard", r"status"],
|
|
139
|
+
["dashboard"],
|
|
140
|
+
),
|
|
141
|
+
(
|
|
142
|
+
"findings-review",
|
|
143
|
+
"Findings Review",
|
|
144
|
+
"Users can browse and review scan findings.",
|
|
145
|
+
[r"findings", r"scan/results", r"findings-table"],
|
|
146
|
+
[r"finding", r"result"],
|
|
147
|
+
["findings"],
|
|
148
|
+
),
|
|
149
|
+
(
|
|
150
|
+
"settings-config",
|
|
151
|
+
"Settings / Configuration",
|
|
152
|
+
"Users can view and update project configuration.",
|
|
153
|
+
[r"settings", r"config$", r"preferences"],
|
|
154
|
+
[r"settings", r"config"],
|
|
155
|
+
["config"],
|
|
156
|
+
),
|
|
157
|
+
(
|
|
158
|
+
"security-scanning",
|
|
159
|
+
"Security Scanning",
|
|
160
|
+
"Run security scans and view reports on the project.",
|
|
161
|
+
[r"security", r"scan", r"guard", r"threats"],
|
|
162
|
+
[r"security", r"scan", r"guard"],
|
|
163
|
+
["security"],
|
|
164
|
+
),
|
|
165
|
+
(
|
|
166
|
+
"history-trends",
|
|
167
|
+
"History & Trends",
|
|
168
|
+
"Users can view scan history and health trends over time.",
|
|
169
|
+
[r"history", r"trend", r"health-breakdown"],
|
|
170
|
+
[r"history", r"trend"],
|
|
171
|
+
["history", "analytics"],
|
|
172
|
+
),
|
|
173
|
+
(
|
|
174
|
+
"test-management",
|
|
175
|
+
"Test Management",
|
|
176
|
+
"Users can run, view, and manage test suites and agents.",
|
|
177
|
+
[r"tests?", r"test-agents", r"test-agents/status"],
|
|
178
|
+
[r"test", r"unit", r"suite"],
|
|
179
|
+
["testing"],
|
|
180
|
+
),
|
|
181
|
+
(
|
|
182
|
+
"review-patches",
|
|
183
|
+
"Review & Fixes",
|
|
184
|
+
"Users can review proposed fixes and apply or reject patches.",
|
|
185
|
+
[r"review", r"fix", r"patch", r"issue"],
|
|
186
|
+
[r"review", r"fix", r"patch"],
|
|
187
|
+
["fixes"],
|
|
188
|
+
),
|
|
189
|
+
(
|
|
190
|
+
"queue-management",
|
|
191
|
+
"Queue Management",
|
|
192
|
+
"Users can view and manage the scan queue (pause/resume/clear).",
|
|
193
|
+
[r"queue", r"queue/pause", r"queue/resume", r"queue/clear"],
|
|
194
|
+
[r"queue"],
|
|
195
|
+
["queue"],
|
|
196
|
+
),
|
|
197
|
+
(
|
|
198
|
+
"hosted-mode",
|
|
199
|
+
"Hosted Mode Management",
|
|
200
|
+
"Users can manage hosted mode, tokens, and IP reputation.",
|
|
201
|
+
[r"hosted", r"tokens", r"block", r"unblock"],
|
|
202
|
+
[r"hosted", r"token", r"reputation"],
|
|
203
|
+
["hosted"],
|
|
204
|
+
),
|
|
205
|
+
(
|
|
206
|
+
"brain-insights",
|
|
207
|
+
"Brain & Memory",
|
|
208
|
+
"Users can view the brain map, memory, import graph, and blast radius.",
|
|
209
|
+
[r"brain", r"memory", r"blast", r"explain"],
|
|
210
|
+
[r"brain", r"memory", r"blast", r"import"],
|
|
211
|
+
["insights"],
|
|
212
|
+
),
|
|
213
|
+
(
|
|
214
|
+
"agent-management",
|
|
215
|
+
"Agent Management",
|
|
216
|
+
"Users can view and manage scanner agents and their status.",
|
|
217
|
+
[r"agents", r"model", r"ai"],
|
|
218
|
+
[r"agent", r"scanner"],
|
|
219
|
+
["agents"],
|
|
220
|
+
),
|
|
221
|
+
(
|
|
222
|
+
"notifications",
|
|
223
|
+
"Notifications",
|
|
224
|
+
"Users can configure and receive notifications and alerts.",
|
|
225
|
+
[r"notifications?", r"notify"],
|
|
226
|
+
[r"notify", r"notification", r"alert"],
|
|
227
|
+
["notifications"],
|
|
228
|
+
),
|
|
229
|
+
(
|
|
230
|
+
"key-management",
|
|
231
|
+
"Key Management",
|
|
232
|
+
"Users can manage API keys for AI providers.",
|
|
233
|
+
[r"keys", r"keys/add", r"keys/remove"],
|
|
234
|
+
[r"key", r"api.key", r"credential"],
|
|
235
|
+
["keys"],
|
|
236
|
+
),
|
|
237
|
+
]
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
_KNOWN_PREFIXES = set(_ROUTE_TO_FLOW.keys())
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ── AI contract summariser ─────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def build_ai_contract_summary(
|
|
247
|
+
file_infos: list[FileInfo],
|
|
248
|
+
routes: list[RouteInfo],
|
|
249
|
+
dead_files: list[str],
|
|
250
|
+
circular_deps: list[Any],
|
|
251
|
+
config: dict | None = None,
|
|
252
|
+
) -> str | None:
|
|
253
|
+
"""
|
|
254
|
+
When AI is configured, send the full AST scanner results to the LLM
|
|
255
|
+
and get back a plain-English summary of the app's critical flows.
|
|
256
|
+
|
|
257
|
+
Returns the AI summary string, or None if no AI is available.
|
|
258
|
+
"""
|
|
259
|
+
if not config:
|
|
260
|
+
return None
|
|
261
|
+
|
|
262
|
+
ai_config = config.get("ai", {})
|
|
263
|
+
has_keys = bool(ai_config.get("keys")) or bool(ai_config.get("local_model_name"))
|
|
264
|
+
if not has_keys:
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
# Build a compact summary of what the AST scanners found
|
|
268
|
+
# Prefer understander-ranked core files when available (Slice 2)
|
|
269
|
+
use_core = False
|
|
270
|
+
try:
|
|
271
|
+
|
|
272
|
+
from patchi.core.brain.body_tags import load_body_tags
|
|
273
|
+
from patchi.core.brain.understander import Understander
|
|
274
|
+
|
|
275
|
+
_root = None
|
|
276
|
+
# try to infer root from first file_infos path (relative), fallback to cwd
|
|
277
|
+
if file_infos and len(file_infos) > 0:
|
|
278
|
+
# file_infos paths are relative to root; we don't have root here, use cwd probe
|
|
279
|
+
for cand in [Path.cwd(), Path(".")]:
|
|
280
|
+
if (cand / ".patchi").exists() or (cand / "pyproject.toml").exists():
|
|
281
|
+
_root = cand.resolve()
|
|
282
|
+
break
|
|
283
|
+
if _root is not None:
|
|
284
|
+
_tags = load_body_tags(_root)
|
|
285
|
+
if _tags:
|
|
286
|
+
_u = Understander(_root, file_infos, _tags, None, routes)
|
|
287
|
+
core = _u.core_files(limit=20)
|
|
288
|
+
file_summaries = []
|
|
289
|
+
for c in core:
|
|
290
|
+
fi = next((x for x in file_infos if x.path == c["path"]), None)
|
|
291
|
+
if fi is None:
|
|
292
|
+
continue
|
|
293
|
+
funcs = ", ".join(f.name for f in fi.functions[:5])
|
|
294
|
+
classes = ", ".join(c.name for c in fi.classes[:3])
|
|
295
|
+
parts = [fi.path + f" // {c['why']}"]
|
|
296
|
+
if fi.purpose:
|
|
297
|
+
parts.append(f"({fi.purpose})")
|
|
298
|
+
if funcs:
|
|
299
|
+
parts.append(f"fns: [{funcs}]")
|
|
300
|
+
if classes:
|
|
301
|
+
parts.append(f"cls: [{classes}]")
|
|
302
|
+
file_summaries.append(" ".join(parts))
|
|
303
|
+
use_core = bool(file_summaries)
|
|
304
|
+
except Exception:
|
|
305
|
+
use_core = False
|
|
306
|
+
if not use_core:
|
|
307
|
+
file_summaries = []
|
|
308
|
+
for fi in file_infos[:50]:
|
|
309
|
+
funcs = ", ".join(f.name for f in fi.functions[:5])
|
|
310
|
+
classes = ", ".join(c.name for c in fi.classes[:3])
|
|
311
|
+
parts = [fi.path]
|
|
312
|
+
if fi.purpose:
|
|
313
|
+
parts.append(f"({fi.purpose})")
|
|
314
|
+
if funcs:
|
|
315
|
+
parts.append(f"fns: [{funcs}]")
|
|
316
|
+
if classes:
|
|
317
|
+
parts.append(f"cls: [{classes}]")
|
|
318
|
+
file_summaries.append(" ".join(parts))
|
|
319
|
+
|
|
320
|
+
route_summaries = [f"{r.method} {r.path}" for r in routes[:30]]
|
|
321
|
+
|
|
322
|
+
prompt = (
|
|
323
|
+
"You are analysing a codebase. Below is a structured summary of:\n"
|
|
324
|
+
"- Every source file with its inferred purpose, functions, and classes\n"
|
|
325
|
+
"- Every HTTP route with its method and path\n"
|
|
326
|
+
f"- Dead/unreachable files: {len(dead_files)}\n"
|
|
327
|
+
f"- Circular dependencies: {len(circular_deps)}\n\n"
|
|
328
|
+
"Based on this data, identify the app's critical flows "
|
|
329
|
+
"(login, checkout, admin, API, data submission, etc.) that must never break.\n\n"
|
|
330
|
+
f"=== FILES ({len(file_infos)} total, showing up to 50) ===\n"
|
|
331
|
+
+ "\n".join(file_summaries)
|
|
332
|
+
+ "\n\n=== ROUTES ===\n"
|
|
333
|
+
+ "\n".join(route_summaries)
|
|
334
|
+
+ "\n\nRespond with a concise JSON list of critical flows. "
|
|
335
|
+
"Each flow must have: name, description (one sentence), "
|
|
336
|
+
"route_paths (list of matching routes), "
|
|
337
|
+
"and file_paths (list of relevant source files). "
|
|
338
|
+
'Format: [{"name": "User Login", "description": "...", '
|
|
339
|
+
'"route_paths": [...], "file_paths": [...]}]'
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
return _call_ai_summary(prompt, config)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _call_ai_summary(prompt: str, config: dict) -> str | None:
|
|
346
|
+
"""Send prompt to the configured AI and return the response."""
|
|
347
|
+
ai_config = config.get("ai", {})
|
|
348
|
+
|
|
349
|
+
local_model = ai_config.get("local_model_name")
|
|
350
|
+
if local_model:
|
|
351
|
+
return _call_ollama(local_model, prompt)
|
|
352
|
+
|
|
353
|
+
keys = ai_config.get("keys", [])
|
|
354
|
+
for key_cfg in keys:
|
|
355
|
+
if key_cfg.get("status") == "error":
|
|
356
|
+
continue
|
|
357
|
+
import os
|
|
358
|
+
|
|
359
|
+
env_var = key_cfg.get("env_var", "")
|
|
360
|
+
api_key = os.environ.get(env_var, "")
|
|
361
|
+
if not api_key:
|
|
362
|
+
continue
|
|
363
|
+
result = _call_openai_compat(
|
|
364
|
+
api_key=api_key,
|
|
365
|
+
base_url=key_cfg.get("base_url", "https://api.openai.com/v1"),
|
|
366
|
+
model=key_cfg.get("model", "gpt-4o-mini"),
|
|
367
|
+
prompt=prompt,
|
|
368
|
+
)
|
|
369
|
+
if result:
|
|
370
|
+
return result
|
|
371
|
+
return None
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _call_ollama(model: str, prompt: str) -> str | None:
|
|
375
|
+
import urllib.request
|
|
376
|
+
|
|
377
|
+
from patchi.core.constants import OLLAMA_GENERATE_URL
|
|
378
|
+
|
|
379
|
+
try:
|
|
380
|
+
payload = json.dumps(
|
|
381
|
+
{
|
|
382
|
+
"model": model,
|
|
383
|
+
"prompt": prompt,
|
|
384
|
+
"stream": False,
|
|
385
|
+
}
|
|
386
|
+
).encode()
|
|
387
|
+
req = urllib.request.Request(
|
|
388
|
+
OLLAMA_GENERATE_URL,
|
|
389
|
+
data=payload,
|
|
390
|
+
headers={"Content-Type": "application/json"},
|
|
391
|
+
method="POST",
|
|
392
|
+
)
|
|
393
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
394
|
+
data = json.loads(resp.read())
|
|
395
|
+
return data.get("response", "")
|
|
396
|
+
except Exception as e:
|
|
397
|
+
_log.warning("_call_ollama failed: %s", e)
|
|
398
|
+
return None
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _call_openai_compat(api_key: str, base_url: str, model: str, prompt: str) -> str | None:
|
|
402
|
+
import urllib.request
|
|
403
|
+
|
|
404
|
+
try:
|
|
405
|
+
payload = json.dumps(
|
|
406
|
+
{
|
|
407
|
+
"model": model,
|
|
408
|
+
"max_tokens": 2000,
|
|
409
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
410
|
+
}
|
|
411
|
+
).encode()
|
|
412
|
+
headers = {
|
|
413
|
+
"Authorization": f"Bearer {api_key}",
|
|
414
|
+
"Content-Type": "application/json",
|
|
415
|
+
}
|
|
416
|
+
req = urllib.request.Request(
|
|
417
|
+
f"{base_url}/chat/completions",
|
|
418
|
+
data=payload,
|
|
419
|
+
headers=headers,
|
|
420
|
+
method="POST",
|
|
421
|
+
)
|
|
422
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
423
|
+
data = json.loads(resp.read())
|
|
424
|
+
return data.get("choices", [{}])[0].get("message", {}).get("content", "")
|
|
425
|
+
except Exception as e:
|
|
426
|
+
_log.warning("_call_openai_compat failed: %s", e)
|
|
427
|
+
return None
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def parse_ai_contract_response(response: str) -> list[dict] | None:
|
|
431
|
+
"""Parse AI response JSON into flow dicts that confirm_flows can use."""
|
|
432
|
+
try:
|
|
433
|
+
data = json.loads(response)
|
|
434
|
+
if isinstance(data, list):
|
|
435
|
+
return data
|
|
436
|
+
except json.JSONDecodeError:
|
|
437
|
+
m = re.search(r"\[.*?\]", response, re.DOTALL)
|
|
438
|
+
if m:
|
|
439
|
+
try:
|
|
440
|
+
return json.loads(m.group())
|
|
441
|
+
except json.JSONDecodeError:
|
|
442
|
+
pass
|
|
443
|
+
return None
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
# ── Contract builder ───────────────────────────────────────────────────────────
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
class ContractBuilder:
|
|
450
|
+
"""
|
|
451
|
+
Infers the App Contract from routes, AST file info, project understanding, and optionally AI.
|
|
452
|
+
|
|
453
|
+
Three inference paths:
|
|
454
|
+
1. project_infer() — reads README, package.json, etc. for deep understanding
|
|
455
|
+
2. offline inference() — pattern-based, free, no API calls
|
|
456
|
+
3. ai_infer() — sends AST scanner summary to LLM for richer contract
|
|
457
|
+
|
|
458
|
+
Usage:
|
|
459
|
+
builder = ContractBuilder(routes, file_infos, root=root)
|
|
460
|
+
flows = builder.project_infer() # smart, reads project files
|
|
461
|
+
flows = builder.infer() # offline, fast
|
|
462
|
+
ai_flows = builder.ai_infer(config) # AI-powered summary
|
|
463
|
+
"""
|
|
464
|
+
|
|
465
|
+
def __init__(
|
|
466
|
+
self,
|
|
467
|
+
routes: list[RouteInfo],
|
|
468
|
+
file_infos: list[FileInfo],
|
|
469
|
+
dead_files: list[str] | None = None,
|
|
470
|
+
circular_deps: list[Any] | None = None,
|
|
471
|
+
root: Path | None = None,
|
|
472
|
+
):
|
|
473
|
+
self.routes = routes
|
|
474
|
+
self.file_infos = file_infos
|
|
475
|
+
self.dead_files = dead_files or []
|
|
476
|
+
self.circular_deps = circular_deps or []
|
|
477
|
+
self.root = root
|
|
478
|
+
|
|
479
|
+
def project_infer(self) -> list[ContractFlow]:
|
|
480
|
+
"""
|
|
481
|
+
Smart inference: reads project files to understand what the project is,
|
|
482
|
+
then builds contract flows based on that understanding.
|
|
483
|
+
|
|
484
|
+
This is much more accurate than pattern matching because it actually
|
|
485
|
+
reads the README, package.json, pyproject.toml, etc.
|
|
486
|
+
"""
|
|
487
|
+
if not self.root:
|
|
488
|
+
return self.infer()
|
|
489
|
+
|
|
490
|
+
from patchi.core.brain.project_reader import read_project_insight
|
|
491
|
+
|
|
492
|
+
insight = read_project_insight(self.root)
|
|
493
|
+
|
|
494
|
+
# Build flows based on project understanding
|
|
495
|
+
flows: list[ContractFlow] = []
|
|
496
|
+
seen_ids: set[str] = set()
|
|
497
|
+
|
|
498
|
+
# 1. Create flows from critical directories
|
|
499
|
+
for dir_name in insight.critical_dirs:
|
|
500
|
+
dir_files = [fi.path for fi in self.file_infos if dir_name.lower() in fi.path.lower()]
|
|
501
|
+
if dir_files:
|
|
502
|
+
flow_id = f"project-{dir_name}"
|
|
503
|
+
if flow_id not in seen_ids:
|
|
504
|
+
seen_ids.add(flow_id)
|
|
505
|
+
flows.append(ContractFlow(
|
|
506
|
+
id=flow_id,
|
|
507
|
+
name=f"{dir_name.title()} Module",
|
|
508
|
+
description=f"The {dir_name} directory containing critical project code.",
|
|
509
|
+
routes=[],
|
|
510
|
+
files=dir_files[:10],
|
|
511
|
+
signals=["project-reader", f"dir:{dir_name}"],
|
|
512
|
+
confirmed=False,
|
|
513
|
+
confidence="high",
|
|
514
|
+
))
|
|
515
|
+
|
|
516
|
+
# 2. Create flows from entry points
|
|
517
|
+
for ep in insight.entry_points:
|
|
518
|
+
ep_files = [fi.path for fi in self.file_infos if ep in fi.path]
|
|
519
|
+
if ep_files:
|
|
520
|
+
flow_id = f"entry-{ep.replace('/', '-').replace('.', '-')}"
|
|
521
|
+
if flow_id not in seen_ids:
|
|
522
|
+
seen_ids.add(flow_id)
|
|
523
|
+
flows.append(ContractFlow(
|
|
524
|
+
id=flow_id,
|
|
525
|
+
name=f"Entry Point: {ep}",
|
|
526
|
+
description=f"Main entry point of the {insight.project_type or 'project'}.",
|
|
527
|
+
routes=[],
|
|
528
|
+
files=ep_files[:5],
|
|
529
|
+
signals=["project-reader", "entry-point"],
|
|
530
|
+
confirmed=False,
|
|
531
|
+
confidence="high",
|
|
532
|
+
))
|
|
533
|
+
|
|
534
|
+
# 3. Create flows from routes (using project context)
|
|
535
|
+
from collections import defaultdict
|
|
536
|
+
clusters: dict[str, list[str]] = defaultdict(list)
|
|
537
|
+
for r in self.routes:
|
|
538
|
+
segments = [s for s in r.path.split("/") if s]
|
|
539
|
+
prefix = segments[0] if segments else "root"
|
|
540
|
+
clusters[prefix].append(r.path)
|
|
541
|
+
|
|
542
|
+
for prefix, matched_routes in clusters.items():
|
|
543
|
+
flow_id = f"route-{prefix}"
|
|
544
|
+
if flow_id in seen_ids:
|
|
545
|
+
continue
|
|
546
|
+
seen_ids.add(flow_id)
|
|
547
|
+
|
|
548
|
+
# Use project context to name the flow
|
|
549
|
+
meta = _ROUTE_TO_FLOW.get(prefix)
|
|
550
|
+
if meta:
|
|
551
|
+
name = meta["name"]
|
|
552
|
+
desc = meta["desc"]
|
|
553
|
+
else:
|
|
554
|
+
name = f"{prefix.title()} Endpoints"
|
|
555
|
+
desc = f"Routes under /{prefix}/"
|
|
556
|
+
|
|
557
|
+
file_hits = [fi.path for fi in self.file_infos if prefix in fi.path.lower()]
|
|
558
|
+
confidence = "high" if file_hits else "medium"
|
|
559
|
+
|
|
560
|
+
flows.append(ContractFlow(
|
|
561
|
+
id=flow_id,
|
|
562
|
+
name=name,
|
|
563
|
+
description=desc,
|
|
564
|
+
routes=matched_routes[:5],
|
|
565
|
+
files=file_hits[:10],
|
|
566
|
+
signals=["project-reader", "route"],
|
|
567
|
+
confirmed=False,
|
|
568
|
+
confidence=confidence,
|
|
569
|
+
))
|
|
570
|
+
|
|
571
|
+
# 4. Merge with offline inference for anything we missed
|
|
572
|
+
if not flows:
|
|
573
|
+
flows = self.infer()
|
|
574
|
+
else:
|
|
575
|
+
offline = self.infer()
|
|
576
|
+
for f in offline:
|
|
577
|
+
if f.id not in seen_ids and not f.suggested:
|
|
578
|
+
flows.append(f)
|
|
579
|
+
|
|
580
|
+
return flows
|
|
581
|
+
|
|
582
|
+
def infer(self) -> list[ContractFlow]:
|
|
583
|
+
"""
|
|
584
|
+
Infer contract flows by clustering route paths by their URL prefix.
|
|
585
|
+
|
|
586
|
+
Each route is assigned to a cluster based on its first path segment.
|
|
587
|
+
Clusters with at least one route produce a ContractFlow at medium confidence.
|
|
588
|
+
Clusters with routes AND matching file purposes produce high confidence.
|
|
589
|
+
Routes that don't match any known prefix produce a single "Other API" flow.
|
|
590
|
+
|
|
591
|
+
If no routes are detected, falls back to file-structure-based inference.
|
|
592
|
+
"""
|
|
593
|
+
from collections import defaultdict
|
|
594
|
+
|
|
595
|
+
clusters: dict[str, list[str]] = defaultdict(list)
|
|
596
|
+
unmatched: list[str] = []
|
|
597
|
+
|
|
598
|
+
for r in self.routes:
|
|
599
|
+
path = r.path
|
|
600
|
+
segments = [s for s in path.split("/") if s]
|
|
601
|
+
prefix = segments[0] if segments else "root"
|
|
602
|
+
if prefix in _KNOWN_PREFIXES:
|
|
603
|
+
clusters[prefix].append(path)
|
|
604
|
+
else:
|
|
605
|
+
unmatched.append(path)
|
|
606
|
+
|
|
607
|
+
found: list[ContractFlow] = []
|
|
608
|
+
|
|
609
|
+
for prefix, matched_routes in sorted(clusters.items()):
|
|
610
|
+
meta = _ROUTE_TO_FLOW[prefix]
|
|
611
|
+
flow_id = prefix
|
|
612
|
+
name = meta["name"]
|
|
613
|
+
desc = meta["desc"]
|
|
614
|
+
signal = meta["signal"]
|
|
615
|
+
|
|
616
|
+
file_hits = [fi.path for fi in self.file_infos if prefix in fi.path.lower()]
|
|
617
|
+
confidence = "high" if file_hits else "medium"
|
|
618
|
+
|
|
619
|
+
found.append(
|
|
620
|
+
ContractFlow(
|
|
621
|
+
id=flow_id,
|
|
622
|
+
name=name,
|
|
623
|
+
description=desc,
|
|
624
|
+
routes=matched_routes[:5],
|
|
625
|
+
files=file_hits[:10],
|
|
626
|
+
signals=[signal],
|
|
627
|
+
confirmed=False,
|
|
628
|
+
user_added=False,
|
|
629
|
+
confidence=confidence,
|
|
630
|
+
suggested=False,
|
|
631
|
+
)
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
if unmatched:
|
|
635
|
+
found.append(
|
|
636
|
+
ContractFlow(
|
|
637
|
+
id="other-api",
|
|
638
|
+
name="Other API Endpoints",
|
|
639
|
+
description="Additional API endpoints that don't fit a named category.",
|
|
640
|
+
routes=unmatched[:10],
|
|
641
|
+
files=[],
|
|
642
|
+
signals=["api"],
|
|
643
|
+
confirmed=False,
|
|
644
|
+
user_added=False,
|
|
645
|
+
confidence="medium",
|
|
646
|
+
suggested=True,
|
|
647
|
+
)
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
# Fallback: if no routes detected, infer from file structure
|
|
651
|
+
if not found and self.file_infos:
|
|
652
|
+
found = self._infer_from_file_structure()
|
|
653
|
+
|
|
654
|
+
return found
|
|
655
|
+
|
|
656
|
+
def _infer_from_file_structure(self) -> list[ContractFlow]:
|
|
657
|
+
"""
|
|
658
|
+
Infer contract flows from file structure when no routes are detected.
|
|
659
|
+
This helps with projects that don't have standard route definitions.
|
|
660
|
+
"""
|
|
661
|
+
from collections import defaultdict
|
|
662
|
+
|
|
663
|
+
# Group files by directory structure
|
|
664
|
+
dir_groups: dict[str, list[str]] = defaultdict(list)
|
|
665
|
+
for fi in self.file_infos:
|
|
666
|
+
parts = fi.path.replace("\\", "/").split("/")
|
|
667
|
+
if len(parts) >= 2:
|
|
668
|
+
# Use first two directory levels as group key
|
|
669
|
+
group_key = "/".join(parts[:2])
|
|
670
|
+
else:
|
|
671
|
+
group_key = "root"
|
|
672
|
+
dir_groups[group_key].append(fi.path)
|
|
673
|
+
|
|
674
|
+
found: list[ContractFlow] = []
|
|
675
|
+
|
|
676
|
+
# Map common directory patterns to contract flows
|
|
677
|
+
dir_to_flow = {
|
|
678
|
+
"api": ("API Endpoints", "API route handlers and controllers."),
|
|
679
|
+
"routes": ("Route Handlers", "Web route handlers and controllers."),
|
|
680
|
+
"pages": ("Page Components", "Web page components and views."),
|
|
681
|
+
"components": ("UI Components", "Reusable UI components."),
|
|
682
|
+
"services": ("Service Layer", "Business logic and service classes."),
|
|
683
|
+
"models": ("Data Models", "Data models and database schemas."),
|
|
684
|
+
"utils": ("Utilities", "Utility functions and helpers."),
|
|
685
|
+
"lib": ("Library Code", "Shared library code."),
|
|
686
|
+
"core": ("Core Logic", "Core application logic."),
|
|
687
|
+
"auth": ("Authentication", "Authentication and authorization logic."),
|
|
688
|
+
"views": ("Views", "View templates and components."),
|
|
689
|
+
"controllers": ("Controllers", "Request handlers and controllers."),
|
|
690
|
+
"middleware": ("Middleware", "Request/response middleware."),
|
|
691
|
+
"tests": ("Test Suite", "Test files and test utilities."),
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
for dir_key, files in dir_groups.items():
|
|
695
|
+
dir_name = dir_key.split("/")[-1].lower()
|
|
696
|
+
|
|
697
|
+
# Check if this directory matches a known pattern
|
|
698
|
+
flow_name = None
|
|
699
|
+
flow_desc = None
|
|
700
|
+
for pattern, (name, desc) in dir_to_flow.items():
|
|
701
|
+
if pattern in dir_name:
|
|
702
|
+
flow_name = name
|
|
703
|
+
flow_desc = desc
|
|
704
|
+
break
|
|
705
|
+
|
|
706
|
+
if not flow_name:
|
|
707
|
+
# Use directory name as flow name
|
|
708
|
+
flow_name = f"{dir_name.title()} Module"
|
|
709
|
+
flow_desc = f"Code in the {dir_name} directory."
|
|
710
|
+
|
|
711
|
+
flow_id = dir_name.replace("/", "-")
|
|
712
|
+
found.append(
|
|
713
|
+
ContractFlow(
|
|
714
|
+
id=flow_id,
|
|
715
|
+
name=flow_name,
|
|
716
|
+
description=flow_desc,
|
|
717
|
+
routes=[],
|
|
718
|
+
files=files[:10],
|
|
719
|
+
signals=["file-structure"],
|
|
720
|
+
confirmed=False,
|
|
721
|
+
user_added=False,
|
|
722
|
+
confidence="low",
|
|
723
|
+
suggested=True,
|
|
724
|
+
)
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
return found[:10] # Limit to top 10 flows
|
|
728
|
+
|
|
729
|
+
def ai_infer(self, config: dict | None = None) -> list[ContractFlow] | None:
|
|
730
|
+
"""
|
|
731
|
+
Use AI to generate a richer contract from AST scanner data.
|
|
732
|
+
Returns AI-inferred flows merged with offline pattern matches,
|
|
733
|
+
or None if AI is not available.
|
|
734
|
+
"""
|
|
735
|
+
response = build_ai_contract_summary(
|
|
736
|
+
self.file_infos,
|
|
737
|
+
self.routes,
|
|
738
|
+
self.dead_files,
|
|
739
|
+
self.circular_deps,
|
|
740
|
+
config,
|
|
741
|
+
)
|
|
742
|
+
if not response:
|
|
743
|
+
return None
|
|
744
|
+
|
|
745
|
+
ai_flows = parse_ai_contract_response(response)
|
|
746
|
+
if not ai_flows:
|
|
747
|
+
return None
|
|
748
|
+
|
|
749
|
+
offline_flows = self.infer()
|
|
750
|
+
{f.id for f in offline_flows}
|
|
751
|
+
|
|
752
|
+
merged: list[ContractFlow] = []
|
|
753
|
+
seen_names: set[str] = set()
|
|
754
|
+
|
|
755
|
+
for i, af in enumerate(ai_flows):
|
|
756
|
+
name = af.get("name", f"AI Flow {i + 1}")
|
|
757
|
+
key = name.lower().replace(" ", "-")
|
|
758
|
+
if key in seen_names:
|
|
759
|
+
continue
|
|
760
|
+
seen_names.add(key)
|
|
761
|
+
merged.append(
|
|
762
|
+
ContractFlow(
|
|
763
|
+
id=key,
|
|
764
|
+
name=name,
|
|
765
|
+
description=af.get("description", ""),
|
|
766
|
+
routes=af.get("route_paths", [])[:5],
|
|
767
|
+
files=af.get("file_paths", [])[:10],
|
|
768
|
+
signals=["ai-inferred"],
|
|
769
|
+
confirmed=False,
|
|
770
|
+
user_added=False,
|
|
771
|
+
)
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
# Mark AI-inferred flows with high confidence
|
|
775
|
+
for f in merged:
|
|
776
|
+
if "ai-inferred" in f.signals:
|
|
777
|
+
f.confidence = "high"
|
|
778
|
+
f.suggested = False
|
|
779
|
+
|
|
780
|
+
# Merge offline-only flows that AI missed — skip suggested (low confidence)
|
|
781
|
+
for f in offline_flows:
|
|
782
|
+
if f.suggested:
|
|
783
|
+
continue
|
|
784
|
+
key = f.name.lower().replace(" ", "-")
|
|
785
|
+
if key not in seen_names:
|
|
786
|
+
merged.append(f)
|
|
787
|
+
|
|
788
|
+
return merged
|
|
789
|
+
|
|
790
|
+
def build_confirmation_message(self, flows: list[ContractFlow], all_flows: bool = False) -> str:
|
|
791
|
+
"""
|
|
792
|
+
Build the plain-English message Patchi shows the user before confirmation.
|
|
793
|
+
Suggested (low-confidence) flows are excluded from the display unless all_flows=True.
|
|
794
|
+
"""
|
|
795
|
+
visible = flows if all_flows else [f for f in flows if not f.suggested]
|
|
796
|
+
hidden_count = len(flows) - len(visible)
|
|
797
|
+
|
|
798
|
+
if not visible:
|
|
799
|
+
msg = (
|
|
800
|
+
"I didn't find any obvious critical flows in your project.\n"
|
|
801
|
+
"You can add them manually below."
|
|
802
|
+
)
|
|
803
|
+
if hidden_count:
|
|
804
|
+
msg += (
|
|
805
|
+
f"\n\n[dim]({hidden_count} low-confidence flow(s) were inferred but hidden — "
|
|
806
|
+
f"run with --all-flows to see them)[/dim]"
|
|
807
|
+
)
|
|
808
|
+
return msg
|
|
809
|
+
|
|
810
|
+
ai_count = sum(1 for f in visible if "ai-inferred" in f.signals)
|
|
811
|
+
len(visible) - ai_count
|
|
812
|
+
|
|
813
|
+
names = [f.name for f in visible]
|
|
814
|
+
if len(names) == 1:
|
|
815
|
+
flow_list = names[0]
|
|
816
|
+
elif len(names) == 2:
|
|
817
|
+
flow_list = f"{names[0]} and {names[1]}"
|
|
818
|
+
else:
|
|
819
|
+
flow_list = ", ".join(names[:-1]) + f", and {names[-1]}"
|
|
820
|
+
|
|
821
|
+
source_note = ""
|
|
822
|
+
if ai_count > 0:
|
|
823
|
+
n_files = len(self.file_infos)
|
|
824
|
+
n_routes = len(self.routes)
|
|
825
|
+
source_note = f"\n(Detected via AI analysis of {n_files} files and {n_routes} routes)"
|
|
826
|
+
if hidden_count:
|
|
827
|
+
source_note += (
|
|
828
|
+
f"\n({hidden_count} low-confidence flow(s) hidden — run --all-flows to see)"
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
return (
|
|
832
|
+
f"I think your critical flows are: {flow_list}.{source_note}\n"
|
|
833
|
+
f"Does that look right? I'll protect these with every fix I make."
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
def confirm_flows(
|
|
838
|
+
flows: list[ContractFlow],
|
|
839
|
+
confirmed_ids: set[str],
|
|
840
|
+
user_additions: list[dict] | None = None,
|
|
841
|
+
) -> list[ContractFlow]:
|
|
842
|
+
"""
|
|
843
|
+
Mark flows as confirmed based on user selection.
|
|
844
|
+
user_additions: list of {name, description, routes, files} dicts for user-added flows.
|
|
845
|
+
Returns the final confirmed contract.
|
|
846
|
+
"""
|
|
847
|
+
result: list[ContractFlow] = []
|
|
848
|
+
|
|
849
|
+
for flow in flows:
|
|
850
|
+
if flow.id in confirmed_ids:
|
|
851
|
+
flow.confirmed = True
|
|
852
|
+
result.append(flow)
|
|
853
|
+
|
|
854
|
+
if user_additions:
|
|
855
|
+
for i, addition in enumerate(user_additions):
|
|
856
|
+
result.append(
|
|
857
|
+
ContractFlow(
|
|
858
|
+
id=f"user-{i}",
|
|
859
|
+
name=addition.get("name", f"Custom Flow {i + 1}"),
|
|
860
|
+
description=addition.get("description", "User-defined critical flow."),
|
|
861
|
+
routes=addition.get("routes", []),
|
|
862
|
+
files=addition.get("files", []),
|
|
863
|
+
signals=["user-defined"],
|
|
864
|
+
confirmed=True,
|
|
865
|
+
user_added=True,
|
|
866
|
+
)
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
return result
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def flows_from_dict(data: list[dict]) -> list[ContractFlow]:
|
|
873
|
+
"""Deserialize flows from stored dict (memory)."""
|
|
874
|
+
return [
|
|
875
|
+
ContractFlow(
|
|
876
|
+
id=d["id"],
|
|
877
|
+
name=d["name"],
|
|
878
|
+
description=d["description"],
|
|
879
|
+
routes=d.get("routes", []),
|
|
880
|
+
files=d.get("files", []),
|
|
881
|
+
signals=d.get("signals", []),
|
|
882
|
+
confirmed=d.get("confirmed", False),
|
|
883
|
+
user_added=d.get("user_added", False),
|
|
884
|
+
critical=d.get("critical", True),
|
|
885
|
+
confidence=d.get("confidence", "medium"),
|
|
886
|
+
suggested=d.get("suggested", False),
|
|
887
|
+
)
|
|
888
|
+
for d in data
|
|
889
|
+
]
|