@eventmodelers/cli 1.0.11 → 1.0.13

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.
package/README.md CHANGED
@@ -312,13 +312,16 @@ npx @eventmodelers/cli listen --port 4000 # same, on a different port
312
312
  ```bash
313
313
  npx @eventmodelers/cli re-init # refresh the installed build kit (.build-kit/) + its skills
314
314
  npx @eventmodelers/cli re-init --modeling # refresh the modeling kit (.agent-modeling-kit/) + its skills
315
+ npx @eventmodelers/cli re-init --stack supabase # override which stack to refresh from (manifest missing/stale, or switching stacks)
315
316
  ```
316
317
 
317
318
  `re-init` re-runs `init` against whichever stack `install-manifest.json` says was installed (no need to pass `--stack` again), but skips step 2 of `init` entirely — the root project scaffold (`package.json`, `src/`, `server.ts`, `docker-compose.yml`, etc.) and the root `CLAUDE.md` router are never touched. Use it after upgrading the CLI to pick up fixes to `ralph.js`/`ralph.sh`/skills without re-scaffolding a project you've since built on top of.
318
319
 
320
+ Pass `--stack <name>` to override the stack instead of relying on the manifest — useful if the manifest is missing/stale, or you want to point a `.build-kit` install at a different built-in stack's templates. It's mutually exclusive with `--modeling`.
321
+
319
322
  Credentials are left alone unless you pass `--force` — same rule `init` already follows when everything required is already configured. `--global` defaults to however skills were originally installed; pass it explicitly to move them.
320
323
 
321
- If the kit dir predates install-manifest.json tracking, or was installed via `init --git <url>` (a community/custom stack, not one of the built-in `STACKS` keys), `re-init` can't tell what to re-copy and tells you to re-run the original `init` command by hand instead.
324
+ If the kit dir predates install-manifest.json tracking, or was installed via `init --git <url>` (a community/custom stack, not one of the built-in `STACKS` keys), and you don't pass `--stack` yourself, `re-init` can't tell what to re-copy and tells you to re-run the original `init` command by hand instead.
322
325
 
323
326
  ### Uninstall
324
327
 
package/cli.js CHANGED
@@ -1635,12 +1635,23 @@ credentialFlags(program
1635
1635
  .command('re-init')
1636
1636
  .description('Refresh an already-installed kit from the current CLI version — re-copies skills and the kit dir (.build-kit or .agent-modeling-kit) so you pick up script/skill updates after upgrading. Unlike `init`, never touches the project root scaffold or the root CLAUDE.md router, and leaves existing credentials alone unless --force is passed.')
1637
1637
  .option('--modeling', 'Refresh the modeling kit (.agent-modeling-kit) instead of a build kit')
1638
+ .option('--stack <name>', `Override which stack to refresh from (${Object.keys(REINITIABLE_STACKS).join(', ')}) instead of the one recorded in install-manifest.json — use this when the manifest is missing/stale, or to switch a .build-kit install to a different stack`)
1638
1639
  .option('--global', 'Re-install skills into ~/.claude/skills/ instead of the project — defaults to however they were originally installed')
1639
1640
  .option('-f, --force', 'Re-prompt for credentials even if a config already has everything required — overwrites the existing config.json'))
