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,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Chain-to-Assurance Bridge — feeds exploit chains and intent gaps into
|
|
3
|
+
the assurance graph as disproved claims with evidence trails.
|
|
4
|
+
|
|
5
|
+
Each multi-step chain becomes a claim like "auth bypass leads to data
|
|
6
|
+
exfiltration via 4-step chain" with refuting evidence from each step.
|
|
7
|
+
Each intent gap becomes a claim like "state-changing route lacks auth"
|
|
8
|
+
with refuting evidence from the route analysis.
|
|
9
|
+
|
|
10
|
+
This lets the assurance page show cross-file attack paths alongside
|
|
11
|
+
the invariant-based claims.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
_log = logging.getLogger("patchi.security.chain_to_assurance")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def feed_chains_to_graph(root: Path) -> int:
|
|
25
|
+
"""Load persisted chain/intent data and upsert into the assurance graph.
|
|
26
|
+
|
|
27
|
+
Returns the number of claims added/updated.
|
|
28
|
+
"""
|
|
29
|
+
ci_path = root / ".patchi" / "chain_intent.json"
|
|
30
|
+
if not ci_path.is_file():
|
|
31
|
+
return 0
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
ci_data = json.loads(ci_path.read_text(encoding="utf-8"))
|
|
35
|
+
except Exception as exc:
|
|
36
|
+
_log.debug("Failed to load chain_intent.json: %s", exc)
|
|
37
|
+
return 0
|
|
38
|
+
|
|
39
|
+
chains = ci_data.get("chains", [])
|
|
40
|
+
intent = ci_data.get("intent_report")
|
|
41
|
+
if not chains and not intent:
|
|
42
|
+
return 0
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
from patchi.core.assurance.graph import AssuranceGraph, Evidence
|
|
46
|
+
except ImportError as exc:
|
|
47
|
+
_log.debug("Assurance graph not available: %s", exc)
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
graph = AssuranceGraph.load(root)
|
|
51
|
+
count = 0
|
|
52
|
+
|
|
53
|
+
# ── Chains → Claims ──────────────────────────────────────────────────
|
|
54
|
+
for i, chain in enumerate(chains):
|
|
55
|
+
severity = chain.get("severity", "medium")
|
|
56
|
+
score = chain.get("score", 0)
|
|
57
|
+
steps = chain.get("steps", [])
|
|
58
|
+
narrative = chain.get("narrative", "")
|
|
59
|
+
|
|
60
|
+
if len(steps) < 2:
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
claim_id = f"chain-{i + 1}-{'-'.join(s.get('type', 'unknown')[:10] for s in steps[:3])}"
|
|
64
|
+
entry_type = steps[0].get("type", "unknown")
|
|
65
|
+
impact_type = steps[-1].get("type", "unknown")
|
|
66
|
+
|
|
67
|
+
statement = (
|
|
68
|
+
f"Attack chain: {entry_type} → {impact_type} "
|
|
69
|
+
f"({len(steps)} steps, severity={severity}, score={score:.0f})"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
graph.upsert_claim(
|
|
73
|
+
claim_id=claim_id,
|
|
74
|
+
statement=statement,
|
|
75
|
+
domain="exploit-chain",
|
|
76
|
+
severity_if_disproved=severity,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Each step is refuting evidence (the chain proves the path exists)
|
|
80
|
+
for j, step in enumerate(steps):
|
|
81
|
+
# Attach remediation suggestion
|
|
82
|
+
step_type = step.get("type", "")
|
|
83
|
+
try:
|
|
84
|
+
from patchi.core.security.remediation import get_remediation
|
|
85
|
+
|
|
86
|
+
rem = get_remediation(step_type)
|
|
87
|
+
fix_hint = rem.action if rem else ""
|
|
88
|
+
except ImportError:
|
|
89
|
+
fix_hint = ""
|
|
90
|
+
|
|
91
|
+
ev = Evidence(
|
|
92
|
+
source=f"chain_analyzer.step{j + 1}",
|
|
93
|
+
detail=f"[{step.get('role', '?')}] {step.get('type', '?')} @ {step.get('file', '?')}:{step.get('line', '?')}"
|
|
94
|
+
+ (f" → FIX: {fix_hint}" if fix_hint else ""),
|
|
95
|
+
supports=False, # refuting = vulnerability confirmed
|
|
96
|
+
timestamp=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
97
|
+
artifact={
|
|
98
|
+
"step": j + 1,
|
|
99
|
+
"role": step.get("role", ""),
|
|
100
|
+
"agent": step.get("agent", ""),
|
|
101
|
+
"remediation": fix_hint,
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
graph.attach_evidence(claim_id, ev)
|
|
105
|
+
count += 1
|
|
106
|
+
|
|
107
|
+
# Narrative evidence
|
|
108
|
+
if narrative:
|
|
109
|
+
ev = Evidence(
|
|
110
|
+
source="chain_analyzer.narrative",
|
|
111
|
+
detail=narrative[:500],
|
|
112
|
+
supports=False,
|
|
113
|
+
timestamp=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
114
|
+
)
|
|
115
|
+
graph.attach_evidence(claim_id, ev)
|
|
116
|
+
count += 1
|
|
117
|
+
|
|
118
|
+
# ── Intent Gaps → Claims ─────────────────────────────────────────────
|
|
119
|
+
if intent:
|
|
120
|
+
gap_categories = [
|
|
121
|
+
("unauthenticated_state_changing", "State-changing routes lack authentication", "high"),
|
|
122
|
+
("admin_without_strict_guard", "Admin routes have no strict auth guard", "high"),
|
|
123
|
+
("unprotected_among_protected", "Routes unprotected among protected peers", "medium"),
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
for cat_key, description, sev in gap_categories:
|
|
127
|
+
routes = intent.get(cat_key, [])
|
|
128
|
+
if not routes:
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
claim_id = f"intent-gap-{cat_key}"
|
|
132
|
+
statement = f"{description} ({len(routes)} routes)"
|
|
133
|
+
|
|
134
|
+
graph.upsert_claim(
|
|
135
|
+
claim_id=claim_id,
|
|
136
|
+
statement=statement,
|
|
137
|
+
domain="intent-gap",
|
|
138
|
+
severity_if_disproved=sev,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
for r in routes:
|
|
142
|
+
method = r.get("method", "?")
|
|
143
|
+
path = r.get("path", "?")
|
|
144
|
+
file_ = r.get("file", "?")
|
|
145
|
+
line = r.get("line", "?")
|
|
146
|
+
has_guard = r.get("has_auth_guard", False)
|
|
147
|
+
|
|
148
|
+
ev = Evidence(
|
|
149
|
+
source="intent_analyzer",
|
|
150
|
+
detail=f"{method} {path} @ {file_}:{line} (guard={'yes' if has_guard else 'no'})",
|
|
151
|
+
supports=False,
|
|
152
|
+
timestamp=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
153
|
+
artifact={"route": path, "method": method, "has_guard": has_guard},
|
|
154
|
+
)
|
|
155
|
+
graph.attach_evidence(claim_id, ev)
|
|
156
|
+
count += 1
|
|
157
|
+
|
|
158
|
+
# Save
|
|
159
|
+
if count > 0:
|
|
160
|
+
graph.save(root)
|
|
161
|
+
_log.info(
|
|
162
|
+
"Fed %d evidence items into assurance graph (chains=%d, intent gaps=%d)",
|
|
163
|
+
count,
|
|
164
|
+
len(chains),
|
|
165
|
+
len([c for c in gap_categories if intent.get(c[0])]) if intent else 0,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return count
|
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Charter — Project Guard Rails.
|
|
3
|
+
|
|
4
|
+
Lets the user declare what the project *should be* via structured rules:
|
|
5
|
+
|
|
6
|
+
p charter set "Frontend must not import backend"
|
|
7
|
+
p charter set "All files must have headers"
|
|
8
|
+
p charter set "No hardcoded secrets"
|
|
9
|
+
|
|
10
|
+
Rules are stored in ``.patchi/memory/charter.json`` with explicit fields
|
|
11
|
+
(no regex/heuristics — structured matching only).
|
|
12
|
+
|
|
13
|
+
Three enforcement points:
|
|
14
|
+
1. Pre-commit (humans): checks changed files against charter
|
|
15
|
+
2. Watch mode (realtime): flags violations on file save
|
|
16
|
+
3. Governor (agents): charter rules become escalation triggers
|
|
17
|
+
|
|
18
|
+
Rule types:
|
|
19
|
+
boundary — forbidden import edges (from -> to)
|
|
20
|
+
convention — file/line limits, naming, test requirements
|
|
21
|
+
security — forbidden patterns (secrets, eval, pickle)
|
|
22
|
+
stack — allowed languages, frameworks
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import logging
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from enum import StrEnum
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
from patchi.core.config import MEMORY_DIR
|
|
35
|
+
|
|
36
|
+
_log = logging.getLogger("patchi.charter")
|
|
37
|
+
|
|
38
|
+
CHARTER_FILE = "charter.json"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RuleType(StrEnum):
|
|
42
|
+
STACK = "stack"
|
|
43
|
+
BOUNDARY = "boundary"
|
|
44
|
+
CONVENTION = "convention"
|
|
45
|
+
SECURITY = "security"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Severity(StrEnum):
|
|
49
|
+
CRITICAL = "critical"
|
|
50
|
+
HIGH = "high"
|
|
51
|
+
MEDIUM = "medium"
|
|
52
|
+
LOW = "low"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class CharterRule:
|
|
57
|
+
"""A single project guard-rail rule with explicit fields."""
|
|
58
|
+
|
|
59
|
+
id: str
|
|
60
|
+
type: RuleType
|
|
61
|
+
description: str
|
|
62
|
+
severity: Severity = Severity.MEDIUM
|
|
63
|
+
enabled: bool = True
|
|
64
|
+
|
|
65
|
+
# Boundary fields
|
|
66
|
+
from_pattern: str = "" # source subsystem/path prefix
|
|
67
|
+
to_pattern: str = "" # target subsystem/path prefix
|
|
68
|
+
|
|
69
|
+
# Convention fields
|
|
70
|
+
max_value: int = 0 # max lines/size
|
|
71
|
+
metric: str = "" # "lines", "file_size", "functions"
|
|
72
|
+
scope: str = "" # "functions", "classes", "all_files"
|
|
73
|
+
|
|
74
|
+
# Security fields
|
|
75
|
+
pattern_type: str = "" # "forbidden_keywords", "forbidden_imports"
|
|
76
|
+
keywords: list[str] = field(default_factory=list)
|
|
77
|
+
|
|
78
|
+
# Stack fields
|
|
79
|
+
allowed_languages: list[str] = field(default_factory=list)
|
|
80
|
+
allowed_frameworks: list[str] = field(default_factory=list)
|
|
81
|
+
|
|
82
|
+
# Generic metadata
|
|
83
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
84
|
+
|
|
85
|
+
def to_dict(self) -> dict[str, Any]:
|
|
86
|
+
d = {
|
|
87
|
+
"id": self.id,
|
|
88
|
+
"type": self.type.value,
|
|
89
|
+
"description": self.description,
|
|
90
|
+
"severity": self.severity.value,
|
|
91
|
+
"enabled": self.enabled,
|
|
92
|
+
}
|
|
93
|
+
if self.from_pattern:
|
|
94
|
+
d["from_pattern"] = self.from_pattern
|
|
95
|
+
if self.to_pattern:
|
|
96
|
+
d["to_pattern"] = self.to_pattern
|
|
97
|
+
if self.max_value:
|
|
98
|
+
d["max_value"] = self.max_value
|
|
99
|
+
if self.metric:
|
|
100
|
+
d["metric"] = self.metric
|
|
101
|
+
if self.scope:
|
|
102
|
+
d["scope"] = self.scope
|
|
103
|
+
if self.keywords:
|
|
104
|
+
d["keywords"] = self.keywords
|
|
105
|
+
if self.allowed_languages:
|
|
106
|
+
d["allowed_languages"] = self.allowed_languages
|
|
107
|
+
if self.allowed_frameworks:
|
|
108
|
+
d["allowed_frameworks"] = self.allowed_frameworks
|
|
109
|
+
if self.metadata:
|
|
110
|
+
d["metadata"] = self.metadata
|
|
111
|
+
return d
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def from_dict(cls, d: dict[str, Any]) -> CharterRule:
|
|
115
|
+
return cls(
|
|
116
|
+
id=d["id"],
|
|
117
|
+
type=RuleType(d["type"]),
|
|
118
|
+
description=d.get("description", ""),
|
|
119
|
+
severity=Severity(d.get("severity", "medium")),
|
|
120
|
+
enabled=d.get("enabled", True),
|
|
121
|
+
from_pattern=d.get("from_pattern", ""),
|
|
122
|
+
to_pattern=d.get("to_pattern", ""),
|
|
123
|
+
max_value=d.get("max_value", 0),
|
|
124
|
+
metric=d.get("metric", ""),
|
|
125
|
+
scope=d.get("scope", ""),
|
|
126
|
+
keywords=d.get("keywords", []),
|
|
127
|
+
allowed_languages=d.get("allowed_languages", []),
|
|
128
|
+
allowed_frameworks=d.get("allowed_frameworks", []),
|
|
129
|
+
metadata=d.get("metadata", {}),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class CharterViolation:
|
|
135
|
+
"""A detected violation of a charter rule."""
|
|
136
|
+
|
|
137
|
+
rule_id: str
|
|
138
|
+
rule_description: str
|
|
139
|
+
severity: str
|
|
140
|
+
file_path: str = ""
|
|
141
|
+
line: int = 0
|
|
142
|
+
message: str = ""
|
|
143
|
+
suggestion: str = ""
|
|
144
|
+
|
|
145
|
+
def to_dict(self) -> dict:
|
|
146
|
+
return {
|
|
147
|
+
"rule_id": self.rule_id,
|
|
148
|
+
"rule_description": self.rule_description,
|
|
149
|
+
"severity": self.severity,
|
|
150
|
+
"file_path": self.file_path,
|
|
151
|
+
"line": self.line,
|
|
152
|
+
"message": self.message,
|
|
153
|
+
"suggestion": self.suggestion,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass
|
|
158
|
+
class Charter:
|
|
159
|
+
"""The full project charter — a collection of guard-rail rules."""
|
|
160
|
+
|
|
161
|
+
text: str = ""
|
|
162
|
+
rules: list[CharterRule] = field(default_factory=list)
|
|
163
|
+
created_at: str = ""
|
|
164
|
+
updated_at: str = ""
|
|
165
|
+
|
|
166
|
+
def to_dict(self) -> dict:
|
|
167
|
+
return {
|
|
168
|
+
"text": self.text,
|
|
169
|
+
"rules": [r.to_dict() for r in self.rules],
|
|
170
|
+
"created_at": self.created_at,
|
|
171
|
+
"updated_at": self.updated_at,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
def from_dict(cls, d: dict) -> Charter:
|
|
176
|
+
return cls(
|
|
177
|
+
text=d.get("text", ""),
|
|
178
|
+
rules=[CharterRule.from_dict(r) for r in d.get("rules", [])],
|
|
179
|
+
created_at=d.get("created_at", ""),
|
|
180
|
+
updated_at=d.get("updated_at", ""),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def add_rule(self, text: str) -> CharterRule | None:
|
|
184
|
+
"""Add a rule from natural language text using structured parsing."""
|
|
185
|
+
rules = parse_nl_to_rules(text)
|
|
186
|
+
if rules:
|
|
187
|
+
self.rules.extend(rules)
|
|
188
|
+
return rules[0]
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
@classmethod
|
|
192
|
+
def load(cls, root: Path | None = None) -> Charter:
|
|
193
|
+
"""Load charter from disk, or return empty."""
|
|
194
|
+
return load_charter(root)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ── Structured NL Parser (no regex) ─────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _classify_text(text: str) -> RuleType:
|
|
201
|
+
"""Classify natural language text into a rule type using keyword matching."""
|
|
202
|
+
lower = text.lower()
|
|
203
|
+
|
|
204
|
+
# Boundary: must/must not/cannot import, never import
|
|
205
|
+
boundary_words = ["must not import", "cannot import", "never import",
|
|
206
|
+
"should not import", "must not use", "cannot use"]
|
|
207
|
+
if any(w in lower for w in boundary_words):
|
|
208
|
+
return RuleType.BOUNDARY
|
|
209
|
+
|
|
210
|
+
# Convention: limits, naming, tests
|
|
211
|
+
convention_words = ["under", "below", "less than", "max", "maximum",
|
|
212
|
+
"must have", "requires tests", "need tests",
|
|
213
|
+
"all files must", "every file", "snake_case", "camelCase"]
|
|
214
|
+
if any(w in lower for w in convention_words):
|
|
215
|
+
return RuleType.CONVENTION
|
|
216
|
+
|
|
217
|
+
# Security: secrets, eval, pickle
|
|
218
|
+
security_words = ["no secrets", "no hardcoded", "no eval", "no pickle",
|
|
219
|
+
"no marshal", "no debug", "use https", "parameterized"]
|
|
220
|
+
if any(w in lower for w in security_words):
|
|
221
|
+
return RuleType.SECURITY
|
|
222
|
+
|
|
223
|
+
# Stack: languages, frameworks
|
|
224
|
+
stack_words = ["only python", "only javascript", "use go", "use rust",
|
|
225
|
+
"frameworks allowed", "languages allowed", "stack is"]
|
|
226
|
+
if any(w in lower for w in stack_words):
|
|
227
|
+
return RuleType.STACK
|
|
228
|
+
|
|
229
|
+
return RuleType.CONVENTION # default
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _parse_boundary(text: str) -> CharterRule:
|
|
233
|
+
"""Parse a boundary rule from text."""
|
|
234
|
+
lower = text.lower()
|
|
235
|
+
|
|
236
|
+
# Extract source and target from "X must not import Y"
|
|
237
|
+
from_pattern = ""
|
|
238
|
+
to_pattern = ""
|
|
239
|
+
|
|
240
|
+
# Try to find the pattern: "X must not import Y" or "X cannot import Y"
|
|
241
|
+
for sep in ["must not import", "cannot import", "never import",
|
|
242
|
+
"should not import", "must not use", "cannot use"]:
|
|
243
|
+
if sep in lower:
|
|
244
|
+
parts = lower.split(sep)
|
|
245
|
+
if len(parts) == 2:
|
|
246
|
+
from_pattern = parts[0].strip().rstrip()
|
|
247
|
+
to_pattern = parts[1].strip().lstrip()
|
|
248
|
+
break
|
|
249
|
+
|
|
250
|
+
# Clean up extracted patterns
|
|
251
|
+
# Remove leading articles/determiners
|
|
252
|
+
for prefix in ["the ", "a ", "an "]:
|
|
253
|
+
if from_pattern.startswith(prefix):
|
|
254
|
+
from_pattern = from_pattern[len(prefix):]
|
|
255
|
+
if to_pattern.startswith(prefix):
|
|
256
|
+
to_pattern = to_pattern[len(prefix):]
|
|
257
|
+
|
|
258
|
+
return CharterRule(
|
|
259
|
+
id="b-001",
|
|
260
|
+
type=RuleType.BOUNDARY,
|
|
261
|
+
description=text,
|
|
262
|
+
severity=Severity.HIGH,
|
|
263
|
+
from_pattern=from_pattern,
|
|
264
|
+
to_pattern=to_pattern,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _parse_convention(text: str) -> CharterRule:
|
|
269
|
+
"""Parse a convention rule from text."""
|
|
270
|
+
lower = text.lower()
|
|
271
|
+
|
|
272
|
+
# Extract numeric limits
|
|
273
|
+
max_value = 0
|
|
274
|
+
metric = ""
|
|
275
|
+
scope = ""
|
|
276
|
+
|
|
277
|
+
# Try "under N lines" pattern
|
|
278
|
+
if "under" in lower or "below" in lower or "less than" in lower:
|
|
279
|
+
for word in lower.split():
|
|
280
|
+
if word.isdigit():
|
|
281
|
+
max_value = int(word)
|
|
282
|
+
break
|
|
283
|
+
if "lines" in lower:
|
|
284
|
+
metric = "lines"
|
|
285
|
+
# Extract scope
|
|
286
|
+
for s in ["file", "files", "function", "functions",
|
|
287
|
+
"class", "classes", "service", "services"]:
|
|
288
|
+
if s in lower:
|
|
289
|
+
scope = s + "s" if not s.endswith("s") else s
|
|
290
|
+
break
|
|
291
|
+
|
|
292
|
+
# Try "must have tests" pattern
|
|
293
|
+
if "must have" in lower or "need" in lower or "require" in lower:
|
|
294
|
+
if "test" in lower:
|
|
295
|
+
scope = "tests_required"
|
|
296
|
+
|
|
297
|
+
# Try naming conventions
|
|
298
|
+
if "snake_case" in lower:
|
|
299
|
+
scope = "snake_case"
|
|
300
|
+
elif "camelCase" in lower:
|
|
301
|
+
scope = "camel_case"
|
|
302
|
+
elif "PascalCase" in lower:
|
|
303
|
+
scope = "pascal_case"
|
|
304
|
+
|
|
305
|
+
# Try "all files must have X" pattern
|
|
306
|
+
if "all files must" in lower or "every file" in lower:
|
|
307
|
+
if "header" in lower:
|
|
308
|
+
scope = "headers_required"
|
|
309
|
+
elif "docstring" in lower:
|
|
310
|
+
scope = "docstrings_required"
|
|
311
|
+
|
|
312
|
+
return CharterRule(
|
|
313
|
+
id="c-001",
|
|
314
|
+
type=RuleType.CONVENTION,
|
|
315
|
+
description=text,
|
|
316
|
+
severity=Severity.MEDIUM,
|
|
317
|
+
max_value=max_value,
|
|
318
|
+
metric=metric,
|
|
319
|
+
scope=scope,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _parse_security(text: str) -> CharterRule:
|
|
324
|
+
"""Parse a security rule from text."""
|
|
325
|
+
lower = text.lower()
|
|
326
|
+
|
|
327
|
+
# Extract forbidden keywords
|
|
328
|
+
keywords = []
|
|
329
|
+
if "hardcoded" in lower or "hard coded" in lower:
|
|
330
|
+
keywords.extend(["password", "secret", "api_key", "token"])
|
|
331
|
+
if "eval" in lower:
|
|
332
|
+
keywords.append("eval")
|
|
333
|
+
if "pickle" in lower:
|
|
334
|
+
keywords.append("pickle")
|
|
335
|
+
if "marshal" in lower:
|
|
336
|
+
keywords.append("marshal")
|
|
337
|
+
if "debug" in lower:
|
|
338
|
+
keywords.append("debug")
|
|
339
|
+
if "http" in lower and "https" not in lower:
|
|
340
|
+
keywords.append("http://")
|
|
341
|
+
if "parameterized" in lower:
|
|
342
|
+
keywords.append("raw_sql")
|
|
343
|
+
|
|
344
|
+
return CharterRule(
|
|
345
|
+
id="s-001",
|
|
346
|
+
type=RuleType.SECURITY,
|
|
347
|
+
description=text,
|
|
348
|
+
severity=Severity.HIGH if keywords else Severity.MEDIUM,
|
|
349
|
+
pattern_type="forbidden_keywords",
|
|
350
|
+
keywords=keywords,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _parse_stack(text: str) -> CharterRule:
|
|
355
|
+
"""Parse a stack rule from text."""
|
|
356
|
+
lower = text.lower()
|
|
357
|
+
|
|
358
|
+
# Extract allowed languages
|
|
359
|
+
allowed_languages = []
|
|
360
|
+
lang_keywords = {
|
|
361
|
+
"python": "python", "javascript": "javascript", "typescript": "typescript",
|
|
362
|
+
"go": "go", "rust": "rust", "java": "java", "c#": "csharp",
|
|
363
|
+
}
|
|
364
|
+
for kw, lang in lang_keywords.items():
|
|
365
|
+
if kw in lower:
|
|
366
|
+
allowed_languages.append(lang)
|
|
367
|
+
|
|
368
|
+
return CharterRule(
|
|
369
|
+
id="t-001",
|
|
370
|
+
type=RuleType.STACK,
|
|
371
|
+
description=text,
|
|
372
|
+
severity=Severity.MEDIUM,
|
|
373
|
+
allowed_languages=allowed_languages,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def parse_nl_to_rules(text: str) -> list[CharterRule]:
|
|
378
|
+
"""Parse natural language into structured rules (no regex, no heuristics).
|
|
379
|
+
|
|
380
|
+
Uses keyword matching to classify text into rule types, then extracts
|
|
381
|
+
explicit fields for each rule type.
|
|
382
|
+
"""
|
|
383
|
+
rules = []
|
|
384
|
+
rule_type = _classify_text(text)
|
|
385
|
+
|
|
386
|
+
if rule_type == RuleType.BOUNDARY:
|
|
387
|
+
rules.append(_parse_boundary(text))
|
|
388
|
+
elif rule_type == RuleType.CONVENTION:
|
|
389
|
+
rules.append(_parse_convention(text))
|
|
390
|
+
elif rule_type == RuleType.SECURITY:
|
|
391
|
+
rules.append(_parse_security(text))
|
|
392
|
+
elif rule_type == RuleType.STACK:
|
|
393
|
+
rules.append(_parse_stack(text))
|
|
394
|
+
|
|
395
|
+
return rules
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
# ── Rule Checking (structured matching) ──────────────────────────────────────
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def check_boundary_violations(
|
|
402
|
+
charter: Charter,
|
|
403
|
+
import_edges: list[tuple[str, str]],
|
|
404
|
+
) -> list[CharterViolation]:
|
|
405
|
+
"""Check if import edges violate boundary rules using structured matching."""
|
|
406
|
+
violations: list[CharterViolation] = []
|
|
407
|
+
boundary_rules = [r for r in charter.rules
|
|
408
|
+
if r.type == RuleType.BOUNDARY and r.enabled]
|
|
409
|
+
|
|
410
|
+
for rule in boundary_rules:
|
|
411
|
+
from_pat = rule.from_pattern.lower()
|
|
412
|
+
to_pat = rule.to_pattern.lower()
|
|
413
|
+
|
|
414
|
+
if not from_pat or not to_pat:
|
|
415
|
+
continue
|
|
416
|
+
|
|
417
|
+
for src, dst in import_edges:
|
|
418
|
+
# Check if source matches from_pattern and target matches to_pattern
|
|
419
|
+
src_lower = src.lower()
|
|
420
|
+
dst_lower = dst.lower()
|
|
421
|
+
|
|
422
|
+
# Match using prefix matching (structured, not regex)
|
|
423
|
+
if from_pat in src_lower and to_pat in dst_lower:
|
|
424
|
+
violations.append(CharterViolation(
|
|
425
|
+
rule_id=rule.id,
|
|
426
|
+
rule_description=rule.description,
|
|
427
|
+
severity=rule.severity.value,
|
|
428
|
+
file_path=src,
|
|
429
|
+
message=f"'{src}' imports '{dst}' — violates boundary rule",
|
|
430
|
+
suggestion=f"Remove import of {to_pat} from {from_pat}",
|
|
431
|
+
))
|
|
432
|
+
|
|
433
|
+
return violations
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def check_convention_violations(
|
|
437
|
+
charter: Charter,
|
|
438
|
+
file_infos: list[Any],
|
|
439
|
+
) -> list[CharterViolation]:
|
|
440
|
+
"""Check files against convention rules using structured matching."""
|
|
441
|
+
violations: list[CharterViolation] = []
|
|
442
|
+
conv_rules = [r for r in charter.rules
|
|
443
|
+
if r.type == RuleType.CONVENTION and r.enabled]
|
|
444
|
+
|
|
445
|
+
for rule in conv_rules:
|
|
446
|
+
if rule.max_value > 0 and rule.metric == "lines":
|
|
447
|
+
for fi in file_infos:
|
|
448
|
+
path = getattr(fi, "path", "")
|
|
449
|
+
if path.endswith((".py", ".ts", ".js", ".go", ".java", ".rs")):
|
|
450
|
+
line_count = getattr(fi, "line_count", 0)
|
|
451
|
+
if line_count and line_count > rule.max_value:
|
|
452
|
+
violations.append(CharterViolation(
|
|
453
|
+
rule_id=rule.id,
|
|
454
|
+
rule_description=rule.description,
|
|
455
|
+
severity=rule.severity.value,
|
|
456
|
+
file_path=path,
|
|
457
|
+
message=f"{path} is {line_count} lines (max {rule.max_value})",
|
|
458
|
+
suggestion=f"Split into modules under {rule.max_value} lines",
|
|
459
|
+
))
|
|
460
|
+
|
|
461
|
+
return violations
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def check_all_violations(
|
|
465
|
+
charter: Charter,
|
|
466
|
+
import_edges: list[tuple[str, str]] | None = None,
|
|
467
|
+
file_infos: list[Any] | None = None,
|
|
468
|
+
) -> list[CharterViolation]:
|
|
469
|
+
"""Run all charter checks and return combined violations."""
|
|
470
|
+
violations: list[CharterViolation] = []
|
|
471
|
+
|
|
472
|
+
if import_edges:
|
|
473
|
+
violations.extend(check_boundary_violations(charter, import_edges))
|
|
474
|
+
|
|
475
|
+
if file_infos:
|
|
476
|
+
violations.extend(check_convention_violations(charter, file_infos))
|
|
477
|
+
|
|
478
|
+
return violations
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
# ── Persistence ───────────────────────────────────────────────────────────────
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _charter_path(root: Path | None = None) -> Path:
|
|
485
|
+
r = root or Path.cwd()
|
|
486
|
+
return r / MEMORY_DIR / CHARTER_FILE
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def load_charter(root: Path | None = None) -> Charter:
|
|
490
|
+
"""Load charter from disk, or return empty."""
|
|
491
|
+
path = _charter_path(root)
|
|
492
|
+
if not path.exists():
|
|
493
|
+
return Charter()
|
|
494
|
+
try:
|
|
495
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
496
|
+
return Charter.from_dict(data)
|
|
497
|
+
except Exception:
|
|
498
|
+
_log.warning("Failed to load charter from %s", path)
|
|
499
|
+
return Charter()
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def save_charter(charter: Charter, root: Path | None = None) -> Path:
|
|
503
|
+
"""Save charter to disk."""
|
|
504
|
+
path = _charter_path(root)
|
|
505
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
506
|
+
path.write_text(
|
|
507
|
+
json.dumps(charter.to_dict(), indent=2, ensure_ascii=False),
|
|
508
|
+
encoding="utf-8",
|
|
509
|
+
)
|
|
510
|
+
return path
|