@milehimikey/em 1.13.1 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/.claude/agents/em-critic.md +26 -0
  2. package/.claude/agents/em-implementer.md +34 -0
  3. package/.claude/agents/em-reviewer.md +31 -0
  4. package/.claude/agents/em-validator.md +27 -0
  5. package/.claude/skills/event-modeling/SKILL.md +50 -11
  6. package/.claude/skills/event-modeling-conform/SKILL.md +1 -1
  7. package/.claude/skills/event-modeling-design/SKILL.md +126 -18
  8. package/.claude/skills/event-modeling-discover/SKILL.md +1 -1
  9. package/.claude/skills/event-modeling-engagement/SKILL.md +192 -0
  10. package/.claude/skills/event-modeling-engagement/reference/engagement.md +143 -0
  11. package/.claude/skills/event-modeling-implement/SKILL.md +1 -1
  12. package/.claude/skills/event-modeling-implement/reference/implement.md +38 -11
  13. package/.claude/skills/event-modeling-review/SKILL.md +1 -1
  14. package/.claude/skills/event-modeling-shared/reference/em-dsl.md +274 -189
  15. package/.claude/skills/event-modeling-shared/reference/operating-principles.md +61 -0
  16. package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +108 -5
  17. package/.claude/skills/event-modeling-shared/templates/constitution.md +18 -1
  18. package/.claude/skills/event-modeling-shared/templates/slice.md +64 -39
  19. package/.claude/skills/event-modeling-shared/templates/state.md +5 -1
  20. package/.claude-plugin/marketplace.json +14 -0
  21. package/README.md +25 -11
  22. package/dist/catalog/apiFirst.d.ts +28 -0
  23. package/dist/catalog/apiFirst.js +49 -0
  24. package/dist/catalog/apiFirst.js.map +1 -0
  25. package/dist/catalog/docJoin.d.ts +13 -0
  26. package/dist/catalog/docJoin.js +11 -0
  27. package/dist/catalog/docJoin.js.map +1 -1
  28. package/dist/catalog/docModelConsistencyValidate.js +44 -4
  29. package/dist/catalog/docModelConsistencyValidate.js.map +1 -1
  30. package/dist/catalog/invariantsValidate.d.ts +4 -0
  31. package/dist/catalog/invariantsValidate.js +102 -0
  32. package/dist/catalog/invariantsValidate.js.map +1 -0
  33. package/dist/catalog/scenarios.d.ts +15 -0
  34. package/dist/catalog/scenarios.js +97 -0
  35. package/dist/catalog/scenarios.js.map +1 -0
  36. package/dist/catalog/sliceDoc.d.ts +8 -0
  37. package/dist/catalog/sliceDoc.js +9 -0
  38. package/dist/catalog/sliceDoc.js.map +1 -1
  39. package/dist/catalog/sliceReadyValidate.d.ts +31 -4
  40. package/dist/catalog/sliceReadyValidate.js +66 -8
  41. package/dist/catalog/sliceReadyValidate.js.map +1 -1
  42. package/dist/catalog/sliceSections.d.ts +55 -0
  43. package/dist/catalog/sliceSections.js +317 -0
  44. package/dist/catalog/sliceSections.js.map +1 -0
  45. package/dist/cli/agentsMd.js +7 -1
  46. package/dist/cli/agentsMd.js.map +1 -1
  47. package/dist/cli/api.d.ts +90 -0
  48. package/dist/cli/api.js +318 -0
  49. package/dist/cli/api.js.map +1 -0
  50. package/dist/cli/ciInit.d.ts +82 -21
  51. package/dist/cli/ciInit.js +468 -64
  52. package/dist/cli/ciInit.js.map +1 -1
  53. package/dist/cli/conformScope.d.ts +28 -0
  54. package/dist/cli/conformScope.js +67 -0
  55. package/dist/cli/conformScope.js.map +1 -1
  56. package/dist/cli/coverage.d.ts +9 -0
  57. package/dist/cli/coverage.js +60 -8
  58. package/dist/cli/coverage.js.map +1 -1
  59. package/dist/cli/engagement.d.ts +210 -0
  60. package/dist/cli/engagement.js +444 -0
  61. package/dist/cli/engagement.js.map +1 -0
  62. package/dist/cli/engagementFile.d.ts +86 -0
  63. package/dist/cli/engagementFile.js +255 -0
  64. package/dist/cli/engagementFile.js.map +1 -0
  65. package/dist/cli/metrics.d.ts +10 -0
  66. package/dist/cli/metrics.js +49 -1
  67. package/dist/cli/metrics.js.map +1 -1
  68. package/dist/cli/pluginPin.d.ts +55 -0
  69. package/dist/cli/pluginPin.js +188 -0
  70. package/dist/cli/pluginPin.js.map +1 -0
  71. package/dist/cli/ratify.d.ts +27 -2
  72. package/dist/cli/ratify.js +112 -13
  73. package/dist/cli/ratify.js.map +1 -1
  74. package/dist/cli/reratify.d.ts +3 -2
  75. package/dist/cli/reratify.js +36 -14
  76. package/dist/cli/reratify.js.map +1 -1
  77. package/dist/cli/scopeInputs.d.ts +48 -0
  78. package/dist/cli/scopeInputs.js +176 -0
  79. package/dist/cli/scopeInputs.js.map +1 -0
  80. package/dist/cli/skillCheck.d.ts +15 -1
  81. package/dist/cli/skillCheck.js +22 -1
  82. package/dist/cli/skillCheck.js.map +1 -1
  83. package/dist/cli/skillDirs.d.ts +5 -0
  84. package/dist/cli/skillDirs.js +13 -1
  85. package/dist/cli/skillDirs.js.map +1 -1
  86. package/dist/cli/skillSync.d.ts +4 -0
  87. package/dist/cli/skillSync.js +23 -0
  88. package/dist/cli/skillSync.js.map +1 -1
  89. package/dist/cli/sliceNew.d.ts +11 -7
  90. package/dist/cli/sliceNew.js +91 -18
  91. package/dist/cli/sliceNew.js.map +1 -1
  92. package/dist/cli/sliceSync.d.ts +50 -0
  93. package/dist/cli/sliceSync.js +170 -0
  94. package/dist/cli/sliceSync.js.map +1 -0
  95. package/dist/cli/status.d.ts +24 -1
  96. package/dist/cli/status.js +20 -1
  97. package/dist/cli/status.js.map +1 -1
  98. package/dist/cli/systemInputs.d.ts +53 -10
  99. package/dist/cli/systemInputs.js +186 -11
  100. package/dist/cli/systemInputs.js.map +1 -1
  101. package/dist/cli/upgrade.d.ts +16 -3
  102. package/dist/cli/upgrade.js +366 -30
  103. package/dist/cli/upgrade.js.map +1 -1
  104. package/dist/cli/usageLog.d.ts +4 -2
  105. package/dist/cli/usageLog.js +4 -2
  106. package/dist/cli/usageLog.js.map +1 -1
  107. package/dist/cli.js +554 -94
  108. package/dist/cli.js.map +1 -1
  109. package/dist/emit/api.d.ts +29 -0
  110. package/dist/emit/api.js +220 -0
  111. package/dist/emit/api.js.map +1 -0
  112. package/dist/emit/apiCheckJson.d.ts +3 -0
  113. package/dist/emit/apiCheckJson.js +21 -0
  114. package/dist/emit/apiCheckJson.js.map +1 -0
  115. package/dist/emit/codeownersJson.d.ts +3 -0
  116. package/dist/emit/codeownersJson.js +19 -0
  117. package/dist/emit/codeownersJson.js.map +1 -0
  118. package/dist/emit/diffJson.d.ts +1 -1
  119. package/dist/emit/diffJson.js +8 -1
  120. package/dist/emit/diffJson.js.map +1 -1
  121. package/dist/emit/engagementJson.d.ts +8 -0
  122. package/dist/emit/engagementJson.js +46 -0
  123. package/dist/emit/engagementJson.js.map +1 -0
  124. package/dist/emit/glossaryJson.d.ts +1 -1
  125. package/dist/emit/glossaryJson.js +4 -1
  126. package/dist/emit/glossaryJson.js.map +1 -1
  127. package/dist/emit/json.d.ts +17 -1
  128. package/dist/emit/json.js +30 -1
  129. package/dist/emit/json.js.map +1 -1
  130. package/dist/emit/metricsJson.d.ts +1 -1
  131. package/dist/emit/metricsJson.js +4 -1
  132. package/dist/emit/metricsJson.js.map +1 -1
  133. package/dist/emit/queryJson.d.ts +1 -1
  134. package/dist/emit/queryJson.js +7 -1
  135. package/dist/emit/queryJson.js.map +1 -1
  136. package/dist/emit/scopeJson.d.ts +12 -0
  137. package/dist/emit/scopeJson.js +26 -0
  138. package/dist/emit/scopeJson.js.map +1 -0
  139. package/dist/emit/skillCheckJson.d.ts +3 -2
  140. package/dist/emit/skillCheckJson.js +10 -2
  141. package/dist/emit/skillCheckJson.js.map +1 -1
  142. package/dist/emit/statusJson.d.ts +1 -1
  143. package/dist/emit/statusJson.js +15 -1
  144. package/dist/emit/statusJson.js.map +1 -1
  145. package/dist/emit/systemJson.d.ts +15 -8
  146. package/dist/emit/systemJson.js +28 -13
  147. package/dist/emit/systemJson.js.map +1 -1
  148. package/dist/emit/validateJson.d.ts +3 -3
  149. package/dist/emit/validateJson.js +10 -4
  150. package/dist/emit/validateJson.js.map +1 -1
  151. package/dist/mcp/server.d.ts +1 -1
  152. package/dist/mcp/server.js +163 -31
  153. package/dist/mcp/server.js.map +1 -1
  154. package/dist/model/diff.d.ts +4 -1
  155. package/dist/model/diff.js +44 -8
  156. package/dist/model/diff.js.map +1 -1
  157. package/dist/model/glossary.d.ts +2 -0
  158. package/dist/model/glossary.js +1 -0
  159. package/dist/model/glossary.js.map +1 -1
  160. package/dist/model/model.d.ts +55 -2
  161. package/dist/model/model.js +57 -0
  162. package/dist/model/model.js.map +1 -1
  163. package/dist/model/qualifiedRef.d.ts +18 -0
  164. package/dist/model/qualifiedRef.js +26 -0
  165. package/dist/model/qualifiedRef.js.map +1 -1
  166. package/dist/model/queryIndex.d.ts +9 -1
  167. package/dist/model/queryIndex.js +18 -2
  168. package/dist/model/queryIndex.js.map +1 -1
  169. package/dist/model/rules.d.ts +143 -2
  170. package/dist/model/rules.js +162 -3
  171. package/dist/model/rules.js.map +1 -1
  172. package/dist/model/validate.d.ts +40 -0
  173. package/dist/model/validate.js +147 -1
  174. package/dist/model/validate.js.map +1 -1
  175. package/dist/parser/ast.d.ts +31 -3
  176. package/dist/parser/parser.js +148 -14
  177. package/dist/parser/parser.js.map +1 -1
  178. package/dist/query/format.js +1 -1
  179. package/dist/query/format.js.map +1 -1
  180. package/dist/query/verbs.d.ts +6 -0
  181. package/dist/query/verbs.js +6 -2
  182. package/dist/query/verbs.js.map +1 -1
  183. package/dist/system/adaptation.d.ts +14 -0
  184. package/dist/system/adaptation.js +49 -0
  185. package/dist/system/adaptation.js.map +1 -0
  186. package/dist/system/codeowners.d.ts +77 -0
  187. package/dist/system/codeowners.js +242 -0
  188. package/dist/system/codeowners.js.map +1 -0
  189. package/dist/system/manifest.d.ts +10 -5
  190. package/dist/system/manifest.js +54 -26
  191. package/dist/system/manifest.js.map +1 -1
  192. package/dist/system/migrateManifest.d.ts +28 -0
  193. package/dist/system/migrateManifest.js +155 -0
  194. package/dist/system/migrateManifest.js.map +1 -0
  195. package/dist/system/scope.d.ts +46 -0
  196. package/dist/system/scope.js +132 -0
  197. package/dist/system/scope.js.map +1 -0
  198. package/dist/system/verify.d.ts +50 -13
  199. package/dist/system/verify.js +179 -58
  200. package/dist/system/verify.js.map +1 -1
  201. package/dist/templates.d.ts +2 -2
  202. package/dist/templates.js +23 -2
  203. package/dist/templates.js.map +1 -1
  204. package/package.json +5 -2
  205. package/plugin/.claude-plugin/plugin.json +18 -0
  206. package/plugin/agents/em-critic.md +27 -0
  207. package/plugin/agents/em-implementer.md +35 -0
  208. package/plugin/agents/em-reviewer.md +32 -0
  209. package/plugin/agents/em-validator.md +28 -0
  210. package/plugin/shared/reference/em-dsl.md +866 -0
  211. package/plugin/shared/reference/methodology.md +329 -0
  212. package/plugin/shared/reference/operating-principles.md +222 -0
  213. package/plugin/shared/reference/slice-doc-schema.md +566 -0
  214. package/plugin/shared/templates/conformance-report.md +85 -0
  215. package/plugin/shared/templates/constitution.md +136 -0
  216. package/plugin/shared/templates/model-readme.md +46 -0
  217. package/plugin/shared/templates/slice.md +251 -0
  218. package/plugin/shared/templates/state.md +77 -0
  219. package/plugin/skills/conform/SKILL.md +93 -0
  220. package/plugin/skills/conform/reference/conform.md +428 -0
  221. package/plugin/skills/design/SKILL.md +297 -0
  222. package/plugin/skills/discover/SKILL.md +90 -0
  223. package/plugin/skills/discover/reference/extract.md +147 -0
  224. package/plugin/skills/engagement/SKILL.md +193 -0
  225. package/plugin/skills/engagement/reference/engagement.md +144 -0
  226. package/plugin/skills/event-modeling/SKILL.md +132 -0
  227. package/plugin/skills/implement/SKILL.md +61 -0
  228. package/plugin/skills/implement/reference/implement.md +439 -0
  229. package/plugin/skills/review/SKILL.md +74 -0
  230. package/dist/emit/typespec.d.ts +0 -26
  231. package/dist/emit/typespec.js +0 -356
  232. package/dist/emit/typespec.js.map +0 -1