1640
1641
  .action(async (opts, command) => {
1641
1642
  const globalOpts = command.optsWithGlobals();
1642
1643
  const targetDir = process.cwd();
1643
1644
 
1645
+ if (opts.modeling && opts.stack) {
1646
+ console.error('❌ --modeling and --stack are mutually exclusive — pick one.');
1647
+ process.exit(1);
1648
+ }
1649
+
1650
+ if (opts.stack && !REINITIABLE_STACKS[opts.stack]) {
1651
+ console.error(`❌ Unknown stack "${opts.stack}". Available: ${Object.keys(REINITIABLE_STACKS).join(', ')}`);
1652
+ process.exit(1);
1653
+ }
1654
+
1644
1655
  const kitDirName = opts.modeling ? MODELING_KIT.kitDirName : STACKS.node.kitDirName;
1645
1656
  const kitDir = join(targetDir, kitDirName);
1646
1657
 
@@ -1650,12 +1661,12 @@ credentialFlags(program
1650
1661
  }
1651
1662
 
1652
1663
  const manifest = readJsonSafe(join(kitDir, '.eventmodelers', 'install-manifest.json'));
1653
- const stackKey = opts.modeling ? MODELING_KIT.key : manifest.stack;
1664
+ const stackKey = opts.modeling ? MODELING_KIT.key : (opts.stack || manifest.stack);
1654
1665
  const stackCfg = stackKey ? REINITIABLE_STACKS[stackKey] : null;
1655
1666
 
1656
1667
  if (!stackCfg) {
1657
1668
  console.error(`❌ Can't tell which stack ${relative(targetDir, kitDir)} was installed from (${manifest.stack ? `"${manifest.stack}" isn't one re-init recognizes — likely a --git community stack` : 'its install manifest predates this tracking, or is missing'}).`);
1658
- console.error(' Re-run the original `init --git <url> --stack <name>` command by hand instead.');
1669
+ console.error(' Pass --stack <name> explicitly, or re-run the original `init --git <url> --stack <name>` command by hand instead.');
1659
1670
  process.exit(1);
1660
1671
  }
1661
1672
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "Eventmodelers CLI — real-time Claude agent + skills for Claude Code, for any stack (Node, Supabase, Axon, Cratis, or modeling-only)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -55,6 +55,9 @@ If the Event Modeling artifact includes slice details with `## Scenarios (GWTs)`
55
55
  test cases. GWT format for automations: `Given (events) → Then (command | NOTHING)`. Events in
56
56
  Given include read-model-building events first, trigger event last.
57
57
 
58
+ `slice.json` may also carry an optional `storylines[]` array — see the "Storyline-Derived Tests"
59
+ section under Step 5 for how a trigger-event beat in one of these can add a supplementary test.
60
+
58
61
  If the slice details contain `## Implementation Guidelines`, **follow them**.
59
62
 
60
63
  ### Stateless vs With Read Model Decision
@@ -387,6 +390,25 @@ class {AutomationName}ProcessorTest {
387
390
  | Command in Then | `verify(commandDispatcher).send(eq(expectedCommand), any())` |
388
391
  | NOTHING in Then | `verifyNoInteractions(commandDispatcher)` |
389
392
 
393
+ ### Storyline-Derived Tests (Optional)
394
+
395
+ `slice.json` may also carry a `storylines[]` array — narrated walkthroughs with an ordered
396
+ `elements[]` "beats" sequence (EVENT/COMMAND/READMODEL/...). This is a secondary, supplementary
397
+ source; `specifications[]` above stays the primary and default source of test cases. Most slices
398
+ have no `storylines[]` — skip silently when there's nothing relevant.
399
+
400
+ A storyline embedded in this slice's slice.json already belongs entirely to this slice — no need
401
+ to match beats against `events[]`/`commands[]` by id/title. Find a beat whose `type` is `EVENT`
402
+ immediately followed by a `COMMAND` beat. That pair is a ready-made test: `given` = the cumulative
403
+ preceding `EVENT` beats (setup events) through the trigger beat, `then` =
404
+ `verify(commandDispatcher).send(eq(...), any())` built from the command beat's fields — same shape
405
+ as the "Mapping GWT Scenarios to Tests" row above, just sourced from the storyline instead of
406
+ `specifications[]`.
407
+
408
+ If the beat following the trigger event isn't a COMMAND this automation dispatches (e.g. it's a
409
+ READMODEL or SCREEN beat), don't force a test — leave it undocumented rather
410
+ than fabricating an assertion.
411
+
390
412
  ## References
391
413
 
392
414
  - [Stateless Automation Example](references/automation-test-example.md) — Complete Java test example
@@ -403,5 +425,6 @@ Before marking this slice as `Done`, verify the implementation against slice.jso
403
425
  - [ ] The command dispatched matches the target command defined in slice.json
404
426
  - [ ] All fields mapped from trigger event to command come from the event fields defined in slice.json — no invented mappings
405
427
  - [ ] Every GWT scenario in `specifications[]` maps to a test case in the test class
428
+ - [ ] If `storylines[]` is present: every trigger-EVENT→target-COMMAND beat pair for this automation has a storyline test — or was deliberately skipped as untraceable
406
429
  - [ ] No filtering conditions were invented — all conditions come from slice.json `description` or `comments`
407
430
  - [ ] No field names were assumed or guessed — if a field is not in slice.json, it is not in the code
@@ -30,6 +30,8 @@ Read `.build-kit/.slices/{context}/{slicename}/slice.json`. Extract, and use **o
30
30
  - `specifications[]` (GWT scenarios) → one test method per scenario
31
31
  - Which command field(s) have `idAttribute: true` — these carry `@TargetEntityId` (see Step 1) and the
32
32
  matching event field(s) carry `@EventTag`
33
+ - `storylines[]` (optional, may be absent) → narrated walkthroughs with ordered `elements[]` "beats";
34
+ see Step 7b for how a COMMAND beat in one of these can add a supplementary test
33
35
 
34
36
  Never invent a field, business rule, or event that isn't in slice.json.
35
37
 
@@ -384,6 +386,36 @@ Fluent API cheat sheet and known gotchas (wrong `EventCriteria`/`Tag` package, a
384
386
  was renamed between AF5 patch versions, `Customization.disableAxonServer()` not existing in 5.1.1):
385
387
  see [references/axon-test-fixture-patterns.md](references/axon-test-fixture-patterns.md).
386
388
 
389
+ ## Step 7b: Storyline-Derived Tests (Optional)
390
+
391
+ `slice.json` may carry a `storylines[]` array alongside `specifications[]` — narrated walkthroughs
392
+ where an ordered sequence of `elements[]` "beats" (EVENT/COMMAND/READMODEL/...) shows one use case
393
+ end to end. This is a secondary, supplementary source — `specifications[]` (Step 7) stays the
394
+ primary and default source of test methods. Most slices have no `storylines[]`; skip this step
395
+ silently when there's nothing relevant.
396
+
397
+ A storyline embedded in this slice's slice.json already belongs entirely to this slice — no need
398
+ to match beats against `commands[]` by id/title. Find beats whose `type` is `COMMAND`. For each such
399
+ beat, the storyline gives you a ready-made `AxonTestFixture` test: `given` =
400
+ the cumulative ordered `EVENT` beats preceding it in the storyline, `when` = the command built from
401
+ the beat's `fields`, `then` = the `EVENT` beat(s) immediately following it in the storyline.
402
+
403
+ ```java
404
+ @Test
405
+ @DisplayName("Storyline: {storyline.title} — {commandBeat.title}")
406
+ void storylineBeat() {
407
+ fixture.given().event(new {PrecedingEventName}(/* fields from earlier beats */))
408
+ .when().command(new {SliceName}Command(/* fields from the command beat */))
409
+ .then().success()
410
+ .events(new {EventName}(/* fields from the following event beat */));
411
+ }
412
+ ```
413
+
414
+ Do **not** try to also assert read-model state in this same test — that half (the following
415
+ EVENT→READMODEL beats) belongs to `build-state-view`'s own storyline step, since this fixture never
416
+ touches a projector. If the beat immediately after the command isn't an EVENT, don't force a test —
417
+ leave it undocumented rather than fabricating an assertion.
418
+
387
419
  ## Final Verification
388
420
 
389
421
  Before considering the slice done:
@@ -391,6 +423,7 @@ Before considering the slice done:
391
423
  - [ ] Every field in slice.json's `commands[]` is in the Command record — no invented fields, none missing
392
424
  - [ ] Every field in slice.json's `events[]` is in the Event record — no invented fields, none missing
393
425
  - [ ] Every `specifications[]` scenario has a corresponding test method
426
+ - [ ] If `storylines[]` is present: every COMMAND beat for this slice's command has a storyline test — or was deliberately skipped as untraceable
394
427
  - [ ] No business rule exists in the handler that isn't traceable to slice.json's `description`/`comments`
395
428
  - [ ] `mvn compile -q`, then run the slice's own tests only
396
429
  - [ ] If checks pass, commit with `feat: {Slice Name}` and set slice status to `Done`
@@ -204,6 +204,48 @@ Do not design your own test cases unless specifically instructed to do so.
204
204
  | Event in Given | call `projector.on(event)` |
205
205
  | Information in Then | `assertThat(result.items()).containsExactlyInAnyOrder(...)` |
206
206
 
207
+ ## Step 4b: Storyline-Derived Tests (Optional)
208
+
209
+ `slice.json` may also carry a `storylines[]` array — narrated walkthroughs where the *same* read
210
+ model appears as multiple ordered "beats" across one flow (see `elements[]` on each storyline).
211
+ This is a secondary, supplementary source: `specifications[]` (Step 4) remains the primary and
212
+ default source of test cases. Most slices have no `storylines[]` — skip this step silently when
213
+ there's nothing relevant.
214
+
215
+ A storyline embedded in this slice's slice.json already belongs entirely to this slice — no need
216
+ to match beats against `readmodels[]` by id/title. For each storyline, find beats whose `type` is
217
+ `READMODEL`. Two such beats **adjacent with only `EVENT` beat(s) between them** describe one clean,
218
+ isolable projection test:
219
+
220
+ - events = the cumulative ordered `EVENT` beats from the start of the storyline through the
221
+ intervening event(s)
222
+ - expected result = the later `READMODEL` beat's `fields`/`examples`/`expectEmptyList`
223
+
224
+ Write these as ordinary `@Test` methods (Step 5's `projector.on(event)` / `projector.handle(query)`
225
+ pattern applies unchanged), but keep them in a clearly separate `@Nested` class named after the
226
+ storyline's title, so they never get confused with the exhaustive `specifications[]` suite:
227
+
228
+ ```java
229
+ @Nested
230
+ @DisplayName("Storyline: {storyline.title}")
231
+ class StorylineTests {
232
+ @Test
233
+ @DisplayName("after {EventName}, read model shows {expected state}")
234
+ void beatTransition() {
235
+ projector.on(new {EventName}(/* fields from the intervening beat(s) */));
236
+
237
+ var result = projector.handle(new Get{SliceName}(/* filter */));
238
+
239
+ assertThat(result.items()).containsExactly(/* expected shape from the later beat */);
240
+ }
241
+ }
242
+ ```
243
+
244
+ If a beat between two read-model states is a `COMMAND` rather than an `EVENT`, that half belongs
245
+ to `build-state-change` (its own command-handler test), not here — only project the `EVENT`→
246
+ `READMODEL` half. If a storyline segment involves a `SCREEN`/other untraceable beat, don't force a
247
+ test — leave it undocumented in code rather than fabricating an assertion.
248
+
207
249
  ## Step 5: Implement the Slice Test
208
250
 
209
251
  Pure unit tests — instantiate the projector directly, no Spring context needed.
@@ -278,5 +320,6 @@ Before marking this slice as `Done`, verify the implementation against slice.jso
278
320
  - [ ] Every field in the read model / query result definition in slice.json has a field in `{SliceName}Summary` — no invented fields
279
321
  - [ ] Every event type in `events[]` has an `@EventHandler` in the projector — no events missed or assumed
280
322
  - [ ] Every GWT scenario in `specifications[]` maps to a test case in `{SliceName}ProjectorTest`
323
+ - [ ] If `storylines[]` is present: every adjacent READMODEL↔READMODEL beat pair for this slice's read model (with only EVENT beats between) has a `@Nested` storyline test — or was deliberately skipped as untraceable
281
324
  - [ ] No extra query parameters or filter logic were added beyond what slice.json defines
282
325
  - [ ] No field names were assumed or guessed — if a field is not in slice.json, it is not in the code
@@ -36,6 +36,7 @@ slice, which services are available for side effects, and how existing translati
36
36
  | **Target command** | For translation: which command to `Execute`, and how its fields map from the event |
37
37
  | **Idempotency** | The reaction must be safe to run more than once (replay/recovery) |
38
38
  | **Specifications** | Each scenario → an executable spec |
39
+ | **Storylines** (optional) | `storylines[]` — ordered walkthrough "beats"; an event-triggering-this-reactor transition is a supplementary spec source, see Step 4a |
39
40
 
40
41
  If a command field has no source in the trigger event or an injected service, do not invent it.
41
42
 
@@ -86,6 +87,26 @@ command); for automations, assert the side-effect service was invoked. Cover ide
86
87
  slice calls for it. One spec per scenario. Run `dotnet test --filter "FullyQualifiedName~<SliceName>"`.
87
88
  See [references/patterns.md](references/patterns.md).
88
89
 
90
+ ## Step 4a — Storyline-derived specs (optional)
91
+
92
+ If `storylines[]` is non-empty — a storyline embedded in this slice's slice.json already belongs
93
+ entirely to this slice, no need to match beats against `events[]`/`commands[]` by id/title — look
94
+ for an `EVENT → (COMMAND | EVENT)` transition: a trigger event beat, followed by the command it
95
+ triggers (translation) or a further event/side-effect signal (automation).
96
+
97
+ - **Establish** — append the trigger event (and any preceding events in the storyline needed to
98
+ reach it).
99
+ - **Because** — invoke/replay the reactor.
100
+ - **should_\*** — for a translation, assert `ICommandPipeline.Execute` was called with the mapped
101
+ command from the beat's `fields`; for a plain automation, assert the mocked side-effect service
102
+ was invoked.
103
+
104
+ If the storyline continues past the triggered command into that command's own event, or into a
105
+ read model, those halves belong to **build-state-change** / **build-state-view** respectively — a
106
+ reactor spec only covers the event-in → reaction-out step.
107
+
108
+ Skip silently when the beat sequence can't be isolated (e.g. no traceable trigger event).
109
+
89
110
  ## Final verification — does the implementation match `slice.json`?
90
111
 
91
112
  - [ ] The reactor observes exactly the trigger event(s) named in the slice.
@@ -93,6 +114,8 @@ See [references/patterns.md](references/patterns.md).
93
114
  - [ ] No new events written via `IEventLog` — only via `ICommandPipeline`.
94
115
  - [ ] The reaction is idempotent; the reactor is stateless.
95
116
  - [ ] Every scenario → an executable spec; `dotnet build` clean; specs pass.
117
+ - [ ] A `storylines[]` transition triggering this reactor has a spec, or was deliberately skipped
118
+ (not silently ignored).
96
119
 
97
120
  ## References
98
121
  - [references/patterns.md](references/patterns.md) — full reactor/translation code and reactor specs.
@@ -49,6 +49,7 @@ Extract, regardless of input format:
49
49
  | **Business rules** | Preconditions, invariants, idempotency — from `description` / `comments` only |
50
50
  | **State needed for rules** | Which read model must be inspected (DCB) to evaluate a rule |
51
51
  | **Specifications** | Each GWT / scenario maps 1:1 to an executable spec |
52
+ | **Storylines** (optional) | `storylines[]` — ordered walkthrough "beats"; a `COMMAND → EVENT` transition in one is a supplementary spec source, see Step 5a |
52
53
 
53
54
  **If a field is not in `slice.json`, it does not go in the code.** If requirements are unclear, ask
54
55
  the user before proceeding.
@@ -142,6 +143,31 @@ Chronicle integration specs use `Given<context>` + `ChronicleOutOfProcessFixture
142
143
 
143
144
  Run `dotnet test --filter "FullyQualifiedName~<SliceName>"`. Fix all failures.
144
145
 
146
+ ## Step 5a — Storyline-derived specs (optional)
147
+
148
+ If `storylines[]` is non-empty — a storyline embedded in this slice's slice.json already belongs
149
+ entirely to this slice, no need to match beats against `commands[]` by id/title — look for a
150
+ `COMMAND → EVENT` transition: a `type: "COMMAND"` beat, followed by its resulting event beat(s) —
151
+ possibly followed by a READMODEL beat after that.
152
+
153
+ - **Establish** — append every event from the storyline's start up to (not including) the command
154
+ beat.
155
+ - **Because** — `Handle()` the command, built from the beat's `fields`.
156
+ - **should_\*** — assert the expected event(s) (the beat(s) immediately following the command).
157
+
158
+ Only the `COMMAND → EVENT` half lives here — if the storyline continues `EVENT → READMODEL`, that
159
+ half is a separate spec in **build-state-view** (its Step 4a). Don't chase a single spec across
160
+ both a command dispatch and a read-model assertion unless the project already has an established
161
+ pattern doing that — Chronicle integration specs assert appended events, not read-model state,
162
+ and vice versa for projection specs.
163
+
164
+ Place these alongside the `specifications[]`-derived specs but in their own
165
+ `and_<storyline-title>.cs` file, so a reader can tell at a glance which specs are exhaustive
166
+ coverage and which are one narrated walkthrough.
167
+
168
+ Skip silently (no spec) when the transition isn't isolable — e.g. the command beat has no
169
+ traceable preceding events.
170
+
145
171
  ## Step 6 — Frontend (only if the command is UI-triggered)
146
172
 
147
173
  After `dotnet build` generated the proxy (co-located next to the `.cs`), add
@@ -155,6 +181,8 @@ page. See the shared conventions doc's React section and [references/patterns.md
155
181
  - [ ] Every `events[]` entry → an `[EventType]` record; names match exactly; fields match.
156
182
  - [ ] Every specification / GWT scenario → an executable spec.
157
183
  - [ ] No business rule in `Handle()` that is absent from the slice `description` / `comments`.
184
+ - [ ] A `storylines[]` `COMMAND → EVENT` transition for this command has a spec, or was
185
+ deliberately skipped (not silently ignored).
158
186
  - [ ] `dotnet build` is clean (0 warnings / 0 errors); slice specs pass.
159
187
 
160
188
  ## References
@@ -36,6 +36,7 @@ state-change skill's Step 0 for the resolve endpoint).
36
36
  | **Source events** | Which events feed each field (projections **join events, never read models**) |
37
37
  | **Queries** | The queries to expose; whether each is snapshot or real-time (observable) |
38
38
  | **Specifications** | Each scenario → an executable spec proving the projection result |
39
+ | **Storylines** (optional) | `storylines[]` — ordered walkthrough "beats"; a read-model chain in one is a supplementary spec source, see Step 4a |
39
40
 
40
41
  If a field has no source event in `slice.json`, do not invent one. One read model per use case —
41
42
  never reuse a model across slices.
@@ -100,6 +101,32 @@ Write specs proving the projection/reducer produces the expected read model from
100
101
  one spec per scenario in `slice.json`. Run `dotnet test --filter "FullyQualifiedName~<SliceName>"`.
101
102
  See [references/patterns.md](references/patterns.md).
102
103
 
104
+ ## Step 4a — Storyline-derived specs (optional)
105
+
106
+ If the slice's `storylines[]` array (in `slice.json`, alongside `specifications[]`) is non-empty —
107
+ a storyline embedded in this slice's slice.json already belongs entirely to this slice, no need to
108
+ match beats against `readmodels[]` by id/title — look for a **read-model chain**: two beats of
109
+ `type: "READMODEL"`, with only `EVENT` beat(s) between them. That's a self-contained projection
110
+ spec:
111
+
112
+ - **Establish** — append every event from the storyline's start up through the intervening
113
+ event(s), in order.
114
+ - **Because** — run the projection.
115
+ - **should_\*** — assert against the *later* beat's `fields`/`examples`/`expectEmptyList`.
116
+
117
+ Put these in their own `for_<ReadModel>/when_<storyline-title>/` folder (not `when_<behavior>/`),
118
+ so they're never confused with the exhaustive `specifications[]` suite — a storyline is a
119
+ narrated walkthrough, not exhaustive coverage.
120
+
121
+ If the storyline's transition into this read model is instead `COMMAND → EVENT → READMODEL`, only
122
+ the `EVENT → READMODEL` half belongs here — the `COMMAND → EVENT` half is a separate spec in
123
+ **build-state-change** (see its Step 5a). Don't try to assert read-model state from a dispatched
124
+ command in one spec unless the project already has an established pattern for that.
125
+
126
+ Skip a beat sequence entirely — no spec, no placeholder — if it can't be isolated (e.g. a SCREEN
127
+ beat with no traceable event). A storyline is source material for tests, not a mandate to write
128
+ one for every beat.
129
+
103
130
  ## Step 5 — Frontend
104
131
 
105
132
  Add `<Module>/<Feature>/<Slice>/<Component>.tsx` importing the co-located generated query proxy from
@@ -113,6 +140,8 @@ feature's composition page; add routing in `App.tsx` if it's a new page.
113
140
  - [ ] Every source event used exists; projections map from **events**, never read models.
114
141
  - [ ] Every query in the slice is exposed as a static method; observable where the slice wants live data.
115
142
  - [ ] Every scenario → an executable spec.
143
+ - [ ] A `storylines[]` read-model chain for this read model has a spec, or was deliberately
144
+ skipped (not silently ignored).
116
145
  - [ ] `dotnet build` clean; specs pass.
117
146
 
118
147
  ## References
@@ -43,6 +43,7 @@ From the slice definition, extract:
43
43
  - `processorId` — unique kebab-case identifier
44
44
  - **commands[]** — command data fields
45
45
  - **events[]** — events emitted by the command
46
+ - **storylines[]** (optional) — present only when the board author built an explicit walkthrough for this flow; most slices have none. See the note at the end of Step 2.
46
47
  > **Comments & description**: Each element (commands, events, readmodels, processors, screens, tables) carries a `comments: string[]` array (board comments on that node) and a `description` field. The slice itself also has `comments: string[]`. Use these as implementation hints — pass them as code comments, documentation, or validation logic where they add value. When done, resolve each used comment: `POST <BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/nodes/<nodeId>/comments/<commentId>/resolve` (get comment IDs first via GET on the same path without the last two segments).
47
48
 
48
49
 
@@ -58,6 +59,10 @@ Follow the **build-state-change** skill to create:
58
59
 
59
60
  Refer to the build-state-change skill for the full command handler structure.
60
61
 
62
+ ### Storyline-derived tests
63
+
64
+ If `storylines[]` includes a beat sequence running through this automation's trigger event → this slice's command, the command-handler test for that segment is already covered by build-state-change's own "Storyline-derived tests" step — it runs as part of following that skill above. This skill has no separate reactor-test format: the processor's event-to-command field mapping is exercised only indirectly, through that command-handler test, never by a dedicated `processor.test.ts`.
65
+
61
66
  ---
62
67
 
63
68
  ## Step 3 — Ensure the trigger event type exists
@@ -263,4 +268,5 @@ src/common/
263
268
  - [ ] Every processor in `processors[]` has a corresponding `processor.ts` implementation
264
269
  - [ ] Command data fields map exclusively from fields available on the trigger event per slice.json — no invented mappings
265
270
  - [ ] No filtering conditions were invented — all conditions come from slice.json `description` or `comments`
266
- - [ ] No field names were assumed or guessed — if a field is not in slice.json, it is not in the code
271
+ - [ ] No field names were assumed or guessed — if a field is not in slice.json, it is not in the code
272
+ - [ ] If `storylines[]` is present, its command-handler segment was covered via build-state-change's storyline-derived tests (no separate reactor test needed)
@@ -26,6 +26,7 @@ From the slice definition, extract:
26
26
  - **commands[]** — list of commands with their data fields
27
27
  - **events[]** — list of events emitted by each command
28
28
  - **specifications[]** — test scenarios (given/when/then)
29
+ - **storylines[]** (optional) — present only when the board author built an explicit walkthrough for this flow; most slices have none. See "Storyline-derived tests" under Step 4.
29
30
  > **Comments & description**: Each element (commands, events, readmodels, processors, screens, tables) carries a `comments: string[]` array (board comments on that node) and a `description` field. The slice itself also has `comments: string[]`. Use these as implementation hints — pass them as code comments, documentation, or validation logic where they add value. When done, resolve each used comment: `POST <BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/nodes/<nodeId>/comments/<commentId>/resolve` (get comment IDs first via GET on the same path without the last two segments).
30
31
 
31
32
 
@@ -227,6 +228,16 @@ describe('{SliceName} Specification', () => {
227
228
 
228
229
  Add one test per specification in the slice.json. If the spec has no precondition events, use `given([])`.
229
230
 
231
+ ### Storyline-derived tests (optional)
232
+
233
+ `storylines[]` in slice.json (optional — only present when the board author built an explicit walkthrough) can also supply command-handler tests, in addition to the specifications-derived ones above. A storyline embedded in this slice's slice.json already belongs entirely to this slice — no need to match beats against `commands[]` by id/title. Just scan each storyline's ordered `elements` for a `type: COMMAND` beat, followed by its emitted EVENT beat(s):
234
+
235
+ - `given` — the cumulative ordered events from the start of the storyline up to (not including) the command beat
236
+ - `when` — the command, built from that beat's `fields`
237
+ - `then` — the following EVENT beat(s)' data
238
+
239
+ Put these in their own `describe` block named after the storyline (same pattern as build-state-view's Storyline-derived tests section). Don't try to also assert read-model state in this test — a `DeciderSpecification` test only sees emitted events, never a materialized read model; the READMODEL half of the same storyline segment is build-state-view's job, not this skill's.
240
+
230
241
  ---
231
242
 
232
243
  ## Step 5 — Create `routes.ts`
@@ -341,4 +352,5 @@ Before marking this slice as `Done`, verify the implementation against slice.jso
341
352
  - [ ] Every field in each event's data has a corresponding field in the TypeScript event type
342
353
  - [ ] Every entry in `specifications[]` maps to a test case in `{SliceName}.test.ts`
343
354
  - [ ] No business rules, defaults, or constraints were added that do not appear in slice.json `description` or `comments`
344
- - [ ] No field names were assumed or guessed — if a field is not in slice.json, it is not in the code
355
+ - [ ] No field names were assumed or guessed — if a field is not in slice.json, it is not in the code
356
+ - [ ] If `storylines[]` is present, a storyline-derived test was added for every COMMAND beat matching this slice's command
@@ -22,6 +22,7 @@ From the slice definition, extract:
22
22
  - **context** — bounded context
23
23
  - **events[]** — events this projection handles (its `canHandle` list)
24
24
  - **readModel / fields** — the columns of the output table
25
+ - **storylines[]** (optional) — present only when the board author built an explicit walkthrough for this flow; most slices have none. See "Storyline-derived tests" under Step 5.
25
26
  > **Comments & description**: Each element (commands, events, readmodels, processors, screens, tables) carries a `comments: string[]` array (board comments on that node) and a `description` field. The slice itself also has `comments: string[]`. Use these as implementation hints — pass them as code comments, documentation, or validation logic where they add value. When done, resolve each used comment: `POST <BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/nodes/<nodeId>/comments/<commentId>/resolve` (get comment IDs first via GET on the same path without the last two segments).
26
27
 
27
28
 
@@ -324,6 +325,27 @@ describe('{SliceName} Specification', () => {
324
325
 
325
326
  Write one `it` block per specification in the slice.json. Use `given([events]).when([]).then(assertReadModel)`.
326
327
 
328
+ ### Storyline-derived tests (optional)
329
+
330
+ `storylines[]` in slice.json is optional — present only when the board author explicitly built a walkthrough for this flow; most slices have none. When present, mine it for **additional** read-model-chain tests, on top of (never instead of) the specifications-derived tests above.
331
+
332
+ A storyline is `{ id, title, elements: [...] }`, where `elements` is an ordered list of beats — the same element can repeat to show its state at different points in the flow. A storyline embedded in this slice's slice.json already belongs entirely to this slice — no need to match beats against `readmodels[]` by id/title. Just scan for a pair of adjacent beats that are both `type: READMODEL`, with only EVENT beat(s) between them and no COMMAND beat in that run. That pair is one self-contained projection test:
333
+
334
+ - `given` — the cumulative ordered events from the start of the storyline through the intervening event beat(s)
335
+ - `then` — assert the read model matches the later READMODEL beat's `fields`/`examples`/`expectEmptyList`, same as a specifications-derived assertion
336
+
337
+ Put these in their own `describe` block, named after the storyline, so they're never confused with the exhaustive `specifications[]` suite:
338
+
339
+ ```typescript
340
+ describe('{SliceName} Storyline: {storyline.title}', () => {
341
+ it('spec: {storyline.title} — after {EventA}', async () => {
342
+ // same given([...]).when([]).then(assertReadModel) shape as above
343
+ });
344
+ });
345
+ ```
346
+
347
+ Skip a beat pair when a COMMAND beat sits in between (that half belongs to build-state-change's command-handler test, not this one) or when the run includes a SCREEN/AUTOMATION beat with no traceable event — don't fabricate a test for those; a one-line comment noting the storyline segment exists is enough.
348
+
327
349
  ---
328
350
 
329
351
  ## Step 6 — Create `routes.ts`
@@ -415,4 +437,5 @@ src/common/
415
437
  o- [ ] Every field in the read model definition in slice.json has a column in the migration and a field in the TypeScript type — no invented columns
416
438
  - [ ] Every event type in `events[]` is listed in the projection's `canHandle` — no assumed events
417
439
  - [ ] No extra columns or fields were added beyond what slice.json defines
418
- - [ ] No field names were assumed or guessed — if a field is not in slice.json, it is not in the code
440
+ - [ ] No field names were assumed or guessed — if a field is not in slice.json, it is not in the code
441
+ - [ ] If `storylines[]` is present, a storyline-derived test was added for every isolable read-model-chain transition (adjacent READMODEL beats with only EVENT beats between them)
@@ -43,6 +43,7 @@ From the slice definition, extract:
43
43
  - `processorId` — unique kebab-case identifier
44
44
  - **commands[]** — command data fields
45
45
  - **events[]** — events emitted by the command
46
+ - **storylines[]** (optional) — narrated walkthroughs; see the note in Step 2
46
47
  > **Comments & description**: Each element (commands, events, readmodels, processors, screens, tables) carries a `comments: string[]` array (board comments on that node) and a `description` field. The slice itself also has `comments: string[]`. Use these as implementation hints — pass them as code comments, documentation, or validation logic where they add value. When done, resolve each used comment: `POST <BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/nodes/<nodeId>/comments/<commentId>/resolve` (get comment IDs first via GET on the same path without the last two segments).
47
48
 
48
49
 
@@ -58,6 +59,8 @@ Follow the **build-state-change** skill to create:
58
59
 
59
60
  Refer to the build-state-change skill for the full command handler structure.
60
61
 
62
+ > **Storyline-derived tests**: if slice.json has a `storylines[]` array, build-state-change's Step 4b applies here too — treat the trigger EVENT beat as the "given" and the fired command's resulting EVENT beat(s) as "then", exactly as it would for an ordinary command-change slice. Skip silently if there's nothing relevant.
63
+
61
64
  ---
62
65
 
63
66
  ## Step 3 — Ensure the trigger event type exists
@@ -260,6 +263,7 @@ src/common/
260
263
  - [ ] `schema.migrate()` is called in `loadPostgresEventstore.ts`
261
264
  - [ ] No `routes.ts` created (automations are not exposed via HTTP)
262
265
  - [ ] Command handler tests cover idempotency (what happens if the command fires twice)
266
+ - [ ] If `storylines[]` is present, storyline-derived command-handler tests were added per build-state-change's Step 4b (or skipped with a documented reason)
263
267
  - [ ] Every processor in `processors[]` has a corresponding `processor.ts` implementation
264
268
  - [ ] Command data fields map exclusively from fields available on the trigger event per slice.json — no invented mappings
265
269
  - [ ] No filtering conditions were invented — all conditions come from slice.json `description` or `comments`
@@ -26,6 +26,7 @@ From the slice definition, extract:
26
26
  - **commands[]** — list of commands with their data fields
27
27
  - **events[]** — list of events emitted by each command
28
28
  - **specifications[]** — test scenarios (given/when/then)
29
+ - **storylines[]** (optional) — narrated walkthroughs that may yield additional command-handler tests; see Step 4b
29
30
  > **Comments & description**: Each element (commands, events, readmodels, processors, screens, tables) carries a `comments: string[]` array (board comments on that node) and a `description` field. The slice itself also has `comments: string[]`. Use these as implementation hints — pass them as code comments, documentation, or validation logic where they add value. When done, resolve each used comment: `POST <BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/nodes/<nodeId>/comments/<commentId>/resolve` (get comment IDs first via GET on the same path without the last two segments).
30
31
 
31
32
 
@@ -229,6 +230,23 @@ Add one test per specification in the slice.json. If the spec has no preconditio
229
230
 
230
231
  ---
231
232
 
233
+ ## Step 4b — Storyline-derived tests (optional)
234
+
235
+ Some slices also have a `storylines[]` array in slice.json — narrated walkthroughs of one use case as an ordered sequence of beats (`elements[]`). Most slices have no storylines; skip this step silently when `storylines[]` is empty or absent.
236
+
237
+ A storyline embedded in this slice's slice.json already belongs entirely to this slice — no need to match beats against `commands[]` by id/title. For each storyline, find a `type: COMMAND` beat directly followed by its EVENT beat(s). That pair is a command-handler test — the same shape as the specifications-derived tests above:
238
+ - `given` — the cumulative ordered EVENT beats from the start of the storyline up to (but not including) the command beat
239
+ - `when` — the command, built from the beat's `fields`
240
+ - `then` — the event(s) immediately following the command beat, built from their `fields`
241
+
242
+ Add these alongside the specifications-derived tests in the same `describe` block, or in their own block named after the storyline's `title` if it helps distinguish them.
243
+
244
+ A storyline walking COMMAND → EVENT → READMODEL is **not** one test here: the READMODEL half belongs to **build-state-view** — `DeciderSpecification` can only assert emitted events, never read-model state.
245
+
246
+ Skip (do not fabricate) a segment when the command beat has no immediately-following event beat relevant to this slice, or when the beat's fields don't give enough to construct a valid command.
247
+
248
+ ---
249
+
232
250
  ## Step 5 — Create `routes.ts`
233
251
 
234
252
  File: `src/slices/{context}/{SliceName}/routes.ts`
@@ -340,5 +358,6 @@ Before marking this slice as `Done`, verify the implementation against slice.jso
340
358
  - [ ] Every event in `events[]` has a corresponding type in `{Context}Events.ts` — names match exactly
341
359
  - [ ] Every field in each event's data has a corresponding field in the TypeScript event type
342
360
  - [ ] Every entry in `specifications[]` maps to a test case in `{SliceName}.test.ts`
361
+ - [ ] If `storylines[]` is present, each command-to-event transition relevant to this slice has a corresponding test (or a documented reason it was skipped)
343
362
  - [ ] No business rules, defaults, or constraints were added that do not appear in slice.json `description` or `comments`
344
363
  - [ ] No field names were assumed or guessed — if a field is not in slice.json, it is not in the code
@@ -22,6 +22,7 @@ From the slice definition, extract:
22
22
  - **context** — bounded context
23
23
  - **events[]** — events this projection handles (its `canHandle` list)
24
24
  - **readModel / fields** — the columns of the output table
25
+ - **storylines[]** (optional) — narrated walkthroughs that may yield additional projection tests; see Step 5b
25
26
  > **Comments & description**: Each element (commands, events, readmodels, processors, screens, tables) carries a `comments: string[]` array (board comments on that node) and a `description` field. The slice itself also has `comments: string[]`. Use these as implementation hints — pass them as code comments, documentation, or validation logic where they add value. When done, resolve each used comment: `POST <BASE_URL>/api/org/<ORG_ID>/boards/<BOARD_ID>/nodes/<nodeId>/comments/<commentId>/resolve` (get comment IDs first via GET on the same path without the last two segments).
26
27
 
27
28
 
@@ -306,6 +307,22 @@ Write one `it` block per specification in the slice.json. Use `given([events]).w
306
307
 
307
308
  ---
308
309
 
310
+ ## Step 5b — Storyline-derived tests (optional)
311
+
312
+ Some slices also have a `storylines[]` array in slice.json — narrated walkthroughs where the *same* read model repeats across several beats in one ordered `elements[]` list, showing its state at different points in a single flow. Most slices have no storylines; skip this step silently when `storylines[]` is empty or absent.
313
+
314
+ A storyline embedded in this slice's slice.json already belongs entirely to this slice — no need to match beats against `readmodels[]` by id/title. For each storyline, scan `elements[]` for pairs of adjacent `type: 'READMODEL'` beats with only EVENT beat(s) in between. Each such pair is a clean, self-contained projection test — the same shape as the specifications-derived tests above:
315
+ - `given` — the cumulative ordered EVENT beats from the start of the storyline through the intervening event(s), not just the two beats either side (earlier events in the same storyline still apply to accumulated state)
316
+ - `then` — assert the read model matches the later beat's shape, using its `fields`/`examples`/`expectEmptyList` exactly like a specifications-derived assertion
317
+
318
+ Put these in their own `describe` block per storyline, named after the storyline's `title` (e.g. `describe('Storyline: {storyline.title}', ...)`) — never merge them into the `specifications[]` suite; storylines are a supplementary narrative check, not a replacement for scenario coverage.
319
+
320
+ Skip (do not fabricate) a segment when:
321
+ - a COMMAND beat sits between two read-model beats — that half belongs to **build-state-change** (the command → event transition); only the event → read-model half after it is testable here
322
+ - a beat (e.g. SCREEN) can't be traced to an event affecting the read model
323
+
324
+ ---
325
+
309
326
  ## Step 6 — Create `routes.ts`
310
327
 
311
328
  File: `src/slices/{context}/{SliceName}/routes.ts`
@@ -390,6 +407,8 @@ src/common/
390
407
  - [ ] `finally { await db.destroy() }` present in every `evolve` handler
391
408
  - [ ] Tests use `runFlywayMigrations()` to apply the real schema
392
409
  - [ ] One test scenario per specification in slice.json
410
+ - [ ] If `storylines[]` is present, each read-model-to-read-model transition relevant to this slice has a corresponding storyline test (or a documented reason it was skipped)
411
+ - [ ] Storyline-derived tests live in their own `describe` block, separate from the specifications suite
393
412
  - [ ] Every field in the read model definition in slice.json has a column in the migration and a field in the TypeScript type — no invented columns
394
413
  - [ ] Every event type in `events[]` is listed in the projection's `canHandle` — no assumed events
395
414
  - [ ] No extra columns or fields were added beyond what slice.json defines