@milehimikey/em 1.4.0 → 1.6.0

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 (95) hide show
  1. package/.claude/skills/event-modeling/SKILL.md +121 -30
  2. package/.claude/skills/event-modeling/reference/conform.md +36 -7
  3. package/.claude/skills/event-modeling/reference/em-dsl.md +210 -48
  4. package/.claude/skills/event-modeling/reference/extract.md +24 -7
  5. package/.claude/skills/event-modeling/reference/methodology.md +129 -32
  6. package/.claude/skills/event-modeling/templates/conformance-report.md +9 -7
  7. package/.claude/skills/event-modeling/templates/model-readme.md +9 -0
  8. package/.claude/skills/event-modeling/templates/slice.md +20 -1
  9. package/.claude/skills/event-modeling/templates/state.md +8 -0
  10. package/README.md +1 -0
  11. package/dist/catalog/build.d.ts +33 -0
  12. package/dist/catalog/build.js +117 -0
  13. package/dist/catalog/build.js.map +1 -0
  14. package/dist/catalog/classify.d.ts +14 -0
  15. package/dist/catalog/classify.js +44 -0
  16. package/dist/catalog/classify.js.map +1 -0
  17. package/dist/catalog/pages.d.ts +45 -0
  18. package/dist/catalog/pages.js +174 -0
  19. package/dist/catalog/pages.js.map +1 -0
  20. package/dist/catalog/sliceDoc.d.ts +10 -0
  21. package/dist/catalog/sliceDoc.js +20 -0
  22. package/dist/catalog/sliceDoc.js.map +1 -0
  23. package/dist/cli/catalog-inputs.d.ts +16 -0
  24. package/dist/cli/catalog-inputs.js +18 -0
  25. package/dist/cli/catalog-inputs.js.map +1 -0
  26. package/dist/cli/glossary-inputs.d.ts +15 -0
  27. package/dist/cli/glossary-inputs.js +17 -0
  28. package/dist/cli/glossary-inputs.js.map +1 -0
  29. package/dist/cli/render-inputs.d.ts +27 -0
  30. package/dist/cli/render-inputs.js +41 -0
  31. package/dist/cli/render-inputs.js.map +1 -0
  32. package/dist/cli.js +188 -10
  33. package/dist/cli.js.map +1 -1
  34. package/dist/emit/diffJson.d.ts +1 -1
  35. package/dist/emit/diffJson.js +8 -1
  36. package/dist/emit/diffJson.js.map +1 -1
  37. package/dist/emit/dot.d.ts +7 -0
  38. package/dist/emit/dot.js +10 -4
  39. package/dist/emit/dot.js.map +1 -1
  40. package/dist/emit/glossaryJson.d.ts +13 -0
  41. package/dist/emit/glossaryJson.js +36 -0
  42. package/dist/emit/glossaryJson.js.map +1 -0
  43. package/dist/emit/json.d.ts +16 -2
  44. package/dist/emit/json.js +57 -6
  45. package/dist/emit/json.js.map +1 -1
  46. package/dist/emit/theme.d.ts +3 -0
  47. package/dist/emit/theme.js +19 -0
  48. package/dist/emit/theme.js.map +1 -1
  49. package/dist/model/diff.d.ts +36 -1
  50. package/dist/model/diff.js +198 -35
  51. package/dist/model/diff.js.map +1 -1
  52. package/dist/model/edges.d.ts +12 -1
  53. package/dist/model/edges.js +26 -24
  54. package/dist/model/edges.js.map +1 -1
  55. package/dist/model/glossary.d.ts +68 -0
  56. package/dist/model/glossary.js +138 -0
  57. package/dist/model/glossary.js.map +1 -0
  58. package/dist/model/model.d.ts +35 -0
  59. package/dist/model/model.js +41 -0
  60. package/dist/model/model.js.map +1 -1
  61. package/dist/model/validate.js +252 -1
  62. package/dist/model/validate.js.map +1 -1
  63. package/dist/parser/ast.d.ts +20 -0
  64. package/dist/parser/parser.js +158 -29
  65. package/dist/parser/parser.js.map +1 -1
  66. package/dist/render/drawEdges.d.ts +3 -1
  67. package/dist/render/drawEdges.js +119 -7
  68. package/dist/render/drawEdges.js.map +1 -1
  69. package/dist/render/drawNotes.d.ts +7 -3
  70. package/dist/render/drawNotes.js +75 -28
  71. package/dist/render/drawNotes.js.map +1 -1
  72. package/dist/render/render.d.ts +20 -1
  73. package/dist/render/render.js +57 -17
  74. package/dist/render/render.js.map +1 -1
  75. package/dist/render/serve.js +155 -0
  76. package/dist/render/serve.js.map +1 -1
  77. package/dist/render/sliceDiagram.d.ts +11 -0
  78. package/dist/render/sliceDiagram.js +198 -0
  79. package/dist/render/sliceDiagram.js.map +1 -0
  80. package/dist/render/sliceOverlay.d.ts +21 -0
  81. package/dist/render/sliceOverlay.js +111 -0
  82. package/dist/render/sliceOverlay.js.map +1 -0
  83. package/dist/render/sliceStatus.d.ts +5 -0
  84. package/dist/render/sliceStatus.js +26 -0
  85. package/dist/render/sliceStatus.js.map +1 -0
  86. package/dist/render/statusOverlay.d.ts +16 -0
  87. package/dist/render/statusOverlay.js +110 -0
  88. package/dist/render/statusOverlay.js.map +1 -0
  89. package/dist/render/svgGeometry.d.ts +43 -0
  90. package/dist/render/svgGeometry.js +69 -3
  91. package/dist/render/svgGeometry.js.map +1 -1
  92. package/dist/render/watch.d.ts +1 -1
  93. package/dist/render/watch.js +5 -2
  94. package/dist/render/watch.js.map +1 -1
  95. package/package.json +3 -2
