@milehimikey/em 1.13.1 → 1.14.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/agents/em-critic.md +26 -0
- package/.claude/agents/em-implementer.md +34 -0
- package/.claude/agents/em-reviewer.md +31 -0
- package/.claude/agents/em-validator.md +27 -0
- package/.claude/skills/event-modeling/SKILL.md +50 -11
- package/.claude/skills/event-modeling-conform/SKILL.md +1 -1
- package/.claude/skills/event-modeling-design/SKILL.md +126 -18
- package/.claude/skills/event-modeling-discover/SKILL.md +1 -1
- package/.claude/skills/event-modeling-engagement/SKILL.md +192 -0
- package/.claude/skills/event-modeling-engagement/reference/engagement.md +143 -0
- package/.claude/skills/event-modeling-implement/SKILL.md +1 -1
- package/.claude/skills/event-modeling-implement/reference/implement.md +38 -11
- package/.claude/skills/event-modeling-review/SKILL.md +1 -1
- package/.claude/skills/event-modeling-shared/reference/em-dsl.md +274 -189
- package/.claude/skills/event-modeling-shared/reference/operating-principles.md +61 -0
- package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +108 -5
- package/.claude/skills/event-modeling-shared/templates/constitution.md +18 -1
- package/.claude/skills/event-modeling-shared/templates/slice.md +64 -39
- package/.claude/skills/event-modeling-shared/templates/state.md +5 -1
- package/.claude-plugin/marketplace.json +14 -0
- package/README.md +25 -11
- package/dist/catalog/apiFirst.d.ts +28 -0
- package/dist/catalog/apiFirst.js +49 -0
- package/dist/catalog/apiFirst.js.map +1 -0
- package/dist/catalog/docJoin.d.ts +13 -0
- package/dist/catalog/docJoin.js +11 -0
- package/dist/catalog/docJoin.js.map +1 -1
- package/dist/catalog/docModelConsistencyValidate.js +44 -4
- package/dist/catalog/docModelConsistencyValidate.js.map +1 -1
- package/dist/catalog/invariantsValidate.d.ts +4 -0
- package/dist/catalog/invariantsValidate.js +102 -0
- package/dist/catalog/invariantsValidate.js.map +1 -0
- package/dist/catalog/scenarios.d.ts +15 -0
- package/dist/catalog/scenarios.js +97 -0
- package/dist/catalog/scenarios.js.map +1 -0
- package/dist/catalog/sliceDoc.d.ts +8 -0
- package/dist/catalog/sliceDoc.js +9 -0
- package/dist/catalog/sliceDoc.js.map +1 -1
- package/dist/catalog/sliceReadyValidate.d.ts +31 -4
- package/dist/catalog/sliceReadyValidate.js +66 -8
- package/dist/catalog/sliceReadyValidate.js.map +1 -1
- package/dist/catalog/sliceSections.d.ts +55 -0
- package/dist/catalog/sliceSections.js +317 -0
- package/dist/catalog/sliceSections.js.map +1 -0
- package/dist/cli/agentsMd.js +7 -1
- package/dist/cli/agentsMd.js.map +1 -1
- package/dist/cli/api.d.ts +90 -0
- package/dist/cli/api.js +318 -0
- package/dist/cli/api.js.map +1 -0
- package/dist/cli/ciInit.d.ts +82 -21
- package/dist/cli/ciInit.js +468 -64
- package/dist/cli/ciInit.js.map +1 -1
- package/dist/cli/conformScope.d.ts +28 -0
- package/dist/cli/conformScope.js +67 -0
- package/dist/cli/conformScope.js.map +1 -1
- package/dist/cli/coverage.d.ts +9 -0
- package/dist/cli/coverage.js +60 -8
- package/dist/cli/coverage.js.map +1 -1
- package/dist/cli/engagement.d.ts +210 -0
- package/dist/cli/engagement.js +444 -0
- package/dist/cli/engagement.js.map +1 -0
- package/dist/cli/engagementFile.d.ts +86 -0
- package/dist/cli/engagementFile.js +255 -0
- package/dist/cli/engagementFile.js.map +1 -0
- package/dist/cli/metrics.d.ts +10 -0
- package/dist/cli/metrics.js +49 -1
- package/dist/cli/metrics.js.map +1 -1
- package/dist/cli/pluginPin.d.ts +55 -0
- package/dist/cli/pluginPin.js +188 -0
- package/dist/cli/pluginPin.js.map +1 -0
- package/dist/cli/ratify.d.ts +27 -2
- package/dist/cli/ratify.js +112 -13
- package/dist/cli/ratify.js.map +1 -1
- package/dist/cli/reratify.d.ts +3 -2
- package/dist/cli/reratify.js +36 -14
- package/dist/cli/reratify.js.map +1 -1
- package/dist/cli/scopeInputs.d.ts +48 -0
- package/dist/cli/scopeInputs.js +176 -0
- package/dist/cli/scopeInputs.js.map +1 -0
- package/dist/cli/skillCheck.d.ts +15 -1
- package/dist/cli/skillCheck.js +22 -1
- package/dist/cli/skillCheck.js.map +1 -1
- package/dist/cli/skillDirs.d.ts +5 -0
- package/dist/cli/skillDirs.js +13 -1
- package/dist/cli/skillDirs.js.map +1 -1
- package/dist/cli/skillSync.d.ts +4 -0
- package/dist/cli/skillSync.js +23 -0
- package/dist/cli/skillSync.js.map +1 -1
- package/dist/cli/sliceNew.d.ts +11 -7
- package/dist/cli/sliceNew.js +91 -18
- package/dist/cli/sliceNew.js.map +1 -1
- package/dist/cli/sliceSync.d.ts +50 -0
- package/dist/cli/sliceSync.js +170 -0
- package/dist/cli/sliceSync.js.map +1 -0
- package/dist/cli/status.d.ts +24 -1
- package/dist/cli/status.js +20 -1
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/systemInputs.d.ts +53 -10
- package/dist/cli/systemInputs.js +186 -11
- package/dist/cli/systemInputs.js.map +1 -1
- package/dist/cli/upgrade.d.ts +16 -3
- package/dist/cli/upgrade.js +366 -30
- package/dist/cli/upgrade.js.map +1 -1
- package/dist/cli/usageLog.d.ts +4 -2
- package/dist/cli/usageLog.js +4 -2
- package/dist/cli/usageLog.js.map +1 -1
- package/dist/cli.js +554 -94
- package/dist/cli.js.map +1 -1
- package/dist/emit/api.d.ts +29 -0
- package/dist/emit/api.js +220 -0
- package/dist/emit/api.js.map +1 -0
- package/dist/emit/apiCheckJson.d.ts +3 -0
- package/dist/emit/apiCheckJson.js +21 -0
- package/dist/emit/apiCheckJson.js.map +1 -0
- package/dist/emit/codeownersJson.d.ts +3 -0
- package/dist/emit/codeownersJson.js +19 -0
- package/dist/emit/codeownersJson.js.map +1 -0
- 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/engagementJson.d.ts +8 -0
- package/dist/emit/engagementJson.js +46 -0
- package/dist/emit/engagementJson.js.map +1 -0
- package/dist/emit/glossaryJson.d.ts +1 -1
- package/dist/emit/glossaryJson.js +4 -1
- package/dist/emit/glossaryJson.js.map +1 -1
- package/dist/emit/json.d.ts +17 -1
- package/dist/emit/json.js +30 -1
- package/dist/emit/json.js.map +1 -1
- package/dist/emit/metricsJson.d.ts +1 -1
- package/dist/emit/metricsJson.js +4 -1
- package/dist/emit/metricsJson.js.map +1 -1
- package/dist/emit/queryJson.d.ts +1 -1
- package/dist/emit/queryJson.js +7 -1
- package/dist/emit/queryJson.js.map +1 -1
- package/dist/emit/scopeJson.d.ts +12 -0
- package/dist/emit/scopeJson.js +26 -0
- package/dist/emit/scopeJson.js.map +1 -0
- package/dist/emit/skillCheckJson.d.ts +3 -2
- package/dist/emit/skillCheckJson.js +10 -2
- package/dist/emit/skillCheckJson.js.map +1 -1
- package/dist/emit/statusJson.d.ts +1 -1
- package/dist/emit/statusJson.js +15 -1
- package/dist/emit/statusJson.js.map +1 -1
- package/dist/emit/systemJson.d.ts +15 -8
- package/dist/emit/systemJson.js +28 -13
- package/dist/emit/systemJson.js.map +1 -1
- package/dist/emit/validateJson.d.ts +3 -3
- package/dist/emit/validateJson.js +10 -4
- package/dist/emit/validateJson.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +163 -31
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/diff.d.ts +4 -1
- package/dist/model/diff.js +44 -8
- package/dist/model/diff.js.map +1 -1
- package/dist/model/glossary.d.ts +2 -0
- package/dist/model/glossary.js +1 -0
- package/dist/model/glossary.js.map +1 -1
- package/dist/model/model.d.ts +55 -2
- package/dist/model/model.js +57 -0
- package/dist/model/model.js.map +1 -1
- package/dist/model/qualifiedRef.d.ts +18 -0
- package/dist/model/qualifiedRef.js +26 -0
- package/dist/model/qualifiedRef.js.map +1 -1
- package/dist/model/queryIndex.d.ts +9 -1
- package/dist/model/queryIndex.js +18 -2
- package/dist/model/queryIndex.js.map +1 -1
- package/dist/model/rules.d.ts +143 -2
- package/dist/model/rules.js +162 -3
- package/dist/model/rules.js.map +1 -1
- package/dist/model/validate.d.ts +40 -0
- package/dist/model/validate.js +147 -1
- package/dist/model/validate.js.map +1 -1
- package/dist/parser/ast.d.ts +31 -3
- package/dist/parser/parser.js +148 -14
- package/dist/parser/parser.js.map +1 -1
- package/dist/query/format.js +1 -1
- package/dist/query/format.js.map +1 -1
- package/dist/query/verbs.d.ts +6 -0
- package/dist/query/verbs.js +6 -2
- package/dist/query/verbs.js.map +1 -1
- package/dist/system/adaptation.d.ts +14 -0
- package/dist/system/adaptation.js +49 -0
- package/dist/system/adaptation.js.map +1 -0
- package/dist/system/codeowners.d.ts +77 -0
- package/dist/system/codeowners.js +242 -0
- package/dist/system/codeowners.js.map +1 -0
- package/dist/system/manifest.d.ts +10 -5
- package/dist/system/manifest.js +54 -26
- package/dist/system/manifest.js.map +1 -1
- package/dist/system/migrateManifest.d.ts +28 -0
- package/dist/system/migrateManifest.js +155 -0
- package/dist/system/migrateManifest.js.map +1 -0
- package/dist/system/scope.d.ts +46 -0
- package/dist/system/scope.js +132 -0
- package/dist/system/scope.js.map +1 -0
- package/dist/system/verify.d.ts +50 -13
- package/dist/system/verify.js +179 -58
- package/dist/system/verify.js.map +1 -1
- package/dist/templates.d.ts +2 -2
- package/dist/templates.js +23 -2
- package/dist/templates.js.map +1 -1
- package/package.json +5 -2
- package/plugin/.claude-plugin/plugin.json +18 -0
- package/plugin/agents/em-critic.md +27 -0
- package/plugin/agents/em-implementer.md +35 -0
- package/plugin/agents/em-reviewer.md +32 -0
- package/plugin/agents/em-validator.md +28 -0
- package/plugin/shared/reference/em-dsl.md +866 -0
- package/plugin/shared/reference/methodology.md +329 -0
- package/plugin/shared/reference/operating-principles.md +222 -0
- package/plugin/shared/reference/slice-doc-schema.md +566 -0
- package/plugin/shared/templates/conformance-report.md +85 -0
- package/plugin/shared/templates/constitution.md +136 -0
- package/plugin/shared/templates/model-readme.md +46 -0
- package/plugin/shared/templates/slice.md +251 -0
- package/plugin/shared/templates/state.md +77 -0
- package/plugin/skills/conform/SKILL.md +93 -0
- package/plugin/skills/conform/reference/conform.md +428 -0
- package/plugin/skills/design/SKILL.md +297 -0
- package/plugin/skills/discover/SKILL.md +90 -0
- package/plugin/skills/discover/reference/extract.md +147 -0
- package/plugin/skills/engagement/SKILL.md +193 -0
- package/plugin/skills/engagement/reference/engagement.md +144 -0
- package/plugin/skills/event-modeling/SKILL.md +132 -0
- package/plugin/skills/implement/SKILL.md +61 -0
- package/plugin/skills/implement/reference/implement.md +439 -0
- package/plugin/skills/review/SKILL.md +74 -0
- package/dist/emit/typespec.d.ts +0 -26
- package/dist/emit/typespec.js +0 -356
- package/dist/emit/typespec.js.map +0 -1
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
<!-- GENERATED FILE from .claude/skills/event-modeling-shared/reference/em-dsl.md by scripts/build-plugin.ts — do not hand-edit; run npm run docs:generate -->
|
|
2
|
+
<!-- DSL behavior change? Update BOTH docs/dsl.md and .claude/skills/event-modeling-shared/reference/em-dsl.md -->
|
|
3
|
+
|
|
4
|
+
# `em` DSL Reference & Cheatsheet
|
|
5
|
+
|
|
6
|
+
The `em` tool (`@milehimikey/em`) is a slice-first text DSL rendered to a strict Graphviz
|
|
7
|
+
swimlane grid. Use this reference so the models you generate are **valid** and render cleanly.
|
|
8
|
+
Keep `.em` files focused on **structure**; put deep design in markdown linked via `note`.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## CLI
|
|
13
|
+
|
|
14
|
+
<!-- GENERATED:cli:start -- run `npm run docs:generate` to refresh, do not hand-edit -->
|
|
15
|
+
```bash
|
|
16
|
+
em --version # print the installed em version
|
|
17
|
+
em init [file] # scaffold a starter .em model
|
|
18
|
+
em init [file] -f, --force # overwrite if the file exists
|
|
19
|
+
em scaffold <name> # scaffold a full project: <slug>/<slug>.em, README.md, .event-modeling.md, and constitution.md (the implementation constitution — skipped, with a note, in a spec-kit project, where `.specify/memory/constitution.md` IS that document) (see docs/cli.md — for just a starter .em, use `em init`; for a multi-model project, pass --under to nest it under a shared parent directory)
|
|
20
|
+
em scaffold <name> -f, --force # overwrite the directory's contents if it already exists
|
|
21
|
+
em scaffold <name> --under <dir> # parent directory to scaffold into — writes <dir>/<slug>/ instead of ./<slug>/, the supported multi-model layout (docs/cli.md, "Multi-model projects"): one directory per model, so each model's slices/ never collides with a sibling model's
|
|
22
|
+
em render <file> # transpile a model and render it (or emit DOT)
|
|
23
|
+
em render <file> -o, --out <path> # output path (extension picks the format)
|
|
24
|
+
em render <file> -T, --format <fmt> # output format (svg, png, pdf, ...)
|
|
25
|
+
em render <file> --slice <name> # render only this slice, redrawn in its own canonical pattern shape (default out: slices/<kebab-slug>.svg)
|
|
26
|
+
em render <file> --emit-dot # print the generated DOT instead of rendering
|
|
27
|
+
em render <file> --keep-empty-lanes # keep the API lane even when empty
|
|
28
|
+
em export <file> # export a versioned JSON snapshot of the normalized model
|
|
29
|
+
em export <file> -o, --out <path> # write to a file instead of stdout
|
|
30
|
+
em export <file> --slice <key> # export only this slice's object (pattern/fields/doc) instead of the whole model (export key, MIL-128) — refuses only if THIS slice has an error; an unrelated slice's breakage elsewhere in the model doesn't block it (see docs/cli.md)
|
|
31
|
+
em api generate <file> # write the TypeSpec contract for the model's public commands, events and views to <model dir>/contracts/<model key>.tsp
|
|
32
|
+
em api generate <file> -o, --out <path> # write to this path instead of <model dir>/contracts/<model key>.tsp
|
|
33
|
+
em api generate <file> --stdout # print the contract instead of writing it
|
|
34
|
+
em api check <file> # is the committed contract current? With --base, annotate each public-surface change since that revision additive or breaking (annotation only — exits 1 only when stale)
|
|
35
|
+
em api check <file> --base <rev> # git revision to diff the public surface against
|
|
36
|
+
em api check <file> --json # print a JSON document instead of the text report (see docs/cli.md)
|
|
37
|
+
em typespec <file> # DEPRECATED alias of `em api generate --stdout` (use em api generate)
|
|
38
|
+
em typespec <file> -o, --out <path> # write to a file instead of stdout
|
|
39
|
+
em diff <old> [new] # compare two models structurally (two files, or one file across git revisions)
|
|
40
|
+
em diff <old> [new] --from <rev> # diff <old> against this git revision instead of a second file
|
|
41
|
+
em diff <old> [new] --to <rev> # diff against this git revision instead of the current file (requires --from)
|
|
42
|
+
em diff <old> [new] --exit-code # exit 1 if the models differ, 0 if identical (git-diff convention)
|
|
43
|
+
em diff <old> [new] --json # print a JSON document instead of the text report (see docs/cli.md)
|
|
44
|
+
em glossary <files> # cross-model glossary of terms, with consistency checks across models (see docs/cli.md)
|
|
45
|
+
em glossary <files> --json # print the full glossary document instead of the text report
|
|
46
|
+
em glossary <files> -o, --out <path> # write the JSON document to a file instead of stdout (requires --json)
|
|
47
|
+
em glossary <files> --list-conflicts # print only the conflict lines, no summary
|
|
48
|
+
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)
|
|
49
|
+
em catalog <files> # generate a browsable static HTML catalog site over one or more .em models (see docs/cli.md)
|
|
50
|
+
em catalog <files> -o, --out <dir> # output directory
|
|
51
|
+
em catalog <files> -T, --format <fmt> # diagram format embedded in the catalog (svg or png)
|
|
52
|
+
em catalog <files> --title <text> # catalog site title
|
|
53
|
+
em catalog <files> --keep-empty-lanes # keep the API lane even when empty
|
|
54
|
+
em slice new <name> # scaffold a fresh slices/<key>.md doc — the 5 frontmatter keys required at `status: draft`, the `# Slice:` heading and diagram-image stub, then every template section: the generated regions (command/event/view field tables, Invariants) filled from the model with --wire (template placeholders otherwise — `em slice sync` fills them), the authored sections (Intent, Scenarios, Open Questions, ...) with placeholder bullets (see .claude/skills/event-modeling-shared/reference/slice-doc-schema.md, templates/slice.md)
|
|
55
|
+
em slice new <name> --pattern <pattern> # slice pattern: state-change | state-view | automation | translation
|
|
56
|
+
em slice new <name> --swimlane <swimlane> # swimlane, e.g. "Persona → Context"
|
|
57
|
+
em slice new <name> -f, --force # overwrite the file if it already exists
|
|
58
|
+
em slice new <name> --wire <model-file> # also insert the `note "slices/<key>.md"` line onto the slice's primary element in this .em file (matched by export key), instead of just printing it to paste by hand (MIL-161)
|
|
59
|
+
em slice new <name> --stub # write a near-free stub instead: same 5 frontmatter keys, but a one-line placeholder body instead of the diagram-image stub and every judgment section (MIL-184) — deepen it later by re-running without --stub and -f
|
|
60
|
+
em slice sync <model-file> [key] # regenerate the GENERATED regions (command/event/view field tables, the Invariants list) of existing slice docs in place from the model, authored sections untouched (MIL-266); docs without regions are skipped with a note. --check never writes and exits 1 when a doc is stale
|
|
61
|
+
em slice sync <model-file> [key] --check # report ok / stale / no-regions per doc without writing; exit 1 when any is stale
|
|
62
|
+
em slice sync <model-file> [key] --json # print the machine-readable report (sliceSyncSchemaVersion 1.0)
|
|
63
|
+
em slice stub-all <file> # scaffold + wire a near-free stub (`em slice new --stub`'s content, MIL-184) for every slice in <file> with no resolvable doc — the fast path to status coloring for an exploratory/backbone model without hand-running `slice new` per slice. Skips a continuation slice (MIL-208, it has no doc of its own), an already-documented slice, and a slice whose pattern can't be classified
|
|
64
|
+
em slice stub-all <file> --status <status> # target status for every stub: draft | reviewed | ready-to-implement | implemented
|
|
65
|
+
em slice stub-all <file> --by <name> # identity for reviewedBy/ratifiedBy — required unless --status draft
|
|
66
|
+
em slice stub-all <file> --implemented-in <url> # PR/commit URL for implementedIn — required with --status implemented
|
|
67
|
+
em slice stub-all <file> --dry-run # list what would be stubbed/wired without writing anything
|
|
68
|
+
em slice index <file> # rewrite the model's sibling README.md's GENERATED Slices table from `em export`'s slice facts (key, pattern, doc status/implementedIn) — the hand-maintained table is deprecated
|
|
69
|
+
em slice index <file> --check # verify the table is current; exit non-zero on drift without writing (CI)
|
|
70
|
+
em slice mark-implemented <file> <slice-key> <pr-url> # flip a slice doc's frontmatter to `status: implemented` / `implementedIn: <pr-url>` — the one edit an implementing agent makes to a ratified doc at merge (MIL-103, replaces the em-sdd-bridge `em-sdd-mark-implemented` script; see reference/implement.md §6). Idempotent on the same URL; refuses to overwrite a different one; never touches `version:` or the doc body
|
|
71
|
+
em slice review <file> <slice-key> # flip a slice doc's frontmatter to `status: reviewed` and record `reviewedBy:`/`reviewedOn:` — the FIRST of the two human gates (MIL-201, docs/process.md#the-slice-lifecycle-gates): the room walked this slice and every open question it raised is resolved. Ratification is a separate, later gate (`em slice ratify`). Legal from `status: draft`; idempotent on the same --by/--on pair; refuses to overwrite a different one already recorded; refuses a doc already `ready-to-implement`/`implemented`; never touches `version:` or the doc body
|
|
72
|
+
em slice review <file> <slice-key> --by <name> # the reviewer's (or facilitator's) name
|
|
73
|
+
em slice review <file> <slice-key> --on <date> # review date, YYYY-MM-DD (default: today, local date)
|
|
74
|
+
em slice ratify <file> <slice-key> # flip a slice doc's frontmatter to `status: ready-to-implement` and record `ratifiedBy:`/`ratifiedOn:` — the handoff sign-off (MIL-165, docs/process.md#the-slice-lifecycle-gates) that makes who ratified, and when, a first-class recorded fact. The review gate is a check on the doc's current `status:`, not on whether `reviewedBy:`/`reviewedOn:` are populated: a doc at `reviewed` or `ready-to-implement` passes (the latter is where `em slice reratify` leaves a doc, so the follow-up ratify needs no --skip-review); any other status — a `draft` that never went through `em slice review` — is refused unless --skip-review is passed. Idempotent on the same --by/--on pair; refuses to overwrite a different one already recorded; never touches `version:` or the doc body
|
|
75
|
+
em slice ratify <file> <slice-key> --by <name> # the ratifier's name
|
|
76
|
+
em slice ratify <file> <slice-key> --on <date> # ratification date, YYYY-MM-DD (default: today, local date)
|
|
77
|
+
em slice ratify <file> <slice-key> --skip-review # ratify without a recorded review — prints a loud notice on stderr
|
|
78
|
+
em slice ratify <file> <slice-key> --meaning-unchanged # API-first sign-off (MIL-238): this version does not change what the public contract means — records `meaningConfirmed: true`; required (or --contract-change) on a slice touching a `public` element
|
|
79
|
+
em slice ratify <file> <slice-key> --contract-change <why> # API-first sign-off (MIL-238): a consumer must read this change differently, and why — records `contractChange: "<why>"`
|
|
80
|
+
em slice reratify <file> <slice-key> # bump `version:` and flip a shipped slice doc's frontmatter back to `status: ready-to-implement` — the re-ratification mechanical edit (MIL-161, mirrors `em slice mark-implemented`). Applies to a doc at `status: implemented`, or (MIL-258) to a `ready-to-implement` doc that is ratified but not yet shipped — there it bumps `version:` and clears the sign-off but leaves `status:` alone; a `ready-to-implement` doc with no `ratifiedBy:` refuses (awaiting `em slice ratify --by`). Clears any stale `ratifiedBy:`/`ratifiedOn:`/`reviewedBy:`/`reviewedOn:` (they describe the PRIOR version's review and sign-off) so a follow-up `em slice ratify --by` applies cleanly — and needs no fresh review; never touches `implementedIn:` or the doc body
|
|
81
|
+
em slice reratify <file> <slice-key> --meaning-unchanged # API-first sign-off (MIL-238): the new version does not change what the public contract means — records `meaningConfirmed: true`; required (or --contract-change) on a slice touching a `public` element
|
|
82
|
+
em slice reratify <file> <slice-key> --contract-change <why> # API-first sign-off (MIL-238): a consumer must read the new version differently, and why — records `contractChange: "<why>"`
|
|
83
|
+
em slice conform <file> <slice-key> # record per-slice-per-version conformance certification: sets `conformedVersion:`/`conformedAt:`/`conformedOn:` on a slice doc (MIL-214) — the fact `driftSignal: in-sync` now depends on. Legal only for `status: implemented` with an `implementedIn:` link. Idempotent on the same (version, --at) pair; a different --at simply overwrites (a later re-certification is normal — there's no way for em to tell 'later' from 'earlier' for an arbitrary revision string, so there's no --force to reach for). Refuses when the newest conformance/*-findings.json matching --at still has an unruled finding in scope for this slice; --skip-findings-check overrides with a loud notice.
|
|
84
|
+
em slice conform <file> <slice-key> --at <rev> # the target-repo revision this certification sweep diffed against
|
|
85
|
+
em slice conform <file> <slice-key> --on <date> # certification date, YYYY-MM-DD (default: today, local date)
|
|
86
|
+
em slice conform <file> <slice-key> --skip-findings-check # certify even with unruled findings in scope — prints a loud notice on stderr
|
|
87
|
+
em changelog <file> # render a model's git history as a business-readable ledger (see docs/cli.md)
|
|
88
|
+
em changelog <file> --from <rev> # start the walk at this revision (inclusive)
|
|
89
|
+
em changelog <file> --to <rev> # end the walk at this revision (inclusive; default HEAD)
|
|
90
|
+
em changelog <file> -o, --out <path> # write to a file instead of stdout
|
|
91
|
+
em state read [dir] # print the state file's mechanical fields as JSON
|
|
92
|
+
em state set-phase <phase> [dir] # rewrite Current phase: (and Last updated:); --step also rewrites Current step:
|
|
93
|
+
em state set-phase <phase> [dir] --step <n> # also set Current step: to this value
|
|
94
|
+
em state set-conformance <revision> [dir] # rewrite Last conformance: (and Last updated:) in the exact format reference/conform.md parses. Refuses (MIL-214) while any `implemented` slice has an unruled conformance finding in the findings JSON beside --report (a `slice: null` finding blocks every slice); --partial records the marker with a ` (partial)` suffix instead, with a loud notice. Without a findings JSON beside --report: warns once and records the marker as given (migration path for a report predating `em conform-findings`)
|
|
95
|
+
em state set-conformance <revision> [dir] --report <path> # path to the conformance report just written
|
|
96
|
+
em state set-conformance <revision> [dir] --partial # record the marker as PARTIAL even though some in-scope findings are still unruled
|
|
97
|
+
em state set-review <date> [dir] # rewrite Last stakeholder review: (and Last updated:)
|
|
98
|
+
em state log-usage <file> # append one Usage log line — phase(s) touched + em validate's diagnostic categories hit, deduped and canonically formatted (MIL-161, docs/usage-data.md) — the mechanical half of 'save state at the end of every session' that used to be run-validate-then-hand-format; state file resolved next to <file>, same convention as em conform-scope
|
|
99
|
+
em state log-usage <file> --phases <list> # comma-separated phase(s) touched this session: discover, extract, model, slice, implement, conform, review, validate, watch, engagement
|
|
100
|
+
em model version bump <file> # bump the model's design version: write model-versions/v<N+1>.json (sibling of slices/, conformance/) and rewrite the state file's `Model version:` bullet (MIL-218). Refuses when the state file is missing (run `em scaffold` first), when --by is empty, and when nothing has changed since the current version (same model content hash AND same slice-version vector) unless --force — a no-op bump is the one case --force is right for.
|
|
101
|
+
em model version bump <file> --by <name> # the bumper's name
|
|
102
|
+
em model version bump <file> --on <date> # bump date, YYYY-MM-DD (default: today, local date)
|
|
103
|
+
em model version bump <file> --force # bump even though nothing has changed since the current version
|
|
104
|
+
em model version show <file> # print the model's current design version and the most recently certified version, if any (model-versions/*.json — MIL-218)
|
|
105
|
+
em model version show <file> --json # print a JSON document instead of text
|
|
106
|
+
em usage-report [root] # aggregate every .event-modeling.md's Usage log under [root] into phase/diagnostic-category tallies (MIL-161) — replaces docs/usage-data.md's hand-rolled grep/awk/sort pipeline; a logged line that doesn't match the canonical em state log-usage format is reported under unparseableLines rather than silently mistallied or dropped
|
|
107
|
+
em usage-report [root] --json # print a JSON document instead of the text report
|
|
108
|
+
em conform-scope <file> # mechanize conform phase step 1 (reference/conform.md): map the target repo's changed paths since Last conformance: to slices via each slice doc's implementedIn, JSON to stdout — --seed-asis also seeds the <model>-asis.em scratch model (see docs/cli.md)
|
|
109
|
+
em conform-scope <file> --repo <path> # path to (or inside) the target codebase's git repository
|
|
110
|
+
em conform-scope <file> --full # ignore Last conformance:/changed paths; scope every implemented slice
|
|
111
|
+
em conform-scope <file> --seed-asis # write <model>-asis.em as a byte copy of the canonical model and ensure it's gitignored
|
|
112
|
+
em conform-supersede <file> <report-path> # stamp a conformance report with a "superseded as of <rev>" banner once its findings have been ruled on (MIL-164, docs/process.md#what-ratified-means) — the companion step to run at ratification time so a reader following the report's file:line citations knows they describe an ancestor of the current model. Additive-only splice, never a rewrite of the report; idempotent on the same --as-of/--findings/--on stamp; refuses if the report doesn't exist. --locus/--by (MIL-214, both required together) additionally record the ruling itself — locus/resolvedBy/resolvedOn — on the findings named by --findings in the sibling conformance/<date>-findings.json, when one exists (warns once and stamps the banner only when it doesn't — the migration path for a report predating em conform-findings)
|
|
113
|
+
em conform-supersede <file> <report-path> --as-of <rev> # the revision this ruling was made against — same value passed to `em state set-conformance`
|
|
114
|
+
em conform-supersede <file> <report-path> --findings <spec> # which finding number(s) this stamps as ruled, e.g. "1-3" or "1,2,4"
|
|
115
|
+
em conform-supersede <file> <report-path> --on <date> # ruling date, YYYY-MM-DD (default: today, local date)
|
|
116
|
+
em conform-supersede <file> <report-path> --locus <locus> # who/what the named finding(s) say is wrong: model | doc | code | none (requires --by)
|
|
117
|
+
em conform-supersede <file> <report-path> --by <name> # who ruled on the named finding(s) (requires --locus)
|
|
118
|
+
em conform-findings check <path> # shape-validate a conformance/<date>-findings.json file (findingsSchemaVersion, required fields, enum values, ids sorted+unique, a ruled finding carries resolvedBy+resolvedOn) — exit 1 on shape errors. The conform skill writes this file directly alongside its report; this is how a headless run verifies what it just wrote.
|
|
119
|
+
em conform-findings check <path> --json # print a JSON document instead of the text report
|
|
120
|
+
em watch <file> # re-render on every save
|
|
121
|
+
em watch <file> -o, --out <path> # output path (extension picks the format)
|
|
122
|
+
em watch <file> -T, --format <fmt> # output format (svg, png, pdf, ...)
|
|
123
|
+
em watch <file> --keep-empty-lanes # keep the API lane even when empty
|
|
124
|
+
em watch <file> --serve # serve a live viewer with instant push-reload (no polling)
|
|
125
|
+
em watch <file> --port <n> # port for --serve (default 5173)
|
|
126
|
+
em validate <file> # check a model against event-modeling rules
|
|
127
|
+
em validate <file> --list-issues # print only open `issue` diagnostics (slice, element, line, text)
|
|
128
|
+
em validate <file> --list-divergences # print only accepted-divergence annotations (slice, element, line, text) — never fails the build
|
|
129
|
+
em validate <file> --list-public # print only commands, events and views marked `public` (slice, kind, name, line) — an integration-surface audit, never fails the build
|
|
130
|
+
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)
|
|
131
|
+
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, a recorded ratifiedBy, and — for a slice touching a `public` element — a current contracts/<model key>.tsp; exits non-zero if not ready (MIL-87, MIL-259, MIL-238)
|
|
132
|
+
em validate <file> --json # print a JSON document instead of text — works on a model WITH errors, unlike `em export` (MIL-128, see docs/cli.md); exit codes are unchanged
|
|
133
|
+
em migrate <file> # rewrite the old two-slice Automation/Translation shape into the merged single-slice shape MIL-120 made canonical (see docs/cli.md)
|
|
134
|
+
em migrate <file> --write # apply the rewrite to the file (default: dry run — report only, write nothing)
|
|
135
|
+
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)
|
|
136
|
+
em ledger <file> --from <rev> # baseline revision
|
|
137
|
+
em ledger <file> --to <rev> # compare revision (default: current working tree)
|
|
138
|
+
em ledger <file> --waive <slice-key> # excuse a doc-content-without-version-bump finding for this slice key (repeatable; MIL-185, see docs/cli.md) — never waives a version regression or bump-without-content-change
|
|
139
|
+
em ledger <file> --json # print a JSON document instead of the text report (see docs/cli.md)
|
|
140
|
+
em coverage <file> # check that every INV-* invariant ID cited in an implemented slice doc is cited by a test under --tests <dir> (MIL-130/MIL-207) — mechanizes reference/implement.md's definition-of-done citation check; advisory by default, --strict for CI; --include-ready also counts ready-to-implement docs (forward-looking report); a continuation slice (an `again` view instance with no doc of its own, MIL-208) is excluded from the report — its invariants, if any, live in the originating slice's own doc; --slice <key> scopes the report to one slice whatever its status (MIL-255), the pre-merge definition-of-done check
|
|
141
|
+
em coverage <file> --tests <dir> # directory to scan recursively for test files citing invariant IDs
|
|
142
|
+
em coverage <file> --slice <key> # only this slice (export key), whether ready-to-implement or implemented — no --include-ready needed; --strict then fails on this slice's uncited invariants, or if the slice is not in scope (draft/reviewed/unbound: nothing checked) (MIL-255)
|
|
143
|
+
em coverage <file> --strict # exit non-zero if any invariant ID has zero citations (CI)
|
|
144
|
+
em coverage <file> --include-ready # also count ready-to-implement docs, not just implemented (MIL-207)
|
|
145
|
+
em coverage <file> --json # print a JSON document instead of the text report (see docs/cli.md)
|
|
146
|
+
em status <files> # deterministic state-of-the-system rollup over one or more .em models: slices by lifecycle status, driftSignal breakdown, invariant coverage totals (with --tests), open issue markers + unchecked Open Questions, and last-conformance commits-behind-HEAD (MIL-163, see docs/cli.md)
|
|
147
|
+
em status <files> --tests <dir> # directory to scan for INV-* test citations — enables invariant coverage totals
|
|
148
|
+
em status <files> --repo <path> # git repo to compute commits-behind-HEAD in (default: each model's own directory)
|
|
149
|
+
em status <files> --json # print a JSON document instead of the text report (see docs/cli.md)
|
|
150
|
+
em status <files> --md # print a markdown block suited for README embedding
|
|
151
|
+
em status <files> --badge # print a generated SVG badge
|
|
152
|
+
em status <files> -o, --out <path> # write output to a file instead of stdout
|
|
153
|
+
em freshness <file> # standalone freshness signal for one model's conformance record (MIL-164): "last conformed <rev> — N commits and M slice-PRs behind HEAD", computed from the same conform-scope machinery `em status`'s conformance clause uses — for when you want just this fact, no full state-of-the-system rollup (see docs/cli.md)
|
|
154
|
+
em freshness <file> --repo <path> # git repo to compute behind-HEAD in (default: the model's own directory)
|
|
155
|
+
em freshness <file> --json # print a JSON document instead of the text line
|
|
156
|
+
em metrics <file> # the pilot metrics named in advance by the register, computed from git history alone (MIL-170): ratification turnaround (reviewed -> ratified -> implemented), conform-cycle cadence + finding counts, and status-vs-reality disagreement over time — plus a fourth, reported as not computable from history (see docs/usage-data.md). `<file>` is an anchor .em file, used only to locate slices/, conformance/, and .event-modeling.md relative to it — same convention as em ledger; never parsed or compiled
|
|
157
|
+
em metrics <file> --from <rev> # baseline revision
|
|
158
|
+
em metrics <file> --to <rev> # compare revision (default: HEAD)
|
|
159
|
+
em metrics <file> --json # print a JSON document instead of the text report (see docs/cli.md)
|
|
160
|
+
em query consumers <files> # views/reactions consuming an event, plus their slices
|
|
161
|
+
em query consumers <files> --event <ref-or-name> # the event's export ref or display name
|
|
162
|
+
em query consumers <files> --json # print a JSON document instead of the text report
|
|
163
|
+
em query producers <files> # commands producing an event, plus their slices and ui triggers
|
|
164
|
+
em query producers <files> --event <ref-or-name> # the event's export ref or display name
|
|
165
|
+
em query producers <files> --json # print a JSON document instead of the text report
|
|
166
|
+
em query downstream <files> # transitive closure along legal edges from an element — impact analysis
|
|
167
|
+
em query downstream <files> --of <ref-or-name> # the starting element's export ref or display name
|
|
168
|
+
em query downstream <files> --depth <n> # limit traversal to n hops (default: unlimited)
|
|
169
|
+
em query downstream <files> --json # print a JSON document instead of the text report
|
|
170
|
+
em query upstream <files> # transitive closure against legal-edge direction from an element
|
|
171
|
+
em query upstream <files> --of <ref-or-name> # the starting element's export ref or display name
|
|
172
|
+
em query upstream <files> --depth <n> # limit traversal to n hops (default: unlimited)
|
|
173
|
+
em query upstream <files> --json # print a JSON document instead of the text report
|
|
174
|
+
em query slices <files> # filtered slice list — pattern/status/context/persona/tag filters AND-combine
|
|
175
|
+
em query slices <files> --pattern <p> # state-change | state-view | automation | translation | unclassified
|
|
176
|
+
em query slices <files> --status <s> # the slice's joined doc status (draft, reviewed, ready-to-implement, implemented, ...)
|
|
177
|
+
em query slices <files> --context <c> # match a slice with an event in this @Context
|
|
178
|
+
em query slices <files> --persona <p> # match a slice with a ui in this @Persona
|
|
179
|
+
em query slices <files> --tag <t> # match a slice with an event carrying this tag key
|
|
180
|
+
em query slices <files> --json # print a JSON document instead of the text report
|
|
181
|
+
em query invariant <files> # declaring slice + doc facts for one INV-* id, and (with --tests) its test citations
|
|
182
|
+
em query invariant <files> --id <inv-id> # the INV-* id to look up
|
|
183
|
+
em query invariant <files> --tests <dir> # directory to scan for test files citing this id
|
|
184
|
+
em query invariant <files> --json # print a JSON document instead of the text report
|
|
185
|
+
em query field <files> # one field's facts on an element — type, tag/assigned markers, renamed-from chain
|
|
186
|
+
em query field <files> --of <element-ref> # the element's export ref or display name
|
|
187
|
+
em query field <files> --name <field> # the field's name
|
|
188
|
+
em query field <files> --json # print a JSON document instead of the text report
|
|
189
|
+
em query path <files> # shortest path between two elements through the six legal connection types
|
|
190
|
+
em query path <files> --from <ref-or-name> # the starting element's export ref or display name
|
|
191
|
+
em query path <files> --to <ref-or-name> # the ending element's export ref or display name
|
|
192
|
+
em query path <files> --json # print a JSON document instead of the text report
|
|
193
|
+
em engagement new <file> <slug> # write <model dir>/engagements/<slug>.md — frontmatter (slices, parallel ceiling) plus the generated Ledger table; selection by --slices, --context, or --downstream-of (exactly one)
|
|
194
|
+
em engagement new <file> <slug> --slices <keys> # comma-separated slice export keys
|
|
195
|
+
em engagement new <file> <slug> --context <name> # every slice with an event in this context
|
|
196
|
+
em engagement new <file> <slug> --downstream-of <ref> # a slice key, element ref, or display name: it and everything downstream of it (loops-to excluded)
|
|
197
|
+
em engagement new <file> <slug> --parallel <n> # how many slices of one level may be in flight at once
|
|
198
|
+
em engagement new <file> <slug> --by <name> # who created the engagement (createdBy)
|
|
199
|
+
em engagement new <file> <slug> --force # overwrite an existing engagement file with this slug
|
|
200
|
+
em engagement plan <file> <slug> # the engagement's build plan: dependency levels over model.edges (loops-to excluded), and per slice its readiness, branch impl/<key>, base (main or impl/<upstream>), and any hold
|
|
201
|
+
em engagement plan <file> <slug> --json # print a JSON document instead of text (same document as the MCP engagement_plan tool)
|
|
202
|
+
em engagement set <file> <slug> <key> # the only write path to the Ledger: set one slice's state (and optionally branch/base/pr); idempotent; merged is terminal; --state held records a human hold
|
|
203
|
+
em engagement set <file> <slug> <key> --state <state> # one of: planned | building | validating | review | awaiting-merge | merged | held | gap
|
|
204
|
+
em engagement set <file> <slug> <key> --branch <name> # the slice's branch
|
|
205
|
+
em engagement set <file> <slug> <key> --base <name> # the branch the slice's PR targets
|
|
206
|
+
em engagement set <file> <slug> <key> --pr <url> # the slice's PR URL
|
|
207
|
+
em engagement status <file> <slug> # the Ledger joined with each slice doc's current status, plus whether the engagement is closable
|
|
208
|
+
em engagement status <file> <slug> --json # print a JSON document instead of text (same document as the MCP engagement_status tool)
|
|
209
|
+
em engagement close <file> <slug> # set status: closed — refuses unless every slice is merged (recorded or inferred) or gap
|
|
210
|
+
em system [target] # verify a system — every model's `consumes` refs resolved against the other models' `public` events/views, the cross-model half of "both ends of a flow" (MIL-194/MIL-235, see docs/cli.md). Reads system.yaml when given (or found in the directory), else discovers every *.em in the repo
|
|
211
|
+
em system [target] --json # print a JSON document instead of the text report (see docs/cli.md)
|
|
212
|
+
em system codeowners [target] # generate (or --check) the managed CODEOWNERS block that routes review: each model's team on its design directory, and every team that `consumes` a model's public surface on that model's contract file (MIL-234, see docs/ci.md and docs/cli.md)
|
|
213
|
+
em system codeowners [target] -o, --output <path> # the CODEOWNERS file to splice into (default: the first of CODEOWNERS, .github/CODEOWNERS, docs/CODEOWNERS that exists, else CODEOWNERS at the repo root)
|
|
214
|
+
em system codeowners [target] --check # verify the committed file already carries the generated block; never writes, exit 1 on drift (CI)
|
|
215
|
+
em system codeowners [target] --json # print a JSON document instead of the text report (see docs/cli.md)
|
|
216
|
+
em system scope [target] # check a change set against the seams (MIL-240): fail only when it alters a producer's public surface (or contract file) AND a consuming model's design dir together (`seam-crossing`); warn on other multi-model change sets. Files changed only by `Em-Upgrade:` commits are exempt. There is no override - review on the contract file is the gate (see docs/cli.md, docs/ci.md)
|
|
217
|
+
em system scope [target] --base <rev> # the change set committed since <rev> (merge-base form, <rev>...HEAD); CI passes the PR base
|
|
218
|
+
em system scope [target] --staged # also (or only) check what is staged right now; never exempted
|
|
219
|
+
em system scope [target] --json # print a JSON document instead of the text report (see docs/cli.md)
|
|
220
|
+
em contract # print the packaged implementation contract (reference/implement.md) to stdout — the agent-neutral discovery path for any agent that can run a shell, not just Claude Code (MIL-129); see docs/cli.md
|
|
221
|
+
em mcp # start an MCP (Model Context Protocol) server over stdio (MIL-21) — a structured, agent-facing alternative to shelling out to `em`; every tool mirrors a CLI `--json` surface byte-for-byte. docs/mcp.md isn't vendored into the skill bundle — for the handshake and the full, current tool table (the list changes as commands gain MCP parity, so it's not repeated here — MIL-187) see https://github.com/milehimikey/em/blob/main/docs/mcp.md. Equivalent to running the `em-mcp` bin directly
|
|
222
|
+
em skill install # copy the event-modeling skill bundle into .claude/skills/ (event-modeling, event-modeling-discover/-design/-implement/-conform/-review, event-modeling-shared); across a structural bundle change (e.g. the MIL-157 split of the old single event-modeling/ directory into this six-directory bundle), `em skill sync` is the migration path — --force now performs the same reconcile (MIL-180)
|
|
223
|
+
em skill install -f, --force # overwrite an existing installation, reconciling added/updated/removed files within the bundle's own managed directories the same way `em skill sync` does — never touching an unrelated sibling skill (MIL-180)
|
|
224
|
+
em skill install --no-agents-md # skip writing/updating the AGENTS.md agent-contract section (on by default, MIL-129)
|
|
225
|
+
em skill sync [path] # update the vendored .claude/skills/ event-modeling skill bundle in [path] to match the installed em package (overwrites unconditionally; local edits are never merged, MIL-93)
|
|
226
|
+
em skill sync [path] --no-agents-md # skip writing/updating the AGENTS.md agent-contract section (on by default, MIL-129)
|
|
227
|
+
em skill check [path] # check [path]'s em skills for drift against the installed em package: the em Claude Code plugin's pin (settings + this machine's marketplace registry) when the repo declares it, and the vendored .claude/skills/ bundle when present; exits non-zero on any mismatch (CI-ready, MIL-93, MIL-231)
|
|
228
|
+
em skill check [path] --json # print a JSON document instead of the text report (see docs/cli.md)
|
|
229
|
+
em skill check [path] --ci # also fail (exit 1) when the declared plugin is not registered on this machine (implied by CI=true in the environment); without it that finding prints the install commands on stderr and exits 0
|
|
230
|
+
em ci init <model> # install .github/workflows/em-ci.yml (PR gates: em validate, em slice index --check, em coverage --strict, em ledger, em skill check, em glossary --fail-on-conflicts, plus a push-triggered status-badge rebuild) and em-conform.yml (scheduled, advisory-only conformance cadence) — same install discipline as `em skill install`: marker-delimited, idempotent, --check for CI self-verification (MIL-166, see docs/ci.md). Give it a system.yaml (or its directory) instead of a model to cover every model in the system: one workflow, per-model jobs, a conform matrix (MIL-233)
|
|
231
|
+
em ci init <model> --tests <dir> # test directory the coverage/status steps scan for INV-* citations
|
|
232
|
+
em ci init <model> -f, --force # replace an existing workflow file that has no GENERATED markers, or a managed block generated for a different set of models
|
|
233
|
+
em ci init <model> --check # verify both files match the current preset; exit non-zero on drift without writing (CI)
|
|
234
|
+
em upgrade <file> # bring a model repo authored under an older em (1.6 forward) up to the installed version: an ordered list of mechanical steps (skill bundle, reaction shape, state-file bullets, generated CI blocks, constitution scaffold) plus a human list of things no command can safely decide by itself (MIL-219, see docs/upgrading.md). Dry-run by default; --apply makes one git commit per applicable step, refusing on a dirty working tree
|
|
235
|
+
em upgrade <file> --apply # apply every applicable mechanical step, one git commit each, then a final `Em version:` commit
|
|
236
|
+
em upgrade <file> --check # exit non-zero only on a hard incompatibility (an existing but unparseable state file, an un-migratable .em shape) — writes nothing; what CI runs; the final line says why
|
|
237
|
+
em upgrade <file> --json # print a JSON document instead of text (dry-run/--check only, never with --apply)
|
|
238
|
+
```
|
|
239
|
+
<!-- GENERATED:cli:end -->
|
|
240
|
+
|
|
241
|
+
Install if missing: `npm i -g @milehimikey/em`. SVG, PNG, and PDF all work with no system
|
|
242
|
+
deps; only rarer formats (ps, eps, ...) need `rsvg-convert`.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Grammar
|
|
247
|
+
|
|
248
|
+
```
|
|
249
|
+
model "Name" [owner "Team"] # diagram title; optional owning team(s), quoted free text
|
|
250
|
+
|
|
251
|
+
persona Name # a UI swimlane row (actor)
|
|
252
|
+
context Name # an event swimlane row (bounded context / aggregate)
|
|
253
|
+
|
|
254
|
+
slice "Name" [source "url"] { # one vertical time step (a column); source is optional
|
|
255
|
+
ui Free Text @Persona # screen; @Persona picks its row (defaults to first/"User")
|
|
256
|
+
command Free Text # state-changing request (API band)
|
|
257
|
+
invariant INV-ORD-1 "Rule" # rule guarding the command/event just above (see Clauses)
|
|
258
|
+
view Free Text from "Event A", "Event B" # read model fed by event(s)
|
|
259
|
+
view Free Text again from "Event C" # later instance of an evolving read model (see Clauses)
|
|
260
|
+
event Free Text @Context # recorded fact; @Context picks its row (defaults to "Domain")
|
|
261
|
+
processor Free Text from "View" # automation; aliases: automation | saga | translation
|
|
262
|
+
translation Free Text consumes other-model:event.slug # binds another model's public event/view
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
arrow From Element -> To Element # explicit cross-slice edge (overrides inferred flow)
|
|
266
|
+
|
|
267
|
+
type Name { field: Type, ... } # named structured type, reusable from any field (see Named types)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Element kinds (8 keywords, nothing else)
|
|
271
|
+
| Keyword | Band | Meaning | Tag | Extra clauses |
|
|
272
|
+
|---|---|---|---|---|
|
|
273
|
+
| `ui` | persona | screen / interface | `@Persona` | `note`, `issue`, `divergence`, `{ fields }` |
|
|
274
|
+
| `command` | API | state-changing request | — | `note`, `issue`, `divergence`, `renamed from`, `{ fields }`, standalone `invariant` line |
|
|
275
|
+
| `view` | API | read model / projection | — | `from "Event"…`, `note`, `issue`, `divergence`, `public`, `{ fields }` |
|
|
276
|
+
| `event` | context | recorded fact (past tense) | `@Context` | `note`, `issue`, `divergence`, `public`, `tag`, `renamed from`, `loops-to`, field-level `assigned`, `{ fields }`, standalone `invariant` line |
|
|
277
|
+
| `processor` / `automation` / `saga` / `translation` | automation | system reaction / adapter | — | `from "…"`, `note`, `issue`, `divergence`, `{ fields }`; `translation` only: `consumes` |
|
|
278
|
+
|
|
279
|
+
### Clauses
|
|
280
|
+
- **Tags:** `@Persona` only on `ui`; `@Context` only on `event`. Undeclared tags auto-create a
|
|
281
|
+
row. Multi-word tags need no quoting — the tag captures everything after `@` to end of line
|
|
282
|
+
(`ui Ticket Queue @Customer Service` matches `persona Customer Service`).
|
|
283
|
+
- **`from "X"`** on views/automations declares the source(s); names are quoted, comma-separated.
|
|
284
|
+
Matching is case-insensitive and whitespace-normalized. A `from` may never point at an event or
|
|
285
|
+
view that first appears in a LATER slice (forward-only timeline — validation error).
|
|
286
|
+
Kinds never cross: a view's `from` resolves ONLY to events; a reaction's `from` resolves ONLY
|
|
287
|
+
to read models — naming an event on a reaction is a validation error even though the event
|
|
288
|
+
exists. Feed an event to a reaction by projecting it into a view first (the automation's
|
|
289
|
+
"to-do list") and pointing the reaction at that view. Name that view after the pending work
|
|
290
|
+
(`Payments To Process`), never after the triggering event — a view reusing the event's name
|
|
291
|
+
collides in the shared namespace and draws a duplicate-name warning.
|
|
292
|
+
- **`again`** (views only): `view <Name> again [from "Event", …]` declares a later instance of an
|
|
293
|
+
already-declared read model — the forward-only device for a view that evolves as later events
|
|
294
|
+
land. Instances are ONE logical view: the first declaration owns the `note` binding; each
|
|
295
|
+
instance's `from` lists only the NEW events landing at that point (not cumulative); a reaction
|
|
296
|
+
(`from "View"`) reads the nearest instance at-or-before its own slice. Instances are NEVER
|
|
297
|
+
connected to one another — continuity is implied by the shared name, and the events reaching each
|
|
298
|
+
instance are what show the view changing. `again` with no earlier declaration is a validation error.
|
|
299
|
+
Use `again` (not a plain repeated `view` name) whenever the view is referenced by a
|
|
300
|
+
`from`/`arrow` — plain repeats are only warning-free while unreferenced. A later instance is
|
|
301
|
+
also a **continuation** for the doc lifecycle (MIL-208): it stays a slice on the timeline but
|
|
302
|
+
is not a spec unit — no doc, no status, no ratification, no PR of its own; the originating
|
|
303
|
+
slice's doc carries its behavior and `em export`'s `alsoReads` carries the union of events it
|
|
304
|
+
reads. See `slice-doc-schema.md`'s "Continuations" section for the full contract.
|
|
305
|
+
- **`loops-to "View"`** (events only): marks an event as re-feeding an earlier read model — a
|
|
306
|
+
poll/retry loop where a later fact must re-trigger an automation that already ran (a lapsed
|
|
307
|
+
reservation re-notifying the next person waiting in line, and similar re-triggers). Written
|
|
308
|
+
after the event's name/`@Context`, either on the header line or trailing an
|
|
309
|
+
inline `{ fields }` block; repeatable (`loops-to "A" loops-to "B"`, or comma-joined). The
|
|
310
|
+
target must resolve to a `view` instance STRICTLY EARLIER on the timeline — the opposite of
|
|
311
|
+
`from`'s "nearest at-or-before" rule; pointing at the same slice or later is a validation
|
|
312
|
+
error naming `from` on a later `view … again` instance as the forward-flowing alternative,
|
|
313
|
+
and naming a view that doesn't exist is a separate unresolved-target error. A resolved
|
|
314
|
+
`loops-to` counts as the event being read for both-ends-of-a-flow completeness. **No arrow is
|
|
315
|
+
ever drawn** for it — drawing one backward would violate the timeline laws — instead the
|
|
316
|
+
event gets an indigo ↩ corner marker (bottom-left) and the legend gains a "Loops" section:
|
|
317
|
+
`↩ 1 <Event> re-feeds "<View>"` (the number identifies the event, not the target). `em
|
|
318
|
+
export` carries it as `loopsTo: { name, ref }[] | null` (`from`'s shape); `model.edges` gains
|
|
319
|
+
`{ from: <event ref>, to: <view ref>, source: "loops-to" }`; `em query`'s adjacency labels the
|
|
320
|
+
connection `"loops-to"` too, so `downstream`/`upstream` follow it (cycle-safely).
|
|
321
|
+
- **`note "path.md"`** on ANY element links a markdown doc. Relative to the `.em` file. Renders as
|
|
322
|
+
a clickable marker in SVG and a legend entry in PNG/PDF. **This is how slice docs attach.**
|
|
323
|
+
- **`issue "text"`** on ANY element flags an open question inline — the diagram-visible red
|
|
324
|
+
sticky note. Renders as a red corner marker (opposite corner from `note`, so both can coexist
|
|
325
|
+
on one element) plus a legend entry; `em validate` warns on every open issue. **Prefer this
|
|
326
|
+
over a `# TBD` comment for anything that should show up on the rendered diagram** — `# TBD` is
|
|
327
|
+
invisible once rendered, `issue` isn't.
|
|
328
|
+
- **`divergence "text"`** on ANY element records a reasoned, ratified deviation between this
|
|
329
|
+
element and its implementation — the *resolved* sibling of `issue` (lint-suppression-with-
|
|
330
|
+
rationale for the `conform` phase). Renders as a teal corner marker (bottom-right — distinct
|
|
331
|
+
from `note`'s top-right and `issue`'s top-left, so all three can coexist) plus a legend entry.
|
|
332
|
+
Raises **no** `em validate` warning by design; use `--list-divergences` to audit. `em diff
|
|
333
|
+
--json` carries it forward as `acceptedDivergence` on the affected change/removal entry, so
|
|
334
|
+
`conform` can cite an already-ratified deviation instead of re-flagging it as drift every run.
|
|
335
|
+
- **`public`** (events and views): marks the element as part of the published integration
|
|
336
|
+
surface — a published event contract (e.g. AsyncAPI), or a view whose read API/response
|
|
337
|
+
shape external consumers depend on — as opposed to an internal-only fact or projection.
|
|
338
|
+
Plain structural flag, no free text and no diagram marker (same posture as `again`). Write
|
|
339
|
+
it as the last token on the line; on an event, optionally right before a trailing
|
|
340
|
+
`@Context` — `event X @Context public` or `event X public @Context` both work. A `public`
|
|
341
|
+
element is exempt from the unread-event / unconsumed-view warnings (its consumer is outside
|
|
342
|
+
this model). `em export` carries it as `public: true`/`false`; `em diff` reports a flip as
|
|
343
|
+
`event marked public`/`event unmarked public` (the entry cites the element's kind, so a
|
|
344
|
+
view flip reads `view "X"`); `em validate --list-public` audits the whole public surface.
|
|
345
|
+
**Exposure of a read model (MIL-215) is three cases, no dedicated keyword:** a person/client
|
|
346
|
+
reads it → draw the `ui` that reads it (ships the endpoint); another model/system reads it →
|
|
347
|
+
`public` (a cross-model contract its consumer binds with `consumes`, verified by `em system`, not `em validate`); only
|
|
348
|
+
an automation reads it → nothing — internal by design, the read a reaction is required to
|
|
349
|
+
make, not an endpoint. `em validate --list-public` flags a `public` view with no `ui`/reaction
|
|
350
|
+
reading it anywhere in this model as an audit note ("no in-model reader"), not a warning — a
|
|
351
|
+
cross-model `public` view's reader legitimately lives outside this file (see "`em validate`
|
|
352
|
+
rules" below).
|
|
353
|
+
- **`consumes`** (translation only, MIL-235): `translation X consumes <modelKey>:<kind>.<slug>[, …]`
|
|
354
|
+
binds this boundary-crossing reaction to another model's `public` event or view. The binding is
|
|
355
|
+
declared on the consumer and has no version. `kind` is `event` or `view` (never `command`).
|
|
356
|
+
`modelKey` is the kebab-slug of the producer's `model "Name"`, and `slug` is the kebab-slug of
|
|
357
|
+
the element's name. There is no slice segment, so the producer can move the element between
|
|
358
|
+
slices without breaking consumers. Refs are unquoted and comma-separated. Write the clause before
|
|
359
|
+
or after `from "…"`, before or after a `{ … }` block, never inside the braces. On any other kind
|
|
360
|
+
it is a parse error: ``line N: `consumes` is only valid on translation — only a boundary-crossing
|
|
361
|
+
reaction binds to another model's public surface``. `em validate` checks the grammar only.
|
|
362
|
+
`em system` resolves the ref (`consumes-unknown-model` / `consumes-unknown-element`) and
|
|
363
|
+
derives `dangling-public-event` / `unbound-translation` from it. Two `public` elements of one
|
|
364
|
+
kind sharing a slug are a `public-name-not-unique` error, because a ref couldn't tell them
|
|
365
|
+
apart. Exported as `consumes: string[] | null`.
|
|
366
|
+
- **`owner`** (model header, MIL-235): `model "Checkout" owner "Storefront team"[, "…"]`. Quoted
|
|
367
|
+
free text, kept as written, exported as `model.owner: string[]`. It replaces a 1.0
|
|
368
|
+
`system.yaml`'s per-model `owner:`.
|
|
369
|
+
- **`tag`** (events only): declares a DCB (Dynamic Consistency Boundary) tag key. Three forms:
|
|
370
|
+
a trailing `tag` on a field line inside the event's `{ … }` block (`priceId: UUID tag`, or
|
|
371
|
+
typeless `priceId tag`) — an identity tag, key defaults to the field's own name; a bare field
|
|
372
|
+
whose ENTIRE text is just `tag` is a field named `tag`, not a clause. `tag <key> from a, b`
|
|
373
|
+
(composite, ≥2 bare/unquoted field names — unlike a view's quoted `from`). `tag <key>
|
|
374
|
+
external "text"` (external — the string is documentation only, never parsed). Composite/
|
|
375
|
+
external clauses are element-level: write them as a trailing clause on the event (same
|
|
376
|
+
family as `note`/`issue`), or as one or more standalone `tag ...` lines immediately following
|
|
377
|
+
the event inside the slice body (attaches to the most recently declared element, which must
|
|
378
|
+
be an event); multiple accumulate. `tag` anywhere on a command/view/`ui`/`type` field, or an
|
|
379
|
+
element-level `tag` clause on a non-event, is a parse error. `em export` carries every tag
|
|
380
|
+
key under the event's `tags` array (`{ key, kind, fields, description }`); `em validate`
|
|
381
|
+
flags a composite tag naming an unknown field and a duplicate tag key on one event.
|
|
382
|
+
- **`renamed from "Old1", "Old2"`** (events and commands only): records the prior name(s) an
|
|
383
|
+
element, or one of its fields, was known as — codegen metadata for converting
|
|
384
|
+
already-stored payloads instead of an upcaster chain. Element-level trails the element's own
|
|
385
|
+
name (same trailing-clause family as `@Context`/`public`); field-level trails a field's type,
|
|
386
|
+
or the bare name of a typeless field, inside the `{ … }` block — event/command fields only, a
|
|
387
|
+
parse error on a view/`ui`/automation-kind field or a `type` field. The list is quoted,
|
|
388
|
+
comma-separated, most-recent-old-name first. Inside one inline `{ … }` block, a bare quoted
|
|
389
|
+
field name immediately after a renamed field is read as a CONTINUATION of that list, not a
|
|
390
|
+
new field — give the quoted-name field a type, or write fields one per line, to avoid the
|
|
391
|
+
ambiguity. `em export` carries it as `renamedFrom: string[] | null` on both the element and
|
|
392
|
+
each field. **`em diff` does not read this clause** — a rename still reports as remove+add.
|
|
393
|
+
- **`assigned`** (event fields only): marks a field as system-assigned — set by the server/
|
|
394
|
+
handler, never supplied by the triggering command (a minted ID, a decision-time timestamp).
|
|
395
|
+
Trails a field's type, or the bare name of a typeless field (`orderId assigned`,
|
|
396
|
+
`placedAt: Instant assigned`) — same trailing-clause family as `tag`/`renamed from`, composes
|
|
397
|
+
with either in any order. A field whose ENTIRE text is just `assigned` is a field named
|
|
398
|
+
`assigned`, not a clause. `assigned` on a command/view/`ui`/`type` field is a parse error.
|
|
399
|
+
Excludes the field from `em validate`'s event ← command fields-completeness check (and thus
|
|
400
|
+
from `--slice-ready`, since the diagnostic never fires) without narrowing view ← event
|
|
401
|
+
tracing. `em export` carries it as `assigned: boolean` on the field, always present, same
|
|
402
|
+
`=== true` convention as `tag`. **Reach for this before echoing a server-set field onto the
|
|
403
|
+
command just to silence a fields-completeness warning** — that misrepresents what the client
|
|
404
|
+
actually sends.
|
|
405
|
+
- **`derived`** (view fields only, MIL-200): marks a field as computed from which events have
|
|
406
|
+
landed, rather than copied from a single source event's payload (a status stepping through
|
|
407
|
+
states as events arrive, a computed ordinal, an availability flag two events can flip).
|
|
408
|
+
Trails a field's type, or the bare name of a typeless field — bare `derived` (no traced
|
|
409
|
+
source) or traced `derived from "Event A", "Event B"` (the two-word form parsed before the
|
|
410
|
+
bare check, same discipline as `renamed from` vs. plain `from`). A field whose ENTIRE text is
|
|
411
|
+
just `derived` is a field named `derived`, not a clause. `derived`/`derived from` on a
|
|
412
|
+
command/event/`ui`/`type` field is a parse error. Excludes the field from
|
|
413
|
+
`fields-completeness/view-field-no-source` entirely (and thus from `--slice-ready`). The
|
|
414
|
+
traced form still checks something: every named event must resolve among the view's ACTUAL
|
|
415
|
+
sources (its own `from` list, or the same-slice events for a `from`-less view) — an
|
|
416
|
+
unresolved name is a validation error, `derived-from-unresolved`. `em export` carries it as
|
|
417
|
+
`derived: boolean` on the field (always present, same `=== true` convention as `tag`/
|
|
418
|
+
`assigned`) and `derivedFrom: string[] | null` (the traced event name(s), `null` on a bare
|
|
419
|
+
`derived` field). **Reach for this instead of dropping a computed field out of the `.em` and
|
|
420
|
+
into doc-only prose** — the marker keeps `em export`/the model aware the field exists (the
|
|
421
|
+
`doc-model-field-mismatch` doc↔model check does not compare view field tables at all today —
|
|
422
|
+
see the slice template's field-table guidance — so this is an export-side win specifically,
|
|
423
|
+
not yet a doc-drift check).
|
|
424
|
+
- **Fields:** `command Place Order { orderId: UUID, items: LineItem[], customerId }` — inline or
|
|
425
|
+
one-per-line. Types are free text (no semantic checking) UNLESS the type string names a
|
|
426
|
+
declared `type` (see Named types below), in which case it resolves to a structured
|
|
427
|
+
reference. Keep these light; full field specs with rules live in the slice doc.
|
|
428
|
+
- **`invariant`** (command/event only, MIL-265): a standalone line directly after the element,
|
|
429
|
+
`invariant INV-ORD-1 "Order total must be positive"` — ID, then an optional quoted rule
|
|
430
|
+
sentence (always write one). Several lines accumulate in order. Parse error after a
|
|
431
|
+
view/`ui`/reaction, before any element, or inside a `{ … }` block. The ID must match
|
|
432
|
+
`INV-<MNEMONIC>-<n>` (2-4 uppercase letters/digits, a number) — `invariants/malformed-id` and
|
|
433
|
+
`invariants/duplicate-id` are errors. **The model declares the ID and the rule; the slice doc's
|
|
434
|
+
Invariants section cites the ID and elaborates** (`- INV-ORD-1 — why, edge cases`), never
|
|
435
|
+
restating it as a `**INV-ORD-1:**` rule (`invariants/declared-in-both`). A model-declared ID
|
|
436
|
+
belongs to its element's slice for `em coverage`/`em query invariant`; doc-declared IDs (the
|
|
437
|
+
1.13 style) still work as the fallback. `em export` carries `invariants: [{ id, rule }] | null`
|
|
438
|
+
per element.
|
|
439
|
+
- **Comments:** `# ...` anywhere outside quotes (full-line or trailing).
|
|
440
|
+
- **Quoted strings:** everything between a `"` and its matching `"` is literal — `{`, `}`, `#`
|
|
441
|
+
included, never re-interpreted as a field block or comment — so REST path-template
|
|
442
|
+
placeholders are safe as-is: `issue "PUT v3/widgets/{widgetId}/suspend"`. Escape a literal
|
|
443
|
+
`"` or `\` inside a string with `\"`/`\\`; no other backslash sequence is special. An
|
|
444
|
+
unterminated string (missing closing `"`) is a parse error naming the clause it broke in.
|
|
445
|
+
- **`source "url"` on a slice header** (the only slice-level clause — everything above is
|
|
446
|
+
per-element) links the slice back to the ticket/conversation it traces to, e.g.
|
|
447
|
+
`slice "Checkout" source "https://linear.app/team/issue/MIL-60" { ... }`. Exports as
|
|
448
|
+
`model.slices[].source` via `em export`, so an intake loop stays machine-traversable instead
|
|
449
|
+
of relying on prose. Purely metadata — no visual marker, not validated. Don't confuse it with
|
|
450
|
+
an element's `note "path.md"` (a markdown file link, not a URL).
|
|
451
|
+
|
|
452
|
+
### Named types
|
|
453
|
+
|
|
454
|
+
`type Name { field: Type, ... }` declares a reusable structured shape at the top level (any
|
|
455
|
+
order relative to `persona`/`context`/`slice`, no clauses). Reference it from any field
|
|
456
|
+
anywhere — bare (`winner: QuoteAcceptedLine`) for a nested object, `Name[]` for an array
|
|
457
|
+
(`lines: QuoteAcceptedLine[]`). Resolution is opportunistic: a type string only becomes a
|
|
458
|
+
structured reference when it names a declared type (case/whitespace-insensitive match); every
|
|
459
|
+
other type string stays free text exactly as before, so declaring no `type` blocks changes
|
|
460
|
+
nothing. Recursion between declared types is allowed as a DAG (e.g. a diamond shape like
|
|
461
|
+
`Order` referencing `Address` twice) — a bare/singular self- or mutual-cycle
|
|
462
|
+
(`type Node { child: Node }`) is a validation error, but the same shape through an array
|
|
463
|
+
(`type Node { children: Node[] }`) is legal since the array can terminate at runtime; this is
|
|
464
|
+
how tree/recursive data (categories, org charts, comment threads, BOMs) gets expressed.
|
|
465
|
+
`em export` lists every declared type under `model.types[]` (stable `ref`, e.g.
|
|
466
|
+
`types/quote-accepted-line`) and adds an additive `typeRef` key to every field (declared-type
|
|
467
|
+
fields and ordinary element fields alike) — `{ name, ref, array }` when resolved, `null`
|
|
468
|
+
otherwise. `em diff` tracks types added/removed and field changes on surviving types.
|
|
469
|
+
|
|
470
|
+
### Swimlane band order (top → bottom)
|
|
471
|
+
Header row → **Automation** (only if used) → **persona** rows (in declared order) → **API**
|
|
472
|
+
(commands + views share this lane) → **context** rows (in declared order).
|
|
473
|
+
|
|
474
|
+
### Colors (for orientation)
|
|
475
|
+
UI = white, command = blue, event = amber/orange, view = green, automation = gray.
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## Pattern → DSL mapping
|
|
480
|
+
|
|
481
|
+
```em
|
|
482
|
+
# 1. State Change: UI -> Command -> Event
|
|
483
|
+
slice "Place Order" {
|
|
484
|
+
ui Checkout @Customer
|
|
485
|
+
command Place Order
|
|
486
|
+
event Order Placed @Order
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
# 2. State View: Event(s) -> Read Model -> UI
|
|
490
|
+
slice "Open Orders" {
|
|
491
|
+
view Open Orders from "Order Placed"
|
|
492
|
+
ui Order List @Customer
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
# 3. Automation: read model in the slice before, reaction + command + event together
|
|
496
|
+
slice "Orders To Fulfill" {
|
|
497
|
+
view Orders To Fulfill from "Order Placed"
|
|
498
|
+
}
|
|
499
|
+
slice "Ship Order" { # reaction, command, and event all share this one slice
|
|
500
|
+
processor Fulfillment Service from "Orders To Fulfill"
|
|
501
|
+
command Ship Order
|
|
502
|
+
event Order Shipped @Shipping
|
|
503
|
+
}
|
|
504
|
+
slice "Open Orders — shipped" {
|
|
505
|
+
view Open Orders again from "Order Shipped" # every event needs a reader
|
|
506
|
+
ui Order List @Customer # ...and every read model needs a consumer
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
# 4a. Translation (external trigger, no durable artifact): external call -> translation -> command -> event
|
|
510
|
+
slice "Confirm Delivery" {
|
|
511
|
+
translation Carrier Adapter # inbound from outside the model; no internal `from`
|
|
512
|
+
command Confirm Delivery
|
|
513
|
+
event Delivery Confirmed @Shipping
|
|
514
|
+
}
|
|
515
|
+
slice "Open Orders — delivered" {
|
|
516
|
+
view Open Orders again from "Delivery Confirmed"
|
|
517
|
+
ui Order List @Customer
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
# 4b. Translation (external trigger, durable artifact): persisted inbound queue -> translation -> command -> event
|
|
521
|
+
slice "Receive Carrier Event" { # ingest: persist the raw webhook before reacting to it
|
|
522
|
+
ui Webhook Endpoint @Carrier
|
|
523
|
+
command Receive Carrier Event
|
|
524
|
+
event Carrier Event Received @Shipping # scoped to @Shipping's fact, not the @Carrier caller
|
|
525
|
+
}
|
|
526
|
+
slice "Inbound Carrier Events" {
|
|
527
|
+
view Inbound Carrier Events from "Carrier Event Received" # the persisted inbound queue
|
|
528
|
+
}
|
|
529
|
+
slice "Acknowledge Carrier Event" {
|
|
530
|
+
translation Carrier Queue Adapter from "Inbound Carrier Events" # different producer than 4a's Carrier Adapter, so a distinct name
|
|
531
|
+
command Acknowledge Carrier Event
|
|
532
|
+
event Carrier Event Acknowledged @Shipping
|
|
533
|
+
}
|
|
534
|
+
slice "Inbound Carrier Events — processed" {
|
|
535
|
+
view Inbound Carrier Events again from "Carrier Event Acknowledged"
|
|
536
|
+
ui Delivery Board @Ops
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
# 4c. Translation (internal trigger, durable artifact): read model -> translation -> command -> event
|
|
540
|
+
slice "Accept Quote" {
|
|
541
|
+
ui Quote Screen @Customer # every command needs a trigger: a ui, or a reaction, in this slice
|
|
542
|
+
command Accept Quote
|
|
543
|
+
event Quote Accepted @Quote
|
|
544
|
+
}
|
|
545
|
+
slice "Quotes To Sync" {
|
|
546
|
+
view Accepted Quotes from "Quote Accepted"
|
|
547
|
+
}
|
|
548
|
+
slice "Record Sync" {
|
|
549
|
+
translation CRM Sync from "Accepted Quotes" # reacts to our own state via the read model
|
|
550
|
+
command Record Crm Sync
|
|
551
|
+
event Quote Synced @Quote
|
|
552
|
+
}
|
|
553
|
+
slice "Accepted Quotes — synced" {
|
|
554
|
+
view Accepted Quotes again from "Quote Synced"
|
|
555
|
+
ui Sync Status @Customer
|
|
556
|
+
}
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
Trigger source (external/internal) and durable artifact (`view`-backed or not) are independent
|
|
560
|
+
axes — 4a and 4b are both "externally triggered" but only 4b has a queue, and a `view`-backed
|
|
561
|
+
translation reacts the same whether the view was filled by an outside system (4b) or the model's
|
|
562
|
+
own event (4c). An inbound message earns `event` status the same way any event does: scope it to
|
|
563
|
+
the context/lane whose fact it represents (e.g. `Carrier Event Received @Shipping` or its own
|
|
564
|
+
`Carrier` context), not by who committed it — that keeps it a legitimate boundary fact (à la an
|
|
565
|
+
Anti-Corruption-Layer event in DDD) instead of a technical artifact leaking foreign vocabulary
|
|
566
|
+
into the model.
|
|
567
|
+
|
|
568
|
+
A `translation` (like a `processor`) is a **reaction**: it triggers a command in the same slice —
|
|
569
|
+
same shape as the Automation pattern above, and the same shape `ui` already uses in State Change.
|
|
570
|
+
The reaction never records the event directly (there's no `reaction -> event` edge); the command
|
|
571
|
+
it triggers is what records it, and that event happens to live in the reaction's own slice now
|
|
572
|
+
because the command does.
|
|
573
|
+
|
|
574
|
+
Note the read slice closing each pattern: **every event must be read by some read model**
|
|
575
|
+
(warning 4 below). A command slice is not finished until the slice that projects its event
|
|
576
|
+
exists. Reactions don't count — they read *views*, not events.
|
|
577
|
+
|
|
578
|
+
### Headless / API systems & repeated read models
|
|
579
|
+
|
|
580
|
+
A headless system (no screens — clients call an API) still uses `ui`/`persona`: a slice **is**
|
|
581
|
+
trigger → command → event, read vertically, so the trigger belongs *in* the slice, never split into
|
|
582
|
+
one of its own. Declare a persona per external caller/role and treat its `ui` boxes as API calls
|
|
583
|
+
instead of shipped screens — same two patterns (State Change, State View) as any other slice, no new
|
|
584
|
+
shape:
|
|
585
|
+
|
|
586
|
+
```em
|
|
587
|
+
persona IntegratorAPI # API-flagged lane: its `ui` boxes are API calls, not shipped screens
|
|
588
|
+
|
|
589
|
+
slice "Create Quote" {
|
|
590
|
+
ui Create Quote @IntegratorAPI
|
|
591
|
+
command Create Quote
|
|
592
|
+
event QuoteCreated @Quote
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
slice "Read Quote — created" {
|
|
596
|
+
view Quote from "QuoteCreated"
|
|
597
|
+
ui Read Quote @IntegratorAPI
|
|
598
|
+
}
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
- **`translation` stays reserved for genuine reactions and real external-system boundaries** — an
|
|
602
|
+
internal automation, or a webhook/adapter crossing into another system (see the Automation and
|
|
603
|
+
4a/4b/4c Translation examples above, unchanged). It is **not** how you model a synchronous
|
|
604
|
+
request/response API call — that's Pattern 1 (State Change) with an API persona, exactly like the
|
|
605
|
+
`Create Quote` slice above.
|
|
606
|
+
- **Internal-only commands and views (no public route) carry no `ui` at all.** They follow the
|
|
607
|
+
ordinary Automation pattern already documented: the reaction shares the command's slice, and an
|
|
608
|
+
internal-only read model is consumed by that reaction (from the slice before), not by a screen
|
|
609
|
+
or an API query.
|
|
610
|
+
- **Repeat the read model** in every slice where it's read so the timeline flows left-to-right, and
|
|
611
|
+
**prefer `view X again`** for every instance after the first (see Clauses) — **each instance
|
|
612
|
+
carries its own consumer** (the `ui` that reads it, or a reaction), not just the last one. `again`
|
|
613
|
+
instances are exempt from the duplicate-name warning even when referenced, and each reference
|
|
614
|
+
resolves to the right instance — a plain repeat only stays warning-free while nothing references it by name, and
|
|
615
|
+
resolves to the *first* declaration when something does. **Wire each event to a read model exactly
|
|
616
|
+
once:** a repeated instance's `from` lists only the **new** events since the previous instance
|
|
617
|
+
(not cumulative), or the event draws a duplicate arrow to the same read model at every repeat. (An
|
|
618
|
+
event may still feed several *different* read models, once each.)
|
|
619
|
+
- **Instances are never joined to one another.** No arrow between two instances of one read model,
|
|
620
|
+
ever — an explicit one is a validation error. The repeat is a timeline device: continuity is
|
|
621
|
+
implied by the shared name, and the events arriving at each instance are what show it changing.
|
|
622
|
+
- **Keep arrows span-1: put each repeat right after its feeding event.** Place a read-model instance
|
|
623
|
+
immediately after the event that updates it, sourcing only that single adjacent event. The
|
|
624
|
+
renderer routes a long arrow *around* intervening boxes rather than through them, so it no longer
|
|
625
|
+
reads as a forbidden read→read link — but distance is still a real problem: the arrowhead lands
|
|
626
|
+
columns away from the event that produced it, so **the write slice reads as dangling**, as if
|
|
627
|
+
nothing consumed its event. You have to trace a line across the diagram to see the connection.
|
|
628
|
+
Keep the read model adjacent to its event, and keep a sub-flow that detours into another context
|
|
629
|
+
together rather than parking it at the end of the model.
|
|
630
|
+
|
|
631
|
+
What this means for whoever builds a slice, not just whoever draws it: a `ui` trigger or
|
|
632
|
+
consumer is an obligation to ship an endpoint, no `ui` means internal-only, and `public` marks
|
|
633
|
+
the published integration surface — see `reference/implement.md`'s "Interface obligations"
|
|
634
|
+
paragraph in the `event-modeling-implement` skill (§2).
|
|
635
|
+
|
|
636
|
+
### Reaction wiring — no positional gotcha
|
|
637
|
+
|
|
638
|
+
A reaction's command is inferred from **same-slice presence**, exactly like `ui -> command`: put
|
|
639
|
+
`processor`/`translation` in the same slice as the command it triggers, in either order, and `em`
|
|
640
|
+
wires it correctly regardless of what any other slice contains or where it sits in the file. There's
|
|
641
|
+
no adjacency to get right and no reordering hazard — the read model it watches (if any) is named
|
|
642
|
+
explicitly via `from "View"`, resolved by name to the nearest instance at or before its own slice,
|
|
643
|
+
not by position either.
|
|
644
|
+
|
|
645
|
+
The one thing that still doesn't wire on its own: a `ui` never triggers a reaction — no pattern has
|
|
646
|
+
a `ui` wired to `processor`/`automation`/`saga`/`translation`, only to `command`. A `ui` left in a
|
|
647
|
+
reaction's slice renders with no outgoing edge, disconnected, and `em validate` warns on it — move
|
|
648
|
+
it to the slice that displays the read model, or drop it.
|
|
649
|
+
|
|
650
|
+
---
|
|
651
|
+
|
|
652
|
+
## `em validate` rules (design to satisfy these)
|
|
653
|
+
|
|
654
|
+
**Errors (must fix):**
|
|
655
|
+
1. **Band collision** — two elements of the same band in one slice (e.g. two `command`s, or two
|
|
656
|
+
`ui`s in the *same* persona row). Split them into separate slices/personas.
|
|
657
|
+
2. **Unknown event source** — `view X from "Event"` where the event doesn't exist anywhere.
|
|
658
|
+
3. **Unknown read-model source** — `processor X from "View"` where the view doesn't exist.
|
|
659
|
+
4. **Arrow endpoint mismatch** — `arrow A -> B` where A or B matches no element name.
|
|
660
|
+
5. **Backward timeline** ("time flows left to right") — an event feeding a view instance in an
|
|
661
|
+
EARLIER slice (fix: add a `view X again` instance where the event lands and move the source
|
|
662
|
+
there), a reaction reading a view before any instance of it exists, or an explicit backward
|
|
663
|
+
`arrow`.
|
|
664
|
+
6. **`again` without an earlier declaration** — declare the view plainly the first time it appears.
|
|
665
|
+
7. **Illegal connection** — an `arrow` joining kinds the patterns don't connect. Only
|
|
666
|
+
`ui -> command`, `command -> event`, `event -> view`, `view -> ui`, `view -> reaction`, and
|
|
667
|
+
`reaction -> command` are allowed. A command straight to a view is the CQRS violation (an event
|
|
668
|
+
has to sit between them); a view straight to a command needs a reaction between them; and two
|
|
669
|
+
instances of one view are never connected at all. Inferred edges are always legal, so this only
|
|
670
|
+
ever fires on a hand-written `arrow`.
|
|
671
|
+
8. **Cyclic type reference** — a declared `type` nesting itself with no array to terminate it
|
|
672
|
+
(`type Node { child: Node }`, or the same shape across several types). Break the cycle, or
|
|
673
|
+
route the self/mutual reference through an array (`children: Node[]`) if the data is
|
|
674
|
+
genuinely tree-shaped.
|
|
675
|
+
9. **Lineage-ref errors** (slice-doc frontmatter, fs-aware) — a
|
|
676
|
+
`split-from`/`merged-from`/`superseded-by` ref with malformed `<slice-key>@v<N>` grammar, a
|
|
677
|
+
self-reference or cycle, a `superseded-by` naming a slice absent from the current model, or
|
|
678
|
+
a ref naming a version higher than the target's own `version:`. A `split-from`/`merged-from`
|
|
679
|
+
naming a key absent from the tree is deliberately silent — that's the normal state after a
|
|
680
|
+
real split/merge (see the four `lineage-*` codes in the table below).
|
|
681
|
+
10. **`loops-to` not earlier, or unresolved** — an event's `loops-to "View"` naming a view at the
|
|
682
|
+
same slice or later (`loops-to-forward`; that's what `from` on a later `view … again`
|
|
683
|
+
instance is for), or naming a view that doesn't exist at all, or a name that resolves to a
|
|
684
|
+
non-view (`loops-to-unresolved`).
|
|
685
|
+
11. **`derived from` names an unresolved event** — a view field's traced `derived from "Event
|
|
686
|
+
A", "Event B"` clause naming an event that isn't among the view's actual sources (its own
|
|
687
|
+
`from` list, or the same-slice events for a `from`-less view) — `derived-from-unresolved`.
|
|
688
|
+
Name one of the view's real sources, or drop the traced event.
|
|
689
|
+
|
|
690
|
+
**Warnings (should fix):**
|
|
691
|
+
1. **Reaction with no command** — a `processor`/`automation`/`saga`/`translation` that triggers
|
|
692
|
+
nothing. It never records an event itself; add the command it issues in this same slice, or an
|
|
693
|
+
explicit `arrow` to one elsewhere.
|
|
694
|
+
2. **`ui` shares slice with a reaction** — a `ui` only ever wires to a `command` a person issues; no
|
|
695
|
+
pattern has a `ui` triggering an automation/processor/saga/translation. Left in the reaction's
|
|
696
|
+
own slice it renders disconnected, with no edge, whether or not that slice also has the
|
|
697
|
+
reaction's own command. Move it to the slice that displays the read model, or drop it.
|
|
698
|
+
3. **Command with no trigger** — nothing issues it. A command needs a `ui` in its slice, or the
|
|
699
|
+
reaction (automation/processor/saga/translation) that triggers it, *also* in this slice. The
|
|
700
|
+
input-side mirror of (5): a command nothing points at is a write nobody can start.
|
|
701
|
+
4. **Command without event** — every command should record at least one event.
|
|
702
|
+
5. **Event nobody reads** — the mirror of (4): recording an event no read model projects is a
|
|
703
|
+
write with no reader. Follow every write slice with the read slice that consumes its event.
|
|
704
|
+
Counts as read when a `view` names it in `from` (any `again` instance will do), when a view
|
|
705
|
+
with no `from` of its own sits in its slice, via an explicit `event -> view` arrow, or via a
|
|
706
|
+
resolved `loops-to "View"` clause (a loop-back is a real consumer too — an UNRESOLVED or
|
|
707
|
+
forward `loops-to` does not count, so the event still warns as unread alongside its own
|
|
708
|
+
`loops-to-forward`/`loops-to-unresolved` error). (Events marked `public` are exempt — their
|
|
709
|
+
reader is outside the model; same for a `public` view and the no-consumer warning.)
|
|
710
|
+
6. **Read model without source** — add `from "Event"` or place the view in a slice with an event.
|
|
711
|
+
7. **Duplicate name** — the same name defined N times; references resolve to the first. Rename.
|
|
712
|
+
(A duplicate `type` name always warns, unlike a duplicate element name — there's no
|
|
713
|
+
legitimate unreferenced-duplicate case for a named type.)
|
|
714
|
+
8. **Open issue** — an element carries `issue "text"`; resolve the question, then remove the
|
|
715
|
+
clause. `em validate --list-issues` prints just these; `--fail-on-issues` (opt-in) makes CI
|
|
716
|
+
fail while any remain open.
|
|
717
|
+
9. **View field with no source** — a `view` field whose name matches no field on any instance
|
|
718
|
+
of its source events. Only checked once BOTH the view and at least one source event declare
|
|
719
|
+
`{ fields }` — unless the field is marked `derived` (optionally `derived from "Event A",
|
|
720
|
+
"Event B"`, whose names must be among the view's sources), which exempts it entirely.
|
|
721
|
+
10. **Event field not from a command** — an `event` field whose name matches no field on any
|
|
722
|
+
command in the same slice. Only checked once BOTH the event and at least one same-slice
|
|
723
|
+
command declare `{ fields }`. This is the payoff of the fields feature for slicing rigor:
|
|
724
|
+
once fields are written down, `em validate` checks that data flows forward consistently.
|
|
725
|
+
11. **Event with no producing command** — a fact with no traceable cause; add the command that
|
|
726
|
+
records it (either order within the slice), or an explicit `arrow` from one.
|
|
727
|
+
12. **Two same-named `translation`s reading different producers** — rename one; fires
|
|
728
|
+
unconditionally (unlike the duplicate-name warning), since the confusion is in reading
|
|
729
|
+
the model, not resolving a reference.
|
|
730
|
+
13. **Frontmatter coherence** (slice docs, fs-aware) — `status: implemented` with no
|
|
731
|
+
`implementedIn` link at all. The re-ratification mismatch (status flipped back while
|
|
732
|
+
`implementedIn` names the prior PR) is deliberately never flagged — it's the drift
|
|
733
|
+
signal, not incoherence.
|
|
734
|
+
14. **Doc-join problems** (`em export`/`--slice-ready`) — `binding-missing-file` (a
|
|
735
|
+
`note "slices/<key>.md"` binding pointing at a file that doesn't exist) and
|
|
736
|
+
`frontmatter-invalid` (the doc exists but lacks a frontmatter block or a required key).
|
|
737
|
+
|
|
738
|
+
`divergence "text"` is deliberately NOT in this list — it raises no warning at all, since it
|
|
739
|
+
records a deviation already reasoned through and accepted, not something to fix. Use
|
|
740
|
+
`em validate --list-divergences` to audit them on demand.
|
|
741
|
+
|
|
742
|
+
### Full rule code reference
|
|
743
|
+
|
|
744
|
+
Every rule above (and every fs-aware check `em validate` layers on top — lineage, frontmatter
|
|
745
|
+
coherence) has a stable `code`, generated below from the same registry `em` itself validates
|
|
746
|
+
against (`src/model/rules.ts`) — a new rule shows up here the moment it's registered, whether or
|
|
747
|
+
not the prose above has caught up yet. `--slice-ready <key>`-only codes are excluded; see
|
|
748
|
+
`docs/cli.md#em-validate-file` in the
|
|
749
|
+
[em repository](https://github.com/milehimikey/em/blob/main/docs/cli.md#em-validate-file).
|
|
750
|
+
|
|
751
|
+
<!-- GENERATED:validate-rules:start -- run `npm run docs:generate` to refresh, do not hand-edit -->
|
|
752
|
+
| Code | Severity | Title | Fix |
|
|
753
|
+
|---|---|---|---|
|
|
754
|
+
| `arrow-backward` | error | Backward arrow | Restructure so the target comes later. |
|
|
755
|
+
| `arrow-unresolved-source` | error | Arrow source unresolved | Fix the arrow's source name. |
|
|
756
|
+
| `arrow-unresolved-target` | error | Arrow target unresolved | Fix the arrow's target name. |
|
|
757
|
+
| `binding-missing-file` | warning | Doc binding points at a missing file | Create the slice doc, or fix the `note` path. |
|
|
758
|
+
| `both-ends-of-a-flow/command-no-event` | warning | Command without event | Add the event this command records. |
|
|
759
|
+
| `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. |
|
|
760
|
+
| `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. |
|
|
761
|
+
| `both-ends-of-a-flow/event-unread` | warning | Event nobody reads | Project it into a view, or reconsider recording it. |
|
|
762
|
+
| `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. |
|
|
763
|
+
| `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. |
|
|
764
|
+
| `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. |
|
|
765
|
+
| `code-spans-seam` | warning | Code change spans a seam | Keep one code module per model and change the two sides of a seam in separate change sets. |
|
|
766
|
+
| `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. |
|
|
767
|
+
| `consumer-not-adapted` | error | Consuming translation no longer matches the producer's public element | Update the translation's field block to the producer's current fields (a renamed field names its new name in the message). Additive producer changes never raise this — consumers tolerate unknown fields. |
|
|
768
|
+
| `consumes-unknown-element` | error | `consumes` names no public element of that model | Point the ref at an element the producer marks `public` (`em export` lists them), or ask the producer to publish it. |
|
|
769
|
+
| `consumes-unknown-model` | error | `consumes` names a model the system does not have | Fix the model key (the kebab-slug of the producer's `model "Name"`), or add that model to the system. |
|
|
770
|
+
| `continuation-has-own-doc` | warning | Continuation slice has its own doc | Fold this doc's scenarios into slices/<originating-key>.md and delete it; a later `view X again` instance is documented by the view's originating slice. |
|
|
771
|
+
| `cross-model-slice-doc-collision` | warning | Colliding slice doc path across models | Give each model its own directory (see docs/cli.md, "Multi-model projects"). |
|
|
772
|
+
| `dangling-public-event` | warning | Public event/view no seam consumes | Declare the seam that reads it, or drop `public` if nothing outside the model does. |
|
|
773
|
+
| `derived-from-unresolved` | error | `derived from` names an unknown event | Name one of the view's actual sources, or drop the traced event from `derived from`. |
|
|
774
|
+
| `doc-model-element-not-in-doc` | warning | Model element the doc doesn't mention | Add the matching marker to the doc, or remove the element from the model. |
|
|
775
|
+
| `doc-model-element-not-in-model` | warning | Doc names an element the model doesn't have | Add the element to the model, or fix/remove it in the doc. |
|
|
776
|
+
| `doc-model-field-mismatch` | warning | Doc/model field mismatch | Reconcile the field table with the model's fields — names and types. |
|
|
777
|
+
| `doc-model-pattern-mismatch` | warning | Doc/model pattern mismatch | Fix the doc's `pattern:` frontmatter to match the model, or restructure the slice to match the doc. |
|
|
778
|
+
| `duplicate-element-ref` | warning | Duplicate element ref | Rename the element so its export ref is unique. |
|
|
779
|
+
| `duplicate-model-key` | warning | Duplicate model key | Give each model a unique `model "Name"` so its key (and every cross-model ref) is stable. |
|
|
780
|
+
| `duplicate-name` | warning | Duplicate name | Rename one of the duplicates. |
|
|
781
|
+
| `duplicate-slice-name` | warning | Duplicate slice name | Rename the slice so its export key is unique. |
|
|
782
|
+
| `duplicate-type-name` | warning | Duplicate type name | Rename one of the duplicate `type` declarations. |
|
|
783
|
+
| `duplicate-type-ref` | warning | Duplicate type ref | Rename the type so its export ref is unique. |
|
|
784
|
+
| `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. |
|
|
785
|
+
| `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. |
|
|
786
|
+
| `frontmatter-coherence-implemented-without-link` | warning | Implemented without a link | Add `implementedIn` once the slice ships. |
|
|
787
|
+
| `frontmatter-invalid` | warning | Invalid or missing frontmatter | Add the required frontmatter keys, or add a frontmatter block. |
|
|
788
|
+
| `grid-collision` | error | Band collision | Split the colliding elements into separate slices. |
|
|
789
|
+
| `invariants/declared-in-both` | warning | Invariant declared in the model and in a slice doc | Keep the model's `invariant` line; in the doc, cite the ID and elaborate instead of restating it as a `**INV-…:**` rule. |
|
|
790
|
+
| `invariants/doc-cites-undeclared` | warning | Slice doc cites an undeclared invariant | Declare the ID in the model (or in a slice doc's Invariants section), or fix the citation. |
|
|
791
|
+
| `invariants/duplicate-id` | error | Duplicate invariant ID | Give each model-declared invariant its own ID — a rule is declared once, on the element it guards. |
|
|
792
|
+
| `invariants/malformed-id` | error | Malformed invariant ID | Rename the ID to the `INV-<MNEMONIC>-<n>` shape, e.g. `INV-ORD-1` (2-4 uppercase letters/digits, then a number). |
|
|
793
|
+
| `invariants/public-command-without-invariants` | warning | Public command declares no invariants | Add an `invariant INV-<MNEMONIC>-<n> "rule"` line after the command, or declare its rules in the slice doc's Invariants section. |
|
|
794
|
+
| `lineage-forward-dangling` | error | Dangling forward lineage ref | Fix the key, or remove the stale successor. |
|
|
795
|
+
| `lineage-ref-cycle` | error | Lineage cycle | Break the cycle — a slice can't be its own ancestor. |
|
|
796
|
+
| `lineage-ref-malformed` | error | Malformed lineage ref | Fix the value to `<slice-key>@v<N>`, or remove it. |
|
|
797
|
+
| `lineage-version-impossible` | error | Impossible lineage version | Fix the referenced version, or ratify the target slice first. |
|
|
798
|
+
| `loops-to-forward` | error | `loops-to` target not earlier on the timeline | Point `loops-to` at an earlier view, or use `from` on a later `view … again` instance instead. |
|
|
799
|
+
| `loops-to-unresolved` | error | `loops-to` target unresolved | Name a view that exists, or declare it before this event. |
|
|
800
|
+
| `model-version-stale` | warning | Model version stale | Run `em model version bump` to record the current model content/slice-version vector. |
|
|
801
|
+
| `multi-model-change-set` | warning | Change set touches more than one model | Prefer one model per change set. Unrelated models in one change set are fine; models joined by a seam are not, once a contract changes. |
|
|
802
|
+
| `note-binding-dangling` | warning | Dangling cross-slice note | Create the doc at that path, or fix/remove the note. |
|
|
803
|
+
| `note-binding-extra` | warning | Extra doc-binding note, ignored | Remove the note, or point it at the slice's actual bound doc. |
|
|
804
|
+
| `note-binding-unratified` | warning | Unratified cross-slice note | Add `covers: <this-slice-key>` to that doc's frontmatter, or correct the note's path. |
|
|
805
|
+
| `note-binding-unusable` | warning | Cross-slice note to a doc with unusable frontmatter | Fix that doc's frontmatter, or fix/remove the note. |
|
|
806
|
+
| `open-issue` | warning | Open issue | Resolve the question, then remove the `issue` clause. |
|
|
807
|
+
| `orphaned-slice-doc` | warning | Orphaned slice doc | Rename it to a current slice's key, add `covers:` (plus a `note` binding) to attach it to a live slice, or delete it. |
|
|
808
|
+
| `owner-not-a-handle` | warning | Model owner is not a CODEOWNERS handle | Write the owner as a GitHub user (`@alice`), a team (`@org/team`), or an email address, so `em system codeowners` can route review to it. |
|
|
809
|
+
| `public-field-type-unresolved` | error | Public field type unresolved | Give the field a type from the public type table (string, text, int, long, decimal, boolean, uuid, date, datetime, duration, bytes), `X[]` of one, or a declared `type` — or drop `public`. |
|
|
810
|
+
| `public-name-not-unique` | error | Two public elements of one kind share a name | Rename one of them (or drop `public` from one): a consumer's `consumes <model>:<kind>.<slug>` must name exactly one element. |
|
|
811
|
+
| `reaction-from-future-view` | error | Backward timeline (reaction reads a future view) | Declare the view in or before the reaction's slice. |
|
|
812
|
+
| `reaction-from-unresolved` | error | Unknown read-model source | Project the event into a view first, or fix the `from` reference. |
|
|
813
|
+
| `seam-consumer-not-reaction` | error | Seam consumer is not a reaction | Point `to` at a translation/automation element (or a slice containing exactly one). |
|
|
814
|
+
| `seam-crossing` | error | Change set alters a contract and its consumer together | Split the change set: land the producer's public-surface change first, then adapt the consuming model in a later change. Review on the contract file is the only override. |
|
|
815
|
+
| `seam-crossing-greenfield` | warning | New public surface lands with its first consumer | Nothing to fix: a public element no consumer was bound to at the base revision is new surface, not a crossing. Later changes to it are crossings. |
|
|
816
|
+
| `seam-duplicate` | warning | Duplicate seam | Remove the repeated `from`/`to` pair. |
|
|
817
|
+
| `seam-endpoint-unresolved` | error | Seam endpoint does not resolve | Fix the ref to an element the named model actually exports (`em export` lists every ref), or re-declare the seam after a rename. |
|
|
818
|
+
| `seam-source-not-public` | error | Seam source is not `public` | Mark the event/view `public` in its model, or point the seam at the element that is. |
|
|
819
|
+
| `slice-doc/structured-section-malformed` | warning | Malformed structured section | Balance the `<!-- GENERATED:em-slice-…:start -->`/`:end -->` markers (re-create the doc with `em slice new --force` if they are lost), restore the template's table header and run `em slice sync`, and give every `### Scenario:` block a Given, When and Then bullet. |
|
|
820
|
+
| `system-manifest-invalid` | error | Seam manifest invalid | Fix the manifest: `systemSchemaVersion: "2.0"`, `models:` with a readable `source` per model, and nothing else (`seams:`/`owner:` moved into the models — run `em upgrade`). |
|
|
821
|
+
| `system-manifest-outdated` | warning | Seam manifest is schema 1.0 | Run `em upgrade <model>.em --apply` once: it moves `seams:` into `consumes` clauses and `owner:` onto each model header, and rewrites the manifest to 2.0. |
|
|
822
|
+
| `system-model-key-mismatch` | error | Manifest model key differs from the export's `model.key` | Rename the manifest's `models:` key to the computed key the message prints. |
|
|
823
|
+
| `tag-composite-unknown-field` | error | Composite tag names an unknown field | Fix the field name, or add it to the event's fields. |
|
|
824
|
+
| `tag-duplicate-key` | error | Duplicate tag key | Rename one of the tags so every key on the event is unique. |
|
|
825
|
+
| `translation-name-collision` | warning | Translation name reused for different producers | Use a distinct name per producer to avoid confusion. |
|
|
826
|
+
| `type-cycle` | error | Cyclic type reference | Break the cycle, or route the self/mutual reference through an array. |
|
|
827
|
+
| `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. |
|
|
828
|
+
| `unbound-translation` | warning | Externally-fed reaction no seam feeds | Declare the seam whose `to` is this reaction, or give it an in-model `from`. |
|
|
829
|
+
| `undeclared-seam-candidate` | warning | Looks connected across models, no seam declared | Declare the seam, or rename one side so the name match stops looking like a link. |
|
|
830
|
+
| `view-again-without-earlier` | error | `again` without an earlier declaration | Declare the view plainly the first time it appears. |
|
|
831
|
+
| `view-from-future-event` | error | Backward timeline (view reads a future event) | Move the source to a later `view X again` instance. |
|
|
832
|
+
| `view-from-unresolved` | error | Unknown event source | Fix the `from` reference to name an existing event. |
|
|
833
|
+
| `view-no-source` | warning | Read model without source | Add `from "Event"`, or place the view in a slice with an event. |
|
|
834
|
+
<!-- GENERATED:validate-rules:end -->
|
|
835
|
+
|
|
836
|
+
**Design rules that keep models valid:**
|
|
837
|
+
- One element per band per slice (multiple personas/contexts are fine — they're different rows).
|
|
838
|
+
- Every `command` slice includes its `event`. Every `view` has a `from` source.
|
|
839
|
+
- **Every `command` has a trigger.** A `ui` in its slice, or the reaction that triggers it, also in
|
|
840
|
+
its slice. A command nothing points at is a write nobody can start.
|
|
841
|
+
- **Every `event` has a reader.** A command slice isn't finished until the read slice that projects
|
|
842
|
+
its event exists. Reactions don't count — they read views, not events. Pair each write slice with
|
|
843
|
+
its read slice as you go rather than sweeping up dangling events at the end.
|
|
844
|
+
- **Every `view` has a consumer.** A `ui` in its slice, a reaction watching it, or (headless) a read
|
|
845
|
+
translation. Every *instance* of a repeat, not just the last — a bare `view X again` slice is a
|
|
846
|
+
half-slice, not a State View.
|
|
847
|
+
- **Only six connections are legal**, and only these are ever inferred:
|
|
848
|
+
`ui → command`, `command → event`, `event → view`, `view → ui`, `view → reaction`,
|
|
849
|
+
`reaction → command`. Anything else in an explicit `arrow` is an error — above all
|
|
850
|
+
`command → view` (the CQRS violation: an event has to sit between them) and `view → command`
|
|
851
|
+
(a reaction has to sit between them). If you reach for an arrow the patterns don't allow, the
|
|
852
|
+
model is missing an element, not an arrow.
|
|
853
|
+
- A reaction (`processor`/`automation`/`saga`/`translation`) shares its slice with the `command` it
|
|
854
|
+
triggers and that command's `event` — the same shape `ui` already uses in State Change. If it's
|
|
855
|
+
internally triggered, the **view** (read model) it watches lives in the slice before, named via
|
|
856
|
+
`from "X"` (which must resolve to a view, never an event directly). Externally-triggered reactions
|
|
857
|
+
start from outside the model — no `from` at all, and no slice before them.
|
|
858
|
+
- Name everything uniquely and consistently.
|
|
859
|
+
- Events are past tense; commands are imperative; views name the thing shown.
|
|
860
|
+
|
|
861
|
+
A reaction that emits an event with no command anywhere between them — `translation T` and
|
|
862
|
+
`event E` in the same slice, nothing issuing it — is exactly what
|
|
863
|
+
`both-ends-of-a-flow/reaction-no-command` (above) catches: any reaction with no `command` in its
|
|
864
|
+
own slice, and no explicit arrow to one, warns. Still route every reaction through a real command
|
|
865
|
+
by construction (`reaction → command → event`) rather than an explicit `arrow "Reaction" -> "Event"`
|
|
866
|
+
— that shape is its own `connection-legality` error (a reaction never records an event itself).
|