ruby_reactor 0.7.1 → 0.8.1

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/.specify/feature.json +1 -1
  4. data/.specify/memory/constitution.md +26 -16
  5. data/.specify/templates/plan-template.md +4 -0
  6. data/.specify/templates/tasks-template.md +1 -1
  7. data/CHANGELOG.md +150 -3
  8. data/CLAUDE.md +1 -1
  9. data/README.md +171 -63
  10. data/lib/ruby_reactor/adapters/active_job/router.rb +2 -2
  11. data/lib/ruby_reactor/adapters/sidekiq/router.rb +2 -2
  12. data/lib/ruby_reactor/context.rb +2 -2
  13. data/lib/ruby_reactor/dsl/interrupt_builder.rb +6 -0
  14. data/lib/ruby_reactor/dsl/map_builder.rb +31 -5
  15. data/lib/ruby_reactor/dsl/reactor.rb +36 -18
  16. data/lib/ruby_reactor/dsl/step_builder.rb +95 -2
  17. data/lib/ruby_reactor/dsl/template_helpers.rb +2 -2
  18. data/lib/ruby_reactor/dsl/validation_helpers.rb +17 -0
  19. data/lib/ruby_reactor/error/input_validation_error.rb +4 -0
  20. data/lib/ruby_reactor/error/step_failure_error.rb +10 -3
  21. data/lib/ruby_reactor/executor/result_handler.rb +9 -3
  22. data/lib/ruby_reactor/executor/retry_manager.rb +2 -1
  23. data/lib/ruby_reactor/executor/step_executor.rb +9 -2
  24. data/lib/ruby_reactor/executor.rb +3 -0
  25. data/lib/ruby_reactor/map/helpers.rb +9 -0
  26. data/lib/ruby_reactor/max_retries_exhausted_failure.rb +3 -2
  27. data/lib/ruby_reactor/reactor.rb +9 -12
  28. data/lib/ruby_reactor/rspec/matchers.rb +3 -6
  29. data/lib/ruby_reactor/step/async_reactor_step.rb +159 -162
  30. data/lib/ruby_reactor/step/compose_step.rb +56 -75
  31. data/lib/ruby_reactor/step/input_contract.rb +128 -0
  32. data/lib/ruby_reactor/step/map_step.rb +182 -218
  33. data/lib/ruby_reactor/step.rb +116 -21
  34. data/lib/ruby_reactor/step_signals.rb +6 -2
  35. data/lib/ruby_reactor/step_worker.rb +25 -10
  36. data/lib/ruby_reactor/template/result.rb +9 -2
  37. data/lib/ruby_reactor/utils/fetch_indifferent.rb +13 -0
  38. data/lib/ruby_reactor/version.rb +1 -1
  39. data/lib/ruby_reactor.rb +5 -2
  40. data/specs/002-step-input-contracts/checklists/requirements.md +49 -0
  41. data/specs/002-step-input-contracts/contracts/dsl-surface.md +193 -0
  42. data/specs/002-step-input-contracts/data-model.md +115 -0
  43. data/specs/002-step-input-contracts/plan.md +165 -0
  44. data/specs/002-step-input-contracts/quickstart.md +170 -0
  45. data/specs/002-step-input-contracts/research.md +233 -0
  46. data/specs/002-step-input-contracts/spec.md +359 -0
  47. data/specs/002-step-input-contracts/tasks.md +367 -0
  48. data/specs/004-inheritable-step-class/checklists/requirements.md +40 -0
  49. data/specs/004-inheritable-step-class/contracts/step-lifecycle.md +85 -0
  50. data/specs/004-inheritable-step-class/data-model.md +116 -0
  51. data/specs/004-inheritable-step-class/plan.md +174 -0
  52. data/specs/004-inheritable-step-class/quickstart.md +112 -0
  53. data/specs/004-inheritable-step-class/research.md +308 -0
  54. data/specs/004-inheritable-step-class/spec.md +316 -0
  55. data/specs/004-inheritable-step-class/tasks.md +258 -0
  56. data/specs/deferred-003-step-lock-declarations/checklists/requirements.md +51 -0
  57. data/specs/deferred-003-step-lock-declarations/contracts/dsl-surface.md +154 -0
  58. data/specs/deferred-003-step-lock-declarations/data-model.md +131 -0
  59. data/specs/deferred-003-step-lock-declarations/plan.md +166 -0
  60. data/specs/deferred-003-step-lock-declarations/quickstart.md +169 -0
  61. data/specs/deferred-003-step-lock-declarations/research.md +196 -0
  62. data/specs/deferred-003-step-lock-declarations/spec.md +447 -0
  63. data/specs/deferred-003-step-lock-declarations/tasks.md +572 -0
  64. data/specs/possible_feature.md +22 -0
  65. metadata +28 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: abde4c2dc313b2e5d4a74873f1fc337743e6847e8f47fcbd121fd1a72be1d557
