@milehimikey/em 1.5.0 → 1.6.1

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 (91) hide show
  1. package/.claude/skills/event-modeling/SKILL.md +62 -17
  2. package/.claude/skills/event-modeling/reference/conform.md +33 -6
  3. package/.claude/skills/event-modeling/reference/em-dsl.md +144 -43
  4. package/.claude/skills/event-modeling/reference/extract.md +12 -2
  5. package/.claude/skills/event-modeling/reference/methodology.md +34 -14
  6. package/.claude/skills/event-modeling/templates/conformance-report.md +9 -7
  7. package/.claude/skills/event-modeling/templates/slice.md +6 -1
  8. package/.claude/skills/event-modeling/templates/state.md +8 -0
  9. package/README.md +1 -0
  10. package/dist/catalog/build.d.ts +33 -0
  11. package/dist/catalog/build.js +117 -0
  12. package/dist/catalog/build.js.map +1 -0
  13. package/dist/catalog/classify.d.ts +14 -0
  14. package/dist/catalog/classify.js +44 -0
  15. package/dist/catalog/classify.js.map +1 -0
  16. package/dist/catalog/pages.d.ts +45 -0
  17. package/dist/catalog/pages.js +174 -0
  18. package/dist/catalog/pages.js.map +1 -0
  19. package/dist/catalog/sliceDoc.d.ts +10 -0
  20. package/dist/catalog/sliceDoc.js +20 -0
  21. package/dist/catalog/sliceDoc.js.map +1 -0
  22. package/dist/cli/catalog-inputs.d.ts +16 -0
  23. package/dist/cli/catalog-inputs.js +18 -0
  24. package/dist/cli/catalog-inputs.js.map +1 -0
  25. package/dist/cli/glossary-inputs.d.ts +15 -0
  26. package/dist/cli/glossary-inputs.js +17 -0
  27. package/dist/cli/glossary-inputs.js.map +1 -0
  28. package/dist/cli/render-inputs.d.ts +27 -0
  29. package/dist/cli/render-inputs.js +41 -0
  30. package/dist/cli/render-inputs.js.map +1 -0
  31. package/dist/cli.js +188 -10
  32. package/dist/cli.js.map +1 -1
  33. package/dist/emit/diffJson.d.ts +1 -1
  34. package/dist/emit/diffJson.js +8 -1
  35. package/dist/emit/diffJson.js.map +1 -1
  36. package/dist/emit/dot.d.ts +7 -0
  37. package/dist/emit/dot.js +10 -4
  38. package/dist/emit/dot.js.map +1 -1
  39. package/dist/emit/glossaryJson.d.ts +13 -0
  40. package/dist/emit/glossaryJson.js +36 -0
  41. package/dist/emit/glossaryJson.js.map +1 -0
  42. package/dist/emit/json.d.ts +16 -2
  43. package/dist/emit/json.js +57 -6
  44. package/dist/emit/json.js.map +1 -1
  45. package/dist/emit/theme.d.ts +3 -0
  46. package/dist/emit/theme.js +19 -0
  47. package/dist/emit/theme.js.map +1 -1
  48. package/dist/model/diff.d.ts +36 -1
  49. package/dist/model/diff.js +198 -35
  50. package/dist/model/diff.js.map +1 -1
  51. package/dist/model/edges.d.ts +12 -1
  52. package/dist/model/edges.js +19 -8
  53. package/dist/model/edges.js.map +1 -1
  54. package/dist/model/glossary.d.ts +68 -0
  55. package/dist/model/glossary.js +138 -0
  56. package/dist/model/glossary.js.map +1 -0
  57. package/dist/model/model.d.ts +35 -0
  58. package/dist/model/model.js +41 -0
  59. package/dist/model/model.js.map +1 -1
  60. package/dist/model/validate.js +114 -3
  61. package/dist/model/validate.js.map +1 -1
  62. package/dist/parser/ast.d.ts +20 -0
  63. package/dist/parser/parser.js +170 -31
  64. package/dist/parser/parser.js.map +1 -1
  65. package/dist/render/drawNotes.d.ts +7 -3
  66. package/dist/render/drawNotes.js +75 -28
  67. package/dist/render/drawNotes.js.map +1 -1
  68. package/dist/render/render.d.ts +20 -1
  69. package/dist/render/render.js +52 -14
  70. package/dist/render/render.js.map +1 -1
  71. package/dist/render/serve.js +155 -0
  72. package/dist/render/serve.js.map +1 -1
  73. package/dist/render/sliceDiagram.d.ts +11 -0
  74. package/dist/render/sliceDiagram.js +198 -0
  75. package/dist/render/sliceDiagram.js.map +1 -0
  76. package/dist/render/sliceOverlay.d.ts +21 -0
  77. package/dist/render/sliceOverlay.js +111 -0
  78. package/dist/render/sliceOverlay.js.map +1 -0
  79. package/dist/render/sliceStatus.d.ts +5 -0
  80. package/dist/render/sliceStatus.js +26 -0
  81. package/dist/render/sliceStatus.js.map +1 -0
  82. package/dist/render/statusOverlay.d.ts +16 -0
  83. package/dist/render/statusOverlay.js +110 -0
  84. package/dist/render/statusOverlay.js.map +1 -0
  85. package/dist/render/svgGeometry.d.ts +30 -0
  86. package/dist/render/svgGeometry.js +33 -14
  87. package/dist/render/svgGeometry.js.map +1 -1
  88. package/dist/render/watch.d.ts +1 -1
  89. package/dist/render/watch.js +5 -2
  90. package/dist/render/watch.js.map +1 -1
  91. package/package.json +3 -2
