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,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.