@milehimikey/em 1.5.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 (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 +138 -43
  4. package/.claude/skills/event-modeling/reference/extract.md +12 -2
  5. package/.claude/skills/event-modeling/reference/methodology.md +29 -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 +98 -1
  61. package/dist/model/validate.js.map +1 -1
  62. package/dist/parser/ast.d.ts +20 -0
  63. package/dist/parser/parser.js +158 -29
  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,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.
@@ -77,10 +82,49 @@ arrow From Element -> To Element # explicit cross-slice edge (overrides infer
77
82
  on one element) plus a legend entry; `em validate` warns on every open issue. **Prefer this
78
83
  over a `# TBD` comment for anything that should show up on the rendered diagram** — `# TBD` is
79
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.
80
99
  - **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.
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.
83
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.
84
128
 
85
129
  ### Swimlane band order (top → bottom)
86
130
  Header row → **Automation** (only if used) → **persona** rows (in declared order) → **API**
@@ -121,7 +165,7 @@ slice "Open Orders — shipped" {
121
165
  ui Order List @Customer # ...and every read model needs a consumer
122
166
  }
123
167
 
124
- # 4a. Translation (external trigger): external input -> translation -> command -> event
168
+ # 4a. Translation (external trigger, no durable artifact): external call -> translation -> command -> event
125
169
  slice "Carrier Webhook" {
126
170
  translation Carrier Adapter # inbound from outside the model; no internal `from`
127
171
  }
@@ -134,7 +178,26 @@ slice "Open Orders — delivered" {
134
178
  ui Order List @Customer
135
179
  }
136
180
 
137
- # 4b. Translation (internal trigger): read model -> translation -> command -> event
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
+ }
199
+
200
+ # 4c. Translation (internal trigger, durable artifact): read model -> translation -> command -> event
138
201
  slice "Accept Quote" {
139
202
  ui Quote Screen @Customer # every command needs a trigger: a ui, or a reaction before it
140
203
  command Accept Quote
@@ -154,6 +217,15 @@ slice "Accepted Quotes — synced" {
154
217
  }
155
218
  ```
156
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
+
157
229
  A `translation` (like a `processor`) is a **reaction**: it triggers a command and never carries an
158
230
  `event` in its own slice. Same two-slice split as the Automation pattern above.
159
231
 
@@ -161,38 +233,42 @@ Note the read slice closing each pattern: **every event must be read by some rea
161
233
  (warning 4 below). A command slice is not finished until the slice that projects its event
162
234
  exists. Reactions don't count — they read *views*, not events.
163
235
 
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:
236
+ ### Headless / API systems & repeated read models
167
237
 
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`).
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:
170
243
 
171
244
  ```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
- }
245
+ persona IntegratorAPI # API-flagged lane: its `ui` boxes are API calls, not shipped screens
246
+
176
247
  slice "Create Quote" {
248
+ ui Create Quote @IntegratorAPI
177
249
  command Create Quote
178
250
  event QuoteCreated @Quote
179
251
  }
180
252
 
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
253
  slice "Read Quote — created" {
184
254
  view Quote from "QuoteCreated"
185
- translation SalesRep BU Read Quote
255
+ ui Read Quote @IntegratorAPI
186
256
  }
187
257
  ```
188
258
 
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.
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.
192
267
  - **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
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
196
272
  resolves to the *first* declaration when something does. **Wire each event to a read model exactly
197
273
  once:** a repeated instance's `from` lists only the **new** events since the previous instance