@@ -42,11 +42,14 @@ validation rules) before doing real work — they are the source of truth. Templ
42
42
  and show the user what changed. Encourage running the live view (`watch`) so a team can follow.
43
43
  - **Keep the `.em` structural; put depth in `note` docs.** The diagram holds flow; invariants,
44
44
  fields, and scenarios live in `slices/*.md` linked via `note "slices/<name>.md"`.
45
- - **No UI in headless/API models.** If the system is headless (clients call an API, not screens),
46
- use no `ui`/`persona`: writes are `external translation → command → event` (name the inbound
47
- adapter for the caller/role); reads are `read model → read translation` (an API query that
48
- triggers **no** command — the analog of `view → ui`). **Repeat read models** across slices so the
49
- timeline flows left-to-right — put each repeat **right after the event that feeds it**, sourcing
45
+ - **Headless/API models still use `ui`.** If the system is headless (clients call an API, not
46
+ screens), declare a persona per external caller/role (e.g. `IntegratorAPI`) and treat its `ui`
47
+ boxes as API calls, not screens: writes are `ui → command → event` and reads are
48
+ `read model → ui`, exactly like any other State Change/State View slice — no separate trigger
49
+ slice. `translation` stays reserved for genuine reactions and real external-system boundaries,
50
+ not a synchronous request/response call. Internal-only commands/views with no public route carry
51
+ no `ui` at all — they follow the ordinary Automation pattern instead. **Repeat read models**
52
+ across slices so the timeline flows left-to-right — put each repeat **right after the event that feeds it**, sourcing
50
53
  only that one adjacent event, so every arrow is short (a read model far from its source events
51
54
  draws a long arrow whose head lands columns away, making the *write* slice read as dangling —
52
55
  keep a sub-flow that detours into another context together, not parked at the end of the model).
@@ -74,7 +77,11 @@ validation rules) before doing real work — they are the source of truth. Templ
74
77
  above all `command → read model` (the CQRS violation) or `read model → command` — means the model
75
78
  is missing an **element**, not an arrow.
76
79
  - **Validate continuously.** Run `em validate` and fix errors/warnings as you go (see DSL ref).
77
- - **Save state at the end of every session** so work resumes cleanly.
80
+ - **Save state at the end of every session** so work resumes cleanly. Also append one line to
81
+ the Usage log: the phase(s) touched and the `em validate` diagnostic *categories* that fired
82
+ (rule name only, e.g. "read model nothing consumes" — never the full message or domain
83
+ content). This is the team's only usage signal today (`docs/usage-data.md`) — keep it cheap
84
+ and habitual, not a task to skip.
78
85
 
79
86
  ## Preconditions (run first)
80
87
 
@@ -133,9 +140,9 @@ phase applies to field tables, below.
133
140
  they aren't re-proposed later. Capture the survivors as a flat list.
134
141
  2. **Plot / storyboard (step 2).** Order the events into the narrative. Identify **personas**
135
142
  (actors) and the **UI** screens at each step. Add `persona` declarations and `ui` elements.
136
- In a **headless/API** model there are no personas or screens — identify the external
137
- **callers** at each step instead; they become the inbound write translations and read
138
- translations (see `reference/methodology.md`).
143
+ In a **headless/API** model there are no human personas or screens — identify the external
144
+ **callers** at each step instead; declare one persona per caller/role, and its `ui` boxes are
145
+ the API calls (see `reference/methodology.md`).
139
146
  3. **Inputs (step 3).** For each event, find the **command** that causes it (imperative name).
140
147
  Form `command → event` slices (State Change pattern). Every command needs a **trigger** in the
141
148
  same breath: the screen the user issues it from (a `ui` in the slice), or — for the Automation
@@ -143,10 +150,10 @@ phase applies to field tables, below.
143
150
  a write nobody can start. *"Who does this, and where are they when they do it?"*
144
151
  4. **Outputs (step 4).** Identify the **read models / views** consumers need and wire them with
145
152
  `from "Event"` (State View pattern), each with the screen (or reaction) that consumes it — a
