@milehimikey/em 1.17.0 → 1.18.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.
Files changed (183) hide show
  1. package/.claude/skills/event-modeling/SKILL.md +1 -1
  2. package/.claude/skills/event-modeling-conform/SKILL.md +1 -1
  3. package/.claude/skills/event-modeling-design/SKILL.md +46 -5
  4. package/.claude/skills/event-modeling-discover/SKILL.md +43 -8
  5. package/.claude/skills/event-modeling-discover/reference/extract.md +6 -1
  6. package/.claude/skills/event-modeling-discover/reference/prd-rubric.md +102 -0
  7. package/.claude/skills/event-modeling-discover/reference/prd-spec-path.md +171 -0
  8. package/.claude/skills/event-modeling-engagement/SKILL.md +8 -3
  9. package/.claude/skills/event-modeling-implement/SKILL.md +1 -1
  10. package/.claude/skills/event-modeling-implement/reference/implement.md +8 -0
  11. package/.claude/skills/event-modeling-review/SKILL.md +1 -1
  12. package/.claude/skills/event-modeling-shared/reference/em-dsl.md +90 -5
  13. package/.claude/skills/event-modeling-shared/reference/methodology.md +268 -0
  14. package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +96 -2
  15. package/.claude/skills/event-modeling-shared/templates/slice.md +11 -3
  16. package/.claude-plugin/marketplace.json +2 -2
  17. package/dist/catalog/docModelConsistencyValidate.js +160 -0
  18. package/dist/catalog/docModelConsistencyValidate.js.map +1 -1
  19. package/dist/catalog/invariantsValidate.d.ts +2 -0
  20. package/dist/catalog/invariantsValidate.js +67 -1
  21. package/dist/catalog/invariantsValidate.js.map +1 -1
  22. package/dist/catalog/scenarioQualityValidate.d.ts +4 -0
  23. package/dist/catalog/scenarioQualityValidate.js +148 -0
  24. package/dist/catalog/scenarioQualityValidate.js.map +1 -0
  25. package/dist/catalog/scenarios.d.ts +5 -0
  26. package/dist/catalog/scenarios.js +80 -0
  27. package/dist/catalog/scenarios.js.map +1 -1
  28. package/dist/catalog/sliceReadyValidate.d.ts +8 -0
  29. package/dist/catalog/sliceReadyValidate.js +35 -3
  30. package/dist/catalog/sliceReadyValidate.js.map +1 -1
  31. package/dist/catalog/sliceSections.d.ts +3 -2
  32. package/dist/catalog/sliceSections.js +99 -9
  33. package/dist/catalog/sliceSections.js.map +1 -1
  34. package/dist/cli/ciInit.d.ts +4 -11
  35. package/dist/cli/ciInit.js +24 -6
  36. package/dist/cli/ciInit.js.map +1 -1
  37. package/dist/cli/coverage.d.ts +9 -0
  38. package/dist/cli/coverage.js +18 -0
  39. package/dist/cli/coverage.js.map +1 -1
  40. package/dist/cli/defer.d.ts +28 -0
  41. package/dist/cli/defer.js +87 -4
  42. package/dist/cli/defer.js.map +1 -1
  43. package/dist/cli/engagement.d.ts +2 -2
  44. package/dist/cli/engagement.js +24 -1
  45. package/dist/cli/engagement.js.map +1 -1
  46. package/dist/cli/markImplemented.d.ts +14 -10
  47. package/dist/cli/markImplemented.js +49 -1
  48. package/dist/cli/markImplemented.js.map +1 -1
  49. package/dist/cli/prdTriage.d.ts +109 -0
  50. package/dist/cli/prdTriage.js +378 -0
  51. package/dist/cli/prdTriage.js.map +1 -0
  52. package/dist/cli/render-inputs.d.ts +2 -0
  53. package/dist/cli/render-inputs.js +4 -0
  54. package/dist/cli/render-inputs.js.map +1 -1
  55. package/dist/cli/revise.d.ts +2 -0
  56. package/dist/cli/revise.js +7 -2
  57. package/dist/cli/revise.js.map +1 -1
  58. package/dist/cli/shape.d.ts +27 -0
  59. package/dist/cli/shape.js +77 -0
  60. package/dist/cli/shape.js.map +1 -0
  61. package/dist/cli/sliceIndex.d.ts +6 -1
  62. package/dist/cli/sliceIndex.js +28 -13
  63. package/dist/cli/sliceIndex.js.map +1 -1
  64. package/dist/cli/sliceNew.d.ts +4 -1
  65. package/dist/cli/sliceNew.js +26 -9
  66. package/dist/cli/sliceNew.js.map +1 -1
  67. package/dist/cli/status.d.ts +21 -1
  68. package/dist/cli/status.js +25 -3
  69. package/dist/cli/status.js.map +1 -1
  70. package/dist/cli/storylineSuggest.d.ts +26 -0
  71. package/dist/cli/storylineSuggest.js +252 -0
  72. package/dist/cli/storylineSuggest.js.map +1 -0
  73. package/dist/cli/upgrade.d.ts +15 -3
  74. package/dist/cli/upgrade.js +426 -7
  75. package/dist/cli/upgrade.js.map +1 -1
  76. package/dist/cli/upgradeReshape.d.ts +60 -0
  77. package/dist/cli/upgradeReshape.js +425 -0
  78. package/dist/cli/upgradeReshape.js.map +1 -0
  79. package/dist/cli.js +286 -29
  80. package/dist/cli.js.map +1 -1
  81. package/dist/emit/dot.d.ts +2 -0
  82. package/dist/emit/dot.js +44 -8
  83. package/dist/emit/dot.js.map +1 -1
  84. package/dist/emit/engagementJson.d.ts +1 -1
  85. package/dist/emit/engagementJson.js +3 -1
  86. package/dist/emit/engagementJson.js.map +1 -1
  87. package/dist/emit/json.d.ts +21 -1
  88. package/dist/emit/json.js +26 -2
  89. package/dist/emit/json.js.map +1 -1
  90. package/dist/emit/queryJson.d.ts +1 -1
  91. package/dist/emit/queryJson.js +3 -1
  92. package/dist/emit/queryJson.js.map +1 -1
  93. package/dist/emit/statusJson.d.ts +1 -1
  94. package/dist/emit/statusJson.js +6 -1
  95. package/dist/emit/statusJson.js.map +1 -1
  96. package/dist/emit/theme.d.ts +1 -0
  97. package/dist/emit/theme.js +13 -0
  98. package/dist/emit/theme.js.map +1 -1
  99. package/dist/emit/validateJson.d.ts +5 -2
  100. package/dist/emit/validateJson.js +36 -2
  101. package/dist/emit/validateJson.js.map +1 -1
  102. package/dist/layout/grid.d.ts +5 -1
  103. package/dist/layout/grid.js +93 -2
  104. package/dist/layout/grid.js.map +1 -1
  105. package/dist/mcp/server.js +46 -11
  106. package/dist/mcp/server.js.map +1 -1
  107. package/dist/model/edges.d.ts +7 -0
  108. package/dist/model/edges.js +33 -16
  109. package/dist/model/edges.js.map +1 -1
  110. package/dist/model/model.d.ts +33 -0
  111. package/dist/model/model.js +87 -3
  112. package/dist/model/model.js.map +1 -1
  113. package/dist/model/queryIndex.js +2 -2
  114. package/dist/model/queryIndex.js.map +1 -1
  115. package/dist/model/refs.js +8 -5
  116. package/dist/model/refs.js.map +1 -1
  117. package/dist/model/rules.d.ts +149 -0
  118. package/dist/model/rules.js +144 -0
  119. package/dist/model/rules.js.map +1 -1
  120. package/dist/model/shapeLint.d.ts +12 -0
  121. package/dist/model/shapeLint.js +170 -0
  122. package/dist/model/shapeLint.js.map +1 -0
  123. package/dist/model/storylines.d.ts +51 -0
  124. package/dist/model/storylines.js +188 -0
  125. package/dist/model/storylines.js.map +1 -0
  126. package/dist/model/validate.d.ts +6 -1
  127. package/dist/model/validate.js +317 -14
  128. package/dist/model/validate.js.map +1 -1
  129. package/dist/parser/againSugar.d.ts +30 -0
  130. package/dist/parser/againSugar.js +105 -0
  131. package/dist/parser/againSugar.js.map +1 -0
  132. package/dist/parser/ast.d.ts +33 -0
  133. package/dist/parser/lexer.d.ts +2 -0
  134. package/dist/parser/lexer.js +4 -0
  135. package/dist/parser/lexer.js.map +1 -1
  136. package/dist/parser/parser.js +284 -4
  137. package/dist/parser/parser.js.map +1 -1
  138. package/dist/query/format.js +1 -0
  139. package/dist/query/format.js.map +1 -1
  140. package/dist/query/verbs.d.ts +4 -0
  141. package/dist/query/verbs.js +30 -3
  142. package/dist/query/verbs.js.map +1 -1
  143. package/dist/render/drawEdges.js +100 -4
  144. package/dist/render/drawEdges.js.map +1 -1
  145. package/dist/render/render.js +20 -2
  146. package/dist/render/render.js.map +1 -1
  147. package/dist/render/sliceDiagram.js +5 -5
  148. package/dist/render/sliceDiagram.js.map +1 -1
  149. package/dist/render/storylineBands.d.ts +11 -0
  150. package/dist/render/storylineBands.js +80 -0
  151. package/dist/render/storylineBands.js.map +1 -0
  152. package/dist/render/storylineDiagram.d.ts +14 -0
  153. package/dist/render/storylineDiagram.js +142 -0
  154. package/dist/render/storylineDiagram.js.map +1 -0
  155. package/dist/roster/check.d.ts +32 -0
  156. package/dist/roster/check.js +92 -0
  157. package/dist/roster/check.js.map +1 -0
  158. package/dist/roster/context.d.ts +3 -0
  159. package/dist/roster/context.js +23 -0
  160. package/dist/roster/context.js.map +1 -0
  161. package/dist/roster/init.d.ts +15 -0
  162. package/dist/roster/init.js +52 -0
  163. package/dist/roster/init.js.map +1 -0
  164. package/dist/roster/roster.d.ts +79 -0
  165. package/dist/roster/roster.js +298 -0
  166. package/dist/roster/roster.js.map +1 -0
  167. package/package.json +1 -1
  168. package/plugin/.claude-plugin/plugin.json +2 -2
  169. package/plugin/shared/reference/em-dsl.md +90 -5
  170. package/plugin/shared/reference/methodology.md +268 -0
  171. package/plugin/shared/reference/slice-doc-schema.md +96 -2
  172. package/plugin/shared/templates/slice.md +11 -3
  173. package/plugin/skills/conform/SKILL.md +1 -1
  174. package/plugin/skills/design/SKILL.md +46 -5
  175. package/plugin/skills/discover/SKILL.md +43 -8
  176. package/plugin/skills/discover/reference/extract.md +6 -1
  177. package/plugin/skills/discover/reference/prd-rubric.md +103 -0
  178. package/plugin/skills/discover/reference/prd-spec-path.md +172 -0
  179. package/plugin/skills/engagement/SKILL.md +8 -3
  180. package/plugin/skills/event-modeling/SKILL.md +1 -1
  181. package/plugin/skills/implement/SKILL.md +1 -1
  182. package/plugin/skills/implement/reference/implement.md +8 -0
  183. package/plugin/skills/review/SKILL.md +1 -1