@@ -23,8 +23,10 @@
23
23
  // team that would rather pin the vanilla preset and gate on drift — opt-in, the same posture
24
24
  // docs/ci.md already recommends for `em skill check` ("if you'd rather pin ... add `em skill
25
25
  // check` as its own gate instead of silently overwriting").
26
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
27
- import { dirname, join } from "node:path";
26
+ import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
27
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
28
+ import { parseManifest } from "../system/manifest.js";
29
+ import { pluginInstallCommands } from "./pluginPin.js";
28
30
  import { applyMarker, markerPair, markerRegex } from "../util/markers.js";
29
31
  export const CI_WORKFLOW_MARKER = "em-ci";
30
32
  export const CONFORM_WORKFLOW_MARKER = "em-conform";
@@ -44,6 +46,106 @@ export function findUnsafeCiInitArg(value) {
44
46
  return UNSAFE_ARG_CHARS.test(value) ? value : null;
45
47
  }
46
48
  // ---- em-ci.yml (PR gates + push-triggered badge rebuild) ----
49
+ /** The `skill-check` job - identical in the single-model and multi-model presets (it checks the
50
+ * repo-wide skill install). Runs when the repo vendors the skill bundle OR pins the em plugin in
51
+ * `.claude/settings.json` (MIL-231); a plugin repo first registers the pinned plugin on the runner,
52
+ * because `em skill check --ci` fails when the plugin is not registered on the machine. Ends
53
+ * without a trailing blank line. */
54
+ function skillCheckJob(em, emVersion) {
55
+ const [addCmd, installCmd] = pluginInstallCommands(emVersion);
56
+ return ` skill-check:
57
+ name: em skill check (vendored skill / plugin drift)
58
+ if: github.event_name == 'pull_request'
59
+ runs-on: ubuntu-latest
60
+ steps:
61
+ - uses: actions/checkout@v4
62
+ - uses: actions/setup-node@v4
63
+ with:
64
+ node-version: 20
65
+ - name: Register the pinned em plugin
66
+ run: |
67
+ if grep -Eq '"em-[0-9]+-[0-9]+-[0-9]+"' .claude/settings.json 2>/dev/null; then
68
+ npm i -g @anthropic-ai/claude-code
69
+ ${addCmd}
70
+ ${installCmd}
71
+ else
72
+ echo "no em plugin pinned in .claude/settings.json - nothing to register"
73
+ fi
74
+ - name: Check the em skills match installed em
75
+ run: |
76
+ if [ -d .claude/skills/event-modeling ] || grep -Eq '"em-[0-9]+-[0-9]+-[0-9]+"' .claude/settings.json 2>/dev/null; then
77
+ ${em} skill check --ci
78
+ else
79
+ echo "no vendored skill and no em plugin pin - skipping (install the em plugin to opt in, docs/ai-workflow.md)"
80
+ fi`;
81
+ }
82
+ /** The `system` job (MIL-239 consumer adaptation, MIL-231 wiring) - multi-model only. Fetches full
83
+ * history because the failure message names the producer commit. */
84
+ function systemJob(em) {
85
+ return ` system:
86
+ name: "em system (consumer adaptation, seam verification)"
87
+ if: github.event_name == 'pull_request'
88
+ runs-on: ubuntu-latest
89
+ steps:
90
+ - uses: actions/checkout@v4
91
+ with:
92
+ fetch-depth: 0
93
+ - uses: actions/setup-node@v4
94
+ with:
95
+ node-version: 20
96
+ - name: Check seams and consumer adaptation
97
+ run: ${em} system .`;
98
+ }
99
+ /** The `system-scope` job (MIL-240 seam-crossing gate, MIL-231 wiring) - multi-model only; needs
100
+ * the base revision. */
101
+ function systemScopeJob(em) {
102
+ return ` system-scope:
103
+ name: "em system scope (seam-crossing change set)"
104
+ if: github.event_name == 'pull_request'
105
+ runs-on: ubuntu-latest
106
+ steps:
107
+ - uses: actions/checkout@v4
108
+ with:
109
+ fetch-depth: 0
110
+ - uses: actions/setup-node@v4
111
+ with:
112
+ node-version: 20
113
+ - name: Check the change set against the seams
114
+ run: |
115
+ base="\${{ github.event.pull_request.base.sha }}"
116
+ ${em} system scope --base "$base"`;
117
+ }
118
+ /** The `codeowners-check` job (MIL-234 command, MIL-231 wiring) - multi-model presets only: a
119
+ * single-model repo has no seams to route review for. One job for the whole model set. */
120
+ function codeownersCheckJob(em) {
121
+ return ` codeowners-check:
122
+ name: "em system codeowners --check (review routing current)"
123
+ if: github.event_name == 'pull_request'
124
+ runs-on: ubuntu-latest
125
+ steps:
126
+ - uses: actions/checkout@v4
127
+ - uses: actions/setup-node@v4
128
+ with:
129
+ node-version: 20
130
+ - name: Check the generated CODEOWNERS block is current
131
+ run: ${em} system codeowners --check .`;
132
+ }
133
+ /** The `glossary` job - identical in both presets (it already spans every tracked `*.em`). */
134
+ function glossaryJob(em) {
135
+ return ` glossary:
136
+ name: "em glossary --fail-on-conflicts (cross-model vocabulary)"
137
+ if: github.event_name == 'pull_request'
138
+ runs-on: ubuntu-latest
139
+ steps:
140
+ - uses: actions/checkout@v4
141
+ - uses: actions/setup-node@v4
142
+ with:
143
+ node-version: 20
144
+ - name: Check glossary consistency across models
145
+ run: |
146
+ mapfile -t models < <(git ls-files '*.em')
147
+ ${em} glossary "\${models[@]}" --fail-on-conflicts`;
148
+ }
47
149
  /** The managed block only (no header/`on:`/`jobs:` scaffolding) — what gets written between
48
150
  * the marker pair, both for a fresh file and to patch an existing marked one in place.
49
151
  *
@@ -85,6 +187,24 @@ export function ciManagedBody(model, testsDir, emVersion) {
85
187
  done
86
188
  exit $status
87
189
 
190
+ api-check:
191
+ name: "em api check (contract current; additive/breaking annotation)"
192
+ if: github.event_name == 'pull_request'
193
+ runs-on: ubuntu-latest
194
+ steps:
195
+ - uses: actions/checkout@v4
196
+ with:
197
+ fetch-depth: 0
198
+ - uses: actions/setup-node@v4
199
+ with:
200
+ node-version: 20
201
+ - name: Check the generated contract is current
202
+ # Fails only when contracts/<model key>.tsp is missing or stale (run em api generate).
203
+ # The additive/breaking lines are annotation for the reviewer, never a gate (MIL-237).
204
+ run: |
205
+ base="\${{ github.event.pull_request.base.sha }}"
206
+ ${em} api check "${model}" --base "$base"
207
+
88
208
  slice-index:
89
209
  name: "em slice index --check (README table drift)"
90
210
  if: github.event_name == 'pull_request'
@@ -126,22 +246,7 @@ export function ciManagedBody(model, testsDir, emVersion) {
126
246
  - name: Check slice doc version/content agreement
127
247
  run: ${em} ledger "${model}" --from "\${{ github.event.pull_request.base.sha }}"
128
248
 
129
- skill-check:
130
- name: em skill check (vendored skill drift)
131
- if: github.event_name == 'pull_request'
132
- runs-on: ubuntu-latest
133
- steps:
134
- - uses: actions/checkout@v4
135
- - uses: actions/setup-node@v4
136
- with:
137
- node-version: 20
138
- - name: Check vendored skill matches installed em
139
- run: |
140
- if [ -d .claude/skills/event-modeling ]; then
141
- ${em} skill check
142
- else
143
- echo "no vendored skill at .claude/skills/event-modeling - skipping (run em skill install to opt in)"
144
- fi
249
+ ${skillCheckJob(em, emVersion)}
145
250
 
146
251
  upgrade-check:
147
252
  name: "em upgrade --check (advisory - hard incompatibilities only, MIL-219)"
@@ -160,19 +265,7 @@ export function ciManagedBody(model, testsDir, emVersion) {
160
265
  # steps are applicable" case. See docs/upgrading.md.
161
266
  run: ${em} upgrade "${model}" --check
162
267
 
163
- glossary:
164
- name: "em glossary --fail-on-conflicts (cross-model vocabulary)"
165
- if: github.event_name == 'pull_request'
166
- runs-on: ubuntu-latest
167
- steps:
168
- - uses: actions/checkout@v4
169
- - uses: actions/setup-node@v4
170
- with:
171
- node-version: 20
172
- - name: Check glossary consistency across models
173
- run: |
174
- mapfile -t models < <(git ls-files '*.em')
175
- ${em} glossary "\${models[@]}" --fail-on-conflicts
268
+ ${glossaryJob(em)}
176
269
 
177
270
  status-badge:
178
271
  name: rebuild status badge (advisory - publish only, never a gate)
@@ -233,12 +326,199 @@ export function buildCiWorkflowFile(model, testsDir, emVersion) {
233
326
  const { start, end } = markerPair(CI_WORKFLOW_MARKER, "hash");
234
327
  return `${ciWorkflowHeader(model, emVersion)}${start}\n${ciManagedBody(model, testsDir, emVersion)}\n${end}\n`;
235
328
  }
329
+ /** Deterministic, valid GitHub Actions job-id suffix per model key. Keys are kebab-case with an
330
+ * optional `~N` collision suffix (MIL-193), and `~` is not an Actions id character, so it
331
+ * becomes `-`; if two keys then collide (`a-2` and `a~2`), the later one in key order gets a
332
+ * numeric suffix until it is unique. Keys are processed in sorted order, so the mapping never
333
+ * depends on manifest order. */
334
+ export function ciJobIds(keys) {
335
+ const ids = new Map();
336
+ const used = new Set();
337
+ for (const key of [...keys].sort(compareKeys)) {
338
+ const base = key.toLowerCase().replace(/[^a-z0-9-]/g, "-");
339
+ let id = base;
340
+ for (let n = 2; used.has(id); n++)
341
+ id = `${base}-${n}`;
342
+ used.add(id);
343
+ ids.set(key, id);
344
+ }
345
+ return ids;
346
+ }
347
+ function compareKeys(a, b) {
348
+ return a < b ? -1 : a > b ? 1 : 0;
349
+ }
350
+ /** Models in the order the generators emit them (key order, not manifest order) so reordering
351
+ * the manifest never reads as drift. */
352
+ function sortedModels(models) {
353
+ return [...models].sort((a, b) => compareKeys(a.key, b.key));
354
+ }
355
+ /** The directory a model's design artifacts live in (R6: `dirname(<model>.em)`). */
356
+ export function ciModelDir(modelPath) {
357
+ const d = dirname(modelPath);
358
+ return d === "" ? "." : d;
359
+ }
360
+ function modelJobs(m, id, testsDir, em) {
361
+ const dir = ciModelDir(m.path);
362
+ const pathspec = dir === "." ? "'*.em'" : `"${dir}/*.em"`;
363
+ const badge = dir === "." ? "status-badge.svg" : `${dir}/status-badge.svg`;
364
+ return ` validate-${id}:
365
+ name: "em validate (changed .em files) [${m.key}]"
366
+ if: github.event_name == 'pull_request'
367
+ runs-on: ubuntu-latest
368
+ steps:
369
+ - uses: actions/checkout@v4
370
+ with:
371
+ fetch-depth: 0
372
+ - uses: actions/setup-node@v4
373
+ with:
374
+ node-version: 20
375
+ - name: Validate changed models
376
+ run: |
377
+ set -e
378
+ base="\${{ github.event.pull_request.base.sha }}"
379
+ head="\${{ github.event.pull_request.head.sha }}"
380
+ changed=$(git diff --name-only "$base" "$head" -- ${pathspec})
381
+ if [ -z "$changed" ]; then
382
+ echo "no .em files changed"
383
+ exit 0
384
+ fi
385
+ status=0
386
+ for f in $changed; do
387
+ echo "::group::em validate $f"
388
+ ${em} validate "$f" || status=1
389
+ ${em} validate "$f" --list-issues
390
+ echo "::endgroup::"
391
+ done
392
+ exit $status
393
+
394
+ api-check-${id}:
395
+ name: "em api check (contract current; additive/breaking annotation) [${m.key}]"
396
+ if: github.event_name == 'pull_request'
397
+ runs-on: ubuntu-latest
398
+ steps:
399
+ - uses: actions/checkout@v4
400
+ with:
401
+ fetch-depth: 0
402
+ - uses: actions/setup-node@v4
403
+ with:
404
+ node-version: 20
405
+ - name: Check the generated contract is current
406
+ # Fails only when contracts/<model key>.tsp is missing or stale (run em api generate).
407
+ # The additive/breaking lines are annotation for the reviewer, never a gate (MIL-237).
408
+ run: |
409
+ base="\${{ github.event.pull_request.base.sha }}"
410
+ ${em} api check "${m.path}" --base "$base"
411
+
412
+ slice-index-${id}:
413
+ name: "em slice index --check (README table drift) [${m.key}]"
414
+ if: github.event_name == 'pull_request'
415
+ runs-on: ubuntu-latest
416
+ steps:
417
+ - uses: actions/checkout@v4
418
+ - uses: actions/setup-node@v4
419
+ with:
420
+ node-version: 20
421
+ - name: Check README Slices table is current
422
+ run: ${em} slice index "${m.path}" --check
423
+
424
+ coverage-${id}:
425
+ name: "em coverage --strict (invariant citations) [${m.key}]"
426
+ if: github.event_name == 'pull_request'
427
+ runs-on: ubuntu-latest
428
+ steps:
429
+ - uses: actions/checkout@v4
430
+ - uses: actions/setup-node@v4
431
+ with:
432
+ node-version: 20
433
+ - name: Check invariant test coverage
434
+ # Counts INV-* IDs in \`implemented\` slice docs only (MIL-207) - a ready-to-implement
435
+ # doc has nothing yet to cite it, so this job is green on a fresh scaffold and on a
436
+ # doc-only ratification PR. Pass --include-ready for the older, forward-looking report.
437
+ run: ${em} coverage "${m.path}" --tests "${testsDir}" --strict
438
+
439
+ ledger-${id}:
440
+ name: "em ledger (slice doc version/content agreement) [${m.key}]"
441
+ if: github.event_name == 'pull_request'
442
+ runs-on: ubuntu-latest
443
+ steps:
444
+ - uses: actions/checkout@v4
445
+ with:
446
+ fetch-depth: 0
447
+ - uses: actions/setup-node@v4
448
+ with:
449
+ node-version: 20
450
+ - name: Check slice doc version/content agreement
451
+ run: ${em} ledger "${m.path}" --from "\${{ github.event.pull_request.base.sha }}"
452
+
453
+ upgrade-check-${id}:
454
+ name: "em upgrade --check (advisory - hard incompatibilities only, MIL-219) [${m.key}]"
455
+ if: github.event_name == 'pull_request'
456
+ runs-on: ubuntu-latest
457
+ steps:
458
+ - uses: actions/checkout@v4
459
+ with:
460
+ fetch-depth: 0
461
+ - uses: actions/setup-node@v4
462
+ with:
463
+ node-version: 20
464
+ - name: Check for hard upgrade incompatibilities
465
+ # Advisory: exits non-zero ONLY on a hard incompatibility (an unparseable state file, an
466
+ # old .em shape em migrate itself refuses) - never on the ordinary "some mechanical
467
+ # steps are applicable" case. See docs/upgrading.md.
468
+ run: ${em} upgrade "${m.path}" --check
469
+
470
+ status-badge-${id}:
471
+ name: "rebuild status badge (advisory - publish only, never a gate) [${m.key}]"
472
+ if: github.event_name == 'push'
473
+ runs-on: ubuntu-latest
474
+ permissions:
475
+ contents: write
476
+ steps:
477
+ - uses: actions/checkout@v4
478
+ - uses: actions/setup-node@v4
479
+ with:
480
+ node-version: 20
481
+ - name: Rebuild status badge
482
+ run: ${em} status "${m.path}" --tests "${testsDir}" --badge -o "${badge}"
483
+ - name: Commit badge if changed
484
+ # Every model has its own badge job and they run in parallel on a push, so rebase onto
485
+ # whatever a sibling job pushed first.
486
+ run: |
487
+ git add -- "${badge}"
488
+ if git diff --cached --quiet -- "${badge}"; then
489
+ echo "${badge} unchanged"
490
+ exit 0
491
+ fi
492
+ git config user.name "em-ci"
493
+ git config user.email "em-ci@users.noreply.github.com"
494
+ git commit -m "em ci: rebuild status badge [skip ci]"
495
+ git pull --rebase origin "$GITHUB_REF_NAME"
496
+ git push`;
497
+ }
498
+ /** The managed block for a whole system: per-model `validate-`/`slice-index-`/`coverage-`/
499
+ * `ledger-`/`upgrade-check-`/`status-badge-<id>` jobs, then ONE `skill-check` and ONE
500
+ * `glossary` (both repo-wide). Same version-pin discipline as `ciManagedBody`. */
501
+ export function ciManagedBodyMulti(models, testsDir, emVersion) {
502
+ const em = `npx @milehimikey/em@${emVersion}`;
503
+ const sorted = sortedModels(models);
504
+ const ids = ciJobIds(sorted.map((m) => m.key));
505
+ const perModel = sorted.map((m) => modelJobs(m, ids.get(m.key), testsDir, em));
506
+ return [...perModel, codeownersCheckJob(em), systemJob(em), systemScopeJob(em), skillCheckJob(em, emVersion), glossaryJob(em)].join("\n\n");
507
+ }
508
+ export function buildCiWorkflowFileMulti(arg, models, testsDir, emVersion) {
509
+ const { start, end } = markerPair(CI_WORKFLOW_MARKER, "hash");
510
+ return `${ciWorkflowHeader(arg, emVersion)}${start}\n${ciManagedBodyMulti(models, testsDir, emVersion)}\n${end}\n`;
511
+ }
236
512
  // ---- em-conform.yml (scheduled, advisory-only conformance cadence) ----
