@eventmodelers/cli 1.0.129 → 1.0.131

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -107,6 +107,8 @@ Use skills in Claude Code with `/skill-name`:
107
107
  | `/timeline` | Live event storming facilitator |
108
108
  | `/wdyt` | Business analyst review of your event model |
109
109
  | `/walkthrough` | Slice-by-slice player through a feature, or through every slice a planned feature needs (existing, to adjust, new) |
110
+ | `/data-journey` | Where one piece of information (e.g. the customer email) originates, is stored, transformed, used and sent out — as a table in the chat |
111
+ | `/stressor-analysis` | Residuality-theory stress test of one chapter: wide stressors and their residues, a stressor × element matrix that exposes hidden coupling, redesigns proposed as new slices |
110
112
  | `/storyboard` | Build a full visual storyboard |
111
113
  | `/html-screen` | Design individual real HTML/CSS screens (default) |
112
114
  | `/storyboard-screen` | Design individual wireframe/sketch screens (explicit request only) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventmodelers/cli",
3
- "version": "1.0.129",
3
+ "version": "1.0.131",
4
4
  "description": "Eventmodelers CLI — real-time Claude agent + skills for Claude Code, for any stack (Node, Supabase, Axon, OpenCQRS, UmaDB, Kurrent, or modeling-only)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1122,8 +1122,11 @@ Rule of thumb: if you are about to write "Shall I…?", "Want me to…?", "A or
1122
1122
  | to show a command or payload | `code` | nothing |
1123
1123
  | to show numbers as a diagram (counts per slice, a trend, shares of a whole) | `report` | nothing |
1124
1124
  | to explain how a feature works, or walk through what a planned feature takes, slice by slice ("how does registration work?", "we need to add an email — walk me through it") | `walkthrough` | nothing — selecting a step pans the canvas |
1125
+ | to show what a screen could look like, without putting it on the board | `screen` | nothing |
1126
+ | to answer where one piece of information comes from and where it ends up ("where is the customer email used?") | `dataJourney` | nothing — element names pan the canvas |
1127
+ | to lay out rows and columns — a comparison, a matrix (stressors × elements), a short list with a few attributes | `table` | nothing — linked cells pan the canvas |
1125
1128
 
1126
- `confirm`, `poll`, `tasks` and `changes` are questions: a person answers once, then they show as answered. `link`, `image`, `code`, `report` and `walkthrough` are display only and never wait for anything.
1129
+ `confirm`, `poll`, `tasks` and `changes` are questions: a person answers once, then they show as answered. `link`, `image`, `code`, `report`, `walkthrough`, `screen`, `dataJourney` and `table` are display only and never wait for anything.
1127
1130
 
1128
1131
  ### Shapes
1129
1132
  ```typescript
@@ -1157,6 +1160,22 @@ Rule of thumb: if you are about to write "Shall I…?", "Want me to…?", "A or
1157
1160
  screenId?: string, // a SCREEN / HTML_SCREEN outside the slice the person sees in this step
1158
1161
  rules?: string[], // business rules of this slice
1159
1162
  changes?: string[] }> } // planned feature: what has to change or be built here
1163
+ { kind: 'screen', html: string, // a screen mock-up as a small HTML page (≤ 16000 chars), shown sandboxed
1164
+ title?: string, caption?: string }
1165
+ { kind: 'dataJourney', subject: string, // the piece of information followed, in the person's words ("Customer email")
1166
+ summary?: string, // 1–2 sentences: where it comes from, where it goes
1167
+ stops: Array<{ title: string, // 1+, in flow order: the element the data passes through
1168
+ nodeId?: string, // makes the title a link; the element's type is read from the board
1169
+ elementType?: string, // SCREEN, COMMAND, EVENT, READMODEL, AUTOMATION … for an element not on the board
1170
+ role: 'origin' | 'stored' | 'transformed' | 'used' | 'sent', // entered/imported here / kept in this event / renamed, derived, reformatted / read, shown, decided on / leaves the system
1171
+ field?: string, // the field holding it here — names change along the way (email → customerEmail)
1172
+ slice?: string,
1173
+ note?: string }> } // how it is used or changed here, a few words
1174
+ { kind: 'table', title?: string, caption?: string,
1175
+ columns: Array<string | { label: string, nodeId?: string, // 1–12; nodeId makes the header a link (a matrix with one column per element)
1176
+ align?: 'left' | 'center' | 'right' }>,
1177
+ rows: Array<Array<string | number | true | null // 1–200, one cell per column; true = check mark, null = empty
1178
+ | { text: string, nodeId?: string }>> } // a cell linking to an element; shown 5 rows per page
1160
1179
  ```
