@milehimikey/em 1.6.1 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/.claude/skills/event-modeling/SKILL.md +94 -49
  2. package/.claude/skills/event-modeling/reference/conform.md +30 -8
  3. package/.claude/skills/event-modeling/reference/em-dsl.md +194 -73
  4. package/.claude/skills/event-modeling/reference/extract.md +14 -11
  5. package/.claude/skills/event-modeling/reference/implement.md +172 -0
  6. package/.claude/skills/event-modeling/reference/methodology.md +40 -33
  7. package/.claude/skills/event-modeling/templates/conformance-report.md +15 -9
  8. package/.claude/skills/event-modeling/templates/model-readme.md +7 -7
  9. package/.claude/skills/event-modeling/templates/slice.md +73 -7
  10. package/.claude/skills/event-modeling/templates/state.md +7 -6
  11. package/README.md +1 -0
  12. package/dist/catalog/build.d.ts +5 -0
  13. package/dist/catalog/build.js +1 -3
  14. package/dist/catalog/build.js.map +1 -1
  15. package/dist/catalog/classify.d.ts +7 -8
  16. package/dist/catalog/classify.js +7 -8
  17. package/dist/catalog/classify.js.map +1 -1
  18. package/dist/catalog/docJoin.d.ts +39 -0
  19. package/dist/catalog/docJoin.js +86 -0
  20. package/dist/catalog/docJoin.js.map +1 -0
  21. package/dist/catalog/driftSignal.d.ts +16 -0
  22. package/dist/catalog/driftSignal.js +24 -0
  23. package/dist/catalog/driftSignal.js.map +1 -0
  24. package/dist/catalog/frontmatterCoherenceValidate.d.ts +8 -0
  25. package/dist/catalog/frontmatterCoherenceValidate.js +52 -0
  26. package/dist/catalog/frontmatterCoherenceValidate.js.map +1 -0
  27. package/dist/catalog/lineageValidate.d.ts +9 -0
  28. package/dist/catalog/lineageValidate.js +161 -0
  29. package/dist/catalog/lineageValidate.js.map +1 -0
  30. package/dist/catalog/readSliceDoc.d.ts +2 -0
  31. package/dist/catalog/readSliceDoc.js +21 -0
  32. package/dist/catalog/readSliceDoc.js.map +1 -0
  33. package/dist/catalog/sliceDoc.d.ts +94 -3
  34. package/dist/catalog/sliceDoc.js +157 -7
  35. package/dist/catalog/sliceDoc.js.map +1 -1
  36. package/dist/catalog/sliceReadyValidate.d.ts +11 -0
  37. package/dist/catalog/sliceReadyValidate.js +85 -0
  38. package/dist/catalog/sliceReadyValidate.js.map +1 -0
  39. package/dist/cli/diff-inputs.d.ts +20 -0
  40. package/dist/cli/diff-inputs.js +75 -18
  41. package/dist/cli/diff-inputs.js.map +1 -1
  42. package/dist/cli/ledgerCheck.d.ts +38 -0
  43. package/dist/cli/ledgerCheck.js +116 -0
  44. package/dist/cli/ledgerCheck.js.map +1 -0
  45. package/dist/cli/skillCheck.d.ts +18 -0
  46. package/dist/cli/skillCheck.js +63 -0
  47. package/dist/cli/skillCheck.js.map +1 -0
  48. package/dist/cli/skillSync.d.ts +22 -0
  49. package/dist/cli/skillSync.js +69 -0
  50. package/dist/cli/skillSync.js.map +1 -0
  51. package/dist/cli/skillVersionCheck.d.ts +32 -0
  52. package/dist/cli/skillVersionCheck.js +145 -0
  53. package/dist/cli/skillVersionCheck.js.map +1 -0
  54. package/dist/cli.d.ts +3 -1
  55. package/dist/cli.js +203 -32
  56. package/dist/cli.js.map +1 -1
  57. package/dist/emit/diffJson.d.ts +1 -1
  58. package/dist/emit/diffJson.js +18 -14
  59. package/dist/emit/diffJson.js.map +1 -1
  60. package/dist/emit/dot.js +46 -5
  61. package/dist/emit/dot.js.map +1 -1
  62. package/dist/emit/json.d.ts +10 -25
  63. package/dist/emit/json.js +70 -103
  64. package/dist/emit/json.js.map +1 -1
  65. package/dist/emit/ledgerJson.d.ts +5 -0
  66. package/dist/emit/ledgerJson.js +25 -0
  67. package/dist/emit/ledgerJson.js.map +1 -0
  68. package/dist/emit/skillCheckJson.d.ts +5 -0
  69. package/dist/emit/skillCheckJson.js +32 -0
  70. package/dist/emit/skillCheckJson.js.map +1 -0
  71. package/dist/model/diff.d.ts +44 -1
  72. package/dist/model/diff.js +51 -9
  73. package/dist/model/diff.js.map +1 -1
  74. package/dist/model/edges.js +17 -17
  75. package/dist/model/edges.js.map +1 -1
  76. package/dist/model/model.d.ts +1 -1
  77. package/dist/model/refs.d.ts +23 -0
  78. package/dist/model/refs.js +75 -0
  79. package/dist/model/refs.js.map +1 -0
  80. package/dist/model/rules.d.ts +253 -0
  81. package/dist/model/rules.js +254 -0
  82. package/dist/model/rules.js.map +1 -0
  83. package/dist/model/validate.d.ts +23 -1
  84. package/dist/model/validate.js +207 -69
  85. package/dist/model/validate.js.map +1 -1
  86. package/dist/parser/ast.d.ts +3 -2
  87. package/dist/parser/lexer.d.ts +17 -2
  88. package/dist/parser/lexer.js +119 -13
  89. package/dist/parser/lexer.js.map +1 -1
  90. package/dist/parser/parser.js +75 -34
  91. package/dist/parser/parser.js.map +1 -1
  92. package/dist/pipeline.d.ts +9 -0
  93. package/dist/pipeline.js +4 -2
  94. package/dist/pipeline.js.map +1 -1
  95. package/dist/render/sliceDiagram.js +33 -35
  96. package/dist/render/sliceDiagram.js.map +1 -1
  97. package/dist/render/sliceStatus.js +1 -1
  98. package/dist/render/sliceStatus.js.map +1 -1
  99. package/dist/util/isMainModule.d.ts +14 -0
  100. package/dist/util/isMainModule.js +33 -0
  101. package/dist/util/isMainModule.js.map +1 -0
  102. package/dist/util/walkDir.d.ts +4 -0
  103. package/dist/util/walkDir.js +28 -0
  104. package/dist/util/walkDir.js.map +1 -0
  105. package/package.json +5 -2
