@milehimikey/em 1.19.6 → 1.20.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 (102) 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-conform/reference/conform.md +32 -4
  4. package/.claude/skills/event-modeling-design/SKILL.md +14 -3
  5. package/.claude/skills/event-modeling-discover/SKILL.md +1 -1
  6. package/.claude/skills/event-modeling-discover/reference/extract.md +8 -0
  7. package/.claude/skills/event-modeling-engagement/SKILL.md +1 -1
  8. package/.claude/skills/event-modeling-implement/SKILL.md +7 -2
  9. package/.claude/skills/event-modeling-implement/reference/implement.md +73 -6
  10. package/.claude/skills/event-modeling-review/SKILL.md +1 -1
  11. package/.claude/skills/event-modeling-shared/reference/em-dsl.md +24 -4
  12. package/.claude/skills/event-modeling-shared/reference/methodology.md +16 -0
  13. package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +1 -1
  14. package/.claude/skills/event-modeling-shared/templates/slice.md +6 -1
  15. package/.claude-plugin/marketplace.json +2 -2
  16. package/README.md +11 -1
  17. package/dist/catalog/docBadge.d.ts +16 -0
  18. package/dist/catalog/docBadge.js +50 -0
  19. package/dist/catalog/docBadge.js.map +1 -0
  20. package/dist/catalog/docModelConsistencyValidate.js +9 -2
  21. package/dist/catalog/docModelConsistencyValidate.js.map +1 -1
  22. package/dist/catalog/invTokens.d.ts +49 -0
  23. package/dist/catalog/invTokens.js +94 -0
  24. package/dist/catalog/invTokens.js.map +1 -0
  25. package/dist/catalog/pages.js +48 -1
  26. package/dist/catalog/pages.js.map +1 -1
  27. package/dist/catalog/scenarios.d.ts +4 -0
  28. package/dist/catalog/scenarios.js +4 -1
  29. package/dist/catalog/scenarios.js.map +1 -1
  30. package/dist/catalog/sliceDoc.d.ts +46 -1
  31. package/dist/catalog/sliceDoc.js +90 -2
  32. package/dist/catalog/sliceDoc.js.map +1 -1
  33. package/dist/catalog/sliceDocRender.d.ts +3 -0
  34. package/dist/catalog/sliceDocRender.js +295 -0
  35. package/dist/catalog/sliceDocRender.js.map +1 -0
  36. package/dist/cli/conformSupersede.js +13 -3
  37. package/dist/cli/conformSupersede.js.map +1 -1
  38. package/dist/cli/coverage.d.ts +13 -23
  39. package/dist/cli/coverage.js +51 -91
  40. package/dist/cli/coverage.js.map +1 -1
  41. package/dist/cli/markImplemented.js +21 -3
  42. package/dist/cli/markImplemented.js.map +1 -1
  43. package/dist/cli/refsCheck.d.ts +52 -0
  44. package/dist/cli/refsCheck.js +127 -1
  45. package/dist/cli/refsCheck.js.map +1 -1
  46. package/dist/cli/sliceConform.d.ts +100 -1
  47. package/dist/cli/sliceConform.js +182 -9
  48. package/dist/cli/sliceConform.js.map +1 -1
  49. package/dist/cli/status.js +4 -3
  50. package/dist/cli/status.js.map +1 -1
  51. package/dist/cli/watchStoryline.d.ts +27 -0
  52. package/dist/cli/watchStoryline.js +55 -0
  53. package/dist/cli/watchStoryline.js.map +1 -0
  54. package/dist/cli.js +184 -24
  55. package/dist/cli.js.map +1 -1
  56. package/dist/emit/docJson.d.ts +99 -0
  57. package/dist/emit/docJson.js +197 -0
  58. package/dist/emit/docJson.js.map +1 -0
  59. package/dist/emit/refsJson.d.ts +5 -3
  60. package/dist/emit/refsJson.js +5 -2
  61. package/dist/emit/refsJson.js.map +1 -1
  62. package/dist/mcp/server.js +23 -3
  63. package/dist/mcp/server.js.map +1 -1
  64. package/dist/model/continuation.d.ts +9 -0
  65. package/dist/model/continuation.js +30 -0
  66. package/dist/model/continuation.js.map +1 -1
  67. package/dist/model/rules.d.ts +8 -0
  68. package/dist/model/rules.js +8 -0
  69. package/dist/model/rules.js.map +1 -1
  70. package/dist/model/shapeLint.js +48 -0
  71. package/dist/model/shapeLint.js.map +1 -1
  72. package/dist/model/validate.js +119 -0
  73. package/dist/model/validate.js.map +1 -1
  74. package/dist/parser/ast.d.ts +8 -0
  75. package/dist/parser/parser.js +51 -1
  76. package/dist/parser/parser.js.map +1 -1
  77. package/dist/render/render.js +1 -1
  78. package/dist/render/render.js.map +1 -1
  79. package/dist/render/sliceOverlay.d.ts +7 -2
  80. package/dist/render/sliceOverlay.js +28 -3
  81. package/dist/render/sliceOverlay.js.map +1 -1
  82. package/dist/render/storylinePicker.d.ts +9 -0
  83. package/dist/render/storylinePicker.js +17 -0
  84. package/dist/render/storylinePicker.js.map +1 -0
  85. package/dist/render/viewerHtml.js +1 -1
  86. package/dist/render/viewerHtml.js.map +1 -1
  87. package/package.json +1 -1
  88. package/plugin/.claude-plugin/plugin.json +2 -2
  89. package/plugin/shared/reference/em-dsl.md +24 -4
  90. package/plugin/shared/reference/methodology.md +16 -0
  91. package/plugin/shared/reference/slice-doc-schema.md +1 -1
  92. package/plugin/shared/templates/slice.md +6 -1
  93. package/plugin/skills/conform/SKILL.md +1 -1
  94. package/plugin/skills/conform/reference/conform.md +32 -4
  95. package/plugin/skills/design/SKILL.md +14 -3
  96. package/plugin/skills/discover/SKILL.md +1 -1
  97. package/plugin/skills/discover/reference/extract.md +8 -0
  98. package/plugin/skills/engagement/SKILL.md +1 -1
  99. package/plugin/skills/event-modeling/SKILL.md +1 -1
  100. package/plugin/skills/implement/SKILL.md +7 -2
  101. package/plugin/skills/implement/reference/implement.md +73 -6
  102. package/plugin/skills/review/SKILL.md +1 -1
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: event-modeling
3
- em-version: 1.19.6
3
+ em-version: 1.20.0
4
4
  description: >-
5
5
  Use when the user runs `/event-modeling` (with or without a phase name), or wants to resume an
6
6
  event-modeling session without specifying which part of the process applies. Reads the model's
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: event-modeling-conform
3
- em-version: 1.19.6
3
+ em-version: 1.20.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
@@ -39,7 +39,9 @@ difference is what you do with what you find: extract *builds* a model from the
39
39
  (or the status the user says corresponds to shipped code) for whichever slices are in scope.
40
40
  Checking a `draft` or `ready-to-implement` slice against code is a wasted walk — ratified or
