@eventmodelers/cli 1.0.150 → 1.0.152

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +1 -0
  2. package/cli.js +20 -36
  3. package/lib/bash-guard.js +39 -1
  4. package/package.json +1 -1
  5. package/shared/build-kit/lib/adapters/pocketbase-realtime-adapter.js +14 -7
  6. package/shared/build-kit/lib/adapters/realtime-adapter.js +65 -2
  7. package/shared/build-kit/lib/adapters/supabase-realtime-adapter.js +19 -10
  8. package/shared/build-kit/lib/ralph.js +30 -62
  9. package/shared/skills/learn-eventmodelers-api/SKILL.md +38 -1
  10. package/stacks/axon/templates/.claude/skills/build-automation/SKILL.md +2 -0
  11. package/stacks/axon/templates/.claude/skills/build-state-change/SKILL.md +2 -0
  12. package/stacks/axon/templates/.claude/skills/build-state-view/SKILL.md +2 -0
  13. package/stacks/classic/templates/.claude/skills/build-automation/SKILL.md +2 -0
  14. package/stacks/classic/templates/.claude/skills/build-state-change/SKILL.md +2 -0
  15. package/stacks/classic/templates/.claude/skills/build-state-view/SKILL.md +2 -0
  16. package/stacks/kurrent/templates/.claude/skills/build-automation/SKILL.md +2 -0
  17. package/stacks/kurrent/templates/.claude/skills/build-state-change/SKILL.md +2 -0
  18. package/stacks/kurrent/templates/.claude/skills/build-state-view/SKILL.md +2 -0
  19. package/stacks/modeling-kit/templates/.claude/skills/eventmodeling-elaborating-scenarios/SKILL.md +69 -3
  20. package/stacks/modeling-kit/templates/.claude/skills/place-element/SKILL.md +1 -1
  21. package/stacks/modeling-kit/templates/.claude/skills/prototype/SKILL.md +181 -0
  22. package/stacks/modeling-kit/templates/.claude/skills/prototype/assets/index.template.html +490 -0
  23. package/stacks/modeling-kit/templates/.claude/skills/prototype/assets/prototype.mjs +224 -0
  24. package/stacks/modeling-kit/templates/.claude/skills/prototype/assets/runtime.js +434 -0
  25. package/stacks/modeling-kit/templates/.claude/skills/prototype/assets/slices.example.html +201 -0
  26. package/stacks/modeling-kit/templates/.claude/skills/prototype/references/api-fallback.md +30 -0
  27. package/stacks/modeling-kit/templates/kit/CLAUDE.md +2 -0
  28. package/stacks/node/templates/.claude/skills/build-automation/SKILL.md +2 -0
  29. package/stacks/node/templates/.claude/skills/build-state-change/SKILL.md +2 -0
  30. package/stacks/node/templates/.claude/skills/build-state-view/SKILL.md +2 -0
  31. package/stacks/opencqrs/templates/.claude/skills/build-automation/SKILL.md +2 -0
  32. package/stacks/opencqrs/templates/.claude/skills/build-state-change/SKILL.md +2 -0
  33. package/stacks/opencqrs/templates/.claude/skills/build-state-view/SKILL.md +2 -0
  34. package/stacks/supabase/templates/.claude/skills/build-state-change/SKILL.md +2 -0
  35. package/stacks/supabase/templates/.claude/skills/build-state-view/SKILL.md +2 -0
  36. package/stacks/umadb/templates/.claude/skills/build-automation/SKILL.md +2 -0
  37. package/stacks/umadb/templates/.claude/skills/build-state-change/SKILL.md +2 -0
  38. package/stacks/umadb/templates/.claude/skills/build-state-view/SKILL.md +2 -0
