@eventmodelers/cli 1.0.150 → 1.0.152

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 (38) hide show
  1. package/README.md +1 -0
  2. package/cli.js +20 -36
  3. package/lib/bash-guard.js +39 -1
  4. package/package.json +1 -1
  5. package/shared/build-kit/lib/adapters/pocketbase-realtime-adapter.js +14 -7
  6. package/shared/build-kit/lib/adapters/realtime-adapter.js +65 -2
  7. package/shared/build-kit/lib/adapters/supabase-realtime-adapter.js +19 -10
  8. package/shared/build-kit/lib/ralph.js +30 -62
  9. package/shared/skills/learn-eventmodelers-api/SKILL.md +38 -1
  10. package/stacks/axon/templates/.claude/skills/build-automation/SKILL.md +2 -0
  11. package/stacks/axon/templates/.claude/skills/build-state-change/SKILL.md +2 -0
  12. package/stacks/axon/templates/.claude/skills/build-state-view/SKILL.md +2 -0
  13. package/stacks/classic/templates/.claude/skills/build-automation/SKILL.md +2 -0
  14. package/stacks/classic/templates/.claude/skills/build-state-change/SKILL.md +2 -0
  15. package/stacks/classic/templates/.claude/skills/build-state-view/SKILL.md +2 -0
  16. package/stacks/kurrent/templates/.claude/skills/build-automation/SKILL.md +2 -0
  17. package/stacks/kurrent/templates/.claude/skills/build-state-change/SKILL.md +2 -0
  18. package/stacks/kurrent/templates/.claude/skills/build-state-view/SKILL.md +2 -0
  19. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-elaborating-scenarios/SKILL.md +69 -3
  20. package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +1 -1
  21. package/stacks/modeling-kit/templates/.claude/skills/prototype/SKILL.md +181 -0
  22. package/stacks/modeling-kit/templates/.claude/skills/prototype/assets/index.template.html +490 -0
  23. package/stacks/modeling-kit/templates/.claude/skills/prototype/assets/prototype.mjs +224 -0
  24. package/stacks/modeling-kit/templates/.claude/skills/prototype/assets/runtime.js +434 -0
  25. package/stacks/modeling-kit/templates/.claude/skills/prototype/assets/slices.example.html +201 -0
  26. package/stacks/modeling-kit/templates/.claude/skills/prototype/references/api-fallback.md +30 -0
  27. package/stacks/modeling-kit/templates/kit/CLAUDE.md +2 -0
  28. package/stacks/node/templates/.claude/skills/build-automation/SKILL.md +2 -0
  29. package/stacks/node/templates/.claude/skills/build-state-change/SKILL.md +2 -0
  30. package/stacks/node/templates/.claude/skills/build-state-view/SKILL.md +2 -0
  31. package/stacks/opencqrs/templates/.claude/skills/build-automation/SKILL.md +2 -0
  32. package/stacks/opencqrs/templates/.claude/skills/build-state-change/SKILL.md +2 -0
  33. package/stacks/opencqrs/templates/.claude/skills/build-state-view/SKILL.md +2 -0
  34. package/stacks/supabase/templates/.claude/skills/build-state-change/SKILL.md +2 -0
  35. package/stacks/supabase/templates/.claude/skills/build-state-view/SKILL.md +2 -0
  36. package/stacks/umadb/templates/.claude/skills/build-automation/SKILL.md +2 -0
  37. package/stacks/umadb/templates/.claude/skills/build-state-change/SKILL.md +2 -0
  38. package/stacks/umadb/templates/.claude/skills/build-state-view/SKILL.md +2 -0
