@milehimikey/em 1.16.1 → 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 (219) hide show
  1. package/.claude/skills/event-modeling/SKILL.md +1 -1
  2. package/.claude/skills/event-modeling-conform/SKILL.md +3 -2
  3. package/.claude/skills/event-modeling-conform/reference/conform.md +7 -0
  4. package/.claude/skills/event-modeling-design/SKILL.md +46 -5
  5. package/.claude/skills/event-modeling-discover/SKILL.md +43 -8
  6. package/.claude/skills/event-modeling-discover/reference/extract.md +6 -1
  7. package/.claude/skills/event-modeling-discover/reference/prd-rubric.md +102 -0
  8. package/.claude/skills/event-modeling-discover/reference/prd-spec-path.md +171 -0
  9. package/.claude/skills/event-modeling-engagement/SKILL.md +28 -6
  10. package/.claude/skills/event-modeling-engagement/reference/engagement.md +5 -4
  11. package/.claude/skills/event-modeling-implement/SKILL.md +1 -1
  12. package/.claude/skills/event-modeling-implement/reference/implement.md +18 -8
  13. package/.claude/skills/event-modeling-review/SKILL.md +1 -1
  14. package/.claude/skills/event-modeling-shared/reference/em-dsl.md +107 -10
  15. package/.claude/skills/event-modeling-shared/reference/methodology.md +268 -0
  16. package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +122 -15
  17. package/.claude/skills/event-modeling-shared/templates/slice.md +11 -3
  18. package/.claude-plugin/marketplace.json +2 -2
  19. package/dist/catalog/docJoin.d.ts +13 -5
  20. package/dist/catalog/docJoin.js +6 -1
  21. package/dist/catalog/docJoin.js.map +1 -1
  22. package/dist/catalog/docModelConsistencyValidate.js +160 -0
  23. package/dist/catalog/docModelConsistencyValidate.js.map +1 -1
  24. package/dist/catalog/invariantsValidate.d.ts +2 -0
  25. package/dist/catalog/invariantsValidate.js +67 -1
  26. package/dist/catalog/invariantsValidate.js.map +1 -1
  27. package/dist/catalog/scenarioQualityValidate.d.ts +4 -0
  28. package/dist/catalog/scenarioQualityValidate.js +148 -0
  29. package/dist/catalog/scenarioQualityValidate.js.map +1 -0
  30. package/dist/catalog/scenarios.d.ts +5 -0
  31. package/dist/catalog/scenarios.js +80 -0
  32. package/dist/catalog/scenarios.js.map +1 -1
  33. package/dist/catalog/signedBodyValidate.d.ts +7 -0
  34. package/dist/catalog/signedBodyValidate.js +29 -0
  35. package/dist/catalog/signedBodyValidate.js.map +1 -0
  36. package/dist/catalog/sliceDoc.d.ts +53 -7
  37. package/dist/catalog/sliceDoc.js +89 -10
  38. package/dist/catalog/sliceDoc.js.map +1 -1
  39. package/dist/catalog/sliceReadyValidate.d.ts +23 -2
  40. package/dist/catalog/sliceReadyValidate.js +54 -22
  41. package/dist/catalog/sliceReadyValidate.js.map +1 -1
  42. package/dist/catalog/sliceSections.d.ts +3 -2
  43. package/dist/catalog/sliceSections.js +99 -9
  44. package/dist/catalog/sliceSections.js.map +1 -1
  45. package/dist/cli/ciInit.d.ts +4 -11
  46. package/dist/cli/ciInit.js +24 -6
  47. package/dist/cli/ciInit.js.map +1 -1
  48. package/dist/cli/coverage.d.ts +9 -0
  49. package/dist/cli/coverage.js +18 -0
  50. package/dist/cli/coverage.js.map +1 -1
  51. package/dist/cli/defer.d.ts +60 -17
  52. package/dist/cli/defer.js +187 -86
  53. package/dist/cli/defer.js.map +1 -1
  54. package/dist/cli/engagement.d.ts +46 -0
  55. package/dist/cli/engagement.js +156 -1
  56. package/dist/cli/engagement.js.map +1 -1
  57. package/dist/cli/engagementFoundation.d.ts +47 -0
  58. package/dist/cli/engagementFoundation.js +164 -0
  59. package/dist/cli/engagementFoundation.js.map +1 -0
  60. package/dist/cli/frontmatterSurgery.d.ts +5 -0
  61. package/dist/cli/frontmatterSurgery.js +15 -0
  62. package/dist/cli/frontmatterSurgery.js.map +1 -1
  63. package/dist/cli/ledgerCheck.d.ts +5 -1
  64. package/dist/cli/ledgerCheck.js +50 -20
  65. package/dist/cli/ledgerCheck.js.map +1 -1
  66. package/dist/cli/markImplemented.d.ts +14 -10
  67. package/dist/cli/markImplemented.js +49 -1
  68. package/dist/cli/markImplemented.js.map +1 -1
  69. package/dist/cli/prdTriage.d.ts +109 -0
  70. package/dist/cli/prdTriage.js +378 -0
  71. package/dist/cli/prdTriage.js.map +1 -0
  72. package/dist/cli/ratify.d.ts +14 -2
  73. package/dist/cli/ratify.js +34 -6
  74. package/dist/cli/ratify.js.map +1 -1
  75. package/dist/cli/refsCheck.d.ts +78 -0
  76. package/dist/cli/refsCheck.js +153 -0
  77. package/dist/cli/refsCheck.js.map +1 -0
  78. package/dist/cli/render-inputs.d.ts +2 -0
  79. package/dist/cli/render-inputs.js +4 -0
  80. package/dist/cli/render-inputs.js.map +1 -1
  81. package/dist/cli/revise.d.ts +12 -3
  82. package/dist/cli/revise.js +43 -10
  83. package/dist/cli/revise.js.map +1 -1
  84. package/dist/cli/shape.d.ts +27 -0
  85. package/dist/cli/shape.js +77 -0
  86. package/dist/cli/shape.js.map +1 -0
  87. package/dist/cli/sliceConform.js +9 -0
  88. package/dist/cli/sliceConform.js.map +1 -1
  89. package/dist/cli/sliceIndex.d.ts +6 -1
  90. package/dist/cli/sliceIndex.js +28 -13
  91. package/dist/cli/sliceIndex.js.map +1 -1
  92. package/dist/cli/sliceNew.d.ts +4 -1
  93. package/dist/cli/sliceNew.js +26 -9
  94. package/dist/cli/sliceNew.js.map +1 -1
  95. package/dist/cli/status.d.ts +26 -5
  96. package/dist/cli/status.js +28 -6
  97. package/dist/cli/status.js.map +1 -1
  98. package/dist/cli/storylineSuggest.d.ts +26 -0
  99. package/dist/cli/storylineSuggest.js +252 -0
  100. package/dist/cli/storylineSuggest.js.map +1 -0
  101. package/dist/cli/upgrade.d.ts +15 -3
  102. package/dist/cli/upgrade.js +537 -8
  103. package/dist/cli/upgrade.js.map +1 -1
  104. package/dist/cli/upgradeReshape.d.ts +60 -0
  105. package/dist/cli/upgradeReshape.js +425 -0
  106. package/dist/cli/upgradeReshape.js.map +1 -0
  107. package/dist/cli.js +430 -57
  108. package/dist/cli.js.map +1 -1
  109. package/dist/emit/dot.d.ts +2 -0
  110. package/dist/emit/dot.js +44 -8
  111. package/dist/emit/dot.js.map +1 -1
  112. package/dist/emit/engagementJson.d.ts +4 -1
  113. package/dist/emit/engagementJson.js +27 -1
  114. package/dist/emit/engagementJson.js.map +1 -1
  115. package/dist/emit/json.d.ts +21 -1
  116. package/dist/emit/json.js +29 -2
  117. package/dist/emit/json.js.map +1 -1
  118. package/dist/emit/ledgerJson.d.ts +1 -1
  119. package/dist/emit/ledgerJson.js +6 -1
  120. package/dist/emit/ledgerJson.js.map +1 -1
  121. package/dist/emit/queryJson.d.ts +1 -1
  122. package/dist/emit/queryJson.js +3 -1
  123. package/dist/emit/queryJson.js.map +1 -1
  124. package/dist/emit/refsJson.d.ts +4 -0
  125. package/dist/emit/refsJson.js +19 -0
  126. package/dist/emit/refsJson.js.map +1 -0
  127. package/dist/emit/statusJson.d.ts +1 -1
  128. package/dist/emit/statusJson.js +6 -1
  129. package/dist/emit/statusJson.js.map +1 -1
  130. package/dist/emit/theme.d.ts +1 -0
  131. package/dist/emit/theme.js +13 -0
  132. package/dist/emit/theme.js.map +1 -1
  133. package/dist/emit/validateJson.d.ts +5 -2
  134. package/dist/emit/validateJson.js +41 -2
  135. package/dist/emit/validateJson.js.map +1 -1
  136. package/dist/layout/grid.d.ts +5 -1
  137. package/dist/layout/grid.js +93 -2
  138. package/dist/layout/grid.js.map +1 -1
  139. package/dist/mcp/server.js +79 -14
  140. package/dist/mcp/server.js.map +1 -1
  141. package/dist/model/edges.d.ts +7 -0
  142. package/dist/model/edges.js +33 -16
  143. package/dist/model/edges.js.map +1 -1
  144. package/dist/model/model.d.ts +33 -0
  145. package/dist/model/model.js +87 -3
  146. package/dist/model/model.js.map +1 -1
  147. package/dist/model/queryIndex.js +2 -2
  148. package/dist/model/queryIndex.js.map +1 -1
  149. package/dist/model/refs.js +8 -5
  150. package/dist/model/refs.js.map +1 -1
  151. package/dist/model/rules.d.ts +167 -1
  152. package/dist/model/rules.js +164 -3
  153. package/dist/model/rules.js.map +1 -1
  154. package/dist/model/shapeLint.d.ts +12 -0
  155. package/dist/model/shapeLint.js +170 -0
  156. package/dist/model/shapeLint.js.map +1 -0
  157. package/dist/model/storylines.d.ts +51 -0
  158. package/dist/model/storylines.js +188 -0
  159. package/dist/model/storylines.js.map +1 -0
  160. package/dist/model/validate.d.ts +6 -1
  161. package/dist/model/validate.js +317 -14
  162. package/dist/model/validate.js.map +1 -1
  163. package/dist/parser/againSugar.d.ts +30 -0
  164. package/dist/parser/againSugar.js +105 -0
  165. package/dist/parser/againSugar.js.map +1 -0
  166. package/dist/parser/ast.d.ts +33 -0
  167. package/dist/parser/lexer.d.ts +9 -1
  168. package/dist/parser/lexer.js +17 -2
  169. package/dist/parser/lexer.js.map +1 -1
  170. package/dist/parser/parser.js +284 -4
  171. package/dist/parser/parser.js.map +1 -1
  172. package/dist/query/format.js +1 -0
  173. package/dist/query/format.js.map +1 -1
  174. package/dist/query/verbs.d.ts +4 -0
  175. package/dist/query/verbs.js +30 -3
  176. package/dist/query/verbs.js.map +1 -1
  177. package/dist/render/drawEdges.js +100 -4
  178. package/dist/render/drawEdges.js.map +1 -1
  179. package/dist/render/render.js +20 -2
  180. package/dist/render/render.js.map +1 -1
  181. package/dist/render/sliceDiagram.js +5 -5
  182. package/dist/render/sliceDiagram.js.map +1 -1
  183. package/dist/render/storylineBands.d.ts +11 -0
  184. package/dist/render/storylineBands.js +80 -0
  185. package/dist/render/storylineBands.js.map +1 -0
  186. package/dist/render/storylineDiagram.d.ts +14 -0
  187. package/dist/render/storylineDiagram.js +142 -0
  188. package/dist/render/storylineDiagram.js.map +1 -0
  189. package/dist/roster/check.d.ts +32 -0
  190. package/dist/roster/check.js +92 -0
  191. package/dist/roster/check.js.map +1 -0
  192. package/dist/roster/context.d.ts +3 -0
  193. package/dist/roster/context.js +23 -0
  194. package/dist/roster/context.js.map +1 -0
  195. package/dist/roster/init.d.ts +15 -0
  196. package/dist/roster/init.js +52 -0
  197. package/dist/roster/init.js.map +1 -0
  198. package/dist/roster/roster.d.ts +79 -0
  199. package/dist/roster/roster.js +298 -0
  200. package/dist/roster/roster.js.map +1 -0
  201. package/package.json +1 -1
  202. package/plugin/.claude-plugin/plugin.json +2 -2
  203. package/plugin/shared/reference/em-dsl.md +107 -10
  204. package/plugin/shared/reference/methodology.md +268 -0
  205. package/plugin/shared/reference/slice-doc-schema.md +122 -15
  206. package/plugin/shared/templates/slice.md +11 -3
  207. package/plugin/skills/conform/SKILL.md +3 -2
  208. package/plugin/skills/conform/reference/conform.md +7 -0
  209. package/plugin/skills/design/SKILL.md +46 -5
  210. package/plugin/skills/discover/SKILL.md +43 -8
  211. package/plugin/skills/discover/reference/extract.md +6 -1
  212. package/plugin/skills/discover/reference/prd-rubric.md +103 -0
  213. package/plugin/skills/discover/reference/prd-spec-path.md +172 -0
  214. package/plugin/skills/engagement/SKILL.md +28 -6
  215. package/plugin/skills/engagement/reference/engagement.md +5 -4
  216. package/plugin/skills/event-modeling/SKILL.md +1 -1
  217. package/plugin/skills/implement/SKILL.md +1 -1
  218. package/plugin/skills/implement/reference/implement.md +18 -8
  219. package/plugin/skills/review/SKILL.md +1 -1
