@milehimikey/em 1.13.0 → 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 (239) 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 +52 -12
  6. package/.claude/skills/event-modeling-conform/SKILL.md +1 -1
  7. package/.claude/skills/event-modeling-conform/reference/conform.md +1 -1
  8. package/.claude/skills/event-modeling-design/SKILL.md +140 -18
  9. package/.claude/skills/event-modeling-discover/SKILL.md +1 -1
  10. package/.claude/skills/event-modeling-engagement/SKILL.md +192 -0
  11. package/.claude/skills/event-modeling-engagement/reference/engagement.md +143 -0
  12. package/.claude/skills/event-modeling-implement/SKILL.md +7 -4
  13. package/.claude/skills/event-modeling-implement/reference/implement.md +51 -12
  14. package/.claude/skills/event-modeling-review/SKILL.md +1 -1
  15. package/.claude/skills/event-modeling-shared/reference/em-dsl.md +274 -188
  16. package/.claude/skills/event-modeling-shared/reference/operating-principles.md +61 -0
  17. package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +116 -4
  18. package/.claude/skills/event-modeling-shared/templates/constitution.md +18 -1
  19. package/.claude/skills/event-modeling-shared/templates/slice.md +64 -39
  20. package/.claude/skills/event-modeling-shared/templates/state.md +5 -1
  21. package/.claude-plugin/marketplace.json +14 -0
  22. package/README.md +25 -11
  23. package/dist/catalog/apiFirst.d.ts +28 -0
  24. package/dist/catalog/apiFirst.js +49 -0
  25. package/dist/catalog/apiFirst.js.map +1 -0
  26. package/dist/catalog/docJoin.d.ts +13 -0
  27. package/dist/catalog/docJoin.js +11 -0
  28. package/dist/catalog/docJoin.js.map +1 -1
  29. package/dist/catalog/docModelConsistencyValidate.js +44 -4
  30. package/dist/catalog/docModelConsistencyValidate.js.map +1 -1
  31. package/dist/catalog/invariantsValidate.d.ts +4 -0
  32. package/dist/catalog/invariantsValidate.js +102 -0
  33. package/dist/catalog/invariantsValidate.js.map +1 -0
  34. package/dist/catalog/scenarios.d.ts +15 -0
  35. package/dist/catalog/scenarios.js +97 -0
  36. package/dist/catalog/scenarios.js.map +1 -0
  37. package/dist/catalog/sliceDoc.d.ts +8 -0
  38. package/dist/catalog/sliceDoc.js +9 -0
  39. package/dist/catalog/sliceDoc.js.map +1 -1
  40. package/dist/catalog/sliceReadyValidate.d.ts +31 -4
  41. package/dist/catalog/sliceReadyValidate.js +66 -8
  42. package/dist/catalog/sliceReadyValidate.js.map +1 -1
  43. package/dist/catalog/sliceSections.d.ts +55 -0
  44. package/dist/catalog/sliceSections.js +317 -0
  45. package/dist/catalog/sliceSections.js.map +1 -0
  46. package/dist/cli/agentsMd.js +7 -1
  47. package/dist/cli/agentsMd.js.map +1 -1
  48. package/dist/cli/api.d.ts +90 -0
  49. package/dist/cli/api.js +318 -0
  50. package/dist/cli/api.js.map +1 -0
  51. package/dist/cli/ciInit.d.ts +88 -3
  52. package/dist/cli/ciInit.js +507 -50
  53. package/dist/cli/ciInit.js.map +1 -1
  54. package/dist/cli/conformScope.d.ts +28 -0
  55. package/dist/cli/conformScope.js +67 -0
  56. package/dist/cli/conformScope.js.map +1 -1
  57. package/dist/cli/coverage.d.ts +25 -2
  58. package/dist/cli/coverage.js +86 -12
  59. package/dist/cli/coverage.js.map +1 -1
  60. package/dist/cli/engagement.d.ts +210 -0
  61. package/dist/cli/engagement.js +444 -0
  62. package/dist/cli/engagement.js.map +1 -0
  63. package/dist/cli/engagementFile.d.ts +86 -0
  64. package/dist/cli/engagementFile.js +255 -0
  65. package/dist/cli/engagementFile.js.map +1 -0
  66. package/dist/cli/metrics.d.ts +10 -0
  67. package/dist/cli/metrics.js +49 -1
  68. package/dist/cli/metrics.js.map +1 -1
  69. package/dist/cli/pluginPin.d.ts +55 -0
  70. package/dist/cli/pluginPin.js +188 -0
  71. package/dist/cli/pluginPin.js.map +1 -0
  72. package/dist/cli/ratify.d.ts +27 -2
  73. package/dist/cli/ratify.js +112 -13
  74. package/dist/cli/ratify.js.map +1 -1
  75. package/dist/cli/reratify.d.ts +11 -4
  76. package/dist/cli/reratify.js +89 -35
  77. package/dist/cli/reratify.js.map +1 -1
  78. package/dist/cli/scopeInputs.d.ts +48 -0
  79. package/dist/cli/scopeInputs.js +176 -0
  80. package/dist/cli/scopeInputs.js.map +1 -0
  81. package/dist/cli/skillCheck.d.ts +15 -1
  82. package/dist/cli/skillCheck.js +22 -1
  83. package/dist/cli/skillCheck.js.map +1 -1
  84. package/dist/cli/skillDirs.d.ts +5 -0
  85. package/dist/cli/skillDirs.js +13 -1
  86. package/dist/cli/skillDirs.js.map +1 -1
  87. package/dist/cli/skillSync.d.ts +4 -0
  88. package/dist/cli/skillSync.js +23 -0
  89. package/dist/cli/skillSync.js.map +1 -1
  90. package/dist/cli/sliceNew.d.ts +11 -7
  91. package/dist/cli/sliceNew.js +91 -18
  92. package/dist/cli/sliceNew.js.map +1 -1
  93. package/dist/cli/sliceSync.d.ts +50 -0
  94. package/dist/cli/sliceSync.js +170 -0
  95. package/dist/cli/sliceSync.js.map +1 -0
  96. package/dist/cli/status.d.ts +24 -1
  97. package/dist/cli/status.js +20 -1
  98. package/dist/cli/status.js.map +1 -1
  99. package/dist/cli/systemInputs.d.ts +53 -10
  100. package/dist/cli/systemInputs.js +186 -11
  101. package/dist/cli/systemInputs.js.map +1 -1
  102. package/dist/cli/upgrade.d.ts +30 -7
  103. package/dist/cli/upgrade.js +434 -50
  104. package/dist/cli/upgrade.js.map +1 -1
  105. package/dist/cli/usageLog.d.ts +4 -2
  106. package/dist/cli/usageLog.js +4 -2
  107. package/dist/cli/usageLog.js.map +1 -1
  108. package/dist/cli.js +631 -100
  109. package/dist/cli.js.map +1 -1
  110. package/dist/emit/api.d.ts +29 -0
  111. package/dist/emit/api.js +220 -0
  112. package/dist/emit/api.js.map +1 -0
  113. package/dist/emit/apiCheckJson.d.ts +3 -0
  114. package/dist/emit/apiCheckJson.js +21 -0
  115. package/dist/emit/apiCheckJson.js.map +1 -0
  116. package/dist/emit/codeownersJson.d.ts +3 -0
  117. package/dist/emit/codeownersJson.js +19 -0
  118. package/dist/emit/codeownersJson.js.map +1 -0
  119. package/dist/emit/coverageJson.d.ts +5 -2
  120. package/dist/emit/coverageJson.js +6 -2
  121. package/dist/emit/coverageJson.js.map +1 -1
  122. package/dist/emit/diffJson.d.ts +1 -1
  123. package/dist/emit/diffJson.js +8 -1
  124. package/dist/emit/diffJson.js.map +1 -1
  125. package/dist/emit/engagementJson.d.ts +8 -0
  126. package/dist/emit/engagementJson.js +46 -0
  127. package/dist/emit/engagementJson.js.map +1 -0
  128. package/dist/emit/glossaryJson.d.ts +1 -1
  129. package/dist/emit/glossaryJson.js +4 -1
  130. package/dist/emit/glossaryJson.js.map +1 -1
  131. package/dist/emit/json.d.ts +17 -1
  132. package/dist/emit/json.js +30 -1
  133. package/dist/emit/json.js.map +1 -1
  134. package/dist/emit/metricsJson.d.ts +1 -1
  135. package/dist/emit/metricsJson.js +4 -1
  136. package/dist/emit/metricsJson.js.map +1 -1
  137. package/dist/emit/queryJson.d.ts +1 -1
  138. package/dist/emit/queryJson.js +7 -1
  139. package/dist/emit/queryJson.js.map +1 -1
  140. package/dist/emit/scopeJson.d.ts +12 -0
  141. package/dist/emit/scopeJson.js +26 -0
  142. package/dist/emit/scopeJson.js.map +1 -0
  143. package/dist/emit/skillCheckJson.d.ts +3 -2
  144. package/dist/emit/skillCheckJson.js +10 -2
  145. package/dist/emit/skillCheckJson.js.map +1 -1
  146. package/dist/emit/statusJson.d.ts +1 -1
  147. package/dist/emit/statusJson.js +15 -1
  148. package/dist/emit/statusJson.js.map +1 -1
  149. package/dist/emit/systemJson.d.ts +15 -8
  150. package/dist/emit/systemJson.js +28 -13
  151. package/dist/emit/systemJson.js.map +1 -1
  152. package/dist/emit/upgradeJson.d.ts +1 -1
  153. package/dist/emit/upgradeJson.js +4 -1
  154. package/dist/emit/upgradeJson.js.map +1 -1
  155. package/dist/emit/validateJson.d.ts +3 -3
  156. package/dist/emit/validateJson.js +10 -4
  157. package/dist/emit/validateJson.js.map +1 -1
  158. package/dist/mcp/server.d.ts +1 -1
  159. package/dist/mcp/server.js +186 -37
  160. package/dist/mcp/server.js.map +1 -1
  161. package/dist/model/diff.d.ts +4 -1
  162. package/dist/model/diff.js +44 -8
  163. package/dist/model/diff.js.map +1 -1
  164. package/dist/model/glossary.d.ts +2 -0
  165. package/dist/model/glossary.js +1 -0
  166. package/dist/model/glossary.js.map +1 -1
  167. package/dist/model/model.d.ts +55 -2
  168. package/dist/model/model.js +57 -0
  169. package/dist/model/model.js.map +1 -1
  170. package/dist/model/qualifiedRef.d.ts +18 -0
  171. package/dist/model/qualifiedRef.js +26 -0
  172. package/dist/model/qualifiedRef.js.map +1 -1
  173. package/dist/model/queryIndex.d.ts +9 -1
  174. package/dist/model/queryIndex.js +18 -2
  175. package/dist/model/queryIndex.js.map +1 -1
  176. package/dist/model/rules.d.ts +143 -2
  177. package/dist/model/rules.js +162 -3
  178. package/dist/model/rules.js.map +1 -1
  179. package/dist/model/validate.d.ts +40 -0
  180. package/dist/model/validate.js +147 -1
  181. package/dist/model/validate.js.map +1 -1
  182. package/dist/parser/ast.d.ts +31 -3
  183. package/dist/parser/parser.js +148 -14
  184. package/dist/parser/parser.js.map +1 -1
  185. package/dist/query/format.js +1 -1
  186. package/dist/query/format.js.map +1 -1
  187. package/dist/query/verbs.d.ts +6 -0
  188. package/dist/query/verbs.js +6 -2
  189. package/dist/query/verbs.js.map +1 -1
  190. package/dist/system/adaptation.d.ts +14 -0
  191. package/dist/system/adaptation.js +49 -0
  192. package/dist/system/adaptation.js.map +1 -0
  193. package/dist/system/codeowners.d.ts +77 -0
  194. package/dist/system/codeowners.js +242 -0
  195. package/dist/system/codeowners.js.map +1 -0
  196. package/dist/system/manifest.d.ts +10 -5
  197. package/dist/system/manifest.js +54 -26
  198. package/dist/system/manifest.js.map +1 -1
  199. package/dist/system/migrateManifest.d.ts +28 -0
  200. package/dist/system/migrateManifest.js +155 -0
  201. package/dist/system/migrateManifest.js.map +1 -0
  202. package/dist/system/scope.d.ts +46 -0
  203. package/dist/system/scope.js +132 -0
  204. package/dist/system/scope.js.map +1 -0
  205. package/dist/system/verify.d.ts +50 -13
  206. package/dist/system/verify.js +179 -58
  207. package/dist/system/verify.js.map +1 -1
  208. package/dist/templates.d.ts +2 -2
  209. package/dist/templates.js +23 -2
  210. package/dist/templates.js.map +1 -1
  211. package/package.json +6 -3
  212. package/plugin/.claude-plugin/plugin.json +18 -0
  213. package/plugin/agents/em-critic.md +27 -0
  214. package/plugin/agents/em-implementer.md +35 -0
  215. package/plugin/agents/em-reviewer.md +32 -0
  216. package/plugin/agents/em-validator.md +28 -0
  217. package/plugin/shared/reference/em-dsl.md +866 -0
  218. package/plugin/shared/reference/methodology.md +329 -0
  219. package/plugin/shared/reference/operating-principles.md +222 -0
  220. package/plugin/shared/reference/slice-doc-schema.md +566 -0
  221. package/plugin/shared/templates/conformance-report.md +85 -0
  222. package/plugin/shared/templates/constitution.md +136 -0
  223. package/plugin/shared/templates/model-readme.md +46 -0
  224. package/plugin/shared/templates/slice.md +251 -0
  225. package/plugin/shared/templates/state.md +77 -0
  226. package/plugin/skills/conform/SKILL.md +93 -0
  227. package/plugin/skills/conform/reference/conform.md +428 -0
  228. package/plugin/skills/design/SKILL.md +297 -0
  229. package/plugin/skills/discover/SKILL.md +90 -0
  230. package/plugin/skills/discover/reference/extract.md +147 -0
  231. package/plugin/skills/engagement/SKILL.md +193 -0
  232. package/plugin/skills/engagement/reference/engagement.md +144 -0
  233. package/plugin/skills/event-modeling/SKILL.md +132 -0
  234. package/plugin/skills/implement/SKILL.md +61 -0
  235. package/plugin/skills/implement/reference/implement.md +439 -0
  236. package/plugin/skills/review/SKILL.md +74 -0
  237. package/dist/emit/typespec.d.ts +0 -26
  238. package/dist/emit/typespec.js +0 -356
  239. package/dist/emit/typespec.js.map +0 -1
