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,475 @@
1
+ """Provider-call helpers for the main agent loop.
2
+
3
+ These functions are transport/compatibility shims around ``client.chat`` and
4
+ LLM error classification. They are used by the agent loop, session runtime,
5
+ and the non-repo response path alike, and carry no routing semantics of their
6
+ own; they live here so the agent loop does not depend on the router module for
7
+ its own provider calls.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from collections.abc import Callable
14
+ from typing import TYPE_CHECKING, Any
15
+
16
+ from ..language_policy import DEFAULT_REPLY_LANGUAGE, DEFAULT_REPLY_SCRIPT
17
+ from ..llm.base import effective_tools_for_client
18
+ from ..llm.types import LLMError
19
+ from .prompt_context import _INLINE_CODE_SPAN_RE
20
+ from .turn_path import (
21
+ _build_turn_language_system_message,
22
+ _normalize_turn_language_name,
23
+ _normalize_turn_script_name,
24
+ )
25
+
26
+ if TYPE_CHECKING:
27
+ from .tools_assembly import ToolDef
28
+
29
+
30
+ def _llm_error_status_code(err: LLMError) -> int | None:
31
+ match = re.match(r"LLM error (\d{3}):", str(err or "").strip())
32
+ if match is None:
33
+ return None
34
+ try:
35
+ return int(match.group(1))
36
+ except ValueError:
37
+ return None
38
+
39
+
40
+ def _is_fatal_non_repo_llm_error(err: LLMError) -> bool:
41
+ status_code = _llm_error_status_code(err)
42
+ # Provider/account and request-shape failures cannot be repaired by the
43
+ # static clarification route. Surface them after the transport's own
44
+ # compatibility retries instead of disguising them as a successful
45
+ # "Could you clarify..." response.
46
+ if status_code in {400, 401, 402, 403, 404, 422, 429}:
47
+ return True
48
+
49
+ msg = str(err).lower()
50
+ markers = (
51
+ "invalid_api_key",
52
+ "incorrect api key",
53
+ "api key",
54
+ "authentication",
55
+ "unauthorized",
56
+ "forbidden",
57
+ "permission denied",
58
+ "access denied",
59
+ "credit balance",
60
+ "purchase credits",
61
+ "billing",
62
+ "insufficient_quota",
63
+ "quota exhausted",
64
+ "quota exceeded",
65
+ "rate limit",
66
+ )
67
+ if any(marker in msg for marker in markers):
68
+ return True
69
+
70
+ # Recognized Alysis Code MiMo trial proxy errors (trial_expired,
71
+ # quota_exhausted, rate_limit_exceeded, ...) must propagate so the REPL's
72
+ # friendly-error renderer shows an actionable message, instead of being
73
+ # swallowed into the generic "Could you clarify..." fallback. Lazy import
74
+ # avoids an llm -> agent import cycle.
75
+ from ..llm.openai_compat import alysis_trial_error_message
76
+
77
+ return alysis_trial_error_message(err) is not None
78
+
79
+
80
+ def _is_stream_unsupported_error(err: LLMError) -> bool:
81
+ msg = str(err).lower()
82
+ mentions_stream = "stream" in msg or "sse" in msg
83
+ unsupported = "unsupported" in msg or "not support" in msg
84
+ bad_status = "llm error 400" in msg or "llm error 404" in msg or "llm error 422" in msg
85
+ return mentions_stream and (unsupported or bad_status)
86
+
87
+
88
+ def _tool_schema_function_name(tool_schema: dict[str, Any]) -> str:
89
+ if not isinstance(tool_schema, dict):
90
+ return ""
91
+ function = tool_schema.get("function")
92
+ if not isinstance(function, dict):
93
+ return ""
94
+ return str(function.get("name") or "").strip()
95
+
96
+
97
+ def _registered_tool_schema_list(
98
+ tool_defs: dict[str, ToolDef],
99
+ tool_list: list[dict[str, Any]] | None,
100
+ ) -> list[dict[str, Any]]:
101
+ if not tool_defs:
102
+ return []
103
+ canonical_by_name = {name: tool.as_openai_tool() for name, tool in tool_defs.items()}
104
+ ordered: list[dict[str, Any]] = []
105
+ added: set[str] = set()
106
+ for tool_schema in tool_list or []:
107
+ name = _tool_schema_function_name(tool_schema)
108
+ if name not in canonical_by_name or name in added:
109
+ continue
110
+ ordered.append(canonical_by_name[name])
111
+ added.add(name)
112
+ for name, tool_schema in canonical_by_name.items():
113
+ if name not in added:
114
+ ordered.append(tool_schema)
115
+ return ordered
116
+
117
+
118
+ def _main_agent_chat(
119
+ *,
120
+ client: Any,
121
+ messages: list[dict[str, Any]],
122
+ tools: list[dict[str, Any]] | None,
123
+ stream: bool,
124
+ on_text_delta: Callable[[str], None] | None,
125
+ on_reasoning_delta: Callable[[str], None] | None = None,
126
+ temperature: float | None = None,
127
+ max_tokens: int | None = None,
128
+ cancellation_token: Any | None = None,
129
+ tool_choice: Any | None = None,
130
+ ) -> Any:
131
+ tools = effective_tools_for_client(client, tools)
132
+ if tools is None:
133
+ tool_choice = None
134
+ kwargs: dict[str, Any] = {
135
+ "messages": messages,
136
+ "tools": tools,
137
+ "stream": stream,
138
+ "on_text_delta": on_text_delta,
139
+ "on_reasoning_delta": on_reasoning_delta,
140
+ "temperature": temperature,
141
+ }
142
+ # Pass the token only when present so older/test clients keep working via the
143
+ # TypeError fallbacks below; clients that accept it can abort an in-flight
144
+ # request the instant the user interrupts (even before the first token).
145
+ if cancellation_token is not None:
146
+ kwargs["cancellation_token"] = cancellation_token
147
+ if max_tokens is not None:
148
+ # Deliberately not part of the compatibility-removal loop: a bounded
149
+ # control-plane request must fail rather than silently become unbounded.
150
+ kwargs["max_tokens"] = max_tokens
151
+ if tool_choice is not None:
152
+ kwargs["tool_choice"] = tool_choice
153
+ for compatibility_arg in (
154
+ "cancellation_token",
155
+ "on_reasoning_delta",
156
+ "temperature",
157
+ "tool_choice",
158
+ ):
159
+ try:
160
+ return client.chat(**kwargs)
161
+ except TypeError:
162
+ kwargs.pop(compatibility_arg, None)
163
+ return client.chat(**kwargs)
164
+
165
+
166
+ def _client_supports_tool_calling(client: Any) -> bool:
167
+ return getattr(client, "supports_tool_calling", True) is not False
168
+
169
+
170
+ def _client_reasoning_or_thinking_active(client: Any) -> bool:
171
+ for attr in ("reasoning_active", "thinking_active"):
172
+ value = getattr(client, attr, None)
173
+ if isinstance(value, bool):
174
+ return value
175
+ if getattr(client, "enable_thinking", None) is True:
176
+ return True
177
+ reasoning_effort = str(getattr(client, "reasoning_effort", "") or "").strip().casefold()
178
+ return bool(reasoning_effort and reasoning_effort != "none")
179
+
180
+
181
+ def _client_supports_forced_tool_choice(client: Any) -> bool:
182
+ return (
183
+ getattr(client, "supports_forced_tool_choice", False) is True
184
+ and _client_supports_tool_calling(client)
185
+ and not _client_reasoning_or_thinking_active(client)
186
+ )
187
+
188
+
189
+ def _function_tool_choice(tool_name: str) -> dict[str, Any]:
190
+ return {"type": "function", "function": {"name": str(tool_name)}}
191
+
192
+
193
+ def _safe_forced_tool_choice_for_recovery(
194
+ *,
195
+ client: Any,
196
+ tools: list[dict[str, Any]] | None,
197
+ preferred_tool_names: tuple[str, ...],
198
+ ) -> dict[str, Any] | None:
199
+ if not _client_supports_forced_tool_choice(client):
200
+ return None
201
+ available: set[str] = set()
202
+ for tool in tools or []:
203
+ if not isinstance(tool, dict):
204
+ continue
205
+ name = _tool_schema_function_name(tool)
206
+ if name:
207
+ available.add(name)
208
+ for name in preferred_tool_names:
209
+ if name in available:
210
+ return _function_tool_choice(name)
211
+ return None
212
+
213
+
214
+ # ---------------------------------------------------------------------------
215
+ # Ephemeral request-message injection
216
+ # ---------------------------------------------------------------------------
217
+
218
+
219
+ _VOLATILE_HOST_CONTEXT_PREFIXES = (
220
+ "<workspace_binding_context>",
221
+ "<task_brief>",
222
+ "<environment_context>",
223
+ )
224
+
225
+
226
+ def _is_volatile_host_context_message(message: dict[str, Any]) -> bool:
227
+ if str(message.get("role") or "").strip().lower() != "user":
228
+ return False
229
+ content = message.get("content")
230
+ if not isinstance(content, str):
231
+ return False
232
+ stripped = content.lstrip()
233
+ return stripped.startswith(_VOLATILE_HOST_CONTEXT_PREFIXES)
234
+
235
+
236
+ def _request_messages_with_volatile_suffix(
237
+ *,
238
+ messages: list[dict[str, Any]],
239
+ turn_system_prompts: list[str] | tuple[str, ...] | None = None,
240
+ turn_user_contexts: list[str] | tuple[str, ...] | None = None,
241
+ step_system_prompts: list[str] | tuple[str, ...] | None = None,
242
+ ) -> list[dict[str, Any]]:
243
+ """Keep every call-varying host message after reusable transcript history.
244
+
245
+ Prompt caches match byte prefixes. Never place a replaceable host context or
246
+ an ephemeral turn/step message ahead of stable conversation content. This is
247
+ a request-local reorder: persistent history and the information sent to the
248
+ model are unchanged.
249
+ """
250
+
251
+ stable_messages: list[dict[str, Any]] = []
252
+ volatile_messages: list[dict[str, Any]] = []
253
+ for message in messages:
254
+ target = (
255
+ volatile_messages if _is_volatile_host_context_message(message) else stable_messages
256
+ )
257
+ target.append(message)
258
+
259
+ cleaned_turn_system = [str(prompt or "").strip() for prompt in (turn_system_prompts or [])]
260
+ cleaned_turn_user = [str(content or "").strip() for content in (turn_user_contexts or [])]
261
+ cleaned_step_system = [str(prompt or "").strip() for prompt in (step_system_prompts or [])]
262
+ return [
263
+ *stable_messages,
264
+ *volatile_messages,
265
+ *({"role": "system", "content": prompt} for prompt in cleaned_turn_system if prompt),
266
+ *({"role": "user", "content": content} for content in cleaned_turn_user if content),
267
+ *({"role": "system", "content": prompt} for prompt in cleaned_step_system if prompt),
268
+ ]
269
+
270
+
271
+ def _request_messages_with_ephemeral_system_prompts(
272
+ *,
273
+ messages: list[dict[str, Any]],
274
+ insert_index: int,
275
+ prompts: list[str] | tuple[str, ...] | None,
276
+ ) -> list[dict[str, Any]]:
277
+ cleaned_prompts = [str(prompt or "").strip() for prompt in (prompts or [])]
278
+ cleaned_prompts = [prompt for prompt in cleaned_prompts if prompt]
279
+ if not cleaned_prompts:
280
+ return list(messages)
281
+ bounded_index = max(0, min(len(messages), insert_index))
282
+ injected = [{"role": "system", "content": prompt} for prompt in cleaned_prompts]
283
+ return list(messages[:bounded_index]) + injected + list(messages[bounded_index:])
284
+
285
+
286
+ def _request_messages_with_ephemeral_system_prompt_suffixes(
287
+ *,
288
+ messages: list[dict[str, Any]],
289
+ prompts: list[str] | tuple[str, ...] | None,
290
+ ) -> list[dict[str, Any]]:
291
+ cleaned_prompts = [str(prompt or "").strip() for prompt in (prompts or [])]
292
+ cleaned_prompts = [prompt for prompt in cleaned_prompts if prompt]
293
+ if not cleaned_prompts:
294
+ return list(messages)
295
+ injected = [{"role": "system", "content": prompt} for prompt in cleaned_prompts]
296
+ return list(messages) + injected
297
+
298
+
299
+ def _request_messages_with_ephemeral_user_messages(
300
+ *,
301
+ messages: list[dict[str, Any]],
302
+ insert_index: int,
303
+ contents: list[str] | tuple[str, ...] | None,
304
+ ) -> list[dict[str, Any]]:
305
+ cleaned_contents = [str(content or "").strip() for content in (contents or [])]
306
+ cleaned_contents = [content for content in cleaned_contents if content]
307
+ if not cleaned_contents:
308
+ return list(messages)
309
+ bounded_index = max(0, min(len(messages), insert_index))
310
+ injected = [{"role": "user", "content": content} for content in cleaned_contents]
311
+ return list(messages[:bounded_index]) + injected + list(messages[bounded_index:])
312
+
313
+
314
+ # ---------------------------------------------------------------------------
315
+ # Final-summary language rewrite
316
+ # ---------------------------------------------------------------------------
317
+
318
+
319
+ _FINAL_SUMMARY_REWRITE_SYSTEM_PROMPT = """Rewrite one successful coding-task final summary into the selected reply language/script.
320
+
321
+ Output only the rewritten summary.
322
+ Preserve technical meaning exactly.
323
+ Do not add or remove implementation claims, test claims, warnings, or blockers.
324
+ Keep file paths, code identifiers, CLI commands, config keys, JSON keys, fenced code blocks, and inline code exactly as written.
325
+ If the summary is already in the requested language/script, return it unchanged.
326
+ """
327
+
328
+
329
+ _FENCED_CODE_BLOCK_RE = re.compile(r"```.*?```", re.DOTALL)
330
+
331
+
332
+ _REWRITE_PROTECTED_TOKEN_RE = re.compile(r"\b[A-Za-z0-9_./:-]*[./_-][A-Za-z0-9_./:-]+\b")
333
+
334
+
335
+ def _non_repo_chat(
336
+ *,
337
+ client: Any,
338
+ messages: list[dict[str, Any]],
339
+ temperature: float,
340
+ tools: list[dict[str, Any]] | None = None,
341
+ stream: bool = False,
342
+ on_text_delta: Callable[[str], None] | None = None,
343
+ on_reasoning_delta: Callable[[str], None] | None = None,
344
+ ) -> Any:
345
+ tools = effective_tools_for_client(client, tools)
346
+ base: dict[str, Any] = {"messages": messages, "tools": tools, "stream": stream}
347
+ try:
348
+ return client.chat(
349
+ **base,
350
+ on_text_delta=on_text_delta,
351
+ on_reasoning_delta=on_reasoning_delta,
352
+ temperature=temperature,
353
+ )
354
+ except TypeError:
355
+ pass
356
+ try:
357
+ return client.chat(**base, on_text_delta=on_text_delta, temperature=temperature)
358
+ except TypeError:
359
+ pass
360
+ try:
361
+ return client.chat(**base, temperature=temperature)
362
+ except TypeError:
363
+ return client.chat(**base)
364
+
365
+
366
+ def _extract_rewrite_protected_fragments(text: str) -> list[str]:
367
+ fragments: list[str] = []
368
+ seen: set[str] = set()
369
+
370
+ def _add(fragment: str) -> None:
371
+ candidate = str(fragment or "")
372
+ if not candidate or candidate in seen:
373
+ return
374
+ seen.add(candidate)
375
+ fragments.append(candidate)
376
+
377
+ for fragment in _FENCED_CODE_BLOCK_RE.findall(str(text or "")):
378
+ _add(fragment)
379
+ for fragment in _INLINE_CODE_SPAN_RE.findall(str(text or "")):
380
+ _add(fragment)
381
+ for token in _REWRITE_PROTECTED_TOKEN_RE.findall(str(text or "")):
382
+ _add(token)
383
+ return fragments
384
+
385
+
386
+ def _rewritten_text_preserves_technical_tokens(original: str, rewritten: str) -> bool:
387
+ if not original.strip():
388
+ return True
389
+ if not rewritten.strip():
390
+ return False
391
+ protected = _extract_rewrite_protected_fragments(original)
392
+ return all(fragment in rewritten for fragment in protected)
393
+
394
+
395
+ def _rewrite_final_summary_for_language(
396
+ *,
397
+ client: Any,
398
+ final_text: str,
399
+ language: str = "",
400
+ script: str = "",
401
+ explicit_language_override: bool = False,
402
+ record_usage: Callable[..., Any] | None = None,
403
+ ) -> tuple[str, dict[str, Any] | None]:
404
+ clean = str(final_text or "").strip()
405
+ if not clean:
406
+ return clean, None
407
+
408
+ resolved_language = _normalize_turn_language_name(language)
409
+ resolved_script = _normalize_turn_script_name(script)
410
+ if not (resolved_language or resolved_script):
411
+ return clean, None
412
+ if (
413
+ not explicit_language_override
414
+ and resolved_language == DEFAULT_REPLY_LANGUAGE
415
+ and resolved_script in {"", DEFAULT_REPLY_SCRIPT}
416
+ ):
417
+ return clean, None
418
+
419
+ payload: dict[str, Any] = {
420
+ "language": resolved_language,
421
+ "script": resolved_script,
422
+ "explicit_language_override": explicit_language_override,
423
+ }
424
+ language_directive = _build_turn_language_system_message(
425
+ resolved_language,
426
+ resolved_script,
427
+ explicit_language_override=explicit_language_override,
428
+ )
429
+ if not language_directive:
430
+ payload.update({"status": "skipped", "reason": "no_language_directive"})
431
+ return clean, payload
432
+
433
+ protected_fragments = _extract_rewrite_protected_fragments(clean)
434
+ payload["protected_fragment_count"] = len(protected_fragments)
435
+ rewrite_messages = [
436
+ {"role": "system", "content": _FINAL_SUMMARY_REWRITE_SYSTEM_PROMPT},
437
+ {"role": "system", "content": language_directive},
438
+ {
439
+ "role": "user",
440
+ "content": (
441
+ "Rewrite this final assistant summary only.\n\n"
442
+ "<assistant_summary>\n"
443
+ f"{clean}\n"
444
+ "</assistant_summary>"
445
+ ),
446
+ },
447
+ ]
448
+ try:
449
+ response = _non_repo_chat(client=client, messages=rewrite_messages, temperature=0.0)
450
+ except Exception as exc: # noqa: BLE001
451
+ payload.update({"status": "kept_original", "reason": "rewrite_error", "error": str(exc)})
452
+ return clean, payload
453
+
454
+ if record_usage is not None:
455
+ record_usage(
456
+ response=response,
457
+ messages=rewrite_messages,
458
+ tool_list=None,
459
+ operation="final_summary_language_rewrite",
460
+ )
461
+
462
+ rewritten = str(getattr(response, "content", "") or "").strip()
463
+ if not rewritten:
464
+ payload.update({"status": "kept_original", "reason": "empty_rewrite"})
465
+ return clean, payload
466
+ if not _rewritten_text_preserves_technical_tokens(clean, rewritten):
467
+ payload.update({"status": "kept_original", "reason": "protected_tokens_missing"})
468
+ return clean, payload
469
+ payload.update(
470
+ {
471
+ "status": "applied" if rewritten != clean else "unchanged",
472
+ "reason": "rewrite_succeeded",
473
+ }
474
+ )
475
+ return rewritten, payload
@@ -0,0 +1,227 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import StrEnum
5
+ from pathlib import Path, PurePosixPath
6
+
7
+ from ..runtime_artifacts import is_runtime_artifact_path
8
+
9
+
10
+ class MutationPathCategory(StrEnum):
11
+ BENIGN_RUNTIME_ARTIFACT = "BENIGN_RUNTIME_ARTIFACT"
12
+ MATERIAL_SOURCE_OR_CONFIG = "MATERIAL_SOURCE_OR_CONFIG"
13
+ MATERIAL_DELIVERABLE = "MATERIAL_DELIVERABLE"
14
+ UNKNOWN_MATERIAL = "UNKNOWN_MATERIAL"
15
+
16
+
17
+ _SOURCE_DIR_NAMES = {
18
+ "app",
19
+ "apps",
20
+ "bin",
21
+ "cmd",
22
+ "lib",
23
+ "pkg",
24
+ "packages",
25
+ "server",
26
+ "src",
27
+ "test",
28
+ "tests",
29
+ }
30
+ _SOURCE_SUFFIXES = {
31
+ ".c",
32
+ ".cc",
33
+ ".cpp",
34
+ ".cs",
35
+ ".css",
36
+ ".go",
37
+ ".h",
38
+ ".hpp",
39
+ ".html",
40
+ ".java",
41
+ ".js",
42
+ ".jsx",
43
+ ".kt",
44
+ ".mjs",
45
+ ".php",
46
+ ".py",
47
+ ".rb",
48
+ ".rs",
49
+ ".scala",
50
+ ".sh",
51
+ ".swift",
52
+ ".ts",
53
+ ".tsx",
54
+ ".vue",
55
+ }
56
+ _CONFIG_SUFFIXES = {".cfg", ".conf", ".ini", ".json", ".toml", ".yaml", ".yml"}
57
+ _CONFIG_FILENAMES = {
58
+ ".env",
59
+ "cargo.lock",
60
+ "cargo.toml",
61
+ "dockerfile",
62
+ "go.mod",
63
+ "go.sum",
64
+ "justfile",
65
+ "makefile",
66
+ "package-lock.json",
67
+ "package.json",
68
+ "pnpm-lock.yaml",
69
+ "pyproject.toml",
70
+ "requirements.txt",
71
+ "setup.cfg",
72
+ "setup.py",
73
+ "tsconfig.json",
74
+ "yarn.lock",
75
+ }
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class MutationPathClassification:
80
+ path: str
81
+ category: MutationPathCategory
82
+ reason: str
83
+ tracked: bool | None = None
84
+ existed_before: bool | None = None
85
+
86
+ @property
87
+ def is_material(self) -> bool:
88
+ return self.category != MutationPathCategory.BENIGN_RUNTIME_ARTIFACT
89
+
90
+ def as_payload(self) -> dict[str, object]:
91
+ return {
92
+ "path": self.path,
93
+ "category": self.category.value,
94
+ "reason": self.reason,
95
+ "tracked": self.tracked,
96
+ "existed_before": self.existed_before,
97
+ }
98
+
99
+
100
+ def _normalize_rel_path(path: str) -> str:
101
+ cleaned = str(path or "").strip().replace("\\", "/")
102
+ while cleaned.startswith("./"):
103
+ cleaned = cleaned[2:]
104
+ return cleaned
105
+
106
+
107
+ def _requested_paths_contain(path: str, requested_paths: set[str] | None) -> bool:
108
+ if not requested_paths:
109
+ return False
110
+ key = path.casefold()
111
+ return any(key == _normalize_rel_path(item).casefold() for item in requested_paths)
112
+
113
+
114
+ def _looks_like_source_or_config_path(path: str) -> bool:
115
+ pure = PurePosixPath(path)
116
+ parts = tuple(part.casefold() for part in pure.parts if part not in {"", "."})
117
+ if not parts:
118
+ return False
119
+ name = pure.name.casefold()
120
+ suffix = pure.suffix.casefold()
121
+ if name in _CONFIG_FILENAMES:
122
+ return True
123
+ if suffix in _CONFIG_SUFFIXES:
124
+ return True
125
+ if suffix in _SOURCE_SUFFIXES:
126
+ return True
127
+ return bool(set(parts[:-1]) & _SOURCE_DIR_NAMES)
128
+
129
+
130
+ def classify_mutation_path(
131
+ path: str,
132
+ *,
133
+ root: Path | None = None,
134
+ requested_paths: set[str] | None = None,
135
+ tracked: bool | None = None,
136
+ existed_before: bool | None = None,
137
+ command_was_verification: bool = False,
138
+ ) -> MutationPathClassification:
139
+ normalized = _normalize_rel_path(path)
140
+ if not normalized:
141
+ return MutationPathClassification(
142
+ path="",
143
+ category=MutationPathCategory.UNKNOWN_MATERIAL,
144
+ reason="empty_path",
145
+ tracked=tracked,
146
+ existed_before=existed_before,
147
+ )
148
+ if is_runtime_artifact_path(normalized, root=root):
149
+ return MutationPathClassification(
150
+ path=normalized,
151
+ category=MutationPathCategory.BENIGN_RUNTIME_ARTIFACT,
152
+ reason="runtime_artifact_policy",
153
+ tracked=tracked,
154
+ existed_before=existed_before,
155
+ )
156
+ if _requested_paths_contain(normalized, requested_paths) or existed_before is False:
157
+ return MutationPathClassification(
158
+ path=normalized,
159
+ category=MutationPathCategory.MATERIAL_DELIVERABLE,
160
+ reason="requested_or_new_output",
161
+ tracked=tracked,
162
+ existed_before=existed_before,
163
+ )
164
+ if _looks_like_source_or_config_path(normalized):
165
+ return MutationPathClassification(
166
+ path=normalized,
167
+ category=MutationPathCategory.MATERIAL_SOURCE_OR_CONFIG,
168
+ reason="source_or_config_path",
169
+ tracked=tracked,
170
+ existed_before=existed_before,
171
+ )
172
+ return MutationPathClassification(
173
+ path=normalized,
174
+ category=MutationPathCategory.UNKNOWN_MATERIAL,
175
+ reason="conservative_material_fallback",
176
+ tracked=tracked,
177
+ existed_before=existed_before,
178
+ )
179
+
180
+
181
+ def classify_mutation_paths(
182
+ paths: set[str] | list[str] | tuple[str, ...],
183
+ *,
184
+ root: Path | None = None,
185
+ requested_paths: set[str] | None = None,
186
+ command_was_verification: bool = False,
187
+ ) -> tuple[MutationPathClassification, ...]:
188
+ return tuple(
189
+ classify_mutation_path(
190
+ path,
191
+ root=root,
192
+ requested_paths=requested_paths,
193
+ command_was_verification=command_was_verification,
194
+ )
195
+ for path in sorted({str(item) for item in paths if str(item).strip()})
196
+ )
197
+
198
+
199
+ def material_mutation_paths(
200
+ paths: set[str] | list[str] | tuple[str, ...],
201
+ *,
202
+ root: Path | None = None,
203
+ requested_paths: set[str] | None = None,
204
+ command_was_verification: bool = False,
205
+ ) -> list[str]:
206
+ return [
207
+ item.path
208
+ for item in classify_mutation_paths(
209
+ paths,
210
+ root=root,
211
+ requested_paths=requested_paths,
212
+ command_was_verification=command_was_verification,
213
+ )
214
+ if item.is_material
215
+ ]
216
+
217
+
218
+ def benign_runtime_mutation_paths(
219
+ paths: set[str] | list[str] | tuple[str, ...],
220
+ *,
221
+ root: Path | None = None,
222
+ ) -> list[str]:
223
+ return [
224
+ item.path
225
+ for item in classify_mutation_paths(paths, root=root)
226
+ if item.category == MutationPathCategory.BENIGN_RUNTIME_ARTIFACT
227
+ ]