ruby_reactor 0.8.3 → 0.8.4

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/.specify/feature.json +1 -1
  4. data/CHANGELOG.md +12 -0
  5. data/CLAUDE.md +1 -1
  6. data/README.md +93 -91
  7. data/lib/ruby_reactor/dsl/async_reactor_builder.rb +3 -6
  8. data/lib/ruby_reactor/dsl/compose_builder.rb +3 -10
  9. data/lib/ruby_reactor/dsl/reactor.rb +8 -11
  10. data/lib/ruby_reactor/dsl/retryable.rb +45 -0
  11. data/lib/ruby_reactor/dsl/step_builder.rb +40 -14
  12. data/lib/ruby_reactor/error/undeclared_input_error.rb +13 -0
  13. data/lib/ruby_reactor/executor/compensation_manager.rb +2 -2
  14. data/lib/ruby_reactor/executor/result_handler.rb +3 -2
  15. data/lib/ruby_reactor/executor/retry_manager.rb +11 -12
  16. data/lib/ruby_reactor/rspec/test_subject.rb +3 -5
  17. data/lib/ruby_reactor/step/async_reactor_step.rb +6 -2
  18. data/lib/ruby_reactor/step/compose_step.rb +8 -4
  19. data/lib/ruby_reactor/step/input_contract.rb +5 -0
  20. data/lib/ruby_reactor/step/inputs.rb +57 -0
  21. data/lib/ruby_reactor/step/map_step.rb +32 -22
  22. data/lib/ruby_reactor/step.rb +13 -7
  23. data/lib/ruby_reactor/version.rb +1 -1
  24. data/lib/ruby_reactor.rb +3 -1
  25. data/specs/006-step-retry-declarations/checklists/requirements.md +41 -0
  26. data/specs/006-step-retry-declarations/contracts/dsl-surface.md +89 -0
  27. data/specs/006-step-retry-declarations/data-model.md +58 -0
  28. data/specs/006-step-retry-declarations/plan.md +187 -0
  29. data/specs/006-step-retry-declarations/quickstart.md +80 -0
  30. data/specs/006-step-retry-declarations/research.md +194 -0
  31. data/specs/006-step-retry-declarations/spec.md +453 -0
  32. data/specs/006-step-retry-declarations/tasks.md +382 -0
  33. data/specs/specs-inputs-by-method-md-piped-wigderson.md +77 -0
  34. metadata +13 -1