146
- read model nothing displays is information dropped on the floor. In a **headless/API** model there is no UI — a read model is
147
- consumed by an **external read translation** (the API query, triggers no command), and read
148
- models are **repeated** in each slice where they're read (use `view X again` after the first) so
149
- the timeline flows (see `reference/methodology.md`).
153
+ read model nothing displays is information dropped on the floor. In a **headless/API** model
154
+ the consumer is the caller persona's `ui` (the API query) — same as any screen. Read models are
155
+ **repeated** in each slice where they're read (use `view X again` after the first, each instance
156
+ with its own consumer) so the timeline flows (see `reference/methodology.md`).
150
157
  **Close the loop before leaving this step:** every event from step 3 must be read by at least one
151
158
  read model. Walk the event list and ask, for each one, *"who looks at this, and what do they do
152
159
  with it?"* An event with no answer is either missing its read model or shouldn't be recorded —
@@ -230,12 +237,16 @@ For each slice:
230
237
  already defined elsewhere.
231
238
  2. Write the doc to `slices/<slice-name>.md` (kebab-case the slice name). Record the originating
232
239
  need (ticket/conversation link) in the Intent section when one exists.
233
- 3. Wire it into the `.em`: add `note "slices/<slice-name>.md"` to the slice's primary element
240
+ 3. Render the slice's own diagram: `em render <model>.em --slice "<slice name>" -o
241
+ slices/<slice-name>.svg` (kebab-case, matching the doc's filename) — redraws just this slice
242
+ in its own canonical pattern shape, and add `![Diagram](./<slice-name>.svg)` near the top of
243
+ the doc, right after the metadata bullet list.
244
+ 4. Wire it into the `.em`: add `note "slices/<slice-name>.md"` to the slice's primary element
234
245
  (the command for State Change, the view for State View, the processor for Automation, the
235
246
  translation for Translation).
236
- 4. Update `README.md`'s slice index — the one canonical slice table (`draft` →
247
+ 5. Update `README.md`'s slice index — the one canonical slice table (`draft` →
237
248
  `ready-to-implement`, later `implemented` once shipped, with `Implemented in:` filled in).
238
- 5. Re-render and `em validate`.
249
+ 6. Re-render and `em validate`.
239
250
 
240
251
  ## Phase: `conform` — drift check against the codebase
241
252
 
@@ -273,6 +284,37 @@ appear the moment you save, with no idle churn between edits.
273
284
  `live.html?svg=<model-name>.svg` in a browser. It polls every ~2s and reloads without flicker. No
274
285
  editing of `live.html` is needed — the `?svg=` query picks the model.
275
286
 
287
+ ## Phase: `review` — stakeholder walkthrough
288
+
289
+ Goal: step a real stakeholder review session through the model slice by slice, one slice
290
+ spotlighted at a time, with any open questions the room raises captured live as `issue "..."`
291
+ red notes.
292
+
293
+ **Trigger: a real stakeholder review session is on the calendar — a scheduled use, not
294
+ speculation.** Don't propose this phase proactively; it exists for facilitated reviews with
295
+ non-engineer stakeholders in the room, not routine model editing.
296
+
297
+ Start the same live server as `watch`: `em watch <model-name>.em -o <model-name>.svg --serve`
298
+ (run_in_background). Open the printed URL, click **Review mode** in the header, and share the
299
+ screen. Use Prev/Next (or the left/right arrow keys) to step through slices in declaration
300
+ order — each one pans/zooms into view with everything else dimmed, so the room's attention
301
+ tracks one slice at a time.
302
+
303
+ **Live capture:** when a stakeholder raises something unresolved, add `issue "..."` to the
304
+ relevant element in the `.em` file and save — exactly the same mechanism as any other issue
305
+ (see `validate` below). The browser updates over the existing SSE push within moments, without
306
+ losing the current slice or resetting review mode.
307
+
308
+ Wrap-up: run `em validate --list-issues` to sweep everything captured during the session and
309
+ walk each one with the user, same as any open issue. Update the state file's Participants
310
+ section with who attended, and set a `Last stakeholder review:` marker (mirrors `Last
311
+ conformance:`).
312
+
313
+ End of phase: state file's `Last stakeholder review:` marker updated, every issue captured
314
+ live triaged (resolved on the spot, moved to Open questions / parking lot, or left open on
315
+ purpose). Review doesn't chain to another phase — it's a recurring, scheduled activity like
316
+ `conform`, not a step in the discover → model → slice sequence.
317
+
276
318
  ## Phase: `validate`
277
319
 
278
320
  Run `em validate <model-name>.em` and walk through each diagnostic with the user, explaining the
@@ -310,9 +352,12 @@ em init <name>.em # optional starter scaffold
310
352
  em validate <name>.em # check rules; exit 0 if clean/warnings only
311
353
  em render <name>.em -o <name>.svg # render (svg/png/pdf by extension)
312
354
  em render <name>.em --emit-dot # inspect generated Graphviz DOT
355
+ em render <name>.em --slice "<slice-name>" -o slices/<slug>.svg # this slice's own diagram
313
356
  em watch <name>.em -o <name>.svg # re-render on save (run in background)
314
357
  em watch <name>.em -o <name>.svg --serve # + live viewer with instant push-reload (--port N)
358
+ # click "Review mode" for a slice-by-slice storyboard walkthrough
315
359
  ```
