ruby_reactor 0.7.0 → 0.8.0

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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/demo-app-e2e-verify/SKILL.md +226 -0
  3. data/.claude/skills/speckit-demo-tests/SKILL.md +144 -0
  4. data/.release-please-manifest.json +1 -1
  5. data/.specify/feature.json +1 -1
  6. data/.specify/memory/constitution.md +92 -15
  7. data/.specify/templates/plan-template.md +4 -0
  8. data/.specify/templates/tasks-template.md +8 -1
  9. data/CHANGELOG.md +151 -0
  10. data/CLAUDE.md +2 -2
  11. data/README.md +149 -40
  12. data/lib/ruby_reactor/context.rb +9 -2
  13. data/lib/ruby_reactor/context_serializer.rb +13 -0
  14. data/lib/ruby_reactor/dsl/interrupt_builder.rb +6 -0
  15. data/lib/ruby_reactor/dsl/lockable.rb +2 -2
  16. data/lib/ruby_reactor/dsl/reactor.rb +36 -18
  17. data/lib/ruby_reactor/dsl/step_builder.rb +95 -2
  18. data/lib/ruby_reactor/dsl/template_helpers.rb +13 -5
  19. data/lib/ruby_reactor/dsl/validation_helpers.rb +17 -0
  20. data/lib/ruby_reactor/error/input_validation_error.rb +4 -0
  21. data/lib/ruby_reactor/error/step_failure_error.rb +10 -3
  22. data/lib/ruby_reactor/executor/compensation_manager.rb +58 -45
  23. data/lib/ruby_reactor/executor/ordered_lock_support.rb +9 -9
  24. data/lib/ruby_reactor/executor/result_handler.rb +44 -13
  25. data/lib/ruby_reactor/executor/retry_manager.rb +6 -2
  26. data/lib/ruby_reactor/executor/step_executor.rb +27 -14
  27. data/lib/ruby_reactor/executor.rb +20 -15
  28. data/lib/ruby_reactor/map/element_executor.rb +7 -1
  29. data/lib/ruby_reactor/map/helpers.rb +9 -7
  30. data/lib/ruby_reactor/map/result_enumerator.rb +2 -0
  31. data/lib/ruby_reactor/map/sweeper.rb +1 -1
  32. data/lib/ruby_reactor/max_retries_exhausted_failure.rb +3 -2
  33. data/lib/ruby_reactor/open_telemetry.rb +7 -4
  34. data/lib/ruby_reactor/ordered_lock.rb +3 -3
  35. data/lib/ruby_reactor/reactor.rb +9 -12
  36. data/lib/ruby_reactor/rspec/matchers.rb +64 -17
  37. data/lib/ruby_reactor/rspec/test_subject.rb +8 -8
  38. data/lib/ruby_reactor/step/async_reactor_step.rb +159 -162
  39. data/lib/ruby_reactor/step/compose_step.rb +56 -75
  40. data/lib/ruby_reactor/step/input_contract.rb +128 -0
  41. data/lib/ruby_reactor/step/map_step.rb +178 -215
  42. data/lib/ruby_reactor/step.rb +119 -18
  43. data/lib/ruby_reactor/step_signals.rb +37 -0
  44. data/lib/ruby_reactor/step_worker.rb +25 -10
  45. data/lib/ruby_reactor/storage/adapter.rb +4 -0
  46. data/lib/ruby_reactor/storage/redis_adapter.rb +1 -72
  47. data/lib/ruby_reactor/storage/redis_reactor_scan.rb +116 -0
  48. data/lib/ruby_reactor/template/result.rb +9 -2
  49. data/lib/ruby_reactor/utils/fetch_indifferent.rb +13 -0
  50. data/lib/ruby_reactor/version.rb +1 -1
  51. data/lib/ruby_reactor/web/api.rb +23 -6
  52. data/lib/ruby_reactor/web/public/assets/index-BQvIWPdx.css +1 -0
  53. data/lib/ruby_reactor/web/public/assets/index-Dw4KV4QY.js +22 -0
  54. data/lib/ruby_reactor/web/public/index.html +2 -2
  55. data/lib/ruby_reactor.rb +61 -9
  56. data/specs/002-step-input-contracts/checklists/requirements.md +49 -0
  57. data/specs/002-step-input-contracts/contracts/dsl-surface.md +193 -0
  58. data/specs/002-step-input-contracts/data-model.md +115 -0
  59. data/specs/002-step-input-contracts/plan.md +165 -0
  60. data/specs/002-step-input-contracts/quickstart.md +170 -0
  61. data/specs/002-step-input-contracts/research.md +233 -0
  62. data/specs/002-step-input-contracts/spec.md +359 -0
  63. data/specs/002-step-input-contracts/tasks.md +367 -0
  64. data/specs/004-inheritable-step-class/checklists/requirements.md +40 -0
  65. data/specs/004-inheritable-step-class/contracts/step-lifecycle.md +85 -0
  66. data/specs/004-inheritable-step-class/data-model.md +116 -0
  67. data/specs/004-inheritable-step-class/plan.md +174 -0
  68. data/specs/004-inheritable-step-class/quickstart.md +112 -0
  69. data/specs/004-inheritable-step-class/research.md +308 -0
  70. data/specs/004-inheritable-step-class/spec.md +316 -0
  71. data/specs/004-inheritable-step-class/tasks.md +258 -0
  72. data/specs/deferred-003-step-lock-declarations/checklists/requirements.md +51 -0
  73. data/specs/deferred-003-step-lock-declarations/contracts/dsl-surface.md +154 -0
  74. data/specs/deferred-003-step-lock-declarations/data-model.md +131 -0
  75. data/specs/deferred-003-step-lock-declarations/plan.md +166 -0
  76. data/specs/deferred-003-step-lock-declarations/quickstart.md +169 -0
  77. data/specs/deferred-003-step-lock-declarations/research.md +196 -0
  78. data/specs/deferred-003-step-lock-declarations/spec.md +447 -0
  79. data/specs/deferred-003-step-lock-declarations/tasks.md +572 -0
  80. data/specs/possible_feature.md +22 -0
  81. metadata +34 -11
  82. data/lib/ruby_reactor/web/public/assets/index-B46p-M6K.css +0 -1
  83. data/lib/ruby_reactor/web/public/assets/index-DPmP4yXT.js +0 -22
  84. data/specs/001-background-async-steps/checklists/requirements.md +0 -39
  85. data/specs/001-background-async-steps/contracts/public-dsl.md +0 -154
  86. data/specs/001-background-async-steps/data-model.md +0 -117
  87. data/specs/001-background-async-steps/plan.md +0 -168
  88. data/specs/001-background-async-steps/quickstart.md +0 -102
  89. data/specs/001-background-async-steps/research.md +0 -150
  90. data/specs/001-background-async-steps/spec.md +0 -146
  91. data/specs/001-background-async-steps/tasks.md +0 -271