237
513
  /** The managed block only (no header/`on:`/`permissions:`/`jobs:` scaffolding) — what gets
238
514
  * written between the marker pair, both for a fresh file and to patch an existing marked one
239
515
  * in place. */
240
- export function conformManagedBody(model, emVersion) {
516
+ export function conformManagedBody(model, emVersion, usePlugin = false) {
241
517
  const modelDir = dirname(model) === "." ? "." : dirname(model);
518
+ // MIL-231: a repo that declares the em plugin installs it (pinned to this em's version) and runs
519
+ // /em:conform; any other repo keeps the vendored bundle install and /event-modeling conform.
520
+ const install = usePlugin ? `${pluginInstallCommands(emVersion).join(" && ")}` : "em skill install --force";
521
+ const invoke = usePlugin ? "/em:conform" : "/event-modeling conform";
242
522
  return ` conform:
243
523
  runs-on: ubuntu-latest
244
524
  steps:
@@ -253,8 +533,8 @@ export function conformManagedBody(model, emVersion) {
253
533
  - name: Run conform phase
254
534
  run: |
255
535
  npm i -g @milehimikey/em@${emVersion} @anthropic-ai/claude-code
256
- em skill install --force
257
- claude -p "/event-modeling conform" \\
536
+ ${install}
537
+ claude -p "${invoke}" \\
258
538
  --allowedTools "Bash(em:*),Bash(git:*),Read,Grep,Glob,Write,Edit"
259
539
  env:
260
540
  ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
@@ -296,34 +576,108 @@ permissions:
296
576
  jobs:
297
577
  `;
298
578
  }
299
- export function buildConformWorkflowFile(model, emVersion) {
579
+ export function buildConformWorkflowFile(model, emVersion, usePlugin = false) {
300
580
  const { start, end } = markerPair(CONFORM_WORKFLOW_MARKER, "hash");
301
- return `${conformWorkflowHeader(model, emVersion)}${start}\n${conformManagedBody(model, emVersion)}\n${end}\n`;
581
+ return `${conformWorkflowHeader(model, emVersion)}${start}\n${conformManagedBody(model, emVersion, usePlugin)}\n${end}\n`;
582
+ }
583
+ /** Conformance for a whole system: the same single `conform` job fanned out over a matrix of
584
+ * model directories (`MODEL_DIR: ${{ matrix.model }}`). Directories, not models: two models in
585
+ * one directory share a conformance folder, so the matrix lists each directory once. */
586
+ export function conformManagedBodyMulti(models, emVersion, usePlugin = false) {
587
+ const dirs = [...new Set(models.map((m) => ciModelDir(m.path)))].sort(compareKeys);
588
+ const single = conformManagedBody("x/x.em", emVersion, usePlugin);
589
+ const strategy = ` strategy:\n fail-fast: false\n matrix:\n model:\n${dirs.map((d) => ` - ${JSON.stringify(d)}`).join("\n")}\n`;
590
+ return single
591
+ .replace(" runs-on: ubuntu-latest\n", ` runs-on: ubuntu-latest\n${strategy}`)
592
+ .replace(/MODEL_DIR: x$/, "MODEL_DIR: ${{ matrix.model }}");
593
+ }
594
+ export function buildConformWorkflowFileMulti(arg, models, emVersion, usePlugin = false) {
595
+ const { start, end } = markerPair(CONFORM_WORKFLOW_MARKER, "hash");
596
+ return `${conformWorkflowHeader(arg, emVersion)}${start}\n${conformManagedBodyMulti(models, emVersion, usePlugin)}\n${end}\n`;
597
+ }
598
+ function dedupe(values) {
599
+ return [...new Set(values)];
302
600
  }
303
601
  /**
304
- * Which model an existing managed block was generated for, read from the block's own content
305
- * (MIL-256). Deliberately content-derived rather than a new marker line: blocks written by em
306
- * 1.13.0 and earlier carry no machine-readable model, but every one of them bakes the model into
307
- * the per-model `run:` lines (em-ci.yml: `slice index "<model>" --check`; em-conform.yml:
308
- * `MODEL_DIR: <dir>`). Neither pattern includes the `npx @milehimikey/em@<ver>` pin, so a
309
- * version difference can never read as a different model. Returns null when the block was
310
- * hand-edited past recognition - callers then treat it as the same model (the conservative,
311
- * pre-MIL-256 behavior) rather than refusing on a guess.
602
+ * The model SET an existing managed block was generated for, read from the block's own content
603
+ * (MIL-256, widened to sets by MIL-233). Deliberately content-derived rather than a new marker
604
+ * line: blocks written by em 1.13.0 and earlier carry no machine-readable model, but every one
605
+ * of them bakes the model into the per-model `run:` lines. em-ci.yml: every
606
+ * `slice index "<model>" --check` line (one per model); em-conform.yml: every `matrix.model`
607
+ * entry of a multi-model block, else the single `MODEL_DIR: <dir>` line. None of these patterns
608
+ * includes the `npx @milehimikey/em@<ver>` pin, so a version difference can never read as a
609
+ * different model. Returns `[]` when the block was hand-edited past recognition - callers then
610
+ * treat it as covering the requested models (the conservative, pre-MIL-256 behavior) rather than
611
+ * refusing on a guess.
312
612
  */
313
- export function managedBlockModel(markerName, blockBody) {
613
+ export function managedBlockModels(markerName, blockBody) {
314
614
  if (markerName === CONFORM_WORKFLOW_MARKER) {
615
+ const lines = blockBody.split("\n");
616
+ const at = lines.findIndex((l) => /^\s*matrix:\s*$/.test(l));
617
+ if (at >= 0) {
618
+ const flow = /^\s*model:\s*\[(.*)\]\s*$/.exec(lines[at + 1] ?? "");
619
+ if (flow)
620
+ return dedupe(flow[1].split(",").map((v) => unquoteScalar(v.trim())).filter((v) => v !== ""));
621
+ if (/^\s*model:\s*$/.test(lines[at + 1] ?? "")) {
622
+ const out = [];
623
+ for (const l of lines.slice(at + 2)) {
624
+ const item = /^\s*- (.+?)\s*$/.exec(l);
625
+ if (!item)
626
+ break;
627
+ out.push(unquoteScalar(item[1]));
628
+ }
629
+ return dedupe(out);
630
+ }
631
+ }
315
632
  const m = /^\s*MODEL_DIR: (.+?)\s*$/m.exec(blockBody);
316
- return m ? m[1] : null;
633
+ return m && !m[1].startsWith("${{") ? [m[1]] : [];
317
634
  }
318
- const m = /\bslice index "([^"]*)" --check\b/.exec(blockBody);
319
- return m ? m[1] : null;
635
+ return dedupe([...blockBody.matchAll(/\bslice index "([^"]*)" --check\b/g)].map((m) => m[1]));
636
+ }
637
+ function unquoteScalar(v) {
638
+ if (v.startsWith('"') && v.endsWith('"') && v.length >= 2) {
639
+ try {
640
+ return JSON.parse(v);
641
+ }
642
+ catch {
643
+ return v.slice(1, -1);
644
+ }
645
+ }
646
+ return v;
320
647
  }
321
648
  function existingBlockBody(content, markerName) {
322
649
  const m = markerRegex(markerName, "hash").exec(content);
323
650
  return m ? m[2] : null;
324
651
  }
325
- /** `--force` over another model's block also retargets the generated header's own wording
326
- * (`em ci init <model>` on `#` comment lines above the start marker) at the new model, taken
652
+ /** The model set of an existing workflow file's managed block (`[]` when unmarked/unreadable). */
653
+ export function fileBlockModels(content, markerName) {
654
+ const body = existingBlockBody(content, markerName);
655
+ return body === null ? [] : managedBlockModels(markerName, body);
656
+ }
657
+ /** `--check`'s extra detail for a stale multi-model block: which models the manifest adds and
658
+ * which the workflow still names that the manifest no longer does. `null` when the sets agree. */
659
+ export function describeSetChange(previous, requested) {
660
+ const added = requested.filter((m) => !previous.includes(m));
661
+ const removed = previous.filter((m) => !requested.includes(m));
662
+ if (added.length === 0 && removed.length === 0)
663
+ return null;
664
+ const parts = [];
665
+ if (added.length > 0)
666
+ parts.push(`no section for ${added.join(", ")}`);
667
+ if (removed.length > 0)
668
+ parts.push(`section for ${removed.join(", ")} is not in the manifest`);
669
+ return parts.join("; ");
670
+ }
671
+ /** The one-line explanation of an `other-models` refusal (also printed by `--check`), ending in
672
+ * what to do about it. Names both sets. */
673
+ export function describeOtherModels(path, status) {
674
+ if (status.narrowing) {
675
+ return `${path} covers ${status.previous.length} models (${status.previous.join(", ")}) - re-run with the system manifest, or --force to narrow it to ${status.requested.join(", ")}`;
676
+ }
677
+ return `${path} was generated for ${status.previous.join(", ")}, not ${status.requested.join(", ")} - re-run with --force to replace it`;
678
+ }
679
+ /** `--force` over another model set's block also retargets the generated header's own wording
680
+ * (`em ci init <arg>` on `#` comment lines above the start marker) at the new argument, taken
327
681
  * from the freshly generated header. Only those exact generated phrasings are touched; a
328
682
  * header the repo reworded, and everything below the start marker, is left alone. */
329
683
  function retargetHeader(content, generated, markerName) {
@@ -345,13 +699,18 @@ function retargetHeader(content, generated, markerName) {
345
699
  * the text between the markers, reused to patch an existing marked file in place without
346
700
  * touching whatever a repo added around it.
347
701
  *
348
- * MIL-256 (#174): the managed block is single-model (multi-model is MIL-233). A marked file
349
- * whose block was generated for a DIFFERENT model than `managedBody` is `other-model` (refused)
350
- * unless `force`, then `replace-model`. The same model with any other difference (version pin,
351
- * old text) stays `stale`, exactly as before. `em upgrade`'s ci-block step re-derives the model
352
- * from the existing file itself, so it only ever plans the same model.
702
+ * Block identity (MIL-256 #174, MIL-233): a block is identified by the SET of models it names
703
+ * (`managedBlockModels`). The requested set wins unless that would lose models by accident:
704
+ * - disjoint sets are `other-models` (refused without `force`, `replace-models` with it);
705
+ * - `multi` false (a single `<model.em>` argument) against a block naming several models is
706
+ * `other-models` too (`narrowing`) - the single-model form must never silently narrow it;
707
+ * - otherwise (same set, superset, subset or overlap, requested from the system manifest) the
708
+ * block is regenerated for the requested set - `stale` when anything differs, e.g. a version
709
+ * pin, an added model or a removed one.
710
+ * `em upgrade`'s ci-block step re-derives its arguments from the existing file (and, for a
711
+ * multi-model block, from the manifest), so it only ever plans the same set.
353
712
  */
354
- export function planCiFile(path, generated, managedBody, markerName, force) {
713
+ export function planCiFile(path, generated, managedBody, markerName, force, multi = false) {
355
714
  if (!existsSync(path))
356
715
  return { kind: "create", content: generated };
357
716
  const original = readFileSync(path, "utf8");
@@ -361,19 +720,64 @@ export function planCiFile(path, generated, managedBody, markerName, force) {
361
720
  }
362
721
  if (updated === original)
363
722
  return { kind: "ok", content: original };
364
- const existing = existingBlockBody(original, markerName);
365
- const previous = existing === null ? null : managedBlockModel(markerName, existing);
366
- const next = managedBlockModel(markerName, managedBody);
367
- if (previous !== null && next !== null && previous !== next) {
368
- if (!force)
369
- return { kind: "other-model", previous };
370
- return { kind: "replace-model", content: retargetHeader(updated, generated, markerName), previous };
723
+ const previous = fileBlockModels(original, markerName);
724
+ const requested = managedBlockModels(markerName, managedBody);
725
+ if (previous.length > 0 && requested.length > 0) {
726
+ const overlap = requested.some((m) => previous.includes(m));
727
+ const narrowing = overlap && !multi && previous.length > 1;
728
+ if (!overlap || narrowing) {
729
+ if (!force)
730
+ return { kind: "other-models", previous, requested, narrowing };
731
+ return { kind: "replace-models", content: retargetHeader(updated, generated, markerName), previous, requested, narrowing };
732
+ }
371
733
  }
372
734
  return { kind: "stale", content: updated, current: original };
373
735
  }
374
736
  export function applyCiFile(path, status) {
375
- if (status.kind === "create" || status.kind === "stale" || status.kind === "would-replace" || status.kind === "replace-model") {
737
+ if (status.kind === "create" || status.kind === "stale" || status.kind === "would-replace" || status.kind === "replace-models") {
376
738
  writeFileSync(path, status.content, "utf8");
377
739
  }
378
740
  }
741
+ /** Read the system manifest at `target` (a file, or a directory holding `system.yaml`) and turn
742
+ * its `models:` into `CiModel`s whose paths are relative to `baseDir` - the repo root the
743
+ * workflows run from. Reuses MIL-235's `parseManifest`; compiling the models is not needed to
744
+ * write a workflow, so a model that does not compile does not block it. Every path goes through
745
+ * `findUnsafeCiInitArg`; a source must be a `.em` file inside `baseDir`. */
746
+ export function ciModelsFromManifest(target, baseDir) {
747
+ let manifestPath = target;
748
+ try {
749
+ if (statSync(target).isDirectory())
750
+ manifestPath = join(target, "system.yaml");
751
+ }
752
+ catch {
753
+ return { ok: false, message: `cannot read ${target}` };
754
+ }
755
+ let text;
756
+ try {
757
+ text = readFileSync(manifestPath, "utf8");
758
+ }
759
+ catch {
760
+ return { ok: false, message: `cannot read ${manifestPath}` };
761
+ }
762
+ const parsed = parseManifest(text);
763
+ if (!parsed.ok) {
764
+ return { ok: false, message: `${manifestPath} is not a valid system manifest: ${parsed.diagnostics.map((d) => d.message).join("; ")}` };
765
+ }
766
+ const models = [];
767
+ for (const entry of parsed.manifest.models) {
768
+ if (!entry.source.endsWith(".em")) {
769
+ return { ok: false, message: `${manifestPath}: model "${entry.key}" source ${entry.source} is not a .em file - the CI jobs run em against .em sources` };
770
+ }
771
+ const rel = relative(resolve(baseDir), resolve(dirname(manifestPath), entry.source)).split(sep).join("/");
772
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
773
+ return { ok: false, message: `${manifestPath}: model "${entry.key}" source ${entry.source} is outside the repository root (${baseDir})` };
774
+ }
775
+ const unsafe = findUnsafeCiInitArg(rel);
776
+ if (unsafe) {
777
+ return { ok: false, message: `${manifestPath}: model "${entry.key}" path must not contain '"', '\`', '$', or a newline: ${unsafe}` };
778
+ }
779
+ models.push({ key: entry.key, path: rel });
780
+ }
781
+ return { ok: true, models, manifestPath };
782
+ }
379
783
  //# sourceMappingURL=ciInit.js.map