316
360
 
317
361
  Always finish a working session by: re-rendering, running `em validate`, and updating
318
- `.event-modeling.md` with the current phase/step, decisions, and open questions.
362
+ `.event-modeling.md` with the current phase/step, decisions, open questions, and a Usage log
363
+ entry (phases touched + validate diagnostic categories hit).
@@ -102,7 +102,16 @@ For each slice in scope:
102
102
  materialized projection in code — a direct event handler with criteria-sourced state is
103
103
  the same pattern expressed in the code idiom (extract.md's R5 reaction rules). That is
104
104
  NOT structural drift; note the idiom in the evidence log instead of reporting the view
105
- as missing.
105
+ as missing. This is the *general*, idiom-level version of the next bullet — prefer citing
106
+ an actual `divergence` annotation when the element carries one; fall back to this idiom
107
+ note only when it doesn't.
108
+ - **Accepted divergences:** before treating any finding as drift, check whether the
109
+ relevant canonical element already carries a `divergence "..."` annotation (visible in
110
+ the `.em`, in `em export --json`'s `divergence` field, or — for a structural finding — as
111
+ the `acceptedDivergence` field already attached to that `em diff --json` entry, see step
112
+ 3). If it does, this is not a fresh finding to litigate: classify it as **Accepted
113
+ divergence** (step 4) and cite the annotation text as the evidence. This is the specific,
114
+ per-element, ratified counterpart to the idiom-level bullet above.
106
115
  3. Write what you found into the scratch model (`<model-name>-asis.em`, see below). **Seed the
107
116
  scratch model as a copy of the canonical `.em` and replace only the in-scope slices** with
108
117
  the as-is picture, leaving out-of-scope slices byte-identical. This is not an optimization:
@@ -112,9 +121,13 @@ For each slice in scope:
112
121
  this phase exists to avoid. It also keeps the scratch model compilable (see step 3). Within
113
122
  the slices you do rewrite, **reuse the canonical model's slice and element names wherever
114
123
  the code genuinely matches them** — the canonical model is the vocabulary anchor. Give a
115
- new name only to code behavior the model doesn't cover at all. Record the mapping decision
116
- for every element you name this way (e.g. `CartItemAdded` code class ↔ `"Cart Item Added"`
117
- model event) so it's auditable, not a silent judgment call. **Match the canonical model's field granularity:** declare `{ fields }`
124
+ new name only to code behavior the model doesn't cover at all. When you do need a new name,
125
+ check `em glossary` across sibling `.em` models first (same aid the `extract` phase uses) —
126
+ if the term already exists elsewhere under a different kind or spelling, that's a signal to
127
+ reuse the established name rather than mint another synonym for the same concept. Record the
128
+ mapping decision for every element you name this way (e.g. `CartItemAdded` code class ↔
129
+ `"Cart Item Added"` model event) so it's auditable, not a silent judgment call. **Match the
130
+ canonical model's field granularity:** declare `{ fields }`
118
131
  in the scratch model only where the canonical model declares them (if the canonical model
119
132
  puts fields on commands but not events, so does the scratch model — writing code-derived
120
133
  event fields there would flood the diff with `field-added` noise). Schema claims at finer
@@ -147,7 +160,10 @@ Then run `em diff <model-name>.em <model-name>-asis.em --json`. `em`, not you, d
147
160
  structurally differs — don't re-derive by eye what the tool already computed. Parse the JSON
148
161
  (`docs/cli.md`'s diff section documents the envelope: `diffSchemaVersion`, `counts`, `changes`,
149
162
  `removals`, every optional `ChangeEntry` field explicit-`null` when unused) and use it as the
150
- structural-surface findings directly.
163
+ structural-surface findings directly. Check each entry's `acceptedDivergence` field first: a
164
+ non-null value means `em` itself already found a `divergence` annotation on the canonical
165
+ element behind this entry — that finding is Accepted divergence (step 4), not something to
166
+ re-litigate.
151
167
 
152
168
  ### 4. Classify
153
169
 
@@ -157,6 +173,11 @@ step 2 — sorts into exactly one class:
157
173
  - **Real drift** — code evidence contradicts the model or doc.
158
174
  - **Model gap** — code evidence shows behavior neither the model nor the doc covers.
159
175
  - **Internal inconsistency** — the doc and the `.em` disagree with each other (surface 3 only).
176
+ - **Accepted divergence** — the relevant canonical element carries a `divergence "..."`
177
+ annotation (cited directly from a structural entry's `acceptedDivergence` field, or from the
178
+ `.em`/`em export` for a spec/internal finding). This is what the annotation is *for*: a
179
+ reasoned, ratified deviation stops being re-reported as drift on every run. Cite the
180
+ annotation text, not fresh judgment — that judgment already happened when it was written.
160
181
  - **Extraction uncertainty** — no evidence either way. Never reported as drift or a gap.
161
182
 
162
183
  Every classification must cite the evidence recorded in step 2 (or the `em diff` entry, for
@@ -169,7 +190,9 @@ Write `conformance/<YYYY-MM-DD>-report.md` in the model directory, from
169
190
  ready-to-apply red note — `issue "conformance: <text>"` on the right element, written out in
170
191
  the report exactly as it should be pasted into the `.em` — when a model-side marker is the
171
192
  right fix; when the fix is purely doc wording (or the finding is an internal inconsistency
172
- with no clear side to flag), omit the red note and say why instead. **You never edit the
193
+ with no clear side to flag), omit the red note and say why instead. **Never propose a red note
194
+ for a finding already classified Accepted divergence** — report it (so the annotation itself
195
+ stays auditable) but don't ask the user to ratify what's already ratified. **You never edit the
173
196
  canonical model or a slice doc unprompted** — walk the report with the user, apply only the
174
197
  proposals they approve, then re-render and `em validate`.
175
198
 
@@ -228,3 +251,7 @@ whatever cadence the user wants (see `docs/ci.md` for a scheduled-run recipe).
228
251
  - **Re-deriving what `em diff` already decided** — once you have the JSON, use it; don't
229
252
  eyeball the two `.em` files yourself looking for structural differences the tool already
230
253
  computed for you.
254
+ - **Re-litigating an accepted divergence** — a non-null `acceptedDivergence` on a diff entry
255
+ (or a `divergence` annotation on the element behind a spec/internal finding) means this exact
256
+ question was already reasoned through and ratified. Classify it as Accepted divergence and
257
+ cite the annotation; don't propose a new red note or re-open the judgment call from scratch.
@@ -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,22 +48,30 @@ 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.
61
66
  - **`from "X"`** on views/automations declares the source(s); names are quoted, comma-separated.
62
67
  Matching is case-insensitive and whitespace-normalized. A `from` may never point at an event or
63
68
  view that first appears in a LATER slice (forward-only timeline — validation error).
69
+ Kinds never cross: a view's `from` resolves ONLY to events; a reaction's `from` resolves ONLY
70
+ to read models — naming an event on a reaction is a validation error even though the event
71
+ exists. Feed an event to a reaction by projecting it into a view first (the automation's
72
+ "to-do list") and pointing the reaction at that view. Name that view after the pending work
73
+ (`Payments To Process`), never after the triggering event — a view reusing the event's name
74
+ collides in the shared namespace and draws a duplicate-name warning.
64
75
  - **`again`** (views only): `view <Name> again [from "Event", …]` declares a later instance of an
65
76
  already-declared read model — the forward-only device for a view that evolves as later events
66
77
  land. Instances are ONE logical view: the first declaration owns the `note` binding; each
@@ -77,10 +88,49 @@ arrow From Element -> To Element # explicit cross-slice edge (overrides infer
77
88
  on one element) plus a legend entry; `em validate` warns on every open issue. **Prefer this
