sase 0.1.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 (370) hide show
  1. sase/__init__.py +3 -0
  2. sase/__main__.py +6 -0
  3. sase/accept_workflow/__init__.py +26 -0
  4. sase/accept_workflow/conflict_check.py +226 -0
  5. sase/accept_workflow/parsing.py +183 -0
  6. sase/accept_workflow/renumber.py +549 -0
  7. sase/accept_workflow/workflow.py +395 -0
  8. sase/ace/AGENTS.md +24 -0
  9. sase/ace/CLAUDE.md +1 -0
  10. sase/ace/GEMINI.md +1 -0
  11. sase/ace/README.md +153 -0
  12. sase/ace/__init__.py +5 -0
  13. sase/ace/agent_runner.py +104 -0
  14. sase/ace/archive.py +185 -0
  15. sase/ace/changespec/__init__.py +160 -0
  16. sase/ace/changespec/locking.py +156 -0
  17. sase/ace/changespec/models.py +440 -0
  18. sase/ace/changespec/parser.py +346 -0
  19. sase/ace/changespec/raw_text.py +82 -0
  20. sase/ace/changespec/section_parsers.py +357 -0
  21. sase/ace/changespec/suffix_utils.py +78 -0
  22. sase/ace/changespec/validation.py +316 -0
  23. sase/ace/cl_status.py +37 -0
  24. sase/ace/comments/__init__.py +31 -0
  25. sase/ace/comments/core.py +117 -0
  26. sase/ace/comments/operations.py +456 -0
  27. sase/ace/constants.py +13 -0
  28. sase/ace/dismissed_agents.py +77 -0
  29. sase/ace/display.py +627 -0
  30. sase/ace/display_helpers.py +156 -0
  31. sase/ace/handlers/__init__.py +24 -0
  32. sase/ace/handlers/edit_hooks.py +304 -0
  33. sase/ace/handlers/mail.py +106 -0
  34. sase/ace/handlers/reword.py +375 -0
  35. sase/ace/handlers/show_diff.py +73 -0
  36. sase/ace/handlers/workflow_handlers.py +415 -0
  37. sase/ace/hint_types.py +29 -0
  38. sase/ace/hints.py +210 -0
  39. sase/ace/hooks/__init__.py +143 -0
  40. sase/ace/hooks/defaults.py +22 -0
  41. sase/ace/hooks/execution.py +672 -0
  42. sase/ace/hooks/history.py +81 -0
  43. sase/ace/hooks/mutations.py +505 -0
  44. sase/ace/hooks/processes.py +510 -0
  45. sase/ace/hooks/status.py +197 -0
  46. sase/ace/hooks/test_targets.py +176 -0
  47. sase/ace/hooks/timestamps.py +153 -0
  48. sase/ace/hooks/workflow_queries.py +170 -0
  49. sase/ace/last_selection.py +26 -0
  50. sase/ace/mail_ops.py +588 -0
  51. sase/ace/mentors.py +630 -0
  52. sase/ace/operations.py +261 -0
  53. sase/ace/query/__init__.py +62 -0
  54. sase/ace/query/evaluator.py +444 -0
  55. sase/ace/query/highlighting.py +285 -0
  56. sase/ace/query/parser.py +307 -0
  57. sase/ace/query/tokenizer.py +419 -0
  58. sase/ace/query/types.py +175 -0
  59. sase/ace/query_history.py +124 -0
  60. sase/ace/query_selection.py +52 -0
  61. sase/ace/restore.py +224 -0
  62. sase/ace/revert.py +190 -0
  63. sase/ace/saved_queries.py +165 -0
  64. sase/ace/saved_tag_names.py +79 -0
  65. sase/ace/scheduler/__init__.py +17 -0
  66. sase/ace/scheduler/checks_runner.py +528 -0
  67. sase/ace/scheduler/comments_handler.py +48 -0
  68. sase/ace/scheduler/hook_checks.py +538 -0
  69. sase/ace/scheduler/hooks_runner.py +655 -0
  70. sase/ace/scheduler/mentor_checks.py +630 -0
  71. sase/ace/scheduler/mentor_runner.py +223 -0
  72. sase/ace/scheduler/orphan_cleanup.py +68 -0
  73. sase/ace/scheduler/stale_running_cleanup.py +70 -0
  74. sase/ace/scheduler/suffix_transforms.py +374 -0
  75. sase/ace/scheduler/workflows_runner/__init__.py +16 -0
  76. sase/ace/scheduler/workflows_runner/completer.py +451 -0
  77. sase/ace/scheduler/workflows_runner/monitor.py +134 -0
  78. sase/ace/scheduler/workflows_runner/starter.py +597 -0
  79. sase/ace/status.py +37 -0
  80. sase/ace/sync_cache.py +106 -0
  81. sase/ace/tui/__init__.py +5 -0
  82. sase/ace/tui/_workflow_context.py +23 -0
  83. sase/ace/tui/actions/__init__.py +31 -0
  84. sase/ace/tui/actions/agent_workflow/__init__.py +21 -0
  85. sase/ace/tui/actions/agent_workflow/_agent_launch.py +411 -0
  86. sase/ace/tui/actions/agent_workflow/_editor.py +146 -0
  87. sase/ace/tui/actions/agent_workflow/_entry_points.py +223 -0
  88. sase/ace/tui/actions/agent_workflow/_prompt_bar.py +340 -0
  89. sase/ace/tui/actions/agent_workflow/_ref_resolution.py +143 -0
  90. sase/ace/tui/actions/agent_workflow/_types.py +26 -0
  91. sase/ace/tui/actions/agent_workflow/_workflow_exec.py +273 -0
  92. sase/ace/tui/actions/agents/__init__.py +9 -0
  93. sase/ace/tui/actions/agents/_core.py +309 -0
  94. sase/ace/tui/actions/agents/_folding.py +140 -0
  95. sase/ace/tui/actions/agents/_interaction.py +246 -0
  96. sase/ace/tui/actions/agents/_killing.py +469 -0
  97. sase/ace/tui/actions/agents/_notification_actions.py +463 -0
  98. sase/ace/tui/actions/agents/_notifications.py +196 -0
  99. sase/ace/tui/actions/agents/_revive.py +119 -0
  100. sase/ace/tui/actions/agents/_workflow_hitl.py +173 -0
  101. sase/ace/tui/actions/axe.py +264 -0
  102. sase/ace/tui/actions/axe_bgcmd.py +315 -0
  103. sase/ace/tui/actions/axe_display.py +358 -0
  104. sase/ace/tui/actions/base.py +695 -0
  105. sase/ace/tui/actions/changespec.py +434 -0
  106. sase/ace/tui/actions/clipboard.py +645 -0
  107. sase/ace/tui/actions/event_handlers.py +186 -0
  108. sase/ace/tui/actions/hints/__init__.py +19 -0
  109. sase/ace/tui/actions/hints/_accept.py +225 -0
  110. sase/ace/tui/actions/hints/_files.py +113 -0
  111. sase/ace/tui/actions/hints/_hooks.py +288 -0
  112. sase/ace/tui/actions/hints/_processing.py +290 -0
  113. sase/ace/tui/actions/hints/_rewind.py +138 -0
  114. sase/ace/tui/actions/hints/_types.py +44 -0
  115. sase/ace/tui/actions/marking.py +128 -0
  116. sase/ace/tui/actions/navigation/__init__.py +23 -0
  117. sase/ace/tui/actions/navigation/_advanced.py +197 -0
  118. sase/ace/tui/actions/navigation/_basic.py +222 -0
  119. sase/ace/tui/actions/navigation/_tree.py +283 -0
  120. sase/ace/tui/actions/navigation/_types.py +50 -0
  121. sase/ace/tui/actions/proposal_rebase.py +469 -0
  122. sase/ace/tui/actions/rename.py +245 -0
  123. sase/ace/tui/actions/sync.py +188 -0
  124. sase/ace/tui/app.py +481 -0
  125. sase/ace/tui/bgcmd.py +360 -0
  126. sase/ace/tui/changespec_history.py +141 -0
  127. sase/ace/tui/modals/__init__.py +69 -0
  128. sase/ace/tui/modals/agent_name_modal.py +65 -0
  129. sase/ace/tui/modals/base.py +80 -0
  130. sase/ace/tui/modals/command_history_modal.py +271 -0
  131. sase/ace/tui/modals/command_input_modal.py +76 -0
  132. sase/ace/tui/modals/confirm_delete_modal.py +53 -0
  133. sase/ace/tui/modals/confirm_kill_modal.py +53 -0
  134. sase/ace/tui/modals/help_modal/__init__.py +6 -0
  135. sase/ace/tui/modals/help_modal/bindings.py +235 -0
  136. sase/ace/tui/modals/help_modal/modal.py +250 -0
  137. sase/ace/tui/modals/help_modal/query_sections.py +236 -0
  138. sase/ace/tui/modals/hook_history_modal.py +183 -0
  139. sase/ace/tui/modals/notification_modal.py +374 -0
  140. sase/ace/tui/modals/parent_select_modal.py +42 -0
  141. sase/ace/tui/modals/plan_approval_modal.py +166 -0
  142. sase/ace/tui/modals/process_select_modal.py +152 -0
  143. sase/ace/tui/modals/project_select_modal.py +302 -0
  144. sase/ace/tui/modals/prompt_history_modal.py +298 -0
  145. sase/ace/tui/modals/query_edit_modal.py +64 -0
  146. sase/ace/tui/modals/rename_cl_modal.py +122 -0
  147. sase/ace/tui/modals/revive_agent_modal.py +263 -0
  148. sase/ace/tui/modals/runners_modal.py +515 -0
  149. sase/ace/tui/modals/status_modal.py +57 -0
  150. sase/ace/tui/modals/tag_input_modal.py +182 -0
  151. sase/ace/tui/modals/user_question_modal.py +568 -0
  152. sase/ace/tui/modals/workflow_hitl_modal.py +188 -0
  153. sase/ace/tui/modals/workflow_select_modal.py +42 -0
  154. sase/ace/tui/modals/workspace_input_modal.py +73 -0
  155. sase/ace/tui/modals/xprompt_select_modal.py +249 -0
  156. sase/ace/tui/models/__init__.py +21 -0
  157. sase/ace/tui/models/_loaders/__init__.py +31 -0
  158. sase/ace/tui/models/_loaders/_artifact_loaders.py +331 -0
  159. sase/ace/tui/models/_loaders/_changespec_loaders.py +159 -0
  160. sase/ace/tui/models/_loaders/_workflow_loaders.py +412 -0
  161. sase/ace/tui/models/_timestamps.py +156 -0
  162. sase/ace/tui/models/agent.py +331 -0
  163. sase/ace/tui/models/agent_loader.py +392 -0
  164. sase/ace/tui/models/fold_state.py +123 -0
  165. sase/ace/tui/models/workflow.py +102 -0
  166. sase/ace/tui/styles.tcss +1345 -0
  167. sase/ace/tui/thinking/__init__.py +18 -0
  168. sase/ace/tui/thinking/parser.py +218 -0
  169. sase/ace/tui/thinking/session_resolver.py +185 -0
  170. sase/ace/tui/widgets/__init__.py +39 -0
  171. sase/ace/tui/widgets/agent_detail.py +587 -0
  172. sase/ace/tui/widgets/agent_info_panel.py +76 -0
  173. sase/ace/tui/widgets/agent_list.py +378 -0
  174. sase/ace/tui/widgets/ancestors_children_panel.py +559 -0
  175. sase/ace/tui/widgets/axe_dashboard.py +489 -0
  176. sase/ace/tui/widgets/axe_info_panel.py +113 -0
  177. sase/ace/tui/widgets/bgcmd_list.py +174 -0
  178. sase/ace/tui/widgets/changespec_detail.py +430 -0
  179. sase/ace/tui/widgets/changespec_info_panel.py +59 -0
  180. sase/ace/tui/widgets/changespec_list.py +220 -0
  181. sase/ace/tui/widgets/comments_builder.py +67 -0
  182. sase/ace/tui/widgets/commits_builder.py +245 -0
  183. sase/ace/tui/widgets/file_panel.py +588 -0
  184. sase/ace/tui/widgets/hint_input_bar.py +171 -0
  185. sase/ace/tui/widgets/hint_tracker.py +15 -0
  186. sase/ace/tui/widgets/hooks_builder.py +286 -0
  187. sase/ace/tui/widgets/keybinding_footer.py +543 -0
  188. sase/ace/tui/widgets/mentors_builder.py +211 -0
  189. sase/ace/tui/widgets/notification_indicator.py +33 -0
  190. sase/ace/tui/widgets/prompt_input_bar.py +209 -0
  191. sase/ace/tui/widgets/prompt_panel/__init__.py +27 -0
  192. sase/ace/tui/widgets/prompt_panel/_agent_display.py +339 -0
  193. sase/ace/tui/widgets/prompt_panel/_helpers.py +250 -0
  194. sase/ace/tui/widgets/prompt_panel/_workflow_display.py +662 -0
  195. sase/ace/tui/widgets/section_builders.py +18 -0
  196. sase/ace/tui/widgets/suffix_formatting.py +145 -0
  197. sase/ace/tui/widgets/tab_bar.py +97 -0
  198. sase/ace/tui/widgets/thinking_panel.py +459 -0
  199. sase/ace/workflows/__init__.py +7 -0
  200. sase/ace/workflows/crs.py +231 -0
  201. sase/agent_names.py +155 -0
  202. sase/amend_workflow.py +255 -0
  203. sase/axe/__init__.py +78 -0
  204. sase/axe/check_cycles.py +276 -0
  205. sase/axe/chop_script_context.py +146 -0
  206. sase/axe/chop_script_runner.py +106 -0
  207. sase/axe/cli.py +185 -0
  208. sase/axe/config.py +112 -0
  209. sase/axe/hook_jobs.py +226 -0
  210. sase/axe/lumberjack.py +233 -0
  211. sase/axe/orchestrator.py +151 -0
  212. sase/axe/process.py +210 -0
  213. sase/axe/runner_pool.py +248 -0
  214. sase/axe/state.py +538 -0
  215. sase/axe_crs_runner.py +150 -0
  216. sase/axe_fix_hook_runner.py +262 -0
  217. sase/axe_mentor_runner.py +145 -0
  218. sase/axe_run_agent_runner.py +452 -0
  219. sase/axe_run_workflow_runner.py +276 -0
  220. sase/axe_runner_utils.py +116 -0
  221. sase/axe_summarize_hook_runner.py +171 -0
  222. sase/change_actions.py +651 -0
  223. sase/chat_history.py +235 -0
  224. sase/command_history.py +207 -0
  225. sase/commit_utils/__init__.py +41 -0
  226. sase/commit_utils/entries.py +439 -0
  227. sase/commit_utils/modifiers.py +392 -0
  228. sase/commit_utils/workspace.py +136 -0
  229. sase/commit_workflow/__init__.py +59 -0
  230. sase/commit_workflow/branch_info.py +23 -0
  231. sase/commit_workflow/changespec_operations.py +218 -0
  232. sase/commit_workflow/changespec_queries.py +75 -0
  233. sase/commit_workflow/cl_formatting.py +41 -0
  234. sase/commit_workflow/editor_utils.py +72 -0
  235. sase/commit_workflow/project_file_utils.py +45 -0
  236. sase/commit_workflow/workflow.py +303 -0
  237. sase/config.py +148 -0
  238. sase/crs_workflow.py +236 -0
  239. sase/default_config.yml +48 -0
  240. sase/gemini_wrapper/__init__.py +32 -0
  241. sase/gemini_wrapper/file_references.py +517 -0
  242. sase/gemini_wrapper/wrapper.py +168 -0
  243. sase/gh_workspace.py +425 -0
  244. sase/git_submit.py +329 -0
  245. sase/git_utils.py +49 -0
  246. sase/git_workspace.py +312 -0
  247. sase/github_config.py +14 -0
  248. sase/hook_history.py +120 -0
  249. sase/llm_provider/__init__.py +36 -0
  250. sase/llm_provider/_invoke.py +207 -0
  251. sase/llm_provider/_subprocess.py +200 -0
  252. sase/llm_provider/base.py +46 -0
  253. sase/llm_provider/claude.py +246 -0
  254. sase/llm_provider/config.py +25 -0
  255. sase/llm_provider/gemini.py +119 -0
  256. sase/llm_provider/postprocessing.py +244 -0
  257. sase/llm_provider/preprocessing.py +183 -0
  258. sase/llm_provider/registry.py +72 -0
  259. sase/llm_provider/types.py +27 -0
  260. sase/main/__init__.py +5 -0
  261. sase/main/cl_handler.py +171 -0
  262. sase/main/entry.py +389 -0
  263. sase/main/notify_handler.py +49 -0
  264. sase/main/parser.py +423 -0
  265. sase/main/plan_approve_handler.py +252 -0
  266. sase/main/query_handler/__init__.py +15 -0
  267. sase/main/query_handler/_editor.py +204 -0
  268. sase/main/query_handler/_query.py +471 -0
  269. sase/main/query_handler/_resume.py +76 -0
  270. sase/main/query_handler/special_cases.py +197 -0
  271. sase/main/user_question_handler.py +169 -0
  272. sase/main/utils.py +71 -0
  273. sase/mentor_config.py +190 -0
  274. sase/mentor_workflow.py +303 -0
  275. sase/metahook_config.py +107 -0
  276. sase/notifications/__init__.py +30 -0
  277. sase/notifications/models.py +55 -0
  278. sase/notifications/senders.py +148 -0
  279. sase/notifications/store.py +128 -0
  280. sase/plugin_discovery.py +54 -0
  281. sase/prompt_history.py +237 -0
  282. sase/py.typed +0 -0
  283. sase/renumber_utils.py +213 -0
  284. sase/rewind_workflow/__init__.py +9 -0
  285. sase/rewind_workflow/renumber.py +461 -0
  286. sase/rewind_workflow/workflow.py +199 -0
  287. sase/rich_utils.py +222 -0
  288. sase/running_field.py +575 -0
  289. sase/sase_utils.py +235 -0
  290. sase/scripts/__init__.py +142 -0
  291. sase/scripts/gh_setup.py +52 -0
  292. sase/scripts/git_setup.py +53 -0
  293. sase/scripts/hg_setup.py +71 -0
  294. sase/scripts/new_pr_desc_get_context.py +88 -0
  295. sase/scripts/pr_create_changespec.py +58 -0
  296. sase/scripts/sase_chop_cl_submitted_checks.py +28 -0
  297. sase/scripts/sase_chop_comment_checks.py +28 -0
  298. sase/scripts/sase_chop_comment_zombie_checks.py +37 -0
  299. sase/scripts/sase_chop_error_digest.py +29 -0
  300. sase/scripts/sase_chop_hook_checks.py +37 -0
  301. sase/scripts/sase_chop_mentor_checks.py +37 -0
  302. sase/scripts/sase_chop_orphan_cleanup.py +37 -0
  303. sase/scripts/sase_chop_pending_checks_poll.py +37 -0
  304. sase/scripts/sase_chop_stale_running_cleanup.py +33 -0
  305. sase/scripts/sase_chop_suffix_transforms.py +38 -0
  306. sase/scripts/sase_chop_wait_checks.py +91 -0
  307. sase/scripts/sase_chop_workflow_checks.py +37 -0
  308. sase/scripts/sase_commit_workflow +357 -0
  309. sase/scripts/sase_json_workflow +80 -0
  310. sase/scripts/sase_migrate_statuses +176 -0
  311. sase/scripts/sase_split_prepare_execute +116 -0
  312. sase/scripts/sase_split_setup +55 -0
  313. sase/scripts/sync_attempt.py +39 -0
  314. sase/scripts/sync_report.py +30 -0
  315. sase/scripts/sync_setup.py +36 -0
  316. sase/shared_utils.py +309 -0
  317. sase/status_state_machine/__init__.py +49 -0
  318. sase/status_state_machine/constants.py +88 -0
  319. sase/status_state_machine/field_updates.py +409 -0
  320. sase/status_state_machine/mail_suffix.py +96 -0
  321. sase/status_state_machine/transitions.py +636 -0
  322. sase/summarize_utils.py +37 -0
  323. sase/summarize_workflow.py +140 -0
  324. sase/vcs_provider/__init__.py +39 -0
  325. sase/vcs_provider/_base.py +234 -0
  326. sase/vcs_provider/_command_runner.py +105 -0
  327. sase/vcs_provider/_errors.py +21 -0
  328. sase/vcs_provider/_hookspec.py +154 -0
  329. sase/vcs_provider/_plugin_manager.py +201 -0
  330. sase/vcs_provider/_registry.py +174 -0
  331. sase/vcs_provider/_types.py +20 -0
  332. sase/vcs_provider/config.py +43 -0
  333. sase/vcs_provider/plugins/__init__.py +1 -0
  334. sase/vcs_provider/plugins/_git_common.py +301 -0
  335. sase/vcs_provider/plugins/bare_git.py +30 -0
  336. sase/workflow_base.py +27 -0
  337. sase/workflow_utils.py +194 -0
  338. sase/workspace_changespec.py +147 -0
  339. sase/xprompt/__init__.py +120 -0
  340. sase/xprompt/_disabled_regions.py +78 -0
  341. sase/xprompt/_exceptions.py +19 -0
  342. sase/xprompt/_fenced_blocks.py +52 -0
  343. sase/xprompt/_jinja.py +244 -0
  344. sase/xprompt/_parsing.py +471 -0
  345. sase/xprompt/_step_input_loader.py +70 -0
  346. sase/xprompt/directives.py +170 -0
  347. sase/xprompt/loader.py +747 -0
  348. sase/xprompt/models.py +250 -0
  349. sase/xprompt/output_validation.py +487 -0
  350. sase/xprompt/processor.py +304 -0
  351. sase/xprompt/workflow_executor.py +497 -0
  352. sase/xprompt/workflow_executor_loops.py +439 -0
  353. sase/xprompt/workflow_executor_parallel.py +362 -0
  354. sase/xprompt/workflow_executor_steps.py +49 -0
  355. sase/xprompt/workflow_executor_steps_embedded.py +593 -0
  356. sase/xprompt/workflow_executor_steps_prompt.py +409 -0
  357. sase/xprompt/workflow_executor_steps_script.py +363 -0
  358. sase/xprompt/workflow_executor_types.py +82 -0
  359. sase/xprompt/workflow_executor_utils.py +128 -0
  360. sase/xprompt/workflow_hitl.py +317 -0
  361. sase/xprompt/workflow_loader.py +347 -0
  362. sase/xprompt/workflow_loader_parse.py +430 -0
  363. sase/xprompt/workflow_models.py +280 -0
  364. sase/xprompt/workflow_output.py +432 -0
  365. sase/xprompt/workflow_runner.py +407 -0
  366. sase/xprompt/workflow_validator.py +579 -0
  367. sase-0.1.0.dist-info/METADATA +26 -0
  368. sase-0.1.0.dist-info/RECORD +370 -0
  369. sase-0.1.0.dist-info/WHEEL +4 -0
  370. sase-0.1.0.dist-info/entry_points.txt +22 -0
