@milehimikey/em 1.20.0 → 1.21.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/skills/event-modeling/SKILL.md +1 -1
- package/.claude/skills/event-modeling-conform/SKILL.md +1 -1
- package/.claude/skills/event-modeling-design/SKILL.md +23 -8
- package/.claude/skills/event-modeling-discover/SKILL.md +2 -2
- package/.claude/skills/event-modeling-discover/reference/extract.md +5 -1
- package/.claude/skills/event-modeling-engagement/SKILL.md +1 -1
- package/.claude/skills/event-modeling-implement/SKILL.md +1 -1
- package/.claude/skills/event-modeling-review/SKILL.md +19 -7
- package/.claude/skills/event-modeling-shared/reference/em-dsl.md +35 -3
- package/.claude/skills/event-modeling-shared/reference/methodology.md +35 -2
- package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +2 -1
- package/.claude/skills/event-modeling-shared/templates/model-readme.md +10 -2
- package/.claude/skills/event-modeling-shared/templates/slice.md +7 -1
- package/.claude-plugin/marketplace.json +2 -2
- package/dist/catalog/build.js +6 -5
- package/dist/catalog/build.js.map +1 -1
- package/dist/catalog/coverage.d.ts +3 -0
- package/dist/catalog/coverage.js +12 -0
- package/dist/catalog/coverage.js.map +1 -1
- package/dist/catalog/docLocate.d.ts +17 -0
- package/dist/catalog/docLocate.js +97 -0
- package/dist/catalog/docLocate.js.map +1 -0
- package/dist/catalog/frontmatterCoherenceValidate.js +16 -1
- package/dist/catalog/frontmatterCoherenceValidate.js.map +1 -1
- package/dist/catalog/orphanedSliceDocValidate.js +15 -10
- package/dist/catalog/orphanedSliceDocValidate.js.map +1 -1
- package/dist/catalog/readSliceDoc.js +5 -4
- package/dist/catalog/readSliceDoc.js.map +1 -1
- package/dist/catalog/scenarioQualityValidate.js +2 -2
- package/dist/catalog/scenarioQualityValidate.js.map +1 -1
- 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/sliceSections.d.ts +18 -1
- package/dist/catalog/sliceSections.js +38 -4
- package/dist/catalog/sliceSections.js.map +1 -1
- package/dist/cli/defer.js +3 -2
- package/dist/cli/defer.js.map +1 -1
- package/dist/cli/ledgerCheck.js +11 -8
- package/dist/cli/ledgerCheck.js.map +1 -1
- package/dist/cli/markImplemented.js +2 -2
- package/dist/cli/markImplemented.js.map +1 -1
- package/dist/cli/ratify.js +2 -2
- package/dist/cli/ratify.js.map +1 -1
- package/dist/cli/refsCheck.js +3 -2
- package/dist/cli/refsCheck.js.map +1 -1
- package/dist/cli/render-inputs.d.ts +1 -1
- package/dist/cli/render-inputs.js +3 -2
- package/dist/cli/render-inputs.js.map +1 -1
- package/dist/cli/review.js +2 -2
- package/dist/cli/review.js.map +1 -1
- package/dist/cli/revise.js +2 -2
- package/dist/cli/revise.js.map +1 -1
- package/dist/cli/skillDirs.d.ts +22 -0
- package/dist/cli/skillDirs.js +24 -0
- package/dist/cli/skillDirs.js.map +1 -1
- package/dist/cli/sliceConform.js +2 -1
- package/dist/cli/sliceConform.js.map +1 -1
- package/dist/cli/sliceIndex.d.ts +19 -3
- package/dist/cli/sliceIndex.js +81 -17
- package/dist/cli/sliceIndex.js.map +1 -1
- package/dist/cli/sliceNew.js +2 -0
- package/dist/cli/sliceNew.js.map +1 -1
- package/dist/cli/sliceStubAll.js +7 -2
- package/dist/cli/sliceStubAll.js.map +1 -1
- package/dist/cli/sliceSync.d.ts +12 -0
- package/dist/cli/sliceSync.js +42 -4
- package/dist/cli/sliceSync.js.map +1 -1
- package/dist/cli/status.d.ts +3 -4
- package/dist/cli/status.js +5 -4
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/storylineSplit.d.ts +18 -0
- package/dist/cli/storylineSplit.js +110 -0
- package/dist/cli/storylineSplit.js.map +1 -0
- package/dist/cli/upgrade.js +39 -14
- package/dist/cli/upgrade.js.map +1 -1
- package/dist/cli/watchStoryline.d.ts +6 -0
- package/dist/cli/watchStoryline.js +10 -0
- package/dist/cli/watchStoryline.js.map +1 -1
- package/dist/cli.js +265 -67
- package/dist/cli.js.map +1 -1
- package/dist/emit/json.d.ts +22 -1
- package/dist/emit/json.js +12 -1
- package/dist/emit/json.js.map +1 -1
- package/dist/mcp/server.js +20 -6
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/edges.d.ts +1 -1
- package/dist/model/edges.js +8 -1
- package/dist/model/edges.js.map +1 -1
- package/dist/model/loadModel.d.ts +38 -0
- package/dist/model/loadModel.js +122 -0
- package/dist/model/loadModel.js.map +1 -0
- package/dist/model/model.d.ts +9 -4
- package/dist/model/model.js +4 -0
- package/dist/model/model.js.map +1 -1
- package/dist/model/rules.d.ts +23 -0
- package/dist/model/rules.js +23 -0
- package/dist/model/rules.js.map +1 -1
- package/dist/model/shapeLint.js +95 -0
- package/dist/model/shapeLint.js.map +1 -1
- package/dist/model/sliceDocIndex.js +13 -5
- package/dist/model/sliceDocIndex.js.map +1 -1
- package/dist/model/sliceDocPath.d.ts +19 -0
- package/dist/model/sliceDocPath.js +49 -0
- package/dist/model/sliceDocPath.js.map +1 -0
- package/dist/model/storylineMap.d.ts +20 -0
- package/dist/model/storylineMap.js +55 -0
- package/dist/model/storylineMap.js.map +1 -0
- package/dist/model/validate.d.ts +7 -0
- package/dist/model/validate.js +8 -0
- package/dist/model/validate.js.map +1 -1
- package/dist/parser/ast.d.ts +19 -0
- package/dist/parser/parser.d.ts +17 -1
- package/dist/parser/parser.js +91 -2
- package/dist/parser/parser.js.map +1 -1
- package/dist/pipeline.d.ts +16 -0
- package/dist/pipeline.js +19 -2
- package/dist/pipeline.js.map +1 -1
- package/dist/render/drawEdges.d.ts +5 -1
- package/dist/render/drawEdges.js +57 -4
- package/dist/render/drawEdges.js.map +1 -1
- package/dist/render/drawNotes.d.ts +4 -1
- package/dist/render/drawNotes.js +40 -10
- package/dist/render/drawNotes.js.map +1 -1
- package/dist/render/render.d.ts +11 -0
- package/dist/render/render.js +94 -11
- package/dist/render/render.js.map +1 -1
- package/dist/render/serve.d.ts +5 -0
- package/dist/render/serve.js +18 -2
- package/dist/render/serve.js.map +1 -1
- package/dist/render/sliceDiagram.js +3 -0
- package/dist/render/sliceDiagram.js.map +1 -1
- package/dist/render/sliceOverlay.d.ts +4 -5
- package/dist/render/sliceOverlay.js +4 -7
- package/dist/render/sliceOverlay.js.map +1 -1
- package/dist/render/sliceStatus.d.ts +8 -0
- package/dist/render/sliceStatus.js +22 -15
- package/dist/render/sliceStatus.js.map +1 -1
- package/dist/render/storylineBands.d.ts +0 -3
- package/dist/render/storylineBands.js +2 -22
- package/dist/render/storylineBands.js.map +1 -1
- package/dist/render/storylineDiagram.js +17 -69
- package/dist/render/storylineDiagram.js.map +1 -1
- package/dist/render/storylineMap.d.ts +18 -0
- package/dist/render/storylineMap.js +263 -0
- package/dist/render/storylineMap.js.map +1 -0
- package/dist/render/svgGeometry.d.ts +9 -0
- package/dist/render/svgGeometry.js +16 -0
- package/dist/render/svgGeometry.js.map +1 -1
- package/dist/render/viewerHtml.js +1 -1
- package/dist/render/viewerHtml.js.map +1 -1
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +10 -2
- package/dist/templates.js.map +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/shared/reference/em-dsl.md +35 -3
- package/plugin/shared/reference/methodology.md +35 -2
- package/plugin/shared/reference/slice-doc-schema.md +2 -1
- package/plugin/shared/templates/model-readme.md +10 -2
- package/plugin/shared/templates/slice.md +7 -1
- package/plugin/skills/conform/SKILL.md +1 -1
- package/plugin/skills/design/SKILL.md +23 -8
- package/plugin/skills/discover/SKILL.md +2 -2
- package/plugin/skills/discover/reference/extract.md +5 -1
- package/plugin/skills/engagement/SKILL.md +1 -1
- package/plugin/skills/event-modeling/SKILL.md +1 -1
- package/plugin/skills/implement/SKILL.md +1 -1
- package/plugin/skills/review/SKILL.md +19 -7
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from "node:fs";
|
|
4
|
-
import { cp, mkdir } from "node:fs/promises";
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, renameSync } from "node:fs";
|
|
4
|
+
import { cp, mkdir, writeFile } from "node:fs/promises";
|
|
5
5
|
import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { Command } from "commander";
|
|
8
|
-
import {
|
|
8
|
+
import { compileModelFile, compileModelText } from "./pipeline.js";
|
|
9
|
+
import { ModelLoadError, attributeDiagnosticFiles, diskReader } from "./model/loadModel.js";
|
|
9
10
|
import { isMainModule } from "./util/isMainModule.js";
|
|
10
11
|
import { localIsoDate } from "./util/localDate.js";
|
|
11
12
|
import { continuationOf } from "./model/continuation.js";
|
|
@@ -14,12 +15,14 @@ import { renderDot, layoutDot, composeSvg, writeRendered, formatFromPath } from
|
|
|
14
15
|
import { buildSliceDiagram } from "./render/sliceDiagram.js";
|
|
15
16
|
import { resolveSliceArg, defaultSliceOut, defaultStorylineOut } from "./cli/render-inputs.js";
|
|
16
17
|
import { buildStorylineDiagram, resolveStorylineArg } from "./render/storylineDiagram.js";
|
|
18
|
+
import { buildStorylineMapHtml } from "./render/storylineMap.js";
|
|
19
|
+
import { storylineMap } from "./model/storylineMap.js";
|
|
17
20
|
import { embedStorylinePicker } from "./render/storylinePicker.js";
|
|
18
|
-
import { resolveWatchStoryline, storylineFileFor, storylineVariants } from "./cli/watchStoryline.js";
|
|
21
|
+
import { liveViewUrl, resolveWatchStoryline, storylineFileFor, storylineVariants } from "./cli/watchStoryline.js";
|
|
19
22
|
import { resolveSliceKey } from "./model/resolveSlice.js";
|
|
20
23
|
import { serializeBuilds, watchFile } from "./render/watch.js";
|
|
21
24
|
import { startLiveServer } from "./render/serve.js";
|
|
22
|
-
import { formatDiagnostic, hasErrors, publicViewsWithoutInModelReader } from "./model/validate.js";
|
|
25
|
+
import { formatDiagnostic, hasErrors, publicViewsWithoutInModelReader, formatDiagnosticWithFile } from "./model/validate.js";
|
|
23
26
|
import { buildExport, buildSliceExport, GENERATOR_VERSION } from "./emit/json.js";
|
|
24
27
|
import { buildDocJsonExport } from "./emit/docJson.js";
|
|
25
28
|
import { generateContract, runApiCheck, formatApiCheckText } from "./cli/api.js";
|
|
@@ -68,7 +71,7 @@ import { checkSkillSyncBundle, checkAgentFiles } from "./cli/skillCheck.js";
|
|
|
68
71
|
import { buildSkillCheckJson } from "./emit/skillCheckJson.js";
|
|
69
72
|
import { checkPlugin, claudePluginsDir, detectPlugin, pluginInstallCommands } from "./cli/pluginPin.js";
|
|
70
73
|
import { readContract } from "./cli/contract.js";
|
|
71
|
-
import { EM_SKILL_DIR_NAMES, EM_SHARED_DIR_NAMES, EM_ALL_SKILL_BUNDLE_DIRS, EM_SKILL_ANCHOR_DIR, EM_AGENT_FILES } from "./cli/skillDirs.js";
|
|
74
|
+
import { EM_SKILL_DIR_NAMES, EM_SHARED_DIR_NAMES, EM_ALL_SKILL_BUNDLE_DIRS, EM_SKILL_ANCHOR_DIR, EM_AGENT_FILES, resolveVendoredDirs } from "./cli/skillDirs.js";
|
|
72
75
|
import { createServer as createMcpServer } from "./mcp/server.js";
|
|
73
76
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
74
77
|
import { syncAgentsMd } from "./cli/agentsMd.js";
|
|
@@ -102,6 +105,7 @@ import { kebabSlug } from "./util/slug.js";
|
|
|
102
105
|
import { ciWorkflowPath, conformWorkflowPath, buildCiWorkflowFile, buildConformWorkflowFile, buildCiWorkflowFileMulti, buildConformWorkflowFileMulti, ciManagedBody, ciManagedBodyMulti, conformManagedBody, conformManagedBodyMulti, ciModelsFromManifest, describeOtherModels, describeSetChange, fileBlockModels, planCiFile, refreshCiPullRequestPaths, applyCiPullRequestPathsRefresh, applyCiFile, findUnsafeCiInitArg, CI_WORKFLOW_MARKER, CONFORM_WORKFLOW_MARKER, } from "./cli/ciInit.js";
|
|
103
106
|
import { resolveRepoRoot, detectUpgrade, applyUpgrade, checkUpgrade } from "./cli/upgrade.js";
|
|
104
107
|
import { buildStorylineSuggestion } from "./cli/storylineSuggest.js";
|
|
108
|
+
import { planStorylineSplit } from "./cli/storylineSplit.js";
|
|
105
109
|
import { buildUpgradeJson } from "./emit/upgradeJson.js";
|
|
106
110
|
import { isOlder } from "./util/semver.js";
|
|
107
111
|
import { RULES } from "./model/rules.js";
|
|
@@ -117,6 +121,8 @@ import { runCodeowners } from "./system/codeowners.js";
|
|
|
117
121
|
import { buildCodeownersJson } from "./emit/codeownersJson.js";
|
|
118
122
|
import { verifySystem } from "./system/verify.js";
|
|
119
123
|
import { buildSystemJson } from "./emit/systemJson.js";
|
|
124
|
+
import { docDirOf } from "./model/sliceDocPath.js";
|
|
125
|
+
import { docDirs } from "./catalog/docLocate.js";
|
|
120
126
|
/** GH #253: the trailing note `em slice defer` prints after `deferred: …` on a ratified/shipped
|
|
121
127
|
* doc — the item went into the signed body, outside `ratifiedHash`, and when the stamp was still
|
|
122
128
|
* in an older scope (pre-1.17 whole-body, or 1.17–1.18) it was re-stamped in the same edit (body unchanged). */
|
|
@@ -201,12 +207,17 @@ program
|
|
|
201
207
|
.option("-T, --format <fmt>", "output format (svg, png, pdf, ...)")
|
|
202
208
|
.option("--slice <name>", "render only this slice, redrawn in its own canonical pattern shape " +
|
|
203
209
|
"(default out: slices/<kebab-slug>.svg)")
|
|
204
|
-
.option("--storyline <name>", "render only this storyline: its slices in order,
|
|
205
|
-
"they read from outside it (and the end state of a storyline it comes `after`) " +
|
|
210
|
+
.option("--storyline <name>", "render only this storyline: its own slices in order, nothing from outside it " +
|
|
206
211
|
"(default out: storylines/<kebab-slug>.svg)")
|
|
212
|
+
.option("--map", "render every storyline to storylines/<kebab-slug>.svg plus a draggable canvas of them all, " +
|
|
213
|
+
"with the flow between storylines, at storylines/map.html (-o picks the html path)")
|
|
207
214
|
.option("--emit-dot", "print the generated DOT instead of rendering")
|
|
208
215
|
.option("--keep-empty-lanes", "keep the API lane even when empty")
|
|
209
216
|
.action(async (file, opts) => {
|
|
217
|
+
if (opts.map && (opts.slice !== undefined || opts.storyline !== undefined || opts.emitDot)) {
|
|
218
|
+
console.error("em render: --map cannot be combined with --slice, --storyline or --emit-dot");
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
210
221
|
if (opts.slice !== undefined && opts.emitDot) {
|
|
211
222
|
console.error("em render: --slice cannot be combined with --emit-dot");
|
|
212
223
|
process.exit(1);
|
|
@@ -239,6 +250,27 @@ program
|
|
|
239
250
|
console.error("not rendering: fix the errors above");
|
|
240
251
|
process.exit(1);
|
|
241
252
|
}
|
|
253
|
+
if (opts.map) {
|
|
254
|
+
const storylines = model.storylines ?? [];
|
|
255
|
+
if (storylines.length === 0) {
|
|
256
|
+
console.error("em render --map: this model declares no storylines");
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
const out = opts.out ?? join(dirname(file), "storylines", "map.html");
|
|
260
|
+
const outDir = dirname(out);
|
|
261
|
+
await mkdir(outDir, { recursive: true });
|
|
262
|
+
const frames = [];
|
|
263
|
+
for (const st of storylines) {
|
|
264
|
+
const sub = buildStorylineDiagram(model, st, { keepEmptyLanes: opts.keepEmptyLanes });
|
|
265
|
+
const svg = composeSvg(await layoutDot(sub.dot), sub.model, sub.grid, dirname(file), outDir);
|
|
266
|
+
const slug = kebabSlug(st.name);
|
|
267
|
+
await writeRendered(svg, join(outDir, `${slug}.svg`), "svg");
|
|
268
|
+
frames.push({ name: st.name, slug, sliceCount: st.sliceIndexes.length, svg });
|
|
269
|
+
}
|
|
270
|
+
await writeFile(out, buildStorylineMapHtml(model, frames, storylineMap(model)), "utf8");
|
|
271
|
+
console.log(`rendered ${out} (+ ${frames.length} storyline svg${frames.length === 1 ? "" : "s"})`);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
242
274
|
if (opts.storyline !== undefined) {
|
|
243
275
|
const found = resolveStorylineArg(model, opts.storyline);
|
|
244
276
|
if ("error" in found) {
|
|
@@ -261,7 +293,7 @@ program
|
|
|
261
293
|
console.error(lookup.error);
|
|
262
294
|
process.exit(1);
|
|
263
295
|
}
|
|
264
|
-
const out = opts.out ?? defaultSliceOut(file, opts.slice);
|
|
296
|
+
const out = opts.out ?? defaultSliceOut(file, opts.slice, docDirOf(model.slices[lookup.index]));
|
|
265
297
|
const fmt = opts.format ?? formatFromPath(out);
|
|
266
298
|
await mkdir(dirname(out), { recursive: true }); // slices/ may not exist yet
|
|
267
299
|
const { model: sliceModel, grid: sliceGrid, dot: sliceDot } = buildSliceDiagram(model, lookup.index, {
|
|
@@ -441,7 +473,10 @@ program
|
|
|
441
473
|
oldDoc: (key) => readSliceDoc(dirname(plan.oldFile), key),
|
|
442
474
|
newDoc: (key) => readSliceDoc(dirname(plan.newFile), key),
|
|
443
475
|
};
|
|
444
|
-
runDiff(oldSource, plan.oldFile, newSource, plan.newFile, opts.exitCode, opts.json, lineage
|
|
476
|
+
runDiff(oldSource, plan.oldFile, newSource, plan.newFile, opts.exitCode, opts.json, lineage, {
|
|
477
|
+
old: diskReader(dirname(plan.oldFile)),
|
|
478
|
+
new: diskReader(dirname(plan.newFile)),
|
|
479
|
+
});
|
|
445
480
|
return;
|
|
446
481
|
}
|
|
447
482
|
const oldSource = readAtRevision(plan.file, plan.from);
|
|
@@ -452,7 +487,10 @@ program
|
|
|
452
487
|
oldDoc: (key) => resolveDocAtRevision(plan.file, key, plan.from),
|
|
453
488
|
newDoc: (key) => plan.to ? resolveDocAtRevision(plan.file, key, plan.to) : readSliceDoc(dirname(plan.file), key),
|
|
454
489
|
};
|
|
455
|
-
runDiff(oldSource, oldLabel, newSource, newLabel, opts.exitCode, opts.json, lineage
|
|
490
|
+
runDiff(oldSource, oldLabel, newSource, newLabel, opts.exitCode, opts.json, lineage, {
|
|
491
|
+
old: gitReader(plan.file, plan.from),
|
|
492
|
+
new: plan.to ? gitReader(plan.file, plan.to) : diskReader(dirname(plan.file)),
|
|
493
|
+
});
|
|
456
494
|
});
|
|
457
495
|
program
|
|
458
496
|
.command("glossary")
|
|
@@ -475,7 +513,7 @@ program
|
|
|
475
513
|
let anyErrors = false;
|
|
476
514
|
for (const file of files) {
|
|
477
515
|
const source = readFileOrExit(file);
|
|
478
|
-
const result = compileSource(source, file);
|
|
516
|
+
const result = compileSource(source, file, diskReader(dirname(file)));
|
|
479
517
|
printDiagnosticsFor(file, result.diagnostics);
|
|
480
518
|
if (hasErrors(result.diagnostics))
|
|
481
519
|
anyErrors = true;
|
|
@@ -596,8 +634,12 @@ slice
|
|
|
596
634
|
process.exit(1);
|
|
597
635
|
}
|
|
598
636
|
const key = sliceDocKey(name);
|
|
599
|
-
|
|
600
|
-
|
|
637
|
+
// The note clause written into the model is always `slices/<key>.md`, relative to the file
|
|
638
|
+
// that declares the slice (GH #421). Where the FILE goes: `slices/` under the cwd, or — with
|
|
639
|
+
// --wire naming a model whose slice sits in a storyline folder — that folder's `slices/`.
|
|
640
|
+
const notePath = `slices/${key}.md`;
|
|
641
|
+
let dir = "slices";
|
|
642
|
+
let path = join(dir, `${key}.md`);
|
|
601
643
|
if (existsSync(path) && !opts.force) {
|
|
602
644
|
console.error(`refusing to overwrite ${path} (use --force)`);
|
|
603
645
|
process.exit(1);
|
|
@@ -617,12 +659,25 @@ slice
|
|
|
617
659
|
console.error(`em slice new --wire: not wiring — fix the errors in ${opts.wire} above`);
|
|
618
660
|
process.exit(1);
|
|
619
661
|
}
|
|
620
|
-
|
|
662
|
+
// GH #421: a slice declared in a storyline model file is wired in THAT file (its line
|
|
663
|
+
// numbers count there); a root slice in the root as before.
|
|
664
|
+
const sliceFile = model.slices[refs.sliceKeys.indexOf(key)]?.file;
|
|
665
|
+
const wireTarget = sliceFile ? resolve(dirname(opts.wire), sliceFile) : opts.wire;
|
|
666
|
+
const wireSource = sliceFile ? readFileSync(wireTarget, "utf8") : source;
|
|
667
|
+
if (sliceFile) {
|
|
668
|
+
dir = join(dirname(opts.wire), docDirOf(model.slices[refs.sliceKeys.indexOf(key)]), "slices");
|
|
669
|
+
path = join(dir, `${key}.md`);
|
|
670
|
+
if (existsSync(path) && !opts.force) {
|
|
671
|
+
console.error(`refusing to overwrite ${path} (use --force)`);
|
|
672
|
+
process.exit(1);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
const result = wireSliceNote(wireSource, model, refs, key, opts.pattern, notePath);
|
|
621
676
|
if (!result.ok) {
|
|
622
677
|
console.error(`em slice new --wire: ${result.message}`);
|
|
623
678
|
process.exit(1);
|
|
624
679
|
}
|
|
625
|
-
wired = result;
|
|
680
|
+
wired = { ...result, target: wireTarget };
|
|
626
681
|
regions = buildSliceRegions(model, [refs.sliceKeys.indexOf(key)]);
|
|
627
682
|
const sliceEvents = model.slices[refs.sliceKeys.indexOf(key)].elements.filter((e) => e.kind === "event");
|
|
628
683
|
alternatives = sliceEvents.filter((e) => e.alternativeGroup !== undefined).map((e) => e.name);
|
|
@@ -634,8 +689,8 @@ slice
|
|
|
634
689
|
writeFileSync(path, content);
|
|
635
690
|
console.log(`wrote ${path}`);
|
|
636
691
|
if (wired) {
|
|
637
|
-
writeFileSync(
|
|
638
|
-
console.log(`wired ${path} onto ${wired.elementName} in slice "${wired.sliceName}" (${
|
|
692
|
+
writeFileSync(wired.target, wired.content);
|
|
693
|
+
console.log(`wired ${path} onto ${wired.elementName} in slice "${wired.sliceName}" (${wired.target})`);
|
|
639
694
|
}
|
|
640
695
|
else {
|
|
641
696
|
console.log(`add this to the slice's primary element in the .em file:`);
|
|
@@ -739,6 +794,7 @@ slice
|
|
|
739
794
|
"facts (key, pattern, doc status/implementedIn) — the hand-maintained table is deprecated")
|
|
740
795
|
.argument("<file>", "input .em file")
|
|
741
796
|
.option("--check", "verify the table is current; exit non-zero on drift without writing (CI)")
|
|
797
|
+
.option("--full", "every lifecycle column (reviewed/ratified/owner/tracking/implemented/conformed) instead of the reader's five")
|
|
742
798
|
.action((file, opts) => {
|
|
743
799
|
const { model, diagnostics, refs } = compileFile(file);
|
|
744
800
|
printDiagnostics(diagnostics);
|
|
@@ -746,7 +802,7 @@ slice
|
|
|
746
802
|
console.error("not indexing: fix the errors above");
|
|
747
803
|
process.exit(1);
|
|
748
804
|
}
|
|
749
|
-
const result = runSliceIndex(model, refs, file, !!opts.check);
|
|
805
|
+
const result = runSliceIndex(model, refs, file, !!opts.check, { full: !!opts.full });
|
|
750
806
|
printDiagnostics(newDiagnostics(result.table.diagnostics, diagnostics));
|
|
751
807
|
if (!result.ok) {
|
|
752
808
|
console.error(result.message);
|
|
@@ -758,6 +814,10 @@ slice
|
|
|
758
814
|
else {
|
|
759
815
|
console.log(`ok — ${result.readmePath}'s Slices table is already up to date`);
|
|
760
816
|
}
|
|
817
|
+
if (result.storylines === "absent") {
|
|
818
|
+
console.log(`note: ${result.readmePath} has no <!-- GENERATED:storylines:start --> / :end markers, so the storyline table ` +
|
|
819
|
+
"was not written — add the ## Storylines section from templates/model-readme.md to get it");
|
|
820
|
+
}
|
|
761
821
|
});
|
|
762
822
|
slice
|
|
763
823
|
.command("mark-implemented")
|
|
@@ -1445,8 +1505,8 @@ state
|
|
|
1445
1505
|
process.exit(1);
|
|
1446
1506
|
}
|
|
1447
1507
|
const phases = sortUsagePhases(rawPhases);
|
|
1448
|
-
const { model, diagnostics, refs,
|
|
1449
|
-
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics,
|
|
1508
|
+
const { model, diagnostics, refs, hashSource } = compileFile(file);
|
|
1509
|
+
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics, hashSource);
|
|
1450
1510
|
const categorySet = new Set(allDiagnostics.map((d) => RULES[d.code].usageCategory));
|
|
1451
1511
|
const categories = categorySet.size > 0 ? [...categorySet].sort() : ["none"];
|
|
1452
1512
|
const loaded = loadStateFile(dirname(file));
|
|
@@ -1489,13 +1549,13 @@ modelVersionCmd
|
|
|
1489
1549
|
}
|
|
1490
1550
|
const today = localIsoDate();
|
|
1491
1551
|
const on = opts.on ?? today;
|
|
1492
|
-
const { model: compiledModel, refs, diagnostics,
|
|
1552
|
+
const { model: compiledModel, refs, diagnostics, hashSource } = compileFile(file);
|
|
1493
1553
|
printDiagnostics(diagnostics);
|
|
1494
1554
|
if (hasErrors(diagnostics)) {
|
|
1495
1555
|
console.error(`em model version bump: "${file}" has errors — fix them first`);
|
|
1496
1556
|
process.exit(1);
|
|
1497
1557
|
}
|
|
1498
|
-
const result = runModelVersionBump(baseDir, basename(file), compiledModel, refs,
|
|
1558
|
+
const result = runModelVersionBump(baseDir, basename(file), compiledModel, refs, hashSource, opts.by, on, GENERATOR_VERSION, opts.force === true);
|
|
1499
1559
|
if (!result.ok) {
|
|
1500
1560
|
console.error(`em model version bump: ${result.message}`);
|
|
1501
1561
|
process.exit(1);
|
|
@@ -1745,6 +1805,10 @@ program
|
|
|
1745
1805
|
// --storyline resolves, in which case build() below points it at that storyline's own
|
|
1746
1806
|
// subset file instead (GH #309).
|
|
1747
1807
|
let liveFile = basename(out);
|
|
1808
|
+
// GH #422: the storyline canvas (the page `em render --map` writes), rebuilt on every build
|
|
1809
|
+
// and served live at /map — undefined while the model declares no storylines, so /map 404s.
|
|
1810
|
+
let canvas;
|
|
1811
|
+
let hasStorylines = false;
|
|
1748
1812
|
// serializeBuilds: chokidar can fire several events for one save (and an
|
|
1749
1813
|
// editor's write can land while a slow png/pdf render is still in flight) —
|
|
1750
1814
|
// overlapping builds would race each other writing the same output file.
|
|
@@ -1787,14 +1851,26 @@ program
|
|
|
1787
1851
|
await writeRendered(fullSvg, out, "svg");
|
|
1788
1852
|
// Every declared storyline's own subset (GH #277's `em render --storyline` subset,
|
|
1789
1853
|
// reused verbatim) — rendered alongside the full model on every build, independent of
|
|
1790
|
-
// --storyline, so the viewer's picker always has somewhere to navigate to.
|
|
1854
|
+
// --storyline, so the viewer's picker always has somewhere to navigate to. The same
|
|
1855
|
+
// subsets fill the live canvas's frames (GH #422), each frame opening into its
|
|
1856
|
+
// storyline's viewer page.
|
|
1857
|
+
const frames = [];
|
|
1791
1858
|
for (const st of model.storylines ?? []) {
|
|
1792
1859
|
const sub = buildStorylineDiagram(model, st, { keepEmptyLanes: opts.keepEmptyLanes });
|
|
1793
1860
|
const subFile = storylineFileFor(st.name);
|
|
1794
1861
|
const subRaw = await layoutDot(sub.dot);
|
|
1795
1862
|
const subSvg = embedStorylinePicker(composeSvg(subRaw, sub.model, sub.grid, baseDir, outDir), storylineVariants(model, outFile, subFile));
|
|
1796
1863
|
await writeRendered(subSvg, join(outDir, subFile), "svg");
|
|
1864
|
+
frames.push({
|
|
1865
|
+
name: st.name,
|
|
1866
|
+
slug: kebabSlug(st.name),
|
|
1867
|
+
sliceCount: st.sliceIndexes.length,
|
|
1868
|
+
svg: subSvg,
|
|
1869
|
+
href: `/?svg=${encodeURIComponent(subFile)}`,
|
|
1870
|
+
});
|
|
1797
1871
|
}
|
|
1872
|
+
hasStorylines = frames.length > 0;
|
|
1873
|
+
canvas = hasStorylines ? buildStorylineMapHtml(model, frames, storylineMap(model), { live: true }) : undefined;
|
|
1798
1874
|
}
|
|
1799
1875
|
else {
|
|
1800
1876
|
await renderDot(dot, model, grid, out, fmt, dirname(file));
|
|
@@ -1812,11 +1888,16 @@ program
|
|
|
1812
1888
|
try {
|
|
1813
1889
|
// Serve the directory the SVG is written to — that's what the browser
|
|
1814
1890
|
// fetches, and note "..." links inside the SVG resolve relative to it.
|
|
1815
|
-
server = await startLiveServer({ dir: dirname(resolve(out)), port: opts.port });
|
|
1891
|
+
server = await startLiveServer({ dir: dirname(resolve(out)), port: opts.port, canvas: () => canvas });
|
|
1816
1892
|
// The viewer inlines SVG only — for png/pdf watches there's nothing
|
|
1817
1893
|
// live to embed, so don't print a ?svg= URL that can't work.
|
|
1818
1894
|
if (fmt === "svg") {
|
|
1819
|
-
|
|
1895
|
+
const pinned = opts.storyline !== undefined;
|
|
1896
|
+
console.log(`→ live view: ${liveViewUrl(server.url, liveFile, hasStorylines, pinned)}`);
|
|
1897
|
+
if (hasStorylines && !pinned) {
|
|
1898
|
+
console.log(" the storyline canvas — click a storyline's “open ›” to step through it;");
|
|
1899
|
+
console.log(` the full timeline: ${server.url}/?svg=${encodeURIComponent(liveFile)}`);
|
|
1900
|
+
}
|
|
1820
1901
|
console.log(" open it in a browser and share your screen");
|
|
1821
1902
|
}
|
|
1822
1903
|
else {
|
|
@@ -1833,7 +1914,20 @@ program
|
|
|
1833
1914
|
// the .em file. Watching the directory (not a glob — chokidar 4 has none)
|
|
1834
1915
|
// still fires for docs authored after watch starts; the filter keeps it to .md.
|
|
1835
1916
|
const slicesDir = join(dirname(file), "slices");
|
|
1836
|
-
|
|
1917
|
+
// GH #421: a storyline model file is part of the model — watch each one the root declares
|
|
1918
|
+
// (resolved once at start; a storyline added to the root mid-session needs a restart).
|
|
1919
|
+
const storylineFiles = (() => {
|
|
1920
|
+
try {
|
|
1921
|
+
return compileModelFile(file).files.map((f) => resolve(dirname(file), f));
|
|
1922
|
+
}
|
|
1923
|
+
catch {
|
|
1924
|
+
return [];
|
|
1925
|
+
}
|
|
1926
|
+
})();
|
|
1927
|
+
const watched = new Set([resolve(file), ...storylineFiles]);
|
|
1928
|
+
// GH #421 step 2: docs live beside their declaring file — watch every existing doc folder
|
|
1929
|
+
const docFoldersNow = docDirs(dirname(file));
|
|
1930
|
+
watchFile([file, ...storylineFiles, slicesDir, ...docFoldersNow.filter((d) => resolve(d) !== resolve(slicesDir))], build, 80, (p) => watched.has(resolve(p)) || p.endsWith(".md"));
|
|
1837
1931
|
console.log(`watching ${file} … (ctrl-c to stop)`);
|
|
1838
1932
|
if (!opts.serve)
|
|
1839
1933
|
console.log("tip: add --serve for the live browser view");
|
|
@@ -1892,9 +1986,9 @@ program
|
|
|
1892
1986
|
console.error("em validate: --shape cannot be combined with --slice-ready, --list-*, --fail-on-issues or --check-signed-bodies");
|
|
1893
1987
|
process.exit(1);
|
|
1894
1988
|
}
|
|
1895
|
-
const { model, diagnostics, refs, source } = compileFile(file);
|
|
1989
|
+
const { model, diagnostics, refs, source, hashSource } = compileFile(file);
|
|
1896
1990
|
if (opts.shape) {
|
|
1897
|
-
const all = computeAllDiagnostics(file, model, refs, diagnostics,
|
|
1991
|
+
const all = computeAllDiagnostics(file, model, refs, diagnostics, hashSource);
|
|
1898
1992
|
const report = buildShapeReport(all, opts.waiveFile ? readWaiveFile(opts.waiveFile) : []);
|
|
1899
1993
|
const modelErrors = all.filter((d) => d.severity === "error").length;
|
|
1900
1994
|
for (const k of report.staleWaivers)
|
|
@@ -1908,7 +2002,7 @@ program
|
|
|
1908
2002
|
// GH #257: an opt-in addition to the unconditional set, so --json, the text report and the
|
|
1909
2003
|
// exit code all see it the same way every other diagnostic is seen.
|
|
1910
2004
|
const allDiagnostics = [
|
|
1911
|
-
...computeAllDiagnostics(file, model, refs, diagnostics,
|
|
2005
|
+
...computeAllDiagnostics(file, model, refs, diagnostics, hashSource),
|
|
1912
2006
|
...(opts.checkSignedBodies ? validateSignedBodies(model, refs, dirname(file)) : []),
|
|
1913
2007
|
];
|
|
1914
2008
|
// GH #293: --strict promotes advisory lints to errors (the --slice-ready branch below does
|
|
@@ -2310,11 +2404,11 @@ program
|
|
|
2310
2404
|
const compiled = [];
|
|
2311
2405
|
let anyErrors = false;
|
|
2312
2406
|
for (const file of files) {
|
|
2313
|
-
const { model, refs, diagnostics, source } = compileFile(file);
|
|
2407
|
+
const { model, refs, diagnostics, source, hashSource } = compileFile(file);
|
|
2314
2408
|
printDiagnosticsFor(file, diagnostics);
|
|
2315
2409
|
if (hasErrors(diagnostics))
|
|
2316
2410
|
anyErrors = true;
|
|
2317
|
-
compiled.push({ file, model, refs, source });
|
|
2411
|
+
compiled.push({ file, model, refs, source, hashSource });
|
|
2318
2412
|
}
|
|
2319
2413
|
if (anyErrors) {
|
|
2320
2414
|
console.error("em status: not reporting status — fix the errors above");
|
|
@@ -2372,7 +2466,7 @@ program
|
|
|
2372
2466
|
return resolveConformanceEntry(file, opts.repo, sliceDocFacts);
|
|
2373
2467
|
});
|
|
2374
2468
|
// MIL-218: one model-version entry per input file.
|
|
2375
|
-
const modelVersion = compiled.map(({ file, model, refs,
|
|
2469
|
+
const modelVersion = compiled.map(({ file, model, refs, hashSource }) => resolveModelVersionStatusEntry(file, model, refs, hashSource));
|
|
2376
2470
|
// MIL-219: one em-version entry per input file whose state file resolved.
|
|
2377
2471
|
const emVersion = compiled
|
|
2378
2472
|
.map(({ file }) => resolveEmVersionStatusEntry(file, PKG_VERSION))
|
|
@@ -2680,8 +2774,8 @@ function resolveSliceOrExit(command, model, refs, input) {
|
|
|
2680
2774
|
/** Compile `file` with the full `em validate` diagnostic set — what `engagement plan` needs for
|
|
2681
2775
|
* each slice's `--slice-ready` verdict (R34). */
|
|
2682
2776
|
function engagementModelInput(file) {
|
|
2683
|
-
const { model, refs, diagnostics, source } = compileFile(file);
|
|
2684
|
-
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics,
|
|
2777
|
+
const { model, refs, diagnostics, source, hashSource } = compileFile(file);
|
|
2778
|
+
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics, hashSource);
|
|
2685
2779
|
return { file, model, refs, baseDir: dirname(file), contract: { file, source, diagnostics }, allDiagnostics };
|
|
2686
2780
|
}
|
|
2687
2781
|
engagement
|
|
@@ -3014,17 +3108,24 @@ const scopeCommand = systemCommand
|
|
|
3014
3108
|
function packagedSkillsRoot() {
|
|
3015
3109
|
return join(dirname(fileURLToPath(import.meta.url)), "..", ".claude", "skills");
|
|
3016
3110
|
}
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
// MIL-269 (R32): the four em sub-agent definitions live in
|
|
3111
|
+
// GH #412: the consumer side is `.claude/skills/` + `.claude/agents/` by default, `--skills-dir` /
|
|
3112
|
+
// `--agents-dir` when given, or `.agents/skills/` (+ `.agents/agents/`) when that neutral folder
|
|
3113
|
+
// exists and `.claude/skills/` does not — see resolveVendoredDirs (src/cli/skillDirs.ts).
|
|
3114
|
+
// MIL-269 (R32): the four em sub-agent definitions live in the agents dir, a directory shared
|
|
3021
3115
|
// with the consumer's own agents - install/sync/check only ever handle the fixed EM_AGENT_FILES.
|
|
3022
3116
|
function packagedAgentsRoot() {
|
|
3023
3117
|
return join(dirname(fileURLToPath(import.meta.url)), "..", ".claude", "agents");
|
|
3024
3118
|
}
|
|
3025
|
-
function
|
|
3026
|
-
|
|
3119
|
+
function vendoredDirs(repoRoot, opts) {
|
|
3120
|
+
const dirs = resolveVendoredDirs(repoRoot, opts);
|
|
3121
|
+
if (dirs.detected)
|
|
3122
|
+
console.log(`using ${relative(resolve(repoRoot), dirs.skillsRoot)}/ (no .claude/skills/ here; pass --skills-dir to choose)`);
|
|
3123
|
+
return dirs;
|
|
3027
3124
|
}
|
|
3125
|
+
const SKILLS_DIR_HELP = "where the vendored skill bundle lives, relative to the repo root (default .claude/skills; " +
|
|
3126
|
+
".agents/skills when that exists and .claude/skills does not — GH #412)";
|
|
3127
|
+
const AGENTS_DIR_HELP = "where the four em sub-agent definitions live, relative to the repo root (default .claude/agents, " +
|
|
3128
|
+
"or .agents/agents beside an .agents/skills bundle — GH #412)";
|
|
3028
3129
|
program
|
|
3029
3130
|
.command("contract")
|
|
3030
3131
|
.description("print the packaged implementation contract (reference/implement.md) to stdout — the " +
|
|
@@ -3066,9 +3167,8 @@ const skill = program
|
|
|
3066
3167
|
.description("manage Claude Code skills bundled with em");
|
|
3067
3168
|
// MIL-269: reconcile the four `.claude/agents/em-*.md` files (create the directory if absent);
|
|
3068
3169
|
// byte-exact restore, never touches any other file there.
|
|
3069
|
-
function installAgentFiles(
|
|
3170
|
+
function installAgentFiles(vendored) {
|
|
3070
3171
|
const packaged = packagedAgentsRoot();
|
|
3071
|
-
const vendored = vendoredAgentsRoot(repoRoot);
|
|
3072
3172
|
const plan = planAgentSync(packaged, vendored, EM_AGENT_FILES);
|
|
3073
3173
|
if (plan.changes.length === 0)
|
|
3074
3174
|
return;
|
|
@@ -3088,10 +3188,13 @@ skill
|
|
|
3088
3188
|
"bundle's own managed directories the same way `em skill sync` does — never touching an " +
|
|
3089
3189
|
"unrelated sibling skill (MIL-180)")
|
|
3090
3190
|
.option("--no-agents-md", "skip writing/updating the AGENTS.md agent-contract section (on by default, MIL-129)")
|
|
3191
|
+
.option("--skills-dir <dir>", SKILLS_DIR_HELP)
|
|
3192
|
+
.option("--agents-dir <dir>", AGENTS_DIR_HELP)
|
|
3091
3193
|
.action(async (opts) => {
|
|
3092
3194
|
warnVendoredDeprecated();
|
|
3093
3195
|
const srcRoot = packagedSkillsRoot();
|
|
3094
|
-
const
|
|
3196
|
+
const dirs = vendoredDirs(process.cwd(), opts);
|
|
3197
|
+
const destRoot = dirs.skillsRoot;
|
|
3095
3198
|
const anchor = join(destRoot, EM_SKILL_ANCHOR_DIR);
|
|
3096
3199
|
if (existsSync(anchor) && !opts.force) {
|
|
3097
3200
|
console.log(`skill already installed at ${anchor}`);
|
|
@@ -3121,7 +3224,7 @@ skill
|
|
|
3121
3224
|
`(${removedCount} removed), ${totalUnchanged} unchanged`);
|
|
3122
3225
|
}
|
|
3123
3226
|
console.log("in Claude Code, run /event-modeling to start a guided session");
|
|
3124
|
-
installAgentFiles(
|
|
3227
|
+
installAgentFiles(dirs.agentsRoot);
|
|
3125
3228
|
}
|
|
3126
3229
|
else {
|
|
3127
3230
|
await mkdir(destRoot, { recursive: true });
|
|
@@ -3130,7 +3233,7 @@ skill
|
|
|
3130
3233
|
}
|
|
3131
3234
|
console.log(`installed event-modeling skill bundle → ${destRoot}`);
|
|
3132
3235
|
console.log("in Claude Code, run /event-modeling to start a guided session");
|
|
3133
|
-
installAgentFiles(
|
|
3236
|
+
installAgentFiles(dirs.agentsRoot);
|
|
3134
3237
|
}
|
|
3135
3238
|
if (opts.agentsMd !== false) {
|
|
3136
3239
|
console.log(agentsMdMessage(syncAgentsMd(process.cwd())));
|
|
@@ -3142,15 +3245,18 @@ skill
|
|
|
3142
3245
|
"installed em package (overwrites unconditionally; local edits are never merged, MIL-93)")
|
|
3143
3246
|
.argument("[path]", "consumer repo root", ".")
|
|
3144
3247
|
.option("--no-agents-md", "skip writing/updating the AGENTS.md agent-contract section (on by default, MIL-129)")
|
|
3248
|
+
.option("--skills-dir <dir>", SKILLS_DIR_HELP)
|
|
3249
|
+
.option("--agents-dir <dir>", AGENTS_DIR_HELP)
|
|
3145
3250
|
.action(async (path, opts) => {
|
|
3146
3251
|
warnVendoredDeprecated();
|
|
3147
3252
|
const packagedRoot = packagedSkillsRoot();
|
|
3148
|
-
const
|
|
3253
|
+
const dirs = vendoredDirs(path, opts);
|
|
3254
|
+
const vendoredRoot = dirs.skillsRoot;
|
|
3149
3255
|
const bundlePlan = planSkillSyncBundle(packagedRoot, vendoredRoot, EM_ALL_SKILL_BUNDLE_DIRS);
|
|
3150
3256
|
const skillChanges = bundlePlan.reduce((n, { plan }) => n + plan.changes.length, 0);
|
|
3151
3257
|
// MIL-269 (R32): the fixed agent-file list, never the shared .claude/agents/ directory as a whole.
|
|
3152
3258
|
const packagedAgents = packagedAgentsRoot();
|
|
3153
|
-
const vendoredAgents =
|
|
3259
|
+
const vendoredAgents = dirs.agentsRoot;
|
|
3154
3260
|
const agentPlan = planAgentSync(packagedAgents, vendoredAgents, EM_AGENT_FILES);
|
|
3155
3261
|
const totalChanges = skillChanges + agentPlan.changes.length;
|
|
3156
3262
|
const totalUnchanged = bundlePlan.reduce((n, { plan }) => n + plan.unchangedCount, 0) + agentPlan.unchangedCount;
|
|
@@ -3181,9 +3287,13 @@ skill
|
|
|
3181
3287
|
.option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
|
|
3182
3288
|
.option("--ci", "also fail (exit 1) when the declared plugin is not registered on this machine (implied by CI=true in the environment); " +
|
|
3183
3289
|
"without it that finding prints the install commands on stderr and exits 0")
|
|
3290
|
+
.option("--skills-dir <dir>", SKILLS_DIR_HELP)
|
|
3291
|
+
.option("--agents-dir <dir>", AGENTS_DIR_HELP)
|
|
3184
3292
|
.action((path, opts) => {
|
|
3185
3293
|
const packagedRoot = packagedSkillsRoot();
|
|
3186
|
-
|
|
3294
|
+
// Detection is silent here: a check prints findings and one verdict line, nothing else.
|
|
3295
|
+
const dirs = resolveVendoredDirs(path, opts);
|
|
3296
|
+
const vendoredRoot = dirs.skillsRoot;
|
|
3187
3297
|
const ciMode = !!opts.ci || process.env.CI === "true";
|
|
3188
3298
|
// MIL-231: the plugin signal (a pinned marketplace in .claude/settings*.json) sits beside the
|
|
3189
3299
|
// vendored signal (the anchor directory). Plugin only -> only the plugin is checked; vendored
|
|
@@ -3196,7 +3306,7 @@ skill
|
|
|
3196
3306
|
// MIL-269 (R32): the four agent files, only when the vendored bundle is present (a plugin-only
|
|
3197
3307
|
// repo ships them via the plugin); checked by fixed name, never the shared directory.
|
|
3198
3308
|
const agents = vendoredPresent
|
|
3199
|
-
? checkAgentFiles(packagedAgentsRoot(),
|
|
3309
|
+
? checkAgentFiles(packagedAgentsRoot(), dirs.agentsRoot, EM_AGENT_FILES)
|
|
3200
3310
|
: { findings: [], ok: true };
|
|
3201
3311
|
const findings = [...vendored.findings, ...agents.findings, ...(pluginCheck?.findings ?? [])];
|
|
3202
3312
|
const failing = findings.filter((f) => f.code !== "plugin-not-installed-locally" || ciMode);
|
|
@@ -3373,7 +3483,74 @@ ci.command("init")
|
|
|
3373
3483
|
console.log(`${path} already up to date`);
|
|
3374
3484
|
}
|
|
3375
3485
|
});
|
|
3376
|
-
const storylineCmd = program.command("storyline").description("
|
|
3486
|
+
const storylineCmd = program.command("storyline").description("storylines (GH #277, #421): adopt storyline blocks, split them into storyline model files");
|
|
3487
|
+
storylineCmd
|
|
3488
|
+
.command("split")
|
|
3489
|
+
.description("move each inline `storyline \"X\" { … }` block into its own storyline model file <slug>/<slug>.em " +
|
|
3490
|
+
"(a `storyline \"X\" [source]` header, then the block's slices) and leave the one-line declaration " +
|
|
3491
|
+
"`storyline \"X\" [after \"Y\"]` in the root (GH #421). Slice docs stay under the root's slices/. " +
|
|
3492
|
+
"A storyline declared in several blocks is left alone (merge them first)")
|
|
3493
|
+
.argument("<file>", "the root .em model")
|
|
3494
|
+
.option("--storyline <name>", "split only this storyline")
|
|
3495
|
+
.option("--dry-run", "print what would be written without touching anything")
|
|
3496
|
+
.option("-f, --force", "overwrite a storyline model file that already exists")
|
|
3497
|
+
.action((file, opts) => {
|
|
3498
|
+
const { model, refs, diagnostics, source } = compileFile(file);
|
|
3499
|
+
printDiagnostics(diagnostics.filter((d) => d.severity === "error"));
|
|
3500
|
+
if (hasErrors(diagnostics)) {
|
|
3501
|
+
console.error("em storyline split: not splitting — fix the errors above");
|
|
3502
|
+
process.exit(1);
|
|
3503
|
+
}
|
|
3504
|
+
if (opts.storyline !== undefined && !(model.storylines ?? []).some((s) => s.name.trim().toLowerCase() === opts.storyline.trim().toLowerCase())) {
|
|
3505
|
+
console.error(`em storyline split: no storyline "${opts.storyline}" (declared: ${(model.storylines ?? []).map((s) => `"${s.name}"`).join(", ") || "none"})`);
|
|
3506
|
+
process.exit(1);
|
|
3507
|
+
}
|
|
3508
|
+
const plan = planStorylineSplit(model, source, opts.storyline);
|
|
3509
|
+
for (const s of plan.skipped)
|
|
3510
|
+
console.log(`skip "${s.storyline}" — ${s.reason}`);
|
|
3511
|
+
if (plan.files.length === 0) {
|
|
3512
|
+
console.log("nothing to split");
|
|
3513
|
+
return;
|
|
3514
|
+
}
|
|
3515
|
+
const rootDir = dirname(file);
|
|
3516
|
+
for (const f of plan.files) {
|
|
3517
|
+
const abs = resolve(rootDir, f.path);
|
|
3518
|
+
if (existsSync(abs) && !opts.force) {
|
|
3519
|
+
console.error(`em storyline split: ${f.path} already exists (use --force to overwrite)`);
|
|
3520
|
+
process.exit(1);
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
if (opts.dryRun) {
|
|
3524
|
+
for (const f of plan.files)
|
|
3525
|
+
console.log(`would write ${f.path} (storyline "${f.storyline}", ${f.content.split(/\r?\n/).filter((l) => /^slice\b/.test(l)).length} slice(s))`);
|
|
3526
|
+
console.log(`would rewrite ${file}`);
|
|
3527
|
+
return;
|
|
3528
|
+
}
|
|
3529
|
+
for (const f of plan.files) {
|
|
3530
|
+
const abs = resolve(rootDir, f.path);
|
|
3531
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
3532
|
+
writeFileSync(abs, f.content, "utf8");
|
|
3533
|
+
console.log(`wrote ${f.path} (storyline "${f.storyline}")`);
|
|
3534
|
+
// GH #421 step 2: the story's docs (and per-slice diagrams) move beside its file
|
|
3535
|
+
const st = (model.storylines ?? []).find((s) => s.name === f.storyline);
|
|
3536
|
+
const folder = dirname(abs);
|
|
3537
|
+
let moved = 0;
|
|
3538
|
+
for (const i of st?.sliceIndexes ?? []) {
|
|
3539
|
+
for (const ext of [".md", ".svg"]) {
|
|
3540
|
+
const src = join(rootDir, "slices", `${refs.sliceKeys[i]}${ext}`);
|
|
3541
|
+
if (!existsSync(src))
|
|
3542
|
+
continue;
|
|
3543
|
+
mkdirSync(join(folder, "slices"), { recursive: true });
|
|
3544
|
+
renameSync(src, join(folder, "slices", `${refs.sliceKeys[i]}${ext}`));
|
|
3545
|
+
moved++;
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
if (moved > 0)
|
|
3549
|
+
console.log(` moved ${moved} doc/diagram file(s) into ${relative(rootDir, join(folder, "slices"))}/`);
|
|
3550
|
+
}
|
|
3551
|
+
writeFileSync(file, plan.rootText, "utf8");
|
|
3552
|
+
console.log(`rewrote ${file}: ${plan.files.length} storyline declaration(s) now point at their files`);
|
|
3553
|
+
});
|
|
3377
3554
|
storylineCmd
|
|
3378
3555
|
.command("suggest")
|
|
3379
3556
|
.description("advisory (GH #290): propose `storyline` blocks for a model that has none — one per distinct slice " +
|
|
@@ -3516,8 +3693,8 @@ program
|
|
|
3516
3693
|
* stands, so an estate sees how many advisory shape findings it carries. Never writes a waive file:
|
|
3517
3694
|
* baselining shapes you accept is a decision (`--waive-file`, and a `shape-accepted:` Decisions-log bullet). */
|
|
3518
3695
|
function printUpgradeShapeSection(file) {
|
|
3519
|
-
const { model, diagnostics, refs,
|
|
3520
|
-
const all = computeAllDiagnostics(file, model, refs, diagnostics,
|
|
3696
|
+
const { model, diagnostics, refs, hashSource } = compileFile(file);
|
|
3697
|
+
const all = computeAllDiagnostics(file, model, refs, diagnostics, hashSource);
|
|
3521
3698
|
const report = buildShapeReport(all, []);
|
|
3522
3699
|
console.log("");
|
|
3523
3700
|
console.log("Shape (em validate --shape, advisory):");
|
|
@@ -3543,18 +3720,17 @@ function compileFile(file, opts = {},
|
|
|
3543
3720
|
// watcher (and the --serve live view with it) is a session-ending failure.
|
|
3544
3721
|
// One-shot commands keep the exit-with-message behavior.
|
|
3545
3722
|
{ survive = false } = {}) {
|
|
3546
|
-
|
|
3547
|
-
try {
|
|
3548
|
-
source = readFileSync(file, "utf8");
|
|
3549
|
-
}
|
|
3550
|
-
catch {
|
|
3723
|
+
if (!existsSync(file)) {
|
|
3551
3724
|
if (survive)
|
|
3552
3725
|
throw new Error(`cannot read ${file}`);
|
|
3553
3726
|
console.error(`cannot read ${file}`);
|
|
3554
3727
|
process.exit(1);
|
|
3555
3728
|
}
|
|
3556
3729
|
try {
|
|
3557
|
-
|
|
3730
|
+
// GH #421: the root plus every storyline model file it declares, compiled as one model.
|
|
3731
|
+
// `source` is the root text (line-based edits of root entities), `files`/`sources` the
|
|
3732
|
+
// storyline files, `hashSource` what a model-level hash covers.
|
|
3733
|
+
return compileModelFile(file, opts);
|
|
3558
3734
|
}
|
|
3559
3735
|
catch (e) {
|
|
3560
3736
|
if (e instanceof ParseError) {
|
|
@@ -3563,6 +3739,12 @@ function compileFile(file, opts = {},
|
|
|
3563
3739
|
console.error(`parse error in ${file} ${e.message}`);
|
|
3564
3740
|
process.exit(1);
|
|
3565
3741
|
}
|
|
3742
|
+
if (e instanceof ModelLoadError) {
|
|
3743
|
+
if (survive)
|
|
3744
|
+
throw new Error(e.message);
|
|
3745
|
+
console.error(e.message);
|
|
3746
|
+
process.exit(1);
|
|
3747
|
+
}
|
|
3566
3748
|
throw e;
|
|
3567
3749
|
}
|
|
3568
3750
|
}
|
|
@@ -3578,7 +3760,8 @@ function compileFile(file, opts = {},
|
|
|
3578
3760
|
* report, since its whole job is logging which categories fired. `source` is the `.em` file's
|
|
3579
3761
|
* own text — only `validateModelVersionStale` reads it (the modelHash comparison). */
|
|
3580
3762
|
function computeAllDiagnostics(file, model, refs, diagnostics, source) {
|
|
3581
|
-
|
|
3763
|
+
// GH #421: the fs-aware rules below point at elements too — give each its storyline file.
|
|
3764
|
+
return attributeDiagnosticFiles(model, refs, [
|
|
3582
3765
|
...diagnostics,
|
|
3583
3766
|
...validateLineage(model, refs, dirname(file)),
|
|
3584
3767
|
...validateFrontmatterCoherence(model, refs, dirname(file)),
|
|
@@ -3587,7 +3770,7 @@ function computeAllDiagnostics(file, model, refs, diagnostics, source) {
|
|
|
3587
3770
|
...validateOrphanedSliceDocs(model, refs, dirname(file)),
|
|
3588
3771
|
...validateModelVersionStale(model, refs, dirname(file), source),
|
|
3589
3772
|
...validateInvariants(model, refs, dirname(file)),
|
|
3590
|
-
];
|
|
3773
|
+
]);
|
|
3591
3774
|
}
|
|
3592
3775
|
/** Read a file's text, or exit with a clear error — shared by `em diff` (both forms) and
|
|
3593
3776
|
* `em glossary`, which each read one or more plain `.em` files off disk before compiling. */
|
|
@@ -3601,18 +3784,31 @@ function readFileOrExit(file) {
|
|
|
3601
3784
|
}
|
|
3602
3785
|
}
|
|
3603
3786
|
/** Parse+normalize source text for `em diff`, exiting with a labeled parse error on failure. */
|
|
3604
|
-
function compileSource(source, label) {
|
|
3787
|
+
function compileSource(source, label, read) {
|
|
3605
3788
|
try {
|
|
3606
|
-
|
|
3789
|
+
// GH #421: a split model's storyline files come through `read` (disk, or git at a revision).
|
|
3790
|
+
return compileModelText(source, read, label);
|
|
3607
3791
|
}
|
|
3608
3792
|
catch (e) {
|
|
3609
3793
|
if (e instanceof ParseError) {
|
|
3610
3794
|
console.error(`parse error in ${label} ${e.message}`);
|
|
3611
3795
|
process.exit(1);
|
|
3612
3796
|
}
|
|
3797
|
+
if (e instanceof ModelLoadError) {
|
|
3798
|
+
console.error(e.message);
|
|
3799
|
+
process.exit(1);
|
|
3800
|
+
}
|
|
3613
3801
|
throw e;
|
|
3614
3802
|
}
|
|
3615
3803
|
}
|
|
3804
|
+
/** GH #421: a reader for a text with no file behind it — a split root cannot be compiled from text alone. */
|
|
3805
|
+
const noStorylineFiles = (rel) => {
|
|
3806
|
+
throw new ModelLoadError(`storyline model file ${rel} is not available for this text`);
|
|
3807
|
+
};
|
|
3808
|
+
/** GH #421: storyline model files of `file` as of git revision `rev`. */
|
|
3809
|
+
function gitReader(file, rev) {
|
|
3810
|
+
return (rel) => readAtRevision(join(dirname(file), rel), rev);
|
|
3811
|
+
}
|
|
3616
3812
|
/** Resolve `file`'s content at a git revision, exiting with a clear error on any git failure. */
|
|
3617
3813
|
function readAtRevision(file, rev) {
|
|
3618
3814
|
const result = resolveRevision(file, rev);
|
|
@@ -3623,9 +3819,11 @@ function readAtRevision(file, rev) {
|
|
|
3623
3819
|
return result.content;
|
|
3624
3820
|
}
|
|
3625
3821
|
/** Shared body for both `em diff` forms: compile both sides, gate on errors, print, exit-code. */
|
|
3626
|
-
function runDiff(oldSource, oldLabel, newSource, newLabel, exitCode, json, lineage
|
|
3627
|
-
|
|
3628
|
-
|
|
3822
|
+
function runDiff(oldSource, oldLabel, newSource, newLabel, exitCode, json, lineage,
|
|
3823
|
+
// GH #421: how each side's storyline model files are read (disk, or git at a revision).
|
|
3824
|
+
readers = { old: noStorylineFiles, new: noStorylineFiles }) {
|
|
3825
|
+
const oldResult = compileSource(oldSource, oldLabel, readers.old);
|
|
3826
|
+
const newResult = compileSource(newSource, newLabel, readers.new);
|
|
3629
3827
|
printDiagnostics(oldResult.diagnostics);
|
|
3630
3828
|
printDiagnostics(newResult.diagnostics);
|
|
3631
3829
|
if (hasErrors(oldResult.diagnostics) || hasErrors(newResult.diagnostics)) {
|
|
@@ -3715,7 +3913,7 @@ function printDiagnostics(diags) {
|
|
|
3715
3913
|
if (diags.length === 0)
|
|
3716
3914
|
return;
|
|
3717
3915
|
for (const d of diags) {
|
|
3718
|
-
const line =
|
|
3916
|
+
const line = formatDiagnosticWithFile(d);
|
|
3719
3917
|
if (d.severity === "error")
|
|
3720
3918
|
console.error(line);
|
|
3721
3919
|
else
|