@@ -22,6 +22,7 @@ em render <file> # transpile
22
22
  em render <file> -o, --out <path> # output path (extension picks the format)
23
23
  em render <file> -T, --format <fmt> # output format (svg, png, pdf, ...)
24
24
  em render <file> --slice <name> # render only this slice, redrawn in its own canonical pattern shape (default out: slices/<kebab-slug>.svg)
25
+ em render <file> --storyline <name> # render only this storyline: its slices in order, preceded by dimmed stub columns for what they read from outside it (and the end state of a storyline it comes `after`) (default out: storylines/<kebab-slug>.svg)
25
26
  em render <file> --emit-dot # print the generated DOT instead of rendering
26
27
  em render <file> --keep-empty-lanes # keep the API lane even when empty
27
28
  em export <file> # export a versioned JSON snapshot of the normalized model
@@ -68,13 +69,16 @@ em slice index <file> # rewrite th
68
69
  em slice index <file> --check # verify the table is current; exit non-zero on drift without writing (CI)
69
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). Applies only to a `ready-to-implement` doc (MIL-277: a `draft`/`reviewed` doc refuses — ratify first). Also records the shipped version beside the link (MIL-284): `shippedVersion:`, `shippedRef:` (the doc's `ratifiedRef:`, when it has one) and `shippedOn:`, and opens the next version as a `draft` when an Open Question was deferred to it (MIL-283). Idempotent on the same URL — also after that draft opened; refuses to overwrite a different one; never touches the doc body except to re-open those deferred questions
70
71
  em slice mark-implemented <file> <slice-key> <pr-url> --on <date> # merge date recorded as `shippedOn:`, YYYY-MM-DD (default: today, local date; MIL-284)
72
+ em slice mark-implemented <file> <slice-key> <pr-url> --json # print a JSON document instead of text: { path, changed, implementedIn, nextDraft: { version, reopened[] } | null } (GH #285)
71
73
  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
74
  em slice review <file> <slice-key> --by <name> # the reviewer's (or facilitator's) name
73
75
  em slice review <file> <slice-key> --on <date> # review date, YYYY-MM-DD (default: today, local date)
76
+ em slice review <file> <slice-key> --roster <path> # ratifier roster to check --by against (GH #259; default: the nearest ratifiers.yaml above the model, none = unrestricted)
74
77
  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` passes; a `ready-to-implement` doc only when the same signer is completing their own record (since 1.15.0, MIL-283 — a new sign-off is a new version: `em slice revise`); 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, and (MIL-284) a same-pair re-run on a doc whose body moved after sign-off; never touches `version:` or the doc body. Inside a git work tree also records `ratifiedRef:` (HEAD, refusing a doc with uncommitted changes) and always `ratifiedHash:`; with --no-ref (GH #255) only the hash — for a repo whose squash/rebase merges would leave the ref unreachable — and git is not consulted
75
78
  em slice ratify <file> <slice-key> --by <name> # the ratifier's name
76
79
  em slice ratify <file> <slice-key> --no-ref # record ratifiedHash: only, no ratifiedRef: (GH #255) — the signed content keeps its identity, nothing points at a commit a squash or rebase merge would orphan; the uncommitted-changes check does not apply
77
80
  em slice ratify <file> <slice-key> --on <date> # ratification date, YYYY-MM-DD (default: today, local date)
81
+ em slice ratify <file> <slice-key> --roster <path> # ratifier roster to check --by against (GH #259; default: the nearest ratifiers.yaml above the model, none = unrestricted)
78
82
  em slice ratify <file> <slice-key> --skip-review # ratify without a recorded review — prints a loud notice on stderr and records `reviewSkipped: <status>` on the doc (GH #260) so the bypass is visible to later readers, `em export` and `--slice-ready --json`; cleared by `em slice revise`
79
83
  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
80
84
  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>"`
@@ -128,6 +132,10 @@ em conform-supersede <file> <report-path> --locus <locus> # who/what t
128
132
  em conform-supersede <file> <report-path> --by <name> # who ruled on the named finding(s) (requires --locus)
129
133
  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.
130
134
  em conform-findings check <path> --json # print a JSON document instead of the text report
135
+ em discover triage-check <file> # validate a discovery/prd-triage.json (prdTriageSchemaVersion 1.0): shape, the shipped rubric version, every statement/storyline quote appearing verbatim in the input document, and every statement carrying a category, an outcome and (modeled/rewritten) a group + storyline — exit 1 on errors. The discover skill writes this file directly; this is how a headless run verifies it.
136
+ em discover triage-check <file> --source <path> # the input PRD/spec to check quotes against (default: the file's own `source`, relative to it)
137
+ em discover triage-check <file> --markdown # after a passing check, print the Markdown view of the record instead of the summary
138
+ em discover triage-check <file> --json # print a JSON document instead of the text report
131
139
  em watch <file> # re-render on every save
132
140
  em watch <file> -o, --out <path> # output path (extension picks the format)
133
141
  em watch <file> -T, --format <fmt> # output format (svg, png, pdf, ...)
@@ -141,7 +149,11 @@ em validate <file> --list-public # print only
141
149
  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)
