ruby_reactor 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/skills/demo-app-e2e-verify/SKILL.md +226 -0
- data/.claude/skills/speckit-demo-tests/SKILL.md +144 -0
- data/.release-please-manifest.json +1 -1
- data/.specify/feature.json +1 -1
- data/.specify/memory/constitution.md +79 -12
- data/.specify/templates/tasks-template.md +7 -0
- data/CHANGELOG.md +125 -1
- data/CLAUDE.md +5 -0
- data/README.md +155 -25
- data/lib/ruby_reactor/adapters/active_job/router.rb +21 -6
- data/lib/ruby_reactor/adapters/active_job/step_worker.rb +20 -0
- data/lib/ruby_reactor/adapters/active_job/worker.rb +6 -1
- data/lib/ruby_reactor/adapters/sidekiq/router.rb +21 -6
- data/lib/ruby_reactor/adapters/sidekiq/step_worker.rb +17 -0
- data/lib/ruby_reactor/adapters/sidekiq/worker.rb +5 -2
- data/lib/ruby_reactor/async_waiter.rb +113 -0
- data/lib/ruby_reactor/configuration.rb +30 -1
- data/lib/ruby_reactor/context.rb +28 -0
- data/lib/ruby_reactor/context_serializer.rb +15 -0
- data/lib/ruby_reactor/dsl/async_macros.rb +176 -0
- data/lib/ruby_reactor/dsl/async_reactor_builder.rb +66 -0
- data/lib/ruby_reactor/dsl/compose_builder.rb +23 -4
- data/lib/ruby_reactor/dsl/interrupt_builder.rb +3 -2
- data/lib/ruby_reactor/dsl/interrupt_step_config.rb +7 -1
- data/lib/ruby_reactor/dsl/lockable.rb +3 -3
- data/lib/ruby_reactor/dsl/map_builder.rb +1 -2
- data/lib/ruby_reactor/dsl/reactor.rb +29 -10
- data/lib/ruby_reactor/dsl/step_builder.rb +30 -11
- data/lib/ruby_reactor/dsl/template_helpers.rb +11 -3
- data/lib/ruby_reactor/error/async_result_pending.rb +21 -0
- data/lib/ruby_reactor/error/async_wait_timeout_error.rb +10 -0
- data/lib/ruby_reactor/error/deprecated_dsl_error.rb +11 -0
- data/lib/ruby_reactor/executor/async_step_dispatch.rb +110 -0
- data/lib/ruby_reactor/executor/compensation_manager.rb +58 -45
- data/lib/ruby_reactor/executor/ordered_lock_support.rb +11 -11
- data/lib/ruby_reactor/executor/result_handler.rb +47 -11
- data/lib/ruby_reactor/executor/retry_manager.rb +13 -6
- data/lib/ruby_reactor/executor/step_executor.rb +77 -24
- data/lib/ruby_reactor/executor.rb +179 -23
- data/lib/ruby_reactor/lock.rb +19 -0
- data/lib/ruby_reactor/map/element_executor.rb +13 -2
- data/lib/ruby_reactor/map/helpers.rb +10 -8
- data/lib/ruby_reactor/map/result_enumerator.rb +7 -1
- data/lib/ruby_reactor/map/result_summary.rb +63 -0
- data/lib/ruby_reactor/map/sweeper.rb +1 -1
- data/lib/ruby_reactor/open_telemetry.rb +8 -5
- data/lib/ruby_reactor/ordered_lock.rb +3 -3
- data/lib/ruby_reactor/reactor.rb +25 -2
- data/lib/ruby_reactor/rspec/matchers.rb +61 -11
- data/lib/ruby_reactor/rspec/sidekiq_helpers.rb +2 -1
- data/lib/ruby_reactor/rspec/step_executor_patch.rb +2 -2
- data/lib/ruby_reactor/rspec/test_subject.rb +74 -19
- data/lib/ruby_reactor/semaphore.rb +10 -0
- data/lib/ruby_reactor/step/async_reactor_step.rb +207 -0
- data/lib/ruby_reactor/step/compose_step.rb +1 -1
- data/lib/ruby_reactor/step/map_step.rb +6 -2
- data/lib/ruby_reactor/step.rb +10 -4
- data/lib/ruby_reactor/step_signals.rb +33 -0
- data/lib/ruby_reactor/step_sweeper.rb +72 -0
- data/lib/ruby_reactor/step_worker.rb +260 -0
- data/lib/ruby_reactor/storage/adapter.rb +26 -1
- data/lib/ruby_reactor/storage/redis_adapter.rb +4 -71
- data/lib/ruby_reactor/storage/redis_locking.rb +7 -0
- data/lib/ruby_reactor/storage/redis_pub_sub.rb +31 -0
- data/lib/ruby_reactor/storage/redis_reactor_scan.rb +116 -0
- data/lib/ruby_reactor/storage/redis_step_results.rb +49 -0
- data/lib/ruby_reactor/sweeper.rb +7 -1
- data/lib/ruby_reactor/sweeper_job.rb +1 -0
- data/lib/ruby_reactor/template/result.rb +151 -5
- data/lib/ruby_reactor/version.rb +1 -1
- data/lib/ruby_reactor/web/api.rb +117 -22
- data/lib/ruby_reactor/web/public/assets/index-BQvIWPdx.css +1 -0
- data/lib/ruby_reactor/web/public/assets/index-Dw4KV4QY.js +22 -0
- data/lib/ruby_reactor/web/public/index.html +2 -2
- data/lib/ruby_reactor/worker.rb +53 -3
- data/lib/ruby_reactor.rb +80 -10
- data/specs/active_job.md +1 -1
- metadata +23 -3
- data/lib/ruby_reactor/web/public/assets/index-CCnNVQy5.css +0 -1
- data/lib/ruby_reactor/web/public/assets/index-D7IBZvos.js +0 -21
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: abde4c2dc313b2e5d4a74873f1fc337743e6847e8f47fcbd121fd1a72be1d557
|
|
4
|
+
data.tar.gz: defd25588b78022c339ffd105d4e304d80e676498272c887cafecb60f100dc7e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 595c701f476086c166d0e157c062e0bea0e6fb243b1c5466017bf488461b987b44c70b56e081770c586a1d83519311c58da9769ae83e302fb7d17d1cdc8f37fb
|
|
7
|
+
data.tar.gz: 7dadad987df2ec6d9c1b44e93ed328f07d77660310726dceeb1ee8957cc9e63b73bd3c2338792280bfc780a95650bcac12b384af3d2aad83b3309aa4896f6d0d
|
|
@@ -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.
|
data/.specify/feature.json
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
SYNC IMPACT REPORT
|
|
3
3
|
==================
|
|
4
|
-
Version change:
|
|
4
|
+
Version change: 1.1.0 → 1.2.0 (MINOR: Principle VI expanded with a 4th requirement —
|
|
5
|
+
docker-compose.yml currency + docker-run acceptance tests)
|
|
5
6
|
|
|
6
|
-
Modified principles:
|
|
7
|
+
Modified principles:
|
|
8
|
+
- Principle VI: Demo-App Proof of Feature — added requirement 4, "Docker acceptance
|
|
9
|
+
run" (docker-compose.yml MUST track demo_app's services; demo: rake tasks are the
|
|
10
|
+
project's acceptance suite, runnable via `docker compose run`)
|
|
7
11
|
|
|
8
|
-
Added sections:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- Development Workflow
|
|
12
|
-
- Governance
|
|
12
|
+
Added sections: none (existing Principle VI section extended)
|
|
13
|
+
|
|
14
|
+
Removed sections: none
|
|
13
15
|
|
|
14
16
|
Templates checked:
|
|
15
|
-
- .specify/templates/plan-template.md ✅ Constitution Check gate
|
|
16
|
-
|
|
17
|
-
- .specify/templates/
|
|
17
|
+
- .specify/templates/plan-template.md ✅ Constitution Check gate is generic ("Gates determined
|
|
18
|
+
based on constitution file") — no edit required
|
|
19
|
+
- .specify/templates/spec-template.md ✅ No principle-specific content — no edit required
|
|
20
|
+
- .specify/templates/tasks-template.md ✅ Already covers demo-app polish tasks — no edit required
|
|
21
|
+
- .specify/templates/checklist-template.md ✅ Generic — no edit required
|
|
22
|
+
- .specify/extensions.yml ✅ No before/after_constitution hooks registered
|
|
18
23
|
|
|
19
24
|
Deferred TODOs: none
|
|
20
25
|
-->
|
|
@@ -89,6 +94,58 @@ surprise breaking change in a MINOR bump costs downstream teams debugging time
|
|
|
89
94
|
they did not budget for. Simplicity keeps the library auditable and the
|
|
90
95
|
upgrade path predictable.
|
|
91
96
|
|
|
97
|
+
### VI. Demo-App Proof of Feature (NON-NEGOTIABLE)
|
|
98
|
+
|
|
99
|
+
Every user-facing feature or public API change MUST ship with a runnable example
|
|
100
|
+
in `demo_app/`. Three artifacts are required together — a change is incomplete if
|
|
101
|
+
any one is missing:
|
|
102
|
+
|
|
103
|
+
1. **Example reactor**: a reactor (or step) demonstrating the feature MUST live in
|
|
104
|
+
`demo_app/app/reactors/`, one file per reactor, named `<snake_case>_reactor.rb`
|
|
105
|
+
matching its class name. The example MUST exercise the feature end to end —
|
|
106
|
+
including its failure and compensation path where the feature has one — and MUST
|
|
107
|
+
use class-based step definitions per the Development Workflow rule.
|
|
108
|
+
2. **Rake entry**: the example MUST be registered as a task in
|
|
109
|
+
`demo_app/lib/tasks/demo_reactors.rake` under the `demo:` namespace, with a `desc`
|
|
110
|
+
line describing what it demonstrates, and depending on `[:environment, :flush_redis]`
|
|
111
|
+
so each run starts from clean Redis state. The task MUST print observable outcomes
|
|
112
|
+
(success, failure, background dispatch, pause) so an operator can verify behavior
|
|
113
|
+
without a debugger.
|
|
114
|
+
3. **Spec**: a matching spec MUST live at
|
|
115
|
+
`demo_app/spec/reactors/<reactor>_spec.rb`, declared `type: :reactor`, and MUST use
|
|
116
|
+
**only** the built-in test surface exported by `lib/ruby_reactor/rspec.rb` — the
|
|
117
|
+
helpers (`test_reactor`, `drain_async_jobs`), the `TestSubject` API
|
|
118
|
+
(`mock_step`, `failing_at`, `map`, `composed`, `async_step`, `resume`,
|
|
119
|
+
`step_result`, `ensure_executed!`), and the matchers (`be_success`, `be_failure`,
|
|
120
|
+
`have_run_step(...).after(...)`, `have_retried_step`, `have_validation_error`,
|
|
121
|
+
`be_paused`, `be_paused_at`, `have_ready_interrupts`, `be_halted`, `be_skipped`,
|
|
122
|
+
`be_locked`, `have_available_tokens`, `have_held_tokens`, `have_rate_limit_count`,
|
|
123
|
+
`be_period_marked`, and the ordered-lock matchers).
|
|
124
|
+
|
|
125
|
+
Hand-rolled test scaffolding is forbidden in `demo_app/spec/reactors/`: no direct
|
|
126
|
+
`RubyReactor::Executor`/`Storage` calls, no bespoke Sidekiq draining, no manual Redis
|
|
127
|
+
assertions, no stubbing of reactor internals. If an assertion cannot be expressed with
|
|
128
|
+
the built-in surface, the missing matcher or helper MUST be added to
|
|
129
|
+
`lib/ruby_reactor/rspec/` in the same change — extending the shared test API, not
|
|
130
|
+
bypassing it.
|
|
131
|
+
|
|
132
|
+
4. **Docker acceptance run**: `docker-compose.yml` MUST stay current with `demo_app`'s
|
|
133
|
+
runtime dependencies (Redis, Sidekiq, the Rails service itself) so that
|
|
134
|
+
`docker compose run --rm demo-app bin/rails demo:<task>` runs the new rake task
|
|
135
|
+
end to end against real Redis, with no manual setup beyond `docker compose up`.
|
|
136
|
+
A new demo service or environment variable required by a feature MUST be added to
|
|
137
|
+
`docker-compose.yml` in the same change. The `demo:` rake tasks in
|
|
138
|
+
`demo_app/lib/tasks/demo_reactors.rake` constitute the project's acceptance test
|
|
139
|
+
suite for user-facing behavior — CI or a release checklist MAY invoke them via
|
|
140
|
+
`docker compose run` to confirm the demo still passes before a MINOR/MAJOR release.
|
|
141
|
+
|
|
142
|
+
**Rationale**: `demo_app/` is the only place the gem is consumed the way users consume
|
|
143
|
+
it. An example that is written but never listed is never run; a spec written with
|
|
144
|
+
private internals passes while the public API is broken. Forcing every feature through
|
|
145
|
+
the public reactor DSL, a runnable rake task, and the shipped matchers means the
|
|
146
|
+
documented API, the demo, and the test surface are validated by the same change — and
|
|
147
|
+
gaps in the matcher library surface as work instead of as workarounds.
|
|
148
|
+
|
|
92
149
|
## Technical Constraints
|
|
93
150
|
|
|
94
151
|
- **Ruby**: >= 3.0.0 required. No polyfills for older Rubies.
|
|
@@ -113,7 +170,17 @@ upgrade path predictable.
|
|
|
113
170
|
- Class-based step definitions are the preferred authoring style (not inline
|
|
114
171
|
lambdas). Documentation and examples MUST reflect this.
|
|
115
172
|
- The `demo_app/` directory serves as a living integration example. Changes to
|
|
116
|
-
public API surface MUST be reflected there
|
|
173
|
+
public API surface MUST be reflected there per Principle VI — example reactor in
|
|
174
|
+
`demo_app/app/reactors/`, rake task in `demo_app/lib/tasks/demo_reactors.rake`,
|
|
175
|
+
and a spec in `demo_app/spec/reactors/` using only the built-in RSpec matchers
|
|
176
|
+
and helpers from `lib/ruby_reactor/rspec.rb`.
|
|
177
|
+
- PR reviews MUST reject any feature change whose demo example is missing, unlisted
|
|
178
|
+
in the rake file, or tested with hand-rolled scaffolding instead of the shipped
|
|
179
|
+
matcher library.
|
|
180
|
+
- `docker-compose.yml` MUST be kept current with `demo_app`'s services (Redis,
|
|
181
|
+
Sidekiq, Rails) so `docker compose up` and `docker compose run --rm demo-app
|
|
182
|
+
bin/rails demo:<task>` are the supported way to run the `demo:` rake tasks as
|
|
183
|
+
acceptance tests, with no host-side Ruby/Redis setup required.
|
|
117
184
|
|
|
118
185
|
## Governance
|
|
119
186
|
|
|
@@ -131,4 +198,4 @@ justified in the `Complexity Tracking` table of the plan.
|
|
|
131
198
|
Compliance review: at each MINOR or MAJOR gem release, confirm this constitution
|
|
132
199
|
still accurately reflects the codebase and update as needed.
|
|
133
200
|
|
|
134
|
-
**Version**: 1.
|
|
201
|
+
**Version**: 1.2.0 | **Ratified**: 2025-10-02 | **Last Amended**: 2026-09-09
|
|
@@ -158,6 +158,13 @@ Examples of foundational tasks (adjust based on your project):
|
|
|
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
|