@@ -10,20 +10,61 @@ Keep `.em` files focused on **structure**; put deep design in markdown linked vi
10
10
 
11
11
  ## CLI
12
12
 
13
+ <!-- GENERATED:cli:start -- run `npm run docs:generate` to refresh, do not hand-edit -->
13
14
  ```bash
14
- em init [file] # scaffold a starter model.em (default: model.em); -f to overwrite
15
- em render <file> -o out.svg # render (extension picks format: svg, png, pdf)
16
- em render <file> --emit-dot # print the Graphviz DOT instead of rendering
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
19
- em watch <file> -o out.svg # re-render on every save (file-based)
20
- em watch <file> -o out.svg --serve # + localhost live viewer, instant SSE push-reload (--port N)
21
- em validate <file> # check event-modeling rules; exit 0 if only warnings/clean
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)
25
- em validate <file> --fail-on-issues # opt-in CI gate: exit non-zero while any issue remains open
15
+ em --version # print the installed em version
16
+ em init [file] # scaffold a starter .em model
17
+ em init [file] -f, --force # overwrite if the file exists
18
+ em render <file> # transpile a model and render it (or emit DOT)
19
+ em render <file> -o, --out <path> # output path (extension picks the format)
20
+ em render <file> -T, --format <fmt> # output format (svg, png, pdf, ...)
21
+ em render <file> --slice <name> # render only this slice, redrawn in its own canonical pattern shape (default out: slices/<kebab-slug>.svg)
22
+ em render <file> --emit-dot # print the generated DOT instead of rendering
23
+ em render <file> --keep-empty-lanes # keep the API lane even when empty
24
+ em export <file> # export a versioned JSON snapshot of the normalized model
25
+ em export <file> -o, --out <path> # write to a file instead of stdout
26
+ em diff <old> [new] # compare two models structurally (two files, or one file across git revisions)
27
+ em diff <old> [new] --from <rev> # diff <old> against this git revision instead of a second file
28
+ em diff <old> [new] --to <rev> # diff against this git revision instead of the current file (requires --from)
29
+ em diff <old> [new] --exit-code # exit 1 if the models differ, 0 if identical (git-diff convention)
30
+ em diff <old> [new] --json # print a JSON document instead of the text report (see docs/cli.md)
31
+ em glossary <files> # cross-model glossary of terms, with consistency checks across models (see docs/cli.md)
32
+ em glossary <files> --json # print the full glossary document instead of the text report
33
+ em glossary <files> -o, --out <path> # write the JSON document to a file instead of stdout (requires --json)
34
+ em glossary <files> --list-conflicts # print only the conflict lines, no summary
35
+ em glossary <files> --fail-on-conflicts # exit non-zero if any cross-model term conflicts were found (opt-in — conflicts are warnings and don't block by default)
36
+ em catalog <files> # generate a browsable static HTML catalog site over one or more .em models (see docs/cli.md)
37
+ em catalog <files> -o, --out <dir> # output directory
38
+ em catalog <files> -T, --format <fmt> # diagram format embedded in the catalog (svg or png)
39
+ em catalog <files> --title <text> # catalog site title
40
+ em catalog <files> --keep-empty-lanes # keep the API lane even when empty
41
+ em changelog <file> # render a model's git history as a business-readable ledger (see docs/cli.md)
42
+ em changelog <file> --from <rev> # start the walk at this revision (inclusive)
43
+ em changelog <file> --to <rev> # end the walk at this revision (inclusive; default HEAD)
44
+ em changelog <file> -o, --out <path> # write to a file instead of stdout
45
+ em watch <file> # re-render on every save
46
+ em watch <file> -o, --out <path> # output path (extension picks the format)
47
+ em watch <file> -T, --format <fmt> # output format (svg, png, pdf, ...)
48
+ em watch <file> --keep-empty-lanes # keep the API lane even when empty
49
+ em watch <file> --serve # serve a live viewer with instant push-reload (no polling)
50
+ em watch <file> --port <n> # port for --serve (default 5173)
51
+ em validate <file> # check a model against event-modeling rules
52
+ em validate <file> --list-issues # print only open `issue` diagnostics (slice, element, line, text)
53
+ em validate <file> --list-divergences # print only accepted-divergence annotations (slice, element, line, text) — never fails the build
54
+ em validate <file> --list-public # print only events and views marked `public` (slice, kind, name, line) — an integration-surface audit, never fails the build
55
+ em validate <file> --fail-on-issues # exit non-zero if the model has any open `issue`s (opt-in — issues are warnings and don't block by default)
56
+ em validate <file> --slice-ready <key> # readiness gate for one slice (export key): status ready-to-implement, doc resolvable via note binding, zero unchecked Open Questions — exits non-zero if not ready (MIL-87)
57
+ em ledger <file> # check slice docs' version: field agrees with their content across two git revisions (opt-in CI check, MIL-89 — never part of `em validate`, see docs/ci.md)
58
+ em ledger <file> --from <rev> # baseline revision
59
+ em ledger <file> --to <rev> # compare revision (default: current working tree)
60
+ em ledger <file> --json # print a JSON document instead of the text report (see docs/cli.md)
61
+ em skill install # copy the event-modeling skill into .claude/skills/event-modeling/
62
+ em skill install -f, --force # overwrite an existing installation
63
+ em skill sync [path] # update the vendored .claude/skills/event-modeling/ copy in [path] to match the installed em package (overwrites unconditionally; local edits are never merged, MIL-93)
64
+ em skill check [path] # check the vendored .claude/skills/event-modeling/ copy in [path] for drift against the installed em package; exits non-zero on any mismatch (CI-ready, MIL-93)
65
+ em skill check [path] --json # print a JSON document instead of the text report (see docs/cli.md)
26
66
  ```