142
150
  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)
143
151
  em validate <file> --require-ratified-hash # with --slice-ready (GH #256): strict mode — a ready-to-implement doc with no `ratifiedHash:` is NOT ready (`slice-ready-missing-ratified-hash`, error; `ready: false` in --json). Off by default so docs ratified before em 1.15 keep passing; turn it on once legacy docs are re-ratified
152
+ em validate <file> --strict # promote advisory lints (GH #293: scenario-quality warnings) to errors: they fail the run and, with --slice-ready, block readiness
153
+ em validate <file> --roster <path> # with --slice-ready (GH #259): the ratifier roster to check `ratifiedBy` against (default: the nearest ratifiers.yaml above the model; none = unrestricted). A name the roster does not permit for the slice's context is `ratified-by-not-in-roster` (error)
144
154
  em validate <file> --check-signed-bodies # opt-in (GH #257): compare every slice doc that records a `ratifiedHash:` — ready, implemented, any status — to its body; a mismatch is `slice-body-changed-since-ratification` (error). A pre-1.17 whole-body hash with an intact body passes; a deferral written by `em slice defer` is outside the hash. The one check that still watches a shipped doc's signed content
155
+ em validate <file> --shape # GH #282: run only the advisory shape-family lints (storyline shape) and print rule / count / first ref / docs anchor; exit 0 always. With --json: `shapeSchemaVersion` 1.0 with per-rule finding arrays
156
+ em validate <file> --waive-file <path> # with --shape: a baseline file (`<rule-code> <slice-key>` per line, or a bare slice key for every shape rule; `#` comments) whose shape findings are listed as waived and not counted; missing file = no waivers
145
157
  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
146
158
  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)
147
159
  em migrate <file> --write # apply the rewrite to the file (default: dry run — report only, write nothing)
@@ -195,6 +207,7 @@ em query slices <files> --status <s> # the slice'
195
207
  em query slices <files> --context <c> # match a slice with an event in this @Context
196
208
  em query slices <files> --persona <p> # match a slice with a ui in this @Persona
197
209
  em query slices <files> --tag <t> # match a slice with an event carrying this tag key
210
+ em query slices <files> --storyline <name> # match a slice declared in this `storyline` block
198
211
  em query slices <files> --json # print a JSON document instead of the text report
199
212
  em query invariant <files> # declaring slice + doc facts for one INV-* id, and (with --tests) its test citations
200
213
  em query invariant <files> --id <inv-id> # the INV-* id to look up
@@ -229,6 +242,12 @@ em engagement set <file> <slug> <key> --pr <url> # the slice'
229
242
  em engagement status <file> <slug> # the Ledger joined with each slice doc's current status, plus whether the engagement is closable
230
243
  em engagement status <file> <slug> --json # print a JSON document instead of text (same document as the MCP engagement_status tool)
231
244
  em engagement close <file> <slug> # set status: closed — refuses unless every slice is merged (recorded or inferred) or gap