@@ -15,10 +15,13 @@ em init [file] # scaffold a starter model.em (default: model.em)
15
15
  em render <file> -o out.svg # render (extension picks format: svg, png, pdf)
16
16
  em render <file> --emit-dot # print the Graphviz DOT instead of rendering
17
17
  em render <file> --keep-empty-lanes # keep the API lane even when empty
18
+ em render <file> --slice "<slice-name>" -o slices/<slug>.svg # this slice's own diagram
18
19
  em watch <file> -o out.svg # re-render on every save (file-based)
19
20
  em watch <file> -o out.svg --serve # + localhost live viewer, instant SSE push-reload (--port N)
20
21
  em validate <file> # check event-modeling rules; exit 0 if only warnings/clean
21
22
  em validate <file> --list-issues # print only open `issue "..."` clauses (slice, element, line, text)
23
+ em validate <file> --list-divergences # print only `divergence "..."` clauses (slice, element, line, text)
24
+ em validate <file> --list-public # print only events marked `public` (slice, name, line)
22
25
  em validate <file> --fail-on-issues # opt-in CI gate: exit non-zero while any issue remains open
23
26
  ```
24
27
 
@@ -35,7 +38,7 @@ model "Name" # diagram title
35
38
  persona Name # a UI swimlane row (actor)
36
39
  context Name # an event swimlane row (bounded context / aggregate)
37
40
 
38
- slice "Name" { # one vertical time step (a column)
41
+ slice "Name" [source "url"] { # one vertical time step (a column); source is optional
39
42
  ui Free Text @Persona # screen; @Persona picks its row (defaults to first/"User")
40
43
  command Free Text # state-changing request (API band)
41
44
  view Free Text from "Event A", "Event B" # read model fed by event(s)
@@ -45,16 +48,18 @@ slice "Name" { # one vertical time step (a column)
45
48
  }
46
49
 
47
50
  arrow From Element -> To Element # explicit cross-slice edge (overrides inferred flow)
51
+
52
+ type Name { field: Type, ... } # named structured type, reusable from any field (see Named types)
48
53
  ```
49
54
 
50
55
  ### Element kinds (8 keywords, nothing else)
51
56
  | Keyword | Band | Meaning | Tag | Extra clauses |
52
57
  |---|---|---|---|---|
53
- | `ui` | persona | screen / interface | `@Persona` | `note`, `issue`, `{ fields }` |
54
- | `command` | API | state-changing request | — | `note`, `issue`, `{ fields }` |
55
- | `view` | API | read model / projection | — | `from "Event"…`, `note`, `issue`, `{ fields }` |
56
- | `event` | context | recorded fact (past tense) | `@Context` | `note`, `issue`, `{ fields }` |
57
- | `processor` / `automation` / `saga` / `translation` | automation | system reaction / adapter | — | `from "…"`, `note`, `issue`, `{ fields }` |
58
+ | `ui` | persona | screen / interface | `@Persona` | `note`, `issue`, `divergence`, `{ fields }` |
59
+ | `command` | API | state-changing request | — | `note`, `issue`, `divergence`, `{ fields }` |
60
+ | `view` | API | read model / projection | — | `from "Event"…`, `note`, `issue`, `divergence`, `{ fields }` |
61
+ | `event` | context | recorded fact (past tense) | `@Context` | `note`, `issue`, `divergence`, `public`, `{ fields }` |
62
+ | `processor` / `automation` / `saga` / `translation` | automation | system reaction / adapter | — | `from "…"`, `note`, `issue`, `divergence`, `{ fields }` |
58
63
 