67
+ <!-- GENERATED:cli:end -->
27
68
 
28
69
  Install if missing: `npm i -g @milehimikey/em`. PNG works with no system deps; PDF needs
29
70
  `rsvg-convert`.
@@ -57,12 +98,14 @@ type Name { field: Type, ... } # named structured type, reusable from any f
57
98
  |---|---|---|---|---|
58
99
  | `ui` | persona | screen / interface | `@Persona` | `note`, `issue`, `divergence`, `{ fields }` |
59
100
  | `command` | API | state-changing request | — | `note`, `issue`, `divergence`, `{ fields }` |
60
- | `view` | API | read model / projection | — | `from "Event"…`, `note`, `issue`, `divergence`, `{ fields }` |
101
+ | `view` | API | read model / projection | — | `from "Event"…`, `note`, `issue`, `divergence`, `public`, `{ fields }` |
61
102
  | `event` | context | recorded fact (past tense) | `@Context` | `note`, `issue`, `divergence`, `public`, `{ fields }` |
62
103
  | `processor` / `automation` / `saga` / `translation` | automation | system reaction / adapter | — | `from "…"`, `note`, `issue`, `divergence`, `{ fields }` |
63
104
 
64
105
  ### Clauses
65
- - **Tags:** `@Persona` only on `ui`; `@Context` only on `event`. Undeclared tags auto-create a row.
106
+ - **Tags:** `@Persona` only on `ui`; `@Context` only on `event`. Undeclared tags auto-create a
107
+ row. Multi-word tags need no quoting — the tag captures everything after `@` to end of line
108
+ (`ui Ticket Queue @Customer Service` matches `persona Customer Service`).
66
109
  - **`from "X"`** on views/automations declares the source(s); names are quoted, comma-separated.
67
110
  Matching is case-insensitive and whitespace-normalized. A `from` may never point at an event or
68
111
  view that first appears in a LATER slice (forward-only timeline — validation error).
@@ -95,18 +138,26 @@ type Name { field: Type, ... } # named structured type, reusable from any f
95
138
  Raises **no** `em validate` warning by design; use `--list-divergences` to audit. `em diff
96
139
  --json` carries it forward as `acceptedDivergence` on the affected change/removal entry, so
97
140
  `conform` can cite an already-ratified deviation instead of re-flagging it as drift every run.
98
- - **`public`** (events only): marks this event as part of the published integration surface
99
- (e.g. an AsyncAPI contract), as opposed to an internal-only fact. Plain structural flag, no
100
- free text and no diagram marker (same posture as `again`). Write it as the last token on the
101
- line, optionally right before a trailing `@Context` — `event X @Context public` or
102
- `event X public @Context` both work. `em export` carries it as `public: true`/`false`;
103
- `em diff` reports a flip as `event marked public`/`event unmarked public`;
104
- `em validate --list-public` audits which events are public.
141
+ - **`public`** (events and views): marks the element as part of the published integration
142
+ surface — a published event contract (e.g. AsyncAPI), or a view whose read API/response
143
+ shape external consumers depend on — as opposed to an internal-only fact or projection.
144
+ Plain structural flag, no free text and no diagram marker (same posture as `again`). Write
145
+ it as the last token on the line; on an event, optionally right before a trailing
146
+ `@Context` — `event X @Context public` or `event X public @Context` both work. A `public`
147
+ element is exempt from the unread-event / unconsumed-view warnings (its consumer is outside
148
+ this model). `em export` carries it as `public: true`/`false`; `em diff` reports a flip as
149
+ `event marked public`/`event unmarked public` (the entry cites the element's kind, so a
150
+ view flip reads `view "X"`); `em validate --list-public` audits the whole public surface.
105
151
  - **Fields:** `command Place Order { orderId: UUID, items: LineItem[], customerId }` — inline or
