@eventmodelers/cli 1.0.27 → 1.0.29
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/shared/skills/learn-eventmodelers-api/SKILL.md +3 -3
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-brainstorming-events/SKILL.md +13 -10
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-elaborating-scenarios/SKILL.md +1 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/SKILL.md +7 -4
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-orchestrating-event-modeling/SKILL.md +8 -6
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-plotting-events/SKILL.md +3 -1
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-storyboarding-events/SKILL.md +13 -0
- package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-validating-event-models-checklist/SKILL.md +2 -2
- package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +4 -3
package/package.json
CHANGED
|
@@ -388,11 +388,11 @@ Get a single node.
|
|
|
388
388
|
---
|
|
389
389
|
|
|
390
390
|
### POST `/api/org/:orgId/boards/:boardId/nodes/:nodeId/auto-connect`
|
|
391
|
-
Auto-connect a node to its timeline neighbors — mirrors the frontend's auto-connect-on-place behavior. Looks only at the node's own timeline column and the previous column (never ahead), and creates `edge:added` events to every type-compatible neighbor found there, using the same pairing rules as connections created via node events (COMMAND→EVENT, SCREEN→COMMAND, EVENT→READMODEL, READMODEL→SCREEN, READMODEL→AUTOMATION, AUTOMATION→COMMAND).
|
|
391
|
+
Auto-connect a node to its timeline neighbors — mirrors the frontend's auto-connect-on-place behavior. Looks only at the node's own timeline column and the previous column (never ahead), and creates `edge:added` events to every type-compatible neighbor found there, using the same pairing rules as connections created via node events (COMMAND→EVENT, SCREEN→COMMAND, EVENT→READMODEL, READMODEL→SCREEN, READMODEL→AUTOMATION, AUTOMATION→COMMAND).
|
|
392
392
|
|
|
393
393
|
Incompatible or already-connected neighbors are reported in `skipped`, not an error. Returns an empty result for nodes not placed on any timeline, or not a connectable element type (e.g. SCENARIO/spec nodes are never auto-connected).
|
|
394
394
|
|
|
395
|
-
|
|
395
|
+
A COMMAND is driven by exactly one upstream trigger — one SCREEN or one AUTOMATION, never both. The previous column's SCREEN/AUTOMATION is skipped whenever the COMMAND already has an inbound trigger, whether that's a SCREEN or AUTOMATION sitting in its own column, or a pre-existing inbound edge already in the DB (e.g. from a prior auto-connect run or a manual connection).
|
|
396
396
|
|
|
397
397
|
**Connections (both auto-connect and `set_connection`) only ever pair nodes on the same timeline** — a node in Chapter A can never be wired directly to a node in Chapter B, even when the type pair is otherwise valid (e.g. EVENT→READMODEL). No direct cross-timeline connection is possible.
|
|
398
398
|
|
|
@@ -411,7 +411,7 @@ Create a single type-checked directed edge between two existing nodes — the RE
|
|
|
411
411
|
|
|
412
412
|
**Response**: `200`/`201` — `{ edgeId, source, target }` on success · `400` — the pair is not one of the allowed type combinations · `404` — a node id doesn't exist
|
|
413
413
|
|
|
414
|
-
**`EVENT → READMODEL` is exempt from column ordering** — an event in a later column can connect to a read model in an earlier column,
|
|
414
|
+
**`EVENT → READMODEL` is conditionally exempt from column ordering** — an event in a later column can connect to a read model in an earlier column, but only when that read model already feeds an AUTOMATION (i.e. a `READMODEL → AUTOMATION` edge already exists). Such an accumulator read model is a continuously-listening projection, not a point-in-time action, so it can go on collecting events from anywhere later on its timeline (e.g. a running total feeding a downstream process). A plain display read model with no automation still rejects a backward connection — wire the `READMODEL → AUTOMATION` edge first if the backward connect is rejected and you expect this exemption to apply. This exemption is deliberate-only via this endpoint — auto-connect never infers a backward `EVENT → READMODEL` pairing. Every other pair (`SCREEN → COMMAND`, `COMMAND → EVENT`, `READMODEL → SCREEN`, `READMODEL → AUTOMATION`, `AUTOMATION → COMMAND`) is always forward-only, no exceptions. If a connection you expect to work gets rejected, retry once before concluding it's blocked — a transient rejection has been observed on an otherwise-valid pair.
|
|
415
415
|
|
|
416
416
|
---
|
|
417
417
|
|
package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-brainstorming-events/SKILL.md
CHANGED
|
@@ -207,8 +207,8 @@ curl -s -X POST "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/nodes/events" \
|
|
|
207
207
|
}]'
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
-
**Stack timelines vertically so they do not overlap.**
|
|
211
|
-
After creating each chapter, position it below the previous one. Use `y = index * 1200` (0-based creation order), `x = 0`. If existing chapters are already on the board, query their positions first
|
|
210
|
+
**Stack timelines vertically so they do not overlap, and in general place new chapters close to existing ones they relate to.**
|
|
211
|
+
After creating each chapter, position it below the previous one. Use `y = index * 1200` (0-based creation order), `x = 0`. If existing chapters are already on the board, query their positions first. Prefer placing the new chapter directly below the existing chapter it is most closely related to (e.g. the same bounded context or an adjacent workflow), rather than mechanically appending below the lowest one — this keeps related chapters visually near each other on the canvas. Only fall back to `y = maxExistingY + 1200` when no related chapter exists yet. Pass this directly as `x`/`y` on `create_chapter` above, or reposition an existing chapter with:
|
|
212
212
|
|
|
213
213
|
**Prefer MCP:**
|
|
214
214
|
```
|
|
@@ -353,6 +353,8 @@ mcp__eventmodelers__submit_node_events {
|
|
|
353
353
|
|
|
354
354
|
> **Never call `drop` after using `cellId` in `node:created`.** The drop endpoint adds a second cell reference without removing the first. `node:created + cellId` is the only placement step needed.
|
|
355
355
|
|
|
356
|
+
> **One EVENT per column (hard rule).** A column is a single moment in the timeline — never place two EVENT nodes in the same column, even when they sit in different swimlane rows (e.g. two systems under Conway's Law). Step A above always appends a fresh column per event, which already satisfies this; if you ever reuse an existing column instead of appending, first confirm no swimlane row in that column already holds an EVENT. **Don't resolve this by checkerboarding across swimlanes** — see "Swimlane Rules (Mandatory)" below: this chapter's own swimlane carries the continuous story; the other swimlane only ever gets an isolated single-column handover, then the story resumes back in this swimlane.
|
|
357
|
+
|
|
356
358
|
### Mode B — Free-form brainstorming (no chapter yet)
|
|
357
359
|
|
|
358
360
|
When chapters have not been created yet, events may be created without `chapterId` or `cellId`. They appear as free-floating sticky notes on the canvas. This is valid during open discovery.
|
|
@@ -366,15 +368,15 @@ An event left without a chapter and cell reference will never appear in any time
|
|
|
366
368
|
|
|
367
369
|
## Swimlane Rules (Mandatory)
|
|
368
370
|
|
|
369
|
-
|
|
371
|
+
**Use swimlanes sparingly — a swimlane exists for exactly one purpose: marking where integration with another system happens. Nothing else justifies one.** Not a different actor, not a different role, not visual grouping, not "an explicit business rule" in the abstract. Every chapter starts with, and in the common case keeps, a single default swimlane holding all of this bounded context's own domain events. Before adding a lane, check whether an existing lane already covers the element's type. If yes, place the element in that lane.
|
|
372
|
+
|
|
373
|
+
**The only valid reason to create a second `swimlane`-type lane: another system's own events cross into this chapter as integration triggers for an automation** (see `eventmodeling-identifying-outputs` Step 5b). Label it for that system and place its trigger events there — never fold them into this chapter's own event swimlane (they are not this bounded context's domain facts) and never treat them as an informal "signal" with no EVENT node at all.
|
|
370
374
|
|
|
371
|
-
**
|
|
372
|
-
- A new actor is introduced that has no existing lane, **or**
|
|
373
|
-
- An explicit business rule requires a distinct lane
|
|
375
|
+
**Never** add a swimlane for any other reason — not a new actor, not a new role, not visual grouping. Human roles get their own **actor** lane during Step 3 (Storyboarding) — a different row type entirely — never a new swimlane here.
|
|
374
376
|
|
|
375
|
-
**
|
|
377
|
+
**When two swimlanes exist side by side, tell a continuous story from this chapter's own swimlane as much as possible — don't checkerboard between them.** Having a second swimlane for an external system does not mean every other column should alternate between "our event" and "their event." Keep a run of consecutive columns in this chapter's own swimlane while the domain process is unfolding, and only place an event in the external swimlane where a real cross-system trigger occurs (the external system reacting to, or producing, an event that actually drives the next step of this chapter's process). If two adjacent columns each have an event in a different swimlane but neither one triggers or is triggered by the other, that's a sign the events were placed for visual symmetry rather than because the story actually crossed system boundaries there — revisit the plotting instead.
|
|
376
378
|
|
|
377
|
-
**
|
|
379
|
+
**An event in the other swimlane is a handover, not a relocation.** It marks a single moment where control passes to the other system — it does not mean the narrative now belongs to that swimlane for a run of columns. Immediately after the handover column, the story resumes in *this* chapter's own swimlane (the next column back in this swimlane), not in the other one. The other swimlane should typically show isolated, single-column events at each genuine handover point, never a multi-column stretch of its own — a long run of consecutive events in the external swimlane is a sign the events belong in a chapter of their own, not this one's.
|
|
378
380
|
|
|
379
381
|
---
|
|
380
382
|
|
|
@@ -502,7 +504,7 @@ Present as a Role Catalog:
|
|
|
502
504
|
```
|
|
503
505
|
|
|
504
506
|
This catalog feeds directly into:
|
|
505
|
-
- **Step 3 (Storyboarding)**: One
|
|
507
|
+
- **Step 3 (Storyboarding)**: One actor lane per human role (not a swimlane — see Swimlane Rules above)
|
|
506
508
|
- **Step 4 (Inputs)**: Every command attributed to a specific role/actor
|
|
507
509
|
- **Step 7 (Scenarios)**: Scenarios reference roles by name
|
|
508
510
|
- **Step 8 (Completeness)**: Verify every role has at least one command path
|
|
@@ -672,4 +674,5 @@ Include error and boundary conditions:
|
|
|
672
674
|
- [ ] No overlapping event semantics — two events don't mean the same thing
|
|
673
675
|
- [ ] Every event is placed into a **named chapter** — no event left in an untitled or default timeline
|
|
674
676
|
- [ ] **Multiple chapters are stacked vertically** (y offset of 1200 per chapter) — no two chapters overlap on the canvas
|
|
675
|
-
- [ ] No unnecessary swimlanes created —
|
|
677
|
+
- [ ] No unnecessary swimlanes created — a swimlane exists only to mark integration with another system; never added for a new actor, a new role, or visual grouping
|
|
678
|
+
- [ ] **When a second (external-system) swimlane exists, events follow one continuous story from this chapter's own swimlane throughout** — the external swimlane only ever holds isolated single-column handovers, never a run of its own, and the story resumes in this chapter's swimlane immediately after each one
|
package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-elaborating-scenarios/SKILL.md
CHANGED
|
@@ -809,7 +809,7 @@ After posting, tell the user:
|
|
|
809
809
|
**Per read model — this is a separate, equally mandatory pass, not an afterthought of the command pass above:**
|
|
810
810
|
- [ ] **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.
|
|
811
811
|
- [ ] **Population scenario** — the view shows correct data after its source event(s)
|
|
812
|
-
- [ ] **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 (see `learn-eventmodelers-api` §3)
|
|
812
|
+
- [ ] **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) — a later event connecting back to an earlier-placed 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 — model the removal/update the forward way instead (the superseding event lives in a later column, so express it as a scenario on a read model placed in that later column, or as a new version/copy of the read model there). Only skip this scenario, with a documented gap (TASK comment), when the superseding event genuinely lives in a different chapter.
|
|
813
813
|
- [ ] **GWT vs. storyline decided per read model, not applied uniformly** — reach for a storyline wherever the *same* read model row genuinely walks through multiple states worth narrating; the rest of the read models in the same model may be correctly GWT-only. Don't default to one format for every read model just because it worked for the first one, and don't judge "multiple states" by counting *distinct connected event types* — that undercounts real candidates. **A single event type recurring with different data is just as valid a storyline driver as several different event types**: `AccountFunded($40)` then `AccountFunded($70)` walking a balance read model from $40 to $110 is exactly as strong a storyline as a multi-event lifecycle. In practice this means almost every list/aggregate read model qualifies — a titles list growing from one row to two as the same `TitleAdded`-shaped event recurs, a dashboard's counters incrementing as the same `CopyAdded` event recurs, are both genuine storylines, not "just" GWT territory. Ask "does replaying this read model's *actually connected* event(s) more than once produce an interesting accumulated/changed state?" — not "how many different event types feed this."
|
|
814
814
|
- [ ] **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.
|
|
815
815
|
- [ ] **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
|
package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-identifying-outputs/SKILL.md
CHANGED
|
@@ -385,7 +385,9 @@ Read models exist to serve the elements already on the board:
|
|
|
385
385
|
|
|
386
386
|
For each SCREEN node, look at its rendered layout and its `meta.fields` and identify its components — groups of fields/UI elements a user would perceive as one area: a stats tile, a list below it, a summary card, a detail panel, a status column in a table, etc. **Most screens genuinely have exactly one component — do not force a split.** A screen has more than one component only when a user would point at two separate areas and describe them as different things.
|
|
387
387
|
|
|
388
|
-
A single homogeneous list/table is still one component, even when its rows draw on many different event types (e.g. a catalog whose per-row status is set by many different lifecycle events scattered across the timeline)
|
|
388
|
+
A single homogeneous list/table is still one component, even when its rows draw on many different event types (e.g. a catalog whose per-row status is set by many different lifecycle events scattered across the timeline) — don't split a table by row or by source event. **But do not use this as a reason to also fold in other fields that don't share that same wide fan-in.** A book's title/author (set by 1-2 events, updated rarely) and a copy's live availability status (derived across its full lifecycle, many events) are two different kinds of computation even when they render on the same visible page — a user pointing at "the book's info" versus "whether it's available right now" is describing two different things, whatever the layout looks like. If a candidate component mixes a field with irreducible wide fan-in and a field that only needs 1-2 events, that is **always** two components — split it now, at this step, before any read model is built from it, rather than discovering the mismatch after the fact.
|
|
389
|
+
|
|
390
|
+
Once a component's fields are genuinely homogeneous (every field needs the same wide-lifecycle fan-in, none of them are cheap identity/fact fields riding along), its read model will legitimately need wide fan-in. This is expected, not a modeling error — document it with a MARKDOWN note (per the orchestrating skill's "Documenting decisions inline, at any step") naming *which field(s)* have that irreducible fan-in and why, e.g. "CopyAvailabilityView.copyStatus is one per-copy status derived across that copy's full lifecycle (repair, loss, reservation, return)." That note documents a specific field's fan-in, not a blanket exemption for the node — re-verify every field on the node against the ">3-events heuristic" (`eventmodeling-orchestrating-event-modeling`) at every later step that touches it, including Step 5i's mandatory per-node verification below, rather than treating a prior note as settled once and for all.
|
|
389
391
|
|
|
390
392
|
After this step is done, **every SCREEN and every AUTOMATION on the board must be connected to at least one read model** via a `READMODEL → SCREEN` or `READMODEL → AUTOMATION` connection, and every screen identified above as having 2+ components must have been broken apart per Step 5c before any read model is placed. If a screen or automation has no incoming read model connection, it is a gap — either a read model is missing or the connection arrow is missing.
|
|
391
393
|
|
|
@@ -413,9 +415,9 @@ Even an automation that looks like a "pure signal relay" (e.g. translating an ex
|
|
|
413
415
|
**Fields**: a todo-list read model's fields describe the pending item — the identity it's about (e.g. `customerId`) plus enough context to act on it (e.g. `email`, `notificationType`). Do not add a `status` field to mark items done — a todo list's "open" state is *membership in the list itself* (the row exists at all), not a status flag on a row that never leaves. If the same underlying data is also useful with an explicit status column for a different consumer, that is a different read model, not this one.
|
|
414
416
|
|
|
415
417
|
**Connections and placement**: the todo-list read model goes in the interaction lane, same column as its automation (actor lane) — the same `READMODEL → AUTOMATION` placement covered in Step 5f below.
|
|
418
|
+
- Connect READMODEL → AUTOMATION **first** (the automation reads its own todo list) — the closing connection below is only accepted once this edge exists.
|
|
416
419
|
- Connect every **opening** EVENT → READMODEL.
|
|
417
|
-
- Connect every **closing** EVENT → READMODEL — including the automation's own resulting event, even though that event is produced by the command this same automation issues. This is not a backward arrow: `EVENT → READMODEL` connections are exempt from column ordering (see `learn-eventmodelers-api` §3)
|
|
418
|
-
- Connect READMODEL → AUTOMATION (the automation reads its own todo list).
|
|
420
|
+
- Connect every **closing** EVENT → READMODEL — including the automation's own resulting event, even though that event is produced by the command this same automation issues. This is not a backward arrow: `EVENT → READMODEL` connections are exempt from column ordering when the read model already feeds an AUTOMATION (see `learn-eventmodelers-api` §3) — this todo-list read model qualifies because of the `READMODEL → AUTOMATION` edge above. A read model in this shape is a live projection, not a frozen snapshot — a later event closing an earlier-opened item is the normal case, not an exception to reach for only when convenient.
|
|
419
421
|
|
|
420
422
|
After this step, every AUTOMATION on the board must have an incoming `READMODEL → AUTOMATION` connection to a todo-list read model — treat a connectionless automation exactly like a connectionless screen with no exemption: a gap to fix, not something to note and move past.
|
|
421
423
|
|
|
@@ -651,6 +653,7 @@ Do not declare Step 5 complete on the strength of the read models you happened t
|
|
|
651
653
|
3. If it's an AUTOMATION and not connected, it is **never** exempt — go back to Step 5b and identify its todo-list read model now.
|
|
652
654
|
4. If a SCREEN is neither connected nor exempt, it is an **unresolved gap**. Fix it now: design the missing read model (pulling from its `meta.fields`/`mapping` as above) and wire the connection. Do not move to Step 6 with an unresolved gap silently carried forward — either fix it or explicitly flag it to the user as accepted debt.
|
|
653
655
|
5. Does any screen still carry more than one component undivided (a Step 5a/5c miss)? If so, break it apart now per Step 5c before counting it as resolved.
|
|
656
|
+
6. **Re-fetch every READMODEL too** and run the >3-events heuristic (`eventmodeling-orchestrating-event-modeling`) on each one field by field — including read models a MARKDOWN note already justified as a roll-up. A prior note documents one field's irreducible fan-in; it does not exempt the rest of that node's fields from this check. The failure mode this catches: a wide-fan-in field (e.g. live per-copy availability) bundled together with a cheap, low-fan-in identity/fact field (e.g. a title set by 1-2 events) that has nothing to do with the roll-up — that pairing is always two read models, never one, no matter how the note reads.
|
|
654
657
|
|
|
655
658
|
List the result of this pass (connected / exempt / fixed) for every screen and automation checked — this list is the evidence the orchestrator's Step 5 gate ("every screen data need is satisfied by a read model") actually holds, not just an assumption.
|
|
656
659
|
|
|
@@ -772,7 +775,7 @@ Identify UI needs without event sources:
|
|
|
772
775
|
- [ ] **No read model spans more than one component** — a screen with N distinct components gets N read models and N highlighted screen copies (same screen name, one component crisp per copy), not one screen-wide read model
|
|
773
776
|
- [ ] **Every multi-component screen was broken apart in Step 5c** — each copy keeps the original screen's name, differs only in which component is marked/highlighted
|
|
774
777
|
- [ ] **Every automation's todo-list read model identifies its opening and closing events** (Step 5b) — including the automation's own resulting event as a closing event where applicable, not just the triggering event
|
|
775
|
-
- [ ] **
|
|
778
|
+
- [ ] **A field's genuinely irreducible wide fan-in is documented per-field** (inline MARKDOWN note, per "Documenting decisions inline") — and that note is never treated as clearing every other field on the same read model from the >3-events check; a cheap identity/fact field bundled alongside a wide roll-up field is always split out, never excused by the roll-up's own note
|
|
776
779
|
- [ ] Every read model has clear purpose
|
|
777
780
|
- [ ] Every data field has event source
|
|
778
781
|
- [ ] Update logic for each event is explicit
|
|
@@ -43,16 +43,18 @@ A read model does not necessarily serve a whole screen — **one component in a
|
|
|
43
43
|
|
|
44
44
|
A screen-wide read model that has to aggregate from events scattered across many columns is the most common source of unnecessary coupling — a "god read model." A screen with a stats row and a list below it is two components: two read models and two screen copies of the *same page* (same screen name on both copies; the copies differ only in which component is marked/highlighted crisp while the other is blurred/dimmed), not one screen-wide read model. Each narrower read model then sits naturally close to its own source event, and the `EVENT → READMODEL → SCREEN` chain for each component stays short and forward. See `eventmodeling-identifying-outputs`'s "Step 5a — Enumerate consumers and identify components" and "Step 5c — Break apart multi-component screens into copies" for the mechanics — apply this during Step 5, before the placement problem exists, rather than reordering columns to patch it afterward.
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
The narrow exception to "prefer a new read model" is a single homogeneous list/table (e.g. a catalog whose per-row status comes from many different lifecycle events): that is still **one** component — you cannot split "one field's value" into multiple read models just because many event types feed it, and a field like that legitimately keeps its wide fan-in. Reach for this exception only after genuinely failing to find a split, not as the default read. **But treat this as a per-field fact about one specific field's irreducible fan-in, never as a label you apply to the whole read model or the whole screen.** There is no "roll-up component" exemption that, once invoked, clears every other field sharing that node from scrutiny — that shortcut is exactly how a god read model smuggles in unrelated fields (e.g. bundling a book's title/author — needing only its own 1-2 events — into the same node as a copy's live availability status, just because both happen to render on the same page). Run the checks below on **every** read model, every time, regardless of whether some field on it was already justified as wide.
|
|
47
47
|
|
|
48
|
-
### The >3-events heuristic — evaluate,
|
|
48
|
+
### The >3-events heuristic — evaluate every field, every time
|
|
49
49
|
|
|
50
|
-
Whenever a read model ends up connected to more than 3 events,
|
|
50
|
+
**Wide fan-in is the absolute exception, not a comfortable resting point.** Default to assuming a wide read model is wrong and a new, narrower read model is almost always the better answer — the burden of proof is on keeping the connections, not on splitting them. Whenever a read model ends up connected to more than 3 events, your first hypothesis should be "this can and should become two (or more) read models," and you only abandon that hypothesis after the two checks below genuinely fail to find a split — not because the component looks homogeneous at a glance, not because a prior step called it a roll-up, and not because splitting would mean more nodes/columns on the board. More nodes is a fine outcome; a wide read model that could have been narrower is not.
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
2. **Kind-of-computation split.** Within one visual component, a field needing 1-2 simple, non-overlapping events (e.g. a monotonic counter incremented/decremented by its own dedicated events) is a fundamentally different kind of computation from a field needing wide fan-in across a whole entity's lifecycle (e.g. "is this available right now," which by construction depends on every event that can change that state). When a read model bundles both kinds, split the wide-fan-in field(s) out into their own read model and screen-copy, even if the surrounding fields stay together in a narrower one — don't let a handful of cheap counters justify leaving a genuine roll-up field un-isolated, and don't let a genuine roll-up field justify dragging cheap counters along with it.
|
|
52
|
+
Two distinct checks, both required, and neither is satisfied by having done it once for a different field on the same node:
|
|
54
53
|
|
|
55
|
-
|
|
54
|
+
1. **Field-level minimality.** Re-derive, field by field, exactly which connected events each field actually needs. Any event connected to the read model that no field's `mapping` traces back to is a prunable connection — remove it (`set_connection` with `action: "remove"`), regardless of how the read model got wide.
|
|
55
|
+
2. **Kind-of-computation split.** Within one visual component, a field needing 1-2 simple, non-overlapping events (e.g. a monotonic counter incremented/decremented by its own dedicated events, or an identity/fact field set once and occasionally updated) is a fundamentally different kind of computation from a field needing wide fan-in across a whole entity's lifecycle (e.g. "is this available right now," which by construction depends on every event that can change that state). When a read model bundles both kinds, split the wide-fan-in field(s) out into their own read model and screen-copy — always, with no exception for "but the wide field is a legitimate roll-up so the node as a whole is fine." A legitimate wide field never excuses a narrow field riding along in the same node, and a handful of narrow fields never excuses leaving a wide field un-isolated.
|
|
56
|
+
|
|
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.
|
|
56
58
|
|
|
57
59
|
### No unplaced elements (0,0 nodes)
|
|
58
60
|
|
|
@@ -129,7 +129,9 @@ Work within the chosen timeline only. For each event in the final chronological
|
|
|
129
129
|
| `boardId` | `BOARD_ID` |
|
|
130
130
|
| `timelineId` | the chapter this event belongs to |
|
|
131
131
|
|
|
132
|
-
Reuse existing empty columns, do not blindly create new ones.
|
|
132
|
+
Reuse existing empty columns, do not blindly create new ones. "Empty" means no EVENT anywhere in that column — **never place two EVENT nodes in the same column, even across different swimlanes**; each column is a single moment in the timeline. If the column already holds an EVENT in another swimlane, place this one in a new column instead (see `place-element`'s one-EVENT-per-column rule).
|
|
133
|
+
|
|
134
|
+
**Do not resolve that conflict by checkerboarding.** When a chapter has more than one swimlane (e.g. a second system's events, see `eventmodeling-brainstorming-events`'s Swimlane Rules), the chapter's own swimlane carries the continuous story throughout — do not alternate "our event, their event, our event, their event" column by column just because each needs its own column, and do not give the other swimlane a multi-column run of its own either. An event in the other swimlane is a single-column handover: it marks one moment where control passes to the other system, and the very next column returns to this chapter's own swimlane to continue the story. Two swimlanes trading events every column with no genuine trigger behind each handover is a sign of forcing visual symmetry rather than following the actual causal order — re-sequence instead. If the other swimlane ends up needing a long run of its own consecutive events, that's a sign those events belong in a chapter of their own, not this one.
|
|
133
135
|
|
|
134
136
|
Process events in order, one at a time. Do not skip any event.
|
|
135
137
|
|
package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-storyboarding-events/SKILL.md
CHANGED
|
@@ -613,6 +613,17 @@ Examples:
|
|
|
613
613
|
|
|
614
614
|
Human roles get SCREEN nodes placed in their own labeled actor lane. System actors and processors get AUTOMATION nodes placed in the shared default actor lane. Place both types during storyboarding — do not defer automations to a later step.
|
|
615
615
|
|
|
616
|
+
### Maintain a Consistent Actor Perspective Across Columns
|
|
617
|
+
|
|
618
|
+
Tell the story from **one actor's perspective as much as possible**. When walking the timeline column by column, keep placing screens in the same role's lane as the previous screen unless the process genuinely hands the story off to a different actor (e.g. a Member requests a reservation, then the Librarian must act on that request — that's a real handoff, place the Librarian's screen next). Do not alternate actors column-to-column just because a different role's read model also changed at that point — a state change only earns its own screen when someone would actually look at it as the next beat in the story, not for symmetry between lanes.
|
|
619
|
+
|
|
620
|
+
Before placing a screen, ask "whose turn is it in the story?" If the answer is still the same actor as the last screen, keep the story there — it's fine, and expected, for a stretch of consecutive columns to belong entirely to one actor while another actor's lane sits empty. It's also fine to occasionally cut away to show what another actor sees, but that should read as a deliberate narrative beat, not a mechanical alternation.
|
|
621
|
+
|
|
622
|
+
```
|
|
623
|
+
✅ Consistent: Member screens in cols 1, 2, 3 (their whole request flow), then Librarian screens in cols 4, 5 (their whole fulfillment flow)
|
|
624
|
+
❌ Checkerboard: Librarian, Member, Librarian, Member, Librarian across cols 1-5 with no real handoff driving each switch
|
|
625
|
+
```
|
|
626
|
+
|
|
616
627
|
### One Screen Per Column (Hard Rule)
|
|
617
628
|
|
|
618
629
|
**Never place more than one SCREEN node in the same column**, even across different actor lanes. Each column represents a single moment in the timeline. Two screens in the same column means two different interactions at the same moment — that breaks the visual narrative and always signals a design error.
|
|
@@ -722,6 +733,7 @@ Failure produces: [Event]
|
|
|
722
733
|
|
|
723
734
|
- [ ] **Every screen has rendered content** — HTML pages by default (`create_screen`/`render_screen` with `contentType: "html"` returned success), or a wireframe sketch only when explicitly requested — no exceptions
|
|
724
735
|
- [ ] **No column contains more than one SCREEN node** across all actor lanes
|
|
736
|
+
- [ ] **Screens follow one actor's perspective across consecutive columns** — no mechanical alternation between two actors' lanes column-to-column; a different actor's screen appears only where the story genuinely hands off to them
|
|
725
737
|
- [ ] Every screen's wireframe shows real field labels matching the event/command fields
|
|
726
738
|
- [ ] Every displayed field has a source event
|
|
727
739
|
- [ ] Every user action maps to a command
|
|
@@ -747,6 +759,7 @@ Failure produces: [Event]
|
|
|
747
759
|
3. **Completeness**: All needed data is visible
|
|
748
760
|
4. **Clarity**: UI clearly shows system state
|
|
749
761
|
5. **Consistency**: Same data presented consistently across screens
|
|
762
|
+
6. **Narrative Continuity**: Tell the story from one actor's perspective across consecutive columns; switch actors only for a genuine handoff, not for symmetry between lanes
|
|
750
763
|
|
|
751
764
|
## Common Patterns
|
|
752
765
|
|
|
@@ -65,7 +65,7 @@ Use the board nodes as the model input. After the checklist, use `handle-comment
|
|
|
65
65
|
- Check 1.3: No hard dependencies between command handlers (orchestrated via events only)
|
|
66
66
|
- Check 1.4: Each command is issued by exactly one thing — no COMMAND node has more than one inbound SCREEN/AUTOMATION edge
|
|
67
67
|
|
|
68
|
-
**Anti-pattern to catch**: Sharing state across handlers or treating state as persistent aggregate; a command wired from two issuers (
|
|
68
|
+
**Anti-pattern to catch**: Sharing state across handlers or treating state as persistent aggregate; a command wired from two issuers — auto-connect itself now guards against this (see `learn-eventmodelers-api` §3), so a double-issuer command found on the board is most likely a manual `set_connection` call or an edge left over from before that guard existed, not a fresh auto-connect artifact
|
|
69
69
|
|
|
70
70
|
### Phase 2: Event Quality Validation (3 checks)
|
|
71
71
|
- Check 2.1: Events represent domain facts, not calculations
|
|
@@ -221,7 +221,7 @@ distinctly-named command (or the SCREEN issuing it directly, with the automation
|
|
|
221
221
|
not two issuers sharing one command.
|
|
222
222
|
```
|
|
223
223
|
|
|
224
|
-
**Why**: A command is never issued by more than one thing. Each command represents one specific trigger's decision to act — collapsing two triggers onto one command node hides which actor is actually responsible, and usually means either a naming/slice-boundary mistake or a stray auto-connect
|
|
224
|
+
**Why**: A command is never issued by more than one thing. Each command represents one specific trigger's decision to act — collapsing two triggers onto one command node hides which actor is actually responsible, and usually means either a naming/slice-boundary mistake or a stray manual/pre-existing edge (auto-connect itself now guards against this — see `learn-eventmodelers-api` §3). Fix by removing the extra edge via `set_connection` (`action: "remove"`), not by keeping both.
|
|
225
225
|
|
|
226
226
|
---
|
|
227
227
|
|
|
@@ -237,6 +237,8 @@ Using the `timelineData` already fetched in Step 3 (re-fetch if a column was jus
|
|
|
237
237
|
|
|
238
238
|
Cell IDs are always `<rowId>-<columnId>` — no cell array search needed.
|
|
239
239
|
|
|
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
|
+
|
|
240
242
|
**Check if the cell is already occupied.**
|
|
241
243
|
|
|
242
244
|
**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):
|
|
@@ -253,6 +255,7 @@ curl -s "$BASE_URL/api/org/$ORG_ID/boards/$BOARD_ID/nodes?cellId=$CELL_ID"
|
|
|
253
255
|
|---|---|---|
|
|
254
256
|
| `READMODEL` | `COMMAND` (state-change slice already owns this column) | Insert a **new column immediately after** the current column (not at the end) and use that new column as the target. |
|
|
255
257
|
| `SCREEN` (view/output screen) | any | Same as READMODEL — insert immediately after. |
|
|
258
|
+
| `EVENT` | `EVENT` in **any other swimlane row of the same column** | Insert a new column immediately after — an EVENT never shares a column with another EVENT, even across different swimlanes. |
|
|
256
259
|
| Any | Same element type | Stop and tell the user — true conflict, no safe default. |
|
|
257
260
|
| Any | Different type but not a known pairing | Stop and tell the user. |
|
|
258
261
|
|
|
@@ -448,9 +451,7 @@ Response: `{ "hashes": { "<event-uuid>": "<hash>" } }`
|
|
|
448
451
|
|
|
449
452
|
## Step 7c — Verify the command has exactly one issuer
|
|
450
453
|
|
|
451
|
-
**A command is never issued by more than one thing.**
|
|
452
|
-
|
|
453
|
-
**Why this can go wrong**: auto-connect only skips the previous column's SCREEN when the COMMAND's own column already has a SCREEN — it does not check for an AUTOMATION there. So a COMMAND whose own column holds an AUTOMATION, with a SCREEN sitting in the previous column, ends up wired from *both* — the automation (same column) and the screen (previous column) — and now looks issued by two things.
|
|
454
|
+
**A command is never issued by more than one thing.** The server's fire-and-forget auto-connect (`learn-eventmodelers-api` §3) already enforces this — it skips wiring the previous column's SCREEN/AUTOMATION into a COMMAND that already has an inbound trigger, whether that trigger came from the COMMAND's own column or from a pre-existing edge in the DB. Still, run this check whenever `elementType` is `SCREEN`, `AUTOMATION`, or `COMMAND` as a sanity check — e.g. a manual `set_connection` call, or an edge created before this guard existed, can still leave a COMMAND with two issuers.
|
|
454
455
|
|
|
455
456
|
After placing, resolve the relevant COMMAND node (the one just placed, or the one in the same/adjacent column as the SCREEN/AUTOMATION just placed) and inspect its edges:
|
|
456
457
|
|