59
64
  ### Clauses
60
65
  - **Tags:** `@Persona` only on `ui`; `@Context` only on `event`. Undeclared tags auto-create a row.
@@ -64,9 +69,10 @@ arrow From Element -> To Element # explicit cross-slice edge (overrides infer
64
69
  - **`again`** (views only): `view <Name> again [from "Event", …]` declares a later instance of an
65
70
  already-declared read model — the forward-only device for a view that evolves as later events
66
71
  land. Instances are ONE logical view: the first declaration owns the `note` binding; each
67
- instance's `from` lists only the NEW events landing at that point (not cumulative); instances are
68
- linked left-to-right with a continuity arrow; a reaction (`from "View"`) reads the nearest
69
- instance at-or-before its own slice. `again` with no earlier declaration is a validation error.
72
+ instance's `from` lists only the NEW events landing at that point (not cumulative); a reaction
73
+ (`from "View"`) reads the nearest instance at-or-before its own slice. Instances are NEVER
74
+ connected to one another — continuity is implied by the shared name, and the events reaching each
75
+ instance are what show the view changing. `again` with no earlier declaration is a validation error.
70
76
  Use `again` (not a plain repeated `view` name) whenever the view is referenced by a
71
77
  `from`/`arrow` — plain repeats are only warning-free while unreferenced.
72
78
  - **`note "path.md"`** on ANY element links a markdown doc. Relative to the `.em` file. Renders as
@@ -76,10 +82,49 @@ arrow From Element -> To Element # explicit cross-slice edge (overrides infer
76
82
  on one element) plus a legend entry; `em validate` warns on every open issue. **Prefer this
77
83
  over a `# TBD` comment for anything that should show up on the rendered diagram** — `# TBD` is
78
84
  invisible once rendered, `issue` isn't.
85
+ - **`divergence "text"`** on ANY element records a reasoned, ratified deviation between this
86
+ element and its implementation — the *resolved* sibling of `issue` (lint-suppression-with-
87
+ rationale for the `conform` phase). Renders as a teal corner marker (bottom-right — distinct
88
+ from `note`'s top-right and `issue`'s top-left, so all three can coexist) plus a legend entry.
89
+ Raises **no** `em validate` warning by design; use `--list-divergences` to audit. `em diff
90
+ --json` carries it forward as `acceptedDivergence` on the affected change/removal entry, so
91
+ `conform` can cite an already-ratified deviation instead of re-flagging it as drift every run.
92
+ - **`public`** (events only): marks this event as part of the published integration surface
93
+ (e.g. an AsyncAPI contract), as opposed to an internal-only fact. Plain structural flag, no
94
+ free text and no diagram marker (same posture as `again`). Write it as the last token on the
95
+ line, optionally right before a trailing `@Context` — `event X @Context public` or
96
+ `event X public @Context` both work. `em export` carries it as `public: true`/`false`;
97
+ `em diff` reports a flip as `event marked public`/`event unmarked public`;
98
+ `em validate --list-public` audits which events are public.
79
99
  - **Fields:** `command Place Order { orderId: UUID, items: LineItem[], customerId }` — inline or
80
- one-per-line. Types are free text (no semantic checking). Keep these light; full field specs
81
- with rules live in the slice doc.
100
+ one-per-line. Types are free text (no semantic checking) UNLESS the type string names a
101
+ declared `type` (see Named types below), in which case it resolves to a structured
102
+ reference. Keep these light; full field specs with rules live in the slice doc.
82
103
  - **Comments:** `# ...` anywhere outside quotes (full-line or trailing).
