ruby_reactor 0.8.4 → 0.8.5

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/.specify/feature.json +1 -1
  4. data/CHANGELOG.md +196 -0
  5. data/CLAUDE.md +1 -1
  6. data/README.md +47 -11
  7. data/lib/ruby_reactor/dsl/async_macros.rb +30 -1
  8. data/lib/ruby_reactor/dsl/async_reactor_builder.rb +12 -6
  9. data/lib/ruby_reactor/dsl/compose_builder.rb +12 -6
  10. data/lib/ruby_reactor/dsl/interrupt_builder.rb +1 -3
  11. data/lib/ruby_reactor/dsl/map_builder.rb +0 -2
  12. data/lib/ruby_reactor/dsl/step_builder.rb +91 -19
  13. data/lib/ruby_reactor/error/argument_resolution_error.rb +19 -0
  14. data/lib/ruby_reactor/error/rescuable.rb +28 -0
  15. data/lib/ruby_reactor/executor/compensation_manager.rb +30 -26
  16. data/lib/ruby_reactor/executor/result_handler.rb +18 -16
  17. data/lib/ruby_reactor/executor/step_coordination.rb +11 -8
  18. data/lib/ruby_reactor/executor/step_executor.rb +59 -49
  19. data/lib/ruby_reactor/executor.rb +38 -4
  20. data/lib/ruby_reactor/map/collector.rb +21 -11
  21. data/lib/ruby_reactor/map/dispatcher.rb +29 -3
  22. data/lib/ruby_reactor/map/element_executor.rb +9 -3
  23. data/lib/ruby_reactor/map/helpers.rb +32 -2
  24. data/lib/ruby_reactor/map/result_enumerator.rb +18 -12
  25. data/lib/ruby_reactor/reactor.rb +24 -0
  26. data/lib/ruby_reactor/rspec/matchers.rb +19 -3
  27. data/lib/ruby_reactor/step/compose_step.rb +7 -1
  28. data/lib/ruby_reactor/step/map_step.rb +109 -4
  29. data/lib/ruby_reactor/step.rb +7 -0
  30. data/lib/ruby_reactor/step_worker.rb +46 -22
  31. data/lib/ruby_reactor/storage/adapter.rb +4 -0
  32. data/lib/ruby_reactor/storage/redis_adapter.rb +9 -0
  33. data/lib/ruby_reactor/storage/redis_reactor_scan.rb +1 -1
  34. data/lib/ruby_reactor/version.rb +1 -1
  35. data/lib/ruby_reactor/web/api.rb +1 -1
  36. data/lib/ruby_reactor/web/public/assets/{index-CeZU-ESu.js → index-CQbgHtd0.js} +10 -10
  37. data/lib/ruby_reactor/web/public/index.html +1 -1
  38. data/lib/ruby_reactor/worker.rb +3 -1
  39. data/lib/ruby_reactor.rb +17 -6
  40. data/specs/007-execution-flow-analysis/analysis/README.md +147 -0
  41. data/specs/007-execution-flow-analysis/analysis/execution-order.md +359 -0
  42. data/specs/007-execution-flow-analysis/analysis/findings-and-options.md +502 -0
  43. data/specs/007-execution-flow-analysis/analysis/invariants.md +109 -0
  44. data/specs/007-execution-flow-analysis/checklists/requirements.md +39 -0
  45. data/specs/007-execution-flow-analysis/contracts/report-structure.md +71 -0
  46. data/specs/007-execution-flow-analysis/data-model.md +83 -0
  47. data/specs/007-execution-flow-analysis/evidence/harness.rb +229 -0
  48. data/specs/007-execution-flow-analysis/evidence/output.txt +333 -0
  49. data/specs/007-execution-flow-analysis/evidence/probes/01_plain.rb +122 -0
  50. data/specs/007-execution-flow-analysis/evidence/probes/02_compose.rb +182 -0
  51. data/specs/007-execution-flow-analysis/evidence/probes/03_map.rb +232 -0
  52. data/specs/007-execution-flow-analysis/evidence/probes/04_async.rb +132 -0
  53. data/specs/007-execution-flow-analysis/evidence/probes/05_background.rb +58 -0
  54. data/specs/007-execution-flow-analysis/evidence/probes/06_coordination.rb +158 -0
  55. data/specs/007-execution-flow-analysis/evidence/probes/07_interrupts_manual.rb +185 -0
  56. data/specs/007-execution-flow-analysis/evidence/run.rb +15 -0
  57. data/specs/007-execution-flow-analysis/plan.md +127 -0
  58. data/specs/007-execution-flow-analysis/quickstart.md +51 -0
  59. data/specs/007-execution-flow-analysis/research.md +202 -0
  60. data/specs/007-execution-flow-analysis/spec.md +270 -0
  61. data/specs/007-execution-flow-analysis/tasks.md +257 -0
  62. data/specs/008-rollback-reliability/checklists/requirements.md +43 -0
  63. data/specs/008-rollback-reliability/contracts/api-surface.md +126 -0
  64. data/specs/008-rollback-reliability/contracts/rollback-semantics.md +76 -0
  65. data/specs/008-rollback-reliability/data-model.md +139 -0
  66. data/specs/008-rollback-reliability/plan.md +233 -0
  67. data/specs/008-rollback-reliability/quickstart.md +105 -0
  68. data/specs/008-rollback-reliability/research.md +653 -0
  69. data/specs/008-rollback-reliability/spec.md +561 -0
  70. data/specs/008-rollback-reliability/tasks.md +1110 -0
  71. data/specs/future_improvements.md +48 -0
  72. metadata +35 -2