245
+ em roster init <file> # write ratifiers.yaml beside the model, seeded from the names already recorded in the slice docs' `ratifiedBy:`/`reviewedBy:` (whole-model entries; narrow them with `contexts:` by hand). Refuses to overwrite an existing file unless --force; `em upgrade`'s grandfathered placeholder is not a name
246
+ em roster init <file> --roster <path> # write the roster here instead of <model dir>/ratifiers.yaml
247
+ em roster init <file> --force # overwrite an existing roster file
248
+ em roster check <file> # check EVERY slice doc's recorded signers against the ratifier roster (GH #259) - `ratifiedBy` on ready-to-implement and implemented docs, `reviewedBy` on any doc - the whole-model CI gate that `--slice-ready <key>` (one ready slice) is not. Exit 1 on any violation or an invalid roster; no roster file = nothing to check, exit 0. The `grandfathered ...` placeholder is exempt (counted)
249
+ em roster check <file> --roster <path> # roster to check against (default: the nearest ratifiers.yaml above the model)
250
+ em roster check <file> --json # print a JSON document (per-doc results, diagnostics with refs); exit codes unchanged
232
251
  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
233
252
  em system [target] --json # print a JSON document instead of the text report (see docs/cli.md)
234
253
  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)
@@ -253,10 +272,13 @@ em ci init <model> # install .g
253
272
  em ci init <model> --tests <dir> # test directory the coverage/status steps scan for INV-* citations
254
273
  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
255
274
  em ci init <model> --check # verify both files match the current preset; exit non-zero on drift without writing (CI)
275
+ em storyline suggest <file> # advisory (GH #290): propose `storyline` blocks for a model that has none — one per distinct slice `source "url"`, the remaining slices grouped by the view-instance chains (`again`) they feed — printed as a unified diff on stdout (apply with `patch -p1` or `git apply`); never writes the model
256
276
  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
257
277
  em upgrade <file> --apply # apply every applicable mechanical step, one git commit each, then a final `Em version:` commit
258
278
  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
259
279
  em upgrade <file> --json # print a JSON document instead of text (dry-run/--check only, never with --apply)
280
+ em upgrade <file> --fold-continuations # also run the opt-in fold-continuations step (stack consecutive continuations of one view into the first; continuationOf, doc join and index rows unchanged)
281
+ em upgrade <file> --to-sugar # (implies --fold-continuations; without --apply it is a dry run listing exactly what would fold): fold continuations into their host command slice as `view X again … shown on` lines — CHANGES continuationOf and the doc join to `<host>--again` and removes the folded slices' index rows
260
282
  ```
261
283
  <!-- GENERATED:cli:end -->
262
284
 
@@ -273,6 +295,10 @@ model "Name" [owner "Team"] # diagram title; optional owning team(s), quote
273
295
  persona Name # a UI swimlane row (actor)
274
296
  context Name # an event swimlane row (bounded context / aggregate)
275
297
 
298
+ storyline "Name" [source "url"] [after "Other"] { # optional chapter grouping slices (see Storylines)
299
+ slice "Name" { ... }
300
+ }
301
+
276
302
  slice "Name" [source "url"] { # one vertical time step (a column); source is optional
277
303
  ui Free Text @Persona # screen; @Persona picks its row (defaults to first/"User")
278
304
  command Free Text # state-changing request (API band)
@@ -295,7 +321,7 @@ type Name { field: Type, ... } # named structured type, reusable from any f
295
321
  | `ui` | persona | screen / interface | `@Persona` | `note`, `issue`, `divergence`, `{ fields }` |
296
322
  | `command` | API | state-changing request | — | `note`, `issue`, `divergence`, `renamed from`, `{ fields }`, standalone `invariant` line |
297
323
  | `view` | API | read model / projection | — | `from "Event"…`, `note`, `issue`, `divergence`, `public`, `{ fields }` |
298
- | `event` | context | recorded fact (past tense) | `@Context` | `note`, `issue`, `divergence`, `public`, `tag`, `renamed from`, `loops-to`, field-level `assigned`, `{ fields }`, standalone `invariant` line |
324
+ | `event` | context | recorded fact (past tense); `event A \| B @Context` = alternative outcomes of ONE command, one slice, one shared cell | `@Context` | `note`, `issue`, `divergence`, `public`, `tag`, `renamed from`, `loops-to`, field-level `assigned`, `{ fields }`, standalone `invariant` line |
299
325
  | `processor` / `automation` / `saga` / `translation` | automation | system reaction / adapter | — | `from "…"`, `note`, `issue`, `divergence`, `{ fields }`; `translation` only: `consumes` |
300
326
 
301
327
  ### Clauses
@@ -311,6 +337,15 @@ type Name { field: Type, ... } # named structured type, reusable from any f
311
337
  "to-do list") and pointing the reaction at that view. Name that view after the pending work
312
338
  (`Payments To Process`), never after the triggering event — a view reusing the event's name
313
339
  collides in the shared namespace and draws a duplicate-name warning.
340
+ - **One event recorded by several commands** (two intents, one fact — `Create Product` and
341
+ `Create Product Variant` both ending in `Product Created`): repeat the event BARE in the later
342
+ slice with the same name and context. A bare repeat of an event with the same name and context is
343
+ ONE logical event — no `duplicate-name`, no marker (`again` stays view-only). The repeat inherits
344
+ the first declaration's fields, element-level `tag` clauses and `public`; declaring different ones
345
+ is the error `shared-event-field-mismatch`. A view's `from "E"` draws an arrow from every recording
346
+ site at or before it, and EVERY site needs a reader at or after it (add `view V again from "E"`
347
+ after the last site, or `event-unread` warns for that site). The same name in a different context
348
+ is still a `duplicate-name` warning.
314
349
  - **`again`** (views only): `view <Name> again [from "Event", …]` declares a later instance of an
315
350
  already-declared read model — the forward-only device for a view that evolves as later events
316
351
  land. Instances are ONE logical view: the first declaration owns the `note` binding; each
@@ -442,7 +477,11 @@ type Name { field: Type, ... } # named structured type, reusable from any f
442
477
  into doc-only prose** — the marker keeps `em export`/the model aware the field exists (the
443
478
  `doc-model-field-mismatch` doc↔model check does not compare view field tables at all today —
444
479
  see the slice template's field-table guidance — so this is an export-side win specifically,
445
- not yet a doc-drift check).
480
+ not yet a doc-drift check). **Collection fields (GH #288):** a view field typed `T[]` for a
481
+ declared `type T`, with `derived from "Child Created", "Child Deleted"`, embeds a child entity:
482
+ each field of `T` must trace to a named event (`fields-completeness/view-field-no-source`
483
+ otherwise). No add/remove/key clauses — `em slice sync` writes a `Collection of` note plus
484
+ one Given/Then scenario stub per named event in the view region (copy into `## Scenarios`), and `em export` sets `collection: true`.
446
485
  - **Fields:** `command Place Order { orderId: UUID, items: LineItem[], customerId }` — inline or