@@ -0,0 +1,316 @@
1
+ # Feature Specification: Inheritable Step Class
2
+
3
+ **Feature Branch**: `step_validations`
4
+
5
+ **Created**: 2026-09-11
6
+
7
+ **Status**: Draft
8
+
9
+ **Input**: User description: "Step complexity and refactor — the step input contracts feature (`specs/002-step-input-contracts`) added complexity to steps through workarounds that preserve the original `include RubyReactor::Step` + `def self.run(args, context)` interface. That mixin/singleton approach was chosen to keep steps stateless and easy to bolt onto existing brownfield services, but validations and upcoming features do not compose well with it: `lib/ruby_reactor/step.rb` now wraps `run` on the singleton class with hacky, magic-like interception. Revisit the decision and produce a better structure: an inheritable parent step class where the author declares inputs, writes an instance-level `run`/`undo`/`compensate`, and the parent handles validation and any future lifecycle features before invoking the body. Signals raised inside the body (`fail!`, `success!`, …) must surface as the proper result wrapper from the class-level call. No backward compatibility or deprecation handling is required — nothing is in production. If supporting both a mixin style and an inheritance style adds variance or risk, support only one."
10
+
11
+ ## User Scenarios & Testing *(mandatory)*
12
+
13
+ ### User Story 1 - Author a step by inheriting from the base step (Priority: P1)
14
+
15
+ A workflow author creates a step by subclassing the library's base step class. Inside the
16
+ subclass they declare the step's inputs and write the step's work as an ordinary instance
17
+ method. Inside that method they read the validated inputs and the workflow context through
18
+ accessors, return a result wrapper, or end early with a signal such as `fail!`.
19
+
20
+ Reading the base class alone explains the whole lifecycle in plain order: build an instance
21
+ with the supplied values and context, enforce the input contract, run the body, translate
22
+ any signal into a result. No method the author defines is silently intercepted, wrapped,
23
+ or replaced behind their back.
24
+
25
+ **Why this priority**: This is the whole feature. Every other story depends on the base
26
+ class existing and behaving predictably.
27
+
28
+ **Independent Test**: Define a subclass declaring one typed input and an instance-level
29
+ body; invoke it the way the reactor invokes steps, with conforming and violating values,
30
+ and observe a success result in the first case and a step-attributed validation failure in
31
+ the second — with the body never running in the failure case.
32
+
33
+ **Acceptance Scenarios**:
34
+
35
+ 1. **Given** a subclass declaring a required input and a body that returns a success
36
+ wrapper, **When** the step is invoked with conforming values, **Then** the body runs
37
+ once, sees the validated values and the context, and the invocation returns that
38
+ success wrapper.
39
+ 2. **Given** the same subclass, **When** invoked with values that violate the contract,
40
+ **Then** the invocation fails with a validation error naming the step and the offending
41
+ fields, the body never runs, and the resulting failure is marked non-retryable — the
42
+ same invalid values would fail identically on a retry, so no automatic retry path may
43
+ attempt the body.
44
+ 3. **Given** a subclass whose body calls `fail!("nop")`, **When** the step is invoked,
45
+ **Then** the invocation returns a failure wrapper carrying `"nop"` — the signal does not
46
+ escape as an exception to the caller.
47
+ 4. **Given** a subclass whose body calls `success!`, `skip!`, or `halt!`, **When** the step
48
+ is invoked, **Then** the invocation returns the matching success, skipped, or halt
49
+ wrapper.
50
+ 5. **Given** a subclass that declares no inputs, **When** invoked with arbitrary values,
51
+ **Then** the body runs with those values unchanged and no validation step is performed.
52
+ 6. **Given** a subclass that does not define a body, **When** invoked, **Then** the
53
+ invocation raises a clear "must implement" error naming the subclass.
54
+
55
+ ---
56
+
57
+ ### User Story 2 - Reactor execution paths use the new step uniformly (Priority: P1)
58
+
59
+ A workflow author uses inheriting step classes inside reactors exactly as they use steps
60
+ today: synchronous execution, asynchronous execution through the background worker, retry,
61
+ compensation and undo on rollback, composition of reactors, map steps, and the shipped
62
+ RSpec test surface. Every one of those paths obtains the same result from the same step
63
+ with the same validation, because they all go through one entry point on the step class.
64
+
65
+ **Why this priority**: A base class that only works on the happy synchronous path breaks
66
+ the saga guarantees the library exists to provide.
67
+
68
+ **Independent Test**: Run the existing integration suites (sync, async worker, retry,
69
+ compensation, compose, map, RSpec helpers) against steps rewritten in the inheriting style
70
+ and observe identical outcomes to the pre-refactor baseline.
71
+
72
+ **Acceptance Scenarios**:
73
+
74
+ 1. **Given** a reactor composed of inheriting steps, **When** run synchronously, **Then**
75
+ dependency order, results, and failure attribution match the pre-refactor behaviour.
76
+ 2. **Given** an inheriting step marked for asynchronous execution, **When** the background
77
+ worker executes it, **Then** input validation is enforced there too and the outcome is
78
+ recorded exactly as the synchronous path would record it.
79
+ 3. **Given** a downstream step fails, **When** rollback runs, **Then** each completed
80
+ inheriting step's undo receives that step's own result, each failed step's compensation
81
+ receives the failure reason, and both are invoked on a fresh instance carrying the
82
+ original values and context.
83
+ 4. **Given** a step defines neither undo nor compensation, **When** rollback reaches it,
84
+ **Then** the step is skipped and rollback continues (today's default behaviour).
85
+ 5. **Given** a signal is thrown inside undo or compensation, **When** rollback reaches
86
+ it, **Then** the signal is translated into a result wrapper just as it is for the body.
87
+ 6. **Given** a spec written with the shipped test surface (`test_reactor`, `mock_step`,
88
+ `failing_at`, matchers), **When** its subject is a reactor of inheriting steps,
89
+ **Then** the spec passes unchanged.
90
+
91
+ ---
92
+
93
+ ### User Story 3 - Wrap an existing service as a step (Priority: P2)
94
+
95
+ A developer integrating the library into an existing codebase already has service objects
96
+ with their own constructors and `call` methods. They wrap one as a step by writing a small
97
+ subclass whose body instantiates the existing service with the validated inputs, calls it,
98
+ and maps its outcome to a success or failure wrapper. The existing service is not modified
99
+ and does not need to know about the library.
100
+
101
+ **Why this priority**: The brownfield use case motivated the original mixin design; the
102
+ new design must serve it at least as well, or the refactor loses a stated goal.
103
+
104
+ **Independent Test**: Take an untouched plain Ruby service class, write a subclass of the
105
+ base step of no more than a handful of lines that delegates to it, and run it through a
106
+ reactor with both a successful and a failing service outcome.
107
+
108
+ **Acceptance Scenarios**:
109
+
110
+ 1. **Given** an existing service class with no library dependency, **When** a developer
111
+ writes an adapter step subclass that delegates to it, **Then** the service runs
112
+ unmodified and its success maps to a success wrapper carrying the service's output.
113
+ 2. **Given** the same adapter, **When** the service reports failure, **Then** the step
114
+ returns a failure wrapper carrying the service's error and triggers rollback.
115
+ 3. **Given** the adapter declares inputs, **When** the reactor supplies invalid values,
116
+ **Then** the service is never instantiated.
117
+
118
+ ---
119
+
120
+ ### User Story 4 - Step contracts and behaviour inherit across subclasses (Priority: P2)
121
+
122
+ A workflow author builds a family of related steps: a base step in their own codebase
123
+ declares shared inputs and helpers, and concrete steps inherit from it adding their own
124
+ inputs and body. Contracts merge parent-first, and a subclass may override the body,
125
+ undo, or compensation while still benefitting from validation.
126
+
127
+ **Why this priority**: Inheritance is the natural extension point of the new design; if it
128
+ breaks contract merging that the input-contracts feature already delivers, the refactor
129
+ regresses shipped behaviour.
130
+
131
+ **Independent Test**: Define a two-level hierarchy where the parent declares one input and
132
+ the child another; invoke the child with values missing either input and confirm both are
133
+ enforced; invoke with both and confirm the child's body runs.
134
+
135
+ **Acceptance Scenarios**:
136
+
137
+ 1. **Given** a parent step declaring input A and a child declaring input B, **When** the
138
+ child is invoked without A, **Then** validation fails on A.
139
+ 2. **Given** the same hierarchy, **When** invoked with A and B, **Then** the child's body
140
+ runs and sees both values.
141
+ 3. **Given** a child overrides the body defined by its parent, **When** invoked, **Then**
142
+ the child's body runs and validation still precedes it.
143
+
144
+ ---
145
+
146
+ ### User Story 5 - Documentation and demo reflect the single authoring style (Priority: P3)
147
+
148
+ A newcomer reads the README, the documentation folder, and the demo application and finds
149
+ exactly one way to write a class-based step: the inheriting style. No example, guide, or
150
+ demo reactor still uses the previous mixin style, and the changelog states plainly that the
151
+ old style was removed and how to convert.
152
+
153
+ **Why this priority**: The refactor is a breaking public-API change; a mixed documentation
154
+ set would confuse every new adopter and violates the project's documentation rule.
155
+
156
+ **Independent Test**: Search README, documentation, and the demo app for the previous
157
+ authoring form and find zero occurrences; run every `demo:` rake task and observe the same
158
+ printed outcomes as before the refactor.
159
+
160
+ **Acceptance Scenarios**:
161
+
162
+ 1. **Given** the completed change, **When** the previous authoring form is searched for
163
+ across README, documentation, demo app, and specs, **Then** no occurrences remain.
164
+ 2. **Given** the demo application, **When** each `demo:` task is run against a clean Redis,
165
+ **Then** each prints the same observable outcomes as before the refactor and its spec
166
+ passes using only the shipped test surface.
167
+ 3. **Given** the changelog, **When** a reader looks for this change, **Then** it appears
168
+ under a breaking-change heading with a before/after conversion example.
169
+
170
+ ---
171
+
172
+ ### Edge Cases
173
+
174
+ - A step body raises an ordinary exception (not a signal): it propagates as it does today,
175
+ so retry and failure attribution behave unchanged.
176
+ - A step body rescues broadly (`rescue Exception`): signals still reach the step boundary
177
+ and produce the intended wrapper, as guaranteed today.
178
+ - The step is invoked directly (outside any reactor, e.g., in a unit test): validation and
179
+ signal translation still apply, because there is one entry point.
180
+ - Run and rollback happen in different processes (asynchronous execution): undo and
181
+ compensation must not rely on instance state left over from the run, only on the stored
182
+ values, result, and context they are given.
183
+ - A step's validation failure reaches the caller by a route other than the dispatching
184
+ process's own failure result — surfaced through a composed reactor, or reported by an
185
+ `async_step`/`background` worker: it is non-retryable through that route too, not only
186
+ when the validation happens to fail in the same process that will decide whether to
187
+ retry it.
188
+ - A subclass defines its own class-level entry point: it replaces the lifecycle for that
189
+ class (validation included) and this is documented as deliberately unsupported behaviour
190
+ rather than silently patched around.
191
+ - Inline step bodies declared with a block inside a reactor definition keep working and
192
+ keep access to the same signal helpers; they are unaffected by this change.
193
+ - The shipped built-in steps (compose, map, async child reactor) continue to behave
194
+ identically to callers regardless of how they are structured internally.
195
+
196
+ ## Requirements *(mandatory)*
197
+
198
+ ### Functional Requirements
199
+
200
+ - **FR-001**: The library MUST provide a single base step class that authors subclass to
201
+ define a step; it MUST be reachable under the same public name the mixin used, so that
202
+ `RubyReactor::Step` denotes "the way you write a step".
203
+ - **FR-002**: Subclasses MUST declare inputs and validation rules on the class using the
204
+ same declarations the input-contracts feature introduced, with parent-first contract
205
+ merging across subclass hierarchies preserved.
206
+ - **FR-003**: Subclasses MUST define the step's work as an instance-level body, with
207
+ accessors for the validated inputs and the workflow context; undo and compensation are
208
+ likewise instance-level, receiving respectively the step's stored result and the failure
209
+ reason.
210
+ - **FR-004**: The base class MUST expose exactly one class-level entry point per lifecycle
211
+ action (run, undo, compensate), and `call` MUST be accepted as an alias for run. Every
212
+ execution path in the library (synchronous executor, asynchronous worker, compensation
213
+ manager, RSpec test subject) MUST invoke steps only through those entry points.
214
+ - **FR-005**: The class-level run entry point MUST, in order: build a fresh instance from
215
+ the supplied values and context, enforce the declared input contract (skipping
216
+ enforcement when nothing is declared), invoke the instance body, and return the body's
217
+ result wrapper.
218
+ - **FR-006**: Input validation failures MUST be reported as the existing structured
219
+ validation error carrying the step name and field errors, and the body MUST NOT run.
220
+ The resulting failure MUST report itself as non-retryable, on every path that can
221
+ produce one from it — a direct synchronous run, an asynchronous worker, and a step
222
+ surfaced through a composed reactor — without each of those paths having to say so
223
+ individually.
224
+ - **FR-007**: Signals (`success!`, `skip!`, `fail!`, `halt!`) thrown anywhere inside the
225
+ body, undo, or compensation MUST be translated at the class-level entry point into the
226
+ matching result wrapper. Callers MUST never observe the signal mechanism.
227
+ - **FR-008**: The result-wrapper constructors (`Success`, `Failure`, `Halt`, `Skipped`)
228
+ and the signal helpers MUST be available inside instance methods of a subclass.
229
+ - **FR-009**: Undo and compensation MUST default to "skipped" when a subclass does not
230
+ define them, and MUST run on a fresh instance so that behaviour is identical whether or
231
+ not the run happened in the same process.
232
+ - **FR-010**: A subclass that defines no body MUST raise a clear "must implement" error
233
+ naming the subclass when invoked.
234
+ - **FR-011**: The base class MUST NOT intercept, wrap, prepend to, or otherwise alter
235
+ methods the author defines; the lifecycle MUST be readable top-to-bottom as ordinary
236
+ method calls.
237
+ - **FR-012**: The previous mixin authoring style (`include` on a plain class with
238
+ class-level `run`) MUST be removed. No compatibility shim or deprecation path is
239
+ provided.
240
+ - **FR-013**: Inline block-based step bodies in the reactor definition DSL MUST keep
241
+ working unchanged, including access to the signal helpers.
242
+ - **FR-014**: All library-internal steps (compose, map, async child reactor) and every
243
+ step in the test suite and demo application MUST be migrated to the new style, and the
244
+ full RSpec suite and RuboCop MUST pass.
245
+ - **FR-015**: README, every affected file under the documentation folder, the demo
246
+ application, and the changelog MUST be updated in the same change; the changelog entry
247
+ MUST be marked as a breaking change with a conversion example.
248
+ - **FR-016**: The demo application MUST include a reactor demonstrating the inheriting
249
+ style end-to-end, including its failure/rollback path and a brownfield service adapter,
250
+ registered as a `demo:` rake task with a matching spec that uses only the shipped test
251
+ surface.
252
+ - **FR-017**: A step's input-validation failure MUST be non-retryable as a single,
253
+ centrally-enforced property of that failure — not a flag every caller that builds a
254
+ result from it has to remember to set — so that a future execution path gets the
255
+ guarantee automatically. This closes a gap found while implementing this feature: today
256
+ only the asynchronous worker path sets this explicitly; the synchronous path and a
257
+ validation failure surfaced through `compose` currently default to retryable, silently.
258
+
259
+ ### Key Entities
260
+
261
+ - **Base step class**: the single inheritable parent every class-based step derives from.
262
+ Owns the lifecycle (instantiate, validate, run, translate signals) and the default undo
263
+ and compensation.
264
+ - **Step instance**: a short-lived object built per lifecycle action from the supplied
265
+ values and context; the author's body, undo, and compensation execute on it. Carries no
266
+ state across actions.
267
+ - **Input contract**: the declared inputs and rules for a step (existing entity); attached
268
+ to the class and merged parent-first through the hierarchy.
269
+ - **Result wrapper**: success, failure, skipped, or halt outcome (existing entity);
270
+ the only thing a class-level entry point ever returns.
271
+ - **Signal**: an early-exit helper from within a step body (existing entity); always
272
+ translated to a result wrapper at the entry point.
273
+
274
+ ## Success Criteria *(mandatory)*
275
+
276
+ ### Measurable Outcomes
277
+
278
+ - **SC-001**: A reader can explain a step's full execution order (instantiate, validate,
279
+ run, translate signal) from the base class file alone, with no hidden method
280
+ interception present anywhere in the step lifecycle.
281
+ - **SC-002**: 100% of class-based steps in the library, test suite, documentation, and demo
282
+ application use the inheriting style; a search for the previous authoring form returns
283
+ zero results.
284
+ - **SC-003**: The full existing test suite passes after migration with no reduction in
285
+ scenario coverage, and every `demo:` rake task prints the same observable outcomes as
286
+ before the change.
287
+ - **SC-004**: A brownfield service can be wrapped as a step in a subclass of no more than
288
+ ten lines, without modifying the service.
289
+ - **SC-005**: Adding a future per-step lifecycle feature (for example, a pre-run hook)
290
+ requires touching only the base class's lifecycle sequence, not any execution path or
291
+ any author-written step.
292
+
293
+ ## Assumptions
294
+
295
+ - **One authoring style.** The description allows dropping dual support if it adds
296
+ variance or risk; it does. The mixin style is removed entirely and the inheritable class
297
+ takes the `RubyReactor::Step` name. The brownfield case is served by a thin adapter
298
+ subclass (User Story 3) rather than by mixing the library into an existing class.
299
+ - **No compatibility work.** Per the description, nothing runs in production; there is no
300
+ deprecation window or shim. The changelog still records the change as breaking, per the
301
+ project's SemVer rule.
302
+ - **Instance accessors, not method parameters.** The body reads inputs and context through
303
+ accessors on the instance, matching the `def run` shape in the description; undo and
304
+ compensation receive the one value the body cannot know (stored result, failure reason)
305
+ as a parameter.
306
+ - **Existing contract semantics carry over.** Input declarations, cross-field rules,
307
+ optional inputs, `false` handling, and parent-first merging behave exactly as delivered
308
+ by the input-contracts feature; this change moves where enforcement lives, not what it
309
+ does.
310
+ - **Signal mechanism unchanged.** Signals keep their existing early-exit semantics
311
+ (including surviving broad rescues); only the place where they are caught moves to the
312
+ step's single entry point.
313
+ - **Inline block steps are out of scope** beyond confirming they still work; they remain
314
+ the lightweight alternative to a class.
315
+ - **Naming of the step-level `argument` DSL** and other input-contracts follow-ups are not
316
+ part of this change.
@@ -0,0 +1,258 @@
1
+ # Tasks: Inheritable Step Class
2
+
3
+ **Input**: Design documents from `/specs/004-inheritable-step-class/`
4
+
5
+ **Prerequisites**: plan.md, spec.md, research.md (D1–D10), data-model.md, contracts/step-lifecycle.md, quickstart.md
6
+
7
+ **Tests**: REQUIRED. Constitution Principle III mandates Red-Green-Refactor with RSpec against real Redis. Every test task below is written first and confirmed failing before its implementation task.
8
+
9
+ **Organization**: Grouped by user story. This is a refactor of one subsystem, so the Foundational phase is larger than usual: `require "ruby_reactor"` cannot even load until `step.rb`, the four namespace reopenings, and the three built-in steps change together (research.md D9). Everything after that is migration and proof, story by story.
10
+
11
+ ## Format: `[ID] [P?] [Story] Description`
12
+
13
+ - **[P]**: Can run in parallel (different files, no dependencies)
14
+ - **[Story]**: Which user story this task belongs to (US1–US5 from spec.md)
15
+ - Every task names its exact file path(s)
16
+
17
+ ## Path Conventions
18
+
19
+ Single Ruby gem: `lib/ruby_reactor/`, `spec/`, plus the `demo_app/` Rails example (own Gemfile; run its specs from inside `demo_app/` or via `docker compose run --rm demo-app`). Migration grep pattern throughout: `grep -rnE "include RubyReactor::Step\b"` (the `\b` excludes the legitimate `StepSignals` include).
20
+
21
+ ---
22
+
23
+ ## Phase 1: Setup (Baseline)
24
+
25
+ **Purpose**: Capture the pre-refactor green state so SC-003 ("identical outcomes") is provable, not asserted.
26
+
27
+ - [X] T001 Run `bundle exec rspec` and `bundle exec rubocop` on the untouched branch; record pass counts and any pre-existing failures in `specs/004-inheritable-step-class/baseline.md` (this file is the comparison target for T047 and is deleted before merge)
28
+ - [X] T002 [P] Run the demo acceptance suite via `docker compose run --rm demo-app bin/rails demo:all` and `docker compose run --rm demo-app bundle exec rspec spec/reactors`; append printed outcomes and spec counts to `specs/004-inheritable-step-class/baseline.md`
29
+
30
+ ---
31
+
32
+ ## Phase 2: Foundational (Base Class + Namespace Flip + Built-ins)
33
+
34
+ **Purpose**: The one atomic change the library needs to boot again. After this phase `require "ruby_reactor"` loads, the three built-in steps work, and the new base class has its own green spec. Every user-authored step in `spec/` and `demo_app/` is still on the old form and will fail to load until its story phase — that is expected; run only the files named in each task until Phase 3 lands.
35
+
36
+ **⚠️ CRITICAL**: T004–T009 must land in one commit. A `module Step` reopening loaded after `class Step` raises `TypeError` (D9), and `include RubyReactor::Step` on a class raises `TypeError: wrong argument type Class` once `Step` is a class.
37
+
38
+ - [X] T003 Write the red lifecycle spec `spec/ruby_reactor/step_spec.rb` per data-model.md and contracts/step-lifecycle.md: (a) a subclass with `input :n, :integer` and instance `run` reading `inputs[:n]` and `context` returns the body's `Success`; (b) `.call` is an alias of `.run`; (c) violating inputs raise `Error::InputValidationError` with `step_name` set and the body never runs; (d) a subclass declaring no inputs receives arbitrary arguments unchanged; (e) `fail!`/`success!`/`skip!`/`halt!` inside `run`, `undo`, and `compensate` each return the matching wrapper from the class-level call, never `UncaughtThrowError`; (f) omitted `undo`/`compensate` return `Skipped`; (g) omitted `run` raises `NotImplementedError` naming the subclass; (h) fresh instance per action: an ivar set inside `run` is `nil` inside a subsequent `undo` on the same class (D2); (i) `undo` sees `result`, `compensate` sees `reason`. Confirm the file fails (it cannot load: `RubyReactor::Step` is a module today)
39
+ - [X] T004 Rewrite `lib/ruby_reactor/step.rb` as `class RubyReactor::Step`: delete `InputEnforcement`, `ClassMethods`, `self.included`, and `inherited`; keep the class-level DSL (`input`, `validate_inputs`, `input_contract` with parent-first merge, `declared_inputs`, `required_input_names`, `declares_inputs?`, private `own_input_contract`) verbatim as `class << self` methods; add `initialize(inputs, context, result: nil, reason: nil)` with `attr_reader :inputs, :context, :result, :reason`; add instance `run` (raises `NotImplementedError, "#{self.class} must implement #run"`), instance `undo`/`compensate` (return `RubyReactor.Skipped()`); `include RubyReactor::StepSignals` at instance level and define instance `Success`/`Failure`/`Halt`/`Skipped` delegating to `RubyReactor.*`; add class-level `run(arguments, context)` = enforce contract (raise with `step_name = name` on `InputValidationError`, outside the catch) → `new(validated, context)` → `catch(StepSignals::TAG) { instance.run }`; `undo(result, arguments, context)` and `compensate(reason, arguments, context)` = `new(arguments, context, result:/reason:)` → `catch(StepSignals::TAG) { instance.undo/compensate }`; `class << self; alias_method :call, :run; end`. Header comment states the lifecycle order and the fresh-instance rule (FR-009, FR-011)
40
+ - [X] T005 [P] Change `module Step` to `class Step` in `lib/ruby_reactor/step/input_contract.rb` (namespace line only; no logic change)
41
+ - [X] T006 [P] Migrate `lib/ruby_reactor/step/compose_step.rb`: `class Step` namespace, `class ComposeStep < RubyReactor::Step`, delete the dead `initialize(composed_reactor_class, argument_mappings)` and its `attr_reader`s (D6), move `self.run`/`self.compensate`/`self.undo` bodies to instance methods reading `inputs`/`context`/`reason`/`result`, move the `class << self; private` helpers to private instance methods
42
+ - [X] T007 [P] Migrate `lib/ruby_reactor/step/map_step.rb`: `class Step` namespace, `class MapStep < RubyReactor::Step`, instance `run`/`compensate` reading `inputs`/`context`, private helpers to private instance methods — EXCEPT `build_mapped_inputs` and `resolve_element`, which stay public class methods because `lib/ruby_reactor/map/helpers.rb:32` calls them (D6)
43
+ - [X] T008 [P] Migrate `lib/ruby_reactor/step/async_reactor_step.rb`: `class Step` namespace, `class AsyncReactorStep < RubyReactor::Step`, instance `run` reading `inputs`/`context`, private `class << self` helpers to private instance methods
44
+ - [X] T009 Update the catch-site comment in `lib/ruby_reactor/step_signals.rb` (lines 10–13) to say class steps are caught at `RubyReactor::Step`'s class-level `run`/`undo`/`compensate` and inline blocks at `step_executor.rb`/`compensation_manager.rb` (D4)
45
+ - [X] T010 Run `bundle exec rspec spec/ruby_reactor/step_spec.rb spec/ruby_reactor/step/map_step_spec.rb spec/compose_spec.rb spec/map spec/ruby_reactor/dsl/async_step_spec.rb` and `bundle exec rubocop lib/ruby_reactor/step.rb lib/ruby_reactor/step/`; all green before any story phase starts
46
+
47
+ **Checkpoint**: Library loads, base class contract proven, built-ins behave as before. Old-form steps in `spec/` and `demo_app/` still fail to load — expected until Phases 3–7.
48
+
49
+ ---
50
+
51
+ ## Phase 3: User Story 1 — Author a step by inheriting from the base step (Priority: P1) 🎯 MVP
52
+
53
+ **Goal**: Every step-authoring suite in `spec/` uses the inheriting form and passes, proving scenarios 1–6 of US1 against real suites, not just the new lifecycle spec.
54
+
55
+ **Independent Test**: `bundle exec rspec spec/ruby_reactor/step_spec.rb spec/ruby_reactor/step_signals_spec.rb spec/ruby_reactor/step_contract_enforcement_spec.rb spec/ruby_reactor/dsl/step_input_contract_spec.rb spec/ruby_reactor/halt_helper_spec.rb` is green with zero `include RubyReactor::Step` in those files.
56
+
57
+ ### Implementation for User Story 1
58
+
59
+ Each migration below means: replace `include RubyReactor::Step` with `< RubyReactor::Step`, turn `def self.run(args, ctx)` into `def run` reading `inputs`/`context`, `def self.undo(result, args, ctx)` into `def undo` reading `result`/`inputs`/`context`, `def self.compensate(reason, args, ctx)` into `def compensate` reading `reason`/`inputs`/`context`; keep every assertion unchanged.
60
+
61
+ - [X] T011 [P] [US1] Migrate step classes in `spec/ruby_reactor/step_signals_spec.rb` (includes `compensate and undo bodies` examples at line ~190)
62
+ - [X] T012 [P] [US1] Migrate step classes in `spec/ruby_reactor/step_contract_enforcement_spec.rb`
63
+ - [X] T013 [P] [US1] Migrate step classes in `spec/ruby_reactor/dsl/step_input_contract_spec.rb`
64
+ - [X] T014 [P] [US1] Migrate step classes in `spec/ruby_reactor/dsl/inline_step_contract_spec.rb` (inline `inputs do ... end` blocks stay as they are; only class steps change)
65
+ - [X] T015 [P] [US1] Migrate step classes in `spec/ruby_reactor/dsl/step_contract_conflict_spec.rb`
66
+ - [X] T016 [P] [US1] Migrate step classes in `spec/ruby_reactor/dsl/step_contract_wiring_spec.rb`
67
+ - [X] T017 [P] [US1] Migrate step classes in `spec/ruby_reactor/step_contract_deprecation_spec.rb`
68
+ - [X] T018 [P] [US1] Migrate step classes in `spec/ruby_reactor/halt_helper_spec.rb`
69
+ - [X] T019 [P] [US1] Migrate step classes in `spec/ruby_reactor/falsey_input_resolution_spec.rb`
70
+ - [X] T020 [P] [US1] Migrate shared step classes in `spec/support/reactors/step_contract_reactors.rb`
71
+ - [X] T021 [US1] Run the Independent Test command above plus every file touched in T011–T020; all green
72
+
73
+ **Checkpoint**: US1 delivered — a developer can author, validate, and signal from an inheriting step, and the existing contract/signal suites prove it.
74
+
75
+ ---
76
+
77
+ ## Phase 4: User Story 2 — Reactor execution paths use the new step uniformly (Priority: P1)
78
+
79
+ **Goal**: Sync executor, async worker, retry, compensation/undo, compose, map, middleware, telemetry, and the RSpec `TestSubject` interception surface all pass against inheriting steps; the worker-path signal bug (D4) is fixed with a red-first test; and a validation failure is non-retryable on every one of those paths (D10), also proven red-first.
80
+
81
+ **Independent Test**: `bundle exec rspec spec/ruby_reactor/step_signals_worker_spec.rb spec/ruby_reactor/step_contract_retryable_spec.rb spec/ruby_reactor/rspec/test_subject_mock_step_spec.rb spec/ruby_reactor/middleware_spec.rb spec/ruby_reactor/telemetry_spec.rb spec/integration spec/async_retry_integration_spec.rb spec/compose_spec.rb` is green.
82
+
83
+ ### Tests for User Story 2
84
+
85
+ - [X] T022 [US2] Write the red spec `spec/ruby_reactor/step_signals_worker_spec.rb`: a `class WorkerFailStep < RubyReactor::Step` whose `run` calls `fail!("nope")`, wired via `async_step :boom, WorkerFailStep` in a reactor; run with `test_reactor` + `drain_async_jobs`; assert `be_failure` and `result.error == "nope"`. Before T004 this failed with an `UncaughtThrowError` error value — with the base class in place it should already pass; keep it as the regression guard for the worker path (D4, US2 scenario 2). If it still fails, `StepWorker#execute_step_body` is bypassing `impl.run` somewhere — fix there, never by adding a `catch` to the worker
86
+ - [X] T023 [US2] Write the red spec `spec/ruby_reactor/step_contract_retryable_spec.rb` (FR-017, research.md D10): (a) a reactor whose only step declares `input :n, :integer` and is run synchronously with a violating value — assert `result.retryable?` is `false`; (b) the same violating step wired as the child of a `compose` — assert the **parent** reactor's `Failure.retryable?` is also `false` (proves `ComposeStep#handle_execution_result` does not silently reset it). Confirm both are red today: neither `Executor::ResultHandler#build_validation_failure` nor `Step::ComposeStep#handle_execution_result` passes `retryable:` explicitly, so `RubyReactor::Failure`'s default (`error.respond_to?(:retryable?) ? error.retryable? : true`) currently resolves to `true` in both cases — `Error::InputValidationError` has no `retryable?` method yet
87
+ - [X] T024 [US2] Add `def retryable? = false` to `lib/ruby_reactor/error/input_validation_error.rb`, mirroring `Error::StepFailureError#retryable?` (`lib/ruby_reactor/error/step_failure_error.rb`). Run T023's spec plus `spec/ruby_reactor/step_contract_async_spec.rb` (the existing worker-path regression guard, lines 24–33 and 54–59); all green with no other file touched — this one method is the entire fix (D10)
88
+
89
+ ### Implementation for User Story 2
90
+
91
+ - [X] T025 [P] [US2] Migrate step classes in `spec/ruby_reactor/rspec/test_subject_mock_step_spec.rb` (exercises `TestSubject#apply_mock_interceptor`'s `impl.run` path)
92
+ - [X] T026 [P] [US2] Migrate the duck-typed `MiddlewareTestStep` in `spec/ruby_reactor/middleware_spec.rb` (plain class with `def self.run`, no mixin — still a class step, must inherit per SC-002)
93
+ - [X] T027 [P] [US2] Migrate the duck-typed `TelemetrySimpleStep` and `TelemetrySensitiveStep` in `spec/ruby_reactor/telemetry_spec.rb`
94
+ - [X] T028 [P] [US2] Migrate step classes in `spec/support/payment_workflow.rb` (compensation/undo bodies; used by integration and compose suites)
95
+ - [X] T029 [P] [US2] Migrate step classes in `spec/support/examples/data_pipeline.rb`
96
+ - [X] T030 [US2] Run `bundle exec rspec` (full suite) and `bundle exec rubocop`; compare against `specs/004-inheritable-step-class/baseline.md` — same pass count, no new failures, no new offenses. Grep confirms zero `include RubyReactor::Step\b` and zero mixin-free `def self.run(args, ctx)` step classes under `spec/`
97
+
98
+ **Checkpoint**: Every library execution path proven against the new class, including the non-retryable guarantee on validation failures; US1 + US2 together are the gem-level MVP.
99
+
100
+ ---
101
+
102
+ ## Phase 5: User Story 3 — Wrap an existing service as a step (Priority: P2)
103
+
104
+ **Goal**: A brownfield service class with no library dependency is wrapped by a ≤10-line adapter subclass, proven in the demo app end to end (success, failure/rollback, invalid inputs never instantiate the service).
105
+
106
+ **Independent Test**: `docker compose run --rm demo-app bin/rails demo:inheritable_step` prints success, failure-with-rollback, and validation-rejected outcomes; `docker compose run --rm demo-app bundle exec rspec spec/reactors/inheritable_step_demo_reactor_spec.rb` is green using only shipped matchers.
107
+
108
+ ### Implementation for User Story 3 (Constitution Principle VI artifacts)
109
+
110
+ - [X] T031 [P] [US3] Create `demo_app/app/reactors/inheritable_step_demo_reactor.rb` containing: a plain `LegacyChargeService` (own `initialize(user_id)` + `call` returning an object with `success?`/`id`/`error`, no RubyReactor reference); `ChargeStep < RubyReactor::Step` with `input :user_id, :integer, gt?: 0` and a `run` delegating to the service (≤10 lines, SC-004) plus an `undo` that records the refund; a second inheriting step that `fail!`s when `inputs[:fail]` is true to force rollback; and `InheritableStepDemoReactor < RubyReactor::Reactor` wiring them with `input :user_id`, `input :fail`
111
+ - [X] T032 [US3] Register `demo:inheritable_step` in `demo_app/lib/tasks/demo_reactors.rake` with a `desc` and `[:environment, :flush_redis]`, printing three runs: valid inputs (success), `fail: true` (failure + `ChargeStep` undo printed), `user_id: 0` (validation failure, service never instantiated, and the printed result shows it is non-retryable per FR-017); add `:inheritable_step` to the `demo:all` dependency list at line ~242
112
+ - [X] T033 [P] [US3] Create `demo_app/spec/reactors/inheritable_step_demo_reactor_spec.rb` (`type: :reactor`) asserting `be_success`, `be_failure` + `have_run_step(:charge)` + rollback, and `have_validation_error` for `user_id: 0` — using only `test_reactor`, `drain_async_jobs`, and the shipped matchers from `lib/ruby_reactor/rspec.rb`; if an assertion needs a matcher that does not exist, add it to `lib/ruby_reactor/rspec/` in the same commit
113
+ - [X] T034 [US3] Run the Independent Test commands above; both green
114
+
115
+ **Checkpoint**: Brownfield use case proven the way users consume the gem.
116
+
117
+ ---
118
+
119
+ ## Phase 6: User Story 4 — Step contracts and behaviour inherit across subclasses (Priority: P2)
120
+
121
+ **Goal**: Parent-first contract merging and body overriding survive the move to a real class hierarchy with `inherited` deleted.
122
+
123
+ **Independent Test**: `bundle exec rspec spec/ruby_reactor/step_inheritance_spec.rb` is green.
124
+
125
+ ### Tests for User Story 4
126
+
127
+ - [X] T035 [US4] Write `spec/ruby_reactor/step_inheritance_spec.rb`: `BaseStep < RubyReactor::Step` declares `input :a, :integer`; `ChildStep < BaseStep` declares `input :b, :integer` and defines `run` returning `Success(sum: inputs[:a] + inputs[:b])`; assert (1) invoking `ChildStep` without `:a` raises `InputValidationError` naming `:a`; (2) with both, the child's body runs and sees both; (3) a `GrandchildStep < ChildStep` overriding `run` still has validation run first (invalid `:b` never reaches the override); (4) `ChildStep.input_contract.declarations.keys == [:a, :b]` and `BaseStep.input_contract.declarations.keys == [:a]` (parent not mutated). Confirm red if T004 left any `inherited`-dependent memoization bug; otherwise it passes immediately and stays as the regression guard
128
+
129
+ ### Implementation for User Story 4
130
+
131
+ - [X] T036 [US4] Run T035's spec plus `spec/ruby_reactor/step_contract_enforcement_spec.rb` and `spec/ruby_reactor/dsl/step_input_contract_spec.rb`; fix only if red — there should be nothing to implement (data-model.md: `inherited` deleted, contract ivars are already per-class)
132
+
133
+ **Checkpoint**: Hierarchies of user steps behave exactly as the input-contracts feature promised.
134
+
135
+ ---
136
+
137
+ ## Phase 7: User Story 5 — Documentation and demo reflect the single authoring style (Priority: P3)
138
+
139
+ **Goal**: Zero occurrences of the old form anywhere a newcomer reads; all existing demo reactors migrated; changelog records the break with a conversion example and the retryable fix.
140
+
141
+ **Independent Test**: `grep -rnE "include RubyReactor::Step\b" lib spec demo_app README.md documentation` returns nothing; `docker compose run --rm demo-app bin/rails demo:all` prints the same outcomes recorded in `baseline.md`; `docker compose run --rm demo-app bundle exec rspec spec/reactors` is green.
142
+
143
+ ### Implementation for User Story 5 — demo app migration
144
+
145
+ - [X] T037 [P] [US5] Migrate `demo_app/app/reactors/validated_user_step.rb` (1 step)
146
+ - [X] T038 [P] [US5] Migrate `demo_app/app/reactors/user_etl_reactor.rb` (5 steps, includes compensate/undo bodies)
147
+ - [X] T039 [P] [US5] Migrate `demo_app/app/reactors/reserve_inventory.rb` (1 step with undo)
148
+ - [X] T040 [P] [US5] Migrate `demo_app/spec/support/payment_workflow.rb` and `demo_app/spec/support/examples/data_pipeline.rb` (demo-side copies of the spec support files)
149
+
150
+ ### Implementation for User Story 5 — documentation (REQUIRED — Constitution Development Workflow)
151
+
152
+ Convert every `include RubyReactor::Step` + `def self.run(arguments, context)` example to `class X < RubyReactor::Step` + `def run` reading `inputs`/`context`; same for `undo`/`compensate` examples. Do not change surrounding prose beyond what the new form requires.
153
+
154
+ - [ ] T041 [P] [US5] Update `README.md` (5 occurrences: Quick Start and core-usage examples)
155
+ - [X] T042 [P] [US5] Update `documentation/getting_started.md` and `documentation/core_concepts.md` (the primary step-authoring guides — also add a short "Instance readers: `inputs`, `context`, `result`, `reason`" note, the fresh-instance-per-action rule, and a one-line note that validation failures are always non-retryable, to `core_concepts.md`)
156
+ - [X] T043 [P] [US5] Update `documentation/composition.md`, `documentation/async_reactors.md`, `documentation/README.md`
157
+ - [X] T044 [P] [US5] Update `documentation/examples/order_processing.md`, `documentation/examples/payment_processing.md`, and `demo_app/documentation/core_concepts.md`
158
+ - [X] T045 [US5] Add a `### ⚠ BREAKING CHANGES` entry under `## Unreleased` in `CHANGELOG.md`: mixin form removed, `RubyReactor::Step` is now a base class, before/after conversion example (class-level `run` → instance `run` with `inputs`/`context`; `undo`/`compensate` likewise); note that class steps now translate signals correctly on the `async_step`/`background` worker path (D4); note that a step's input-validation failure is now guaranteed non-retryable on every path, including through `compose` (D10, FR-017 — previously only the async worker path was correct); and a one-line known-issue that inline block steps on the worker path still do not catch signals (pre-existing, out of scope)
159
+ - [X] T046 [US5] Run the Independent Test commands above; grep empty, demo `all` outcomes match `baseline.md`, demo specs green
160
+
161
+ **Checkpoint**: A newcomer finds exactly one way to write a step everywhere they look.
162
+
163
+ ---
164
+
165
+ ## Phase 8: Polish & Cross-Cutting Concerns
166
+
167
+ - [X] T047 Run `bundle exec rspec` and `bundle exec rubocop` one final time; compare with `specs/004-inheritable-step-class/baseline.md` (SC-003); then delete `baseline.md`
168
+ - [X] T048 [P] Walk `specs/004-inheritable-step-class/quickstart.md` sections 1, 1b, 3, 4, 5 verbatim and confirm each expected outcome, including the `result.retryable? # => false` check
169
+ - [X] T049 [P] Read `lib/ruby_reactor/step.rb` top to bottom and confirm SC-001: the lifecycle (instantiate → validate → run → translate signal) is readable as plain method calls with no `prepend`, `extend`, `define_method`, or `method_missing` anywhere in the file
170
+ - [ ] T050 Commit with a `feat!:` subject so release-please bumps 0.7.0 → 0.8.0 (`bump-minor-pre-major: true`); body carries the CHANGELOG conversion example
171
+
172
+ ---
173
+
174
+ ## Dependencies & Execution Order
175
+
176
+ ### Phase Dependencies
177
+
178
+ - **Phase 1 (Baseline)**: none — start immediately; T001 and T002 in parallel
179
+ - **Phase 2 (Foundational)**: after Phase 1; T003 first (red), then T004–T009 as one commit (T005–T008 parallel once T004's class shape is known), then T010 — **BLOCKS all stories**
180
+ - **Phase 3 (US1)**: after Phase 2; T011–T020 all parallel (distinct files), T021 gates
181
+ - **Phase 4 (US2)**: after Phase 2; T022 and T023 first (red guards, can run in parallel with each other — distinct files), then T024 (the one-line retryable fix), then T025–T029 parallel, then T030 gates. Independent of Phase 3 except that T030's full-suite run needs Phase 3's files migrated too — so in practice finish Phase 3 before T030
182
+ - **Phase 5 (US3)**: after Phase 2; T031 and T033 parallel, T032 after T031, T034 gates. Independent of Phases 3–4
183
+ - **Phase 6 (US4)**: after Phase 2; T035 then T036. Independent of Phases 3–5
184
+ - **Phase 7 (US5)**: after Phase 2 for the demo migrations (T037–T040 parallel); T041–T044 parallel and can start any time after T004 (they only need the final API shape); T045 after T022 and T024 (it documents both fixes); T046 gates and needs Phases 5 and 7 complete (demo `all` includes the new task)
185
+ - **Phase 8 (Polish)**: after everything
186
+
187
+ ### User Story Dependencies
188
+
189
+ - **US1 (P1)**: Foundational only
190
+ - **US2 (P1)**: Foundational only; full-suite gate (T030) practically needs US1's migrations
191
+ - **US3 (P2)**: Foundational only
192
+ - **US4 (P2)**: Foundational only
193
+ - **US5 (P3)**: Foundational; final gate (T046) needs US3's demo task registered
194
+
195
+ ### Parallel Opportunities
196
+
197
+ - T001 ∥ T002
198
+ - T005 ∥ T006 ∥ T007 ∥ T008 (after T004)
199
+ - T011–T020 all parallel (10 distinct spec files)
200
+ - T022 ∥ T023 (distinct new spec files)
201
+ - T025–T029 all parallel
202
+ - T031 ∥ T033; T035 ∥ everything in Phase 5
203
+ - T037–T044 all parallel (12 distinct files across demo + docs)
204
+ - T048 ∥ T049
205
+
206
+ ---
207
+
208
+ ## Parallel Example: User Story 1
209
+
210
+ ```bash
211
+ # After T010 is green, launch all ten spec-file migrations together:
212
+ Task: "Migrate step classes in spec/ruby_reactor/step_signals_spec.rb"
213
+ Task: "Migrate step classes in spec/ruby_reactor/step_contract_enforcement_spec.rb"
214
+ Task: "Migrate step classes in spec/ruby_reactor/dsl/step_input_contract_spec.rb"
215
+ Task: "Migrate step classes in spec/ruby_reactor/dsl/inline_step_contract_spec.rb"
216
+ Task: "Migrate step classes in spec/ruby_reactor/dsl/step_contract_conflict_spec.rb"
217
+ Task: "Migrate step classes in spec/ruby_reactor/dsl/step_contract_wiring_spec.rb"
218
+ Task: "Migrate step classes in spec/ruby_reactor/step_contract_deprecation_spec.rb"
219
+ Task: "Migrate step classes in spec/ruby_reactor/halt_helper_spec.rb"
220
+ Task: "Migrate step classes in spec/ruby_reactor/falsey_input_resolution_spec.rb"
221
+ Task: "Migrate shared step classes in spec/support/reactors/step_contract_reactors.rb"
222
+ # Then T021 gate.
223
+ ```
224
+
225
+ ---
226
+
227
+ ## Implementation Strategy
228
+
229
+ ### MVP First (Foundational + US1)
230
+
231
+ 1. Phase 1: capture baseline (10 min)
232
+ 2. Phase 2: red lifecycle spec → base class + namespace flip + built-ins in one commit → T010 green
233
+ 3. Phase 3: migrate the ten step-authoring spec files → T021 green
234
+ 4. **STOP and VALIDATE**: `spec/ruby_reactor/step_spec.rb` + the US1 suites prove a developer can author, validate, and signal from an inheriting step
235
+
236
+ ### Incremental Delivery
237
+
238
+ 1. + US2 (worker-signal guard, non-retryable-validation guard, remaining spec migrations) → full `bundle exec rspec` green → gem-level feature complete
239
+ 2. + US3 (demo reactor, rake task, spec) → Principle VI satisfied for the new API
240
+ 3. + US4 (inheritance spec) → regression guard in place
241
+ 4. + US5 (demo migration, docs, changelog) → nothing old-form left anywhere
242
+ 5. Polish → `feat!:` commit
243
+
244
+ ### Single-Developer Reality
245
+
246
+ This is one person's refactor on one branch. Recommended commit boundaries: (1) baseline, (2) Phase 2 as a whole, (3) Phases 3+4 together (the suite is only green once both land), (4) Phase 5, (5) Phase 6, (6) Phase 7, (7) Polish. Between (2) and (3) the full suite is red by design — do not push that state.
247
+
248
+ ---
249
+
250
+ ## Notes
251
+
252
+ - Anchor every grep with `\b`; `include RubyReactor::StepSignals` in `step.rb` and `dsl/template_helpers.rb` is correct and stays
253
+ - Duck-typed step classes (plain class + `def self.run`, no mixin) in `middleware_spec.rb` and `telemetry_spec.rb` still *work* with the executor but violate SC-002 — migrate them (T026, T027)
254
+ - Never add a `catch(StepSignals::TAG)` to `step_worker.rb`; FR-007 puts signal translation in the step class (D4)
255
+ - Never re-add `inherited`, `prepend`, or any interception to `step.rb` (FR-011); if a subclass needs different lifecycle behavior, that is a spec change, not a hook
256
+ - `MapStep.build_mapped_inputs` / `resolve_element` stay class-level (T007); everything else on the built-ins goes instance-level
257
+ - The non-retryable fix (T024) is exactly one method on `Error::InputValidationError` — resist the urge to also pass `retryable: false` at `build_validation_failure` or `ComposeStep#handle_execution_result`; the whole point of D10 is that neither call site needs to know
258
+ - Commit after each phase gate; stop at any checkpoint to validate the story independently
@@ -0,0 +1,51 @@
1
+ # Specification Quality Checklist: Step-Scoped Coordination
2
+
3
+ **Purpose**: Validate specification completeness and quality before proceeding to planning
4
+ **Created**: 2026-09-10
5
+ **Feature**: [spec.md](../spec.md)
6
+
7
+ ## Content Quality
8
+
9
+ - [x] No implementation details (languages, frameworks, APIs)
10
+ - [x] Focused on user value and business needs
11
+ - [x] Written for non-technical stakeholders
12
+ - [x] All mandatory sections completed
13
+
14
+ ## Requirement Completeness
15
+
16
+ - [x] No [NEEDS CLARIFICATION] markers remain
17
+ - [x] Requirements are testable and unambiguous
18
+ - [x] Success criteria are measurable
19
+ - [x] Success criteria are technology-agnostic (no implementation details)
20
+ - [x] All acceptance scenarios are defined
21
+ - [x] Edge cases are identified
22
+ - [x] Scope is clearly bounded
23
+ - [x] Dependencies and assumptions identified
24
+
25
+ ## Feature Readiness
26
+
27
+ - [x] All functional requirements have clear acceptance criteria
28
+ - [x] User scenarios cover primary flows
29
+ - [x] Feature meets measurable outcomes defined in Success Criteria
30
+ - [x] No implementation details leak into specification
31
+
32
+ ## Notes
33
+
34
+ - All items pass. 0 [NEEDS CLARIFICATION] markers remain.
35
+ - Resolved with the user on 2026-09-10:
36
+ - **Contention**: park the execution and retry it later rather than failing (FR-015). No
37
+ queue exists in a synchronous run, so that path waits then fails (FR-016) — the split is
38
+ documented rather than hidden.
39
+ - **Scope**: all five primitives at step level (FR-002), with the two whose meaning does not
40
+ narrow trivially pinned down explicitly — deduplication skips the step rather than halting
41
+ the reactor (FR-003), strict ordering sequences that step only (FR-004).
42
+ - **Rollback**: exclusivity and concurrency ceilings are re-taken for compensate/undo
43
+ (FR-024); rate ceilings and dedup windows are not (FR-025).
44
+ - **Re-entrancy**: reuses the nested-workflow rules unchanged — execution-owned holds,
45
+ counted nesting, an execution-wide held-key registry, refusal at hand-off when ownership
46
+ cannot cross a process boundary, and keep-ownership-across-parks (FR-019 to FR-022,
47
+ FR-018).
48
+ - "Non-technical stakeholders" is read as *developers who are not this library's
49
+ maintainers*: the spec names no Ruby constructs, gems, or file paths outside the verbatim
50
+ user input.
51
+ - Ready for `/speckit-plan`.