@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.
- package/.claude/agents/em-critic.md +26 -0
- package/.claude/agents/em-implementer.md +34 -0
- package/.claude/agents/em-reviewer.md +31 -0
- package/.claude/agents/em-validator.md +27 -0
- package/.claude/skills/event-modeling/SKILL.md +52 -12
- package/.claude/skills/event-modeling-conform/SKILL.md +1 -1
- package/.claude/skills/event-modeling-conform/reference/conform.md +1 -1
- package/.claude/skills/event-modeling-design/SKILL.md +140 -18
- package/.claude/skills/event-modeling-discover/SKILL.md +1 -1
- package/.claude/skills/event-modeling-engagement/SKILL.md +192 -0
- package/.claude/skills/event-modeling-engagement/reference/engagement.md +143 -0
- package/.claude/skills/event-modeling-implement/SKILL.md +7 -4
- package/.claude/skills/event-modeling-implement/reference/implement.md +51 -12
- package/.claude/skills/event-modeling-review/SKILL.md +1 -1
- package/.claude/skills/event-modeling-shared/reference/em-dsl.md +274 -188
- package/.claude/skills/event-modeling-shared/reference/operating-principles.md +61 -0
- package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +116 -4
- package/.claude/skills/event-modeling-shared/templates/constitution.md +18 -1
- package/.claude/skills/event-modeling-shared/templates/slice.md +64 -39
- package/.claude/skills/event-modeling-shared/templates/state.md +5 -1
- package/.claude-plugin/marketplace.json +14 -0
- package/README.md +25 -11
- package/dist/catalog/apiFirst.d.ts +28 -0
- package/dist/catalog/apiFirst.js +49 -0
- package/dist/catalog/apiFirst.js.map +1 -0
- package/dist/catalog/docJoin.d.ts +13 -0
- package/dist/catalog/docJoin.js +11 -0
- package/dist/catalog/docJoin.js.map +1 -1
- package/dist/catalog/docModelConsistencyValidate.js +44 -4
- package/dist/catalog/docModelConsistencyValidate.js.map +1 -1
- package/dist/catalog/invariantsValidate.d.ts +4 -0
- package/dist/catalog/invariantsValidate.js +102 -0
- package/dist/catalog/invariantsValidate.js.map +1 -0
- package/dist/catalog/scenarios.d.ts +15 -0
- package/dist/catalog/scenarios.js +97 -0
- package/dist/catalog/scenarios.js.map +1 -0
- package/dist/catalog/sliceDoc.d.ts +8 -0
- package/dist/catalog/sliceDoc.js +9 -0
- package/dist/catalog/sliceDoc.js.map +1 -1
- package/dist/catalog/sliceReadyValidate.d.ts +31 -4
- package/dist/catalog/sliceReadyValidate.js +66 -8
- package/dist/catalog/sliceReadyValidate.js.map +1 -1
- package/dist/catalog/sliceSections.d.ts +55 -0
- package/dist/catalog/sliceSections.js +317 -0
- package/dist/catalog/sliceSections.js.map +1 -0
- package/dist/cli/agentsMd.js +7 -1
- package/dist/cli/agentsMd.js.map +1 -1
- package/dist/cli/api.d.ts +90 -0
- package/dist/cli/api.js +318 -0
- package/dist/cli/api.js.map +1 -0
- package/dist/cli/ciInit.d.ts +88 -3
- package/dist/cli/ciInit.js +507 -50
- package/dist/cli/ciInit.js.map +1 -1
- package/dist/cli/conformScope.d.ts +28 -0
- package/dist/cli/conformScope.js +67 -0
- package/dist/cli/conformScope.js.map +1 -1
- package/dist/cli/coverage.d.ts +25 -2
- package/dist/cli/coverage.js +86 -12
- package/dist/cli/coverage.js.map +1 -1
- package/dist/cli/engagement.d.ts +210 -0
- package/dist/cli/engagement.js +444 -0
- package/dist/cli/engagement.js.map +1 -0
- package/dist/cli/engagementFile.d.ts +86 -0
- package/dist/cli/engagementFile.js +255 -0
- package/dist/cli/engagementFile.js.map +1 -0
- package/dist/cli/metrics.d.ts +10 -0
- package/dist/cli/metrics.js +49 -1
- package/dist/cli/metrics.js.map +1 -1
- package/dist/cli/pluginPin.d.ts +55 -0
- package/dist/cli/pluginPin.js +188 -0
- package/dist/cli/pluginPin.js.map +1 -0
- package/dist/cli/ratify.d.ts +27 -2
- package/dist/cli/ratify.js +112 -13
- package/dist/cli/ratify.js.map +1 -1
- package/dist/cli/reratify.d.ts +11 -4
- package/dist/cli/reratify.js +89 -35
- package/dist/cli/reratify.js.map +1 -1
- package/dist/cli/scopeInputs.d.ts +48 -0
- package/dist/cli/scopeInputs.js +176 -0
- package/dist/cli/scopeInputs.js.map +1 -0
- package/dist/cli/skillCheck.d.ts +15 -1
- package/dist/cli/skillCheck.js +22 -1
- package/dist/cli/skillCheck.js.map +1 -1
- package/dist/cli/skillDirs.d.ts +5 -0
- package/dist/cli/skillDirs.js +13 -1
- package/dist/cli/skillDirs.js.map +1 -1
- package/dist/cli/skillSync.d.ts +4 -0
- package/dist/cli/skillSync.js +23 -0
- package/dist/cli/skillSync.js.map +1 -1
- package/dist/cli/sliceNew.d.ts +11 -7
- package/dist/cli/sliceNew.js +91 -18
- package/dist/cli/sliceNew.js.map +1 -1
- package/dist/cli/sliceSync.d.ts +50 -0
- package/dist/cli/sliceSync.js +170 -0
- package/dist/cli/sliceSync.js.map +1 -0
- package/dist/cli/status.d.ts +24 -1
- package/dist/cli/status.js +20 -1
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/systemInputs.d.ts +53 -10
- package/dist/cli/systemInputs.js +186 -11
- package/dist/cli/systemInputs.js.map +1 -1
- package/dist/cli/upgrade.d.ts +30 -7
- package/dist/cli/upgrade.js +434 -50
- package/dist/cli/upgrade.js.map +1 -1
- package/dist/cli/usageLog.d.ts +4 -2
- package/dist/cli/usageLog.js +4 -2
- package/dist/cli/usageLog.js.map +1 -1
- package/dist/cli.js +631 -100
- package/dist/cli.js.map +1 -1
- package/dist/emit/api.d.ts +29 -0
- package/dist/emit/api.js +220 -0
- package/dist/emit/api.js.map +1 -0
- package/dist/emit/apiCheckJson.d.ts +3 -0
- package/dist/emit/apiCheckJson.js +21 -0
- package/dist/emit/apiCheckJson.js.map +1 -0
- package/dist/emit/codeownersJson.d.ts +3 -0
- package/dist/emit/codeownersJson.js +19 -0
- package/dist/emit/codeownersJson.js.map +1 -0
- package/dist/emit/coverageJson.d.ts +5 -2
- package/dist/emit/coverageJson.js +6 -2
- package/dist/emit/coverageJson.js.map +1 -1
- package/dist/emit/diffJson.d.ts +1 -1
- package/dist/emit/diffJson.js +8 -1
- package/dist/emit/diffJson.js.map +1 -1
- package/dist/emit/engagementJson.d.ts +8 -0
- package/dist/emit/engagementJson.js +46 -0
- package/dist/emit/engagementJson.js.map +1 -0
- package/dist/emit/glossaryJson.d.ts +1 -1
- package/dist/emit/glossaryJson.js +4 -1
- package/dist/emit/glossaryJson.js.map +1 -1
- package/dist/emit/json.d.ts +17 -1
- package/dist/emit/json.js +30 -1
- package/dist/emit/json.js.map +1 -1
- package/dist/emit/metricsJson.d.ts +1 -1
- package/dist/emit/metricsJson.js +4 -1
- package/dist/emit/metricsJson.js.map +1 -1
- package/dist/emit/queryJson.d.ts +1 -1
- package/dist/emit/queryJson.js +7 -1
- package/dist/emit/queryJson.js.map +1 -1
- package/dist/emit/scopeJson.d.ts +12 -0
- package/dist/emit/scopeJson.js +26 -0
- package/dist/emit/scopeJson.js.map +1 -0
- package/dist/emit/skillCheckJson.d.ts +3 -2
- package/dist/emit/skillCheckJson.js +10 -2
- package/dist/emit/skillCheckJson.js.map +1 -1
- package/dist/emit/statusJson.d.ts +1 -1
- package/dist/emit/statusJson.js +15 -1
- package/dist/emit/statusJson.js.map +1 -1
- package/dist/emit/systemJson.d.ts +15 -8
- package/dist/emit/systemJson.js +28 -13
- package/dist/emit/systemJson.js.map +1 -1
- package/dist/emit/upgradeJson.d.ts +1 -1
- package/dist/emit/upgradeJson.js +4 -1
- package/dist/emit/upgradeJson.js.map +1 -1
- package/dist/emit/validateJson.d.ts +3 -3
- package/dist/emit/validateJson.js +10 -4
- package/dist/emit/validateJson.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +186 -37
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/diff.d.ts +4 -1
- package/dist/model/diff.js +44 -8
- package/dist/model/diff.js.map +1 -1
- package/dist/model/glossary.d.ts +2 -0
- package/dist/model/glossary.js +1 -0
- package/dist/model/glossary.js.map +1 -1
- package/dist/model/model.d.ts +55 -2
- package/dist/model/model.js +57 -0
- package/dist/model/model.js.map +1 -1
- package/dist/model/qualifiedRef.d.ts +18 -0
- package/dist/model/qualifiedRef.js +26 -0
- package/dist/model/qualifiedRef.js.map +1 -1
- package/dist/model/queryIndex.d.ts +9 -1
- package/dist/model/queryIndex.js +18 -2
- package/dist/model/queryIndex.js.map +1 -1
- package/dist/model/rules.d.ts +143 -2
- package/dist/model/rules.js +162 -3
- package/dist/model/rules.js.map +1 -1
- package/dist/model/validate.d.ts +40 -0
- package/dist/model/validate.js +147 -1
- package/dist/model/validate.js.map +1 -1
- package/dist/parser/ast.d.ts +31 -3
- package/dist/parser/parser.js +148 -14
- package/dist/parser/parser.js.map +1 -1
- package/dist/query/format.js +1 -1
- package/dist/query/format.js.map +1 -1
- package/dist/query/verbs.d.ts +6 -0
- package/dist/query/verbs.js +6 -2
- package/dist/query/verbs.js.map +1 -1
- package/dist/system/adaptation.d.ts +14 -0
- package/dist/system/adaptation.js +49 -0
- package/dist/system/adaptation.js.map +1 -0
- package/dist/system/codeowners.d.ts +77 -0
- package/dist/system/codeowners.js +242 -0
- package/dist/system/codeowners.js.map +1 -0
- package/dist/system/manifest.d.ts +10 -5
- package/dist/system/manifest.js +54 -26
- package/dist/system/manifest.js.map +1 -1
- package/dist/system/migrateManifest.d.ts +28 -0
- package/dist/system/migrateManifest.js +155 -0
- package/dist/system/migrateManifest.js.map +1 -0
- package/dist/system/scope.d.ts +46 -0
- package/dist/system/scope.js +132 -0
- package/dist/system/scope.js.map +1 -0
- package/dist/system/verify.d.ts +50 -13
- package/dist/system/verify.js +179 -58
- package/dist/system/verify.js.map +1 -1
- package/dist/templates.d.ts +2 -2
- package/dist/templates.js +23 -2
- package/dist/templates.js.map +1 -1
- package/package.json +6 -3
- package/plugin/.claude-plugin/plugin.json +18 -0
- package/plugin/agents/em-critic.md +27 -0
- package/plugin/agents/em-implementer.md +35 -0
- package/plugin/agents/em-reviewer.md +32 -0
- package/plugin/agents/em-validator.md +28 -0
- package/plugin/shared/reference/em-dsl.md +866 -0
- package/plugin/shared/reference/methodology.md +329 -0
- package/plugin/shared/reference/operating-principles.md +222 -0
- package/plugin/shared/reference/slice-doc-schema.md +566 -0
- package/plugin/shared/templates/conformance-report.md +85 -0
- package/plugin/shared/templates/constitution.md +136 -0
- package/plugin/shared/templates/model-readme.md +46 -0
- package/plugin/shared/templates/slice.md +251 -0
- package/plugin/shared/templates/state.md +77 -0
- package/plugin/skills/conform/SKILL.md +93 -0
- package/plugin/skills/conform/reference/conform.md +428 -0
- package/plugin/skills/design/SKILL.md +297 -0
- package/plugin/skills/discover/SKILL.md +90 -0
- package/plugin/skills/discover/reference/extract.md +147 -0
- package/plugin/skills/engagement/SKILL.md +193 -0
- package/plugin/skills/engagement/reference/engagement.md +144 -0
- package/plugin/skills/event-modeling/SKILL.md +132 -0
- package/plugin/skills/implement/SKILL.md +61 -0
- package/plugin/skills/implement/reference/implement.md +439 -0
- package/plugin/skills/review/SKILL.md +74 -0
- package/dist/emit/typespec.d.ts +0 -26
- package/dist/emit/typespec.js +0 -356
- package/dist/emit/typespec.js.map +0 -1
package/dist/cli/ciInit.js
CHANGED
|
@@ -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 {
|
|
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)
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
|
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})
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
255
|
-
claude -p "
|
|
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
|
-
|
|
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
|
|
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})
|
|
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
|
|
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
|
-
|
|
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
|