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,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
AUTO_WEB_SEARCH_ADAPTER = "auto"
|
|
4
|
+
OPENAI_RESPONSES_ADAPTER = "openai_responses"
|
|
5
|
+
XAI_RESPONSES_ADAPTER = "xai_responses"
|
|
6
|
+
ANTHROPIC_MESSAGES_ADAPTER = "anthropic_messages"
|
|
7
|
+
GEMINI_GROUNDING_ADAPTER = "gemini_grounding"
|
|
8
|
+
OPENROUTER_WEB_ADAPTER = "openrouter_web"
|
|
9
|
+
DASHSCOPE_CHAT_ADAPTER = "dashscope_chat"
|
|
10
|
+
MOONSHOT_KIMI_ADAPTER = "moonshot_kimi"
|
|
11
|
+
ZHIPU_WEB_SEARCH_ADAPTER = "zhipu_web_search"
|
|
12
|
+
VOLCENGINE_WEB_SEARCH_ADAPTER = "volcengine_web_search"
|
|
13
|
+
PERPLEXITY_SONAR_ADAPTER = "perplexity_sonar"
|
|
14
|
+
GROQ_COMPOUND_ADAPTER = "groq_compound"
|
|
15
|
+
MISTRAL_CONVERSATIONS_ADAPTER = "mistral_conversations"
|
|
16
|
+
MINIMAX_CODING_PLAN_ADAPTER = "minimax_coding_plan"
|
|
17
|
+
COHERE_WEB_SEARCH_ADAPTER = "cohere_web_search"
|
|
18
|
+
TAVILY_ADAPTER = "tavily"
|
|
19
|
+
DDGS_ADAPTER = "ddgs"
|
|
20
|
+
|
|
21
|
+
NATIVE_WEB_SEARCH_ADAPTERS: frozenset[str] = frozenset(
|
|
22
|
+
{
|
|
23
|
+
OPENAI_RESPONSES_ADAPTER,
|
|
24
|
+
XAI_RESPONSES_ADAPTER,
|
|
25
|
+
ANTHROPIC_MESSAGES_ADAPTER,
|
|
26
|
+
GEMINI_GROUNDING_ADAPTER,
|
|
27
|
+
OPENROUTER_WEB_ADAPTER,
|
|
28
|
+
DASHSCOPE_CHAT_ADAPTER,
|
|
29
|
+
MOONSHOT_KIMI_ADAPTER,
|
|
30
|
+
ZHIPU_WEB_SEARCH_ADAPTER,
|
|
31
|
+
VOLCENGINE_WEB_SEARCH_ADAPTER,
|
|
32
|
+
PERPLEXITY_SONAR_ADAPTER,
|
|
33
|
+
GROQ_COMPOUND_ADAPTER,
|
|
34
|
+
MISTRAL_CONVERSATIONS_ADAPTER,
|
|
35
|
+
MINIMAX_CODING_PLAN_ADAPTER,
|
|
36
|
+
COHERE_WEB_SEARCH_ADAPTER,
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
EXTERNAL_WEB_SEARCH_ADAPTERS: frozenset[str] = frozenset({TAVILY_ADAPTER, DDGS_ADAPTER})
|
|
40
|
+
|
|
41
|
+
VALID_WEB_SEARCH_ADAPTERS: frozenset[str] = frozenset(
|
|
42
|
+
{
|
|
43
|
+
AUTO_WEB_SEARCH_ADAPTER,
|
|
44
|
+
*NATIVE_WEB_SEARCH_ADAPTERS,
|
|
45
|
+
*EXTERNAL_WEB_SEARCH_ADAPTERS,
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
WEB_SEARCH_ADAPTER_CHOICES: tuple[str, ...] = tuple(sorted(VALID_WEB_SEARCH_ADAPTERS))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def web_search_adapter_is_native(adapter: str) -> bool:
|
|
52
|
+
return str(adapter or "").strip().lower() in NATIVE_WEB_SEARCH_ADAPTERS
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def web_search_adapter_is_external(adapter: str) -> bool:
|
|
56
|
+
return str(adapter or "").strip().lower() in EXTERNAL_WEB_SEARCH_ADAPTERS
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def normalize_web_search_adapter(raw: object) -> str:
|
|
60
|
+
value = str(raw or "").strip().lower()
|
|
61
|
+
if not value:
|
|
62
|
+
return AUTO_WEB_SEARCH_ADAPTER
|
|
63
|
+
if value in VALID_WEB_SEARCH_ADAPTERS:
|
|
64
|
+
return value
|
|
65
|
+
allowed = ", ".join(WEB_SEARCH_ADAPTER_CHOICES)
|
|
66
|
+
raise ValueError(f"web_search_adapter must be one of: {allowed}")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
WEB_SEARCH_POLICY_OFF = "off"
|
|
4
|
+
WEB_SEARCH_POLICY_AUTO = "auto"
|
|
5
|
+
VALID_WEB_SEARCH_POLICIES: frozenset[str] = frozenset(
|
|
6
|
+
{
|
|
7
|
+
WEB_SEARCH_POLICY_OFF,
|
|
8
|
+
WEB_SEARCH_POLICY_AUTO,
|
|
9
|
+
}
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def normalize_web_search_policy(raw: object) -> str:
|
|
14
|
+
"""Normalize the legacy access switch used by existing configuration files.
|
|
15
|
+
|
|
16
|
+
Search intent is decided by the active model from the registered tool contract.
|
|
17
|
+
The former ``always`` classifier mode is accepted as a compatibility alias for
|
|
18
|
+
model-led ``auto`` behavior.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
value = str(raw or "").strip().lower()
|
|
22
|
+
if not value or value == "always":
|
|
23
|
+
return WEB_SEARCH_POLICY_AUTO
|
|
24
|
+
if value in VALID_WEB_SEARCH_POLICIES:
|
|
25
|
+
return value
|
|
26
|
+
allowed = ", ".join(sorted(VALID_WEB_SEARCH_POLICIES))
|
|
27
|
+
raise ValueError(f"web_search_policy must be one of: {allowed}")
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .repo_scan import _MANIFEST_SPECS, _README_NAMES, scan_workspace
|
|
8
|
+
from .runtime_artifacts import ROOT_RUNTIME_ARTIFACT_DIR_NAMES
|
|
9
|
+
from .workspace_context import WorkspaceContext, WorkspaceContextError, resolve_workspace_context
|
|
10
|
+
|
|
11
|
+
_PROJECT_SIGNAL_DIR_NAMES = frozenset({"src", "tests"})
|
|
12
|
+
_BROAD_PLAIN_DIR_THRESHOLD = 12
|
|
13
|
+
_PROJECT_CONTAINER_DIR_NAMES = frozenset({"code", "dev", "projects", "repos", "src", "work"})
|
|
14
|
+
_MAX_DISCOVERY_CHILDREN = 24
|
|
15
|
+
_MAX_DISCOVERY_RESULTS = 8
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WorkspaceBindingError(RuntimeError):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WorkspaceRiskLevel:
|
|
23
|
+
HEALTHY = "healthy"
|
|
24
|
+
GUARDED = "guarded"
|
|
25
|
+
BLOCKED = "blocked"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class WorkspaceAction:
|
|
29
|
+
CHAT = "chat"
|
|
30
|
+
FORGE = "forge"
|
|
31
|
+
FORGE_PLAN = "forge_plan"
|
|
32
|
+
FORGE_RUN = "forge_run"
|
|
33
|
+
SWARM = "swarm"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class WorkspacePolicy:
|
|
38
|
+
action: str
|
|
39
|
+
allow_guarded_override: bool
|
|
40
|
+
interactive_guarded_resolution: bool
|
|
41
|
+
interactive_allow_use_current: bool
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class WorkspaceBinding:
|
|
46
|
+
requested_path: Path
|
|
47
|
+
resolved_candidate_path: Path
|
|
48
|
+
created_path: bool
|
|
49
|
+
workspace_context: WorkspaceContext
|
|
50
|
+
binding_source: str
|
|
51
|
+
risk_level: str
|
|
52
|
+
broad_workspace_override_used: bool = False
|
|
53
|
+
risk_reasons: tuple[str, ...] = ()
|
|
54
|
+
warnings: tuple[str, ...] = ()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class WorkspaceCandidate:
|
|
59
|
+
path: Path
|
|
60
|
+
score: int
|
|
61
|
+
project_signals: tuple[str, ...]
|
|
62
|
+
source: str
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def summary(self) -> str:
|
|
66
|
+
if self.project_signals:
|
|
67
|
+
return ", ".join(self.project_signals)
|
|
68
|
+
return "existing directory"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def prepare_workspace_path(path: Path, *, create_if_missing: bool = False) -> tuple[Path, bool]:
|
|
72
|
+
requested_path = path.expanduser().resolve(strict=False)
|
|
73
|
+
if requested_path.exists():
|
|
74
|
+
if not requested_path.is_dir():
|
|
75
|
+
raise WorkspaceBindingError(f"Workspace path is not a directory: {requested_path}")
|
|
76
|
+
return requested_path.resolve(), False
|
|
77
|
+
|
|
78
|
+
if not create_if_missing:
|
|
79
|
+
raise WorkspaceBindingError(
|
|
80
|
+
f"Workspace path does not exist: {requested_path}. "
|
|
81
|
+
"Pass create_if_missing=True to bootstrap it."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
requested_path.mkdir(parents=True, exist_ok=False)
|
|
86
|
+
except FileExistsError as e:
|
|
87
|
+
raise WorkspaceBindingError(f"Workspace path is not a directory: {requested_path}") from e
|
|
88
|
+
except OSError as e:
|
|
89
|
+
raise WorkspaceBindingError(f"Unable to create workspace path: {requested_path}") from e
|
|
90
|
+
return requested_path.resolve(), True
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def resolve_workspace_binding(
|
|
94
|
+
path: Path,
|
|
95
|
+
*,
|
|
96
|
+
create_if_missing: bool = False,
|
|
97
|
+
allow_broad_workspace: bool = False,
|
|
98
|
+
source: str = "explicit_path",
|
|
99
|
+
) -> WorkspaceBinding:
|
|
100
|
+
requested_path, created_path = prepare_workspace_path(
|
|
101
|
+
path,
|
|
102
|
+
create_if_missing=create_if_missing,
|
|
103
|
+
)
|
|
104
|
+
try:
|
|
105
|
+
workspace_context = resolve_workspace_context(requested_path)
|
|
106
|
+
except WorkspaceContextError as e:
|
|
107
|
+
raise WorkspaceBindingError(str(e)) from e
|
|
108
|
+
|
|
109
|
+
risk_level, risk_reasons = classify_workspace_risk(workspace_context)
|
|
110
|
+
warnings: list[str] = []
|
|
111
|
+
broad_workspace_override_used = False
|
|
112
|
+
if created_path:
|
|
113
|
+
warnings.append("Workspace directory was created at startup.")
|
|
114
|
+
if risk_level == WorkspaceRiskLevel.GUARDED and allow_broad_workspace:
|
|
115
|
+
broad_workspace_override_used = True
|
|
116
|
+
warnings.append("Guarded workspace accepted because allow_broad_workspace=True.")
|
|
117
|
+
return WorkspaceBinding(
|
|
118
|
+
requested_path=requested_path,
|
|
119
|
+
resolved_candidate_path=workspace_context.input_path,
|
|
120
|
+
created_path=created_path,
|
|
121
|
+
workspace_context=workspace_context,
|
|
122
|
+
binding_source=(source.strip() or "explicit_path"),
|
|
123
|
+
risk_level=risk_level,
|
|
124
|
+
broad_workspace_override_used=broad_workspace_override_used,
|
|
125
|
+
risk_reasons=risk_reasons,
|
|
126
|
+
warnings=tuple(warnings),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def workspace_policy(action: str) -> WorkspacePolicy:
|
|
131
|
+
normalized = str(action or "").strip().lower()
|
|
132
|
+
if normalized == WorkspaceAction.FORGE:
|
|
133
|
+
return WorkspacePolicy(
|
|
134
|
+
action=WorkspaceAction.FORGE,
|
|
135
|
+
allow_guarded_override=False,
|
|
136
|
+
interactive_guarded_resolution=False,
|
|
137
|
+
interactive_allow_use_current=False,
|
|
138
|
+
)
|
|
139
|
+
if normalized == WorkspaceAction.FORGE_PLAN:
|
|
140
|
+
return WorkspacePolicy(
|
|
141
|
+
action=WorkspaceAction.FORGE_PLAN,
|
|
142
|
+
allow_guarded_override=True,
|
|
143
|
+
interactive_guarded_resolution=True,
|
|
144
|
+
interactive_allow_use_current=False,
|
|
145
|
+
)
|
|
146
|
+
if normalized == WorkspaceAction.FORGE_RUN:
|
|
147
|
+
# Same strength as swarm: both execute plan tasks and mutate the workspace.
|
|
148
|
+
# It gets its own action so the guard message names the command the user ran.
|
|
149
|
+
return WorkspacePolicy(
|
|
150
|
+
action=WorkspaceAction.FORGE_RUN,
|
|
151
|
+
allow_guarded_override=True,
|
|
152
|
+
interactive_guarded_resolution=False,
|
|
153
|
+
interactive_allow_use_current=False,
|
|
154
|
+
)
|
|
155
|
+
if normalized == WorkspaceAction.SWARM:
|
|
156
|
+
return WorkspacePolicy(
|
|
157
|
+
action=WorkspaceAction.SWARM,
|
|
158
|
+
allow_guarded_override=True,
|
|
159
|
+
interactive_guarded_resolution=False,
|
|
160
|
+
interactive_allow_use_current=False,
|
|
161
|
+
)
|
|
162
|
+
return WorkspacePolicy(
|
|
163
|
+
action=WorkspaceAction.CHAT,
|
|
164
|
+
allow_guarded_override=True,
|
|
165
|
+
interactive_guarded_resolution=True,
|
|
166
|
+
interactive_allow_use_current=True,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def workspace_action_label(action: str) -> str:
|
|
171
|
+
normalized = workspace_policy(action).action
|
|
172
|
+
if normalized == WorkspaceAction.FORGE:
|
|
173
|
+
return "Forge"
|
|
174
|
+
if normalized == WorkspaceAction.FORGE_PLAN:
|
|
175
|
+
return "forge plan"
|
|
176
|
+
if normalized == WorkspaceAction.FORGE_RUN:
|
|
177
|
+
return "forge run"
|
|
178
|
+
if normalized == WorkspaceAction.SWARM:
|
|
179
|
+
return "forge swarm"
|
|
180
|
+
return "chat/run startup"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def workspace_policy_violation_message(
|
|
184
|
+
binding: WorkspaceBinding,
|
|
185
|
+
*,
|
|
186
|
+
action: str,
|
|
187
|
+
) -> str:
|
|
188
|
+
policy = workspace_policy(action)
|
|
189
|
+
label = workspace_action_label(policy.action)
|
|
190
|
+
root_label = os.fspath(binding.workspace_context.workspace_root)
|
|
191
|
+
requested_label = os.fspath(binding.requested_path)
|
|
192
|
+
detail = "; ".join(binding.risk_reasons) or "workspace selection is not allowed"
|
|
193
|
+
common_guidance = "Use --path <project>, choose/create a project folder, or start alysis from inside a project."
|
|
194
|
+
if binding.risk_level == WorkspaceRiskLevel.BLOCKED:
|
|
195
|
+
return f"{label} is blocked for {requested_label}: {detail}. {common_guidance}"
|
|
196
|
+
if policy.action == WorkspaceAction.FORGE:
|
|
197
|
+
return (
|
|
198
|
+
f"{label} requires a narrower workspace than {root_label}: {detail}. {common_guidance}"
|
|
199
|
+
)
|
|
200
|
+
if policy.action in {WorkspaceAction.FORGE_RUN, WorkspaceAction.SWARM}:
|
|
201
|
+
return (
|
|
202
|
+
f"{label} requires a healthy workspace. Current binding for {requested_label} is "
|
|
203
|
+
f"guarded: {detail}. {common_guidance} "
|
|
204
|
+
f"Pass --allow-broad-workspace only if you intentionally want {label} to operate "
|
|
205
|
+
"on this broad workspace."
|
|
206
|
+
)
|
|
207
|
+
if policy.action == WorkspaceAction.FORGE_PLAN:
|
|
208
|
+
return (
|
|
209
|
+
f"{label} is guarded for {requested_label}: {detail}. {common_guidance} "
|
|
210
|
+
"Pass --allow-broad-workspace only if you intentionally want to plan from this "
|
|
211
|
+
"broad workspace."
|
|
212
|
+
)
|
|
213
|
+
return (
|
|
214
|
+
f"{label} is guarded for {requested_label}: {detail}. {common_guidance} "
|
|
215
|
+
"Pass --allow-broad-workspace to continue non-interactively."
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def ensure_workspace_policy(
|
|
220
|
+
binding: WorkspaceBinding,
|
|
221
|
+
*,
|
|
222
|
+
action: str,
|
|
223
|
+
allow_broad_workspace: bool = False,
|
|
224
|
+
) -> WorkspaceBinding:
|
|
225
|
+
policy = workspace_policy(action)
|
|
226
|
+
if binding.risk_level == WorkspaceRiskLevel.HEALTHY:
|
|
227
|
+
return binding
|
|
228
|
+
if binding.risk_level == WorkspaceRiskLevel.BLOCKED:
|
|
229
|
+
raise WorkspaceBindingError(
|
|
230
|
+
workspace_policy_violation_message(binding, action=policy.action)
|
|
231
|
+
)
|
|
232
|
+
if policy.allow_guarded_override and allow_broad_workspace:
|
|
233
|
+
return binding
|
|
234
|
+
raise WorkspaceBindingError(workspace_policy_violation_message(binding, action=policy.action))
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def classify_workspace_risk(workspace_context: WorkspaceContext) -> tuple[str, tuple[str, ...]]:
|
|
238
|
+
workspace_root = workspace_context.workspace_root
|
|
239
|
+
if _is_filesystem_root(workspace_root):
|
|
240
|
+
return (
|
|
241
|
+
WorkspaceRiskLevel.BLOCKED,
|
|
242
|
+
("filesystem root '/' is not a valid forge workspace",),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
home_dir = _home_directory()
|
|
246
|
+
if home_dir is not None and workspace_root == home_dir:
|
|
247
|
+
return (
|
|
248
|
+
WorkspaceRiskLevel.GUARDED,
|
|
249
|
+
("home directory is a broad workspace; pass an explicit override to continue",),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
if workspace_context.git_root is not None:
|
|
253
|
+
return (WorkspaceRiskLevel.HEALTHY, ())
|
|
254
|
+
|
|
255
|
+
return _classify_plain_directory(workspace_context)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _classify_plain_directory(workspace_context: WorkspaceContext) -> tuple[str, tuple[str, ...]]:
|
|
259
|
+
scan = scan_workspace(context=workspace_context)
|
|
260
|
+
top_level_names = {entry.get("path", "") for entry in scan.top_level_entries}
|
|
261
|
+
if not top_level_names:
|
|
262
|
+
return (WorkspaceRiskLevel.HEALTHY, ())
|
|
263
|
+
|
|
264
|
+
signals = project_signals_for_path(workspace_context.workspace_root)
|
|
265
|
+
has_project_signals = bool(signals)
|
|
266
|
+
if has_project_signals:
|
|
267
|
+
return (WorkspaceRiskLevel.HEALTHY, ())
|
|
268
|
+
|
|
269
|
+
if len(top_level_names) >= _BROAD_PLAIN_DIR_THRESHOLD:
|
|
270
|
+
return (
|
|
271
|
+
WorkspaceRiskLevel.GUARDED,
|
|
272
|
+
("plain directory has many top-level entries without strong project signals",),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
return (WorkspaceRiskLevel.HEALTHY, ())
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _home_directory() -> Path | None:
|
|
279
|
+
try:
|
|
280
|
+
return Path.home().resolve()
|
|
281
|
+
except OSError:
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _is_filesystem_root(path: Path) -> bool:
|
|
286
|
+
normalized = path.resolve()
|
|
287
|
+
return normalized == normalized.parent and normalized == Path(os.path.abspath(os.sep))
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def project_signals_for_path(path: Path) -> tuple[str, ...]:
|
|
291
|
+
resolved = path.expanduser().resolve(strict=False)
|
|
292
|
+
signals: list[str] = []
|
|
293
|
+
git_dir = resolved / ".git"
|
|
294
|
+
if git_dir.exists():
|
|
295
|
+
signals.append(".git")
|
|
296
|
+
|
|
297
|
+
manifest_names = [name for name, _kind in _MANIFEST_SPECS if (resolved / name).is_file()]
|
|
298
|
+
signals.extend(manifest_names[:3])
|
|
299
|
+
|
|
300
|
+
if any((resolved / name).is_file() for name in _README_NAMES):
|
|
301
|
+
signals.append("README")
|
|
302
|
+
if (resolved / "src").is_dir():
|
|
303
|
+
signals.append("src/")
|
|
304
|
+
if (resolved / "tests").is_dir():
|
|
305
|
+
signals.append("tests/")
|
|
306
|
+
return tuple(signals)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def discover_workspace_candidates(
|
|
310
|
+
path: Path,
|
|
311
|
+
*,
|
|
312
|
+
limit: int = _MAX_DISCOVERY_RESULTS,
|
|
313
|
+
) -> tuple[WorkspaceCandidate, ...]:
|
|
314
|
+
base_dir = path.expanduser().resolve()
|
|
315
|
+
if not base_dir.exists() or not base_dir.is_dir():
|
|
316
|
+
return ()
|
|
317
|
+
|
|
318
|
+
candidates: dict[Path, WorkspaceCandidate] = {}
|
|
319
|
+
immediate_children = _iter_candidate_dirs(base_dir, max_children=_MAX_DISCOVERY_CHILDREN)
|
|
320
|
+
for child in immediate_children:
|
|
321
|
+
_maybe_record_candidate(
|
|
322
|
+
candidates,
|
|
323
|
+
child,
|
|
324
|
+
source="child",
|
|
325
|
+
)
|
|
326
|
+
if child.name.casefold() not in _PROJECT_CONTAINER_DIR_NAMES:
|
|
327
|
+
continue
|
|
328
|
+
for nested in _iter_candidate_dirs(child, max_children=_MAX_DISCOVERY_CHILDREN):
|
|
329
|
+
_maybe_record_candidate(
|
|
330
|
+
candidates,
|
|
331
|
+
nested,
|
|
332
|
+
source=f"nested:{child.name}",
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
ranked = sorted(
|
|
336
|
+
candidates.values(),
|
|
337
|
+
key=lambda item: (-item.score, item.path.name.casefold(), os.fspath(item.path)),
|
|
338
|
+
)
|
|
339
|
+
return tuple(ranked[: max(1, limit)])
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _iter_candidate_dirs(path: Path, *, max_children: int) -> list[Path]:
|
|
343
|
+
children: list[Path] = []
|
|
344
|
+
try:
|
|
345
|
+
entries = sorted(path.iterdir(), key=lambda candidate: candidate.name.casefold())
|
|
346
|
+
except OSError:
|
|
347
|
+
return []
|
|
348
|
+
for candidate in entries:
|
|
349
|
+
if len(children) >= max_children:
|
|
350
|
+
break
|
|
351
|
+
if not candidate.is_dir():
|
|
352
|
+
continue
|
|
353
|
+
if candidate.name.startswith(".") or candidate.name in ROOT_RUNTIME_ARTIFACT_DIR_NAMES:
|
|
354
|
+
continue
|
|
355
|
+
children.append(candidate.resolve())
|
|
356
|
+
return children
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _maybe_record_candidate(
|
|
360
|
+
existing: dict[Path, WorkspaceCandidate],
|
|
361
|
+
path: Path,
|
|
362
|
+
*,
|
|
363
|
+
source: str,
|
|
364
|
+
) -> None:
|
|
365
|
+
signals = project_signals_for_path(path)
|
|
366
|
+
if not signals:
|
|
367
|
+
return
|
|
368
|
+
score = _project_signal_score(signals)
|
|
369
|
+
candidate = WorkspaceCandidate(
|
|
370
|
+
path=path,
|
|
371
|
+
score=score,
|
|
372
|
+
project_signals=signals,
|
|
373
|
+
source=source,
|
|
374
|
+
)
|
|
375
|
+
current = existing.get(path)
|
|
376
|
+
if current is None or candidate.score > current.score:
|
|
377
|
+
existing[path] = candidate
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _project_signal_score(signals: tuple[str, ...]) -> int:
|
|
381
|
+
score = 0
|
|
382
|
+
for signal in signals:
|
|
383
|
+
if signal == ".git":
|
|
384
|
+
score += 100
|
|
385
|
+
elif signal in {"src/", "tests/", "README"}:
|
|
386
|
+
score += 10
|
|
387
|
+
else:
|
|
388
|
+
score += 20
|
|
389
|
+
return score
|