41
41
  not, nobody has built to it yet; tell the user and skip it rather than reporting "drift"
42
- against it.
42
+ against it. (GH #400: the ONE exception is a deliberate pre-ship advisory run — see
43
+ `--preview` under step 5 below — not a reason to treat this precondition as optional for an
44
+ ordinary post-ship sweep.)
43
45
  3. The codebase to check: the state file's **Existing system refs** (populated during
44
46
  `extract`, if this model came from one) or ask the user for the repo path if not set. Record
45
47
  it if missing. In a headless/scheduled run with no user to ask, default to the repository
@@ -335,6 +337,32 @@ been through this) instead of just trusting `implementedIn`. Legal only for `sta
335
337
  with a link, and itself refuses if an unruled finding for that slice still exists at this
336
338
  revision — the mechanical proof this step comes only after rulings, not before.
337
339
 
340
+ **Pre-ship advisory mode: `--preview` (GH #400).** The certify step above is legal only for a
341
+ **shipped** slice (`status: implemented`/`shippedVersion:` + `implementedIn:`) — precondition 2
342
+ reflects that this phase ordinarily walks shipped code. `--preview` is the deliberate exception:
343
+ the SAME doc-resolution and findings-check read, legal for a slice that hasn't shipped yet
344
+ (the shipped/`implementedIn:` preconditions are waived), and it **never writes anything** —
345
+ no `conformedVersion`/`conformedAt`/`conformedOn`, ever, regardless of outcome:
346
+
347
+ ```
348
+ em slice conform <model-name>.em <slice-key> --at <target-repo revision> --preview
349
+ ```
350
+
351
+ Use this to run a conform sweep against your OWN implementing branch before it merges — catching
352
+ spec/code drift while you can still fix it on the same branch, rather than after shipping. The
353
+ walk itself (steps 1-4 above) is unchanged; only the certify step's preconditions differ. Output
354
+ is ephemeral by default (nothing is written, not even to `conformance/`); pass
355
+ `--write-findings` to persist the run's own unruled findings as a normal
356
+ `conformance/<date>-<slice-key>-preview-findings.json` (this is deliberate — `docs/cli.md`'s
357
+ `em slice conform --preview` section documents the exact shape and naming). Know before opting
358
+ in: the written file has no sibling report (`report` is synthetic), so `em conform-supersede`
359
+ refuses to rule on it — unruled entries in it gate certify/`set-conformance` at that revision
360
+ until you rule them in the file by hand, delete the snapshot, or author the sibling report.
361
+ `em state set-conformance` and the unruled-findings gates are unaffected by
362
+ this mode either way: a default preview run writes nothing for them to read, so it can never
363
+ block a later certify or `set-conformance` call. There is no `--slice-ready` integration for this
364
+ mode yet.
365
+
338
366
  End of run: update the state file's `Last conformance:` marker with
339
367
 
340
368
  ```
@@ -342,7 +370,7 @@ em state set-conformance <target-repo revision> --report conformance/YYYY-MM-DD-
342
370
  ```
343
371
 
344
372
  — the revision is the one you just diffed against, so the next run's scope starts from here.
345
- The command writes the exact format step 1 parses back out; never hand-edit the bullet.
373
+ The command writes the exact format step 1 parses back out; never hand-edit the bullet. Gates read every same-revision findings file (the report's own `-findings.json` first, else every file whose `revision` matches). The same-revision lookup uses the report's OWN revision (its `Target repo: … @ <rev>` header line; the caller's `<rev>` / `--as-of` only when the report has none).
346
374
  **Refuses (MIL-214) while any `implemented` slice still has an unruled finding** in the findings
347
375
  JSON beside the report — rule on every finding first (or certify what's already ruled via `em
348
376
  slice conform` above), or pass `--partial` to record the marker with a loud notice instead
@@ -363,7 +391,7 @@ a ruling you're only banner-stamping after the fact), run the same command witho
363
391
  em conform-supersede <model-name>.em conformance/YYYY-MM-DD-report.md --as-of <target-repo revision> --findings <spec>
364
392
  ```
365
393
 
366
- — stamping the report with a "superseded as of `<rev>`" banner so a later reader who finds it
394
+ — a ruling is written to the file the report names (`<x>-report.md` <-> `<x>-findings.json`; with only per-slice files at that revision, pass the per-slice report or it refuses; `--as-of` is NOT the lookup key — the report's own `Target repo: … @ <rev>` header revision is, and `--as-of` only when the report has none) — stamping the report with a "superseded as of `<rev>`" banner so a later reader who finds it
367
395
  (a search hit, a bookmark, a link from an old PR description) sees immediately that its
368
396
  file:line citations describe an ancestor of the current model, not its current state. This is
369
397
  what keeps a superseded report an honest piece of history rather than a silently-misleading
@@ -379,7 +407,7 @@ each call adds its own stamp rather than overwriting the last.
379
407
  the gitignoring (`*-asis.em`, idempotent) mechanically, never built up from nothing and never
380
408
  hand-maintained. It's regenerated every conform run.
381
409
  - **Report location:** `conformance/<YYYY-MM-DD>-report.md` in the model directory. One file
382
- per run; don't overwrite a prior date's report.
410
+ per run; don't overwrite a prior date's report. Its findings go in the same-stem `-findings.json`; gates read every same-revision findings file, so per-slice runs may write `<date>-<name>-findings.json`, and a ruling is written to the file the report names. The same-revision lookup uses the report's OWN revision (its `Target repo: … @ <rev>` header line; the caller's `<rev>` / `--as-of` only when the report has none).
383
411
  - **Red note wording:** `issue "conformance: <what's wrong, plainly>"` — plain enough that
384
412
  whoever ratifies it later understands the claim without re-reading the report.
385
413
  - **Naming/matching rules:** same past-tense-event / imperative-command / thing-shown-view
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: event-modeling-design
3
- em-version: 1.19.6
3
+ em-version: 1.20.0
4
4
  description: >-
5
5
  Use when structuring a draft event model into swimlanes and the four patterns (State Change,
6
6
  State View, Automation, Translation), evaluating a model's structural completeness, or writing
@@ -88,10 +88,14 @@ Goal: a structurally complete, **validated** model with correct patterns and swi
88
88
  shape — reshape it with the "Reshaping an existing model" checklist. Then the **intent test**
89
89
  per command (would a business person say this name at this step in the story? an endpoint-shaped
90
90
  name fails), the **question test** per view (write the question it answers; no question, no
91
- read model; a child view only when its query needs what the parent view lacks), and the
91
+ read model; a child view only when its query needs what the parent view lacks), the
92
+ **views-as-lookups check** per `from "E"` on a view (does E add, change or remove a row here? if
93
+ it only fills a column, put the column on the event that creates the row and drop E — see
94
+ `../event-modeling-shared/reference/methodology.md`, "Views as lookups"), and the
92
95
  **conditional-invariant signal** (invariants or fields that apply only "if `<field>` is set"
93
96
  mean two intents in one command: split it). The `timeline-entity-clustered`,
