ruby_reactor 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/demo-app-e2e-verify/SKILL.md +226 -0
  3. data/.claude/skills/speckit-demo-tests/SKILL.md +144 -0
  4. data/.release-please-manifest.json +1 -1
  5. data/.specify/feature.json +1 -1
  6. data/.specify/memory/constitution.md +92 -15
  7. data/.specify/templates/plan-template.md +4 -0
  8. data/.specify/templates/tasks-template.md +8 -1
  9. data/CHANGELOG.md +151 -0
  10. data/CLAUDE.md +2 -2
  11. data/README.md +149 -40
  12. data/lib/ruby_reactor/context.rb +9 -2
  13. data/lib/ruby_reactor/context_serializer.rb +13 -0
  14. data/lib/ruby_reactor/dsl/interrupt_builder.rb +6 -0
  15. data/lib/ruby_reactor/dsl/lockable.rb +2 -2
  16. data/lib/ruby_reactor/dsl/reactor.rb +36 -18
  17. data/lib/ruby_reactor/dsl/step_builder.rb +95 -2
  18. data/lib/ruby_reactor/dsl/template_helpers.rb +13 -5
  19. data/lib/ruby_reactor/dsl/validation_helpers.rb +17 -0
  20. data/lib/ruby_reactor/error/input_validation_error.rb +4 -0
  21. data/lib/ruby_reactor/error/step_failure_error.rb +10 -3
  22. data/lib/ruby_reactor/executor/compensation_manager.rb +58 -45
  23. data/lib/ruby_reactor/executor/ordered_lock_support.rb +9 -9
  24. data/lib/ruby_reactor/executor/result_handler.rb +44 -13
  25. data/lib/ruby_reactor/executor/retry_manager.rb +6 -2
  26. data/lib/ruby_reactor/executor/step_executor.rb +27 -14
  27. data/lib/ruby_reactor/executor.rb +20 -15
  28. data/lib/ruby_reactor/map/element_executor.rb +7 -1
  29. data/lib/ruby_reactor/map/helpers.rb +9 -7
  30. data/lib/ruby_reactor/map/result_enumerator.rb +2 -0
  31. data/lib/ruby_reactor/map/sweeper.rb +1 -1
  32. data/lib/ruby_reactor/max_retries_exhausted_failure.rb +3 -2
  33. data/lib/ruby_reactor/open_telemetry.rb +7 -4
  34. data/lib/ruby_reactor/ordered_lock.rb +3 -3
  35. data/lib/ruby_reactor/reactor.rb +9 -12
  36. data/lib/ruby_reactor/rspec/matchers.rb +64 -17
  37. data/lib/ruby_reactor/rspec/test_subject.rb +8 -8
  38. data/lib/ruby_reactor/step/async_reactor_step.rb +159 -162
  39. data/lib/ruby_reactor/step/compose_step.rb +56 -75
  40. data/lib/ruby_reactor/step/input_contract.rb +128 -0
  41. data/lib/ruby_reactor/step/map_step.rb +178 -215
  42. data/lib/ruby_reactor/step.rb +119 -18
  43. data/lib/ruby_reactor/step_signals.rb +37 -0
  44. data/lib/ruby_reactor/step_worker.rb +25 -10
  45. data/lib/ruby_reactor/storage/adapter.rb +4 -0
  46. data/lib/ruby_reactor/storage/redis_adapter.rb +1 -72
  47. data/lib/ruby_reactor/storage/redis_reactor_scan.rb +116 -0
  48. data/lib/ruby_reactor/template/result.rb +9 -2
  49. data/lib/ruby_reactor/utils/fetch_indifferent.rb +13 -0
  50. data/lib/ruby_reactor/version.rb +1 -1
  51. data/lib/ruby_reactor/web/api.rb +23 -6
  52. data/lib/ruby_reactor/web/public/assets/index-BQvIWPdx.css +1 -0
  53. data/lib/ruby_reactor/web/public/assets/index-Dw4KV4QY.js +22 -0
  54. data/lib/ruby_reactor/web/public/index.html +2 -2
  55. data/lib/ruby_reactor.rb +61 -9
  56. data/specs/002-step-input-contracts/checklists/requirements.md +49 -0
  57. data/specs/002-step-input-contracts/contracts/dsl-surface.md +193 -0
  58. data/specs/002-step-input-contracts/data-model.md +115 -0
  59. data/specs/002-step-input-contracts/plan.md +165 -0
  60. data/specs/002-step-input-contracts/quickstart.md +170 -0
  61. data/specs/002-step-input-contracts/research.md +233 -0
  62. data/specs/002-step-input-contracts/spec.md +359 -0
  63. data/specs/002-step-input-contracts/tasks.md +367 -0
  64. data/specs/004-inheritable-step-class/checklists/requirements.md +40 -0
  65. data/specs/004-inheritable-step-class/contracts/step-lifecycle.md +85 -0
  66. data/specs/004-inheritable-step-class/data-model.md +116 -0
  67. data/specs/004-inheritable-step-class/plan.md +174 -0
  68. data/specs/004-inheritable-step-class/quickstart.md +112 -0
  69. data/specs/004-inheritable-step-class/research.md +308 -0
  70. data/specs/004-inheritable-step-class/spec.md +316 -0
  71. data/specs/004-inheritable-step-class/tasks.md +258 -0
  72. data/specs/deferred-003-step-lock-declarations/checklists/requirements.md +51 -0
  73. data/specs/deferred-003-step-lock-declarations/contracts/dsl-surface.md +154 -0
  74. data/specs/deferred-003-step-lock-declarations/data-model.md +131 -0
  75. data/specs/deferred-003-step-lock-declarations/plan.md +166 -0
  76. data/specs/deferred-003-step-lock-declarations/quickstart.md +169 -0
  77. data/specs/deferred-003-step-lock-declarations/research.md +196 -0
  78. data/specs/deferred-003-step-lock-declarations/spec.md +447 -0
  79. data/specs/deferred-003-step-lock-declarations/tasks.md +572 -0
  80. data/specs/possible_feature.md +22 -0
  81. metadata +34 -11
  82. data/lib/ruby_reactor/web/public/assets/index-B46p-M6K.css +0 -1
  83. data/lib/ruby_reactor/web/public/assets/index-DPmP4yXT.js +0 -22
  84. data/specs/001-background-async-steps/checklists/requirements.md +0 -39
  85. data/specs/001-background-async-steps/contracts/public-dsl.md +0 -154
  86. data/specs/001-background-async-steps/data-model.md +0 -117
  87. data/specs/001-background-async-steps/plan.md +0 -168
  88. data/specs/001-background-async-steps/quickstart.md +0 -102
  89. data/specs/001-background-async-steps/research.md +0 -150
  90. data/specs/001-background-async-steps/spec.md +0 -146
  91. data/specs/001-background-async-steps/tasks.md +0 -271
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b08f95b4b201fa008ca5f98e8e221f7bb8da00becac3307f7f107406b4a3fd95
4
- data.tar.gz: d3884e832408aea3d1caee63342b71c16823474abba8b98536a76dbe55ca1fc2
3
+ metadata.gz: 6e671a274b5dc3854189ab1f3c40ff4d7e307a567ed960dbad0be1405f05ff58
4
+ data.tar.gz: 0a44e9cf8dbed99b72762381300c4c503500d3025f21288e473eef377a28dbec
5
5
  SHA512:
