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,275 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import mimetypes
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import tempfile
|
|
8
|
+
import uuid
|
|
9
|
+
from contextlib import suppress
|
|
10
|
+
from dataclasses import replace
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Literal
|
|
13
|
+
|
|
14
|
+
from ..forge import RunPaths, now_iso
|
|
15
|
+
from .index import AssetIndex
|
|
16
|
+
from .models import AssetAlreadyExistsError, AssetError, AssetRecord
|
|
17
|
+
from .paths import asset_raw_path, asset_thumbnail_path, repo_rel
|
|
18
|
+
|
|
19
|
+
_TEXT_SUFFIXES = {
|
|
20
|
+
".txt",
|
|
21
|
+
".md",
|
|
22
|
+
".markdown",
|
|
23
|
+
".json",
|
|
24
|
+
".yaml",
|
|
25
|
+
".yml",
|
|
26
|
+
".toml",
|
|
27
|
+
".ini",
|
|
28
|
+
".cfg",
|
|
29
|
+
".csv",
|
|
30
|
+
".tsv",
|
|
31
|
+
".log",
|
|
32
|
+
".py",
|
|
33
|
+
".js",
|
|
34
|
+
".jsx",
|
|
35
|
+
".ts",
|
|
36
|
+
".tsx",
|
|
37
|
+
".html",
|
|
38
|
+
".css",
|
|
39
|
+
".xml",
|
|
40
|
+
".sql",
|
|
41
|
+
".sh",
|
|
42
|
+
".ps1",
|
|
43
|
+
}
|
|
44
|
+
_IMAGE_THUMB_MAX_EDGE = 512
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def ingest_asset(
|
|
48
|
+
path: Path,
|
|
49
|
+
*,
|
|
50
|
+
title: str,
|
|
51
|
+
description: str = "",
|
|
52
|
+
run_paths: RunPaths,
|
|
53
|
+
pinned: bool = False,
|
|
54
|
+
added_by: dict[str, Any] | None = None,
|
|
55
|
+
dedupe_policy: Literal["reject", "link"] = "reject",
|
|
56
|
+
) -> AssetRecord:
|
|
57
|
+
source = path.expanduser().resolve()
|
|
58
|
+
clean_title = title.strip()
|
|
59
|
+
if not clean_title:
|
|
60
|
+
raise AssetError("Asset title is required.")
|
|
61
|
+
if not source.exists():
|
|
62
|
+
raise AssetError(f"Asset file does not exist: {source}")
|
|
63
|
+
if not source.is_file():
|
|
64
|
+
raise AssetError(f"Asset path is not a file: {source}")
|
|
65
|
+
|
|
66
|
+
raw = source.read_bytes()
|
|
67
|
+
digest = hashlib.sha256(raw).hexdigest()
|
|
68
|
+
index = AssetIndex(run_paths)
|
|
69
|
+
existing = index.find_by_sha256(digest)
|
|
70
|
+
if existing is not None:
|
|
71
|
+
if dedupe_policy == "link":
|
|
72
|
+
return existing
|
|
73
|
+
raise AssetAlreadyExistsError(existing.id)
|
|
74
|
+
|
|
75
|
+
kind, mime = _classify_asset(source, raw)
|
|
76
|
+
staging_dir = _staging_dir(run_paths)
|
|
77
|
+
staged_raw = staging_dir / source.name
|
|
78
|
+
staged_thumbnail = staging_dir / "thumbnail.png"
|
|
79
|
+
try:
|
|
80
|
+
_atomic_write_bytes(staged_raw, raw)
|
|
81
|
+
if kind == "image":
|
|
82
|
+
_write_thumbnail(source, staged_thumbnail)
|
|
83
|
+
return index.add_with_publish(
|
|
84
|
+
sha256=digest,
|
|
85
|
+
dedupe_policy=dedupe_policy,
|
|
86
|
+
build_record=lambda existing_ids: _build_record(
|
|
87
|
+
run_paths=run_paths,
|
|
88
|
+
existing_ids=existing_ids,
|
|
89
|
+
digest=digest,
|
|
90
|
+
title=clean_title,
|
|
91
|
+
description=description,
|
|
92
|
+
kind=kind,
|
|
93
|
+
mime=mime,
|
|
94
|
+
original_filename=source.name,
|
|
95
|
+
size_bytes=len(raw),
|
|
96
|
+
pinned=pinned,
|
|
97
|
+
added_by=added_by,
|
|
98
|
+
),
|
|
99
|
+
publish=lambda record: _publish_staged_asset(
|
|
100
|
+
run_paths=run_paths,
|
|
101
|
+
record=record,
|
|
102
|
+
staged_raw=staged_raw,
|
|
103
|
+
staged_thumbnail=staged_thumbnail if kind == "image" else None,
|
|
104
|
+
),
|
|
105
|
+
rollback=lambda record: _rollback_published_asset(run_paths, record),
|
|
106
|
+
)
|
|
107
|
+
finally:
|
|
108
|
+
with suppress(OSError):
|
|
109
|
+
shutil.rmtree(staging_dir)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _staging_dir(run_paths: RunPaths) -> Path:
|
|
113
|
+
return run_paths.assets_raw_dir / ".staging" / uuid.uuid4().hex
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _build_record(
|
|
117
|
+
*,
|
|
118
|
+
run_paths: RunPaths,
|
|
119
|
+
existing_ids: set[str],
|
|
120
|
+
digest: str,
|
|
121
|
+
title: str,
|
|
122
|
+
description: str,
|
|
123
|
+
kind: Literal["text", "image"],
|
|
124
|
+
mime: str,
|
|
125
|
+
original_filename: str,
|
|
126
|
+
size_bytes: int,
|
|
127
|
+
pinned: bool,
|
|
128
|
+
added_by: dict[str, Any] | None,
|
|
129
|
+
) -> AssetRecord:
|
|
130
|
+
asset_id = _asset_id_for_sha256(digest, existing_ids)
|
|
131
|
+
stored_path = asset_raw_path(run_paths, asset_id, original_filename)
|
|
132
|
+
record = AssetRecord(
|
|
133
|
+
id=asset_id,
|
|
134
|
+
title=title,
|
|
135
|
+
description=description,
|
|
136
|
+
kind=kind,
|
|
137
|
+
mime=mime,
|
|
138
|
+
original_filename=original_filename,
|
|
139
|
+
size_bytes=size_bytes,
|
|
140
|
+
sha256=digest,
|
|
141
|
+
stored_path=repo_rel(run_paths.root, stored_path),
|
|
142
|
+
extracted_text_path=None,
|
|
143
|
+
thumbnail_path=None,
|
|
144
|
+
pinned=bool(pinned),
|
|
145
|
+
added_at=now_iso(),
|
|
146
|
+
added_by=dict(added_by or {}),
|
|
147
|
+
deleted_at=None,
|
|
148
|
+
comprehension_status="pending",
|
|
149
|
+
comprehension_current_version=None,
|
|
150
|
+
)
|
|
151
|
+
if kind == "text":
|
|
152
|
+
return _with_text_paths(record)
|
|
153
|
+
return _with_thumbnail(
|
|
154
|
+
record, repo_rel(run_paths.root, asset_thumbnail_path(run_paths, asset_id))
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _publish_staged_asset(
|
|
159
|
+
*,
|
|
160
|
+
run_paths: RunPaths,
|
|
161
|
+
record: AssetRecord,
|
|
162
|
+
staged_raw: Path,
|
|
163
|
+
staged_thumbnail: Path | None,
|
|
164
|
+
) -> None:
|
|
165
|
+
final_raw = run_paths.root / record.stored_path
|
|
166
|
+
final_raw.parent.mkdir(parents=True, exist_ok=True)
|
|
167
|
+
os.replace(staged_raw, final_raw)
|
|
168
|
+
if staged_thumbnail is not None and record.thumbnail_path is not None:
|
|
169
|
+
final_thumbnail = run_paths.root / record.thumbnail_path
|
|
170
|
+
final_thumbnail.parent.mkdir(parents=True, exist_ok=True)
|
|
171
|
+
os.replace(staged_thumbnail, final_thumbnail)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _rollback_published_asset(run_paths: RunPaths, record: AssetRecord) -> None:
|
|
175
|
+
with suppress(OSError):
|
|
176
|
+
shutil.rmtree((run_paths.root / record.stored_path).parent)
|
|
177
|
+
if record.thumbnail_path is not None:
|
|
178
|
+
with suppress(FileNotFoundError):
|
|
179
|
+
(run_paths.root / record.thumbnail_path).unlink()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _with_text_paths(record: AssetRecord) -> AssetRecord:
|
|
183
|
+
return replace(record, extracted_text_path=record.stored_path)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _with_thumbnail(record: AssetRecord, thumbnail_path: str) -> AssetRecord:
|
|
187
|
+
return replace(record, thumbnail_path=thumbnail_path)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _classify_asset(source: Path, raw: bytes) -> tuple[Literal["text", "image"], str]:
|
|
191
|
+
mime, _ = mimetypes.guess_type(source.name)
|
|
192
|
+
suffix = source.suffix.lower()
|
|
193
|
+
if mime and mime.startswith("image/"):
|
|
194
|
+
_ensure_pillow_can_open(source)
|
|
195
|
+
return "image", mime
|
|
196
|
+
if mime and mime.startswith("text/"):
|
|
197
|
+
_ensure_utf8_text(raw, source)
|
|
198
|
+
return "text", mime
|
|
199
|
+
if suffix in _TEXT_SUFFIXES:
|
|
200
|
+
_ensure_utf8_text(raw, source)
|
|
201
|
+
return "text", mime or "text/plain"
|
|
202
|
+
raise AssetError(f"Unsupported asset file type: {source.name}")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _ensure_utf8_text(raw: bytes, source: Path) -> None:
|
|
206
|
+
if b"\x00" in raw:
|
|
207
|
+
raise AssetError(f"Unsupported binary text asset: {source.name}")
|
|
208
|
+
try:
|
|
209
|
+
raw.decode("utf-8")
|
|
210
|
+
except UnicodeDecodeError as e:
|
|
211
|
+
raise AssetError(f"Text asset must be UTF-8: {source.name}") from e
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _ensure_pillow_can_open(source: Path) -> None:
|
|
215
|
+
try:
|
|
216
|
+
from PIL import Image
|
|
217
|
+
except ModuleNotFoundError as e:
|
|
218
|
+
raise AssetError("Pillow is required for image asset ingestion.") from e
|
|
219
|
+
try:
|
|
220
|
+
with Image.open(source) as image:
|
|
221
|
+
image.verify()
|
|
222
|
+
except Exception as e: # noqa: BLE001 - Pillow raises multiple format-specific errors
|
|
223
|
+
raise AssetError(f"Unsupported image asset: {source.name}") from e
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _write_thumbnail(source: Path, destination: Path) -> None:
|
|
227
|
+
try:
|
|
228
|
+
from PIL import Image
|
|
229
|
+
except ModuleNotFoundError as e:
|
|
230
|
+
raise AssetError("Pillow is required for image asset thumbnails.") from e
|
|
231
|
+
with Image.open(source) as image:
|
|
232
|
+
normalized = image.convert("RGB")
|
|
233
|
+
normalized.thumbnail((_IMAGE_THUMB_MAX_EDGE, _IMAGE_THUMB_MAX_EDGE))
|
|
234
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
235
|
+
fd, temp_name = tempfile.mkstemp(
|
|
236
|
+
dir=destination.parent,
|
|
237
|
+
prefix=f".{destination.name}.",
|
|
238
|
+
suffix=".tmp",
|
|
239
|
+
)
|
|
240
|
+
temp_path = Path(temp_name)
|
|
241
|
+
os.close(fd)
|
|
242
|
+
try:
|
|
243
|
+
normalized.save(temp_path, format="PNG")
|
|
244
|
+
os.replace(temp_path, destination)
|
|
245
|
+
finally:
|
|
246
|
+
with suppress(FileNotFoundError):
|
|
247
|
+
temp_path.unlink()
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _atomic_write_bytes(path: Path, data: bytes) -> None:
|
|
251
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
252
|
+
fd, temp_name = tempfile.mkstemp(
|
|
253
|
+
dir=path.parent,
|
|
254
|
+
prefix=f".{path.name}.",
|
|
255
|
+
suffix=".tmp",
|
|
256
|
+
)
|
|
257
|
+
temp_path = Path(temp_name)
|
|
258
|
+
try:
|
|
259
|
+
with os.fdopen(fd, "wb") as handle:
|
|
260
|
+
handle.write(data)
|
|
261
|
+
handle.flush()
|
|
262
|
+
os.fsync(handle.fileno())
|
|
263
|
+
os.replace(temp_path, path)
|
|
264
|
+
finally:
|
|
265
|
+
with suppress(FileNotFoundError):
|
|
266
|
+
temp_path.unlink()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _asset_id_for_sha256(sha256: str, existing_ids: set[str]) -> str:
|
|
270
|
+
digest = sha256.strip().lower()
|
|
271
|
+
for offset in range(0, max(len(digest) - 7, 1), 8):
|
|
272
|
+
candidate = f"ast_{digest[offset : offset + 8]}"
|
|
273
|
+
if len(candidate) == 12 and candidate not in existing_ids:
|
|
274
|
+
return candidate
|
|
275
|
+
raise AssetError("Unable to allocate stable asset id without collision.")
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import errno
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import socket
|
|
9
|
+
import time
|
|
10
|
+
from contextlib import suppress
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Literal
|
|
14
|
+
|
|
15
|
+
from ..config import AppConfig
|
|
16
|
+
from ..forge import RunPaths, now_iso
|
|
17
|
+
from .models import AssetAlreadyExistsError, AssetError
|
|
18
|
+
from .surface import AssetSurface
|
|
19
|
+
|
|
20
|
+
LOGGER = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
PLAN_SCHEMA_VERSION_V1 = 1
|
|
23
|
+
PLAN_SCHEMA_VERSION_V2 = 2
|
|
24
|
+
_LOCK_POLL_SECONDS = 0.05
|
|
25
|
+
_LOCK_WAIT_SECONDS = 30.0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class MigratedLegacyAsset:
|
|
30
|
+
legacy_stored_path: str
|
|
31
|
+
legacy_text_copy_path: str | None
|
|
32
|
+
new_asset_id: str
|
|
33
|
+
title: str
|
|
34
|
+
description: str
|
|
35
|
+
comprehension_handle_running: bool
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class LegacyAssetMigrationResult:
|
|
40
|
+
schema_version_before: int
|
|
41
|
+
schema_version_after: int
|
|
42
|
+
migrated_assets: list[MigratedLegacyAsset]
|
|
43
|
+
skipped_existing: list[str]
|
|
44
|
+
failed: list[tuple[str, str]]
|
|
45
|
+
plan_assets_array_cleared: bool
|
|
46
|
+
plan_v2_written: bool
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def migrate_legacy_assets(
|
|
50
|
+
*,
|
|
51
|
+
cfg: AppConfig,
|
|
52
|
+
run_paths: RunPaths,
|
|
53
|
+
surface: AssetSurface,
|
|
54
|
+
comprehend_mode: Literal["sync", "async", "skip"] = "async",
|
|
55
|
+
) -> LegacyAssetMigrationResult:
|
|
56
|
+
plan = _read_plan(run_paths.plan_json_path)
|
|
57
|
+
schema_before = _schema_version(plan)
|
|
58
|
+
if schema_before >= PLAN_SCHEMA_VERSION_V2:
|
|
59
|
+
return LegacyAssetMigrationResult(
|
|
60
|
+
schema_version_before=schema_before,
|
|
61
|
+
schema_version_after=schema_before,
|
|
62
|
+
migrated_assets=[],
|
|
63
|
+
skipped_existing=[],
|
|
64
|
+
failed=[],
|
|
65
|
+
plan_assets_array_cleared=False,
|
|
66
|
+
plan_v2_written=False,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
with LegacyMigrationLock(run_paths):
|
|
70
|
+
plan = _read_plan(run_paths.plan_json_path)
|
|
71
|
+
schema_before = _schema_version(plan)
|
|
72
|
+
if schema_before >= PLAN_SCHEMA_VERSION_V2:
|
|
73
|
+
return LegacyAssetMigrationResult(
|
|
74
|
+
schema_version_before=schema_before,
|
|
75
|
+
schema_version_after=schema_before,
|
|
76
|
+
migrated_assets=[],
|
|
77
|
+
skipped_existing=[],
|
|
78
|
+
failed=[],
|
|
79
|
+
plan_assets_array_cleared=False,
|
|
80
|
+
plan_v2_written=False,
|
|
81
|
+
)
|
|
82
|
+
result = _migrate_locked(
|
|
83
|
+
cfg=cfg,
|
|
84
|
+
run_paths=run_paths,
|
|
85
|
+
surface=surface,
|
|
86
|
+
plan=plan,
|
|
87
|
+
schema_before=schema_before,
|
|
88
|
+
comprehend_mode="skip" if not cfg.assets.enabled else comprehend_mode,
|
|
89
|
+
)
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class LegacyMigrationLock:
|
|
94
|
+
def __init__(self, run_paths: RunPaths) -> None:
|
|
95
|
+
self.path = run_paths.run_dir / "legacy_migration.lock"
|
|
96
|
+
self._fd: int | None = None
|
|
97
|
+
|
|
98
|
+
def __enter__(self) -> LegacyMigrationLock:
|
|
99
|
+
deadline = time.monotonic() + _LOCK_WAIT_SECONDS
|
|
100
|
+
while True:
|
|
101
|
+
try:
|
|
102
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
fd = os.open(self.path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
|
104
|
+
except FileExistsError:
|
|
105
|
+
if self._recover_stale_lock():
|
|
106
|
+
continue
|
|
107
|
+
if time.monotonic() >= deadline:
|
|
108
|
+
raise AssetError(
|
|
109
|
+
f"Timed out waiting for legacy asset migration lock: {self.path}"
|
|
110
|
+
) from None
|
|
111
|
+
time.sleep(_LOCK_POLL_SECONDS)
|
|
112
|
+
continue
|
|
113
|
+
self._fd = fd
|
|
114
|
+
payload = {
|
|
115
|
+
"pid": os.getpid(),
|
|
116
|
+
"hostname": socket.gethostname(),
|
|
117
|
+
"created_at": now_iso(),
|
|
118
|
+
"created_epoch": time.time(),
|
|
119
|
+
}
|
|
120
|
+
os.write(fd, (json.dumps(payload, sort_keys=True) + "\n").encode("utf-8"))
|
|
121
|
+
os.fsync(fd)
|
|
122
|
+
return self
|
|
123
|
+
|
|
124
|
+
def __exit__(self, exc_type: object, exc: object, tb: object) -> None:
|
|
125
|
+
if self._fd is not None:
|
|
126
|
+
os.close(self._fd)
|
|
127
|
+
self._fd = None
|
|
128
|
+
with suppress(FileNotFoundError):
|
|
129
|
+
self.path.unlink()
|
|
130
|
+
|
|
131
|
+
def _recover_stale_lock(self) -> bool:
|
|
132
|
+
try:
|
|
133
|
+
payload = json.loads(self.path.read_text(encoding="utf-8"))
|
|
134
|
+
except (OSError, json.JSONDecodeError):
|
|
135
|
+
return False
|
|
136
|
+
if not isinstance(payload, dict):
|
|
137
|
+
return False
|
|
138
|
+
created_epoch = _safe_float(payload.get("created_epoch"))
|
|
139
|
+
if created_epoch is None or time.time() - created_epoch < _LOCK_WAIT_SECONDS:
|
|
140
|
+
return False
|
|
141
|
+
hostname = str(payload.get("hostname") or "").strip()
|
|
142
|
+
if hostname and hostname != socket.gethostname():
|
|
143
|
+
return False
|
|
144
|
+
pid = _safe_int(payload.get("pid"))
|
|
145
|
+
if pid is not None and _process_is_running(pid) is not False:
|
|
146
|
+
return False
|
|
147
|
+
with suppress(FileNotFoundError):
|
|
148
|
+
self.path.unlink()
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _migrate_locked(
|
|
153
|
+
*,
|
|
154
|
+
cfg: AppConfig,
|
|
155
|
+
run_paths: RunPaths,
|
|
156
|
+
surface: AssetSurface,
|
|
157
|
+
plan: dict[str, Any],
|
|
158
|
+
schema_before: int,
|
|
159
|
+
comprehend_mode: Literal["sync", "async", "skip"],
|
|
160
|
+
) -> LegacyAssetMigrationResult:
|
|
161
|
+
legacy_assets = plan.get("assets")
|
|
162
|
+
if not isinstance(legacy_assets, list):
|
|
163
|
+
raise AssetError("Invalid legacy plan: 'assets' must be an array before migration.")
|
|
164
|
+
title_counts: dict[str, int] = {}
|
|
165
|
+
migrated: list[MigratedLegacyAsset] = []
|
|
166
|
+
skipped: list[str] = []
|
|
167
|
+
failed: list[tuple[str, str]] = []
|
|
168
|
+
|
|
169
|
+
for entry in legacy_assets:
|
|
170
|
+
if not isinstance(entry, dict):
|
|
171
|
+
failed.append(("(invalid legacy asset entry)", "Legacy asset entry must be an object"))
|
|
172
|
+
continue
|
|
173
|
+
stored_path = str(entry.get("stored_path") or "").strip()
|
|
174
|
+
text_copy_path = _optional_string(entry.get("text_copy_path"))
|
|
175
|
+
if not stored_path:
|
|
176
|
+
failed.append(("(missing stored_path)", "Legacy asset is missing stored_path"))
|
|
177
|
+
continue
|
|
178
|
+
source = (run_paths.root / stored_path).resolve()
|
|
179
|
+
try:
|
|
180
|
+
_ensure_under_root(run_paths.root, source)
|
|
181
|
+
digest = hashlib.sha256(source.read_bytes()).hexdigest()
|
|
182
|
+
except OSError as exc:
|
|
183
|
+
failed.append((stored_path, str(exc)))
|
|
184
|
+
continue
|
|
185
|
+
except AssetError as exc:
|
|
186
|
+
failed.append((stored_path, str(exc)))
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
existing = surface.index.find_by_sha256(digest, include_deleted=False)
|
|
190
|
+
if existing is not None:
|
|
191
|
+
skipped.append(stored_path)
|
|
192
|
+
LOGGER.info(
|
|
193
|
+
"legacy_asset_migration skipped existing legacy_stored_path=%s asset_id=%s",
|
|
194
|
+
stored_path,
|
|
195
|
+
existing.id,
|
|
196
|
+
)
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
title = _deduped_title(_derive_title(source), title_counts)
|
|
200
|
+
try:
|
|
201
|
+
add_result = surface.add_asset(
|
|
202
|
+
source,
|
|
203
|
+
title=title,
|
|
204
|
+
description="",
|
|
205
|
+
pinned=False,
|
|
206
|
+
added_by={
|
|
207
|
+
"phase": "legacy_migration",
|
|
208
|
+
"schema_version_before": schema_before,
|
|
209
|
+
"legacy_stored_path": stored_path,
|
|
210
|
+
},
|
|
211
|
+
comprehend=comprehend_mode,
|
|
212
|
+
dedupe_policy="link",
|
|
213
|
+
)
|
|
214
|
+
except AssetAlreadyExistsError as exc:
|
|
215
|
+
skipped.append(stored_path)
|
|
216
|
+
LOGGER.info(
|
|
217
|
+
"legacy_asset_migration skipped dedupe legacy_stored_path=%s asset_id=%s",
|
|
218
|
+
stored_path,
|
|
219
|
+
exc.existing_id,
|
|
220
|
+
)
|
|
221
|
+
continue
|
|
222
|
+
except AssetError as exc:
|
|
223
|
+
failed.append((stored_path, str(exc)))
|
|
224
|
+
continue
|
|
225
|
+
handle = add_result.comprehension_handle
|
|
226
|
+
migrated.append(
|
|
227
|
+
MigratedLegacyAsset(
|
|
228
|
+
legacy_stored_path=stored_path,
|
|
229
|
+
legacy_text_copy_path=text_copy_path,
|
|
230
|
+
new_asset_id=add_result.record.id,
|
|
231
|
+
title=add_result.record.title,
|
|
232
|
+
description=add_result.record.description,
|
|
233
|
+
comprehension_handle_running=bool(handle is not None and not handle.is_done()),
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
LOGGER.info(
|
|
237
|
+
"legacy_asset_migration migrated legacy_stored_path=%s asset_id=%s comprehend=%s",
|
|
238
|
+
stored_path,
|
|
239
|
+
add_result.record.id,
|
|
240
|
+
comprehend_mode,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if failed:
|
|
244
|
+
return LegacyAssetMigrationResult(
|
|
245
|
+
schema_version_before=schema_before,
|
|
246
|
+
schema_version_after=schema_before,
|
|
247
|
+
migrated_assets=migrated,
|
|
248
|
+
skipped_existing=skipped,
|
|
249
|
+
failed=failed,
|
|
250
|
+
plan_assets_array_cleared=False,
|
|
251
|
+
plan_v2_written=False,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
plan["schema_version"] = PLAN_SCHEMA_VERSION_V2
|
|
255
|
+
plan["assets"] = []
|
|
256
|
+
plan["legacy_assets_migrated_at"] = now_iso()
|
|
257
|
+
_bind_migrated_assets_to_matching_tasks(plan=plan, surface=surface, migrated=migrated)
|
|
258
|
+
from ..forge import save_plan
|
|
259
|
+
|
|
260
|
+
save_plan(run_paths, plan)
|
|
261
|
+
return LegacyAssetMigrationResult(
|
|
262
|
+
schema_version_before=schema_before,
|
|
263
|
+
schema_version_after=PLAN_SCHEMA_VERSION_V2,
|
|
264
|
+
migrated_assets=migrated,
|
|
265
|
+
skipped_existing=skipped,
|
|
266
|
+
failed=[],
|
|
267
|
+
plan_assets_array_cleared=True,
|
|
268
|
+
plan_v2_written=True,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _bind_migrated_assets_to_matching_tasks(
|
|
273
|
+
*,
|
|
274
|
+
plan: dict[str, Any],
|
|
275
|
+
surface: AssetSurface,
|
|
276
|
+
migrated: list[MigratedLegacyAsset],
|
|
277
|
+
) -> None:
|
|
278
|
+
if not migrated:
|
|
279
|
+
return
|
|
280
|
+
from .plan_binding import bind_asset_to_matching_tasks
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
active_records = surface.index.records(include_deleted=False)
|
|
284
|
+
except AssetError:
|
|
285
|
+
return
|
|
286
|
+
by_id = {record.id: record for record in active_records}
|
|
287
|
+
for item in migrated:
|
|
288
|
+
record = by_id.get(item.new_asset_id)
|
|
289
|
+
if record is None:
|
|
290
|
+
continue
|
|
291
|
+
bind_asset_to_matching_tasks(
|
|
292
|
+
plan=plan,
|
|
293
|
+
record=record,
|
|
294
|
+
active_records=active_records,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _read_plan(path: Path) -> dict[str, Any]:
|
|
299
|
+
try:
|
|
300
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
301
|
+
except OSError as exc:
|
|
302
|
+
raise AssetError(f"Failed to read plan for legacy migration: {path}") from exc
|
|
303
|
+
except json.JSONDecodeError as exc:
|
|
304
|
+
raise AssetError(f"Invalid plan JSON for legacy migration: {path}") from exc
|
|
305
|
+
if not isinstance(payload, dict):
|
|
306
|
+
raise AssetError(f"Invalid plan structure for legacy migration: {path}")
|
|
307
|
+
return payload
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _schema_version(plan: dict[str, Any]) -> int:
|
|
311
|
+
try:
|
|
312
|
+
return int(plan.get("schema_version", PLAN_SCHEMA_VERSION_V1) or PLAN_SCHEMA_VERSION_V1)
|
|
313
|
+
except (TypeError, ValueError):
|
|
314
|
+
return PLAN_SCHEMA_VERSION_V1
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _derive_title(source: Path) -> str:
|
|
318
|
+
stem = source.stem.replace("_", " ").replace("-", " ").strip().casefold()
|
|
319
|
+
stem = " ".join(stem.split())
|
|
320
|
+
if not stem:
|
|
321
|
+
return "Legacy asset"
|
|
322
|
+
return stem[:1].upper() + stem[1:]
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _deduped_title(title: str, counts: dict[str, int]) -> str:
|
|
326
|
+
count = counts.get(title, 0) + 1
|
|
327
|
+
counts[title] = count
|
|
328
|
+
return title if count == 1 else f"{title} {count}"
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _optional_string(value: Any) -> str | None:
|
|
332
|
+
if value is None:
|
|
333
|
+
return None
|
|
334
|
+
text = str(value).strip()
|
|
335
|
+
return text or None
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _safe_int(value: Any) -> int | None:
|
|
339
|
+
try:
|
|
340
|
+
return int(value)
|
|
341
|
+
except (TypeError, ValueError):
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _safe_float(value: Any) -> float | None:
|
|
346
|
+
try:
|
|
347
|
+
return float(value)
|
|
348
|
+
except (TypeError, ValueError):
|
|
349
|
+
return None
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _process_is_running(pid: int) -> bool | None:
|
|
353
|
+
try:
|
|
354
|
+
if pid <= 0:
|
|
355
|
+
return False
|
|
356
|
+
except TypeError:
|
|
357
|
+
return None
|
|
358
|
+
if os.name == "nt":
|
|
359
|
+
return _windows_process_is_running(pid)
|
|
360
|
+
try:
|
|
361
|
+
os.kill(pid, 0)
|
|
362
|
+
except ProcessLookupError:
|
|
363
|
+
return False
|
|
364
|
+
except PermissionError:
|
|
365
|
+
return True
|
|
366
|
+
except OSError as exc:
|
|
367
|
+
if getattr(exc, "errno", None) == errno.ESRCH:
|
|
368
|
+
return False
|
|
369
|
+
return None
|
|
370
|
+
return True
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _windows_process_is_running(pid: int) -> bool | None:
|
|
374
|
+
try:
|
|
375
|
+
import ctypes
|
|
376
|
+
from ctypes import wintypes
|
|
377
|
+
|
|
378
|
+
process_query_limited_information = 0x1000
|
|
379
|
+
still_active = 259
|
|
380
|
+
error_invalid_parameter = 87
|
|
381
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
382
|
+
kernel32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
|
|
383
|
+
kernel32.OpenProcess.restype = wintypes.HANDLE
|
|
384
|
+
kernel32.GetExitCodeProcess.argtypes = [
|
|
385
|
+
wintypes.HANDLE,
|
|
386
|
+
ctypes.POINTER(wintypes.DWORD),
|
|
387
|
+
]
|
|
388
|
+
kernel32.GetExitCodeProcess.restype = wintypes.BOOL
|
|
389
|
+
kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
|
|
390
|
+
kernel32.CloseHandle.restype = wintypes.BOOL
|
|
391
|
+
|
|
392
|
+
handle = kernel32.OpenProcess(process_query_limited_information, False, pid)
|
|
393
|
+
if not handle:
|
|
394
|
+
error = ctypes.get_last_error()
|
|
395
|
+
if error == error_invalid_parameter:
|
|
396
|
+
return False
|
|
397
|
+
return None
|
|
398
|
+
exit_code = wintypes.DWORD()
|
|
399
|
+
try:
|
|
400
|
+
if not kernel32.GetExitCodeProcess(handle, ctypes.byref(exit_code)):
|
|
401
|
+
return None
|
|
402
|
+
return exit_code.value == still_active
|
|
403
|
+
finally:
|
|
404
|
+
kernel32.CloseHandle(handle)
|
|
405
|
+
except Exception:
|
|
406
|
+
return None
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _ensure_under_root(root: Path, path: Path) -> None:
|
|
410
|
+
try:
|
|
411
|
+
path.resolve().relative_to(root.resolve())
|
|
412
|
+
except ValueError as exc:
|
|
413
|
+
raise AssetError(f"Legacy asset path escapes workspace root: {path}") from exc
|