@@ -145,6 +145,17 @@ command** — it never records an event directly.
145
145
  duplicated name.
146
146
  - Socratic prompts: *"What should happen without a human? What condition triggers it? What work
147
147
  list does the processor watch? What command does it fire? What if it fails or retries?"*
148
+ - **Outcomes are events of the SAME slice.** Success, failure, retry-scheduled and timeout are
149
+ outcome events of the one command (`Fulfilment Succeeded` / `Fulfilment Failed` / `Fulfilment
150
+ Retry Scheduled`), not sibling slices per control-flow branch (1.18.0 adds the
151
+ lint `automation-split-by-outcome`). Retry policy and failure handling are invariants and scenarios
152
+ in that slice's doc. A new slice only when a different actor or a different trigger is
153
+ involved. Canon: Dilger, "one business action… two different possible outcomes"
154
+ (`PaymentIssued` / `PaymentFailed`); a failure that continues the story is "Linearized" as a
155
+ later chapter linked to the originating event, never a branch; it reads the failure event
156
+ through a view or a reaction (Law 1 forbids event-to-event arrows). A retry is the processor's
157
+ to-do view reading the retry-scheduled event (`view X again`), so the same reaction re-fires. Same rule for State Change: one
158
+ command, several outcome events, one slice.
148
159
 
