ruby_reactor 0.8.4 → 0.8.5
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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/.specify/feature.json +1 -1
- data/CHANGELOG.md +196 -0
- data/CLAUDE.md +1 -1
- data/README.md +47 -11
- data/lib/ruby_reactor/dsl/async_macros.rb +30 -1
- data/lib/ruby_reactor/dsl/async_reactor_builder.rb +12 -6
- data/lib/ruby_reactor/dsl/compose_builder.rb +12 -6
- data/lib/ruby_reactor/dsl/interrupt_builder.rb +1 -3
- data/lib/ruby_reactor/dsl/map_builder.rb +0 -2
- data/lib/ruby_reactor/dsl/step_builder.rb +91 -19
- data/lib/ruby_reactor/error/argument_resolution_error.rb +19 -0
- data/lib/ruby_reactor/error/rescuable.rb +28 -0
- data/lib/ruby_reactor/executor/compensation_manager.rb +30 -26
- data/lib/ruby_reactor/executor/result_handler.rb +18 -16
- data/lib/ruby_reactor/executor/step_coordination.rb +11 -8
- data/lib/ruby_reactor/executor/step_executor.rb +59 -49
- data/lib/ruby_reactor/executor.rb +38 -4
- data/lib/ruby_reactor/map/collector.rb +21 -11
- data/lib/ruby_reactor/map/dispatcher.rb +29 -3
- data/lib/ruby_reactor/map/element_executor.rb +9 -3
- data/lib/ruby_reactor/map/helpers.rb +32 -2
- data/lib/ruby_reactor/map/result_enumerator.rb +18 -12
- data/lib/ruby_reactor/reactor.rb +24 -0
- data/lib/ruby_reactor/rspec/matchers.rb +19 -3
- data/lib/ruby_reactor/step/compose_step.rb +7 -1
- data/lib/ruby_reactor/step/map_step.rb +109 -4
- data/lib/ruby_reactor/step.rb +7 -0
- data/lib/ruby_reactor/step_worker.rb +46 -22
- data/lib/ruby_reactor/storage/adapter.rb +4 -0
- data/lib/ruby_reactor/storage/redis_adapter.rb +9 -0
- data/lib/ruby_reactor/storage/redis_reactor_scan.rb +1 -1
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor/web/api.rb +1 -1
- data/lib/ruby_reactor/web/public/assets/{index-CeZU-ESu.js → index-CQbgHtd0.js} +10 -10
- data/lib/ruby_reactor/web/public/index.html +1 -1
- data/lib/ruby_reactor/worker.rb +3 -1
- data/lib/ruby_reactor.rb +17 -6
- data/specs/007-execution-flow-analysis/analysis/README.md +147 -0
- data/specs/007-execution-flow-analysis/analysis/execution-order.md +359 -0
- data/specs/007-execution-flow-analysis/analysis/findings-and-options.md +502 -0
- data/specs/007-execution-flow-analysis/analysis/invariants.md +109 -0
- data/specs/007-execution-flow-analysis/checklists/requirements.md +39 -0
- data/specs/007-execution-flow-analysis/contracts/report-structure.md +71 -0
- data/specs/007-execution-flow-analysis/data-model.md +83 -0
- data/specs/007-execution-flow-analysis/evidence/harness.rb +229 -0
- data/specs/007-execution-flow-analysis/evidence/output.txt +333 -0
- data/specs/007-execution-flow-analysis/evidence/probes/01_plain.rb +122 -0
- data/specs/007-execution-flow-analysis/evidence/probes/02_compose.rb +182 -0
- data/specs/007-execution-flow-analysis/evidence/probes/03_map.rb +232 -0
- data/specs/007-execution-flow-analysis/evidence/probes/04_async.rb +132 -0
- data/specs/007-execution-flow-analysis/evidence/probes/05_background.rb +58 -0
- data/specs/007-execution-flow-analysis/evidence/probes/06_coordination.rb +158 -0
- data/specs/007-execution-flow-analysis/evidence/probes/07_interrupts_manual.rb +185 -0
- data/specs/007-execution-flow-analysis/evidence/run.rb +15 -0
- data/specs/007-execution-flow-analysis/plan.md +127 -0
- data/specs/007-execution-flow-analysis/quickstart.md +51 -0
- data/specs/007-execution-flow-analysis/research.md +202 -0
- data/specs/007-execution-flow-analysis/spec.md +270 -0
- data/specs/007-execution-flow-analysis/tasks.md +257 -0
- data/specs/008-rollback-reliability/checklists/requirements.md +43 -0
- data/specs/008-rollback-reliability/contracts/api-surface.md +126 -0
- data/specs/008-rollback-reliability/contracts/rollback-semantics.md +76 -0
- data/specs/008-rollback-reliability/data-model.md +139 -0
- data/specs/008-rollback-reliability/plan.md +233 -0
- data/specs/008-rollback-reliability/quickstart.md +105 -0
- data/specs/008-rollback-reliability/research.md +653 -0
- data/specs/008-rollback-reliability/spec.md +561 -0
- data/specs/008-rollback-reliability/tasks.md +1110 -0
- data/specs/future_improvements.md +48 -0
- metadata +35 -2
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
# Feature Specification: Reliable Rollback Across Constructs
|
|
2
|
+
|
|
3
|
+
**Feature Branch**: `execution_flow_analysis`
|
|
4
|
+
|
|
5
|
+
**Created**: 2026-09-26
|
|
6
|
+
|
|
7
|
+
**Status**: Draft, revised 2026-09-27 after the PR #65 review
|
|
8
|
+
|
|
9
|
+
**Input**: User description: "In this investigation specs/007-execution-flow-analysis/analysis/findings-and-options.md
|
|
10
|
+
we found issues that we should fix and make the flows predictable and reliable. This round is to at
|
|
11
|
+
least fix the High issues. We should consider if some refactor is needed. For example: Now that we
|
|
12
|
+
moved more features into the Step we could consider the step holding more logic and functionality
|
|
13
|
+
for its own processing and execution and lean the reactor coordination simpler delegating more
|
|
14
|
+
logic to the step. This is only a suggestion and maybe that not the solution, but it's worth
|
|
15
|
+
exploring."
|
|
16
|
+
|
|
17
|
+
## Context
|
|
18
|
+
|
|
19
|
+
The 007 analysis ([findings-and-options.md](../007-execution-flow-analysis/analysis/findings-and-options.md))
|
|
20
|
+
found four **High** findings. Each one leaves completed side effects in place without saying so, or
|
|
21
|
+
treats rolled-back work as done:
|
|
22
|
+
|
|
23
|
+
| Finding | Gap | Invariants |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| F-01 | Map elements that succeeded are never rolled back, whether the map fails or a later step fails | INV-19, INV-20 |
|
|
26
|
+
| F-02 | A retried composed reactor resumes a child whose earlier steps were already undone | INV-13 |
|
|
27
|
+
| F-03 | Some failures (argument preparation errors, non-standard exceptions) skip rollback entirely | INV-06 |
|
|
28
|
+
| F-04 | An async step's own `compensate`/`undo` are accepted by the DSL but never run | INV-24 |
|
|
29
|
+
|
|
30
|
+
This feature closes those four. It also closes the Medium/Low findings that the same rules fix:
|
|
31
|
+
F-05 (fan-out leftovers depend on scheduling), which has to be solved for F-01 to hold in fan-out
|
|
32
|
+
mode; F-06 (a raising condition compensates a step that never started), closed by removing
|
|
33
|
+
`where`/`guard`; and F-13 (failures without step attribution), which follows the same attribution
|
|
34
|
+
rule as F-03.
|
|
35
|
+
|
|
36
|
+
**Revision after the PR #65 review (2026-09-27)**. Four points in the first implementation were
|
|
37
|
+
wrong and are corrected here:
|
|
38
|
+
|
|
39
|
+
| Review point | Before | Now |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| A nested reactor must never be retried as a whole by its parent | `retries` on a `compose` started a fresh child per attempt | `retries` on a `compose` or an `async_reactor` is rejected. The child's own steps declare their retries. This closes F-02 by removing the path (007 option O-02-c) |
|
|
42
|
+
| `where`/`guard` are an old implementation that may be stale | kept, with raising conditions made "never started" | removed from the DSL. A step that should not run returns `Skipped` from its body. This closes F-06 by removing the path |
|
|
43
|
+
| All errors raised by reactor code must roll back | only standard errors rolled back. Every other exception marked the run aborted | every exception raised by reactor code rolls back. Only process-termination exceptions skip rollback |
|
|
44
|
+
| `Skipped` can mean "not required" or "already done" | a `Skipped` step was never undone, and it also suppressed an `after:` hand-off and a period mark | `Skipped` is an instrumentation mark that never changes execution; a skipped step is still never undone (FR-029, FR-030) |
|
|
45
|
+
|
|
46
|
+
**Readers**: reactor authors, who need to predict what gets rolled back, and RubyReactor
|
|
47
|
+
maintainers, who need to change rollback behavior safely.
|
|
48
|
+
|
|
49
|
+
## Clarifications
|
|
50
|
+
|
|
51
|
+
### Session 2026-09-26
|
|
52
|
+
|
|
53
|
+
- Q: How should a map roll back the elements that succeeded? → A: Automatic replay of each
|
|
54
|
+
succeeded element's own step `undo`s, the same as compose. No new map DSL (FR-006).
|
|
55
|
+
- Q: When should an `async_step`'s own `compensate` run? → A: Unit-local: once, in the unit's own
|
|
56
|
+
job, when its body finally fails. `undo` on an `async_step` is rejected at definition time
|
|
57
|
+
(FR-019, FR-020). Refined in planning: an `undo` inherited from a step class is warned, not
|
|
58
|
+
rejected (research R-09).
|
|
59
|
+
|
|
60
|
+
### Session 2026-09-27 (PR #65 review)
|
|
61
|
+
|
|
62
|
+
- Q: Should a `compose` keep `retries`, with a fresh child per attempt? → A: No. A composed reactor
|
|
63
|
+
is never retried as a whole by its parent. The child knows how to retry its own steps. `retries`
|
|
64
|
+
on a `compose` is rejected at definition time (FR-009, FR-010). This replaces the fresh-child
|
|
65
|
+
decision (research R-05). The same rule applies to `async_reactor`, the other construct that runs
|
|
66
|
+
a nested reactor.
|
|
67
|
+
- Q: Should `where`/`guard` stay? → A: No. Remove both completely. A step that decides it should
|
|
68
|
+
not run returns `Skipped` from its body (FR-014).
|
|
69
|
+
- Q: Which exceptions skip rollback? → A: Only the ones that mean the process is ending: a signal
|
|
70
|
+
(including an interrupt), a request to exit, and out of memory. Any other exception raised by
|
|
71
|
+
reactor code (a `run`, `compensate` or `undo` body, an argument source or transform, a step
|
|
72
|
+
definition) is a failure and rolls back, whether or not it is a standard error (FR-016, FR-018,
|
|
73
|
+
FR-028).
|
|
74
|
+
- Q: Is a `Skipped` step undone? → A: *(first defaulted to "no"; superseded by the review answer
|
|
75
|
+
below)*.
|
|
76
|
+
- Q (review 2026-09-27): What does `Skipped` change? → A: Nothing in execution. `Skipped` is an
|
|
77
|
+
instrumentation mark, so an engineer reviewing the execution can see the step did not need to
|
|
78
|
+
run: the run continues, a `background` hand-off happens as for a completed step, and a period
|
|
79
|
+
bucket is marked (FR-030). Rollback is the exception: a skipped step is never undone (FR-029;
|
|
80
|
+
briefly changed to "undone like Success", then reverted the same day).
|
|
81
|
+
- Q (review 2026-09-27): Cap a stored failure's backtrace? → A: Yes (FR-031).
|
|
82
|
+
- Q (review 2026-09-27): What must happen when a resume arrives while the reactor is compensating?
|
|
83
|
+
→ A: The resume fails; a resume is accepted only by a reactor paused at an interrupt step
|
|
84
|
+
(FR-032).
|
|
85
|
+
|
|
86
|
+
## User Scenarios & Testing *(mandatory)*
|
|
87
|
+
|
|
88
|
+
### User Story 1 - Succeeded map elements are rolled back (Priority: P1)
|
|
89
|
+
|
|
90
|
+
A reactor author uses a map to charge a list of orders. If the map fails partway through, or a later
|
|
91
|
+
step fails after the map completed, every order that was charged is refunded. This is the same
|
|
92
|
+
behavior a composed reactor's completed steps already get.
|
|
93
|
+
|
|
94
|
+
**Why this priority**: F-01 is the widest gap. Map is the main batch construct, the README promises
|
|
95
|
+
automatic rollback for it, and no test covers the case today.
|
|
96
|
+
|
|
97
|
+
**Independent Test**: Run a reactor `a → map(4 elements) → b` whose element steps record `run` and
|
|
98
|
+
`undo`. Check the recorded sequence (a) when element 2 fails and (b) when `b` fails. Do this in
|
|
99
|
+
inline mode and in fan-out mode.
|
|
100
|
+
|
|
101
|
+
**Acceptance Scenarios**:
|
|
102
|
+
|
|
103
|
+
1. **Given** an inline fail-fast map where elements 0 and 1 succeed and element 2 fails, **When**
|
|
104
|
+
the map fails, **Then** element 2 rolls itself back, then elements 1 and 0 are rolled back, then
|
|
105
|
+
the steps before the map are undone. The failure names the map step and the failing element.
|
|
106
|
+
2. **Given** a map that completed, **When** a later step fails, **Then** every succeeded element is
|
|
107
|
+
rolled back at the map's position in the parent's reverse-completion order, before earlier steps
|
|
108
|
+
are undone.
|
|
109
|
+
3. **Given** a fan-out fail-fast map where other elements are still running or already finished
|
|
110
|
+
when element 2 fails, **When** the map fails, **Then** every element that succeeded is rolled back,
|
|
111
|
+
including elements that finish after the failure was detected. The set of rolled-back elements
|
|
112
|
+
is the set of succeeded elements, whatever order the jobs ran in.
|
|
113
|
+
4. **Given** a map with `fail_fast false` that has both successes and failures, **When** a later step
|
|
114
|
+
fails, **Then** the successful elements are rolled back and the failed elements (already
|
|
115
|
+
self-rolled-back) are not rolled back again.
|
|
116
|
+
5. **Given** one element whose rollback fails, **When** the map rolls back, **Then** the other
|
|
117
|
+
elements still roll back, and the final failure's rollback failures list that element's failure
|
|
118
|
+
with the map step and element position.
|
|
119
|
+
6. **Given** nesting (a map inside a composed reactor, or a composed reactor inside a map element),
|
|
120
|
+
**When** rollback runs, **Then** it unwinds innermost-first, as composed reactors already do.
|
|
121
|
+
7. **Given** an execution containing a completed map, **When** it is undone manually, **Then** the
|
|
122
|
+
map's succeeded elements are rolled back too.
|
|
123
|
+
8. **Given** an existing map whose element steps already declare `undo`, **When** a later step
|
|
124
|
+
fails, **Then** those `undo` blocks now run for every succeeded element. No map-level declaration
|
|
125
|
+
is needed.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### User Story 2 - A nested reactor is never retried as a whole (Priority: P1)
|
|
130
|
+
|
|
131
|
+
A reactor author wants a sub-workflow to survive a transient failure. They declare `retries` on the
|
|
132
|
+
child's steps that can fail transiently. The child retries those steps itself. The parent never
|
|
133
|
+
re-runs the child as a whole: a child whose steps ran out of retries has failed, has rolled itself
|
|
134
|
+
back, and fails the parent. Declaring `retries` on the `compose` (or `async_reactor`) itself is
|
|
135
|
+
rejected, with a message that says where the retries belong.
|
|
136
|
+
|
|
137
|
+
**Why this priority**: F-02 today returns a success built on a result that was already undone. A
|
|
138
|
+
later failure then undoes only part of the child. It is silent data corruption. Retrying the whole
|
|
139
|
+
child from the parent is the path that causes it. The child already owns its steps' retry policies,
|
|
140
|
+
so the parent-level retry adds nothing but a second, conflicting retry layer.
|
|
141
|
+
|
|
142
|
+
**Independent Test**: Define a reactor that declares `retries` on a `compose` and check that the
|
|
143
|
+
class definition is rejected. Separately, run a composed child `c1 → c2` where `c2` declares
|
|
144
|
+
`retries` and fails on its first attempt only. Check that `c1` runs once, `c2` runs twice, the
|
|
145
|
+
compose succeeds, and a later parent failure undoes `c2` then `c1` once each.
|
|
146
|
+
|
|
147
|
+
**Acceptance Scenarios**:
|
|
148
|
+
|
|
149
|
+
1. **Given** a reactor that declares `retries` on a `compose`, in either the class form or the
|
|
150
|
+
inline block form, **When** the reactor class is defined, **Then** the definition is rejected
|
|
151
|
+
with a message naming the compose step and saying that retries belong on the child's own steps.
|
|
152
|
+
2. **Given** a reactor that declares `retries` on an `async_reactor`, **When** the reactor class is
|
|
153
|
+
defined, **Then** it is rejected the same way.
|
|
154
|
+
3. **Given** a composed child `c1 → c2` where `c2` declares `retries` and fails on its first attempt
|
|
155
|
+
only, **When** the parent runs, **Then** `c1` runs once, `c2` is retried inside the child, and the
|
|
156
|
+
compose result is the child's result.
|
|
157
|
+
4. **Given** the same reactor, where a later parent step then fails, **When** rollback runs,
|
|
158
|
+
**Then** `c2` and then `c1` are each undone exactly once.
|
|
159
|
+
5. **Given** a composed child whose step fails after its own retries are exhausted, **When** the
|
|
160
|
+
child fails, **Then** the child rolls back its completed steps, the compose fails, the parent's
|
|
161
|
+
completed steps are undone, and the child is not run again.
|
|
162
|
+
6. **Given** a child that is parked mid-run (contention wait or background hand-off) and later
|
|
163
|
+
resumed, **When** it resumes, **Then** the steps it completed before the park are **not** run
|
|
164
|
+
again. A resume is not a retry.
|
|
165
|
+
7. **Given** any execution that resumes after a rollback, **When** it continues, **Then** no
|
|
166
|
+
rolled-back step is treated as completed.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### User Story 3 - Every failure after completed work rolls back (Priority: P1)
|
|
171
|
+
|
|
172
|
+
A reactor author writes an argument transform, a dynamic argument source or a result path that
|
|
173
|
+
raises. Or a step body raises an exception that is not a standard error: a step class that does not
|
|
174
|
+
implement `run`, a runaway recursion, a custom exception class. Step `a` has already completed. The
|
|
175
|
+
author expects `a` to be undone and the failure to name the step that failed. Today nothing is
|
|
176
|
+
rolled back, and the failure has no step name or is not returned at all.
|
|
177
|
+
|
|
178
|
+
**Why this priority**: F-03 breaks the README's core promise ("automatically triggers compensation")
|
|
179
|
+
for common coding mistakes, and gives the reader nothing to trace.
|
|
180
|
+
|
|
181
|
+
**Independent Test**: Run `a → b`, where `b`'s argument transform raises. Check that `a` is undone,
|
|
182
|
+
`b` is not compensated, and the failure carries the step name `b` and the reason. Repeat with `b`'s
|
|
183
|
+
body raising a not-implemented error and a custom exception that is not a standard error: `a` is
|
|
184
|
+
undone, `b` is compensated, and the failure names `b`.
|
|
185
|
+
|
|
186
|
+
**Acceptance Scenarios**:
|
|
187
|
+
|
|
188
|
+
1. **Given** `a` completed and `b`'s argument transform, dynamic source or result path raises,
|
|
189
|
+
**When** the execution fails, **Then** `a` is undone, `b` is not compensated (its body never
|
|
190
|
+
started), and the failure names reactor, step `b` and the reason.
|
|
191
|
+
2. **Given** `a` completed and `b`'s body raises an exception that is not a standard error and is
|
|
192
|
+
not a process-termination exception (for example a not-implemented error, a stack overflow, or a
|
|
193
|
+
custom exception class), **When** the execution fails, **Then** `b` is compensated, `a` is
|
|
194
|
+
undone, and the failure is returned to the caller naming reactor, step `b` and the original
|
|
195
|
+
exception class.
|
|
196
|
+
3. **Given** argument preparation that happens in a worker (an async step unit, or the first step
|
|
197
|
+
after a `background before:` hand-off), **When** it raises, **Then** the same rule applies in
|
|
198
|
+
that process.
|
|
199
|
+
4. **Given** any other unexpected exception during an execution after at least one step completed,
|
|
200
|
+
**When** it happens, **Then** the completed steps are rolled back and the failure reports any
|
|
201
|
+
rollback that did not complete.
|
|
202
|
+
5. **Given** an inline execution interrupted by a process-termination exception (a signal including
|
|
203
|
+
an interrupt, a request to exit, or out of memory), **When** it propagates, **Then** no rollback
|
|
204
|
+
code runs in that process and the exception reaches the caller unchanged. The execution is
|
|
205
|
+
recorded as **aborted** with completed work outstanding, and the existing manual undo rolls it
|
|
206
|
+
back. Worker behavior (redelivery) is unchanged.
|
|
207
|
+
6. **Given** a rollback in progress, **When** a `compensate` or `undo` raises an exception that is
|
|
208
|
+
not a process-termination exception, **Then** it is recorded as a rollback failure for that
|
|
209
|
+
step and the remaining rollback continues.
|
|
210
|
+
7. **Given** a failing step whose own compensation also fails, **When** the failure is returned,
|
|
211
|
+
**Then** it still carries the reactor and step name.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
### User Story 4 - An async step's rollback hooks run as declared (Priority: P2)
|
|
216
|
+
|
|
217
|
+
A reactor author declares `compensate`/`undo` on an `async_step`, as the documentation says works.
|
|
218
|
+
Today those blocks never run. They are accepted and silently ignored.
|
|
219
|
+
|
|
220
|
+
**Why this priority**: fewer reactors use `async_step` rollback hooks than maps or composes, but a
|
|
221
|
+
DSL that accepts dead code and documentation that promises it runs are both traps.
|
|
222
|
+
|
|
223
|
+
**Independent Test**: Run an `async_step` unit whose body always fails and that declares
|
|
224
|
+
`compensate`. Drain its job and check that `compensate` ran once, after the last attempt.
|
|
225
|
+
Separately, declare `undo` on an `async_step` and check that the class definition is rejected.
|
|
226
|
+
|
|
227
|
+
**Acceptance Scenarios**:
|
|
228
|
+
|
|
229
|
+
1. **Given** an `async_step` whose body finally fails (after its retries) and which declares
|
|
230
|
+
`compensate`, **When** the unit fails, **Then** its `compensate` runs once, in the unit's own
|
|
231
|
+
job, after the last attempt. It runs whether or not any step reads the result.
|
|
232
|
+
2. **Given** an `async_step` unit whose first attempt fails and whose retry succeeds, **When** it
|
|
233
|
+
completes, **Then** `compensate` never runs.
|
|
234
|
+
3. **Given** a unit that compensated itself and a reader that then surfaces the unit's failure,
|
|
235
|
+
**When** the parent rolls back, **Then** the reader is compensated and the parent's completed
|
|
236
|
+
steps are undone, and the unit is **not** compensated a second time.
|
|
237
|
+
4. **Given** an `async_step` that declares an inline `undo`, **When** the reactor class is defined,
|
|
238
|
+
**Then** the declaration is rejected with a message explaining why and where to put the cleanup
|
|
239
|
+
instead. If the `undo` comes from the step class, a warning is emitted instead.
|
|
240
|
+
5. **Given** the documentation for async steps, **When** a reader follows it, **Then** it matches
|
|
241
|
+
the behavior.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
### User Story 5 - One rollback rule for every construct (Priority: P3)
|
|
246
|
+
|
|
247
|
+
A reactor author reads a reactor class and predicts what a failure will roll back from one rule:
|
|
248
|
+
*completed work is undone, work that started and failed is compensated, work that never started is
|
|
249
|
+
left alone*. Each construct (step, composed reactor, map, async step, async reactor) says what
|
|
250
|
+
"undo" and "compensate" mean for itself. A maintainer who changes one construct's rollback changes
|
|
251
|
+
it in that construct.
|
|
252
|
+
|
|
253
|
+
**Why this priority**: this is the structural goal behind the first four stories (the user's "step
|
|
254
|
+
owns its own lifecycle" direction). It pays off only once they land, and it is judged by review
|
|
255
|
+
more than by any single test.
|
|
256
|
+
|
|
257
|
+
**Independent Test**: Rebuild the F-10 rollback coverage table from the new behavior. Compose and
|
|
258
|
+
map rows read the same. Async rows differ only by the documented independence of async units.
|
|
259
|
+
|
|
260
|
+
**Acceptance Scenarios**:
|
|
261
|
+
|
|
262
|
+
1. **Given** the F-10 coverage table, **When** it is rebuilt after this feature, **Then** no cell
|
|
263
|
+
reads "left in place" for compose or map, and the async rows match the documentation.
|
|
264
|
+
2. **Given** the coordinator that runs a reactor, **When** a maintainer reviews it, **Then** it
|
|
265
|
+
applies the one rule above to every construct. No construct kind is excluded from rollback by a
|
|
266
|
+
special case in the coordinator. The construct's own definition decides what its rollback does.
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
### User Story 6 - One way to skip a step (Priority: P2)
|
|
271
|
+
|
|
272
|
+
A reactor author wants a step to do nothing under some condition. There is one way to say it: the
|
|
273
|
+
step's body returns `Skipped`. The older `where`/`guard` declarations, which decided before the
|
|
274
|
+
step started and followed their own failure rules, no longer exist. A reactor that still declares
|
|
275
|
+
them is rejected when it is defined, with a message that shows the replacement.
|
|
276
|
+
|
|
277
|
+
**Why this priority**: `where`/`guard` is an old, barely documented path with its own failure
|
|
278
|
+
behavior (F-06). It duplicates `Skipped`, and every rollback rule has to account for it. Removing it
|
|
279
|
+
removes a whole failure category instead of classifying it.
|
|
280
|
+
|
|
281
|
+
**Independent Test**: Define a step that declares `where`, and another that declares `guard`. Check
|
|
282
|
+
that each class definition is rejected with a message pointing to `Skipped`. Rewrite the same step
|
|
283
|
+
to return `Skipped` from its body and check that the reactor continues past it.
|
|
284
|
+
|
|
285
|
+
**Acceptance Scenarios**:
|
|
286
|
+
|
|
287
|
+
1. **Given** a step, async step or interrupt that declares `where` or `guard`, **When** the reactor
|
|
288
|
+
class is defined, **Then** the definition is rejected with a message naming the step and saying
|
|
289
|
+
to return `Skipped` from the step body instead.
|
|
290
|
+
2. **Given** a step whose body returns `Skipped`, **When** the reactor runs, **Then** everything
|
|
291
|
+
happens exactly as for `Success` (value, hand-off, period mark), the execution trace records
|
|
292
|
+
the skip, and a later failure does not undo it.
|
|
293
|
+
3. **Given** the README and `./documentation`, **When** a reader looks for `where`, `guard` or the
|
|
294
|
+
condition error, **Then** the only mentions are in the migration note.
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
### Edge Cases
|
|
299
|
+
|
|
300
|
+
- **Map with zero elements**: nothing to roll back. The map rollback is a no-op, not an error.
|
|
301
|
+
- **Every element fails (fail-fast)**: only the first failing element rolled back its own steps.
|
|
302
|
+
There are no succeeded elements to roll back.
|
|
303
|
+
- **Collect step raises after all elements succeeded**: the map fails, and every succeeded element is
|
|
304
|
+
rolled back.
|
|
305
|
+
- **Large map (10,000 elements)**: rollback must stay possible without the parent's stored state
|
|
306
|
+
growing past existing storage limits.
|
|
307
|
+
- **Element rollback under step locks**: element steps re-take their own locks for undo, the same as
|
|
308
|
+
any step undo today (including the `rollback_wait` skip-and-report behavior).
|
|
309
|
+
- **Element that returned `Halt`**: `Halt` semantics are unchanged. No rollback (F-07 is out of
|
|
310
|
+
scope).
|
|
311
|
+
- **Fan-out failure latency**: a fail-fast fan-out map reports failure only after the elements in
|
|
312
|
+
flight have finished and been rolled back. The latency grows to the slowest element in flight.
|
|
313
|
+
This is documented.
|
|
314
|
+
- **Existing reactor that declares `retries` on a `compose` or `async_reactor`**: it now fails when
|
|
315
|
+
its class is loaded, with the migration message. It does not silently run with one attempt.
|
|
316
|
+
- **Compose inline block that declares `retries` meaning "for the steps inside"**: rejected like any
|
|
317
|
+
compose-level `retries`. The message says to declare `retries` inside each child step.
|
|
318
|
+
- **Existing reactor that declares `where`/`guard`**: fails when its class is loaded, with the
|
|
319
|
+
migration message.
|
|
320
|
+
- **A `background before:` hand-off at a step that used a `where` condition**: the condition kept
|
|
321
|
+
the hand-off from happening. After migration the step's body returns `Skipped`, so the hand-off
|
|
322
|
+
happens at that step. This is documented in the migration note.
|
|
323
|
+
- **A step class that does not implement `run`**: its not-implemented error is a failure of that
|
|
324
|
+
step. Completed steps are undone.
|
|
325
|
+
- **A process-termination exception during a rollback that is already running**: the rollback
|
|
326
|
+
stops. The execution is recorded as aborted with the steps not yet undone still outstanding, and
|
|
327
|
+
manual undo finishes the rollback.
|
|
328
|
+
- **Awaited async result times out during argument preparation**: this already rolls back. It stays
|
|
329
|
+
unchanged.
|
|
330
|
+
- **Process-termination exception in a worker**: the job is redelivered and resumes from its last
|
|
331
|
+
checkpoint. Unchanged.
|
|
332
|
+
- **A step that returns `Skipped`, then a later step fails**: it is not undone (FR-029). The same
|
|
333
|
+
holds for the library's own skips (`with_period`, `with_ordered_lock`).
|
|
334
|
+
- **`background after: :x` where `:x` returns `Halt`**: the run halts; nothing is handed off.
|
|
335
|
+
|
|
336
|
+
## Requirements *(mandatory)*
|
|
337
|
+
|
|
338
|
+
### Functional Requirements
|
|
339
|
+
|
|
340
|
+
#### Map rollback (F-01, F-05)
|
|
341
|
+
|
|
342
|
+
- **FR-001**: When a map step fails, every element of that map that had succeeded MUST be rolled
|
|
343
|
+
back before the parent continues its own rollback.
|
|
344
|
+
- **FR-002**: When a step after a completed map fails, or the execution is undone manually, every
|
|
345
|
+
succeeded element of that map MUST be rolled back at the map step's position in reverse-completion
|
|
346
|
+
order.
|
|
347
|
+
- **FR-003**: Map rollback MUST produce the same set of rolled-back elements in inline and fan-out
|
|
348
|
+
modes. In fan-out mode it MUST include elements that were in flight when the failure was detected,
|
|
349
|
+
so that no succeeded element escapes rollback because of job scheduling.
|
|
350
|
+
- **FR-004**: An element that failed and already rolled itself back MUST NOT be rolled back again.
|
|
351
|
+
- **FR-005**: A rollback failure for one element MUST NOT stop rollback of the others. It MUST appear
|
|
352
|
+
in the final failure's rollback failures, attributed to the map step and the element position.
|
|
353
|
+
- **FR-006**: A succeeded element MUST be rolled back by replaying that element's own completed
|
|
354
|
+
steps' `undo`, in reverse completion order, exactly as a completed composed reactor is rolled back
|
|
355
|
+
today. The map adds no new rollback DSL; the `undo` blocks authors already write on element steps
|
|
356
|
+
become the element's rollback.
|
|
357
|
+
- **FR-007**: A map whose collection step fails after its elements ran MUST be treated as a failed
|
|
358
|
+
map. FR-001 applies to its succeeded elements.
|
|
359
|
+
- **FR-008**: Making maps rollback-capable MUST NOT make the parent execution's stored state grow per
|
|
360
|
+
element in a way that breaks maps that run today within storage limits.
|
|
361
|
+
|
|
362
|
+
#### Nested reactors are never retried as a whole (F-02)
|
|
363
|
+
|
|
364
|
+
- **FR-009** *(revised 2026-09-27)*: Declaring `retries` on a `compose` or an `async_reactor` MUST be
|
|
365
|
+
rejected when the reactor class is defined, in both the class form and the inline block form. The
|
|
366
|
+
message MUST name the step and say that retries belong on the child reactor's own steps.
|
|
367
|
+
- **FR-010** *(revised 2026-09-27)*: A parent MUST NOT run a failed nested child again. The only
|
|
368
|
+
retries inside a child are its own steps' retries. The compose result, and any later undo of the
|
|
369
|
+
compose, reflect the child's single run.
|
|
370
|
+
- **FR-011**: A resume that is not a retry (redelivery after a park, a contention wait, a background
|
|
371
|
+
hand-off) MUST keep resuming without re-running completed, not-rolled-back steps.
|
|
372
|
+
- **FR-012**: No execution MUST ever treat a rolled-back step as completed when it resumes.
|
|
373
|
+
|
|
374
|
+
#### Failures that skip rollback (F-03, F-06, F-13)
|
|
375
|
+
|
|
376
|
+
- **FR-013**: An exception raised while preparing a step's arguments (argument sources, transforms,
|
|
377
|
+
result paths) MUST fail that step, MUST NOT compensate it, and MUST undo all completed steps.
|
|
378
|
+
This covers every exception except the process-termination ones (FR-018).
|
|
379
|
+
- **FR-014** *(revised 2026-09-27)*: The `where` and `guard` step declarations MUST be removed.
|
|
380
|
+
Declaring either on a step, async step or interrupt MUST be rejected when the reactor class is
|
|
381
|
+
defined, with a message naming the step and saying to return `Skipped` from the step body instead.
|
|
382
|
+
With them goes the failure category they created (F-06): a condition that raises.
|
|
383
|
+
- **FR-015**: FR-013 MUST also hold when argument preparation happens in a worker process (an async
|
|
384
|
+
step unit, or a `background` hand-off).
|
|
385
|
+
- **FR-016** *(revised 2026-09-27)*: Any exception raised during an execution after at least one step
|
|
386
|
+
completed MUST roll back the completed steps, unless it is a process-termination exception
|
|
387
|
+
(FR-018). This includes exceptions that are not standard errors, for example a not-implemented
|
|
388
|
+
error, a load or syntax error from lazily loaded code, a stack overflow, or a custom exception
|
|
389
|
+
class. A step body that raises one of these MUST be compensated, like any step body failure.
|
|
390
|
+
- **FR-017**: Every failure produced under FR-013 and FR-016, and every failure whose compensation
|
|
391
|
+
itself failed, MUST carry the reactor name, the step name (when a step was executing), the reason
|
|
392
|
+
and the original exception class, with rollback failures attached as for any other failure.
|
|
393
|
+
- **FR-018** *(revised 2026-09-27)*: A process-termination exception MUST propagate to the caller
|
|
394
|
+
unchanged and MUST NOT run rollback code in the same process. Process-termination exceptions are
|
|
395
|
+
exactly: a signal (including an interrupt), a request to exit the process, out of memory, and the
|
|
396
|
+
interruption an enclosing timeout raises into the running code (it is not raised by reactor code,
|
|
397
|
+
and swallowing it would stop the caller's timeout from firing; research R-16). For
|
|
398
|
+
an inline execution, the execution MUST be recorded as **aborted** (distinct from running and
|
|
399
|
+
failed) while the process is still able to record it, and the existing manual undo MUST roll it
|
|
400
|
+
back.
|
|
401
|
+
|
|
402
|
+
#### Async step rollback hooks (F-04)
|
|
403
|
+
|
|
404
|
+
- **FR-019**: An `async_step` unit's own `compensate` MUST run in the unit's own job, exactly once,
|
|
405
|
+
when its body finally fails (after its last retry), whether or not any step reads the unit's
|
|
406
|
+
result. This mirrors how an `async_reactor` child rolls itself back. A failed attempt that is then
|
|
407
|
+
retried MUST NOT compensate.
|
|
408
|
+
- **FR-020**: An inline `undo` block declared on an `async_step` MUST be rejected when the reactor
|
|
409
|
+
class is defined, because nothing ever undoes an independent unit that succeeded. The message
|
|
410
|
+
MUST say where that cleanup belongs (the reading step's `compensate`, or an `async_reactor` child
|
|
411
|
+
whose steps declare `undo`). A step class that defines `undo` and is used with `async_step` MUST
|
|
412
|
+
produce a definition-time warning saying that `undo` will not run for this use. It is not an
|
|
413
|
+
error, because the same class is legitimately reused by ordinary steps (research R-09).
|
|
414
|
+
- **FR-021**: The outcome of an async unit's rollback, including any rollback failure, MUST be
|
|
415
|
+
recorded on the unit's own execution record and emitted through the existing observability events.
|
|
416
|
+
|
|
417
|
+
#### One rollback rule (F-10)
|
|
418
|
+
|
|
419
|
+
- **FR-022**: Each construct kind (step, composed reactor, map, async step, async reactor) MUST
|
|
420
|
+
define what its own compensate and undo do. The coordinator MUST apply one rule to all of them:
|
|
421
|
+
completed work is recorded for undo, work that started and failed is compensated, work that never
|
|
422
|
+
started is not compensated.
|
|
423
|
+
- **FR-023**: The documented independence of async units from their parent's rollback MUST be
|
|
424
|
+
expressed by the async constructs' own undo definitions, not by a coordinator special case.
|
|
425
|
+
|
|
426
|
+
#### Documentation, demo and tests
|
|
427
|
+
|
|
428
|
+
- **FR-024**: Every README.md and `./documentation` claim listed in the 007 documentation audit for
|
|
429
|
+
an in-scope finding MUST be corrected in the same change as its fix. The documentation MUST NOT
|
|
430
|
+
describe `retries` on a `compose`/`async_reactor`, `where`, `guard` or the condition error outside
|
|
431
|
+
the migration notes, and MUST describe the aborted status as the result of a process-termination
|
|
432
|
+
exception only.
|
|
433
|
+
- **FR-025**: Each user-visible behavior change MUST ship with a demo reactor, a listed demo rake
|
|
434
|
+
task and a demo spec written with the shipped matchers (Constitution VI): map rollback, a compose
|
|
435
|
+
whose child step retries on its own (replacing the compose retry demo), failure rollback for
|
|
436
|
+
argument errors and for an exception that is not a standard error, async step hooks, and a step
|
|
437
|
+
that skips itself by returning `Skipped` (replacing any `where`/`guard` example).
|
|
438
|
+
- **FR-026**: CHANGELOG.md MUST record each behavior change under the correct heading. Breaking
|
|
439
|
+
changes MUST include a migration note. The removal of `retries` on `compose`/`async_reactor` and
|
|
440
|
+
of `where`/`guard` are breaking, and each migration note MUST show the replacement.
|
|
441
|
+
- **FR-027**: Each invariant this feature makes hold MUST be covered by at least one automated test
|
|
442
|
+
against real infrastructure that fails on the pre-change behavior. The existing async step test
|
|
443
|
+
that passes whether or not the unit's hooks run MUST be tightened.
|
|
444
|
+
|
|
445
|
+
#### Rollback code and skipped steps (review 2026-09-27)
|
|
446
|
+
|
|
447
|
+
- **FR-028**: A `compensate` or `undo` that raises any exception other than a process-termination
|
|
448
|
+
exception MUST be recorded as a rollback failure for its step, and the remaining rollback MUST
|
|
449
|
+
continue.
|
|
450
|
+
- **FR-029** *(revised 2026-09-27)*: `Skipped` is an instrumentation mark (execution trace,
|
|
451
|
+
`skipped?`, telemetry) and MUST NOT change execution. A `Skipped` step MUST NOT be undone or
|
|
452
|
+
compensated: it had nothing to do.
|
|
453
|
+
- **FR-030**: A `background after: :x` hand-off MUST fire when `:x` returns `Skipped`, and MUST NOT
|
|
454
|
+
fire when `:x` returns `Halt`. A `with_period` step whose body returns `Skipped` MUST mark its
|
|
455
|
+
bucket.
|
|
456
|
+
- **FR-031**: A stored failure MUST keep at most 100 backtrace frames, so a stack overflow does not
|
|
457
|
+
inflate the stored context.
|
|
458
|
+
- **FR-032**: A resume (`continue`) MUST be accepted only while the execution is paused at an
|
|
459
|
+
interrupt step. A resume that arrives while the execution is running or rolling back MUST fail
|
|
460
|
+
without changing it. With several pending interrupts, each takes its resume once the run has
|
|
461
|
+
paused again. A resume for a second pending interrupt that arrives while the first resume is
|
|
462
|
+
still executing is rejected for now (deferred, `specs/future_improvements.md`).
|
|
463
|
+
|
|
464
|
+
### Key Entities
|
|
465
|
+
|
|
466
|
+
- **Construct**: a unit of work in a reactor: a step, composed reactor, map, async step or async
|
|
467
|
+
reactor. Each defines its own forward run, compensate and undo.
|
|
468
|
+
- **Undo record**: what an execution keeps about a completed construct so it can undo it later
|
|
469
|
+
(construct, its arguments, its result). For a map this includes whatever is needed to roll back
|
|
470
|
+
each succeeded element.
|
|
471
|
+
- **Element outcome**: per map element: succeeded, failed (self-rolled-back), skipped (never
|
|
472
|
+
started), or in flight. Rollback coverage is decided from this.
|
|
473
|
+
- **Attempt**: one try of a retried step. Only steps declare retries. A nested reactor (compose,
|
|
474
|
+
async reactor) has exactly one run per parent step.
|
|
475
|
+
- **Rollback failure**: a compensate or undo that did not complete, attributed to its construct
|
|
476
|
+
(and element position for maps), reported on the final failure.
|
|
477
|
+
- **Process-termination exception**: a signal (including an interrupt), a request to exit the
|
|
478
|
+
process, out of memory, or an enclosing timeout's interruption. The only exceptions that skip
|
|
479
|
+
rollback.
|
|
480
|
+
- **Aborted execution**: an inline execution cut short by a process-termination exception. Its
|
|
481
|
+
completed work is still outstanding, and it can be found and undone manually.
|
|
482
|
+
- **Skipped step**: a step whose body returned `Skipped`. The run continues as for a `Success`;
|
|
483
|
+
the trace marks it, and it is never undone (FR-029).
|
|
484
|
+
|
|
485
|
+
## Success Criteria *(mandatory)*
|
|
486
|
+
|
|
487
|
+
### Measurable Outcomes
|
|
488
|
+
|
|
489
|
+
- **SC-001**: The invariants tied to in-scope findings change status to HOLDS: INV-06, INV-13,
|
|
490
|
+
INV-19, INV-20 and INV-24 (VIOLATED today), and INV-07 and INV-22 (CONDITIONAL today; for INV-22
|
|
491
|
+
the "left in place" clause, since which fan-out elements run stays scheduling-dependent). Each is
|
|
492
|
+
covered by at least one automated test that fails on the 0.8.3 baseline. After the review, INV-13
|
|
493
|
+
holds because no nested reactor can be retried as a whole (its test is the definition-time
|
|
494
|
+
rejection), INV-07 no longer lists `where`/`guard`, and INV-06 holds for every exception except
|
|
495
|
+
process-termination ones.
|
|
496
|
+
- **SC-002**: When the 63 scenarios of the 007 evidence set are re-run, every scenario tied to an
|
|
497
|
+
in-scope finding produces its corrected sequence. Every other scenario produces the same sequence
|
|
498
|
+
as the baseline (0 unintended changes).
|
|
499
|
+
- **SC-003**: Across 100 runs of a fail-fast fan-out map with randomized job order, 0 runs leave a
|
|
500
|
+
succeeded element without rollback.
|
|
501
|
+
- **SC-004**: In the 007 failure-kinds table, every failure after completed work either rolls
|
|
502
|
+
completed work back or, for process-termination exceptions only, leaves an execution recorded as
|
|
503
|
+
aborted that manual undo rolls back. 0 rows end with nothing rolled back and nothing reported.
|
|
504
|
+
- **SC-005**: 100% of failures produced on in-scope paths carry reactor name, step name (where a step
|
|
505
|
+
was executing) and reason.
|
|
506
|
+
- **SC-006**: A 10,000-element map can fail and roll back all its succeeded elements without a
|
|
507
|
+
storage size error.
|
|
508
|
+
- **SC-007**: Every row of the 007 documentation audit tied to an in-scope finding reads CONFIRMED
|
|
509
|
+
against the new behavior.
|
|
510
|
+
- **SC-008**: The rebuilt F-10 coverage table has no "left in place" cell for compose or map.
|
|
511
|
+
- **SC-009**: The full test suite, the style checks and the demo acceptance tasks pass.
|
|
512
|
+
- **SC-010**: Each exception kind named in FR-016 (not-implemented, load or syntax error, stack
|
|
513
|
+
overflow, custom exception class), raised from a step body and from an argument transform, rolls
|
|
514
|
+
back completed work in an automated test. 0 of them leave the execution aborted.
|
|
515
|
+
- **SC-011**: 0 ways remain to retry a nested reactor as a whole or to declare `where`/`guard`: each
|
|
516
|
+
one is rejected at definition time by an automated test, and the README and `./documentation`
|
|
517
|
+
mention them only in migration notes.
|
|
518
|
+
|
|
519
|
+
## Assumptions
|
|
520
|
+
|
|
521
|
+
- **Scope**: the four High findings, plus F-05, F-06 and F-13, which the same rules fix. F-02 and
|
|
522
|
+
F-06 are closed by removing the constructs that caused them (compose `retries`, `where`/`guard`),
|
|
523
|
+
not by fixing their behavior. Out of
|
|
524
|
+
scope: F-07 (nested `Halt`), F-08 (manual undo outside the reactor lock), F-09 (async units running
|
|
525
|
+
after their dispatcher rolled back), F-11, F-12, F-14, F-15 and F-16. They stay as documented in
|
|
526
|
+
the 007 analysis.
|
|
527
|
+
- **Refactor direction**: planning evaluates the "each step owns its own lifecycle" direction and
|
|
528
|
+
adopts it where it makes these fixes simpler and removes construct-kind special cases from the
|
|
529
|
+
coordinator (FR-022, FR-023). Moving more (retries, coordination, argument preparation) into steps
|
|
530
|
+
is adopted only if it reduces the complexity of these fixes (Constitution V, YAGNI). The decision
|
|
531
|
+
and its alternatives are recorded in the planning research.
|
|
532
|
+
- **Async independence stays**: a parent's rollback still does not cancel or undo async units
|
|
533
|
+
(INV-25, documented). FR-023 changes only where that rule lives.
|
|
534
|
+
- **Retry ownership**: a child reactor owns the retries of its own steps. The parent never retries
|
|
535
|
+
a nested reactor as a whole (review 2026-09-27). `async_reactor` follows the same rule as
|
|
536
|
+
`compose`: its `retries` could only re-dispatch, or in inline mode re-run, the whole child.
|
|
537
|
+
- **`where`/`guard` removal**: they are removed, not deprecated, because they are an old path with
|
|
538
|
+
their own failure rules and `Skipped` covers the use. Code that relied on a condition to prevent a
|
|
539
|
+
`background` hand-off changes behavior and is called out in the migration note.
|
|
540
|
+
- **Fan-out failure latency**: waiting for elements in flight is accepted in exchange for
|
|
541
|
+
predictable rollback.
|
|
542
|
+
- **Process-termination exceptions**: running user rollback code while the process is being
|
|
543
|
+
signalled, is exiting or is out of memory is unsafe, so FR-018 records the execution for later
|
|
544
|
+
undo instead. Every other exception comes from reactor code (a body, a transform, a definition)
|
|
545
|
+
and is a failure of that code. Rolling it back is what the saga promises (review 2026-09-27).
|
|
546
|
+
- **Baseline**: commit `faf90e8d` (0.8.3 + #61, #63). The 007 evidence harness is reused as the
|
|
547
|
+
regression check for SC-002.
|
|
548
|
+
- **Versioning**: behavior changes follow Constitution V. Two are knowingly breaking and need
|
|
549
|
+
migration notes: element-step `undo` blocks now run when a map is rolled back (FR-006), and
|
|
550
|
+
`undo` on an `async_step` is now rejected (FR-020). An `async_step`'s `compensate` that never ran
|
|
551
|
+
before now runs (FR-019). The review adds three more breaking changes: `retries` on
|
|
552
|
+
`compose`/`async_reactor` is rejected (FR-009), `where`/`guard` are removed (FR-014), and
|
|
553
|
+
exceptions that are not standard errors now roll back instead of propagating (FR-016). Planning
|
|
554
|
+
decides the SemVer level of each change.
|
|
555
|
+
- **Deferred by the review (2026-09-27)**, recorded in `specs/future_improvements.md`: a resume for
|
|
556
|
+
a second pending interrupt while the first executes; re-running an interrupted `compensate` of
|
|
557
|
+
the failing step on manual undo; two resumes in the same instant (accepted).
|
|
558
|
+
- **Revision of existing work**: the first implementation of this feature is already on the branch.
|
|
559
|
+
Planning updates the research decisions this revision reverses (R-05 fresh child per attempt,
|
|
560
|
+
R-06 condition errors, R-08 aborted on every non-standard exception) and the tasks that
|
|
561
|
+
implemented them.
|