@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.
- package/.claude/skills/event-modeling/SKILL.md +62 -17
- package/.claude/skills/event-modeling/reference/conform.md +33 -6
- package/.claude/skills/event-modeling/reference/em-dsl.md +138 -43
- package/.claude/skills/event-modeling/reference/extract.md +12 -2
- package/.claude/skills/event-modeling/reference/methodology.md +29 -14
- package/.claude/skills/event-modeling/templates/conformance-report.md +9 -7
- package/.claude/skills/event-modeling/templates/slice.md +6 -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 +19 -8
- 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 +98 -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/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 +52 -14
- 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 +30 -0
- package/dist/render/svgGeometry.js +33 -14
- 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
|
@@ -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
|
-
- **
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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;
|
|
138
|
-
|
|
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
|
|
147
|
-
|
|
148
|
-
|
|
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.
|
|
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 `` 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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
116
|
-
|
|
117
|
-
|
|
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. **
|
|
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" {
|
|
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)
|
|
82
|
-
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
169
|
-
|
|
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
|
-
#
|
|
173
|
-
|
|
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
|
-
|
|
255
|
+
ui Read Quote @IntegratorAPI
|
|
186
256
|
}
|
|
187
257
|
```
|
|
188
258
|
|
|
189
|
-
-
|
|
190
|
-
|
|
191
|
-
|
|
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)
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
219
|
-
by a `command` slice, or the
|
|
220
|
-
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.)
|
|
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.
|
|
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 (
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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:
|
|
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
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
147
|
-
- **
|
|
148
|
-
|
|
149
|
-
- **
|
|
150
|
-
|
|
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 "..." #
|
|
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
|
|
246
|
-
|
|
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**
|