ruby_reactor 0.7.1 → 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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/.specify/feature.json +1 -1
- data/.specify/memory/constitution.md +26 -16
- data/.specify/templates/plan-template.md +4 -0
- data/.specify/templates/tasks-template.md +1 -1
- data/CHANGELOG.md +140 -0
- data/CLAUDE.md +1 -1
- data/README.md +125 -31
- data/lib/ruby_reactor/context.rb +2 -2
- data/lib/ruby_reactor/dsl/interrupt_builder.rb +6 -0
- data/lib/ruby_reactor/dsl/reactor.rb +36 -18
- data/lib/ruby_reactor/dsl/step_builder.rb +95 -2
- data/lib/ruby_reactor/dsl/template_helpers.rb +2 -2
- data/lib/ruby_reactor/dsl/validation_helpers.rb +17 -0
- data/lib/ruby_reactor/error/input_validation_error.rb +4 -0
- data/lib/ruby_reactor/error/step_failure_error.rb +10 -3
- data/lib/ruby_reactor/executor/result_handler.rb +9 -3
- data/lib/ruby_reactor/executor/retry_manager.rb +2 -1
- data/lib/ruby_reactor/executor/step_executor.rb +9 -2
- data/lib/ruby_reactor/executor.rb +3 -0
- data/lib/ruby_reactor/max_retries_exhausted_failure.rb +3 -2
- data/lib/ruby_reactor/reactor.rb +9 -12
- data/lib/ruby_reactor/rspec/matchers.rb +3 -6
- data/lib/ruby_reactor/step/async_reactor_step.rb +159 -162
- data/lib/ruby_reactor/step/compose_step.rb +56 -75
- data/lib/ruby_reactor/step/input_contract.rb +128 -0
- data/lib/ruby_reactor/step/map_step.rb +177 -218
- data/lib/ruby_reactor/step.rb +116 -21
- data/lib/ruby_reactor/step_signals.rb +6 -2
- data/lib/ruby_reactor/step_worker.rb +25 -10
- data/lib/ruby_reactor/template/result.rb +9 -2
- data/lib/ruby_reactor/utils/fetch_indifferent.rb +13 -0
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor.rb +5 -2
- data/specs/002-step-input-contracts/checklists/requirements.md +49 -0
- data/specs/002-step-input-contracts/contracts/dsl-surface.md +193 -0
- data/specs/002-step-input-contracts/data-model.md +115 -0
- data/specs/002-step-input-contracts/plan.md +165 -0
- data/specs/002-step-input-contracts/quickstart.md +170 -0
- data/specs/002-step-input-contracts/research.md +233 -0
- data/specs/002-step-input-contracts/spec.md +359 -0
- data/specs/002-step-input-contracts/tasks.md +367 -0
- data/specs/004-inheritable-step-class/checklists/requirements.md +40 -0
- data/specs/004-inheritable-step-class/contracts/step-lifecycle.md +85 -0
- data/specs/004-inheritable-step-class/data-model.md +116 -0
- data/specs/004-inheritable-step-class/plan.md +174 -0
- data/specs/004-inheritable-step-class/quickstart.md +112 -0
- data/specs/004-inheritable-step-class/research.md +308 -0
- data/specs/004-inheritable-step-class/spec.md +316 -0
- data/specs/004-inheritable-step-class/tasks.md +258 -0
- data/specs/deferred-003-step-lock-declarations/checklists/requirements.md +51 -0
- data/specs/deferred-003-step-lock-declarations/contracts/dsl-surface.md +154 -0
- data/specs/deferred-003-step-lock-declarations/data-model.md +131 -0
- data/specs/deferred-003-step-lock-declarations/plan.md +166 -0
- data/specs/deferred-003-step-lock-declarations/quickstart.md +169 -0
- data/specs/deferred-003-step-lock-declarations/research.md +196 -0
- data/specs/deferred-003-step-lock-declarations/spec.md +447 -0
- data/specs/deferred-003-step-lock-declarations/tasks.md +572 -0
- data/specs/possible_feature.md +22 -0
- metadata +28 -1
|
@@ -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`.
|