106
152
  one-per-line. Types are free text (no semantic checking) UNLESS the type string names a
107
153
  declared `type` (see Named types below), in which case it resolves to a structured
108
154
  reference. Keep these light; full field specs with rules live in the slice doc.
109
155
  - **Comments:** `# ...` anywhere outside quotes (full-line or trailing).
156
+ - **Quoted strings:** everything between a `"` and its matching `"` is literal — `{`, `}`, `#`
157
+ included, never re-interpreted as a field block or comment — so REST path-template
158
+ placeholders are safe as-is: `issue "PUT v3/widgets/{widgetId}/suspend"`. Escape a literal
159
+ `"` or `\` inside a string with `\"`/`\\`; no other backslash sequence is special. An
160
+ unterminated string (missing closing `"`) is a parse error naming the clause it broke in.
110
161
  - **`source "url"` on a slice header** (the only slice-level clause — everything above is
111
162
  per-element) links the slice back to the ticket/conversation it traces to, e.g.
112
163
  `slice "Checkout" source "https://linear.app/team/issue/MIL-60" { ... }`. Exports as
@@ -157,12 +208,12 @@ slice "Open Orders" {
157
208
  ui Order List @Customer
158
209
  }
159
210
 
160
- # 3. Automation: split across TWO slices (plus the read slice that consumes the event)
211
+ # 3. Automation: read model in the slice before, reaction + command + event together
161
212
  slice "Orders To Fulfill" {
162
213
  view Orders To Fulfill from "Order Placed"
163
- processor Fulfillment Service
164
214
  }