1161
1180
 
1162
1181
  ### How to use them
@@ -1168,6 +1187,9 @@ Rule of thumb: if you are about to write "Shall I…?", "Want me to…?", "A or
1168
1187
  - `get_chat_session` shows your snippet on the message as `snippet`, and the person's answer is the user message after it, carrying `context.snippetReply.messageId` (your message's id) — useful to tell an answer to a snippet from a new message.
1169
1188
  - **`report`** draws the numbers you send — it computes nothing, so aggregate first. A series whose length differs from `labels`, or a non-number, makes the whole snippet invisible. Keep `text` as a one-line summary of what the diagram shows. Add `confirm` when the diagram leads to a follow-up question ("Want me to look into the backward arrows?"): the answer comes back like a `confirm` answer, and without it the report is display only.
1170
1189
  - **`walkthrough`** answers "how does X work?" and "walk me through what changes for Y", **one step per slice**. It opens with a summary page — the slice count (with `status`: how many exist, need adjustment, are new) above your `summary` — then shows each slice in its lanes like Slice View (screen, command / read model, events), a `new` slice sketched from its `elements` (its SCREEN rendered from the `html` you write: a real, simple page — a form, a list, a table — with inline styles only; scripts and external resources are blocked; without `html` a plain form/table is generated from the fields); each step zooms the canvas to its slice, and **Play** opens it full-screen with description, changes and rules underneath. Build it from the board, never from memory — the `walkthrough` skill (modeling kit) has the procedure. Every `nodeId`/`screenId` must be a real id from this board. Keep `text` to one line naming the feature and the number of slices.