sase/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Structured Agentic Software Engineering."""
2
+
3
+ __version__ = "0.1.0"
sase/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Allow running the package as a module: python -m sase"""
2
+
3
+ from sase.main.entry import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,26 @@
1
+ """Accept workflow package for accepting proposed COMMITS entries."""
2
+
3
+ from .conflict_check import ConflictCheckResult, ConflictPair, run_conflict_check
4
+ from .parsing import (
5
+ expand_shorthand_proposals,
6
+ find_proposal_entry,
7
+ parse_proposal_entries,
8
+ parse_proposal_entries_with_shorthand,
9
+ parse_proposal_id,
10
+ )
11
+ from .renumber import renumber_commit_entries
12
+ from .workflow import AcceptWorkflow, main
13
+
14
+ __all__ = [
15
+ "AcceptWorkflow",
16
+ "ConflictCheckResult",
17
+ "ConflictPair",
18
+ "expand_shorthand_proposals",
19
+ "find_proposal_entry",
20
+ "main",
21
+ "parse_proposal_entries",
22
+ "parse_proposal_entries_with_shorthand",
23
+ "parse_proposal_id",
24
+ "renumber_commit_entries",
25
+ "run_conflict_check",
26
+ ]
@@ -0,0 +1,226 @@
1
+ """Conflict checking for accept workflow proposals."""
2
+
3
+ from dataclasses import dataclass
4
+ from itertools import combinations
5
+
6
+ from sase.ace.changespec import CommitEntry
7
+ from sase.commit_utils import apply_diffs_to_workspace, clean_workspace
8
+ from sase.rich_utils import print_status
9
+
10
+
11
+ @dataclass
12
+ class ConflictPair:
13
+ """A pair of proposals that conflict with each other."""
14
+
15
+ proposal_a: tuple[int, str] # (base_num, letter)
16
+ proposal_b: tuple[int, str]
17
+ error_message: str
18
+
19
+
20
+ @dataclass
21
+ class ConflictCheckResult:
22
+ """Result of a conflict check."""
23
+
24
+ success: bool
25
+ failed_proposal: tuple[int, str] | None
26
+ conflicting_pairs: list[ConflictPair]
27
+
28
+
29
+ def _format_proposal_id(base_num: int, letter: str) -> str:
30
+ """Format a proposal ID for display.
31
+
32
+ Args:
33
+ base_num: The base number of the proposal.
34
+ letter: The proposal letter.
35
+
36
+ Returns:
37
+ Formatted proposal ID like "(2a)".
38
+ """
39
+ return f"({base_num}{letter})"
40
+
41
+
42
+ def _apply_all_proposals(
43
+ workspace_dir: str,
44
+ proposals: list[tuple[int, str, CommitEntry]],
45
+ ) -> tuple[bool, str]:
46
+ """Apply all proposals together and check if they succeed.
47
+
48
+ Args:
49
+ workspace_dir: The workspace directory.
50
+ proposals: List of (base_num, letter, entry) tuples.
51
+
52
+ Returns:
53
+ Tuple of (success, error_message).
54
+ """
55
+ diff_paths = []
56
+ for _base_num, _letter, entry in proposals:
57
+ assert entry.diff is not None
58
+ diff_paths.append(entry.diff)
59
+
60
+ return apply_diffs_to_workspace(workspace_dir, diff_paths)
61
+
62
+
63
+ def _find_conflicting_pairs(
64
+ workspace_dir: str,
65
+ proposals: list[tuple[int, str, CommitEntry]],
66
+ ) -> list[ConflictPair]:
67
+ """Find all unique pairs of proposals that conflict.
68
+
69
+ Tests each unique pair by applying both diffs together in a single command.
70
+
71
+ Args:
72
+ workspace_dir: The workspace directory.
73
+ proposals: List of (base_num, letter, entry) tuples.
74
+
75
+ Returns:
76
+ List of ConflictPair for each pair that conflicts.
77
+ """
78
+ conflicting_pairs: list[ConflictPair] = []
79
+
80
+ for (num_a, letter_a, entry_a), (num_b, letter_b, entry_b) in combinations(
81
+ proposals, 2
82
+ ):
83
+ # Clean workspace before each pair test
84
+ clean_workspace(workspace_dir)
85
+
86
+ # Apply both proposals together
87
+ assert entry_a.diff is not None
88
+ assert entry_b.diff is not None
89
+ success, error_msg = apply_diffs_to_workspace(
90
+ workspace_dir, [entry_a.diff, entry_b.diff]
91
+ )
92
+ if not success:
93
+ conflicting_pairs.append(
94
+ ConflictPair(
95
+ proposal_a=(num_a, letter_a),
96
+ proposal_b=(num_b, letter_b),
97
+ error_message=error_msg,
98
+ )
99
+ )
100
+
101
+ return conflicting_pairs
102
+
103
+
104
+ def run_conflict_check(
105
+ workspace_dir: str,
106
+ validated_proposals: list[tuple[int, str, str | None, CommitEntry]],
107
+ verbose: bool = True,
108
+ ) -> ConflictCheckResult:
109
+ """Run conflict check on a set of proposals.
110
+
111
+ This function tests whether all proposals can be applied together using
112
+ a single hg import command. If they fail and there are >2 proposals,
113
+ it identifies which specific pairs conflict.
114
+
115
+ Args:
116
+ workspace_dir: The workspace directory to test in.
117
+ validated_proposals: List of (base_num, letter, msg, entry) tuples.
118
+ The msg field is ignored for conflict checking.
119
+ verbose: If True, print progress messages.
120
+
121
+ Returns:
122
+ ConflictCheckResult indicating success or failure details.
123
+ """
124
+ # Edge case: 0-1 proposals can't conflict
125
+ if len(validated_proposals) <= 1:
126
+ return ConflictCheckResult(
127
+ success=True,
128
+ failed_proposal=None,
129
+ conflicting_pairs=[],
130
+ )
131
+
132
+ # Extract just what we need for conflict checking
133
+ proposals = [
134
+ (base_num, letter, entry)
135
+ for base_num, letter, _msg, entry in validated_proposals
136
+ ]
137
+
138
+ if verbose:
139
+ print_status(
140
+ f"Running conflict check on {len(proposals)} proposals...",
141
+ "progress",
142
+ )
143
+
144
+ # Try applying all proposals together
145
+ success, error_msg = _apply_all_proposals(workspace_dir, proposals)
146
+
147
+ # Clean workspace after test
148
+ clean_workspace(workspace_dir)
149
+
150
+ if success:
151
+ return ConflictCheckResult(
152
+ success=True,
153
+ failed_proposal=None,
154
+ conflicting_pairs=[],
155
+ )
156
+
157
+ # Proposals failed to apply together
158
+ if verbose:
159
+ print_status(
160
+ f"Conflict detected when applying proposals together: {error_msg}",
161
+ "error",
162
+ )
163
+
164
+ # For 2 proposals, no need to check pairs - we know they conflict
165
+ if len(proposals) == 2:
166
+ if verbose:
167
+ print_status(
168
+ "Accept aborted. Try accepting non-conflicting proposals separately.",
169
+ "error",
170
+ )
171
+ return ConflictCheckResult(
172
+ success=False,
173
+ failed_proposal=None, # Can't determine which one failed
174
+ conflicting_pairs=[],
175
+ )
176
+
177
+ # For >2 proposals, find which specific pairs conflict
178
+ if verbose:
179
+ print_status("Checking which proposals conflict...", "progress")
180
+
181
+ conflicting_pairs = _find_conflicting_pairs(workspace_dir, proposals)
182
+
183
+ # Clean workspace after pair testing
184
+ clean_workspace(workspace_dir)
185
+
186
+ if verbose:
187
+ for pair in conflicting_pairs:
188
+ print_status(
189
+ f"Conflicting pair: {_format_proposal_id(*pair.proposal_a)} and "
190
+ f"{_format_proposal_id(*pair.proposal_b)}",
191
+ "error",
192
+ )
193
+ print_status(
194
+ "Accept aborted. Try accepting non-conflicting proposals separately.",
195
+ "error",
196
+ )
197
+
198
+ return ConflictCheckResult(
199
+ success=False,
200
+ failed_proposal=None, # Can't determine which one failed
201
+ conflicting_pairs=conflicting_pairs,
202
+ )
203
+
204
+
205
+ def format_conflict_message(result: ConflictCheckResult) -> str:
206
+ """Format conflict information for display.
207
+
208
+ Args:
209
+ result: The conflict check result.
210
+
211
+ Returns:
212
+ A formatted message describing the conflicts.
213
+ """
214
+ lines: list[str] = []
215
+
216
+ # Add conflicting pair messages (only if there are any)
217
+ for pair in result.conflicting_pairs:
218
+ lines.append(
219
+ f"Conflicting pair: {_format_proposal_id(*pair.proposal_a)} and "
220
+ f"{_format_proposal_id(*pair.proposal_b)}"
221
+ )
222
+
223
+ # Always add the abort message
224
+ lines.append("Accept aborted. Try accepting non-conflicting proposals separately.")
225
+
226
+ return "\n".join(lines)
@@ -0,0 +1,183 @@
1
+ """Proposal parsing and lookup functions for accept workflow."""
2
+
3
+ import re
4
+
5
+ from sase.ace.changespec import CommitEntry
6
+
7
+
8
+ def parse_proposal_id(proposal_id: str) -> tuple[int, str] | None:
9
+ """Parse a proposal ID into base number and letter.
10
+
11
+ Args:
12
+ proposal_id: The proposal ID (e.g., "2a", "2b").
13
+
14
+ Returns:
15
+ Tuple of (base_number, letter) or None if invalid.
16
+ """
17
+ match = re.match(r"^(\d+)([a-z])$", proposal_id)
18
+ if not match:
19
+ return None
20
+ return int(match.group(1)), match.group(2)
21
+
22
+
23
+ def parse_proposal_entries(args: list[str]) -> list[tuple[str, str | None]] | None:
24
+ """Parse proposal entry arguments into (id, msg) tuples.
25
+
26
+ Supports:
27
+ - New syntax: "2b(Add foobar field)" - id with optional message in parentheses
28
+ - Legacy syntax: "2b" followed by optional separate message argument
29
+
30
+ Args:
31
+ args: List of arguments (e.g., ["2a(msg)", "2b"] or ["2a", "msg"]).
32
+
33
+ Returns:
34
+ List of (id, msg) tuples or None if invalid format.
35
+ """
36
+ if not args:
37
+ return None
38
+
39
+ # Regex patterns
40
+ id_with_msg_pattern = re.compile(r"^(\d+[a-z])\((.+)\)$") # "2a(msg)"
41
+ bare_id_pattern = re.compile(r"^(\d+[a-z])$") # "2a"
42
+
43
+ entries: list[tuple[str, str | None]] = []
44
+
45
+ i = 0
46
+ while i < len(args):
47
+ arg = args[i]
48
+
49
+ # Check for new syntax with message in parentheses: "2a(msg)"
50
+ match_with_msg = id_with_msg_pattern.match(arg)
51
+ if match_with_msg:
52
+ proposal_id = match_with_msg.group(1)
53
+ msg = match_with_msg.group(2)
54
+ entries.append((proposal_id, msg))
55
+ i += 1
56
+ continue
57
+
58
+ # Check for bare ID: "2a"
59
+ match_bare = bare_id_pattern.match(arg)
60
+ if match_bare:
61
+ proposal_id = match_bare.group(1)
62
+ # Check if next arg is a message (not another proposal ID)
63
+ if i + 1 < len(args):
64
+ next_arg = args[i + 1]
65
+ # If next arg doesn't look like a proposal ID (with or without msg),
66
+ # treat it as a legacy message
67
+ if not id_with_msg_pattern.match(
68
+ next_arg
69
+ ) and not bare_id_pattern.match(next_arg):
70
+ entries.append((proposal_id, next_arg))
71
+ i += 2
72
+ continue
73
+ entries.append((proposal_id, None))
74
+ i += 1
75
+ continue
76
+
77
+ # Invalid format
78
+ return None
79
+
80
+ return entries if entries else None
81
+
82
+
83
+ def expand_shorthand_proposals(
84
+ args: list[str],
85
+ last_accepted_base: str | None,
86
+ ) -> list[str] | None:
87
+ """Expand shorthand proposal entries to full IDs.
88
+
89
+ Shorthand format:
90
+ - "a" -> "2a" (where 2 is last_accepted_base)
91
+ - "a(msg)" -> "2a(msg)"
92
+
93
+ Full format (pass through unchanged):
94
+ - "2a", "2b(msg)", etc.
95
+
96
+ Args:
97
+ args: List of arguments (may contain shorthand or full IDs).
98
+ last_accepted_base: The base number to prepend (e.g., "2").
99
+ If None, shorthand cannot be expanded.
100
+
101
+ Returns:
102
+ List of expanded arguments, or None if shorthand used but
103
+ last_accepted_base is not available or invalid format encountered.
104
+ """
105
+ # Regex patterns
106
+ shorthand_bare = re.compile(r"^([a-z])$") # "a"
107
+ shorthand_with_msg = re.compile(r"^([a-z])\((.+)\)$") # "a(msg)"
108
+ full_id_bare = re.compile(r"^\d+[a-z]$") # "2a"
109
+ full_id_with_msg = re.compile(r"^\d+[a-z]\(.+\)$") # "2a(msg)"
110
+
111
+ expanded: list[str] = []
112
+
113
+ for arg in args:
114
+ # Check if already full format
115
+ if full_id_bare.match(arg) or full_id_with_msg.match(arg):
116
+ expanded.append(arg)
117
+ continue
118
+
119
+ # Check for shorthand bare letter
120
+ match_bare = shorthand_bare.match(arg)
121
+ if match_bare:
122
+ if last_accepted_base is None:
123
+ return None # Cannot expand without base
124
+ letter = match_bare.group(1)
125
+ expanded.append(f"{last_accepted_base}{letter}")
126
+ continue
127
+
128
+ # Check for shorthand letter with message
129
+ match_with_msg = shorthand_with_msg.match(arg)
130
+ if match_with_msg:
131
+ if last_accepted_base is None:
132
+ return None
133
+ letter = match_with_msg.group(1)
134
+ msg = match_with_msg.group(2)
135
+ expanded.append(f"{last_accepted_base}{letter}({msg})")
136
+ continue
137
+
138
+ # Invalid format
139
+ return None
140
+
141
+ return expanded
142
+
143
+
144
+ def parse_proposal_entries_with_shorthand(
145
+ args: list[str],
146
+ last_accepted_base: str | None,
147
+ ) -> list[tuple[str, str | None]] | None:
148
+ """Parse proposal entries with shorthand expansion support.
149
+
150
+ Args:
151
+ args: List of arguments (shorthand or full format).
152
+ last_accepted_base: Base number for shorthand expansion.
153
+
154
+ Returns:
155
+ List of (id, msg) tuples or None if invalid.
156
+ """
157
+ expanded = expand_shorthand_proposals(args, last_accepted_base)
158
+ if expanded is None:
159
+ return None
160
+ return parse_proposal_entries(expanded)
161
+
162
+
163
+ def find_proposal_entry(
164
+ commits: list[CommitEntry] | None,
165
+ base_number: int,
166
+ letter: str,
167
+ ) -> CommitEntry | None:
168
+ """Find a proposal entry in commits by base number and letter.
169
+
170
+ Args:
171
+ commits: List of commit entries.
172
+ base_number: The base number (e.g., 2 for "2a").
173
+ letter: The proposal letter (e.g., "a" for "2a").
174
+
175
+ Returns:
176
+ The matching CommitEntry or None if not found.
177
+ """
178
+ if not commits:
179
+ return None
180
+ for entry in commits:
181
+ if entry.number == base_number and entry.proposal_letter == letter:
182
+ return entry
183
+ return None