104
+ - **`source "url"` on a slice header** (the only slice-level clause — everything above is
105
+ per-element) links the slice back to the ticket/conversation it traces to, e.g.
106
+ `slice "Checkout" source "https://linear.app/team/issue/MIL-60" { ... }`. Exports as
107
+ `model.slices[].source` via `em export`, so an intake loop stays machine-traversable instead
108
+ of relying on prose. Purely metadata — no visual marker, not validated. Don't confuse it with
109
+ an element's `note "path.md"` (a markdown file link, not a URL).
110
+
111
+ ### Named types
112
+
113
+ `type Name { field: Type, ... }` declares a reusable structured shape at the top level (any
114
+ order relative to `persona`/`context`/`slice`, no clauses). Reference it from any field
115
+ anywhere — bare (`winner: QuoteAcceptedLine`) for a nested object, `Name[]` for an array
116
+ (`lines: QuoteAcceptedLine[]`). Resolution is opportunistic: a type string only becomes a
117
+ structured reference when it names a declared type (case/whitespace-insensitive match); every
118
+ other type string stays free text exactly as before, so declaring no `type` blocks changes
119
+ nothing. Recursion between declared types is allowed as a DAG (e.g. a diamond shape like
120
+ `Order` referencing `Address` twice) — a bare/singular self- or mutual-cycle
121
+ (`type Node { child: Node }`) is a validation error, but the same shape through an array
122
+ (`type Node { children: Node[] }`) is legal since the array can terminate at runtime; this is
123
+ how tree/recursive data (categories, org charts, comment threads, BOMs) gets expressed.
124
+ `em export` lists every declared type under `model.types[]` (stable `ref`, e.g.
125
+ `types/quote-accepted-line`) and adds an additive `typeRef` key to every field (declared-type
126
+ fields and ordinary element fields alike) — `{ name, ref, array }` when resolved, `null`
127
+ otherwise. `em diff` tracks types added/removed and field changes on surviving types.
83
128
 
84
129
  ### Swimlane band order (top → bottom)
85
130
  Header row → **Automation** (only if used) → **persona** rows (in declared order) → **API**
@@ -106,7 +151,7 @@ slice "Open Orders" {
106
151
  ui Order List @Customer
107
152
  }
108
153
 