94
- `command-second-intent` and `automation-split-by-outcome` lints are the mechanical half; the
97
+ `command-second-intent`, `automation-split-by-outcome` and `view-lookup-source` lints are the
98
+ mechanical half (the last one only catches the cross-context cases — do the check by eye too); the
95
99
  1.18.0 design exit gate is `em validate --shape`. Shape the model first, scaffold docs after: a rename,
96
100
  split or merge from the reshape would orphan docs already wired with `--wire`. The `slice` phase is unchanged (timeline order).
97
101
  3. **Elaborate scenarios — first pass (step 6).** Scaffold every slice's doc now, one per
@@ -130,6 +134,13 @@ Goal: a structurally complete, **validated** model with correct patterns and swi
130
134
  that reads it; another model/system reads it → mark it `public`; only an automation reads
131
135
  it → nothing, it's internal by design. No new keyword — reachability is already what `ui` and
132
136
  `public` mean.
137
+ **Per `view`, also ask whether each of its sources earns its place (GH #398):** for every
138
+ `from "E"` on the view, does E add, change or remove a row here — or only fill a column on a
139
+ row some other event already created? The latter is the views-as-lookups smell (methodology
140
+ reference, "Views as lookups"): drop E as a source and have the event that creates the row
141
+ carry the field instead. `view-lookup-source` flags the cross-context instances of this
142
+ mechanically; a lookup from the view's own context (no `@Context` mismatch to key on) only
143
+ this by-eye check catches.
133
144
  **Per translation fed from another model, declare what it consumes (MIL-235):** write
134
145
  `translation Order Intake consumes checkout:event.order-submitted`. The ref is
135
146
  `<modelKey>:<kind>.<slug>`, where kind is `event` or `view` and the target is marked `public`
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: event-modeling-discover
3
- em-version: 1.19.6
3
+ em-version: 1.20.0
4
4
  description: >-
5
5
  Use when starting a brand-new event model from scratch — greenfield discovery: brainstorming
6
6
  past-tense events, storyboarding the happy path, finding the commands and read models — or when
@@ -96,6 +96,14 @@ it is fine to finish a round with `# TBD`s still parked.
96
96
  read model* warnings: real systems do record fire-and-forget events. In an as-is model that is
97
97
  a **finding about the system**, not a modelling mistake — never invent a reader to silence it
98
98
  (that would be desired state). Park it in the Decisions log and carry the warning.
99
+ **Expect lookup-style sources too.** A lifted model will faithfully reproduce the projection's
100
+ lookup joins — a view wired to `from "Order Created"` only because the current query joins the
101
+ orders table to get `customerName`, not because this view owns any row that event changes. Wire
102
+ it as-is (current-state-only stance: that join is how the system behaves today); don't fix it
103
+ here, that would be inventing desired state. Note it as a finding in the Decisions log the same
104
+ way as an unread event, and leave it for `model`/`slice` to apply the views-as-lookups rule
105
+ (`../../event-modeling-shared/reference/methodology.md`, "Views as lookups") once the as-is
106
+ model is agreed.
99
107
  - **R5 — Boundaries & reactions.** Current automations and integrations as-is: schedulers,
100
108
  listeners, outbound syncs, inbound webhooks. Model each as a reaction sharing its slice with
101
109
  the `command` + `event` it triggers (`reaction → command → event`, per `em-dsl.md`) — the
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: event-modeling-engagement
3
- em-version: 1.19.6
3
+ em-version: 1.20.0
4
4
  description: >-
5
5
  Use when leading an engagement: building a named set of ratified event-modeling slices
6
6
  (`em engagement`) into one independent PR per slice, each against main, by dispatching the
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: event-modeling-implement
3
- em-version: 1.19.6
3
+ em-version: 1.20.0
4
4
  description: >-
5
5
  Use when building a single ratified event-modeling slice (status: ready-to-implement) into
6
6
  merged, tested code — the one phase where the agent builds rather than facilitates. Gates on
@@ -52,7 +52,12 @@ routing table (§7) is what names the skill set for each pattern.
52
52
  Preconditions (tool check, model location) are the same as every other phase — see
53
53
  `../event-modeling-shared/reference/operating-principles.md` — but this phase additionally
54
54
  requires the target slice to already be ratified; if it isn't, stop and say so rather than
55
- starting on unratified work.
55
+ starting on unratified work. The one documented exception is **inline ratification** (GH #399,
56
+ `reference/implement.md` §1a): for an internal (non-public-touching, non-engagement) slice whose
57
+ doc is already `reviewed`, drafting code on the same branch ahead of the human's `em slice
58
+ ratify` is sanctioned — provided you never flip the gate yourself and re-confirm
59
+ `--slice-ready` reads `true` before treating the slice as done. `event-modeling-engagement` is
60
+ unchanged by this: it still requires every dispatched slice ratified before its branch is cut.
56
61
 
57
62
  End of phase: PR merged, slice doc flipped to `status: implemented` with `implementedIn`
58
63
  filled, `em slice index <model-name>.em` run to refresh `README.md`'s Slices table. Implement
@@ -46,15 +46,82 @@ A warning whose code is in this list blocks; any other warning never does — `c
46
46
 
47
47
  Because of that list, `ready` can be `false` with every `gates` entry `true` (a `both-ends-of-a-flow/*` warning on one of the slice's own elements, say). Don't work it out from `gates`: read **`blockedBy`** (schema `1.5`, GH #377), the codes that flipped `ready`, `[]` exactly when it is `true`, and report those codes with the `false` gates when you hand the slice back.
48
48
 
49
- **`ready: false` means stop** — with one named exception: a slice already `status: implemented`
50
- with `driftSignal: in-sync` or `uncertified` (MIL-214) also reads `ready: false` (its
51
- `statusReady` gate fails because nothing is newly ratified), and that specific case is
52
- re-implementation/repair, not a blocker — see §3's third mode before treating it as one. For
49
+ **`ready: false` means stop** — with two named exceptions. First: a slice already `status:
50
+ implemented` with `driftSignal: in-sync` or `uncertified` (MIL-214) also reads `ready: false`
51
+ (its `statusReady` gate fails because nothing is newly ratified), and that specific case is
52
+ re-implementation/repair, not a blocker — see §3's third mode before treating it as one. Second:
53
+ under **inline ratification** (§1a) a slice doc that's only `status: reviewed` — not yet
54
+ `ready-to-implement` — is a sanctioned starting point for drafting code on the SAME branch, with
55
+ the explicit, non-negotiable condition that you treat nothing as actually ratified, and open
56
+ nothing for merge, until a human has run `em slice ratify` and `--slice-ready` reads `true` for
57
+ real; §1a names exactly which slices qualify and what that condition requires in practice. For
53
58
  every other `false`, report which `gates`
54
59
  entries are `false` and what `blockedBy` names (and any `diagnostics` entries concerning this
55
60
  slice) and hand the slice back to the humans. Never make the gate pass yourself — checking an open-question box, flipping
56
- `status`, or editing frontmatter are ratification decisions, and ratification happens in a
57
- facilitated session, not in an implementation branch.
61
+ `status`, or editing frontmatter are ratification decisions, made by a human in a ratification
62
+ session, never by you running `em slice ratify` on their behalf. That session is usually a
63
+ separate doc-only PR, merged before an implementation branch is even cut — but it doesn't have
64
+ to be: **inline ratification** (GH #399, §1a below) lets the session be the PR review of the
65
+ very branch you're implementing on, for internal slices. Either shape, the rule here is the
66
+ same: you never flip the gate yourself, whatever branch you're on.
67
+
68
+ ### 1a. The inline variant: ratifying on the implementation branch
69
+
70
+ Most of this document assumes the ordinary shape — a slice is already `ready-to-implement` when
71
+ you start, ratified in its own prior PR, and §1's gate reads `true` before you write a line of
72
+ code. **Inline ratification** is the documented alternative
73
+ (`docs/process.md`'s
74
+ ["Inline ratification" section](https://github.com/milehimikey/em/blob/main/docs/process.md#inline-ratification-approval-as-the-ratification-session)):
75
+ draft the slice doc *and* build it on the same branch, spec commits first, and let the human's
76
+ review of that one PR be the ratification session — which means you are, deliberately and
77
+ sanctioned, building for most of that branch's life against a doc that reads `ready: false`.
78
+ What doesn't change: you still never flip `status` yourself, never run `em slice ratify` on the
79
+ human's behalf, and never write a word of the ratification record — see §1's second named
80
+ exception for the precise condition this depends on. What's different is sequencing and tiering:
81
+
82
+ - **Draft the doc first, then build on the same branch.** Commit the slice doc (through the
83
+ normal `slice` phase), get it reviewed (`em slice review --by`, flipping it to `status:
84
+ reviewed` — a human has to do this part too, same as always) before any implementation code,
85
+ even though doc and code will both live on one branch and merge in one PR. Writing code before
86
+ the doc exists, or before a human has looked at it, is retrofit — the exact failure mode inline
87
+ mode is named after avoiding — whether or not anyone ever checks for it.
88
+ - **You may keep building past a `reviewed`-but-not-yet-`ready-to-implement` doc, but nothing is
89
+ ratified until `em slice ratify` actually runs.** `--slice-ready` keeps reading `false` the
90
+ whole time you're building — that's expected, not a signal to stop, as long as the doc's
91
+ `status` is `reviewed` (not `draft`: an un-reviewed doc is never a valid base, inline or not).
92
+ On an inline branch the human typically runs `em slice ratify --no-ref` as part of approving
93
+ the PR (`--no-ref` because the branch is headed for a squash/rebase merge) — not something you
94
+ do for them, and not something you should expect to have happened before the PR is up for
95
+ review. Before you report the slice done, or treat §6's merge-time flip as legitimate,
96
+ re-check `--slice-ready` yourself and confirm it now reads `true` with `ratifiedBy:` set — if
97
+ it still doesn't, the slice isn't ratified yet and you say so instead of proceeding.
98
+ - **A post-signature doc edit still trips the hash check.** `ratifiedHash` doesn't know or care
99
+ which branch it was computed on — if something changes the doc's body after the human has
100
+ signed it (a gap surfaces, §4), `--slice-ready` and `em slice ratify` both refuse the stale
101
+ signature exactly as they would across two PRs. Route it through `em slice revise --withdraw`,
102
+ review, re-ratify (§4) — inline doesn't shortcut that loop, it just means the re-sign happens
103
+ on the branch you're already on instead of a fresh one.
104
+ - **Not every slice is a candidate.** A slice owning a `public` command, event or view
105
+ (`isPublicTouchingSlice`) keeps the separate doc PR — the contract is a promise to consumers
106
+ who never see your implementation diff, and `em slice ratify` already refuses a public-touching
107
+ slice without `--meaning-unchanged`/`--contract-change` regardless of which branch it runs on.
108
+ An engagement-dispatched slice is never inline either — engagements structurally need every
109
+ contract frozen before any implementer branch is cut, so the engagement skill never offers you
110
+ this shape (it is explicitly unchanged by GH #399 — this is a note for your own judgment
111
+ outside an engagement, not something to apply inside one).
112
+ - **Pre-ship advisory conform is the loop's natural checkpoint.** Before the PR merges, a human
113
+ (or you, if asked) can run `em slice conform <model>.em <key> --at <rev> --preview` — the
114
+ pre-ship, non-certifying mode (GH #400): same doc-resolution and findings-check read as a real
115
+ certification, legal before the slice has shipped, never writes `conformedVersion:`/
116
+ `conformedAt:`/`conformedOn:`. It's advisory and ephemeral by default (nothing written unless
117
+ `--write-findings` is passed) — a cheap "did I just drift from the doc I signed" look before
118
+ asking for the PR review that will also be the ratification.
119
+ - **The ordering advisory is informational, not yours to act on.** `em refs --check-order`
120
+ (GH #399) can report whether a commit on this branch touched code before the doc's
121
+ `ratifiedRef` — a retrofit smoke signal for the human reviewing the PR. It's advisory, never a
122
+ gate, and nothing in this skill asks you to run it or react to its output; if a human shows you
123
+ a `code-before-ratification` result, treat it the same as any other gap raised against your
124
+ work (§4), not as something to silence.
58
125
 
59
126
  ## 2. Read the spec
60
127
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: event-modeling-review
3
- em-version: 1.19.6
3
+ em-version: 1.20.0
4
4
  description: >-
5
5
  Use when starting em's live browser viewer (`em watch --serve`) so a team can watch a model
6
6
  update in real time, or facilitating a SCHEDULED stakeholder walkthrough that steps through
@@ -28,6 +28,8 @@ em render <file> --keep-empty-lanes # keep the A
28
28
  em export <file> # export a versioned JSON snapshot of the normalized model
29
29
  em export <file> -o, --out <path> # write to a file instead of stdout
30
30
  em export <file> --slice <key> # export only this slice's object (pattern/fields/doc) instead of the whole model (export key, MIL-128) — refuses only if THIS slice has an error; an unrelated slice's breakage elsewhere in the model doesn't block it (see docs/cli.md)
31
+ em export <file> --doc-json # emit the structured slice-doc export (GH #401) instead of the model snapshot: per slice the frontmatter join plus the parsed sections as data (fields, invariants, scenarios, error flows, open questions) and a model-level invariant catalog. Own schema version; whole model only (does not combine with --slice)
32
+ em export <file> --include-html # with --doc-json, include each slice doc's rendered HTML body (excluded by default; the authored body never appears in a plain export)
31
33
  em api generate <file> # write the TypeSpec contract for the model's public commands, events and views to <model dir>/contracts/<model key>.tsp
32
34
  em api generate <file> -o, --out <path> # write to this path instead of <model dir>/contracts/<model key>.tsp
33
35
  em api generate <file> --stdout # print the contract instead of writing it
@@ -98,10 +100,13 @@ em slice defer <file> <slice-key> [question] --decision <text> # what this
98
100
  em slice defer <file> <slice-key> [question] --from-issue <text> # defer a captured `issue "..."` on this slice instead of a doc Open Question (case-sensitive substring of its text)
99
101
  em slice defer <file> <slice-key> [question] --by <name> # who deferred it
100
102
  em slice defer <file> <slice-key> [question] --on <date> # deferral date, YYYY-MM-DD (default: today, local date)
101
- em slice conform <file> <slice-key> # record per-slice-per-version conformance certification: sets `conformedVersion:`/`conformedAt:`/`conformedOn:` on a slice doc (MIL-214) — the fact `driftSignal: in-sync` now depends on. Legal only for a shipped slice (`status: implemented`, or any status with a `shippedVersion:` — MIL-283) with an `implementedIn:` link; `conformedVersion:` is the shipped version. Idempotent on the same (version, --at) pair; a different --at simply overwrites (a later re-certification is normal — there's no way for em to tell 'later' from 'earlier' for an arbitrary revision string, so there's no --force to reach for). Refuses when the conformance/<date>[-<name>]-findings.json file matching --at still has an unruled finding in scope for this slice; --skip-findings-check overrides with a loud notice.
103
+ em slice conform <file> <slice-key> # record per-slice-per-version conformance certification: sets `conformedVersion:`/`conformedAt:`/`conformedOn:` on a slice doc (MIL-214) — the fact `driftSignal: in-sync` now depends on. Legal only for a shipped slice (`status: implemented`, or any status with a `shippedVersion:` — MIL-283) with an `implementedIn:` link; `conformedVersion:` is the shipped version. Idempotent on the same (version, --at) pair; a different --at simply overwrites (a later re-certification is normal — there's no way for em to tell 'later' from 'earlier' for an arbitrary revision string, so there's no --force to reach for). Refuses when the conformance/<date>[-<name>]-findings.json file matching --at still has an unruled finding in scope for this slice; --skip-findings-check overrides with a loud notice. GH #400: --preview runs the same doc-resolution and findings-check read for a slice that hasn't shipped yet — the shipped/implementedIn gates are waived and NOTHING is ever written to the doc; output is ephemeral (text or --json) unless --write-findings opts into persisting a normal findings file.
102
104
  em slice conform <file> <slice-key> --at <rev> # the target-repo revision this certification sweep diffed against — or, when what was exercised is not a commit that stays reachable (a per-PR environment under squash/rebase merges, a build or image), an opaque `<kind>:<id>` such as build:1234 or deploy:eu-1-2026-10-15 (GH #261); recorded verbatim, skipped by git-reachability checks
103
105
  em slice conform <file> <slice-key> --on <date> # certification date, YYYY-MM-DD (default: today, local date)
104
106
  em slice conform <file> <slice-key> --skip-findings-check # certify even with unruled findings in scope — prints a loud notice on stderr
107
+ em slice conform <file> <slice-key> --preview # GH #400: pre-ship advisory mode — waives the shipped-status/`implementedIn:` gates, writes NOTHING to the doc (no conformedVersion/conformedAt/conformedOn, ever); ephemeral by default, --write-findings opts into persisting a normal findings file. --skip-findings-check has no effect in this mode (preview never refuses on findings)
108
+ em slice conform <file> <slice-key> --write-findings # with --preview: persist the preview's own unruled findings as conformance/<on>-<slice-key>-preview-findings.json
109
+ em slice conform <file> <slice-key> --json # with --preview: print a JSON document instead of the text report
105
110
  em changelog <file> # render a model's git history as a business-readable ledger (see docs/cli.md)
106
111
  em changelog <file> --from <rev> # start the walk at this revision (inclusive)
107
112
  em changelog <file> --to <rev> # end the walk at this revision (inclusive; default HEAD)
@@ -109,7 +114,7 @@ em changelog <file> -o, --out <path> # write to a
109
114
  em state read [dir] # print the state file's mechanical fields as JSON
110
115
  em state set-phase <phase> [dir] # rewrite Current phase: (and Last updated:); --step also rewrites Current step:
111
116
  em state set-phase <phase> [dir] --step <n> # also set Current step: to this value
112
- em state set-conformance <revision> [dir] # rewrite Last conformance: (and Last updated:) in the exact format reference/conform.md parses. Refuses (MIL-214) while any `implemented` slice has an unruled conformance finding in the findings JSON beside --report (a `slice: null` finding blocks every slice); --partial records the marker with a ` (partial)` suffix instead, with a loud notice. Without a findings JSON beside --report: warns once and records the marker as given (migration path for a report predating `em conform-findings`)
117
+ em state set-conformance <revision> [dir] # rewrite Last conformance: (and Last updated:) in the exact format reference/conform.md parses. Refuses (MIL-214) while any `implemented` slice has an unruled conformance finding in the findings JSON beside --report, or, when that is absent, in every findings file at the report's own revision (a `slice: null` finding blocks every slice); --partial records the marker with a ` (partial)` suffix instead, with a loud notice. Without a findings JSON beside --report and none at the report's revision: warns once and records the marker as given (migration path for a report predating `em conform-findings`)
113
118
  em state set-conformance <revision> [dir] --report <path> # path to the conformance report just written
114
119
  em state set-conformance <revision> [dir] --partial # record the marker as PARTIAL even though some in-scope findings are still unruled
115
120
  em state set-review <date> [dir] # rewrite Last stakeholder review: (and Last updated:)
@@ -127,7 +132,7 @@ em conform-scope <file> # mechanize
127
132
  em conform-scope <file> --repo <path> # path to (or inside) the target codebase's git repository
128
133
  em conform-scope <file> --full # ignore Last conformance:/changed paths; scope every implemented slice
129
134
  em conform-scope <file> --seed-asis # write <model>-asis.em as a byte copy of the canonical model and ensure it's gitignored
130
- em conform-supersede <file> <report-path> # stamp a conformance report with a "superseded as of <rev>" banner once its findings have been ruled on (MIL-164, docs/process.md#what-ratified-means) — the companion step to run at ratification time so a reader following the report's file:line citations knows they describe an ancestor of the current model. Additive-only splice, never a rewrite of the report; idempotent on the same --as-of/--findings/--on stamp; refuses if the report doesn't exist. --locus/--by (MIL-214, both required together) additionally record the ruling itself — locus/resolvedBy/resolvedOn — on the findings named by --findings in the sibling conformance/<date>-findings.json, when one exists (warns once and stamps the banner only when it doesn't — the migration path for a report predating em conform-findings)
135
+ em conform-supersede <file> <report-path> # stamp a conformance report with a "superseded as of <rev>" banner once its findings have been ruled on (MIL-164, docs/process.md#what-ratified-means) — the companion step to run at ratification time so a reader following the report's file:line citations knows they describe an ancestor of the current model. Additive-only splice, never a rewrite of the report; idempotent on the same --as-of/--findings/--on stamp; refuses if the report doesn't exist. --locus/--by (MIL-214, both required together) additionally record the ruling itself — locus/resolvedBy/resolvedOn — on the findings named by --findings in the sibling conformance/<date>-findings.json, when one exists (refuses, naming them, when only other findings files exist at the report's revision; warns once and stamps the banner only when it doesn't — the migration path for a report predating em conform-findings)
131
136
  em conform-supersede <file> <report-path> --as-of <rev> # the revision this ruling was made against — same value passed to `em state set-conformance`
132
137
  em conform-supersede <file> <report-path> --findings <spec> # which finding number(s) this stamps as ruled, e.g. "1-3" or "1,2,4"
133
138
  em conform-supersede <file> <report-path> --on <date> # ruling date, YYYY-MM-DD (default: today, local date)
@@ -145,6 +150,7 @@ em watch <file> -T, --format <fmt> # output for
145
150
  em watch <file> --keep-empty-lanes # keep the API lane even when empty
146
151
  em watch <file> --serve # serve a live viewer with instant push-reload (no polling)
147
152
  em watch <file> --port <n> # port for --serve (default 5173)
153
+ em watch <file> --storyline <name> # GH #309 (deferred from #277): filter the live view to one storyline's subset — the same subset `em render --storyline` draws (its own slices in order, plus dimmed stub columns for what they read from outside it and the `after` end state). Whenever the model declares any storylines the viewer also offers a picker to switch to the full model or another declared storyline, live, without restarting. An unknown name errors, listing the declared storylines; requires --serve
148
154
  em validate <file> # check a model against event-modeling rules
149
155
  em validate <file> --list-issues # print only open `issue` diagnostics (slice, element, line, text)
150
156
  em validate <file> --list-divergences # print only accepted-divergence annotations (slice, element, line, text) — never fails the build
@@ -164,6 +170,7 @@ em refs <file> # check that
164
170
  em refs <file> --reachable-from <rev> # the branch or commit every ref must be an ancestor of (e.g. origin/main)
165
171
  em refs <file> --waive <slice-key> # excuse this slice's non-ancestor/unresolved/undecidable results from the exit code (repeatable); still reported
166
172
  em refs <file> --waive-file <path> # a baseline file of slice keys to waive, one per line (`#` comments allowed); missing file = no waivers
173
+ em refs <file> --check-order # advisory-only (GH #399): for every slice doc with a ratifiedRef, report whether a commit between the branch's fork point and ratifiedRef already touched code other than the doc itself — a retrofit signal for inline ratification (spec written to describe code that already exists). History is rewritable, so this NEVER affects the exit code or `ok`, and is never a validate gate — see docs/process.md's inline-ratification section
167
174
  em refs <file> --json # print a JSON document instead of the text report (see docs/cli.md)
168
175
  em ledger <file> # check slice docs' version: field agrees with their content across two git revisions (opt-in CI check, MIL-89 — never part of `em validate`, see docs/ci.md)
169
176
  em ledger <file> --from <rev> # baseline revision
@@ -489,7 +496,19 @@ type Name { field: Type, ... } # named structured type, reusable from any f
489
496
  declared `type T`, with `derived from "Child Created", "Child Deleted"`, embeds a child entity:
490
497
  each field of `T` must trace to a named event (`fields-completeness/view-field-no-source`
491
498
  otherwise); a field also traces one level through the declared element type of an array field
492
- on the event (`optionValues: VariantOptionValue[]`), no deeper and with no path syntax. An
499
+ on the event (`optionValues: VariantOptionValue[]`), no deeper by same name. When the event
500
+ renamed or flattened the field, declare the element field's source by **path** into the named
501
+ event, as the unquoted form of the same clause (GH #383): `attributeValueIds: string[] derived
502
+ from values[].attributeValueId` — a `.`-joined chain of identifiers, each `[]`-suffixed exactly
503
+ when it steps through an array field (the marker is enforced against the event field's actual
504
+ arrayness). The path must resolve on a named event — every named event is tried; non-leaf
505
+ segments must be `type`-valued fields — and the leaf field's type must match the element
506
+ field's (array markers stripped); an unresolved path or leaf-type mismatch is
507
+ `fields-completeness/view-field-no-source` (implicit same-name nested tracing matches names
508
+ only; the explicit path form earns the stronger leaf-type check). The quoted event-list form
509
+ and the unquoted path form are mutually exclusive on one field — both at once is a parse error.
510
+ A plain (non-collection) view field may carry the same path form, checked the same way against
511
+ the view's source events — never silently exempt like a bare `derived` field. An
493
512
  element field the projection computes rather than copies (an audit stamp from event metadata,
494
513
  say) is marked `derived` on the `type` field itself — bare or `derived from "<events>"`, same
495
514
  syntax as a view field (GH #382): it is then skipped by `view-field-no-source`, and a traced
@@ -954,6 +973,7 @@ not the prose above has caught up yet. `--slice-ready <key>`-only codes are excl
954
973
  | `view-again-without-earlier` | error | `again` without an earlier declaration | Declare the view plainly the first time it appears. |
955
974
  | `view-from-future-event` | error | Backward timeline (view reads a future event) | Move the source to a later `view X again` instance. |
956
975
  | `view-from-unresolved` | error | Unknown event source | Fix the `from` reference to name an existing event. |
976
+ | `view-lookup-source` | warning | View reads an event only as a lookup | Carry the field on the event that creates this view's own row instead; if this is a genuine cross-context row change, accept it on the shape exit gate (docs/validation.md#the-shape-exit-gate) rather than fixing it (docs/patterns.md#state-view). |
957
977
  | `view-no-source` | warning | Read model without source | Add `from "Event"`, or place the view in a slice with an event. |
958
978
  <!-- GENERATED:validate-rules:end -->
959
979
 
@@ -316,6 +316,22 @@ drift into **CRUD laid on a timeline**; these rules stop that. Quoted lines are
316
316
  view carries ids; child with its own data → embedded in the parent view; child queried without the
317
317
  parent id → its own view. Aggregate rules decide write-side consistency, never read models. *(em
318
318
  ruling)*
319
+ - **Views as lookups: an event that creates a row carries what the row shows.** A view reads another
320
+ event only when that event adds, changes or removes a row the view already owns — never merely to
321
+ fill a column on a row some *other* event created. **Per-view check:** for each `from "E"` on a view,
322
+ ask *does E add, change or remove a row here? If it only fills a column, put the column on the event
323
+ that creates the row and drop E.* Generated models routinely come out wrong here — a wide fan-out of
324
+ "reference" events into every view that shows a name, and views keeping a lookup table ("every
325
+ neighbor's name and current email, every tool's name") beside their own rows. Fix: the decider
326
+ already read what it needed to enforce its own invariants (e.g. a checkout decider that reads Tool
327
+ Added and Neighbor Registered to validate the request already has `toolName`/`neighborName` in hand),
328
+ so have it assign those fields onto the event it records — the view then needs nothing beyond that
329
+ one event. A slice doc that needs a "Bookkeeping" paragraph (the view keeps, beyond its own rows, a
330
+ lookup of some other entity's fields) has this smell; see `docs/patterns.md#state-view` for the
331
+ worked overdue-notice example.
332
+ `view-lookup-source` is the mechanical, advisory half — it catches the cross-context cases with a few
333
+ false positives; a within-context lookup (`Tool Checked Out` read only to learn whose loan a later
334
+ event concerns) is invisible to it, so this check still has to be done by eye.
319
335
  - **Write side: children belong in the parent's context** when the child's invariants are checks against
320
336
  the parent. A sub-resource schema shared across parent types is one event per parent on the parent's
321
337
  context, not a shared aggregate — check the spec's event reference, not its resource section. Where
@@ -70,7 +70,7 @@ to decide `frontmatter-invalid` without re-deriving frontmatter-shape rules of i
70
70
  | `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 |
71
71
  | `ratifiedHash` | string | `sha256:<64 hex>` | written only by `em slice ratify` (MIL-284): `sha256` over the **authored text** of the doc body (everything after the frontmatter fence, EOLs normalised). Since 1.19.0 (GH #347) the text strictly between each [generated region](#generated-regions-and-authored-sections)'s two marker lines is left out (the marker lines themselves stay in), so `em slice sync` can refresh a ratified or shipped doc's regions without moving the hash — the regions restate the model, whose contract changes surface through model validation and its pin (`model-versions/v<N>.json`); a doc whose markers don't balance has nothing left out. Since 1.17.0 (GH #253) the doc also leaves out **what describes a later version**: inside `## Open Questions` the heading line, blank lines and every `deferred to v<n>` item, then trailing whitespace is trimmed — so `em slice defer` can record a question on a ratified or shipped doc without moving the hash. Answered `- [x]` history, any `- [ ]`, the region marker lines and every other section stay signed. The signed content's identity without git. A hash in an older scope whose body is intact is still read as intact — recorded by 1.17.0–1.18.x (generated regions inside) or before 1.17.0 (whole body); `em upgrade`'s `ratified-hash-scope` step re-stamps it, and so do `em slice sync` and `em slice defer` when they first write to the doc. 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`) |
72
72
  | `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 |
73
- | `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 |
73
+ | `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) — a `shippedRef:` line left by the previous version's ship is removed on the flip (GH #392). Same caveat as `ratifiedRef`: an address, valid while reachable — `ratifiedHash` identifies the content |
74
74
  | `shippedOn` | string | `YYYY-MM-DD` | written only by `em slice mark-implemented` (MIL-284): the local date of the merge record, or `--on <date>` |
75
75
  | `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) |
76
76
  | `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) |
@@ -164,7 +164,12 @@ an event directly.}}
164
164
  line and field table are a GENERATED region (one per read model; `em-slice-view-<view-slug>`
165
165
  when there are several): Source / Notes names the source event a field is copied from, or
166
166
  `Derived` for a field marked `derived` in the `.em` (docs/dsl.md#derived-fields) — say how
167
- it is derived in prose below the region. `{{ }}` marks a field the model cannot trace. -->
167
+ it is derived in prose below the region. `{{ }}` marks a field the model cannot trace.
168
+ If describing a field here needs a "Bookkeeping" paragraph — this view also keeps, beyond its
169
+ own rows, a lookup of some other entity's name/email/etc. so it can display it — that is the
170
+ views-as-lookups smell (methodology reference, "Views as lookups"): the field belongs on the
171
+ event that creates this view's row instead, copied from state the command/decider already
172
+ reads to enforce its own invariants. Fix the `.em`, not the prose. -->
168
173
  <!-- GENERATED:em-slice-view:start — generated from the model by `em slice sync`; do not hand-edit -->
169
174
  - **View:** `{{View Name}}` built from events: {{"Event A", "Event B"}}
170
175
 
@@ -1,9 +1,9 @@
1
1
  {
2
- "name": "em-1-19-6",
2
+ "name": "em-1-20-0",
3
3
  "owner": {
4
4
  "name": "Mike Key"
5
5
  },
6
- "description": "em v1.19.6: the Event Modeling skill bundle and MCP server as a Claude Code plugin.",
6
+ "description": "em v1.20.0: the Event Modeling skill bundle and MCP server as a Claude Code plugin.",
7
7
  "plugins": [
8
8
  {
9
9
  "name": "em",
package/README.md CHANGED
@@ -109,7 +109,7 @@ flowchart LR
109
109
 
110
110
  ## Model with AI
111
111
 
112
- `em` ships a Claude Code skill bundle — one router skill plus six focused, SDLC-stage skills
112
+ `em` ships a plugin skill bundle for Claude Code and Codex — one router skill plus six focused, SDLC-stage skills
113
113
  (discover/extract, model/slice, implement, conform/validate, watch/review) — that runs a
114
114
  facilitated Event Modeling session: the AI asks the questions, you supply the domain, and the
115
115
  model renders live as it grows.
@@ -122,6 +122,16 @@ claude plugin marketplace add milehimikey/em@v1.14.0 --scope project
122
122
  claude plugin install em@em-1-14-0 --scope project
123
123
  ```
124
124
 
125
+ Codex uses the same marketplace and plugin bundle:
126
+
127
+ ```bash
128
+ codex plugin marketplace add milehimikey/em --ref v1.14.0
129
+ codex plugin add em@em-1-14-0
130
+ ```
131
+
132
+ Start a new Codex session after installation. The same skills and MCP server are available; Codex
133
+ does not use Claude's `/em:` slash-command syntax.
134
+
125
135
  Then run `/em:event-modeling` in Claude Code. The older vendored route (`em skill install`,
126
136
  which copies the bundle into `.claude/skills/`) still works but is deprecated since 1.14;
127
137
  `em upgrade --apply` moves a vendored repo to the pinned plugin in one commit. The same bundle
@@ -0,0 +1,16 @@
1
+ export interface FrontmatterBadgeInput {
2
+ /** `SliceDoc.pattern` — kebab-case or null. */
3
+ pattern: string | null;
4
+ /** `SliceDoc.version` or null. */
5
+ version: number | null;
6
+ /** `SliceDoc.status`, lowercase, or null. */
7
+ status: string | null;
8
+ /** `classifyImplementationDrift(doc)`'s result, as its string value. */
9
+ driftSignal: string;
10
+ }
11
+ /** GH #402: a compact badge row — pattern, version, lifecycle status, implementation-drift
12
+ * signal — built from a slice doc's own frontmatter facts. Callers gate this on
13
+ * `doc.frontmatterPresent` themselves (catalog/pages.ts, render/sliceOverlay.ts): a legacy
14
+ * freeform doc with no frontmatter at all renders with no badge row, exactly as it did before
15
+ * this feature. */
16
+ export declare function renderFrontmatterBadgeRow(input: FrontmatterBadgeInput): string;
@@ -0,0 +1,50 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // GH #402: the compact frontmatter badge row shown above a slice doc's body wherever
3
+ // `SliceDoc.html` is rendered — `em catalog`'s per-slice page (catalog/pages.ts) and the live
4
+ // viewer's doc flyout (render/sliceOverlay.ts). Both callers already hold a resolved `SliceDoc`
5
+ // and can compute its `classifyImplementationDrift()` (catalog/driftSignal.ts) themselves; this
6
+ // module takes the four already-resolved primitives rather than either type, so it carries no
7
+ // dependency on catalog/sliceDoc.ts or catalog/driftSignal.ts and can't become part of a cycle
8
+ // with either. Pure: primitives in, an HTML string out.
9
+ function escapeHtml(s) {
10
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
11
+ }
12
+ /** `"state-change"` -> `"State Change"`, `"ready-to-implement"` -> `"Ready To Implement"` — the
13
+ * frontmatter's kebab-case machine values, in the display label shape the skill prose already
14
+ * uses for `pattern` (sliceDoc.ts's own comment: "the frontmatter value is a machine key, not a
15
+ * display label"). */
16
+ function titleCase(kebab) {
17
+ return kebab
18
+ .split("-")
19
+ .map((w) => (w.length > 0 ? w[0].toUpperCase() + w.slice(1) : w))
20
+ .join(" ");
21
+ }
22
+ /** Readable labels for `classifyImplementationDrift`'s `DriftSignalKind` — duplicated here as
23
+ * strings rather than importing the union type, so this module stays free of any dependency on
24
+ * catalog/driftSignal.ts (see file header). An unrecognized value (there shouldn't be one) falls
25
+ * back to showing itself verbatim rather than disappearing. */
26
+ const DRIFT_LABELS = {
27
+ "in-sync": "In sync",
28
+ "never-implemented": "Not implemented",
29
+ "unpropagated-delta": "Delta pending",
30
+ "implemented-without-link": "Implemented, no link",
31
+ uncertified: "Uncertified",
32
+ };
33
+ /** GH #402: a compact badge row — pattern, version, lifecycle status, implementation-drift
34
+ * signal — built from a slice doc's own frontmatter facts. Callers gate this on
35
+ * `doc.frontmatterPresent` themselves (catalog/pages.ts, render/sliceOverlay.ts): a legacy
36
+ * freeform doc with no frontmatter at all renders with no badge row, exactly as it did before
37
+ * this feature. */
38
+ export function renderFrontmatterBadgeRow(input) {
39
+ const badges = [];
40
+ if (input.pattern)
41
+ badges.push(`<span class="doc-badge doc-badge--pattern">${escapeHtml(titleCase(input.pattern))}</span>`);
42
+ if (input.version !== null)
43
+ badges.push(`<span class="doc-badge doc-badge--version">v${input.version}</span>`);
44
+ if (input.status)
45
+ badges.push(`<span class="doc-badge doc-badge--status">${escapeHtml(titleCase(input.status))}</span>`);
46
+ const driftLabel = DRIFT_LABELS[input.driftSignal] ?? input.driftSignal;
47
+ badges.push(`<span class="doc-badge doc-badge--drift doc-badge--drift-${escapeHtml(input.driftSignal)}">${escapeHtml(driftLabel)}</span>`);
48
+ return `<div class="doc-badge-row">${badges.join("")}</div>`;
49
+ }
50
+ //# sourceMappingURL=docBadge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"docBadge.js","sourceRoot":"","sources":["../../src/catalog/docBadge.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,qFAAqF;AACrF,8FAA8F;AAC9F,gGAAgG;AAChG,gGAAgG;AAChG,8FAA8F;AAC9F,+FAA+F;AAC/F,wDAAwD;AAExD,SAAS,UAAU,CAAC,CAAS;IAC3B,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AACtG,CAAC;AAED;;;uBAGuB;AACvB,SAAS,SAAS,CAAC,KAAa;IAC9B,OAAO,KAAK;SACT,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;SAChE,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC;AAED;;;gEAGgE;AAChE,MAAM,YAAY,GAA2B;IAC3C,SAAS,EAAE,SAAS;IACpB,mBAAmB,EAAE,iBAAiB;IACtC,oBAAoB,EAAE,eAAe;IACrC,0BAA0B,EAAE,sBAAsB;IAClD,WAAW,EAAE,aAAa;CAC3B,CAAC;AAaF;;;;oBAIoB;AACpB,MAAM,UAAU,yBAAyB,CAAC,KAA4B;IACpE,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,KAAK,CAAC,OAAO;QAAE,MAAM,CAAC,IAAI,CAAC,8CAA8C,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC;IAC5H,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI;QAAE,MAAM,CAAC,IAAI,CAAC,+CAA+C,KAAK,CAAC,OAAO,SAAS,CAAC,CAAC;IAC/G,IAAI,KAAK,CAAC,MAAM;QAAE,MAAM,CAAC,IAAI,CAAC,6CAA6C,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC;IACzH,MAAM,UAAU,GAAG,YAAY,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,KAAK,CAAC,WAAW,CAAC;IACxE,MAAM,CAAC,IAAI,CACT,4DAA4D,UAAU,CAAC,KAAK,CAAC,WAAW,CAAC,KAAK,UAAU,CAAC,UAAU,CAAC,SAAS,CAC9H,CAAC;IACF,OAAO,8BAA8B,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,QAAQ,CAAC;AAC/D,CAAC"}
@@ -66,7 +66,7 @@ import { isSignedSliceDoc } from "./sliceDoc.js";
66
66
  import { checkScenarioQuality } from "./scenarioQualityValidate.js";
67
67
  import { authoredTextOf, findStructuredSectionProblems, generatedRegionPlaceholders, splitCells } from "./sliceSections.js";
68
68
  import { duplicateScenarioTitles, parseScenariosLenient } from "./scenarios.js";
69
- import { viewSourceNames } from "../model/continuation.js";
69
+ import { viewLoopedEventNames, viewSourceNames } from "../model/continuation.js";
70
70
  const COMMAND_MARKER = /^\*\*Command:\*\*\s*`([^`]+)`\s*$/;
71
71
  const EVENT_MARKER = /^\*\*Event:\*\*\s*`([^`]+)`(?:\s*→\s*context\s*`[^`]+`\s*)?$/;
72
72
  const VIEW_MARKER = /^-\s*\*\*View:\*\*\s*`([^`]+)`/;
@@ -405,6 +405,9 @@ function checkFields(diags, kind, docPath, docByName, modelByName, model, refs)
405
405
  // doc-names-unfed-event — the doc's field-table Source column (or, when the doc has exactly one
406
406
  // view, a scenario Given/Then item) names an event that exists in the model but is not among
407
407
  // the view's sources (the toolshed case: prose anticipated an edge the model never drew).
408
+ // An event reaching the view via a resolved `loops-to "<View>"` edge (MIL-199) counts as a
409
+ // source too (GH #397) — the DSL reference already says so, and `em export`'s `model.edges`
410
+ // carries the edge with `source: "loops-to"`.
408
411
  // doc-omits-fed-event — the model feeds the view an event the doc body never mentions.
409
412
  // Event names are found by scanning text for the model's own event names (longest first, word-
410
413
  // bounded, case-insensitive), so a name embedded in a longer event name is not double-counted.
@@ -505,7 +508,11 @@ function checkViewFeeds(diags, doc, docPath, markers, sliceIndices, ownerSlice,
505
508
  for (const marker of matched) {
506
509
  const view = modelViews.get(normalizeName(marker.name));
507
510
  const sources = viewSourceNames(model, view);
508
- const fed = new Set(sources.map(normalizeName));
511
+ // GH #397: a `loops-to "<View>"` edge re-feeds this view just like an ordinary `from` —
512
+ // an event naming only a loop-back is still read by the view, so the doc isn't wrong to
513
+ // list it under the view's sources.
514
+ const looped = viewLoopedEventNames(model, view);
515
+ const fed = new Set([...sources, ...looped].map(normalizeName));
509
516
  const refsOut = [refs.sliceKeys[view.sliceIndex], refs.refById.get(view.id)];
510
517
  let end = lines.length;
511
518
  for (let j = marker.bodyLine + 1; j < lines.length; j++) {