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,904 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from fnmatch import fnmatchcase
|
|
6
|
+
from pathlib import PurePosixPath
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
from .file_classification import is_code_implementation_path, is_test_path
|
|
10
|
+
from .task_scope import (
|
|
11
|
+
extract_forbidden_repo_path_hint_records,
|
|
12
|
+
normalize_repo_path_entry,
|
|
13
|
+
split_normalized_repo_path_list,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
PLANNING_CONSTRAINTS_KEY = "planning_constraints"
|
|
17
|
+
PLANNING_CONSTRAINTS_SCHEMA_VERSION = 1
|
|
18
|
+
|
|
19
|
+
PlanningConstraintKind = Literal[
|
|
20
|
+
"target_root",
|
|
21
|
+
"forbidden_root",
|
|
22
|
+
"decoy_root",
|
|
23
|
+
"unrelated_root",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
_PATHLIKE_RE = re.compile(
|
|
27
|
+
r"(?<![\w/-])("
|
|
28
|
+
r"(?:[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)+)"
|
|
29
|
+
r"|(?:[A-Za-z0-9_.-]+\.[A-Za-z0-9_*?\[\]-]+)"
|
|
30
|
+
r"|(?:README(?:\.md)?)"
|
|
31
|
+
r"|(?:Dockerfile|Makefile|LICENSE|NOTICE|CHANGELOG)"
|
|
32
|
+
r")(?![\w/-])"
|
|
33
|
+
)
|
|
34
|
+
_CONTEXT_SPLIT_RE = re.compile(r"[\n;]+|(?<=[.!?])\s+")
|
|
35
|
+
_MONOREPO_CONTAINER_NAMES = frozenset(
|
|
36
|
+
{
|
|
37
|
+
"app",
|
|
38
|
+
"apps",
|
|
39
|
+
"component",
|
|
40
|
+
"components",
|
|
41
|
+
"crate",
|
|
42
|
+
"crates",
|
|
43
|
+
"lib",
|
|
44
|
+
"libs",
|
|
45
|
+
"module",
|
|
46
|
+
"modules",
|
|
47
|
+
"package",
|
|
48
|
+
"packages",
|
|
49
|
+
"service",
|
|
50
|
+
"services",
|
|
51
|
+
"workspace",
|
|
52
|
+
"workspaces",
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
_TARGET_DIRECT_RE = re.compile(
|
|
56
|
+
r"\b(?:only|only\s+(?:edit|fix|modify|touch|change|update)|"
|
|
57
|
+
r"stay\s+(?:scoped\s+)?(?:to|inside|within)|scope(?:d)?\s+(?:to|inside|within)|"
|
|
58
|
+
r"target(?:ing)?|target\s+root|switch\s+to|focus\s+on|work\s+on|"
|
|
59
|
+
r"inside|under|within|in|for)\b",
|
|
60
|
+
re.IGNORECASE,
|
|
61
|
+
)
|
|
62
|
+
_TARGET_INTENT_RE = re.compile(
|
|
63
|
+
r"\b(?:add|adding|build|building|change|changing|configure|configuring|edit|editing|"
|
|
64
|
+
r"enable|enabling|fix|fixing|implement|implementing|improve|improving|modify|"
|
|
65
|
+
r"modifying|patch|patching|repair|repairing|support|supporting|update|updating|"
|
|
66
|
+
r"wire|wiring|work\s+on|working\s+on)\b",
|
|
67
|
+
re.IGNORECASE,
|
|
68
|
+
)
|
|
69
|
+
_FORBIDDEN_DIRECT_RE = re.compile(
|
|
70
|
+
r"\b(?:do\s+not|don't|dont|must\s+not|never)\s+"
|
|
71
|
+
r"(?:edit|modify|change|touch|write(?:\s+to)?|update|delete|remove|create)\b"
|
|
72
|
+
r"|\b(?:leave|keep)\s+(?:the\s+)?(?:file\s+)?(?:[\w./-]+\s+)?"
|
|
73
|
+
r"(?:unchanged|untouched|unmodified)\b"
|
|
74
|
+
r"|\b(?:remain|stays?|stay|left)\s+(?:unchanged|untouched|unmodified)\b",
|
|
75
|
+
re.IGNORECASE,
|
|
76
|
+
)
|
|
77
|
+
_FORBIDDEN_AFTER_RE = re.compile(
|
|
78
|
+
r"^\s*(?:file\s+)?(?:is|are|must\s+remain|should\s+remain|stays?|stay|left)?\s*"
|
|
79
|
+
r"(?:not\s+)?(?:untouched|unchanged|unmodified|preserved|not\s+modified|"
|
|
80
|
+
r"not\s+changed|not\s+touched)\b"
|
|
81
|
+
r"|^\s*(?:file\s+)?not\s+(?:modified|changed|touched|written)\b",
|
|
82
|
+
re.IGNORECASE,
|
|
83
|
+
)
|
|
84
|
+
_DECOY_RE = re.compile(
|
|
85
|
+
r"\b(?:decoy|negative\s+example|example\s+only|not\s+an?\s+(?:target|task))\b",
|
|
86
|
+
re.IGNORECASE,
|
|
87
|
+
)
|
|
88
|
+
_UNRELATED_RE = re.compile(
|
|
89
|
+
r"\b(?:unrelated|out\s+of\s+scope|not\s+relevant|irrelevant)\b",
|
|
90
|
+
re.IGNORECASE,
|
|
91
|
+
)
|
|
92
|
+
_IGNORE_RE = re.compile(r"\b(?:ignore|skip|exclude)\b", re.IGNORECASE)
|
|
93
|
+
_ONLY_RE = re.compile(r"\bonly\b", re.IGNORECASE)
|
|
94
|
+
_ADDITIVE_TARGET_RE = re.compile(
|
|
95
|
+
r"\b(?:also|additionally|as\s+well|too|include)\b",
|
|
96
|
+
re.IGNORECASE,
|
|
97
|
+
)
|
|
98
|
+
_EXCLUSIVE_TARGET_RE = re.compile(
|
|
99
|
+
r"\b(?:only|stay\s+(?:scoped\s+)?(?:to|inside|within)|"
|
|
100
|
+
r"scope(?:d)?\s+(?:to|inside|within)|target\s+root)\b",
|
|
101
|
+
re.IGNORECASE,
|
|
102
|
+
)
|
|
103
|
+
_RETARGET_RE = re.compile(
|
|
104
|
+
r"\b(?:actually|instead|now|switch\s+to|retarget(?:\s+to)?|"
|
|
105
|
+
r"change\s+(?:the\s+)?(?:target|scope|focus)\s+to|"
|
|
106
|
+
r"move\s+(?:the\s+)?(?:target|scope|focus)\s+to)\b",
|
|
107
|
+
re.IGNORECASE,
|
|
108
|
+
)
|
|
109
|
+
_MARKER_WINDOW_CHARS = 80
|
|
110
|
+
_SHARED_EVIDENCE_RE = re.compile(
|
|
111
|
+
r"\b(?:shared|common|cross[- ]?package|cross[- ]?service|dependency|depends\s+on|"
|
|
112
|
+
r"library|lib|used\s+by|imported\s+by)\b",
|
|
113
|
+
re.IGNORECASE,
|
|
114
|
+
)
|
|
115
|
+
_TEST_EVIDENCE_RE = re.compile(r"\b(?:test|tests|spec|coverage|regression)\b", re.IGNORECASE)
|
|
116
|
+
_NON_TARGET_SURFACE_RE = re.compile(
|
|
117
|
+
r"^(?:README(?:\.md)?|docs?/|notes?/|changelog(?:\.md)?|LICENSE|NOTICE)",
|
|
118
|
+
re.IGNORECASE,
|
|
119
|
+
)
|
|
120
|
+
_GLOB_CHARS = ("*", "?", "[")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True)
|
|
124
|
+
class PlanningPathConstraint:
|
|
125
|
+
path: str
|
|
126
|
+
kind: PlanningConstraintKind
|
|
127
|
+
reason_code: str
|
|
128
|
+
evidence: str
|
|
129
|
+
|
|
130
|
+
def to_payload(self) -> dict[str, str]:
|
|
131
|
+
return {
|
|
132
|
+
"path": self.path,
|
|
133
|
+
"kind": self.kind,
|
|
134
|
+
"reason_code": self.reason_code,
|
|
135
|
+
"evidence": self.evidence,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@dataclass(frozen=True)
|
|
140
|
+
class PlanningScopeConstraints:
|
|
141
|
+
target_roots: tuple[PlanningPathConstraint, ...] = ()
|
|
142
|
+
forbidden_roots: tuple[PlanningPathConstraint, ...] = ()
|
|
143
|
+
decoy_roots: tuple[PlanningPathConstraint, ...] = ()
|
|
144
|
+
unrelated_roots: tuple[PlanningPathConstraint, ...] = ()
|
|
145
|
+
|
|
146
|
+
@property
|
|
147
|
+
def has_constraints(self) -> bool:
|
|
148
|
+
return bool(
|
|
149
|
+
self.target_roots or self.forbidden_roots or self.decoy_roots or self.unrelated_roots
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def blocked_roots(self) -> tuple[PlanningPathConstraint, ...]:
|
|
154
|
+
return (*self.forbidden_roots, *self.decoy_roots, *self.unrelated_roots)
|
|
155
|
+
|
|
156
|
+
def to_payload(self) -> dict[str, Any]:
|
|
157
|
+
return {
|
|
158
|
+
"schema_version": PLANNING_CONSTRAINTS_SCHEMA_VERSION,
|
|
159
|
+
"target_roots": [item.to_payload() for item in self.target_roots],
|
|
160
|
+
"forbidden_roots": [item.to_payload() for item in self.forbidden_roots],
|
|
161
|
+
"decoy_roots": [item.to_payload() for item in self.decoy_roots],
|
|
162
|
+
"unrelated_roots": [item.to_payload() for item in self.unrelated_roots],
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass(frozen=True)
|
|
167
|
+
class PlanningScopeViolation:
|
|
168
|
+
path: str
|
|
169
|
+
classification: str
|
|
170
|
+
reason_code: str
|
|
171
|
+
evidence: str
|
|
172
|
+
constraint_path: str | None = None
|
|
173
|
+
|
|
174
|
+
def to_payload(self) -> dict[str, str]:
|
|
175
|
+
payload = {
|
|
176
|
+
"path": self.path,
|
|
177
|
+
"classification": self.classification,
|
|
178
|
+
"reason_code": self.reason_code,
|
|
179
|
+
"evidence": self.evidence,
|
|
180
|
+
}
|
|
181
|
+
if self.constraint_path:
|
|
182
|
+
payload["constraint_path"] = self.constraint_path
|
|
183
|
+
return payload
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _dedupe_constraints(
|
|
187
|
+
constraints: list[PlanningPathConstraint],
|
|
188
|
+
) -> tuple[PlanningPathConstraint, ...]:
|
|
189
|
+
seen: set[tuple[str, str]] = set()
|
|
190
|
+
out: list[PlanningPathConstraint] = []
|
|
191
|
+
for item in constraints:
|
|
192
|
+
key = (item.kind, _constraint_identity(item.path))
|
|
193
|
+
if key in seen:
|
|
194
|
+
continue
|
|
195
|
+
seen.add(key)
|
|
196
|
+
out.append(item)
|
|
197
|
+
return tuple(out)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _constraint_identity(path: str) -> str:
|
|
201
|
+
return _constraint_root(path).casefold()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _constraint_root(path: str) -> str:
|
|
205
|
+
cleaned = str(path or "").strip().replace("\\", "/")
|
|
206
|
+
while cleaned.startswith("./"):
|
|
207
|
+
cleaned = cleaned[2:]
|
|
208
|
+
cleaned = cleaned.rstrip("/")
|
|
209
|
+
if cleaned.endswith("/**"):
|
|
210
|
+
cleaned = cleaned[:-3].rstrip("/")
|
|
211
|
+
if "/**/" in cleaned:
|
|
212
|
+
cleaned = cleaned.split("/**/", 1)[0].rstrip("/")
|
|
213
|
+
return cleaned
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _normalize_constraint_path(value: str) -> str | None:
|
|
217
|
+
normalized = normalize_repo_path_entry(str(value or ""), allow_extensionless_file=True)
|
|
218
|
+
if not normalized:
|
|
219
|
+
return None
|
|
220
|
+
return _constraint_root(normalized)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _looks_like_file_path(path: str) -> bool:
|
|
224
|
+
name = PurePosixPath(_constraint_root(path)).name
|
|
225
|
+
return "." in name or name in {
|
|
226
|
+
"README",
|
|
227
|
+
"Dockerfile",
|
|
228
|
+
"Makefile",
|
|
229
|
+
"LICENSE",
|
|
230
|
+
"NOTICE",
|
|
231
|
+
"CHANGELOG",
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _evidence_fragment(context: str) -> str:
|
|
236
|
+
cleaned = " ".join(str(context or "").strip().split())
|
|
237
|
+
if len(cleaned) <= 180:
|
|
238
|
+
return cleaned
|
|
239
|
+
return cleaned[:177].rstrip() + "..."
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _path_mentions(text: str) -> list[tuple[str, str]]:
|
|
243
|
+
mentions: list[tuple[str, str]] = []
|
|
244
|
+
seen: set[str] = set()
|
|
245
|
+
contexts = [item.strip() for item in _CONTEXT_SPLIT_RE.split(text or "") if item.strip()]
|
|
246
|
+
for context in contexts:
|
|
247
|
+
for match in _PATHLIKE_RE.finditer(context):
|
|
248
|
+
normalized = _normalize_constraint_path(match.group(1))
|
|
249
|
+
if not normalized:
|
|
250
|
+
continue
|
|
251
|
+
key = (context.casefold(), normalized.casefold())
|
|
252
|
+
if str(key) in seen:
|
|
253
|
+
continue
|
|
254
|
+
seen.add(str(key))
|
|
255
|
+
mentions.append((normalized, context))
|
|
256
|
+
return mentions
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _path_context_windows(context: str, path: str) -> tuple[str, str, str]:
|
|
260
|
+
context_text = str(context or "")
|
|
261
|
+
context_cf = context_text.casefold()
|
|
262
|
+
path_cf = str(path or "").casefold()
|
|
263
|
+
start = context_cf.find(path_cf)
|
|
264
|
+
if start < 0:
|
|
265
|
+
return context_text, "", context_text
|
|
266
|
+
end = start + len(path_cf)
|
|
267
|
+
before = context_text[max(0, start - _MARKER_WINDOW_CHARS) : start]
|
|
268
|
+
after = context_text[end : end + _MARKER_WINDOW_CHARS]
|
|
269
|
+
return before + context_text[start:end] + after, before, after
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _negative_marker_applies_before_path(
|
|
273
|
+
*,
|
|
274
|
+
before: str,
|
|
275
|
+
marker_re: re.Pattern[str],
|
|
276
|
+
) -> bool:
|
|
277
|
+
matches = list(marker_re.finditer(before))
|
|
278
|
+
if not matches:
|
|
279
|
+
return False
|
|
280
|
+
last = matches[-1]
|
|
281
|
+
between = before[last.end() :]
|
|
282
|
+
return _TARGET_INTENT_RE.search(between) is None
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _context_reason_for_path(context: str, path: str) -> tuple[PlanningConstraintKind, str] | None:
|
|
286
|
+
local, before, after = _path_context_windows(context, path)
|
|
287
|
+
if _FORBIDDEN_AFTER_RE.search(after) or _negative_marker_applies_before_path(
|
|
288
|
+
before=before,
|
|
289
|
+
marker_re=_FORBIDDEN_DIRECT_RE,
|
|
290
|
+
):
|
|
291
|
+
return "forbidden_root", "direct_path_forbidden_instruction"
|
|
292
|
+
if _DECOY_RE.search(after) or _negative_marker_applies_before_path(
|
|
293
|
+
before=before,
|
|
294
|
+
marker_re=_DECOY_RE,
|
|
295
|
+
):
|
|
296
|
+
return "decoy_root", "decoy_path_constraint"
|
|
297
|
+
if _UNRELATED_RE.search(after) or _negative_marker_applies_before_path(
|
|
298
|
+
before=before,
|
|
299
|
+
marker_re=_UNRELATED_RE,
|
|
300
|
+
):
|
|
301
|
+
return "unrelated_root", "unrelated_path_constraint"
|
|
302
|
+
if _IGNORE_RE.search(after) or _negative_marker_applies_before_path(
|
|
303
|
+
before=before,
|
|
304
|
+
marker_re=_IGNORE_RE,
|
|
305
|
+
):
|
|
306
|
+
return "decoy_root", "ignored_path_constraint"
|
|
307
|
+
if not _looks_like_file_path(path) and (
|
|
308
|
+
_TARGET_DIRECT_RE.search(local) or _TARGET_INTENT_RE.search(local)
|
|
309
|
+
):
|
|
310
|
+
return "target_root", "user_target_root"
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _append_constraint(
|
|
315
|
+
items: list[PlanningPathConstraint],
|
|
316
|
+
*,
|
|
317
|
+
path: str,
|
|
318
|
+
kind: PlanningConstraintKind,
|
|
319
|
+
reason_code: str,
|
|
320
|
+
evidence: str,
|
|
321
|
+
) -> None:
|
|
322
|
+
normalized = _normalize_constraint_path(path)
|
|
323
|
+
if normalized is None:
|
|
324
|
+
return
|
|
325
|
+
items.append(
|
|
326
|
+
PlanningPathConstraint(
|
|
327
|
+
path=normalized,
|
|
328
|
+
kind=kind,
|
|
329
|
+
reason_code=reason_code,
|
|
330
|
+
evidence=_evidence_fragment(evidence),
|
|
331
|
+
)
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _workspace_candidate_roots(workspace_context: dict[str, Any] | None) -> tuple[str, ...]:
|
|
336
|
+
if not isinstance(workspace_context, dict):
|
|
337
|
+
return ()
|
|
338
|
+
roots: list[str] = []
|
|
339
|
+
for entry in workspace_context.get("manifests") or []:
|
|
340
|
+
if not isinstance(entry, dict):
|
|
341
|
+
continue
|
|
342
|
+
path = _normalize_constraint_path(str(entry.get("path") or ""))
|
|
343
|
+
if not path or "/" not in path:
|
|
344
|
+
continue
|
|
345
|
+
parent = path.rsplit("/", 1)[0]
|
|
346
|
+
if parent and parent != ".":
|
|
347
|
+
roots.append(parent)
|
|
348
|
+
for path in workspace_context.get("observed_paths") or []:
|
|
349
|
+
normalized = _normalize_constraint_path(str(path or ""))
|
|
350
|
+
if not normalized or "/" not in normalized:
|
|
351
|
+
continue
|
|
352
|
+
parts = [part for part in normalized.split("/") if part]
|
|
353
|
+
if len(parts) >= 2 and parts[0].casefold() in _MONOREPO_CONTAINER_NAMES:
|
|
354
|
+
roots.append("/".join(parts[:2]))
|
|
355
|
+
focus_relpath = _normalize_constraint_path(str(workspace_context.get("focus_relpath") or ""))
|
|
356
|
+
if focus_relpath and focus_relpath != "." and "/" in focus_relpath:
|
|
357
|
+
roots.append(focus_relpath)
|
|
358
|
+
|
|
359
|
+
seen: set[str] = set()
|
|
360
|
+
out: list[str] = []
|
|
361
|
+
for root in roots:
|
|
362
|
+
key = root.casefold()
|
|
363
|
+
if key in seen:
|
|
364
|
+
continue
|
|
365
|
+
seen.add(key)
|
|
366
|
+
out.append(root)
|
|
367
|
+
return tuple(out)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _workspace_known_scope_paths(workspace_context: dict[str, Any] | None) -> tuple[str, ...]:
|
|
371
|
+
if not isinstance(workspace_context, dict):
|
|
372
|
+
return ()
|
|
373
|
+
raw_paths: list[str] = []
|
|
374
|
+
raw_paths.extend(str(item or "") for item in workspace_context.get("observed_paths") or [])
|
|
375
|
+
raw_paths.extend(str(item or "") for item in workspace_context.get("readme_paths") or [])
|
|
376
|
+
conventions_path = str(workspace_context.get("conventions_path") or "").strip()
|
|
377
|
+
if conventions_path:
|
|
378
|
+
raw_paths.append(conventions_path)
|
|
379
|
+
for entry in workspace_context.get("manifests") or []:
|
|
380
|
+
if isinstance(entry, dict):
|
|
381
|
+
raw_paths.append(str(entry.get("path") or ""))
|
|
382
|
+
for entry in workspace_context.get("top_level_entries") or []:
|
|
383
|
+
if isinstance(entry, dict):
|
|
384
|
+
raw_paths.append(str(entry.get("path") or ""))
|
|
385
|
+
|
|
386
|
+
known: list[str] = []
|
|
387
|
+
seen: set[str] = set()
|
|
388
|
+
for raw in raw_paths:
|
|
389
|
+
normalized = _normalize_constraint_path(raw)
|
|
390
|
+
if not normalized:
|
|
391
|
+
continue
|
|
392
|
+
parts = [part for part in normalized.split("/") if part]
|
|
393
|
+
candidates = [normalized]
|
|
394
|
+
if len(parts) > 1:
|
|
395
|
+
candidates.extend("/".join(parts[:index]) for index in range(1, len(parts)))
|
|
396
|
+
for candidate in candidates:
|
|
397
|
+
key = candidate.casefold()
|
|
398
|
+
if key in seen:
|
|
399
|
+
continue
|
|
400
|
+
seen.add(key)
|
|
401
|
+
known.append(candidate)
|
|
402
|
+
return tuple(known)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _path_is_grounded_in_workspace_context(
|
|
406
|
+
path: str,
|
|
407
|
+
*,
|
|
408
|
+
workspace_context: dict[str, Any] | None,
|
|
409
|
+
) -> bool:
|
|
410
|
+
if not isinstance(workspace_context, dict):
|
|
411
|
+
return True
|
|
412
|
+
normalized = _normalize_constraint_path(path)
|
|
413
|
+
if not normalized:
|
|
414
|
+
return False
|
|
415
|
+
path_key = _constraint_root(normalized).casefold()
|
|
416
|
+
if not path_key:
|
|
417
|
+
return False
|
|
418
|
+
for candidate in (
|
|
419
|
+
*_workspace_candidate_roots(workspace_context),
|
|
420
|
+
*_workspace_known_scope_paths(workspace_context),
|
|
421
|
+
):
|
|
422
|
+
candidate_key = _constraint_root(candidate).casefold()
|
|
423
|
+
if not candidate_key:
|
|
424
|
+
continue
|
|
425
|
+
if (
|
|
426
|
+
path_key == candidate_key
|
|
427
|
+
or candidate_key.startswith(path_key + "/")
|
|
428
|
+
or ("/" in candidate_key and path_key.startswith(candidate_key + "/"))
|
|
429
|
+
):
|
|
430
|
+
return True
|
|
431
|
+
return False
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _candidate_roots_by_leaf(workspace_context: dict[str, Any] | None) -> dict[str, str]:
|
|
435
|
+
grouped: dict[str, list[str]] = {}
|
|
436
|
+
for root in _workspace_candidate_roots(workspace_context):
|
|
437
|
+
leaf = root.rsplit("/", 1)[-1].casefold()
|
|
438
|
+
if not leaf:
|
|
439
|
+
continue
|
|
440
|
+
grouped.setdefault(leaf, []).append(root)
|
|
441
|
+
return {
|
|
442
|
+
leaf: roots[0]
|
|
443
|
+
for leaf, roots in grouped.items()
|
|
444
|
+
if len({root.casefold() for root in roots}) == 1
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _word_present(text: str, word: str) -> bool:
|
|
449
|
+
return re.search(rf"(?<![A-Za-z0-9_]){re.escape(word)}(?![A-Za-z0-9_])", text, re.I) is not None
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _word_context_windows(context: str, word: str) -> tuple[str, str, str] | None:
|
|
453
|
+
match = re.search(rf"(?<![A-Za-z0-9_]){re.escape(word)}(?![A-Za-z0-9_])", context, re.I)
|
|
454
|
+
if match is None:
|
|
455
|
+
return None
|
|
456
|
+
start, end = match.span()
|
|
457
|
+
before = context[max(0, start - _MARKER_WINDOW_CHARS) : start]
|
|
458
|
+
after = context[end : end + _MARKER_WINDOW_CHARS]
|
|
459
|
+
return before + context[start:end] + after, before, after
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _leaf_name_constraints(
|
|
463
|
+
text: str,
|
|
464
|
+
*,
|
|
465
|
+
workspace_context: dict[str, Any] | None,
|
|
466
|
+
) -> list[PlanningPathConstraint]:
|
|
467
|
+
by_leaf = _candidate_roots_by_leaf(workspace_context)
|
|
468
|
+
if not by_leaf:
|
|
469
|
+
return []
|
|
470
|
+
constraints: list[PlanningPathConstraint] = []
|
|
471
|
+
contexts = [item.strip() for item in _CONTEXT_SPLIT_RE.split(text or "") if item.strip()]
|
|
472
|
+
for context in contexts:
|
|
473
|
+
context_cf = context.casefold()
|
|
474
|
+
for leaf, root in by_leaf.items():
|
|
475
|
+
if not _word_present(context_cf, leaf):
|
|
476
|
+
continue
|
|
477
|
+
windows = _word_context_windows(context, leaf)
|
|
478
|
+
if windows is None:
|
|
479
|
+
continue
|
|
480
|
+
local, before, after = windows
|
|
481
|
+
if _FORBIDDEN_AFTER_RE.search(after) or _negative_marker_applies_before_path(
|
|
482
|
+
before=before,
|
|
483
|
+
marker_re=_FORBIDDEN_DIRECT_RE,
|
|
484
|
+
):
|
|
485
|
+
_append_constraint(
|
|
486
|
+
constraints,
|
|
487
|
+
path=root,
|
|
488
|
+
kind="forbidden_root",
|
|
489
|
+
reason_code="direct_path_forbidden_instruction",
|
|
490
|
+
evidence=context,
|
|
491
|
+
)
|
|
492
|
+
elif _DECOY_RE.search(after) or _negative_marker_applies_before_path(
|
|
493
|
+
before=before,
|
|
494
|
+
marker_re=_DECOY_RE,
|
|
495
|
+
):
|
|
496
|
+
_append_constraint(
|
|
497
|
+
constraints,
|
|
498
|
+
path=root,
|
|
499
|
+
kind="decoy_root",
|
|
500
|
+
reason_code="decoy_path_constraint",
|
|
501
|
+
evidence=context,
|
|
502
|
+
)
|
|
503
|
+
elif (
|
|
504
|
+
_UNRELATED_RE.search(after)
|
|
505
|
+
or _negative_marker_applies_before_path(before=before, marker_re=_UNRELATED_RE)
|
|
506
|
+
or _IGNORE_RE.search(after)
|
|
507
|
+
or _negative_marker_applies_before_path(before=before, marker_re=_IGNORE_RE)
|
|
508
|
+
):
|
|
509
|
+
_append_constraint(
|
|
510
|
+
constraints,
|
|
511
|
+
path=root,
|
|
512
|
+
kind="unrelated_root",
|
|
513
|
+
reason_code="unrelated_path_constraint",
|
|
514
|
+
evidence=context,
|
|
515
|
+
)
|
|
516
|
+
elif (
|
|
517
|
+
_ONLY_RE.search(local)
|
|
518
|
+
or _TARGET_DIRECT_RE.search(local)
|
|
519
|
+
or _TARGET_INTENT_RE.search(local)
|
|
520
|
+
):
|
|
521
|
+
_append_constraint(
|
|
522
|
+
constraints,
|
|
523
|
+
path=root,
|
|
524
|
+
kind="target_root",
|
|
525
|
+
reason_code="user_target_root_name",
|
|
526
|
+
evidence=context,
|
|
527
|
+
)
|
|
528
|
+
return constraints
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def extract_planning_scope_constraints(
|
|
532
|
+
text: str,
|
|
533
|
+
*,
|
|
534
|
+
workspace_context: dict[str, Any] | None = None,
|
|
535
|
+
) -> PlanningScopeConstraints:
|
|
536
|
+
raw_constraints: list[PlanningPathConstraint] = []
|
|
537
|
+
for forbidden in extract_forbidden_repo_path_hint_records(text or ""):
|
|
538
|
+
_append_constraint(
|
|
539
|
+
raw_constraints,
|
|
540
|
+
path=forbidden.path,
|
|
541
|
+
kind="forbidden_root",
|
|
542
|
+
reason_code=forbidden.reason_code,
|
|
543
|
+
evidence=forbidden.evidence,
|
|
544
|
+
)
|
|
545
|
+
for path, context in _path_mentions(text or ""):
|
|
546
|
+
reason = _context_reason_for_path(context, path)
|
|
547
|
+
if reason is None:
|
|
548
|
+
continue
|
|
549
|
+
kind, reason_code = reason
|
|
550
|
+
if kind == "target_root" and not _path_is_grounded_in_workspace_context(
|
|
551
|
+
path,
|
|
552
|
+
workspace_context=workspace_context,
|
|
553
|
+
):
|
|
554
|
+
continue
|
|
555
|
+
_append_constraint(
|
|
556
|
+
raw_constraints,
|
|
557
|
+
path=path,
|
|
558
|
+
kind=kind,
|
|
559
|
+
reason_code=reason_code,
|
|
560
|
+
evidence=context,
|
|
561
|
+
)
|
|
562
|
+
raw_constraints.extend(_leaf_name_constraints(text or "", workspace_context=workspace_context))
|
|
563
|
+
return _constraints_from_items(raw_constraints)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _constraints_from_items(items: list[PlanningPathConstraint]) -> PlanningScopeConstraints:
|
|
567
|
+
forbidden = [item for item in items if item.kind == "forbidden_root"]
|
|
568
|
+
decoy = [item for item in items if item.kind == "decoy_root"]
|
|
569
|
+
unrelated = [item for item in items if item.kind == "unrelated_root"]
|
|
570
|
+
blocked_keys = {_constraint_identity(item.path) for item in [*forbidden, *decoy, *unrelated]}
|
|
571
|
+
target = [
|
|
572
|
+
item
|
|
573
|
+
for item in items
|
|
574
|
+
if item.kind == "target_root" and _constraint_identity(item.path) not in blocked_keys
|
|
575
|
+
]
|
|
576
|
+
return PlanningScopeConstraints(
|
|
577
|
+
target_roots=_dedupe_constraints(target),
|
|
578
|
+
forbidden_roots=_dedupe_constraints(forbidden),
|
|
579
|
+
decoy_roots=_dedupe_constraints(decoy),
|
|
580
|
+
unrelated_roots=_dedupe_constraints(unrelated),
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def planning_constraints_from_payload(raw: Any) -> PlanningScopeConstraints:
|
|
585
|
+
if not isinstance(raw, dict):
|
|
586
|
+
return PlanningScopeConstraints()
|
|
587
|
+
items: list[PlanningPathConstraint] = []
|
|
588
|
+
for key, kind in (
|
|
589
|
+
("target_roots", "target_root"),
|
|
590
|
+
("forbidden_roots", "forbidden_root"),
|
|
591
|
+
("decoy_roots", "decoy_root"),
|
|
592
|
+
("unrelated_roots", "unrelated_root"),
|
|
593
|
+
):
|
|
594
|
+
for entry in raw.get(key) or []:
|
|
595
|
+
if not isinstance(entry, dict):
|
|
596
|
+
continue
|
|
597
|
+
path = _normalize_constraint_path(str(entry.get("path") or ""))
|
|
598
|
+
if not path:
|
|
599
|
+
continue
|
|
600
|
+
items.append(
|
|
601
|
+
PlanningPathConstraint(
|
|
602
|
+
path=path,
|
|
603
|
+
kind=kind, # type: ignore[arg-type]
|
|
604
|
+
reason_code=str(entry.get("reason_code") or kind).strip() or kind,
|
|
605
|
+
evidence=str(entry.get("evidence") or "").strip(),
|
|
606
|
+
)
|
|
607
|
+
)
|
|
608
|
+
return _constraints_from_items(items)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def planning_constraints_from_plan(plan: dict[str, Any]) -> PlanningScopeConstraints:
|
|
612
|
+
return planning_constraints_from_payload(plan.get(PLANNING_CONSTRAINTS_KEY))
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
def merge_planning_scope_constraints(
|
|
616
|
+
*constraints: PlanningScopeConstraints,
|
|
617
|
+
replace_target_roots: bool = False,
|
|
618
|
+
unblock_target_roots: bool = False,
|
|
619
|
+
) -> PlanningScopeConstraints:
|
|
620
|
+
items: list[PlanningPathConstraint] = []
|
|
621
|
+
latest = constraints[-1] if constraints else PlanningScopeConstraints()
|
|
622
|
+
latest_target_keys = (
|
|
623
|
+
{_constraint_identity(item.path) for item in latest.target_roots}
|
|
624
|
+
if unblock_target_roots
|
|
625
|
+
else set()
|
|
626
|
+
)
|
|
627
|
+
for index, constraint_set in enumerate(constraints):
|
|
628
|
+
is_latest = index == len(constraints) - 1
|
|
629
|
+
if not replace_target_roots or is_latest:
|
|
630
|
+
items.extend(constraint_set.target_roots)
|
|
631
|
+
for blocked in [
|
|
632
|
+
*constraint_set.forbidden_roots,
|
|
633
|
+
*constraint_set.decoy_roots,
|
|
634
|
+
*constraint_set.unrelated_roots,
|
|
635
|
+
]:
|
|
636
|
+
if (
|
|
637
|
+
unblock_target_roots
|
|
638
|
+
and not is_latest
|
|
639
|
+
and _constraint_identity(blocked.path) in latest_target_keys
|
|
640
|
+
):
|
|
641
|
+
continue
|
|
642
|
+
items.append(blocked)
|
|
643
|
+
return _constraints_from_items(items)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def _latest_text_replaces_target_roots(
|
|
647
|
+
*,
|
|
648
|
+
text: str,
|
|
649
|
+
extracted: PlanningScopeConstraints,
|
|
650
|
+
direction_change: Any | None,
|
|
651
|
+
) -> bool:
|
|
652
|
+
if not extracted.target_roots:
|
|
653
|
+
return False
|
|
654
|
+
if _ADDITIVE_TARGET_RE.search(text or ""):
|
|
655
|
+
return False
|
|
656
|
+
if direction_change is not None:
|
|
657
|
+
return True
|
|
658
|
+
return bool(_EXCLUSIVE_TARGET_RE.search(text or "") or _RETARGET_RE.search(text or ""))
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def merge_latest_planning_scope_constraints(
|
|
662
|
+
existing: PlanningScopeConstraints,
|
|
663
|
+
*,
|
|
664
|
+
text: str,
|
|
665
|
+
workspace_context: dict[str, Any] | None = None,
|
|
666
|
+
direction_change: Any | None = None,
|
|
667
|
+
) -> PlanningScopeConstraints:
|
|
668
|
+
extracted = extract_planning_scope_constraints(text, workspace_context=workspace_context)
|
|
669
|
+
return merge_planning_scope_constraints(
|
|
670
|
+
existing,
|
|
671
|
+
extracted,
|
|
672
|
+
replace_target_roots=_latest_text_replaces_target_roots(
|
|
673
|
+
text=text,
|
|
674
|
+
extracted=extracted,
|
|
675
|
+
direction_change=direction_change,
|
|
676
|
+
),
|
|
677
|
+
unblock_target_roots=bool(extracted.target_roots),
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def update_plan_planning_constraints(
|
|
682
|
+
plan: dict[str, Any],
|
|
683
|
+
*,
|
|
684
|
+
text: str,
|
|
685
|
+
workspace_context: dict[str, Any] | None = None,
|
|
686
|
+
direction_change: Any | None = None,
|
|
687
|
+
) -> tuple[PlanningScopeConstraints, bool]:
|
|
688
|
+
existing = planning_constraints_from_plan(plan)
|
|
689
|
+
merged = merge_latest_planning_scope_constraints(
|
|
690
|
+
existing,
|
|
691
|
+
text=text,
|
|
692
|
+
workspace_context=workspace_context,
|
|
693
|
+
direction_change=direction_change,
|
|
694
|
+
)
|
|
695
|
+
if not merged.has_constraints:
|
|
696
|
+
return merged, False
|
|
697
|
+
payload = merged.to_payload()
|
|
698
|
+
if plan.get(PLANNING_CONSTRAINTS_KEY) == payload:
|
|
699
|
+
return merged, False
|
|
700
|
+
plan[PLANNING_CONSTRAINTS_KEY] = payload
|
|
701
|
+
return merged, True
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def planning_constraints_prompt_payload(
|
|
705
|
+
constraints: PlanningScopeConstraints,
|
|
706
|
+
) -> dict[str, Any] | None:
|
|
707
|
+
if not constraints.has_constraints:
|
|
708
|
+
return None
|
|
709
|
+
return constraints.to_payload()
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _has_glob(path: str) -> bool:
|
|
713
|
+
return any(char in path for char in _GLOB_CHARS)
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def _glob_prefix(path: str) -> str:
|
|
717
|
+
prefix_chars: list[str] = []
|
|
718
|
+
for char in path:
|
|
719
|
+
if char in _GLOB_CHARS:
|
|
720
|
+
break
|
|
721
|
+
prefix_chars.append(char)
|
|
722
|
+
return _constraint_root("".join(prefix_chars))
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def path_matches_planning_root(path: str, root: str) -> bool:
|
|
726
|
+
normalized_path = _normalize_constraint_path(path)
|
|
727
|
+
normalized_root = _normalize_constraint_path(root)
|
|
728
|
+
if not normalized_path or not normalized_root:
|
|
729
|
+
return False
|
|
730
|
+
if _has_glob(normalized_root):
|
|
731
|
+
return fnmatchcase(normalized_path, normalized_root)
|
|
732
|
+
if _has_glob(normalized_path):
|
|
733
|
+
prefix = _glob_prefix(normalized_path)
|
|
734
|
+
return bool(prefix) and (
|
|
735
|
+
prefix.casefold() == normalized_root.casefold()
|
|
736
|
+
or prefix.casefold().startswith(normalized_root.casefold() + "/")
|
|
737
|
+
)
|
|
738
|
+
path_key = normalized_path.casefold()
|
|
739
|
+
root_key = normalized_root.casefold()
|
|
740
|
+
return path_key == root_key or path_key.startswith(root_key + "/")
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def path_within_target_roots(
|
|
744
|
+
path: str,
|
|
745
|
+
constraints: PlanningScopeConstraints,
|
|
746
|
+
) -> bool:
|
|
747
|
+
if not constraints.target_roots:
|
|
748
|
+
return True
|
|
749
|
+
return any(path_matches_planning_root(path, item.path) for item in constraints.target_roots)
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def _path_text_parts(path: str) -> set[str]:
|
|
753
|
+
cleaned = _constraint_root(path).replace("\\", "/").casefold()
|
|
754
|
+
parts: set[str] = set()
|
|
755
|
+
for part in PurePosixPath(cleaned).parts:
|
|
756
|
+
if not part or part == ".":
|
|
757
|
+
continue
|
|
758
|
+
stem = part.split(".", 1)[0]
|
|
759
|
+
parts.add(part)
|
|
760
|
+
if stem:
|
|
761
|
+
parts.add(stem)
|
|
762
|
+
return parts
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
def _target_labels(constraints: PlanningScopeConstraints) -> set[str]:
|
|
766
|
+
labels: set[str] = set()
|
|
767
|
+
for item in constraints.target_roots:
|
|
768
|
+
labels.update(_path_text_parts(item.path))
|
|
769
|
+
return labels
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
def _task_text(task: dict[str, Any]) -> str:
|
|
773
|
+
acceptance = task.get("acceptance_criteria") or []
|
|
774
|
+
acceptance_items = acceptance if isinstance(acceptance, list) else []
|
|
775
|
+
return "\n".join(
|
|
776
|
+
[
|
|
777
|
+
str(task.get("title") or ""),
|
|
778
|
+
str(task.get("description") or ""),
|
|
779
|
+
*(str(item or "") for item in acceptance_items),
|
|
780
|
+
]
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def _scope_entry_requires_target(path: str) -> bool:
|
|
785
|
+
normalized = _constraint_root(path)
|
|
786
|
+
if not normalized:
|
|
787
|
+
return False
|
|
788
|
+
if _NON_TARGET_SURFACE_RE.match(normalized):
|
|
789
|
+
return False
|
|
790
|
+
if is_code_implementation_path(normalized) or is_test_path(normalized):
|
|
791
|
+
return True
|
|
792
|
+
pure = PurePosixPath(normalized)
|
|
793
|
+
name = pure.name.casefold()
|
|
794
|
+
if name in {"package.json", "pyproject.toml", "cargo.toml", "go.mod", "pom.xml"}:
|
|
795
|
+
return True
|
|
796
|
+
if _has_glob(normalized):
|
|
797
|
+
return not any(part.casefold() in {"docs", "doc", "notes"} for part in pure.parts)
|
|
798
|
+
if "/" in normalized and "." not in pure.name:
|
|
799
|
+
return True
|
|
800
|
+
return False
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
def _has_explicit_target_evidence(
|
|
804
|
+
*,
|
|
805
|
+
path: str,
|
|
806
|
+
task_text: str,
|
|
807
|
+
constraints: PlanningScopeConstraints,
|
|
808
|
+
) -> bool:
|
|
809
|
+
text = str(task_text or "")
|
|
810
|
+
text_cf = text.casefold()
|
|
811
|
+
labels = _target_labels(constraints)
|
|
812
|
+
path_parts = _path_text_parts(path)
|
|
813
|
+
label_match = bool(labels & path_parts) or any(label and label in text_cf for label in labels)
|
|
814
|
+
if is_test_path(path):
|
|
815
|
+
return label_match and _TEST_EVIDENCE_RE.search(text) is not None
|
|
816
|
+
if is_code_implementation_path(path) or "/" in _constraint_root(path):
|
|
817
|
+
return label_match and _SHARED_EVIDENCE_RE.search(text) is not None
|
|
818
|
+
return label_match
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
def task_scope_constraint_violations(
|
|
822
|
+
task: dict[str, Any],
|
|
823
|
+
constraints: PlanningScopeConstraints,
|
|
824
|
+
) -> list[PlanningScopeViolation]:
|
|
825
|
+
if not constraints.has_constraints:
|
|
826
|
+
return []
|
|
827
|
+
estimated_files, _dropped_estimated = split_normalized_repo_path_list(
|
|
828
|
+
task.get("estimated_files")
|
|
829
|
+
)
|
|
830
|
+
write_scope, _dropped_write_scope = split_normalized_repo_path_list(task.get("write_scope"))
|
|
831
|
+
paths = list(dict.fromkeys([*estimated_files, *write_scope]))
|
|
832
|
+
if not paths:
|
|
833
|
+
return []
|
|
834
|
+
text = _task_text(task)
|
|
835
|
+
violations: list[PlanningScopeViolation] = []
|
|
836
|
+
for path in paths:
|
|
837
|
+
for blocked in constraints.blocked_roots:
|
|
838
|
+
if not path_matches_planning_root(path, blocked.path):
|
|
839
|
+
continue
|
|
840
|
+
violations.append(
|
|
841
|
+
PlanningScopeViolation(
|
|
842
|
+
path=path,
|
|
843
|
+
classification=blocked.kind,
|
|
844
|
+
reason_code=blocked.reason_code,
|
|
845
|
+
evidence=blocked.evidence,
|
|
846
|
+
constraint_path=blocked.path,
|
|
847
|
+
)
|
|
848
|
+
)
|
|
849
|
+
break
|
|
850
|
+
else:
|
|
851
|
+
if (
|
|
852
|
+
constraints.target_roots
|
|
853
|
+
and _scope_entry_requires_target(path)
|
|
854
|
+
and not path_within_target_roots(path, constraints)
|
|
855
|
+
and not _has_explicit_target_evidence(
|
|
856
|
+
path=path,
|
|
857
|
+
task_text=text,
|
|
858
|
+
constraints=constraints,
|
|
859
|
+
)
|
|
860
|
+
):
|
|
861
|
+
violations.append(
|
|
862
|
+
PlanningScopeViolation(
|
|
863
|
+
path=path,
|
|
864
|
+
classification="outside_target_root",
|
|
865
|
+
reason_code="invalid_out_of_scope",
|
|
866
|
+
evidence=(
|
|
867
|
+
"target roots: "
|
|
868
|
+
+ ", ".join(item.path for item in constraints.target_roots)
|
|
869
|
+
),
|
|
870
|
+
)
|
|
871
|
+
)
|
|
872
|
+
return violations
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def task_has_target_root_scope(
|
|
876
|
+
task: dict[str, Any],
|
|
877
|
+
constraints: PlanningScopeConstraints,
|
|
878
|
+
) -> bool:
|
|
879
|
+
if not constraints.target_roots:
|
|
880
|
+
return False
|
|
881
|
+
estimated_files, _dropped_estimated = split_normalized_repo_path_list(
|
|
882
|
+
task.get("estimated_files")
|
|
883
|
+
)
|
|
884
|
+
write_scope, _dropped_write_scope = split_normalized_repo_path_list(task.get("write_scope"))
|
|
885
|
+
return any(
|
|
886
|
+
path_within_target_roots(path, constraints) for path in [*estimated_files, *write_scope]
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def filter_scope_entries_for_planning_constraints(
|
|
891
|
+
paths: list[str],
|
|
892
|
+
*,
|
|
893
|
+
task: dict[str, Any],
|
|
894
|
+
constraints: PlanningScopeConstraints,
|
|
895
|
+
) -> tuple[list[str], list[PlanningScopeViolation]]:
|
|
896
|
+
if not constraints.has_constraints:
|
|
897
|
+
return paths, []
|
|
898
|
+
text_task = dict(task)
|
|
899
|
+
text_task["estimated_files"] = list(paths)
|
|
900
|
+
text_task["write_scope"] = list(paths)
|
|
901
|
+
violations = task_scope_constraint_violations(text_task, constraints)
|
|
902
|
+
violating = {_constraint_identity(violation.path) for violation in violations}
|
|
903
|
+
kept = [path for path in paths if _constraint_identity(path) not in violating]
|
|
904
|
+
return kept, violations
|