447
486
  one-per-line. Types are free text (no semantic checking) UNLESS the type string names a
448
487
  declared `type` (see Named types below), in which case it resolves to a structured
@@ -471,6 +510,22 @@ type Name { field: Type, ... } # named structured type, reusable from any f
471
510
  of relying on prose. Purely metadata — no visual marker, not validated. Don't confuse it with
472
511
  an element's `note "path.md"` (a markdown file link, not a URL).
473
512
 
513
+ - **`storyline "Name" [source "url"] [after "Other"] { slice … }`** (GH #277) groups slices into a
514
+ named chapter — the main journey, and secondary stories (retire a product, extend an attribute).
515
+ Slices keep their file-order timeline position; a slice outside any storyline is allowed;
516
+ storylines don't nest and a repeated name continues that storyline (its slices join it; a continuation may not add or change
517
+ `source`/`after` — parse error). The full render draws a
518
+ labelled band above the slice headers; `em render --storyline "Name"` draws one story alone, with the
519
+ elements it reads from outside as dimmed stub columns. `em export` carries `slice.storyline` and
520
+ `model.storylines[]`; `em query slices --storyline`; `em slice index` adds a Storyline column. Once
521
+ any storyline exists a slice outside every one is the advisory warning `slice-outside-storyline`.
522
+ `after "<other>"` reads the storyline as if its slices followed the referenced storyline's last
523
+ slice: `from`/`again`/reaction `from` resolve against the composed timeline (referenced → this), the
524
+ forward-only checks use it, a backward feed into the referenced storyline, an `after` cycle
525
+ (`storyline-after-cycle`), an unknown target (`storyline-after-unknown`) or a storyline declared above
526
+ the one it comes after when a flow would draw right-to-left (`storyline-after-out-of-order`) is an
527
+ error. Without `after`, file order is the only timeline.
528
+
474
529
  ### Named types
475
530
 