@@ -0,0 +1,453 @@
1
+ # Feature Specification: Step-Scoped Retry Declarations
2
+
3
+ **Feature Branch**: `retry_confs_in_steps`
4
+
5
+ **Created**: 2026-09-25
6
+
7
+ **Status**: Draft
8
+
9
+ **Input**: User description: "As we did previously with `locks` we have to continue moving
10
+ configurations from the main reactor to the steps. Currently:
11
+
12
+ ```ruby
13
+ class PaymentReactor < RubyReactor::Reactor
14
+ background all: true
15
+
16
+ step :charge_card, ChargeCard do
17
+ retries max_attempts: 3, backoff: :exponential, base_delay: 5.seconds
18
+ end
19
+ end
20
+ ```
21
+
22
+ But we are designing the DSL so that steps are encapsulated units of work that know the best way
23
+ to be executed. The preferred way:
24
+
25
+ ```ruby
26
+ class ChargeCard < RubyReactor::Step
27
+ with_lock(...) # already implemented
28
+ input :email, :string, format?: EMAIL_REGEX
29
+ retries max_attempts: 3 # AND!!
30
+
31
+ def run
32
+ logic
33
+ end
34
+ end
35
+ ```
36
+
37
+ As always, inline reactor steps and reactor declarations are supported and maintained. This is
38
+ valid:
39
+
40
+ ```ruby
41
+ class PaymentReactor < RubyReactor::Reactor
42
+ background all: true
43
+
44
+ step :charge_card do
45
+ retries max_attempts: 3, backoff: :exponential, base_delay: 5.seconds
46
+ run { PaymentService.charge(card_token, amount) }
47
+ end
48
+ end
49
+ ```
50
+
51
+ We have to emulate the logic and behaviour we implemented for locks and inputs."
52
+
53
+ Follow-up from the user: "And we should remove altogether the retry defaults (`retry_defaults`
54
+ on the reactor). Those are a bad practice and make retries unpredictable."
55
+
56
+ ## Clarifications
57
+
58
+ ### Session 2026-09-25
59
+
60
+ - Q: Should a direct call to a step class (outside any reactor) honor the class's retry
61
+ policy? → A: No. A direct call runs once. Only the reactor coordinates retries.
62
+
63
+ ## User Scenarios & Testing *(mandatory)*
64
+
65
+ ### User Story 1 - Remove reactor-wide retry defaults first (Priority: P1, delivered first)
66
+
67
+ An author reading a reactor must be able to tell how each step is retried by looking at that
68
+ step alone: its class or its step block. Reactor-wide retry defaults break this, because a
69
+ line at the top of a reactor silently changes how every step without its own policy behaves.
70
+ The same step class can be retried three times in one reactor and never in another, and
71
+ moving a line up or down in the reactor changes which steps it applies to.
72
+
73
+ Reactor-wide retry defaults are removed. A step that declares no policy runs once. Reactors
74
+ that still call the removed declaration fail to load with a message explaining the
75
+ replacement.
76
+
77
+ **Why this priority**: Reactor-wide defaults are the main source of unpredictable retries.
78
+ Removing them first also makes the rest of this feature simpler: once they are gone, a step's
79
+ policy can only come from two places (its step block or its step class), so no later story
80
+ has to decide how a step class policy ranks against a reactor default, or whether it depends
81
+ on where the defaults line sits in the reactor. This story is delivered and verified on its
82
+ own, before any step class declaration work starts (see Delivery order).
83
+
84
+ **Independent Test**: Define a reactor that uses the removed reactor-wide defaults declaration
85
+ and confirm it fails to load with a message naming the reactor and pointing to step-level
86
+ `retries`. Then define a reactor whose steps declare no policy and confirm each failing step
87
+ is attempted exactly once.
88
+
89
+ **Acceptance Scenarios**:
90
+
91
+ 1. **Given** a reactor that declares reactor-wide retry defaults, **When** the reactor class is
92
+ loaded, **Then** loading fails with a message naming the reactor, stating that reactor-wide
93
+ defaults were removed, and telling the author to declare `retries` on each step class or
94
+ step block that needs it.
95
+ 2. **Given** a step with no retry declaration in its class or its step block, **When** it
96
+ fails, **Then** it is attempted exactly once and the failure proceeds to rollback.
97
+ 3. **Given** a reactor that did not use reactor-wide defaults before this change, **When** it
98
+ runs after the change, **Then** every step behaves exactly as before.
99
+ 4. **Given** nested reactors placed with `compose` or `async_reactor`, **When** their step
100
+ blocks declare no `retries`, **Then** they are attempted once. They no longer read a
101
+ reactor-wide default.
102
+
103
+ ---
104
+
105
+ ### User Story 2 - A step class declares its own retry policy (Priority: P1)
106
+
107
+ A workflow author writes a step that talks to a flaky external service: charging a card,
108
+ calling a partner API, sending an email. The author knows how that operation should be retried
109
+ (how many attempts, how far apart), because the author knows the service. Today that
110
+ knowledge can only be written in each reactor that uses the step, so every reactor has to
111
+ repeat it and can get it wrong.
112
+
113
+ The author declares the retry policy in the step class, next to its inputs and its lock. Any
114
+ reactor that uses the step gets that policy without mentioning it.
115
+
116
+ **Why this priority**: This is the main feature. Without it, the step is not a complete unit
117
+ of work: part of how it should run still lives in every caller. It builds on US1: by the
118
+ time it starts, reactor-wide defaults no longer exist.
119
+
120
+ **Independent Test**: Define a step class that declares three attempts and fails on its first
121
+ two runs. Place it in a reactor with no retry wiring and confirm the step succeeds on its third
122
+ attempt. Then make it fail on every run and confirm the workflow fails after exactly three
123
+ attempts.
124
+
125
+ **Acceptance Scenarios**:
126
+
127
+ 1. **Given** a step class declaring a maximum of N attempts, **When** a reactor uses the step
128
+ without any retry wiring and the step fails with a retryable failure, **Then** the step is
129
+ attempted again, up to N attempts in total.
130
+ 2. **Given** the same step, **When** an attempt succeeds before N attempts are used,
131
+ **Then** the workflow continues with that attempt's result and makes no further attempts.
132
+ 3. **Given** the same step, **When** all N attempts fail, **Then** the step's failure reports
133
+ the step name and the number of attempts made, and rollback proceeds as for any step
134
+ failure.
135
+ 4. **Given** a step class declaring a backoff strategy and base delay, **When** attempts are
136
+ retried, **Then** the wait between attempts follows that strategy and delay.
137
+ 5. **Given** a step class declaring retries with only some options (for example only the
138
+ number of attempts), **When** it runs, **Then** the options it left out take the same
139
+ defaults the reactor-side `retries` uses today.
140
+ 6. **Given** a step fails with a failure marked as not retryable, **When** its class declares
141
+ retries, **Then** it is not attempted again, exactly as with reactor-side retries today.
142
+
143
+ ---
144
+
145
+ ### User Story 3 - Existing step-level declarations keep working unchanged (Priority: P1)
146
+
147
+ Authors already declare retries in a reactor's step block, for inline steps and for class
148
+ steps. Both keep working exactly as they do today. The class-level declaration is the
149
+ preferred form for class steps, not a replacement.
150
+
151
+ **Why this priority**: Inline steps are a supported style, and existing step-block
152
+ declarations must not break.
153
+
154
+ **Independent Test**: Run the existing step-level retry tests unchanged and confirm they pass.
155
+ Then declare `retries` inside an inline step block and confirm the behavior matches the class
156
+ form.
157
+
158
+ **Acceptance Scenarios**:
159
+
160
+ 1. **Given** an inline step that declares `retries` and a `run` body in its block, **When** it
161
+ fails, **Then** it is retried according to that declaration, exactly as today.
162
+ 2. **Given** a class step that declares no retries, **When** its reactor step block declares
163
+ `retries`, **Then** the step-block declaration applies, exactly as today.
164
+ 3. **Given** an inline step and a class step with the same retry declaration, **When** both
165
+ fail the same way, **Then** their attempt counts, delays, and final outcome are the same.
166
+ 4. **Given** an inline step's `retries` line, **When** the author moves the step into a class,
167
+ **Then** the same line works unchanged in the class body.
168
+
169
+ ---
170
+
171
+ ### User Story 4 - One declaration per step (Priority: P1)
172
+
173
+ A step's policy is declared in exactly one place. Declaring it in the step class and again in
174
+ the reactor's step block for that class is refused when the reactor is defined, the same way a
175
+ lock declared in both places is refused today.
176
+
177
+ **Why this priority**: If both declarations were accepted silently, one would be ignored and
178
+ the author would not know which. That is how retries end up wrong in production.
179
+
180
+ **Independent Test**: Declare retries on a step class, then add a `retries` line to a
181
+ reactor's step block for that class. Confirm the reactor fails to load with a message naming
182
+ the reactor, the step, and the class, and explaining how to resolve it.
183
+
184
+ **Acceptance Scenarios**:
185
+
186
+ 1. **Given** a step class declaring retries, **When** a reactor also declares `retries` in that
187
+ step's block, **Then** the reactor is refused at definition time with a message naming the
188
+ reactor, the step, and the class, and telling the author to keep only one declaration.
189
+ 2. **Given** a step class that declares no retries, **When** a reactor declares `retries` in
190
+ its step block, **Then** no conflict is raised (US3 scenario 2).
191
+ 3. **Given** a step class declaring a single attempt, **When** it runs, **Then** it is not
192
+ retried. Declaring one attempt is a valid, explicit "do not retry".
193
+
194
+ ---
195
+
196
+ ### User Story 5 - The policy follows the step on every execution path (Priority: P2)
197
+
198
+ A step class's retry policy applies wherever the step runs: in the calling process, in a
199
+ background worker after the whole reactor is handed off, after a mid-workflow hand-off, as an
200
+ independently dispatched step, and after the workflow resumes from an interrupt. Attempts
201
+ already made are remembered when a retry is scheduled for later, so the attempt limit holds
202
+ across those gaps.
203
+
204
+ **Why this priority**: A policy that only applies on some paths is a trap, and background runs
205
+ are where retries matter most. But the synchronous path alone already delivers value.
206
+
207
+ **Independent Test**: Run the same reactor with the same always-failing class step
208
+ synchronously and in the background. Confirm both make exactly the declared number of
209
+ attempts, and that the background run schedules later attempts without blocking the worker.
210
+
211
+ **Acceptance Scenarios**:
212
+
213
+ 1. **Given** a class step with a declared policy in a reactor that runs in the calling process,
214
+ **When** it fails, **Then** retries wait in-process between attempts, as reactor-side
215
+ retries do today.
216
+ 2. **Given** the same step in a reactor that runs in a background worker, **When** it fails,
217
+ **Then** the next attempt is scheduled for later instead of blocking the worker, as
218
+ reactor-side retries do today.
219
+ 3. **Given** the same step placed as an independently dispatched step, **When** it fails,
220
+ **Then** its declared policy governs its retries.
221
+ 4. **Given** a retry scheduled for later, **When** the next attempt runs, **Then** the attempts
222
+ already made count toward the limit, and the workflow never exceeds the declared maximum.
223
+ 5. **Given** a step that exhausts its attempts in any of these paths, **When** it fails for the
224
+ last time, **Then** earlier steps compensate exactly as they do for reactor-side retries.
225
+
226
+ ---
227
+
228
+ ### User Story 6 - Step subclasses inherit the policy (Priority: P2)
229
+
230
+ A team keeps a base step class for all calls to one partner API, with the retry policy that
231
+ API needs. Each concrete step inherits that policy. A subclass that needs a different policy
232
+ declares its own, and this does not change its parent or its siblings.
233
+
234
+ **Why this priority**: Step inheritance is already supported for inputs and locks. Retries
235
+ must behave the same way, but the feature works without it.
236
+
237
+ **Independent Test**: Declare retries on a base step class, subclass it twice, override the
238
+ policy in one subclass, and confirm each class runs with the expected policy.
239
+
240
+ **Acceptance Scenarios**:
241
+
242
+ 1. **Given** a base step class declaring retries, **When** a subclass declares none, **Then**
243
+ the subclass runs with the base policy.
244
+ 2. **Given** a subclass that declares its own retries, **When** it runs, **Then** its own
245
+ policy applies, and the base class and other subclasses keep the base policy.
246
+ 3. **Given** a reactor needs a different policy for a class step that already declares one,
247
+ **When** the author subclasses the step and declares the new policy there, **Then** the
248
+ reactor uses the subclass with no conflict. This is the documented way to vary the policy
249
+ per workflow.
250
+
251
+ ---
252
+
253
+ ### User Story 7 - Tests and operators can see the policy (Priority: P2)
254
+
255
+ A developer writing a spec for a reactor uses the shipped test helpers to check that a
256
+ class-declared policy was applied: how many times a step was retried, and that it failed or
257
+ succeeded afterward. Operators and tooling can find out what retry policy a step will run with
258
+ and where it was declared.
259
+
260
+ **Why this priority**: Required by the project's testing and observability rules. The feature
261
+ works without it, but cannot be verified or debugged without it.
262
+
263
+ **Independent Test**: Write a reactor spec using only the shipped test surface that makes a
264
+ class step fail and asserts it was retried the declared number of times. Then look up the
265
+ step's effective policy from the reactor and confirm it reports the class as its source.
266
+
267
+ **Acceptance Scenarios**:
268
+
269
+ 1. **Given** a class step with a declared policy, **When** it fails in a test using the shipped
270
+ test helpers, **Then** the existing retry assertions report the attempts it made.
271
+ 2. **Given** a class step whose body is replaced by a test mock, **When** the mock fails,
272
+ **Then** the class's declared policy still applies, so tests exercise the real policy.
273
+ 3. **Given** any step in a reactor, **When** its effective retry policy is looked up, **Then**
274
+ the answer includes the maximum attempts, backoff, base delay, and whether it came from the
275
+ step class, the step block, or no declaration.
276
+ 4. **Given** a class step being retried, **When** instrumentation is enabled, **Then** each
277
+ retry attempt is reported with the step name and attempt number, exactly as for
278
+ reactor-side retries today.
279
+
280
+ ---
281
+
282
+ ### Edge Cases
283
+
284
+ - **Invalid values**: an unknown backoff strategy, a negative delay, or an attempt count that
285
+ is not a positive whole number is refused when the step class (or inline step) is defined,
286
+ naming the step and the bad value. Today an unknown backoff strategy only fails at the first
287
+ retry, in production.
288
+ - **Direct invocation**: a step class that declares retries is called directly from
289
+ application code, or from another step's body, not through a reactor. The call runs once
290
+ and its retries do not apply: only a reactor coordinates retries. A failure is returned to
291
+ the caller straight away. This differs on purpose from locks, which a direct call does take.
292
+ - **Reactor subclasses and test doubles**: a reactor subclass, or a test copy of a reactor
293
+ made by the shipped test helpers, must not bring back reactor-wide defaults by copying them
294
+ from a parent.
295
+ - **Retries and locks**: a step class declares both a lock and retries. Their combined behavior
296
+ must be the same as when the same two declarations are made on the reactor side today. This
297
+ feature changes where the policy is declared, not how retries use locks.
298
+ - **Retries and input validation**: a step whose inputs fail its own contract is not retried.
299
+ Invalid inputs will not become valid on another attempt, and today reactor-side retries do
300
+ not retry contract failures either.
301
+ - **Skipped steps**: a step skipped by a condition or guard makes no attempts and uses none of
302
+ its retry budget.
303
+ - **Policy changed between attempts**: a retry scheduled for later runs under the policy in the
304
+ code that the worker is running. Attempts already made still count.
305
+ - **Duck-typed implementations**: a step implementation that is not a step class has no class
306
+ policy. Only its step-block declaration applies, and without one it runs once.
307
+ - **Nested reactors and map elements**: `compose`, `async_reactor`, and `map` place reactors,
308
+ not step classes. Their step-block `retries` declarations keep working. A step class used
309
+ inside a nested or mapped reactor brings its own policy there too.
310
+
311
+ ## Requirements *(mandatory)*
312
+
313
+ ### Functional Requirements
314
+
315
+ #### Declaration
316
+
317
+ - **FR-001**: A step class MUST be able to declare its retry policy (maximum attempts, backoff
318
+ strategy, base delay) using the same `retries` vocabulary and defaults as the reactor-side
319
+ step block.
320
+ - **FR-002**: An inline step MUST keep being able to declare `retries` in its reactor step
321
+ block, with identical behavior to the class form.
322
+ - **FR-003**: A class step that declares no retries MUST keep accepting a `retries`
323
+ declaration in its reactor step block, with today's behavior.
324
+ - **FR-004**: Invalid retry values MUST be refused at definition time, in either form, with a
325
+ message naming the step and the offending value.
326
+
327
+ #### Removal of reactor-wide defaults
328
+
329
+ - **FR-005**: The reactor-wide retry defaults declaration MUST be removed. A reactor that uses
330
+ it MUST fail to load with a message naming the reactor, stating that the declaration was
331
+ removed, and telling the author to declare `retries` on each step class or step block that
332
+ needs it. This follows how other removed reactor declarations are handled.
333
+ - **FR-006**: No step, nested reactor, or dispatched unit MAY take its retry policy from its
334
+ reactor. The only sources are the step class and the step block.
335
+ - **FR-007**: A step with no retry declaration in either place MUST be attempted exactly once.
336
+
337
+ #### Precedence and conflicts
338
+
339
+ - **FR-008**: A step's effective policy MUST come from its step block if it declares one,
340
+ otherwise from its step class, otherwise no retries.
341
+ - **FR-009**: Declaring `retries` both on a step class and in a reactor's step block for that
342
+ class MUST be refused at reactor definition time, with a message naming the reactor, the
343
+ step, and the class, and telling the author to keep one declaration. This matches how
344
+ conflicting lock declarations are refused.
345
+
346
+ #### Inheritance
347
+
348
+ - **FR-010**: A step subclass MUST inherit its parent's retry policy unless it declares its
349
+ own. Redeclaring MUST NOT affect the parent or its siblings.
350
+
351
+ #### Execution
352
+
353
+ - **FR-011**: A class-declared policy MUST apply on every path a reactor-side policy applies
354
+ to today: the calling process, background hand-off of the whole reactor or part of it,
355
+ independently dispatched steps, and runs resumed after an interrupt or a scheduled retry.
356
+ - **FR-012**: Retry behavior under a class-declared policy MUST match a step-block declaration
357
+ with the same values: which failures are retried, attempt counting across scheduled
358
+ retries, the wait between attempts, in-process waiting versus scheduling for later, the
359
+ final failure report, and compensation after the last attempt.
360
+ - **FR-013**: A direct invocation of a step class (outside any reactor, including from
361
+ another step's body) MUST run exactly once and MUST NOT apply the class's retry policy. Only
362
+ a reactor coordinates retries. Documentation MUST state this, and MUST state that it differs
363
+ from locks, which a direct call does take.
364
+
365
+ #### Visibility and verification
366
+
367
+ - **FR-014**: A step's effective retry policy and its source (step class, step block, or none)
368
+ MUST be available to tooling and tests.
369
+ - **FR-015**: The shipped test surface MUST verify class-declared retries with the existing
370
+ retry assertions, including when the step's body is replaced by a test mock.
371
+ - **FR-016**: Retry attempts under a class-declared policy MUST produce the same
372
+ instrumentation events and failure details as step-block retries.
373
+
374
+ #### Delivery order
375
+
376
+ - **FR-017**: The removal of reactor-wide defaults (US1, FR-005 to FR-007) MUST be delivered
377
+ first as a standalone change: removal, removal error, rewritten tests, documentation and
378
+ migration note, with the full suite passing. Step class declarations (US2 onward) MUST start
379
+ only after that change is complete, and MUST NOT include any handling of reactor-wide
380
+ defaults.
381
+
382
+ #### Delivery
383
+
384
+ - **FR-018**: The feature MUST ship a runnable demo reactor, a demo task, and a spec that uses
385
+ only the shipped test surface. Together they MUST show a class step that succeeds after
386
+ retries, a class step that exhausts its retries and triggers compensation, and a step with
387
+ no declaration that is attempted once.
388
+ - **FR-019**: Documentation MUST present the step class form as the preferred way to declare
389
+ retries, and MUST state the precedence order, the conflict rule, and how to vary a policy per
390
+ workflow by subclassing. Every example that uses reactor-wide defaults MUST be rewritten to
391
+ step-level declarations.
392
+ - **FR-020**: The release MUST carry a migration note explaining the removal of reactor-wide
393
+ defaults and showing how to move each default onto the steps that need it.
394
+
395
+ ### Key Entities
396
+
397
+ - **Retry Policy**: how a step is re-attempted after a retryable failure. Attributes: maximum
398
+ attempts, backoff strategy (exponential, linear, fixed), base delay.
399
+ - **Policy Source**: where a step's effective policy came from. One of: step block (inline or
400
+ reactor-side), step class, or none.
401
+ - **Attempt Record**: the per-execution count of attempts made for each step. It is kept
402
+ across scheduled retries and hand-offs, and checked against the effective policy's maximum.
403
+
404
+ ## Success Criteria *(mandatory)*
405
+
406
+ ### Measurable Outcomes
407
+
408
+ - **SC-001**: A step class that declares its retry policy can be used in any number of reactors
409
+ with zero retry lines in those reactors, and gets the same policy in each.
410
+ - **SC-002**: Every existing step-level retry test passes unchanged. Tests that exercised
411
+ reactor-wide defaults are replaced by tests of the removal error.
412
+ - **SC-003**: For the same failure sequence, a class-declared policy and a step-block
413
+ declaration with the same values produce the same attempt count, the same waits between
414
+ attempts, and the same final outcome, on both the in-process and the background paths.
415
+ - **SC-004**: A step's retry policy can be determined by reading only that step's class or
416
+ step block, in 100% of cases.
417
+ - **SC-005**: 100% of reactors still using reactor-wide defaults, and 100% of conflicting
418
+ double declarations (class plus step block), are refused when the reactor loads. None reach
419
+ run time.
420
+ - **SC-006**: 100% of invalid retry values are refused when the step is defined. None surface
421
+ first at a retry in production.
422
+ - **SC-007**: A developer can find a step's effective policy and where it was declared without
423
+ reading the step or reactor source.
424
+ - **SC-008**: The demo runs end to end in the project's container setup and shows the
425
+ succeed-after-retry, exhaust-and-compensate, and single-attempt outcomes.
426
+
427
+ ## Assumptions
428
+
429
+ - The audience is developers writing reactors and steps with this library.
430
+ - The feature changes where the retry policy is declared, not how retries run. The runtime
431
+ behavior of retries (retryable failures, backoff formulas, attempt counting, scheduling in
432
+ background runs) is reused unchanged.
433
+ - Delivering the removal first is deliberate: it deletes the reactor-default fallback before
434
+ a second source of policy (the step class) is added, so the new work only ever deals with
435
+ two sources and needs no ordering or precedence rules involving the reactor.
436
+ - Removing reactor-wide defaults is a breaking change to the public API. It is released under
437
+ the project's versioning policy for breaking changes, with a migration note. It follows the
438
+ existing pattern for removed declarations: the old call raises a clear error at load time
439
+ instead of being silently ignored.
440
+ - A reactor that never declared reactor-wide defaults already runs undeclared steps once, so
441
+ it sees no behavior change.
442
+ - The conflict rule for class-plus-step-block declarations follows the existing rule for
443
+ conflicting lock declarations: refuse, don't silently choose one. To use a different policy
444
+ in one workflow, the author subclasses the step (US6 scenario 3). There is no per-reactor
445
+ override of a class policy.
446
+ - The step-block `retries` form stays fully supported for inline steps, for class steps that
447
+ declare no policy, and for `compose`, `async_reactor`, and `map`. It is not deprecated in
448
+ this feature.
449
+ - Refusing invalid values at definition time tightens the inline form slightly. A value that
450
+ fails at the first retry today will fail when the reactor loads instead. Values that work
451
+ today keep working, with one exception: an attempt count of zero, which today silently means
452
+ "do not retry", must now be written as one.
453
+ - Step inheritance works for retries the same way it already works for inputs and locks.