6
- metadata.gz: 2df63b3d9a9603d61baa4c51ea453399c2890b012fd74e11e01b7931081dfc9d3cc1eba358930166c2847c6cef964d9491b968e40c448d6a62fcb8ea0ae90e90
7
- data.tar.gz: 809eb63f486448fb5d0c0d8d15eec39c3e358e3de26dee6c1db8318c9a2f0449fbf32a7f91695e75d58b8eee2ac026e46ce0e063181b567f4b777c35b44d2b52
6
+ metadata.gz: a39666bdba5d7c911589da0f30a12e29a9f5187c31a9a1bfb9ddc0c55aba26cac201db3e8083854f79858d6845ed98097589979873a0a4aa7cc8dfdaca635856
7
+ data.tar.gz: a5069c91a3942f1697a317baee2223b790b4262ca6d90f61a1b3aa0cd8b61ff9b199b7a05f58f791df11bd7b4336611907a6abd3cbce6ebfd007d9bc92949c4e
@@ -0,0 +1,226 @@
1
+ ---
2
+ name: "demo-app-e2e-verify"
3
+ description: "Boot demo_app via docker-compose, run demo_reactors.rake, auto-derive acceptance criteria from the reactor/rake source, and cross-check actual behavior against both the RubyReactor API and dashboard at localhost:[port]/ruby_reactor. Writes findings + a fix plan to a tmp report."
4
+ argument-hint: "Optional: specific rake task(s)/reactor(s) to focus on (default: all)"
5
+ compatibility: "Requires docker compose and the demo_app/lib/tasks/demo_reactors.rake tasks"
6
+ metadata:
7
+ author: "project"
8
+ user-invocable: true
9
+ disable-model-invocation: false
10
+ ---
11
+
12
+ ## User Input
13
+
14
+ ```text
15
+ $ARGUMENTS
16
+ ```
17
+
18
+ If non-empty, scope the run to the named rake task(s)/reactor(s) instead of `demo:all`.
19
+ Otherwise run everything.
20
+
21
+ ## Purpose
22
+
23
+ End-to-end confidence check for `demo_app`: does what the rake demos claim to do actually
24
+ show up correctly in the RubyReactor API and dashboard? This is not a spec-kit workflow —
25
+ it derives its own expectations from the current code rather than from `specs/`.
26
+
27
+ ## Step 1: Bring the stack up
28
+
29
+ 1. `docker compose ps` to see what's already running; avoid tearing down containers the
30
+ user didn't ask to stop.
31
+ 2. `docker compose up -d demo-redis demo-sidekiq demo-app` (skip `redis-test`/`teley`
32
+ unless already running — they're not needed for this check). If it fails with a
33
+ container name conflict (`container_name` is static, and this repo runs from multiple
34
+ git worktrees sharing one docker daemon), check `docker ps -a --filter name=ruby_reactor`
35
+ — if the conflicting containers are `Exited` (not running), it's safe to `docker rm`
36
+ them and retry; never `rm` a container that's currently `Up` without asking, it may
37
+ belong to another worktree's active session.
38
+ 3. Poll `docker compose ps` / container healthchecks until `demo-redis` and `demo-sidekiq`
39
+ report healthy, and until the Rails server inside `demo-app` responds. Use a
40
+ short polling loop (not a fixed sleep) against `http://localhost:3789/ruby_reactor` —
41
+ treat a non-timeout HTTP response (even a 404/500) as "server is up", empty/refused as
42
+ "still booting". Cap the wait (e.g. ~90s) and report clearly if it never comes up.
43
+ 4. Read the exposed host port for `demo-app` from `docker-compose.yml` at runtime rather
44
+ than assuming `3789` — the file may have changed.
45
+
46
+ ## Step 2: Auto-learn the expected behavior (before running anything)
47
+
48
+ For each rake task in scope, build an acceptance-criteria list by reading the source, not
49
+ by assuming:
50
+
51
+ 1. Read `demo_app/lib/tasks/demo_reactors.rake` — for each in-scope task, note: which
52
+ reactor(s) it calls, what inputs/scenarios it loops over (e.g. `fail_at` values), and
53
+ what output markers it prints for success/failure/background/pause (`✅`, `❌`, `⏳`,
54
+ etc.).
55
+ 2. Read the corresponding reactor class(es) in `demo_app/app/reactors/`. For each step,
56
+ note: what triggers it to succeed/fail/halt/skip/retry-veto, what its rollback (undo)
57
+ does if it has side effects, and any locks/rate-limits/periods/interrupts it declares.
58
+ 3. From that, derive one acceptance criterion per scenario, e.g.:
59
+ - "`fail_at: nil` → task prints ✅, reactor status is `success`, `execution_trace`
60
+ contains every step in dependency order, `error` is nil."
61
+ - "`fail_at: :capture_payment` → task prints ❌, reactor status is `failed`/`halted`,
62
+ steps after `capture_payment` never ran, steps before it with side effects appear in
63
+ `undo_stack` (or show a compensation trace), `error` mentions the trigger."
64
+ - For background/async reactors: expect a `DispatchResult`/`⏳` print, then (after
65
+ `drain`/wait) a terminal status reachable via the API.
66
+ - For interrupt/pause reactors: expect `be_paused`-equivalent status and
67
+ `ready_interrupts`/`current_step` matching the interrupt step.
68
+ 4. Keep this list in memory (or a scratch note) — it is what Step 4 checks results against.
69
+ If a reactor's behavior can't be inferred confidently from its source, say so explicitly
70
+ rather than guessing an expectation.
71
+
72
+ ## Step 3: Run the demos
73
+
74
+ 1. Execute the in-scope rake task(s) inside the running container:
75
+ `docker compose exec -T demo-app bin/rails demo:<task>` (or `demo:all` by default).
76
+ 2. Capture full stdout. Extract, per scenario run: reactor class, inputs used (especially
77
+ any `fail_at`/scenario discriminator), the printed outcome marker, and any
78
+ `execution_id`/`context_id`/`order_id` printed — these are the join key to the API in
79
+ Step 4. If a task doesn't print an id, look up the id via the API list endpoint
80
+ (Step 4.1) by class name + recency instead of skipping verification.
81
+ 3. For async/background scenarios, allow Sidekiq (already running as `demo-sidekiq`) time
82
+ to process, then re-check rather than asserting immediately. `demo:all` runs heavy
83
+ background tasks back-to-back (`map`'s ~30 jobs, `ar`'s ~100 product jobs, the lock/
84
+ semaphore/coordination holds) — an async/await step later in the chain (e.g.
85
+ `async_step_demo`, `async_reactor_demo`) can hit `async_wait_timeout` purely from
86
+ Sidekiq queue backlog, not a real bug. Before reporting an async timeout as a finding,
87
+ re-run that one task in isolation (`bin/rails demo:<task>`) once the queue has drained
88
+ — if it passes clean alone, it's a demo-ordering/concurrency artifact (note it under
89
+ Suggestions, e.g. "raise Sidekiq concurrency" or "reorder demo:all"), not a code bug.
90
+
91
+ ### Splitting the run across agents (avoid context overload)
92
+
93
+ For `demo:all` (or any multi-task scope), don't run Steps 2-5 for every task in one
94
+ context — the full run is ~19 tasks / 80+ scenarios and will blow past a useful context
95
+ budget long before the report is written. Instead:
96
+
97
+ 1. Do Step 1 (stack up) and a first pass of Step 2 yourself: skim
98
+ `demo_reactors.rake` and `git status`/`git log` for the reactors involved to identify
99
+ which ones are new/actively-changing right now (untracked files, recent commits touching
100
+ `app/reactors/`) — those get priority full-depth verification.
101
+ 2. Group the remaining tasks (e.g. by rake task, or a few related tasks per group) and
102
+ fan them out to parallel background agents, each self-contained: give it the exact
103
+ `docker compose exec` command, the port, which reactors/tasks it owns, the acceptance-
104
+ criteria method from Step 2, and the Step 4/5 validation + fix instructions verbatim.
105
+ Tell each agent explicitly which scenarios warrant full API depth-check (`GET .../<id>`
106
+ against every field) versus which can be validated via stdout markers plus a 1-2
107
+ scenario spot-check — exhaustively hitting the API for every scenario in a large group
108
+ is what overloads context, not running the scenarios themselves.
109
+ 3. Have each agent write its own findings; collect and merge into the single report in
110
+ Step 6 yourself (or have one agent own the merge) rather than re-deriving everything in
111
+ your own context.
112
+
113
+ ## Step 4: Validate against both data sources
114
+
115
+ For every scenario captured in Step 3, check it against **both**:
116
+
117
+ **(a) The API** (`http://localhost:<port>/ruby_reactor/api/reactors`):
118
+ - `GET /ruby_reactor/api/reactors` — list; confirm the run's reactor/context appears.
119
+ Known limitation to watch for: the storage adapter's `scan_reactors` caps at `count: 50`
120
+ via Redis `SCAN` with no chronological ordering — on a run with 50+ total executions
121
+ (e.g. `demo:all`), some earlier ones can be genuinely absent from the list endpoint even
122
+ though `GET .../<id>` finds them directly by id. Don't assume "missing from the list" ==
123
+ "reactor didn't run" — try the direct id lookup before concluding it's a bug, but if it
124
+ IS a real gap, it's a known gem-level finding (no `limit`/`cursor` param threaded through
125
+ `lib/ruby_reactor/web/api.rb` → `lib/ruby_reactor/storage/redis_adapter.rb` `scan_reactors`).
126
+ - `GET /ruby_reactor/api/reactors/<id>` — confirm `status`, `current_step`,
127
+ `execution_trace` (`steps`), `undo_stack`, `retry_count`/`step_attempts`, `error`,
128
+ `coordination`, and `structure` match the acceptance criterion from Step 2. Compare
129
+ step ordering and dependency edges against `structure`/`depends_on`, not just final
130
+ status.
131
+ - **Cross-check the rake task's own printer branches against every outcome type the
132
+ reactor can actually produce**, not just its final API status. A common bug class here:
133
+ a `run_*` helper in the rake task branches on `DispatchResult` / `success?` / `failure?`
134
+ but is missing a `halted?` (or `skipped?`) branch, so a reactor that legitimately halts
135
+ (e.g. a dedup/period-lock halt, which is `Halt < Success`) falls through to the
136
+ `success?` branch and prints a misleading `✅ SUCCESS: nil` while the API correctly shows
137
+ `status: "halted"`. Read every `run_*`/`report_demo_result` helper the in-scope tasks use
138
+ and check its branch list covers everything the reactor's `signals`/return values allow.
139
+
140
+ **(b) The dashboard** (`http://localhost:<port>/ruby_reactor`):
141
+ - Confirm the root path serves the SPA (HTTP 200, HTML with its mount point) — a broken
142
+ dashboard build is itself a finding.
143
+ - Where practical, fetch the same reactor's dashboard state the UI would render (it is
144
+ backed by the same API, so this mainly confirms the mount + asset pipeline are healthy
145
+ end-to-end, not just the API module in isolation). Note any divergence between what the
146
+ rake stdout claimed and what the API/dashboard actually recorded — that divergence is
147
+ the primary class of bug this skill hunts for.
148
+
149
+ Also check for **coverage gaps**: any reactor in `demo_app/app/reactors/` with no
150
+ corresponding rake task (nothing exercises it), and any acceptance criterion from Step 2
151
+ that neither data source could confirm (e.g. no id was ever discoverable).
152
+
153
+ ## Step 5: For every bug found, propose a fix and a regression test
154
+
155
+ A finding is not done at "here's what's wrong" — for each confirmed bug/error:
156
+
157
+ 1. **Root-cause it**: trace the mismatch to the actual code (a step's outcome, the API
158
+ serializer, storage, the dashboard build) rather than stopping at "API returned X,
159
+ expected Y". Name the file(s)/line(s) responsible.
160
+ 2. **Propose a concrete fix**: a diff-level description (or, if the fix is small and
161
+ unambiguous, apply it — see below) of what changes and why, referencing the gem's
162
+ principles (`.specify/memory/constitution.md`) where relevant (e.g. don't silently
163
+ widen behavior beyond what the demo actually needs).
164
+ 3. **Propose the regression test** that would have caught it, and place it correctly:
165
+ - **Bug in gem behavior** (executor, signals, storage, adapters, DSL, the `Web::API`
166
+ serializer, etc.) → a unit/integration spec under `spec/` (mirror the existing
167
+ layout, e.g. `spec/ruby_reactor/web/` for API bugs, `spec/ruby_reactor/executor/`
168
+ for execution-order bugs) using the gem's normal spec helpers — **not** the
169
+ `RubyReactor::RSpec` demo helpers, those are for `demo_app`.
170
+ - **Bug only reachable through the demo reactor's own definition, the rake task, or
171
+ dashboard integration** → a spec under `demo_app/spec/reactors/`, `type: :reactor`,
172
+ using only the built-in surface from `lib/ruby_reactor/rspec.rb` (see
173
+ `speckit-demo-tests` skill for the exact matcher/helper list and the "no hand-rolled
174
+ scaffolding" rule — apply the same constraint here).
175
+ - If the bug spans both (a gem defect that happens to surface via a demo reactor),
176
+ propose **both**: a gem-level spec pinning the underlying behavior, and a demo spec
177
+ confirming the demo reactor now reports it correctly.
178
+ 4. Do not apply gem-behavior fixes automatically without asking — surface them in the
179
+ report. You MAY apply a fix that is confined to `demo_app/` (a demo reactor, the rake
180
+ task, or a demo spec) directly if it's unambiguous, then note that it was applied and
181
+ re-verify it in this same run. Never silently skip proposing the regression test even
182
+ when you don't apply the fix itself.
183
+
184
+ ## Step 6: Write the report
185
+
186
+ Write findings to `demo_app/tmp/demo_verification/<UTC timestamp>-report.md` (create the
187
+ directory if needed; it's already gitignored via `demo_app/.gitignore`). Structure:
188
+
189
+ ```markdown
190
+ # Demo App E2E Verification — <timestamp>
191
+
192
+ ## Scope
193
+ <tasks/reactors run, port used, docker services status>
194
+
195
+ ## Acceptance criteria checked
196
+ <the Step 2 list, one line per scenario>
197
+
198
+ ## Results
199
+ | Reactor | Scenario | Rake output | API result | Dashboard | Verdict |
200
+ |---|---|---|---|---|---|
201
+ ...
202
+
203
+ ## Errors found
204
+ <concrete mismatches: expected vs actual, with the reactor/context id and relevant
205
+ API JSON excerpt or rake stdout excerpt, plus root cause (file/line)>
206
+
207
+ ## Suggestions / improvements
208
+ <non-broken but worth-fixing observations: missing rake coverage, confusing output,
209
+ missing undo, etc.>
210
+
211
+ ## Fix plan
212
+ <per error: ordered, concrete steps to resolve it — file(s) to touch, what changes,
213
+ whether it was already applied — followed by the regression test proposed for it
214
+ (file path + what it asserts) and whether it was added/passing>
215
+ ```
216
+
217
+ Keep the table terse; put raw JSON/log excerpts in fenced code blocks under "Errors
218
+ found", not inline in the table.
219
+
220
+ ## Step 7: Summarize to the user
221
+
222
+ Report: report file path, pass/fail counts, and the top few errors (if any) inline —
223
+ each with its proposed fix and regression test location — so the user doesn't have to
224
+ open the file to know whether anything broke or what to do about it. If everything
225
+ passed, say so plainly — don't manufacture findings. Do not tear down the docker services
226
+ unless the user asks; mention they're still running.
@@ -0,0 +1,144 @@
1
+ ---
2
+ name: "speckit-demo-tests"
3
+ description: "Generate the demo_app example reactor, rake task, and RSpec spec required by Constitution Principle VI for the current spec-kit feature, or for a context the user describes when no feature is active."
4
+ argument-hint: "Optional: what to demo/test (required if no active speckit feature is detected)"
5
+ compatibility: "Requires spec-kit project structure with .specify/ directory; writes into demo_app/"
6
+ metadata:
7
+ author: "project"
8
+ user-invocable: true
9
+ disable-model-invocation: false
10
+ ---
11
+
12
+ ## User Input
13
+
14
+ ```text
15
+ $ARGUMENTS
16
+ ```
17
+
18
+ You **MUST** consider the user input before proceeding (if not empty).
19
+
20
+ ## Purpose
21
+
22
+ Constitution Principle VI ("Demo-App Proof of Feature", `.specify/memory/constitution.md`)
23
+ requires every feature to ship three artifacts together: an example reactor in
24
+ `demo_app/app/reactors/`, a task entry in `demo_app/lib/tasks/demo_reactors.rake`, and a
25
+ spec in `demo_app/spec/reactors/` written **only** with the built-in RSpec surface from
26
+ `lib/ruby_reactor/rspec.rb`. This skill produces all three in one pass.
27
+
28
+ ## Step 1: Determine the context
29
+
30
+ Try to detect the current spec-kit feature first; only fall back to the user for context
31
+ if detection fails.
32
+
33
+ 1. Run `.specify/scripts/bash/check-prerequisites.sh --json --paths-only` from the repo
34
+ root and parse `FEATURE_DIR` and `BRANCH`.
35
+ 2. **Speckit detected** if `FEATURE_DIR` resolves to an existing directory under `specs/`
36
+ containing a `spec.md`. In that case:
37
+ - Read `spec.md` (and `plan.md`/`data-model.md` if present) in `FEATURE_DIR`.
38
+ - Extract the functional requirements (`FR-###`) and Given/When/Then acceptance
39
+ scenarios that describe observable, runnable behavior — the ones a reactor run can
40
+ demonstrate (happy path, a failure/compensation path, and any distinctive semantics
41
+ like pause/resume, skip, halt, retry).
42
+ - This extracted behavior is the context for Step 2. Do not ask the user anything
43
+ unless the spec is ambiguous about what a runnable example should exercise.
44
+ 3. **No speckit detected** (script fails, no `FEATURE_DIR`, or no `spec.md`) — the
45
+ feature being tested must come from the user:
46
+ - If `$ARGUMENTS` is non-empty, treat it as the context describing what to build a
47
+ demo/test for.
48
+ - If `$ARGUMENTS` is empty, use `AskUserQuestion` (or a plain question if that tool is
49
+ unavailable) to ask what reactor behavior/feature the demo and spec should cover.
50
+ Do not invent a feature — block here until the user answers.
51
+
52
+ ## Step 2: Study existing conventions before writing anything
53
+
54
+ Read 1-2 existing examples so the new files match house style exactly:
55
+
56
+ - One existing reactor from `demo_app/app/reactors/` whose shape is closest to the new
57
+ one (e.g. a failure/compensation demo, a background demo, an interrupt demo — pick by
58
+ relevance to the context from Step 1).
59
+ - Its matching spec in `demo_app/spec/reactors/`.
60
+ - The full built-in matcher/helper surface: skim `lib/ruby_reactor/rspec.rb` and its
61
+ `lib/ruby_reactor/rspec/*.rb` files (`helpers.rb`, `test_subject.rb`, `matchers.rb`,
62
+ `sidekiq_helpers.rb`, `async_test_helpers.rb`). Confirm the exact set currently
63
+ available — do not rely on a memorized list, the gem may have grown matchers since.
64
+ - The tail of `demo_app/lib/tasks/demo_reactors.rake`, in particular the `desc "All demo
65
+ reactors"` / `task all: [...]` entry — the new task name must be appended to that
66
+ dependency list.
67
+
68
+ ## Step 3: Generate the example reactor
69
+
70
+ Create `demo_app/app/reactors/<name>_reactor.rb` (snake_case file matching the
71
+ `CamelCase` class name), where `<name>` is derived from the context (ask the user only
72
+ if the right name is genuinely ambiguous). Requirements:
73
+
74
+ - Class-based step definitions, matching Development Workflow conventions (no inline
75
+ lambdas as the primary authoring style).
76
+ - Exercises the feature end-to-end: the happy path, and — where the feature has one — a
77
+ failure/compensation path, typically toggled via a `fail_at:`-style input like other
78
+ demo reactors already do.
79
+ - If the feature under test is signal-specific (Halt, Skipped, retry veto, pause/resume,
80
+ locks, rate limits, periods), the example MUST produce that outcome observably (e.g. a
81
+ step that calls `halt!`/`skip!`/`fail!(..., retry: false)` under a controllable input),
82
+ not just something that happens to compile.
83
+ - Do not duplicate an existing demo reactor's purpose — if one already covers this
84
+ behavior, extend it or say so instead of creating a near-duplicate file.
85
+
86
+ ## Step 4: Register the rake task
87
+
88
+ Edit `demo_app/lib/tasks/demo_reactors.rake`:
89
+
90
+ - Add a `desc "<Reactor> — <one-line of what it demonstrates>"` /
91
+ `task <name>: [:environment, :flush_redis] do ... end` block, following the existing
92
+ print-the-outcome pattern (`✅ SUCCESS`, `❌ FAILED`, `⏳ BACKGROUND`, etc. — match
93
+ whatever branch the existing tasks use for `DispatchResult`/`success?`/`failure?`).
94
+ - Run through the same scenarios the spec will assert (e.g. loop over `fail_at`
95
+ values) so `rake demo:<name>` is a working live demonstration on its own.
96
+ - Append `:<name>` to the `task all: [...]` dependency list so it runs as part of
97
+ `rake demo:all`.
98
+
99
+ ## Step 5: Write the spec using only the built-in RSpec surface
100
+
101
+ Create `demo_app/spec/reactors/<name>_reactor_spec.rb`:
102
+
103
+ - `RSpec.describe <ReactorClass>, type: :reactor do ... end`.
104
+ - Build the subject with `test_reactor(described_class, inputs)` (see
105
+ `lib/ruby_reactor/rspec/helpers.rb`).
106
+ - Use only the `TestSubject` API (`lib/ruby_reactor/rspec/test_subject.rb`) — e.g.
107
+ `mock_step`, `failing_at`, `map`, `composed`, `async_step`, `async_reactor`, `resume`,
108
+ `step_result`, `ensure_executed!`, `process_pending_jobs` — and only the matchers
109
+ defined in `lib/ruby_reactor/rspec/matchers.rb` (`be_success`, `be_failure`,
110
+ `have_run_step(...).after(...)`, `have_retried_step`, `have_validation_error`,
111
+ `be_paused`, `be_paused_at`, `have_ready_interrupts`, `be_halted`, `be_skipped`,
112
+ `be_locked`, `have_available_tokens`, `have_held_tokens`, `have_rate_limit_count`,
113
+ `be_period_marked`, and the ordered-lock matchers) plus `drain_async_jobs` /
114
+ `pending_async_jobs` from `sidekiq_helpers.rb` for async paths.
115
+ - **Forbidden**: any direct `RubyReactor::Executor`/`Storage`/Redis call, manual Sidekiq
116
+ job draining, stubbing reactor internals, or any other hand-rolled scaffolding in this
117
+ spec file.
118
+ - **If an assertion needs a matcher or helper that does not exist yet**: add it to the
119
+ appropriate file under `lib/ruby_reactor/rspec/` in this same change (with its own
120
+ unit coverage if the gem's own spec suite conventions call for it), then use it from
121
+ the demo spec. Do not work around the gap with ad-hoc internals — that is what
122
+ Principle VI explicitly forbids.
123
+ - Cover at least: the happy path (`be_success`, step ordering via `have_run_step`), and
124
+ the failure/compensation or signal-specific path identified in Step 1.
125
+
126
+ ## Step 6: Validate
127
+
128
+ Run, from `demo_app/`:
129
+
130
+ - `bundle exec rspec spec/reactors/<name>_reactor_spec.rb` — must pass.
131
+ - `bundle exec rake demo:<name>` — must run cleanly and print observable outcomes.
132
+ - `bundle exec rubocop app/reactors/<name>_reactor.rb lib/tasks/demo_reactors.rake spec/reactors/<name>_reactor_spec.rb` (from the repo root, or wherever the project's rubocop config resolves) — fix violations before finishing.
133
+
134
+ If any command fails, fix the generated files and re-run — do not report success on
135
+ files that don't pass.
136
+
137
+ ## Step 7: Summarize
138
+
139
+ Report to the user:
140
+
141
+ - Files created/edited (reactor, rake task block + `all` list update, spec).
142
+ - How to run the demo (`rake demo:<name>`) and the spec.
143
+ - Whether any new matcher/helper was added to `lib/ruby_reactor/rspec/` and why.
144
+ - Confirmation that validation (Step 6) passed.
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.7.0"
2
+ ".": "0.8.0"
3
3
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "feature_directory": "specs/001-background-async-steps"
2
+ "feature_directory": "specs/004-inheritable-step-class"
3
3
  }
@@ -1,20 +1,27 @@
1
1
  <!--
2
2
  SYNC IMPACT REPORT
3
3
  ==================
4
- Version change: [unset] → 1.0.0 (MINOR: initial constitution, all principles defined from scratch)
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)
5
8
 
6
- Modified principles: N/A (first ratification)
9
+ Modified principles: none (Development Workflow section, not a Core Principle)
7
10
 
8
- Added sections:
9
- - Core Principles (5 principles)
10
- - Technical Constraints
11
- - Development Workflow
12
- - Governance
11
+ Added sections: none
12
+
13
+ Removed sections: none
13
14
 
14
15
  Templates checked:
15
- - .specify/templates/plan-template.md ✅ Constitution Check gate present, no update needed
16
- - .specify/templates/spec-template.md Aligned with Saga/library constraints
17
- - .specify/templates/tasks-template.md Task phases align with Red-Green workflow
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
+ - .specify/templates/checklist-template.md ✅ Generic — no edit required
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
18
25
 
19
26
  Deferred TODOs: none
20
27
  -->
@@ -89,6 +96,58 @@ surprise breaking change in a MINOR bump costs downstream teams debugging time
89
96
  they did not budget for. Simplicity keeps the library auditable and the
90
97
  upgrade path predictable.
91
98
 
99
+ ### VI. Demo-App Proof of Feature (NON-NEGOTIABLE)
100
+
101
+ Every user-facing feature or public API change MUST ship with a runnable example
102
+ in `demo_app/`. Three artifacts are required together — a change is incomplete if
103
+ any one is missing:
104
+
105
+ 1. **Example reactor**: a reactor (or step) demonstrating the feature MUST live in
106
+ `demo_app/app/reactors/`, one file per reactor, named `<snake_case>_reactor.rb`
107
+ matching its class name. The example MUST exercise the feature end to end —
108
+ including its failure and compensation path where the feature has one — and MUST
109
+ use class-based step definitions per the Development Workflow rule.
110
+ 2. **Rake entry**: the example MUST be registered as a task in
111
+ `demo_app/lib/tasks/demo_reactors.rake` under the `demo:` namespace, with a `desc`
112
+ line describing what it demonstrates, and depending on `[:environment, :flush_redis]`
113
+ so each run starts from clean Redis state. The task MUST print observable outcomes
114
+ (success, failure, background dispatch, pause) so an operator can verify behavior
115
+ without a debugger.
116
+ 3. **Spec**: a matching spec MUST live at
117
+ `demo_app/spec/reactors/<reactor>_spec.rb`, declared `type: :reactor`, and MUST use
118
+ **only** the built-in test surface exported by `lib/ruby_reactor/rspec.rb` — the
119
+ helpers (`test_reactor`, `drain_async_jobs`), the `TestSubject` API
120
+ (`mock_step`, `failing_at`, `map`, `composed`, `async_step`, `resume`,
121
+ `step_result`, `ensure_executed!`), and the matchers (`be_success`, `be_failure`,
122
+ `have_run_step(...).after(...)`, `have_retried_step`, `have_validation_error`,
123
+ `be_paused`, `be_paused_at`, `have_ready_interrupts`, `be_halted`, `be_skipped`,
124
+ `be_locked`, `have_available_tokens`, `have_held_tokens`, `have_rate_limit_count`,
125
+ `be_period_marked`, and the ordered-lock matchers).
126
+
127
+ Hand-rolled test scaffolding is forbidden in `demo_app/spec/reactors/`: no direct
128
+ `RubyReactor::Executor`/`Storage` calls, no bespoke Sidekiq draining, no manual Redis
129
+ assertions, no stubbing of reactor internals. If an assertion cannot be expressed with
130
+ the built-in surface, the missing matcher or helper MUST be added to
131
+ `lib/ruby_reactor/rspec/` in the same change — extending the shared test API, not
132
+ bypassing it.
133
+
134
+ 4. **Docker acceptance run**: `docker-compose.yml` MUST stay current with `demo_app`'s
135
+ runtime dependencies (Redis, Sidekiq, the Rails service itself) so that
136
+ `docker compose run --rm demo-app bin/rails demo:<task>` runs the new rake task
137
+ end to end against real Redis, with no manual setup beyond `docker compose up`.
138
+ A new demo service or environment variable required by a feature MUST be added to
139
+ `docker-compose.yml` in the same change. The `demo:` rake tasks in
140
+ `demo_app/lib/tasks/demo_reactors.rake` constitute the project's acceptance test
141
+ suite for user-facing behavior — CI or a release checklist MAY invoke them via
142
+ `docker compose run` to confirm the demo still passes before a MINOR/MAJOR release.
143
+
144
+ **Rationale**: `demo_app/` is the only place the gem is consumed the way users consume
145
+ it. An example that is written but never listed is never run; a spec written with
146
+ private internals passes while the public API is broken. Forcing every feature through
147
+ the public reactor DSL, a runnable rake task, and the shipped matchers means the
148
+ documented API, the demo, and the test surface are validated by the same change — and
149
+ gaps in the matcher library surface as work instead of as workarounds.
150
+
92
151
  ## Technical Constraints
93
152
 
94
153
  - **Ruby**: >= 3.0.0 required. No polyfills for older Rubies.
@@ -107,13 +166,31 @@ upgrade path predictable.
107
166
  - Feature branches target `main`. PRs MUST pass CI (RSpec + RuboCop) before merge.
108
167
  - Releases are managed by release-please. Version bump lives in
109
168
  `lib/ruby_reactor/version.rb`. Do not manually edit the version in gemspec.
110
- - New features MUST update `README.md` documentation and add entries to
111
- `CHANGELOG.md` under the correct semantic heading (`Features`, `Bug Fixes`,
112
- `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.
113
180
  - Class-based step definitions are the preferred authoring style (not inline
114
181
  lambdas). Documentation and examples MUST reflect this.
115
182
  - The `demo_app/` directory serves as a living integration example. Changes to
116
- public API surface MUST be reflected there.
183
+ public API surface MUST be reflected there per Principle VI — example reactor in
184
+ `demo_app/app/reactors/`, rake task in `demo_app/lib/tasks/demo_reactors.rake`,
185
+ and a spec in `demo_app/spec/reactors/` using only the built-in RSpec matchers
186
+ and helpers from `lib/ruby_reactor/rspec.rb`.
187
+ - PR reviews MUST reject any feature change whose demo example is missing, unlisted
188
+ in the rake file, or tested with hand-rolled scaffolding instead of the shipped
189
+ matcher library.
190
+ - `docker-compose.yml` MUST be kept current with `demo_app`'s services (Redis,
191
+ Sidekiq, Rails) so `docker compose up` and `docker compose run --rm demo-app
192
+ bin/rails demo:<task>` are the supported way to run the `demo:` rake tasks as
193
+ acceptance tests, with no host-side Ruby/Redis setup required.
117
194
 
118
195
  ## Governance
119
196
 
@@ -131,4 +208,4 @@ justified in the `Complexity Tracking` table of the plan.
131
208
  Compliance review: at each MINOR or MAJOR gem release, confirm this constitution
132
209
  still accurately reflects the codebase and update as needed.
133
210
 
134
- **Version**: 1.0.0 | **Ratified**: 2025-10-02 | **Last Amended**: 2026-06-24
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,13 +151,20 @@ 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/
158
158
  - [ ] TXXX Security hardening
159
159
  - [ ] TXXX Run quickstart.md validation
160
160
 
161
+ **Constitution Principle VI — Demo-App Proof of Feature (required for any public API change):**
162
+
163
+ - [ ] TXXX [P] Example reactor in `demo_app/app/reactors/<name>_reactor.rb`
164
+ - [ ] TXXX Register `demo:<name>` task in `demo_app/lib/tasks/demo_reactors.rake` (desc + `[:environment, :flush_redis]`)
165
+ - [ ] TXXX Spec in `demo_app/spec/reactors/<name>_reactor_spec.rb` (`type: :reactor`), using only built-in matchers/helpers from `lib/ruby_reactor/rspec.rb`
166
+ - [ ] TXXX If an assertion needs a matcher that does not exist, add it to `lib/ruby_reactor/rspec/` — never bypass the shared test API
167
+
161
168
  ---
162
169
 
163
170
  ## Dependencies & Execution Order