@milehimikey/em 1.19.7 → 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-conform/reference/conform.md +29 -1
- package/.claude/skills/event-modeling-design/SKILL.md +33 -7
- package/.claude/skills/event-modeling-discover/SKILL.md +2 -2
- package/.claude/skills/event-modeling-discover/reference/extract.md +12 -0
- package/.claude/skills/event-modeling-engagement/SKILL.md +1 -1
- package/.claude/skills/event-modeling-implement/SKILL.md +7 -2
- package/.claude/skills/event-modeling-implement/reference/implement.md +73 -6
- package/.claude/skills/event-modeling-review/SKILL.md +19 -7
- package/.claude/skills/event-modeling-shared/reference/em-dsl.md +57 -5
- package/.claude/skills/event-modeling-shared/reference/methodology.md +51 -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 +12 -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/docBadge.d.ts +16 -0
- package/dist/catalog/docBadge.js +50 -0
- package/dist/catalog/docBadge.js.map +1 -0
- 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/docModelConsistencyValidate.js +9 -2
- package/dist/catalog/docModelConsistencyValidate.js.map +1 -1
- package/dist/catalog/frontmatterCoherenceValidate.js +16 -1
- package/dist/catalog/frontmatterCoherenceValidate.js.map +1 -1
- package/dist/catalog/invTokens.d.ts +49 -0
- package/dist/catalog/invTokens.js +94 -0
- package/dist/catalog/invTokens.js.map +1 -0
- package/dist/catalog/orphanedSliceDocValidate.js +15 -10
- package/dist/catalog/orphanedSliceDocValidate.js.map +1 -1
- package/dist/catalog/pages.js +48 -1
- package/dist/catalog/pages.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/scenarios.d.ts +4 -0
- package/dist/catalog/scenarios.js +4 -1
- package/dist/catalog/scenarios.js.map +1 -1
- package/dist/catalog/sliceDoc.d.ts +54 -1
- package/dist/catalog/sliceDoc.js +99 -2
- package/dist/catalog/sliceDoc.js.map +1 -1
- package/dist/catalog/sliceDocRender.d.ts +3 -0
- package/dist/catalog/sliceDocRender.js +295 -0
- package/dist/catalog/sliceDocRender.js.map +1 -0
- 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/coverage.d.ts +13 -23
- package/dist/cli/coverage.js +51 -91
- package/dist/cli/coverage.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.d.ts +52 -0
- package/dist/cli/refsCheck.js +129 -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.d.ts +57 -1
- package/dist/cli/sliceConform.js +111 -10
- 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 +33 -0
- package/dist/cli/watchStoryline.js +65 -0
- package/dist/cli/watchStoryline.js.map +1 -0
- package/dist/cli.js +423 -71
- package/dist/cli.js.map +1 -1
- package/dist/emit/docJson.d.ts +99 -0
- package/dist/emit/docJson.js +197 -0
- package/dist/emit/docJson.js.map +1 -0
- 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/emit/refsJson.d.ts +5 -3
- package/dist/emit/refsJson.js +5 -2
- package/dist/emit/refsJson.js.map +1 -1
- package/dist/mcp/server.js +43 -9
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/continuation.d.ts +9 -0
- package/dist/model/continuation.js +30 -0
- package/dist/model/continuation.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 +31 -0
- package/dist/model/rules.js +31 -0
- package/dist/model/rules.js.map +1 -1
- package/dist/model/shapeLint.js +143 -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 +127 -0
- package/dist/model/validate.js.map +1 -1
- package/dist/parser/ast.d.ts +27 -0
- package/dist/parser/parser.d.ts +17 -1
- package/dist/parser/parser.js +142 -3
- 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 +93 -10
- 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 +5 -1
- package/dist/render/sliceOverlay.js +24 -2
- 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/storylinePicker.d.ts +9 -0
- package/dist/render/storylinePicker.js +17 -0
- package/dist/render/storylinePicker.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 +57 -5
- package/plugin/shared/reference/methodology.md +51 -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 +12 -1
- package/plugin/skills/conform/SKILL.md +1 -1
- package/plugin/skills/conform/reference/conform.md +29 -1
- package/plugin/skills/design/SKILL.md +33 -7
- package/plugin/skills/discover/SKILL.md +2 -2
- package/plugin/skills/discover/reference/extract.md +12 -0
- package/plugin/skills/engagement/SKILL.md +1 -1
- package/plugin/skills/event-modeling/SKILL.md +1 -1
- package/plugin/skills/implement/SKILL.md +7 -2
- package/plugin/skills/implement/reference/implement.md +73 -6
- 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,11 +15,16 @@ 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";
|
|
20
|
+
import { embedStorylinePicker } from "./render/storylinePicker.js";
|
|
21
|
+
import { liveViewUrl, resolveWatchStoryline, storylineFileFor, storylineVariants } from "./cli/watchStoryline.js";
|
|
17
22
|
import { resolveSliceKey } from "./model/resolveSlice.js";
|
|
18
23
|
import { serializeBuilds, watchFile } from "./render/watch.js";
|
|
19
24
|
import { startLiveServer } from "./render/serve.js";
|
|
20
|
-
import { formatDiagnostic, hasErrors, publicViewsWithoutInModelReader } from "./model/validate.js";
|
|
25
|
+
import { formatDiagnostic, hasErrors, publicViewsWithoutInModelReader, formatDiagnosticWithFile } from "./model/validate.js";
|
|
21
26
|
import { buildExport, buildSliceExport, GENERATOR_VERSION } from "./emit/json.js";
|
|
27
|
+
import { buildDocJsonExport } from "./emit/docJson.js";
|
|
22
28
|
import { generateContract, runApiCheck, formatApiCheckText } from "./cli/api.js";
|
|
23
29
|
import { buildApiCheckJson } from "./emit/apiCheckJson.js";
|
|
24
30
|
import { buildValidateJson, buildShapeJson, buildSliceReadyJson, buildValidateListJson, collectMarkers } from "./emit/validateJson.js";
|
|
@@ -50,7 +56,7 @@ import { planMigration, verifyMigration } from "./cli/migrateReactionShape.js";
|
|
|
50
56
|
import { buildLedgerJson } from "./emit/ledgerJson.js";
|
|
51
57
|
import { buildRefsJson } from "./emit/refsJson.js";
|
|
52
58
|
import { buildShapeReport, formatShapeText } from "./cli/shape.js";
|
|
53
|
-
import { applyRefWaivers, checkRefs, readWaiveFile } from "./cli/refsCheck.js";
|
|
59
|
+
import { applyRefWaivers, checkRatificationOrder, checkRefs, readWaiveFile } from "./cli/refsCheck.js";
|
|
54
60
|
import { buildCoverageReport, resolveScopedSlices, resolveCoverageSliceKey } from "./cli/coverage.js";
|
|
55
61
|
import { buildCoverageJson } from "./emit/coverageJson.js";
|
|
56
62
|
import { resolveSliceStatusFacts, countOpenIssues, resolveConformanceEntry, aggregateInvariantTotals, buildStatusReport, formatStatusText, formatStatusMarkdown, buildStatusBadge, formatConformancePart, findSpecifyRoot, resolveModelVersionStatusEntry, resolveEmVersionStatusEntry, } from "./cli/status.js";
|
|
@@ -65,7 +71,7 @@ import { checkSkillSyncBundle, checkAgentFiles } from "./cli/skillCheck.js";
|
|
|
65
71
|
import { buildSkillCheckJson } from "./emit/skillCheckJson.js";
|
|
66
72
|
import { checkPlugin, claudePluginsDir, detectPlugin, pluginInstallCommands } from "./cli/pluginPin.js";
|
|
67
73
|
import { readContract } from "./cli/contract.js";
|
|
68
|
-
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";
|
|
69
75
|
import { createServer as createMcpServer } from "./mcp/server.js";
|
|
70
76
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
71
77
|
import { syncAgentsMd } from "./cli/agentsMd.js";
|
|
@@ -89,7 +95,7 @@ import { listModelCommits } from "./cli/changelog-git.js";
|
|
|
89
95
|
import { buildChangelogDoc } from "./cli/changelogBuild.js";
|
|
90
96
|
import { runRevise } from "./cli/revise.js";
|
|
91
97
|
import { runDefer, runDeferFromIssue } from "./cli/defer.js";
|
|
92
|
-
import { runSliceConform, resolveFindingsForReport, findingsFilesOf, unruledAcrossFiles } from "./cli/sliceConform.js";
|
|
98
|
+
import { runSliceConform, runSliceConformPreview, writePreviewFindings, resolveFindingsForReport, findingsFilesOf, unruledAcrossFiles, } from "./cli/sliceConform.js";
|
|
93
99
|
import { checkPrdTriage, buildCheckPrdTriageJson, renderPrdTriageMarkdown, validatePrdTriage } from "./cli/prdTriage.js";
|
|
94
100
|
import { checkFindingsFile, buildCheckFindingsJson, FINDING_LOCI } from "./cli/findings.js";
|
|
95
101
|
import { STATE_FILE_NAME, PHASES, isPhase, loadStateFile, parseState, setPhase, setConformance, setModelVersion, setCertified, setEmVersion, setReview, isValidDateString, modelPathMismatch, } from "./cli/stateFile.js";
|
|
@@ -99,6 +105,7 @@ import { kebabSlug } from "./util/slug.js";
|
|
|
99
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";
|
|
100
106
|
import { resolveRepoRoot, detectUpgrade, applyUpgrade, checkUpgrade } from "./cli/upgrade.js";
|
|
101
107
|
import { buildStorylineSuggestion } from "./cli/storylineSuggest.js";
|
|
108
|
+
import { planStorylineSplit } from "./cli/storylineSplit.js";
|
|
102
109
|
import { buildUpgradeJson } from "./emit/upgradeJson.js";
|
|
103
110
|
import { isOlder } from "./util/semver.js";
|
|
104
111
|
import { RULES } from "./model/rules.js";
|
|
@@ -114,6 +121,8 @@ import { runCodeowners } from "./system/codeowners.js";
|
|
|
114
121
|
import { buildCodeownersJson } from "./emit/codeownersJson.js";
|
|
115
122
|
import { verifySystem } from "./system/verify.js";
|
|
116
123
|
import { buildSystemJson } from "./emit/systemJson.js";
|
|
124
|
+
import { docDirOf } from "./model/sliceDocPath.js";
|
|
125
|
+
import { docDirs } from "./catalog/docLocate.js";
|
|
117
126
|
/** GH #253: the trailing note `em slice defer` prints after `deferred: …` on a ratified/shipped
|
|
118
127
|
* doc — the item went into the signed body, outside `ratifiedHash`, and when the stamp was still
|
|
119
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). */
|
|
@@ -198,12 +207,17 @@ program
|
|
|
198
207
|
.option("-T, --format <fmt>", "output format (svg, png, pdf, ...)")
|
|
199
208
|
.option("--slice <name>", "render only this slice, redrawn in its own canonical pattern shape " +
|
|
200
209
|
"(default out: slices/<kebab-slug>.svg)")
|
|
201
|
-
.option("--storyline <name>", "render only this storyline: its slices in order,
|
|
202
|
-
"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 " +
|
|
203
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)")
|
|
204
214
|
.option("--emit-dot", "print the generated DOT instead of rendering")
|
|
205
215
|
.option("--keep-empty-lanes", "keep the API lane even when empty")
|
|
206
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
|
+
}
|
|
207
221
|
if (opts.slice !== undefined && opts.emitDot) {
|
|
208
222
|
console.error("em render: --slice cannot be combined with --emit-dot");
|
|
209
223
|
process.exit(1);
|
|
@@ -236,6 +250,27 @@ program
|
|
|
236
250
|
console.error("not rendering: fix the errors above");
|
|
237
251
|
process.exit(1);
|
|
238
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
|
+
}
|
|
239
274
|
if (opts.storyline !== undefined) {
|
|
240
275
|
const found = resolveStorylineArg(model, opts.storyline);
|
|
241
276
|
if ("error" in found) {
|
|
@@ -258,7 +293,7 @@ program
|
|
|
258
293
|
console.error(lookup.error);
|
|
259
294
|
process.exit(1);
|
|
260
295
|
}
|
|
261
|
-
const out = opts.out ?? defaultSliceOut(file, opts.slice);
|
|
296
|
+
const out = opts.out ?? defaultSliceOut(file, opts.slice, docDirOf(model.slices[lookup.index]));
|
|
262
297
|
const fmt = opts.format ?? formatFromPath(out);
|
|
263
298
|
await mkdir(dirname(out), { recursive: true }); // slices/ may not exist yet
|
|
264
299
|
const { model: sliceModel, grid: sliceGrid, dot: sliceDot } = buildSliceDiagram(model, lookup.index, {
|
|
@@ -283,9 +318,35 @@ program
|
|
|
283
318
|
.option("--slice <key>", "export only this slice's object (pattern/fields/doc) instead of the whole model (export " +
|
|
284
319
|
"key, MIL-128) — refuses only if THIS slice has an error; an unrelated slice's breakage " +
|
|
285
320
|
"elsewhere in the model doesn't block it (see docs/cli.md)")
|
|
321
|
+
.option("--doc-json", "emit the structured slice-doc export (GH #401) instead of the model snapshot: per slice the " +
|
|
322
|
+
"frontmatter join plus the parsed sections as data (fields, invariants, scenarios, error " +
|
|
323
|
+
"flows, open questions) and a model-level invariant catalog. Own schema version; whole " +
|
|
324
|
+
"model only (does not combine with --slice)")
|
|
325
|
+
.option("--include-html", "with --doc-json, include each slice doc's rendered HTML body (excluded by default; the " +
|
|
326
|
+
"authored body never appears in a plain export)")
|
|
286
327
|
.action((file, opts) => {
|
|
287
328
|
const { model, refs, diagnostics, source } = compileFile(file);
|
|
288
329
|
printDiagnostics(diagnostics);
|
|
330
|
+
if (opts.docJson) {
|
|
331
|
+
if (opts.slice) {
|
|
332
|
+
console.error("em export --doc-json: exports the whole model and does not combine with --slice");
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
if (hasErrors(diagnostics)) {
|
|
336
|
+
console.error("not exporting: fix the errors above");
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
const exportedDoc = buildDocJsonExport(model, refs, diagnostics, source, file, { includeHtml: opts.includeHtml });
|
|
340
|
+
printDiagnostics(newDiagnostics(exportedDoc.diagnostics, diagnostics));
|
|
341
|
+
if (opts.out) {
|
|
342
|
+
writeFileSync(opts.out, exportedDoc.text + "\n");
|
|
343
|
+
console.log(`wrote ${opts.out}`);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
process.stdout.write(exportedDoc.text + "\n");
|
|
347
|
+
}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
289
350
|
if (opts.slice) {
|
|
290
351
|
// Scoped export deliberately does NOT reuse the whole-model error guard below: the
|
|
291
352
|
// ticket's own motivation for `--slice` is an agent implementing one already-ratified
|
|
@@ -412,7 +473,10 @@ program
|
|
|
412
473
|
oldDoc: (key) => readSliceDoc(dirname(plan.oldFile), key),
|
|
413
474
|
newDoc: (key) => readSliceDoc(dirname(plan.newFile), key),
|
|
414
475
|
};
|
|
415
|
-
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
|
+
});
|
|
416
480
|
return;
|
|
417
481
|
}
|
|
418
482
|
const oldSource = readAtRevision(plan.file, plan.from);
|
|
@@ -423,7 +487,10 @@ program
|
|
|
423
487
|
oldDoc: (key) => resolveDocAtRevision(plan.file, key, plan.from),
|
|
424
488
|
newDoc: (key) => plan.to ? resolveDocAtRevision(plan.file, key, plan.to) : readSliceDoc(dirname(plan.file), key),
|
|
425
489
|
};
|
|
426
|
-
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
|
+
});
|
|
427
494
|
});
|
|
428
495
|
program
|
|
429
496
|
.command("glossary")
|
|
@@ -446,7 +513,7 @@ program
|
|
|
446
513
|
let anyErrors = false;
|
|
447
514
|
for (const file of files) {
|
|
448
515
|
const source = readFileOrExit(file);
|
|
449
|
-
const result = compileSource(source, file);
|
|
516
|
+
const result = compileSource(source, file, diskReader(dirname(file)));
|
|
450
517
|
printDiagnosticsFor(file, result.diagnostics);
|
|
451
518
|
if (hasErrors(result.diagnostics))
|
|
452
519
|
anyErrors = true;
|
|
@@ -567,8 +634,12 @@ slice
|
|
|
567
634
|
process.exit(1);
|
|
568
635
|
}
|
|
569
636
|
const key = sliceDocKey(name);
|
|
570
|
-
|
|
571
|
-
|
|
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`);
|
|
572
643
|
if (existsSync(path) && !opts.force) {
|
|
573
644
|
console.error(`refusing to overwrite ${path} (use --force)`);
|
|
574
645
|
process.exit(1);
|
|
@@ -588,12 +659,25 @@ slice
|
|
|
588
659
|
console.error(`em slice new --wire: not wiring — fix the errors in ${opts.wire} above`);
|
|
589
660
|
process.exit(1);
|
|
590
661
|
}
|
|
591
|
-
|
|
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);
|
|
592
676
|
if (!result.ok) {
|
|
593
677
|
console.error(`em slice new --wire: ${result.message}`);
|
|
594
678
|
process.exit(1);
|
|
595
679
|
}
|
|
596
|
-
wired = result;
|
|
680
|
+
wired = { ...result, target: wireTarget };
|
|
597
681
|
regions = buildSliceRegions(model, [refs.sliceKeys.indexOf(key)]);
|
|
598
682
|
const sliceEvents = model.slices[refs.sliceKeys.indexOf(key)].elements.filter((e) => e.kind === "event");
|
|
599
683
|
alternatives = sliceEvents.filter((e) => e.alternativeGroup !== undefined).map((e) => e.name);
|
|
@@ -605,8 +689,8 @@ slice
|
|
|
605
689
|
writeFileSync(path, content);
|
|
606
690
|
console.log(`wrote ${path}`);
|
|
607
691
|
if (wired) {
|
|
608
|
-
writeFileSync(
|
|
609
|
-
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})`);
|
|
610
694
|
}
|
|
611
695
|
else {
|
|
612
696
|
console.log(`add this to the slice's primary element in the .em file:`);
|
|
@@ -710,6 +794,7 @@ slice
|
|
|
710
794
|
"facts (key, pattern, doc status/implementedIn) — the hand-maintained table is deprecated")
|
|
711
795
|
.argument("<file>", "input .em file")
|
|
712
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")
|
|
713
798
|
.action((file, opts) => {
|
|
714
799
|
const { model, diagnostics, refs } = compileFile(file);
|
|
715
800
|
printDiagnostics(diagnostics);
|
|
@@ -717,7 +802,7 @@ slice
|
|
|
717
802
|
console.error("not indexing: fix the errors above");
|
|
718
803
|
process.exit(1);
|
|
719
804
|
}
|
|
720
|
-
const result = runSliceIndex(model, refs, file, !!opts.check);
|
|
805
|
+
const result = runSliceIndex(model, refs, file, !!opts.check, { full: !!opts.full });
|
|
721
806
|
printDiagnostics(newDiagnostics(result.table.diagnostics, diagnostics));
|
|
722
807
|
if (!result.ok) {
|
|
723
808
|
console.error(result.message);
|
|
@@ -729,6 +814,10 @@ slice
|
|
|
729
814
|
else {
|
|
730
815
|
console.log(`ok — ${result.readmePath}'s Slices table is already up to date`);
|
|
731
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
|
+
}
|
|
732
821
|
});
|
|
733
822
|
slice
|
|
734
823
|
.command("mark-implemented")
|
|
@@ -1088,7 +1177,10 @@ slice
|
|
|
1088
1177
|
"later re-certification is normal — there's no way for em to tell 'later' from 'earlier' " +
|
|
1089
1178
|
"for an arbitrary revision string, so there's no --force to reach for). Refuses when the " +
|
|
1090
1179
|
"conformance/<date>[-<name>]-findings.json file matching --at still has an unruled finding in scope " +
|
|
1091
|
-
"for this slice; --skip-findings-check overrides with a loud notice."
|
|
1180
|
+
"for this slice; --skip-findings-check overrides with a loud notice. GH #400: --preview runs the " +
|
|
1181
|
+
"same doc-resolution and findings-check read for a slice that hasn't shipped yet — the shipped/" +
|
|
1182
|
+
"implementedIn gates are waived and NOTHING is ever written to the doc; output is ephemeral " +
|
|
1183
|
+
"(text or --json) unless --write-findings opts into persisting a normal findings file.")
|
|
1092
1184
|
.argument("<file>", "input .em file")
|
|
1093
1185
|
.argument("<slice-key>", "slice export key (kebab-case)")
|
|
1094
1186
|
.requiredOption("--at <rev>", "the target-repo revision this certification sweep diffed against — or, when what was exercised is not a commit " +
|
|
@@ -1096,12 +1188,31 @@ slice
|
|
|
1096
1188
|
"`<kind>:<id>` such as build:1234 or deploy:eu-1-2026-10-15 (GH #261); recorded verbatim, skipped by git-reachability checks")
|
|
1097
1189
|
.option("--on <date>", "certification date, YYYY-MM-DD (default: today, local date)")
|
|
1098
1190
|
.option("--skip-findings-check", "certify even with unruled findings in scope — prints a loud notice on stderr")
|
|
1191
|
+
.option("--preview", "GH #400: pre-ship advisory mode — waives the shipped-status/`implementedIn:` gates, writes NOTHING to the " +
|
|
1192
|
+
"doc (no conformedVersion/conformedAt/conformedOn, ever); ephemeral by default, --write-findings opts " +
|
|
1193
|
+
"into persisting a normal findings file. --skip-findings-check has no effect in this mode (preview " +
|
|
1194
|
+
"never refuses on findings)")
|
|
1195
|
+
.option("--write-findings", "with --preview: persist the preview's own unruled findings as conformance/<on>-<slice-key>-preview-findings.json")
|
|
1196
|
+
.option("--json", "with --preview: print a JSON document instead of the text report")
|
|
1099
1197
|
.action((file, sliceKey, opts) => {
|
|
1198
|
+
if (opts.writeFindings === true && opts.preview !== true) {
|
|
1199
|
+
console.error("em slice conform: --write-findings only applies with --preview");
|
|
1200
|
+
process.exit(1);
|
|
1201
|
+
}
|
|
1202
|
+
if (opts.json === true && opts.preview !== true) {
|
|
1203
|
+
console.error("em slice conform: --json only applies with --preview");
|
|
1204
|
+
process.exit(1);
|
|
1205
|
+
}
|
|
1206
|
+
if (opts.preview === true && opts.skipFindingsCheck === true) {
|
|
1207
|
+
console.error("em slice conform: --skip-findings-check has no effect with --preview (preview never blocks on findings)");
|
|
1208
|
+
process.exit(1);
|
|
1209
|
+
}
|
|
1100
1210
|
const { model, refs, diagnostics } = compileFile(file);
|
|
1101
1211
|
printDiagnostics(diagnostics);
|
|
1102
1212
|
sliceKey = resolveSliceOrExit("em slice conform", model, refs, sliceKey);
|
|
1103
1213
|
// Scoped the same way `em slice ratify`/`em slice revise`/`em slice mark-implemented` are:
|
|
1104
|
-
// only an error concerning THIS slice refuses.
|
|
1214
|
+
// only an error concerning THIS slice refuses. Not waived by --preview — an erroring model
|
|
1215
|
+
// isn't a fact worth previewing against either.
|
|
1105
1216
|
const scopedErrors = diagnostics.filter((d) => d.severity === "error" && d.refs?.some((r) => r === sliceKey || r.startsWith(`${sliceKey}/`)));
|
|
1106
1217
|
if (scopedErrors.length > 0) {
|
|
1107
1218
|
console.error(`em slice conform: slice "${sliceKey}" has errors — fix them first`);
|
|
@@ -1113,6 +1224,44 @@ slice
|
|
|
1113
1224
|
console.error(`em slice conform: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
|
|
1114
1225
|
process.exit(1);
|
|
1115
1226
|
}
|
|
1227
|
+
if (opts.preview === true) {
|
|
1228
|
+
const baseDir = dirname(file);
|
|
1229
|
+
const result = runSliceConformPreview(model, refs, baseDir, sliceKey, opts.at);
|
|
1230
|
+
if (!result.ok) {
|
|
1231
|
+
if (opts.json) {
|
|
1232
|
+
console.log(JSON.stringify({ ok: false, message: result.message }, null, 2));
|
|
1233
|
+
}
|
|
1234
|
+
else {
|
|
1235
|
+
console.error(`em slice conform --preview: ${result.message}`);
|
|
1236
|
+
}
|
|
1237
|
+
process.exit(1);
|
|
1238
|
+
}
|
|
1239
|
+
const writtenTo = opts.writeFindings === true ? writePreviewFindings(baseDir, file, sliceKey, result.at, on, result.unruledFindings) : null;
|
|
1240
|
+
if (opts.json) {
|
|
1241
|
+
console.log(JSON.stringify({
|
|
1242
|
+
ok: true,
|
|
1243
|
+
preview: true,
|
|
1244
|
+
slice: sliceKey,
|
|
1245
|
+
path: result.path,
|
|
1246
|
+
status: result.status,
|
|
1247
|
+
version: result.version,
|
|
1248
|
+
at: result.at,
|
|
1249
|
+
findingsFiles: result.findingsFiles,
|
|
1250
|
+
unruledFindings: result.unruledFindings,
|
|
1251
|
+
writtenTo,
|
|
1252
|
+
}, null, 2));
|
|
1253
|
+
}
|
|
1254
|
+
else {
|
|
1255
|
+
const findingsNote = result.unruledFindings.length === 0
|
|
1256
|
+
? result.findingsFiles.length === 0
|
|
1257
|
+
? `no conformance findings file matches --at ${result.at} (conformance/<date>[-<name>]-findings.json with that revision)`
|
|
1258
|
+
: `no unruled conformance findings in scope (checked: ${result.findingsFiles.join(", ")})`
|
|
1259
|
+
: `${result.unruledFindings.length} unruled conformance finding(s) in scope (id ${result.unruledFindings.map((f) => f.id).join(", ")}) in ${result.findingsFiles.join(", ")}`;
|
|
1260
|
+
console.log(`preview: ${result.path} (status: ${result.status ?? "(empty)"}) @ ${result.at} — ${findingsNote}`);
|
|
1261
|
+
console.log(writtenTo !== null ? `wrote: ${writtenTo}` : "ephemeral — nothing written; pass --write-findings to persist a snapshot");
|
|
1262
|
+
}
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1116
1265
|
const result = runSliceConform(model, refs, dirname(file), sliceKey, opts.at, on, opts.skipFindingsCheck === true);
|
|
1117
1266
|
if (!result.ok) {
|
|
1118
1267
|
console.error(`em slice conform: ${result.message}`);
|
|
@@ -1356,8 +1505,8 @@ state
|
|
|
1356
1505
|
process.exit(1);
|
|
1357
1506
|
}
|
|
1358
1507
|
const phases = sortUsagePhases(rawPhases);
|
|
1359
|
-
const { model, diagnostics, refs,
|
|
1360
|
-
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics,
|
|
1508
|
+
const { model, diagnostics, refs, hashSource } = compileFile(file);
|
|
1509
|
+
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics, hashSource);
|
|
1361
1510
|
const categorySet = new Set(allDiagnostics.map((d) => RULES[d.code].usageCategory));
|
|
1362
1511
|
const categories = categorySet.size > 0 ? [...categorySet].sort() : ["none"];
|
|
1363
1512
|
const loaded = loadStateFile(dirname(file));
|
|
@@ -1400,13 +1549,13 @@ modelVersionCmd
|
|
|
1400
1549
|
}
|
|
1401
1550
|
const today = localIsoDate();
|
|
1402
1551
|
const on = opts.on ?? today;
|
|
1403
|
-
const { model: compiledModel, refs, diagnostics,
|
|
1552
|
+
const { model: compiledModel, refs, diagnostics, hashSource } = compileFile(file);
|
|
1404
1553
|
printDiagnostics(diagnostics);
|
|
1405
1554
|
if (hasErrors(diagnostics)) {
|
|
1406
1555
|
console.error(`em model version bump: "${file}" has errors — fix them first`);
|
|
1407
1556
|
process.exit(1);
|
|
1408
1557
|
}
|
|
1409
|
-
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);
|
|
1410
1559
|
if (!result.ok) {
|
|
1411
1560
|
console.error(`em model version bump: ${result.message}`);
|
|
1412
1561
|
process.exit(1);
|
|
@@ -1638,10 +1787,28 @@ program
|
|
|
1638
1787
|
.option("--keep-empty-lanes", "keep the API lane even when empty")
|
|
1639
1788
|
.option("--serve", "serve a live viewer with instant push-reload (no polling)")
|
|
1640
1789
|
.option("--port <n>", "port for --serve (default 5173)", (v) => parseInt(v, 10))
|
|
1790
|
+
.option("--storyline <name>", "GH #309 (deferred from #277): filter the live view to one storyline's subset — the same " +
|
|
1791
|
+
"subset `em render --storyline` draws (its own slices in order, plus dimmed stub columns " +
|
|
1792
|
+
"for what they read from outside it and the `after` end state). Whenever the model " +
|
|
1793
|
+
"declares any storylines the viewer also offers a picker to switch to the full model or " +
|
|
1794
|
+
"another declared storyline, live, without restarting. An unknown name errors, listing " +
|
|
1795
|
+
"the declared storylines; requires --serve")
|
|
1641
1796
|
.action(async (file, opts) => {
|
|
1642
1797
|
const out = opts.out ?? defaultOut(file, opts.format ?? "svg");
|
|
1643
1798
|
const fmt = opts.format ?? formatFromPath(out);
|
|
1799
|
+
if (opts.storyline !== undefined && !opts.serve) {
|
|
1800
|
+
console.error("em watch: --storyline applies only with --serve");
|
|
1801
|
+
process.exit(1);
|
|
1802
|
+
}
|
|
1644
1803
|
let server;
|
|
1804
|
+
// The file the printed live-view URL points at — basename(out) (the full model) unless
|
|
1805
|
+
// --storyline resolves, in which case build() below points it at that storyline's own
|
|
1806
|
+
// subset file instead (GH #309).
|
|
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;
|
|
1645
1812
|
// serializeBuilds: chokidar can fire several events for one save (and an
|
|
1646
1813
|
// editor's write can land while a slow png/pdf render is still in flight) —
|
|
1647
1814
|
// overlapping builds would race each other writing the same output file.
|
|
@@ -1662,7 +1829,52 @@ program
|
|
|
1662
1829
|
server?.notifyError(first ? formatDiagnostic(first).trim() : "model has errors — see terminal");
|
|
1663
1830
|
return;
|
|
1664
1831
|
}
|
|
1665
|
-
|
|
1832
|
+
// GH #309: --storyline is validated on every build, same as `em render --storyline`
|
|
1833
|
+
// (resolveWatchStoryline shares findStoryline's resolution) — a save that renames or
|
|
1834
|
+
// drops the storyline must not kill the watcher.
|
|
1835
|
+
const resolved = resolveWatchStoryline(model, opts.storyline);
|
|
1836
|
+
if ("error" in resolved) {
|
|
1837
|
+
console.error(resolved.error);
|
|
1838
|
+
server?.notifyError(resolved.error);
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
if (opts.serve && fmt === "svg") {
|
|
1842
|
+
const baseDir = dirname(file);
|
|
1843
|
+
const outDir = dirname(out);
|
|
1844
|
+
const outFile = basename(out);
|
|
1845
|
+
liveFile = "storyline" in resolved ? storylineFileFor(resolved.storyline.name) : outFile;
|
|
1846
|
+
// The full model, always rendered to `out` (unchanged default) — carries the
|
|
1847
|
+
// storyline-picker metadata (a no-op when the model declares none) so the viewer can
|
|
1848
|
+
// offer it regardless of whether --storyline was passed.
|
|
1849
|
+
const fullRaw = await layoutDot(dot);
|
|
1850
|
+
const fullSvg = embedStorylinePicker(composeSvg(fullRaw, model, grid, baseDir, outDir), storylineVariants(model, outFile, liveFile));
|
|
1851
|
+
await writeRendered(fullSvg, out, "svg");
|
|
1852
|
+
// Every declared storyline's own subset (GH #277's `em render --storyline` subset,
|
|
1853
|
+
// reused verbatim) — rendered alongside the full model on every build, independent of
|
|
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 = [];
|
|
1858
|
+
for (const st of model.storylines ?? []) {
|
|
1859
|
+
const sub = buildStorylineDiagram(model, st, { keepEmptyLanes: opts.keepEmptyLanes });
|
|
1860
|
+
const subFile = storylineFileFor(st.name);
|
|
1861
|
+
const subRaw = await layoutDot(sub.dot);
|
|
1862
|
+
const subSvg = embedStorylinePicker(composeSvg(subRaw, sub.model, sub.grid, baseDir, outDir), storylineVariants(model, outFile, subFile));
|
|
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
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
hasStorylines = frames.length > 0;
|
|
1873
|
+
canvas = hasStorylines ? buildStorylineMapHtml(model, frames, storylineMap(model), { live: true }) : undefined;
|
|
1874
|
+
}
|
|
1875
|
+
else {
|
|
1876
|
+
await renderDot(dot, model, grid, out, fmt, dirname(file));
|
|
1877
|
+
}
|
|
1666
1878
|
console.log(`rendered ${out} (${Date.now() - started}ms)`);
|
|
1667
1879
|
server?.notify();
|
|
1668
1880
|
}
|
|
@@ -1676,11 +1888,16 @@ program
|
|
|
1676
1888
|
try {
|
|
1677
1889
|
// Serve the directory the SVG is written to — that's what the browser
|
|
1678
1890
|
// fetches, and note "..." links inside the SVG resolve relative to it.
|
|
1679
|
-
server = await startLiveServer({ dir: dirname(resolve(out)), port: opts.port });
|
|
1891
|
+
server = await startLiveServer({ dir: dirname(resolve(out)), port: opts.port, canvas: () => canvas });
|
|
1680
1892
|
// The viewer inlines SVG only — for png/pdf watches there's nothing
|
|
1681
1893
|
// live to embed, so don't print a ?svg= URL that can't work.
|
|
1682
1894
|
if (fmt === "svg") {
|
|
1683
|
-
|
|
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
|
+
}
|
|
1684
1901
|
console.log(" open it in a browser and share your screen");
|
|
1685
1902
|
}
|
|
1686
1903
|
else {
|
|
@@ -1697,7 +1914,20 @@ program
|
|
|
1697
1914
|
// the .em file. Watching the directory (not a glob — chokidar 4 has none)
|
|
1698
1915
|
// still fires for docs authored after watch starts; the filter keeps it to .md.
|
|
1699
1916
|
const slicesDir = join(dirname(file), "slices");
|
|
1700
|
-
|
|
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"));
|
|
1701
1931
|
console.log(`watching ${file} … (ctrl-c to stop)`);
|
|
1702
1932
|
if (!opts.serve)
|
|
1703
1933
|
console.log("tip: add --serve for the live browser view");
|
|
@@ -1756,9 +1986,9 @@ program
|
|
|
1756
1986
|
console.error("em validate: --shape cannot be combined with --slice-ready, --list-*, --fail-on-issues or --check-signed-bodies");
|
|
1757
1987
|
process.exit(1);
|
|
1758
1988
|
}
|
|
1759
|
-
const { model, diagnostics, refs, source } = compileFile(file);
|
|
1989
|
+
const { model, diagnostics, refs, source, hashSource } = compileFile(file);
|
|
1760
1990
|
if (opts.shape) {
|
|
1761
|
-
const all = computeAllDiagnostics(file, model, refs, diagnostics,
|
|
1991
|
+
const all = computeAllDiagnostics(file, model, refs, diagnostics, hashSource);
|
|
1762
1992
|
const report = buildShapeReport(all, opts.waiveFile ? readWaiveFile(opts.waiveFile) : []);
|
|
1763
1993
|
const modelErrors = all.filter((d) => d.severity === "error").length;
|
|
1764
1994
|
for (const k of report.staleWaivers)
|
|
@@ -1772,7 +2002,7 @@ program
|
|
|
1772
2002
|
// GH #257: an opt-in addition to the unconditional set, so --json, the text report and the
|
|
1773
2003
|
// exit code all see it the same way every other diagnostic is seen.
|
|
1774
2004
|
const allDiagnostics = [
|
|
1775
|
-
...computeAllDiagnostics(file, model, refs, diagnostics,
|
|
2005
|
+
...computeAllDiagnostics(file, model, refs, diagnostics, hashSource),
|
|
1776
2006
|
...(opts.checkSignedBodies ? validateSignedBodies(model, refs, dirname(file)) : []),
|
|
1777
2007
|
];
|
|
1778
2008
|
// GH #293: --strict promotes advisory lints to errors (the --slice-ready branch below does
|
|
@@ -1938,6 +2168,10 @@ program
|
|
|
1938
2168
|
.requiredOption("--reachable-from <rev>", "the branch or commit every ref must be an ancestor of (e.g. origin/main)")
|
|
1939
2169
|
.option("--waive <slice-key>", "excuse this slice's non-ancestor/unresolved/undecidable results from the exit code (repeatable); still reported", (value, previous) => [...previous, value], [])
|
|
1940
2170
|
.option("--waive-file <path>", "a baseline file of slice keys to waive, one per line (`#` comments allowed); missing file = no waivers")
|
|
2171
|
+
.option("--check-order", "advisory-only (GH #399): for every slice doc with a ratifiedRef, report whether a commit between the branch's " +
|
|
2172
|
+
"fork point and ratifiedRef already touched code other than the doc itself — a retrofit signal for inline " +
|
|
2173
|
+
"ratification (spec written to describe code that already exists). History is rewritable, so this NEVER " +
|
|
2174
|
+
"affects the exit code or `ok`, and is never a validate gate — see docs/process.md's inline-ratification section")
|
|
1941
2175
|
.option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
|
|
1942
2176
|
.action((file, opts) => {
|
|
1943
2177
|
const { model, refs } = compileFile(file);
|
|
@@ -1953,8 +2187,11 @@ program
|
|
|
1953
2187
|
}
|
|
1954
2188
|
if (result.shallow)
|
|
1955
2189
|
console.error("warn: shallow clone — every ref is undecidable; check out with fetch-depth: 0");
|
|
2190
|
+
// GH #399: computed only with --check-order, and never folded into `result`/`result.ok` —
|
|
2191
|
+
// it is a sibling report, not a new ref field, and must never move the exit code (R5).
|
|
2192
|
+
const orderResults = opts.checkOrder ? checkRatificationOrder(model, refs, dirname(file), result.target, result.shallow) : null;
|
|
1956
2193
|
if (opts.json) {
|
|
1957
|
-
process.stdout.write(buildRefsJson(result, file) + "\n");
|
|
2194
|
+
process.stdout.write(buildRefsJson(result, file, orderResults) + "\n");
|
|
1958
2195
|
}
|
|
1959
2196
|
else {
|
|
1960
2197
|
for (const r of result.results) {
|
|
@@ -1971,8 +2208,19 @@ program
|
|
|
1971
2208
|
else {
|
|
1972
2209
|
console.log(`${bad} ref(s) not reachable from ${opts.reachableFrom} (${result.checkedRefs} checked on ${result.checkedDocs} doc(s)${waivedNote})`);
|
|
1973
2210
|
}
|
|
2211
|
+
if (orderResults) {
|
|
2212
|
+
console.log("");
|
|
2213
|
+
console.log("ratification order (advisory — never affects exit code):");
|
|
2214
|
+
if (orderResults.length === 0) {
|
|
2215
|
+
console.log(" no slice doc carries a ratifiedRef to check");
|
|
2216
|
+
}
|
|
2217
|
+
for (const o of orderResults) {
|
|
2218
|
+
console.log(` ${o.status.padEnd(23)} ${o.sliceKey} ratifiedRef: ${o.ratifiedRef}${o.detail ? ` — ${o.detail}` : ""}`);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
1974
2221
|
}
|
|
1975
2222
|
// Set the code rather than process.exit(): stdout to a pipe shouldn't risk truncation.
|
|
2223
|
+
// `orderResults` never participates — advisory only, per R5.
|
|
1976
2224
|
if (!result.ok)
|
|
1977
2225
|
process.exitCode = 1;
|
|
1978
2226
|
});
|
|
@@ -2156,11 +2404,11 @@ program
|
|
|
2156
2404
|
const compiled = [];
|
|
2157
2405
|
let anyErrors = false;
|
|
2158
2406
|
for (const file of files) {
|
|
2159
|
-
const { model, refs, diagnostics, source } = compileFile(file);
|
|
2407
|
+
const { model, refs, diagnostics, source, hashSource } = compileFile(file);
|
|
2160
2408
|
printDiagnosticsFor(file, diagnostics);
|
|
2161
2409
|
if (hasErrors(diagnostics))
|
|
2162
2410
|
anyErrors = true;
|
|
2163
|
-
compiled.push({ file, model, refs, source });
|
|
2411
|
+
compiled.push({ file, model, refs, source, hashSource });
|
|
2164
2412
|
}
|
|
2165
2413
|
if (anyErrors) {
|
|
2166
2414
|
console.error("em status: not reporting status — fix the errors above");
|
|
@@ -2218,7 +2466,7 @@ program
|
|
|
2218
2466
|
return resolveConformanceEntry(file, opts.repo, sliceDocFacts);
|
|
2219
2467
|
});
|
|
2220
2468
|
// MIL-218: one model-version entry per input file.
|
|
2221
|
-
const modelVersion = compiled.map(({ file, model, refs,
|
|
2469
|
+
const modelVersion = compiled.map(({ file, model, refs, hashSource }) => resolveModelVersionStatusEntry(file, model, refs, hashSource));
|
|
2222
2470
|
// MIL-219: one em-version entry per input file whose state file resolved.
|
|
2223
2471
|
const emVersion = compiled
|
|
2224
2472
|
.map(({ file }) => resolveEmVersionStatusEntry(file, PKG_VERSION))
|
|
@@ -2526,8 +2774,8 @@ function resolveSliceOrExit(command, model, refs, input) {
|
|
|
2526
2774
|
/** Compile `file` with the full `em validate` diagnostic set — what `engagement plan` needs for
|
|
2527
2775
|
* each slice's `--slice-ready` verdict (R34). */
|
|
2528
2776
|
function engagementModelInput(file) {
|
|
2529
|
-
const { model, refs, diagnostics, source } = compileFile(file);
|
|
2530
|
-
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);
|
|
2531
2779
|
return { file, model, refs, baseDir: dirname(file), contract: { file, source, diagnostics }, allDiagnostics };
|
|
2532
2780
|
}
|
|
2533
2781
|
engagement
|
|
@@ -2860,17 +3108,24 @@ const scopeCommand = systemCommand
|
|
|
2860
3108
|
function packagedSkillsRoot() {
|
|
2861
3109
|
return join(dirname(fileURLToPath(import.meta.url)), "..", ".claude", "skills");
|
|
2862
3110
|
}
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
// 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
|
|
2867
3115
|
// with the consumer's own agents - install/sync/check only ever handle the fixed EM_AGENT_FILES.
|
|
2868
3116
|
function packagedAgentsRoot() {
|
|
2869
3117
|
return join(dirname(fileURLToPath(import.meta.url)), "..", ".claude", "agents");
|
|
2870
3118
|
}
|
|
2871
|
-
function
|
|
2872
|
-
|
|
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;
|
|
2873
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)";
|
|
2874
3129
|
program
|
|
2875
3130
|
.command("contract")
|
|
2876
3131
|
.description("print the packaged implementation contract (reference/implement.md) to stdout — the " +
|
|
@@ -2912,9 +3167,8 @@ const skill = program
|
|
|
2912
3167
|
.description("manage Claude Code skills bundled with em");
|
|
2913
3168
|
// MIL-269: reconcile the four `.claude/agents/em-*.md` files (create the directory if absent);
|
|
2914
3169
|
// byte-exact restore, never touches any other file there.
|
|
2915
|
-
function installAgentFiles(
|
|
3170
|
+
function installAgentFiles(vendored) {
|
|
2916
3171
|
const packaged = packagedAgentsRoot();
|
|
2917
|
-
const vendored = vendoredAgentsRoot(repoRoot);
|
|
2918
3172
|
const plan = planAgentSync(packaged, vendored, EM_AGENT_FILES);
|
|
2919
3173
|
if (plan.changes.length === 0)
|
|
2920
3174
|
return;
|
|
@@ -2934,10 +3188,13 @@ skill
|
|
|
2934
3188
|
"bundle's own managed directories the same way `em skill sync` does — never touching an " +
|
|
2935
3189
|
"unrelated sibling skill (MIL-180)")
|
|
2936
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)
|
|
2937
3193
|
.action(async (opts) => {
|
|
2938
3194
|
warnVendoredDeprecated();
|
|
2939
3195
|
const srcRoot = packagedSkillsRoot();
|
|
2940
|
-
const
|
|
3196
|
+
const dirs = vendoredDirs(process.cwd(), opts);
|
|
3197
|
+
const destRoot = dirs.skillsRoot;
|
|
2941
3198
|
const anchor = join(destRoot, EM_SKILL_ANCHOR_DIR);
|
|
2942
3199
|
if (existsSync(anchor) && !opts.force) {
|
|
2943
3200
|
console.log(`skill already installed at ${anchor}`);
|
|
@@ -2967,7 +3224,7 @@ skill
|
|
|
2967
3224
|
`(${removedCount} removed), ${totalUnchanged} unchanged`);
|
|
2968
3225
|
}
|
|
2969
3226
|
console.log("in Claude Code, run /event-modeling to start a guided session");
|
|
2970
|
-
installAgentFiles(
|
|
3227
|
+
installAgentFiles(dirs.agentsRoot);
|
|
2971
3228
|
}
|
|
2972
3229
|
else {
|
|
2973
3230
|
await mkdir(destRoot, { recursive: true });
|
|
@@ -2976,7 +3233,7 @@ skill
|
|
|
2976
3233
|
}
|
|
2977
3234
|
console.log(`installed event-modeling skill bundle → ${destRoot}`);
|
|
2978
3235
|
console.log("in Claude Code, run /event-modeling to start a guided session");
|
|
2979
|
-
installAgentFiles(
|
|
3236
|
+
installAgentFiles(dirs.agentsRoot);
|
|
2980
3237
|
}
|
|
2981
3238
|
if (opts.agentsMd !== false) {
|
|
2982
3239
|
console.log(agentsMdMessage(syncAgentsMd(process.cwd())));
|
|
@@ -2988,15 +3245,18 @@ skill
|
|
|
2988
3245
|
"installed em package (overwrites unconditionally; local edits are never merged, MIL-93)")
|
|
2989
3246
|
.argument("[path]", "consumer repo root", ".")
|
|
2990
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)
|
|
2991
3250
|
.action(async (path, opts) => {
|
|
2992
3251
|
warnVendoredDeprecated();
|
|
2993
3252
|
const packagedRoot = packagedSkillsRoot();
|
|
2994
|
-
const
|
|
3253
|
+
const dirs = vendoredDirs(path, opts);
|
|
3254
|
+
const vendoredRoot = dirs.skillsRoot;
|
|
2995
3255
|
const bundlePlan = planSkillSyncBundle(packagedRoot, vendoredRoot, EM_ALL_SKILL_BUNDLE_DIRS);
|
|
2996
3256
|
const skillChanges = bundlePlan.reduce((n, { plan }) => n + plan.changes.length, 0);
|
|
2997
3257
|
// MIL-269 (R32): the fixed agent-file list, never the shared .claude/agents/ directory as a whole.
|
|
2998
3258
|
const packagedAgents = packagedAgentsRoot();
|
|
2999
|
-
const vendoredAgents =
|
|
3259
|
+
const vendoredAgents = dirs.agentsRoot;
|
|
3000
3260
|
const agentPlan = planAgentSync(packagedAgents, vendoredAgents, EM_AGENT_FILES);
|
|
3001
3261
|
const totalChanges = skillChanges + agentPlan.changes.length;
|
|
3002
3262
|
const totalUnchanged = bundlePlan.reduce((n, { plan }) => n + plan.unchangedCount, 0) + agentPlan.unchangedCount;
|
|
@@ -3027,9 +3287,13 @@ skill
|
|
|
3027
3287
|
.option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
|
|
3028
3288
|
.option("--ci", "also fail (exit 1) when the declared plugin is not registered on this machine (implied by CI=true in the environment); " +
|
|
3029
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)
|
|
3030
3292
|
.action((path, opts) => {
|
|
3031
3293
|
const packagedRoot = packagedSkillsRoot();
|
|
3032
|
-
|
|
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;
|
|
3033
3297
|
const ciMode = !!opts.ci || process.env.CI === "true";
|
|
3034
3298
|
// MIL-231: the plugin signal (a pinned marketplace in .claude/settings*.json) sits beside the
|
|
3035
3299
|
// vendored signal (the anchor directory). Plugin only -> only the plugin is checked; vendored
|
|
@@ -3042,7 +3306,7 @@ skill
|
|
|
3042
3306
|
// MIL-269 (R32): the four agent files, only when the vendored bundle is present (a plugin-only
|
|
3043
3307
|
// repo ships them via the plugin); checked by fixed name, never the shared directory.
|
|
3044
3308
|
const agents = vendoredPresent
|
|
3045
|
-
? checkAgentFiles(packagedAgentsRoot(),
|
|
3309
|
+
? checkAgentFiles(packagedAgentsRoot(), dirs.agentsRoot, EM_AGENT_FILES)
|
|
3046
3310
|
: { findings: [], ok: true };
|
|
3047
3311
|
const findings = [...vendored.findings, ...agents.findings, ...(pluginCheck?.findings ?? [])];
|
|
3048
3312
|
const failing = findings.filter((f) => f.code !== "plugin-not-installed-locally" || ciMode);
|
|
@@ -3219,7 +3483,74 @@ ci.command("init")
|
|
|
3219
3483
|
console.log(`${path} already up to date`);
|
|
3220
3484
|
}
|
|
3221
3485
|
});
|
|
3222
|
-
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
|
+
});
|
|
3223
3554
|
storylineCmd
|
|
3224
3555
|
.command("suggest")
|
|
3225
3556
|
.description("advisory (GH #290): propose `storyline` blocks for a model that has none — one per distinct slice " +
|
|
@@ -3362,8 +3693,8 @@ program
|
|
|
3362
3693
|
* stands, so an estate sees how many advisory shape findings it carries. Never writes a waive file:
|
|
3363
3694
|
* baselining shapes you accept is a decision (`--waive-file`, and a `shape-accepted:` Decisions-log bullet). */
|
|
3364
3695
|
function printUpgradeShapeSection(file) {
|
|
3365
|
-
const { model, diagnostics, refs,
|
|
3366
|
-
const all = computeAllDiagnostics(file, model, refs, diagnostics,
|
|
3696
|
+
const { model, diagnostics, refs, hashSource } = compileFile(file);
|
|
3697
|
+
const all = computeAllDiagnostics(file, model, refs, diagnostics, hashSource);
|
|
3367
3698
|
const report = buildShapeReport(all, []);
|
|
3368
3699
|
console.log("");
|
|
3369
3700
|
console.log("Shape (em validate --shape, advisory):");
|
|
@@ -3389,18 +3720,17 @@ function compileFile(file, opts = {},
|
|
|
3389
3720
|
// watcher (and the --serve live view with it) is a session-ending failure.
|
|
3390
3721
|
// One-shot commands keep the exit-with-message behavior.
|
|
3391
3722
|
{ survive = false } = {}) {
|
|
3392
|
-
|
|
3393
|
-
try {
|
|
3394
|
-
source = readFileSync(file, "utf8");
|
|
3395
|
-
}
|
|
3396
|
-
catch {
|
|
3723
|
+
if (!existsSync(file)) {
|
|
3397
3724
|
if (survive)
|
|
3398
3725
|
throw new Error(`cannot read ${file}`);
|
|
3399
3726
|
console.error(`cannot read ${file}`);
|
|
3400
3727
|
process.exit(1);
|
|
3401
3728
|
}
|
|
3402
3729
|
try {
|
|
3403
|
-
|
|
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);
|
|
3404
3734
|
}
|
|
3405
3735
|
catch (e) {
|
|
3406
3736
|
if (e instanceof ParseError) {
|
|
@@ -3409,6 +3739,12 @@ function compileFile(file, opts = {},
|
|
|
3409
3739
|
console.error(`parse error in ${file} ${e.message}`);
|
|
3410
3740
|
process.exit(1);
|
|
3411
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
|
+
}
|
|
3412
3748
|
throw e;
|
|
3413
3749
|
}
|
|
3414
3750
|
}
|
|
@@ -3424,7 +3760,8 @@ function compileFile(file, opts = {},
|
|
|
3424
3760
|
* report, since its whole job is logging which categories fired. `source` is the `.em` file's
|
|
3425
3761
|
* own text — only `validateModelVersionStale` reads it (the modelHash comparison). */
|
|
3426
3762
|
function computeAllDiagnostics(file, model, refs, diagnostics, source) {
|
|
3427
|
-
|
|
3763
|
+
// GH #421: the fs-aware rules below point at elements too — give each its storyline file.
|
|
3764
|
+
return attributeDiagnosticFiles(model, refs, [
|
|
3428
3765
|
...diagnostics,
|
|
3429
3766
|
...validateLineage(model, refs, dirname(file)),
|
|
3430
3767
|
...validateFrontmatterCoherence(model, refs, dirname(file)),
|
|
@@ -3433,7 +3770,7 @@ function computeAllDiagnostics(file, model, refs, diagnostics, source) {
|
|
|
3433
3770
|
...validateOrphanedSliceDocs(model, refs, dirname(file)),
|
|
3434
3771
|
...validateModelVersionStale(model, refs, dirname(file), source),
|
|
3435
3772
|
...validateInvariants(model, refs, dirname(file)),
|
|
3436
|
-
];
|
|
3773
|
+
]);
|
|
3437
3774
|
}
|
|
3438
3775
|
/** Read a file's text, or exit with a clear error — shared by `em diff` (both forms) and
|
|
3439
3776
|
* `em glossary`, which each read one or more plain `.em` files off disk before compiling. */
|
|
@@ -3447,18 +3784,31 @@ function readFileOrExit(file) {
|
|
|
3447
3784
|
}
|
|
3448
3785
|
}
|
|
3449
3786
|
/** Parse+normalize source text for `em diff`, exiting with a labeled parse error on failure. */
|
|
3450
|
-
function compileSource(source, label) {
|
|
3787
|
+
function compileSource(source, label, read) {
|
|
3451
3788
|
try {
|
|
3452
|
-
|
|
3789
|
+
// GH #421: a split model's storyline files come through `read` (disk, or git at a revision).
|
|
3790
|
+
return compileModelText(source, read, label);
|
|
3453
3791
|
}
|
|
3454
3792
|
catch (e) {
|
|
3455
3793
|
if (e instanceof ParseError) {
|
|
3456
3794
|
console.error(`parse error in ${label} ${e.message}`);
|
|
3457
3795
|
process.exit(1);
|
|
3458
3796
|
}
|
|
3797
|
+
if (e instanceof ModelLoadError) {
|
|
3798
|
+
console.error(e.message);
|
|
3799
|
+
process.exit(1);
|
|
3800
|
+
}
|
|
3459
3801
|
throw e;
|
|
3460
3802
|
}
|
|
3461
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
|
+
}
|
|
3462
3812
|
/** Resolve `file`'s content at a git revision, exiting with a clear error on any git failure. */
|
|
3463
3813
|
function readAtRevision(file, rev) {
|
|
3464
3814
|
const result = resolveRevision(file, rev);
|
|
@@ -3469,9 +3819,11 @@ function readAtRevision(file, rev) {
|
|
|
3469
3819
|
return result.content;
|
|
3470
3820
|
}
|
|
3471
3821
|
/** Shared body for both `em diff` forms: compile both sides, gate on errors, print, exit-code. */
|
|
3472
|
-
function runDiff(oldSource, oldLabel, newSource, newLabel, exitCode, json, lineage
|
|
3473
|
-
|
|
3474
|
-
|
|
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);
|
|
3475
3827
|
printDiagnostics(oldResult.diagnostics);
|
|
3476
3828
|
printDiagnostics(newResult.diagnostics);
|
|
3477
3829
|
if (hasErrors(oldResult.diagnostics) || hasErrors(newResult.diagnostics)) {
|
|
@@ -3561,7 +3913,7 @@ function printDiagnostics(diags) {
|
|
|
3561
3913
|
if (diags.length === 0)
|
|
3562
3914
|
return;
|
|
3563
3915
|
for (const d of diags) {
|
|
3564
|
-
const line =
|
|
3916
|
+
const line = formatDiagnosticWithFile(d);
|
|
3565
3917
|
if (d.severity === "error")
|
|
3566
3918
|
console.error(line);
|
|
3567
3919
|
else
|