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,537 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from ..assets import (
|
|
14
|
+
AssetAlreadyExistsError,
|
|
15
|
+
AssetError,
|
|
16
|
+
AssetSurface,
|
|
17
|
+
AssetSurfaceDetail,
|
|
18
|
+
AssetSurfaceEntry,
|
|
19
|
+
asset_reference_check,
|
|
20
|
+
bind_asset_to_matching_tasks,
|
|
21
|
+
)
|
|
22
|
+
from ..assets.surface import build_asset_surface
|
|
23
|
+
from ..config import AppConfig, load_config
|
|
24
|
+
from ..forge import ForgeError, RunPaths, load_current_run_paths, load_plan, save_plan
|
|
25
|
+
from ..surface.console import make_console
|
|
26
|
+
|
|
27
|
+
assets_app = typer.Typer(add_completion=False, help="Manage first-class forge assets.")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@assets_app.command("list")
|
|
31
|
+
def assets_list(
|
|
32
|
+
path: Path = typer.Option(
|
|
33
|
+
Path("."), "--path", help="Workspace path or repository subdirectory."
|
|
34
|
+
),
|
|
35
|
+
include_deleted: bool = typer.Option(
|
|
36
|
+
False,
|
|
37
|
+
"--include-deleted",
|
|
38
|
+
help="Include tombstoned assets.",
|
|
39
|
+
),
|
|
40
|
+
output_format: str = typer.Option(
|
|
41
|
+
"table",
|
|
42
|
+
"--format",
|
|
43
|
+
help="Output format: table or json.",
|
|
44
|
+
),
|
|
45
|
+
) -> None:
|
|
46
|
+
surface = _load_surface_or_exit(path)
|
|
47
|
+
entries = surface.list_assets(include_deleted=include_deleted)
|
|
48
|
+
fmt = _normalize_format(output_format, allowed={"table", "json"})
|
|
49
|
+
if fmt == "json":
|
|
50
|
+
_print_json(
|
|
51
|
+
{
|
|
52
|
+
"run_id": surface.run_paths.run_id,
|
|
53
|
+
"assets": [_entry_payload(entry) for entry in entries],
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
return
|
|
57
|
+
table = Table(title=f"forge assets · {surface.run_paths.run_id}")
|
|
58
|
+
table.add_column("status")
|
|
59
|
+
table.add_column("kind")
|
|
60
|
+
table.add_column("pinned")
|
|
61
|
+
table.add_column("id")
|
|
62
|
+
table.add_column("title")
|
|
63
|
+
table.add_column("size")
|
|
64
|
+
for entry in entries:
|
|
65
|
+
table.add_row(
|
|
66
|
+
entry.comprehension_status,
|
|
67
|
+
entry.record.kind,
|
|
68
|
+
"yes" if entry.record.pinned else "no",
|
|
69
|
+
entry.record.id,
|
|
70
|
+
entry.record.title,
|
|
71
|
+
_format_size(entry.record.size_bytes),
|
|
72
|
+
)
|
|
73
|
+
_console().print(table)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@assets_app.command("show")
|
|
77
|
+
def assets_show(
|
|
78
|
+
asset_id: str = typer.Argument(..., help="Asset id."),
|
|
79
|
+
path: Path = typer.Option(
|
|
80
|
+
Path("."), "--path", help="Workspace path or repository subdirectory."
|
|
81
|
+
),
|
|
82
|
+
output_format: str = typer.Option("text", "--format", help="Output format: text or json."),
|
|
83
|
+
) -> None:
|
|
84
|
+
surface = _load_surface_or_exit(path)
|
|
85
|
+
try:
|
|
86
|
+
detail = surface.show_asset(asset_id)
|
|
87
|
+
except AssetError as exc:
|
|
88
|
+
_asset_error(str(exc))
|
|
89
|
+
fmt = _normalize_format(output_format, allowed={"text", "json"})
|
|
90
|
+
if fmt == "json":
|
|
91
|
+
_print_json(_detail_payload(detail))
|
|
92
|
+
return
|
|
93
|
+
record = detail.record
|
|
94
|
+
status = "deleted" if record.deleted_at else detail.comprehension_status
|
|
95
|
+
_console().print(f"Asset {record.id}")
|
|
96
|
+
_console().print(f"- title: {record.title}")
|
|
97
|
+
_console().print(f"- status: {status}")
|
|
98
|
+
_console().print(f"- kind: {record.kind}")
|
|
99
|
+
_console().print(f"- size: {record.size_bytes} bytes")
|
|
100
|
+
_console().print(f"- pinned: {'yes' if record.pinned else 'no'}")
|
|
101
|
+
_console().print(f"- original file: {record.original_filename}")
|
|
102
|
+
if detail.comprehension is not None:
|
|
103
|
+
_console().print(f"- source: {detail.comprehension.source}")
|
|
104
|
+
_console().print(f"- detected language: {detail.comprehension.detected_language or '-'}")
|
|
105
|
+
summary = detail.comprehension.data.semantic_summary.strip()
|
|
106
|
+
if summary:
|
|
107
|
+
_console().print(f"- summary: {summary}")
|
|
108
|
+
if detail.versions:
|
|
109
|
+
_console().print(f"- versions: {', '.join(str(version) for version in detail.versions)}")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@assets_app.command("add")
|
|
113
|
+
def assets_add(
|
|
114
|
+
file: Path = typer.Argument(..., help="File to ingest."),
|
|
115
|
+
path: Path = typer.Option(
|
|
116
|
+
Path("."), "--path", help="Workspace path or repository subdirectory."
|
|
117
|
+
),
|
|
118
|
+
title: str | None = typer.Option(None, "--title", help="Asset title."),
|
|
119
|
+
description: str = typer.Option("", "--description", help="Asset description."),
|
|
120
|
+
pinned: bool = typer.Option(False, "--pinned", help="Pin asset to all tasks."),
|
|
121
|
+
wait: bool = typer.Option(False, "--wait", help="Wait for comprehension to complete."),
|
|
122
|
+
link: bool = typer.Option(False, "--link", help="Return existing asset on dedupe collision."),
|
|
123
|
+
) -> None:
|
|
124
|
+
clean_title = _resolve_title(title)
|
|
125
|
+
surface = _load_surface_or_exit(path)
|
|
126
|
+
if wait:
|
|
127
|
+
_require_subscription_comprehension(surface.cfg)
|
|
128
|
+
try:
|
|
129
|
+
result = surface.add_asset(
|
|
130
|
+
file,
|
|
131
|
+
title=clean_title,
|
|
132
|
+
description=description,
|
|
133
|
+
pinned=pinned,
|
|
134
|
+
added_by={"phase": "cli", "command": "forge assets add"},
|
|
135
|
+
comprehend="sync" if wait else "skip",
|
|
136
|
+
dedupe_policy="link" if link else "reject",
|
|
137
|
+
)
|
|
138
|
+
except AssetAlreadyExistsError as exc:
|
|
139
|
+
_asset_error(f"{exc} existing_id={exc.existing_id}")
|
|
140
|
+
except AssetError as exc:
|
|
141
|
+
_asset_error(str(exc))
|
|
142
|
+
record = result.record
|
|
143
|
+
_console().print(f"Asset: {record.id}")
|
|
144
|
+
_console().print(f"- title: {record.title}")
|
|
145
|
+
_console().print(f"- kind: {record.kind}")
|
|
146
|
+
_console().print(f"- pinned: {'yes' if record.pinned else 'no'}")
|
|
147
|
+
if result.comprehension_record is not None:
|
|
148
|
+
_console().print(f"- comprehension: {result.comprehension_record.status}")
|
|
149
|
+
elif wait:
|
|
150
|
+
_console().print(f"- comprehension: {record.comprehension_status}")
|
|
151
|
+
else:
|
|
152
|
+
_console().print("- comprehension: pending")
|
|
153
|
+
_console().print(f"- next: alysis forge assets refresh {record.id} --path {path} --wait")
|
|
154
|
+
bound_task_ids = _bind_asset_to_current_plan(surface=surface, record=record)
|
|
155
|
+
if bound_task_ids:
|
|
156
|
+
_console().print("- bound tasks: " + ", ".join(bound_task_ids))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@assets_app.command("delete")
|
|
160
|
+
def assets_delete(
|
|
161
|
+
asset_id: str = typer.Argument(..., help="Asset id."),
|
|
162
|
+
path: Path = typer.Option(
|
|
163
|
+
Path("."), "--path", help="Workspace path or repository subdirectory."
|
|
164
|
+
),
|
|
165
|
+
yes: bool = typer.Option(False, "--yes", help="Skip confirmation."),
|
|
166
|
+
) -> None:
|
|
167
|
+
if not yes:
|
|
168
|
+
if not sys.stdin.isatty():
|
|
169
|
+
_asset_error("--yes is required when stdin is not a terminal")
|
|
170
|
+
if not typer.confirm(f"Delete asset {asset_id}?", default=False):
|
|
171
|
+
_console().print("Cancelled.")
|
|
172
|
+
return
|
|
173
|
+
surface = _load_surface_or_exit(path)
|
|
174
|
+
try:
|
|
175
|
+
deleted = surface.delete_asset(asset_id)
|
|
176
|
+
except AssetError as exc:
|
|
177
|
+
_asset_error(str(exc))
|
|
178
|
+
_console().print(f"Deleted asset: {deleted.id}")
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@assets_app.command("edit")
|
|
182
|
+
def assets_edit(
|
|
183
|
+
asset_id: str = typer.Argument(..., help="Asset id."),
|
|
184
|
+
path: Path = typer.Option(
|
|
185
|
+
Path("."), "--path", help="Workspace path or repository subdirectory."
|
|
186
|
+
),
|
|
187
|
+
title: str | None = typer.Option(None, "--title", help="New title."),
|
|
188
|
+
description: str | None = typer.Option(None, "--description", help="New description."),
|
|
189
|
+
pinned: bool | None = typer.Option(None, "--pin/--unpin", help="Set pinned state."),
|
|
190
|
+
refresh: bool = typer.Option(False, "--refresh", help="Refresh comprehension after edit."),
|
|
191
|
+
) -> None:
|
|
192
|
+
if title is None and description is None and pinned is None and not refresh:
|
|
193
|
+
_asset_error("Nothing to edit. Use --title, --description, --pin, --unpin, or --refresh.")
|
|
194
|
+
surface = _load_surface_or_exit(path)
|
|
195
|
+
if refresh:
|
|
196
|
+
_require_subscription_comprehension(surface.cfg)
|
|
197
|
+
try:
|
|
198
|
+
detail = surface.edit_asset(
|
|
199
|
+
asset_id,
|
|
200
|
+
title=title,
|
|
201
|
+
description=description,
|
|
202
|
+
pinned=pinned,
|
|
203
|
+
retrigger_comprehension=False,
|
|
204
|
+
)
|
|
205
|
+
refreshed_record = (
|
|
206
|
+
surface.refresh_comprehension(asset_id, mode="sync").join() if refresh else None
|
|
207
|
+
)
|
|
208
|
+
except AssetError as exc:
|
|
209
|
+
_asset_error(str(exc))
|
|
210
|
+
_console().print(f"Updated asset: {detail.record.id}")
|
|
211
|
+
_console().print(f"- pinned: {'yes' if detail.record.pinned else 'no'}")
|
|
212
|
+
if refreshed_record is not None:
|
|
213
|
+
_console().print(f"- comprehension: {refreshed_record.status}")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@assets_app.command("refresh")
|
|
217
|
+
def assets_refresh(
|
|
218
|
+
asset_id: str = typer.Argument(..., help="Asset id."),
|
|
219
|
+
path: Path = typer.Option(
|
|
220
|
+
Path("."), "--path", help="Workspace path or repository subdirectory."
|
|
221
|
+
),
|
|
222
|
+
wait: bool = typer.Option(False, "--wait", help="Wait for comprehension to complete."),
|
|
223
|
+
) -> None:
|
|
224
|
+
surface = _load_surface_or_exit(path)
|
|
225
|
+
if not wait:
|
|
226
|
+
_asset_error(
|
|
227
|
+
"CLI refresh requires --wait. Use /assets for live background refresh in chat."
|
|
228
|
+
)
|
|
229
|
+
_require_subscription_comprehension(surface.cfg)
|
|
230
|
+
try:
|
|
231
|
+
handle = surface.refresh_comprehension(asset_id, mode="sync")
|
|
232
|
+
except AssetError as exc:
|
|
233
|
+
_asset_error(str(exc))
|
|
234
|
+
record = handle.join() if wait else None
|
|
235
|
+
_console().print(f"Refresh started: {asset_id}")
|
|
236
|
+
if record is not None:
|
|
237
|
+
_console().print(f"- comprehension: {record.status}")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@assets_app.command("cancel-pending")
|
|
241
|
+
def assets_cancel_pending(
|
|
242
|
+
path: Path = typer.Option(
|
|
243
|
+
Path("."), "--path", help="Workspace path or repository subdirectory."
|
|
244
|
+
),
|
|
245
|
+
) -> None:
|
|
246
|
+
_ = _load_surface_or_exit(path)
|
|
247
|
+
_console().print(
|
|
248
|
+
"No persistent CLI background comprehensions are running. "
|
|
249
|
+
"Use /assets to cancel live modal refreshes."
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@assets_app.command("check-plan")
|
|
254
|
+
def assets_check_plan(
|
|
255
|
+
path: Path = typer.Option(
|
|
256
|
+
Path("."), "--path", help="Workspace path or repository subdirectory."
|
|
257
|
+
),
|
|
258
|
+
output_format: str = typer.Option("text", "--format", help="Output format: text or json."),
|
|
259
|
+
) -> None:
|
|
260
|
+
surface = _load_surface_or_exit(path)
|
|
261
|
+
fmt = _normalize_format(output_format, allowed={"text", "json"})
|
|
262
|
+
try:
|
|
263
|
+
plan = load_plan(surface.run_paths)
|
|
264
|
+
except ForgeError as exc:
|
|
265
|
+
_asset_error(str(exc))
|
|
266
|
+
report = asset_reference_check(plan, surface)
|
|
267
|
+
payload = {
|
|
268
|
+
"deleted_referenced": [
|
|
269
|
+
{"task_id": task_id, "asset_id": asset_id}
|
|
270
|
+
for task_id, asset_id in report.deleted_referenced
|
|
271
|
+
],
|
|
272
|
+
"missing_referenced": [
|
|
273
|
+
{"task_id": task_id, "asset_id": asset_id}
|
|
274
|
+
for task_id, asset_id in report.missing_referenced
|
|
275
|
+
],
|
|
276
|
+
"pinned_added": report.pinned_added,
|
|
277
|
+
}
|
|
278
|
+
if fmt == "json":
|
|
279
|
+
_print_json(payload)
|
|
280
|
+
else:
|
|
281
|
+
_console().print("Plan asset reference check")
|
|
282
|
+
_console().print(f"- deleted references: {len(report.deleted_referenced)}")
|
|
283
|
+
_console().print(f"- missing references: {len(report.missing_referenced)}")
|
|
284
|
+
_console().print(f"- pinned assets not bound: {len(report.pinned_added)}")
|
|
285
|
+
if report.deleted_referenced or report.missing_referenced:
|
|
286
|
+
raise typer.Exit(code=1)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@assets_app.command("prune-legacy")
|
|
290
|
+
def assets_prune_legacy(
|
|
291
|
+
path: Path = typer.Option(
|
|
292
|
+
Path("."), "--path", help="Workspace path or repository subdirectory."
|
|
293
|
+
),
|
|
294
|
+
yes: bool = typer.Option(False, "--yes", help="Skip confirmation."),
|
|
295
|
+
output_format: str = typer.Option("text", "--format", help="Output format: text or json."),
|
|
296
|
+
) -> None:
|
|
297
|
+
fmt = _normalize_format(output_format, allowed={"text", "json"})
|
|
298
|
+
try:
|
|
299
|
+
cfg = load_config()
|
|
300
|
+
run_paths = load_current_run_paths(path)
|
|
301
|
+
plan = load_plan(run_paths, migrate_legacy=False)
|
|
302
|
+
surface = build_surface(cfg=cfg, run_paths=run_paths)
|
|
303
|
+
except (ForgeError, AssetError) as exc:
|
|
304
|
+
_asset_error(str(exc))
|
|
305
|
+
schema_version = _plan_schema_version(plan)
|
|
306
|
+
if schema_version < 2:
|
|
307
|
+
_asset_error("Legacy asset pruning requires plan schema_version=2 after migration.")
|
|
308
|
+
legacy_files = _legacy_asset_files(run_paths)
|
|
309
|
+
known_hashes = {
|
|
310
|
+
record.sha256
|
|
311
|
+
for record in surface.index.records(include_deleted=True)
|
|
312
|
+
if str(record.sha256 or "").strip()
|
|
313
|
+
}
|
|
314
|
+
verified: list[str] = []
|
|
315
|
+
unverified: list[str] = []
|
|
316
|
+
for file_path in legacy_files:
|
|
317
|
+
digest = _sha256_file(file_path)
|
|
318
|
+
rel = file_path.relative_to(run_paths.root).as_posix()
|
|
319
|
+
if digest in known_hashes:
|
|
320
|
+
verified.append(rel)
|
|
321
|
+
else:
|
|
322
|
+
unverified.append(rel)
|
|
323
|
+
payload = {
|
|
324
|
+
"run_id": run_paths.run_id,
|
|
325
|
+
"verified": verified,
|
|
326
|
+
"unverified": unverified,
|
|
327
|
+
"deleted": [],
|
|
328
|
+
}
|
|
329
|
+
if unverified:
|
|
330
|
+
if fmt == "json":
|
|
331
|
+
_print_json(payload)
|
|
332
|
+
else:
|
|
333
|
+
_console().print("Refusing to prune unverified legacy asset files:")
|
|
334
|
+
for item in unverified:
|
|
335
|
+
_console().print(f"- {item}")
|
|
336
|
+
raise typer.Exit(code=1)
|
|
337
|
+
if not verified:
|
|
338
|
+
if fmt == "json":
|
|
339
|
+
_print_json(payload)
|
|
340
|
+
else:
|
|
341
|
+
_console().print("No legacy asset files to prune.")
|
|
342
|
+
return
|
|
343
|
+
if not yes:
|
|
344
|
+
if not sys.stdin.isatty():
|
|
345
|
+
_asset_error("--yes is required when stdin is not a terminal")
|
|
346
|
+
if not typer.confirm(
|
|
347
|
+
f"Delete {len(verified)} verified legacy asset file(s)?", default=False
|
|
348
|
+
):
|
|
349
|
+
_console().print("Cancelled.")
|
|
350
|
+
return
|
|
351
|
+
deleted: list[str] = []
|
|
352
|
+
for file_path in legacy_files:
|
|
353
|
+
rel = file_path.relative_to(run_paths.root).as_posix()
|
|
354
|
+
if rel not in verified:
|
|
355
|
+
continue
|
|
356
|
+
try:
|
|
357
|
+
file_path.unlink()
|
|
358
|
+
except OSError as exc:
|
|
359
|
+
_asset_error(f"Failed to delete {rel}: {exc}")
|
|
360
|
+
deleted.append(rel)
|
|
361
|
+
_remove_empty_legacy_dirs(run_paths)
|
|
362
|
+
payload["deleted"] = deleted
|
|
363
|
+
if fmt == "json":
|
|
364
|
+
_print_json(payload)
|
|
365
|
+
else:
|
|
366
|
+
_console().print(f"Pruned {len(deleted)} verified legacy asset file(s).")
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _load_surface_or_exit(path: Path) -> AssetSurface:
|
|
370
|
+
try:
|
|
371
|
+
cfg = load_config()
|
|
372
|
+
run_paths = load_current_run_paths(path)
|
|
373
|
+
load_plan(run_paths)
|
|
374
|
+
return build_surface(cfg=cfg, run_paths=run_paths)
|
|
375
|
+
except (ForgeError, AssetError) as exc:
|
|
376
|
+
_asset_error(str(exc))
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def build_surface(*, cfg: AppConfig, run_paths: RunPaths) -> AssetSurface:
|
|
380
|
+
return build_asset_surface(cfg=cfg, run_paths=run_paths)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _require_subscription_comprehension(cfg: AppConfig) -> None:
|
|
384
|
+
from ..profiles import active_subscription_selection_ready, get_active_profile
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
profile = get_active_profile(cfg)
|
|
388
|
+
except Exception:
|
|
389
|
+
return
|
|
390
|
+
provider_id = str(profile.auth_provider or "").strip()
|
|
391
|
+
if not provider_id:
|
|
392
|
+
return
|
|
393
|
+
if not active_subscription_selection_ready(cfg):
|
|
394
|
+
_asset_error(
|
|
395
|
+
"Choose the subscription model and reasoning effort in "
|
|
396
|
+
"/config → Default Model before running asset comprehension."
|
|
397
|
+
)
|
|
398
|
+
try:
|
|
399
|
+
from ..provider_auth import create_provider_auth
|
|
400
|
+
|
|
401
|
+
adapter = create_provider_auth(provider_id)
|
|
402
|
+
compatible = profile.protocol == adapter.protocol and profile.base_url.rstrip(
|
|
403
|
+
"/"
|
|
404
|
+
) == adapter.base_url.rstrip("/")
|
|
405
|
+
status = adapter.account_status()
|
|
406
|
+
except Exception as exc: # noqa: BLE001
|
|
407
|
+
_asset_error(f"Subscription status check failed: {exc}")
|
|
408
|
+
if not compatible:
|
|
409
|
+
_asset_error("The subscription profile is incompatible; reconnect it through setup.")
|
|
410
|
+
if not status.connected:
|
|
411
|
+
detail = f" ({status.detail})" if status.detail else ""
|
|
412
|
+
_asset_error(
|
|
413
|
+
f"The selected AI subscription is not connected{detail}. "
|
|
414
|
+
f"Run `alysis auth login {provider_id}`."
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _bind_asset_to_current_plan(*, surface: AssetSurface, record: Any) -> list[str]:
|
|
419
|
+
try:
|
|
420
|
+
plan = load_plan(surface.run_paths)
|
|
421
|
+
active_records = surface.index.records(include_deleted=False)
|
|
422
|
+
bound_task_ids = bind_asset_to_matching_tasks(
|
|
423
|
+
plan=plan,
|
|
424
|
+
record=record,
|
|
425
|
+
active_records=active_records,
|
|
426
|
+
)
|
|
427
|
+
if bound_task_ids:
|
|
428
|
+
save_plan(surface.run_paths, plan)
|
|
429
|
+
return bound_task_ids
|
|
430
|
+
except (ForgeError, AssetError):
|
|
431
|
+
return []
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _resolve_title(title: str | None) -> str:
|
|
435
|
+
if title is None:
|
|
436
|
+
if not sys.stdin.isatty():
|
|
437
|
+
_asset_error("--title is required when stdin is not a terminal")
|
|
438
|
+
title = typer.prompt("Title")
|
|
439
|
+
clean = str(title or "").strip()
|
|
440
|
+
if not clean:
|
|
441
|
+
_asset_error("Asset title is required.")
|
|
442
|
+
return clean
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _normalize_format(value: str, *, allowed: set[str]) -> str:
|
|
446
|
+
normalized = str(value or "").strip().lower()
|
|
447
|
+
if normalized not in allowed:
|
|
448
|
+
_asset_error(f"--format must be one of: {', '.join(sorted(allowed))}")
|
|
449
|
+
return normalized
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _entry_payload(entry: AssetSurfaceEntry) -> dict[str, Any]:
|
|
453
|
+
return {
|
|
454
|
+
"record": entry.record.to_dict(),
|
|
455
|
+
"comprehension_status": entry.comprehension_status,
|
|
456
|
+
"comprehension_source": entry.comprehension_source,
|
|
457
|
+
"comprehension_summary_preview": entry.comprehension_summary_preview,
|
|
458
|
+
"detected_language": entry.detected_language,
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _detail_payload(detail: AssetSurfaceDetail) -> dict[str, Any]:
|
|
463
|
+
return {
|
|
464
|
+
"record": detail.record.to_dict(),
|
|
465
|
+
"comprehension_status": detail.comprehension_status,
|
|
466
|
+
"comprehension": (
|
|
467
|
+
detail.comprehension.to_dict() if detail.comprehension is not None else None
|
|
468
|
+
),
|
|
469
|
+
"versions": detail.versions,
|
|
470
|
+
"extracted_text_preview": detail.extracted_text_preview,
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _print_json(payload: dict[str, Any]) -> None:
|
|
475
|
+
typer.echo(json.dumps(payload, ensure_ascii=False, sort_keys=True))
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _format_size(size_bytes: int) -> str:
|
|
479
|
+
size = float(size_bytes)
|
|
480
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
481
|
+
if size < 1024 or unit == "GB":
|
|
482
|
+
return f"{size:.0f} {unit}" if unit == "B" else f"{size:.1f} {unit}"
|
|
483
|
+
size /= 1024
|
|
484
|
+
return f"{size_bytes} B"
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _console() -> Console:
|
|
488
|
+
return make_console()
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
def _asset_error(message: str) -> None:
|
|
492
|
+
typer.echo(f"Asset error: {message}", err=True)
|
|
493
|
+
raise typer.Exit(code=2)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _plan_schema_version(plan: dict[str, Any]) -> int:
|
|
497
|
+
try:
|
|
498
|
+
return int(plan.get("schema_version", 1) or 1)
|
|
499
|
+
except (TypeError, ValueError):
|
|
500
|
+
return 1
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _legacy_asset_files(run_paths: RunPaths) -> list[Path]:
|
|
504
|
+
roots = [run_paths.assets_dir, run_paths.assets_text_dir]
|
|
505
|
+
files: list[Path] = []
|
|
506
|
+
for root in roots:
|
|
507
|
+
if not root.exists():
|
|
508
|
+
continue
|
|
509
|
+
files.extend(path for path in sorted(root.rglob("*")) if path.is_file())
|
|
510
|
+
return files
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _sha256_file(path: Path) -> str:
|
|
514
|
+
digest = hashlib.sha256()
|
|
515
|
+
with path.open("rb") as handle:
|
|
516
|
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
517
|
+
digest.update(chunk)
|
|
518
|
+
return digest.hexdigest()
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _remove_empty_legacy_dirs(run_paths: RunPaths) -> None:
|
|
522
|
+
for root in (run_paths.assets_text_dir, run_paths.assets_dir):
|
|
523
|
+
if not root.exists():
|
|
524
|
+
continue
|
|
525
|
+
for directory in sorted(
|
|
526
|
+
[path for path in root.rglob("*") if path.is_dir()],
|
|
527
|
+
key=lambda item: len(item.parts),
|
|
528
|
+
reverse=True,
|
|
529
|
+
):
|
|
530
|
+
try:
|
|
531
|
+
directory.rmdir()
|
|
532
|
+
except OSError:
|
|
533
|
+
pass
|
|
534
|
+
try:
|
|
535
|
+
root.rmdir()
|
|
536
|
+
except OSError:
|
|
537
|
+
pass
|