@@ -384,6 +384,8 @@ class {AutomationName}HandlingTest {
384
384
 
385
385
  ### Storyline-Derived Tests (Optional)
386
386
 
387
+ > **Note beats**: an entry with `type: STORYLINE_NOTE` is a note the board author placed in the walkthrough — its `description` is markdown context (an assumption, an external condition, a time gap), and its `id` is not a board node. Read it as background for the beats around it, but it is never a step: skip it when looking for COMMAND/EVENT/READMODEL beats or judging which beats are adjacent, and never turn it into test setup or an assertion on its own.
388
+
387
389
  `slice.json` may also carry a `storylines[]` array — narrated walkthroughs with an ordered
388
390
  `elements[]` "beats" sequence (EVENT/COMMAND/READMODEL/...). This is a secondary, supplementary
389
391
  source; `specifications[]` above stays the primary and default source of test cases. Most slices
@@ -376,6 +376,8 @@ the same reconstruction path production uses.
376
376
 
377
377
  ## Step 6b: Storyline-Derived Tests (Optional)
378
378
 
379
+ > **Note beats**: an entry with `type: STORYLINE_NOTE` is a note the board author placed in the walkthrough — its `description` is markdown context (an assumption, an external condition, a time gap), and its `id` is not a board node. Read it as background for the beats around it, but it is never a step: skip it when looking for COMMAND/EVENT/READMODEL beats or judging which beats are adjacent, and never turn it into test setup or an assertion on its own.
380
+
379
381
  `slice.json` may carry a `storylines[]` array alongside `specifications[]` — narrated walkthroughs
380
382
  where an ordered sequence of `elements[]` "beats" (EVENT/COMMAND/READMODEL/...) shows one use case
381
383
  end to end. This is a secondary, supplementary source — `specifications[]` (Step 7) stays the
@@ -341,6 +341,8 @@ package — which it already does, following this stack's convention (`{SliceNam
341
341
 
342
342
  ## Step 5b: Storyline-Derived Tests (Optional)
343
343
 
344
+ > **Note beats**: an entry with `type: STORYLINE_NOTE` is a note the board author placed in the walkthrough — its `description` is markdown context (an assumption, an external condition, a time gap), and its `id` is not a board node. Read it as background for the beats around it, but it is never a step: skip it when looking for COMMAND/EVENT/READMODEL beats or judging which beats are adjacent, and never turn it into test setup or an assertion on its own.
345
+
344
346
  `slice.json` may also carry a `storylines[]` array — narrated walkthroughs where the *same* read
345
347
  model appears as multiple ordered "beats" across one flow (see `elements[]` on each storyline).
346
348
  This is a secondary, supplementary source: `specifications[]` (Step 4) remains the primary and
@@ -15,6 +15,19 @@ This step applies the shared element rules in **`eventmodeling-core-rules`** —
15
15
 
16
16
  Prefer `mcp__eventmodelers__*` tools when available (registered by the `connect` skill) — the curl blocks below are the fallback for sessions without MCP connected.
17
17
 
18
+ ## Examples Are the Scenario — Non-Negotiable
19
+
20
+ **A scenario without concrete example values is not a scenario.** The element names in
21
+ `given`/`when`/`then` only say *which* facts are involved; the example values say *what* happened
22
+ and *why* the outcome follows — they are the actual specification, and the only thing a generated
23
+ test can assert against. Every value **essential to the scenario** — the ones the rule decides on
24
+ and the ones that tie the steps together — is filled in as `fields: [{name, example}]` on the
25
+ step it belongs to (`given` events, the `when` command, `then` events, `context`/`then` read
26
+ models, storyline beats), and list read models carry their expected rows in `examples` (or
27
+ `expectEmptyList: true`). Never post a scenario, never report one done, and never mark this step
28
+ complete while an essential value is missing. Details: "Fill the values essential to the scenario"
29
+ below.
30
+
18
31
  ## GWT vs. Storyline — Decision Rule
19
32
 
20
33
  A GWT scenario asserts one isolated transition (one precondition → one action → one outcome). A
@@ -50,6 +63,52 @@ narrated lifecycle, GWT covers everything else about that read model.
50
63
  A storyline threads all three beats into one walkthrough; three separate GWTs could each assert one
51
64
  transition but not the lifecycle.
52
65
 
66
+ ## Notes — Last Resort Only
67
+
68
+ A scenario can carry a `contextNote` (markdown), and a storyline can hold note beats
69
+ (`{type: "STORYLINE_NOTE", refId: "", note: "…"}`). **Notes are only for what cannot be expressed
70
+ with events and the other elements.** Before writing one, ask: could this be a `given` event, a
71
+ `context` read model, a field example on the command, or a `then` outcome? If yes, model it that
72
+ way — a scenario that tells its story in a note instead of in events is incomplete, and code
73
+ generated from it can't test what's only in prose.
74
+
75
+ Legit uses: a condition outside the system ("the payment provider is in maintenance"), a business
76
+ assumption or regulation behind the case, why the case matters, a time gap between storyline beats
77
+ ("14 days later"). Not legit: "the customer has already ordered twice" (→ `given` events), "the
78
+ cart total is 120 €" (→ field examples), "an email is sent" (→ a `then` event), restating the
79
+ title. Most scenarios need no note; keep any note to a few lines.
80
+
81
+ ### Fill the values essential to the scenario — mandatory
82
+
83
+ The goal is not to fill every field, but every value **the scenario depends on**: change one of
84
+ them and the outcome changes, or the steps no longer belong together. Essential are:
85
+
86
+ - **the values the rule decides on** — the dates of an expiry rule, the wrong token, the outcome
87
+ `Unclear` that routes a check into review, the second registration's duplicate email;
88
+ - **the ids linking the steps** — the same `customerId` in the given events, the command and the
89
+ outcome, so it is clear they are about the same thing;
90
+ - **the asserted outcome** — the `then` event's or read model's values that the scenario expects
91
+ (the `status: Rejected` and its reason, the row that appears in the list).
92
+
93
+ **Leave every other field empty — that is best practice, not laziness.** The empty fields are
94
+ what make it clear which data is relevant: a reader sees at a glance that this case turns on the
95
+ dates and the token, not on the name, address or phone number. Padding every step with every field
96
+ buries the values that carry the rule. The test for the result: someone reading only the filled
97
+ values can tell why the outcome follows. A
98
+ scenario whose essential values are missing is incomplete, even when a note explains the
99
+ situation: the fields are the specification, the note is not.
100
+
101
+ The values are what make the rule visible and testable. For an expiry rule, the dates go into the
102
+ fields — `CustomerRegistered.registeredAt: 2026-10-12`, `ConfirmEmail.confirmedAt: 2026-10-13` —
103
+ not into a note saying "registered on 2026-10-12, confirms on 2026-10-13". Keep ids consistent
104
+ across the steps of one scenario (the same `customerId` in every given event, the command and the
105
+ outcome), and pick values that sit on the interesting side of the rule (one day past the limit, not
106
+ a vague "long ago").
107
+
108
+ Read the elements' field names once (`get_nodes` with `projection: "fields"`, scoped by
109
+ `chapterId`) and pick the essential ones per scenario from that. If a value the rule depends on has no field on any element (e.g. the expiry rule needs a
110
+ timestamp no event carries), add that field to the element — don't move the value into the note.
111
+
53
112
  ### Storyline data shape