4
- data.tar.gz: defd25588b78022c339ffd105d4e304d80e676498272c887cafecb60f100dc7e
3
+ metadata.gz: 0d5c03b684aeac0b3d682581e9746e59219622f07752a5b5e30b5381582c09c9
4
+ data.tar.gz: bccbbdf3eec2187481fba0c5d724b1b2b61d96a32c93139dcf5fcd20e42ff491
5
5
  SHA512:
6
- metadata.gz: 595c701f476086c166d0e157c062e0bea0e6fb243b1c5466017bf488461b987b44c70b56e081770c586a1d83519311c58da9769ae83e302fb7d17d1cdc8f37fb
7
- data.tar.gz: 7dadad987df2ec6d9c1b44e93ed328f07d77660310726dceeb1ee8957cc9e63b73bd3c2338792280bfc780a95650bcac12b384af3d2aad83b3309aa4896f6d0d
6
+ metadata.gz: ab06166bc3462aa2cf8f4569015575feb7c37ae025022ae84b886dcc171040c2a89a88dbc69208c8b1ec84be6dd19c08c418915856a510506402e7ab72f26044
7
+ data.tar.gz: cfb0973443827d2d50572f834b1d9d2760464eab4290097b38c73465fcb7268f9e381d211b4a2fe1815f6bb410ac56ffa64c4f61ed17f0a765966392c50494a4
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.7.1"
2
+ ".": "0.8.1"
3
3
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "feature_directory": "specs/001-reactor-signal-semantics"
2
+ "feature_directory": "specs/004-inheritable-step-class"
3
3
  }
@@ -1,25 +1,27 @@
1
1
  <!--
2
2
  SYNC IMPACT REPORT
3
3
  ==================
4
- Version change: 1.1.0 → 1.2.0 (MINOR: Principle VI expanded with a 4th requirement —
5
- docker-compose.yml currency + docker-run acceptance tests)
4
+ Version change: 1.2.1 → 1.3.0 (MINOR: Development Workflow now REQUIRES every generated
5
+ plan.md and tasks.md to carry an explicit documentation-update task/gate, with the
6
+ requirement propagated into both templates — a new enforcement mechanism, not just
7
+ wording)
6
8
 
7
- Modified principles:
8
- - Principle VI: Demo-App Proof of Feature — added requirement 4, "Docker acceptance
9
- run" (docker-compose.yml MUST track demo_app's services; demo: rake tasks are the
10
- project's acceptance suite, runnable via `docker compose run`)
9
+ Modified principles: none (Development Workflow section, not a Core Principle)
11
10
 
12
- Added sections: none (existing Principle VI section extended)
11
+ Added sections: none
13
12
 
14
13
  Removed sections: none
15
14
 
16
15
  Templates checked:
17
- - .specify/templates/plan-template.md ✅ Constitution Check gate is generic ("Gates determined
18
- based on constitution file") — no edit required
19
- - .specify/templates/spec-template.md ✅ No principle-specific content — no edit required
20
- - .specify/templates/tasks-template.md ✅ Already covers demo-app polish tasks — no edit required
16
+ - .specify/templates/plan-template.md ✅ Added Constitution Check checklist item:
17
+ identify README.md/./documentation impact
18
+ - .specify/templates/spec-template.md ✅ No principle-specific content — no edit required
19
+ - .specify/templates/tasks-template.md ✅ Polish-phase doc task now explicit and marked
20
+ REQUIRED, citing README.md + ./documentation
21
21
  - .specify/templates/checklist-template.md ✅ Generic — no edit required
22
- - .specify/extensions.yml ✅ No before/after_constitution hooks registered
22
+ - .specify/extensions.yml ✅ No before/after_constitution hooks registered
23
+ - README.md ✅ No principle-specific claims to update
24
+ - documentation/*.md ✅ No principle-specific claims to update
23
25
 
24
26
  Deferred TODOs: none
25
27
  -->
@@ -164,9 +166,17 @@ gaps in the matcher library surface as work instead of as workarounds.
164
166
  - Feature branches target `main`. PRs MUST pass CI (RSpec + RuboCop) before merge.
165
167
  - Releases are managed by release-please. Version bump lives in
166
168
  `lib/ruby_reactor/version.rb`. Do not manually edit the version in gemspec.
167
- - New features MUST update `README.md` documentation and add entries to
168
- `CHANGELOG.md` under the correct semantic heading (`Features`, `Bug Fixes`,
169
- `documentation`).
169
+ - Every task MUST update documentation as part of its own change, not as a
170
+ follow-up: `README.md` and any affected file under `./documentation` MUST be
171
+ kept current with the behavior the task introduces, changes, or removes. A
172
+ task that alters observable behavior without a matching documentation update
173
+ is incomplete. New features additionally MUST add entries to `CHANGELOG.md`
174
+ under the correct semantic heading (`Features`, `Bug Fixes`, `documentation`).
175
+ - Every `plan.md` produced by `/speckit-plan` and every `tasks.md` produced by
176
+ `/speckit-tasks` MUST include at least one explicit task or checklist item to
177
+ update `README.md` and the relevant file(s) under `./documentation` for the
178
+ feature's behavior. A plan or task list that omits a documentation-update
179
+ task is incomplete and MUST be rejected in review.
170
180
  - Class-based step definitions are the preferred authoring style (not inline
171
181
  lambdas). Documentation and examples MUST reflect this.
172
182
  - The `demo_app/` directory serves as a living integration example. Changes to
@@ -198,4 +208,4 @@ justified in the `Complexity Tracking` table of the plan.
198
208
  Compliance review: at each MINOR or MAJOR gem release, confirm this constitution
199
209
  still accurately reflects the codebase and update as needed.
200
210
 
201
- **Version**: 1.2.0 | **Ratified**: 2025-10-02 | **Last Amended**: 2026-09-09
211
+ **Version**: 1.3.0 | **Ratified**: 2025-10-02 | **Last Amended**: 2026-09-11
@@ -42,6 +42,10 @@
42
42
 
43
43
  [Gates determined based on constitution file]
44
44
 
45
+ - [ ] Documentation impact identified: which `README.md` sections and which
46
+ file(s) under `./documentation` this feature will require updating
47
+ (Constitution Development Workflow — carried into tasks.md as a required task)
48
+
45
49
  ## Project Structure
46
50
 
47
51
  ### Documentation (this feature)
@@ -151,7 +151,7 @@ Examples of foundational tasks (adjust based on your project):
151
151
 
152
152
  **Purpose**: Improvements that affect multiple user stories
153
153
 
154
- - [ ] TXXX [P] Documentation updates in docs/
154
+ - [ ] TXXX [P] Update `README.md` and affected file(s) under `./documentation` for this feature (REQUIRED — Constitution Development Workflow)
155
155
  - [ ] TXXX Code cleanup and refactoring
156
156
  - [ ] TXXX Performance optimization across all stories
157
157
  - [ ] TXXX [P] Additional unit tests (if requested) in tests/unit/
data/CHANGELOG.md CHANGED
@@ -1,5 +1,152 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ### ⚠ BREAKING CHANGES
6
+
7
+ * **`RubyReactor::Step` is now a base class, not a mixin.** `include RubyReactor::Step` on a
8
+ plain class with `def self.run(arguments, context)` is gone — no compatibility shim, no dual
9
+ authoring style. A step subclasses `RubyReactor::Step` and writes `run` (and optionally
10
+ `compensate`/`undo`) as **instance** methods reading the validated arguments and the context
11
+ through `inputs`/`context` accessors instead of parameters. Every instance is built fresh for
12
+ its one action (`run`, `undo`, or `compensate`) from the stored arguments/result/reason alone —
13
+ nothing an author sets in `run` is visible in a later `undo`/`compensate`, which is exactly what
14
+ makes rollback behave identically whether it lands in the same process as `run` or, as with an
15
+ `async_step`, in a separate later one. `inputs` holds the same values in all three actions: the
16
+ arguments with the contract's defaults applied. Only `run` enforces the contract; `undo` and
17
+ `compensate` never do, so rollback cannot fail on the inputs that caused the failure.
18
+
19
+ ```ruby
20
+ # Before
21
+ class ChargeStep
22
+ include RubyReactor::Step
23
+
24
+ input :amount, :integer, gteq?: 1
25
+
26
+ def self.run(arguments, context)
27
+ Success(charge!(arguments[:amount]))
28
+ end
29
+
30
+ def self.undo(result, arguments, context)
31
+ refund!(result[:charge_id])
32
+ Success()
33
+ end
34
+ end
35
+
36
+ # After
37
+ class ChargeStep < RubyReactor::Step
38
+ input :amount, :integer, gteq?: 1
39
+
40
+ def run
41
+ Success(charge!(inputs[:amount]))
42
+ end
43
+
44
+ def undo
45
+ refund!(result[:charge_id])
46
+ Success()
47
+ end
48
+ end
49
+ ```
50
+
51
+ **Migration:** for every class step, replace `include RubyReactor::Step` with
52
+ `< RubyReactor::Step`, turn `def self.run(args, ctx)` into `def run` reading `inputs`/`context`,
53
+ and likewise for `def self.undo(result, args, ctx)` / `def self.compensate(reason, args, ctx)` →
54
+ `def undo` / `def compensate` reading `result`/`reason`/`inputs`/`context`. The class-level
55
+ `MyStep.run(arguments, context)` / `.call` / `.undo(result, arguments, context)` /
56
+ `.compensate(reason, arguments, context)` entry points every caller (executor, worker, a direct
57
+ unit-test call) already used are unchanged. Inline `step { run { |args, ctx| ... } }` blocks are
58
+ untouched by this change.
59
+
60
+ * **A class step's signal helpers (`success!`/`fail!`/`skip!`/`halt!`) now translate correctly on
61
+ every execution path, including the `async_step`/`background` worker.** Previously the worker had
62
+ no `catch` of its own, so a signal thrown from a class step running there escaped as an
63
+ `UncaughtThrowError` instead of the intended `Success`/`Failure`/`Skipped`/`Halt` — the base
64
+ class's class-level `run`/`undo`/`compensate` now own that translation, so every caller gets it
65
+ for free with no worker change. Known remaining gap, unchanged by this release: an **inline**
66
+ `run`/`compensate`/`undo` block's signals are still uncaught on the worker path.
67
+
68
+ * **A step's own input-validation failure is now guaranteed non-retryable on every path.**
69
+ `result.retryable?` is `false` whether the violation happened synchronously, inside an
70
+ `async_step`/`background` worker, or inside a `compose`d child's own step — previously only the
71
+ async worker path got this right; the synchronous path and a validation failure surfacing
72
+ through `compose` both defaulted to `retryable? == true`.
73
+
74
+ * **`Failure(...)` takes the same arguments everywhere.** Inside a class step and inside an inline
75
+ `run`/`compensate`/`undo` block, `Failure` now forwards every argument to `RubyReactor.Failure`,
76
+ so options such as `Failure("declined", retryable: false)` work instead of raising
77
+ `ArgumentError`. A bare `Failure()` with no error is no longer accepted, matching
78
+ `RubyReactor.Failure`, and a hash error needs braces, `Failure({ code: 1 })`, because a braceless
79
+ `Failure(code: 1)` is now read as options.
80
+
81
+ ### Features
82
+
83
+ * **Step input contracts.** A step class declares its own inputs with `input :name, :type, **predicates`
84
+ (plus `optional:`, `default:`, `redact:`, the `do |i| ... end` macro block and `validate:`) and
85
+ cross-field rules with `validate_inputs`. The contract is enforced before `run` on every path
86
+ (inline, retries, `async_step` and `background` workers, resume, `map`, and a direct
87
+ `Step.run(args, context)` call), and a violation fails with `validation_errors` and the step's
88
+ name after completed steps are rolled back. Subclasses inherit and extend the contract.
89
+ Introspection: `input_contract`, `declared_inputs`, `required_input_names`, `declares_inputs?`.
90
+ * **Inline step contracts.** `inputs do ... end` inside a `step` block takes the same `input` /
91
+ `validate_inputs` lines as a step class and is enforced the same way.
92
+ * **Wiring by name.** A declared input with no `argument` resolves from the reactor input of the
93
+ same name (never from a step result). A required input that is neither wired nor a reactor input
94
+ raises `Error::ValidationError` before any step runs. `Reactor.validate_definition!` runs that
95
+ check on demand, e.g. from an initializer or CI.
96
+ * For a step that owns a contract, a type or predicate on `argument`, `validate_args`, or an
97
+ `argument` for an undeclared input raises `Error::ValidationError` when the `step` line is
98
+ evaluated.
99
+ * `have_validation_error` now also matches validation failures raised at a step, not only
100
+ reactor-input failures.
101
+ * `Failure#to_h` includes `retryable`, so a non-retryable failure stays non-retryable after it
102
+ crosses a worker boundary.
103
+ * A step that returns another unit's validation failure (e.g. an `async_step` reader propagating
104
+ the worker's `Failure`) keeps its `validation_errors` on the reactor's final failure.
105
+
106
+ ### Deprecations
107
+
108
+ * Rules on `argument` (`argument :x, src, :type, **predicates`) and `validate_args` keep working
109
+ for steps without a contract, and print one deprecation notice per declaration site. Move them
110
+ to `input` / `validate_inputs` on the step class, or into an `inputs do ... end` block for an
111
+ inline step, and keep `argument :x, src` for wiring. Removal is no earlier than the next major
112
+ version. See "Step Input Contracts" in the README for the migration.
113
+
114
+ ### Bug Fixes
115
+
116
+ * A supplied `false` reactor input or step result no longer resolves to `nil`.
117
+ `Context#get_input`, `Context#get_result` and `Template::Result#fetch` now check whether the key
118
+ exists instead of whether the value is truthy. Code that relied on `false` arriving as `nil`
119
+ will now see `false`.
120
+ * An input-validation `Failure` stays non-retryable across serialization. `Failure`'s
121
+ hash extractor and the reactor's stored `failure_reason` both dropped a `retryable: false`
122
+ (`||` swallowed the `false`, and the reactor never stored the flag at all), so a failure
123
+ rebuilt from JSON or reloaded with `Reactor.find` reported `retryable? == true`.
124
+ * A step-contract violation inside `compose` now reaches the parent with its `validation_errors`
125
+ and retryability intact, instead of being rebuilt from the child's error message alone.
126
+ * A reactor reopened after its first run is re-checked: `validate_definition!` no longer memoizes,
127
+ so a step declared later can no longer reach execution with a required input unwired.
128
+ * A class step that calls `halt!` under `async_step` is recorded as a halt rather than an
129
+ ordinary `nil` success, and `result(:step)` hands the reader the `Halt` — the same way it
130
+ already hands over a `Failure`.
131
+
132
+ ## [0.8.1](https://github.com/arturictus/ruby_reactor/compare/v0.8.0...v0.8.1) (2026-09-22)
133
+
134
+
135
+ ### Miscellaneous Chores
136
+
137
+ * Update documentation clarifying background and async concepts ([#55](https://github.com/arturictus/ruby_reactor/issues/55)) ([968e32b](https://github.com/arturictus/ruby_reactor/commit/968e32bbc695e45471d04b7630d8cd437ff41624))
138
+
139
+ ## [0.8.0](https://github.com/arturictus/ruby_reactor/compare/v0.7.1...v0.8.0) (2026-09-21)
140
+
141
+
142
+ ### ⚠ BREAKING CHANGES
143
+
144
+ * Step Instance and input validations per step ([#51](https://github.com/arturictus/ruby_reactor/issues/51))
145
+
146
+ ### Features
147
+
148
+ * Step Instance and input validations per step ([#51](https://github.com/arturictus/ruby_reactor/issues/51)) ([4a20583](https://github.com/arturictus/ruby_reactor/commit/4a2058399d73e9b3bf767659dd5ba080537dc291))
149
+
3
150
  ## [0.7.1](https://github.com/arturictus/ruby_reactor/compare/v0.7.0...v0.7.1) (2026-09-14)
4
151
 
5
152
 
@@ -58,9 +205,9 @@
58
205
  point (`:x` stays in the calling process); the two coincide in a linear chain
59
206
  but pin different steps in a DAG.
60
207
 
61
- Not affected: the map-internal `async` element dispatch option
62
- (`map :items do async true, batch_size: 2 end`) — a different mechanism that
63
- keeps working unchanged.
208
+ Also affected: the map-internal element dispatch option was renamed from
209
+ `async true` to `fan_out`, so `map :items do async true, batch_size: 2 end`
210
+ now raises and should become `map :items do fan_out batch_size: 2 end`.
64
211
 
65
212
  One behavior change falls out of "exactly one hand-off point per reactor":
66
213
  resuming a reactor past its hand-off point now finishes in the resuming
data/CLAUDE.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <!-- SPECKIT START -->
2
2
  For additional context about technologies to be used, project structure,
3
3
  shell commands, and other important information, read the current plan:
4
- `specs/001-reactor-signal-semantics/plan.md`
4
+ `specs/004-inheritable-step-class/plan.md`
5
5
  <!-- SPECKIT END -->