165
- slice "Ship Order" { # the triggered command goes in the NEXT slice
215
+ slice "Ship Order" { # reaction, command, and event all share this one slice
216
+ processor Fulfillment Service from "Orders To Fulfill"
166
217
  command Ship Order
167
218
  event Order Shipped @Shipping
168
219
  }
@@ -172,10 +223,8 @@ slice "Open Orders — shipped" {
172
223
  }
173
224
 
174
225
  # 4a. Translation (external trigger, no durable artifact): external call -> translation -> command -> event
175
- slice "Carrier Webhook" {
226
+ slice "Confirm Delivery" {
176
227
  translation Carrier Adapter # inbound from outside the model; no internal `from`
177
- }
178
- slice "Confirm Delivery" { # the triggered command goes in the NEXT slice
179
228
  command Confirm Delivery
180
229
  event Delivery Confirmed @Shipping
181
230
  }
@@ -192,9 +241,9 @@ slice "Receive Carrier Event" { # ingest: persist the raw webhook befo
192
241
  }
193
242
  slice "Inbound Carrier Events" {
194
243
  view Inbound Carrier Events from "Carrier Event Received" # the persisted inbound queue
195
- translation Carrier Adapter
196
244
  }
197
245
  slice "Acknowledge Carrier Event" {
246
+ translation Carrier Queue Adapter from "Inbound Carrier Events" # different producer than 4a's Carrier Adapter, so a distinct name
198
247
  command Acknowledge Carrier Event
199
248
  event Carrier Event Acknowledged @Shipping
200
249
  }
@@ -205,15 +254,15 @@ slice "Inbound Carrier Events — processed" {
205
254
 
206
255
  # 4c. Translation (internal trigger, durable artifact): read model -> translation -> command -> event
207
256
  slice "Accept Quote" {
208
- ui Quote Screen @Customer # every command needs a trigger: a ui, or a reaction before it
257
+ ui Quote Screen @Customer # every command needs a trigger: a ui, or a reaction, in this slice
209
258
  command Accept Quote
210
259
  event Quote Accepted @Quote
211
260
  }
212
261
  slice "Quotes To Sync" {
213
262
  view Accepted Quotes from "Quote Accepted"
214
- translation CRM Sync # reacts to our own state via the read model in this slice
215
263
  }
216
264
  slice "Record Sync" {
265
+ translation CRM Sync from "Accepted Quotes" # reacts to our own state via the read model
217
266
  command Record Crm Sync
218
267
  event Quote Synced @Quote
219
268
  }
@@ -232,8 +281,11 @@ the context/lane whose fact it represents (e.g. `Carrier Event Received @Shippin
232
281
  Anti-Corruption-Layer event in DDD) instead of a technical artifact leaking foreign vocabulary
233
282
  into the model.
234
283
 
235
- A `translation` (like a `processor`) is a **reaction**: it triggers a command and never carries an
236
- `event` in its own slice. Same two-slice split as the Automation pattern above.
284
+ A `translation` (like a `processor`) is a **reaction**: it triggers a command in the same slice —
285
+ same shape as the Automation pattern above, and the same shape `ui` already uses in State Change.
286
+ The reaction never records the event directly (there's no `reaction -> event` edge); the command
287
+ it triggers is what records it, and that event happens to live in the reaction's own slice now
288
+ because the command does.
237
289
 
238
290
  Note the read slice closing each pattern: **every event must be read by some read model**
239
291
  (warning 4 below). A command slice is not finished until the slice that projects its event
@@ -268,8 +320,9 @@ slice "Read Quote — created" {
268
320
  request/response API call — that's Pattern 1 (State Change) with an API persona, exactly like the
269
321
  `Create Quote` slice above.
270
322
  - **Internal-only commands and views (no public route) carry no `ui` at all.** They follow the
271
- ordinary Automation pattern already documented: the reaction sits in the previous slice, and an
272
- internal-only read model is consumed by that reaction, not by a screen or an API query.
323
+ ordinary Automation pattern already documented: the reaction shares the command's slice, and an
324
+ internal-only read model is consumed by that reaction (from the slice before), not by a screen
325
+ or an API query.
273
326
  - **Repeat the read model** in every slice where it's read so the timeline flows left-to-right, and
274
327
  **prefer `view X again`** for every instance after the first (see Clauses) — **each instance
275
328
  carries its own consumer** (the `ui` that reads it, or a reaction), not just the last one. `again`
@@ -291,23 +344,19 @@ slice "Read Quote — created" {
291
344
  Keep the read model adjacent to its event, and keep a sub-flow that detours into another context
292
345
  together rather than parking it at the end of the model.
293
346
 
294
- ### Slice-ordering gotchas (edge inference)
295
-
296
- `em` infers cross-slice arrows positionally, so **slice order matters**:
297
- - A **reaction** (`processor`/`translation` that triggers a command) wires to the command in the
298
- **immediately next** slice. So a reaction slice must be *directly* followed by its command slice —
299
- don't insert a read slice between them.
300
- - A read slice whose consumer is a **reaction** (`view` + `processor`/`translation`, no command in
301
- the same slice) must **not** be immediately followed by a `command` slice, or the reaction will be
302
- mis-wired to that command. Put it after a command+event slice, or before another read/reaction
303
- slice instead. (A `ui`-consumed read slice has no such risk — `ui` never wires as a reaction.)
304
- - A read model fed by an early event (e.g. a queue or to-do view) can't always sit directly after
305
- its source event when a reaction must immediately precede its command slice — placing the read
306
- later, in narrative order with a longer arrow, is the correct trade-off, not something to force-fix.
307
- - A `ui` never triggers a reaction — no pattern has a `ui` wired to `processor`/`automation`/
308
- `saga`/`translation`, only to `command`. A `ui` left in the reaction's own slice (instead of the
309
- read-model or command slice) renders with no outgoing edge, disconnected, and `em validate` now
310
- warns on it.
347
+ ### Reaction wiring — no positional gotcha
348
+
349
+ A reaction's command is inferred from **same-slice presence**, exactly like `ui -> command`: put
350
+ `processor`/`translation` in the same slice as the command it triggers, in either order, and `em`
351
+ wires it correctly regardless of what any other slice contains or where it sits in the file. There's
352
+ no adjacency to get right and no reordering hazard — the read model it watches (if any) is named
353
+ explicitly via `from "View"`, resolved by name to the nearest instance at or before its own slice,
354
+ not by position either.
355
+
356
+ The one thing that still doesn't wire on its own: a `ui` never triggers a reaction — no pattern has
357
+ a `ui` wired to `processor`/`automation`/`saga`/`translation`, only to `command`. A `ui` left in a
358
+ reaction's slice renders with no outgoing edge, disconnected, and `em validate` warns on it — move
359
+ it to the slice that displays the read model, or drop it.
311
360
 
312
361
  ---
313
362
 
@@ -334,22 +383,31 @@ slice "Read Quote — created" {
334
383
  (`type Node { child: Node }`, or the same shape across several types). Break the cycle, or
335
384
  route the self/mutual reference through an array (`children: Node[]`) if the data is
336
385
  genuinely tree-shaped.
386
+ 9. **Lineage-ref errors** (slice-doc frontmatter, fs-aware) — a
387
+ `split-from`/`merged-from`/`superseded-by` ref with malformed `<slice-key>@v<N>` grammar, a
388
+ self-reference or cycle, a `superseded-by` naming a slice absent from the current model, or
389
+ a ref naming a version higher than the target's own `version:`. A `split-from`/`merged-from`
390
+ naming a key absent from the tree is deliberately silent — that's the normal state after a
391
+ real split/merge (see the four `lineage-*` codes in the table below).
337
392
 
338
393
  **Warnings (should fix):**
339
- 1. **Automation/translation shares slice with its command** — both `automation`/`processor` and
340
- `translation` are reactions; put the triggered command in the *next* slice.
341
- 2. **`ui` shares slice with a reaction, no command** — a `ui` only ever wires to a `command`; no
394
+ 1. **Reaction with no command** — a `processor`/`automation`/`saga`/`translation` that triggers
395
+ nothing. It never records an event itself; add the command it issues in this same slice, or an
396
+ explicit `arrow` to one elsewhere.
397
+ 2. **`ui` shares slice with a reaction** — a `ui` only ever wires to a `command` a person issues; no
342
398
  pattern has a `ui` triggering an automation/processor/saga/translation. Left in the reaction's
343
- own slice it renders disconnected, with no edge. Move it to the read-model slice, or to the
344
- slice with the command this eventually triggers.
345
- 3. **Command with no trigger** — nothing issues it. A command needs a `ui` in its slice, or a
346
- reaction (automation/processor/saga/translation) in the slice *before* it. The input-side mirror
347
- of (5): a command nothing points at is a write nobody can start.
399
+ own slice it renders disconnected, with no edge, whether or not that slice also has the
400
+ reaction's own command. Move it to the slice that displays the read model, or drop it.
401
+ 3. **Command with no trigger** — nothing issues it. A command needs a `ui` in its slice, or the
402
+ reaction (automation/processor/saga/translation) that triggers it, *also* in this slice. The
403
+ input-side mirror of (5): a command nothing points at is a write nobody can start.
348
404
  4. **Command without event** — every command should record at least one event.
349
405
  5. **Event nobody reads** — the mirror of (4): recording an event no read model projects is a
350
406
  write with no reader. Follow every write slice with the read slice that consumes its event.
351
- Counts as read when a `view` names it in `from` (any `again` instance will do), when a
352
- fieldless-`from` view sits in its slice, or via an explicit `event -> view` arrow.
407
+ Counts as read when a `view` names it in `from` (any `again` instance will do), when a view
408
+ with no `from` of its own sits in its slice, or via an explicit `event -> view` arrow.
409
+ (Events marked `public` are exempt — their reader is outside the model; same for a
410
+ `public` view and the no-consumer warning.)
353
411
  6. **Read model without source** — add `from "Event"` or place the view in a slice with an event.
354
412
  7. **Duplicate name** — the same name defined N times; references resolve to the first. Rename.
355
413
  (A duplicate `type` name always warns, unlike a duplicate element name — there's no
@@ -364,16 +422,78 @@ slice "Read Quote — created" {
364
422
  command in the same slice. Only checked once BOTH the event and at least one same-slice
365
423
  command declare `{ fields }`. This is the payoff of the fields feature for slicing rigor:
366
424
  once fields are written down, `em validate` checks that data flows forward consistently.
425
+ 11. **Event with no producing command** — a fact with no traceable cause; add the command that
426
+ records it (either order within the slice), or an explicit `arrow` from one.
427
+ 12. **Two same-named `translation`s reading different producers** — rename one; fires
428
+ unconditionally (unlike the duplicate-name warning), since the confusion is in reading
429
+ the model, not resolving a reference.
430
+ 13. **Frontmatter coherence** (slice docs, fs-aware) — `status: implemented` with no
431
+ `implementedIn` link at all. The re-ratification mismatch (status flipped back while
432
+ `implementedIn` names the prior PR) is deliberately never flagged — it's the drift
433
+ signal, not incoherence.
434
+ 14. **Doc-join problems** (`em export`/`--slice-ready`) — `binding-missing-file` (a
435
+ `note "slices/<key>.md"` binding pointing at a file that doesn't exist) and
436
+ `frontmatter-invalid` (the doc exists but lacks a frontmatter block or a required key).
367
437
 
368
438
  `divergence "text"` is deliberately NOT in this list — it raises no warning at all, since it
369
439
  records a deviation already reasoned through and accepted, not something to fix. Use
370
440
  `em validate --list-divergences` to audit them on demand.
371
441
 
442
+ ### Full rule code reference
443
+
444
+ Every rule above (and every fs-aware check `em validate` layers on top — lineage, frontmatter
445
+ coherence) has a stable `code`, generated below from the same registry `em` itself validates
446
+ against (`src/model/rules.ts`) — a new rule shows up here the moment it's registered, whether or
447
+ not the prose above has caught up yet. `--slice-ready <key>`-only codes are excluded; see
448
+ `docs/cli.md#em-validate-file` in the
449
+ [em repository](https://github.com/milehimikey/em/blob/main/docs/cli.md#em-validate-file).
450
+
451
+ <!-- GENERATED:validate-rules:start -- run `npm run docs:generate` to refresh, do not hand-edit -->
452
+ | Code | Severity | Title | Fix |
453
+ |---|---|---|---|
454
+ | `arrow-backward` | error | Backward arrow | Restructure so the target comes later. |
455
+ | `arrow-unresolved-source` | error | Arrow source unresolved | Fix the arrow's source name. |
456
+ | `arrow-unresolved-target` | error | Arrow target unresolved | Fix the arrow's target name. |
457
+ | `binding-missing-file` | warning | Doc binding points at a missing file | Create the slice doc, or fix the `note` path. |
458
+ | `both-ends-of-a-flow/command-no-event` | warning | Command without event | Add the event this command records. |
459
+ | `both-ends-of-a-flow/command-untriggered` | warning | Command with no trigger | Add a `ui` or the reaction that issues it, both in this slice. |
460
+ | `both-ends-of-a-flow/event-unproduced` | warning | Event with no producing command | Add the command that records it, or an explicit arrow from one. |
461
+ | `both-ends-of-a-flow/event-unread` | warning | Event nobody reads | Project it into a view, or reconsider recording it. |
462
+ | `both-ends-of-a-flow/reaction-no-command` | warning | Reaction with no command | Add the command it triggers, in this slice, or an explicit arrow to one. |
463
+ | `both-ends-of-a-flow/ui-unbacked` | warning | `ui` with no read model or command | Add a `view` it displays, or the command it triggers. |
464
+ | `both-ends-of-a-flow/view-unconsumed` | warning | Read model with no consumer | Add a `ui` or reaction that consumes it, or drop this instance. |
465
+ | `connection-legality/illegal-pair` | error | Illegal connection | Only ui→command→event→view→ui and view→reaction→command are legal — the message names the missing step. |
466
+ | `duplicate-element-ref` | warning | Duplicate element ref | Rename the element so its export ref is unique. |
467
+ | `duplicate-name` | warning | Duplicate name | Rename one of the duplicates. |
468
+ | `duplicate-slice-name` | warning | Duplicate slice name | Rename the slice so its export key is unique. |
469
+ | `duplicate-type-name` | warning | Duplicate type name | Rename one of the duplicate `type` declarations. |
470
+ | `duplicate-type-ref` | warning | Duplicate type ref | Rename the type so its export ref is unique. |
471
+ | `fields-completeness/event-field-no-source` | warning | Event field not from a command | Add the field to a command in the slice, or remove it from the event. |
472
+ | `fields-completeness/view-field-no-source` | warning | View field with no source | Add the field to a source event, or remove it from the view. |
473
+ | `frontmatter-coherence-implemented-without-link` | warning | Implemented without a link | Add `implementedIn` once the slice ships. |
474
+ | `frontmatter-invalid` | warning | Invalid or missing frontmatter | Add the required frontmatter keys, or add a frontmatter block. |
475
+ | `grid-collision` | error | Band collision | Split the colliding elements into separate slices. |
476
+ | `lineage-forward-dangling` | error | Dangling forward lineage ref | Fix the key, or remove the stale successor. |
477
+ | `lineage-ref-cycle` | error | Lineage cycle | Break the cycle — a slice can't be its own ancestor. |
478
+ | `lineage-ref-malformed` | error | Malformed lineage ref | Fix the value to `<slice-key>@v<N>`, or remove it. |
479
+ | `lineage-version-impossible` | error | Impossible lineage version | Fix the referenced version, or ratify the target slice first. |
480
+ | `open-issue` | warning | Open issue | Resolve the question, then remove the `issue` clause. |
481
+ | `reaction-from-future-view` | error | Backward timeline (reaction reads a future view) | Declare the view in or before the reaction's slice. |
482
+ | `reaction-from-unresolved` | error | Unknown read-model source | Project the event into a view first, or fix the `from` reference. |
483
+ | `translation-name-collision` | warning | Translation name reused for different producers | Use a distinct name per producer to avoid confusion. |
484
+ | `type-cycle` | error | Cyclic type reference | Break the cycle, or route the self/mutual reference through an array. |
485
+ | `ui-shares-slice-with-automation` | warning | `ui` shares slice with a reaction | Move the `ui` to the slice that displays the read model, or drop it. |
486
+ | `view-again-without-earlier` | error | `again` without an earlier declaration | Declare the view plainly the first time it appears. |
487
+ | `view-from-future-event` | error | Backward timeline (view reads a future event) | Move the source to a later `view X again` instance. |
488
+ | `view-from-unresolved` | error | Unknown event source | Fix the `from` reference to name an existing event. |
489
+ | `view-no-source` | warning | Read model without source | Add `from "Event"`, or place the view in a slice with an event. |
490
+ <!-- GENERATED:validate-rules:end -->
491
+
372
492
  **Design rules that keep models valid:**
373
493
  - One element per band per slice (multiple personas/contexts are fine — they're different rows).
374
494
  - Every `command` slice includes its `event`. Every `view` has a `from` source.
375
- - **Every `command` has a trigger.** A `ui` in its slice, or a reaction in the slice *before* it.
376
- A command nothing points at is a write nobody can start.
495
+ - **Every `command` has a trigger.** A `ui` in its slice, or the reaction that triggers it, also in
496
+ its slice. A command nothing points at is a write nobody can start.
377
497
  - **Every `event` has a reader.** A command slice isn't finished until the read slice that projects
378
498
  its event exists. Reactions don't count — they read views, not events. Pair each write slice with
379
499
  its read slice as you go rather than sweeping up dangling events at the end.
@@ -386,16 +506,17 @@ records a deviation already reasoned through and accepted, not something to fix.
386
506
  `command → view` (the CQRS violation: an event has to sit between them) and `view → command`
387
507
  (a reaction has to sit between them). If you reach for an arrow the patterns don't allow, the
388
508
  model is missing an element, not an arrow.
389
- - Automations **and translations** are always two slices: the reaction (plus its read model, if
390
- internally triggered) in one slice, the triggered `command` + its `event` in the next. A
391
- translation/automation slice **never contains an `event`** — reactions trigger commands, not
392
- events. Externally-triggered reactions start from outside the model (no `from`);
393
- internally-triggered ones read a **view** (read model) — `from "X"` must resolve to a view.
509
+ - A reaction (`processor`/`automation`/`saga`/`translation`) shares its slice with the `command` it
510
+ triggers and that command's `event` — the same shape `ui` already uses in State Change. If it's
511
+ internally triggered, the **view** (read model) it watches lives in the slice before, named via
512
+ `from "X"` (which must resolve to a view, never an event directly). Externally-triggered reactions
513
+ start from outside the model — no `from` at all, and no slice before them.
394
514
  - Name everything uniquely and consistently.
395
515
  - Events are past tense; commands are imperative; views name the thing shown.
396
516
 
397
- > ⚠️ **Validator blind spot:** `em validate` does **not** flag a translation/automation that emits
398
- > an event directly (e.g. `translation T` and `event E` in the same slice with no command). It only
399
- > warns when a reaction shares a slice with a *command*. So enforce the two-slice
400
- > `reaction → command → event` split **by construction** — never rely on `em validate` to catch a
401
- > reaction wired straight to an event.
517
+ A reaction that emits an event with no command anywhere between them — `translation T` and
518
+ `event E` in the same slice, nothing issuing it — is exactly what
519
+ `both-ends-of-a-flow/reaction-no-command` (above) catches: any reaction with no `command` in its
520
+ own slice, and no explicit arrow to one, warns. Still route every reaction through a real command
521
+ by construction (`reaction → command → event`) rather than an explicit `arrow "Reaction" -> "Event"`
522
+ — that shape is its own `connection-legality` error (a reaction never records an event itself).
@@ -3,8 +3,8 @@
3
3
  Read this **before doing any extract work**. It governs the `extract` phase and **overrides
4
4
  `methodology.md` step 1's stance** for the duration of extraction: you are capturing how the
5
5
  system behaves **today**, not modeling what the business needs. Everything else in the
6
- methodology — the 4 patterns, the "is it an event?" test, the Socratic stance, the two-slice
7
- reaction split — still applies.
6
+ methodology — the 4 patterns, the "is it an event?" test, the Socratic stance, the
7
+ reaction-shares-a-slice-with-its-command shape — still applies.
8
8
 
9
9
  ## Stance override — current-state-only
10
10
 
@@ -81,7 +81,7 @@ it is fine to finish a round with `# TBD`s still parked.
81
81
  `em render` here.**
82
82
  - **R3 — Commands / inputs.** For each event, the request that actually produces it today
83
83
  (imperative name), *and what triggers it* — the real screen, or the scheduler/listener that
84
- fires it (model that as a reaction in the preceding slice, per R5). Unclear trigger → `# TBD`,
84
+ fires it (model that as a reaction sharing this same slice, per R5). Unclear trigger → `# TBD`,
85
85
  not a plausible guess; expect a *command has nothing that triggers it* warning until R5 fills
86
86
  it in, and don't invent a screen to silence it.
87
87
  - **R4 — Read models / outputs.** The projections/queries the system actually serves; wire
@@ -91,16 +91,18 @@ it is fine to finish a round with `# TBD`s still parked.
91
91
  a **finding about the system**, not a modelling mistake — never invent a reader to silence it
92
92
  (that would be desired state). Park it in the Decisions log and carry the warning.
93
93
  - **R5 — Boundaries & reactions.** Current automations and integrations as-is: schedulers,
94
- listeners, outbound syncs, inbound webhooks. Model each as a reaction with the two-slice
95
- `reaction → command → event` split and the slice-ordering rules in `em-dsl.md` — even when
96
- the current code wires it as one procedural step (note the as-built shape in the slice's
97
- `# TBD`/Decisions if it differs).
94
+ listeners, outbound syncs, inbound webhooks. Model each as a reaction sharing its slice with
95
+ the `command` + `event` it triggers (`reaction → command → event`, per `em-dsl.md`) — the
96
+ read model it watches, if any, lives in the slice before, named via the reaction's own
97
+ `from`. This matches how the current code usually already reads (scheduler/listener → one
98
+ procedural step); note any real as-built difference in the slice's `# TBD`/Decisions.
98
99
  - **R6 — Gap & TBD reconciliation.** Walk every parked `# TBD` with the user. Resolve only
99
100
  what they can confirm **as a current-state fact**; everything else stays parked. Explicitly
100
101
  refuse to resolve a TBD by inventing desired behavior.
101
- - **R7 — Convergence.** Final `em render` + `em validate` (fix errors and warnings; remember
102
- the validator's blind spot — check the reaction split by hand). User confirms the model is a
103
- faithful picture of today. Mark extraction complete in the state file and hand off.
102
+ - **R7 — Convergence.** Final `em render` + `em validate` (fix errors and warnings — this now
103
+ catches a reaction wired straight to an event too, via *reaction triggers no command*). User
104
+ confirms the model is a faithful picture of today. Mark extraction complete in the state file
105
+ and hand off.
104
106
 
105
107
  Seven rounds is the default shape, not a quota — collapse rounds for a small system, repeat
106
108
  one for a sprawling one. Record any deviation in the Decisions log.
@@ -124,7 +126,8 @@ accepted case, and each one is worth naming explicitly as a finding); every unkn
124
126
  `# TBD`; and the user has confirmed the model reflects today's behavior. Then:
125
127
 
126
128
  1. State file: set phase progress (extraction rounds checked), log the completion decision.
127
- Seed `README.md`'s **slice index** (every slice, pattern, status `none`).
129
+ Seed `README.md`'s **slice index** (every slice, pattern, status "no doc yet" — no slice
130
+ docs exist until the `slice` phase writes them).
128
131
  2. The extracted model **replaces `discover`** — suggest `/event-modeling model` next
129
132
  (steps 5-7: swimlanes, patterns, completeness). Desired-state changes belong *after* the
130
133
  as-is model is agreed — as normal `model`/`slice` evolution, clearly separated from