@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.
- package/README.md +1 -0
- package/cli.js +20 -36
- package/lib/bash-guard.js +39 -1
- package/package.json +1 -1
- package/shared/build-kit/lib/adapters/pocketbase-realtime-adapter.js +14 -7
- package/shared/build-kit/lib/adapters/realtime-adapter.js +65 -2
- package/shared/build-kit/lib/adapters/supabase-realtime-adapter.js +19 -10
- package/shared/build-kit/lib/ralph.js +30 -62
- package/shared/skills/learn-eventmodelers-api/SKILL.md +38 -1
- package/stacks/axon/templates/.claude/skills/build-automation/SKILL.md +2 -0
- package/stacks/axon/templates/.claude/skills/build-state-change/SKILL.md +2 -0
- package/stacks/axon/templates/.claude/skills/build-state-view/SKILL.md +2 -0
- package/stacks/classic/templates/.claude/skills/build-automation/SKILL.md +2 -0
- package/stacks/classic/templates/.claude/skills/build-state-change/SKILL.md +2 -0
- package/stacks/classic/templates/.claude/skills/build-state-view/SKILL.md +2 -0
- package/stacks/kurrent/templates/.claude/skills/build-automation/SKILL.md +2 -0
- package/stacks/kurrent/templates/.claude/skills/build-state-change/SKILL.md +2 -0
- package/stacks/kurrent/templates/.claude/skills/build-state-view/SKILL.md +2 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-elaborating-scenarios/SKILL.md +69 -3
- package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/prototype/SKILL.md +181 -0
- package/stacks/modeling-kit/templates/.claude/skills/prototype/assets/index.template.html +490 -0
- package/stacks/modeling-kit/templates/.claude/skills/prototype/assets/prototype.mjs +224 -0
- package/stacks/modeling-kit/templates/.claude/skills/prototype/assets/runtime.js +434 -0
- package/stacks/modeling-kit/templates/.claude/skills/prototype/assets/slices.example.html +201 -0
- package/stacks/modeling-kit/templates/.claude/skills/prototype/references/api-fallback.md +30 -0
- package/stacks/modeling-kit/templates/kit/CLAUDE.md +2 -0
- package/stacks/node/templates/.claude/skills/build-automation/SKILL.md +2 -0
- package/stacks/node/templates/.claude/skills/build-state-change/SKILL.md +2 -0
- package/stacks/node/templates/.claude/skills/build-state-view/SKILL.md +2 -0
- package/stacks/opencqrs/templates/.claude/skills/build-automation/SKILL.md +2 -0
- package/stacks/opencqrs/templates/.claude/skills/build-state-change/SKILL.md +2 -0
- package/stacks/opencqrs/templates/.claude/skills/build-state-view/SKILL.md +2 -0
- package/stacks/supabase/templates/.claude/skills/build-state-change/SKILL.md +2 -0
- package/stacks/supabase/templates/.claude/skills/build-state-view/SKILL.md +2 -0
- package/stacks/umadb/templates/.claude/skills/build-automation/SKILL.md +2 -0
- package/stacks/umadb/templates/.claude/skills/build-state-change/SKILL.md +2 -0
- 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
|