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,269 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Reasoning Engine — Phase 3 of the Patchi super-agent.
|
|
3
|
+
|
|
4
|
+
Sits *above* the Layered Brain and turns it into answers. Given a change-set, a
|
|
5
|
+
file, or a natural-language question, it reasons over the cached layer summaries
|
|
6
|
+
(never re-reading raw source) and produces structured, human-readable output:
|
|
7
|
+
|
|
8
|
+
- impact_analysis(changed_files) → blast radius across layers
|
|
9
|
+
- explain(target) → a layer's summary + its dependency context
|
|
10
|
+
- why(path) → why a file matters (dependents, purpose)
|
|
11
|
+
- ask(question) → NL answer built from layer summaries
|
|
12
|
+
|
|
13
|
+
Everything is offline-safe (pure heuristics over the Layered Brain). If an LLM
|
|
14
|
+
is configured, :meth:`ReasoningEngine.ask` can optionally enrich the answer.
|
|
15
|
+
|
|
16
|
+
This module is additive: it only reads the Layered Brain persisted at
|
|
17
|
+
``.patchi/memory/layers.json`` and existing ``layered_brain`` helpers.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import re
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import TYPE_CHECKING
|
|
26
|
+
|
|
27
|
+
from patchi.core.brain.brain_watcher import ChangeSet, affected_layers
|
|
28
|
+
from patchi.core.brain.layered_brain import (
|
|
29
|
+
find_layers_for_file,
|
|
30
|
+
get_layer_context,
|
|
31
|
+
layers_from_dict,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_log = logging.getLogger("patchi.brain.reasoning")
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from patchi.core.brain.layered_brain import Layer
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class ImpactAnalysis:
|
|
42
|
+
"""Structured result of reasoning about a change-set."""
|
|
43
|
+
|
|
44
|
+
changed_files: list[str] = field(default_factory=list)
|
|
45
|
+
affected_layers: list[str] = field(default_factory=list) # directly touched
|
|
46
|
+
impacted_layers: list[str] = field(default_factory=list) # blast radius (dependents)
|
|
47
|
+
summary: str = ""
|
|
48
|
+
|
|
49
|
+
def to_dict(self) -> dict:
|
|
50
|
+
return {
|
|
51
|
+
"changed_files": self.changed_files,
|
|
52
|
+
"affected_layers": self.affected_layers,
|
|
53
|
+
"impacted_layers": self.impacted_layers,
|
|
54
|
+
"summary": self.summary,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ReasoningEngine:
|
|
59
|
+
"""Answers questions using the cached Layered Brain (no raw-source reads)."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, root: Path):
|
|
62
|
+
self.root = Path(root)
|
|
63
|
+
self.layers: dict[str, Layer] = self._load_layers()
|
|
64
|
+
|
|
65
|
+
def _load_layers(self) -> dict[str, Layer]:
|
|
66
|
+
from patchi.core import memory as mem
|
|
67
|
+
|
|
68
|
+
data = mem.get_layers(self.root) or {}
|
|
69
|
+
if not data.get("layers"):
|
|
70
|
+
return {}
|
|
71
|
+
return layers_from_dict(data)
|
|
72
|
+
|
|
73
|
+
# ── Impact analysis (blast radius) ──────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
def impact_analysis(self, changed_files: list[str]) -> ImpactAnalysis:
|
|
76
|
+
if not self.layers:
|
|
77
|
+
return ImpactAnalysis(
|
|
78
|
+
changed_files=changed_files,
|
|
79
|
+
summary="No layered brain available. Run `p scan` first to build it.",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
changes = ChangeSet(changed=set(changed_files))
|
|
83
|
+
affected = affected_layers(changes, self.layers)
|
|
84
|
+
|
|
85
|
+
# Blast radius: transitively follow dependents of every affected layer.
|
|
86
|
+
impacted: set[str] = set()
|
|
87
|
+
stack = list(affected)
|
|
88
|
+
while stack:
|
|
89
|
+
cur = stack.pop()
|
|
90
|
+
layer = self.layers.get(cur)
|
|
91
|
+
if not layer:
|
|
92
|
+
continue
|
|
93
|
+
for dep in layer.dependents:
|
|
94
|
+
if dep in self.layers and dep not in impacted:
|
|
95
|
+
impacted.add(dep)
|
|
96
|
+
stack.append(dep)
|
|
97
|
+
|
|
98
|
+
blast = impacted - affected
|
|
99
|
+
summary = self._impact_summary(changed_files, affected, blast)
|
|
100
|
+
return ImpactAnalysis(
|
|
101
|
+
changed_files=changed_files,
|
|
102
|
+
affected_layers=sorted(affected),
|
|
103
|
+
impacted_layers=sorted(blast),
|
|
104
|
+
summary=summary,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def _impact_summary(self, changed, affected, blast) -> str:
|
|
108
|
+
if not changed:
|
|
109
|
+
return "No changes provided."
|
|
110
|
+
parts = [f"{len(changed)} file(s) changed"]
|
|
111
|
+
if affected:
|
|
112
|
+
parts.append(
|
|
113
|
+
f"{len(affected)} layer(s) directly affected: {', '.join(sorted(affected))}"
|
|
114
|
+
)
|
|
115
|
+
if blast:
|
|
116
|
+
parts.append(
|
|
117
|
+
f"{len(blast)} downstream layer(s) in the blast radius: {', '.join(sorted(blast))}"
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
parts.append("no downstream layers depend on the changed code")
|
|
121
|
+
return "; ".join(parts) + "."
|
|
122
|
+
|
|
123
|
+
# ── Explain a layer / file ───────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
def explain(self, target: str) -> dict:
|
|
126
|
+
"""Return a layer's summary plus its direct dependency context.
|
|
127
|
+
|
|
128
|
+
``target`` may be a file path or a layer name.
|
|
129
|
+
"""
|
|
130
|
+
if not self.layers:
|
|
131
|
+
return {"error": "No layered brain available. Run `p scan` first."}
|
|
132
|
+
|
|
133
|
+
# Resolve to a layer name.
|
|
134
|
+
if target in self.layers:
|
|
135
|
+
layer_name = target
|
|
136
|
+
else:
|
|
137
|
+
matches = find_layers_for_file(self.layers, target)
|
|
138
|
+
if not matches:
|
|
139
|
+
return {"error": f"No layer found for '{target}'."}
|
|
140
|
+
layer_name = matches[0]
|
|
141
|
+
|
|
142
|
+
layer = self.layers[layer_name]
|
|
143
|
+
context = get_layer_context(self.layers, layer_name, max_depth=1)
|
|
144
|
+
return {
|
|
145
|
+
"layer": layer_name,
|
|
146
|
+
"level": layer.level,
|
|
147
|
+
"summary": layer.summary,
|
|
148
|
+
"purpose": layer.purpose,
|
|
149
|
+
"depends_on": layer.depends_on,
|
|
150
|
+
"dependents": layer.dependents,
|
|
151
|
+
"context": {k: v for k, v in context.items() if k != layer_name},
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# ── Why does a file matter ───────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
def why(self, path: str) -> dict:
|
|
157
|
+
"""Explain why a file matters: its layer, dependents, and purpose."""
|
|
158
|
+
if not self.layers:
|
|
159
|
+
return {"error": "No layered brain available. Run `p scan` first."}
|
|
160
|
+
|
|
161
|
+
matches = find_layers_for_file(self.layers, path)
|
|
162
|
+
if not matches:
|
|
163
|
+
return {"error": f"'{path}' is not part of any known layer."}
|
|
164
|
+
|
|
165
|
+
layer_name = matches[0]
|
|
166
|
+
layer = self.layers[layer_name]
|
|
167
|
+
dependents = layer.dependents
|
|
168
|
+
importance = "critical" if dependents else ("shared" if len(layer.files) > 1 else "leaf")
|
|
169
|
+
return {
|
|
170
|
+
"file": path,
|
|
171
|
+
"layer": layer_name,
|
|
172
|
+
"layer_summary": layer.summary,
|
|
173
|
+
"purpose": layer.purpose,
|
|
174
|
+
"depended_on_by": dependents,
|
|
175
|
+
"importance": importance,
|
|
176
|
+
"files_in_layer": len(layer.files),
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# ── Natural-language问答 (TF-IDF over layers + body_tags, LLM optional) ─
|
|
180
|
+
|
|
181
|
+
def ask(self, question: str, use_ai: bool = False, ask_ai: bool | None = None, **_) -> str:
|
|
182
|
+
"""Answer a question from layer summaries. TF-IDF ranked, optionally LLM-synthesized.
|
|
183
|
+
|
|
184
|
+
Spec L3: RAG over cached layers[].summary built at scan time (layers.json rag_index),
|
|
185
|
+
query via cosine, ask_ai=True wraps RAG context + call_ai. use_ai is alias for backward compat.
|
|
186
|
+
"""
|
|
187
|
+
if ask_ai is not None:
|
|
188
|
+
use_ai = bool(ask_ai)
|
|
189
|
+
if not self.layers:
|
|
190
|
+
return (
|
|
191
|
+
"I don't have a brain-map of this project yet. "
|
|
192
|
+
"Run `p scan` to build the layered brain, then ask again."
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
q = question.lower()
|
|
196
|
+
tokens = {t for t in re.findall(r"[a-z0-9_]+", q) if len(t) > 2}
|
|
197
|
+
if not tokens:
|
|
198
|
+
return "Could you rephrase? Try naming a subsystem (auth, api, data, ui…)."
|
|
199
|
+
|
|
200
|
+
# Try understander/body_tags boost if available
|
|
201
|
+
body_boost: dict[str, float] = {}
|
|
202
|
+
try:
|
|
203
|
+
from patchi.core.brain.body_tags import load_body_tags
|
|
204
|
+
|
|
205
|
+
tags = load_body_tags(self.root)
|
|
206
|
+
for _path, tag in tags.items():
|
|
207
|
+
layer_hint = tag.get("layer", "")
|
|
208
|
+
if layer_hint:
|
|
209
|
+
body_boost[layer_hint] = max(body_boost.get(layer_hint, 0), float(tag.get("score", 0)) / 100.0)
|
|
210
|
+
except Exception:
|
|
211
|
+
body_boost = {}
|
|
212
|
+
|
|
213
|
+
# TF-IDF-ish: term freq * inverse doc freq approximation
|
|
214
|
+
doc_freq: dict[str, int] = {}
|
|
215
|
+
for t in tokens:
|
|
216
|
+
df = sum(1 for n, layer in self.layers.items() if t in f"{n} {layer.summary} {layer.purpose}".lower())
|
|
217
|
+
doc_freq[t] = max(1, df)
|
|
218
|
+
import math
|
|
219
|
+
|
|
220
|
+
scored: list[tuple[float, str, Layer]] = []
|
|
221
|
+
for name, layer in self.layers.items():
|
|
222
|
+
if layer.level == 4:
|
|
223
|
+
continue
|
|
224
|
+
text = f"{name} {layer.summary} {layer.purpose}".lower()
|
|
225
|
+
tfidf = 0.0
|
|
226
|
+
for t in tokens:
|
|
227
|
+
if t in text:
|
|
228
|
+
tf = text.count(t)
|
|
229
|
+
idf = math.log(len(self.layers) / doc_freq[t] + 1)
|
|
230
|
+
tfidf += tf * idf
|
|
231
|
+
tfidf += body_boost.get(name, 0.0)
|
|
232
|
+
if tfidf > 0:
|
|
233
|
+
scored.append((tfidf, name, layer))
|
|
234
|
+
|
|
235
|
+
if not scored:
|
|
236
|
+
return (
|
|
237
|
+
"I couldn't match that to any layer I know. "
|
|
238
|
+
"Known layers: "
|
|
239
|
+
+ ", ".join(sorted(n for n in self.layers if n != "__project__"))
|
|
240
|
+
+ "."
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
scored.sort(key=lambda x: x[0], reverse=True)
|
|
244
|
+
top = scored[:3]
|
|
245
|
+
if use_ai:
|
|
246
|
+
try:
|
|
247
|
+
import os
|
|
248
|
+
|
|
249
|
+
if not os.environ.get("PATCHI_OFFLINE"):
|
|
250
|
+
from patchi.core import config as cfg
|
|
251
|
+
from patchi.core.ai.client import call_ai
|
|
252
|
+
|
|
253
|
+
cfgd = cfg.load(self.root) if hasattr(cfg, "load") else {}
|
|
254
|
+
ctx = "\n".join(f"[{n}] {lay.summary} — {lay.purpose}" for _, n, lay in top)
|
|
255
|
+
ans = call_ai(cfgd, "You are Patchi understander. Answer from layered brain context.", f"Question: {question}\n\nContext:\n{ctx}", max_tokens=400)
|
|
256
|
+
if ans:
|
|
257
|
+
return ans
|
|
258
|
+
except Exception as _exc:
|
|
259
|
+
_log.debug("reasoning AI synthesis fallback: %s", _exc)
|
|
260
|
+
lines = [f"Based on the layered brain, here's what I know about '{question.strip()}':", ""]
|
|
261
|
+
for _, name, layer in top:
|
|
262
|
+
lines.append(f"[bold]{name}[/bold] ({_level_name(layer.level)}): {layer.summary}")
|
|
263
|
+
lines.append("")
|
|
264
|
+
lines.append("[dim]Answers are derived from cached layer summaries + body_tags ranking.[/dim]")
|
|
265
|
+
return "\n".join(lines)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _level_name(level: int) -> str:
|
|
269
|
+
return {1: "module", 2: "subsystem", 4: "project"}.get(level, f"L{level}")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from patchi.core.brain.route_detector.base import BaseRouteDetector
|
|
2
|
+
from patchi.core.brain.route_detector.registry import (
|
|
3
|
+
_DETECTOR_REGISTRY,
|
|
4
|
+
detect_routes,
|
|
5
|
+
get_detector,
|
|
6
|
+
register_detector,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"BaseRouteDetector",
|
|
11
|
+
"detect_routes",
|
|
12
|
+
"get_detector",
|
|
13
|
+
"register_detector",
|
|
14
|
+
"_DETECTOR_REGISTRY",
|
|
15
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BaseRouteDetector(ABC):
|
|
5
|
+
@abstractmethod
|
|
6
|
+
def detect(self, content: str, file_path: str) -> list[dict]: ...
|
|
7
|
+
|
|
8
|
+
def _make_route(
|
|
9
|
+
self,
|
|
10
|
+
method: str,
|
|
11
|
+
path: str,
|
|
12
|
+
handler: str,
|
|
13
|
+
file_path: str,
|
|
14
|
+
line: int,
|
|
15
|
+
framework: str = "",
|
|
16
|
+
middleware: list[str] | None = None,
|
|
17
|
+
auth_required: bool | None = None,
|
|
18
|
+
) -> dict:
|
|
19
|
+
return {
|
|
20
|
+
"method": method,
|
|
21
|
+
"path": path,
|
|
22
|
+
"handler": handler,
|
|
23
|
+
"file": file_path,
|
|
24
|
+
"line": line,
|
|
25
|
+
"middleware": middleware or [],
|
|
26
|
+
"auth_required": auth_required,
|
|
27
|
+
"framework": framework,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def _path_from_parts(self, parts: list[str]) -> str:
|
|
31
|
+
return "/" + "/".join(parts).replace("[", ":").replace("]", "")
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""
|
|
2
|
+
C# route detector — ASP.NET Core attribute-based routing.
|
|
3
|
+
|
|
4
|
+
Node types below (attribute, attribute_list, attribute_argument_list,
|
|
5
|
+
attribute_argument, method_declaration, class_declaration) were verified
|
|
6
|
+
directly against the installed tree-sitter-c-sharp grammar, not assumed from
|
|
7
|
+
generic documentation — including that both `method_declaration` and
|
|
8
|
+
`attribute` expose a reliable `name` field via child_by_field_name, which is
|
|
9
|
+
what this walker relies on instead of guessing child position.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
from patchi.core.brain.route_detector.base import BaseRouteDetector
|
|
16
|
+
from patchi.core.brain.route_detector.registry import register_detector
|
|
17
|
+
|
|
18
|
+
_log = logging.getLogger("patchi.brain.csharp")
|
|
19
|
+
|
|
20
|
+
_HTTP_ATTRS = {
|
|
21
|
+
"httpget": "GET",
|
|
22
|
+
"httppost": "POST",
|
|
23
|
+
"httpput": "PUT",
|
|
24
|
+
"httpdelete": "DELETE",
|
|
25
|
+
"httppatch": "PATCH",
|
|
26
|
+
"httphead": "HEAD",
|
|
27
|
+
"httpoptions": "OPTIONS",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@register_detector("c_sharp")
|
|
32
|
+
class CSharpRouteDetector(BaseRouteDetector):
|
|
33
|
+
def detect(self, content: str, file_path: str) -> list[dict]:
|
|
34
|
+
routes = self._detect_ts(content, file_path)
|
|
35
|
+
if routes:
|
|
36
|
+
return routes
|
|
37
|
+
return self._detect_fallback(content, file_path)
|
|
38
|
+
|
|
39
|
+
def _detect_ts(self, content: str, file_path: str) -> list[dict] | None:
|
|
40
|
+
try:
|
|
41
|
+
from patchi.core.brain.languages import Lang, get_parser
|
|
42
|
+
|
|
43
|
+
parser = get_parser(Lang.C_SHARP)
|
|
44
|
+
if parser is None:
|
|
45
|
+
return None
|
|
46
|
+
tree = parser.parse(content.encode("utf-8"))
|
|
47
|
+
except Exception as e:
|
|
48
|
+
_log.warning("CSharpRouteDetector._detect_ts failed: %s", e)
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
routes: list[dict] = []
|
|
52
|
+
self._walk(tree.root_node, content, file_path, routes)
|
|
53
|
+
return routes
|
|
54
|
+
|
|
55
|
+
def _walk(self, node: object, content: str, file_path: str, routes: list[dict]) -> None:
|
|
56
|
+
ntype = getattr(node, "type", "")
|
|
57
|
+
if ntype == "method_declaration":
|
|
58
|
+
self._check_method(node, content, file_path, routes)
|
|
59
|
+
for child in getattr(node, "children", []):
|
|
60
|
+
self._walk(child, content, file_path, routes)
|
|
61
|
+
|
|
62
|
+
def _check_method(
|
|
63
|
+
self, method_node: object, content: str, file_path: str, routes: list[dict]
|
|
64
|
+
) -> None:
|
|
65
|
+
http_method = None
|
|
66
|
+
route_suffix = ""
|
|
67
|
+
method_auth = False
|
|
68
|
+
|
|
69
|
+
for child in getattr(method_node, "children", []):
|
|
70
|
+
if child.type != "attribute_list":
|
|
71
|
+
continue
|
|
72
|
+
for attr in getattr(child, "children", []):
|
|
73
|
+
if attr.type != "attribute":
|
|
74
|
+
continue
|
|
75
|
+
name = self._attr_name(attr, content).lower()
|
|
76
|
+
if name in _HTTP_ATTRS:
|
|
77
|
+
http_method = _HTTP_ATTRS[name]
|
|
78
|
+
route_suffix = self._attr_string_arg(attr, content)
|
|
79
|
+
elif name == "route" and http_method is None:
|
|
80
|
+
http_method = "ANY"
|
|
81
|
+
route_suffix = self._attr_string_arg(attr, content)
|
|
82
|
+
elif name in ("authorize",):
|
|
83
|
+
method_auth = True
|
|
84
|
+
|
|
85
|
+
if http_method is None:
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
class_prefix, class_auth = self._enclosing_class_context(method_node, content)
|
|
89
|
+
path = self._combine_path(class_prefix, route_suffix)
|
|
90
|
+
|
|
91
|
+
name_node = (
|
|
92
|
+
method_node.child_by_field_name("name")
|
|
93
|
+
if hasattr(method_node, "child_by_field_name")
|
|
94
|
+
else None
|
|
95
|
+
)
|
|
96
|
+
handler = self._node_text(name_node, content) if name_node else ""
|
|
97
|
+
line = getattr(method_node, "start_point", (0, 0))[0] + 1
|
|
98
|
+
|
|
99
|
+
auth = method_auth or class_auth
|
|
100
|
+
routes.append(
|
|
101
|
+
self._make_route(
|
|
102
|
+
http_method,
|
|
103
|
+
path,
|
|
104
|
+
handler,
|
|
105
|
+
file_path,
|
|
106
|
+
line,
|
|
107
|
+
framework="ASP.NET Core",
|
|
108
|
+
auth_required=auth if auth else None,
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def _enclosing_class_context(self, node: object, content: str) -> tuple[str, bool]:
|
|
113
|
+
"""Walk up to the enclosing class_declaration for its [Route] prefix and [Authorize]."""
|
|
114
|
+
cur = node
|
|
115
|
+
for _ in range(10):
|
|
116
|
+
cur = getattr(cur, "parent", None)
|
|
117
|
+
if cur is None:
|
|
118
|
+
break
|
|
119
|
+
if getattr(cur, "type", "") == "class_declaration":
|
|
120
|
+
prefix = ""
|
|
121
|
+
auth = False
|
|
122
|
+
for child in getattr(cur, "children", []):
|
|
123
|
+
if child.type != "attribute_list":
|
|
124
|
+
continue
|
|
125
|
+
for attr in getattr(child, "children", []):
|
|
126
|
+
if attr.type != "attribute":
|
|
127
|
+
continue
|
|
128
|
+
name = self._attr_name(attr, content).lower()
|
|
129
|
+
if name == "route":
|
|
130
|
+
prefix = self._attr_string_arg(attr, content)
|
|
131
|
+
elif name == "authorize":
|
|
132
|
+
auth = True
|
|
133
|
+
return prefix, auth
|
|
134
|
+
return "", False
|
|
135
|
+
|
|
136
|
+
def _combine_path(self, prefix: str, suffix: str) -> str:
|
|
137
|
+
# [controller] is ASP.NET's token substitution for the class name minus
|
|
138
|
+
# "Controller" -- approximated here rather than resolved exactly, since
|
|
139
|
+
# getting it exact requires the class name, which is a reasonable
|
|
140
|
+
# follow-up refinement rather than a blocker for a first working version.
|
|
141
|
+
parts = [p.strip("/") for p in (prefix, suffix) if p]
|
|
142
|
+
return "/" + "/".join(parts) if parts else ""
|
|
143
|
+
|
|
144
|
+
def _attr_name(self, attr_node: object, content: str) -> str:
|
|
145
|
+
name_node = (
|
|
146
|
+
attr_node.child_by_field_name("name")
|
|
147
|
+
if hasattr(attr_node, "child_by_field_name")
|
|
148
|
+
else None
|
|
149
|
+
)
|
|
150
|
+
return self._node_text(name_node, content) if name_node else ""
|
|
151
|
+
|
|
152
|
+
def _attr_string_arg(self, attr_node: object, content: str) -> str:
|
|
153
|
+
for child in getattr(attr_node, "children", []):
|
|
154
|
+
if child.type != "attribute_argument_list":
|
|
155
|
+
continue
|
|
156
|
+
for arg in getattr(child, "children", []):
|
|
157
|
+
if arg.type == "attribute_argument":
|
|
158
|
+
for lit in getattr(arg, "children", []):
|
|
159
|
+
if lit.type == "string_literal":
|
|
160
|
+
return self._node_text(lit, content).strip('"')
|
|
161
|
+
return ""
|
|
162
|
+
|
|
163
|
+
def _node_text(self, node: object, content: str) -> str:
|
|
164
|
+
try:
|
|
165
|
+
return content[node.start_byte : node.end_byte]
|
|
166
|
+
except Exception as e:
|
|
167
|
+
_log.debug("CSharpRouteDetector._node_text failed: %s", e)
|
|
168
|
+
return ""
|
|
169
|
+
|
|
170
|
+
def _detect_fallback(self, content: str, file_path: str) -> list[dict]:
|
|
171
|
+
routes: list[dict] = []
|
|
172
|
+
verb_names = "|".join(_HTTP_ATTRS)
|
|
173
|
+
pat = re.compile(rf'\[({verb_names}|Route)(?:\("([^"]*)"\))?\]', re.IGNORECASE)
|
|
174
|
+
auth_pat = re.compile(r"\[Authorize\]", re.IGNORECASE)
|
|
175
|
+
|
|
176
|
+
lines = content.splitlines()
|
|
177
|
+
for i, line in enumerate(lines, 1):
|
|
178
|
+
m = pat.search(line)
|
|
179
|
+
if not m:
|
|
180
|
+
continue
|
|
181
|
+
verb_raw = m.group(1).lower()
|
|
182
|
+
method = _HTTP_ATTRS.get(verb_raw, "ANY")
|
|
183
|
+
path = "/" + (m.group(2) or "").strip("/")
|
|
184
|
+
|
|
185
|
+
handler = ""
|
|
186
|
+
for j in range(i, min(i + 5, len(lines) + 1)):
|
|
187
|
+
hm = re.search(
|
|
188
|
+
r"\b(?:public|private|protected)\s+\w+(?:<[\w,\s]+>)?\s+(\w+)\s*\(",
|
|
189
|
+
lines[j - 1],
|
|
190
|
+
)
|
|
191
|
+
if hm:
|
|
192
|
+
handler = hm.group(1)
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
ctx = "\n".join(lines[max(0, i - 5) : i])
|
|
196
|
+
auth = bool(auth_pat.search(ctx))
|
|
197
|
+
|
|
198
|
+
routes.append(
|
|
199
|
+
self._make_route(
|
|
200
|
+
method,
|
|
201
|
+
path,
|
|
202
|
+
handler,
|
|
203
|
+
file_path,
|
|
204
|
+
i,
|
|
205
|
+
framework="ASP.NET Core",
|
|
206
|
+
auth_required=auth if auth else None,
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
return routes
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from pathlib import PurePosixPath
|
|
3
|
+
|
|
4
|
+
from patchi.core.brain.route_detector.base import BaseRouteDetector
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class NextJsRouteDetector(BaseRouteDetector):
|
|
8
|
+
def detect(self, content: str, file_path: str) -> list[dict]:
|
|
9
|
+
routes: list[dict] = []
|
|
10
|
+
p = PurePosixPath(file_path)
|
|
11
|
+
parts = p.parts
|
|
12
|
+
|
|
13
|
+
if "pages" in parts and "api" in parts:
|
|
14
|
+
idx = parts.index("pages")
|
|
15
|
+
path_parts = parts[idx + 1 :]
|
|
16
|
+
url_path = "/" + "/".join(path_parts).replace("[", ":").replace("]", "")
|
|
17
|
+
url_path = re.sub(r"\.(?:js|ts|jsx|tsx)$", "", url_path)
|
|
18
|
+
if url_path.endswith("/index"):
|
|
19
|
+
url_path = url_path[:-6] or "/"
|
|
20
|
+
routes.append(
|
|
21
|
+
self._make_route("ANY", url_path, p.stem, file_path, 1, framework="Next.js")
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
elif "app" in parts and p.stem in ("route", "page"):
|
|
25
|
+
idx = parts.index("app")
|
|
26
|
+
path_parts = [x for x in parts[idx + 1 : -1] if not x.startswith("(")]
|
|
27
|
+
url_path = "/" + "/".join(path_parts).replace("[", ":").replace("]", "")
|
|
28
|
+
if not url_path:
|
|
29
|
+
url_path = "/"
|
|
30
|
+
routes.append(
|
|
31
|
+
self._make_route(
|
|
32
|
+
"ANY" if p.stem == "route" else "GET",
|
|
33
|
+
url_path,
|
|
34
|
+
p.stem,
|
|
35
|
+
file_path,
|
|
36
|
+
1,
|
|
37
|
+
framework="Next.js",
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return routes
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class NuxtRouteDetector(BaseRouteDetector):
|
|
45
|
+
def detect(self, content: str, file_path: str) -> list[dict]:
|
|
46
|
+
routes: list[dict] = []
|
|
47
|
+
p = PurePosixPath(file_path)
|
|
48
|
+
if "pages" not in p.parts:
|
|
49
|
+
return routes
|
|
50
|
+
|
|
51
|
+
idx = p.parts.index("pages")
|
|
52
|
+
path_parts = list(p.parts[idx + 1 :])
|
|
53
|
+
stem = PurePosixPath(path_parts[-1]).stem if path_parts else ""
|
|
54
|
+
path_parts[-1] = stem
|
|
55
|
+
url_path = "/" + "/".join(path_parts).replace("[", ":").replace("]", "")
|
|
56
|
+
if url_path.endswith("/index"):
|
|
57
|
+
url_path = url_path[:-6] or "/"
|
|
58
|
+
routes.append(self._make_route("GET", url_path, stem, file_path, 1, framework="Nuxt"))
|
|
59
|
+
return routes
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class SvelteKitRouteDetector(BaseRouteDetector):
|
|
63
|
+
def detect(self, content: str, file_path: str) -> list[dict]:
|
|
64
|
+
routes: list[dict] = []
|
|
65
|
+
p = PurePosixPath(file_path)
|
|
66
|
+
if "routes" not in p.parts:
|
|
67
|
+
return routes
|
|
68
|
+
|
|
69
|
+
idx = p.parts.index("routes")
|
|
70
|
+
path_parts = [x for x in p.parts[idx + 1 : -1] if not x.startswith("(")]
|
|
71
|
+
url_path = "/" + "/".join(path_parts).replace("[", ":").replace("]", "")
|
|
72
|
+
if not url_path:
|
|
73
|
+
url_path = "/"
|
|
74
|
+
|
|
75
|
+
method = (
|
|
76
|
+
"ANY" if (p.name.endswith("+server.ts") or p.name.endswith("+server.js")) else "GET"
|
|
77
|
+
)
|
|
78
|
+
routes.append(
|
|
79
|
+
self._make_route(method, url_path, p.stem, file_path, 1, framework="SvelteKit")
|
|
80
|
+
)
|
|
81
|
+
return routes
|