78
89
  over a `# TBD` comment for anything that should show up on the rendered diagram** — `# TBD` is
79
90
  invisible once rendered, `issue` isn't.
91
+ - **`divergence "text"`** on ANY element records a reasoned, ratified deviation between this
92
+ element and its implementation — the *resolved* sibling of `issue` (lint-suppression-with-
93
+ rationale for the `conform` phase). Renders as a teal corner marker (bottom-right — distinct
94
+ from `note`'s top-right and `issue`'s top-left, so all three can coexist) plus a legend entry.
95
+ Raises **no** `em validate` warning by design; use `--list-divergences` to audit. `em diff
96
+ --json` carries it forward as `acceptedDivergence` on the affected change/removal entry, so
97
+ `conform` can cite an already-ratified deviation instead of re-flagging it as drift every run.
98
+ - **`public`** (events only): marks this event as part of the published integration surface
99
+ (e.g. an AsyncAPI contract), as opposed to an internal-only fact. Plain structural flag, no
100
+ free text and no diagram marker (same posture as `again`). Write it as the last token on the
101
+ line, optionally right before a trailing `@Context` — `event X @Context public` or
102
+ `event X public @Context` both work. `em export` carries it as `public: true`/`false`;
103
+ `em diff` reports a flip as `event marked public`/`event unmarked public`;
104
+ `em validate --list-public` audits which events are public.
80
105
  - **Fields:** `command Place Order { orderId: UUID, items: LineItem[], customerId }` — inline or
81
- one-per-line. Types are free text (no semantic checking). Keep these light; full field specs
82
- with rules live in the slice doc.
106
+ one-per-line. Types are free text (no semantic checking) UNLESS the type string names a
107
+ declared `type` (see Named types below), in which case it resolves to a structured
108
+ reference. Keep these light; full field specs with rules live in the slice doc.
83
109
  - **Comments:** `# ...` anywhere outside quotes (full-line or trailing).