1190
+ - **`screen`** shows a mock-up in the chat — "what could the registration screen look like?", a variant to compare — without creating anything on the board (that is the `html-screen` skill's job). `html` follows the walkthrough's screen rules: body content, inline styles or one `<style>` block, no scripts, no external images, fonts or stylesheets (they are blocked), at most 16000 characters.
1191
+ - **`dataJourney`** answers "where does X come from / where is X used?" for **one piece of information**, as a table: one stop per element it passes through, in flow order, each with its `role`, the field name there and the slice. A headline counts the roles ("Originates in 1 place, stored in 2 elements and used in 3 places"). Build it from the board — the `data-journey` skill (modeling kit) has the procedure; every `nodeId` must be a real id from this board, and a field the board doesn't carry is not a stop. Keep `text` to one line naming the data and where it originates.
1192
+ - **`table`** is for anything that is rows and columns and not a chart: a comparison of options, an incidence matrix (rows = stressors, columns = elements, `true` where it hits), a short list with a few attributes per item. It computes nothing — totals are a row you add. Put element ids in `nodeId` (header or cell) so names are links; a row longer than the columns is cut, a shorter one padded, an empty one dropped. Prefer `report` for numbers you want *seen* as a trend or share, `dataJourney` for one piece of data's path.
1171
1193
  - A snippet is shown exactly as you send it — keep secrets and tokens out of `code` and `image`.
1172
1194
 
1173
1195
  ---
@@ -0,0 +1,88 @@
1
+ ---
2
+ name: data-journey
3
+ description: Follow one piece of information through the model — "where does the customer email come from, and where is it used?" — and show it in the chat as a table. One stop per element it passes through, in flow order — where it originates (a screen, an external event, an import), which events store it, where it is renamed or derived, which read models, screens and automations use it, and where it leaves the system — each with the field name it has there and its slice. Read-only — changes nothing on the board.
4
+ ---
5
+
6
+ # Data journey — where a piece of information comes from and where it goes
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. See `learn-eventmodelers-api` for the curl fallback and §16 *Chat snippets* for the `dataJourney` shape.
11
+
12
+ The result is **one `dataJourney` snippet** in the chat: a table with one row ("stop") per element the information passes through, in the order it flows. Each stop has a **role** — `origin`, `stored`, `transformed`, `used`, `sent` — the **field** it is held in there (names change along the way: `email` on the screen, `customerEmail` in the event), the slice and a few words on what happens to it. A headline above the table counts the roles ("Originates in 1 place, stored in 2 elements and used in 3 places"). Element names are links that zoom the canvas.
13
+
14
+ Typical questions: *"Where is the customer email used?"*, *"Where does the delivery address come from?"*, *"Which events store the IBAN?"*, *"What happens to the discount code?"*, *"/data-journey customer email"*.
15
+
16
+ **This skill never writes to the board.**
17
+
18
+ ---
19
+
20
+ ## Step 1 — The subject
21
+
22
+ The **subject** is one piece of information in the person's words ("customer email", "order total"). From `$ARGUMENTS` / the message, and the turn's `context=`: a selected element with one field selected, or a message like "this field", means that field.
23
+
24
+ No subject, or several equally likely ("the customer data"): **ask first** and end the turn — a `poll` of the candidates (at most ~8 field names you found, each option's `message` a complete `/data-journey <name>` request), or text only when there is nothing to offer. One piece of information per journey: "name and email" is two journeys — do the first and offer the second in the text.
25
+
26
+ ---
27
+
28
+ ## Step 2 — Find every element that carries it
29
+
30
+ Cheap reads first, never the whole board in full:
31
+
32
+ 1. **Name search**: `get_nodes { boardId, name: "<term>", projection: "line" }` finds elements whose *title* mentions it; the field names are in `fields`. Search for the obvious variants (`email`, `mail`, `e-mail`; the board's language).
33
+ 2. **Field search**: per chapter, `get_slice_data { boardId, contextName, projection: "fields", format: "toon" }` — every element's fields with their `mapping`. Collect every field that holds the subject: the same name, a prefixed/renamed one (`customerEmail`, `newEmail`, `recipient`), and — via `mapping` — every field mapped **from** one you already have. Follow `mapping` both ways until nothing new turns up; that is how a rename is traced, not by guessing from names. A field that only *sounds* related (`emailVerified`, `emailCount`) is a different piece of information — leave it out, or mention it in `note` on the stop it sits on.
34
+ 3. Note `pii: true` on a field — say so in the `summary` if the subject is personal data.
35
+
36
+ ---
37
+
38
+ ## Step 3 — Order and classify the stops
39
+
40
+ Order the stops the way the information flows — follow the edges (screen → command → event → read model → screen; event → automation → command → …), from where it enters to where it ends. Branches follow their source: after the event that stores it, list the read models fed by it, then the automations.
41
+
42
+ | `role` | The element … |
43
+ |---|---|
44
+ | `origin` | is where the information first enters the system: a SCREEN the person types it into, an external/translated event, an import, an API. Usually one; more when it can enter in several ways (registration *and* "change email") |
45
+ | `stored` | is an EVENT that records it — the source of truth. Every event carrying the field is a stop |
46
+ | `transformed` | changes it: a renamed field via `mapping` (`email` → `recipient`), a derived value, a normalisation stated in the element's description or scenarios ("lower-cased") — say what in `note` |
47
+ | `used` | reads it: a COMMAND that only passes it through or checks it, a READMODEL that projects it, a SCREEN that shows it, a scenario that decides on it ("email must be unique") — `note` says how |
48
+ | `sent` | hands it outside the system: an automation calling an external service (mail provider, payment, CRM), an outbound integration event |
49
+
50
+ A command that takes the field from its screen is `used` (it carries it), unless the screen isn't modelled — then the command is the `origin`. Each element appears once; pick the role that says most about it.
51
+
52
+ Per stop: `title` as on the board, `nodeId` (the element's real id), `field` (the field name on that element), `slice` (the slice title from `list_slices`), `note` in a few business words — no ids, no cell addresses. `elementType` only for a stop that is not a board node.
53
+
54
+ **Faithfulness.** Only what the board says. If the information is shown on a screen but no read model carries it, or an event stores it but nothing ever reads it, that is a finding — put it in the `summary` ("Stored in Email Changed, but no read model picks the new email up"), not as an invented stop.
55
+
56
+ ---
57
+
58
+ ## Step 4 — Post it
59
+
60
+ **One** `post_chat_message`: in a `CHAT` turn `replyTo` = this turn's `message_id`; in a prompt turn that came from a chat, `sessionId` = `CHAT_SESSION_ID` and no `replyTo`.
61
+
62
+ ```
63
+ mcp__eventmodelers__post_chat_message {
64
+ "boardId": "$BOARD_ID",
65
+ "replyTo": "<message_id>",
66
+ "text": "The customer email enters at registration and ends up in 5 more places.",
67
+ "snippet": {
68
+ "kind": "dataJourney",
69
+ "subject": "Customer email",
70
+ "summary": "Entered once at registration (personal data), kept in two events and used for login and mails. Nothing shows the changed email after Email Changed.",
71
+ "stops": [
72
+ { "title": "Registration", "nodeId": "<id>", "role": "origin", "field": "email", "slice": "Register Customer", "note": "typed in by the customer" },
73
+ { "title": "Register Customer", "nodeId": "<id>", "role": "used", "field": "email", "slice": "Register Customer", "note": "must be unique" },
74
+ { "title": "Customer Registered", "nodeId": "<id>", "role": "stored", "field": "customerEmail", "slice": "Register Customer" },
75
+ { "title": "Email Changed", "nodeId": "<id>", "role": "stored", "field": "newEmail", "slice": "Change Email" },
76
+ { "title": "Customer Profile", "nodeId": "<id>", "role": "used", "field": "email", "slice": "Customer Profile", "note": "shown on the profile page" },
77
+ { "title": "Send Welcome Mail", "nodeId": "<id>", "role": "sent", "field": "recipient", "slice": "Welcome Mail", "note": "to the mail provider" }
78
+ ]
79
+ }
80
+ }
81
+ ```
82
+
83
+ - `text` is one line — the subject and where it originates; it is the fallback and must make sense alone.
84
+ - Every `nodeId` is an id you read in Step 2 — never a guessed one.
85
+ - Long journeys page themselves (5 rows per page); keep every stop in the one snippet.
86
+ - If the person then asks to change something (add the field to a read model, mark it as `pii`), that is ordinary work — prompts per the kit's CLAUDE.md.
87
+
88
+ **No chat** (run from a terminal): print the summary and the stops as a table (role, element, field, slice, note) instead of posting.
@@ -0,0 +1,89 @@
1
+ ---
2
+ name: stressor-analysis
3
+ description: Residuality-theory stressor analysis of one chapter, run as a chat loop. Finds wide stressors, describes the residue of each, builds a stressor x element incidence matrix to expose hidden coupling, and proposes redesigns as new slices. Read-only until the person confirms.
4
+ ---
5
+
6
+ # Stressor analysis — what is left of the model when things go wrong
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. See `learn-eventmodelers-api` for the curl fallback and §16 *Chat snippets* for the `tasks`, `table`, `poll` and `confirm` shapes.
11
+
12
+ The analysis runs over several chat turns: each step ends with **one** `post_chat_message` carrying one snippet, and the person's click is the next turn. In a `CHAT` turn `replyTo` = this turn's `message_id`; in a prompt turn that came from a chat, `sessionId` = `CHAT_SESSION_ID` and no `replyTo`. Find where you are from the session (`get_chat_session`): the answer to your last snippet says which step comes next.
13
+
14
+ **This skill never writes to the board** until the person says yes in Step 3 — and then only through `create_prompt`.
15
+
16
+ ---
17
+
18
+ ## Concepts (get these right)
19
+
20
+ - **Stressor**: anything that can happen to the system or its environment: technical, business, legal, market, people. Deliberately WIDE, far-fetched ones included. Not a risk list, not a prediction.
21
+ - **Residue**: what is LEFT of the system after the stressor hits: which slices still work, which state or data survives or is lost. You observe a residue; you don't build it. The fix is NOT the residue.
22
+ - **Goal**: change the model until every residue is acceptable. The architecture becomes the sum of those residues, so it also survives stressors nobody listed.
23
+ - **Incidence matrix**: stressors × elements. Elements hit by the SAME stressors are hidden-coupled.
24
+
25
+ ---
26
+
27
+ ## Step 0 — Read once
28
+
29
+ The chapter comes from `$ARGUMENTS` / the message, else the turn's `context=` (`timelineId`, selection). None and more than one chapter on the board: ask with a `poll` of the chapters (each option's `message` a complete `/stressor-analysis <chapter>` request) and end the turn.
30
+
31
+ Read once and reuse for the whole round: `get_board_outline` (gives `sliceStatus` per slice) and `get_slice_data { boardId, contextName, projection: "fields", format: "toon" }` for the chapter. Ground every statement in real element and field names; every `nodeId` you send is an id you read here.
32
+
33
+ ---
34
+
35
+ ## Step 1 — Stressors and their residues (`tasks`)
36
+
37
+ About **15** wide stressors. Per task: `title` = the stressor, `description` = `Residue: <what still works / what is lost>`, `nodeId` = the element most affected, `id` = `S1`, `S2`, …. `headline`: *"Tick the ones whose residue is unacceptable"*, `submitLabel`: *"These are unacceptable"*.
38
+
39
+ ```
40
+ "snippet": { "kind": "tasks", "headline": "Tick the ones whose residue is unacceptable", "submitLabel": "These are unacceptable",
41
+ "tasks": [ { "id": "S1", "title": "Payment provider is down for a day", "description": "Residue: orders are placed, Order Paid never arrives — Shipping waits forever", "nodeId": "<Order Paid id>" }, … ] }
42
+ ```
43
+
44
+ If fewer than **5** come back ticked: explain that the matrix can't show coupling yet, and offer the list again (the same snippet, plus a few new stressors).
45
+
46
+ ---
47
+
48
+ ## Step 2 — Incidence matrix (`table`)
49
+
50
+ For each ticked stressor, list every element it hits (not just the most affected one) — from the read in Step 0, never guessed. Then one `table` snippet:
51
+
52
+ - one row per element hit at least once, sorted by hits (most first) — the hotspots on top;
53
+ - columns: `Element` (the cell `{text, nodeId}` as a link), `Hits` (`align: "center"`), then one column per ticked stressor, labelled `S1`, `S2` … (`align: "center"`), `true` where it hits, `null` where not;
54
+ - more than 10 ticked stressors: replace the stressor columns with one `Hit by` column (`"S1, S4, S9"`);
55
+ - `caption`: the legend, `S1 = <stressor>; S4 = …`.
56
+
57
+ ```
58
+ "snippet": { "kind": "table", "title": "Stressors × elements", "caption": "S1 = Payment provider down; S4 = Price changes mid-checkout; …",
59
+ "columns": ["Element", {"label": "Hits", "align": "center"}, {"label": "S1", "align": "center"}, {"label": "S4", "align": "center"}],
60
+ "rows": [ [{"text": "Order Paid", "nodeId": "<id>"}, 2, true, true], [{"text": "Shipping List", "nodeId": "<id>"}, 1, true, null] ] }
61
+ ```
62
+
63
+ In the text, name the **clusters** (elements hit by the same stressor pair), the shared cause behind each cluster, and the hotspots (most hits). Ask which cluster to redesign first — the matrix is this message's snippet, so the question is in the text; when the person answers in words, go on with that cluster.
64
+
65
+ ---
66
+
67
+ ## Step 3 — Redesign one cluster (`confirm`)
68
+
69
+ Propose new slices (translations, reconciliation todo lists, grace periods, second entry paths …) and state the **new residue** for each stressor that hit the cluster. A `confirm` snippet: `headline` = *"Add these N slices for <cluster>?"*; the slices and their new residues go in the text, kept short.
70
+
71
+ Never touch elements in non-`Created` slices without an explicit yes; name them instead ("Order Paid sits in a Done slice — I'd leave it and add a translation next to it").
72
+
73
+ ## Step 4 — On yes
74
+
75
+ One `create_prompt` per new slice (`originMessageId` = the yes), phrased so the prompt turn can do it on its own (chapter, slice name, its elements and fields). Reply in one line. A no: acknowledge it and offer the next cluster.
76
+
77
+ ## Step 5 — Validate
78
+
79
+ Generate a **fresh** set of stressors not used in rounds 1–3, and compare how many have an acceptable residue before vs after the redesign (the residual index) — a `tasks` list again, the person ticks the unacceptable ones, and the text states the index ("before: 6 of 12 acceptable, after: 10 of 12"). Repeat rounds until new stressors stop hitting new elements.
80
+
81
+ ---
82
+
83
+ ## Chat style
84
+
85
+ - Explain a residue with a before/after example if asked (same stressor, different leftover).
86
+ - Text ≤ 1000 characters; the detail goes in the snippet.
87
+ - No ids or cell addresses in the text — element names are enough.
88
+
89
+ **No chat** (run from a terminal): print the stressors with residues, ask which are unacceptable, and print the matrix as a table instead of posting.
@@ -208,9 +208,17 @@ for it. Instead:
208
208
  subject (a bare `/walkthrough`) it asks first — which flow, or which new feature — and that question is the reply. A planned change
209
209
  ("we need to add an email") is its impact mode: every slice marked existing / needs adjustment / new, new ones sketched — the walkthrough is the proposal, and the changes become work only
210
210
  when the person asks for them afterwards.
211
+ - **data journey** — "where does X come from?", "where is X used?", "which events store X?", or `/data-journey …`, about
212
+ **one piece of information** (a field, "the customer email"): run the `/data-journey` skill **in this turn** — it only reads
213
+ the board, so no prompt is needed. Its `dataJourney` snippet (a table: origin, stored, transformed, used, sent — per element,
214
+ with the field name there) is your one reply. Without a clear subject it asks first which information.
215
+ - **stressor analysis** — "what breaks if …?", "stress-test this chapter", or `/stressor-analysis …`: run the
216
+ `/stressor-analysis` skill **in this turn** — it only reads the board until the person says yes, so no prompt is needed
217
+ for its steps. It is a loop over several turns (stressors as `tasks`, the incidence matrix as a `table`, a redesign as
218
+ `confirm`); an answer to one of its snippets continues it at the next step. Only the confirmed new slices become prompts.
211
219
  - **a skill by name** — the message starts with `/<name>` of one of your skills (the chat panel's menu
212
- writes these: `/analyze-existing-model`, `/analyze-code-base`, `/detect-model-drift`, `/add-next-slice`, `/examples`, `/timeline …`,
213
- `/html-screen …`, `/storyboard …`): the person chose that skill, so it **is** the work — create one prompt,
220
+ writes these: `/analyze-existing-model`, `/analyze-code-base`, `/detect-model-drift`, `/timeline …`,
221
+ `/storyboard …`; `/add-next-slice`, `/examples`, `/html-screen …` can still be typed): the person chose that skill, so it **is** the work — create one prompt,
214
222
  *"Run /<name> <the rest of the message>"*, with the target resolved from `context=` like any other
215
223
  (selected elements, chapter), and say so in one line. Don't re-interpret it as a question. A skill that
216
224
  needs something the message lacks (which attribute? which screen?) is a **clarify**: ask, with a
@@ -274,6 +282,9 @@ for it. Instead:
274
282
  - a **choice between alternatives** ("A or B?", "which one first?") → `poll`;
275
283
  - a **list of things for the person to pick from** (findings, candidates, drifts, "which of these?") → `tasks`;
276
284
  - a **pointer to an element, a picture or a command** → `link` / `image` / `code`, instead of describing it.
285
+ - a **screen idea** in the conversation ("what could that screen look like?") → `screen`, a sandboxed HTML mock-up in the chat; putting a screen on the board is `/html-screen` work;
286
+ - **where one piece of information comes from and where it is used** → `dataJourney` (the `/data-journey` skill);
287
+ - **rows and columns** — a comparison, a matrix, a short list with a few attributes per item → `table`.
277
288
  Plain text stays for answers, explanations and open questions that need words. `text` is still
278
289
  required and stays short — it is the lead-in, the snippet carries the detail. A snippet changes
279
290
  nothing on the board: the click comes back as the next `CHAT` turn, and only then do you