109
- # 3. Automation: split across TWO slices
154
+ # 3. Automation: split across TWO slices (plus the read slice that consumes the event)
110
155
  slice "Orders To Fulfill" {
111
156
  view Orders To Fulfill from "Order Placed"
112
157
  processor Fulfillment Service
@@ -115,8 +160,12 @@ slice "Ship Order" { # the triggered command goes in the NEXT slice
115
160
  command Ship Order
116
161
  event Order Shipped @Shipping
117
162
  }
163
+ slice "Open Orders — shipped" {
164
+ view Open Orders again from "Order Shipped" # every event needs a reader
165
+ ui Order List @Customer # ...and every read model needs a consumer
166
+ }
118
167
 
119
- # 4a. Translation (external trigger): external input -> translation -> command -> event
168
+ # 4a. Translation (external trigger, no durable artifact): external call -> translation -> command -> event
120
169
  slice "Carrier Webhook" {
121
170
  translation Carrier Adapter # inbound from outside the model; no internal `from`
122
171
  }
@@ -124,8 +173,36 @@ slice "Confirm Delivery" { # the triggered command goes in the NEXT s
124
173
  command Confirm Delivery
125
174
  event Delivery Confirmed @Shipping
126
175
  }
176
+ slice "Open Orders — delivered" {
177
+ view Open Orders again from "Delivery Confirmed"
178
+ ui Order List @Customer
179
+ }
180
+
181
+ # 4b. Translation (external trigger, durable artifact): persisted inbound queue -> translation -> command -> event
182
+ slice "Receive Carrier Event" { # ingest: persist the raw webhook before reacting to it
183
+ ui Webhook Endpoint @Carrier
184
+ command Receive Carrier Event
185
+ event Carrier Event Received @Shipping # scoped to @Shipping's fact, not the @Carrier caller
186
+ }
187
+ slice "Inbound Carrier Events" {
188
+ view Inbound Carrier Events from "Carrier Event Received" # the persisted inbound queue
189
+ translation Carrier Adapter
190
+ }
191
+ slice "Acknowledge Carrier Event" {
192
+ command Acknowledge Carrier Event
193
+ event Carrier Event Acknowledged @Shipping
194
+ }
195
+ slice "Inbound Carrier Events — processed" {
196
+ view Inbound Carrier Events again from "Carrier Event Acknowledged"
197
+ ui Delivery Board @Ops
198
+ }
127
199
 
128
- # 4b. Translation (internal trigger): read model -> translation -> command -> event
200
+ # 4c. Translation (internal trigger, durable artifact): read model -> translation -> command -> event
201
+ slice "Accept Quote" {
202
+ ui Quote Screen @Customer # every command needs a trigger: a ui, or a reaction before it
203
+ command Accept Quote
204
+ event Quote Accepted @Quote
205
+ }
129
206
  slice "Quotes To Sync" {
130
207
  view Accepted Quotes from "Quote Accepted"
131
208
  translation CRM Sync # reacts to our own state via the read model in this slice
@@ -134,40 +211,79 @@ slice "Record Sync" {
134
211
  command Record Crm Sync
135
212
  event Quote Synced @Quote
136
213
  }
214
+ slice "Accepted Quotes — synced" {
215
+ view Accepted Quotes again from "Quote Synced"
216
+ ui Sync Status @Customer
217
+ }
137
218
  ```
138
219
 
220
+ Trigger source (external/internal) and durable artifact (`view`-backed or not) are independent
221
+ axes — 4a and 4b are both "externally triggered" but only 4b has a queue, and a `view`-backed
222
+ translation reacts the same whether the view was filled by an outside system (4b) or the model's
223
+ own event (4c). An inbound message earns `event` status the same way any event does: scope it to
224
+ the context/lane whose fact it represents (e.g. `Carrier Event Received @Shipping` or its own
225
+ `Carrier` context), not by who committed it — that keeps it a legitimate boundary fact (à la an
226
+ Anti-Corruption-Layer event in DDD) instead of a technical artifact leaking foreign vocabulary
227
+ into the model.
228
+
139
229
  A `translation` (like a `processor`) is a **reaction**: it triggers a command and never carries an
140
230
  `event` in its own slice. Same two-slice split as the Automation pattern above.
141
231
 
142
- ### Headless / API systems (no UI) & repeated read models
232
+ Note the read slice closing each pattern: **every event must be read by some read model**
233
+ (warning 4 below). A command slice is not finished until the slice that projects its event
234
+ exists. Reactions don't count — they read *views*, not events.
235
+
236
+ ### Headless / API systems & repeated read models
143
237
 
144
- When the system is headless (no UI — clients call an API), drop `ui`/`persona` entirely:
238
+ A headless system (no screens — clients call an API) still uses `ui`/`persona`: a slice **is**
239
+ trigger → command → event, read vertically, so the trigger belongs *in* the slice, never split into
240
+ one of its own. Declare a persona per external caller/role and treat its `ui` boxes as API calls
241
+ instead of shipped screens — same two patterns (State Change, State View) as any other slice, no new
242
+ shape:
145
243
 
146
244
  ```em
147
- # Write: external translation -> command -> event (name the inbound adapter for the caller/role)
148
- slice "Create Quote (request)" { translation SalesRep BU Create Quote }
149
- slice "Create Quote" { command Create Quote event QuoteCreated @Quote }
245
+ persona IntegratorAPI # API-flagged lane: its `ui` boxes are API calls, not shipped screens
150
246
 
151
- # Read: event(s) -> read model -> READ translation (the external caller's API query, replaces UI)
152
- slice "Read Quote — created" { view Quote from "QuoteCreated" translation SalesRep BU Read Quote }
247
+ slice "Create Quote" {
248
+ ui Create Quote @IntegratorAPI
249
+ command Create Quote
250
+ event QuoteCreated @Quote
251
+ }
252
+
253
+ slice "Read Quote — created" {
254
+ view Quote from "QuoteCreated"
255
+ ui Read Quote @IntegratorAPI
256
+ }
153
257
  ```
154
258
 
155
- - A **read translation triggers no command** (it returns data outbound) — it is the headless analog
156
- of `view → ui`, *not* a reaction. Only *reaction* translations/processors trigger commands. A read
157
- slice has no `command` and no `event`, so `em validate` stays quiet about it.
158
- - **Repeat the read model** in every slice where it's read so the timeline flows left-to-right.
159
- Recurring `view`/`translation` names **render cleanly and stay warning-free** as long as nothing
160
- references the repeated name via `from`/`arrow` (the duplicate-name warning fires only for a
161
- *referenced* duplicate, resolving to the first). **Wire each event to a read model exactly once:**
162
- a repeated instance's `from` lists only the **new** events since the previous instance (not
163
- cumulative), or the event draws a duplicate arrow to the same read model at every repeat. (An
164
- event may still feed several *different* read models, once each.) When a repeated view **must be
165
- referenced** — e.g. a reaction reads it via `from` — declare its later instances with
166
- `view X again` instead (see Clauses); instances resolve properly and draw continuity arrows.
259
+ - **`translation` stays reserved for genuine reactions and real external-system boundaries** — an
260
+ internal automation, or a webhook/adapter crossing into another system (see the Automation and
261
+ 4a/4b/4c Translation examples above, unchanged). It is **not** how you model a synchronous
262
+ request/response API call — that's Pattern 1 (State Change) with an API persona, exactly like the
263
+ `Create Quote` slice above.
264
+ - **Internal-only commands and views (no public route) carry no `ui` at all.** They follow the
265
+ ordinary Automation pattern already documented: the reaction sits in the previous slice, and an
266
+ internal-only read model is consumed by that reaction, not by a screen or an API query.
267
+ - **Repeat the read model** in every slice where it's read so the timeline flows left-to-right, and
268
+ **prefer `view X again`** for every instance after the first (see Clauses) — **each instance
269
+ carries its own consumer** (the `ui` that reads it, or a reaction), not just the last one. `again`
270
+ instances are exempt from the duplicate-name warning even when referenced, and each reference
271
+ resolves to the right instance — a plain repeat only stays warning-free while nothing references it by name, and
272
+ resolves to the *first* declaration when something does. **Wire each event to a read model exactly
273
+ once:** a repeated instance's `from` lists only the **new** events since the previous instance
274
+ (not cumulative), or the event draws a duplicate arrow to the same read model at every repeat. (An
275
+ event may still feed several *different* read models, once each.)
276
+ - **Instances are never joined to one another.** No arrow between two instances of one read model,
277
+ ever — an explicit one is a validation error. The repeat is a timeline device: continuity is
278
+ implied by the shared name, and the events arriving at each instance are what show it changing.
167
279
  - **Keep arrows span-1: put each repeat right after its feeding event.** Place a read-model instance
168
- immediately after the event that updates it, sourcing only that single adjacent event. A read
169
- model far from its source events draws long arrows that cross the read-model row and read as
170
- read→read (an Event Modeling violation) — repeat the read model next to each event instead.
280
+ immediately after the event that updates it, sourcing only that single adjacent event. The
281
+ renderer routes a long arrow *around* intervening boxes rather than through them, so it no longer
282
+ reads as a forbidden read→read link — but distance is still a real problem: the arrowhead lands
283
+ columns away from the event that produced it, so **the write slice reads as dangling**, as if
284
+ nothing consumed its event. You have to trace a line across the diagram to see the connection.
285
+ Keep the read model adjacent to its event, and keep a sub-flow that detours into another context
286
+ together rather than parking it at the end of the model.
171
287
 
172
288
  ### Slice-ordering gotchas (edge inference)
173
289
 
@@ -175,12 +291,17 @@ slice "Read Quote — created" { view Quote from "QuoteCreated" translation S
175
291
  - A **reaction** (`processor`/`translation` that triggers a command) wires to the command in the
176
292
  **immediately next** slice. So a reaction slice must be *directly* followed by its command slice —
177
293
  don't insert a read slice between them.
178
- - A **read** slice (read model → read translation, no command) must **not** be immediately followed
179
- by a `command` slice, or the read translation will be mis-wired to that command. Put reads after a
180
- command+event slice, or before another read / a reaction / an inbound `(request)` slice.
294
+ - A read slice whose consumer is a **reaction** (`view` + `processor`/`translation`, no command in
295
+ the same slice) must **not** be immediately followed by a `command` slice, or the reaction will be
296
+ mis-wired to that command. Put it after a command+event slice, or before another read/reaction
297
+ slice instead. (A `ui`-consumed read slice has no such risk — `ui` never wires as a reaction.)
181
298
  - A read model fed by an early event (e.g. a queue or to-do view) can't always sit directly after
182
299
  its source event when a reaction must immediately precede its command slice — placing the read
183
300
  later, in narrative order with a longer arrow, is the correct trade-off, not something to force-fix.
301
+ - A `ui` never triggers a reaction — no pattern has a `ui` wired to `processor`/`automation`/
302
+ `saga`/`translation`, only to `command`. A `ui` left in the reaction's own slice (instead of the
303
+ read-model or command slice) renders with no outgoing edge, disconnected, and `em validate` now
304
+ warns on it.
184
305
 
185
306
  ---
186
307
 
@@ -197,27 +318,68 @@ slice "Read Quote — created" { view Quote from "QuoteCreated" translation S
197
318
  there), a reaction reading a view before any instance of it exists, or an explicit backward
198
319
  `arrow`.
199
320
  6. **`again` without an earlier declaration** — declare the view plainly the first time it appears.
321
+ 7. **Illegal connection** — an `arrow` joining kinds the patterns don't connect. Only
322
+ `ui -> command`, `command -> event`, `event -> view`, `view -> ui`, `view -> reaction`, and
323
+ `reaction -> command` are allowed. A command straight to a view is the CQRS violation (an event
324
+ has to sit between them); a view straight to a command needs a reaction between them; and two
325
+ instances of one view are never connected at all. Inferred edges are always legal, so this only
326
+ ever fires on a hand-written `arrow`.
327
+ 8. **Cyclic type reference** — a declared `type` nesting itself with no array to terminate it
328
+ (`type Node { child: Node }`, or the same shape across several types). Break the cycle, or
329
+ route the self/mutual reference through an array (`children: Node[]`) if the data is
330
+ genuinely tree-shaped.
200
331
 
201
332
  **Warnings (should fix):**
202
333
  1. **Automation/translation shares slice with its command** — both `automation`/`processor` and
203
334
  `translation` are reactions; put the triggered command in the *next* slice.
204
- 2. **Command without event** — every command should record at least one event.
205
- 3. **Read model without source** — add `from "Event"` or place the view in a slice with an event.
206
- 4. **Duplicate name** — the same name defined N times; references resolve to the first. Rename.
207
- 5. **Open issue** — an element carries `issue "text"`; resolve the question, then remove the
335
+ 2. **`ui` shares slice with a reaction, no command** — a `ui` only ever wires to a `command`; no
336
+ pattern has a `ui` triggering an automation/processor/saga/translation. Left in the reaction's
337
+ own slice it renders disconnected, with no edge. Move it to the read-model slice, or to the
338
+ slice with the command this eventually triggers.
339
+ 3. **Command with no trigger** — nothing issues it. A command needs a `ui` in its slice, or a
340
+ reaction (automation/processor/saga/translation) in the slice *before* it. The input-side mirror
341
+ of (5): a command nothing points at is a write nobody can start.
342
+ 4. **Command without event** — every command should record at least one event.
343
+ 5. **Event nobody reads** — the mirror of (4): recording an event no read model projects is a
344
+ write with no reader. Follow every write slice with the read slice that consumes its event.
345
+ Counts as read when a `view` names it in `from` (any `again` instance will do), when a
346
+ fieldless-`from` view sits in its slice, or via an explicit `event -> view` arrow.
347
+ 6. **Read model without source** — add `from "Event"` or place the view in a slice with an event.
348
+ 7. **Duplicate name** — the same name defined N times; references resolve to the first. Rename.
349
+ (A duplicate `type` name always warns, unlike a duplicate element name — there's no
350
+ legitimate unreferenced-duplicate case for a named type.)
351
+ 8. **Open issue** — an element carries `issue "text"`; resolve the question, then remove the
208
352
  clause. `em validate --list-issues` prints just these; `--fail-on-issues` (opt-in) makes CI
209
353
  fail while any remain open.
210
- 6. **View field with no source** — a `view` field whose name matches no field on any instance
354
+ 9. **View field with no source** — a `view` field whose name matches no field on any instance
211
355
  of its source events. Only checked once BOTH the view and at least one source event declare
212
356
  `{ fields }`.
213
- 7. **Event field not from a command** — an `event` field whose name matches no field on any
214
- command in the same slice. Only checked once BOTH the event and at least one same-slice
215
- command declare `{ fields }`. This is the payoff of the fields feature for slicing rigor:
216
- once fields are written down, `em validate` checks that data flows forward consistently.
357
+ 10. **Event field not from a command** — an `event` field whose name matches no field on any
358
+ command in the same slice. Only checked once BOTH the event and at least one same-slice
359
+ command declare `{ fields }`. This is the payoff of the fields feature for slicing rigor:
360
+ once fields are written down, `em validate` checks that data flows forward consistently.
361
+
362
+ `divergence "text"` is deliberately NOT in this list — it raises no warning at all, since it
363
+ records a deviation already reasoned through and accepted, not something to fix. Use
364
+ `em validate --list-divergences` to audit them on demand.
217
365
 
218
366
  **Design rules that keep models valid:**
219
367
  - One element per band per slice (multiple personas/contexts are fine — they're different rows).
220
368
  - Every `command` slice includes its `event`. Every `view` has a `from` source.
369
+ - **Every `command` has a trigger.** A `ui` in its slice, or a reaction in the slice *before* it.
370
+ A command nothing points at is a write nobody can start.
371
+ - **Every `event` has a reader.** A command slice isn't finished until the read slice that projects
372
+ its event exists. Reactions don't count — they read views, not events. Pair each write slice with
373
+ its read slice as you go rather than sweeping up dangling events at the end.
374
+ - **Every `view` has a consumer.** A `ui` in its slice, a reaction watching it, or (headless) a read
375
+ translation. Every *instance* of a repeat, not just the last — a bare `view X again` slice is a
376
+ half-slice, not a State View.
377
+ - **Only six connections are legal**, and only these are ever inferred:
378
+ `ui → command`, `command → event`, `event → view`, `view → ui`, `view → reaction`,
379
+ `reaction → command`. Anything else in an explicit `arrow` is an error — above all
380
+ `command → view` (the CQRS violation: an event has to sit between them) and `view → command`
381
+ (a reaction has to sit between them). If you reach for an arrow the patterns don't allow, the
382
+ model is missing an element, not an arrow.
221
383
  - Automations **and translations** are always two slices: the reaction (plus its read model, if
222
384
  internally triggered) in one slice, the triggered `command` + its `event` in the next. A
223
385
  translation/automation slice **never contains an `event`** — reactions trigger commands, not
@@ -58,6 +58,15 @@ Mixed systems happen: pick the dominant mode, note the exception areas in the st
58
58
  implies several events. Apply the event filter hard here — procedural systems are where
59
59
  derived values and activity records masquerade as facts.
60
60
 
61
+ Before minting a new candidate event/command/view name, check whether sibling `.em` models
62
+ already exist in the repo. If they do, run `em glossary <this-model>.em <sibling>.em ...` (or
63
+ `--json` it) as a fuzzy-name-matching aid: it surfaces the same normalized term used with a
64
+ different kind or a different field type across models. A hit there usually means the
65
+ extracted system already has an established name/kind elsewhere — prefer that over minting a
66
+ near-duplicate synonym (e.g. "Order Confirmed" vs. an existing "Order Confirmation"). This is
67
+ informational judgment, not a gate: `em glossary` only flags conflicts, it doesn't resolve
68
+ them.
69
+
61
70
  ## The confirm-and-clarify loop (~7 rounds)
62
71
 
63
72
  One concern per round. After every round: mirror what you extracted back to the user, update
@@ -67,13 +76,20 @@ it is fine to finish a round with `# TBD`s still parked.
67
76
  - **R1 — Candidate events.** Extract (event-driven) or synthesize (procedural) the past-tense
68
77
  facts; apply the event test; user confirms/renames/rejects. Ambiguous candidates → `# TBD`.
69
78
  - **R2 — Timeline order.** Order the confirmed events into the as-is narrative. Identify the
70
- *actual* actors/callers and the *real* UI or API surfaces (headless systems: no
71
- `ui`/`persona` — use write/read translations per `em-dsl.md`). **First `em render` here.**
79
+ *actual* actors/callers and the *real* UI or API surfaces (headless systems: still `ui`/`persona`
80
+ — one persona per caller/role, its `ui` boxes are the API calls, per `em-dsl.md`). **First
81
+ `em render` here.**
72
82
  - **R3 — Commands / inputs.** For each event, the request that actually produces it today
73
- (imperative name). Unclear trigger → `# TBD`, not a plausible guess.
83
+ (imperative name), *and what triggers it* — the real screen, or the scheduler/listener that
84
+ fires it (model that as a reaction in the preceding slice, per R5). Unclear trigger → `# TBD`,
85
+ not a plausible guess; expect a *command has nothing that triggers it* warning until R5 fills
86
+ it in, and don't invent a screen to silence it.
74
87
  - **R4 — Read models / outputs.** The projections/queries the system actually serves; wire
75
- `from "Event"` (repeat read models span-1 per `em-dsl.md`). **Start running `em validate`**
76
- each round from here.
88
+ `from "Event"` (repeat read models span-1 per `em-dsl.md`, using `view X again` after the
89
+ first). **Start running `em validate`** each round from here. Expect *event is not read by any
90
+ read model* warnings: real systems do record fire-and-forget events. In an as-is model that is
91
+ a **finding about the system**, not a modelling mistake — never invent a reader to silence it
92
+ (that would be desired state). Park it in the Decisions log and carry the warning.
77
93
  - **R5 — Boundaries & reactions.** Current automations and integrations as-is: schedulers,
78
94
  listeners, outbound syncs, inbound webhooks. Model each as a reaction with the two-slice
79
95
  `reaction → command → event` split and the slice-ordering rules in `em-dsl.md` — even when
@@ -103,8 +119,9 @@ one for a sprawling one. Record any deviation in the Decisions log.
103
119
  ## Completion & handoff
104
120
 
105
121
  Extraction is complete when: the model renders; `em validate` is clean (or remaining warnings
106
- are consciously accepted in the Decisions log); every unknown is a parked `# TBD`; and the
107
- user has confirmed the model reflects today's behavior. Then:
122
+ are consciously accepted in the Decisions log — unread events in an as-is model are the usual
123
+ accepted case, and each one is worth naming explicitly as a finding); every unknown is a parked
124
+ `# TBD`; and the user has confirmed the model reflects today's behavior. Then:
108
125
 
109
126
  1. State file: set phase progress (extraction rounds checked), log the completion decision.
110
127
  Seed `README.md`'s **slice index** (every slice, pattern, status `none`).