198
274
  (not cumulative), or the event draws a duplicate arrow to the same read model at every repeat. (An
@@ -215,12 +291,17 @@ slice "Read Quote — created" {
215
291
  - A **reaction** (`processor`/`translation` that triggers a command) wires to the command in the
216
292
  **immediately next** slice. So a reaction slice must be *directly* followed by its command slice —
217
293
  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.
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.)
221
298
  - A read model fed by an early event (e.g. a queue or to-do view) can't always sit directly after
222
299
  its source event when a reaction must immediately precede its command slice — placing the read
223
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.
224
305
 
225
306
  ---
226
307
 
@@ -243,30 +324,44 @@ slice "Read Quote — created" {
243
324
  has to sit between them); a view straight to a command needs a reaction between them; and two
244
325
  instances of one view are never connected at all. Inferred edges are always legal, so this only
245
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.
246
331
 
247
332
  **Warnings (should fix):**
248
333
  1. **Automation/translation shares slice with its command** — both `automation`/`processor` and
249
334
  `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
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
251
340
  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
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
255
344
  write with no reader. Follow every write slice with the read slice that consumes its event.
256
345
  Counts as read when a `view` names it in `from` (any `again` instance will do), when a
257
346
  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
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
261
352
  clause. `em validate --list-issues` prints just these; `--fail-on-issues` (opt-in) makes CI
262
353
  fail while any remain open.
263
- 8. **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
264
355
  of its source events. Only checked once BOTH the view and at least one source event declare
265
356
  `{ 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.
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.
270
365
 
271
366
  **Design rules that keep models valid:**
272
367
  - 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`,
@@ -77,12 +77,11 @@ against invariants and records one or more events.
77
77
  **Event(s) → Read Model → UI.** Past events are projected into a read model that a screen
78
78
  displays. Read-only; changes no state.
79
79
  - `em` shape: `slice { view V from "Event A", "Event B" ui Screen @Persona }`
80
- - **Headless / API systems (no UI):** when there is no UI, a read model is consumed by an
81
- **external caller querying the API**, modeled as a **read translation** — the outbound analog of
82
- `→ ui`. Shape: `slice { view V from "..." translation <Caller> Read <V> }`. A read translation
83
- **triggers no command** (it returns data); it is *not* a reaction, so the
84
- `reaction → command → event` rule does **not** apply to it. (Contrast: a *reaction* translation in
85
- pattern 4 triggers a command.)
80
+ - **Headless / API systems:** still use `ui`/`persona` — declare a persona per external
81
+ caller/role (e.g. `IntegratorAPI`) and treat its `ui` boxes as API calls, not screens. Shape:
82
+ `slice { view V from "..." ui Read V @IntegratorAPI }`, same as any State View slice.
83
+ `translation` stays reserved for genuine reactions/external-system boundaries (pattern 4), not
84
+ for a synchronous request/response API call.
86
85
  - **Repeat read models across the timeline.** A read model is drawn fresh in **every** slice where
87
86
  it is read — after the events that update it, before the actions that consume it — so the diagram
88
87
  shows state flowing left-to-right (the information-completeness staircase). The same read-model
@@ -143,15 +142,30 @@ records an event directly.
143
142
  **Boundary crossing → command → event.** An adapter translates data across a boundary (an external
144
143
  system, or another bounded context) into the model's own language. Like an automation, a
145
144
  translation is a *reaction*: it **triggers a command**, never records an event directly. Two
146
- trigger forms:
147
- - **Externally triggered:** `external input → translation → command → event`. The trigger comes
148
- from outside the model, so the translation has no internal `from`.
149
- - **Internally triggered** (reacting to the modeled system's own state, e.g. pushing data out):
150
- `read model → translation → command → event`. The translation reads a **view** via `from`.
145
+ independent questions shape which form it takes — don't conflate them:
146
+ - **Trigger source:** does the input come from outside the model, or from the model's own state
147
+ pushed back out?
148
+ - **Durable artifact:** is there a queryable, persisted thing behind the trigger (a queue, topic,
149
+ or log), or is it an ephemeral call with nothing to query afterward?
150
+
151
+ The `em` shape only tracks the second question — trigger source doesn't change it:
152
+ - **No durable artifact:** `external input → translation → command → event`. No internal `from`.
153
+ Typically (not necessarily) externally triggered — a bare webhook call with nothing persisted
154
+ behind it.
155
+ - **Durable artifact:** `read model → translation → command → event`. The translation reads a
156
+ **view** via `from`. This shape is the same whether the view was filled by the model's own event
157
+ (internally triggered, e.g. pushing data out) or by an external system's persisted queue
158
+ (externally triggered, e.g. a webhook whose inbound message is stored first for retries,
159
+ ordering, or audit) — an externally triggered translation backed by a real queue is
160
+ architecturally closer to this case than to the no-artifact one above.
161
+ - **Event legitimacy:** a received external message counts as a legitimate domain `event` when
162
+ it's scoped to the context/lane whose fact it represents, not by who committed it — the same
163
+ move as an Anti-Corruption-Layer boundary event in DDD — and stays legitimate as long as it
164
+ doesn't leak into the model's own domain vocabulary.
151
165
  - `em` shape: **two slices**, exactly like an automation, plus the read slice for the event:
152
166
  ```em
153
167
  slice "Boundary" {
154
- view Source from "..." # internally triggered only; omit for an external trigger
168
+ view Source from "..." # durable-artifact form only; omit if there's nothing persisted
155
169
  translation T from "Source"
156
170
  }
157
171
  slice "Record It" {
@@ -242,8 +256,9 @@ Walk the whole model with stakeholders and check for loose ends:
242
256
  - Every **command** has something that **triggers** it — a `ui` in its slice, or a reaction in the
243
257
  slice before it. A command nothing points at is a write nobody can start. `em validate` warns.
244
258
  - Every **command** produces at least one **event**.
245
- - Every **view** has at least one source event, **and a consumer** — a `ui`, a reaction watching it,
246
- or (headless) a read translation. Every *instance* of a repeated view, not just the last one.
259
+ - Every **view** has at least one source event, **and a consumer** — a `ui` (headless: the `ui`
260
+ tagged to the API-caller persona), or a reaction watching it. Every *instance* of a repeated
261
+ view, not just the last one.
247
262
  A read model nothing displays is information projected out and then dropped. `em validate` warns.
248
263
  - Every **event** is **read by a read model** — a `view` naming it in `from` (any `again` instance
249
264
  counts), or a `view` with no `from` sitting in its slice. A reaction consuming it does **not**