@@ -0,0 +1,201 @@
1
+ <!--
2
+ Example slice blocks — the contract for what the `prototype` skill generates, one block per slice,
3
+ pasted at @@SLICES@@ in index.html. One of each kind: state change, state view, automation.
4
+
5
+ Inside a block:
6
+ - JSX and modern JavaScript; React hooks via React.useState etc.
7
+ - useCommand, useReadModel, useNav, useNotModeled are globals (the kit defines them).
8
+ - EMRuntime.reject('<message>') rejects a command with a scenario's errorDescription.
9
+ - Never the text of a closing script tag, not even in a string or comment.
10
+ - Every element belongs to exactly one slice: a command to its state-change slice, a read model to
11
+ its state-view slice, a todo list and its automation to the automation slice.
12
+ -->
13
+
14
+ <script type="text/plain" data-em-slice="11111111-1111-4111-8111-111111111111" data-em-title="Register Customer" data-em-hash="0f3c9a1d2b4e5f60">
15
+ // State change: the screen part is the form the command's inputs come from, plus its button.
16
+ const ofType = (events, type) => events.filter((e) => e.type === type);
17
+
18
+ function RegisterCustomerForm() {
19
+ const register = useCommand('RegisterCustomer');
20
+ const { go } = useNav();
21
+ const [form, setForm] = React.useState({ name: '', email: '' });
22
+ const set = (k) => (e) => setForm({ ...form, [k]: e.target.value });
23
+ // Markup translated 1:1 from the screen's HTML page; inputs bound to the command's fields.
24
+ return (
25
+ <section className="section">
26
+ <div className="box" style={{ maxWidth: 420 }}>
27
+ <h1 className="title is-4">Register</h1>
28
+ <div className="field">
29
+ <label className="label" htmlFor="name">Name</label>
30
+ <input id="name" className="input" placeholder="Jane Doe" value={form.name} onChange={set('name')} />
31
+ </div>
32
+ <div className="field">
33
+ <label className="label" htmlFor="email">Email</label>
34
+ <input id="email" className="input" placeholder="jane@example.com" value={form.email} onChange={set('email')} />
35
+ </div>
36
+ {register.error && <p className="help is-danger">{register.error}</p>}
37
+ <button id="register" className="button is-primary" onClick={() => register.run(form).ok && go('Customers')}>
38
+ Register Customer
39
+ </button>
40
+ </div>
41
+ </section>
42
+ );
43
+ }
44
+
45
+ EMRuntime.defineSlice({
46
+ id: '11111111-1111-4111-8111-111111111111',
47
+ title: 'Register Customer',
48
+ order: 1,
49
+ commands: {
50
+ RegisterCustomer: {
51
+ decide(history, cmd, ctx) {
52
+ // Scenario "Email must be unique"
53
+ if (ofType(history, 'CustomerRegistered').some((e) => e.data.email === cmd.email)) {
54
+ EMRuntime.reject('Email is already registered');
55
+ }
56
+ return [{ type: 'CustomerRegistered', data: { customerId: ctx.uuid(), name: cmd.name, email: cmd.email } }];
57
+ },
58
+ },
59
+ },
60
+ specs: [
61
+ {
62
+ title: 'Customer registers',
63
+ kind: 'command',
64
+ given: [],
65
+ when: { type: 'RegisterCustomer', data: { name: 'Jane Doe', email: 'jane@example.com' } },
66
+ then: [{ type: 'CustomerRegistered', data: { name: 'Jane Doe', email: 'jane@example.com' } }],
67
+ },
68
+ {
69
+ title: 'Email must be unique',
70
+ kind: 'command',
71
+ given: [{ type: 'CustomerRegistered', data: { customerId: 'c-1', name: 'Jane Doe', email: 'jane@example.com' } }],
72
+ when: { type: 'RegisterCustomer', data: { name: 'John Doe', email: 'jane@example.com' } },
73
+ error: 'Email is already registered',
74
+ },
75
+ ],
76
+ screens: { Registration: { actor: 'Customer', part: RegisterCustomerForm } },
77
+ });
78
+ </script>
79
+
80
+ <script type="text/plain" data-em-slice="22222222-2222-4222-8222-222222222222" data-em-title="Customer List" data-em-hash="7a1e44c09d2b3f81">
81
+ // State view: the read model is one component — the region of the screen that shows its data.
82
+ function CustomerTable() {
83
+ const customers = useReadModel('Customers');
84
+ const notModeled = useNotModeled();
85
+ return (
86
+ <section className="section">
87
+ <h1 className="title is-4">Customers</h1>
88
+ {customers.length === 0 ? (
89
+ <p>No customers yet.</p>
90
+ ) : (
91
+ <table className="table">
92
+ <tbody>
93
+ {customers.map((c) => (
94
+ <tr key={c.customerId}>
95
+ <td>{c.name}</td>
96
+ <td>{c.email}</td>
97
+ <td>{c.mailSent ? 'mail sent' : 'pending'}</td>
98
+ </tr>
99
+ ))}
100
+ </tbody>
101
+ </table>
102
+ )}
103
+ <button className="button" onClick={() => notModeled('Export')}>Export</button>
104
+ </section>
105
+ );
106
+ }
107
+
108
+ EMRuntime.defineSlice({
109
+ id: '22222222-2222-4222-8222-222222222222',
110
+ title: 'Customer List',
111
+ order: 2,
112
+ readModels: {
113
+ Customers: {
114
+ initial: () => [],
115
+ evolve(state, e) {
116
+ if (e.type === 'CustomerRegistered') return [...state, { customerId: e.data.customerId, name: e.data.name, email: e.data.email }];
117
+ if (e.type === 'ConfirmationMailSent') return state.map((c) => (c.customerId === e.data.customerId ? { ...c, mailSent: true } : c));
118
+ return state;
119
+ },
120
+ },
121
+ },
122
+ specs: [
123
+ {
124
+ title: 'Registered customers are listed',
125
+ kind: 'readmodel',
126
+ readModel: 'Customers',
127
+ given: [{ type: 'CustomerRegistered', data: { customerId: 'c-1', name: 'Jane Doe', email: 'jane@example.com' } }],
128
+ expect: [{ customerId: 'c-1', name: 'Jane Doe' }],
129
+ },
130
+ ],
131
+ screens: { Customers: { actor: 'Back Office', part: CustomerTable } },
132
+ });
133
+ </script>
134
+
135
+ <script type="text/plain" data-em-slice="33333333-3333-4333-8333-333333333333" data-em-title="Send Confirmation Mail" data-em-hash="c5d2e8f1a0b39476">
136
+ // Automation: its todo list, the automation and the command it issues. How an item gets onto the
137
+ // list and off it comes from the todo list's scenarios: given CustomerRegistered → listed;
138
+ // given CustomerRegistered + ConfirmationMailSent → empty.
139
+ EMRuntime.defineSlice({
140
+ id: '33333333-3333-4333-8333-333333333333',
141
+ title: 'Send Confirmation Mail',
142
+ order: 3,
143
+ readModels: {
144
+ PendingConfirmationMails: {
145
+ initial: () => ({}),
146
+ evolve(state, e) {
147
+ if (e.type === 'CustomerRegistered') return { ...state, [e.data.customerId]: { customerId: e.data.customerId, email: e.data.email } };
148
+ if (e.type === 'ConfirmationMailSent') {
149
+ const { [e.data.customerId]: _done, ...rest } = state;
150
+ return rest;
151
+ }
152
+ return state;
153
+ },
154
+ query: (state) => Object.values(state),
155
+ },
156
+ },
157
+ commands: {
158
+ SendConfirmationMail: {
159
+ decide: (history, cmd, ctx) => [{ type: 'ConfirmationMailSent', data: { customerId: cmd.customerId, sentAt: ctx.now.toISOString() } }],
160
+ },
161
+ },
162
+ automations: {
163
+ SendConfirmationMail: {
164
+ readModel: 'PendingConfirmationMails',
165
+ trigger: 'auto',
166
+ external: true,
167
+ todo: (items) => items,
168
+ process: (item) => ({
169
+ effect: `E-mail with confirmation link to ${item.email}`,
170
+ command: { type: 'SendConfirmationMail', data: { customerId: item.customerId, email: item.email } },
171
+ }),
172
+ },
173
+ },
174
+ specs: [
175
+ {
176
+ title: 'A new registration is waiting for its mail',
177
+ kind: 'readmodel',
178
+ readModel: 'PendingConfirmationMails',
179
+ given: [{ type: 'CustomerRegistered', data: { customerId: 'c-1', name: 'Jane Doe', email: 'jane@example.com' } }],
180
+ expect: [{ customerId: 'c-1' }],
181
+ },
182
+ {
183
+ title: 'A sent mail leaves the list',
184
+ kind: 'readmodel',
185
+ readModel: 'PendingConfirmationMails',
186
+ given: [
187
+ { type: 'CustomerRegistered', data: { customerId: 'c-1', name: 'Jane Doe', email: 'jane@example.com' } },
188
+ { type: 'ConfirmationMailSent', data: { customerId: 'c-1' } },
189
+ ],
190
+ expect: [],
191
+ },
192
+ {
193
+ title: 'A new registration gets a confirmation mail',
194
+ kind: 'automation',
195
+ automation: 'SendConfirmationMail',
196
+ given: [{ type: 'CustomerRegistered', data: { customerId: 'c-1', name: 'Jane Doe', email: 'jane@example.com' } }],
197
+ expectCommands: [{ type: 'SendConfirmationMail', data: { customerId: 'c-1' } }],
198
+ },
199
+ ],
200
+ });
201
+ </script>
@@ -0,0 +1,30 @@
1
+ # Prototype — curl Fallback Calls
2
+
3
+ Only needed when MCP is not connected. `prototype.mjs status` (Step 2) already uses the REST API directly and needs no MCP.
4
+
5
+ ## Step 1 — Contexts and timelines
6
+
7
+ ```bash
8
+ curl -s -H "x-token: $TOKEN" "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/nodes?type=MODEL_CONTEXT&projection=line"
9
+ curl -s -H "x-token: $TOKEN" "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/nodes?type=CHAPTER&projection=line"
10
+ ```
11
+
12
+ ## Step 3 — One flagged slice
13
+
14
+ ```bash
15
+ # Elements with fields + dependencies
16
+ curl -s -G -H "x-token: $TOKEN" "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/slicedata" \
17
+ --data-urlencode "contextName=<CONTEXT>" -d sliceId=<SLICE_ID> -d projection=fields -d format=json
18
+
19
+ # Scenarios and storylines
20
+ curl -s -G -H "x-token: $TOKEN" "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/slicedata" \
21
+ --data-urlencode "contextName=<CONTEXT>" -d sliceId=<SLICE_ID> -d projection=specs -d format=json
22
+
23
+ # Full screen records (meta.pages, meta.marks, meta.fields)
24
+ curl -s -X POST -H "x-token: $TOKEN" -H "Content-Type: application/json" \
25
+ "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/nodes/query" -d '{"ids": ["<id1>", "<id2>"]}'
26
+
27
+ # A sketch SCREEN's elements — 404 when the screen is an uploaded image
28
+ curl -s -H "x-token: $TOKEN" -H "x-user-id: agent" \
29
+ "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/images/<NODE_ID>/description"
30
+ ```
@@ -316,6 +316,7 @@ for it. Instead:
316
316
  `code` block with Mermaid/PlantUML/ASCII), **never** drawn on the board with stickies, arrows or `create_drawing`.