476
531
  `type Name { field: Type, ... }` declares a reusable structured shape at the top level (any
@@ -633,8 +688,9 @@ slice "Read Quote — created" {
633
688
  **prefer `view X again`** for every instance after the first (see Clauses) — **each instance
634
689
  carries its own consumer** (the `ui` that reads it, or a reaction), not just the last one. `again`
635
690
  instances are exempt from the duplicate-name warning even when referenced, and each reference
636
- resolves to the right instance — a plain repeat only stays warning-free while nothing references it by name, and
637
- resolves to the *first* declaration when something does. **Wire each event to a read model exactly
691
+ resolves to the right instance — a plain repeat of a view only stays warning-free while nothing references it by name, and
692
+ resolves to the *first* declaration when something does (the exception is a same-context event
693
+ repeat, which is one logical event — see above). **Wire each event to a read model exactly
638
694
  once:** a repeated instance's `from` lists only the **new** events since the previous instance
639
695
  (not cumulative), or the event draws a duplicate arrow to the same read model at every repeat. (An
640
696
  event may still feed several *different* read models, once each.)
@@ -655,6 +711,13 @@ consumer is an obligation to ship an endpoint, no `ui` means internal-only, and
655
711
  the published integration surface — see `reference/implement.md`'s "Interface obligations"
656
712
  paragraph in the `event-modeling-implement` skill (§2).
657
713
 
714
+ - **Continuations without ceremony (GH #287).** Several `view X again` lines may share one
715
+ continuation slice (they stack in the `api` cell); `view X again from "E"` written inside a
716
+ command's own slice is sugar for a synthesized continuation column right after it (one column per host slice, lines stacked; key
717
+ `<slice-key>--again`, no doc, not in `em slice index`; consumer = the slice's first `ui`,
718
+ or `shown on "Screen"` — required in a reaction slice with no `ui`); and a continuation slice may omit its name — it is auto-named
719
+ `"<View> View: <Event>"`. Instances still never connect.
720
+
658
721
  ### Reaction wiring — no positional gotcha
659
722
 
660
723
  A reaction's command is inferred from **same-slice presence**, exactly like `ui -> command`: put
@@ -675,7 +738,13 @@ it to the slice that displays the read model, or drop it.
675
738
 
676
739
  **Errors (must fix):**
677
740
  1. **Band collision** — two elements of the same band in one slice (e.g. two `command`s, or two
678
- `ui`s in the *same* persona row). Split them into separate slices/personas.
741
+ `ui`s in the *same* persona row). Split them into separate slices/personas. Two events on one
742
+ context that are alternative outcomes of one command are NOT a split: write
743
+ `event A | B @Context` (identical payloads) or `oneof { event A @Context { … } event B @Context { … } }`
744
+ (own fields/clauses per alternative) — one slice, one shared cell, one command, one shared context; each
745
+ alternative is a full event, and a standalone `invariant` after them binds to all.
746
+ Alternatives with no command in the slice are an error (`alternative-events-no-command`), and the same
747
+ command repeated across slices warns (`command-repeated-across-slices`).
679
748
  2. **Unknown event source** — `view X from "Event"` where the event doesn't exist anywhere.
680
749
  3. **Unknown read-model source** — `processor X from "View"` where the view doesn't exist.
681
750
  4. **Arrow endpoint mismatch** — `arrow A -> B` where A or B matches no element name.
@@ -773,9 +842,11 @@ not the prose above has caught up yet. `--slice-ready <key>`-only codes are excl
773
842
  <!-- GENERATED:validate-rules:start -- run `npm run docs:generate` to refresh, do not hand-edit -->
774
843
  | Code | Severity | Title | Fix |
775
844
  |---|---|---|---|
845
+ | `alternative-events-no-command` | error | Alternative events without a command | Add the command whose decision records one of the alternatives, in the same slice. |
776
846
  | `arrow-backward` | error | Backward arrow | Restructure so the target comes later. |
777
847
  | `arrow-unresolved-source` | error | Arrow source unresolved | Fix the arrow's source name. |
778
848
  | `arrow-unresolved-target` | error | Arrow target unresolved | Fix the arrow's target name. |
849
+ | `automation-split-by-outcome` | warning | Reaction split into one slice per outcome | Model one Automation slice recording the outcomes as alternative events — `event A | B @Context` (docs/patterns.md#automation). |
779
850
  | `binding-missing-file` | warning | Doc binding points at a missing file | Create the slice doc, or fix the `note` path. |
780
851
  | `both-ends-of-a-flow/command-no-event` | warning | Command without event | Add the event this command records. |
781
852
  | `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. |
@@ -785,6 +856,8 @@ not the prose above has caught up yet. `--slice-ready <key>`-only codes are excl
785
856
  | `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. |
786
857
  | `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. |
787
858
  | `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. |
859
+ | `command-repeated-across-slices` | warning | Command repeated across slices | Merge the slices: one command with several outcomes is one slice — `event A | B @Context`. |
860
+ | `command-second-intent` | warning | Command carries a second intent | Split the conditional rule set into its own command; both commands may record the same event (a bare repeat of the event is legal). |
788
861
  | `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. |
789
862
  | `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. |
790
863
  | `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. |
@@ -797,6 +870,8 @@ not the prose above has caught up yet. `--slice-ready <key>`-only codes are excl
797
870
  | `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. |
798
871
  | `doc-model-field-mismatch` | warning | Doc/model field mismatch | Reconcile the field table with the model's fields — names and types. |
799
872
  | `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. |
873
+ | `doc-names-unfed-event` | warning | Doc names an event its view does not read | Add the event to the view's `from` in the model (if the edge is real), or fix the doc's Source column / scenario. |
874
+ | `doc-omits-fed-event` | warning | Doc omits an event its view reads | Mention the event in the view's Source column (run `em slice sync`), or drop it from the view's `from`. |
800
875
  | `duplicate-element-ref` | warning | Duplicate element ref | Rename the element so its export ref is unique. |
801
876
  | `duplicate-model-key` | warning | Duplicate model key | Give each model a unique `model "Name"` so its key (and every cross-model ref) is stable. |
802
877
  | `duplicate-name` | warning | Duplicate name | Rename one of the duplicates. |
@@ -808,6 +883,7 @@ not the prose above has caught up yet. `--slice-ready <key>`-only codes are excl
808
883
  | `frontmatter-coherence-implemented-without-link` | warning | Implemented without a link | Add `implementedIn` once the slice ships. |
809
884
  | `frontmatter-invalid` | warning | Invalid or missing frontmatter | Add the required frontmatter keys, or add a frontmatter block. |
810
885
  | `grid-collision` | error | Band collision | Split the colliding elements into separate slices. |
886
+ | `invariant-without-scenario` | warning | Invariant cited by no scenario | Cite the INV id in the Given/When/Then (or title) of the scenario that exercises it, or add one. |
811
887
  | `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. |
812
888
  | `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. |
813
889
  | `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. |
@@ -832,18 +908,27 @@ not the prose above has caught up yet. `--slice-ready <key>`-only codes are excl
832
908
  | `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. |
833
909
  | `reaction-from-future-view` | error | Backward timeline (reaction reads a future view) | Declare the view in or before the reaction's slice. |
834
910
  | `reaction-from-unresolved` | error | Unknown read-model source | Project the event into a view first, or fix the `from` reference. |
911
+ | `retry-slice-of` | warning | Slice only retries another command | Re-fire the same reaction via `view … again from` the retry-scheduled event; a different actor's override is its own slice with its own command (docs/patterns.md#automation). |
912
+ | `scenario-then-not-outcome` | warning | Scenario's Then names no event or rejection | Make the Then bullets name the event(s) the command records, or the rejection/error it returns. |
913
+ | `scenario-when-not-command` | warning | Scenario's When does not name the slice's command | Make the When bullet name the slice's command (for a reaction slice: the command, the reaction element, or a view of the slice) — each scenario exercises exactly one command. For a Translation, naming the translation element satisfies it. |
835
914
  | `seam-consumer-not-reaction` | error | Seam consumer is not a reaction | Point `to` at a translation/automation element (or a slice containing exactly one). |
836
915
  | `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. |
837
916
  | `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. |
838
917
  | `seam-duplicate` | warning | Duplicate seam | Remove the repeated `from`/`to` pair. |
839
918
  | `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. |
840
919
  | `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. |
920
+ | `shared-event-field-mismatch` | error | Shared event declared with different fields, tags or public | Repeat the event bare to inherit the first declaration's fields, tags and public, or make them identical. |
841
921
  | `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. |
922
+ | `slice-outside-storyline` | warning | Slice outside every storyline | Move the slice inside one of the model's `storyline` blocks (advisory: storylines can be adopted incrementally). |
923
+ | `storyline-after-cycle` | error | Storyline `after` cycle | Remove an `after` clause so no storyline comes after itself. |
924
+ | `storyline-after-out-of-order` | error | Backward timeline (storyline declared above the one it comes after) | Move the storyline block below the storyline it comes `after`, or restructure the feed. |
925
+ | `storyline-after-unknown` | error | `after` names an unknown storyline | Name a storyline declared in this model, or drop the `after` clause. |
842
926
  | `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`). |
843
927
  | `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. |
844
928
  | `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. |
845
929
  | `tag-composite-unknown-field` | error | Composite tag names an unknown field | Fix the field name, or add it to the event's fields. |
846
930
  | `tag-duplicate-key` | error | Duplicate tag key | Rename one of the tags so every key on the event is unique. |
931
+ | `timeline-entity-clustered` | warning | Lifecycle-per-entity shape (storyline order) | If these are create/update/archive steps on one entity, order the slices by the business journey, interleaving contexts as the story does; if they are steps of one journey, show a screen (a view) after each step. See docs/storyline.md. |
847
932
  | `translation-name-collision` | warning | Translation name reused for different producers | Use a distinct name per producer to avoid confusion. |
848
933
  | `type-cycle` | error | Cyclic type reference | Break the cycle, or route the self/mutual reference through an array. |
849
934
  | `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. |
@@ -144,6 +144,17 @@ command** — it never records an event directly.
144
144
  duplicated name.
145
145
  - Socratic prompts: *"What should happen without a human? What condition triggers it? What work
146
146
  list does the processor watch? What command does it fire? What if it fails or retries?"*
147
+ - **Outcomes are events of the SAME slice.** Success, failure, retry-scheduled and timeout are
148
+ outcome events of the one command (`Fulfilment Succeeded` / `Fulfilment Failed` / `Fulfilment
149
+ Retry Scheduled`), not sibling slices per control-flow branch (1.18.0 adds the
150
+ lint `automation-split-by-outcome`). Retry policy and failure handling are invariants and scenarios
151
+ in that slice's doc. A new slice only when a different actor or a different trigger is
152
+ involved. Canon: Dilger, "one business action… two different possible outcomes"
153
+ (`PaymentIssued` / `PaymentFailed`); a failure that continues the story is "Linearized" as a
154
+ later chapter linked to the originating event, never a branch; it reads the failure event
155
+ through a view or a reaction (Law 1 forbids event-to-event arrows). A retry is the processor's
156
+ to-do view reading the retry-scheduled event (`view X again`), so the same reaction re-fires. Same rule for State Change: one
157
+ command, several outcome events, one slice.
147
158
 
148
159
  ### 4. Translation
149
160
  **Boundary crossing → command → event, the reaction and what it triggers together in one slice.**
@@ -193,6 +204,260 @@ The `em` shape only tracks the second question — trigger source doesn't change
193
204
 
194
205
  ---
195
206
 
207
+ ## The story, not the schema
208
+
209
+ A model is a story of what success looks like, not a data model (Dymitruk, *What is Event Modeling*: a
210
+ story "on a time line and with no branching"; commands capture intent, "as opposed to simply saving form
211
+ data to a table"; "all information has to have an origin and a destination"). Models drafted from a spec
212
+ drift into **CRUD laid on a timeline**; these rules stop that. Quoted lines are the canon; rulings marked
213
+ *(em ruling)* are em's own — neither Dymitruk nor Dilger states a child-entity rule or bans "Update" or
214
+ "Set" by name. Full write-up with the canon quotes: `docs/storyline.md`.
215
+
216
+ - **One journey per process, start to finish.** The timeline holds one business journey per process,
217
+ ending in an outcome. The **main storyline** is the end-to-end happy path; retire, extend and
218
+ reorganize are **secondary storylines** (chapters; Dilger: "one linear flow from left to right. No
219
+ exceptions… Linearize it!", "Model one process at a time, each with its own clear ending"). Never one
220
+ lifecycle block per entity. Name the main and every secondary storyline; a `storyline "Name" { … }`
221
+ block groups the slices (`dsl.md`, Storylines).
222
+ - **Build from walkthroughs first.** The story already lives in the spec's walkthroughs, use cases and
223
+ user journeys. Build the timeline from those; the resource/API reference (OpenAPI, endpoint tables,
224
+ migrations) fills only the operations no walkthrough touches, and is read last. Headless: a persona
225
+ per API caller, and that caller's journey is the story.
226
+ - **The CRUD shape.** N entities × (create, update, archive), one trailing view per entity, no read
227
+ across contexts. Recognise it in your own output. Both models below validate; only the second is a
228
+ story:
229
+ ```em
230
+ model "Catalog (CRUD shape)"
231
+
232
+ persona Merchandiser
233
+
234
+ context Product
235
+
236
+ slice "Create Product" {
237
+ ui Product Form @Merchandiser
238
+ command Create Product
239
+ event Product Created @Product
240
+ }
241
+
242
+ slice "Update Product" {
243
+ ui Product Form @Merchandiser
244
+ command Update Product
245
+ event Product Updated @Product
246
+ }
247
+
248
+ slice "Archive Product" {
249
+ ui Product Form @Merchandiser
250
+ command Archive Product
251
+ event Product Archived @Product
252
+ }
253
+
254
+ slice "Products" {
255
+ view Products from "Product Created", "Product Updated", "Product Archived"
256
+ ui Product List @Merchandiser
257
+ }
258
+ ```
259
+ ```em
260
+ model "Catalog (storyline shape)"
261
+
262
+ persona Merchandiser
263
+
264
+ context Product
265
+
266
+ storyline "Bring a product to market" {
267
+ slice "Create Product" {
268
+ ui Product Form @Merchandiser
269
+ command Create Product
270
+ event Product Created @Product
271
+ }
272
+
273
+ slice "Catalog" {
274
+ view Catalog from "Product Created"
275
+ ui Browse Catalog @Merchandiser
276
+ }
277
+
278
+ slice "Set Price" {
279
+ ui Pricing Panel @Merchandiser
280
+ command Set Price
281
+ event Price Set @Product
282
+ }
283
+
284
+ slice "Catalog with Price" {
285
+ view Catalog again from "Price Set"
286
+ ui Browse Catalog @Merchandiser
287
+ }
288
+ }
289
+
290
+ storyline "Retire a product" after "Bring a product to market" {
291
+ slice "Retire Product" {
292
+ ui Pricing Panel @Merchandiser
293
+ command Retire Product
294
+ event Product Retired @Product
295
+ }
296
+
297
+ slice "Catalog without Product" {
298
+ view Catalog again from "Product Retired"
299
+ ui Browse Catalog @Merchandiser
300
+ }
301
+ }
302
+ ```
303
+ The `timeline-entity-clustered` lint is the mechanical half (it fires only once two or more
304
+ contexts are written; this one-entity pair stays silent); once any storyline exists,
305
+ `slice-outside-storyline` flags slices left outside one.
306
+ - **Commands name intent, not endpoints** (Dilger: "Name by intent", "Every command has a reason"). One
307
+ endpoint may carry several intents. Signal of two intents in one command: invariants or field rules
308
+ that apply only "if `<field>` is set" — split it, and both commands may record the same event
309
+ (`CreateProduct` / `CreateProductVariant`, same `POST /products`, same `ProductCreated`). **Intent
310
+ test:** if a storyline step reads awkwardly with the command's name, the command is named after the
311
+ endpoint. `command-second-intent` and `command-repeated-across-slices` are the mechanical half.
312
+ - **A read model is one information need** (Dilger: "Every Read Model answers a question. No question,
313
+ no Read Model."). **Question test:** write the question each view answers; add a view only when no
314
+ existing one answers it. A nested route hints the child belongs in the parent's view; the real test
315
+ is whether the query needs information the parent view lacks. Child with references only → the parent
316
+ view carries ids; child with its own data → embedded in the parent view; child queried without the
317
+ parent id → its own view. Aggregate rules decide write-side consistency, never read models. *(em
318
+ ruling)*
319
+ - **Write side: children belong in the parent's context** when the child's invariants are checks against
320
+ the parent. A sub-resource schema shared across parent types is one event per parent on the parent's
321
+ context, not a shared aggregate — check the spec's event reference, not its resource section. Where
322
+ two slices record the same fact, repeat the event bare (same name, same context). *(em ruling)*
323
+ - **Repeat the view after every command that changes it** (`again`, span-1 — see State View above), so
324
+ the reader sees the information arrive rather than a view at the end.
325
+ - **Outcomes are events of one slice** (Dilger: "One decision. One event. One state change."; one
326
+ action with two outcomes is the legitimate exception): alternative events on one context in one slice
327
+ (`event A | B @Ctx`, GH #278), failure as a scenario first, a chapter only when core (it changes what the user does next, or has
328
+ its own actor/SLA). Stated once for
329
+ automations in the Automation pattern (`docs/patterns.md#automation`); `automation-split-by-outcome`
330
+ and `retry-slice-of` are the mechanical half. Two forms, alternatives always on one context: inline `event A | B @Ctx`
331
+ (shared clauses and fields) and `oneof { … }` (each alternative keeps its own fields and clauses):
332
+ ```em
333
+ model "Payments"
334
+
335
+ persona Customer
336
+
337
+ context Payment
338
+
339
+ slice "Settle Payment" {
340
+ ui Pay Screen @Customer
341
+ command Settle Payment
342
+ event Payment Issued | Payment Failed @Payment { paymentId: UUID }
343
+ }
344
+
345
+ slice "Payment Status" {
346
+ view Payment Status from "Payment Issued", "Payment Failed"
347
+ ui Payment Result @Customer
348
+ }
349
+
350
+ slice "Refund Payment" {
351
+ ui Refund Screen @Customer
352
+ command Refund Payment
353
+ oneof {
354
+ event Refund Issued @Payment { paymentId: UUID, amount: Decimal }
355
+ event Refund Declined @Payment { paymentId: UUID, reason: String }
356
+ }
357
+ }
358
+
359
+ slice "Refund Status" {
360
+ view Refund Status from "Refund Issued", "Refund Declined"
361
+ ui Refund Result @Customer
362
+ }
363
+ ```
364
+ - **One event, two commands.** When two commands record the same fact, repeat the event bare
365
+ (`event X @Ctx`, same name and context) in the second slice: it inherits the first declaration, no
366
+ marker needed. Declaring different fields is `shared-event-field-mismatch`. (The arrow set for a
367
+ shared event read by a repeated view is under review: GH #328.)
368
+ ```em
369
+ model "Ordering"
370
+
371
+ persona Customer
372
+
373
+ context Sales
374
+
375
+ slice "Place Order" {
376
+ ui Checkout @Customer
377
+ command Place Order
378
+ event Order Placed @Sales { orderId: UUID }
379
+ }
380
+
381
+ slice "Order Status" {
382
+ view Order Status from "Order Placed"
383
+ ui Track Order @Customer
384
+ }
385
+
386
+ slice "Reorder" {
387
+ ui Reorder Button @Customer
388
+ command Reorder
389
+ event Order Placed @Sales
390
+ }
391
+
392
+ slice "Order Status after Reorder" {
393
+ view Order Status again from "Order Placed"
394
+ ui Track Order @Customer
395
+ }
396
+ ```
397
+ Combined with alternatives: when one alternative is a shared repeat, use `oneof` and write that
398
+ alternative bare so it inherits its fields (an inline line's fields would be copied onto every
399
+ alternative and mismatch):
400
+ ```em
401
+ model "Payments"
402
+
403
+ persona Customer
404
+
405
+ context Payment
406
+
407
+ slice "Settle Payment" {
408
+ ui Pay Screen @Customer
409
+ command Settle Payment
410
+ oneof {
411
+ event Payment Issued @Payment { paymentId: UUID }
412
+ event Payment Failed @Payment { paymentId: UUID, reason: String }
413
+ }
414
+ }
415
+
416
+ slice "Payment Status" {
417
+ view Payment Status from "Payment Issued", "Payment Failed"
418
+ ui Payment Result @Customer
419
+ }
420
+
421
+ slice "Retry Payment" {
422
+ ui Retry Screen @Customer
423
+ command Retry Payment
424
+ oneof {
425
+ event Payment Issued @Payment
426
+ event Payment Retry Declined @Payment { paymentId: UUID }
427
+ }
428
+ }
429
+
430
+ slice "Payment Status after Retry" {
431
+ view Payment Status again from "Payment Issued", "Payment Retry Declined"
432
+ ui Payment Result @Customer
433
+ }
434
+ ```
435
+ - **Reference-only PRD groups sit outside storylines.** A group that only carries reference material
436
+ (#327) is not a journey; leave it out of every `storyline` block.
437
+ - **Business terms in the events.** "If your Event Model doesn't reflect terms used by business experts —
438
+ especially in the recorded Events — that's a red flag." (Dilger, *Seven insights*.)
439
+
440
+ ### Reshaping an existing model
441
+
442
+ The upgrade path for a CRUD-shaped model. After each step run `em validate`.
443
+
444
+ `em storyline suggest <model>.em` proposes the `storyline` blocks as a diff (advisory, never
445
+ writes); `em upgrade` offers `collapse-alternative-events` and the opt-in `fold-continuations`.
446
+
447
+ 1. Name the processes (list each spec's walkthroughs or journeys, not endpoints): the main storyline and each secondary one.
448
+ 2. Reorder the slices into those stories and wrap each in a `storyline` block (`after "…"` where one
449
+ follows another); a slice that fits no story is a deletion candidate.
450
+ 3. Intent test per command: rename to the business intent; split any command with "if X is set" rules.
451
+ 4. Apply the information-need test to each read model and fold children into parents: merge views
452
+ answering the same question; add one only for an unanswered one.
453
+ 5. Move child-entity events onto the parent's context when the child's invariants check the parent.
454
+ 6. Repeat each view (`again`, span-1) after every command that changes it; drop the lone trailing view.
455
+ 7. Fold success/failure slice pairs into one slice with alternative events.
456
+ 8. Tooling, last: `em validate` (and `em validate --shape`, the 1.18.0 design exit gate), `em slice sync
457
+ --check` then `em slice sync`, `em slice index`, and regenerate TypeSpec/contracts last.
458
+
459
+ ---
460
+
196
461
  ## The 7 steps
197
462
 
198
463
  Run them in order. The skill groups them into phases: **discover = 1–4**, **model = 5–7**,
@@ -256,6 +521,9 @@ dedicated `slice` phase writes the full rich spec (see `../templates/slice.md`).
256
521
  - Prompts: *"Given what starting state, when this command/trigger fires, then what event(s)
257
522
  result? What must always be true? What are the failure paths? Which fields are essential and
258
523
  what are their rules?"*
524
+ - **Failure, retry, timeout surface here?** Add the outcome events and the scenarios to the
525
+ SAME slice, never a new slice per branch (Dymitruk: "a few of these stories to show how a
526
+ command can and cannot succeed" — Given/When/Then scenarios first).
259
527
  - **Existing codebase:** before finalizing field names/types, check adjacent real sources
260
528
  (OpenAPI specs, DB migrations, existing DTOs/event classes in sibling contexts) rather than
261
529
  guessing — mirrors `extract.md`'s whole-system grounding, applied per-slice.