@@ -23,9 +23,11 @@
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";
28
- import { applyMarker, markerPair } from "../util/markers.js";
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";
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";
31
33
  const CI_WORKFLOW_RELPATH = join(".github", "workflows", "em-ci.yml");
@@ -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'
@@ -107,7 +227,7 @@ export function ciManagedBody(model, testsDir, emVersion) {
107
227
  with:
108
228
  node-version: 20
109
229
  - name: Check invariant test coverage
110
- # Counts INV-* IDs in \`implemented\` slice docs only (MIL-207) — a ready-to-implement
230
+ # Counts INV-* IDs in \`implemented\` slice docs only (MIL-207) - a ready-to-implement
111
231
  # doc has nothing yet to cite it, so this job is green on a fresh scaffold and on a
112
232
  # doc-only ratification PR. Pass --include-ready for the older, forward-looking report.
113
233
  run: ${em} coverage "${model}" --tests "${testsDir}" --strict
@@ -126,25 +246,10 @@ 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
- name: "em upgrade --check (advisory — hard incompatibilities only, MIL-219)"
252
+ name: "em upgrade --check (advisory - hard incompatibilities only, MIL-219)"
148
253
  if: github.event_name == 'pull_request'
149
254
  runs-on: ubuntu-latest
150
255
  steps:
@@ -156,24 +261,14 @@ export function ciManagedBody(model, testsDir, emVersion) {
156
261
  node-version: 20
157
262
  - name: Check for hard upgrade incompatibilities
158
263
  # Advisory: exits non-zero ONLY on a hard incompatibility (an unparseable state file, an
159
- # old .em shape em migrate itself refuses) — never on the ordinary "some mechanical
264
+ # old .em shape em migrate itself refuses) - never on the ordinary "some mechanical
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: ${em} glossary $(git ls-files '*.em') --fail-on-conflicts
268
+ ${glossaryJob(em)}
174
269
 
175
270
  status-badge:
176
- name: rebuild status badge (advisory — publish only, never a gate)
271
+ name: rebuild status badge (advisory - publish only, never a gate)
177
272
  if: github.event_name == 'push'
178
273
  runs-on: ubuntu-latest
179
274
  permissions:
@@ -198,14 +293,14 @@ export function ciManagedBody(model, testsDir, emVersion) {
198
293
  git push`;
199
294
  }
200
295
  function ciWorkflowHeader(model, emVersion) {
201
- return `# Generated once by \`em ci init ${model}\` (em ${emVersion}) — docs/ci.md
296
+ return `# Generated once by \`em ci init ${model}\` (em ${emVersion}) - docs/ci.md
202
297
  #
203
298
  # This file is yours from here: edit it, add jobs, remove ones you don't want. The content
204
299
  # between the GENERATED:${CI_WORKFLOW_MARKER} markers below is what a future \`em ci init\`
205
- # (e.g. after upgrading em) refreshes in place — add your own jobs above or below the markers,
300
+ # (e.g. after upgrading em) refreshes in place - add your own jobs above or below the markers,
206
301
  # at the same indent under \`jobs:\`, and they survive a re-run untouched. Every gate here fails
207
302
  # the PR the same way any other required check does; the status-badge job only ever publishes.
208
- # \`em ci init ${model} --check\` reports drift in the managed block — advisory unless you wire
303
+ # \`em ci init ${model} --check\` reports drift in the managed block - advisory unless you wire
209
304
  # it into a gate yourself (docs/ci.md).
210
305
  name: em ci
211
306
 
@@ -231,12 +326,199 @@ export function buildCiWorkflowFile(model, testsDir, emVersion) {
231
326
  const { start, end } = markerPair(CI_WORKFLOW_MARKER, "hash");
232
327
  return `${ciWorkflowHeader(model, emVersion)}${start}\n${ciManagedBody(model, testsDir, emVersion)}\n${end}\n`;
233
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
+ }
234
512
  // ---- em-conform.yml (scheduled, advisory-only conformance cadence) ----
235
513
  /** The managed block only (no header/`on:`/`permissions:`/`jobs:` scaffolding) — what gets
236
514
  * written between the marker pair, both for a fresh file and to patch an existing marked one
237
515
  * in place. */
238
- export function conformManagedBody(model, emVersion) {
516
+ export function conformManagedBody(model, emVersion, usePlugin = false) {
239
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";
240
522
  return ` conform:
241
523
  runs-on: ubuntu-latest
242
524
  steps:
@@ -246,23 +528,23 @@ export function conformManagedBody(model, emVersion) {
246
528
  node-version: 20
247
529
 
248
530
  - name: Note reports already present
249
- run: ls "$MODEL_DIR"/conformance/*-report.md 2>/dev/null | sort > /tmp/reports-before
531
+ run: find "$MODEL_DIR/conformance" -maxdepth 1 -name '*-report.md' 2>/dev/null | sort > /tmp/reports-before
250
532
 
251
533
  - name: Run conform phase
252
534
  run: |
253
535
  npm i -g @milehimikey/em@${emVersion} @anthropic-ai/claude-code
254
- em skill install --force
255
- claude -p "/event-modeling conform" \\
536
+ ${install}
537
+ claude -p "${invoke}" \\
256
538
  --allowedTools "Bash(em:*),Bash(git:*),Read,Grep,Glob,Write,Edit"
257
539
  env:
258
540
  ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
259
541
 
260
542
  - name: Post report
261
543
  run: |
262
- ls "$MODEL_DIR"/conformance/*-report.md 2>/dev/null | sort > /tmp/reports-after
544
+ find "$MODEL_DIR/conformance" -maxdepth 1 -name '*-report.md' 2>/dev/null | sort > /tmp/reports-after
263
545
  report=$(comm -13 /tmp/reports-before /tmp/reports-after | tail -1)
264
546
  if [ -z "$report" ]; then
265
- echo "the run produced no new report — nothing to post"
547
+ echo "the run produced no new report - nothing to post"
266
548
  exit 0
267
549
  fi
268
550
  gh issue create --title "Model conformance report $(date +%F)" \\
@@ -273,9 +555,9 @@ export function conformManagedBody(model, emVersion) {
273
555
  MODEL_DIR: ${modelDir}`;
274
556
  }
275
557
  function conformWorkflowHeader(model, emVersion) {
276
- return `# Generated once by \`em ci init ${model}\` (em ${emVersion}) — docs/ci.md#conformance-cadence-advisory
558
+ return `# Generated once by \`em ci init ${model}\` (em ${emVersion}) - docs/ci.md#conformance-cadence-advisory
277
559
  #
278
- # This file is yours from here: edit it freely. It is advisory-only by construction — the job
560
+ # This file is yours from here: edit it freely. It is advisory-only by construction - the job
279
561
  # never fails the build on drift (findings become a GitHub issue for a human to ratify), and
280
562
  # the state file's \`Last conformance:\` marker only advances when a human ratifies the run's
281
563
  # outcome locally and commits that update. Resist wiring this to every push; cadence, not
@@ -294,9 +576,118 @@ permissions:
294
576
  jobs:
295
577
  `;
296
578
  }
297
- export function buildConformWorkflowFile(model, emVersion) {
579
+ export function buildConformWorkflowFile(model, emVersion, usePlugin = false) {
298
580
  const { start, end } = markerPair(CONFORM_WORKFLOW_MARKER, "hash");
299
- 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)];
600
+ }
601
+ /**
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.
612
+ */
613
+ export function managedBlockModels(markerName, blockBody) {
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
+ }
632
+ const m = /^\s*MODEL_DIR: (.+?)\s*$/m.exec(blockBody);
633
+ return m && !m[1].startsWith("${{") ? [m[1]] : [];
634
+ }
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;
647
+ }
648
+ function existingBlockBody(content, markerName) {
649
+ const m = markerRegex(markerName, "hash").exec(content);
650
+ return m ? m[2] : null;
651
+ }
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
681
+ * from the freshly generated header. Only those exact generated phrasings are touched; a
682
+ * header the repo reworded, and everything below the start marker, is left alone. */
683
+ function retargetHeader(content, generated, markerName) {
684
+ const startLine = markerPair(markerName, "hash").start;
685
+ const at = content.indexOf(startLine);
686
+ const nextModel = /^#.*`em ci init ([^ `]+)/m.exec(generated)?.[1];
687
+ if (at < 0 || !nextModel)
688
+ return content;
689
+ const head = content.slice(0, at).replace(/^(#.*`em ci init )([^ `]+)/gm, (_w, pre) => `${pre}${nextModel}`);
690
+ return head + content.slice(at);
300
691
  }
301
692
  /**
302
693
  * Decide what to do with one generated file: create it if missing, patch the managed block if
@@ -307,8 +698,19 @@ export function buildConformWorkflowFile(model, emVersion) {
307
698
  * marker-wrapped body) — written verbatim for `create`/`would-replace`. `managedBody` is just
308
699
  * the text between the markers, reused to patch an existing marked file in place without
309
700
  * touching whatever a repo added around it.
701
+ *
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.
310
712
  */
311
- export function planCiFile(path, generated, managedBody, markerName, force) {
713
+ export function planCiFile(path, generated, managedBody, markerName, force, multi = false) {
312
714
  if (!existsSync(path))
313
715
  return { kind: "create", content: generated };
314
716
  const original = readFileSync(path, "utf8");
@@ -316,11 +718,66 @@ export function planCiFile(path, generated, managedBody, markerName, force) {
316
718
  if (updated === null) {
317
719
  return force ? { kind: "would-replace", content: generated } : { kind: "missing-markers" };
318
720
  }
319
- return updated === original ? { kind: "ok", content: original } : { kind: "stale", content: updated, current: original };
721
+ if (updated === original)
722
+ return { kind: "ok", content: original };
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
+ }
733
+ }
734
+ return { kind: "stale", content: updated, current: original };
320
735
  }
321
736
  export function applyCiFile(path, status) {
322
- if (status.kind === "create" || status.kind === "stale" || status.kind === "would-replace") {
737
+ if (status.kind === "create" || status.kind === "stale" || status.kind === "would-replace" || status.kind === "replace-models") {
323
738
  writeFileSync(path, status.content, "utf8");
324
739
  }
325
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
+ }
326
783
  //# sourceMappingURL=ciInit.js.map