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.
- checksums.yaml +4 -4
- data/.claude/skills/demo-app-e2e-verify/SKILL.md +226 -0
- data/.claude/skills/speckit-demo-tests/SKILL.md +144 -0
- data/.release-please-manifest.json +1 -1
- data/.specify/feature.json +1 -1
- data/.specify/memory/constitution.md +92 -15
- data/.specify/templates/plan-template.md +4 -0
- data/.specify/templates/tasks-template.md +8 -1
- data/CHANGELOG.md +151 -0
- data/CLAUDE.md +2 -2
- data/README.md +149 -40
- data/lib/ruby_reactor/context.rb +9 -2
- data/lib/ruby_reactor/context_serializer.rb +13 -0
- data/lib/ruby_reactor/dsl/interrupt_builder.rb +6 -0
- data/lib/ruby_reactor/dsl/lockable.rb +2 -2
- 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 +13 -5
- 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/compensation_manager.rb +58 -45
- data/lib/ruby_reactor/executor/ordered_lock_support.rb +9 -9
- data/lib/ruby_reactor/executor/result_handler.rb +44 -13
- data/lib/ruby_reactor/executor/retry_manager.rb +6 -2
- data/lib/ruby_reactor/executor/step_executor.rb +27 -14
- data/lib/ruby_reactor/executor.rb +20 -15
- data/lib/ruby_reactor/map/element_executor.rb +7 -1
- data/lib/ruby_reactor/map/helpers.rb +9 -7
- data/lib/ruby_reactor/map/result_enumerator.rb +2 -0
- data/lib/ruby_reactor/map/sweeper.rb +1 -1
- data/lib/ruby_reactor/max_retries_exhausted_failure.rb +3 -2
- data/lib/ruby_reactor/open_telemetry.rb +7 -4
- data/lib/ruby_reactor/ordered_lock.rb +3 -3
- data/lib/ruby_reactor/reactor.rb +9 -12
- data/lib/ruby_reactor/rspec/matchers.rb +64 -17
- data/lib/ruby_reactor/rspec/test_subject.rb +8 -8
- 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 +178 -215
- data/lib/ruby_reactor/step.rb +119 -18
- data/lib/ruby_reactor/step_signals.rb +37 -0
- data/lib/ruby_reactor/step_worker.rb +25 -10
- data/lib/ruby_reactor/storage/adapter.rb +4 -0
- data/lib/ruby_reactor/storage/redis_adapter.rb +1 -72
- data/lib/ruby_reactor/storage/redis_reactor_scan.rb +116 -0
- 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/web/api.rb +23 -6
- data/lib/ruby_reactor/web/public/assets/index-BQvIWPdx.css +1 -0
- data/lib/ruby_reactor/web/public/assets/index-Dw4KV4QY.js +22 -0
- data/lib/ruby_reactor/web/public/index.html +2 -2
- data/lib/ruby_reactor.rb +61 -9
- 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 +34 -11
- data/lib/ruby_reactor/web/public/assets/index-B46p-M6K.css +0 -1
- data/lib/ruby_reactor/web/public/assets/index-DPmP4yXT.js +0 -22
- data/specs/001-background-async-steps/checklists/requirements.md +0 -39
- data/specs/001-background-async-steps/contracts/public-dsl.md +0 -154
- data/specs/001-background-async-steps/data-model.md +0 -117
- data/specs/001-background-async-steps/plan.md +0 -168
- data/specs/001-background-async-steps/quickstart.md +0 -102
- data/specs/001-background-async-steps/research.md +0 -150
- data/specs/001-background-async-steps/spec.md +0 -146
- data/specs/001-background-async-steps/tasks.md +0 -271
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
# Feature Specification: Step Input Contracts
|
|
2
|
+
|
|
3
|
+
**Feature Branch**: `step_validations`
|
|
4
|
+
|
|
5
|
+
**Created**: 2026-09-10
|
|
6
|
+
|
|
7
|
+
**Status**: Draft
|
|
8
|
+
|
|
9
|
+
**Input**: User description: "We want to improve the structure of validations for steps. Right now validation in steps are declared in the reactor which is not the right place. Steps should be independent units of work and validations should be declared in those units to encapsulate the unit and its inputs. […] the current argument declaration in step covers three functions: 1. declare step dependencies 2. map reactor outputs to expected arguments 3. declare validations. 1 and 2 are clear and should continue to exist, 3rd is convoluted if different validations are declared in a class step and in the step declaration in the reactor. […] Another issue to take into account is the naming: right now the step has `argument` and the RubyReactor::Step would make sense to have `input`."
|
|
10
|
+
|
|
11
|
+
## User Scenarios & Testing *(mandatory)*
|
|
12
|
+
|
|
13
|
+
### User Story 1 - A step class declares its own input contract (Priority: P1)
|
|
14
|
+
|
|
15
|
+
A workflow author writes a reusable step as a standalone class. Inside that class they
|
|
16
|
+
declare, in one place, every value the step needs: its name, its expected type, whether it
|
|
17
|
+
is optional, and the rules it must satisfy. The step is then a self-contained unit — reading
|
|
18
|
+
the class alone tells you what it accepts and what it rejects, with no need to open any
|
|
19
|
+
reactor that happens to use it.
|
|
20
|
+
|
|
21
|
+
When the reactor runs that step, the declared contract is enforced before the step's work
|
|
22
|
+
begins. If a value is missing or violates a rule, the step fails with a structured
|
|
23
|
+
validation error naming the step and the offending fields, and the step's work never runs.
|
|
24
|
+
|
|
25
|
+
**Why this priority**: This is the core of the feature. Without it the unit of work is not
|
|
26
|
+
self-describing, and the same step reused across three reactors can be validated three
|
|
27
|
+
different ways.
|
|
28
|
+
|
|
29
|
+
**Independent Test**: Define a step class with a typed, constrained input contract, run it
|
|
30
|
+
from a minimal reactor with (a) conforming values and (b) violating values, and confirm
|
|
31
|
+
success in the first case and a step-attributed validation failure in the second — with no
|
|
32
|
+
validation rules declared anywhere in the reactor.
|
|
33
|
+
|
|
34
|
+
**Acceptance Scenarios**:
|
|
35
|
+
|
|
36
|
+
1. **Given** a step class declaring a required integer input with a minimum bound, **When**
|
|
37
|
+
the reactor supplies a conforming value, **Then** the step runs and receives the value.
|
|
38
|
+
2. **Given** the same step class, **When** the reactor supplies a value below the bound,
|
|
39
|
+
**Then** execution fails before the step's work runs, and the failure reports the step
|
|
40
|
+
name and the field-level error.
|
|
41
|
+
3. **Given** a step class declaring an optional input, **When** the reactor supplies no
|
|
42
|
+
value for it, **Then** the step runs and that key is absent/nil rather than failing.
|
|
43
|
+
4. **Given** a step class declaring an input typed as a specific class, **When** an instance
|
|
44
|
+
of an unrelated class is supplied, **Then** validation fails with a type error for that
|
|
45
|
+
field.
|
|
46
|
+
5. **Given** a step class with a cross-field rule spanning two of its inputs, **When** the
|
|
47
|
+
two values are individually valid but jointly invalid, **Then** validation fails with the
|
|
48
|
+
cross-field error.
|
|
49
|
+
6. **Given** a step class declaring a required boolean input, **When** the value supplied is
|
|
50
|
+
`false`, **Then** the input counts as provided: validation passes and the step receives
|
|
51
|
+
`false`, not nil.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
### User Story 2 - The reactor wires values without redeclaring rules (Priority: P1)
|
|
56
|
+
|
|
57
|
+
A workflow author composes a reactor from step classes. In the reactor they still say where
|
|
58
|
+
each value comes from — a reactor input, another step's result, a constant, a transform —
|
|
59
|
+
because only the reactor knows the wiring. They do not restate types or rules there; the
|
|
60
|
+
step class owns those.
|
|
61
|
+
|
|
62
|
+
If the author does restate rules for a step class that already declares its own contract,
|
|
63
|
+
the system refuses the definition with a message telling them where the contract lives,
|
|
64
|
+
instead of silently running two overlapping rule sets.
|
|
65
|
+
|
|
66
|
+
**Why this priority**: Wiring and validation are different concerns that today share one
|
|
67
|
+
call. Splitting them is what removes the class-step-vs-reactor conflict the author
|
|
68
|
+
described; without this half, the duplication and its debugging cost remain.
|
|
69
|
+
|
|
70
|
+
**Independent Test**: Wire a contract-owning step class into a reactor with mapping-only
|
|
71
|
+
declarations and confirm it runs; then add a conflicting rule in the reactor and confirm the
|
|
72
|
+
definition is rejected with an actionable message.
|
|
73
|
+
|
|
74
|
+
**Acceptance Scenarios**:
|
|
75
|
+
|
|
76
|
+
1. **Given** a reactor wiring a contract-owning step class with mapping-only declarations,
|
|
77
|
+
**When** the reactor class is loaded, **Then** it loads without error and the step's own
|
|
78
|
+
contract governs at run time.
|
|
79
|
+
2. **Given** a reactor that attaches a type or rule to an argument of a contract-owning step
|
|
80
|
+
class, **When** the reactor class is loaded, **Then** loading fails with an error naming
|
|
81
|
+
the reactor, the step, the argument, and the step class that already owns the contract.
|
|
82
|
+
3. **Given** a reactor that attaches a cross-field rule block to a contract-owning step
|
|
83
|
+
class, **When** the reactor class is loaded, **Then** loading fails with the same class of
|
|
84
|
+
error.
|
|
85
|
+
4. **Given** a step class that declares no contract at all, **When** the reactor attaches
|
|
86
|
+
types and rules to its arguments, **Then** the definition is accepted and those rules
|
|
87
|
+
govern (unchanged from today's behavior).
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
### User Story 3 - Inline steps keep a single, coherent place for rules (Priority: P1)
|
|
92
|
+
|
|
93
|
+
Not every step deserves a class. An author writing a short inline step inside a reactor
|
|
94
|
+
declares its rules in the step block, because for an inline step the step block *is* the
|
|
95
|
+
unit of work. The wording and the rule vocabulary they use there match what they would write
|
|
96
|
+
in a step class, so moving an inline step into a class later is a copy, not a rewrite.
|
|
97
|
+
|
|
98
|
+
**Why this priority**: Inline steps are a first-class authoring style; leaving them without
|
|
99
|
+
an answer would push authors to keep rules in the old place and defeat the split.
|
|
100
|
+
|
|
101
|
+
**Independent Test**: Write an inline step with a typed, constrained input contract and a
|
|
102
|
+
cross-field rule, run it with conforming and violating values, then move the same
|
|
103
|
+
declarations verbatim into a step class and confirm identical behavior.
|
|
104
|
+
|
|
105
|
+
**Acceptance Scenarios**:
|
|
106
|
+
|
|
107
|
+
1. **Given** an inline step declaring its input contract in the step block, **When** the
|
|
108
|
+
reactor runs with violating values, **Then** it fails with the same error shape a step
|
|
109
|
+
class produces.
|
|
110
|
+
2. **Given** an inline step's contract declarations, **When** they are moved unchanged into a
|
|
111
|
+
step class and the reactor keeps only the wiring, **Then** the observable behavior for
|
|
112
|
+
both conforming and violating values is identical.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
### User Story 4 - Missing wiring is caught when the reactor is defined (Priority: P2)
|
|
117
|
+
|
|
118
|
+
Because a step class states which values it requires, a reactor that forgets to wire one of
|
|
119
|
+
them is a mistake that can be reported when the reactor is loaded, not on the unlucky
|
|
120
|
+
production run that first reaches that step.
|
|
121
|
+
|
|
122
|
+
**Why this priority**: High value and cheap once Story 1 exists, but the feature is usable
|
|
123
|
+
without it — the missing value would still be caught at run time by the contract itself.
|
|
124
|
+
|
|
125
|
+
**Independent Test**: Wire a step class but omit one of its required arguments; confirm the
|
|
126
|
+
reactor definition is rejected naming the missing argument.
|
|
127
|
+
|
|
128
|
+
**Acceptance Scenarios**:
|
|
129
|
+
|
|
130
|
+
1. **Given** a step class with two required inputs, **When** a reactor wires one and has no
|
|
131
|
+
reactor input matching the other by name, **Then** loading the reactor fails naming the
|
|
132
|
+
reactor, the step, and the missing input.
|
|
133
|
+
2. **Given** a step class with a required input, **When** the reactor declares a reactor input
|
|
134
|
+
of the same name and wires no argument for it, **Then** loading succeeds and the step
|
|
135
|
+
receives that reactor input's value at run time.
|
|
136
|
+
3. **Given** the same step class, **When** a reactor wires an argument the step does not
|
|
137
|
+
declare, **Then** loading the reactor fails naming the unknown argument.
|
|
138
|
+
4. **Given** a reactor that wires no arguments at all for a step that declares no contract,
|
|
139
|
+
**When** the reactor is loaded, **Then** today's implicit behavior applies unchanged (an
|
|
140
|
+
inline step's body receives all reactor inputs).
|
|
141
|
+
5. **Given** a step class with an optional input and no matching reactor input, **When** a
|
|
142
|
+
reactor omits it, **Then** loading succeeds and the input is absent at run time.
|
|
143
|
+
6. **Given** a step input satisfied by a same-named reactor input, **When** the reactor also
|
|
144
|
+
wires an explicit `argument` for it, **Then** the explicit wiring wins.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
### User Story 5 - Existing reactors keep working through the transition (Priority: P2)
|
|
149
|
+
|
|
150
|
+
Teams already running RubyReactor have reactors full of rules declared on arguments. Their
|
|
151
|
+
code keeps running after upgrading. Where a construct is being retired, they get a clear,
|
|
152
|
+
one-time deprecation message that names the file, the reactor, and the step, and says what to
|
|
153
|
+
write instead — so the migration can be done step by step rather than in one flag day.
|
|
154
|
+
|
|
155
|
+
**Why this priority**: Required by the project's versioning commitments; it does not deliver
|
|
156
|
+
the new capability but it decides whether the new capability is adoptable.
|
|
157
|
+
|
|
158
|
+
**Independent Test**: Run an existing reactor that declares argument-level rules on an
|
|
159
|
+
inline step, unchanged, and confirm identical results plus (where applicable) a deprecation
|
|
160
|
+
notice.
|
|
161
|
+
|
|
162
|
+
**Acceptance Scenarios**:
|
|
163
|
+
|
|
164
|
+
1. **Given** an existing reactor declaring rules on arguments of inline steps, **When** it
|
|
165
|
+
runs after the upgrade, **Then** results and error shapes are unchanged.
|
|
166
|
+
2. **Given** an existing reactor declaring rules on arguments of a step class that declares
|
|
167
|
+
no contract of its own, **When** it runs after the upgrade, **Then** results are
|
|
168
|
+
unchanged.
|
|
169
|
+
3. **Given** a construct scheduled for removal, **When** a reactor using it is loaded, **Then**
|
|
170
|
+
a deprecation notice is emitted once per construct site, naming the replacement.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
### Edge Cases
|
|
175
|
+
|
|
176
|
+
- A step class declares a contract and is used by two reactors that need different bounds
|
|
177
|
+
(e.g. one needs `amount > 0`, another `amount > 100`). The author must resolve this by
|
|
178
|
+
writing two steps, parameterizing the step, or relaxing the contract — the system does not
|
|
179
|
+
offer per-reactor overrides. Documentation must state this explicitly, because the desire
|
|
180
|
+
to override is exactly what produced today's conflict.
|
|
181
|
+
- A step class inherits from another step class that declares inputs: the subclass's contract
|
|
182
|
+
is the parent's declarations plus its own, with a same-named input in the subclass
|
|
183
|
+
replacing the parent's.
|
|
184
|
+
- The same step class is used twice in one reactor under different step names: each use is
|
|
185
|
+
validated independently against the same contract.
|
|
186
|
+
- A step's contract declares an input the reactor maps from a prior step whose result is
|
|
187
|
+
`nil` (skipped, halted, or legitimately nil): the required/optional distinction decides
|
|
188
|
+
pass or fail, and the error must name the source step so the cause is findable.
|
|
189
|
+
- A supplied value is falsey but present — `false`, `0`, `""`, `[]`. "Provided" MUST mean the
|
|
190
|
+
key exists, never that the value is truthy. Today a `false` reactor input resolves to nil by
|
|
191
|
+
the time a step sees it, which contracts would escalate from a silent wrong value into a
|
|
192
|
+
spurious "must be filled" failure, and would make an optional input's default fire on
|
|
193
|
+
`false`. The same applies to a step *result* of `false` mapped into a later step's input,
|
|
194
|
+
and to a falsey value reached through a nested path.
|
|
195
|
+
- Validation runs on every execution path a step can take: inline execution, retried
|
|
196
|
+
attempts, background/async dispatch (validated in the worker), resumed-after-interrupt
|
|
197
|
+
runs, and each iteration of a map.
|
|
198
|
+
- A step class declares a contract but the reactor does not run it (condition/guard is
|
|
199
|
+
false): no validation error is produced for a step that never runs.
|
|
200
|
+
- The optional validation dependency is not installed: a step class that declares a contract
|
|
201
|
+
must fail loudly at load time with an actionable message, never silently skip its rules.
|
|
202
|
+
- A step class is invoked directly in a test or from application code rather than through a
|
|
203
|
+
reactor: the contract still applies, so a direct call and a reactor-run call reject the same
|
|
204
|
+
values. The failure has no reactor name to report, but still names the step and the fields.
|
|
205
|
+
- A step declares an input that is neither wired by an `argument` nor matched by a
|
|
206
|
+
same-named reactor input: the reactor fails to load, naming the step, the input, and both
|
|
207
|
+
ways to satisfy it. A step input is never left to resolve to nil at run time.
|
|
208
|
+
|
|
209
|
+
## Requirements *(mandatory)*
|
|
210
|
+
|
|
211
|
+
### Functional Requirements
|
|
212
|
+
|
|
213
|
+
- **FR-001**: A step class MUST be able to declare its complete input contract — name,
|
|
214
|
+
expected type, optionality, and per-field rules — inside the class itself.
|
|
215
|
+
- **FR-002**: A step class MUST be able to declare rules that span more than one of its
|
|
216
|
+
inputs, expressed with the same rule vocabulary already used elsewhere in the library.
|
|
217
|
+
- **FR-003**: The declared contract MUST be enforced immediately before the step's work runs,
|
|
218
|
+
on every execution path (inline, retry, background/async dispatch, resume after interrupt,
|
|
219
|
+
and each map iteration).
|
|
220
|
+
- **FR-004**: A contract violation MUST produce a failure that (a) prevents the step's work
|
|
221
|
+
from running, (b) identifies the reactor and the step, and (c) exposes field-level errors in
|
|
222
|
+
the same structured shape as today's validation failures, so existing failure handling and
|
|
223
|
+
test matchers keep working.
|
|
224
|
+
- **FR-005**: Reactor-side argument declarations MUST continue to declare step dependencies
|
|
225
|
+
and map values (reactor inputs, prior step results, constants, transforms) to step
|
|
226
|
+
argument names.
|
|
227
|
+
- **FR-006**: When a step class declares its own contract, the reactor MUST NOT be able to
|
|
228
|
+
attach types, per-field rules, or cross-field rule blocks to that step; attempting to do so
|
|
229
|
+
MUST fail when the reactor class is loaded, with a message naming the reactor, step,
|
|
230
|
+
argument, and the step class that owns the contract.
|
|
231
|
+
- **FR-007**: An inline step MUST be able to declare an input contract using the same
|
|
232
|
+
vocabulary a step class uses, such that moving the declarations into a step class requires
|
|
233
|
+
no rewriting of the rules.
|
|
234
|
+
- **FR-008**: When a reactor wires a step class that declares a contract, the system MUST
|
|
235
|
+
verify at reactor-load time that every required input of that step is wired, failing with a
|
|
236
|
+
message naming the reactor, step, and missing input.
|
|
237
|
+
- **FR-009**: Reactor-level `input` declarations and their validation MUST remain unchanged;
|
|
238
|
+
this feature changes only the step layer.
|
|
239
|
+
- **FR-010**: Reactors that declare argument-level rules for inline steps, or for step
|
|
240
|
+
classes that declare no contract, MUST continue to work with unchanged behavior and error
|
|
241
|
+
shapes.
|
|
242
|
+
- **FR-011**: Any construct that this feature retires MUST emit a one-time deprecation notice
|
|
243
|
+
per declaration site naming the replacement, and MUST be recorded as a migration note in the
|
|
244
|
+
project changelog.
|
|
245
|
+
- **FR-012**: A unit of work declares its contract with `input` — the same word the reactor
|
|
246
|
+
uses for the values *it* requires — in both step classes and inline steps. The reactor-side
|
|
247
|
+
wiring declaration keeps the name `argument`, which from now on means only "where this
|
|
248
|
+
step's value comes from". No existing reactor renames anything.
|
|
249
|
+
- **FR-018**: Arguments wired by a reactor for a contract-owning step MUST correspond to
|
|
250
|
+
inputs that step declares; wiring an undeclared argument MUST fail when the reactor is
|
|
251
|
+
loaded, naming the reactor, the step, and the unknown argument.
|
|
252
|
+
- **FR-019**: A step that declares no contract and for which the reactor wires no arguments
|
|
253
|
+
MUST keep today's behavior: an inline step's body receives all reactor inputs. This
|
|
254
|
+
implicit path is unaffected by contract declarations on other steps.
|
|
255
|
+
- **FR-020**: An input declared by a step and not wired by an `argument` MUST be satisfied
|
|
256
|
+
from the reactor input of the same name, when one exists. The fallback MUST consider
|
|
257
|
+
reactor inputs only — never another step's result — so that resolution never depends on the
|
|
258
|
+
order or naming of other steps. An explicit `argument` for the same name MUST take
|
|
259
|
+
precedence.
|
|
260
|
+
- **FR-021**: Every required input of a contract-owning step MUST be resolvable at
|
|
261
|
+
reactor-load time by exactly one of: an explicit `argument`, or a same-named reactor input.
|
|
262
|
+
A required input satisfied by neither MUST fail when the reactor is loaded, with a message
|
|
263
|
+
naming the step, the input, and both ways to satisfy it.
|
|
264
|
+
- **FR-022**: A contract MUST be enforced when the step is invoked directly — from a test or
|
|
265
|
+
from application code — not only when a reactor executes it, so that a step rejects the same
|
|
266
|
+
values through every entry point. A direct-invocation failure carries the step name and
|
|
267
|
+
field errors; the reactor name is absent.
|
|
268
|
+
- **FR-013**: Optional inputs MUST support being absent, and MUST support a declared default
|
|
269
|
+
value applied before the step's work runs when absent.
|
|
270
|
+
- **FR-014**: Contract declarations MUST be introspectable — a step class can be asked which
|
|
271
|
+
inputs it declares, with their types, optionality, and rules — so tooling, the dashboard,
|
|
272
|
+
and reactor-load-time checks can read them.
|
|
273
|
+
- **FR-015**: Redaction of sensitive input values in failures and logs MUST be declarable on a
|
|
274
|
+
step's contract, so a step that receives secrets does not leak them through the
|
|
275
|
+
observability surfaces the project requires.
|
|
276
|
+
- **FR-016**: Documentation (README and library docs) MUST show the class-step form as the
|
|
277
|
+
primary style, the inline form as its equivalent, and the migration path from
|
|
278
|
+
reactor-declared rules.
|
|
279
|
+
- **FR-017**: The feature MUST ship a runnable demo reactor, a `demo:` rake task, and a spec
|
|
280
|
+
using only the shipped test matchers, per the project's demo-app requirement — including
|
|
281
|
+
the failure path where a contract is violated.
|
|
282
|
+
- **FR-023**: Presence MUST be determined by whether a value was supplied, not by whether it
|
|
283
|
+
is truthy. A supplied `false` (or any other falsey value) MUST reach the step unchanged,
|
|
284
|
+
MUST satisfy a required input, and MUST NOT trigger an optional input's default. This holds
|
|
285
|
+
for values sourced from reactor inputs, from prior step results, and from nested paths
|
|
286
|
+
within either.
|
|
287
|
+
|
|
288
|
+
### Key Entities *(include if data involved)*
|
|
289
|
+
|
|
290
|
+
- **Step Input Contract**: the set of input declarations owned by a unit of work (step class
|
|
291
|
+
or inline step). Attributes: input name, expected type, optionality, default, per-field
|
|
292
|
+
rules, cross-field rules, redaction flag. Owned by exactly one unit; never merged across
|
|
293
|
+
layers.
|
|
294
|
+
- **Argument Wiring**: the reactor-side statement binding a step's declared input name to a
|
|
295
|
+
source (reactor input, another step's result, constant, transform). Carries dependency
|
|
296
|
+
information for execution ordering. Contains no rules.
|
|
297
|
+
- **Validation Failure**: the structured outcome of a violated contract. Attributes: reactor
|
|
298
|
+
name, step name, field-level errors, redacted values.
|
|
299
|
+
|
|
300
|
+
## Success Criteria *(mandatory)*
|
|
301
|
+
|
|
302
|
+
### Measurable Outcomes
|
|
303
|
+
|
|
304
|
+
- **SC-001**: A reader can determine every value a step class requires, and the rules on
|
|
305
|
+
each, by reading only that class — verified by the fact that no reactor in the demo app
|
|
306
|
+
declares a rule for any contract-owning step.
|
|
307
|
+
- **SC-002**: A step class reused in more than one reactor is validated identically in every
|
|
308
|
+
reactor, with zero per-reactor rule declarations.
|
|
309
|
+
- **SC-003**: 100% of conflicting declarations (rules in both the step class and the reactor)
|
|
310
|
+
are reported when the reactor is loaded, not at run time.
|
|
311
|
+
- **SC-004**: 100% of unwired required inputs on contract-owning step classes are reported
|
|
312
|
+
when the reactor is loaded, not at run time.
|
|
313
|
+
- **SC-005**: Moving an inline step's declarations into a step class requires no change to
|
|
314
|
+
the rule text, and produces identical outcomes for both conforming and violating values.
|
|
315
|
+
- **SC-006**: Every existing test in the suite that exercises reactor-declared step rules
|
|
316
|
+
passes unchanged, except those covering constructs explicitly retired with a documented
|
|
317
|
+
migration note.
|
|
318
|
+
- **SC-007**: A contract violation reports the reactor, the step, and each offending field, so
|
|
319
|
+
an operator can identify the cause from a single failure record without re-running.
|
|
320
|
+
- **SC-008**: The demo app's contract example runs end to end via the documented container
|
|
321
|
+
command, demonstrating both the passing and the failing path.
|
|
322
|
+
- **SC-009**: A reactor whose input names already match its steps' declared input names needs
|
|
323
|
+
zero argument declarations to run, and 100% of unsatisfiable required step inputs are
|
|
324
|
+
reported when the reactor is loaded rather than at run time.
|
|
325
|
+
- **SC-010**: A step invoked directly and the same step invoked through a reactor accept and
|
|
326
|
+
reject exactly the same values.
|
|
327
|
+
- **SC-011**: A boolean input supplied as `false` — sourced from a reactor input, a prior
|
|
328
|
+
step's result, or a nested path — arrives at the step as `false` in 100% of cases, and is
|
|
329
|
+
never reported as missing.
|
|
330
|
+
|
|
331
|
+
## Assumptions
|
|
332
|
+
|
|
333
|
+
- The audience is developers authoring reactors and steps with this library; "users" in this
|
|
334
|
+
spec means those developers.
|
|
335
|
+
- The existing rule vocabulary (types, predicates such as minimum/maximum/inclusion,
|
|
336
|
+
cross-field rule blocks) is reused as-is; this feature relocates *where* rules are declared
|
|
337
|
+
and *who* owns them, not the rules themselves.
|
|
338
|
+
- Step classes that declare no contract remain fully supported; declaring a contract is
|
|
339
|
+
opt-in per step.
|
|
340
|
+
- Because a contract is owned by the step, per-reactor overrides are deliberately not offered
|
|
341
|
+
— that capability is the source of the ambiguity this feature removes.
|
|
342
|
+
- Composed reactors, map steps, and async/background steps are in scope only insofar as
|
|
343
|
+
contracts must be enforced on their execution paths; their own declaration surfaces are
|
|
344
|
+
unchanged in this feature.
|
|
345
|
+
- Output validation (`validate_output`) is out of scope and keeps its current shape and
|
|
346
|
+
location.
|
|
347
|
+
- Reactor-level input validation is out of scope and unchanged.
|
|
348
|
+
- The optional validation dependency remains optional for the library as a whole; only steps
|
|
349
|
+
that declare a contract require it.
|
|
350
|
+
- A unit of work declares its contract with `input`; the reactor's `argument` becomes
|
|
351
|
+
wiring-only. No existing declaration is renamed.
|
|
352
|
+
- Correcting falsey-value loss during argument resolution is treated as in-scope for this
|
|
353
|
+
feature rather than a separate fix: contracts make presence semantics load-bearing, so
|
|
354
|
+
shipping them over the current behavior would turn a silent wrong value into a visible
|
|
355
|
+
spurious failure. It is a pre-existing defect, so its correction is a fix, not a breaking
|
|
356
|
+
change.
|
|
357
|
+
- Name-based resolution of an unwired step input looks at reactor inputs only. Extending it to
|
|
358
|
+
step results was considered and rejected: it would make a step's wiring depend on the names
|
|
359
|
+
of unrelated steps.
|