110
+ - **`source "url"` on a slice header** (the only slice-level clause — everything above is
111
+ per-element) links the slice back to the ticket/conversation it traces to, e.g.
112
+ `slice "Checkout" source "https://linear.app/team/issue/MIL-60" { ... }`. Exports as
113
+ `model.slices[].source` via `em export`, so an intake loop stays machine-traversable instead
114
+ of relying on prose. Purely metadata — no visual marker, not validated. Don't confuse it with
115
+ an element's `note "path.md"` (a markdown file link, not a URL).
116
+
117
+ ### Named types
118
+
119
+ `type Name { field: Type, ... }` declares a reusable structured shape at the top level (any
120
+ order relative to `persona`/`context`/`slice`, no clauses). Reference it from any field
121
+ anywhere — bare (`winner: QuoteAcceptedLine`) for a nested object, `Name[]` for an array
122
+ (`lines: QuoteAcceptedLine[]`). Resolution is opportunistic: a type string only becomes a
123
+ structured reference when it names a declared type (case/whitespace-insensitive match); every
124
+ other type string stays free text exactly as before, so declaring no `type` blocks changes
125
+ nothing. Recursion between declared types is allowed as a DAG (e.g. a diamond shape like
126
+ `Order` referencing `Address` twice) — a bare/singular self- or mutual-cycle
127
+ (`type Node { child: Node }`) is a validation error, but the same shape through an array
128
+ (`type Node { children: Node[] }`) is legal since the array can terminate at runtime; this is
129
+ how tree/recursive data (categories, org charts, comment threads, BOMs) gets expressed.
130
+ `em export` lists every declared type under `model.types[]` (stable `ref`, e.g.
131
+ `types/quote-accepted-line`) and adds an additive `typeRef` key to every field (declared-type
132
+ fields and ordinary element fields alike) — `{ name, ref, array }` when resolved, `null`
133
+ otherwise. `em diff` tracks types added/removed and field changes on surviving types.
84
134
 
85
135
  ### Swimlane band order (top → bottom)
86
136
  Header row → **Automation** (only if used) → **persona** rows (in declared order) → **API**
@@ -121,7 +171,7 @@ slice "Open Orders — shipped" {
121
171
  ui Order List @Customer # ...and every read model needs a consumer
122
172
  }
123
173
 
124
- # 4a. Translation (external trigger): external input -> translation -> command -> event
174
+ # 4a. Translation (external trigger, no durable artifact): external call -> translation -> command -> event
125
175
  slice "Carrier Webhook" {
126
176
  translation Carrier Adapter # inbound from outside the model; no internal `from`
127
177
  }
@@ -134,7 +184,26 @@ slice "Open Orders — delivered" {
134
184
  ui Order List @Customer
135
185
  }
136
186
 
