@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 +4 -1
- package/cli.js +13 -2
- package/package.json +1 -1
- package/stacks/axon/templates/.claude/skills/build-automation/SKILL.md +23 -0
- package/stacks/axon/templates/.claude/skills/build-state-change/SKILL.md +33 -0
- package/stacks/axon/templates/.claude/skills/build-state-view/SKILL.md +43 -0
- package/stacks/cratis-csharp/templates/.claude/skills/build-automation/SKILL.md +23 -0
- package/stacks/cratis-csharp/templates/.claude/skills/build-state-change/SKILL.md +28 -0
- package/stacks/cratis-csharp/templates/.claude/skills/build-state-view/SKILL.md +29 -0
- package/stacks/node/templates/.claude/skills/build-automation/SKILL.md +7 -1
- package/stacks/node/templates/.claude/skills/build-state-change/SKILL.md +13 -1
- package/stacks/node/templates/.claude/skills/build-state-view/SKILL.md +24 -1
- package/stacks/supabase/templates/.claude/skills/build-automation/SKILL.md +4 -0
- package/stacks/supabase/templates/.claude/skills/build-state-change/SKILL.md +19 -0
- package/stacks/supabase/templates/.claude/skills/build-state-view/SKILL.md +19 -0
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('
|
|
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
|
@@ -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
|