@@ -0,0 +1,333 @@
1
+ # Execution-flow probes — 2026-09-27T22:26:31Z — ruby_reactor 0.8.3 (7a8f4a67)
2
+
3
+ == S-plain-01 a → b(returns Failure) → c [inline]
4
+ expected: run:a run:b compensate:b undo:a => failure(b)
5
+ observed: run:a run:b compensate:b undo:a => failure(b)
6
+ MATCH
7
+
8
+ == S-plain-02 a → b(raises) → c [inline]
9
+ expected: run:a run:b compensate:b undo:a => failure(b)
10
+ observed: run:a run:b compensate:b undo:a => failure(b)
11
+ MATCH
12
+
13
+ == S-plain-03 a → b(fails, its compensate fails) [inline]
14
+ expected: run:a run:b compensate:b undo:a => failure(b)
15
+ observed: run:a run:b compensate:b undo:a => failure(b)
16
+ note: rollback_failures=["b/compensate/returned_failure"]
17
+ MATCH
18
+
19
+ == S-plain-04 a → b(undo raises) → c(fails) [inline]
20
+ expected: run:a run:b run:c compensate:c undo:b undo:a => failure(c)
21
+ observed: run:a run:b run:c compensate:c undo:b undo:a => failure(c)
22
+ note: rollback_failures=["b/undo/raised"]
23
+ MATCH
24
+
25
+ == S-plain-05 a → b(Halt) → c [inline]
26
+ expected: run:a run:b => halt
27
+ observed: run:a run:b => halt
28
+ MATCH
29
+
30
+ == S-plain-06 a → b(Skipped) → c(fails) [inline]
31
+ expected: run:a run:b run:c compensate:c undo:a => failure(c)
32
+ observed: run:a run:b run:c compensate:c undo:a => failure(c)
33
+ MATCH
34
+
35
+ == S-plain-07 a → b(argument transform raises StandardError) [inline]
36
+ expected: run:a undo:a => failure(b)
37
+ observed: run:a undo:a => failure(b)
38
+ note: error=Step 'b' failed: Step 'b' could not resolve its arguments: transform boom
39
+ MATCH
40
+
41
+ == S-plain-08 a → b(output fails validate_output) [inline]
42
+ expected: run:a run:b compensate:b undo:a => failure(b)
43
+ observed: run:a run:b compensate:b undo:a => failure(b)
44
+ MATCH
45
+
46
+ == S-plain-09 DAG a, b → c → d(fails) [inline]
47
+ expected: run:a run:b run:c run:d compensate:d undo:c undo:b undo:a => failure(d)
48
+ observed: run:a run:b run:c run:d compensate:d undo:c undo:b undo:a => failure(d)
49
+ MATCH
50
+
51
+ == S-compose-01 a → compose(c1 → c2 fails) → b [inline]
52
+ expected: run:a run:child.c1 run:child.c2 compensate:child.c2 undo:child.c1 undo:a => failure(child)
53
+ observed: run:a run:child.c1 run:child.c2 compensate:child.c2 undo:child.c1 undo:a => failure(child)
54
+ MATCH
55
+
56
+ == S-compose-02 a → compose(c1 → c2) → b(fails) [inline]
57
+ expected: run:a run:child.c1 run:child.c2 run:b compensate:b undo:child.c2 undo:child.c1 undo:a => failure(b)
58
+ observed: run:a run:child.c1 run:child.c2 run:b compensate:b undo:child.c2 undo:child.c1 undo:a => failure(b)
59
+ MATCH
60
+
61
+ == S-compose-03 compose x(x1 → x2) → compose y(y1 → y2 fails) [Q2] [inline]
62
+ expected: run:x.x1 run:x.x2 run:y.y1 run:y.y2 compensate:y.y2 undo:y.y1 undo:x.x2 undo:x.x1 => failure(y)
63
+ observed: run:x.x1 run:x.x2 run:y.y1 run:y.y2 compensate:y.y2 undo:y.y1 undo:x.x2 undo:x.x1 => failure(y)
64
+ MATCH
65
+
66
+ == S-compose-04 a → compose outer(o1 → compose inner(i1 → i2 fails)) [inline]
67
+ expected: run:a run:outer.o1 run:inner.i1 run:inner.i2 compensate:inner.i2 undo:inner.i1 undo:outer.o1 undo:a => failure(outer)
68
+ observed: run:a run:outer.o1 run:inner.i1 run:inner.i2 compensate:inner.i2 undo:inner.i1 undo:outer.o1 undo:a => failure(outer)
69
+ MATCH
70
+
71
+ == S-compose-05 compose(c1 → c2 fails once) declaring compose-level retries [inline]
72
+ expected: => raised(RubyReactor::Error::DeprecatedDslError)
73
+ observed: => raised(RubyReactor::Error::DeprecatedDslError)
74
+ MATCH
75
+
76
+ == S-compose-05b compose(c1 → c2 fails once, retried by the child) → b(fails) [inline]
77
+ expected: run:child.c1 run:child.c2 retry:c2#1 run:child.c2 run:b compensate:b undo:child.c2 undo:child.c1 => failure(b)
78
+ observed: run:child.c1 run:child.c2 retry:c2#1 run:child.c2 run:b compensate:b undo:child.c2 undo:child.c1 => failure(b)
79
+ MATCH
80
+
81
+ == S-compose-06 background all: a → compose(c1 → c2) → b(fails) [worker]
82
+ expected: run:a run:child.c1 run:child.c2 run:b compensate:b undo:child.c2 undo:child.c1 undo:a => failure(b)
83
+ observed: run:a run:child.c1 run:child.c2 run:b compensate:b undo:child.c2 undo:child.c1 undo:a => failure(b)
84
+ MATCH
85
+
86
+ == S-compose-07 a → compose(c1(undo raises) → c2 fails) [inline]
87
+ expected: run:a run:child.c1 run:child.c2 compensate:child.c2 undo:child.c1 undo:a => failure(child)
88
+ observed: run:a run:child.c1 run:child.c2 compensate:child.c2 undo:child.c1 undo:a => failure(child)
89
+ note: rollback_failures=["c1/undo/raised"]
90
+ MATCH
91
+
92
+ == S-compose-08 a → compose(c1 → c2 returns Halt) → b [inline]
93
+ expected: run:a run:child.c1 run:child.c2 run:b => success
94
+ observed: run:a run:child.c1 run:child.c2 run:b => success
95
+ MATCH
96
+
97
+ == S-map-01 a → map(4 elems, inline, fail_fast; elem 2 fails) → b [Q1] [inline]
98
+ expected: run:a run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] run:e.e1[2] run:e.e2[2] compensate:e.e2[2] undo:e.e1[2] undo:e.e2[1] undo:e.e1[1] undo:e.e2[0] undo:e.e1[0] undo:a => failure(m)
99
+ observed: run:a run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] run:e.e1[2] run:e.e2[2] compensate:e.e2[2] undo:e.e1[2] undo:e.e2[1] undo:e.e1[1] undo:e.e2[0] undo:e.e1[0] undo:a => failure(m)
100
+ MATCH
101
+
102
+ == S-map-02 a → map(inline, fail_fast false; elem 2 fails) → b [inline]
103
+ expected: run:a run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] run:e.e1[2] run:e.e2[2] compensate:e.e2[2] undo:e.e1[2] run:e.e1[3] run:e.e2[3] run:b => success
104
+ observed: run:a run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] run:e.e1[2] run:e.e2[2] compensate:e.e2[2] undo:e.e1[2] run:e.e1[3] run:e.e2[3] run:b => success
105
+ MATCH
106
+
107
+ == S-map-03 a → map(inline, all ok) → b(fails) [Q1: later failure] [inline]
108
+ expected: run:a run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] run:e.e1[2] run:e.e2[2] run:e.e1[3] run:e.e2[3] run:b compensate:b undo:e.e2[3] undo:e.e1[3] undo:e.e2[2] undo:e.e1[2] undo:e.e2[1] undo:e.e1[1] undo:e.e2[0] undo:e.e1[0] undo:a => failure(b)
109
+ observed: run:a run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] run:e.e1[2] run:e.e2[2] run:e.e1[3] run:e.e2[3] run:b compensate:b undo:e.e2[3] undo:e.e1[3] undo:e.e2[2] undo:e.e1[2] undo:e.e2[1] undo:e.e1[1] undo:e.e2[0] undo:e.e1[0] undo:a => failure(b)
110
+ MATCH
111
+
112
+ == S-map-04 a → map(fan_out, fail_fast; elem 2 fails) → b [worker]
113
+ expected: run:a run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] run:e.e1[2] run:e.e2[2] compensate:e.e2[2] undo:e.e1[2] undo:e.e2[1] undo:e.e1[1] undo:e.e2[0] undo:e.e1[0] undo:a => failure(m)
114
+ observed: run:a run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] run:e.e1[2] run:e.e2[2] compensate:e.e2[2] undo:e.e1[2] undo:e.e2[1] undo:e.e1[1] undo:e.e2[0] undo:e.e1[0] undo:a => failure(m)
115
+ MATCH
116
+
117
+ == S-map-05 a → map(fan_out, fail_fast false; elem 2 fails) → b [worker]
118
+ expected: run:a run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] run:e.e1[2] run:e.e2[2] compensate:e.e2[2] undo:e.e1[2] run:e.e1[3] run:e.e2[3] run:b => success
119
+ observed: run:a run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] run:e.e1[2] run:e.e2[2] compensate:e.e2[2] undo:e.e1[2] run:e.e1[3] run:e.e2[3] run:b => success
120
+ MATCH
121
+
122
+ == S-map-06 a → map(fan_out, all ok) → b(fails) [worker]
123
+ expected: run:a run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] run:e.e1[2] run:e.e2[2] run:e.e1[3] run:e.e2[3] run:b compensate:b undo:e.e2[3] undo:e.e1[3] undo:e.e2[2] undo:e.e1[2] undo:e.e2[1] undo:e.e1[1] undo:e.e2[0] undo:e.e1[0] undo:a => failure(b)
124
+ observed: run:a run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] run:e.e1[2] run:e.e2[2] run:e.e1[3] run:e.e2[3] run:b compensate:b undo:e.e2[3] undo:e.e1[3] undo:e.e2[2] undo:e.e1[2] undo:e.e2[1] undo:e.e1[1] undo:e.e2[0] undo:e.e1[0] undo:a => failure(b)
125
+ MATCH
126
+
127
+ == S-map-07 a → compose(c0 → map(elem 2 fails)) [inline]
128
+ expected: run:a run:child.c0 run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] run:e.e1[2] run:e.e2[2] compensate:e.e2[2] undo:e.e1[2] undo:e.e2[1] undo:e.e1[1] undo:e.e2[0] undo:e.e1[0] undo:child.c0 undo:a => failure(child)
129
+ observed: run:a run:child.c0 run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] run:e.e1[2] run:e.e2[2] compensate:e.e2[2] undo:e.e1[2] undo:e.e2[1] undo:e.e1[1] undo:e.e2[0] undo:e.e1[0] undo:child.c0 undo:a => failure(child)
130
+ MATCH
131
+
132
+ == S-map-08 map(elem: e1 → compose(k1) → e2; elem 2 fails) [inline]
133
+ expected: run:e.e1[0] run:k.k1 run:e.e2[0] run:e.e1[1] run:k.k1 run:e.e2[1] run:e.e1[2] run:k.k1 run:e.e2[2] compensate:e.e2[2] undo:k.k1 undo:e.e1[2] undo:e.e2[1] undo:k.k1 undo:e.e1[1] undo:e.e2[0] undo:k.k1 undo:e.e1[0] => failure(m)
134
+ observed: run:e.e1[0] run:k.k1 run:e.e2[0] run:e.e1[1] run:k.k1 run:e.e2[1] run:e.e1[2] run:k.k1 run:e.e2[2] compensate:e.e2[2] undo:k.k1 undo:e.e1[2] undo:e.e2[1] undo:k.k1 undo:e.e1[1] undo:e.e2[0] undo:k.k1 undo:e.e1[0] => failure(m)
135
+ MATCH
136
+
137
+ == S-map-04b map(fan_out, fail_fast; elem 2 fails), element jobs performed 3,2,1,0 [worker]
138
+ expected: run:a run:e.e1[3] run:e.e2[3] run:e.e1[2] run:e.e2[2] compensate:e.e2[2] undo:e.e1[2] undo:e.e2[3] undo:e.e1[3] undo:a => failure(m)
139
+ observed: run:a run:e.e1[3] run:e.e2[3] run:e.e1[2] run:e.e2[2] compensate:e.e2[2] undo:e.e1[2] undo:e.e2[3] undo:e.e1[3] undo:a => failure(m)
140
+ MATCH
141
+
142
+ == S-map-09 a → map(fan_out; each e2 fails once, retries 2) → b [worker]
143
+ expected: run:a run:e.e1[0] run:e.e2[0] retry:e2#1 run:e.e1[1] run:e.e2[1] retry:e2#1 run:e.e2[0] run:e.e2[1] run:b => success
144
+ observed: run:a run:e.e1[0] run:e.e2[0] retry:e2#1 run:e.e1[1] run:e.e2[1] retry:e2#1 run:e.e2[0] run:e.e2[1] run:b => success
145
+ MATCH
146
+
147
+ == S-map-10 a → map(inline; each e2 fails once, retries 2) → b [inline]
148
+ expected: run:a run:e.e1[0] run:e.e2[0] retry:e2#1 run:e.e2[0] run:e.e1[1] run:e.e2[1] retry:e2#1 run:e.e2[1] run:b => success
149
+ observed: run:a run:e.e1[0] run:e.e2[0] retry:e2#1 run:e.e2[0] run:e.e1[1] run:e.e2[1] retry:e2#1 run:e.e2[1] run:b => success
150
+ MATCH
151
+
152
+ == S-map-11 a → map(inline; elem 1 returns Halt) → b [inline]
153
+ expected: run:a run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] => halt
154
+ observed: run:a run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] => halt
155
+ MATCH
156
+
157
+ == S-map-12 a → map(fan_out; element e1 → async_step u, e2; elem 1 fails) → b [worker]
158
+ expected: run:a run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] compensate:e.e2[1] undo:e.e1[1] undo:e.e2[0] undo:e.e1[0] undo:a run:e.u[0] run:e.u[1] => failure(m)
159
+ observed: run:a run:e.e1[0] run:e.e2[0] run:e.e1[1] run:e.e2[1] compensate:e.e2[1] undo:e.e1[1] undo:e.e2[0] undo:e.e1[0] undo:a run:e.u[0] run:e.u[1] => failure(m)
160
+ note: StepWorker jobs left after drain: 0
161
+ MATCH
162
+
163
+ == S-async-01 a → async_step u(fails), no reader; b [inline parent + drained StepWorker]
164
+ expected: run:a run:b run:u compensate:u => success
165
+ observed: run:a run:b run:u compensate:u => success
166
+ MATCH
167
+
168
+ == S-async-02 a → async_step u(fails) → r reads u and fails [inline parent + Sidekiq inline!]
169
+ expected: run:a run:u compensate:u run:r compensate:r undo:a => failure(r)
170
+ observed: run:a run:u compensate:u run:r compensate:r undo:a => failure(r)
171
+ MATCH
172
+
173
+ == S-async-03 a → async_step u(ok); b(fails), no reader [inline parent + drained StepWorker]
174
+ expected: run:a run:b compensate:b undo:a run:u => failure(b)
175
+ observed: run:a run:b compensate:b undo:a run:u => failure(b)
176
+ MATCH
177
+
178
+ == S-async-04 a → async_reactor child(c1 → c2 fails), no reader; b [inline parent + drained Worker]
179
+ expected: run:a run:b run:child.c1 run:child.c2 compensate:child.c2 undo:child.c1 => success
180
+ observed: run:a run:b run:child.c1 run:child.c2 compensate:child.c2 undo:child.c1 => success
181
+ MATCH
182
+
183
+ == S-async-05 a → async_reactor child(c1 → c2 fails) → r reads child and fails [inline parent + Sidekiq inline!]
184
+ expected: run:a run:child.c1 run:child.c2 compensate:child.c2 undo:child.c1 run:r compensate:r undo:a => failure(r)
185
+ observed: run:a run:child.c1 run:child.c2 compensate:child.c2 undo:child.c1 run:r compensate:r undo:a => failure(r)
186
+ MATCH
187
+
188
+ == S-async-06 a → async_reactor child(ok); b(fails) [inline parent + drained Worker]
189
+ expected: run:a run:b compensate:b undo:a run:child.c1 run:child.c2 => failure(b)
190
+ observed: run:a run:b compensate:b undo:a run:child.c1 run:child.c2 => failure(b)
191
+ MATCH
192
+
193
+ == S-async-07 a → async_step u(always fails, retries 3), no reader; b [inline parent + drained StepWorker]
194
+ expected: run:a run:b run:u run:u run:u compensate:u => success
195
+ observed: run:a run:b run:u run:u run:u compensate:u => success
196
+ MATCH
197
+
198
+ == S-async-08 a → async_step u → r reads u; unit never finishes in time [inline parent + drained StepWorker (after)]
199
+ expected: run:a undo:a run:u compensate:u => failure(r)
200
+ observed: run:a undo:a run:u compensate:u => failure(r)
201
+ note: error=Step 'r' failed: Step 'r' could not resolve its arguments: Timed out after 2s waiting for async completion on 'rr:done:fe2a937e-53ea-4b93-8864-79c4cd29f3d4:u'. The dispatched work never reached a terminal state within `RubyReactor.configuration.async_wait_timeout` — check that a worker is running and consuming the queue, or raise the timeout if this unit is legitimately slower.
202
+ MATCH
203
+
204
+ == S-bg-01 background all: a → b → c(fails) [worker]
205
+ expected: run:a run:b run:c compensate:c undo:b undo:a => failure(c)
206
+ observed: run:a run:b run:c compensate:c undo:b undo:a => failure(c)
207
+ MATCH
208
+
209
+ == S-bg-02 a | background after: :a | b → c(fails) (a ran in the caller) [caller + worker]
210
+ expected: run:a run:b run:c compensate:c undo:b undo:a => failure(c)
211
+ observed: run:a run:b run:c compensate:c undo:b undo:a => failure(c)
212
+ note: caller returned with 1 worker job(s) queued
213
+ MATCH
214
+
215
+ == S-bg-03 background all: a → b(always fails, retries 3) [worker]
216
+ expected: run:a run:b retry:b#1 run:b retry:b#2 run:b compensate:b undo:a => failure(b)
217
+ observed: run:a run:b retry:b#1 run:b retry:b#2 run:b compensate:b undo:a => failure(b)
218
+ MATCH
219
+
220
+ == S-bg-04 background all: a → b(fails once, retries 2) → c [worker]
221
+ expected: run:a run:b retry:b#1 run:b run:c => success
222
+ observed: run:a run:b retry:b#1 run:b run:c => success
223
+ MATCH
224
+
225
+ == S-lock-01 reactor with_lock(rk): a → b(fails) [inline]
226
+ expected: lock_acquired:rk run:a run:b compensate:b undo:a lock_released:lock:rk => failure(b)
227
+ observed: lock_acquired:rk run:a run:b compensate:b undo:a lock_released:lock:rk => failure(b)
228
+ MATCH
229
+
230
+ == S-lock-02 a → b(step with_lock sk) → c(fails) [inline]
231
+ expected: run:a lock_acquired:sk run:b lock_released:sk run:c compensate:c lock_acquired:sk undo:b lock_released:sk undo:a => failure(c)
232
+ observed: run:a lock_acquired:sk run:b lock_released:sk run:c compensate:c lock_acquired:sk undo:b lock_released:sk undo:a => failure(c)
233
+ MATCH
234
+
235
+ == S-lock-03 a → b(step with_lock sk, key held by another owner) [inline]
236
+ expected: run:a undo:a => failure(b)
237
+ observed: run:a undo:a => failure(b)
238
+ MATCH
239
+
240
+ == S-lock-04 a → b(step with_semaphore limit 1) → c(fails) [inline]
241
+ expected: run:a semaphore_acquired:sem run:b semaphore_released:sem run:c compensate:c semaphore_acquired:sem undo:b semaphore_released:sem undo:a => failure(c)
242
+ observed: run:a semaphore_acquired:sem run:b semaphore_released:sem run:c compensate:c semaphore_acquired:sem undo:b semaphore_released:sem undo:a => failure(c)
243
+ MATCH
244
+
245
+ == S-lock-05 a → b(step lock sk, rollback_wait 0.3) → c(takes sk as intruder, fails) [inline]
246
+ expected: run:a lock_acquired:sk run:b lock_released:sk run:c compensate:c undo:a => failure(c)
247
+ observed: run:a lock_acquired:sk run:b lock_released:sk run:c compensate:c undo:a => failure(c)
248
+ note: rollback_failures=["b/undo/coordination_unavailable"]
249
+ MATCH
250
+
251
+ == S-lock-06 reactor with_lock(rk): a → compose(child with_lock rk: c1) → b(fails) [inline]
252
+ expected: lock_acquired:rk run:a lock_acquired:rk run:child.c1 lock_released:lock:rk run:b(rk count=1) compensate:b undo:child.c1 undo:a lock_released:lock:rk => failure(b)
253
+ observed: lock_acquired:rk run:a lock_acquired:rk run:child.c1 lock_released:lock:rk run:b(rk count=1) compensate:b undo:child.c1 undo:a lock_released:lock:rk => failure(b)
254
+ MATCH
255
+
256
+ == S-retry-01 a → b(always fails, retries 3) [inline]
257
+ expected: run:a run:b retry:b#1 run:b retry:b#2 run:b compensate:b undo:a => failure(b)
258
+ observed: run:a run:b retry:b#1 run:b retry:b#2 run:b compensate:b undo:a => failure(b)
259
+ MATCH
260
+
261
+ == S-retry-02 a → b(fail! retry: false, retries 3) [inline]
262
+ expected: run:a run:b compensate:b undo:a => failure(b)
263
+ observed: run:a run:b compensate:b undo:a => failure(b)
264
+ MATCH
265
+
266
+ == S-retry-03 a → b(fails once, retries 2) → c [inline]
267
+ expected: run:a run:b retry:b#1 run:b run:c => success
268
+ observed: run:a run:b retry:b#1 run:b run:c => success
269
+ MATCH
270
+
271
+ == S-retry-04 a → b(raises, retries 2) [inline]
272
+ expected: run:a run:b retry:b#1 run:b compensate:b undo:a => failure(b)
273
+ observed: run:a run:b retry:b#1 run:b compensate:b undo:a => failure(b)
274
+ MATCH
275
+
276
+ == S-intr-01 reactor with_lock(rk): a → interrupt → [continue] → c(fails) [inline]
277
+ expected: lock_acquired:rk run:a lock_released:lock:rk lock_acquired:rk run:c compensate:c undo:a lock_released:lock:rk => failure(c)
278
+ observed: lock_acquired:rk run:a lock_released:lock:rk lock_acquired:rk run:c compensate:c undo:a lock_released:lock:rk => failure(c)
279
+ note: first run => paused
280
+ MATCH
281
+
282
+ == S-intr-02 a → interrupt(validate, max_attempts 1) ← invalid payload [inline]
283
+ expected: run:a undo:a => failure(approval)
284
+ observed: run:a undo:a => failure(approval)
285
+ MATCH
286
+
287
+ == S-intr-03 a → interrupt → Reactor.cancel [inline]
288
+ expected: run:a => status=cancelled
289
+ observed: run:a => status=cancelled
290
+ MATCH
291
+
292
+ == S-intr-04 completed a → b; reactor lock rk held by another owner; Reactor.undo(id) [inline]
293
+ expected: lock_acquired:rk run:a run:b lock_released:lock:rk undo:b undo:a => status=cancelled
294
+ observed: lock_acquired:rk run:a run:b lock_released:lock:rk undo:b undo:a => status=cancelled
295
+ MATCH
296
+
297
+ == S-edge-01 background all: a → b(step lock held elsewhere; lock_snooze_max_attempts 2) [worker]
298
+ expected: run:a undo:a => failure(b)
299
+ observed: run:a undo:a => failure(b)
300
+ MATCH
301
+
302
+ == S-edge-02 background all: a → b(worker crashes once) → c; job redelivered [worker]
303
+ expected: run:a run:b crash run:b run:c => success
304
+ observed: run:a run:b crash run:b run:c => success
305
+ MATCH
306
+
307
+ == S-edge-03 a → b(raises a non-StandardError Exception) [inline]
308
+ expected: run:a run:b compensate:b undo:a => failure(b)
309
+ observed: run:a run:b compensate:b undo:a => failure(b)
310
+ MATCH
311
+
312
+ == S-edge-03b a → b(interrupted: raises Interrupt) [inline]
313
+ expected: run:a run:b => raised(Interrupt)
314
+ observed: run:a run:b => raised(Interrupt)
315
+ note: stored status=aborted
316
+ MATCH
317
+
318
+ == S-edge-04 a → b(declares a where-condition) [inline]
319
+ expected: => raised(RubyReactor::Error::DeprecatedDslError)
320
+ observed: => raised(RubyReactor::Error::DeprecatedDslError)
321
+ MATCH
322
+
323
+ == S-edge-05 a → b(argument type check fails) [inline]
324
+ expected: run:a undo:a => failure(b)
325
+ observed: run:a undo:a => failure(b)
326
+ MATCH
327
+
328
+ == S-edge-06 reactor input validation fails [inline]
329
+ expected: => failure(?)
330
+ observed: => failure(?)
331
+ MATCH
332
+
333
+ 64 scenarios, 64 match, 0 mismatch
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Baseline: plain steps, one reactor, inline. Research H1–H6.
4
+
5
+ module P
6
+ class Plain01 < Base
7
+ pstep :a
8
+ pstep :b, after: :a, fail: true
9
+ pstep :c, after: :b
10
+ end
11
+
12
+ class Plain02 < Base
13
+ pstep :a
14
+ pstep :b, after: :a, fail: :raise
15
+ pstep :c, after: :b
16
+ end
17
+
18
+ class Plain03 < Base
19
+ pstep :a
20
+ pstep :b, after: :a, fail: true, compensate: :fail
21
+ end
22
+
23
+ class Plain04 < Base
24
+ pstep :a
25
+ pstep :b, after: :a, undo: :raise
26
+ pstep :c, after: :b, fail: true
27
+ end
28
+
29
+ class Plain05 < Base
30
+ pstep :a
31
+ pstep :b, after: :a do
32
+ run do |_inputs, _ctx|
33
+ Probe.rec("run:b")
34
+ RubyReactor.Halt(reason: "enough")
35
+ end
36
+ end
37
+ pstep :c, after: :b
38
+ end
39
+
40
+ class Plain06 < Base
41
+ pstep :a
42
+ pstep :b, after: :a do
43
+ run do |_inputs, _ctx|
44
+ Probe.rec("run:b")
45
+ RubyReactor.Skipped("nothing to do")
46
+ end
47
+ end
48
+ pstep :c, after: :b, fail: true
49
+ end
50
+
51
+ # An argument transform raising a plain StandardError: resolution happens
52
+ # outside the step's rescue (StepExecutor#execute_step).
53
+ class Plain07 < Base
54
+ pstep :a
55
+ pstep :b do
56
+ argument :x, result(:a), transform: ->(_value) { raise ArgumentError, "transform boom" }
57
+ end
58
+ end
59
+
60
+ class Plain08 < Base
61
+ pstep :a
62
+ pstep :b, after: :a do
63
+ validate_output :integer
64
+ end
65
+ end
66
+
67
+ # DAG: a and b independent, c needs both, d fails.
68
+ class Plain09 < Base
69
+ pstep :a
70
+ pstep :b
71
+ pstep :c, after: %i[a b]
72
+ pstep :d, after: :c, fail: true
73
+ end
74
+ end
75
+
76
+ Probe.scenario "S-plain-01", "a → b(returns Failure) → c",
77
+ mode: :inline, expected: %w[run:a run:b compensate:b undo:a => failure(b)] do
78
+ P::Plain01.run({})
79
+ end
80
+
81
+ Probe.scenario "S-plain-02", "a → b(raises) → c",
82
+ mode: :inline, expected: %w[run:a run:b compensate:b undo:a => failure(b)] do
83
+ P::Plain02.run({})
84
+ end
85
+
86
+ Probe.scenario "S-plain-03", "a → b(fails, its compensate fails)",
87
+ mode: :inline, expected: %w[run:a run:b compensate:b undo:a => failure(b)] do
88
+ Probe.rollback_note(P::Plain03.run({}))
89
+ end
90
+
91
+ Probe.scenario "S-plain-04", "a → b(undo raises) → c(fails)",
92
+ mode: :inline, expected: %w[run:a run:b run:c compensate:c undo:b undo:a => failure(c)] do
93
+ Probe.rollback_note(P::Plain04.run({}))
94
+ end
95
+
96
+ Probe.scenario "S-plain-05", "a → b(Halt) → c",
97
+ mode: :inline, expected: %w[run:a run:b => halt] do
98
+ P::Plain05.run({})
99
+ end
100
+
101
+ Probe.scenario "S-plain-06", "a → b(Skipped) → c(fails)",
102
+ mode: :inline, expected: %w[run:a run:b run:c compensate:c undo:a => failure(c)] do
103
+ P::Plain06.run({})
104
+ end
105
+
106
+ Probe.scenario "S-plain-07", "a → b(argument transform raises StandardError)",
107
+ mode: :inline, expected: %w[run:a undo:a => failure(b)] do
108
+ result = P::Plain07.run({})
109
+ Probe.note("error=#{result.error.to_s.lines.first&.strip}")
110
+ result
111
+ end
112
+
113
+ Probe.scenario "S-plain-08", "a → b(output fails validate_output)",
114
+ mode: :inline, expected: %w[run:a run:b compensate:b undo:a => failure(b)] do
115
+ P::Plain08.run({})
116
+ end
117
+
118
+ Probe.scenario "S-plain-09", "DAG a, b → c → d(fails)",
119
+ mode: :inline,
120
+ expected: %w[run:a run:b run:c run:d compensate:d undo:c undo:b undo:a => failure(d)] do
121
+ P::Plain09.run({})
122
+ end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ # compose: research H10–H13, user question Q2.
4
+
5
+ module P
6
+ class ChildOk < Base
7
+ tag "child"
8
+ input :from_a, optional: true
9
+ pstep :c1
10
+ pstep :c2, after: :c1
11
+ end
12
+
13
+ class ChildFails < Base
14
+ tag "child"
15
+ input :from_a, optional: true
16
+ pstep :c1
17
+ pstep :c2, after: :c1, fail: true
18
+ end
19
+
20
+ class Compose01 < Base
21
+ pstep :a
22
+ compose(:child, ChildFails) { argument :from_a, result(:a) }
23
+ pstep :b, after: :child
24
+ end
25
+
26
+ class Compose02 < Base
27
+ pstep :a
28
+ compose(:child, ChildOk) { argument :from_a, result(:a) }
29
+ pstep :b, after: :child, fail: true
30
+ end
31
+
32
+ class ChildX < Base
33
+ tag "x"
34
+ pstep :x1
35
+ pstep :x2, after: :x1
36
+ end
37
+
38
+ class ChildY < Base
39
+ tag "y"
40
+ input :after_x, optional: true
41
+ pstep :y1
42
+ pstep :y2, after: :y1, fail: true
43
+ end
44
+
45
+ class Compose03 < Base
46
+ compose :x, ChildX
47
+ compose(:y, ChildY) { argument :after_x, result(:x) }
48
+ end
49
+
50
+ class Inner04 < Base
51
+ tag "inner"
52
+ pstep :i1
53
+ pstep :i2, after: :i1, fail: true
54
+ end
55
+
56
+ class Outer04 < Base
57
+ tag "outer"
58
+ input :from_a, optional: true
59
+ pstep :o1
60
+ compose(:inner, Inner04) { argument :from_o1, result(:o1) }
61
+ end
62
+
63
+ class Compose04 < Base
64
+ pstep :a
65
+ compose(:outer, Outer04) { argument :from_a, result(:a) }
66
+ end
67
+
68
+ class ChildFailsOnce < Base
69
+ tag "child"
70
+ pstep :c1
71
+ pstep :c2, after: :c1, fail_times: 1
72
+ end
73
+
74
+ # 008 R-14: the child retries its own step; a compose cannot declare retries.
75
+ class ChildRetriesOnce < Base
76
+ tag "child"
77
+ pstep :c1
78
+ pstep :c2, after: :c1, fail_times: 1, retries: { max_attempts: 2, base_delay: 0 }
79
+ end
80
+
81
+ class Compose05b < Base
82
+ compose :child, ChildRetriesOnce
83
+ pstep :b, after: :child, fail: true
84
+ end
85
+
86
+ class Compose06 < Base
87
+ background all: true
88
+ pstep :a
89
+ compose(:child, ChildOk) { argument :from_a, result(:a) }
90
+ pstep :b, after: :child, fail: true
91
+ end
92
+
93
+ class ChildCompensateFails < Base
94
+ tag "child"
95
+ pstep :c1, undo: :raise
96
+ pstep :c2, after: :c1, fail: true
97
+ end
98
+
99
+ class ChildHalts < Base
100
+ tag "child"
101
+ pstep :c1
102
+ pstep :c2, after: :c1 do
103
+ run do |_inputs, _ctx|
104
+ Probe.rec("run:child.c2")
105
+ RubyReactor.Halt(reason: "child done early")
106
+ end
107
+ end
108
+ end
109
+
110
+ class Compose08 < Base
111
+ pstep :a
112
+ compose(:child, ChildHalts) { argument :from_a, result(:a) }
113
+ pstep :b, after: :child
114
+ end
115
+
116
+ class Compose07 < Base
117
+ pstep :a
118
+ compose(:child, ChildCompensateFails) { argument :from_a, result(:a) }
119
+ end
120
+ end
121
+
122
+ Probe.scenario "S-compose-01", "a → compose(c1 → c2 fails) → b",
123
+ mode: :inline,
124
+ expected: %w[run:a run:child.c1 run:child.c2 compensate:child.c2 undo:child.c1 undo:a
125
+ => failure(child)] do
126
+ P::Compose01.run({})
127
+ end
128
+
129
+ Probe.scenario "S-compose-02", "a → compose(c1 → c2) → b(fails)",
130
+ mode: :inline,
131
+ expected: %w[run:a run:child.c1 run:child.c2 run:b compensate:b undo:child.c2 undo:child.c1
132
+ undo:a => failure(b)] do
133
+ P::Compose02.run({})
134
+ end
135
+
136
+ Probe.scenario "S-compose-03", "compose x(x1 → x2) → compose y(y1 → y2 fails) [Q2]",
137
+ mode: :inline,
138
+ expected: %w[run:x.x1 run:x.x2 run:y.y1 run:y.y2 compensate:y.y2 undo:y.y1 undo:x.x2 undo:x.x1
139
+ => failure(y)] do
140
+ P::Compose03.run({})
141
+ end
142
+
143
+ Probe.scenario "S-compose-04", "a → compose outer(o1 → compose inner(i1 → i2 fails))",
144
+ mode: :inline,
145
+ expected: %w[run:a run:outer.o1 run:inner.i1 run:inner.i2 compensate:inner.i2 undo:inner.i1
146
+ undo:outer.o1 undo:a => failure(outer)] do
147
+ P::Compose04.run({})
148
+ end
149
+
150
+ Probe.scenario "S-compose-05", "compose(c1 → c2 fails once) declaring compose-level retries",
151
+ mode: :inline, expected: %w[=> raised(RubyReactor::Error::DeprecatedDslError)] do
152
+ # 008 R-14: a parent never retries a nested reactor as a whole.
153
+ Class.new(P::Base) { compose(:child, P::ChildFailsOnce) { retries max_attempts: 2, base_delay: 0 } }
154
+ rescue RubyReactor::Error::DeprecatedDslError
155
+ "raised(RubyReactor::Error::DeprecatedDslError)"
156
+ end
157
+
158
+ Probe.scenario "S-compose-05b", "compose(c1 → c2 fails once, retried by the child) → b(fails)",
159
+ mode: :inline,
160
+ expected: %w[run:child.c1 run:child.c2 retry:c2#1 run:child.c2 run:b compensate:b undo:child.c2
161
+ undo:child.c1 => failure(b)] do
162
+ P::Compose05b.run({})
163
+ end
164
+
165
+ Probe.scenario "S-compose-06", "background all: a → compose(c1 → c2) → b(fails)",
166
+ mode: :worker,
167
+ expected: %w[run:a run:child.c1 run:child.c2 run:b compensate:b undo:child.c2 undo:child.c1
168
+ undo:a => failure(b)] do
169
+ Probe.run_async(P::Compose06)
170
+ end
171
+
172
+ Probe.scenario "S-compose-07", "a → compose(c1(undo raises) → c2 fails)",
173
+ mode: :inline,
174
+ expected: %w[run:a run:child.c1 run:child.c2 compensate:child.c2 undo:child.c1 undo:a
175
+ => failure(child)] do
176
+ Probe.rollback_note(P::Compose07.run({}))
177
+ end
178
+
179
+ Probe.scenario "S-compose-08", "a → compose(c1 → c2 returns Halt) → b",
180
+ mode: :inline, expected: %w[run:a run:child.c1 run:child.c2 run:b => success] do
181
+ P::Compose08.run({})
182
+ end