149
160
  ### 4. Translation
150
161
  **Boundary crossing → command → event, the reaction and what it triggers together in one slice.**
@@ -194,6 +205,260 @@ The `em` shape only tracks the second question — trigger source doesn't change
194
205
 
195
206
  ---
196
207
 
208
+ ## The story, not the schema
209
+
210
+ A model is a story of what success looks like, not a data model (Dymitruk, *What is Event Modeling*: a
211
+ story "on a time line and with no branching"; commands capture intent, "as opposed to simply saving form
212
+ data to a table"; "all information has to have an origin and a destination"). Models drafted from a spec
213
+ drift into **CRUD laid on a timeline**; these rules stop that. Quoted lines are the canon; rulings marked
214
+ *(em ruling)* are em's own — neither Dymitruk nor Dilger states a child-entity rule or bans "Update" or
215
+ "Set" by name. Full write-up with the canon quotes: `docs/storyline.md`.
216
+
217
+ - **One journey per process, start to finish.** The timeline holds one business journey per process,
218
+ ending in an outcome. The **main storyline** is the end-to-end happy path; retire, extend and
219
+ reorganize are **secondary storylines** (chapters; Dilger: "one linear flow from left to right. No
220
+ exceptions… Linearize it!", "Model one process at a time, each with its own clear ending"). Never one
221
+ lifecycle block per entity. Name the main and every secondary storyline; a `storyline "Name" { … }`
222
+ block groups the slices (`dsl.md`, Storylines).
223
+ - **Build from walkthroughs first.** The story already lives in the spec's walkthroughs, use cases and
224
+ user journeys. Build the timeline from those; the resource/API reference (OpenAPI, endpoint tables,
225
+ migrations) fills only the operations no walkthrough touches, and is read last. Headless: a persona
226
+ per API caller, and that caller's journey is the story.
227
+ - **The CRUD shape.** N entities × (create, update, archive), one trailing view per entity, no read
228
+ across contexts. Recognise it in your own output. Both models below validate; only the second is a
229
+ story:
230
+ ```em
231
+ model "Catalog (CRUD shape)"
232
+
233
+ persona Merchandiser
234
+
235
+ context Product
236
+
237
+ slice "Create Product" {
238
+ ui Product Form @Merchandiser
239
+ command Create Product
240
+ event Product Created @Product
241
+ }
242
+
243
+ slice "Update Product" {
244
+ ui Product Form @Merchandiser
245
+ command Update Product
246
+ event Product Updated @Product
247
+ }
248
+
249
+ slice "Archive Product" {
250
+ ui Product Form @Merchandiser
251
+ command Archive Product
252
+ event Product Archived @Product
253
+ }
254
+
255
+ slice "Products" {
256
+ view Products from "Product Created", "Product Updated", "Product Archived"
257
+ ui Product List @Merchandiser
258
+ }
259
+ ```
260
+ ```em
261
+ model "Catalog (storyline shape)"
262
+
263
+ persona Merchandiser
264
+
265
+ context Product
266
+
267
+ storyline "Bring a product to market" {
268
+ slice "Create Product" {
269
+ ui Product Form @Merchandiser
270
+ command Create Product
271
+ event Product Created @Product
272
+ }
273
+
274
+ slice "Catalog" {
275
+ view Catalog from "Product Created"
276
+ ui Browse Catalog @Merchandiser
277
+ }
278
+
279
+ slice "Set Price" {
280
+ ui Pricing Panel @Merchandiser
281
+ command Set Price
282
+ event Price Set @Product
283
+ }
284
+
285
+ slice "Catalog with Price" {
286
+ view Catalog again from "Price Set"
287
+ ui Browse Catalog @Merchandiser
288
+ }
289
+ }
290
+
291
+ storyline "Retire a product" after "Bring a product to market" {
292
+ slice "Retire Product" {
293
+ ui Pricing Panel @Merchandiser
294
+ command Retire Product
295
+ event Product Retired @Product
296
+ }
297
+
298
+ slice "Catalog without Product" {
299
+ view Catalog again from "Product Retired"
300
+ ui Browse Catalog @Merchandiser
301
+ }
302
+ }
303
+ ```
304
+ The `timeline-entity-clustered` lint is the mechanical half (it fires only once two or more
305
+ contexts are written; this one-entity pair stays silent); once any storyline exists,
306
+ `slice-outside-storyline` flags slices left outside one.
307
+ - **Commands name intent, not endpoints** (Dilger: "Name by intent", "Every command has a reason"). One
308
+ endpoint may carry several intents. Signal of two intents in one command: invariants or field rules
309
+ that apply only "if `<field>` is set" — split it, and both commands may record the same event
310
+ (`CreateProduct` / `CreateProductVariant`, same `POST /products`, same `ProductCreated`). **Intent
311
+ test:** if a storyline step reads awkwardly with the command's name, the command is named after the
312
+ endpoint. `command-second-intent` and `command-repeated-across-slices` are the mechanical half.
313
+ - **A read model is one information need** (Dilger: "Every Read Model answers a question. No question,
314
+ no Read Model."). **Question test:** write the question each view answers; add a view only when no
315
+ existing one answers it. A nested route hints the child belongs in the parent's view; the real test
316
+ is whether the query needs information the parent view lacks. Child with references only → the parent
317
+ view carries ids; child with its own data → embedded in the parent view; child queried without the
318
+ parent id → its own view. Aggregate rules decide write-side consistency, never read models. *(em
319
+ ruling)*
320
+ - **Write side: children belong in the parent's context** when the child's invariants are checks against
321
+ the parent. A sub-resource schema shared across parent types is one event per parent on the parent's
322
+ context, not a shared aggregate — check the spec's event reference, not its resource section. Where
323
+ two slices record the same fact, repeat the event bare (same name, same context). *(em ruling)*
324
+ - **Repeat the view after every command that changes it** (`again`, span-1 — see State View above), so
325
+ the reader sees the information arrive rather than a view at the end.
326
+ - **Outcomes are events of one slice** (Dilger: "One decision. One event. One state change."; one
327
+ action with two outcomes is the legitimate exception): alternative events on one context in one slice
328
+ (`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
329
+ its own actor/SLA). Stated once for
330
+ automations in the Automation pattern (`docs/patterns.md#automation`); `automation-split-by-outcome`
331
+ and `retry-slice-of` are the mechanical half. Two forms, alternatives always on one context: inline `event A | B @Ctx`
332
+ (shared clauses and fields) and `oneof { … }` (each alternative keeps its own fields and clauses):
333
+ ```em
334
+ model "Payments"
335
+
336
+ persona Customer
337
+
338
+ context Payment
339
+
340
+ slice "Settle Payment" {
341
+ ui Pay Screen @Customer
342
+ command Settle Payment
343
+ event Payment Issued | Payment Failed @Payment { paymentId: UUID }
344
+ }
345
+
346
+ slice "Payment Status" {
347
+ view Payment Status from "Payment Issued", "Payment Failed"
348
+ ui Payment Result @Customer
349
+ }
350
+
351
+ slice "Refund Payment" {
352
+ ui Refund Screen @Customer
353
+ command Refund Payment
354
+ oneof {
355
+ event Refund Issued @Payment { paymentId: UUID, amount: Decimal }
356
+ event Refund Declined @Payment { paymentId: UUID, reason: String }
357
+ }
358
+ }
359
+
360
+ slice "Refund Status" {
361
+ view Refund Status from "Refund Issued", "Refund Declined"
362
+ ui Refund Result @Customer
363
+ }
364
+ ```
365
+ - **One event, two commands.** When two commands record the same fact, repeat the event bare
366
+ (`event X @Ctx`, same name and context) in the second slice: it inherits the first declaration, no
367
+ marker needed. Declaring different fields is `shared-event-field-mismatch`. (The arrow set for a
368
+ shared event read by a repeated view is under review: GH #328.)
369
+ ```em
370
+ model "Ordering"
371
+
372
+ persona Customer
373
+
374
+ context Sales
375
+
376
+ slice "Place Order" {
377
+ ui Checkout @Customer
378
+ command Place Order
379
+ event Order Placed @Sales { orderId: UUID }
380
+ }
381
+
382
+ slice "Order Status" {
383
+ view Order Status from "Order Placed"
384
+ ui Track Order @Customer
385
+ }
386
+
387
+ slice "Reorder" {
388
+ ui Reorder Button @Customer
389
+ command Reorder
390
+ event Order Placed @Sales
391
+ }
392
+
393
+ slice "Order Status after Reorder" {
394
+ view Order Status again from "Order Placed"
395
+ ui Track Order @Customer
396
+ }
397
+ ```
398
+ Combined with alternatives: when one alternative is a shared repeat, use `oneof` and write that
399
+ alternative bare so it inherits its fields (an inline line's fields would be copied onto every
400
+ alternative and mismatch):
401
+ ```em
402
+ model "Payments"
403
+
404
+ persona Customer
405
+
406
+ context Payment
407
+
408
+ slice "Settle Payment" {
409
+ ui Pay Screen @Customer
410
+ command Settle Payment
411
+ oneof {
412
+ event Payment Issued @Payment { paymentId: UUID }
413
+ event Payment Failed @Payment { paymentId: UUID, reason: String }
414
+ }
415
+ }
416
+
417
+ slice "Payment Status" {
418
+ view Payment Status from "Payment Issued", "Payment Failed"
419
+ ui Payment Result @Customer
420
+ }
421
+
422
+ slice "Retry Payment" {
423
+ ui Retry Screen @Customer
424
+ command Retry Payment
425
+ oneof {
426
+ event Payment Issued @Payment
427
+ event Payment Retry Declined @Payment { paymentId: UUID }
428
+ }
429
+ }
430
+
431
+ slice "Payment Status after Retry" {
432
+ view Payment Status again from "Payment Issued", "Payment Retry Declined"
433
+ ui Payment Result @Customer
434
+ }
435
+ ```
436
+ - **Reference-only PRD groups sit outside storylines.** A group that only carries reference material
437
+ (#327) is not a journey; leave it out of every `storyline` block.
438
+ - **Business terms in the events.** "If your Event Model doesn't reflect terms used by business experts —
439
+ especially in the recorded Events — that's a red flag." (Dilger, *Seven insights*.)
440
+
441
+ ### Reshaping an existing model
442
+
443
+ The upgrade path for a CRUD-shaped model. After each step run `em validate`.
444
+
445
+ `em storyline suggest <model>.em` proposes the `storyline` blocks as a diff (advisory, never
446
+ writes); `em upgrade` offers `collapse-alternative-events` and the opt-in `fold-continuations`.
447
+
448
+ 1. Name the processes (list each spec's walkthroughs or journeys, not endpoints): the main storyline and each secondary one.
449
+ 2. Reorder the slices into those stories and wrap each in a `storyline` block (`after "…"` where one
450
+ follows another); a slice that fits no story is a deletion candidate.
451
+ 3. Intent test per command: rename to the business intent; split any command with "if X is set" rules.
452
+ 4. Apply the information-need test to each read model and fold children into parents: merge views
453
+ answering the same question; add one only for an unanswered one.
454
+ 5. Move child-entity events onto the parent's context when the child's invariants check the parent.
455
+ 6. Repeat each view (`again`, span-1) after every command that changes it; drop the lone trailing view.
456
+ 7. Fold success/failure slice pairs into one slice with alternative events.
457
+ 8. Tooling, last: `em validate` (and `em validate --shape`, the 1.18.0 design exit gate), `em slice sync
458
+ --check` then `em slice sync`, `em slice index`, and regenerate TypeSpec/contracts last.
459
+
460
+ ---
461
+
197
462
  ## The 7 steps
198
463
 
199
464
  Run them in order. The skill groups them into phases: **discover = 1–4**, **model = 5–7**,
@@ -257,6 +522,9 @@ dedicated `slice` phase writes the full rich spec (see `../templates/slice.md`).
257
522
  - Prompts: *"Given what starting state, when this command/trigger fires, then what event(s)
258
523
  result? What must always be true? What are the failure paths? Which fields are essential and
259
524
  what are their rules?"*
525
+ - **Failure, retry, timeout surface here?** Add the outcome events and the scenarios to the
526
+ SAME slice, never a new slice per branch (Dymitruk: "a few of these stories to show how a
527
+ command can and cannot succeed" — Given/When/Then scenarios first).
260
528
  - **Existing codebase:** before finalizing field names/types, check adjacent real sources
261
529
  (OpenAPI specs, DB migrations, existing DTOs/event classes in sibling contexts) rather than
262
530
  guessing — mirrors `extract.md`'s whole-system grounding, applied per-slice.
@@ -68,17 +68,18 @@ to decide `frontmatter-invalid` without re-deriving frontmatter-shape rules of i
68
68
  | `reviewedOn` | string | `YYYY-MM-DD` | written only by `em slice review` (MIL-201); joined into `em export`'s `slice.doc.reviewedOn` (schema `1.11`) |
69
69
  | `ratifiedBy` | string | free text (typically a person's name) | written only by `em slice ratify` (MIL-165 — see [cli.md](https://github.com/milehimikey/em/blob/main/docs/cli.md#em-slice-ratify-file-slice-key---by-name)); joined into `em export`'s `slice.doc.ratifiedBy` (schema `1.8`) and `em slice index`'s Ratified by column |
70
70
  | `ratifiedOn` | string | `YYYY-MM-DD` | written only by `em slice ratify` (MIL-165); joined into `em export`'s `slice.doc.ratifiedOn` (schema `1.8`) |
71
- | `ratifiedRef` | string | full commit sha | written only by `em slice ratify` (MIL-284 — see [cli.md](https://github.com/milehimikey/em/blob/main/docs/cli.md#em-slice-ratify-file-slice-key---by-name)) when the model directory is inside a git repository: `HEAD` at ratification, the commit whose tree holds the body that was signed (ratify refuses a doc with uncommitted changes so this holds). Absent when ratified before 1.15 or outside git. Cleared by `em slice reratify`. Joined into `em export`'s `slice.doc.ratifiedRef` (schema `1.16`). Not a target-repo revision: `em slice conform --at` still names that itself |
72
- | `ratifiedHash` | string | `sha256:<64 hex>` | written only by `em slice ratify` (MIL-284): `sha256` over the doc body (everything after the frontmatter fence, EOLs normalised) — the signed content's identity without git. Cleared by `em slice reratify`. Read by `em validate --slice-ready` (`slice-ready-body-changed-since-ratification`); joined into `em export`'s `slice.doc.ratifiedHash` (schema `1.16`) |
71
+ | `ratifiedRef` | string | full commit sha | written only by `em slice ratify` (MIL-284 — see [cli.md](https://github.com/milehimikey/em/blob/main/docs/cli.md#em-slice-ratify-file-slice-key---by-name)) when the model directory is inside a git repository: `HEAD` at ratification, the commit whose tree holds the body that was signed (ratify refuses a doc with uncommitted changes so this holds). Absent when ratified before 1.15, outside git, or with `em slice ratify --no-ref` (GH #255 — for a repo whose squash/rebase merges would leave the commit unreachable from the target branch; `--no-ref` also removes a line already present). **An address, not the identity:** valid only while the commit stays reachable; `ratifiedHash` is what identifies the signed content. Cleared by `em slice revise`. Joined into `em export`'s `slice.doc.ratifiedRef` (schema `1.16`). Not a target-repo revision: `em slice conform --at` still names that itself |
72
+ | `ratifiedHash` | string | `sha256:<64 hex>` | written only by `em slice ratify` (MIL-284): `sha256` over the doc body (everything after the frontmatter fence, EOLs normalised) **minus what describes a later version** (GH #253, since 1.17.0): inside `## Open Questions` the heading line, blank lines and every `deferred to v<n>` item are left out, then trailing whitespace is trimmed — so `em slice defer` can record a question on a ratified or shipped doc without moving the hash, while answered `- [x]` history, any `- [ ]`, and every other section stay signed. The signed content's identity without git. A hash recorded before 1.17.0 (whole body) is still read as intact; `em upgrade`'s `ratified-hash-scope` step re-stamps it. Cleared by `em slice revise`. Read by `em validate --slice-ready` (`slice-ready-body-changed-since-ratification`) and by `ratify`'s same-sign-off re-run; joined into `em export`'s `slice.doc.ratifiedHash` (schema `1.16`) |
73
73
  | `shippedVersion` | integer | positive integer | written only by `em slice mark-implemented` (MIL-284): the doc's `version:` when the merge was recorded. With `shippedRef`/`shippedOn`/`implementedIn`, joined into `em export`'s `slice.doc.shipped` (schema `1.16`). Additive in 1.15 — `status`/`implementedIn` still carry the lifecycle for every reader |
74
- | `shippedRef` | string | full commit sha | written only by `em slice mark-implemented` (MIL-284): the doc's `ratifiedRef` at merge time, i.e. the ref the shipped version was signed at; absent when the doc had none |
74
+ | `shippedRef` | string | full commit sha | written only by `em slice mark-implemented` (MIL-284): the doc's `ratifiedRef` at merge time, i.e. the ref the shipped version was signed at; absent when the doc had none (ratified before 1.15, outside git, or with `--no-ref`, GH #255). Same caveat as `ratifiedRef`: an address, valid while reachable — `ratifiedHash` identifies the content |
75
75
  | `shippedOn` | string | `YYYY-MM-DD` | written only by `em slice mark-implemented` (MIL-284): the local date of the merge record, or `--on <date>` |
76
76
  | `meaningConfirmed` | boolean | `true` | written only by `em slice ratify --meaning-unchanged` (MIL-238 — see [cli.md](https://github.com/milehimikey/em/blob/main/docs/cli.md#em-slice-ratify-file-slice-key---by-name)): the API-first sign-off on a public-touching slice — this version does not change what the public contract means. Joined into `em export`'s `slice.doc.meaningConfirmed` (schema `1.15`; `false` when absent) |
77
77
  | `contractChange` | string | quoted free text (`contractChange: "<why>"`) | written only by `em slice ratify --contract-change "<why>"` (MIL-238): the API-first sign-off on a public-touching slice — why a consumer must read this change differently. Joined into `em export`'s `slice.doc.contractChange` (schema `1.15`, quotes stripped; `null` when absent) |
78
+ | `reviewSkipped` | string | a `status` value (`draft`, …) | written only by `em slice ratify --skip-review` (GH #260 — see [cli.md](https://github.com/milehimikey/em/blob/main/docs/cli.md#em-slice-ratify-file-slice-key---by-name)), and only when the bypass is what let the ratification through: the status the doc was ratified from. A ratification that passed the review gate writes nothing. Cleared by `em slice revise` with the rest of the sign-off. Joined into `em export`'s `slice.doc.reviewSkipped` (schema `1.18`, `null` when absent) and `em validate --slice-ready --json`'s top-level `reviewSkipped` (schema `1.3`) |
78
79
  | `owner` | string | free text (typically a person or team name) | hand-filled — no `em` command writes it; joined into `em export`'s `slice.doc.owner` (schema `1.9`, MIL-171), `em slice index`'s Owner column, and `em status`'s per-slice `owners[]` |
79
80
  | `tracking` | string | free text (typically an external ticket/issue URL) | hand-filled — no `em` command writes it; joined into `em export`'s `slice.doc.tracking` (schema `1.9`, MIL-171) and `em slice index`'s Tracking column. This is the exact field `em-tracker-bridge` reads to find the ticket mirroring this slice — `em` only stores and displays it, it never talks to a tracker itself |
80
81
  | `conformedVersion` | integer | positive integer | written only by `em slice conform` (MIL-214 — see [cli.md](https://github.com/milehimikey/em/blob/main/docs/cli.md#em-slice-conform-file-slice-key---at-rev)): this slice's `version:` at the time a conform sweep last certified it. Joined into `em export`'s `slice.doc.conformedVersion` (schema `1.13`); paired with `version` to compute `slice.doc.driftSignal`'s `"uncertified"` case |
81
- | `conformedAt` | string | free text (a target-repo revision, NOT a `<slice-key>@v<N>` lineage ref) | written only by `em slice conform` (MIL-214): the revision that certification sweep diffed against. Joined into `em export`'s `slice.doc.conformedAt` (schema `1.13`) and `em slice index`'s Conformed column |
82
+ | `conformedAt` | string | a target-repo revision, or `<kind>:<id>` (NOT a `<slice-key>@v<N>` lineage ref) | written only by `em slice conform` (MIL-214): the revision that certification sweep diffed against — or, since 1.17.0 (GH #261), an opaque identifier with an explicit kind (`build:1234`, `deploy:eu-1-2026-10-15`) for what was exercised when that is not a commit that stays reachable (a PR-branch HEAD under squash/rebase merges). A bare value is git (`parseConformedAt()`); reachability checks skip a kinded value. Joined into `em export`'s `slice.doc.conformedAt` (schema `1.13`) and `em slice index`'s Conformed column |
82
83
  | `conformedOn` | string | `YYYY-MM-DD` | written only by `em slice conform` (MIL-214). Joined into `em export`'s `slice.doc.conformedOn` (schema `1.13`) |
83
84
 
84
85
  Five keys — `schemaVersion`/`pattern`/`swimlane`/`status`/`version` — are what
@@ -88,6 +89,68 @@ over an omission — it reports). `implementedIn` and the lineage keys are the g
88
89
  optional ones; `implementedIn`'s required-once-implemented rule in the table below is
89
90
  convention, not parser-enforced.
90
91
 
92
+ ## Who may ratify
93
+
94
+ The schema records *who* signed (`reviewedBy`, `ratifiedBy`) but, by default, accepts any
95
+ non-empty name: who is allowed to sign is the host repo's decision. A host that wants em to
96
+ enforce it supplies an optional **roster**, `ratifiers.yaml`, beside the model. See
97
+ [examples/order-fulfillment/ratifiers.yaml](https://github.com/milehimikey/em/blob/main/examples/order-fulfillment/ratifiers.yaml).
98
+
99
+ ```yaml
100
+ rosterSchemaVersion: "1.0"
101
+ ratifiers:
102
+ - name: Aaron McKay
103
+ contexts: [Product, Attribute] # omit = the whole model
104
+ - name: Mike Key
105
+ - name: Dana Lee
106
+ models: [pim] # omit = every model the roster governs
107
+ reviewers: # optional; same shape, checked by `em slice review --by`
108
+ - name: Mike Key
109
+ ```
110
+
111
+ - **Schema.** `rosterSchemaVersion` is required (`"1.0"`); at least one of `ratifiers:` /
112
+ `reviewers:` is required; each is a non-empty list of `{ name, contexts?, models? }`. Unknown
113
+ keys are errors (`roster-invalid`). A missing `ratifiers:` / `reviewers:` key leaves that role
114
+ unrestricted.
115
+ - **Placement.** The nearest `ratifiers.yaml` walking up from the model's directory, stopping at
116
+ the nearest directory holding `.git` (or the filesystem root). If no `.git` exists anywhere
117
+ above the model, only the model's own directory is searched. `--roster <path>` on `ratify`,
118
+ `review`, `validate --slice-ready`, `roster check` overrides discovery; a path that does not
119
+ exist is an error, never "no roster".
120
+ - **A slice's contexts** are the contexts of its **events** plus the contexts of the events its
121
+ **views read** - each view instance's **own** `from`, never a union across the read model's
122
+ instances (adding a later `again` instance never changes an earlier slice's contexts). A
123
+ continuation slice also takes its originating instance's own `from`. A slice with neither has no
124
+ context and is permitted only by entries without `contexts`.
125
+ A continuation fed from a different context than its originating view (e.g. `tool-shows-on-loan` =
126
+ Loan + Tool) therefore needs a ratifier permitted for every one of those contexts, so a team with
127
+ one lead per context needs a joint or whole-model roster entry for such slices.
128
+ - **An entry permits a slice** when it applies to the model and either has no `contexts` or lists
129
+ **every** context of the slice: a cross-context slice needs a ratifier who may sign for all of
130
+ them (a leading `@` is tolerated in `contexts`).
131
+ - **Per-model scoping.** `models: [<modelKey>]` (the key `em export` reports as `model.key`) limits
132
+ an entry to those models; an entry without it applies to every model the roster governs. Context
133
+ names are matched **per model** against that model's declared contexts, so a shared root roster
134
+ needs `models:` whenever the models' ratifiers differ. A `models:` key the enclosing
135
+ `system.yaml` does not list is `roster-invalid`.
136
+ - **Validation against the model.** A `contexts:` entry the model does not declare (case-sensitive)
137
+ is `roster-invalid`, naming the context; only entries that apply to the model are checked.
138
+ - **File absent: today's behaviour**, no warning. **File present but invalid: fail closed** -
139
+ `ratify` and `review` refuse, `--slice-ready` and `roster check` report `roster-invalid`.
140
+ - **`grandfathered (unsigned before em 1.14)`** is em's own migration marker (`em upgrade`'s
141
+ `ratified-signoff`), not a signer: exempt from the `--slice-ready` check and from `roster check`
142
+ (which counts it) until the slice is re-ratified - but only the **exact** string, and only on a
143
+ doc with no `ratifiedHash` (the state `em upgrade` leaves). `grandfathered by X`, or the marker on
144
+ a doc that carries a hash, is a signer and is checked.
145
+ - **Where it is enforced.** At write time by `em slice ratify` / `em slice review` (the refusal
146
+ lists the permitted names); at check time by `em validate --slice-ready <key>`
147
+ (`ratified-by-not-in-roster`, error - one `ready-to-implement` slice, ratifier only) and by
148
+ `em roster check <model>` (every doc, `ratifiedBy` on `ready-to-implement` and `implemented`,
149
+ `reviewedBy` on any), the command `em ci init` wires into CI when a roster exists - see
150
+ [ci.md](ci.md#ratifier-roster-gate). `em status` reports `roster: present|absent|invalid`.
151
+ - `em roster init <model>` seeds the file from the names already in the docs
152
+ ([cli.md](https://github.com/milehimikey/em/blob/main/docs/cli.md#em-roster-init-file)).
153
+
91
154
  ## Required vs optional, by `status`
92
155
 
93
156
  | Key | draft | reviewed | ready-to-implement | implemented |
@@ -99,6 +162,7 @@ convention, not parser-enforced.
99
162
  | `covers` | optional in every state — present only on a doc that deliberately also serves another slice (MIL-121, see below); most docs never carry it |
100
163
  | `reviewedBy`, `reviewedOn` | optional in every state — present once `em slice review` (MIL-201) has run at least once; a doc that never went through the review gate, or was hand-flipped to `reviewed`, simply omits both. `em slice ratify` never clears them; `em slice reratify` does (they describe the version that shipped) |
101
164
  | `ratifiedBy`, `ratifiedOn` | optional in every state, but `ready-to-implement` needs `ratifiedBy` to pass `em validate --slice-ready` (`slice-ready-not-ratified`, MIL-259) — present once `em slice ratify` (MIL-165) has run; `em slice reratify` clears both. `em upgrade` (`ratified-signoff`, 1.14) grandfathers a `version: 1` `ready-to-implement` doc that never had one by writing `ratifiedBy: "grandfathered (unsigned before em 1.14)"` and `ratifiedOn:` the upgrade date |
165
+ | `reviewSkipped` | optional in every state — present only when `em slice ratify --skip-review` is what let the ratification through (GH #260); the value is the status it skipped from. Written right after `ratifiedOn:`; cleared by `em slice revise` with the rest of the sign-off. Absent on every ratification that passed the review gate, and on docs ratified with `--skip-review` before 1.17.0 |
102
166
  | `meaningConfirmed`, `contractChange` | optional in every state, at most one present — written right after `ratifiedOn:` by `em slice ratify` and right after `version:` by `em slice reratify` (MIL-238). On a **public-touching** slice (one owning a `public` command, event or view) both commands refuse without `--meaning-unchanged` or `--contract-change "<why>"`, so a doc ratified on 1.14+ carries one; elsewhere either is optional. `em slice reratify` clears both before writing its own (they describe the prior version, same as `reviewedBy`/`ratifiedBy`). `em status` counts public-touching `ready-to-implement`/`implemented` docs with neither (`publicSlicesUnconfirmed`) |
103
167
  | `owner`, `tracking` | optional in every state — hand-filled whenever a team wants a who-holds-this / external-tracker link; most docs never carry either (MIL-171) |
104
168
  | `conformedVersion`, `conformedAt`, `conformedOn` | optional in every state — present once `em slice conform` (MIL-214) has run at least once for the CURRENT version; a doc that's never been through a conform sweep, or whose `version` has since bumped past what was certified, simply has none of the three (or a stale triple — see `driftSignal`'s `uncertified` case below) |
@@ -260,6 +324,21 @@ the union of events every later `again` instance of that view reads via `from`,
260
324
  order — so implementing the read model from one export never means walking every `again`
261
325
  slice's doc by hand to assemble the full event list.
262
326
 
327
+ The generated `em-slice-view` region follows the same rule (GH #280, 1.18.0): its `built from
328
+ events:` line lists the union of `from` across the originating instance and every `again`
329
+ instance (originating first, then timeline order, deduped), and each field's `from event` note
330
+ resolves against that union (each later event annotated `(via "<slice>")` with its instance's slice) — whichever instance's slice the doc is bound to. `em slice sync`
331
+ rewrites existing view regions accordingly.
332
+
333
+ **Synthesized continuations (GH #287).** A `view X again` line written inside a command's own slice
334
+ is lifted into a continuation column right after it. That column is a continuation like any other
335
+ (`continuationOf` names the view's originating slice, `alsoReads` counts its events) but exists
336
+ only as sugar over the host slice's text: its export key is `<host-slice-key>--again` (one column per host slice, its lifted lines stacked),
337
+ it never has a doc, and `em slice index` lists no row for it. A slice key never contains `/` (the slug generator emits single dashes only, so `--` cannot occur in a hand-written key); the `/` in an element ref `<slice-key>/<kind>.<slug>` is always the first and only one. A hand-written continuation slice
338
+ may also hold several `again` views, and may omit its name (auto-named `"<View> View: <Event>"`).
339
+ When one slice stacks views of different read models, `continuationOf` names the originating
340
+ slice of the FIRST view; the others are listed in their originating slices' `alsoReads` (with this slice's key as `atSlice`).
341
+
263
342
  **Authoring convention:** a continuation's behavior belongs in the originating doc's own
264
343
  `## Scenarios (Given / When / Then)` section — one scenario per event a later `again` instance
265
344
  feeds — rather than in a doc of its own; no new frontmatter is needed to record it.
@@ -396,7 +475,7 @@ re-run `em slice sync`.
396
475
  |---|---|---|
397
476
  | `em-slice-command` | `## Command / Input` | `**Command:** \`Name\`` + `\| Field \| Type \| Required \| Rules / Validation \|` (Required is `no` for a `name?: Type` field) |
398
477
  | `em-slice-event` | `## Event(s) Emitted` | `**Event:** \`Name\` → context \`Ctx\`` + `\| Field \| Type \| Immutable Fact? \| Source / Notes \|` (Source: the same-slice command field it is copied from, `assigned` for a system-assigned field, else `{{ }}`) |
399
- | `em-slice-view` | `## Read Model / View` | `- **View:** \`Name\` built from events: …` + `\| Field \| Type \| Source / Notes \|` (Source: the source event carrying the field, `Derived` for a `derived` field, else `{{ }}`) |
478
+ | `em-slice-view` | `## Read Model / View` | `- **View:** \`Name\` built from events: …` (the union across the originating and every `again` instance; each later event is annotated `(via "<slice>")` with the slice of the instance that adds it) + `\| Field \| Type \| Source / Notes \|` (Source: the source event, from that union, carrying the field, `Derived` for a `derived` field, `Collection of \`T\`, fed by …` for a `T[] derived from …` field — followed by one neutral Given/Then scenario stub per named event under the table, to copy into `## Scenarios` and complete there (the region itself stays read-only) — else `{{ }}`) |
400
479
  | `em-slice-invariants` | `## Invariants / Business Rules` | one `- **INV-X** — rule` line per model-declared invariant of the slice's commands and events |
401
480
 
402
481
  A slice with several elements of one kind gets one region per element, the name suffixed with
@@ -435,12 +514,22 @@ case is a block:
435
514
  - **Given:**
436
515
  - a basket whose total is 0
437
516
  - **When:**
438
- - the customer submits the order
517
+ - the customer issues PlaceOrder
439
518
  - **Then:**
440
519
  - the command is rejected with a validation error
441
520
  - no event is recorded
442
521
  ```
443
522
 
523
+ **Scenario quality (citation convention).** A scenario is tied to exactly one command: its When
524
+ names the slice's command (a reaction slice: the command it triggers), its Then names an event
525
+ the slice records or a rejection/error, and it **cites the `INV-…` ids it exercises** anywhere in
526
+ the block (the title above, or a Given/When/Then bullet). Every invariant declared for the slice
527
+ (an `invariant` line in the model, or the doc's Invariants section) should be cited by at least one
528
+ scenario. `em validate` advises on all three — `scenario-when-not-command`,
529
+ `scenario-then-not-outcome`, `invariant-without-scenario` — see
530
+ [validation.md](https://github.com/milehimikey/em/blob/main/docs/validation.md#scenario-quality). Older bullet-format scenarios are checked too; a doc with no scenarios is never
531
+ flagged.
532
+
444
533
  A `### Scenario: <title>` heading, then the three column-0 bullets `- **Given:**`, `- **When:**`,
445
534
  `- **Then:**`, each with nested ` - ` items (text after the label on the same line counts as a
446
535
  first item). `em export --json` publishes the parsed blocks as `slice.doc.scenarios`
@@ -491,7 +580,11 @@ not deleted: `em slice defer` checks it and rewrites it as `- [x] <original> —
491
580
  `em slice revise` (and `mark-implemented`, which opens v<n> at merge when something is deferred to it) reads: when the doc reaches v<n> it rewrites each such item back to
492
581
  `- [ ] <original>`, so the question must be resolved or re-deferred before `--slice-ready` passes
493
582
  again. The prune rule above does not apply to a deferred item before v<n> (it stays as the
494
- record); once reopened it is an ordinary open question.
583
+ record); once reopened it is an ordinary open question. A deferred item describes v<n>, not the
584
+ version that was signed, so it sits **outside `ratifiedHash`** (GH #253, with the section heading
585
+ and its blank lines; see the `ratifiedHash` row above) — which is what lets `em slice defer` write
586
+ one into a ratified or shipped doc. Every other line of the section — an answered `- [x]`, a hand-added
587
+ `- [ ]` — is inside the hash and moves it.
495
588
 
496
589
  **Not a parsing/grammar change.** `countOpenQuestions()` (`sliceDoc.ts`) and
497
590
  `slice-ready-open-questions-unchecked` (`sliceReadyValidate.ts`,
@@ -515,6 +608,13 @@ one it is asking about:
515
608
  slice shipped, and which" is `shippedRecordOf()` (`src/catalog/sliceDoc.ts`); `em export`
516
609
  publishes its answer as `slice.doc.shipped`.
517
610
 
611
+ **Reading a PR that shows `status: draft`.** When `mark-implemented` opened the next version
612
+ (`status: draft`, `version: <N+1>`), the diff of that slice doc does not say "never marked": the
613
+ `shippedVersion: <N>` / `implementedIn:` lines **are** the record that v<N> was implemented, and
614
+ `status:` is the state of v<N+1>. The command prints a notice naming both
615
+ ([cli.md](https://github.com/milehimikey/em/blob/main/docs/cli.md#em-slice-mark-implemented-file-slice-key-pr-url)), and `em status` lists the slice
616
+ as `implemented (v<N>) · draft v<N+1> open` rather than a bare `draft`.
617
+
518
618
  So a shipped slice can be a `draft` again. `em slice revise <key>`
519
619
  ([cli.md](https://github.com/milehimikey/em/blob/main/docs/cli.md#em-slice-revise-file-slice-key)) opens v<N+1> as `status: draft`, bumps
520
620
  `version:`, clears the prior version's review and sign-off, and leaves `implementedIn`/`shipped*`
@@ -522,6 +622,10 @@ alone — v<N> stays shipped. `em slice mark-implemented` does the same automati
522
622
  question was deferred to v<N+1> (`em slice defer`), so known work is on the list the moment the
523
623
  previous version lands. Version 2 then takes both human gates like version 1 did: `em slice review
524
624
  --by`, then `em slice ratify --by` (which records a fresh `ratifiedRef`/`ratifiedHash`).
625
+ A ratified doc that never shipped may instead be reopened at the **same** `version:` with
626
+ `em slice revise --withdraw --keep-version` (GH #262) — a correction before a first release, not a
627
+ delta, so no `## Delta` is owed; the status flip and sign-off clearing are the same. Refused on a
628
+ shipped doc: the shipped version keeps its number.
525
629
 
526
630
  The deliberate mismatch — `version: 2` open while `implementedIn`/`shippedVersion` still name v1's
527
631
  work — is not staleness, it's the **drift signal**: `em export`'s `slice.doc.driftSignal` is
@@ -546,14 +650,17 @@ ratification of v<N> is withdrawn and nothing shipped, so no shipped record is w
546
650
  withdraws the only implementable version, `em slice revise` **refuses it without `--withdraw`**
547
651
  (GH #236).
548
652
 
549
- **Questions found against a ratified or shipped version are queued, not written into the body**
550
- (GH #236). `ratifiedHash` covers the whole body, `## Open Questions` included, so `em slice defer` on
551
- a `ready-to-implement` + `ratifiedBy:` or `implemented` doc does not edit the doc: it records the
552
- question in the model's `.event-modeling.md` as queued for `<key>@v<N+1>` (exact bullet shape in
553
- [cli.md](https://github.com/milehimikey/em/blob/main/docs/cli.md#em-slice-defer-file-slice-key-question)), keeping `ratifiedHash` and `--slice-ready`
554
- valid. When v<N+1> opens as a draft — `mark-implemented` at merge, or `revise` — each queued item is
555
- injected into that draft's `## Open Questions` (unchecked, carrying its decision) and the queue is
556
- cleared. `em export` reports the waiting count as `slice.doc.queuedForNext` (schema `1.17`). **A
653
+ **Questions found against a ratified or shipped version are written into the body, outside the
654
+ hash** (GH #253, superseding GH #236's 1.16 queue). `ratifiedHash` leaves out the `## Open Questions`
655
+ heading and its `deferred to v<n>` items, so `em slice defer` on a `ready-to-implement` +
656
+ `ratifiedBy:` or `implemented` doc appends the already-deferred item
657
+ `- [x] <question> — v<N>: <decision>; deferred to v<n> (<date>[, <by>])` to the doc — visible to
658
+ every reader of the ratified doc — and `ratifiedHash`, `--slice-ready` and `ratify`'s re-run all
659
+ stay valid. When v<n> opens as a draft — `mark-implemented` at merge, or `revise` — the item is
660
+ reopened as `- [ ] <question>` exactly as a pre-ratification deferral is. A question `em slice
661
+ defer` queued in `.event-modeling.md` under 1.16.x is still injected when the next draft opens, and
662
+ `em upgrade`'s `ratified-hash-scope` step moves any leftover into its doc. `em export` reports the
663
+ work waiting on later versions as `slice.doc.queuedForNext` (schema `1.17`). **A
557
664
  `ready-to-implement` doc with no `ratifiedBy`** is the leftover of a pre-1.15 `reratify` (which
558
665
  bumped the version and left the doc unsigned at that status); no command produces it any more and
559
666
  `em slice ratify` no longer accepts it — `em slice revise` reopens it as a `draft` at the same
@@ -210,15 +210,23 @@ an event directly.}}
210
210
  --json` → slice.doc.scenarios). Cover the happy path AND the key rule boundaries, one block
211
211
  per case: a `### Scenario: <title>` heading, then exactly the three column-0 bullets
212
212
  `- **Given:**`, `- **When:**`, `- **Then:**`, each with its beats as nested ` - ` bullets.
213
- A block missing any of the three is flagged `slice-doc/structured-section-malformed`. -->
213
+ A block missing any of the three is flagged `slice-doc/structured-section-malformed`.
214
+ Quality (advisory warnings): each scenario is tied to exactly ONE command — its When names
215
+ this slice's command (a reaction slice: the reaction, its view, or the command it triggers); its Then names an event (or a stated no-op)
216
+ this slice records, or a rejection/error; and it CITES the INV ids it exercises (in the
217
+ title or any bullet). Every INV declared for the slice should be cited by some scenario
218
+ (`scenario-when-not-command`, `scenario-then-not-outcome`, `invariant-without-scenario`). -->
214
219
  ### Scenario: Happy path
215
220
  - **Given:**
216
221
  - {{starting state / prior events}}
217
222
  - **When:**
218
- - {{command/trigger}}
223
+ - {{the command, by name}}
219
224
  - **Then:**
220
- - {{event(s) recorded}}
225
+ - {{the event(s) recorded, by name}}
221
226
  - {{resulting read-model change}}
227
+ <!-- Add one block per rule boundary, citing the INV id in the title, e.g.
228
+ `### Scenario: Non-positive total is rejected (INV-CHK-1)` — When names the command, Then
229
+ names the rejection. -->
222
230
 
223
231
  ## Alternate & Error Flows
224
232
  <!-- Failure paths, retries, compensations, timeouts, idempotency. -->
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: conform
3
- em-version: 1.16.1
3
+ em-version: 1.18.0
4
4
  description: >-
5
5
  Use when checking a ratified event model (and its slice docs) for drift against the codebase
6
6
  that implements it — advisory only, never a gate, never an unprompted edit — or when walking
@@ -45,7 +45,8 @@ Ruling on a finding is a human gate, like ratifying a slice
45
45
  decides, and `em conform-supersede ... --locus <model|doc|code|none> --by <name>` records the
46
46
  ruling on the findings record (and stamps the report) — never rule on your own judgment. Once
47
47
  every finding touching a slice is ruled, `em slice conform <model-name>.em <key> --at <rev>`
48
- certifies that version of the slice.
48
+ certifies that version of the slice (`--at build:<id>` / `deploy:<id>` when the swept commit
49
+ will not stay reachable — a PR branch under squash/rebase merges; GH #261).
49
50
 
50
51
  End of phase: state file's `Last conformance:` marker updated (via `em state set-conformance` —
51
52
  refuses while any `implemented` slice still has an unruled finding; `--partial` escapes with a
@@ -320,8 +320,15 @@ slice with no unruled findings left:
320
320
 
321
321
  ```
322
322
  em slice conform <model-name>.em <slice-key> --at <target-repo revision>
323
+ em slice conform <model-name>.em <slice-key> --at <kind>:<id> # e.g. build:1234 — what the sweep exercised, when its commit will not stay reachable (GH #261)
323
324
  ```
324
325
 
326
+ If you certified from a pull-request branch and the repo squash- or rebase-merges, the branch
327
+ HEAD will dangle once the branch is deleted. Either pass a commit the target branch will keep
328
+ (the merge commit) or record the deployment/build you actually swept as `--at build:<id>` /
329
+ `--at deploy:<id>` / `--at env:<id>` — and write the same string as the findings file's
330
+ `revision`, so the findings-check gate still matches.
331
+
325
332
  — records `conformedVersion`/`conformedAt`/`conformedOn` on that slice's doc, so
326
333
  `driftSignal` reads `in-sync` again (it reads `uncertified` for an implemented slice that's never
327
334
  been through this) instead of just trusting `implementedIn`. Legal only for `status: implemented`