137
- # 4b. Translation (internal trigger): read model -> translation -> command -> event
187
+ # 4b. Translation (external trigger, durable artifact): persisted inbound queue -> translation -> command -> event
188
+ slice "Receive Carrier Event" { # ingest: persist the raw webhook before reacting to it
189
+ ui Webhook Endpoint @Carrier
190
+ command Receive Carrier Event
191
+ event Carrier Event Received @Shipping # scoped to @Shipping's fact, not the @Carrier caller
192
+ }
193
+ slice "Inbound Carrier Events" {
194
+ view Inbound Carrier Events from "Carrier Event Received" # the persisted inbound queue
195
+ translation Carrier Adapter
196
+ }
197
+ slice "Acknowledge Carrier Event" {
198
+ command Acknowledge Carrier Event
199
+ event Carrier Event Acknowledged @Shipping
200
+ }
201
+ slice "Inbound Carrier Events — processed" {
202
+ view Inbound Carrier Events again from "Carrier Event Acknowledged"
203
+ ui Delivery Board @Ops
204
+ }
205
+
206
+ # 4c. Translation (internal trigger, durable artifact): read model -> translation -> command -> event
138
207
  slice "Accept Quote" {
139
208
  ui Quote Screen @Customer # every command needs a trigger: a ui, or a reaction before it
140
209
  command Accept Quote
@@ -154,6 +223,15 @@ slice "Accepted Quotes — synced" {
154
223
  }
155
224
  ```
156
225
 
226
+ Trigger source (external/internal) and durable artifact (`view`-backed or not) are independent
227
+ axes — 4a and 4b are both "externally triggered" but only 4b has a queue, and a `view`-backed
228
+ translation reacts the same whether the view was filled by an outside system (4b) or the model's
229
+ own event (4c). An inbound message earns `event` status the same way any event does: scope it to
230
+ the context/lane whose fact it represents (e.g. `Carrier Event Received @Shipping` or its own
231
+ `Carrier` context), not by who committed it — that keeps it a legitimate boundary fact (à la an
232
+ Anti-Corruption-Layer event in DDD) instead of a technical artifact leaking foreign vocabulary
233
+ into the model.
234
+
157
235
  A `translation` (like a `processor`) is a **reaction**: it triggers a command and never carries an
158
236
  `event` in its own slice. Same two-slice split as the Automation pattern above.
159
237
 
@@ -161,38 +239,42 @@ Note the read slice closing each pattern: **every event must be read by some rea
161
239
  (warning 4 below). A command slice is not finished until the slice that projects its event
162
240
  exists. Reactions don't count — they read *views*, not events.
163
241
 
164
- ### Headless / API systems (no UI) & repeated read models
165
-
166
- When the system is headless (no UI — clients call an API), drop `ui`/`persona` entirely:
242
+ ### Headless / API systems & repeated read models
167
243
 
168
- A slice body is always multi-line — there is **no one-line `slice "X" { … }` form**, and writing
169
- one is a parse error (`'slice' is not valid inside a slice`).
244
+ A headless system (no screens — clients call an API) still uses `ui`/`persona`: a slice **is**
245
+ trigger → command → event, read vertically, so the trigger belongs *in* the slice, never split into
246
+ one of its own. Declare a persona per external caller/role and treat its `ui` boxes as API calls
247
+ instead of shipped screens — same two patterns (State Change, State View) as any other slice, no new
248
+ shape:
170
249
 
171
250
  ```em
172
- # Write: external translation -> command -> event (name the inbound adapter for the caller/role)
173
- slice "Create Quote (request)" {
174
- translation SalesRep BU Create Quote
175
- }
251
+ persona IntegratorAPI # API-flagged lane: its `ui` boxes are API calls, not shipped screens
252
+
176
253
  slice "Create Quote" {
254
+ ui Create Quote @IntegratorAPI
177
255
  command Create Quote
178
256
  event QuoteCreated @Quote
179
257
  }
180
258
 
181
- # Read: event(s) -> read model -> READ translation (the external caller's API query, replaces UI).
182
- # This slice is also what makes QuoteCreated a *read* event — without it the model warns.
183
259
  slice "Read Quote — created" {
184
260
  view Quote from "QuoteCreated"
185
- translation SalesRep BU Read Quote
261
+ ui Read Quote @IntegratorAPI
186
262
  }
187
263
  ```
188
264
 
189
- - A **read translation triggers no command** (it returns data outbound) — it is the headless analog
190
- of `view → ui`, *not* a reaction. Only *reaction* translations/processors trigger commands. A read
191
- slice has no `command` and no `event`, so `em validate` stays quiet about it.
265
+ - **`translation` stays reserved for genuine reactions and real external-system boundaries** — an
266
+ internal automation, or a webhook/adapter crossing into another system (see the Automation and
267
+ 4a/4b/4c Translation examples above, unchanged). It is **not** how you model a synchronous
268
+ request/response API call — that's Pattern 1 (State Change) with an API persona, exactly like the
269
+ `Create Quote` slice above.
270
+ - **Internal-only commands and views (no public route) carry no `ui` at all.** They follow the
271
+ ordinary Automation pattern already documented: the reaction sits in the previous slice, and an
272
+ internal-only read model is consumed by that reaction, not by a screen or an API query.
192
273
  - **Repeat the read model** in every slice where it's read so the timeline flows left-to-right, and
193
- **prefer `view X again`** for every instance after the first (see Clauses). `again` instances are
194
- exempt from the duplicate-name warning even when referenced, and each reference resolves to the
195
- right instance — a plain repeat only stays warning-free while nothing references it by name, and
274
+ **prefer `view X again`** for every instance after the first (see Clauses) — **each instance
275
+ carries its own consumer** (the `ui` that reads it, or a reaction), not just the last one. `again`
276
+ instances are exempt from the duplicate-name warning even when referenced, and each reference
277
+ resolves to the right instance — a plain repeat only stays warning-free while nothing references it by name, and
196
278
  resolves to the *first* declaration when something does. **Wire each event to a read model exactly
197
279
  once:** a repeated instance's `from` lists only the **new** events since the previous instance
198
280
  (not cumulative), or the event draws a duplicate arrow to the same read model at every repeat. (An
@@ -215,12 +297,17 @@ slice "Read Quote — created" {
215
297
  - A **reaction** (`processor`/`translation` that triggers a command) wires to the command in the
216
298
  **immediately next** slice. So a reaction slice must be *directly* followed by its command slice —
217
299
  don't insert a read slice between them.
218
- - A **read** slice (read model → read translation, no command) must **not** be immediately followed
219
- by a `command` slice, or the read translation will be mis-wired to that command. Put reads after a
220
- command+event slice, or before another read / a reaction / an inbound `(request)` slice.
300
+ - A read slice whose consumer is a **reaction** (`view` + `processor`/`translation`, no command in
301
+ the same slice) must **not** be immediately followed by a `command` slice, or the reaction will be
302
+ mis-wired to that command. Put it after a command+event slice, or before another read/reaction
303
+ slice instead. (A `ui`-consumed read slice has no such risk — `ui` never wires as a reaction.)
221
304
  - A read model fed by an early event (e.g. a queue or to-do view) can't always sit directly after
222
305
  its source event when a reaction must immediately precede its command slice — placing the read
223
306
  later, in narrative order with a longer arrow, is the correct trade-off, not something to force-fix.
307
+ - A `ui` never triggers a reaction — no pattern has a `ui` wired to `processor`/`automation`/
308
+ `saga`/`translation`, only to `command`. A `ui` left in the reaction's own slice (instead of the
309
+ read-model or command slice) renders with no outgoing edge, disconnected, and `em validate` now
310
+ warns on it.
224
311
 
225
312
  ---
226
313
 
@@ -243,30 +330,44 @@ slice "Read Quote — created" {
243
330
  has to sit between them); a view straight to a command needs a reaction between them; and two
244
331
  instances of one view are never connected at all. Inferred edges are always legal, so this only
245
332
  ever fires on a hand-written `arrow`.
333
+ 8. **Cyclic type reference** — a declared `type` nesting itself with no array to terminate it
334
+ (`type Node { child: Node }`, or the same shape across several types). Break the cycle, or
335
+ route the self/mutual reference through an array (`children: Node[]`) if the data is
336
+ genuinely tree-shaped.
246
337
 
247
338
  **Warnings (should fix):**
248
339
  1. **Automation/translation shares slice with its command** — both `automation`/`processor` and
249
340
  `translation` are reactions; put the triggered command in the *next* slice.
250
- 2. **Command with no trigger** — nothing issues it. A command needs a `ui` in its slice, or a
341
+ 2. **`ui` shares slice with a reaction, no command** — a `ui` only ever wires to a `command`; no
342
+ pattern has a `ui` triggering an automation/processor/saga/translation. Left in the reaction's
343
+ own slice it renders disconnected, with no edge. Move it to the read-model slice, or to the
344
+ slice with the command this eventually triggers.
345
+ 3. **Command with no trigger** — nothing issues it. A command needs a `ui` in its slice, or a
251
346
  reaction (automation/processor/saga/translation) in the slice *before* it. The input-side mirror
252
- of (4): a command nothing points at is a write nobody can start.
253
- 3. **Command without event** — every command should record at least one event.
254
- 4. **Event nobody reads** — the mirror of (3): recording an event no read model projects is a
347
+ of (5): a command nothing points at is a write nobody can start.
348
+ 4. **Command without event** — every command should record at least one event.
349
+ 5. **Event nobody reads** — the mirror of (4): recording an event no read model projects is a
255
350
  write with no reader. Follow every write slice with the read slice that consumes its event.
256
351
  Counts as read when a `view` names it in `from` (any `again` instance will do), when a
257
352
  fieldless-`from` view sits in its slice, or via an explicit `event -> view` arrow.
258
- 5. **Read model without source** — add `from "Event"` or place the view in a slice with an event.
259
- 6. **Duplicate name** — the same name defined N times; references resolve to the first. Rename.
260
- 7. **Open issue** — an element carries `issue "text"`; resolve the question, then remove the
353
+ 6. **Read model without source** — add `from "Event"` or place the view in a slice with an event.
354
+ 7. **Duplicate name** — the same name defined N times; references resolve to the first. Rename.
355
+ (A duplicate `type` name always warns, unlike a duplicate element name — there's no
356
+ legitimate unreferenced-duplicate case for a named type.)
357
+ 8. **Open issue** — an element carries `issue "text"`; resolve the question, then remove the
261
358
  clause. `em validate --list-issues` prints just these; `--fail-on-issues` (opt-in) makes CI
262
359
  fail while any remain open.
263
- 8. **View field with no source** — a `view` field whose name matches no field on any instance
360
+ 9. **View field with no source** — a `view` field whose name matches no field on any instance
264
361
  of its source events. Only checked once BOTH the view and at least one source event declare
265
362
  `{ fields }`.
266
- 9. **Event field not from a command** — an `event` field whose name matches no field on any
267
- command in the same slice. Only checked once BOTH the event and at least one same-slice
268
- command declare `{ fields }`. This is the payoff of the fields feature for slicing rigor:
269
- once fields are written down, `em validate` checks that data flows forward consistently.
363
+ 10. **Event field not from a command** — an `event` field whose name matches no field on any
364
+ command in the same slice. Only checked once BOTH the event and at least one same-slice
365
+ command declare `{ fields }`. This is the payoff of the fields feature for slicing rigor:
366
+ once fields are written down, `em validate` checks that data flows forward consistently.
367
+
368
+ `divergence "text"` is deliberately NOT in this list — it raises no warning at all, since it
369
+ records a deviation already reasoned through and accepted, not something to fix. Use
370
+ `em validate --list-divergences` to audit them on demand.
270
371
 
271
372
  **Design rules that keep models valid:**
272
373
  - One element per band per slice (multiple personas/contexts are fine — they're different rows).
@@ -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,8 +76,9 @@ 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
83
  (imperative name), *and what triggers it* — the real screen, or the scheduler/listener that
74
84
  fires it (model that as a reaction in the preceding slice, per R5). Unclear trigger → `# TBD`,