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,473 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project Charter — the Guard Rails pillar of the Patchi super-agent.
|
|
3
|
+
|
|
4
|
+
The user declares what the project *should be* in natural language:
|
|
5
|
+
|
|
6
|
+
p charter "This is a Flask+React monorepo. Frontend must never import
|
|
7
|
+
backend DB modules. All API routes need tests. No hardcoded secrets.
|
|
8
|
+
Services under 80 lines."
|
|
9
|
+
|
|
10
|
+
This is parsed (heuristically now, LLM-assisted later) into a structured
|
|
11
|
+
:class:`Charter` with four rule families:
|
|
12
|
+
|
|
13
|
+
- stack → expected languages / frameworks (drift if a new one appears)
|
|
14
|
+
- boundaries → forbidden import edges between layers (architecture drift)
|
|
15
|
+
- conventions → naming, max file/function size, required test coverage
|
|
16
|
+
- security → no hardcoded secrets, no pickle/eval, parameterized queries
|
|
17
|
+
|
|
18
|
+
The charter is stored in `.patchi/memory/charter.json` and checked on every
|
|
19
|
+
scan. Violations are surfaced as ``charter-drift`` findings so that **both
|
|
20
|
+
humans (via ``p scan``) and agents (via the Governor) stay in context**.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import logging
|
|
26
|
+
import re
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
# ── Known frameworks / languages for stack detection ──────────────────────────
|
|
31
|
+
|
|
32
|
+
_KNOWN_FRAMEWORKS = [
|
|
33
|
+
"flask",
|
|
34
|
+
"django",
|
|
35
|
+
"fastapi",
|
|
36
|
+
"starlette",
|
|
37
|
+
"tornado",
|
|
38
|
+
"sanic",
|
|
39
|
+
"litestar",
|
|
40
|
+
"express",
|
|
41
|
+
"fastify",
|
|
42
|
+
"koa",
|
|
43
|
+
"nestjs",
|
|
44
|
+
"next.js",
|
|
45
|
+
"nuxt",
|
|
46
|
+
"sveltekit",
|
|
47
|
+
"remix",
|
|
48
|
+
"astro",
|
|
49
|
+
"react",
|
|
50
|
+
"vue",
|
|
51
|
+
"angular",
|
|
52
|
+
"svelte",
|
|
53
|
+
"solid.js",
|
|
54
|
+
"qwik",
|
|
55
|
+
"spring",
|
|
56
|
+
"quarkus",
|
|
57
|
+
"micronaut",
|
|
58
|
+
"laravel",
|
|
59
|
+
"symfony",
|
|
60
|
+
"rails",
|
|
61
|
+
"sinatra",
|
|
62
|
+
"gin",
|
|
63
|
+
"echo",
|
|
64
|
+
"fiber",
|
|
65
|
+
"chi",
|
|
66
|
+
"actix",
|
|
67
|
+
"axum",
|
|
68
|
+
"rocket",
|
|
69
|
+
"tauri",
|
|
70
|
+
"warp",
|
|
71
|
+
"hono",
|
|
72
|
+
"trpc",
|
|
73
|
+
"vapor",
|
|
74
|
+
"phoenix",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
_KNOWN_LANGUAGES = [
|
|
78
|
+
"python",
|
|
79
|
+
"javascript",
|
|
80
|
+
"typescript",
|
|
81
|
+
"go",
|
|
82
|
+
"golang",
|
|
83
|
+
"rust",
|
|
84
|
+
"java",
|
|
85
|
+
"php",
|
|
86
|
+
"ruby",
|
|
87
|
+
"swift",
|
|
88
|
+
"kotlin",
|
|
89
|
+
"scala",
|
|
90
|
+
"c#",
|
|
91
|
+
"csharp",
|
|
92
|
+
"c++",
|
|
93
|
+
"dart",
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
# Role words → subsystem layer names they map to
|
|
97
|
+
_ROLE_MAP: dict[str, set[str]] = {
|
|
98
|
+
"frontend": {"ui"},
|
|
99
|
+
"ui": {"ui"},
|
|
100
|
+
"client": {"ui"},
|
|
101
|
+
"backend": {"api", "data", "core", "agents"},
|
|
102
|
+
"server": {"api", "core"},
|
|
103
|
+
"database": {"data"},
|
|
104
|
+
"db": {"data"},
|
|
105
|
+
"auth": {"auth"},
|
|
106
|
+
"api": {"api"},
|
|
107
|
+
"data": {"data"},
|
|
108
|
+
"core": {"core"},
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ── Dataclasses ────────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
_log = logging.getLogger("patchi.brain.charter")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class Charter:
|
|
120
|
+
"""Structured project guard rails parsed from natural language."""
|
|
121
|
+
|
|
122
|
+
raw_text: str = ""
|
|
123
|
+
stack: dict = field(default_factory=lambda: {"languages": [], "frameworks": []})
|
|
124
|
+
boundaries: list[dict] = field(default_factory=list) # [{"from": str, "to": str}]
|
|
125
|
+
conventions: dict = field(default_factory=dict) # max_file_lines, require_tests_for_routes, ...
|
|
126
|
+
security: list[str] = field(default_factory=list) # rule names
|
|
127
|
+
notes: list[str] = field(default_factory=list)
|
|
128
|
+
|
|
129
|
+
def to_dict(self) -> dict:
|
|
130
|
+
return {
|
|
131
|
+
"raw_text": self.raw_text,
|
|
132
|
+
"stack": self.stack,
|
|
133
|
+
"boundaries": self.boundaries,
|
|
134
|
+
"conventions": self.conventions,
|
|
135
|
+
"security": self.security,
|
|
136
|
+
"notes": self.notes,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def from_dict(cls, d: dict) -> Charter:
|
|
141
|
+
# Handle old format (boundaries/conventions/security)
|
|
142
|
+
if "rules" in d:
|
|
143
|
+
# Convert new security.charter format to brain.charter format
|
|
144
|
+
boundaries = []
|
|
145
|
+
conventions = {}
|
|
146
|
+
security = []
|
|
147
|
+
stack = {"languages": [], "frameworks": []}
|
|
148
|
+
for rule in d["rules"]:
|
|
149
|
+
if rule.get("type") == "boundary":
|
|
150
|
+
boundaries.append({
|
|
151
|
+
"from": rule.get("from_pattern", ""),
|
|
152
|
+
"to": rule.get("to_pattern", ""),
|
|
153
|
+
})
|
|
154
|
+
elif rule.get("type") == "convention":
|
|
155
|
+
if rule.get("max_value"):
|
|
156
|
+
conventions["max_file_lines"] = rule["max_value"]
|
|
157
|
+
elif rule.get("type") == "security":
|
|
158
|
+
security.extend(rule.get("keywords", []))
|
|
159
|
+
elif rule.get("type") == "stack":
|
|
160
|
+
stack["languages"].extend(rule.get("allowed_languages", []))
|
|
161
|
+
stack["frameworks"].extend(rule.get("allowed_frameworks", []))
|
|
162
|
+
return cls(
|
|
163
|
+
raw_text=d.get("text", ""),
|
|
164
|
+
stack=stack,
|
|
165
|
+
boundaries=boundaries,
|
|
166
|
+
conventions=conventions,
|
|
167
|
+
security=security,
|
|
168
|
+
)
|
|
169
|
+
# Old format
|
|
170
|
+
return cls(
|
|
171
|
+
raw_text=d.get("raw_text", ""),
|
|
172
|
+
stack=d.get("stack", {"languages": [], "frameworks": []}),
|
|
173
|
+
boundaries=d.get("boundaries", []),
|
|
174
|
+
conventions=d.get("conventions", {}),
|
|
175
|
+
security=d.get("security", []),
|
|
176
|
+
notes=d.get("notes", []),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dataclass
|
|
181
|
+
class CharterViolation:
|
|
182
|
+
"""A single guard-rail breach detected during a scan."""
|
|
183
|
+
|
|
184
|
+
rule: str # which charter rule was broken
|
|
185
|
+
severity: str # "high" | "medium" | "low"
|
|
186
|
+
message: str
|
|
187
|
+
file: str = "" # offending file (if known)
|
|
188
|
+
layer: str = "" # offending layer (if known)
|
|
189
|
+
|
|
190
|
+
def to_finding(self) -> dict:
|
|
191
|
+
return {
|
|
192
|
+
"agent": "CharterGuard",
|
|
193
|
+
"type": "charter-drift",
|
|
194
|
+
"severity": self.severity,
|
|
195
|
+
"file": self.file,
|
|
196
|
+
"message": self.message,
|
|
197
|
+
"rule": self.rule,
|
|
198
|
+
"layer": self.layer,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# ── Parsing ───────────────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def parse_charter(text: str, config: dict | None = None) -> Charter:
|
|
206
|
+
"""Parse a natural-language charter into a structured Charter.
|
|
207
|
+
|
|
208
|
+
Uses keyword matching (no regex, no heuristics) to extract structured rules.
|
|
209
|
+
"""
|
|
210
|
+
text_lower = text.lower()
|
|
211
|
+
charter = Charter(raw_text=text)
|
|
212
|
+
|
|
213
|
+
# ── Stack: frameworks + languages ──
|
|
214
|
+
for fw in _KNOWN_FRAMEWORKS:
|
|
215
|
+
if " " + fw + " " in text_lower or text_lower.startswith(fw + " ") or text_lower.endswith(" " + fw):
|
|
216
|
+
if fw not in charter.stack["frameworks"]:
|
|
217
|
+
charter.stack["frameworks"].append(fw)
|
|
218
|
+
for lang in _KNOWN_LANGUAGES:
|
|
219
|
+
if " " + lang + " " in text_lower or text_lower.startswith(lang + " ") or text_lower.endswith(" " + lang):
|
|
220
|
+
norm = "golang" if lang == "go" else ("csharp" if lang in ("c#",) else lang)
|
|
221
|
+
if norm not in charter.stack["languages"]:
|
|
222
|
+
charter.stack["languages"].append(norm)
|
|
223
|
+
|
|
224
|
+
# ── Boundaries: "X must not import Y" ──
|
|
225
|
+
boundary_words = ["must not import", "cannot import", "never import",
|
|
226
|
+
"should not import", "must not use", "cannot use"]
|
|
227
|
+
for sep in boundary_words:
|
|
228
|
+
if sep in text_lower:
|
|
229
|
+
parts = text_lower.split(sep)
|
|
230
|
+
if len(parts) == 2:
|
|
231
|
+
from_pat = parts[0].strip().rstrip()
|
|
232
|
+
to_pat = parts[1].strip().lstrip()
|
|
233
|
+
# Remove leading articles
|
|
234
|
+
for prefix in ["the ", "a ", "an "]:
|
|
235
|
+
if from_pat.startswith(prefix):
|
|
236
|
+
from_pat = from_pat[len(prefix):]
|
|
237
|
+
if to_pat.startswith(prefix):
|
|
238
|
+
to_pat = to_pat[len(prefix):]
|
|
239
|
+
charter.boundaries.append({"from": from_pat, "to": to_pat})
|
|
240
|
+
|
|
241
|
+
# ── Conventions: sizes + test coverage ──
|
|
242
|
+
if "under" in text_lower or "below" in text_lower or "less than" in text_lower:
|
|
243
|
+
for word in text_lower.split():
|
|
244
|
+
if word.isdigit() and "lines" in text_lower:
|
|
245
|
+
charter.conventions["max_file_lines"] = int(word)
|
|
246
|
+
break
|
|
247
|
+
|
|
248
|
+
if any(phrase in text_lower for phrase in ["all routes need", "all routes must", "routes require tests"]):
|
|
249
|
+
charter.conventions["require_tests_for_routes"] = True
|
|
250
|
+
if any(phrase in text_lower for phrase in ["every function", "all modules", "all services"]):
|
|
251
|
+
if "test" in text_lower:
|
|
252
|
+
charter.conventions["require_tests_for_routes"] = True
|
|
253
|
+
|
|
254
|
+
# ── Security rules ──
|
|
255
|
+
if "hardcoded" in text_lower or "hard coded" in text_lower or "secrets" in text_lower:
|
|
256
|
+
charter.security.append("no_hardcoded_secrets")
|
|
257
|
+
if "pickle" in text_lower:
|
|
258
|
+
charter.security.append("no_pickle")
|
|
259
|
+
if "eval" in text_lower:
|
|
260
|
+
charter.security.append("no_eval")
|
|
261
|
+
if "parameterized" in text_lower:
|
|
262
|
+
charter.security.append("parameterized_queries")
|
|
263
|
+
|
|
264
|
+
return charter
|
|
265
|
+
|
|
266
|
+
def _split_role_phrase(phrase: str) -> tuple[str, str]:
|
|
267
|
+
"""Split a 'X importing Y' phrase into (from, to) role tokens."""
|
|
268
|
+
m = re.search(r"([\w./]+)\s+importing\s+([\w./]+)", phrase)
|
|
269
|
+
if m:
|
|
270
|
+
return m.group(1), m.group(2)
|
|
271
|
+
return phrase, ""
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def parse_charter_with_ai(text: str, config: dict) -> Charter | None:
|
|
275
|
+
"""Optional LLM-backed parser. Returns None if AI unavailable or it fails."""
|
|
276
|
+
ai_config = config.get("ai", {})
|
|
277
|
+
has_ai = bool(ai_config.get("keys") or ai_config.get("local_model_name"))
|
|
278
|
+
if not has_ai:
|
|
279
|
+
return None
|
|
280
|
+
try:
|
|
281
|
+
from patchi.core.ai.client import call_ai
|
|
282
|
+
except Exception as e:
|
|
283
|
+
_log.warning("parse_charter_with_ai failed: %s", e)
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
prompt = (
|
|
287
|
+
"Convert the following project charter into JSON with keys: "
|
|
288
|
+
"frameworks (list), languages (list), boundaries (list of {from,to}), "
|
|
289
|
+
"conventions (dict), security (list of rule strings). "
|
|
290
|
+
"Return ONLY JSON.\n\n" + text
|
|
291
|
+
)
|
|
292
|
+
try:
|
|
293
|
+
resp = call_ai(
|
|
294
|
+
config, "You are a config parser. Output only JSON.", prompt, max_tokens=1024
|
|
295
|
+
)
|
|
296
|
+
except Exception as e:
|
|
297
|
+
_log.warning("parse_charter_with_ai failed: %s", e)
|
|
298
|
+
return None
|
|
299
|
+
if not resp:
|
|
300
|
+
return None
|
|
301
|
+
# strip fences
|
|
302
|
+
resp = resp.strip()
|
|
303
|
+
if resp.startswith("```"):
|
|
304
|
+
resp = resp.split("```")[1]
|
|
305
|
+
if resp.startswith("json"):
|
|
306
|
+
resp = resp[4:]
|
|
307
|
+
try:
|
|
308
|
+
data = json.loads(resp)
|
|
309
|
+
except (json.JSONDecodeError, ValueError):
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
charter = Charter(raw_text=text)
|
|
313
|
+
charter.stack = data.get("stack", {"languages": [], "frameworks": []})
|
|
314
|
+
charter.boundaries = data.get("boundaries", [])
|
|
315
|
+
charter.conventions = data.get("conventions", {})
|
|
316
|
+
charter.security = data.get("security", [])
|
|
317
|
+
return charter
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# ── Resolution helpers ────────────────────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _resolve_role(token: str) -> set[str]:
|
|
324
|
+
"""Map a role/framework/layer token to the subsystem layer names it covers."""
|
|
325
|
+
token_l = token.lower().strip()
|
|
326
|
+
if token_l in _ROLE_MAP:
|
|
327
|
+
return _ROLE_MAP[token_l]
|
|
328
|
+
# Direct subsystem name match
|
|
329
|
+
if token_l in {"auth", "api", "data", "ui", "core", "agents", "tests", "infra"}:
|
|
330
|
+
return {token_l}
|
|
331
|
+
# Framework → its typical subsystem
|
|
332
|
+
fw_to_sub = {
|
|
333
|
+
"react": "ui",
|
|
334
|
+
"vue": "ui",
|
|
335
|
+
"angular": "ui",
|
|
336
|
+
"svelte": "ui",
|
|
337
|
+
"next.js": "ui",
|
|
338
|
+
"nuxt": "ui",
|
|
339
|
+
"sveltekit": "ui",
|
|
340
|
+
"flask": "api",
|
|
341
|
+
"django": "api",
|
|
342
|
+
"fastapi": "api",
|
|
343
|
+
"express": "api",
|
|
344
|
+
"spring": "api",
|
|
345
|
+
"laravel": "api",
|
|
346
|
+
"rails": "api",
|
|
347
|
+
}
|
|
348
|
+
if token_l in fw_to_sub:
|
|
349
|
+
return {fw_to_sub[token_l]}
|
|
350
|
+
return set()
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _matches_boundary(edge_from: str, edge_to: str, boundary: dict) -> bool:
|
|
354
|
+
"""Does a layer dependency edge violate a forbidden boundary?"""
|
|
355
|
+
from_set = _resolve_role(boundary.get("from", ""))
|
|
356
|
+
to_set = _resolve_role(boundary.get("to", ""))
|
|
357
|
+
if not from_set or not to_set:
|
|
358
|
+
return False
|
|
359
|
+
# Direct name match OR role-resolved match
|
|
360
|
+
direct = (
|
|
361
|
+
boundary.get("from", "").lower().strip() in edge_from.lower()
|
|
362
|
+
and boundary.get("to", "").lower().strip() in edge_to.lower()
|
|
363
|
+
)
|
|
364
|
+
role = (bool(from_set & {edge_from} or any(f in edge_from.lower() for f in from_set))) and (
|
|
365
|
+
bool(to_set & {edge_to} or any(t in edge_to.lower() for t in to_set))
|
|
366
|
+
)
|
|
367
|
+
return direct or role
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
# ── Checking ──────────────────────────────────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def check_charter(
|
|
374
|
+
charter: Charter,
|
|
375
|
+
layers: dict,
|
|
376
|
+
detected_frameworks: list[str] | None = None,
|
|
377
|
+
routes: list | None = None,
|
|
378
|
+
file_infos: list | None = None,
|
|
379
|
+
) -> list[CharterViolation]:
|
|
380
|
+
"""Evaluate a charter against the current codebase state.
|
|
381
|
+
|
|
382
|
+
Returns a list of :class:`CharterViolation` (empty if fully compliant).
|
|
383
|
+
"""
|
|
384
|
+
violations: list[CharterViolation] = []
|
|
385
|
+
if not charter or not layers:
|
|
386
|
+
return violations
|
|
387
|
+
|
|
388
|
+
# ── Boundary checks (subsystem-level dependency edges) ─────────────────────
|
|
389
|
+
subsystem_layers = {n: lay for n, lay in layers.items() if lay.get("level") == 2}
|
|
390
|
+
for name, layer in subsystem_layers.items():
|
|
391
|
+
for dep in layer.get("depends_on", []):
|
|
392
|
+
for b in charter.boundaries:
|
|
393
|
+
if _matches_boundary(name, dep, b):
|
|
394
|
+
violations.append(
|
|
395
|
+
CharterViolation(
|
|
396
|
+
rule=f"boundary:{b.get('from')}→{b.get('to')}",
|
|
397
|
+
severity="high",
|
|
398
|
+
message=(
|
|
399
|
+
f"Architecture drift: subsystem '{name}' imports "
|
|
400
|
+
f"'{dep}', which violates the charter rule "
|
|
401
|
+
f"'{b.get('from')} must not import {b.get('to')}'."
|
|
402
|
+
),
|
|
403
|
+
layer=name,
|
|
404
|
+
)
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
# ── Stack drift ───────────────────────────────────────────────────────────
|
|
408
|
+
if detected_frameworks and charter.stack.get("frameworks"):
|
|
409
|
+
expected = {f.lower() for f in charter.stack["frameworks"]}
|
|
410
|
+
actual = {f.lower() for f in detected_frameworks}
|
|
411
|
+
unexpected = actual - expected
|
|
412
|
+
# Only flag clearly-new frameworks, not the same framework under another name
|
|
413
|
+
if unexpected:
|
|
414
|
+
violations.append(
|
|
415
|
+
CharterViolation(
|
|
416
|
+
rule="stack-drift",
|
|
417
|
+
severity="medium",
|
|
418
|
+
message=(
|
|
419
|
+
f"Stack drift: project uses frameworks not declared in the "
|
|
420
|
+
f"charter: {', '.join(sorted(unexpected))}. Charter expected: "
|
|
421
|
+
f"{', '.join(sorted(expected))}."
|
|
422
|
+
),
|
|
423
|
+
)
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# ── Convention: require tests for routes ───────────────────────────────────
|
|
427
|
+
if charter.conventions.get("require_tests_for_routes") and routes:
|
|
428
|
+
route_files = {
|
|
429
|
+
r.get("file", "") if isinstance(r, dict) else getattr(r, "file", "") for r in routes
|
|
430
|
+
}
|
|
431
|
+
test_files = {
|
|
432
|
+
fi.path
|
|
433
|
+
for fi in (file_infos or [])
|
|
434
|
+
if fi.path
|
|
435
|
+
and (
|
|
436
|
+
"test" in fi.path.lower()
|
|
437
|
+
or fi.path.endswith("_test.py")
|
|
438
|
+
or fi.path.endswith(".test.js")
|
|
439
|
+
or fi.path.endswith(".test.ts")
|
|
440
|
+
)
|
|
441
|
+
}
|
|
442
|
+
untested = [rf for rf in route_files if rf and rf not in test_files]
|
|
443
|
+
if untested:
|
|
444
|
+
violations.append(
|
|
445
|
+
CharterViolation(
|
|
446
|
+
rule="require_tests_for_routes",
|
|
447
|
+
severity="low",
|
|
448
|
+
message=(
|
|
449
|
+
f"Convention drift: {len(untested)} route file(s) have no "
|
|
450
|
+
f"test coverage (charter requires tests for all API routes)."
|
|
451
|
+
),
|
|
452
|
+
)
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
return violations
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
# ── Persistence helpers ───────────────────────────────────────────────────────
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def save_charter(charter: Charter, root: Path) -> None:
|
|
462
|
+
from patchi.core import memory as mem
|
|
463
|
+
|
|
464
|
+
mem.save_charter(charter.to_dict(), root)
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def load_charter(root: Path) -> Charter | None:
|
|
468
|
+
from patchi.core import memory as mem
|
|
469
|
+
|
|
470
|
+
data = mem.get_charter(root)
|
|
471
|
+
if not data:
|
|
472
|
+
return None
|
|
473
|
+
return Charter.from_dict(data)
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File classifier — assigns a purpose label to every file in the project.
|
|
3
|
+
|
|
4
|
+
Philosophy (per supplementary spec §2 offline/AI split):
|
|
5
|
+
80% of files can be classified from their AST and path alone — no AI cost.
|
|
6
|
+
Only genuinely ambiguous files get an AI call.
|
|
7
|
+
|
|
8
|
+
Offline classification uses:
|
|
9
|
+
- File path / name patterns (test_, __init__, cli/, routes/, etc.)
|
|
10
|
+
- Export type (what functions/classes the file defines)
|
|
11
|
+
- Import patterns (what the file imports)
|
|
12
|
+
- File size and structure
|
|
13
|
+
|
|
14
|
+
AI classification (fallback only):
|
|
15
|
+
- Called only when offline pass returns label="unknown"
|
|
16
|
+
- Returns a plain-English one-liner describing the file's purpose
|
|
17
|
+
- Result cached in brain.json so the call only happens once per file
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import ast
|
|
22
|
+
import logging
|
|
23
|
+
import re
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
# ── Offline label lookup ───────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
_PATH_RULES: list[tuple[str, str]] = [
|
|
29
|
+
# Path segment → label
|
|
30
|
+
(r"test[_/]|[_/]test\.py$|tests?/", "test"),
|
|
31
|
+
(r"__init__\.py$", "package init"),
|
|
32
|
+
(r"cli[/\\]|commands?[/\\]", "CLI command"),
|
|
33
|
+
(r"routes?[/\\]|router|handlers?[/\\]", "route handler"),
|
|
34
|
+
(r"middleware[/\\]|middleware\.py", "middleware"),
|
|
35
|
+
(r"models?[/\\]|schemas?[/\\]", "data model / schema"),
|
|
36
|
+
(r"migrations?[/\\]|alembic[/\\]", "database migration"),
|
|
37
|
+
(r"config[/\\]|settings[/\\]|conf\.py", "configuration"),
|
|
38
|
+
(r"constants?\.py$|enums?\.py$", "constants / enums"),
|
|
39
|
+
(r"utils?[/\\]|helpers?[/\\]", "utility functions"),
|
|
40
|
+
(r"static[/\\]|assets?[/\\]", "static asset"),
|
|
41
|
+
(r"templates?[/\\]|views?[/\\]", "template / view"),
|
|
42
|
+
(r"security[/\\]|auth[/\\]", "auth / security"),
|
|
43
|
+
(r"core[/\\]agents?[/\\]|core[/\\]fix[/\\]|core[/\\]security[/\\]", "agent / scanner"),
|
|
44
|
+
(r"core[/\\]|engine[/\\]", "core engine"),
|
|
45
|
+
(r"web[/\\]|server\.py$|api\.py$", "web server / API"),
|
|
46
|
+
(r"agents?[/\\]|scanners?[/\\]", "agent / scanner"),
|
|
47
|
+
(r"brain[/\\]", "brain module"),
|
|
48
|
+
(r"fix[/\\]|fixer[/\\]", "fix agent"),
|
|
49
|
+
(r"notify[/\\]|notification", "notification"),
|
|
50
|
+
(r"queue\.py$|queue[/\\]", "queue system"),
|
|
51
|
+
(r"memory\.py$|memory[/\\]", "memory store"),
|
|
52
|
+
(r"health\.py$", "health checker"),
|
|
53
|
+
(r"setup\.py$|pyproject\.toml$", "package setup"),
|
|
54
|
+
(r"Makefile$|Dockerfile", "build / deploy"),
|
|
55
|
+
(r"\.env$|\.env\.", "environment config"),
|
|
56
|
+
(r"README|CHANGELOG|LICENSE|AGENTS", "documentation"),
|
|
57
|
+
(r"requirements.*\.txt$", "dependency list"),
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
_IMPORT_SIGNALS: dict[str, str] = {
|
|
61
|
+
"fastapi": "FastAPI route handler",
|
|
62
|
+
"flask": "Flask route handler",
|
|
63
|
+
"django": "Django view/model",
|
|
64
|
+
"sqlalchemy": "database model/query",
|
|
65
|
+
"pytest": "test file",
|
|
66
|
+
"unittest": "test file",
|
|
67
|
+
"click": "CLI command",
|
|
68
|
+
"typer": "CLI command",
|
|
69
|
+
"pydantic": "data schema / model",
|
|
70
|
+
"celery": "background task",
|
|
71
|
+
"redis": "cache / queue integration",
|
|
72
|
+
"httpx": "HTTP client",
|
|
73
|
+
"aiohttp": "async HTTP",
|
|
74
|
+
"playwright": "browser automation",
|
|
75
|
+
"selenium": "browser automation",
|
|
76
|
+
"torch": "ML / AI model",
|
|
77
|
+
"tensorflow": "ML / AI model",
|
|
78
|
+
"sklearn": "ML / AI model",
|
|
79
|
+
"logging": "utility / logging",
|
|
80
|
+
"argparse": "CLI entry point",
|
|
81
|
+
"rich": "CLI output formatting",
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
_log = logging.getLogger("patchi.brain.classifier")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def classify_file(rel_path: str, abs_path: Path) -> str:
|
|
89
|
+
"""
|
|
90
|
+
Return a short plain-English purpose label for a file.
|
|
91
|
+
Uses path patterns and AST — no AI call.
|
|
92
|
+
"""
|
|
93
|
+
path_str = rel_path.replace("\\", "/").lower()
|
|
94
|
+
|
|
95
|
+
# 1. Path-based rules — fastest, covers the majority
|
|
96
|
+
for pattern, label in _PATH_RULES:
|
|
97
|
+
if re.search(pattern, path_str):
|
|
98
|
+
return label
|
|
99
|
+
|
|
100
|
+
# 2. Extension quick exits
|
|
101
|
+
ext = abs_path.suffix.lower()
|
|
102
|
+
if ext in {".json", ".yaml", ".yml", ".toml", ".ini", ".cfg"}:
|
|
103
|
+
return "configuration / data file"
|
|
104
|
+
if ext in {".md", ".rst", ".txt"}:
|
|
105
|
+
return "documentation"
|
|
106
|
+
if ext in {".html", ".jinja", ".j2"}:
|
|
107
|
+
return "HTML template"
|
|
108
|
+
if ext in {".css", ".scss", ".less"}:
|
|
109
|
+
return "stylesheet"
|
|
110
|
+
if ext in {".js", ".mjs"} and "test" not in path_str:
|
|
111
|
+
return "JavaScript module"
|
|
112
|
+
if ext in {".ts", ".tsx"} and "test" not in path_str:
|
|
113
|
+
return "TypeScript module"
|
|
114
|
+
|
|
115
|
+
if ext != ".py":
|
|
116
|
+
return "source file"
|
|
117
|
+
|
|
118
|
+
# 3. AST — read imports and top-level definitions
|
|
119
|
+
try:
|
|
120
|
+
src = abs_path.read_text(encoding="utf-8", errors="ignore")
|
|
121
|
+
tree = ast.parse(src)
|
|
122
|
+
except (SyntaxError, OSError):
|
|
123
|
+
return "Python module"
|
|
124
|
+
|
|
125
|
+
imports: list[str] = []
|
|
126
|
+
top_fns: list[str] = []
|
|
127
|
+
top_cls: list[str] = []
|
|
128
|
+
|
|
129
|
+
for node in ast.walk(tree):
|
|
130
|
+
if isinstance(node, ast.Import):
|
|
131
|
+
for a in node.names:
|
|
132
|
+
imports.append(a.name.split(".")[0].lower())
|
|
133
|
+
elif isinstance(node, ast.ImportFrom) and node.module:
|
|
134
|
+
imports.append(node.module.split(".")[0].lower())
|
|
135
|
+
elif isinstance(node, ast.FunctionDef):
|
|
136
|
+
if node.col_offset == 0:
|
|
137
|
+
top_fns.append(node.name)
|
|
138
|
+
elif isinstance(node, ast.ClassDef):
|
|
139
|
+
if node.col_offset == 0:
|
|
140
|
+
top_cls.append(node.name)
|
|
141
|
+
|
|
142
|
+
# 4. Import signal match
|
|
143
|
+
for imp in imports:
|
|
144
|
+
if imp in _IMPORT_SIGNALS:
|
|
145
|
+
return _IMPORT_SIGNALS[imp]
|
|
146
|
+
|
|
147
|
+
# 5. Top-level structure heuristics
|
|
148
|
+
fn_names = " ".join(top_fns).lower()
|
|
149
|
+
cl_names = " ".join(top_cls).lower()
|
|
150
|
+
all_names = fn_names + " " + cl_names
|
|
151
|
+
|
|
152
|
+
if re.search(r"\btest_\w+|\b\w+_test\b", fn_names):
|
|
153
|
+
return "test file"
|
|
154
|
+
if re.search(r"\bget_|post_|put_|delete_|patch_|handle_", fn_names):
|
|
155
|
+
return "request handler"
|
|
156
|
+
if re.search(r"\bscanner\b|\bscanner\b|\bdetect\b|\bcheck\b|\baudit\b", cl_names.lower()):
|
|
157
|
+
return "scanner / detector"
|
|
158
|
+
if re.search(r"\bagent\b|\bworker\b|\bcoordinator\b", all_names):
|
|
159
|
+
return "agent / worker"
|
|
160
|
+
if re.search(r"\bmodel\b|\bschema\b|\bentity\b", all_names):
|
|
161
|
+
return "data model"
|
|
162
|
+
if re.search(r"\bconfig\b|\bsetting\b|\boption\b", all_names):
|
|
163
|
+
return "configuration"
|
|
164
|
+
if re.search(r"\bcommand\b|\bcli\b|\bmain\b", fn_names) and "__main__" in src:
|
|
165
|
+
return "CLI entry point"
|
|
166
|
+
if re.search(r"\bcreate_app\b|\bapp = \b", src):
|
|
167
|
+
return "application factory"
|
|
168
|
+
|
|
169
|
+
return "Python module"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def batch_classify(
|
|
173
|
+
files: list, # list[FileInfo]
|
|
174
|
+
root: Path,
|
|
175
|
+
ai_config: dict | None = None,
|
|
176
|
+
max_ai_calls: int = 20,
|
|
177
|
+
) -> dict[str, str]:
|
|
178
|
+
"""
|
|
179
|
+
Classify all files. Returns {rel_path: purpose_label}.
|
|
180
|
+
AI is called only for truly unknown files, capped at max_ai_calls.
|
|
181
|
+
"""
|
|
182
|
+
result: dict[str, str] = {}
|
|
183
|
+
needs_ai: list[str] = []
|
|
184
|
+
|
|
185
|
+
for fi in files:
|
|
186
|
+
abs_path = root / fi.path
|
|
187
|
+
label = classify_file(fi.path, abs_path)
|
|
188
|
+
if label == "Python module" and ai_config:
|
|
189
|
+
needs_ai.append(fi.path)
|
|
190
|
+
else:
|
|
191
|
+
result[fi.path] = label
|
|
192
|
+
|
|
193
|
+
# AI pass — only for genuinely ambiguous Python files
|
|
194
|
+
if ai_config and needs_ai:
|
|
195
|
+
from patchi.core.fix.base import _call_ai
|
|
196
|
+
|
|
197
|
+
for rel_path in needs_ai[:max_ai_calls]:
|
|
198
|
+
abs_path = root / rel_path
|
|
199
|
+
try:
|
|
200
|
+
snippet = (root / rel_path).read_text(encoding="utf-8", errors="ignore")[:600]
|
|
201
|
+
prompt = (
|
|
202
|
+
f"What does this Python file do? Answer in one sentence, plain English, "
|
|
203
|
+
f"no code. File: {rel_path}\n\n{snippet}"
|
|
204
|
+
)
|
|
205
|
+
label = _call_ai(prompt, ai_config, max_tokens=60).strip()
|
|
206
|
+
result[rel_path] = label or "Python module"
|
|
207
|
+
except Exception as e:
|
|
208
|
+
_log.warning("batch_classify failed: %s", e)
|
|
209
|
+
result[rel_path] = "Python module"
|
|
210
|
+
# remaining uncalled files
|
|
211
|
+
for rel_path in needs_ai[max_ai_calls:]:
|
|
212
|
+
result[rel_path] = "Python module"
|
|
213
|
+
|
|
214
|
+
return result
|