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,710 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared syntax queries over tree-sitter — the replacement for per-agent regex.
|
|
3
|
+
|
|
4
|
+
Agents used to detect code constructs (setInterval calls, `var` declarations,
|
|
5
|
+
JSX text, `require()` …) with regular expressions, which misfire on comments,
|
|
6
|
+
strings, and formatting variants. These helpers parse once with tree-sitter
|
|
7
|
+
(error-tolerant: partial trees still yield captures) and answer structural
|
|
8
|
+
questions. All functions are fail-open: unparseable input returns empty
|
|
9
|
+
collections, never raises.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
|
|
17
|
+
_log = logging.getLogger("patchi.brain.code_query")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class JsCall:
|
|
22
|
+
"""A call site: property name, full dotted callee, line, args, span text."""
|
|
23
|
+
|
|
24
|
+
name: str
|
|
25
|
+
line: int
|
|
26
|
+
arg_kinds: list[str] = field(default_factory=list)
|
|
27
|
+
full: str = ""
|
|
28
|
+
text: str = ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _parser(lang: str):
|
|
32
|
+
try:
|
|
33
|
+
if lang == "tsx":
|
|
34
|
+
import tree_sitter_typescript as tsts
|
|
35
|
+
from tree_sitter import Language, Parser
|
|
36
|
+
|
|
37
|
+
return Parser(Language(tsts.language_tsx()))
|
|
38
|
+
from patchi.core.brain.languages import Lang, get_parser
|
|
39
|
+
|
|
40
|
+
return get_parser(Lang(lang))
|
|
41
|
+
except Exception as exc: # noqa: BLE001
|
|
42
|
+
_log.debug("code_query parser unavailable: %s", exc)
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _query_text(lang_obj, pattern: str, node):
|
|
47
|
+
from tree_sitter import Query, QueryCursor
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
return QueryCursor(Query(lang_obj, pattern)).captures(node)
|
|
51
|
+
except Exception as exc: # noqa: BLE001
|
|
52
|
+
_log.debug("code_query pattern failed %r: %s", pattern, exc)
|
|
53
|
+
return {}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _lang_obj(lang: str):
|
|
57
|
+
from tree_sitter import Language
|
|
58
|
+
|
|
59
|
+
if lang == "typescript":
|
|
60
|
+
import tree_sitter_typescript as tsts
|
|
61
|
+
|
|
62
|
+
return Language(tsts.language_typescript())
|
|
63
|
+
if lang == "tsx":
|
|
64
|
+
import tree_sitter_typescript as tsts
|
|
65
|
+
|
|
66
|
+
return Language(tsts.language_tsx())
|
|
67
|
+
import tree_sitter_javascript as tsjs
|
|
68
|
+
|
|
69
|
+
return Language(tsjs.language())
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def lang_for_file(filename: str) -> str:
|
|
73
|
+
"""Tree-sitter language key for a path (tsx needs its own grammar)."""
|
|
74
|
+
name = filename.lower()
|
|
75
|
+
if name.endswith(".tsx"):
|
|
76
|
+
return "tsx"
|
|
77
|
+
if name.endswith((".ts", ".mts", ".cts")):
|
|
78
|
+
return "typescript"
|
|
79
|
+
return "javascript"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _node_text(node) -> str:
|
|
83
|
+
t = node.text
|
|
84
|
+
if isinstance(t, bytes):
|
|
85
|
+
return t.decode("utf-8", errors="replace")
|
|
86
|
+
return str(t)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _line(node) -> int:
|
|
90
|
+
return node.start_point[0] + 1
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def parse_js(text: str, lang: str = "javascript"):
|
|
94
|
+
"""Parse JS/JSX/TS/TSX source. Returns the tree or None (fail-open)."""
|
|
95
|
+
parser = _parser(lang)
|
|
96
|
+
if parser is None:
|
|
97
|
+
return None
|
|
98
|
+
try:
|
|
99
|
+
return parser.parse(text.encode("utf-8", errors="replace"))
|
|
100
|
+
except Exception as exc: # noqa: BLE001
|
|
101
|
+
_log.debug("code_query parse failed: %s", exc)
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _callee_name(call_node) -> str:
|
|
106
|
+
fn = call_node.child_by_field_name("function")
|
|
107
|
+
if fn is None:
|
|
108
|
+
return ""
|
|
109
|
+
if fn.type == "identifier":
|
|
110
|
+
return _node_text(fn)
|
|
111
|
+
if fn.type == "member_expression":
|
|
112
|
+
prop = fn.child_by_field_name("property")
|
|
113
|
+
if prop is not None:
|
|
114
|
+
return _node_text(prop)
|
|
115
|
+
text = _node_text(fn)
|
|
116
|
+
return text.rsplit(".", 1)[-1] if "." in text else text
|
|
117
|
+
return ""
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def js_calls(tree, lang: str = "javascript") -> list[JsCall]:
|
|
121
|
+
"""Every call site with callee name, full dotted path, line, arg kinds."""
|
|
122
|
+
if tree is None:
|
|
123
|
+
return []
|
|
124
|
+
lang_obj = _lang_obj(lang)
|
|
125
|
+
out: list[JsCall] = []
|
|
126
|
+
for nodes in _query_text(lang_obj, "(call_expression) @c", tree.root_node).values():
|
|
127
|
+
for node in nodes:
|
|
128
|
+
args = node.child_by_field_name("arguments")
|
|
129
|
+
kinds = [c.type for c in args.named_children] if args is not None else []
|
|
130
|
+
fn = node.child_by_field_name("function")
|
|
131
|
+
full = _node_text(fn) if fn is not None else ""
|
|
132
|
+
out.append(
|
|
133
|
+
JsCall(
|
|
134
|
+
name=_callee_name(node),
|
|
135
|
+
line=_line(node),
|
|
136
|
+
arg_kinds=kinds,
|
|
137
|
+
full=full,
|
|
138
|
+
text=_node_text(node),
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
return out
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def js_useeffect_without_cleanup(tree, lang: str = "javascript") -> list[int]:
|
|
145
|
+
"""Lines of useEffect calls whose callback has no return statement."""
|
|
146
|
+
if tree is None:
|
|
147
|
+
return []
|
|
148
|
+
lang_obj = _lang_obj(lang)
|
|
149
|
+
out: list[int] = []
|
|
150
|
+
for nodes in _query_text(lang_obj, "(call_expression) @c", tree.root_node).values():
|
|
151
|
+
for node in nodes:
|
|
152
|
+
if _callee_name(node) != "useEffect":
|
|
153
|
+
continue
|
|
154
|
+
args = node.child_by_field_name("arguments")
|
|
155
|
+
if args is None:
|
|
156
|
+
continue
|
|
157
|
+
has_return = False
|
|
158
|
+
for arg in args.named_children:
|
|
159
|
+
if arg.type in ("arrow_function", "function_expression", "function"):
|
|
160
|
+
ret = _query_text(lang_obj, "(return_statement) @r", arg)
|
|
161
|
+
if any(ret.values()):
|
|
162
|
+
has_return = True
|
|
163
|
+
break
|
|
164
|
+
if not has_return:
|
|
165
|
+
out.append(_line(node))
|
|
166
|
+
return out
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def js_call_names(tree, lang: str = "javascript") -> set[str]:
|
|
170
|
+
"""Set of callee names called anywhere in the file."""
|
|
171
|
+
return {c.name for c in js_calls(tree, lang) if c.name}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def js_var_kinds(tree, lang: str = "javascript") -> list[tuple[str, int]]:
|
|
175
|
+
"""(kind, line) for every var/let/const declaration."""
|
|
176
|
+
lang_obj = _lang_obj(lang)
|
|
177
|
+
out: list[tuple[str, int]] = []
|
|
178
|
+
if tree is None:
|
|
179
|
+
return []
|
|
180
|
+
found = _query_text(
|
|
181
|
+
lang_obj, "[(variable_declaration) (lexical_declaration)] @d", tree.root_node
|
|
182
|
+
)
|
|
183
|
+
for nodes in found.values():
|
|
184
|
+
for node in nodes:
|
|
185
|
+
text = _node_text(node).lstrip()
|
|
186
|
+
kind = text.split(None, 1)[0] if text else ""
|
|
187
|
+
if kind in ("var", "let", "const"):
|
|
188
|
+
out.append((kind, _line(node)))
|
|
189
|
+
return out
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def js_has_await(tree, lang: str = "javascript") -> bool:
|
|
193
|
+
if tree is None:
|
|
194
|
+
return False
|
|
195
|
+
lang_obj = _lang_obj(lang)
|
|
196
|
+
return bool(_query_text(lang_obj, "(await_expression) @a", tree.root_node))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def js_new_names(tree, lang: str = "javascript") -> set[str]:
|
|
200
|
+
"""Constructor names from `new X(...)` expressions."""
|
|
201
|
+
lang_obj = _lang_obj(lang)
|
|
202
|
+
out: set[str] = set()
|
|
203
|
+
found = _query_text(lang_obj, "(new_expression constructor: (_) @c)", tree.root_node)
|
|
204
|
+
for nodes in found.values():
|
|
205
|
+
for node in nodes:
|
|
206
|
+
out.add(_node_text(node))
|
|
207
|
+
return out
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def js_jsx_texts(tree, lang: str = "javascript") -> list[tuple[str, int]]:
|
|
211
|
+
"""Visible JSX text fragments with lines."""
|
|
212
|
+
lang_obj = _lang_obj(lang)
|
|
213
|
+
out: list[tuple[str, int]] = []
|
|
214
|
+
for nodes in _query_text(lang_obj, "(jsx_text) @t", tree.root_node).values():
|
|
215
|
+
for node in nodes:
|
|
216
|
+
text = _node_text(node).strip()
|
|
217
|
+
if text:
|
|
218
|
+
out.append((text, _line(node)))
|
|
219
|
+
return out
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def js_class_heritages(tree, lang: str = "javascript") -> list[tuple[str, int]]:
|
|
223
|
+
"""`extends ...` clause text with lines."""
|
|
224
|
+
lang_obj = _lang_obj(lang)
|
|
225
|
+
out: list[tuple[str, int]] = []
|
|
226
|
+
for nodes in _query_text(lang_obj, "(class_heritage) @h", tree.root_node).values():
|
|
227
|
+
for node in nodes:
|
|
228
|
+
out.append((_node_text(node), _line(node)))
|
|
229
|
+
return out
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def js_identifiers(tree, lang: str = "javascript") -> set[str]:
|
|
233
|
+
"""All identifier spellings (for $store-style prefix checks)."""
|
|
234
|
+
return {text for text, _line_no in js_identifier_lines(tree, lang)}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def js_property_names(tree, lang: str = "javascript") -> set[str]:
|
|
238
|
+
"""Object/dict key spellings (property_identifier nodes)."""
|
|
239
|
+
if tree is None:
|
|
240
|
+
return set()
|
|
241
|
+
lang_obj = _lang_obj(lang)
|
|
242
|
+
out: set[str] = set()
|
|
243
|
+
for nodes in _query_text(lang_obj, "(property_identifier) @p", tree.root_node).values():
|
|
244
|
+
for node in nodes:
|
|
245
|
+
out.add(_node_text(node))
|
|
246
|
+
return out
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def js_identifier_lines(tree, lang: str = "javascript") -> list[tuple[str, int]]:
|
|
250
|
+
"""(identifier spelling, line) pairs."""
|
|
251
|
+
if tree is None:
|
|
252
|
+
return []
|
|
253
|
+
lang_obj = _lang_obj(lang)
|
|
254
|
+
out: list[tuple[str, int]] = []
|
|
255
|
+
for nodes in _query_text(lang_obj, "(identifier) @i", tree.root_node).values():
|
|
256
|
+
for node in nodes:
|
|
257
|
+
out.append((_node_text(node), _line(node)))
|
|
258
|
+
return out
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def js_constructor_di_line(tree, lang: str = "javascript") -> int:
|
|
262
|
+
"""Line of a constructor() with a private/parameter-property param, else 0."""
|
|
263
|
+
if tree is None:
|
|
264
|
+
return 0
|
|
265
|
+
lang_obj = _lang_obj(lang)
|
|
266
|
+
found = _query_text(lang_obj, "(method_definition) @m", tree.root_node)
|
|
267
|
+
for nodes in found.values():
|
|
268
|
+
for node in nodes:
|
|
269
|
+
name_node = node.child_by_field_name("name")
|
|
270
|
+
if name_node is None or _node_text(name_node) != "constructor":
|
|
271
|
+
continue
|
|
272
|
+
params = node.child_by_field_name("parameters")
|
|
273
|
+
if params is not None and "private" in _node_text(params):
|
|
274
|
+
return _line(node)
|
|
275
|
+
return 0
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def js_string_literals(tree, lang: str = "javascript") -> list[tuple[str, int]]:
|
|
279
|
+
"""String literal contents with lines (quotes stripped)."""
|
|
280
|
+
lang_obj = _lang_obj(lang)
|
|
281
|
+
out: list[tuple[str, int]] = []
|
|
282
|
+
for nodes in _query_text(lang_obj, "(string) @s", tree.root_node).values():
|
|
283
|
+
for node in nodes:
|
|
284
|
+
text = _node_text(node)
|
|
285
|
+
if len(text) >= 2 and text[0] == text[-1] and text[0] in "\"'`":
|
|
286
|
+
text = text[1:-1]
|
|
287
|
+
out.append((text, _line(node)))
|
|
288
|
+
return out
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def js_has_try(tree, lang: str = "javascript") -> bool:
|
|
292
|
+
if tree is None:
|
|
293
|
+
return False
|
|
294
|
+
lang_obj = _lang_obj(lang)
|
|
295
|
+
return bool(_query_text(lang_obj, "(try_statement) @t", tree.root_node))
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _catch_in_subtree(lang_obj, node) -> bool:
|
|
299
|
+
sub = _query_text(lang_obj, "(call_expression) @c", node)
|
|
300
|
+
return any(_callee_name(n) == "catch" for ns in sub.values() for n in ns)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _assigned_name(stmt) -> str:
|
|
304
|
+
if stmt.type == "variable_declarator":
|
|
305
|
+
name_node = stmt.child_by_field_name("name")
|
|
306
|
+
if name_node is not None:
|
|
307
|
+
return _node_text(name_node)
|
|
308
|
+
return ""
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def js_new_without_catch(tree, names=("Promise",), lang: str = "javascript") -> list[int]:
|
|
312
|
+
"""Lines of `new X(...)` with no `.catch` — same statement or later `v.catch`."""
|
|
313
|
+
if tree is None:
|
|
314
|
+
return []
|
|
315
|
+
lang_obj = _lang_obj(lang)
|
|
316
|
+
out: list[int] = []
|
|
317
|
+
for nodes in _query_text(lang_obj, "(new_expression) @n", tree.root_node).values():
|
|
318
|
+
for node in nodes:
|
|
319
|
+
ctor = node.child_by_field_name("constructor")
|
|
320
|
+
cname = _node_text(ctor) if ctor is not None else ""
|
|
321
|
+
if cname not in names:
|
|
322
|
+
continue
|
|
323
|
+
stmt = node
|
|
324
|
+
var_name = ""
|
|
325
|
+
while stmt.parent is not None and stmt.type not in (
|
|
326
|
+
"expression_statement",
|
|
327
|
+
"variable_declaration",
|
|
328
|
+
"lexical_declaration",
|
|
329
|
+
"return_statement",
|
|
330
|
+
):
|
|
331
|
+
if stmt.type == "variable_declarator" and not var_name:
|
|
332
|
+
var_name = _assigned_name(stmt)
|
|
333
|
+
stmt = stmt.parent
|
|
334
|
+
if _catch_in_subtree(lang_obj, stmt):
|
|
335
|
+
continue
|
|
336
|
+
if var_name:
|
|
337
|
+
parent = stmt.parent
|
|
338
|
+
if parent is not None:
|
|
339
|
+
kids = parent.named_children
|
|
340
|
+
try:
|
|
341
|
+
idx = kids.index(stmt)
|
|
342
|
+
except ValueError:
|
|
343
|
+
idx = -1
|
|
344
|
+
handled = False
|
|
345
|
+
for sib in kids[idx + 1 :]:
|
|
346
|
+
sib_calls = _query_text(lang_obj, "(call_expression) @c", sib)
|
|
347
|
+
for ns in sib_calls.values():
|
|
348
|
+
for n in ns:
|
|
349
|
+
fn = n.child_by_field_name("function")
|
|
350
|
+
if (
|
|
351
|
+
fn is not None
|
|
352
|
+
and fn.type == "member_expression"
|
|
353
|
+
and _node_text(fn).startswith(var_name + ".")
|
|
354
|
+
and _callee_name(n) == "catch"
|
|
355
|
+
):
|
|
356
|
+
handled = True
|
|
357
|
+
break
|
|
358
|
+
if handled:
|
|
359
|
+
break
|
|
360
|
+
if handled:
|
|
361
|
+
break
|
|
362
|
+
if handled:
|
|
363
|
+
continue
|
|
364
|
+
out.append(_line(node))
|
|
365
|
+
return out
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def js_jsx_elements(tree, lang: str = "javascript") -> list[tuple[str, int]]:
|
|
369
|
+
"""(tag, line) for every JSX opening element."""
|
|
370
|
+
if tree is None:
|
|
371
|
+
return []
|
|
372
|
+
lang_obj = _lang_obj(lang)
|
|
373
|
+
out: list[tuple[str, int]] = []
|
|
374
|
+
found = _query_text(lang_obj, "(jsx_opening_element) @o", tree.root_node)
|
|
375
|
+
for nodes in found.values():
|
|
376
|
+
for node in nodes:
|
|
377
|
+
for child in node.named_children:
|
|
378
|
+
if child.type == "identifier":
|
|
379
|
+
out.append((_node_text(child), _line(node)))
|
|
380
|
+
break
|
|
381
|
+
return out
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def js_jsx_attributes(tree, lang: str = "javascript") -> list[tuple[str, str, int]]:
|
|
385
|
+
"""(tag, attribute_name, line) for every JSX attribute."""
|
|
386
|
+
if tree is None:
|
|
387
|
+
return []
|
|
388
|
+
lang_obj = _lang_obj(lang)
|
|
389
|
+
out: list[tuple[str, str, int]] = []
|
|
390
|
+
found = _query_text(lang_obj, "(jsx_opening_element) @o", tree.root_node)
|
|
391
|
+
for nodes in found.values():
|
|
392
|
+
for node in nodes:
|
|
393
|
+
tag = ""
|
|
394
|
+
for child in node.named_children:
|
|
395
|
+
if child.type == "identifier":
|
|
396
|
+
tag = _node_text(child)
|
|
397
|
+
break
|
|
398
|
+
for child in node.named_children:
|
|
399
|
+
if child.type == "jsx_attribute":
|
|
400
|
+
name_node = child.child_by_field_name("name")
|
|
401
|
+
if name_node is None and child.named_children:
|
|
402
|
+
name_node = child.named_children[0]
|
|
403
|
+
if name_node is not None:
|
|
404
|
+
out.append((tag, _node_text(name_node), _line(child)))
|
|
405
|
+
return out
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def extract_script_blocks(text: str) -> list[str]:
|
|
409
|
+
"""Raw JS from <script> blocks in SFC/HTML (for .vue/.svelte parsing)."""
|
|
410
|
+
from html.parser import HTMLParser
|
|
411
|
+
|
|
412
|
+
out: list[str] = []
|
|
413
|
+
current: list[str] | None = None
|
|
414
|
+
|
|
415
|
+
class _P(HTMLParser):
|
|
416
|
+
def handle_starttag(self, tag, attrs):
|
|
417
|
+
nonlocal current
|
|
418
|
+
if tag == "script":
|
|
419
|
+
current = []
|
|
420
|
+
|
|
421
|
+
def handle_endtag(self, tag):
|
|
422
|
+
nonlocal current
|
|
423
|
+
if tag == "script" and current is not None:
|
|
424
|
+
out.append("".join(current))
|
|
425
|
+
current = None
|
|
426
|
+
|
|
427
|
+
def handle_data(self, data):
|
|
428
|
+
if current is not None:
|
|
429
|
+
current.append(data)
|
|
430
|
+
|
|
431
|
+
try:
|
|
432
|
+
_P().feed(text)
|
|
433
|
+
except Exception as exc: # noqa: BLE001
|
|
434
|
+
_log.debug("script extract failed: %s", exc)
|
|
435
|
+
return out
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def vue_template_texts(text: str) -> list[tuple[str, int]]:
|
|
439
|
+
"""Visible text nodes from Vue/SFC/HTML markup via html.parser."""
|
|
440
|
+
from html.parser import HTMLParser
|
|
441
|
+
|
|
442
|
+
out: list[tuple[str, int]] = []
|
|
443
|
+
|
|
444
|
+
class _P(HTMLParser):
|
|
445
|
+
def handle_data(self, data):
|
|
446
|
+
rest, chunks = data, []
|
|
447
|
+
while "{{" in rest and "}}" in rest:
|
|
448
|
+
before, _, rest = rest.partition("{{")
|
|
449
|
+
chunks.append(before)
|
|
450
|
+
_, _, rest = rest.partition("}}")
|
|
451
|
+
chunks.append(rest)
|
|
452
|
+
stripped = " ".join(chunks).strip()
|
|
453
|
+
if stripped:
|
|
454
|
+
line, _ = self.getpos()
|
|
455
|
+
out.append((stripped, line))
|
|
456
|
+
|
|
457
|
+
try:
|
|
458
|
+
_P().feed(text)
|
|
459
|
+
except Exception as exc: # noqa: BLE001
|
|
460
|
+
_log.debug("template text parse failed: %s", exc)
|
|
461
|
+
return out
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def string_literals(text: str, lang) -> list[tuple[str, int]]:
|
|
465
|
+
"""(unquoted value, 1-based line) for string/symbol literals, any grammar.
|
|
466
|
+
|
|
467
|
+
Tries common string node types so one helper serves every installed
|
|
468
|
+
tree-sitter grammar. Fail-open: [] when the grammar is missing.
|
|
469
|
+
"""
|
|
470
|
+
from tree_sitter import Query, QueryCursor
|
|
471
|
+
|
|
472
|
+
try:
|
|
473
|
+
from patchi.core.brain.languages import get_parser
|
|
474
|
+
|
|
475
|
+
parser = get_parser(lang)
|
|
476
|
+
except Exception:
|
|
477
|
+
return []
|
|
478
|
+
if parser is None:
|
|
479
|
+
return []
|
|
480
|
+
try:
|
|
481
|
+
tree = parser.parse(text.encode("utf-8", errors="replace"))
|
|
482
|
+
except Exception:
|
|
483
|
+
return []
|
|
484
|
+
try:
|
|
485
|
+
language = tree.language
|
|
486
|
+
except Exception:
|
|
487
|
+
return []
|
|
488
|
+
seen: set[tuple[str, int]] = set()
|
|
489
|
+
out: list[tuple[str, int]] = []
|
|
490
|
+
patterns = (
|
|
491
|
+
"(string) @s",
|
|
492
|
+
"(string_literal) @s",
|
|
493
|
+
"(interpreted_string_literal) @s",
|
|
494
|
+
"(raw_string_literal) @s",
|
|
495
|
+
"(encapsed_string) @s",
|
|
496
|
+
"(symbol) @s",
|
|
497
|
+
)
|
|
498
|
+
for pattern in patterns:
|
|
499
|
+
try:
|
|
500
|
+
found = QueryCursor(Query(language, pattern)).captures(tree.root_node)
|
|
501
|
+
except Exception:
|
|
502
|
+
continue
|
|
503
|
+
for nodes in found.values():
|
|
504
|
+
for node in nodes:
|
|
505
|
+
raw = _node_text(node)
|
|
506
|
+
val = raw.strip()
|
|
507
|
+
if len(val) >= 2 and val[0] == val[-1] and val[0] in "\"'`":
|
|
508
|
+
val = val[1:-1]
|
|
509
|
+
val = val.lstrip(":")
|
|
510
|
+
if val:
|
|
511
|
+
key = (val, _line(node))
|
|
512
|
+
if key not in seen:
|
|
513
|
+
seen.add(key)
|
|
514
|
+
out.append(key)
|
|
515
|
+
return out
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def js_string_pairs(tree, lang: str = "javascript") -> list[tuple[str, str, int]]:
|
|
519
|
+
"""(key, value, line) for object properties with string values."""
|
|
520
|
+
if tree is None:
|
|
521
|
+
return []
|
|
522
|
+
lang_obj = _lang_obj(lang)
|
|
523
|
+
out: list[tuple[str, str, int]] = []
|
|
524
|
+
for nodes in _query_text(lang_obj, "(pair) @p", tree.root_node).values():
|
|
525
|
+
for node in nodes:
|
|
526
|
+
key_node = node.child_by_field_name("key")
|
|
527
|
+
val_node = node.child_by_field_name("value")
|
|
528
|
+
if key_node is None or val_node is None:
|
|
529
|
+
continue
|
|
530
|
+
key = _node_text(key_node).strip("\"'`")
|
|
531
|
+
val = _node_text(val_node).strip()
|
|
532
|
+
if len(val) >= 2 and val[0] == val[-1] and val[0] in "\"'`":
|
|
533
|
+
val = val[1:-1]
|
|
534
|
+
out.append((key, val, _line(node)))
|
|
535
|
+
return out
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def member_paths(text: str, lang) -> list[tuple[str, int]]:
|
|
539
|
+
"""(dotted member-access text, line) for attribute/subscript/member nodes.
|
|
540
|
+
|
|
541
|
+
`lang` is a languages.Lang enum member. Tries member-ish node types across
|
|
542
|
+
grammars (member_expression, attribute, subscript_expression, ...),
|
|
543
|
+
skipping types the grammar lacks. Fail-open: [] when unparseable.
|
|
544
|
+
"""
|
|
545
|
+
from tree_sitter import Query, QueryCursor
|
|
546
|
+
|
|
547
|
+
try:
|
|
548
|
+
from patchi.core.brain.languages import get_parser
|
|
549
|
+
|
|
550
|
+
parser = get_parser(lang)
|
|
551
|
+
except Exception:
|
|
552
|
+
return []
|
|
553
|
+
if parser is None:
|
|
554
|
+
return []
|
|
555
|
+
try:
|
|
556
|
+
tree = parser.parse(text.encode("utf-8", errors="replace"))
|
|
557
|
+
language = tree.language
|
|
558
|
+
except Exception:
|
|
559
|
+
return []
|
|
560
|
+
seen: set[tuple[str, int]] = set()
|
|
561
|
+
out: list[tuple[str, int]] = []
|
|
562
|
+
for node_type in (
|
|
563
|
+
"member_expression",
|
|
564
|
+
"attribute",
|
|
565
|
+
"subscript_expression",
|
|
566
|
+
"member_access_expression",
|
|
567
|
+
"scoped_identifier",
|
|
568
|
+
"field_expression",
|
|
569
|
+
):
|
|
570
|
+
try:
|
|
571
|
+
found = QueryCursor(Query(language, f"({node_type}) @m")).captures(
|
|
572
|
+
tree.root_node
|
|
573
|
+
)
|
|
574
|
+
except Exception:
|
|
575
|
+
continue
|
|
576
|
+
for nodes in found.values():
|
|
577
|
+
for node in nodes:
|
|
578
|
+
text_val = _node_text(node)
|
|
579
|
+
# Skip bare identifiers and call-covered nodes; keep dotted paths
|
|
580
|
+
if "." not in text_val and "[" not in text_val:
|
|
581
|
+
continue
|
|
582
|
+
key = (text_val, _line(node))
|
|
583
|
+
if key not in seen:
|
|
584
|
+
seen.add(key)
|
|
585
|
+
out.append(key)
|
|
586
|
+
return out
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def calls_with_dynamic_arg(text: str, lang, names: set[str]) -> list[int]:
|
|
590
|
+
"""Lines of calls to `names` whose arguments build strings dynamically.
|
|
591
|
+
|
|
592
|
+
Python: +/% BinOp, f-strings, .format() in call args (stdlib ast).
|
|
593
|
+
JS/TS: binary_expression / template_string in call args (tree-sitter).
|
|
594
|
+
`lang` is a languages.Lang enum member. Fail-open: [].
|
|
595
|
+
"""
|
|
596
|
+
import ast as _ast
|
|
597
|
+
|
|
598
|
+
try:
|
|
599
|
+
from patchi.core.brain.languages import Lang
|
|
600
|
+
except Exception:
|
|
601
|
+
return []
|
|
602
|
+
wanted = {n.split(".")[-1] for n in names} | set(names)
|
|
603
|
+
if lang == Lang.PYTHON:
|
|
604
|
+
try:
|
|
605
|
+
tree = _ast.parse(text)
|
|
606
|
+
except SyntaxError:
|
|
607
|
+
return []
|
|
608
|
+
out: list[int] = []
|
|
609
|
+
for node in _ast.walk(tree):
|
|
610
|
+
if not isinstance(node, _ast.Call):
|
|
611
|
+
continue
|
|
612
|
+
func = node.func
|
|
613
|
+
if isinstance(func, _ast.Name):
|
|
614
|
+
dotted = func.id
|
|
615
|
+
elif isinstance(func, _ast.Attribute):
|
|
616
|
+
parts, cur = [], func
|
|
617
|
+
while isinstance(cur, _ast.Attribute):
|
|
618
|
+
parts.append(cur.attr)
|
|
619
|
+
cur = cur.value
|
|
620
|
+
if isinstance(cur, _ast.Name):
|
|
621
|
+
parts.append(cur.id)
|
|
622
|
+
dotted = ".".join(reversed(parts))
|
|
623
|
+
else:
|
|
624
|
+
continue
|
|
625
|
+
if dotted not in names and dotted.split(".")[-1] not in wanted:
|
|
626
|
+
continue
|
|
627
|
+
dynamic = False
|
|
628
|
+
for arg in list(node.args) + [kw.value for kw in node.keywords]:
|
|
629
|
+
for sub in _ast.walk(arg):
|
|
630
|
+
if isinstance(sub, (_ast.BinOp, _ast.JoinedStr)):
|
|
631
|
+
dynamic = True
|
|
632
|
+
break
|
|
633
|
+
if isinstance(sub, _ast.Call):
|
|
634
|
+
f = sub.func
|
|
635
|
+
fn = f.id if isinstance(f, _ast.Name) else getattr(f, "attr", "")
|
|
636
|
+
if fn == "format":
|
|
637
|
+
dynamic = True
|
|
638
|
+
break
|
|
639
|
+
if dynamic:
|
|
640
|
+
break
|
|
641
|
+
if dynamic:
|
|
642
|
+
out.append(getattr(node, "lineno", 0))
|
|
643
|
+
return sorted(set(out))
|
|
644
|
+
# tree-sitter languages: dynamic = binary/template args in the call span
|
|
645
|
+
try:
|
|
646
|
+
from patchi.core.brain.languages import get_parser
|
|
647
|
+
|
|
648
|
+
parser = get_parser(lang)
|
|
649
|
+
except Exception:
|
|
650
|
+
return []
|
|
651
|
+
if parser is None:
|
|
652
|
+
return []
|
|
653
|
+
try:
|
|
654
|
+
parsed = parser.parse(text.encode("utf-8", errors="replace"))
|
|
655
|
+
language = parsed.language
|
|
656
|
+
except Exception:
|
|
657
|
+
return []
|
|
658
|
+
from tree_sitter import Query, QueryCursor
|
|
659
|
+
|
|
660
|
+
out = []
|
|
661
|
+
try:
|
|
662
|
+
found = QueryCursor(
|
|
663
|
+
Query(language, "(call_expression) @c")
|
|
664
|
+
).captures(parsed.root_node)
|
|
665
|
+
except Exception:
|
|
666
|
+
return []
|
|
667
|
+
for nodes in found.values():
|
|
668
|
+
for node in nodes:
|
|
669
|
+
fn = node.child_by_field_name("function")
|
|
670
|
+
dotted = _node_text(fn) if fn is not None else ""
|
|
671
|
+
if dotted not in names and dotted.split(".")[-1] not in wanted:
|
|
672
|
+
continue
|
|
673
|
+
args = node.child_by_field_name("arguments")
|
|
674
|
+
if args is None:
|
|
675
|
+
continue
|
|
676
|
+
try:
|
|
677
|
+
dyn = QueryCursor(
|
|
678
|
+
Query(
|
|
679
|
+
language,
|
|
680
|
+
"[(binary_expression) (template_string) (template_substitution)] @d",
|
|
681
|
+
)
|
|
682
|
+
).captures(args)
|
|
683
|
+
except Exception:
|
|
684
|
+
continue
|
|
685
|
+
if any(dyn.values()):
|
|
686
|
+
out.append(_line(node))
|
|
687
|
+
return sorted(set(out))
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def vue_template_attrs(text: str) -> list[tuple[str, str, int]]:
|
|
691
|
+
"""(tag, attr_name, line) from Vue/SFC template markup via html.parser.
|
|
692
|
+
|
|
693
|
+
tree-sitter-javascript cannot parse HTML templates; the stdlib HTML parser
|
|
694
|
+
reads real tag structure instead of regexing angle brackets.
|
|
695
|
+
"""
|
|
696
|
+
from html.parser import HTMLParser
|
|
697
|
+
|
|
698
|
+
out: list[tuple[str, str, int]] = []
|
|
699
|
+
|
|
700
|
+
class _P(HTMLParser):
|
|
701
|
+
def handle_starttag(self, tag, attrs):
|
|
702
|
+
line, _ = self.getpos()
|
|
703
|
+
for name, _value in attrs:
|
|
704
|
+
out.append((tag, name, line))
|
|
705
|
+
|
|
706
|
+
try:
|
|
707
|
+
_P().feed(text)
|
|
708
|
+
except Exception as exc: # noqa: BLE001
|
|
709
|
+
_log.debug("vue template parse failed: %s", exc)
|
|
710
|
+
return out
|