54
113
 
55
114
  ```json
@@ -77,6 +136,8 @@ transition but not the lifecycle.
77
136
  READMODEL. Beats are only READMODEL or EVENT — no COMMAND, SCREEN or other types. Every `refId`
78
137
  must be on the same timeline.
79
138
  - `isError` marks a beat as an alternate/error branch off the previous beat.
139
+ - A note beat (`type: "STORYLINE_NOTE"`, `refId: ""`, markdown in `note`) may sit anywhere and is
140
+ skipped by the order rules — see "Notes — Last Resort Only" above before adding one.
80
141
  - `fields`, `expectEmptyList`, `exampleMode`, `examples` mirror the same fields GWT scenario steps
81
142
  use, letting a beat show concrete example data the same way a "then" readmodel step does.
82
143
 
@@ -449,7 +510,8 @@ Rejection scenario example:
449
510
  "expectError": true,
450
511
  "errorDescription": "reservation date is in the past",
451
512
  "given": [],
452
- "when": [{"id": "<commandNodeId>", "title": "Reservation Command", "type": "COMMAND"}],
513
+ "when": [{"id": "<commandNodeId>", "title": "Reservation Command", "type": "COMMAND",
514
+ "fields": [{"name": "reservationId", "example": "r-123"}, {"name": "date", "example": "2026-10-12"}]}],
453
515
  "then": [],
454
516
  "examples": [],
455
517
  "expectEmptyList": false
@@ -463,8 +525,10 @@ State-violation rejection (given events establish the wrong state):
463
525
  "title": "Reject confirming an already-confirmed reservation",
464
526
  "expectError": true,
465
527
  "errorDescription": "reservation already confirmed",
466
- "given": [{"id": "<eventNodeId>", "title": "ReservationConfirmed", "type": "EVENT"}],
467
- "when": [{"id": "<commandNodeId>", "title": "Confirm Reservation", "type": "COMMAND"}],
528
+ "given": [{"id": "<eventNodeId>", "title": "ReservationConfirmed", "type": "EVENT",
529
+ "fields": [{"name": "reservationId", "example": "r-123"}, {"name": "confirmedAt", "example": "2026-10-12"}]}],
530
+ "when": [{"id": "<commandNodeId>", "title": "Confirm Reservation", "type": "COMMAND",
531
+ "fields": [{"name": "reservationId", "example": "r-123"}]}],
468
532
  "then": [],
469
533
  "examples": [],
470
534
  "expectEmptyList": false
@@ -494,6 +558,8 @@ After posting, tell the user:
494
558
  **No command has only 2 scenarios unless all other types were reviewed and found inapplicable.**
495
559
 
496
560
  **Per read model — this is a separate, equally mandatory pass, not an afterthought of the command pass above:**
561
+ - [ ] **Essential example values filled (blocking — no exceptions)** — every value the rule decides on, the ids linking the steps and the asserted outcome carry a concrete `example` on their step; every other field left empty, so it is clear which data is relevant; a scenario whose values only appear in its note is not done
562
+ - [ ] **Notes carry nothing an element could** — every `contextNote` and note beat holds only background no event, command field or read model can express; anything else is moved into `given`/`when`/`then`/`context` and the note removed
497
563
  - [ ] **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.
498
564
  - [ ] **Population scenario** — the view shows correct data after its source event(s)
499
565
  - [ ] **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.
@@ -138,7 +138,7 @@ mcp__eventmodelers__add_scenario {
138
138
  }
139
139
  ```
