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,642 @@
|
|
|
1
|
+
"""Baseline-first regression attribution (verification protocol, step 3).
|
|
2
|
+
|
|
3
|
+
Attribution needs facts recorded *before* the first edit. A baseline is the
|
|
4
|
+
parsed, per-test outcome of a test run that actually executed before any
|
|
5
|
+
verification-relevant edit. Attribution is then a set difference against the
|
|
6
|
+
observed post-edit outcomes of the **same normalized executed command**:
|
|
7
|
+
|
|
8
|
+
* ``pre_existing`` — a failing/errored test id already failing in the
|
|
9
|
+
same-command baseline (the change did not cause it);
|
|
10
|
+
* ``regression`` — a failing id absent from that baseline (new since the
|
|
11
|
+
change);
|
|
12
|
+
* ``unattributed`` — a failing id with no comparable (same normalized command)
|
|
13
|
+
baseline, so its relationship to the change is unknown — a distinct honest
|
|
14
|
+
state, never silently treated as pre-existing OR as a regression;
|
|
15
|
+
* ``agent_authored`` — a failing id whose test *file* the agent created this
|
|
16
|
+
turn (a failing repro test the agent just wrote is signal, not a regression).
|
|
17
|
+
|
|
18
|
+
This module is pure and side-effect free. It provides:
|
|
19
|
+
|
|
20
|
+
* format parsers for pytest short-summary output and unittest/Django
|
|
21
|
+
``runtests`` output (fact extraction, not NL heuristics); on any ambiguity a
|
|
22
|
+
parser returns *counts-unknown* rather than guessing, and never raises;
|
|
23
|
+
* the normalized baseline key (reusing step 2's command normalization) so a
|
|
24
|
+
post-edit run is compared only against a baseline of the same executed
|
|
25
|
+
command — comparability by identity, never by fuzzy scope inference;
|
|
26
|
+
* the pure diff classifier and its aggregation across several post-edit runs.
|
|
27
|
+
|
|
28
|
+
No git state is ever mutated to reconstruct a baseline: baselines come only from
|
|
29
|
+
runs that actually happened pre-edit.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import re
|
|
35
|
+
from collections.abc import Iterable
|
|
36
|
+
from dataclasses import dataclass
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
from ..branding import env_get
|
|
40
|
+
from ..pipeline_facts import pipeline_meaningful_stage
|
|
41
|
+
from .verification_commands import _normalize_shell_command_for_match
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Kill-switch (mirrors the route-arbitration / evidence-v2 idiom of steps 1-2)
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _regression_baseline_enabled(cfg: Any | None) -> bool:
|
|
49
|
+
"""Kill-switch for the baseline-first regression protocol (step 3).
|
|
50
|
+
|
|
51
|
+
``ALYSIS_REGRESSION_BASELINE`` (off/0/false/no/disabled) wins over the
|
|
52
|
+
config value; default is on. When off, capture may still record telemetry
|
|
53
|
+
but the completion-gate policy is fully legacy.
|
|
54
|
+
"""
|
|
55
|
+
env_value = env_get("ALYSIS_REGRESSION_BASELINE")
|
|
56
|
+
if env_value is not None:
|
|
57
|
+
normalized = str(env_value).strip().lower()
|
|
58
|
+
if normalized in {"off", "0", "false", "no", "disabled"}:
|
|
59
|
+
return False
|
|
60
|
+
if normalized in {"on", "1", "true", "yes", "enabled"}:
|
|
61
|
+
return True
|
|
62
|
+
return bool(getattr(cfg, "regression_baseline_enabled", True))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# Parsed test-run report (fact extraction)
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
|
|
70
|
+
|
|
71
|
+
# pytest short-summary node-id lines, e.g. "FAILED path::test - reason".
|
|
72
|
+
_PYTEST_SUMMARY_LINE_RE = re.compile(r"^(FAILED|ERROR)\s+(\S.*?)\s*$")
|
|
73
|
+
# The "short test summary info" section header; failing node-id lines follow it.
|
|
74
|
+
_PYTEST_SUMMARY_HEADER_RE = re.compile(r"short test summary info")
|
|
75
|
+
# pytest final counts line: decorated with '=' (full verbosity) — e.g.
|
|
76
|
+
# "==== 2 failed, 116 passed in 1.2s ====".
|
|
77
|
+
_PYTEST_COUNTS_LINE_RE = re.compile(r"^=+\s*(?P<body>.*?)\s*=+\s*$")
|
|
78
|
+
# ...or undecorated (quiet mode, ``-q``) — e.g. "2 failed, 116 passed in 1.2s".
|
|
79
|
+
# Requires a trailing "in <time>s" so it never matches a node-id/reason line.
|
|
80
|
+
_PYTEST_UNDECORATED_COUNTS_RE = re.compile(r"\bin\s+\d+(?:\.\d+)?s\b\s*$")
|
|
81
|
+
_PYTEST_COUNT_TOKEN_RE = re.compile(
|
|
82
|
+
r"(\d+)\s+(passed|failed|errors?|skipped|xfailed|xpassed|deselected|warnings?)\b"
|
|
83
|
+
)
|
|
84
|
+
# unittest / Django runtests.
|
|
85
|
+
_UNITTEST_RESULT_LINE_RE = re.compile(r"^(FAIL|ERROR):\s+(\S.*?)\s*$")
|
|
86
|
+
_UNITTEST_RAN_RE = re.compile(r"^Ran\s+(\d+)\s+tests?\s+in\b")
|
|
87
|
+
_UNITTEST_FAILED_HEADER_RE = re.compile(r"^FAILED\s*\((?P<body>.*)\)\s*$")
|
|
88
|
+
_UNITTEST_OK_RE = re.compile(r"^OK\b")
|
|
89
|
+
_UNITTEST_COUNT_TOKEN_RE = re.compile(r"(failures|errors|skipped|expected failures)\s*=\s*(\d+)")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True)
|
|
93
|
+
class TestReport:
|
|
94
|
+
"""Parsed per-test outcome of one executed test run.
|
|
95
|
+
|
|
96
|
+
``counts_known`` is False when the runner's summary could not be parsed
|
|
97
|
+
(truncated/garbled output); such a report can never serve as a baseline.
|
|
98
|
+
``ids_complete`` is True only when every counted failure/error also produced
|
|
99
|
+
a parsed node id — a truncated run (``pytest … | tail -5``) yields
|
|
100
|
+
counts_known=True but ids_complete=False, so it is not comparable and its
|
|
101
|
+
failures fall to ``unattributed`` rather than being guessed.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
runner: str = "unknown"
|
|
105
|
+
failed_ids: tuple[str, ...] = ()
|
|
106
|
+
error_ids: tuple[str, ...] = ()
|
|
107
|
+
passed: int | None = None
|
|
108
|
+
failed: int | None = None
|
|
109
|
+
skipped: int | None = None
|
|
110
|
+
errors: int | None = None
|
|
111
|
+
counts_known: bool = False
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def failing_ids(self) -> tuple[str, ...]:
|
|
115
|
+
return tuple(dict.fromkeys((*self.failed_ids, *self.error_ids)))
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def ids_complete(self) -> bool:
|
|
119
|
+
if not self.counts_known:
|
|
120
|
+
return False
|
|
121
|
+
if self.failed is not None and len(self.failed_ids) != self.failed:
|
|
122
|
+
return False
|
|
123
|
+
if self.errors is not None and len(self.error_ids) != self.errors:
|
|
124
|
+
return False
|
|
125
|
+
return True
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def usable_as_baseline(self) -> bool:
|
|
129
|
+
return self.counts_known and self.ids_complete
|
|
130
|
+
|
|
131
|
+
def as_payload(self) -> dict[str, Any]:
|
|
132
|
+
return {
|
|
133
|
+
"runner": self.runner,
|
|
134
|
+
"failed_ids": list(self.failed_ids),
|
|
135
|
+
"error_ids": list(self.error_ids),
|
|
136
|
+
"passed": self.passed,
|
|
137
|
+
"failed": self.failed,
|
|
138
|
+
"skipped": self.skipped,
|
|
139
|
+
"errors": self.errors,
|
|
140
|
+
"counts_known": self.counts_known,
|
|
141
|
+
"ids_complete": self.ids_complete,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _strip_ansi(text: str) -> str:
|
|
146
|
+
return _ANSI_RE.sub("", str(text or ""))
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _pytest_node_id(raw: str) -> str | None:
|
|
150
|
+
"""Extract a node id from a pytest FAILED/ERROR summary line body.
|
|
151
|
+
|
|
152
|
+
The reason separator is ``" - "`` at bracket depth 0, so a parametrized id
|
|
153
|
+
whose param value contains a literal ``" - "`` (e.g. ``test[a - 2]``) is kept
|
|
154
|
+
whole rather than truncated. Keeps the candidate only when it looks like a
|
|
155
|
+
real node id (contains ``::`` or ends in ``.py``); a candidate that fails this
|
|
156
|
+
shape check is dropped rather than guessed at, so a mis-parsed line never
|
|
157
|
+
becomes a phantom regression.
|
|
158
|
+
"""
|
|
159
|
+
candidate = str(raw or "").strip()
|
|
160
|
+
depth = 0
|
|
161
|
+
index = 0
|
|
162
|
+
length = len(candidate)
|
|
163
|
+
reason_at = -1
|
|
164
|
+
while index < length:
|
|
165
|
+
char = candidate[index]
|
|
166
|
+
if char == "[":
|
|
167
|
+
depth += 1
|
|
168
|
+
elif char == "]":
|
|
169
|
+
if depth > 0:
|
|
170
|
+
depth -= 1
|
|
171
|
+
elif depth == 0 and candidate.startswith(" - ", index):
|
|
172
|
+
reason_at = index
|
|
173
|
+
break
|
|
174
|
+
index += 1
|
|
175
|
+
if reason_at != -1:
|
|
176
|
+
candidate = candidate[:reason_at].strip()
|
|
177
|
+
if not candidate:
|
|
178
|
+
return None
|
|
179
|
+
if "::" in candidate or candidate.endswith(".py"):
|
|
180
|
+
return candidate
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _pytest_counts_body(line: str) -> str | None:
|
|
185
|
+
"""Return the count body of a pytest summary line, decorated or not."""
|
|
186
|
+
stripped = line.strip()
|
|
187
|
+
match = _PYTEST_COUNTS_LINE_RE.match(stripped)
|
|
188
|
+
if match is not None:
|
|
189
|
+
body = match.group("body")
|
|
190
|
+
elif _PYTEST_UNDECORATED_COUNTS_RE.search(stripped):
|
|
191
|
+
body = stripped
|
|
192
|
+
else:
|
|
193
|
+
return None
|
|
194
|
+
if not body:
|
|
195
|
+
return None
|
|
196
|
+
lowered = body.lower()
|
|
197
|
+
if "no tests ran" in lowered or _PYTEST_COUNT_TOKEN_RE.search(lowered):
|
|
198
|
+
return lowered
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def parse_pytest_report(output: str) -> TestReport | None:
|
|
203
|
+
"""Parse pytest output; return ``None`` when it is not pytest output.
|
|
204
|
+
|
|
205
|
+
Failing node ids are read only from the "short test summary info" section, so
|
|
206
|
+
a stray ``FAILED …`` line in captured stdout or logs never becomes a phantom
|
|
207
|
+
id. Counts are read from the final summary line — decorated (``=== … ===``)
|
|
208
|
+
or the undecorated quiet-mode (``-q``) form.
|
|
209
|
+
"""
|
|
210
|
+
text = _strip_ansi(output)
|
|
211
|
+
lines = text.splitlines()
|
|
212
|
+
|
|
213
|
+
counts_body: str | None = None
|
|
214
|
+
for line in lines:
|
|
215
|
+
body = _pytest_counts_body(line)
|
|
216
|
+
if body is not None:
|
|
217
|
+
counts_body = body # keep scanning; the final summary line wins
|
|
218
|
+
if counts_body is None:
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
passed = failed = skipped = errors = 0
|
|
222
|
+
if "no tests ran" not in counts_body:
|
|
223
|
+
for value, word in _PYTEST_COUNT_TOKEN_RE.findall(counts_body):
|
|
224
|
+
amount = int(value)
|
|
225
|
+
if word == "passed":
|
|
226
|
+
passed = amount
|
|
227
|
+
elif word == "failed":
|
|
228
|
+
failed = amount
|
|
229
|
+
elif word.startswith("error"):
|
|
230
|
+
errors = amount
|
|
231
|
+
elif word == "skipped":
|
|
232
|
+
skipped = amount
|
|
233
|
+
|
|
234
|
+
failed_ids: list[str] = []
|
|
235
|
+
error_ids: list[str] = []
|
|
236
|
+
in_summary_section = False
|
|
237
|
+
for line in lines:
|
|
238
|
+
if _PYTEST_SUMMARY_HEADER_RE.search(line):
|
|
239
|
+
in_summary_section = True
|
|
240
|
+
continue
|
|
241
|
+
if not in_summary_section:
|
|
242
|
+
continue
|
|
243
|
+
match = _PYTEST_SUMMARY_LINE_RE.match(line.strip())
|
|
244
|
+
if match is None:
|
|
245
|
+
continue
|
|
246
|
+
node_id = _pytest_node_id(match.group(2))
|
|
247
|
+
if node_id is None:
|
|
248
|
+
continue
|
|
249
|
+
if match.group(1) == "FAILED":
|
|
250
|
+
failed_ids.append(node_id)
|
|
251
|
+
else:
|
|
252
|
+
error_ids.append(node_id)
|
|
253
|
+
|
|
254
|
+
return TestReport(
|
|
255
|
+
runner="pytest",
|
|
256
|
+
failed_ids=tuple(dict.fromkeys(failed_ids)),
|
|
257
|
+
error_ids=tuple(dict.fromkeys(error_ids)),
|
|
258
|
+
passed=passed,
|
|
259
|
+
failed=failed,
|
|
260
|
+
skipped=skipped,
|
|
261
|
+
errors=errors,
|
|
262
|
+
counts_known=True,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def parse_unittest_report(output: str) -> TestReport | None:
|
|
267
|
+
"""Parse unittest/Django ``runtests`` output; ``None`` when not that shape."""
|
|
268
|
+
text = _strip_ansi(output)
|
|
269
|
+
lines = text.splitlines()
|
|
270
|
+
|
|
271
|
+
ran_seen = any(_UNITTEST_RAN_RE.match(line.strip()) for line in lines)
|
|
272
|
+
if not ran_seen:
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
failed_ids: list[str] = []
|
|
276
|
+
error_ids: list[str] = []
|
|
277
|
+
for line in lines:
|
|
278
|
+
match = _UNITTEST_RESULT_LINE_RE.match(line.strip())
|
|
279
|
+
if match is None:
|
|
280
|
+
continue
|
|
281
|
+
identifier = match.group(2).strip()
|
|
282
|
+
if not identifier:
|
|
283
|
+
continue
|
|
284
|
+
if match.group(1) == "FAIL":
|
|
285
|
+
failed_ids.append(identifier)
|
|
286
|
+
else:
|
|
287
|
+
error_ids.append(identifier)
|
|
288
|
+
|
|
289
|
+
failures = errors = skipped = 0
|
|
290
|
+
for line in lines:
|
|
291
|
+
stripped = line.strip()
|
|
292
|
+
if _UNITTEST_OK_RE.match(stripped):
|
|
293
|
+
for word, value in _UNITTEST_COUNT_TOKEN_RE.findall(stripped):
|
|
294
|
+
if word == "skipped":
|
|
295
|
+
skipped = int(value)
|
|
296
|
+
continue
|
|
297
|
+
header = _UNITTEST_FAILED_HEADER_RE.match(stripped)
|
|
298
|
+
if header is None:
|
|
299
|
+
continue
|
|
300
|
+
for word, value in _UNITTEST_COUNT_TOKEN_RE.findall(header.group("body")):
|
|
301
|
+
amount = int(value)
|
|
302
|
+
if word == "failures":
|
|
303
|
+
failures = amount
|
|
304
|
+
elif word == "errors":
|
|
305
|
+
errors = amount
|
|
306
|
+
elif word == "skipped":
|
|
307
|
+
skipped = amount
|
|
308
|
+
|
|
309
|
+
return TestReport(
|
|
310
|
+
runner="unittest",
|
|
311
|
+
failed_ids=tuple(dict.fromkeys(failed_ids)),
|
|
312
|
+
error_ids=tuple(dict.fromkeys(error_ids)),
|
|
313
|
+
passed=None,
|
|
314
|
+
failed=failures,
|
|
315
|
+
skipped=skipped,
|
|
316
|
+
errors=errors,
|
|
317
|
+
counts_known=True,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def parse_test_report(output: str) -> TestReport:
|
|
322
|
+
"""Best-effort format parse of test-runner output.
|
|
323
|
+
|
|
324
|
+
Tries the pytest summary shape, then unittest/Django. Neither matching (or
|
|
325
|
+
truncated/garbled output) yields a counts-unknown report — never an
|
|
326
|
+
exception, never a guess. Deterministic for a given input.
|
|
327
|
+
"""
|
|
328
|
+
try:
|
|
329
|
+
report = parse_pytest_report(output)
|
|
330
|
+
if report is not None:
|
|
331
|
+
return report
|
|
332
|
+
report = parse_unittest_report(output)
|
|
333
|
+
if report is not None:
|
|
334
|
+
return report
|
|
335
|
+
except Exception: # noqa: BLE001 - a parser must never raise on hostile text
|
|
336
|
+
return TestReport(runner="unknown", counts_known=False)
|
|
337
|
+
return TestReport(runner="unknown", counts_known=False)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# ---------------------------------------------------------------------------
|
|
341
|
+
# Baseline keying + records
|
|
342
|
+
# ---------------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def baseline_command_key(command: str) -> str:
|
|
346
|
+
"""Normalized identity of an executed command for baseline comparison.
|
|
347
|
+
|
|
348
|
+
Reuses step 2's pipeline-stage identification and command normalization, so
|
|
349
|
+
``pytest foo``, ``pytest foo | tail -40`` and ``pytest foo | tail -5`` all
|
|
350
|
+
key to the same baseline (comparability by identity, invariant to piping),
|
|
351
|
+
while a genuinely different command keys differently and is not comparable.
|
|
352
|
+
"""
|
|
353
|
+
meaningful = pipeline_meaningful_stage(command)
|
|
354
|
+
target = meaningful if meaningful is not None else str(command or "")
|
|
355
|
+
return _normalize_shell_command_for_match(target)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
_ENV_ASSIGNMENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _program_basename(token: str) -> str:
|
|
362
|
+
"""Basename of a program token, normalizing both path separators.
|
|
363
|
+
|
|
364
|
+
posix ``shlex`` would have eaten backslashes, so a Windows venv path
|
|
365
|
+
(``C:\\venv\\Scripts\\pytest.exe``) is handled here by normalizing ``\\`` to
|
|
366
|
+
``/`` before taking the basename and stripping a ``.exe`` suffix.
|
|
367
|
+
"""
|
|
368
|
+
name = token.replace("\\", "/").rsplit("/", 1)[-1].casefold()
|
|
369
|
+
if name.endswith(".exe"):
|
|
370
|
+
name = name[:-4]
|
|
371
|
+
return name
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def command_is_test_runner(command: str) -> bool:
|
|
375
|
+
"""True when the meaningful first-stage program is pytest or unittest/Django.
|
|
376
|
+
|
|
377
|
+
Baseline capture is scoped to the runners the parsers understand; other
|
|
378
|
+
(qualifying) executions — repo-native validation scripts, linters — do not
|
|
379
|
+
emit per-test ids and are out of scope for regression attribution.
|
|
380
|
+
"""
|
|
381
|
+
meaningful = pipeline_meaningful_stage(command)
|
|
382
|
+
target = meaningful if meaningful is not None else str(command or "")
|
|
383
|
+
tokens = target.split()
|
|
384
|
+
index = 0
|
|
385
|
+
while index < len(tokens):
|
|
386
|
+
token = tokens[index]
|
|
387
|
+
# Strip a leading env-var prefix (``PYTHONPATH=. pytest`` / ``env FOO=b``).
|
|
388
|
+
if token == "env" or _ENV_ASSIGNMENT_RE.match(token):
|
|
389
|
+
index += 1
|
|
390
|
+
continue
|
|
391
|
+
break
|
|
392
|
+
tokens = tokens[index:]
|
|
393
|
+
if not tokens:
|
|
394
|
+
return False
|
|
395
|
+
head = _program_basename(tokens[0])
|
|
396
|
+
lowered = [token.casefold() for token in tokens]
|
|
397
|
+
if head in {"pytest", "py.test"}:
|
|
398
|
+
return True
|
|
399
|
+
if head in {"python", "python3", "py"}:
|
|
400
|
+
# ``-m pytest`` / ``-m unittest`` and the glued ``-mpytest`` / ``-munittest``.
|
|
401
|
+
if len(lowered) >= 3 and lowered[1] == "-m" and lowered[2] in {"pytest", "unittest"}:
|
|
402
|
+
return True
|
|
403
|
+
if len(lowered) >= 2 and lowered[1] in {"-mpytest", "-munittest"}:
|
|
404
|
+
return True
|
|
405
|
+
# manage.py test / runtests.py style django test entrypoints.
|
|
406
|
+
if (
|
|
407
|
+
any(_program_basename(token) == "manage.py" for token in tokens[1:])
|
|
408
|
+
and "test" in lowered
|
|
409
|
+
):
|
|
410
|
+
return True
|
|
411
|
+
if any(_program_basename(token) in {"runtests.py", "runtests"} for token in tokens[1:]):
|
|
412
|
+
return True
|
|
413
|
+
if head in {"runtests", "runtests.py"}:
|
|
414
|
+
return True
|
|
415
|
+
if head == "django-admin" and "test" in lowered:
|
|
416
|
+
return True
|
|
417
|
+
return False
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@dataclass(frozen=True)
|
|
421
|
+
class BaselineRecord:
|
|
422
|
+
"""A test run recorded before any verification-relevant edit (a baseline)."""
|
|
423
|
+
|
|
424
|
+
command: str
|
|
425
|
+
command_key: str
|
|
426
|
+
report: TestReport
|
|
427
|
+
edit_generation: int
|
|
428
|
+
timestamp: str = ""
|
|
429
|
+
|
|
430
|
+
@property
|
|
431
|
+
def failing_ids(self) -> frozenset[str]:
|
|
432
|
+
return frozenset(self.report.failing_ids)
|
|
433
|
+
|
|
434
|
+
@property
|
|
435
|
+
def usable(self) -> bool:
|
|
436
|
+
return self.report.usable_as_baseline
|
|
437
|
+
|
|
438
|
+
def as_payload(self) -> dict[str, Any]:
|
|
439
|
+
return {
|
|
440
|
+
"command": self.command,
|
|
441
|
+
"command_key": self.command_key,
|
|
442
|
+
"edit_generation": self.edit_generation,
|
|
443
|
+
"timestamp": self.timestamp,
|
|
444
|
+
"report": self.report.as_payload(),
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
@dataclass(frozen=True)
|
|
449
|
+
class PostEditTestRun:
|
|
450
|
+
"""A qualifying test run observed after a verification-relevant edit."""
|
|
451
|
+
|
|
452
|
+
command: str
|
|
453
|
+
command_key: str
|
|
454
|
+
report: TestReport
|
|
455
|
+
generation: int
|
|
456
|
+
|
|
457
|
+
def as_payload(self) -> dict[str, Any]:
|
|
458
|
+
return {
|
|
459
|
+
"command": self.command,
|
|
460
|
+
"command_key": self.command_key,
|
|
461
|
+
"generation": self.generation,
|
|
462
|
+
"report": self.report.as_payload(),
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
# ---------------------------------------------------------------------------
|
|
467
|
+
# Diff classification (pure)
|
|
468
|
+
# ---------------------------------------------------------------------------
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _created_path_components(path: str) -> tuple[str, ...]:
|
|
472
|
+
cleaned = str(path or "").strip().replace("\\", "/").casefold()
|
|
473
|
+
while cleaned.startswith("./"):
|
|
474
|
+
cleaned = cleaned[2:]
|
|
475
|
+
return tuple(part for part in cleaned.split("/") if part not in {"", "."})
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def node_id_file_path(test_id: str) -> str | None:
|
|
479
|
+
"""Return the source-file portion of a pytest node id, else ``None``.
|
|
480
|
+
|
|
481
|
+
``tests/test_foo.py::TestX::test_bar`` -> ``tests/test_foo.py``. unittest ids
|
|
482
|
+
(``test_bar (pkg.mod.Class)``) carry no file path and return ``None`` — so
|
|
483
|
+
agent-authored attribution is available for pytest ids only. (Named without a
|
|
484
|
+
``test_`` prefix so importing it into a test module never trips pytest's
|
|
485
|
+
test-collection.)
|
|
486
|
+
"""
|
|
487
|
+
text = str(test_id or "").strip()
|
|
488
|
+
if "::" in text:
|
|
489
|
+
return text.split("::", 1)[0].strip() or None
|
|
490
|
+
if text.endswith(".py"):
|
|
491
|
+
return text
|
|
492
|
+
return None
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _id_file_is_agent_created(test_id: str, created_components: list[tuple[str, ...]]) -> bool:
|
|
496
|
+
file_path = node_id_file_path(test_id)
|
|
497
|
+
if not file_path:
|
|
498
|
+
return False
|
|
499
|
+
id_components = _created_path_components(file_path)
|
|
500
|
+
if not id_components:
|
|
501
|
+
return False
|
|
502
|
+
# Exact normalized-path match only. A suffix/basename match would let a
|
|
503
|
+
# created ``test_foo.py`` collide with a pre-existing ``tests/test_foo.py``
|
|
504
|
+
# and wrongly mark that file's genuine regression as agent-authored — the
|
|
505
|
+
# dangerous direction (a shipped regression). Under-matching instead lets a
|
|
506
|
+
# genuinely agent-authored failure be treated as a regression (a wasted
|
|
507
|
+
# session at worst); we bias toward that safe direction.
|
|
508
|
+
return any(id_components == created for created in created_components if created)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
@dataclass(frozen=True)
|
|
512
|
+
class RegressionDiffResult:
|
|
513
|
+
pre_existing: tuple[str, ...] = ()
|
|
514
|
+
regressions: tuple[str, ...] = ()
|
|
515
|
+
unattributed: tuple[str, ...] = ()
|
|
516
|
+
agent_authored: tuple[str, ...] = ()
|
|
517
|
+
has_comparable_baseline: bool = False
|
|
518
|
+
baseline_command: str | None = None
|
|
519
|
+
|
|
520
|
+
@property
|
|
521
|
+
def blocks(self) -> bool:
|
|
522
|
+
return bool(self.regressions)
|
|
523
|
+
|
|
524
|
+
@property
|
|
525
|
+
def has_failures(self) -> bool:
|
|
526
|
+
return bool(
|
|
527
|
+
self.pre_existing or self.regressions or self.unattributed or self.agent_authored
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
@property
|
|
531
|
+
def all_failures_benign(self) -> bool:
|
|
532
|
+
"""True when there are failures and none is a regression or unattributed."""
|
|
533
|
+
return self.has_failures and not self.regressions and not self.unattributed
|
|
534
|
+
|
|
535
|
+
def as_payload(self) -> dict[str, Any]:
|
|
536
|
+
return {
|
|
537
|
+
"pre_existing": list(self.pre_existing),
|
|
538
|
+
"regressions": list(self.regressions),
|
|
539
|
+
"unattributed": list(self.unattributed),
|
|
540
|
+
"agent_authored": list(self.agent_authored),
|
|
541
|
+
"has_comparable_baseline": self.has_comparable_baseline,
|
|
542
|
+
"baseline_command": self.baseline_command,
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def classify_regression_diff(
|
|
547
|
+
*,
|
|
548
|
+
post_report: TestReport,
|
|
549
|
+
baseline: BaselineRecord | None,
|
|
550
|
+
agent_created_paths: Iterable[str] = (),
|
|
551
|
+
) -> RegressionDiffResult:
|
|
552
|
+
"""Classify one post-edit run's failures against its same-command baseline.
|
|
553
|
+
|
|
554
|
+
A comparable baseline requires both the baseline and the post-edit report to
|
|
555
|
+
be counts-known with complete ids; otherwise every failure is
|
|
556
|
+
``unattributed`` (honest — never guessed as pre-existing or regression).
|
|
557
|
+
Agent-authored test files win first: a failing test the agent just wrote is
|
|
558
|
+
signal, not a regression.
|
|
559
|
+
"""
|
|
560
|
+
created_components = [
|
|
561
|
+
components
|
|
562
|
+
for components in (_created_path_components(path) for path in agent_created_paths)
|
|
563
|
+
if components
|
|
564
|
+
]
|
|
565
|
+
comparable = baseline is not None and baseline.usable and post_report.usable_as_baseline
|
|
566
|
+
baseline_failing = baseline.failing_ids if baseline is not None else frozenset()
|
|
567
|
+
|
|
568
|
+
pre_existing: list[str] = []
|
|
569
|
+
regressions: list[str] = []
|
|
570
|
+
unattributed: list[str] = []
|
|
571
|
+
agent_authored: list[str] = []
|
|
572
|
+
for test_id in post_report.failing_ids:
|
|
573
|
+
if _id_file_is_agent_created(test_id, created_components):
|
|
574
|
+
agent_authored.append(test_id)
|
|
575
|
+
elif not comparable:
|
|
576
|
+
unattributed.append(test_id)
|
|
577
|
+
elif test_id in baseline_failing:
|
|
578
|
+
pre_existing.append(test_id)
|
|
579
|
+
else:
|
|
580
|
+
regressions.append(test_id)
|
|
581
|
+
|
|
582
|
+
return RegressionDiffResult(
|
|
583
|
+
pre_existing=tuple(pre_existing),
|
|
584
|
+
regressions=tuple(regressions),
|
|
585
|
+
unattributed=tuple(unattributed),
|
|
586
|
+
agent_authored=tuple(agent_authored),
|
|
587
|
+
has_comparable_baseline=comparable,
|
|
588
|
+
baseline_command=baseline.command if (comparable and baseline is not None) else None,
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def aggregate_regression_results(
|
|
593
|
+
results: Iterable[RegressionDiffResult],
|
|
594
|
+
) -> RegressionDiffResult:
|
|
595
|
+
"""Combine per-run diffs with honest precedence over shared ids.
|
|
596
|
+
|
|
597
|
+
agent_authored > pre_existing > regression > unattributed: a file the agent
|
|
598
|
+
created wins everywhere; an id seen failing in any comparable baseline is
|
|
599
|
+
pre-existing before it can be called a regression; an id is only
|
|
600
|
+
unattributed when no comparable run ever explained it.
|
|
601
|
+
"""
|
|
602
|
+
results = list(results)
|
|
603
|
+
agent_authored: list[str] = []
|
|
604
|
+
pre_existing: list[str] = []
|
|
605
|
+
regressions: list[str] = []
|
|
606
|
+
unattributed: list[str] = []
|
|
607
|
+
baseline_commands: list[str] = []
|
|
608
|
+
has_comparable = False
|
|
609
|
+
for result in results:
|
|
610
|
+
agent_authored.extend(result.agent_authored)
|
|
611
|
+
pre_existing.extend(result.pre_existing)
|
|
612
|
+
regressions.extend(result.regressions)
|
|
613
|
+
unattributed.extend(result.unattributed)
|
|
614
|
+
has_comparable = has_comparable or result.has_comparable_baseline
|
|
615
|
+
if result.regressions and result.baseline_command:
|
|
616
|
+
baseline_commands.append(result.baseline_command)
|
|
617
|
+
|
|
618
|
+
authored = list(dict.fromkeys(agent_authored))
|
|
619
|
+
authored_set = set(authored)
|
|
620
|
+
pre = [tid for tid in dict.fromkeys(pre_existing) if tid not in authored_set]
|
|
621
|
+
pre_set = set(pre)
|
|
622
|
+
reg = [
|
|
623
|
+
tid for tid in dict.fromkeys(regressions) if tid not in authored_set and tid not in pre_set
|
|
624
|
+
]
|
|
625
|
+
reg_set = set(reg)
|
|
626
|
+
un = [
|
|
627
|
+
tid
|
|
628
|
+
for tid in dict.fromkeys(unattributed)
|
|
629
|
+
if tid not in authored_set and tid not in pre_set and tid not in reg_set
|
|
630
|
+
]
|
|
631
|
+
baseline_command = ", ".join(dict.fromkeys(baseline_commands)) if baseline_commands else None
|
|
632
|
+
return RegressionDiffResult(
|
|
633
|
+
pre_existing=tuple(pre),
|
|
634
|
+
regressions=tuple(reg),
|
|
635
|
+
unattributed=tuple(un),
|
|
636
|
+
agent_authored=tuple(authored),
|
|
637
|
+
has_comparable_baseline=has_comparable,
|
|
638
|
+
baseline_command=baseline_command,
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
EMPTY_REGRESSION_DIFF = RegressionDiffResult()
|