@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.
- package/.claude/skills/event-modeling/SKILL.md +94 -49
- package/.claude/skills/event-modeling/reference/conform.md +30 -8
- package/.claude/skills/event-modeling/reference/em-dsl.md +194 -73
- package/.claude/skills/event-modeling/reference/extract.md +14 -11
- package/.claude/skills/event-modeling/reference/implement.md +172 -0
- package/.claude/skills/event-modeling/reference/methodology.md +40 -33
- package/.claude/skills/event-modeling/templates/conformance-report.md +15 -9
- package/.claude/skills/event-modeling/templates/model-readme.md +7 -7
- package/.claude/skills/event-modeling/templates/slice.md +73 -7
- package/.claude/skills/event-modeling/templates/state.md +7 -6
- package/README.md +1 -0
- package/dist/catalog/build.d.ts +5 -0
- package/dist/catalog/build.js +1 -3
- package/dist/catalog/build.js.map +1 -1
- package/dist/catalog/classify.d.ts +7 -8
- package/dist/catalog/classify.js +7 -8
- package/dist/catalog/classify.js.map +1 -1
- package/dist/catalog/docJoin.d.ts +39 -0
- package/dist/catalog/docJoin.js +86 -0
- package/dist/catalog/docJoin.js.map +1 -0
- package/dist/catalog/driftSignal.d.ts +16 -0
- package/dist/catalog/driftSignal.js +24 -0
- package/dist/catalog/driftSignal.js.map +1 -0
- package/dist/catalog/frontmatterCoherenceValidate.d.ts +8 -0
- package/dist/catalog/frontmatterCoherenceValidate.js +52 -0
- package/dist/catalog/frontmatterCoherenceValidate.js.map +1 -0
- package/dist/catalog/lineageValidate.d.ts +9 -0
- package/dist/catalog/lineageValidate.js +161 -0
- package/dist/catalog/lineageValidate.js.map +1 -0
- package/dist/catalog/readSliceDoc.d.ts +2 -0
- package/dist/catalog/readSliceDoc.js +21 -0
- package/dist/catalog/readSliceDoc.js.map +1 -0
- package/dist/catalog/sliceDoc.d.ts +94 -3
- package/dist/catalog/sliceDoc.js +157 -7
- package/dist/catalog/sliceDoc.js.map +1 -1
- package/dist/catalog/sliceReadyValidate.d.ts +11 -0
- package/dist/catalog/sliceReadyValidate.js +85 -0
- package/dist/catalog/sliceReadyValidate.js.map +1 -0
- package/dist/cli/diff-inputs.d.ts +20 -0
- package/dist/cli/diff-inputs.js +75 -18
- package/dist/cli/diff-inputs.js.map +1 -1
- package/dist/cli/ledgerCheck.d.ts +38 -0
- package/dist/cli/ledgerCheck.js +116 -0
- package/dist/cli/ledgerCheck.js.map +1 -0
- package/dist/cli/skillCheck.d.ts +18 -0
- package/dist/cli/skillCheck.js +63 -0
- package/dist/cli/skillCheck.js.map +1 -0
- package/dist/cli/skillSync.d.ts +22 -0
- package/dist/cli/skillSync.js +69 -0
- package/dist/cli/skillSync.js.map +1 -0
- package/dist/cli/skillVersionCheck.d.ts +32 -0
- package/dist/cli/skillVersionCheck.js +145 -0
- package/dist/cli/skillVersionCheck.js.map +1 -0
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +203 -32
- package/dist/cli.js.map +1 -1
- package/dist/emit/diffJson.d.ts +1 -1
- package/dist/emit/diffJson.js +18 -14
- package/dist/emit/diffJson.js.map +1 -1
- package/dist/emit/dot.js +46 -5
- package/dist/emit/dot.js.map +1 -1
- package/dist/emit/json.d.ts +10 -25
- package/dist/emit/json.js +70 -103
- package/dist/emit/json.js.map +1 -1
- package/dist/emit/ledgerJson.d.ts +5 -0
- package/dist/emit/ledgerJson.js +25 -0
- package/dist/emit/ledgerJson.js.map +1 -0
- package/dist/emit/skillCheckJson.d.ts +5 -0
- package/dist/emit/skillCheckJson.js +32 -0
- package/dist/emit/skillCheckJson.js.map +1 -0
- package/dist/model/diff.d.ts +44 -1
- package/dist/model/diff.js +51 -9
- package/dist/model/diff.js.map +1 -1
- package/dist/model/edges.js +17 -17
- package/dist/model/edges.js.map +1 -1
- package/dist/model/model.d.ts +1 -1
- package/dist/model/refs.d.ts +23 -0
- package/dist/model/refs.js +75 -0
- package/dist/model/refs.js.map +1 -0
- package/dist/model/rules.d.ts +253 -0
- package/dist/model/rules.js +254 -0
- package/dist/model/rules.js.map +1 -0
- package/dist/model/validate.d.ts +23 -1
- package/dist/model/validate.js +207 -69
- package/dist/model/validate.js.map +1 -1
- package/dist/parser/ast.d.ts +3 -2
- package/dist/parser/lexer.d.ts +17 -2
- package/dist/parser/lexer.js +119 -13
- package/dist/parser/lexer.js.map +1 -1
- package/dist/parser/parser.js +75 -34
- package/dist/parser/parser.js.map +1 -1
- package/dist/pipeline.d.ts +9 -0
- package/dist/pipeline.js +4 -2
- package/dist/pipeline.js.map +1 -1
- package/dist/render/sliceDiagram.js +33 -35
- package/dist/render/sliceDiagram.js.map +1 -1
- package/dist/render/sliceStatus.js +1 -1
- package/dist/render/sliceStatus.js.map +1 -1
- package/dist/util/isMainModule.d.ts +14 -0
- package/dist/util/isMainModule.js +33 -0
- package/dist/util/isMainModule.js.map +1 -0
- package/dist/util/walkDir.d.ts +4 -0
- package/dist/util/walkDir.js +28 -0
- package/dist/util/walkDir.js.map +1 -0
- 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
|
|
15
|
-
em
|
|
16
|
-
em
|
|
17
|
-
em render <file>
|
|
18
|
-
em render <file> --
|
|
19
|
-
em
|
|
20
|
-
em
|
|
21
|
-
em
|
|
22
|
-
em
|
|
23
|
-
em
|
|
24
|
-
em
|
|
25
|
-
em
|
|
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
|
|
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
|
|
99
|
-
(e.g.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
`
|
|
104
|
-
|
|
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:
|
|
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" { #
|
|
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 "
|
|
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
|
|
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
|
|
236
|
-
|
|
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
|
|
272
|
-
internal-only read model is consumed by that reaction, not by a screen
|
|
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
|
-
###
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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. **
|
|
340
|
-
|
|
341
|
-
|
|
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
|
|
344
|
-
slice
|
|
345
|
-
3. **Command with no trigger** — nothing issues it. A command needs a `ui` in its slice, or
|
|
346
|
-
reaction (automation/processor/saga/translation)
|
|
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
|
-
|
|
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
|
|
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
|
-
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
|
7
|
-
reaction
|
|
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
|
|
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
|
|
95
|
-
`
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
102
|
-
|
|
103
|
-
faithful picture of today. Mark extraction complete in the state file
|
|
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
|
|
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
|