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.
Files changed (458) hide show
  1. alysis_code/__init__.py +3 -0
  2. alysis_code/__main__.py +4 -0
  3. alysis_code/_build_info.py +14 -0
  4. alysis_code/account_login.py +468 -0
  5. alysis_code/agent/README.md +35 -0
  6. alysis_code/agent/__init__.py +11 -0
  7. alysis_code/agent/acceptance_contract.py +2217 -0
  8. alysis_code/agent/blast_radius.py +1403 -0
  9. alysis_code/agent/cache_keepalive.py +227 -0
  10. alysis_code/agent/completion_certificate.py +366 -0
  11. alysis_code/agent/completion_gate.py +306 -0
  12. alysis_code/agent/empty_response_stall.py +403 -0
  13. alysis_code/agent/errors.py +28 -0
  14. alysis_code/agent/llm_calls.py +475 -0
  15. alysis_code/agent/mutation_classification.py +227 -0
  16. alysis_code/agent/prompt_context.py +2508 -0
  17. alysis_code/agent/read_ledger.py +253 -0
  18. alysis_code/agent/regression_baseline.py +642 -0
  19. alysis_code/agent/reproduction_first.py +610 -0
  20. alysis_code/agent/sensitive_output.py +629 -0
  21. alysis_code/agent/session.py +3218 -0
  22. alysis_code/agent/steering.py +191 -0
  23. alysis_code/agent/subagent_execution.py +5177 -0
  24. alysis_code/agent/subagent_workspace.py +666 -0
  25. alysis_code/agent/tools_assembly.py +4728 -0
  26. alysis_code/agent/turn/__init__.py +101 -0
  27. alysis_code/agent/turn/core.py +8483 -0
  28. alysis_code/agent/turn/events.py +113 -0
  29. alysis_code/agent/turn/exploration.py +590 -0
  30. alysis_code/agent/turn/interventions.py +65 -0
  31. alysis_code/agent/turn/read_cache.py +420 -0
  32. alysis_code/agent/turn/snapshot.py +179 -0
  33. alysis_code/agent/turn_contract.py +661 -0
  34. alysis_code/agent/turn_path.py +129 -0
  35. alysis_code/agent/verification.py +2885 -0
  36. alysis_code/agent/verification_commands.py +512 -0
  37. alysis_code/agent/verification_evidence.py +738 -0
  38. alysis_code/agent_loop.py +693 -0
  39. alysis_code/agent_runtimes/__init__.py +51 -0
  40. alysis_code/agent_runtimes/base.py +114 -0
  41. alysis_code/agent_runtimes/builtins.py +129 -0
  42. alysis_code/agent_runtimes/codex_cli.py +664 -0
  43. alysis_code/agent_runtimes/host.py +263 -0
  44. alysis_code/agent_runtimes/registry.py +64 -0
  45. alysis_code/agent_runtimes/service.py +150 -0
  46. alysis_code/agentbox_client.py +416 -0
  47. alysis_code/agentbox_integration.py +310 -0
  48. alysis_code/alysis_cloud.py +152 -0
  49. alysis_code/approval_scope.py +276 -0
  50. alysis_code/assets/README.md +33 -0
  51. alysis_code/assets/__init__.py +126 -0
  52. alysis_code/assets/asset_read_core.py +281 -0
  53. alysis_code/assets/budget_allocator.py +456 -0
  54. alysis_code/assets/comprehender.py +759 -0
  55. alysis_code/assets/index.py +654 -0
  56. alysis_code/assets/ingestion.py +275 -0
  57. alysis_code/assets/legacy_migration.py +413 -0
  58. alysis_code/assets/models.py +263 -0
  59. alysis_code/assets/ocr.py +239 -0
  60. alysis_code/assets/owl/ascii/f-000.txt +13 -0
  61. alysis_code/assets/owl/ascii/f-001.txt +13 -0
  62. alysis_code/assets/owl/ascii/f-002.txt +13 -0
  63. alysis_code/assets/owl/ascii/f-003.txt +13 -0
  64. alysis_code/assets/owl/ascii/f-004.txt +13 -0
  65. alysis_code/assets/owl/ascii/f-005.txt +13 -0
  66. alysis_code/assets/owl/ascii/f-006.txt +13 -0
  67. alysis_code/assets/owl/ascii/f-007.txt +13 -0
  68. alysis_code/assets/owl/ascii/f-008.txt +13 -0
  69. alysis_code/assets/owl/ascii/f-009.txt +13 -0
  70. alysis_code/assets/owl/ascii/f-010.txt +13 -0
  71. alysis_code/assets/owl/ascii/f-011.txt +13 -0
  72. alysis_code/assets/owl/ascii/f-012.txt +13 -0
  73. alysis_code/assets/owl/ascii/f-013.txt +13 -0
  74. alysis_code/assets/owl/ascii/f-014.txt +13 -0
  75. alysis_code/assets/owl/ascii/f-015.txt +13 -0
  76. alysis_code/assets/owl/ascii/f-016.txt +13 -0
  77. alysis_code/assets/owl/ascii/f-017.txt +13 -0
  78. alysis_code/assets/owl/ascii/f-018.txt +13 -0
  79. alysis_code/assets/owl/ascii/f-019.txt +13 -0
  80. alysis_code/assets/owl/ascii/f-020.txt +13 -0
  81. alysis_code/assets/owl/index.html +98 -0
  82. alysis_code/assets/owl/show-owl.sh +761 -0
  83. alysis_code/assets/paths.py +49 -0
  84. alysis_code/assets/plan_binding.py +326 -0
  85. alysis_code/assets/planner_context.py +466 -0
  86. alysis_code/assets/planner_tools.py +184 -0
  87. alysis_code/assets/prompts.py +101 -0
  88. alysis_code/assets/replanner_context.py +239 -0
  89. alysis_code/assets/surface.py +521 -0
  90. alysis_code/assets/untrusted_content.py +48 -0
  91. alysis_code/assets/usage_logger.py +94 -0
  92. alysis_code/assets/worker_mirror.py +428 -0
  93. alysis_code/assets/worker_section.py +303 -0
  94. alysis_code/assets/worker_tools.py +468 -0
  95. alysis_code/atomic_io.py +83 -0
  96. alysis_code/auth_diagnostics.py +272 -0
  97. alysis_code/background_runner.py +366 -0
  98. alysis_code/branding.py +270 -0
  99. alysis_code/budget_policy.py +390 -0
  100. alysis_code/build_identity.py +465 -0
  101. alysis_code/builtin_hooks/__init__.py +7 -0
  102. alysis_code/builtin_hooks/notify_done_windows.py +65 -0
  103. alysis_code/bwrap_etc.py +76 -0
  104. alysis_code/cancellation.py +41 -0
  105. alysis_code/capabilities.py +137 -0
  106. alysis_code/chatgpt_codex_static_provider.py +133 -0
  107. alysis_code/cli.py +51 -0
  108. alysis_code/cli_impl/__init__.py +1 -0
  109. alysis_code/cli_impl/assets_cli.py +537 -0
  110. alysis_code/cli_impl/assets_modal.py +412 -0
  111. alysis_code/cli_impl/chat/__init__.py +156 -0
  112. alysis_code/cli_impl/chat/commands.py +2616 -0
  113. alysis_code/cli_impl/chat/loop.py +4508 -0
  114. alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
  115. alysis_code/cli_impl/chat/rendering.py +444 -0
  116. alysis_code/cli_impl/chat/state.py +124 -0
  117. alysis_code/cli_impl/chat_resume.py +830 -0
  118. alysis_code/cli_impl/chat_slash_completer.py +258 -0
  119. alysis_code/cli_impl/commands/__init__.py +11 -0
  120. alysis_code/cli_impl/commands/_shared.py +89 -0
  121. alysis_code/cli_impl/commands/auth.py +623 -0
  122. alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
  123. alysis_code/cli_impl/commands/chat_state.py +158 -0
  124. alysis_code/cli_impl/commands/chat_status.py +1248 -0
  125. alysis_code/cli_impl/commands/chat_terminal.py +942 -0
  126. alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
  127. alysis_code/cli_impl/commands/cli_common.py +1223 -0
  128. alysis_code/cli_impl/commands/cli_surface.py +77 -0
  129. alysis_code/cli_impl/commands/config.py +131 -0
  130. alysis_code/cli_impl/commands/conventions.py +85 -0
  131. alysis_code/cli_impl/commands/execution_helpers.py +350 -0
  132. alysis_code/cli_impl/commands/extensions.py +401 -0
  133. alysis_code/cli_impl/commands/forge.py +1282 -0
  134. alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
  135. alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
  136. alysis_code/cli_impl/commands/hooks.py +737 -0
  137. alysis_code/cli_impl/commands/ide_bridge.py +31 -0
  138. alysis_code/cli_impl/commands/mcp.py +700 -0
  139. alysis_code/cli_impl/commands/profile.py +453 -0
  140. alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
  141. alysis_code/cli_impl/commands/report.py +88 -0
  142. alysis_code/cli_impl/commands/root.py +1118 -0
  143. alysis_code/cli_impl/commands/sandbox.py +184 -0
  144. alysis_code/cli_impl/commands/server.py +54 -0
  145. alysis_code/cli_impl/commands/sessions.py +252 -0
  146. alysis_code/cli_impl/commands/skills.py +404 -0
  147. alysis_code/cli_impl/commands/startup.py +946 -0
  148. alysis_code/cli_impl/commands/tools.py +335 -0
  149. alysis_code/cli_impl/commands/update.py +364 -0
  150. alysis_code/cli_impl/commands/welcome.py +972 -0
  151. alysis_code/cli_impl/config_menu.py +3882 -0
  152. alysis_code/cli_impl/forge.py +4509 -0
  153. alysis_code/cli_impl/forge_recovery.py +485 -0
  154. alysis_code/cli_impl/setup_wizard.py +2409 -0
  155. alysis_code/cli_impl/tui/__init__.py +58 -0
  156. alysis_code/cli_impl/tui/app.py +4551 -0
  157. alysis_code/cli_impl/tui/config.py +32 -0
  158. alysis_code/cli_impl/tui/config_flow.py +2754 -0
  159. alysis_code/cli_impl/tui/config_overlay.py +566 -0
  160. alysis_code/cli_impl/tui/content.py +78 -0
  161. alysis_code/cli_impl/tui/footer.py +218 -0
  162. alysis_code/cli_impl/tui/forge_status.py +136 -0
  163. alysis_code/cli_impl/tui/markdown.py +244 -0
  164. alysis_code/cli_impl/tui/owl.py +109 -0
  165. alysis_code/cli_impl/tui/plan_meta.py +477 -0
  166. alysis_code/cli_impl/tui/setup_app.py +519 -0
  167. alysis_code/cli_impl/tui/setup_flow.py +1622 -0
  168. alysis_code/cli_impl/tui/state.py +101 -0
  169. alysis_code/cli_impl/tui/subagent_identity.py +66 -0
  170. alysis_code/cli_impl/tui/subagent_panel.py +186 -0
  171. alysis_code/cli_impl/tui/surface.py +796 -0
  172. alysis_code/cli_impl/tui/transcript.py +514 -0
  173. alysis_code/cli_impl/tui/update_prompt.py +79 -0
  174. alysis_code/cli_impl/tui/workspace_guard.py +384 -0
  175. alysis_code/clipboard.py +172 -0
  176. alysis_code/code_review.py +1211 -0
  177. alysis_code/compaction/__init__.py +28 -0
  178. alysis_code/compaction/conversation_compactor.py +2932 -0
  179. alysis_code/compaction/importance.py +177 -0
  180. alysis_code/compaction/settings.py +297 -0
  181. alysis_code/compaction/tool_output_offload.py +447 -0
  182. alysis_code/config.py +3509 -0
  183. alysis_code/conflict_auto_resolver.py +895 -0
  184. alysis_code/context/__init__.py +1 -0
  185. alysis_code/context/tool_schema_budgeter.py +220 -0
  186. alysis_code/crash_diagnostics.py +282 -0
  187. alysis_code/custom_tools/README.md +34 -0
  188. alysis_code/custom_tools/__init__.py +43 -0
  189. alysis_code/custom_tools/discovery.py +903 -0
  190. alysis_code/custom_tools/runtime.py +1516 -0
  191. alysis_code/custom_tools/session.py +227 -0
  192. alysis_code/custom_tools/trust.py +232 -0
  193. alysis_code/diff_paths.py +113 -0
  194. alysis_code/direction_change.py +293 -0
  195. alysis_code/dispatch_timing.py +306 -0
  196. alysis_code/durable_service_manager.py +1236 -0
  197. alysis_code/edit_discipline.py +659 -0
  198. alysis_code/error_text.py +73 -0
  199. alysis_code/execution_budget.py +411 -0
  200. alysis_code/execution_context.py +915 -0
  201. alysis_code/execution_deadline.py +1065 -0
  202. alysis_code/execution_shared.py +1904 -0
  203. alysis_code/extensions/README.md +30 -0
  204. alysis_code/extensions/__init__.py +93 -0
  205. alysis_code/extensions/activation.py +138 -0
  206. alysis_code/extensions/install.py +1436 -0
  207. alysis_code/extensions/manifest.py +487 -0
  208. alysis_code/extensions/models.py +74 -0
  209. alysis_code/extensions/paths.py +56 -0
  210. alysis_code/extensions/registry.json +4 -0
  211. alysis_code/extensions/registry.py +52 -0
  212. alysis_code/extensions/state.py +83 -0
  213. alysis_code/extensions/workspace_trust.py +101 -0
  214. alysis_code/failed_task_evidence.py +369 -0
  215. alysis_code/failure_category.py +315 -0
  216. alysis_code/feedback_report.py +1647 -0
  217. alysis_code/file_classification.py +485 -0
  218. alysis_code/forge.py +2064 -0
  219. alysis_code/forge_completion.py +362 -0
  220. alysis_code/forge_events.py +475 -0
  221. alysis_code/frontmatter_utils.py +95 -0
  222. alysis_code/git_evidence.py +1181 -0
  223. alysis_code/git_ops.py +560 -0
  224. alysis_code/git_safe.py +62 -0
  225. alysis_code/git_worktrees.py +190 -0
  226. alysis_code/hooks/README.md +33 -0
  227. alysis_code/hooks/__init__.py +67 -0
  228. alysis_code/hooks/audit.py +171 -0
  229. alysis_code/hooks/config.py +225 -0
  230. alysis_code/hooks/dispatcher.py +1110 -0
  231. alysis_code/hooks/models.py +447 -0
  232. alysis_code/hooks/trust.py +202 -0
  233. alysis_code/host_actions.py +543 -0
  234. alysis_code/host_browser.py +103 -0
  235. alysis_code/ide/__init__.py +5 -0
  236. alysis_code/ide/activity_events.py +399 -0
  237. alysis_code/ide/approvals.py +337 -0
  238. alysis_code/ide/artifacts.py +153 -0
  239. alysis_code/ide/browser_egress_proxy.py +1076 -0
  240. alysis_code/ide/cdp_websocket_transport.py +1192 -0
  241. alysis_code/ide/change_ledger.py +1721 -0
  242. alysis_code/ide/context_blocks.py +979 -0
  243. alysis_code/ide/event_stream.py +531 -0
  244. alysis_code/ide/forge_protocol.py +3112 -0
  245. alysis_code/ide/forge_request_ledger.py +737 -0
  246. alysis_code/ide/health.py +965 -0
  247. alysis_code/ide/managed_browser.py +2251 -0
  248. alysis_code/ide/management_protocol.py +3414 -0
  249. alysis_code/ide/mcp_oauth_coordinator.py +744 -0
  250. alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
  251. alysis_code/ide/prompt_queue.py +1070 -0
  252. alysis_code/ide/protocol.py +191 -0
  253. alysis_code/ide/resumable_swarm.py +1543 -0
  254. alysis_code/ide/session_search.py +295 -0
  255. alysis_code/ide/stdio_bridge.py +9935 -0
  256. alysis_code/ide/structured_state.py +1579 -0
  257. alysis_code/ide/swarm_protocol.py +816 -0
  258. alysis_code/integration_gate.py +506 -0
  259. alysis_code/interactive_input_guard.py +39 -0
  260. alysis_code/interactive_plan_mode.py +26 -0
  261. alysis_code/internal_artifacts.py +179 -0
  262. alysis_code/knowledge_base.py +1409 -0
  263. alysis_code/knowledge_capture.py +1190 -0
  264. alysis_code/knowledge_librarian.py +605 -0
  265. alysis_code/language_policy.py +34 -0
  266. alysis_code/litellm_static_provider.py +535 -0
  267. alysis_code/llm/__init__.py +1 -0
  268. alysis_code/llm/anthropic_messages.py +2288 -0
  269. alysis_code/llm/base.py +71 -0
  270. alysis_code/llm/cache_capabilities.py +985 -0
  271. alysis_code/llm/cache_control_blocks.py +244 -0
  272. alysis_code/llm/cache_policy.py +388 -0
  273. alysis_code/llm/factory.py +373 -0
  274. alysis_code/llm/gemini_generate_content.py +2652 -0
  275. alysis_code/llm/gemini_interactions.py +739 -0
  276. alysis_code/llm/metadata.py +450 -0
  277. alysis_code/llm/openai_compat.py +2947 -0
  278. alysis_code/llm/openai_responses.py +2604 -0
  279. alysis_code/llm/protocols.py +609 -0
  280. alysis_code/llm/provider_limits.py +525 -0
  281. alysis_code/llm/request_plan.py +389 -0
  282. alysis_code/llm/request_shape.py +238 -0
  283. alysis_code/llm/streaming.py +108 -0
  284. alysis_code/llm/temperature_compat.py +78 -0
  285. alysis_code/llm/types.py +195 -0
  286. alysis_code/llm/usage_normalization.py +222 -0
  287. alysis_code/llm_error_display.py +315 -0
  288. alysis_code/logging_redaction.py +326 -0
  289. alysis_code/managed_host_deadline.py +196 -0
  290. alysis_code/mcp/README.md +33 -0
  291. alysis_code/mcp/__init__.py +24 -0
  292. alysis_code/mcp/client.py +1137 -0
  293. alysis_code/mcp/config.py +597 -0
  294. alysis_code/mcp/errors.py +113 -0
  295. alysis_code/mcp/forge_scope.py +154 -0
  296. alysis_code/mcp/jsonrpc.py +214 -0
  297. alysis_code/mcp/manager.py +2308 -0
  298. alysis_code/mcp/models.py +666 -0
  299. alysis_code/mcp/oauth.py +972 -0
  300. alysis_code/mcp/oauth_runtime.py +310 -0
  301. alysis_code/mcp/oauth_store.py +276 -0
  302. alysis_code/mcp/prompts.py +329 -0
  303. alysis_code/mcp/resources.py +295 -0
  304. alysis_code/mcp/roots.py +106 -0
  305. alysis_code/mcp/server_requests.py +75 -0
  306. alysis_code/mcp/token_store.py +859 -0
  307. alysis_code/mcp/transport_http.py +1338 -0
  308. alysis_code/mcp/transport_stdio.py +1267 -0
  309. alysis_code/mcp/untrusted_content.py +119 -0
  310. alysis_code/merge_conflict_reviewer.py +729 -0
  311. alysis_code/model_catalog/__init__.py +1 -0
  312. alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
  313. alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
  314. alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
  315. alysis_code/model_metadata_policy.py +223 -0
  316. alysis_code/model_metadata_utils.py +103 -0
  317. alysis_code/model_registry.py +1420 -0
  318. alysis_code/model_router.py +147 -0
  319. alysis_code/permission_policy.py +1016 -0
  320. alysis_code/personas.py +451 -0
  321. alysis_code/pipeline_facts.py +233 -0
  322. alysis_code/plan_assistant.py +4763 -0
  323. alysis_code/plan_mode.py +393 -0
  324. alysis_code/plan_reconciliation.py +1228 -0
  325. alysis_code/plan_repair.py +652 -0
  326. alysis_code/plan_validation.py +1099 -0
  327. alysis_code/planning_constraints.py +904 -0
  328. alysis_code/policy.py +95 -0
  329. alysis_code/preview_server.py +457 -0
  330. alysis_code/process_reaping.py +566 -0
  331. alysis_code/profile_presets.py +1834 -0
  332. alysis_code/profiles.py +666 -0
  333. alysis_code/provider_auth/__init__.py +29 -0
  334. alysis_code/provider_auth/base.py +99 -0
  335. alysis_code/provider_auth/openai_codex.py +951 -0
  336. alysis_code/provider_auth/registry.py +76 -0
  337. alysis_code/provider_auth/store.py +125 -0
  338. alysis_code/provider_diagnostics.py +1209 -0
  339. alysis_code/provider_model_catalog.py +685 -0
  340. alysis_code/provider_telemetry.py +1699 -0
  341. alysis_code/provider_url.py +75 -0
  342. alysis_code/reasoning_contracts.py +911 -0
  343. alysis_code/remote_sync.py +350 -0
  344. alysis_code/replanning.py +1195 -0
  345. alysis_code/repo_scan.py +1152 -0
  346. alysis_code/request_estimation.py +296 -0
  347. alysis_code/review_gate.py +617 -0
  348. alysis_code/run_lock.py +1141 -0
  349. alysis_code/run_outcome.py +58 -0
  350. alysis_code/run_provenance.py +774 -0
  351. alysis_code/run_state.py +445 -0
  352. alysis_code/runtime_artifacts.py +116 -0
  353. alysis_code/runtime_context_features.py +78 -0
  354. alysis_code/runtime_kind.py +52 -0
  355. alysis_code/safety/__init__.py +11 -0
  356. alysis_code/safety/mcp_sanitize.py +29 -0
  357. alysis_code/safety/safe_http.py +297 -0
  358. alysis_code/safety/subagent_report.py +184 -0
  359. alysis_code/sandbox_doctor.py +682 -0
  360. alysis_code/sandbox_runner.py +1025 -0
  361. alysis_code/sandbox_settings.py +423 -0
  362. alysis_code/serialized_paths.py +355 -0
  363. alysis_code/server/__init__.py +3 -0
  364. alysis_code/server/app.py +367 -0
  365. alysis_code/server/auth.py +34 -0
  366. alysis_code/server/job_config.py +30 -0
  367. alysis_code/server/settings.py +215 -0
  368. alysis_code/server/store.py +193 -0
  369. alysis_code/server/worker_runner.py +657 -0
  370. alysis_code/service_persistence.py +355 -0
  371. alysis_code/session_artifacts.py +108 -0
  372. alysis_code/session_metrics.py +331 -0
  373. alysis_code/session_store.py +624 -0
  374. alysis_code/skills/README.md +34 -0
  375. alysis_code/skills/__init__.py +104 -0
  376. alysis_code/skills/conventions.py +84 -0
  377. alysis_code/skills/discovery.py +176 -0
  378. alysis_code/skills/eval_models.py +232 -0
  379. alysis_code/skills/eval_runner.py +372 -0
  380. alysis_code/skills/evals.py +1344 -0
  381. alysis_code/skills/install.py +293 -0
  382. alysis_code/skills/loader.py +118 -0
  383. alysis_code/skills/matching.py +103 -0
  384. alysis_code/skills/models.py +71 -0
  385. alysis_code/skills/paths.py +56 -0
  386. alysis_code/skills/prompting.py +500 -0
  387. alysis_code/skills/scaffold.py +142 -0
  388. alysis_code/skills/state.py +441 -0
  389. alysis_code/skills/transactions.py +125 -0
  390. alysis_code/skills/validation.py +304 -0
  391. alysis_code/step_budget.py +238 -0
  392. alysis_code/subagent_labels.py +49 -0
  393. alysis_code/subagents.py +1072 -0
  394. alysis_code/surface/__init__.py +80 -0
  395. alysis_code/surface/base.py +305 -0
  396. alysis_code/surface/console.py +387 -0
  397. alysis_code/surface/events.py +372 -0
  398. alysis_code/surface/hidden_surface.py +529 -0
  399. alysis_code/surface/noop_surface.py +219 -0
  400. alysis_code/surface/rich_surface.py +1555 -0
  401. alysis_code/surface/styles.py +67 -0
  402. alysis_code/surface/theme.py +455 -0
  403. alysis_code/surface/types.py +100 -0
  404. alysis_code/swarm_backend.py +926 -0
  405. alysis_code/swarm_orchestrator.py +4020 -0
  406. alysis_code/swarm_scheduler.py +441 -0
  407. alysis_code/swarm_trace.py +429 -0
  408. alysis_code/swarm_worker.py +2119 -0
  409. alysis_code/swarm_write_guard.py +348 -0
  410. alysis_code/task_dependencies.py +170 -0
  411. alysis_code/task_readiness.py +992 -0
  412. alysis_code/task_scope.py +2148 -0
  413. alysis_code/terminal_manager.py +762 -0
  414. alysis_code/terminal_ownership.py +460 -0
  415. alysis_code/text_normalization.py +30 -0
  416. alysis_code/token_budget.py +97 -0
  417. alysis_code/tools/README.md +34 -0
  418. alysis_code/tools/__init__.py +1 -0
  419. alysis_code/tools/artifacts.py +127 -0
  420. alysis_code/tools/availability.py +188 -0
  421. alysis_code/tools/fs.py +1456 -0
  422. alysis_code/tools/git.py +461 -0
  423. alysis_code/tools/history.py +229 -0
  424. alysis_code/tools/http_timeout.py +78 -0
  425. alysis_code/tools/image_generation.py +552 -0
  426. alysis_code/tools/registry.py +2936 -0
  427. alysis_code/tools/repo_map.py +476 -0
  428. alysis_code/tools/search.py +563 -0
  429. alysis_code/tools/shell.py +135 -0
  430. alysis_code/tools/symbols.py +1350 -0
  431. alysis_code/tools/test_discovery.py +643 -0
  432. alysis_code/tools/web.py +482 -0
  433. alysis_code/tools/web_search.py +2012 -0
  434. alysis_code/tools/web_search_dashscope.py +557 -0
  435. alysis_code/tools/web_search_ddgs.py +221 -0
  436. alysis_code/tools/web_search_provider_adapters.py +1429 -0
  437. alysis_code/tools/web_search_tavily.py +194 -0
  438. alysis_code/updates.py +933 -0
  439. alysis_code/usage_tracker.py +1990 -0
  440. alysis_code/verification_command_analysis.py +1004 -0
  441. alysis_code/verification_contract.py +574 -0
  442. alysis_code/verification_failure_summary.py +273 -0
  443. alysis_code/verification_repair.py +385 -0
  444. alysis_code/verify_gate.py +3129 -0
  445. alysis_code/web_research.py +1872 -0
  446. alysis_code/web_search_adapters.py +66 -0
  447. alysis_code/web_search_policy.py +27 -0
  448. alysis_code/workspace_binding.py +389 -0
  449. alysis_code/workspace_binding_ui.py +408 -0
  450. alysis_code/workspace_context.py +273 -0
  451. alysis_code/workspace_isolation.py +138 -0
  452. alysis_code/workspace_provisioning.py +455 -0
  453. alysis_code-0.13.0.dist-info/METADATA +507 -0
  454. alysis_code-0.13.0.dist-info/RECORD +458 -0
  455. alysis_code-0.13.0.dist-info/WHEEL +4 -0
  456. alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
  457. alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
  458. 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()