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,620 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Proactive Auto-Fix — Fix charter violations on file save/commit.
|
|
3
|
+
|
|
4
|
+
Automatically fixes SAFE issues (imports, formatting, headers) while
|
|
5
|
+
leaving DANGEROUS issues (secrets, eval, imports) for manual review.
|
|
6
|
+
|
|
7
|
+
Rule types and auto-fix mapping:
|
|
8
|
+
boundary → flag only (requires human review)
|
|
9
|
+
convention → auto-fix if safe (formatting, headers, naming)
|
|
10
|
+
security → flag only (requires human review)
|
|
11
|
+
stack → flag only (requires human review)
|
|
12
|
+
|
|
13
|
+
Safe auto-fixes:
|
|
14
|
+
- headers_required: Add missing file header
|
|
15
|
+
- snake_case / camel_case: Rename variables
|
|
16
|
+
- lines: Flag only (splitting requires judgment)
|
|
17
|
+
- docstrings_required: Add empty docstring placeholder
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from patchi.core.security.charter import (
|
|
27
|
+
CharterRule,
|
|
28
|
+
RuleType,
|
|
29
|
+
load_charter,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_log = logging.getLogger("patchi.security.auto_fix_proactive")
|
|
33
|
+
|
|
34
|
+
# ── Auto-Fix Strategy Mapping ────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
# Convention rules that are safe to auto-fix
|
|
37
|
+
SAFE_AUTO_FIX_SCOPES = {
|
|
38
|
+
"headers_required",
|
|
39
|
+
"docstrings_required",
|
|
40
|
+
"snake_case",
|
|
41
|
+
"camel_case",
|
|
42
|
+
"pascal_case",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Convention rules that require human review (too risky to auto-fix)
|
|
46
|
+
RISKY_SCOPES = {
|
|
47
|
+
"lines",
|
|
48
|
+
"file_size",
|
|
49
|
+
"functions",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class ProactiveFixResult:
|
|
55
|
+
"""Result of proactive fix attempt on a single file."""
|
|
56
|
+
|
|
57
|
+
file_path: str
|
|
58
|
+
rule_id: str
|
|
59
|
+
rule_type: str
|
|
60
|
+
scope: str
|
|
61
|
+
auto_fixed: bool = False
|
|
62
|
+
changes_made: list[str] = field(default_factory=list)
|
|
63
|
+
blocked_reason: str = ""
|
|
64
|
+
original_content: str = ""
|
|
65
|
+
fixed_content: str = ""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class ProactiveFixReport:
|
|
70
|
+
"""Aggregated result of proactive fix scan."""
|
|
71
|
+
|
|
72
|
+
files_scanned: int = 0
|
|
73
|
+
violations_found: int = 0
|
|
74
|
+
auto_fixed: int = 0
|
|
75
|
+
blocked: int = 0
|
|
76
|
+
results: list[ProactiveFixResult] = field(default_factory=list)
|
|
77
|
+
|
|
78
|
+
def to_dict(self) -> dict:
|
|
79
|
+
return {
|
|
80
|
+
"files_scanned": self.files_scanned,
|
|
81
|
+
"violations_found": self.violations_found,
|
|
82
|
+
"auto_fixed": self.auto_fixed,
|
|
83
|
+
"blocked": self.blocked,
|
|
84
|
+
"results": [r.__dict__ for r in self.results],
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ProactiveAutoFixer:
|
|
89
|
+
"""
|
|
90
|
+
Checks files against charter rules and auto-fixes safe violations.
|
|
91
|
+
|
|
92
|
+
Flow:
|
|
93
|
+
1. Load charter from .patchi/memory/charter.json
|
|
94
|
+
2. For each file, check convention rules
|
|
95
|
+
3. If safe to fix (headers, docstrings), apply fix
|
|
96
|
+
4. If risky (lines, secrets), flag for human review
|
|
97
|
+
5. Return report with all changes made
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def __init__(self, root: Path, apply: bool = True):
|
|
101
|
+
self.root = root
|
|
102
|
+
self.apply = apply
|
|
103
|
+
self.charter = load_charter(root)
|
|
104
|
+
self.fix_count = 0
|
|
105
|
+
|
|
106
|
+
def check_and_fix_file(self, file_path: Path) -> list[ProactiveFixResult]:
|
|
107
|
+
"""Check a single file against all charter rules and auto-fix safe ones."""
|
|
108
|
+
results = []
|
|
109
|
+
|
|
110
|
+
if not file_path.is_file():
|
|
111
|
+
return results
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
content = file_path.read_text(encoding="utf-8", errors="ignore")
|
|
115
|
+
except OSError:
|
|
116
|
+
return results
|
|
117
|
+
|
|
118
|
+
rel_path = str(file_path.relative_to(self.root))
|
|
119
|
+
|
|
120
|
+
for rule in self.charter.rules:
|
|
121
|
+
if not rule.enabled:
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
if rule.type == RuleType.CONVENTION:
|
|
125
|
+
result = self._check_convention(rule, rel_path, file_path, content)
|
|
126
|
+
if result:
|
|
127
|
+
results.append(result)
|
|
128
|
+
if result.auto_fixed:
|
|
129
|
+
self.fix_count += 1
|
|
130
|
+
|
|
131
|
+
elif rule.type == RuleType.BOUNDARY:
|
|
132
|
+
result = self._check_boundary(rule, rel_path, content)
|
|
133
|
+
if result:
|
|
134
|
+
results.append(result)
|
|
135
|
+
|
|
136
|
+
elif rule.type == RuleType.SECURITY:
|
|
137
|
+
result = self._check_security(rule, rel_path, content)
|
|
138
|
+
if result:
|
|
139
|
+
results.append(result)
|
|
140
|
+
|
|
141
|
+
elif rule.type == RuleType.STACK:
|
|
142
|
+
result = self._check_stack(rule, rel_path)
|
|
143
|
+
if result:
|
|
144
|
+
results.append(result)
|
|
145
|
+
|
|
146
|
+
return results
|
|
147
|
+
|
|
148
|
+
def check_files(self, file_paths: list[Path]) -> ProactiveFixReport:
|
|
149
|
+
"""Check multiple files and auto-fix safe violations."""
|
|
150
|
+
report = ProactiveFixReport()
|
|
151
|
+
|
|
152
|
+
for fp in file_paths:
|
|
153
|
+
results = self.check_and_fix_file(fp)
|
|
154
|
+
if results:
|
|
155
|
+
report.files_scanned += 1
|
|
156
|
+
report.violations_found += len(results)
|
|
157
|
+
for r in results:
|
|
158
|
+
if r.auto_fixed:
|
|
159
|
+
report.auto_fixed += 1
|
|
160
|
+
elif r.blocked_reason:
|
|
161
|
+
report.blocked += 1
|
|
162
|
+
report.results.append(r)
|
|
163
|
+
|
|
164
|
+
return report
|
|
165
|
+
|
|
166
|
+
def _check_convention(
|
|
167
|
+
self, rule: CharterRule, rel_path: str, abs_path: Path, content: str
|
|
168
|
+
) -> ProactiveFixResult | None:
|
|
169
|
+
"""Check and auto-fix convention rules."""
|
|
170
|
+
result = ProactiveFixResult(
|
|
171
|
+
file_path=rel_path,
|
|
172
|
+
rule_id=rule.id,
|
|
173
|
+
rule_type=rule.type.value,
|
|
174
|
+
scope=rule.scope,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if rule.scope in SAFE_AUTO_FIX_SCOPES:
|
|
178
|
+
fixed, changes = self._auto_fix_convention(rule, abs_path, content)
|
|
179
|
+
if fixed:
|
|
180
|
+
result.auto_fixed = True
|
|
181
|
+
result.changes_made = changes
|
|
182
|
+
result.original_content = content
|
|
183
|
+
if self.apply:
|
|
184
|
+
self._write_file(abs_path, fixed)
|
|
185
|
+
result.fixed_content = fixed
|
|
186
|
+
return result
|
|
187
|
+
return None # No violation found
|
|
188
|
+
|
|
189
|
+
elif rule.scope in RISKY_SCOPES:
|
|
190
|
+
# Check if violation exists, but don't auto-fix
|
|
191
|
+
violated = self._check_convention_violation(rule, abs_path, content)
|
|
192
|
+
if violated:
|
|
193
|
+
result.blocked_reason = (
|
|
194
|
+
f"Convention violation: {rule.description} "
|
|
195
|
+
f"(requires manual fix)"
|
|
196
|
+
)
|
|
197
|
+
return result
|
|
198
|
+
return None
|
|
199
|
+
|
|
200
|
+
# Unknown scope — flag but don't fix
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
def _check_convention_violation(
|
|
204
|
+
self, rule: CharterRule, abs_path: Path, content: str
|
|
205
|
+
) -> bool:
|
|
206
|
+
"""Check if a convention rule is violated."""
|
|
207
|
+
if rule.scope == "lines" and rule.max_value > 0:
|
|
208
|
+
line_count = len(content.splitlines())
|
|
209
|
+
return line_count > rule.max_value
|
|
210
|
+
|
|
211
|
+
if rule.scope == "file_size" and rule.max_value > 0:
|
|
212
|
+
return len(content.encode("utf-8")) > rule.max_value
|
|
213
|
+
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
def _auto_fix_convention(
|
|
217
|
+
self, rule: CharterRule, abs_path: Path, content: str
|
|
218
|
+
) -> tuple[str | None, list[str]]:
|
|
219
|
+
"""Apply safe auto-fix for a convention rule.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
(fixed_content_or_None, list_of_changes_made)
|
|
223
|
+
"""
|
|
224
|
+
changes = []
|
|
225
|
+
|
|
226
|
+
if rule.scope == "headers_required":
|
|
227
|
+
fixed = self._fix_missing_header(abs_path, content)
|
|
228
|
+
if fixed and fixed != content:
|
|
229
|
+
changes.append("Added file header")
|
|
230
|
+
return fixed, changes
|
|
231
|
+
|
|
232
|
+
elif rule.scope == "docstrings_required":
|
|
233
|
+
fixed = self._fix_missing_docstrings(abs_path, content)
|
|
234
|
+
if fixed and fixed != content:
|
|
235
|
+
changes.append("Added docstrings to functions/classes")
|
|
236
|
+
return fixed, changes
|
|
237
|
+
|
|
238
|
+
elif rule.scope in ("snake_case", "camel_case", "pascal_case"):
|
|
239
|
+
# Only flag naming issues — renaming is too risky for auto-fix
|
|
240
|
+
return None, []
|
|
241
|
+
|
|
242
|
+
return None, []
|
|
243
|
+
|
|
244
|
+
def _fix_missing_header(self, abs_path: Path, content: str) -> str | None:
|
|
245
|
+
"""Add a license/author header if missing."""
|
|
246
|
+
if not content.strip():
|
|
247
|
+
return content
|
|
248
|
+
|
|
249
|
+
# Check if header already exists
|
|
250
|
+
first_line = content.split("\n", 1)[0].strip()
|
|
251
|
+
header_indicators = [
|
|
252
|
+
"#!",
|
|
253
|
+
"# Copyright",
|
|
254
|
+
"# License",
|
|
255
|
+
'"""',
|
|
256
|
+
"# !",
|
|
257
|
+
"# *",
|
|
258
|
+
"# -",
|
|
259
|
+
"/*",
|
|
260
|
+
"//",
|
|
261
|
+
"<!--",
|
|
262
|
+
"# coding",
|
|
263
|
+
]
|
|
264
|
+
|
|
265
|
+
for indicator in header_indicators:
|
|
266
|
+
if first_line.startswith(indicator):
|
|
267
|
+
return None # Header already exists
|
|
268
|
+
|
|
269
|
+
# Determine file type for comment syntax
|
|
270
|
+
suffix = abs_path.suffix.lower()
|
|
271
|
+
comment_prefix = {
|
|
272
|
+
".py": "#",
|
|
273
|
+
".js": "//",
|
|
274
|
+
".ts": "//",
|
|
275
|
+
".jsx": "//",
|
|
276
|
+
".tsx": "//",
|
|
277
|
+
".mjs": "//",
|
|
278
|
+
".cjs": "//",
|
|
279
|
+
".go": "//",
|
|
280
|
+
".java": "//",
|
|
281
|
+
".c": "//",
|
|
282
|
+
".cpp": "//",
|
|
283
|
+
".h": "//",
|
|
284
|
+
".hpp": "//",
|
|
285
|
+
".rs": "//",
|
|
286
|
+
".rb": "#",
|
|
287
|
+
".php": "//",
|
|
288
|
+
".cs": "//",
|
|
289
|
+
".swift": "//",
|
|
290
|
+
".scala": "//",
|
|
291
|
+
".kt": "//",
|
|
292
|
+
}.get(suffix)
|
|
293
|
+
|
|
294
|
+
if not comment_prefix:
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
header = (
|
|
298
|
+
f"{comment_prefix} Auto-fixed by Patchi\n"
|
|
299
|
+
f"{comment_prefix} Added missing file header\n\n"
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
return header + content
|
|
303
|
+
|
|
304
|
+
def _fix_missing_docstrings(self, abs_path: Path, content: str) -> str | None:
|
|
305
|
+
"""Add empty docstrings to functions/classes missing them."""
|
|
306
|
+
suffix = abs_path.suffix.lower()
|
|
307
|
+
|
|
308
|
+
if suffix == ".py":
|
|
309
|
+
return self._fix_python_docstrings(content)
|
|
310
|
+
elif suffix in (".js", ".ts", ".jsx", ".tsx"):
|
|
311
|
+
return self._fix_jsdoc(content, suffix)
|
|
312
|
+
# Other languages: skip for safety
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
def _fix_python_docstrings(self, content: str) -> str | None:
|
|
316
|
+
"""Add empty docstrings to Python functions/classes missing them."""
|
|
317
|
+
lines = content.splitlines()
|
|
318
|
+
result = []
|
|
319
|
+
modified = False
|
|
320
|
+
i = 0
|
|
321
|
+
|
|
322
|
+
while i < len(lines):
|
|
323
|
+
result.append(lines[i])
|
|
324
|
+
|
|
325
|
+
# Check if this is a def or class line
|
|
326
|
+
stripped = lines[i].lstrip()
|
|
327
|
+
is_func = stripped.startswith("def ") or stripped.startswith("async def ")
|
|
328
|
+
is_class = stripped.startswith("class ")
|
|
329
|
+
|
|
330
|
+
if (is_func or is_class) and not stripped.startswith("#"):
|
|
331
|
+
# Look for docstring after the line (may be on next non-empty line)
|
|
332
|
+
has_docstring = False
|
|
333
|
+
j = i + 1
|
|
334
|
+
while j < len(lines):
|
|
335
|
+
next_stripped = lines[j].strip()
|
|
336
|
+
if next_stripped:
|
|
337
|
+
if next_stripped.startswith('"""') or next_stripped.startswith("'''"):
|
|
338
|
+
has_docstring = True
|
|
339
|
+
break
|
|
340
|
+
j += 1
|
|
341
|
+
|
|
342
|
+
if not has_docstring and j < len(lines):
|
|
343
|
+
# Add empty docstring on next line
|
|
344
|
+
indent = len(lines[i]) - len(lines[i].lstrip()) + 4
|
|
345
|
+
prefix = " " * indent
|
|
346
|
+
result.append(f'{prefix}"""Add description."""')
|
|
347
|
+
modified = True
|
|
348
|
+
|
|
349
|
+
i += 1
|
|
350
|
+
|
|
351
|
+
if modified:
|
|
352
|
+
return "\n".join(result)
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
def _fix_jsdoc(self, content: str, suffix: str) -> str | None:
|
|
356
|
+
"""Add empty JSDoc to JS/TS functions missing comments."""
|
|
357
|
+
lines = content.splitlines()
|
|
358
|
+
result = []
|
|
359
|
+
modified = False
|
|
360
|
+
|
|
361
|
+
for i, line in enumerate(lines):
|
|
362
|
+
result.append(line)
|
|
363
|
+
stripped = line.lstrip()
|
|
364
|
+
|
|
365
|
+
# Check for function declarations
|
|
366
|
+
is_func = (
|
|
367
|
+
stripped.startswith("function ")
|
|
368
|
+
or stripped.startswith("async function ")
|
|
369
|
+
or (stripped.startswith("const ") and "=>" in stripped)
|
|
370
|
+
or (stripped.startswith("let ") and "=>" in stripped)
|
|
371
|
+
or stripped.startswith("export function ")
|
|
372
|
+
or stripped.startswith("export async function ")
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
if is_func:
|
|
376
|
+
# Check if preceded by a comment
|
|
377
|
+
if i > 0:
|
|
378
|
+
prev = lines[i - 1].strip()
|
|
379
|
+
if prev.endswith("*/") or prev.startswith("//"):
|
|
380
|
+
continue
|
|
381
|
+
|
|
382
|
+
indent = len(line) - len(line.lstrip())
|
|
383
|
+
prefix = " " * indent
|
|
384
|
+
result.insert(-1, f"{prefix}/** Add description. */")
|
|
385
|
+
modified = True
|
|
386
|
+
|
|
387
|
+
if modified:
|
|
388
|
+
return "\n".join(result)
|
|
389
|
+
return None
|
|
390
|
+
|
|
391
|
+
def _check_boundary(
|
|
392
|
+
self, rule: CharterRule, rel_path: str, content: str
|
|
393
|
+
) -> ProactiveFixResult | None:
|
|
394
|
+
"""Check boundary rules — flag only, no auto-fix."""
|
|
395
|
+
# Parse imports from content
|
|
396
|
+
imports = self._extract_imports(rel_path, content)
|
|
397
|
+
|
|
398
|
+
violations = []
|
|
399
|
+
for src_module, imported in imports:
|
|
400
|
+
src_lower = src_module.lower()
|
|
401
|
+
imp_lower = imported.lower()
|
|
402
|
+
|
|
403
|
+
if rule.from_pattern.lower() in src_lower and rule.to_pattern.lower() in imp_lower:
|
|
404
|
+
violations.append((src_module, imported))
|
|
405
|
+
|
|
406
|
+
if violations:
|
|
407
|
+
result = ProactiveFixResult(
|
|
408
|
+
file_path=rel_path,
|
|
409
|
+
rule_id=rule.id,
|
|
410
|
+
rule_type=rule.type.value,
|
|
411
|
+
scope="boundary",
|
|
412
|
+
)
|
|
413
|
+
result.blocked_reason = (
|
|
414
|
+
f"Boundary violation: {rule.description} "
|
|
415
|
+
f"({violations[0][0]} imports {violations[0][1]})"
|
|
416
|
+
)
|
|
417
|
+
return result
|
|
418
|
+
|
|
419
|
+
return None
|
|
420
|
+
|
|
421
|
+
def _extract_imports(self, file_path: str, content: str) -> list[tuple[str, str]]:
|
|
422
|
+
"""Extract import relationships from source code.
|
|
423
|
+
|
|
424
|
+
Returns list of (source_module, imported_module) tuples.
|
|
425
|
+
"""
|
|
426
|
+
imports = []
|
|
427
|
+
suffix = Path(file_path).suffix.lower()
|
|
428
|
+
|
|
429
|
+
if suffix == ".py":
|
|
430
|
+
imports.extend(self._extract_python_imports(content))
|
|
431
|
+
elif suffix in (".js", ".ts", ".jsx", ".tsx", ".mjs"):
|
|
432
|
+
imports.extend(self._extract_js_imports(content))
|
|
433
|
+
elif suffix == ".go":
|
|
434
|
+
imports.extend(self._extract_go_imports(content))
|
|
435
|
+
|
|
436
|
+
return imports
|
|
437
|
+
|
|
438
|
+
def _extract_python_imports(self, content: str) -> list[tuple[str, str]]:
|
|
439
|
+
"""Extract Python import statements."""
|
|
440
|
+
imports = []
|
|
441
|
+
for line in content.splitlines():
|
|
442
|
+
stripped = line.strip()
|
|
443
|
+
if stripped.startswith("from ") and " import " in stripped:
|
|
444
|
+
# from X import Y
|
|
445
|
+
parts = stripped.split(" import ", 1)
|
|
446
|
+
if len(parts) == 2:
|
|
447
|
+
module = parts[0].replace("from ", "").strip()
|
|
448
|
+
imports.append((module, module))
|
|
449
|
+
elif stripped.startswith("import "):
|
|
450
|
+
# import X
|
|
451
|
+
module = stripped.replace("import ", "").strip().split(",")[0].strip()
|
|
452
|
+
imports.append((module, module))
|
|
453
|
+
return imports
|
|
454
|
+
|
|
455
|
+
def _extract_js_imports(self, content: str) -> list[tuple[str, str]]:
|
|
456
|
+
"""Extract JavaScript/TypeScript import statements."""
|
|
457
|
+
imports = []
|
|
458
|
+
for line in content.splitlines():
|
|
459
|
+
stripped = line.strip()
|
|
460
|
+
if "from '" in stripped or 'from "' in stripped:
|
|
461
|
+
parts = stripped.split("from ")[-1].strip().strip("'\"")
|
|
462
|
+
imports.append((parts, parts))
|
|
463
|
+
elif stripped.startswith("import '") or stripped.startswith('import "'):
|
|
464
|
+
mod = stripped.replace("import ", "").strip().strip("'\"")
|
|
465
|
+
imports.append((mod, mod))
|
|
466
|
+
return imports
|
|
467
|
+
|
|
468
|
+
def _extract_go_imports(self, content: str) -> list[tuple[str, str]]:
|
|
469
|
+
"""Extract Go import statements."""
|
|
470
|
+
imports = []
|
|
471
|
+
in_import = False
|
|
472
|
+
for line in content.splitlines():
|
|
473
|
+
stripped = line.strip()
|
|
474
|
+
if stripped.startswith("import ("):
|
|
475
|
+
in_import = True
|
|
476
|
+
continue
|
|
477
|
+
if in_import and stripped == ")":
|
|
478
|
+
in_import = False
|
|
479
|
+
continue
|
|
480
|
+
if in_import and stripped.startswith('"'):
|
|
481
|
+
pkg = stripped.strip('"')
|
|
482
|
+
imports.append((pkg, pkg))
|
|
483
|
+
return imports
|
|
484
|
+
|
|
485
|
+
def _check_security(
|
|
486
|
+
self, rule: CharterRule, rel_path: str, content: str
|
|
487
|
+
) -> ProactiveFixResult | None:
|
|
488
|
+
"""Check security rules — flag only, no auto-fix."""
|
|
489
|
+
if not rule.keywords:
|
|
490
|
+
return None
|
|
491
|
+
|
|
492
|
+
content_lower = content.lower()
|
|
493
|
+
for keyword in rule.keywords:
|
|
494
|
+
if keyword.lower() in content_lower:
|
|
495
|
+
result = ProactiveFixResult(
|
|
496
|
+
file_path=rel_path,
|
|
497
|
+
rule_id=rule.id,
|
|
498
|
+
rule_type=rule.type.value,
|
|
499
|
+
scope="security",
|
|
500
|
+
)
|
|
501
|
+
result.blocked_reason = (
|
|
502
|
+
f"Security violation: {rule.description} "
|
|
503
|
+
f"(found '{keyword}')"
|
|
504
|
+
)
|
|
505
|
+
return result
|
|
506
|
+
|
|
507
|
+
return None
|
|
508
|
+
|
|
509
|
+
def _check_stack(
|
|
510
|
+
self, rule: CharterRule, rel_path: str
|
|
511
|
+
) -> ProactiveFixResult | None:
|
|
512
|
+
"""Check stack rules — flag only, no auto-fix."""
|
|
513
|
+
if not rule.allowed_languages:
|
|
514
|
+
return None
|
|
515
|
+
|
|
516
|
+
# Map file extensions to language names
|
|
517
|
+
ext_to_lang = {
|
|
518
|
+
".py": "python",
|
|
519
|
+
".js": "javascript",
|
|
520
|
+
".ts": "typescript",
|
|
521
|
+
".jsx": "javascript",
|
|
522
|
+
".tsx": "typescript",
|
|
523
|
+
".go": "go",
|
|
524
|
+
".rs": "rust",
|
|
525
|
+
".java": "java",
|
|
526
|
+
".cs": "csharp",
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
suffix = Path(rel_path).suffix.lower()
|
|
530
|
+
lang = ext_to_lang.get(suffix, "")
|
|
531
|
+
|
|
532
|
+
if lang and lang not in rule.allowed_languages:
|
|
533
|
+
result = ProactiveFixResult(
|
|
534
|
+
file_path=rel_path,
|
|
535
|
+
rule_id=rule.id,
|
|
536
|
+
rule_type=rule.type.value,
|
|
537
|
+
scope="stack",
|
|
538
|
+
)
|
|
539
|
+
result.blocked_reason = (
|
|
540
|
+
f"Stack violation: {rule.description} "
|
|
541
|
+
f"(found {lang} file, allowed: {', '.join(rule.allowed_languages)})"
|
|
542
|
+
)
|
|
543
|
+
return result
|
|
544
|
+
|
|
545
|
+
return None
|
|
546
|
+
|
|
547
|
+
def _write_file(self, path: Path, content: str) -> None:
|
|
548
|
+
"""Write content to file safely."""
|
|
549
|
+
try:
|
|
550
|
+
path.write_text(content, encoding="utf-8")
|
|
551
|
+
_log.info("Auto-fixed: %s", path)
|
|
552
|
+
except OSError as e:
|
|
553
|
+
_log.error("Failed to write %s: %s", path, e)
|
|
554
|
+
|
|
555
|
+
def get_fix_history(self) -> list[dict]:
|
|
556
|
+
"""Get recent proactive fixes from memory."""
|
|
557
|
+
from patchi.core import memory as mem
|
|
558
|
+
|
|
559
|
+
history = mem.list_issues(self.root)
|
|
560
|
+
return [
|
|
561
|
+
item for item in history
|
|
562
|
+
if item.get("type") == "proactive_fix"
|
|
563
|
+
]
|
|
564
|
+
|
|
565
|
+
def save_fix_history(self, report: ProactiveFixReport) -> None:
|
|
566
|
+
"""Save proactive fix results to memory."""
|
|
567
|
+
from patchi.core import memory as mem
|
|
568
|
+
|
|
569
|
+
entry = {
|
|
570
|
+
"type": "proactive_fix",
|
|
571
|
+
"files_scanned": report.files_scanned,
|
|
572
|
+
"violations_found": report.violations_found,
|
|
573
|
+
"auto_fixed": report.auto_fixed,
|
|
574
|
+
"blocked": report.blocked,
|
|
575
|
+
"details": [
|
|
576
|
+
{
|
|
577
|
+
"file": r.file_path,
|
|
578
|
+
"rule": r.rule_id,
|
|
579
|
+
"scope": r.scope,
|
|
580
|
+
"auto_fixed": r.auto_fixed,
|
|
581
|
+
}
|
|
582
|
+
for r in report.results
|
|
583
|
+
],
|
|
584
|
+
}
|
|
585
|
+
mem.save_issue(entry, self.root)
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
# ── Convenience Functions ─────────────────────────────────────────────────────
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def proactive_fix_files(
|
|
592
|
+
root: Path,
|
|
593
|
+
file_paths: list[Path],
|
|
594
|
+
apply: bool = True,
|
|
595
|
+
) -> ProactiveFixReport:
|
|
596
|
+
"""Auto-fix charter violations in the given files."""
|
|
597
|
+
fixer = ProactiveAutoFixer(root, apply=apply)
|
|
598
|
+
report = fixer.check_files(file_paths)
|
|
599
|
+
fixer.save_fix_history(report)
|
|
600
|
+
return report
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def proactive_fix_path(
|
|
604
|
+
root: Path,
|
|
605
|
+
path: Path,
|
|
606
|
+
apply: bool = True,
|
|
607
|
+
) -> list[ProactiveFixResult]:
|
|
608
|
+
"""Auto-fix charter violations in a single file."""
|
|
609
|
+
fixer = ProactiveAutoFixer(root, apply=apply)
|
|
610
|
+
results = fixer.check_and_fix_file(path)
|
|
611
|
+
if results:
|
|
612
|
+
report = ProactiveFixReport(
|
|
613
|
+
files_scanned=1,
|
|
614
|
+
violations_found=len(results),
|
|
615
|
+
auto_fixed=sum(1 for r in results if r.auto_fixed),
|
|
616
|
+
blocked=sum(1 for r in results if r.blocked_reason),
|
|
617
|
+
results=results,
|
|
618
|
+
)
|
|
619
|
+
fixer.save_fix_history(report)
|
|
620
|
+
return results
|