@eventmodelers/cli 1.0.32 → 1.0.34
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/package.json +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-elaborating-scenarios/SKILL.md +40 -23
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/SKILL.md +10 -0
- package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +2 -0
package/package.json
CHANGED
package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-elaborating-scenarios/SKILL.md
CHANGED
|
@@ -13,31 +13,40 @@ allowed-tools:
|
|
|
13
13
|
|
|
14
14
|
Prefer `mcp__eventmodelers__*` tools when available (registered by the `connect` skill) — the curl blocks below are the fallback for sessions without MCP connected.
|
|
15
15
|
|
|
16
|
-
##
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
**
|
|
27
|
-
|
|
28
|
-
|
|
16
|
+
## GWT vs. Storyline — Decision Rule
|
|
17
|
+
|
|
18
|
+
A GWT scenario asserts one isolated transition (one precondition → one action → one outcome). A
|
|
19
|
+
storyline instead narrates one use case as an ordered sequence of **beats**, walking the *same*
|
|
20
|
+
element (usually a read model) through multiple states in one flow — something no single GWT can
|
|
21
|
+
express.
|
|
22
|
+
|
|
23
|
+
**Commands: always GWT.** A command has no state progression to narrate — it validates one
|
|
24
|
+
input against one state and either succeeds or is rejected. Never write a storyline for a command.
|
|
25
|
+
|
|
26
|
+
**Read models: storyline when the read model goes through a series of clear state transitions
|
|
27
|
+
driven by events; GWT otherwise.** Decide this **per read model**, not once for the whole pass:
|
|
28
|
+
does replaying this read model's actually-connected event(s) more than once produce an interesting
|
|
29
|
+
accumulated/changed state worth narrating? A single event type recurring with different data counts
|
|
30
|
+
just as much as a multi-event lifecycle — e.g. `AccountFunded($40)` then `AccountFunded($70)`
|
|
31
|
+
walking a balance from $40 to $110 is a genuine storyline driver. **Todo lists are a prime
|
|
32
|
+
candidate**: an item appears when opened and disappears when completed, which is exactly the kind
|
|
33
|
+
of state progression a storyline is for — as is any read model whose rows accumulate, update, or
|
|
34
|
+
get removed across a sequence of events (balances, counters, statuses). Yes → storyline. No →
|
|
35
|
+
GWT-only. Make this call before drafting any payload — a batch pass that reuses one schema for
|
|
36
|
+
every command *and* every read model is a sign the per-read-model judgment got skipped.
|
|
37
|
+
|
|
38
|
+
Even where a read model gets a storyline, still write GWTs for the specific scenarios the
|
|
39
|
+
storyline doesn't cover — validation failures, cross-context sourcing, or any transition outside
|
|
40
|
+
the narrated flow. Storyline and GWT are complementary, not exclusive: the storyline covers the
|
|
41
|
+
narrated lifecycle, GWT covers everything else about that read model.
|
|
29
42
|
|
|
30
43
|
**Example** — a customer-activation walkthrough of a Todos read model:
|
|
31
|
-
1. **Beat 1** —
|
|
32
|
-
2. **Beat 2** — after
|
|
33
|
-
|
|
34
|
-
3. **Beat 3** — after a `CustomerActivated` event, the same Todos read model is empty again (the
|
|
35
|
-
todo was completed and removed).
|
|
44
|
+
1. **Beat 1** — Todos read model, empty.
|
|
45
|
+
2. **Beat 2** — after `CustomerRegistered`, Todos shows one entry ("activate your account").
|
|
46
|
+
3. **Beat 3** — after `CustomerActivated`, Todos is empty again (todo completed and removed).
|
|
36
47
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
useful for a stakeholder-facing walkthrough of a use case, not for exhaustive GWT coverage. Keep
|
|
40
|
-
producing ordinary Given/When/Then scenarios for everything else.
|
|
48
|
+
A storyline threads all three beats into one walkthrough; three separate GWTs could each assert one
|
|
49
|
+
transition but not the lifecycle.
|
|
41
50
|
|
|
42
51
|
### Storyline data shape
|
|
43
52
|
|
|
@@ -275,6 +284,14 @@ Work through each question with the domain in mind. If the answer is "that situa
|
|
|
275
284
|
|
|
276
285
|
## Workflow
|
|
277
286
|
|
|
287
|
+
### 0. Decide GWT vs. storyline for every READMODEL — before drafting any scenario payload
|
|
288
|
+
|
|
289
|
+
Apply the decision test from "GWT vs. Storyline — Decision Rule" above to every READMODEL on the
|
|
290
|
+
board (or in scope). Produce a visible artifact — one line per READMODEL, "yes → storyline" or
|
|
291
|
+
"no → GWT-only" with a one-clause reason — before writing the first scenario payload, not as a
|
|
292
|
+
checklist review after. Deciding this list up front is what stops a batch pass from silently
|
|
293
|
+
collapsing into one reused schema for every command and read model alike.
|
|
294
|
+
|
|
278
295
|
For each command and view, write scenarios in Given-When-Then format:
|
|
279
296
|
|
|
280
297
|
### 1. Command Scenarios (Given-When-Then)
|
|
@@ -812,7 +829,7 @@ After posting, tell the user:
|
|
|
812
829
|
- [ ] **Every READMODEL on the board has at least one view scenario** — GWT (`given`: source EVENTs, `when`: empty, `then`: the READMODEL) or a storyline. A model with dozens of command scenarios and 0 read-model scenarios is not a complete Step 7 — it's easy to walk away thinking coverage is thorough because the command side looks exhaustive, so check the read-model side explicitly before reporting this step done.
|
|
813
830
|
- [ ] **Population scenario** — the view shows correct data after its source event(s)
|
|
814
831
|
- [ ] **Removal/update scenario, where applicable** — a row disappears or changes (`expectEmptyList: true` for list-type views) after an event that supersedes it (expiry, return, archival, withdrawal, status change, etc.). `EVENT → READMODEL` is exempt from column ordering **only when the read model already feeds an AUTOMATION** (`READMODEL → AUTOMATION` edge; see `learn-eventmodelers-api` §3 and `eventmodeling-orchestrating-event-modeling`'s "No backward arrows") — a later event connecting back to an earlier-placed todo-list read model is normal for that accumulator shape, so add the connection if it's missing rather than assuming the scenario is impossible. If the read model has no automation to feed, the backward connection is rejected — by Step 5 this should already have been modeled the forward way (see `eventmodeling-identifying-outputs` Step 5g's copy pattern: a new read model + screen copy in the later event's column, never a link back). If it wasn't, write the scenario against that forward-placed copy rather than the original. Only skip this scenario, with a documented gap (TASK comment), when the superseding event genuinely lives in a different chapter.
|
|
815
|
-
- [ ] **GWT vs. storyline decided per read model, not applied uniformly** —
|
|
832
|
+
- [ ] **GWT vs. storyline decided per read model, not applied uniformly** — apply the decision test from "GWT vs. Storyline — Decision Rule" above to each read model individually; some may qualify for a storyline while the rest of the same model are correctly GWT-only. Don't default to one format for every read model just because it worked for the first one.
|
|
816
833
|
- [ ] **No redundancy or contradiction between a read model's GWT scenarios and its storyline** — if both exist for the same read model, read the storyline's beats before finalizing the GWTs. A GWT that asserts the same state a beat already shows is redundant (delete it); a GWT written without tracing the same causal sequence the storyline encodes can end up asserting something the storyline's beats actually contradict (e.g. claiming two entities coexist in a view when the storyline correctly shows one superseding the other) — delete or fix it, never leave a contradiction on the board.
|
|
817
834
|
- [ ] **Cross-context read models handled honestly** — if a read model's true source events live in a different chapter, `given` can't reference them (same-timeline-only, like connections); write the scenario with an empty `given` and say so explicitly in the scenario title, rather than silently omitting the scenario or fabricating a same-timeline event that isn't the real source
|
|
818
835
|
|
|
@@ -56,6 +56,16 @@ Two distinct checks, both required, and neither is satisfied by having done it o
|
|
|
56
56
|
|
|
57
57
|
After both checks, a read model may still be wide because one or more of its fields have irreducible, genuinely-single-field fan-in — that is an outcome you document (per "Documenting decisions inline" above, naming the specific field and why), not a category ("roll-up component") you assign to the node up front and stop checking. Re-run this evaluation on every read model at every step that touches it, including ones a prior step already looked at — a field added or a screen re-scoped later can introduce exactly the bundling problem this heuristic exists to catch.
|
|
58
58
|
|
|
59
|
+
### Recognizing a dedicated business event behind a derived condition
|
|
60
|
+
|
|
61
|
+
This is a **business-modeling decision, not a technical one** — it is not triggered by a read model's fan-in count and is not a fix for anything the >3-events heuristic flags. A read model landing on wide fan-in is at most a symptom that might prompt you to ask the question below; it is never the reason to answer it one way or the other, and the question is worth asking regardless of how many events are involved.
|
|
62
|
+
|
|
63
|
+
The question to ask is simply: **is this fact worth a new event?** A derived condition — "is this available right now," "has this moved to its next stage," etc. — is worth its own event when it's a fact a domain expert would recognize and name in its own right (not just "some field I compute"), and when that fact is valuable to a later step in the process — another automation would react to it, a different bounded context would want to subscribe to it, some downstream process needs to trigger off of it. If both hold, it deserves to exist as its own dedicated event — e.g. "the copy was marked available" — not just as derived read-model logic recomputed from several other events.
|
|
64
|
+
|
|
65
|
+
When that's the case, model it as its own event, produced via the same todo-list + automation translation pattern Step 5b uses for external integrations, but triggered internally by whichever raw events can produce that outcome. If the condition is purely for display, with nothing downstream that would ever act on it, it stays a plain read-model projection — no new event needed, no matter how many raw events feed it or how wide the resulting fan-in looks.
|
|
66
|
+
|
|
67
|
+
**Only fold together causes that are genuinely redundant for the same outcome — never causes that carry distinct business meaning.** A derived condition's causes typically split into two groups: several distinct events that all mean the *same* thing from the business's point of view (e.g. `CopyReservationReleased`, `CopyReturned`, and `CopyReturnedFromRepair` all mean "the copy is available again"), and several distinct events that each mean something the business still wants told apart (e.g. `CopyReserved` → Reserved, `CopyCheckedOut` → CheckedOut, `CopySentForRepair` → UnderRepair, `CopyReportedLost` → Lost, `CopyWithdrawn` → Withdrawn). Only the first group is safe to consolidate — a dedicated event like `CopyMarkedAvailable` collapses "N different reasons, same outcome" into one reusable signal without losing information. Collapsing the second group into something like a generic `CopyMarkedUnavailable` would erase the *why*, which some downstream consumer may actually need — leave those as separate events and direct connections, even though the read model's fan-in then stays wider than the fully-consolidated ideal. That remaining fan-in is not a failure of anything — it means those events are each individually meaningful, not synonymous with each other, and collapsing them would have been the actual modeling mistake.
|
|
68
|
+
|
|
59
69
|
### No unplaced elements (0,0 nodes)
|
|
60
70
|
|
|
61
71
|
After each step that creates elements (Steps 1–5), scan for any nodes that have no cell reference and are stranded at the default canvas position (0,0). These arise when `node:created` is called without `cellId`.
|
|
@@ -239,6 +239,8 @@ Cell IDs are always `<rowId>-<columnId>` — no cell array search needed.
|
|
|
239
239
|
|
|
240
240
|
**One EVENT per column (hard rule).** A column represents a single moment in the timeline. When `elementType` is `EVENT`, an occupied column means occupied *at all* — even if the existing EVENT sits in a different swimlane row (e.g. a different system under Conway's Law). Never place two EVENT nodes in the same column just because they're in different swimlanes. Before treating the column as available, check `cells` for **every** row whose `type === "swimlane"`, not only `targetRow` — if any of those cells already holds an EVENT node, treat the column as occupied and insert a new column immediately after (see the table below), not the target column itself.
|
|
241
241
|
|
|
242
|
+
**Same swimlane for events emitted by the same command.** When one command emits more than one event, each event still gets its own column (per the hard rule above), but all of them belong in the *same* swimlane row, placed in adjacent columns immediately after the command. Do not scatter a command's sibling events across different swimlane rows — one command producing multiple events is one system doing multiple things in sequence, not several systems reacting independently.
|
|
243
|
+
|
|
242
244
|
**Check if the cell is already occupied.**
|
|
243
245
|
|
|
244
246
|
**No direct MCP equivalent**: `get_nodes` only filters by `type`, not `cellId` — there is no MCP tool that filters nodes by cell. Instead, use the `meta.timelineData.cells` you already fetched in Step 3 via `get_node` on the chapter/timeline node: `cells` is a sparse array, so a `nodeId` absent from the entry for `CELL_ID` means the cell is empty. Only fall back to the curl call below if you haven't already loaded `timelineData` (e.g. MCP wasn't used in Step 3 either):
|