317
317
  Only an explicit request to put it on the board changes that, and then as an image (`/domain-story` § board), not rebuilt from shapes;
318
318
  - **rows and columns** — a comparison, a matrix, a short list with a few attributes per item → `table`.
319
+ - **a prototype you just built** (the `/prototype` skill) → `prototype`, the uploaded page running in the chat with a Full screen button;
319
320
  - anything the customer defined **their own snippet type** for → `custom` with its `ref`: `list_snippet_types`
320
321
  shows them with `instructions` on when to use each; prefer a fitting one over a built-in kind. A click on it arrives
321
322
  with the button's `then` — what the type's author meant the click to do, after the message as a fenced
@@ -422,6 +423,7 @@ nothing to sanitize either, since a board change is not user text.
422
423
  | Find what changed in the code since the last analysis (git history from the chapter note's baseline) that the model doesn't reflect, and propose fixes | `/detect-model-drift` |
423
424
  | Walk someone through a feature slice by slice, or through every slice a planned feature needs (existing, to adjust, new), as a player in the chat | `/walkthrough` |
424
425
  | Turn a document or image (e.g. a tender/RFP) into a chaptered event model with screens, scenarios and a written blueprint summary | `/summarize` |
426
+ | Build or update a clickable prototype of one context — one page uploaded to the platform and played with the timeline ▶ button, built slice by slice (only changed slices are rebuilt), screens as tabs, in-browser event sourcing, logic from the scenarios, automations runnable (read-only on the board) | `/prototype` |
425
427
  | Look up any API endpoint or element type not already covered by the skill you're executing | `/learn-eventmodelers-api` |
426
428
  | Add or rename an attribute across a chain of elements | `/attributes` |
427
429
  | Add or improve example data on element fields | `/examples` |
@@ -64,6 +64,8 @@ Refer to the build-state-change skill for the full command handler structure.
64
64
 
65
65
  ### Storyline-derived tests
66
66
 
67
+ > **Note beats**: an entry with `type: STORYLINE_NOTE` is a note the board author placed in the walkthrough — its `description` is markdown context (an assumption, an external condition, a time gap), and its `id` is not a board node. Read it as background for the beats around it, but it is never a step: skip it when looking for COMMAND/EVENT/READMODEL beats or judging which beats are adjacent, and never turn it into test setup or an assertion on its own.
68
+
67
69
  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`.
68
70
 
69
71
  ---
@@ -230,6 +230,8 @@ Add one test per specification in the slice.json. If the spec has no preconditio
230
230
 
231
231
  ### Storyline-derived tests (optional)
232
232
 
233
+ > **Note beats**: an entry with `type: STORYLINE_NOTE` is a note the board author placed in the walkthrough — its `description` is markdown context (an assumption, an external condition, a time gap), and its `id` is not a board node. Read it as background for the beats around it, but it is never a step: skip it when looking for COMMAND/EVENT/READMODEL beats or judging which beats are adjacent, and never turn it into test setup or an assertion on its own.
234
+
233
235
  `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
236
 
235
237
  - `given` — the cumulative ordered events from the start of the storyline up to (not including) the command beat
@@ -327,6 +327,8 @@ Write one `it` block per specification in the slice.json. Use `given([events]).w
327
327
 
328
328
  ### Storyline-derived tests (optional)
329
329
 
330
+ > **Note beats**: an entry with `type: STORYLINE_NOTE` is a note the board author placed in the walkthrough — its `description` is markdown context (an assumption, an external condition, a time gap), and its `id` is not a board node. Read it as background for the beats around it, but it is never a step: skip it when looking for COMMAND/EVENT/READMODEL beats or judging which beats are adjacent, and never turn it into test setup or an assertion on its own.
331
+
330
332
  `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
333
 
332
334
  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:
@@ -394,6 +394,8 @@ class {AutomationName}ProcessorTest {
394
394
 
395
395
  ### Storyline-Derived Tests (Optional)
396
396
 
397
+ > **Note beats**: an entry with `type: STORYLINE_NOTE` is a note the board author placed in the walkthrough — its `description` is markdown context (an assumption, an external condition, a time gap), and its `id` is not a board node. Read it as background for the beats around it, but it is never a step: skip it when looking for COMMAND/EVENT/READMODEL beats or judging which beats are adjacent, and never turn it into test setup or an assertion on its own.
398
+
397
399
  `slice.json` may also carry a `storylines[]` array — narrated walkthroughs with an ordered
398
400
  `elements[]` "beats" sequence (EVENT/COMMAND/READMODEL/...). This is a secondary, supplementary
399
401
  source; `specifications[]` above stays the primary and default source of test cases. Most slices
@@ -368,6 +368,8 @@ Full fluent-API cheat sheet: see
368
368
 
369
369
  ## Step 7b: Storyline-Derived Tests (Optional)
370
370
 
371
+ > **Note beats**: an entry with `type: STORYLINE_NOTE` is a note the board author placed in the walkthrough — its `description` is markdown context (an assumption, an external condition, a time gap), and its `id` is not a board node. Read it as background for the beats around it, but it is never a step: skip it when looking for COMMAND/EVENT/READMODEL beats or judging which beats are adjacent, and never turn it into test setup or an assertion on its own.
372
+
371
373
  `slice.json` may carry a `storylines[]` array alongside `specifications[]` — narrated walkthroughs
372
374
  where an ordered sequence of `elements[]` "beats" (EVENT/COMMAND/READMODEL/...) shows one use case
373
375
  end to end. This is a secondary, supplementary source — `specifications[]` (Step 7) stays the
@@ -300,6 +300,8 @@ class {SliceName}ProjectorTest {
300
300
 
301
301
  ## Step 4b: Storyline-Derived Tests (Optional)
302
302
 
303
+ > **Note beats**: an entry with `type: STORYLINE_NOTE` is a note the board author placed in the walkthrough — its `description` is markdown context (an assumption, an external condition, a time gap), and its `id` is not a board node. Read it as background for the beats around it, but it is never a step: skip it when looking for COMMAND/EVENT/READMODEL beats or judging which beats are adjacent, and never turn it into test setup or an assertion on its own.
304
+
303
305
  `slice.json` may also carry a `storylines[]` array — narrated walkthroughs where the *same* read
304
306
  model appears as multiple ordered "beats" across one flow (see `elements[]` on each storyline).
305
307
  This is a secondary, supplementary source: `specifications[]` (Step 3) remains the primary and
@@ -232,6 +232,8 @@ Add one test per specification in the slice.json. If the spec has no preconditio
232
232
 
233
233
  ## Step 4b — Storyline-derived tests (optional)
234
234
 
235
+ > **Note beats**: an entry with `type: STORYLINE_NOTE` is a note the board author placed in the walkthrough — its `description` is markdown context (an assumption, an external condition, a time gap), and its `id` is not a board node. Read it as background for the beats around it, but it is never a step: skip it when looking for COMMAND/EVENT/READMODEL beats or judging which beats are adjacent, and never turn it into test setup or an assertion on its own.
236
+
235
237
  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
238
 
237
239
  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:
@@ -309,6 +309,8 @@ Write one `it` block per specification in the slice.json. Use `given([events]).w
309
309
 
310
310
  ## Step 5b — Storyline-derived tests (optional)
311
311
 
312
+ > **Note beats**: an entry with `type: STORYLINE_NOTE` is a note the board author placed in the walkthrough — its `description` is markdown context (an assumption, an external condition, a time gap), and its `id` is not a board node. Read it as background for the beats around it, but it is never a step: skip it when looking for COMMAND/EVENT/READMODEL beats or judging which beats are adjacent, and never turn it into test setup or an assertion on its own.
313
+
312
314
  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
315
 
314
316
  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:
@@ -282,6 +282,8 @@ verifyNoMoreInteractions(targetCommandHandler);
282
282
 
283
283
  ### Storyline-Derived Tests (Optional)
284
284
 
285
+ > **Note beats**: an entry with `type: STORYLINE_NOTE` is a note the board author placed in the walkthrough — its `description` is markdown context (an assumption, an external condition, a time gap), and its `id` is not a board node. Read it as background for the beats around it, but it is never a step: skip it when looking for COMMAND/EVENT/READMODEL beats or judging which beats are adjacent, and never turn it into test setup or an assertion on its own.
286
+
285
287
  `slice.json` may also carry a `storylines[]` array — narrated walkthroughs with an ordered
286
288
  `elements[]` "beats" sequence (EVENT/COMMAND/READMODEL/...). This is a secondary, supplementary
287
289
  source; `specifications[]` above stays the primary and default source of test cases. Most slices
@@ -344,6 +344,8 @@ behaviour, why `subscribe()` isn't faked: see
344
344
 
345
345
  ## Step 7b: Storyline-Derived Tests (Optional)
346
346
 
347
+ > **Note beats**: an entry with `type: STORYLINE_NOTE` is a note the board author placed in the walkthrough — its `description` is markdown context (an assumption, an external condition, a time gap), and its `id` is not a board node. Read it as background for the beats around it, but it is never a step: skip it when looking for COMMAND/EVENT/READMODEL beats or judging which beats are adjacent, and never turn it into test setup or an assertion on its own.
348
+
347
349
  `slice.json` may carry a `storylines[]` array alongside `specifications[]` — narrated walkthroughs
348
350
  where an ordered sequence of `elements[]` "beats" (EVENT/COMMAND/READMODEL/...) shows one use case
349
351
  end to end. This is a secondary, supplementary source — `specifications[]` (Step 7) stays the
@@ -286,6 +286,8 @@ Add a `com.h2database:h2` test-scope dependency once, project-wide, if it isn't
286
286
 
287
287
  ## Step 4b: Storyline-Derived Tests (Optional)
288
288
 
289
+ > **Note beats**: an entry with `type: STORYLINE_NOTE` is a note the board author placed in the walkthrough — its `description` is markdown context (an assumption, an external condition, a time gap), and its `id` is not a board node. Read it as background for the beats around it, but it is never a step: skip it when looking for COMMAND/EVENT/READMODEL beats or judging which beats are adjacent, and never turn it into test setup or an assertion on its own.
290
+
289
291
  `slice.json` may also carry a `storylines[]` array — narrated walkthroughs where the *same* read
290
292
  model appears as multiple ordered "beats" across one flow. This is a secondary, supplementary
291
293
  source: `specifications[]` (Step 4) remains the primary and default source of test cases. Most