140
140
 
141
- Same rules apply (context=READMODELs only, given=EVENTs only, when=at most one COMMAND or QUERY, then=EVENTs only or exactly one READMODEL, `expectError`+`errorDescription` for error cases). This auto-creates the spec node if the cell is empty, same as the curl endpoint.
141
+ Same rules apply (context=READMODELs only, given=EVENTs only, when=at most one COMMAND or QUERY, then=EVENTs only or exactly one READMODEL, `expectError`+`errorDescription` for error cases). The values essential to the scenario — what the rule decides on, the ids linking the steps, the asserted outcome — go into each step's `fields: [{name, example}]`; every other field stays empty so it is clear which data is relevant — see `eventmodeling-elaborating-scenarios` "Fill the values essential to the scenario". This auto-creates the spec node if the cell is empty, same as the curl endpoint.
142
142
 
143
143
  **Fallback (no MCP):** see `references/api-fallback.md` — "Step 4a — SCENARIO: append scenarios via the spec endpoint" for the full curl call and example payload.
144
144
 
@@ -0,0 +1,181 @@
1
+ ---
2
+ name: prototype
3
+ description: Build a clickable, event-sourced prototype of one context (a MODEL_CONTEXT or a timeline's effective context) as a single HTML page the platform serves — React standalone, no build step, no server logic. Built slice by slice — one block per slice, each with a hash of its slice — so a re-run regenerates only the slices that changed on the board. Screens become tabs (one per screen name) rebuilt 1:1 from the board's HTML screens or sketches; each read model is one component, each command the inputs and button that fire it; events are recorded in an in-browser event store and can be browsed; logic and todo lists follow the GWT scenarios, which also run as an in-page check; automations appear in their own tab when their todo list has items. Read-only on the board. Use for "build a prototype of X", "make the Registration context clickable", "update the prototype".
4
+ ---
5
+
6
+ # Prototype — one context, clickable in the browser
7
+
8
+ > **Before doing anything else**, invoke the `connect` skill — if not already connected — to resolve `TOKEN`, `BOARD_ID`, `ORG_ID`, and `BASE_URL`. Do not proceed until it has completed.
9
+
10
+ Prefer `mcp__eventmodelers__*` tools for reading. The curl fallback is in `references/api-fallback.md`.
11
+
12
+ **The result is one file**, `<out>/index.html` (default `out` = `prototypes/<context-slug>/`, `context-slug` = kebab-case name). The board app shows it in a sandboxed iframe (the ▶ button on each timeline), and the libraries it loads come from the app's `public/vendor/` (React 18.3.1, ReactDOM, Babel standalone 7.24.7, the CSS frameworks the HTML screens use). Nothing else is needed at run time: the page compiles its own JSX, keeps its events in the browser, checks its own scenarios on load and shows any failure as a red banner.
13
+
14
+ It is put together from:
15
+
16
+ | Part | What it is | Who writes it |
17
+ |---|---|---|
18
+ | `assets/index.template.html` | the page: platform library tags, the prototype kit (hooks, tabs, event browser, automations tab, simulate/specs/clock panel) and the loader | fixed — never edited per prototype |
19
+ | `assets/runtime.js` | event store, command bus, automation runner, scenario runner, slice registry | fixed — pasted in by `assemble` |
20
+ | **one slice block per slice** | everything of one slice: its command / read model / automation, its scenarios, its part of the screen | **you**, only for new and changed slices — contract in `assets/slices.example.html` |
21
+
22
+ `assets/prototype.mjs` does the bookkeeping: `status` fetches the context, hashes every slice and says which slices are new, changed, unchanged or removed; `assemble` builds the page from the template, the runtime and the blocks; `upload` hands it to the platform (`POST …/prototype/<context id>`), which sanitizes it, gzips it and stores it per effective context at `<board id>/<context id>.gz` in the organization's storage. It runs where you run (the CLI agent), never in the page. `<skill-dir>` below is the folder this SKILL.md is in (usually `.claude/skills/prototype`).
23
+
24
+ **Every run ends with an upload.** The ▶ player only plays what the platform stores — a page that exists only in `<out>` is invisible to everyone, and opening it from disk doesn't work either (its libraries come from the app's `/vendor/`). So a run is done when `upload` has printed where the platform stored the page, never before; there is no "open the local file instead".
25
+
26
+ **This skill never writes to the board.** Gaps it runs into go into the report, not onto the board.
27
+
28
+ **Faithfulness is the point.** People check the model by using the prototype, so it behaves as the board says. Implement as much of the logic as the board defines — from the scenarios first, then fields, names and descriptions — and nothing it doesn't: a rule nobody modelled is not enforced, and a button the board doesn't wire to anything stays visible but says it is not modeled.
29
+
30
+ ---
31
+
32
+ ## Step 1 — Which context
33
+
34
+ From `$ARGUMENTS`: the context (a MODEL_CONTEXT name or a timeline name) and optionally `out=<dir>`.
35
+
36
+ The unit is the **effective context**: a timeline assigned to or connected with a MODEL_CONTEXT belongs to that context; a timeline with neither is its own context. Either name returns the whole effective context — every timeline in it, each slice carrying its `chapter`.
37
+
38
+ No context given, or a name that matches nothing: list the options once —
39
+
40
+ ```
41
+ mcp__eventmodelers__get_nodes { "boardId": "<BOARD_ID>", "type": "MODEL_CONTEXT", "projection": "line" }
42
+ mcp__eventmodelers__get_nodes { "boardId": "<BOARD_ID>", "type": "CHAPTER", "projection": "line" }
43
+ ```
44
+
45
+ — and ask: in a chat turn with a `poll` snippet whose options are complete `/prototype <name>` messages, in a terminal in text. One context per run.
46
+
47
+ ---
48
+
49
+ ## Step 2 — What changed
50
+
51
+ ```bash
52
+ EM_TOKEN="$TOKEN" node <skill-dir>/assets/prototype.mjs status \
53
+ --base "$BASE_URL" --org "$ORG_ID" --board "$BOARD_ID" --context "<CONTEXT>" --out <out>
54
+ ```
55
+
56
+ (`TOKEN` from `connect` — when it is the `$EVENTMODELERS_TOKEN` reference, pass `EM_TOKEN="$EVENTMODELERS_TOKEN"`; never write the token on the command line itself.)
57
+
58
+ Per slice, the hash covers its elements with their fields and dependencies, its scenarios and storylines, and the content of its screens (HTML pages and marks, or the sketch) — not its status, assignee, ticket or comments, so moving a slice to Done rebuilds nothing. The hashes are compared with those on the blocks of the prototype the platform stores for this context — the page itself is the memory, no other state is kept — so a run in another chat session, folder or machine still rebuilds only what changed. `status` downloads it itself; the page never passes through you. Only when the platform has none yet does it fall back to a local `<out>/index.html` (an earlier run whose upload failed); `baseline` in `status.json` says which it was (`platform` / `local` / `none`).
59
+
60
+ `<out>/.work/status.json` carries the context's id (`contextId` — the MODEL_CONTEXT or timeline the name matched; the platform resolves a timeline to its effective context when storing) and lists every slice with `state` (`new` / `changed` / `unchanged`), its `hash`, `order` and `sliceType`, plus `removed` slices and the CSS framework the screens use. The blocks of unchanged slices are already copied to `<out>/.work/slices/<slice id>.html`.
61
+
62
+ **Nothing new or changed and nothing removed** → the local page is up to date, but that says nothing about the platform's copy (an earlier run may have stopped before uploading). Skip writing blocks (Steps 3–5) but still run `assemble` and **Upload** in Step 6 — `assemble` takes the template and runtime fresh from the skill, so a kit fix to the harness reaches the page even when the board didn't change — then report.
63
+
64
+ A change in one slice can matter to another (a renamed event field that a read model in another slice reads): that slice's own JSON changes with it, so it is flagged too. Build only what is flagged.
65
+
66
+ ---
67
+
68
+ ## Step 3 — Read the flagged slices
69
+
70
+ For the new and changed slices only, run in parallel:
71
+
72
+ ```
73
+ mcp__eventmodelers__get_slice_data { "boardId": "<BOARD_ID>", "contextName": "<CONTEXT>", "sliceId": "<id>", "projection": "fields", "format": "json" }
74
+ mcp__eventmodelers__get_slice_data { "boardId": "<BOARD_ID>", "contextName": "<CONTEXT>", "sliceId": "<id>", "projection": "specs", "format": "json" }
75
+ ```
76
+
77
+ - `fields` → `sliceType`, `commands[]`, `events[]`, `readmodels[]`, `screens[]`, `processors[]` (automations), each element with `fields[]` (`type`, `example`, `generated`, `idAttribute`, `cardinality`, `subfields`, `mapping`), `description` and `dependencies[]` (`INBOUND`/`OUTBOUND`) — the wiring SCREEN → COMMAND → EVENT → READMODEL → SCREEN / AUTOMATION → COMMAND.
78
+ - `specs` → `specifications[]` (`given` / `when` / `then` with example values; a rejection is a `then` step of type `SPEC_ERROR` whose `description` is the message; `expectEmptyList` on read-model steps) and `storylines[]`.
79
+
80
+ **Screens.** For each of the slice's screens read its own node — `linkedId` when set (a copy can carry its own marks), else `id` — in one call: `mcp__eventmodelers__get_nodes { "boardId", "nodeIds": [...] }`. `HTML_SCREEN` → `meta.pages`, `meta.marks`, `meta.fields`. `SCREEN` → `mcp__eventmodelers__get_image_snapshot_description { "boardId", "nodeId" }` for its sketch `{ elements }` (grid vocabulary in `storyboard-screen`); none (an uploaded screenshot) → the screen's fields are all there is.
81
+
82
+ **Copies.** An element with `elementCopy: true` is a linked copy (its `id` is the original's). It is defined by the slice that owns the original, never twice. An EVENT whose original lives in another context, or that no COMMAND in this context produces, is an **external event**.
83
+
84
+ ---
85
+
86
+ ## Step 4 — The logic, from the scenarios
87
+
88
+ Write each flagged slice's logic before its UI. Names are the board's titles without spaces (`Register Customer` → `RegisterCustomer`); field names verbatim. Every element belongs to exactly one slice: a command to its state-change slice, a read model to its state-view slice, a todo list and its automation (and the automation's command, when it has no slice of its own) to the automation slice.
89
+
90
+ **Analyse the scenarios first** — they are the logic. Line up all of the element's scenarios and read them together: what the `given` events have in common, what makes the `then` differ. Then implement what they show, as completely as they allow:
91
+
92
+ **Commands** — `decide(history, cmd, ctx)` returns the events or calls `EMRuntime.reject('<message>')`.
93
+ - Each scenario with a `SPEC_ERROR` is a rule: reject exactly the `given` situation, with its `description` as the message. Scenarios producing different events from different histories are branches of `decide`. A command without rejection scenarios never rejects — not even empty input.
94
+ - Event data: each event field from the command field of the same name or its `mapping`; `generated` values here — ids `ctx.uuid()`, timestamps `ctx.now.toISOString()`; values the command doesn't carry but the history does (the email of a `customerId`) from the history, filtered by the `idAttribute` the scenarios use.
95
+
96
+ **Read models** — `initial`, `evolve(state, event)` per INBOUND event, optional `query(state, params)`.
97
+ - The scenarios say which event adds, changes or removes what: compare their `given` sequences with the expected rows. Field names and the `description` fill in only what no scenario shows.
98
+ - `cardinality: "List"` / list-shaped scenarios / `listElement` → an array of rows; otherwise one object, `null` before its first event. `params` scopes a query to what the screen selected.
99
+
100
+ **Todo lists and automations.** A todo list is the read model an automation works off. **How an item gets onto the list and off it comes from the todo list's scenarios** (given the triggering event → the item is listed; given the event the automation's command records as well → the list is empty) and, failing those, from the events the board connects: the INBOUND event that triggers the automation adds the item, the event its command records removes it.
101
+ - Automation entry: `readModel`, `todo(result, ctx)` (the items to act on now — for a time-based one, those due at `ctx.now`), `process(item, ctx)` → `{ effect?, command: { type, data } }`, `trigger: 'auto'` or `'schedule'` (description or scenarios tie it to time), `external: true` when the command calls the outside world (send a mail, charge a card — the `effect` text appears in the outbox, the command records the outcome as if the call succeeded).
102
+ - Automations run when someone presses **Run** in the Automations tab (or with *run automatically* switched on). They only appear there while their todo list has open items — so a todo list that never empties is a bug the prototype makes visible.
103
+
104
+ **External events** — `externalEvents: [{ type, from, fields: [{ name, type, example }] }]` on the slice that consumes them; the simulate panel records them, which is how translation automations get triggered.
105
+
106
+ **Specs — every scenario and every storyline of the slice**, one entry each:
107
+
108
+ | Scenario | Spec entry |
109
+ |---|---|
110
+ | `when` COMMAND, `then` events | `kind: 'command'`, `given`, `when`, `then` |
111
+ | `then` `SPEC_ERROR` | `kind: 'command'`, `error: '<description>'`, no `then` |
112
+ | `then` READMODEL | `kind: 'readmodel'`, `readModel`, `given`, `expect` (`[]` when `expectEmptyList`) |
113
+ | todo list with the automation's command expected | `kind: 'automation'`, `automation`, `given`, `expectCommands` |
114
+ | storyline | walk the beats, accumulating EVENT beats as `given`; a COMMAND beat with its events → a `command` spec, a READMODEL beat with examples → a `readmodel` spec |
115
+
116
+ Example values become typed data (`Int`/`Double` → numbers, `Boolean` → booleans, list fields → arrays of objects from `subfields`). Leave out of `then`/`expect` what the domain generates (ids, timestamps not named by the scenario) — only what an entry names is compared.
117
+
118
+ ---
119
+
120
+ ## Step 5 — The slice's part of the screen
121
+
122
+ **Screens are tabs, one per screen name.** Slices whose screens share a name contribute to the same tab, in timeline order — so a screen shown in three slices with three different marks is one tab with three parts. A slice declares its part as `screens: { '<Screen Title>': { actor: '<lane>', part: Component } }`.
123
+
124
+ **Read the screen HTML for what belongs to this slice:**
125
+ - **a read model is one component** — the region of the screen that shows its data (a table, a card, a summary). The mockup's example values are replaced by `useReadModel('<ReadModel>', params)`; repeated example rows become one row mapped over the result; an empty result shows an empty state, never the sample rows.
126
+ - **a command is its inputs plus the button that fires it** — inputs bound to the command's fields (by the screen's `meta.fields`, else label ↔ field name; controlled state, examples as `placeholder`), the button named like the command (or the screen's only primary button) calling `useCommand('<Command>').run(form)`; on success `useNav().go('<next screen title>')` — the next screen of this actor in timeline order. Show `.error` next to the button.
127
+ - **marks decide when the screen is shared**: the marked element is this slice's part. Unmarked and shared: this slice takes the elements bound to its own command or read model.
128
+ - a button inside a list row navigates with the row's ids (`go('<title>', { orderId })`) or issues that row's command with them.
129
+ - a button the model gives no meaning to: kept as designed, `onClick={() => notModeled('<label>')}` (`const notModeled = useNotModeled()`), and listed in the report.
130
+
131
+ **HTML screens — 1:1**: translate the slice's region of `meta.pages` into JSX without redesigning it (`class` → `className`, `for` → `htmlFor`, style strings → objects, void elements closed). Several pages → a step-through with local `page` state. **Sketch screens — redrawn**: a 1000×800 relative container, each element absolutely positioned at `gridX*20`/`gridY*20` (`gridWidth*20` × `gridHeight*20`) — `rectangle` → `div`, `headline`/`text` → text at its `fontSize`, `button` → `<button>`, `input` → `<input placeholder>`, `image` → a filled box, `line`/`circle` → inline SVG — then wired the same way.
132
+
133
+ Automation slices usually have no screen part — the Automations tab shows them.
134
+
135
+ ---
136
+
137
+ ## Step 6 — Write the blocks, assemble, check
138
+
139
+ Write each new or changed slice to `<out>/.work/slices/<slice id>.html`, exactly in the shape of `assets/slices.example.html`:
140
+
141
+ - one `<script type="text/plain" data-em-slice="<slice id>" data-em-title="<slice title>" data-em-hash="<hash from status.json>">` element whose body ends with one `EMRuntime.defineSlice({ id, title, order, commands, readModels, automations, externalEvents, specs, screens })` call; `order` from `status.json`;
142
+ - JSX and modern JavaScript; React hooks as `React.useState`; `useCommand`, `useReadModel`, `useNav`, `useNotModeled` are globals;
143
+ - never the text of a closing script tag inside the block, not even in a string or comment.
144
+
145
+ Then:
146
+
147
+ ```bash
148
+ node <skill-dir>/assets/prototype.mjs assemble --out <out>
149
+ ```
150
+
151
+ It refuses to write the page while a flagged slice has no block, a block's hash doesn't match, or a block contains a closing script tag. Removed slices simply drop out. The template and the runtime are always taken fresh from the skill, so kit fixes reach every prototype on its next run.
152
+
153
+ **Upload — always, right after `assemble`** (the check below runs against the uploaded page):
154
+
155
+ ```bash
156
+ EM_TOKEN="$TOKEN" node <skill-dir>/assets/prototype.mjs upload \
157
+ --base "$BASE_URL" --org "$ORG_ID" --board "$BOARD_ID" --out <out>
158
+ ```
159
+
160
+ It prints where the platform stored the page and anything its sanitizer removed. If it fails, read the error and fix the cause (an expired token → run `connect` again; a 4xx naming the page → fix the block) and upload again. Only when it still fails, report the failure with the platform's error as the outcome of the run — not the local file as a substitute. A generated page loads scripts and stylesheets only from `/vendor/` and has no inline event handlers, so normally nothing is removed — if something is, a block broke the contract (a CDN link, an `onclick` attribute instead of a JSX `onClick`); fix that block, assemble and upload again.
161
+
162
+ **Check in the browser.** Every check runs in the page: the scenarios on load (red banner listing each failing scenario with expected vs. actual), each slice block compiling on its own (a broken block shows a banner naming its slice; the rest keeps working), and each part rendering inside its own error boundary. If a browser automation MCP (Playwright, Puppeteer, Chrome DevTools) is connected, open the page where the platform serves it (or any server that provides `/vendor/…`), confirm there is no banner, and fire the first tab's main command. A failing scenario means the logic is wrong — fix the block, never the scenario. Two scenarios that contradict each other: keep the one that fits the rest of the model, drop the other from `specs` and report it. Without a browser you cannot see the banner — say in the report that the scenarios were not run.
163
+
164
+ ---
165
+
166
+ ## Step 7 — Report
167
+
168
+ In a few lines:
169
+ - that it is uploaded — where the platform stored it (`upload` prints the path) — and that it plays with the ▶ button on any of the context's timelines (the player sandboxes the page without storage, so the event log lives until the player closes — *Copy log* / *Paste log* keep or move a session, *Reset* clears it).
170
+ - what this run did: `N` slices built (new / changed), `M` unchanged and kept, `K` removed — and the totals: tabs, commands, read models, automations by kind, external events to simulate, scenarios and whether they pass.
171
+ - the gaps found, one line each with the element: buttons not modeled, commands nothing issues, read models nothing shows or nothing feeds, screen fields without a read model, todo lists whose scenarios never remove an item, contradictory scenarios, mixed CSS frameworks (`mixedFrameworks` in `status.json`). These are findings about the model — offer `/wdyt` or the matching modeling skill; don't fix the board from here.
172
+
173
+ In a chat turn this report is the reply (`post_chat_message`, `replyTo` = this turn's `message_id`) **and always carries the prototype as a `prototype` snippet**, so the person can try it right there — with Restart, and Full screen for the board's player:
174
+
175
+ ```json
176
+ { "kind": "prototype", "timelineId": "<contextId from status.json>", "title": "Prototype · <CONTEXT>", "caption": "<N> tabs · <S> scenarios" }
177
+ ```
178
+
179
+ Only after `upload` succeeded — without a stored page the snippet shows "No prototype has been built for this yet". Never send the page itself as `html`: an assembled prototype is far over the snippet's 60000 characters. In a terminal there is no snippet: name the ▶ button on the context's timelines instead.
180
+
181
+ Never hand-edit a block in the assembled page and expect it to survive: the board is the source, and the next run regenerates any slice whose hash changed.