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