alysis-code 0.13.0__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.
- alysis_code/__init__.py +3 -0
- alysis_code/__main__.py +4 -0
- alysis_code/_build_info.py +14 -0
- alysis_code/account_login.py +468 -0
- alysis_code/agent/README.md +35 -0
- alysis_code/agent/__init__.py +11 -0
- alysis_code/agent/acceptance_contract.py +2217 -0
- alysis_code/agent/blast_radius.py +1403 -0
- alysis_code/agent/cache_keepalive.py +227 -0
- alysis_code/agent/completion_certificate.py +366 -0
- alysis_code/agent/completion_gate.py +306 -0
- alysis_code/agent/empty_response_stall.py +403 -0
- alysis_code/agent/errors.py +28 -0
- alysis_code/agent/llm_calls.py +475 -0
- alysis_code/agent/mutation_classification.py +227 -0
- alysis_code/agent/prompt_context.py +2508 -0
- alysis_code/agent/read_ledger.py +253 -0
- alysis_code/agent/regression_baseline.py +642 -0
- alysis_code/agent/reproduction_first.py +610 -0
- alysis_code/agent/sensitive_output.py +629 -0
- alysis_code/agent/session.py +3218 -0
- alysis_code/agent/steering.py +191 -0
- alysis_code/agent/subagent_execution.py +5177 -0
- alysis_code/agent/subagent_workspace.py +666 -0
- alysis_code/agent/tools_assembly.py +4728 -0
- alysis_code/agent/turn/__init__.py +101 -0
- alysis_code/agent/turn/core.py +8483 -0
- alysis_code/agent/turn/events.py +113 -0
- alysis_code/agent/turn/exploration.py +590 -0
- alysis_code/agent/turn/interventions.py +65 -0
- alysis_code/agent/turn/read_cache.py +420 -0
- alysis_code/agent/turn/snapshot.py +179 -0
- alysis_code/agent/turn_contract.py +661 -0
- alysis_code/agent/turn_path.py +129 -0
- alysis_code/agent/verification.py +2885 -0
- alysis_code/agent/verification_commands.py +512 -0
- alysis_code/agent/verification_evidence.py +738 -0
- alysis_code/agent_loop.py +693 -0
- alysis_code/agent_runtimes/__init__.py +51 -0
- alysis_code/agent_runtimes/base.py +114 -0
- alysis_code/agent_runtimes/builtins.py +129 -0
- alysis_code/agent_runtimes/codex_cli.py +664 -0
- alysis_code/agent_runtimes/host.py +263 -0
- alysis_code/agent_runtimes/registry.py +64 -0
- alysis_code/agent_runtimes/service.py +150 -0
- alysis_code/agentbox_client.py +416 -0
- alysis_code/agentbox_integration.py +310 -0
- alysis_code/alysis_cloud.py +152 -0
- alysis_code/approval_scope.py +276 -0
- alysis_code/assets/README.md +33 -0
- alysis_code/assets/__init__.py +126 -0
- alysis_code/assets/asset_read_core.py +281 -0
- alysis_code/assets/budget_allocator.py +456 -0
- alysis_code/assets/comprehender.py +759 -0
- alysis_code/assets/index.py +654 -0
- alysis_code/assets/ingestion.py +275 -0
- alysis_code/assets/legacy_migration.py +413 -0
- alysis_code/assets/models.py +263 -0
- alysis_code/assets/ocr.py +239 -0
- alysis_code/assets/owl/ascii/f-000.txt +13 -0
- alysis_code/assets/owl/ascii/f-001.txt +13 -0
- alysis_code/assets/owl/ascii/f-002.txt +13 -0
- alysis_code/assets/owl/ascii/f-003.txt +13 -0
- alysis_code/assets/owl/ascii/f-004.txt +13 -0
- alysis_code/assets/owl/ascii/f-005.txt +13 -0
- alysis_code/assets/owl/ascii/f-006.txt +13 -0
- alysis_code/assets/owl/ascii/f-007.txt +13 -0
- alysis_code/assets/owl/ascii/f-008.txt +13 -0
- alysis_code/assets/owl/ascii/f-009.txt +13 -0
- alysis_code/assets/owl/ascii/f-010.txt +13 -0
- alysis_code/assets/owl/ascii/f-011.txt +13 -0
- alysis_code/assets/owl/ascii/f-012.txt +13 -0
- alysis_code/assets/owl/ascii/f-013.txt +13 -0
- alysis_code/assets/owl/ascii/f-014.txt +13 -0
- alysis_code/assets/owl/ascii/f-015.txt +13 -0
- alysis_code/assets/owl/ascii/f-016.txt +13 -0
- alysis_code/assets/owl/ascii/f-017.txt +13 -0
- alysis_code/assets/owl/ascii/f-018.txt +13 -0
- alysis_code/assets/owl/ascii/f-019.txt +13 -0
- alysis_code/assets/owl/ascii/f-020.txt +13 -0
- alysis_code/assets/owl/index.html +98 -0
- alysis_code/assets/owl/show-owl.sh +761 -0
- alysis_code/assets/paths.py +49 -0
- alysis_code/assets/plan_binding.py +326 -0
- alysis_code/assets/planner_context.py +466 -0
- alysis_code/assets/planner_tools.py +184 -0
- alysis_code/assets/prompts.py +101 -0
- alysis_code/assets/replanner_context.py +239 -0
- alysis_code/assets/surface.py +521 -0
- alysis_code/assets/untrusted_content.py +48 -0
- alysis_code/assets/usage_logger.py +94 -0
- alysis_code/assets/worker_mirror.py +428 -0
- alysis_code/assets/worker_section.py +303 -0
- alysis_code/assets/worker_tools.py +468 -0
- alysis_code/atomic_io.py +83 -0
- alysis_code/auth_diagnostics.py +272 -0
- alysis_code/background_runner.py +366 -0
- alysis_code/branding.py +270 -0
- alysis_code/budget_policy.py +390 -0
- alysis_code/build_identity.py +465 -0
- alysis_code/builtin_hooks/__init__.py +7 -0
- alysis_code/builtin_hooks/notify_done_windows.py +65 -0
- alysis_code/bwrap_etc.py +76 -0
- alysis_code/cancellation.py +41 -0
- alysis_code/capabilities.py +137 -0
- alysis_code/chatgpt_codex_static_provider.py +133 -0
- alysis_code/cli.py +51 -0
- alysis_code/cli_impl/__init__.py +1 -0
- alysis_code/cli_impl/assets_cli.py +537 -0
- alysis_code/cli_impl/assets_modal.py +412 -0
- alysis_code/cli_impl/chat/__init__.py +156 -0
- alysis_code/cli_impl/chat/commands.py +2616 -0
- alysis_code/cli_impl/chat/loop.py +4508 -0
- alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
- alysis_code/cli_impl/chat/rendering.py +444 -0
- alysis_code/cli_impl/chat/state.py +124 -0
- alysis_code/cli_impl/chat_resume.py +830 -0
- alysis_code/cli_impl/chat_slash_completer.py +258 -0
- alysis_code/cli_impl/commands/__init__.py +11 -0
- alysis_code/cli_impl/commands/_shared.py +89 -0
- alysis_code/cli_impl/commands/auth.py +623 -0
- alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
- alysis_code/cli_impl/commands/chat_state.py +158 -0
- alysis_code/cli_impl/commands/chat_status.py +1248 -0
- alysis_code/cli_impl/commands/chat_terminal.py +942 -0
- alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
- alysis_code/cli_impl/commands/cli_common.py +1223 -0
- alysis_code/cli_impl/commands/cli_surface.py +77 -0
- alysis_code/cli_impl/commands/config.py +131 -0
- alysis_code/cli_impl/commands/conventions.py +85 -0
- alysis_code/cli_impl/commands/execution_helpers.py +350 -0
- alysis_code/cli_impl/commands/extensions.py +401 -0
- alysis_code/cli_impl/commands/forge.py +1282 -0
- alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
- alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
- alysis_code/cli_impl/commands/hooks.py +737 -0
- alysis_code/cli_impl/commands/ide_bridge.py +31 -0
- alysis_code/cli_impl/commands/mcp.py +700 -0
- alysis_code/cli_impl/commands/profile.py +453 -0
- alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
- alysis_code/cli_impl/commands/report.py +88 -0
- alysis_code/cli_impl/commands/root.py +1118 -0
- alysis_code/cli_impl/commands/sandbox.py +184 -0
- alysis_code/cli_impl/commands/server.py +54 -0
- alysis_code/cli_impl/commands/sessions.py +252 -0
- alysis_code/cli_impl/commands/skills.py +404 -0
- alysis_code/cli_impl/commands/startup.py +946 -0
- alysis_code/cli_impl/commands/tools.py +335 -0
- alysis_code/cli_impl/commands/update.py +364 -0
- alysis_code/cli_impl/commands/welcome.py +972 -0
- alysis_code/cli_impl/config_menu.py +3882 -0
- alysis_code/cli_impl/forge.py +4509 -0
- alysis_code/cli_impl/forge_recovery.py +485 -0
- alysis_code/cli_impl/setup_wizard.py +2409 -0
- alysis_code/cli_impl/tui/__init__.py +58 -0
- alysis_code/cli_impl/tui/app.py +4551 -0
- alysis_code/cli_impl/tui/config.py +32 -0
- alysis_code/cli_impl/tui/config_flow.py +2754 -0
- alysis_code/cli_impl/tui/config_overlay.py +566 -0
- alysis_code/cli_impl/tui/content.py +78 -0
- alysis_code/cli_impl/tui/footer.py +218 -0
- alysis_code/cli_impl/tui/forge_status.py +136 -0
- alysis_code/cli_impl/tui/markdown.py +244 -0
- alysis_code/cli_impl/tui/owl.py +109 -0
- alysis_code/cli_impl/tui/plan_meta.py +477 -0
- alysis_code/cli_impl/tui/setup_app.py +519 -0
- alysis_code/cli_impl/tui/setup_flow.py +1622 -0
- alysis_code/cli_impl/tui/state.py +101 -0
- alysis_code/cli_impl/tui/subagent_identity.py +66 -0
- alysis_code/cli_impl/tui/subagent_panel.py +186 -0
- alysis_code/cli_impl/tui/surface.py +796 -0
- alysis_code/cli_impl/tui/transcript.py +514 -0
- alysis_code/cli_impl/tui/update_prompt.py +79 -0
- alysis_code/cli_impl/tui/workspace_guard.py +384 -0
- alysis_code/clipboard.py +172 -0
- alysis_code/code_review.py +1211 -0
- alysis_code/compaction/__init__.py +28 -0
- alysis_code/compaction/conversation_compactor.py +2932 -0
- alysis_code/compaction/importance.py +177 -0
- alysis_code/compaction/settings.py +297 -0
- alysis_code/compaction/tool_output_offload.py +447 -0
- alysis_code/config.py +3509 -0
- alysis_code/conflict_auto_resolver.py +895 -0
- alysis_code/context/__init__.py +1 -0
- alysis_code/context/tool_schema_budgeter.py +220 -0
- alysis_code/crash_diagnostics.py +282 -0
- alysis_code/custom_tools/README.md +34 -0
- alysis_code/custom_tools/__init__.py +43 -0
- alysis_code/custom_tools/discovery.py +903 -0
- alysis_code/custom_tools/runtime.py +1516 -0
- alysis_code/custom_tools/session.py +227 -0
- alysis_code/custom_tools/trust.py +232 -0
- alysis_code/diff_paths.py +113 -0
- alysis_code/direction_change.py +293 -0
- alysis_code/dispatch_timing.py +306 -0
- alysis_code/durable_service_manager.py +1236 -0
- alysis_code/edit_discipline.py +659 -0
- alysis_code/error_text.py +73 -0
- alysis_code/execution_budget.py +411 -0
- alysis_code/execution_context.py +915 -0
- alysis_code/execution_deadline.py +1065 -0
- alysis_code/execution_shared.py +1904 -0
- alysis_code/extensions/README.md +30 -0
- alysis_code/extensions/__init__.py +93 -0
- alysis_code/extensions/activation.py +138 -0
- alysis_code/extensions/install.py +1436 -0
- alysis_code/extensions/manifest.py +487 -0
- alysis_code/extensions/models.py +74 -0
- alysis_code/extensions/paths.py +56 -0
- alysis_code/extensions/registry.json +4 -0
- alysis_code/extensions/registry.py +52 -0
- alysis_code/extensions/state.py +83 -0
- alysis_code/extensions/workspace_trust.py +101 -0
- alysis_code/failed_task_evidence.py +369 -0
- alysis_code/failure_category.py +315 -0
- alysis_code/feedback_report.py +1647 -0
- alysis_code/file_classification.py +485 -0
- alysis_code/forge.py +2064 -0
- alysis_code/forge_completion.py +362 -0
- alysis_code/forge_events.py +475 -0
- alysis_code/frontmatter_utils.py +95 -0
- alysis_code/git_evidence.py +1181 -0
- alysis_code/git_ops.py +560 -0
- alysis_code/git_safe.py +62 -0
- alysis_code/git_worktrees.py +190 -0
- alysis_code/hooks/README.md +33 -0
- alysis_code/hooks/__init__.py +67 -0
- alysis_code/hooks/audit.py +171 -0
- alysis_code/hooks/config.py +225 -0
- alysis_code/hooks/dispatcher.py +1110 -0
- alysis_code/hooks/models.py +447 -0
- alysis_code/hooks/trust.py +202 -0
- alysis_code/host_actions.py +543 -0
- alysis_code/host_browser.py +103 -0
- alysis_code/ide/__init__.py +5 -0
- alysis_code/ide/activity_events.py +399 -0
- alysis_code/ide/approvals.py +337 -0
- alysis_code/ide/artifacts.py +153 -0
- alysis_code/ide/browser_egress_proxy.py +1076 -0
- alysis_code/ide/cdp_websocket_transport.py +1192 -0
- alysis_code/ide/change_ledger.py +1721 -0
- alysis_code/ide/context_blocks.py +979 -0
- alysis_code/ide/event_stream.py +531 -0
- alysis_code/ide/forge_protocol.py +3112 -0
- alysis_code/ide/forge_request_ledger.py +737 -0
- alysis_code/ide/health.py +965 -0
- alysis_code/ide/managed_browser.py +2251 -0
- alysis_code/ide/management_protocol.py +3414 -0
- alysis_code/ide/mcp_oauth_coordinator.py +744 -0
- alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
- alysis_code/ide/prompt_queue.py +1070 -0
- alysis_code/ide/protocol.py +191 -0
- alysis_code/ide/resumable_swarm.py +1543 -0
- alysis_code/ide/session_search.py +295 -0
- alysis_code/ide/stdio_bridge.py +9935 -0
- alysis_code/ide/structured_state.py +1579 -0
- alysis_code/ide/swarm_protocol.py +816 -0
- alysis_code/integration_gate.py +506 -0
- alysis_code/interactive_input_guard.py +39 -0
- alysis_code/interactive_plan_mode.py +26 -0
- alysis_code/internal_artifacts.py +179 -0
- alysis_code/knowledge_base.py +1409 -0
- alysis_code/knowledge_capture.py +1190 -0
- alysis_code/knowledge_librarian.py +605 -0
- alysis_code/language_policy.py +34 -0
- alysis_code/litellm_static_provider.py +535 -0
- alysis_code/llm/__init__.py +1 -0
- alysis_code/llm/anthropic_messages.py +2288 -0
- alysis_code/llm/base.py +71 -0
- alysis_code/llm/cache_capabilities.py +985 -0
- alysis_code/llm/cache_control_blocks.py +244 -0
- alysis_code/llm/cache_policy.py +388 -0
- alysis_code/llm/factory.py +373 -0
- alysis_code/llm/gemini_generate_content.py +2652 -0
- alysis_code/llm/gemini_interactions.py +739 -0
- alysis_code/llm/metadata.py +450 -0
- alysis_code/llm/openai_compat.py +2947 -0
- alysis_code/llm/openai_responses.py +2604 -0
- alysis_code/llm/protocols.py +609 -0
- alysis_code/llm/provider_limits.py +525 -0
- alysis_code/llm/request_plan.py +389 -0
- alysis_code/llm/request_shape.py +238 -0
- alysis_code/llm/streaming.py +108 -0
- alysis_code/llm/temperature_compat.py +78 -0
- alysis_code/llm/types.py +195 -0
- alysis_code/llm/usage_normalization.py +222 -0
- alysis_code/llm_error_display.py +315 -0
- alysis_code/logging_redaction.py +326 -0
- alysis_code/managed_host_deadline.py +196 -0
- alysis_code/mcp/README.md +33 -0
- alysis_code/mcp/__init__.py +24 -0
- alysis_code/mcp/client.py +1137 -0
- alysis_code/mcp/config.py +597 -0
- alysis_code/mcp/errors.py +113 -0
- alysis_code/mcp/forge_scope.py +154 -0
- alysis_code/mcp/jsonrpc.py +214 -0
- alysis_code/mcp/manager.py +2308 -0
- alysis_code/mcp/models.py +666 -0
- alysis_code/mcp/oauth.py +972 -0
- alysis_code/mcp/oauth_runtime.py +310 -0
- alysis_code/mcp/oauth_store.py +276 -0
- alysis_code/mcp/prompts.py +329 -0
- alysis_code/mcp/resources.py +295 -0
- alysis_code/mcp/roots.py +106 -0
- alysis_code/mcp/server_requests.py +75 -0
- alysis_code/mcp/token_store.py +859 -0
- alysis_code/mcp/transport_http.py +1338 -0
- alysis_code/mcp/transport_stdio.py +1267 -0
- alysis_code/mcp/untrusted_content.py +119 -0
- alysis_code/merge_conflict_reviewer.py +729 -0
- alysis_code/model_catalog/__init__.py +1 -0
- alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
- alysis_code/model_metadata_policy.py +223 -0
- alysis_code/model_metadata_utils.py +103 -0
- alysis_code/model_registry.py +1420 -0
- alysis_code/model_router.py +147 -0
- alysis_code/permission_policy.py +1016 -0
- alysis_code/personas.py +451 -0
- alysis_code/pipeline_facts.py +233 -0
- alysis_code/plan_assistant.py +4763 -0
- alysis_code/plan_mode.py +393 -0
- alysis_code/plan_reconciliation.py +1228 -0
- alysis_code/plan_repair.py +652 -0
- alysis_code/plan_validation.py +1099 -0
- alysis_code/planning_constraints.py +904 -0
- alysis_code/policy.py +95 -0
- alysis_code/preview_server.py +457 -0
- alysis_code/process_reaping.py +566 -0
- alysis_code/profile_presets.py +1834 -0
- alysis_code/profiles.py +666 -0
- alysis_code/provider_auth/__init__.py +29 -0
- alysis_code/provider_auth/base.py +99 -0
- alysis_code/provider_auth/openai_codex.py +951 -0
- alysis_code/provider_auth/registry.py +76 -0
- alysis_code/provider_auth/store.py +125 -0
- alysis_code/provider_diagnostics.py +1209 -0
- alysis_code/provider_model_catalog.py +685 -0
- alysis_code/provider_telemetry.py +1699 -0
- alysis_code/provider_url.py +75 -0
- alysis_code/reasoning_contracts.py +911 -0
- alysis_code/remote_sync.py +350 -0
- alysis_code/replanning.py +1195 -0
- alysis_code/repo_scan.py +1152 -0
- alysis_code/request_estimation.py +296 -0
- alysis_code/review_gate.py +617 -0
- alysis_code/run_lock.py +1141 -0
- alysis_code/run_outcome.py +58 -0
- alysis_code/run_provenance.py +774 -0
- alysis_code/run_state.py +445 -0
- alysis_code/runtime_artifacts.py +116 -0
- alysis_code/runtime_context_features.py +78 -0
- alysis_code/runtime_kind.py +52 -0
- alysis_code/safety/__init__.py +11 -0
- alysis_code/safety/mcp_sanitize.py +29 -0
- alysis_code/safety/safe_http.py +297 -0
- alysis_code/safety/subagent_report.py +184 -0
- alysis_code/sandbox_doctor.py +682 -0
- alysis_code/sandbox_runner.py +1025 -0
- alysis_code/sandbox_settings.py +423 -0
- alysis_code/serialized_paths.py +355 -0
- alysis_code/server/__init__.py +3 -0
- alysis_code/server/app.py +367 -0
- alysis_code/server/auth.py +34 -0
- alysis_code/server/job_config.py +30 -0
- alysis_code/server/settings.py +215 -0
- alysis_code/server/store.py +193 -0
- alysis_code/server/worker_runner.py +657 -0
- alysis_code/service_persistence.py +355 -0
- alysis_code/session_artifacts.py +108 -0
- alysis_code/session_metrics.py +331 -0
- alysis_code/session_store.py +624 -0
- alysis_code/skills/README.md +34 -0
- alysis_code/skills/__init__.py +104 -0
- alysis_code/skills/conventions.py +84 -0
- alysis_code/skills/discovery.py +176 -0
- alysis_code/skills/eval_models.py +232 -0
- alysis_code/skills/eval_runner.py +372 -0
- alysis_code/skills/evals.py +1344 -0
- alysis_code/skills/install.py +293 -0
- alysis_code/skills/loader.py +118 -0
- alysis_code/skills/matching.py +103 -0
- alysis_code/skills/models.py +71 -0
- alysis_code/skills/paths.py +56 -0
- alysis_code/skills/prompting.py +500 -0
- alysis_code/skills/scaffold.py +142 -0
- alysis_code/skills/state.py +441 -0
- alysis_code/skills/transactions.py +125 -0
- alysis_code/skills/validation.py +304 -0
- alysis_code/step_budget.py +238 -0
- alysis_code/subagent_labels.py +49 -0
- alysis_code/subagents.py +1072 -0
- alysis_code/surface/__init__.py +80 -0
- alysis_code/surface/base.py +305 -0
- alysis_code/surface/console.py +387 -0
- alysis_code/surface/events.py +372 -0
- alysis_code/surface/hidden_surface.py +529 -0
- alysis_code/surface/noop_surface.py +219 -0
- alysis_code/surface/rich_surface.py +1555 -0
- alysis_code/surface/styles.py +67 -0
- alysis_code/surface/theme.py +455 -0
- alysis_code/surface/types.py +100 -0
- alysis_code/swarm_backend.py +926 -0
- alysis_code/swarm_orchestrator.py +4020 -0
- alysis_code/swarm_scheduler.py +441 -0
- alysis_code/swarm_trace.py +429 -0
- alysis_code/swarm_worker.py +2119 -0
- alysis_code/swarm_write_guard.py +348 -0
- alysis_code/task_dependencies.py +170 -0
- alysis_code/task_readiness.py +992 -0
- alysis_code/task_scope.py +2148 -0
- alysis_code/terminal_manager.py +762 -0
- alysis_code/terminal_ownership.py +460 -0
- alysis_code/text_normalization.py +30 -0
- alysis_code/token_budget.py +97 -0
- alysis_code/tools/README.md +34 -0
- alysis_code/tools/__init__.py +1 -0
- alysis_code/tools/artifacts.py +127 -0
- alysis_code/tools/availability.py +188 -0
- alysis_code/tools/fs.py +1456 -0
- alysis_code/tools/git.py +461 -0
- alysis_code/tools/history.py +229 -0
- alysis_code/tools/http_timeout.py +78 -0
- alysis_code/tools/image_generation.py +552 -0
- alysis_code/tools/registry.py +2936 -0
- alysis_code/tools/repo_map.py +476 -0
- alysis_code/tools/search.py +563 -0
- alysis_code/tools/shell.py +135 -0
- alysis_code/tools/symbols.py +1350 -0
- alysis_code/tools/test_discovery.py +643 -0
- alysis_code/tools/web.py +482 -0
- alysis_code/tools/web_search.py +2012 -0
- alysis_code/tools/web_search_dashscope.py +557 -0
- alysis_code/tools/web_search_ddgs.py +221 -0
- alysis_code/tools/web_search_provider_adapters.py +1429 -0
- alysis_code/tools/web_search_tavily.py +194 -0
- alysis_code/updates.py +933 -0
- alysis_code/usage_tracker.py +1990 -0
- alysis_code/verification_command_analysis.py +1004 -0
- alysis_code/verification_contract.py +574 -0
- alysis_code/verification_failure_summary.py +273 -0
- alysis_code/verification_repair.py +385 -0
- alysis_code/verify_gate.py +3129 -0
- alysis_code/web_research.py +1872 -0
- alysis_code/web_search_adapters.py +66 -0
- alysis_code/web_search_policy.py +27 -0
- alysis_code/workspace_binding.py +389 -0
- alysis_code/workspace_binding_ui.py +408 -0
- alysis_code/workspace_context.py +273 -0
- alysis_code/workspace_isolation.py +138 -0
- alysis_code/workspace_provisioning.py +455 -0
- alysis_code-0.13.0.dist-info/METADATA +507 -0
- alysis_code-0.13.0.dist-info/RECORD +458 -0
- alysis_code-0.13.0.dist-info/WHEEL +4 -0
- alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
- alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
- alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import queue
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
from collections import defaultdict
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SearchError(RuntimeError):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_RG_NO_CONFIG_SUPPORTED: bool | None = None
|
|
21
|
+
_SEARCH_RG_MAX_MATCHES_PER_FILE = 8
|
|
22
|
+
_SEARCH_RG_MAX_MATCH_TEXT_CHARS = 240
|
|
23
|
+
_SEARCH_RG_MAX_OUTPUT_CHARS = 12_000
|
|
24
|
+
_SEARCH_RG_MAX_CONTEXT_LINES = 5
|
|
25
|
+
_SEARCH_RG_MAX_RESULTS = 500
|
|
26
|
+
_RG_PROBE_TIMEOUT_S = 2.0
|
|
27
|
+
_SEARCH_RG_TIMEOUT_S = 15.0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _rg_available() -> bool:
|
|
31
|
+
try:
|
|
32
|
+
cp = subprocess.run(
|
|
33
|
+
["rg", "--version"],
|
|
34
|
+
check=False,
|
|
35
|
+
capture_output=True,
|
|
36
|
+
text=True,
|
|
37
|
+
timeout=_RG_PROBE_TIMEOUT_S,
|
|
38
|
+
)
|
|
39
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
40
|
+
return False
|
|
41
|
+
return cp.returncode == 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _rg_supports_no_config() -> bool:
|
|
45
|
+
global _RG_NO_CONFIG_SUPPORTED
|
|
46
|
+
if _RG_NO_CONFIG_SUPPORTED is not None:
|
|
47
|
+
return _RG_NO_CONFIG_SUPPORTED
|
|
48
|
+
try:
|
|
49
|
+
cp = subprocess.run(
|
|
50
|
+
["rg", "--no-config", "--version"],
|
|
51
|
+
check=False,
|
|
52
|
+
capture_output=True,
|
|
53
|
+
text=True,
|
|
54
|
+
timeout=_RG_PROBE_TIMEOUT_S,
|
|
55
|
+
)
|
|
56
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
57
|
+
_RG_NO_CONFIG_SUPPORTED = False
|
|
58
|
+
return False
|
|
59
|
+
_RG_NO_CONFIG_SUPPORTED = cp.returncode == 0
|
|
60
|
+
return _RG_NO_CONFIG_SUPPORTED
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _clip_match_text(
|
|
64
|
+
text: str, *, max_chars: int = _SEARCH_RG_MAX_MATCH_TEXT_CHARS
|
|
65
|
+
) -> tuple[str, bool]:
|
|
66
|
+
normalized = text.rstrip("\n")
|
|
67
|
+
if len(normalized) <= max_chars:
|
|
68
|
+
return normalized, False
|
|
69
|
+
return normalized[:max_chars].rstrip() + "...(truncated)", True
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _rel_path(root: Path, path: Path) -> str:
|
|
73
|
+
try:
|
|
74
|
+
rel = path.resolve().relative_to(root.resolve())
|
|
75
|
+
except ValueError:
|
|
76
|
+
return os.fspath(path).replace("\\", "/")
|
|
77
|
+
return os.fspath(rel).replace("\\", "/")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _clean_globs(globs: list[str] | None) -> list[str] | None:
|
|
81
|
+
cleaned = [str(pattern) for pattern in (globs or []) if str(pattern).strip()]
|
|
82
|
+
return cleaned or None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _matches_globs(rel_path: str, globs: list[str] | None) -> bool:
|
|
86
|
+
if not globs:
|
|
87
|
+
return True
|
|
88
|
+
return any(fnmatch.fnmatchcase(rel_path, pattern) for pattern in globs)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _is_hidden_relpath(rel_path: str) -> bool:
|
|
92
|
+
return any(part.startswith(".") and part not in {"", ".", ".."} for part in rel_path.split("/"))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _iter_candidate_files(
|
|
96
|
+
*,
|
|
97
|
+
root: Path,
|
|
98
|
+
base: Path,
|
|
99
|
+
globs: list[str] | None,
|
|
100
|
+
include_hidden: bool,
|
|
101
|
+
):
|
|
102
|
+
if base.is_file():
|
|
103
|
+
# ripgrep does not apply --glob filtering to an explicitly targeted file path
|
|
104
|
+
yield base
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
for current_root, dirnames, filenames in os.walk(base):
|
|
108
|
+
if not include_hidden:
|
|
109
|
+
dirnames[:] = sorted(dirname for dirname in dirnames if not dirname.startswith("."))
|
|
110
|
+
else:
|
|
111
|
+
dirnames[:] = sorted(dirnames)
|
|
112
|
+
for filename in sorted(filenames):
|
|
113
|
+
path = Path(current_root) / filename
|
|
114
|
+
if not path.is_file():
|
|
115
|
+
continue
|
|
116
|
+
rel = _rel_path(root, path)
|
|
117
|
+
if not include_hidden and _is_hidden_relpath(rel):
|
|
118
|
+
continue
|
|
119
|
+
base_rel = _rel_path(base, path)
|
|
120
|
+
if _matches_globs(base_rel, globs):
|
|
121
|
+
yield path
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _context_entries_from_lines(
|
|
125
|
+
lines: list[str],
|
|
126
|
+
*,
|
|
127
|
+
line_number: int,
|
|
128
|
+
before_context: int,
|
|
129
|
+
after_context: int,
|
|
130
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], bool]:
|
|
131
|
+
before: list[dict[str, Any]] = []
|
|
132
|
+
after: list[dict[str, Any]] = []
|
|
133
|
+
text_truncated = False
|
|
134
|
+
line_index = min(max(0, line_number - 1), len(lines))
|
|
135
|
+
|
|
136
|
+
before_start = max(0, line_index - before_context)
|
|
137
|
+
for index in range(before_start, line_index):
|
|
138
|
+
clipped_text, clipped = _clip_match_text(lines[index])
|
|
139
|
+
before.append({"line": index + 1, "text": clipped_text})
|
|
140
|
+
text_truncated = text_truncated or clipped
|
|
141
|
+
|
|
142
|
+
after_end = min(len(lines), line_index + 1 + after_context)
|
|
143
|
+
for index in range(line_index + 1, after_end):
|
|
144
|
+
clipped_text, clipped = _clip_match_text(lines[index])
|
|
145
|
+
after.append({"line": index + 1, "text": clipped_text})
|
|
146
|
+
text_truncated = text_truncated or clipped
|
|
147
|
+
|
|
148
|
+
return before, after, text_truncated
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _safe_bounded_int(value: Any, *, default: int, minimum: int, maximum: int) -> int:
|
|
152
|
+
try:
|
|
153
|
+
parsed = int(value)
|
|
154
|
+
except (TypeError, ValueError):
|
|
155
|
+
parsed = default
|
|
156
|
+
return max(minimum, min(parsed, maximum))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _read_context_lines(
|
|
160
|
+
*,
|
|
161
|
+
root: Path,
|
|
162
|
+
path: Path,
|
|
163
|
+
cache: dict[str, list[str] | None],
|
|
164
|
+
) -> list[str] | None:
|
|
165
|
+
try:
|
|
166
|
+
resolved = path.resolve()
|
|
167
|
+
resolved.relative_to(root)
|
|
168
|
+
except (OSError, ValueError):
|
|
169
|
+
return None
|
|
170
|
+
key = os.fspath(resolved)
|
|
171
|
+
if key in cache:
|
|
172
|
+
return cache[key]
|
|
173
|
+
try:
|
|
174
|
+
if resolved.stat().st_size > 1024 * 1024:
|
|
175
|
+
cache[key] = None
|
|
176
|
+
return None
|
|
177
|
+
cache[key] = resolved.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
178
|
+
except OSError:
|
|
179
|
+
cache[key] = None
|
|
180
|
+
return cache[key]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _compile_python_matcher(*, pattern: str, literal: bool, case_sensitive: bool):
|
|
184
|
+
if literal:
|
|
185
|
+
needle = pattern if case_sensitive else pattern.casefold()
|
|
186
|
+
|
|
187
|
+
def _literal_matches(line: str) -> bool:
|
|
188
|
+
haystack = line if case_sensitive else line.casefold()
|
|
189
|
+
return needle in haystack
|
|
190
|
+
|
|
191
|
+
return _literal_matches
|
|
192
|
+
|
|
193
|
+
flags = 0 if case_sensitive else re.IGNORECASE
|
|
194
|
+
try:
|
|
195
|
+
rx = re.compile(pattern, flags=flags)
|
|
196
|
+
except re.error as e:
|
|
197
|
+
raise SearchError(f"Invalid regex: {e}") from e
|
|
198
|
+
|
|
199
|
+
return rx.search
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _rg_output_path_to_context_path(*, raw_path: str, root: Path) -> Path:
|
|
203
|
+
path = Path(raw_path)
|
|
204
|
+
if path.is_absolute():
|
|
205
|
+
return path
|
|
206
|
+
return (root / path).resolve()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _rg_rel_path(*, raw_path: str, root: Path) -> str:
|
|
210
|
+
path = Path(raw_path)
|
|
211
|
+
target = path if path.is_absolute() else root / path
|
|
212
|
+
try:
|
|
213
|
+
return os.fspath(target.resolve().relative_to(root)).replace("\\", "/")
|
|
214
|
+
except (OSError, ValueError):
|
|
215
|
+
return raw_path.replace("\\", "/")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _queue_text_stream_lines(
|
|
219
|
+
stream: Any,
|
|
220
|
+
output_queue: queue.Queue[str | object],
|
|
221
|
+
done_sentinel: object,
|
|
222
|
+
) -> None:
|
|
223
|
+
try:
|
|
224
|
+
for line in iter(stream.readline, ""):
|
|
225
|
+
output_queue.put(line)
|
|
226
|
+
finally:
|
|
227
|
+
try:
|
|
228
|
+
stream.close()
|
|
229
|
+
except Exception:
|
|
230
|
+
pass
|
|
231
|
+
output_queue.put(done_sentinel)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _collect_text_stream(stream: Any, collector: list[str]) -> None:
|
|
235
|
+
try:
|
|
236
|
+
for line in iter(stream.readline, ""):
|
|
237
|
+
collector.append(line)
|
|
238
|
+
finally:
|
|
239
|
+
try:
|
|
240
|
+
stream.close()
|
|
241
|
+
except Exception:
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _stop_process(process: Any) -> int:
|
|
246
|
+
try:
|
|
247
|
+
process.terminate()
|
|
248
|
+
except OSError:
|
|
249
|
+
pass
|
|
250
|
+
try:
|
|
251
|
+
return int(process.wait(timeout=0.5))
|
|
252
|
+
except subprocess.TimeoutExpired:
|
|
253
|
+
try:
|
|
254
|
+
process.kill()
|
|
255
|
+
except OSError:
|
|
256
|
+
pass
|
|
257
|
+
return int(process.wait())
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def search_rg(
|
|
261
|
+
*,
|
|
262
|
+
root: Path,
|
|
263
|
+
pattern: str,
|
|
264
|
+
root_path: str = ".",
|
|
265
|
+
globs: list[str] | None = None,
|
|
266
|
+
max_results: int = 200,
|
|
267
|
+
before_context: int = 0,
|
|
268
|
+
after_context: int = 0,
|
|
269
|
+
literal: bool = False,
|
|
270
|
+
case_sensitive: bool = True,
|
|
271
|
+
include_hidden: bool = False,
|
|
272
|
+
) -> dict[str, Any]:
|
|
273
|
+
root_abs = root.resolve()
|
|
274
|
+
base = (root_abs / root_path).resolve()
|
|
275
|
+
try:
|
|
276
|
+
base.relative_to(root_abs)
|
|
277
|
+
except ValueError as e:
|
|
278
|
+
raise SearchError(f"root_path escapes root: {root_path}") from e
|
|
279
|
+
if pattern == "":
|
|
280
|
+
raise SearchError("pattern must be a non-empty string")
|
|
281
|
+
|
|
282
|
+
safe_max_results = _safe_bounded_int(
|
|
283
|
+
max_results,
|
|
284
|
+
default=200,
|
|
285
|
+
minimum=1,
|
|
286
|
+
maximum=_SEARCH_RG_MAX_RESULTS,
|
|
287
|
+
)
|
|
288
|
+
safe_before_context = _safe_bounded_int(
|
|
289
|
+
before_context,
|
|
290
|
+
default=0,
|
|
291
|
+
minimum=0,
|
|
292
|
+
maximum=_SEARCH_RG_MAX_CONTEXT_LINES,
|
|
293
|
+
)
|
|
294
|
+
safe_after_context = _safe_bounded_int(
|
|
295
|
+
after_context,
|
|
296
|
+
default=0,
|
|
297
|
+
minimum=0,
|
|
298
|
+
maximum=_SEARCH_RG_MAX_CONTEXT_LINES,
|
|
299
|
+
)
|
|
300
|
+
cleaned_globs = _clean_globs(globs)
|
|
301
|
+
matches: list[dict[str, Any]] = []
|
|
302
|
+
per_file_match_counts: dict[str, int] = defaultdict(int)
|
|
303
|
+
context_cache: dict[str, list[str] | None] = {}
|
|
304
|
+
output_chars = 0
|
|
305
|
+
truncated = False
|
|
306
|
+
per_file_truncated = False
|
|
307
|
+
match_text_truncated = False
|
|
308
|
+
context_text_truncated = False
|
|
309
|
+
|
|
310
|
+
def _timeout_message(*, backend: str) -> str:
|
|
311
|
+
return (
|
|
312
|
+
f"{backend} search timed out after {_SEARCH_RG_TIMEOUT_S:g}s; "
|
|
313
|
+
"try a narrower root_path, globs, or pattern"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
def _append_match(
|
|
317
|
+
*,
|
|
318
|
+
path: str,
|
|
319
|
+
line_number: int,
|
|
320
|
+
line_text: str,
|
|
321
|
+
context_lines: list[str] | None = None,
|
|
322
|
+
) -> str:
|
|
323
|
+
nonlocal output_chars, truncated, per_file_truncated, match_text_truncated
|
|
324
|
+
nonlocal context_text_truncated
|
|
325
|
+
if per_file_match_counts[path] >= _SEARCH_RG_MAX_MATCHES_PER_FILE:
|
|
326
|
+
truncated = True
|
|
327
|
+
per_file_truncated = True
|
|
328
|
+
return "per_file_limit"
|
|
329
|
+
clipped_text, clipped = _clip_match_text(line_text)
|
|
330
|
+
entry = {
|
|
331
|
+
"path": path,
|
|
332
|
+
"line": int(line_number),
|
|
333
|
+
"text": clipped_text,
|
|
334
|
+
}
|
|
335
|
+
if clipped:
|
|
336
|
+
entry["text_truncated"] = True
|
|
337
|
+
match_text_truncated = True
|
|
338
|
+
if context_lines is not None and (safe_before_context or safe_after_context):
|
|
339
|
+
before, after, context_clipped = _context_entries_from_lines(
|
|
340
|
+
context_lines,
|
|
341
|
+
line_number=int(line_number),
|
|
342
|
+
before_context=safe_before_context,
|
|
343
|
+
after_context=safe_after_context,
|
|
344
|
+
)
|
|
345
|
+
if before:
|
|
346
|
+
entry["before_context"] = before
|
|
347
|
+
if after:
|
|
348
|
+
entry["after_context"] = after
|
|
349
|
+
context_text_truncated = context_text_truncated or context_clipped
|
|
350
|
+
entry_chars = len(json.dumps(entry, ensure_ascii=True, separators=(",", ":")))
|
|
351
|
+
if len(matches) >= safe_max_results:
|
|
352
|
+
truncated = True
|
|
353
|
+
return "max_results"
|
|
354
|
+
if matches and (output_chars + entry_chars) > _SEARCH_RG_MAX_OUTPUT_CHARS:
|
|
355
|
+
truncated = True
|
|
356
|
+
return "output_cap"
|
|
357
|
+
matches.append(entry)
|
|
358
|
+
per_file_match_counts[path] += 1
|
|
359
|
+
output_chars += entry_chars
|
|
360
|
+
return "appended"
|
|
361
|
+
|
|
362
|
+
if _rg_available():
|
|
363
|
+
cmd = ["rg"]
|
|
364
|
+
if _rg_supports_no_config():
|
|
365
|
+
cmd.append("--no-config")
|
|
366
|
+
if literal:
|
|
367
|
+
cmd.append("--fixed-strings")
|
|
368
|
+
if not case_sensitive:
|
|
369
|
+
cmd.append("--ignore-case")
|
|
370
|
+
if include_hidden:
|
|
371
|
+
cmd.append("--hidden")
|
|
372
|
+
cmd.extend(["--glob", "!.git/**"])
|
|
373
|
+
cmd.extend(
|
|
374
|
+
[
|
|
375
|
+
"--json",
|
|
376
|
+
"--line-buffered",
|
|
377
|
+
"-e",
|
|
378
|
+
pattern,
|
|
379
|
+
]
|
|
380
|
+
)
|
|
381
|
+
if cleaned_globs:
|
|
382
|
+
for g in cleaned_globs:
|
|
383
|
+
cmd.extend(["--glob", g])
|
|
384
|
+
cmd.append(os.fspath(base))
|
|
385
|
+
try:
|
|
386
|
+
proc = subprocess.Popen(
|
|
387
|
+
cmd,
|
|
388
|
+
stdout=subprocess.PIPE,
|
|
389
|
+
stderr=subprocess.PIPE,
|
|
390
|
+
text=True,
|
|
391
|
+
bufsize=1,
|
|
392
|
+
)
|
|
393
|
+
except OSError as e:
|
|
394
|
+
raise SearchError("Failed to execute ripgrep") from e
|
|
395
|
+
|
|
396
|
+
if proc.stdout is None or proc.stderr is None:
|
|
397
|
+
raise SearchError("Failed to capture ripgrep output")
|
|
398
|
+
|
|
399
|
+
stdout_queue: queue.Queue[str | object] = queue.Queue()
|
|
400
|
+
stdout_done = object()
|
|
401
|
+
stderr_lines: list[str] = []
|
|
402
|
+
stdout_thread = threading.Thread(
|
|
403
|
+
target=_queue_text_stream_lines,
|
|
404
|
+
args=(proc.stdout, stdout_queue, stdout_done),
|
|
405
|
+
daemon=True,
|
|
406
|
+
)
|
|
407
|
+
stderr_thread = threading.Thread(
|
|
408
|
+
target=_collect_text_stream,
|
|
409
|
+
args=(proc.stderr, stderr_lines),
|
|
410
|
+
daemon=True,
|
|
411
|
+
)
|
|
412
|
+
stdout_thread.start()
|
|
413
|
+
stderr_thread.start()
|
|
414
|
+
deadline = time.monotonic() + _SEARCH_RG_TIMEOUT_S
|
|
415
|
+
stdout_finished = False
|
|
416
|
+
terminated_early = False
|
|
417
|
+
|
|
418
|
+
while not stdout_finished:
|
|
419
|
+
remaining = deadline - time.monotonic()
|
|
420
|
+
if remaining <= 0:
|
|
421
|
+
_stop_process(proc)
|
|
422
|
+
stdout_thread.join(timeout=0.5)
|
|
423
|
+
stderr_thread.join(timeout=0.5)
|
|
424
|
+
raise SearchError(_timeout_message(backend="ripgrep"))
|
|
425
|
+
try:
|
|
426
|
+
line = stdout_queue.get(timeout=min(0.1, remaining))
|
|
427
|
+
except queue.Empty:
|
|
428
|
+
if proc.poll() is not None and not stdout_thread.is_alive():
|
|
429
|
+
stdout_finished = True
|
|
430
|
+
continue
|
|
431
|
+
if line is stdout_done:
|
|
432
|
+
stdout_finished = True
|
|
433
|
+
continue
|
|
434
|
+
try:
|
|
435
|
+
obj = json.loads(line)
|
|
436
|
+
except json.JSONDecodeError:
|
|
437
|
+
continue
|
|
438
|
+
if obj.get("type") != "match":
|
|
439
|
+
continue
|
|
440
|
+
data = obj.get("data") or {}
|
|
441
|
+
path = ((data.get("path") or {}).get("text")) or ""
|
|
442
|
+
line_number = data.get("line_number")
|
|
443
|
+
lines = ((data.get("lines") or {}).get("text")) or ""
|
|
444
|
+
if not path or not line_number:
|
|
445
|
+
continue
|
|
446
|
+
rel = _rg_rel_path(raw_path=path, root=root_abs)
|
|
447
|
+
context_lines = None
|
|
448
|
+
if safe_before_context or safe_after_context:
|
|
449
|
+
context_lines = _read_context_lines(
|
|
450
|
+
root=root_abs,
|
|
451
|
+
path=_rg_output_path_to_context_path(raw_path=path, root=root_abs),
|
|
452
|
+
cache=context_cache,
|
|
453
|
+
)
|
|
454
|
+
append_status = _append_match(
|
|
455
|
+
path=rel,
|
|
456
|
+
line_number=int(line_number),
|
|
457
|
+
line_text=lines,
|
|
458
|
+
context_lines=context_lines,
|
|
459
|
+
)
|
|
460
|
+
if append_status == "max_results":
|
|
461
|
+
terminated_early = True
|
|
462
|
+
break
|
|
463
|
+
if append_status == "output_cap":
|
|
464
|
+
terminated_early = True
|
|
465
|
+
break
|
|
466
|
+
|
|
467
|
+
returncode = _stop_process(proc) if terminated_early else int(proc.wait())
|
|
468
|
+
stdout_thread.join(timeout=0.5)
|
|
469
|
+
stderr_thread.join(timeout=0.5)
|
|
470
|
+
stderr_text = "".join(stderr_lines).strip()
|
|
471
|
+
if not terminated_early and returncode not in (0, 1): # 1 means no matches
|
|
472
|
+
raise SearchError(stderr_text or f"rg failed with exit code {returncode}")
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
"pattern": pattern,
|
|
476
|
+
"matches": matches,
|
|
477
|
+
"backend": "rg",
|
|
478
|
+
"truncated": truncated,
|
|
479
|
+
"per_file_truncated": per_file_truncated,
|
|
480
|
+
"match_text_truncated": match_text_truncated,
|
|
481
|
+
"context_text_truncated": context_text_truncated,
|
|
482
|
+
"returned_matches": len(matches),
|
|
483
|
+
"literal": bool(literal),
|
|
484
|
+
"case_sensitive": bool(case_sensitive),
|
|
485
|
+
"include_hidden": bool(include_hidden),
|
|
486
|
+
"before_context": safe_before_context,
|
|
487
|
+
"after_context": safe_after_context,
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
matcher = _compile_python_matcher(
|
|
491
|
+
pattern=pattern,
|
|
492
|
+
literal=bool(literal),
|
|
493
|
+
case_sensitive=bool(case_sensitive),
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
deadline = time.monotonic() + _SEARCH_RG_TIMEOUT_S
|
|
497
|
+
|
|
498
|
+
def _raise_if_python_timeout() -> None:
|
|
499
|
+
if time.monotonic() >= deadline:
|
|
500
|
+
raise SearchError(_timeout_message(backend="python fallback"))
|
|
501
|
+
|
|
502
|
+
for p in _iter_candidate_files(
|
|
503
|
+
root=root_abs,
|
|
504
|
+
base=base,
|
|
505
|
+
globs=cleaned_globs,
|
|
506
|
+
include_hidden=bool(include_hidden),
|
|
507
|
+
):
|
|
508
|
+
_raise_if_python_timeout()
|
|
509
|
+
if len(matches) >= safe_max_results:
|
|
510
|
+
truncated = True
|
|
511
|
+
break
|
|
512
|
+
# Skip large files.
|
|
513
|
+
try:
|
|
514
|
+
if p.stat().st_size > 512 * 1024:
|
|
515
|
+
continue
|
|
516
|
+
except OSError:
|
|
517
|
+
continue
|
|
518
|
+
try:
|
|
519
|
+
text = p.read_text(encoding="utf-8", errors="ignore")
|
|
520
|
+
except OSError:
|
|
521
|
+
continue
|
|
522
|
+
stop_search = False
|
|
523
|
+
source_lines = text.splitlines()
|
|
524
|
+
for i, line in enumerate(source_lines, start=1):
|
|
525
|
+
_raise_if_python_timeout()
|
|
526
|
+
if len(matches) >= safe_max_results:
|
|
527
|
+
truncated = True
|
|
528
|
+
stop_search = True
|
|
529
|
+
break
|
|
530
|
+
if matcher(line):
|
|
531
|
+
rel = _rel_path(root_abs, p)
|
|
532
|
+
append_status = _append_match(
|
|
533
|
+
path=rel,
|
|
534
|
+
line_number=i,
|
|
535
|
+
line_text=line,
|
|
536
|
+
context_lines=source_lines,
|
|
537
|
+
)
|
|
538
|
+
if append_status == "max_results":
|
|
539
|
+
stop_search = True
|
|
540
|
+
break
|
|
541
|
+
if append_status == "output_cap":
|
|
542
|
+
stop_search = True
|
|
543
|
+
break
|
|
544
|
+
if append_status == "per_file_limit":
|
|
545
|
+
break
|
|
546
|
+
if stop_search:
|
|
547
|
+
break
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
"pattern": pattern,
|
|
551
|
+
"matches": matches,
|
|
552
|
+
"backend": "python",
|
|
553
|
+
"truncated": truncated,
|
|
554
|
+
"per_file_truncated": per_file_truncated,
|
|
555
|
+
"match_text_truncated": match_text_truncated,
|
|
556
|
+
"context_text_truncated": context_text_truncated,
|
|
557
|
+
"returned_matches": len(matches),
|
|
558
|
+
"literal": bool(literal),
|
|
559
|
+
"case_sensitive": bool(case_sensitive),
|
|
560
|
+
"include_hidden": bool(include_hidden),
|
|
561
|
+
"before_context": safe_before_context,
|
|
562
|
+
"after_context": safe_after_context,
|
|
563
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ..pipeline_facts import (
|
|
10
|
+
build_pipeline_status_capture_command,
|
|
11
|
+
command_has_top_level_pipe,
|
|
12
|
+
extract_pipeline_status,
|
|
13
|
+
pipeline_meaningful_stage,
|
|
14
|
+
split_top_level_pipeline,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ShellError(RuntimeError):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_PYTHON_CMD_PATTERN = re.compile(r"(^|(?:&&|\|\||;)\s*)python(?=\s)")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _python3_fallback_cmd(cmd: str) -> str | None:
|
|
26
|
+
match = _PYTHON_CMD_PATTERN.search(cmd)
|
|
27
|
+
if not match:
|
|
28
|
+
return None
|
|
29
|
+
start, end = match.span()
|
|
30
|
+
prefix = match.group(1)
|
|
31
|
+
replacement = f"{prefix}python3"
|
|
32
|
+
return f"{cmd[:start]}{replacement}{cmd[end:]}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _is_python_permission_denied(stderr: str) -> bool:
|
|
36
|
+
lowered = stderr.lower()
|
|
37
|
+
return "python: permission denied" in lowered
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _pipeline_capture_platform_ok() -> bool:
|
|
41
|
+
# PIPESTATUS capture uses a POSIX-shell wrapper; it is unavailable under the
|
|
42
|
+
# Windows cmd.exe shell. (Kept as a separate function so tests can exercise
|
|
43
|
+
# the capture path on any host.)
|
|
44
|
+
return os.name != "nt"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def shell_run(
|
|
48
|
+
*,
|
|
49
|
+
root: Path,
|
|
50
|
+
cmd: str,
|
|
51
|
+
cwd: str | None = None,
|
|
52
|
+
timeout_s: float = 60,
|
|
53
|
+
runner: Any | None = None,
|
|
54
|
+
capture_pipeline_status: bool = False,
|
|
55
|
+
) -> dict[str, Any]:
|
|
56
|
+
base = root.resolve()
|
|
57
|
+
if cwd:
|
|
58
|
+
cwd_path = (base / cwd).resolve()
|
|
59
|
+
try:
|
|
60
|
+
cwd_path.relative_to(base)
|
|
61
|
+
except ValueError as e:
|
|
62
|
+
raise ShellError(f"cwd escapes root: {cwd}") from e
|
|
63
|
+
else:
|
|
64
|
+
cwd_path = base
|
|
65
|
+
|
|
66
|
+
if runner is None:
|
|
67
|
+
raise ShellError("Shell runner is required; implicit host execution is disabled.")
|
|
68
|
+
|
|
69
|
+
# Observe per-stage pipeline exit codes (bash PIPESTATUS) only when it would
|
|
70
|
+
# matter and can be done safely: a top-level pipeline is present, on a POSIX
|
|
71
|
+
# host. The wrapper preserves stdout/stderr/exit-code exactly, so plain
|
|
72
|
+
# commands and non-capturing runs behave identically to before.
|
|
73
|
+
wants_capture = bool(
|
|
74
|
+
capture_pipeline_status
|
|
75
|
+
and _pipeline_capture_platform_ok()
|
|
76
|
+
and pipeline_meaningful_stage(cmd) is not None
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def _run(command_text: str) -> tuple[Any, list[int] | None, str]:
|
|
80
|
+
run_cmd = (
|
|
81
|
+
build_pipeline_status_capture_command(command_text) if wants_capture else command_text
|
|
82
|
+
)
|
|
83
|
+
completed = runner.run(root=base, cwd=cwd_path, cmd=run_cmd, timeout_s=timeout_s)
|
|
84
|
+
if wants_capture:
|
|
85
|
+
status, cleaned_stderr = extract_pipeline_status(completed.stderr or "")
|
|
86
|
+
return completed, status, cleaned_stderr
|
|
87
|
+
return completed, None, (completed.stderr or "")
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
cp, stage_status, stderr_text = _run(cmd)
|
|
91
|
+
except subprocess.TimeoutExpired as e:
|
|
92
|
+
raise ShellError(f"Command timed out after {timeout_s:g}s") from e
|
|
93
|
+
except Exception as e: # noqa: BLE001
|
|
94
|
+
raise ShellError(f"Failed to run command: {e}") from e
|
|
95
|
+
|
|
96
|
+
effective_cmd = cmd
|
|
97
|
+
fallback_cmd = _python3_fallback_cmd(cmd)
|
|
98
|
+
if fallback_cmd and cp.returncode != 0 and _is_python_permission_denied(stderr_text):
|
|
99
|
+
try:
|
|
100
|
+
cp, stage_status, stderr_text = _run(fallback_cmd)
|
|
101
|
+
effective_cmd = fallback_cmd
|
|
102
|
+
except subprocess.TimeoutExpired as e:
|
|
103
|
+
raise ShellError(f"Command timed out after {timeout_s:g}s") from e
|
|
104
|
+
except Exception as e: # noqa: BLE001
|
|
105
|
+
raise ShellError(f"Failed to run command: {e}") from e
|
|
106
|
+
|
|
107
|
+
stdout = cp.stdout or ""
|
|
108
|
+
stderr = stderr_text
|
|
109
|
+
truncated = False
|
|
110
|
+
limit = 20000
|
|
111
|
+
if len(stdout) > limit:
|
|
112
|
+
stdout = stdout[:limit] + "...(truncated)"
|
|
113
|
+
truncated = True
|
|
114
|
+
if len(stderr) > limit:
|
|
115
|
+
stderr = stderr[:limit] + "...(truncated)"
|
|
116
|
+
truncated = True
|
|
117
|
+
|
|
118
|
+
# A plain command (no top-level pipe) has a one-element status list equal to
|
|
119
|
+
# its exit code. A pipeline whose per-stage status could not be observed
|
|
120
|
+
# leaves the list as None so the classifier falls back to a single re-run.
|
|
121
|
+
if stage_status is None and not command_has_top_level_pipe(effective_cmd):
|
|
122
|
+
stage_status = [cp.returncode]
|
|
123
|
+
pipeline_stages = split_top_level_pipeline(effective_cmd) or [effective_cmd]
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
"cmd": cmd,
|
|
127
|
+
"effective_cmd": effective_cmd,
|
|
128
|
+
"cwd": str(cwd_path),
|
|
129
|
+
"exit_code": cp.returncode,
|
|
130
|
+
"stdout": stdout,
|
|
131
|
+
"stderr": stderr,
|
|
132
|
+
"truncated": truncated,
|
|
133
|
+
"pipeline_stages": pipeline_stages,
|
|
134
|
+
"pipeline_stage_status": stage_status,
|
|
135
|
+
}
|