ruby_reactor 0.8.3 → 0.8.4
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/CHANGELOG.md +12 -0
- data/CLAUDE.md +1 -1
- data/README.md +93 -91
- data/lib/ruby_reactor/dsl/async_reactor_builder.rb +3 -6
- data/lib/ruby_reactor/dsl/compose_builder.rb +3 -10
- data/lib/ruby_reactor/dsl/reactor.rb +8 -11
- data/lib/ruby_reactor/dsl/retryable.rb +45 -0
- data/lib/ruby_reactor/dsl/step_builder.rb +40 -14
- data/lib/ruby_reactor/error/undeclared_input_error.rb +13 -0
- data/lib/ruby_reactor/executor/compensation_manager.rb +2 -2
- data/lib/ruby_reactor/executor/result_handler.rb +3 -2
- data/lib/ruby_reactor/executor/retry_manager.rb +11 -12
- data/lib/ruby_reactor/rspec/test_subject.rb +3 -5
- data/lib/ruby_reactor/step/async_reactor_step.rb +6 -2
- data/lib/ruby_reactor/step/compose_step.rb +8 -4
- data/lib/ruby_reactor/step/input_contract.rb +5 -0
- data/lib/ruby_reactor/step/inputs.rb +57 -0
- data/lib/ruby_reactor/step/map_step.rb +32 -22
- data/lib/ruby_reactor/step.rb +13 -7
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor.rb +3 -1
- data/specs/006-step-retry-declarations/checklists/requirements.md +41 -0
- data/specs/006-step-retry-declarations/contracts/dsl-surface.md +89 -0
- data/specs/006-step-retry-declarations/data-model.md +58 -0
- data/specs/006-step-retry-declarations/plan.md +187 -0
- data/specs/006-step-retry-declarations/quickstart.md +80 -0
- data/specs/006-step-retry-declarations/research.md +194 -0
- data/specs/006-step-retry-declarations/spec.md +453 -0
- data/specs/006-step-retry-declarations/tasks.md +382 -0
- data/specs/specs-inputs-by-method-md-piped-wigderson.md +77 -0
- metadata +13 -1
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
---
|
|
2
|
+
|
|
3
|
+
description: "Task list for Step-Scoped Retry Declarations"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Tasks: Step-Scoped Retry Declarations
|
|
7
|
+
|
|
8
|
+
**Input**: Design documents from `specs/006-step-retry-declarations/`
|
|
9
|
+
|
|
10
|
+
**Prerequisites**: [plan.md](plan.md), [spec.md](spec.md), [research.md](research.md),
|
|
11
|
+
[data-model.md](data-model.md), [contracts/dsl-surface.md](contracts/dsl-surface.md),
|
|
12
|
+
[quickstart.md](quickstart.md)
|
|
13
|
+
|
|
14
|
+
**Tests**: REQUIRED. Constitution III requires test-first: write every spec task first and
|
|
15
|
+
see it fail before starting the implementation tasks in the same phase. Run specs against
|
|
16
|
+
real Redis. Background-path specs must use **constant-named** reactor and step classes (as in
|
|
17
|
+
`spec/ruby_reactor/retry_signals_spec.rb`), because a worker rehydrates reactors by class
|
|
18
|
+
name.
|
|
19
|
+
|
|
20
|
+
**Delivery order (spec FR-017)**: Phase 2 (US1, removing `retry_defaults`) is committed as a
|
|
21
|
+
standalone `feat!` with the suite green **before any later phase starts**. No later task may
|
|
22
|
+
read or mention `retry_defaults`, except the removal stub and its spec.
|
|
23
|
+
|
|
24
|
+
## Format: `[ID] [P?] [Story] Description`
|
|
25
|
+
|
|
26
|
+
- **[P]**: can run in parallel (different files, no dependency on an incomplete task)
|
|
27
|
+
- **[Story]**: the user story from spec.md (US1–US7)
|
|
28
|
+
|
|
29
|
+
## Path Conventions
|
|
30
|
+
|
|
31
|
+
Gem: `lib/ruby_reactor/`, `spec/`. Demo: `demo_app/`. New feature specs:
|
|
32
|
+
`spec/ruby_reactor/step_retries/` (one file per story).
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Phase 1: Setup
|
|
37
|
+
|
|
38
|
+
**Purpose**: record a green baseline so the regressions in the later phases can be told apart.
|
|
39
|
+
|
|
40
|
+
- [X] T001 Confirm Redis is reachable and record the baseline: run `bundle exec rspec spec/async_retry_dsl_spec.rb spec/async_retry_integration_spec.rb spec/ruby_reactor_spec.rb spec/ruby_reactor/retry_signals_spec.rb spec/ruby_reactor/retry_reexecution_spec.rb spec/ruby_reactor/order_processing_reactor_spec.rb` and `bundle exec rubocop` from the repo root. Note any pre-existing failures in the PR description (see the flaky-spec note: rerun a failing file alone before treating it as real).
|
|
41
|
+
- [X] T002 Create the directory `spec/ruby_reactor/step_retries/` for the new feature specs.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Phase 2: User Story 1 — Remove reactor-wide retry defaults first (Priority: P1) 🎯 MVP, ships first
|
|
46
|
+
|
|
47
|
+
**Goal**: `retry_defaults` no longer exists. Calling it fails at class definition. A step
|
|
48
|
+
with no `retries` runs once. Nothing reads a reactor-level default.
|
|
49
|
+
|
|
50
|
+
**Independent Test**: `Class.new(RubyReactor::Reactor) { retry_defaults max_attempts: 3 }`
|
|
51
|
+
raises `DeprecatedDslError`. A reactor whose failing step declares nothing runs it once.
|
|
52
|
+
`grep -rn retry_defaults lib spec demo_app documentation README.md llms*.txt` matches only
|
|
53
|
+
the stub and its spec.
|
|
54
|
+
|
|
55
|
+
### Tests for User Story 1 (write first, confirm failing)
|
|
56
|
+
|
|
57
|
+
- [X] T003 [US1] In `spec/async_retry_dsl_spec.rb`, replace the example `"supports retry_defaults class method"` (lines 23-33) with `"rejects the removed reactor-level retry_defaults"`. It should expect `Class.new(RubyReactor::Reactor) { retry_defaults max_attempts: 5, backoff: :linear, base_delay: 2 }` to raise `RubyReactor::Error::DeprecatedDslError` with a message matching `/retry_defaults.*removed/m` and `/retries/`. Add a second example: calling `retry_defaults` with no arguments on a reactor class raises the same error.
|
|
58
|
+
- [X] T004 [P] [US1] Create `spec/ruby_reactor/step_retries/removal_spec.rb` with these cases:
|
|
59
|
+
- (a) an anonymous reactor whose step `run` always returns `Failure("boom")`, with no `retries` anywhere, returns a failure after exactly 1 attempt (`reactor.context.retry_context.attempts_for_step(:x) == 1`, error not prefixed "failed after");
|
|
60
|
+
- (b) the same for a `compose` step whose block declares no `retries`: `steps[:c].retry_config[:max_attempts] == 1`;
|
|
61
|
+
- (c) the same for `async_reactor`: `steps[:a].retry_config[:max_attempts] == 1`;
|
|
62
|
+
- (d) the `DeprecatedDslError` message names the reactor when the class is constant-named (define `RemovalSpecNamedReactor` via `stub_const` + `Class.new`, then call `retry_defaults` inside `class_eval`).
|
|
63
|
+
|
|
64
|
+
### Implementation for User Story 1
|
|
65
|
+
|
|
66
|
+
- [X] T005 [US1] In `lib/ruby_reactor/dsl/reactor.rb`:
|
|
67
|
+
- delete line 14 (`base.instance_variable_set(:@retry_defaults, …)`);
|
|
68
|
+
- replace `def retry_defaults(**kwargs) … end` (lines 58-68) with `def retry_defaults(*, **)`, which raises `RubyReactor::Error::DeprecatedDslError`. Model the message on the `async` removal stub at lines 48-56 and follow the shape in `contracts/dsl-surface.md` §1: "`retry_defaults` has been removed from #{name || "this reactor"}: reactor-wide defaults silently applied only to steps declared after them. Declare `retries` on each step class (or step block) that should retry; a step with no `retries` runs once."
|
|
69
|
+
- [X] T006 [US1] In `lib/ruby_reactor/dsl/step_builder.rb`:
|
|
70
|
+
- initialize `@retry_config = nil` (line 33, currently `{}`);
|
|
71
|
+
- in `build` (line 163), pass `retry_config: @retry_config`;
|
|
72
|
+
- in `StepConfig`, add `NO_RETRIES = { max_attempts: 1, backoff: :exponential, base_delay: 1 }.freeze` and change line 271 to `@retry_config = config[:retry_config] || NO_RETRIES`.
|
|
73
|
+
|
|
74
|
+
Keep `StepBuilder#retries` for now (it is replaced in Phase 3).
|
|
75
|
+
- [X] T007 [P] [US1] In `lib/ruby_reactor/dsl/compose_builder.rb`, initialize `@retry_config = nil` (line 23) and pass `retry_config: @retry_config` in `build` (line 74).
|
|
76
|
+
- [X] T008 [P] [US1] In `lib/ruby_reactor/dsl/async_reactor_builder.rb`, initialize `@retry_config = nil` (line 20) and pass `retry_config: @retry_config` in `build` (line 52).
|
|
77
|
+
- [X] T009 [P] [US1] In `lib/ruby_reactor/executor/retry_manager.rb` (lines 27-35), make `calculate_backoff_delay` read only `step_config.retry_config[:backoff]` and `[:base_delay]`. Drop the now-unused `reactor_class` parameter from `calculate_backoff_delay` and from its callers `requeue_job_for_step_retry` and `handle_sync_retry` (keep `reactor_class` wherever it is still used, e.g. for `async?` and `MaxRetriesExhaustedFailure`).
|
|
78
|
+
- [X] T010 [P] [US1] In `lib/ruby_reactor/rspec/test_subject.rb`, delete the three `@retry_defaults = superclass.instance_variable_get(:@retry_defaults)` lines (559, 651, 739).
|
|
79
|
+
- [X] T011 [US1] Run `grep -rn "retry_config" lib` and check that no consumer calls `.empty?` on a builder's `@retry_config` or assumes it is a Hash before `StepConfig` normalizes it. Fix any hit in place.
|
|
80
|
+
- [X] T012 [P] [US1] Migrate `spec/ruby_reactor_spec.rb`:
|
|
81
|
+
- remove `retry_defaults max_attempts: 3` (line 14) and add `retries max_attempts: 3` inside the `validate_email`, `hash_password` and `create_user` step blocks;
|
|
82
|
+
- remove `retry_defaults max_attempts: 3` (line 221) and add `retries max_attempts: 3` inside `step :flaky_step`.
|
|
83
|
+
|
|
84
|
+
Do not change any assertion.
|
|
85
|
+
- [X] T013 [P] [US1] Migrate `spec/support/order_processing_reactor.rb`: remove `retry_defaults max_attempts: 5, backoff: :fixed, base_delay: 2` (line 28) and add `retries max_attempts: 5, backoff: :fixed, base_delay: 2` to each step without its own `retries` (`validate_order`, `check_inventory`, `reserve_inventory`, `process_payment`). Do not change `spec/ruby_reactor/order_processing_reactor_spec.rb`.
|
|
86
|
+
- [X] T014 [P] [US1] Apply the identical migration to `demo_app/spec/support/order_processing_reactor.rb` (the file is byte-identical to the gem copy today; keep it that way).
|
|
87
|
+
- [X] T015 [US1] Run `bundle exec rspec` (full suite) and `bundle exec rubocop`; everything is green, including T003/T004.
|
|
88
|
+
|
|
89
|
+
### Documentation for User Story 1 (REQUIRED, Constitution Development Workflow)
|
|
90
|
+
|
|
91
|
+
- [X] T016 [P] [US1] In `documentation/retry_configuration.md`:
|
|
92
|
+
- delete "### Reactor-Level Defaults" (lines 48-70);
|
|
93
|
+
- rewrite the "Complex Retry Scenarios" example (≈ line 176) so each step declares its own `retries`;
|
|
94
|
+
- change the intro sentence "configured at both reactor and step levels" to step level only;
|
|
95
|
+
- add a section "## Migrating from `retry_defaults`" with a before/after example (move the values onto each step that needs them; a step without `retries` runs once; `max_attempts: 0` is not valid, use `1`).
|
|
96
|
+
- [X] T017 [P] [US1] In `documentation/core_concepts.md`:
|
|
97
|
+
- line 327: drop "either reactor-level defaults or";
|
|
98
|
+
- line 345: "Retries are configured per step";
|
|
99
|
+
- ≈ line 360: replace the "Uses reactor defaults" step with an explicit `retries` line or no retries, and adjust the surrounding example so it no longer declares `retry_defaults`.
|
|
100
|
+
- [X] T018 [P] [US1] In `documentation/background_and_async.md`, delete "### Reactor-Level Defaults" (≈ lines 514-530) and adjust any cross-reference to it.
|
|
101
|
+
- [X] T019 [P] [US1] In `documentation/examples/order_processing.md` (line 95), `documentation/examples/payment_processing.md` (lines 77, 228, 300) and `documentation/examples/inventory_management.md` (lines 51, 461), replace each `retry_defaults …` with `retries …` on the step(s) in that example that do external I/O. Keep the same values.
|
|
102
|
+
- [X] T020 [P] [US1] Apply the same changes to the stale copies under `demo_app/documentation/`: `retry_configuration.md` (48-70, 173), `core_concepts.md` (220, 238, 253), `async_reactors.md` (394-404), and `examples/{payment_processing (47, 230, 302, 389), order_processing (47), inventory_management (49, 460)}.md`.
|
|
103
|
+
- [X] T021 [P] [US1] In `README.md` line 1486, change "how to configure retries at the reactor or step level" to "how to configure retries per step". Run `grep -n "retry_defaults" llms.txt llms-full.txt` and fix any hit.
|
|
104
|
+
- [X] T022 [US1] Gate: `grep -rn "retry_defaults" lib spec demo_app documentation README.md llms.txt llms-full.txt` matches only `lib/ruby_reactor/dsl/reactor.rb`, `spec/async_retry_dsl_spec.rb` and `spec/ruby_reactor/step_retries/removal_spec.rb`. `bundle exec rspec` and `bundle exec rubocop` are green. Commit as `feat!: remove reactor-wide retry_defaults`, with a `BREAKING CHANGE:` footer carrying the migration text from T016 (including the `max_attempts: 0` → `1` note, which applies from Phase 3).
|
|
105
|
+
|
|
106
|
+
**Checkpoint**: US1 is shippable on its own. Do not start Phase 3 until T022 is committed.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Phase 3: Foundational — shared `retries` vocabulary (blocks US2–US7)
|
|
111
|
+
|
|
112
|
+
**Purpose**: one `Dsl::Retryable` module gives step classes and every builder the same
|
|
113
|
+
`retries` keyword arguments, defaults and validation (research R3, R4; FR-001, FR-002,
|
|
114
|
+
FR-004).
|
|
115
|
+
|
|
116
|
+
**⚠️ CRITICAL**: no US2–US7 work until this phase is green.
|
|
117
|
+
|
|
118
|
+
- [X] T023 [P] Create `spec/ruby_reactor/step_retries/declaration_spec.rb` (validation, FR-004). For both a step class (`Class.new(RubyReactor::Step) { retries … }`) and an inline step block (`Class.new(RubyReactor::Reactor) { step(:s) { retries …; run { Success() } } }`):
|
|
119
|
+
- `retries` with no args stores `{max_attempts: 3, backoff: :exponential, base_delay: 1}`;
|
|
120
|
+
- `retries max_attempts: 5` keeps the other defaults;
|
|
121
|
+
- `ArgumentError` for `max_attempts: 0`, `-1`, `2.5` and `"3"` (message names the owner and `max_attempts`), for `backoff: :bogus` (names `backoff`) and for `base_delay: -1` (names `base_delay`);
|
|
122
|
+
- `base_delay: 0` and `base_delay: 0.5` are accepted.
|
|
123
|
+
|
|
124
|
+
Also assert that `compose` and `async_reactor` blocks raise the same `ArgumentError` for `backoff: :bogus`.
|
|
125
|
+
- [X] T024 Create `lib/ruby_reactor/dsl/retryable.rb` defining `RubyReactor::Dsl::Retryable` with:
|
|
126
|
+
- `BACKOFF_STRATEGIES = %i[exponential linear fixed].freeze`;
|
|
127
|
+
- `attr_reader :retry_config`;
|
|
128
|
+
- `retries(max_attempts: 3, backoff: :exponential, base_delay: 1)`, which validates as in `data-model.md` (Integer >= 1; strategy in the list; Numeric >= 0) and raises `ArgumentError, "#{retry_owner_label}: retries #{option} must be … (got #{value.inspect})"`, then sets `@retry_config = { max_attempts:, backoff:, base_delay: }`;
|
|
129
|
+
- `inherited(subclass)`: `super`, then copy `@retry_config` to the subclass when set (mirror `Dsl::Lockable::ClassMethods#inherited`, `lib/ruby_reactor/dsl/lockable.rb:33-40`);
|
|
130
|
+
- a private `retry_owner_label` returning `name&.to_s || inspect`.
|
|
131
|
+
|
|
132
|
+
Add `require_relative "ruby_reactor/dsl/retryable"` in `lib/ruby_reactor.rb` right after the `dsl/lockable` require (line 9).
|
|
133
|
+
- [X] T025 In `lib/ruby_reactor/step.rb`, add `extend RubyReactor::Dsl::Retryable` next to `extend RubyReactor::Dsl::Lockable::ClassMethods` (line 36), and update the class header comment ("The one `extend` is …") to mention both modules.
|
|
134
|
+
- [X] T026 In `lib/ruby_reactor/dsl/step_builder.rb`, `include RubyReactor::Dsl::Retryable`, delete `StepBuilder#retries` (lines 133-139), and remove `:retry_config` from the `attr_accessor` list (line 16). The module's reader replaces it.
|
|
135
|
+
- [X] T027 [P] In `lib/ruby_reactor/dsl/compose_builder.rb`, `include RubyReactor::Dsl::Retryable` and delete `ComposeBuilder#retries` (lines 47-53).
|
|
136
|
+
- [X] T028 [P] In `lib/ruby_reactor/dsl/async_reactor_builder.rb`, `include RubyReactor::Dsl::Retryable` and delete `AsyncReactorBuilder#retries` (lines 28-30).
|
|
137
|
+
- [X] T029 Run T023 plus `bundle exec rspec spec/async_retry_dsl_spec.rb spec/ruby_reactor/retry_signals_spec.rb spec/compose_spec.rb spec/ruby_reactor/dsl/` and `bundle exec rubocop lib/ruby_reactor/dsl/retryable.rb`. All green.
|
|
138
|
+
|
|
139
|
+
**Checkpoint**: the `retries` vocabulary is shared. Step classes can declare it, but reactors
|
|
140
|
+
don't read it yet.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Phase 4: User Story 2 — A step class declares its own retry policy (Priority: P1)
|
|
145
|
+
|
|
146
|
+
**Goal**: a reactor using a step class with no retry wiring gets the class's policy (FR-001,
|
|
147
|
+
FR-008 fallback, FR-011, FR-012). A direct call runs once (FR-013).
|
|
148
|
+
|
|
149
|
+
**Independent Test**: a class step declaring `retries max_attempts: 3, base_delay: 0`, whose
|
|
150
|
+
body fails twice, succeeds on attempt 3 in a reactor that has no `retries` line. When the body
|
|
151
|
+
always fails, the reactor fails after 3 attempts and compensates the earlier steps.
|
|
152
|
+
|
|
153
|
+
### Tests for User Story 2 (write first, confirm failing)
|
|
154
|
+
|
|
155
|
+
- [X] T030 [P] [US2] Create `spec/ruby_reactor/step_retries/class_policy_spec.rb` with:
|
|
156
|
+
- (a) fail twice then succeed → success, `attempts_for_step == 3`;
|
|
157
|
+
- (b) always fail → `RubyReactor::MaxRetriesExhaustedFailure` with message `"Step 'charge' failed after 3 attempts: …"`, and an earlier step's `compensate`/`undo` ran;
|
|
158
|
+
- (c) `retries max_attempts: 3, backoff: :linear, base_delay: 0.01` → the sleeps between attempts follow linear backoff: `allow_any_instance_of(RubyReactor::Executor::RetryManager).to receive(:sleep)`, then expect it to have received `sleep(0.01)` and then `sleep(0.02)`;
|
|
159
|
+
- (d) `retries max_attempts: 3` only → `steps[:charge].retry_config == {max_attempts: 3, backoff: :exponential, base_delay: 1}`;
|
|
160
|
+
- (e) a body doing `fail!(StandardError.new("x"), retry: false)` makes 1 attempt;
|
|
161
|
+
- (f) a step class with `input :amount, :integer` given `amount: "x"` makes 1 attempt (contract failure is non-retryable);
|
|
162
|
+
- (g) the step skipped by `where { false }` makes 0 attempts.
|
|
163
|
+
- [X] T031 [P] [US2] In `spec/ruby_reactor/step_retries/class_policy_spec.rb`, add a `describe "direct invocation"` example (FR-013): a step class with `retries max_attempts: 3` whose `run` counts calls and returns `Failure("x")`; `klass.run({})` returns a `Failure`, and the counter is 1. Repeat with the step called from another class step's `run` body inside a reactor: the inner direct call runs once per outer attempt.
|
|
164
|
+
|
|
165
|
+
### Implementation for User Story 2
|
|
166
|
+
|
|
167
|
+
- [X] T032 [US2] In `StepConfig` (`lib/ruby_reactor/dsl/step_builder.rb`), store the raw declaration (`@retry_config = config[:retry_config]`, no default). Remove `:retry_config` from `StepConfig`'s `attr_reader` and add, next to `lock_config` (≈ line 286): `def retry_config = @retry_config || (impl.retry_config if impl.respond_to?(:retry_config)) || NO_RETRIES`. Confirm that `retryable?` (≈ line 376) still reads through it.
|
|
168
|
+
- [X] T033 [US2] Check (no change expected, research R7) that `lib/ruby_reactor/executor/step_coordination.rb` `retry_pending?` (lines 533-540) is guarded by `context_state?` before touching `step_config.retry_config`. When `step_config` is a `Step` class, `retry_config` may be `nil` and must never be indexed. If there is any unguarded path, guard it with `&.` there.
|
|
169
|
+
- [X] T034 [US2] Run T030/T031 and `bundle exec rspec spec/ruby_reactor/step_spec.rb spec/ruby_reactor/step_inheritance_spec.rb spec/ruby_reactor/step_contract_retryable_spec.rb`. All green.
|
|
170
|
+
|
|
171
|
+
**Checkpoint**: the core feature works on the in-process path.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Phase 5: User Story 3 — Existing step-level declarations keep working (Priority: P1)
|
|
176
|
+
|
|
177
|
+
**Goal**: inline and step-block `retries` behave exactly as before, and the same line works
|
|
178
|
+
in a class body (FR-002, FR-003).
|
|
179
|
+
|
|
180
|
+
**Independent Test**: the existing retry specs pass unchanged, and an inline step and a class
|
|
181
|
+
step with the same declaration give identical outcomes.
|
|
182
|
+
|
|
183
|
+
- [X] T035 [P] [US3] Create `spec/ruby_reactor/step_retries/step_block_parity_spec.rb` with:
|
|
184
|
+
- (a) an inline step with `retries max_attempts: 3, backoff: :fixed, base_delay: 0` and a `run` block, failing twice → success on attempt 3;
|
|
185
|
+
- (b) a class step with **no** `retries` plus `step :s, Klass do retries max_attempts: 3, base_delay: 0 end` → retried 3 times, and `steps[:s].retry_config[:max_attempts] == 3`;
|
|
186
|
+
- (c) a parity table: the same failure sequence (fail, fail, succeed) and (always fail) run through an inline step and through a class step declaring the identical `retries` line → equal attempt counts, equal final result class, equal error message.
|
|
187
|
+
- [X] T036 [US3] Run the existing retry specs unchanged: `spec/async_retry_dsl_spec.rb`, `spec/async_retry_integration_spec.rb`, `spec/ruby_reactor/retry_signals_spec.rb`, `spec/ruby_reactor/retry_reexecution_spec.rb`, `spec/ruby_reactor/order_processing_reactor_spec.rb`, `spec/compose_spec.rb`. All green, with no edits to them in this phase.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Phase 6: User Story 4 — One declaration per step (Priority: P1)
|
|
192
|
+
|
|
193
|
+
**Goal**: `retries` on both the class and the step block is refused at reactor definition
|
|
194
|
+
(FR-009).
|
|
195
|
+
|
|
196
|
+
**Independent Test**: adding `retries` to a reactor step block for a class that declares
|
|
197
|
+
`retries` raises `Error::ValidationError` naming the reactor, the step and the class.
|
|
198
|
+
|
|
199
|
+
- [X] T037 [P] [US4] Create `spec/ruby_reactor/step_retries/conflict_spec.rb` with:
|
|
200
|
+
- (a) a class with `retries max_attempts: 3` plus `step :charge, Klass do retries max_attempts: 5 end` → `RubyReactor::Error::ValidationError` matching `/step :charge declares `retries` inline, but .* declares it too/` and `/subclass/`;
|
|
201
|
+
- (b) the same with the class's policy **inherited** from a parent → still refused;
|
|
202
|
+
- (c) a class without `retries` plus a block `retries` → no error;
|
|
203
|
+
- (d) a class with `retries max_attempts: 1` in a reactor → not retried (`attempts == 1`).
|
|
204
|
+
- [X] T038 [US4] In `lib/ruby_reactor/dsl/step_builder.rb`, add a private `check_retry_conflict!` next to `check_coordination_conflicts!` (≈ line 180) and call it from `build`. It returns unless `@retry_config && @impl.respond_to?(:retry_config) && @impl.retry_config`, then raises `Error::ValidationError`: "#{reactor_label} step :#{@name} declares `retries` inline, but #{@impl} declares it too. Keep ONE: drop the inline declaration to use #{@impl}'s, or remove it from #{@impl}. To vary the policy per workflow, subclass #{@impl} and declare `retries` there."
|
|
205
|
+
- [X] T039 [US4] Run T037 and `spec/ruby_reactor/step_coordination/declaration_spec.rb` (to check the neighboring lock conflict check is untouched). Green.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Phase 7: User Story 5 — The policy follows the step on every execution path (Priority: P2)
|
|
210
|
+
|
|
211
|
+
**Goal**: a class policy applies in background hand-off, mid-workflow hand-off,
|
|
212
|
+
`async_step`, and resume, and attempts carry across requeues (FR-011, FR-012).
|
|
213
|
+
|
|
214
|
+
**Independent Test**: the same always-failing class step makes exactly the declared number of
|
|
215
|
+
attempts synchronously and under `background all: true`.
|
|
216
|
+
|
|
217
|
+
- [X] T040 [P] [US5] Create `spec/ruby_reactor/step_retries/execution_paths_spec.rb` with constant-named classes defined at the top of the file (`StepRetriesPathsChargeStep < RubyReactor::Step` with `retries max_attempts: 3, backoff: :fixed, base_delay: 0` and a class-level attempt counter, plus reactors below). Cases:
|
|
218
|
+
- (a) a reactor with `background all: true`, run and drained (follow the pattern in `spec/ruby_reactor/retry_signals_spec.rb` / `spec/async_retry_integration_spec.rb`), always failing → final context failed, 3 attempts, earlier step compensated;
|
|
219
|
+
- (b) fail twice then succeed under `background all: true` → success, and `retry_context.attempts_for_step` stayed consistent across requeues;
|
|
220
|
+
- (c) a reactor with `background after: :first_step` → same count as (a);
|
|
221
|
+
- (d) `async_step :charge, StepRetriesPathsChargeStep` → `StepWorker` retries 3 times (`lib/ruby_reactor/step_worker.rb:344-349` reads `step_config.retry_config`);
|
|
222
|
+
- (e) a reactor with an `interrupt` before `:charge`, resumed, then failing → 3 attempts after the resume.
|
|
223
|
+
- [X] T041 [US5] Run T040. No runtime change is expected (research R5). If (d) fails, fix `lib/ruby_reactor/step_worker.rb` so it reads only `step_config.retry_config` (never `@retry_config` or `impl` directly). If (a)–(c) fail, fix the same kind of issue in `lib/ruby_reactor/executor/retry_manager.rb`.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Phase 8: User Story 6 — Step subclasses inherit the policy (Priority: P2)
|
|
228
|
+
|
|
229
|
+
**Goal**: subclasses inherit and can override, and the parent is unaffected (FR-010).
|
|
230
|
+
|
|
231
|
+
**Independent Test**: a base class with `retries max_attempts: 4`, one subclass that inherits
|
|
232
|
+
it and one that overrides with 2 → 4/4/2 attempts respectively.
|
|
233
|
+
|
|
234
|
+
- [X] T042 [P] [US6] Create `spec/ruby_reactor/step_retries/inheritance_spec.rb` with:
|
|
235
|
+
- (a) a subclass without `retries` → `retry_config` equals the parent's, and in a reactor it makes 4 attempts;
|
|
236
|
+
- (b) a subclass with `retries max_attempts: 2` → 2 attempts; the parent and a sibling still show 4;
|
|
237
|
+
- (c) a subclass-per-workflow: `ReactorA` uses the base class and `ReactorB` uses a subclass with its own policy; both load without a conflict error and retry as declared;
|
|
238
|
+
- (d) a subclass also inherits the parent's `with_lock` and `input` contract alongside `retries` (regression guard that `Retryable#inherited` calls `super`).
|
|
239
|
+
- [X] T043 [US6] Run T042. If (d) fails, fix the `super` chain in `lib/ruby_reactor/dsl/retryable.rb` `inherited`.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Phase 9: User Story 7 — Tests and operators can see the policy (Priority: P2)
|
|
244
|
+
|
|
245
|
+
**Goal**: the effective policy and its source can be looked up, and the shipped test surface
|
|
246
|
+
exercises class policies (FR-014, FR-015, FR-016).
|
|
247
|
+
|
|
248
|
+
**Independent Test**: `steps[:charge].retry_source == :step_class`, and `failing_at(:charge)`
|
|
249
|
+
on a class step is retried under the class policy (`have_retried_step(:charge).times(2)`).
|
|
250
|
+
|
|
251
|
+
- [X] T044 [P] [US7] Create `spec/ruby_reactor/step_retries/introspection_spec.rb` with:
|
|
252
|
+
- (a) `retry_source` is `:step_class`, `:step_block` or `:none` for the three kinds of steps;
|
|
253
|
+
- (b) `retry_config` for each matches the declaration or `NO_RETRIES`;
|
|
254
|
+
- (c) with a middleware registered (see `spec/ruby_reactor/middleware_spec.rb` for the pattern), a failing class step with `retries max_attempts: 3, base_delay: 0` emits `:retry_attempt` twice, with the step name and attempt numbers 1 and 2.
|
|
255
|
+
- [X] T045 [P] [US7] Create `spec/ruby_reactor/step_retries/test_surface_spec.rb` (`type: :reactor`, using only `test_reactor`, `failing_at`, `mock_step`, `have_retried_step`, `be_failure`, `be_success`):
|
|
256
|
+
- (a) `test_reactor(R, inputs).failing_at(:charge)` where `:charge` is a class step with `retries max_attempts: 3, base_delay: 0` → `be_failure` and `have_retried_step(:charge).times(2)`;
|
|
257
|
+
- (b) `mock_step(:charge) { |_a, ctx| ctx.retry_context.attempts_for_step(:charge) < 2 ? RubyReactor.Failure("x") : RubyReactor.Success(1) }` → `be_success` and `have_retried_step(:charge).times(1)`.
|
|
258
|
+
- [X] T046 [US7] In `StepConfig` (`lib/ruby_reactor/dsl/step_builder.rb`), add `retry_source`: `:step_block` if `@retry_config`, else `:step_class` if `impl.respond_to?(:retry_config) && impl.retry_config`, else `:none`. Run T044/T045. Green.
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Phase 10: Demo (Constitution VI)
|
|
263
|
+
|
|
264
|
+
**Purpose**: a runnable proof through the public DSL (FR-018).
|
|
265
|
+
|
|
266
|
+
- [X] T047 [P] Create `demo_app/app/reactors/step_retry_demo_reactor.rb` defining, in one file:
|
|
267
|
+
- `StepRetryDemoLog` (a class-level attempt log with `reset!`);
|
|
268
|
+
- `FlakyChargeStep < RubyReactor::Step` with `input :fail_times, :integer` and `retries max_attempts: 3, backoff: :fixed, base_delay: 0.05`, which fails until the attempt counter reaches `fail_times` (log each attempt);
|
|
269
|
+
- `ReserveStockStep < RubyReactor::Step` with a `compensate` that records `StepRetryDemoLog.compensated = true`;
|
|
270
|
+
- `NotifyStep < RubyReactor::Step` with no `retries`, which fails when `fail_notify` is true.
|
|
271
|
+
|
|
272
|
+
`StepRetryDemoReactor < RubyReactor::Reactor` has inputs `fail_times` and `fail_notify` and steps `reserve_stock` → `charge` → `notify`, with no `retries` lines anywhere in the reactor. Model the file layout on `demo_app/app/reactors/step_lock_demo_reactor.rb`.
|
|
273
|
+
- [X] T048 Add `desc "StepRetryDemoReactor — retries declared on the STEP class; succeed-after-retry, exhaust-and-compensate, undeclared step runs once"` and `task step_retry: [:environment, :flush_redis]` to `demo_app/lib/tasks/demo_reactors.rake`. Touch `StepRetryDemoReactor` first (Zeitwerk note as in the `step_lock` task, ≈ line 685). It prints three scenarios:
|
|
274
|
+
1. `fail_times: 2` → `attempts=3 success?=true`;
|
|
275
|
+
2. `fail_times: 5` → `success?=false attempts=3 compensated=true`;
|
|
276
|
+
3. `fail_times: 0, fail_notify: true` → `notify attempts=1 success?=false`.
|
|
277
|
+
- [X] T049 [P] Create `demo_app/spec/reactors/step_retry_demo_reactor_spec.rb` (`type: :reactor`, `require "rails_helper"`) using only the shipped surface. It covers the three scenarios with `be_success`/`be_failure`, `have_retried_step(:charge).times(2)`, and `expect(reactor).not_to have_retried_step(:notify)`. The compensation assertion reads `StepRetryDemoLog.compensated` (application state, not reactor internals).
|
|
278
|
+
- [X] T050 Run the demo end to end in an isolated compose project: `docker compose -p rr-retry run --rm demo-app bin/rails demo:step_retry` and `docker compose -p rr-retry run --rm demo-app bundle exec rspec spec/reactors/step_retry_demo_reactor_spec.rb`. Confirm the printed outcomes match T048.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Phase 11: Polish & Cross-Cutting Concerns
|
|
283
|
+
|
|
284
|
+
- [X] T051 [P] In `documentation/retry_configuration.md`, add "### Declaring retries on a step class (preferred)" as the first example under "Basic Retry Configuration", with `ChargeCard` using `with_lock`, `input` and `retries` together (as in `contracts/dsl-surface.md` §2). Keep the inline example after it. Add:
|
|
285
|
+
- "### Where a step's policy comes from": the step block, then the step class, then none (runs once);
|
|
286
|
+
- "### One declaration per step": the conflict error and the subclassing recipe;
|
|
287
|
+
- "### Direct calls run once": `ChargeCard.run(args)` never retries, because only a reactor coordinates retries; contrast this with locks, which a direct call does take;
|
|
288
|
+
- validation rules for `max_attempts`, `backoff` and `base_delay`.
|
|
289
|
+
- [X] T052 [P] In `documentation/core_concepts.md` line 108 (the class-step lifecycle paragraph that says direct calls are coordinated like reactor calls), append: "Retries are not: a direct call runs once — only a reactor retries a step (see [Retry Configuration](retry_configuration.md#direct-calls-run-once))." In the retry section (≈ 345), lead with the step-class form.
|
|
290
|
+
- [X] T053 [P] In `README.md`:
|
|
291
|
+
- Features bullet (line 24): "**Retries**: per-step retry policies (declared on the step class or step block) with exponential, linear, or fixed backoff.";
|
|
292
|
+
- in "Defining Steps" (≈ line 245, the class-step example), add a `retries max_attempts: 3` line to the example step class with a one-line comment;
|
|
293
|
+
- Retry Configuration blurb (line 1486): mention the step-class form.
|
|
294
|
+
- [X] T054 [P] Mirror T051–T052 into `demo_app/documentation/retry_configuration.md` and `demo_app/documentation/core_concepts.md`.
|
|
295
|
+
- [X] T055 [P] Update `llms.txt` / `llms-full.txt` if they describe the retry DSL (`grep -n "retries" llms*.txt`), so they show the step-class form.
|
|
296
|
+
- [X] T056 Final gate: `bundle exec rspec`, `bundle exec rubocop`, and T050 are all green. Walk through `quickstart.md` Phase B. Commit as `feat: declare retries on step classes`.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Dependencies & Execution Order
|
|
301
|
+
|
|
302
|
+
### Phase Dependencies
|
|
303
|
+
|
|
304
|
+
- **Phase 1 (Setup)**: none.
|
|
305
|
+
- **Phase 2 (US1, removal)**: after Setup. **Must be committed (T022) before anything else
|
|
306
|
+
starts** (spec FR-017).
|
|
307
|
+
- **Phase 3 (Foundational)**: after T022. Blocks US2–US7.
|
|
308
|
+
- **Phase 4 (US2)**: after Phase 3. T032 is the change the class policy needs to take effect.
|
|
309
|
+
- **Phases 5–9 (US3–US7)**: after Phase 4 (they all run class steps through a reactor, so they
|
|
310
|
+
need T032). They are independent of each other after that.
|
|
311
|
+
- **Phase 10 (Demo)**: after Phase 4. Its spec uses matchers exercised in US7 but needs no US7
|
|
312
|
+
code.
|
|
313
|
+
- **Phase 11 (Polish)**: after the stories it documents. T056 goes last.
|
|
314
|
+
|
|
315
|
+
### User Story Dependencies
|
|
316
|
+
|
|
317
|
+
```text
|
|
318
|
+
US1 (removal) ──commit──▶ Foundational ──▶ US2 ──┬──▶ US3
|
|
319
|
+
├──▶ US4
|
|
320
|
+
├──▶ US5
|
|
321
|
+
├──▶ US6
|
|
322
|
+
├──▶ US7
|
|
323
|
+
└──▶ Demo ──▶ Polish
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### Within Each Phase
|
|
327
|
+
|
|
328
|
+
- Spec tasks are written first and must fail before the implementation tasks of that phase.
|
|
329
|
+
- `lib/ruby_reactor/dsl/step_builder.rb` is touched by T006, T026, T032, T038 and T046, in that
|
|
330
|
+
order, so those tasks are never [P] with each other.
|
|
331
|
+
|
|
332
|
+
### Parallel Opportunities
|
|
333
|
+
|
|
334
|
+
- **US1**: T004 ∥ T003 (different files); T007 ∥ T008 ∥ T009 ∥ T010 after T006; T012 ∥ T013
|
|
335
|
+
∥ T014; T016–T021 all in parallel.
|
|
336
|
+
- **Foundational**: T023 ∥ T024; T027 ∥ T028 after T024.
|
|
337
|
+
- **After US2**: the spec files T035, T037, T040, T042, T044 and T045 are all separate files
|
|
338
|
+
and can be written in parallel. The demo files T047 and T049 can run in parallel with
|
|
339
|
+
those.
|
|
340
|
+
- **Polish**: T051–T055 are separate files.
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## Parallel Example: after Phase 4
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
Task: "T035 step_block_parity_spec.rb (US3)"
|
|
348
|
+
Task: "T037 conflict_spec.rb (US4)"
|
|
349
|
+
Task: "T040 execution_paths_spec.rb (US5)"
|
|
350
|
+
Task: "T042 inheritance_spec.rb (US6)"
|
|
351
|
+
Task: "T044 introspection_spec.rb + T045 test_surface_spec.rb (US7)"
|
|
352
|
+
Task: "T047 step_retry_demo_reactor.rb + T049 its spec (Demo)"
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## Implementation Strategy
|
|
358
|
+
|
|
359
|
+
### MVP = US1 (removal), shipped alone
|
|
360
|
+
|
|
361
|
+
1. Phase 1 → Phase 2.
|
|
362
|
+
2. **STOP**: T022 gate, `feat!` commit. This is releasable on its own and already makes
|
|
363
|
+
retries predictable (a step's policy is always on the step).
|
|
364
|
+
|
|
365
|
+
### Then the core feature
|
|
366
|
+
|
|
367
|
+
3. Phase 3 → Phase 4 (US2): step classes carry their policy. Validate independently with
|
|
368
|
+
T030/T031.
|
|
369
|
+
4. US4 (conflict) next: it is the correctness guard.
|
|
370
|
+
5. Then US3, US5, US6, US7 in any order or in parallel.
|
|
371
|
+
6. Demo → Polish → T056 `feat:` commit.
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## Notes
|
|
376
|
+
|
|
377
|
+
- Keep `lib/ruby_reactor/rspec/test_subject.rb` free of retry special cases. Mocks keep
|
|
378
|
+
`impl`, so the class policy follows automatically (research R8).
|
|
379
|
+
- Do not add a policy object or a reactor-level fallback of any kind (research R3, spec
|
|
380
|
+
FR-006).
|
|
381
|
+
- Spec quirk: specs sharing test Redis under load can flake (the async parked-wait spec and
|
|
382
|
+
the 1s Redis ping). Rerun a failing file alone before debugging.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Plan: `inputs.order_id` (implement specs/inputs_by_method.md)
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
`Step#inputs` is a plain Hash, so a typo like `inputs[:order_id]` (declared `:order_guid`) returns `nil` and blows up later, far from the bug — worst in `undo`/`compensate`, which skip validation. The design doc (`specs/inputs_by_method.md`) decides: step code reads inputs through a frozen read-only `Step::Inputs` object (`inputs.order_id`); any unreadable name raises `Error::UndeclaredInputError` naming the step and its declared inputs. No `[]`, no shim — every call site migrates in the same change (`feat!`). This plan implements that doc as written; deviations/additions found while reading the code are marked **(new)**.
|
|
6
|
+
|
|
7
|
+
Branch: `inputs_by_method` off `main`. Test-first (Constitution III): new specs first, then lib, then migration.
|
|
8
|
+
|
|
9
|
+
**Step 0 (on approval):** copy this plan to `specs/inputs_by_method_plan.md` (next to the design doc) and stop there. Implementation starts only when asked.
|
|
10
|
+
|
|
11
|
+
## 1. New code (lib)
|
|
12
|
+
|
|
13
|
+
**`lib/ruby_reactor/step/inputs.rb`** — `RubyReactor::Step::Inputs` (Zeitwerk picks it up).
|
|
14
|
+
- `initialize(values, contract: nil, owner:)`: `@values = values.to_h` (so passing an `Inputs` or nil is safe), readable `@names` = `contract.declarations.keys` when the contract has declarations, else `@values.keys.map(&:to_sym)` **(new: a contract with only `validate_inputs` and no `input` falls back to present keys instead of making everything unreadable)**; `@redacted = contract&.redacted_names || []`; `freeze`.
|
|
15
|
+
- `method_missing(name, *args)`: readable name and no args → `Utils::FetchIndifferent.call(@values, name)`; else raise `UndeclaredInputError`. `respond_to_missing?` → `@names.include?(name) || super`.
|
|
16
|
+
- `to_h` → supplied readable names only, symbol keys, via FetchIndifferent (key present as sym or string). `alias to_hash to_h`.
|
|
17
|
+
- `inspect` → `to_h` with redacted names replaced by `InputContract::REDACTED`.
|
|
18
|
+
|
|
19
|
+
**`lib/ruby_reactor/error/undeclared_input_error.rb`** — `< NoMethodError`, `def retryable? = false`. Message: `"#{owner} has no input :#{name}. Declared inputs: :a, :b."` (`none` when empty). Retry path already honours it: `Failure#retryable?` (`lib/ruby_reactor.rb:167`) and `StepFailureError#retryable?` ask the error.
|
|
20
|
+
|
|
21
|
+
**Reserved names** — `InputContract#input` (`lib/ruby_reactor/step/input_contract.rb:25`): raise `Error::ValidationError` when `Step::Inputs.public_method_defined?(name)`. Checked all 141 input names in spec/demo_app/docs: no clashes.
|
|
22
|
+
|
|
23
|
+
## 2. Injection points (the only places the object is built)
|
|
24
|
+
|
|
25
|
+
| Where | Change |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `Step#initialize` (`lib/ruby_reactor/step.rb:44`) | `@inputs = Inputs.new(inputs, contract: (self.class.input_contract if self.class.declares_inputs?), owner: self.class.name)`. Covers `.run`/`.undo`/`.compensate`. Update header comment example (`step.rb:8`). |
|
|
28
|
+
| `Step.enforce_contract!` / `with_defaults` (`step.rb:168-179`) | **(new)** `arguments.to_h` first, so a documented nested call `OtherStep.run(inputs, context)` from a step body, and TestSubject's `original` callable, still hand the contract a Hash. |
|
|
29
|
+
| `StepConfig` (`lib/ruby_reactor/dsl/step_builder.rb`) | Add `def wrap_inputs(arguments) = Step::Inputs.new(arguments, contract: input_contract, owner: "step :#{name}")`. |
|
|
30
|
+
| `StepConfig#call_body` (`step_builder.rb:367`) | `run_block.call(wrap_inputs(arguments), context)`. Duck-typed `impl.run` (line 372) unchanged. |
|
|
31
|
+
| `CompensationManager#compensate_step` / `#undo_step` (`lib/ruby_reactor/executor/compensation_manager.rb:126,171`) | Wrap `arguments` with `step_config.wrap_inputs` before the inline block call only. |
|
|
32
|
+
| `TestSubject#original_impl_for` duck-impl lambda (`lib/ruby_reactor/rspec/test_subject.rb:772`) | **(new)** `impl.run(args.to_h, ctx)`. Mock blocks replace `@run_block`, so they receive `Inputs` like the body they stand in for — the `Step` path is covered by the `to_h` above. |
|
|
33
|
+
|
|
34
|
+
## 3. Returning inputs from a step
|
|
35
|
+
|
|
36
|
+
- `RubyReactor.Success` (`lib/ruby_reactor.rb:391`): `Success.new(value.is_a?(Step::Inputs) ? value.to_h : value)`.
|
|
37
|
+
- `ResultHandler#handle_unknown_result` (`lib/ruby_reactor/executor/result_handler.rb:192`): build `success_result` first, then `validate_step_output` on `success_result.value`, **and (new) `@context.set_result(..., success_result.value)`** — the doc's swap alone would still store the raw `Inputs` in context.
|
|
38
|
+
|
|
39
|
+
## 4. Built-in steps
|
|
40
|
+
|
|
41
|
+
Per the doc, `MapStep`, `ComposeStep`, `AsyncReactorStep` declare their keys (`input :source`, `input :fan_out, optional: true`, …, untyped → no validator, `enforce!` is a no-op) and switch `inputs[:x]` → `inputs.x` (31 sites). `fail_fast: inputs.fail_fast.nil? || inputs.fail_fast` stays.
|
|
42
|
+
- **(new)** drop `inputs[:step_name] ||` in `map_step.rb:193`: `step_name` is never wired, and once declared, `validate_definition!` would wire it by name from a reactor input called `:step_name`.
|
|
43
|
+
|
|
44
|
+
## 5. Migration (no shim)
|
|
45
|
+
|
|
46
|
+
Counts: `inputs[` — spec 131, docs 49, README 10, demo_app 44; `args[` — spec 378, docs 157, README 54, demo_app 414 (most in inline blocks).
|
|
47
|
+
- Step bodies only (class `run`/`undo`/`compensate`, inline `run`/`undo`/`compensate` blocks, TestSubject mock blocks): `x[:k]` → `x.k`. Hand edits, not a global replace — lock/semaphore/rate-limit key procs, `validate_inputs`, `where`/`guard`, `transform:` procs stay Hashes.
|
|
48
|
+
- Other Hash use on inputs (`each`, `slice`, `eq(hash)` in specs) → `.to_h`. `**inputs`, `merge(inputs)`, `Success(inputs)` keep working.
|
|
49
|
+
- Docs/README/`demo_app/documentation`: rename inline block param `args` → `inputs`; `documentation/core_concepts.md` step-inputs section documents reader API, `UndeclaredInputError`, reserved names, `to_h`/`**`, `Success(inputs)` conversion (nested not converted).
|
|
50
|
+
- Order: lib → `spec/` → `demo_app/` → docs/README.
|
|
51
|
+
|
|
52
|
+
**Silent-pass trap:** a spec that expects a step to fail still passes if an unmigrated `[]` now fails it for a different reason. So grep is the driver and the suite is the net, not the other way round.
|
|
53
|
+
|
|
54
|
+
## 6. Demo (Constitution VI)
|
|
55
|
+
|
|
56
|
+
Via `/speckit-demo-tests`: `demo_app/app/reactors/undeclared_input_demo_reactor.rb` (step reads a typo'd input), `demo:undeclared_input` in `demo_app/lib/tasks/demo_reactors.rake`, `demo_app/spec/reactors/undeclared_input_demo_reactor_spec.rb` asserting failure with `UndeclaredInputError` and one attempt despite `retries`.
|
|
57
|
+
|
|
58
|
+
## 7. Tests (written first)
|
|
59
|
+
|
|
60
|
+
`spec/ruby_reactor/step/inputs_spec.rb`:
|
|
61
|
+
- reader returns value; declared-optional absent → `nil`; `false` stays `false`; string-keyed hash readable.
|
|
62
|
+
- undeclared name raises `UndeclaredInputError` with exact message; `respond_to?`; clash with private Kernel name (`select`) as a declared input reads fine.
|
|
63
|
+
- contract-less step reads present keys only; unwired inline step with `inputs do` reads only declared.
|
|
64
|
+
- `to_h` omits unsupplied optionals; `**inputs`; `inspect` redacts; frozen, no `[]`.
|
|
65
|
+
- `input :method` → `ValidationError` at class definition.
|
|
66
|
+
- class `undo`/`compensate` and inline `run`/`undo`/`compensate` blocks receive `Inputs`; typo in `compensate` surfaces as rollback failure naming the input.
|
|
67
|
+
- step with `retries max_attempts: 3` + typo → one attempt.
|
|
68
|
+
- `Success(inputs)` sync (`result(:step, :x)` works) and async round trip (Hash, not a string); bare `run { |inputs| inputs }` stored/validated as Hash.
|
|
69
|
+
- nested `OtherStep.run(inputs, context)` from a step body works; TestSubject `mock_step` with `original.call(args, ctx)` on a class step works.
|
|
70
|
+
|
|
71
|
+
## Verification
|
|
72
|
+
|
|
73
|
+
1. `bundle exec rspec` (real Redis) green; rerun flaky async specs alone before chasing them.
|
|
74
|
+
2. `cd demo_app && bundle exec rspec` green; `bin/rails demo:undeclared_input` shows the error.
|
|
75
|
+
3. `bundle exec rubocop` clean.
|
|
76
|
+
4. Residual audit: `grep -rnE 'inputs\[|\bargs\[' lib spec demo_app documentation README.md` — every remaining hit is a Hash context (key procs, validators, `where`/`guard`, reactor DSL `inputs[name]` in `dsl/reactor.rb`).
|
|
77
|
+
5. Commit as `feat!:` with a migration note (`inputs[:x]` → `inputs.x`, Hash methods → `inputs.to_h`).
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_reactor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.8.
|
|
4
|
+
version: 0.8.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Artur
|
|
@@ -165,6 +165,7 @@ files:
|
|
|
165
165
|
- lib/ruby_reactor/dsl/lockable.rb
|
|
166
166
|
- lib/ruby_reactor/dsl/map_builder.rb
|
|
167
167
|
- lib/ruby_reactor/dsl/reactor.rb
|
|
168
|
+
- lib/ruby_reactor/dsl/retryable.rb
|
|
168
169
|
- lib/ruby_reactor/dsl/step_builder.rb
|
|
169
170
|
- lib/ruby_reactor/dsl/template_helpers.rb
|
|
170
171
|
- lib/ruby_reactor/dsl/validation_helpers.rb
|
|
@@ -182,6 +183,7 @@ files:
|
|
|
182
183
|
- lib/ruby_reactor/error/schema_version_error.rb
|
|
183
184
|
- lib/ruby_reactor/error/step_contention_park.rb
|
|
184
185
|
- lib/ruby_reactor/error/step_failure_error.rb
|
|
186
|
+
- lib/ruby_reactor/error/undeclared_input_error.rb
|
|
185
187
|
- lib/ruby_reactor/error/undo_error.rb
|
|
186
188
|
- lib/ruby_reactor/error/validation_error.rb
|
|
187
189
|
- lib/ruby_reactor/executor.rb
|
|
@@ -229,6 +231,7 @@ files:
|
|
|
229
231
|
- lib/ruby_reactor/step/async_reactor_step.rb
|
|
230
232
|
- lib/ruby_reactor/step/compose_step.rb
|
|
231
233
|
- lib/ruby_reactor/step/input_contract.rb
|
|
234
|
+
- lib/ruby_reactor/step/inputs.rb
|
|
232
235
|
- lib/ruby_reactor/step/map_step.rb
|
|
233
236
|
- lib/ruby_reactor/step_signals.rb
|
|
234
237
|
- lib/ruby_reactor/step_sweeper.rb
|
|
@@ -268,8 +271,17 @@ files:
|
|
|
268
271
|
- llms-full.txt
|
|
269
272
|
- llms.txt
|
|
270
273
|
- sig/ruby_reactor.rbs
|
|
274
|
+
- specs/006-step-retry-declarations/checklists/requirements.md
|
|
275
|
+
- specs/006-step-retry-declarations/contracts/dsl-surface.md
|
|
276
|
+
- specs/006-step-retry-declarations/data-model.md
|
|
277
|
+
- specs/006-step-retry-declarations/plan.md
|
|
278
|
+
- specs/006-step-retry-declarations/quickstart.md
|
|
279
|
+
- specs/006-step-retry-declarations/research.md
|
|
280
|
+
- specs/006-step-retry-declarations/spec.md
|
|
281
|
+
- specs/006-step-retry-declarations/tasks.md
|
|
271
282
|
- specs/future_improvements.md
|
|
272
283
|
- specs/scoped_rspec_matchers.md
|
|
284
|
+
- specs/specs-inputs-by-method-md-piped-wigderson.md
|
|
273
285
|
- teley/Dockerfile
|
|
274
286
|
homepage: https://github.com/arturictus/ruby_reactor
|
|
275
287
|
licenses: []
|