@milehimikey/em 1.4.0 → 1.5.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 +62 -16
- package/.claude/skills/event-modeling/reference/conform.md +3 -1
- package/.claude/skills/event-modeling/reference/em-dsl.md +93 -26
- package/.claude/skills/event-modeling/reference/extract.md +12 -5
- package/.claude/skills/event-modeling/reference/methodology.md +103 -21
- package/.claude/skills/event-modeling/templates/model-readme.md +9 -0
- package/.claude/skills/event-modeling/templates/slice.md +15 -1
- package/dist/model/edges.js +7 -16
- package/dist/model/edges.js.map +1 -1
- package/dist/model/validate.js +154 -0
- package/dist/model/validate.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/render.js +5 -3
- package/dist/render/render.js.map +1 -1
- package/dist/render/svgGeometry.d.ts +13 -0
- package/dist/render/svgGeometry.js +47 -0
- package/dist/render/svgGeometry.js.map +1 -1
- package/package.json +1 -1
|
@@ -48,11 +48,31 @@ validation rules) before doing real work — they are the source of truth. Templ
|
|
|
48
48
|
triggers **no** command — the analog of `view → ui`). **Repeat read models** across slices so the
|
|
49
49
|
timeline flows left-to-right — put each repeat **right after the event that feeds it**, sourcing
|
|
50
50
|
only that one adjacent event, so every arrow is short (a read model far from its source events
|
|
51
|
-
draws long
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
draws a long arrow whose head lands columns away, making the *write* slice read as dangling —
|
|
52
|
+
keep a sub-flow that detours into another context together, not parked at the end of the model).
|
|
53
|
+
Declare every instance after the first with
|
|
54
|
+
**`view X again from "..."`** — `again` instances are exempt from the duplicate-name warning even
|
|
55
|
+
when referenced, and each reference resolves to the right instance. Slice order matters: a
|
|
56
|
+
reaction must be directly followed by its command slice, and a read slice must not be immediately
|
|
54
57
|
followed by a command slice (see `em-dsl.md`).
|
|
55
58
|
See `reference/methodology.md` (State View) and `reference/em-dsl.md`.
|
|
59
|
+
- **Never connect two instances of one read model.** The repeat is a timeline device, not a flow:
|
|
60
|
+
continuity is implied by the shared name, and the events arriving at each instance are what show
|
|
61
|
+
it changing. An arrow between instances says the read model feeds itself, and is an error.
|
|
62
|
+
- **Every slice is a COMPLETE pattern, never a half-slice.** A State Change is
|
|
63
|
+
`ui → command → event`; a State View is `event → read model → ui` (or `→ reaction`). So: a
|
|
64
|
+
command needs something that **triggers** it (the `ui` it's issued from, or the reaction in the
|
|
65
|
+
slice before it), its event needs a **reader**, and that read model needs a **consumer** of its
|
|
66
|
+
own. A command nothing points at is a write nobody can start; an event nothing projects is a
|
|
67
|
+
write nobody can see; a read model nothing displays is information dropped on the floor. Write
|
|
68
|
+
the whole chain together rather than sweeping up dangling ends later. Reactions don't count as
|
|
69
|
+
readers of *events* — they read views. **Every instance** of a repeated read model needs its own
|
|
70
|
+
consumer: if you repeat a view next to an event just to keep the arrow short, bring its screen
|
|
71
|
+
along, or don't add the instance.
|
|
72
|
+
- **Only six connections are legal:** `ui → command`, `command → event`, `event → read model`,
|
|
73
|
+
`read model → ui`, `read model → reaction`, `reaction → command`. Reaching for anything else —
|
|
74
|
+
above all `command → read model` (the CQRS violation) or `read model → command` — means the model
|
|
75
|
+
is missing an **element**, not an arrow.
|
|
56
76
|
- **Validate continuously.** Run `em validate` and fix errors/warnings as you go (see DSL ref).
|
|
57
77
|
- **Save state at the end of every session** so work resumes cleanly.
|
|
58
78
|
|
|
@@ -117,12 +137,20 @@ phase applies to field tables, below.
|
|
|
117
137
|
**callers** at each step instead; they become the inbound write translations and read
|
|
118
138
|
translations (see `reference/methodology.md`).
|
|
119
139
|
3. **Inputs (step 3).** For each event, find the **command** that causes it (imperative name).
|
|
120
|
-
Form `command → event` slices (State Change pattern).
|
|
140
|
+
Form `command → event` slices (State Change pattern). Every command needs a **trigger** in the
|
|
141
|
+
same breath: the screen the user issues it from (a `ui` in the slice), or — for the Automation
|
|
142
|
+
and Translation patterns — the reaction in the slice before it. A command nothing points at is
|
|
143
|
+
a write nobody can start. *"Who does this, and where are they when they do it?"*
|
|
121
144
|
4. **Outputs (step 4).** Identify the **read models / views** consumers need and wire them with
|
|
122
|
-
`from "Event"` (State View pattern)
|
|
145
|
+
`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
|
|
123
147
|
consumed by an **external read translation** (the API query, triggers no command), and read
|
|
124
|
-
models are **repeated** in each slice where they're read
|
|
125
|
-
`reference/methodology.md`).
|
|
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`).
|
|
150
|
+
**Close the loop before leaving this step:** every event from step 3 must be read by at least one
|
|
151
|
+
read model. Walk the event list and ask, for each one, *"who looks at this, and what do they do
|
|
152
|
+
with it?"* An event with no answer is either missing its read model or shouldn't be recorded —
|
|
153
|
+
both are worth raising with the user rather than leaving dangling.
|
|
126
154
|
|
|
127
155
|
End of phase: write/refresh the `.em`, render it, update `.event-modeling.md` (steps done,
|
|
128
156
|
decisions, open questions) and `README.md`'s slice index. Tell the user they can stop here and
|
|
@@ -164,12 +192,17 @@ Goal: a structurally complete, **validated** model with correct patterns and swi
|
|
|
164
192
|
event).
|
|
165
193
|
2. **Elaborate scenarios — first pass (step 6).** For each slice, capture the happy-path
|
|
166
194
|
Given/When/Then and the obvious invariants as short notes. (The full spec is the `slice` phase.)
|
|
167
|
-
3. **Evaluate completeness (step 7).** Walk the model: every
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
195
|
+
3. **Evaluate completeness (step 7).** Walk the model: every slice is a **complete** pattern, not a
|
|
196
|
+
half-slice — **every command has something that triggers it**, every command emits an event,
|
|
197
|
+
**every event is read by a read model**, **every read model has a consumer**, every view has a
|
|
198
|
+
source, every UI is reachable, every connection is
|
|
199
|
+
one of the six legal pairs, and automations **and translations** split correctly (each reaction
|
|
200
|
+
triggers a command — none wired straight to an event). Run `em validate` and resolve all errors
|
|
201
|
+
and warnings — but check the reaction→command→event split by hand, since `em validate` does
|
|
202
|
+
**not** flag a translation/automation that emits an event directly (see `reference/em-dsl.md`).
|
|
203
|
+
For an unread event, don't just bolt on a view to silence the warning: ask the user who looks at
|
|
204
|
+
this fact and what they do with it. The honest answers are "here's the read model we missed" or
|
|
205
|
+
"nobody — so why are we recording it", and both improve the model.
|
|
173
206
|
|
|
174
207
|
End of phase: render, update state, suggest `/event-modeling slice` to write implementation specs.
|
|
175
208
|
|
|
@@ -181,7 +214,9 @@ timeline). Check `README.md`'s slice index for what's already done.
|
|
|
181
214
|
This is also where **branch / unhappy-path events** are discovered and added to the model — as a
|
|
182
215
|
slice's alternate/error flows surface (a rejection, removal, cancellation, decline, expiry), add
|
|
183
216
|
the corresponding event/slice to the `.em` and re-render. The happy-path spine from earlier
|
|
184
|
-
phases is the starting point, not the finished event set.
|
|
217
|
+
phases is the starting point, not the finished event set. **Every new event needs its reader too**
|
|
218
|
+
— a rejection or cancellation that nothing projects will warn, and usually the missing piece is
|
|
219
|
+
real (someone has to see that the request was declined).
|
|
185
220
|
|
|
186
221
|
For each slice:
|
|
187
222
|
1. Hold a Socratic deep-dive to fill every section of `templates/slice.md`: intent, trigger/actor,
|
|
@@ -241,8 +276,19 @@ editing of `live.html` is needed — the `?svg=` query picks the model.
|
|
|
241
276
|
## Phase: `validate`
|
|
242
277
|
|
|
243
278
|
Run `em validate <model-name>.em` and walk through each diagnostic with the user, explaining the
|
|
244
|
-
rule (see `reference/em-dsl.md`) and proposing the fix
|
|
245
|
-
|
|
279
|
+
rule (see `reference/em-dsl.md`) and proposing the fix. Apply fixes on agreement. Common ones:
|
|
280
|
+
|
|
281
|
+
| Diagnostic | Fix |
|
|
282
|
+
|---|---|
|
|
283
|
+
| automation/translation shares a slice with a command | Split the command into the next slice |
|
|
284
|
+
| **command has nothing that triggers it** | Add the `ui` it's issued from — or, if the system issues it, the reaction in the slice before it. Ask *"who does this, and where are they when they do it?"* |
|
|
285
|
+
| **read model has no consumer** | Add the screen that displays it, or the reaction that watches it. If it's a repeat added only to shorten an arrow and nothing looks at it there, drop the instance |
|
|
286
|
+
| read model has no source | Add the missing `from "Event"` |
|
|
287
|
+
| command produces no event | Give the command its event, or drop the command |
|
|
288
|
+
| **event is not read by any read model** | Add the read slice that projects it — but ask *who looks at this and what do they do with it* first. "Nobody" is a real answer, and then the question is why it's recorded at all |
|
|
289
|
+
| **illegal `arrow` kind pair** | Don't reroute it — the model is missing an element. `command → view` needs the event between them; `view → command` needs a reaction. Between two instances of one read model, just delete the arrow: the repeat needs no connection |
|
|
290
|
+
| `view X again` with no earlier declaration | Declare the view plainly the first time it appears |
|
|
291
|
+
| event feeding an earlier view instance | Add a `view X again` where the event lands and move the source there |
|
|
246
292
|
|
|
247
293
|
For anything genuinely unresolved rather than a rule violation, prefer `issue "text"` on the
|
|
248
294
|
relevant element over a `# TBD` comment — it shows up as a red marker on the rendered diagram and
|
|
@@ -135,7 +135,9 @@ what every finding in the report will cite — file paths, not vibes.
|
|
|
135
135
|
First `em validate <model-name>-asis.em`. `em diff` refuses when either side has validation
|
|
136
136
|
*errors* (warnings are fine), so a scratch model that doesn't compile stops the run cold. The
|
|
137
137
|
usual cause is a slice that depends on one you rewrote incompletely — a `view X again` whose
|
|
138
|
-
first declaration you dropped,
|
|
138
|
+
first declaration you dropped, an automation `from` a read model that no longer exists, or an
|
|
139
|
+
`arrow` you wrote joining kinds the patterns don't connect. (An unread event is only a *warning*,
|
|
140
|
+
so it never blocks the diff — and in an as-is scratch model it's expected.)
|
|
139
141
|
Seeding from the canonical model (step 2) prevents this; if it still fails, **fix the scratch
|
|
140
142
|
model and report the failure as a tooling problem in the run metadata — never as drift.** A
|
|
141
143
|
model you couldn't compile is an uncertainty about your own scratch file, not a finding
|
|
@@ -64,9 +64,10 @@ arrow From Element -> To Element # explicit cross-slice edge (overrides infer
|
|
|
64
64
|
- **`again`** (views only): `view <Name> again [from "Event", …]` declares a later instance of an
|
|
65
65
|
already-declared read model — the forward-only device for a view that evolves as later events
|
|
66
66
|
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
|
-
|
|
67
|
+
instance's `from` lists only the NEW events landing at that point (not cumulative); a reaction
|
|
68
|
+
(`from "View"`) reads the nearest instance at-or-before its own slice. Instances are NEVER
|
|
69
|
+
connected to one another — continuity is implied by the shared name, and the events reaching each
|
|
70
|
+
instance are what show the view changing. `again` with no earlier declaration is a validation error.
|
|
70
71
|
Use `again` (not a plain repeated `view` name) whenever the view is referenced by a
|
|
71
72
|
`from`/`arrow` — plain repeats are only warning-free while unreferenced.
|
|
72
73
|
- **`note "path.md"`** on ANY element links a markdown doc. Relative to the `.em` file. Renders as
|
|
@@ -106,7 +107,7 @@ slice "Open Orders" {
|
|
|
106
107
|
ui Order List @Customer
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
# 3. Automation: split across TWO slices
|
|
110
|
+
# 3. Automation: split across TWO slices (plus the read slice that consumes the event)
|
|
110
111
|
slice "Orders To Fulfill" {
|
|
111
112
|
view Orders To Fulfill from "Order Placed"
|
|
112
113
|
processor Fulfillment Service
|
|
@@ -115,6 +116,10 @@ slice "Ship Order" { # the triggered command goes in the NEXT slice
|
|
|
115
116
|
command Ship Order
|
|
116
117
|
event Order Shipped @Shipping
|
|
117
118
|
}
|
|
119
|
+
slice "Open Orders — shipped" {
|
|
120
|
+
view Open Orders again from "Order Shipped" # every event needs a reader
|
|
121
|
+
ui Order List @Customer # ...and every read model needs a consumer
|
|
122
|
+
}
|
|
118
123
|
|
|
119
124
|
# 4a. Translation (external trigger): external input -> translation -> command -> event
|
|
120
125
|
slice "Carrier Webhook" {
|
|
@@ -124,8 +129,17 @@ slice "Confirm Delivery" { # the triggered command goes in the NEXT s
|
|
|
124
129
|
command Confirm Delivery
|
|
125
130
|
event Delivery Confirmed @Shipping
|
|
126
131
|
}
|
|
132
|
+
slice "Open Orders — delivered" {
|
|
133
|
+
view Open Orders again from "Delivery Confirmed"
|
|
134
|
+
ui Order List @Customer
|
|
135
|
+
}
|
|
127
136
|
|
|
128
137
|
# 4b. Translation (internal trigger): read model -> translation -> command -> event
|
|
138
|
+
slice "Accept Quote" {
|
|
139
|
+
ui Quote Screen @Customer # every command needs a trigger: a ui, or a reaction before it
|
|
140
|
+
command Accept Quote
|
|
141
|
+
event Quote Accepted @Quote
|
|
142
|
+
}
|
|
129
143
|
slice "Quotes To Sync" {
|
|
130
144
|
view Accepted Quotes from "Quote Accepted"
|
|
131
145
|
translation CRM Sync # reacts to our own state via the read model in this slice
|
|
@@ -134,40 +148,66 @@ slice "Record Sync" {
|
|
|
134
148
|
command Record Crm Sync
|
|
135
149
|
event Quote Synced @Quote
|
|
136
150
|
}
|
|
151
|
+
slice "Accepted Quotes — synced" {
|
|
152
|
+
view Accepted Quotes again from "Quote Synced"
|
|
153
|
+
ui Sync Status @Customer
|
|
154
|
+
}
|
|
137
155
|
```
|
|
138
156
|
|
|
139
157
|
A `translation` (like a `processor`) is a **reaction**: it triggers a command and never carries an
|
|
140
158
|
`event` in its own slice. Same two-slice split as the Automation pattern above.
|
|
141
159
|
|
|
160
|
+
Note the read slice closing each pattern: **every event must be read by some read model**
|
|
161
|
+
(warning 4 below). A command slice is not finished until the slice that projects its event
|
|
162
|
+
exists. Reactions don't count — they read *views*, not events.
|
|
163
|
+
|
|
142
164
|
### Headless / API systems (no UI) & repeated read models
|
|
143
165
|
|
|
144
166
|
When the system is headless (no UI — clients call an API), drop `ui`/`persona` entirely:
|
|
145
167
|
|
|
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`).
|
|
170
|
+
|
|
146
171
|
```em
|
|
147
172
|
# Write: external translation -> command -> event (name the inbound adapter for the caller/role)
|
|
148
|
-
slice "Create Quote (request)" {
|
|
149
|
-
|
|
173
|
+
slice "Create Quote (request)" {
|
|
174
|
+
translation SalesRep BU Create Quote
|
|
175
|
+
}
|
|
176
|
+
slice "Create Quote" {
|
|
177
|
+
command Create Quote
|
|
178
|
+
event QuoteCreated @Quote
|
|
179
|
+
}
|
|
150
180
|
|
|
151
|
-
# Read: event(s) -> read model -> READ translation (the external caller's API query, replaces UI)
|
|
152
|
-
slice
|
|
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
|
+
slice "Read Quote — created" {
|
|
184
|
+
view Quote from "QuoteCreated"
|
|
185
|
+
translation SalesRep BU Read Quote
|
|
186
|
+
}
|
|
153
187
|
```
|
|
154
188
|
|
|
155
189
|
- A **read translation triggers no command** (it returns data outbound) — it is the headless analog
|
|
156
190
|
of `view → ui`, *not* a reaction. Only *reaction* translations/processors trigger commands. A read
|
|
157
191
|
slice has no `command` and no `event`, so `em validate` stays quiet about it.
|
|
158
|
-
- **Repeat the read model** in every slice where it's read so the timeline flows left-to-right
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
192
|
+
- **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
|
|
196
|
+
resolves to the *first* declaration when something does. **Wire each event to a read model exactly
|
|
197
|
+
once:** a repeated instance's `from` lists only the **new** events since the previous instance
|
|
198
|
+
(not cumulative), or the event draws a duplicate arrow to the same read model at every repeat. (An
|
|
199
|
+
event may still feed several *different* read models, once each.)
|
|
200
|
+
- **Instances are never joined to one another.** No arrow between two instances of one read model,
|
|
201
|
+
ever — an explicit one is a validation error. The repeat is a timeline device: continuity is
|
|
202
|
+
implied by the shared name, and the events arriving at each instance are what show it changing.
|
|
167
203
|
- **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
|
|
204
|
+
immediately after the event that updates it, sourcing only that single adjacent event. The
|
|
205
|
+
renderer routes a long arrow *around* intervening boxes rather than through them, so it no longer
|
|
206
|
+
reads as a forbidden read→read link — but distance is still a real problem: the arrowhead lands
|
|
207
|
+
columns away from the event that produced it, so **the write slice reads as dangling**, as if
|
|
208
|
+
nothing consumed its event. You have to trace a line across the diagram to see the connection.
|
|
209
|
+
Keep the read model adjacent to its event, and keep a sub-flow that detours into another context
|
|
210
|
+
together rather than parking it at the end of the model.
|
|
171
211
|
|
|
172
212
|
### Slice-ordering gotchas (edge inference)
|
|
173
213
|
|
|
@@ -197,20 +237,33 @@ slice "Read Quote — created" { view Quote from "QuoteCreated" translation S
|
|
|
197
237
|
there), a reaction reading a view before any instance of it exists, or an explicit backward
|
|
198
238
|
`arrow`.
|
|
199
239
|
6. **`again` without an earlier declaration** — declare the view plainly the first time it appears.
|
|
240
|
+
7. **Illegal connection** — an `arrow` joining kinds the patterns don't connect. Only
|
|
241
|
+
`ui -> command`, `command -> event`, `event -> view`, `view -> ui`, `view -> reaction`, and
|
|
242
|
+
`reaction -> command` are allowed. A command straight to a view is the CQRS violation (an event
|
|
243
|
+
has to sit between them); a view straight to a command needs a reaction between them; and two
|
|
244
|
+
instances of one view are never connected at all. Inferred edges are always legal, so this only
|
|
245
|
+
ever fires on a hand-written `arrow`.
|
|
200
246
|
|
|
201
247
|
**Warnings (should fix):**
|
|
202
248
|
1. **Automation/translation shares slice with its command** — both `automation`/`processor` and
|
|
203
249
|
`translation` are reactions; put the triggered command in the *next* slice.
|
|
204
|
-
2. **Command
|
|
205
|
-
|
|
206
|
-
4
|
|
207
|
-
|
|
250
|
+
2. **Command with no trigger** — nothing issues it. A command needs a `ui` in its slice, or a
|
|
251
|
+
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
|
|
255
|
+
write with no reader. Follow every write slice with the read slice that consumes its event.
|
|
256
|
+
Counts as read when a `view` names it in `from` (any `again` instance will do), when a
|
|
257
|
+
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
|
|
208
261
|
clause. `em validate --list-issues` prints just these; `--fail-on-issues` (opt-in) makes CI
|
|
209
262
|
fail while any remain open.
|
|
210
|
-
|
|
263
|
+
8. **View field with no source** — a `view` field whose name matches no field on any instance
|
|
211
264
|
of its source events. Only checked once BOTH the view and at least one source event declare
|
|
212
265
|
`{ fields }`.
|
|
213
|
-
|
|
266
|
+
9. **Event field not from a command** — an `event` field whose name matches no field on any
|
|
214
267
|
command in the same slice. Only checked once BOTH the event and at least one same-slice
|
|
215
268
|
command declare `{ fields }`. This is the payoff of the fields feature for slicing rigor:
|
|
216
269
|
once fields are written down, `em validate` checks that data flows forward consistently.
|
|
@@ -218,6 +271,20 @@ slice "Read Quote — created" { view Quote from "QuoteCreated" translation S
|
|
|
218
271
|
**Design rules that keep models valid:**
|
|
219
272
|
- One element per band per slice (multiple personas/contexts are fine — they're different rows).
|
|
220
273
|
- Every `command` slice includes its `event`. Every `view` has a `from` source.
|
|
274
|
+
- **Every `command` has a trigger.** A `ui` in its slice, or a reaction in the slice *before* it.
|
|
275
|
+
A command nothing points at is a write nobody can start.
|
|
276
|
+
- **Every `event` has a reader.** A command slice isn't finished until the read slice that projects
|
|
277
|
+
its event exists. Reactions don't count — they read views, not events. Pair each write slice with
|
|
278
|
+
its read slice as you go rather than sweeping up dangling events at the end.
|
|
279
|
+
- **Every `view` has a consumer.** A `ui` in its slice, a reaction watching it, or (headless) a read
|
|
280
|
+
translation. Every *instance* of a repeat, not just the last — a bare `view X again` slice is a
|
|
281
|
+
half-slice, not a State View.
|
|
282
|
+
- **Only six connections are legal**, and only these are ever inferred:
|
|
283
|
+
`ui → command`, `command → event`, `event → view`, `view → ui`, `view → reaction`,
|
|
284
|
+
`reaction → command`. Anything else in an explicit `arrow` is an error — above all
|
|
285
|
+
`command → view` (the CQRS violation: an event has to sit between them) and `view → command`
|
|
286
|
+
(a reaction has to sit between them). If you reach for an arrow the patterns don't allow, the
|
|
287
|
+
model is missing an element, not an arrow.
|
|
221
288
|
- Automations **and translations** are always two slices: the reaction (plus its read model, if
|
|
222
289
|
internally triggered) in one slice, the triggered `command` + its `event` in the next. A
|
|
223
290
|
translation/automation slice **never contains an `event`** — reactions trigger commands, not
|
|
@@ -70,10 +70,16 @@ it is fine to finish a round with `# TBD`s still parked.
|
|
|
70
70
|
*actual* actors/callers and the *real* UI or API surfaces (headless systems: no
|
|
71
71
|
`ui`/`persona` — use write/read translations per `em-dsl.md`). **First `em render` here.**
|
|
72
72
|
- **R3 — Commands / inputs.** For each event, the request that actually produces it today
|
|
73
|
-
(imperative name)
|
|
73
|
+
(imperative name), *and what triggers it* — the real screen, or the scheduler/listener that
|
|
74
|
+
fires it (model that as a reaction in the preceding slice, per R5). Unclear trigger → `# TBD`,
|
|
75
|
+
not a plausible guess; expect a *command has nothing that triggers it* warning until R5 fills
|
|
76
|
+
it in, and don't invent a screen to silence it.
|
|
74
77
|
- **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.
|
|
78
|
+
`from "Event"` (repeat read models span-1 per `em-dsl.md`, using `view X again` after the
|
|
79
|
+
first). **Start running `em validate`** each round from here. Expect *event is not read by any
|
|
80
|
+
read model* warnings: real systems do record fire-and-forget events. In an as-is model that is
|
|
81
|
+
a **finding about the system**, not a modelling mistake — never invent a reader to silence it
|
|
82
|
+
(that would be desired state). Park it in the Decisions log and carry the warning.
|
|
77
83
|
- **R5 — Boundaries & reactions.** Current automations and integrations as-is: schedulers,
|
|
78
84
|
listeners, outbound syncs, inbound webhooks. Model each as a reaction with the two-slice
|
|
79
85
|
`reaction → command → event` split and the slice-ordering rules in `em-dsl.md` — even when
|
|
@@ -103,8 +109,9 @@ one for a sprawling one. Record any deviation in the Decisions log.
|
|
|
103
109
|
## Completion & handoff
|
|
104
110
|
|
|
105
111
|
Extraction is complete when: the model renders; `em validate` is clean (or remaining warnings
|
|
106
|
-
are consciously accepted in the Decisions log
|
|
107
|
-
|
|
112
|
+
are consciously accepted in the Decisions log — unread events in an as-is model are the usual
|
|
113
|
+
accepted case, and each one is worth naming explicitly as a finding); every unknown is a parked
|
|
114
|
+
`# TBD`; and the user has confirmed the model reflects today's behavior. Then:
|
|
108
115
|
|
|
109
116
|
1. State file: set phase progress (extraction rounds checked), log the completion decision.
|
|
110
117
|
Seed `README.md`'s **slice index** (every slice, pattern, status `none`).
|
|
@@ -16,6 +16,20 @@ three ways:
|
|
|
16
16
|
3. There is **no other way** for information to flow. Every box on the diagram is one of:
|
|
17
17
|
UI screen, command, event, read model, or automation/processor.
|
|
18
18
|
|
|
19
|
+
Concretely, that leaves exactly six legal connections, and `em` infers only these:
|
|
20
|
+
`ui → command`, `command → event`, `event → read model`, `read model → ui`,
|
|
21
|
+
`read model → reaction`, `reaction → command`. Every other pair is a rule violation, and an
|
|
22
|
+
explicit `arrow` writing one is a validation error. The two worth naming, because they are the
|
|
23
|
+
tempting shortcuts:
|
|
24
|
+
|
|
25
|
+
- **`command → read model`** — the CQRS violation. A write is only ever visible to a reader
|
|
26
|
+
through the event it recorded. Put the event between them.
|
|
27
|
+
- **`read model → command`** — reads never drive a write directly. A processor or translation
|
|
28
|
+
watches the read model and issues the command, across two slices.
|
|
29
|
+
|
|
30
|
+
If you find yourself wanting an arrow the patterns don't allow, the model is missing an
|
|
31
|
+
**element**, not an arrow.
|
|
32
|
+
|
|
19
33
|
**Automations and translations are not exceptions to rule 1.** They are *reactions* — a processor
|
|
20
34
|
or an adapter that wakes up, decides something must change, and **issues a command** to do it. They
|
|
21
35
|
never record an event themselves. A reaction box always points at a **command** (in the next
|
|
@@ -37,7 +51,24 @@ Every slice is exactly one of these. In `em`, each maps to a specific shape (see
|
|
|
37
51
|
### 1. State Change (Command pattern)
|
|
38
52
|
**UI → Command → Event.** A user (or automation) submits a command; the system validates it
|
|
39
53
|
against invariants and records one or more events.
|
|
40
|
-
-
|
|
54
|
+
- **A State Change never travels alone, and it's joined at both ends.** Something must *trigger*
|
|
55
|
+
the command — the `ui` it's issued from, or (patterns 3 and 4) the reaction in the slice before
|
|
56
|
+
it — and its event needs a read model that projects it. So the unit of work is a State Change
|
|
57
|
+
slice *plus* the State View slice that reads its event. Write them together; a command nothing
|
|
58
|
+
points at is a write nobody can start, an unread event is a write nobody can see, and
|
|
59
|
+
`em validate` warns on each.
|
|
60
|
+
- `em` shape (one element per line — there is no one-line slice form):
|
|
61
|
+
```em
|
|
62
|
+
slice "Do The Thing" {
|
|
63
|
+
ui X @Persona
|
|
64
|
+
command Y
|
|
65
|
+
event Z @Context
|
|
66
|
+
}
|
|
67
|
+
slice "See The Result" { # the State View slice that reads Z — pattern 2, but required here
|
|
68
|
+
view V from "Z"
|
|
69
|
+
ui Screen @Persona
|
|
70
|
+
}
|
|
71
|
+
```
|
|
41
72
|
- This is where **invariants** live — the command is rejected if a rule is violated.
|
|
42
73
|
- Socratic prompts: *"What does the user do here? What request are they making? What must be
|
|
43
74
|
true for it to succeed? What fact gets recorded when it does? What gets rejected and why?"*
|
|
@@ -55,19 +86,30 @@ displays. Read-only; changes no state.
|
|
|
55
86
|
- **Repeat read models across the timeline.** A read model is drawn fresh in **every** slice where
|
|
56
87
|
it is read — after the events that update it, before the actions that consume it — so the diagram
|
|
57
88
|
shows state flowing left-to-right (the information-completeness staircase). The same read-model
|
|
58
|
-
name recurring is intentional and **renders cleanly
|
|
59
|
-
|
|
60
|
-
|
|
89
|
+
name recurring is intentional and **renders cleanly**. Declare every instance after the first with
|
|
90
|
+
**`view V again from "..."`**: `again` instances are exempt from the duplicate-name warning even
|
|
91
|
+
when something references them, and each reference resolves to the right instance. (A plain repeat
|
|
92
|
+
only stays warning-free while nothing references it by name, and resolves to the *first*
|
|
93
|
+
declaration when something does.) **Wire each event to a read model
|
|
61
94
|
exactly once:** a repeated instance's `from` lists only the **new** events since the previous
|
|
62
95
|
instance (not cumulative) — otherwise an event draws a duplicate arrow to the same read model at
|
|
63
96
|
every repeat. (An event may still feed several *different* read models, once each.)
|
|
97
|
+
- **Instances are never connected to one another.** There is no arrow between two appearances of one
|
|
98
|
+
read model, and an explicit one is a validation error. The repeat is an ergonomic device for
|
|
99
|
+
showing the same read model at successive points in time — continuity is implied by the shared
|
|
100
|
+
name, and the events arriving at each instance are what show it changing. An arrow between them
|
|
101
|
+
would say the read model feeds itself.
|
|
64
102
|
- **Place each repeat immediately after its feeding event (span-1).** The cleanest staircase puts a
|
|
65
103
|
read-model instance right after each event that updates it, sourcing **only that one adjacent
|
|
66
104
|
event** — every `event → read model` arrow is then short and forward-flowing. A read model sitting
|
|
67
105
|
far from its source events (e.g. a "list/queue" read placed early but fed by late events) draws
|
|
68
|
-
long arrows that
|
|
69
|
-
|
|
70
|
-
event that
|
|
106
|
+
long arrows that sweep across the diagram. The renderer routes them *around* intervening boxes
|
|
107
|
+
rather than through them, so they no longer *look* like a forbidden read→read link — but the
|
|
108
|
+
distance itself is the problem: the arrowhead lands columns away from the event that produced it,
|
|
109
|
+
so **the write slice reads as dangling**, as if nothing consumed its event. A reader has to trace
|
|
110
|
+
the line across the diagram to see otherwise. The fix is never a `view → view` edge; it's to
|
|
111
|
+
repeat the read model next to each event that feeds it, and to keep a sub-flow that detours into
|
|
112
|
+
another context together rather than parking it at the end of the model.
|
|
71
113
|
- Socratic prompts: *"What does the consumer need to see to make their next decision? Which past
|
|
72
114
|
events provide that information? Is this a screen or an API read? What's the freshness/consistency
|
|
73
115
|
expectation?"*
|
|
@@ -76,10 +118,24 @@ displays. Read-only; changes no state.
|
|
|
76
118
|
**Read Model → Processor → (next slice) Command → Event.** The system reacts on its own: a
|
|
77
119
|
processor watches a read model (a "to-do list" of work) and **issues a command** — it never
|
|
78
120
|
records an event directly.
|
|
79
|
-
- `em` shape: **two slices
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
121
|
+
- `em` shape: **two slices**, then the read slice that consumes the event — three in all:
|
|
122
|
+
```em
|
|
123
|
+
slice "Todo" {
|
|
124
|
+
view Todo from "..."
|
|
125
|
+
processor P
|
|
126
|
+
}
|
|
127
|
+
slice "Do It" { # the triggered command goes in the NEXT slice
|
|
128
|
+
command C
|
|
129
|
+
event E @Context
|
|
130
|
+
}
|
|
131
|
+
slice "Todo — done" {
|
|
132
|
+
view Todo again from "E" # every event needs a reader; here it clears the to-do list
|
|
133
|
+
ui Work Board @Persona # ...and every read model needs a consumer
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
The processor band holds no event; the command in the next slice produces it. Keeping the
|
|
137
|
+
triggered command in the same slice as the processor is a validation warning — always split it.
|
|
138
|
+
Forgetting the third slice leaves `E` unread, which is also a warning.
|
|
83
139
|
- Socratic prompts: *"What should happen without a human? What condition triggers it? What work
|
|
84
140
|
list does the processor watch? What command does it fire? What if it fails or retries?"*
|
|
85
141
|
|
|
@@ -92,9 +148,22 @@ trigger forms:
|
|
|
92
148
|
from outside the model, so the translation has no internal `from`.
|
|
93
149
|
- **Internally triggered** (reacting to the modeled system's own state, e.g. pushing data out):
|
|
94
150
|
`read model → translation → command → event`. The translation reads a **view** via `from`.
|
|
95
|
-
- `em` shape: **two slices**, exactly like an automation
|
|
96
|
-
|
|
97
|
-
|
|
151
|
+
- `em` shape: **two slices**, exactly like an automation, plus the read slice for the event:
|
|
152
|
+
```em
|
|
153
|
+
slice "Boundary" {
|
|
154
|
+
view Source from "..." # internally triggered only; omit for an external trigger
|
|
155
|
+
translation T from "Source"
|
|
156
|
+
}
|
|
157
|
+
slice "Record It" {
|
|
158
|
+
command C
|
|
159
|
+
event E @Context
|
|
160
|
+
}
|
|
161
|
+
slice "Source — recorded" {
|
|
162
|
+
view Source again from "E" # every event needs a reader
|
|
163
|
+
ui Status Screen @Persona # ...and every read model needs a consumer
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
The translation band never carries an event.
|
|
98
167
|
- Socratic prompts: *"What boundary are we crossing, and which way? What outside system or context
|
|
99
168
|
feeds us (or do we feed)? In what format? How do we know its data is trustworthy/complete? What
|
|
100
169
|
internal **command** does it trigger, and what event does that command record?"*
|
|
@@ -170,17 +239,30 @@ dedicated `slice` phase writes the full rich spec (see `templates/slice.md`).
|
|
|
170
239
|
|
|
171
240
|
### Step 7 — Evaluate Completeness *(model)*
|
|
172
241
|
Walk the whole model with stakeholders and check for loose ends:
|
|
242
|
+
- Every **command** has something that **triggers** it — a `ui` in its slice, or a reaction in the
|
|
243
|
+
slice before it. A command nothing points at is a write nobody can start. `em validate` warns.
|
|
173
244
|
- Every **command** produces at least one **event**.
|
|
174
|
-
- Every **view** has at least one source event
|
|
175
|
-
|
|
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.
|
|
247
|
+
A read model nothing displays is information projected out and then dropped. `em validate` warns.
|
|
248
|
+
- Every **event** is **read by a read model** — a `view` naming it in `from` (any `again` instance
|
|
249
|
+
counts), or a `view` with no `from` sitting in its slice. A reaction consuming it does **not**
|
|
250
|
+
count: reactions read views, not events. An event nothing projects is a write nobody can see, so
|
|
251
|
+
there was no point recording it. `em validate` warns on each one.
|
|
176
252
|
- Every **UI** is reachable and leads somewhere.
|
|
253
|
+
- Every connection is one of the six legal pairs (`ui → command`, `command → event`,
|
|
254
|
+
`event → read model`, `read model → ui`, `read model → reaction`, `reaction → command`). No
|
|
255
|
+
arrow between two instances of one read model.
|
|
177
256
|
- Automations **and** translations are split correctly (reaction → command → event, never a
|
|
178
257
|
reaction wired straight to an event); translations cover every external input.
|
|
179
|
-
- Run `em validate` and resolve all errors and warnings.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
258
|
+
- Run `em validate` and resolve all errors and warnings. It now catches illegal `arrow` kind pairs
|
|
259
|
+
(error) and unread events (warning), but it still does **not** catch a translation/automation that
|
|
260
|
+
emits an event directly — verify the two-slice split by hand.
|
|
261
|
+
- Prompts: *"Is there any event nobody reads — and if the business genuinely never looks at it, why
|
|
262
|
+
are we recording it? Any command that just happens, with nobody and nothing asking for it? Any
|
|
263
|
+
screen with no way in or out? Any command that records nothing? Any
|
|
264
|
+
external system we haven't translated? Any translation or automation that records an event instead
|
|
265
|
+
of triggering a command?"*
|
|
184
266
|
|
|
185
267
|
---
|
|
186
268
|
|
|
@@ -21,6 +21,15 @@ Static render: `em render {{model-name}}.em -o {{model-name}}.svg`
|
|
|
21
21
|
- **Automation** — Read Model → Processor → (next slice) Command → Event
|
|
22
22
|
- **Translation** — External input (or Read Model) → Translation → (next slice) Command → Event
|
|
23
23
|
|
|
24
|
+
Between them these are the only legal connections: `ui → command`, `command → event`,
|
|
25
|
+
`event → read model`, `read model → ui`, `read model → reaction`, `reaction → command`. A command
|
|
26
|
+
never reaches a read model directly — the event goes between them. Every slice is joined up at
|
|
27
|
+
both ends: something triggers each command (the screen it's issued from, or the reaction in the
|
|
28
|
+
slice before it), and every event a command records is read by some read model — so each State
|
|
29
|
+
Change slice is paired with the State View slice that projects its event. A read model repeated
|
|
30
|
+
along the timeline (`view X again`) shows the same projection at a later point; the instances are
|
|
31
|
+
never connected to one another.
|
|
32
|
+
|
|
24
33
|
## Slices
|
|
25
34
|
<!-- The canonical slice index — the ONE place slices are enumerated (the state file
|
|
26
35
|
points here rather than keeping its own copy). Keep it in sync as slice docs are
|
|
@@ -32,9 +32,20 @@ an event directly.}}
|
|
|
32
32
|
|-------|------|----------|--------------------|
|
|
33
33
|
| {{field}} | {{Type}} | {{yes/no}} | {{constraints, formats, ranges}} |
|
|
34
34
|
|
|
35
|
+
## Trigger
|
|
36
|
+
<!-- What issues this slice's command. Required: a command nothing points at is a write nobody
|
|
37
|
+
can start. Either the screen the user acts on (a `ui` in this slice), or the reaction that
|
|
38
|
+
issues it (an automation/processor/translation in the PREVIOUS slice). -->
|
|
39
|
+
**Triggered by:** {{screen `X` @Persona | processor `Y` in slice "Z"}}
|
|
40
|
+
|
|
35
41
|
## Event(s) Emitted
|
|
36
42
|
<!-- The immutable facts recorded. List each event and its payload. -->
|
|
37
43
|
**Event:** `{{Event Name}}` → context `{{Context}}`
|
|
44
|
+
**Read by:** {{which read model projects this event, and in which slice}}
|
|
45
|
+
<!-- Required, not optional. Every event must be read by a read model — an event nothing
|
|
46
|
+
projects is a write nobody can see, and `em validate` warns on it. A reaction consuming
|
|
47
|
+
it does NOT count: reactions read views, not events. If the honest answer is "nothing
|
|
48
|
+
reads it", that's a question for the business, not a field to leave blank. -->
|
|
38
49
|
|
|
39
50
|
| Field | Type | Immutable Fact? | Source / Notes |
|
|
40
51
|
|-------|------|-----------------|----------------|
|
|
@@ -43,7 +54,10 @@ an event directly.}}
|
|
|
43
54
|
## Read Model / View
|
|
44
55
|
<!-- For State View slices, and any read model this slice produces or feeds. -->
|
|
45
56
|
- **View:** `{{View Name}}` built from events: {{"Event A", "Event B"}}
|
|
46
|
-
- **Consumed by:** {{which UI screen or
|
|
57
|
+
- **Consumed by:** {{which UI screen, reaction, or read translation}}
|
|
58
|
+
<!-- "Consumed by" is required, not optional. A read model nothing displays or watches is
|
|
59
|
+
information projected out of the system and then dropped, and `em validate` warns. Every
|
|
60
|
+
instance of a repeated view needs its own consumer, not just the last one. -->
|
|
47
61
|
- **Freshness / consistency expectation:** {{real-time | eventual | on-demand}}
|
|
48
62
|
|
|
49
63
|
## Invariants / Business Rules
|