@milehimikey/em 1.20.0 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/em-implementer.md +10 -0
- package/.claude/agents/em-validator.md +1 -0
- package/.claude/skills/event-modeling/SKILL.md +1 -1
- package/.claude/skills/event-modeling-conform/SKILL.md +7 -4
- package/.claude/skills/event-modeling-conform/reference/conform.md +82 -4
- 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-engagement/reference/engagement.md +1 -0
- package/.claude/skills/event-modeling-implement/SKILL.md +5 -3
- package/.claude/skills/event-modeling-implement/reference/implement.md +14 -0
- package/.claude/skills/event-modeling-review/SKILL.md +19 -7
- package/.claude/skills/event-modeling-shared/reference/em-dsl.md +54 -9
- package/.claude/skills/event-modeling-shared/reference/methodology.md +35 -2
- package/.claude/skills/event-modeling-shared/reference/operating-principles.md +26 -15
- 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/api.d.ts +84 -0
- package/dist/cli/api.js +146 -15
- package/dist/cli/api.js.map +1 -1
- package/dist/cli/bindings.d.ts +152 -0
- package/dist/cli/bindings.js +462 -0
- package/dist/cli/bindings.js.map +1 -0
- package/dist/cli/conformScope.d.ts +32 -0
- package/dist/cli/conformScope.js +68 -5
- package/dist/cli/conformScope.js.map +1 -1
- package/dist/cli/defer.js +3 -2
- package/dist/cli/defer.js.map +1 -1
- package/dist/cli/engagement.d.ts +24 -1
- package/dist/cli/engagement.js +50 -6
- package/dist/cli/engagement.js.map +1 -1
- package/dist/cli/engagementBrief.d.ts +127 -0
- package/dist/cli/engagementBrief.js +313 -0
- package/dist/cli/engagementBrief.js.map +1 -0
- package/dist/cli/engagementFile.d.ts +14 -0
- package/dist/cli/engagementFile.js +54 -0
- package/dist/cli/engagementFile.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 +8 -4
- package/dist/cli/status.js +6 -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 +47 -23
- 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 +408 -78
- package/dist/cli.js.map +1 -1
- package/dist/emit/api.d.ts +8 -1
- package/dist/emit/api.js +68 -24
- package/dist/emit/api.js.map +1 -1
- package/dist/emit/apiSurfaceJson.d.ts +3 -0
- package/dist/emit/apiSurfaceJson.js +26 -0
- package/dist/emit/apiSurfaceJson.js.map +1 -0
- package/dist/emit/engagementJson.d.ts +8 -3
- package/dist/emit/engagementJson.js +31 -4
- package/dist/emit/engagementJson.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 +111 -9
- 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 +26 -9
- package/dist/model/model.js +22 -17
- 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 +5 -1
- package/dist/templates.js +39 -2
- package/dist/templates.js.map +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/agents/em-implementer.md +10 -0
- package/plugin/agents/em-validator.md +1 -0
- package/plugin/shared/reference/em-dsl.md +54 -9
- package/plugin/shared/reference/methodology.md +35 -2
- package/plugin/shared/reference/operating-principles.md +26 -15
- 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 +7 -4
- package/plugin/skills/conform/reference/conform.md +82 -4
- 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/engagement/reference/engagement.md +1 -0
- package/plugin/skills/event-modeling/SKILL.md +1 -1
- package/plugin/skills/implement/SKILL.md +5 -3
- package/plugin/skills/implement/reference/implement.md +14 -0
- 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,16 +15,19 @@ 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
|
-
import { generateContract, runApiCheck, formatApiCheckText } from "./cli/api.js";
|
|
28
|
+
import { generateContract, runApiCheck, formatApiCheckText, runApiSurface, formatApiSurfaceText } from "./cli/api.js";
|
|
26
29
|
import { buildApiCheckJson } from "./emit/apiCheckJson.js";
|
|
30
|
+
import { buildApiSurfaceJson } from "./emit/apiSurfaceJson.js";
|
|
27
31
|
import { buildValidateJson, buildShapeJson, buildSliceReadyJson, buildValidateListJson, collectMarkers } from "./emit/validateJson.js";
|
|
28
32
|
import { buildDiffJson } from "./emit/diffJson.js";
|
|
29
33
|
import { diffModels, formatModelDiff, hasChanges } from "./model/diff.js";
|
|
@@ -43,10 +47,11 @@ import { observeSigners, renderRoster } from "./roster/init.js";
|
|
|
43
47
|
import { rosterContextFor } from "./roster/context.js";
|
|
44
48
|
import { computeModelKey } from "./model/qualifiedRef.js";
|
|
45
49
|
import { checkRoster, formatRosterCheck, buildRosterCheckJson, grandfatheredNotice } from "./roster/check.js";
|
|
46
|
-
import { applyEngagementClose, applyEngagementSet, buildNewEngagement, buildUnitIndex, effectiveTracking, engagementMembersFor, engagementPlanFor, engagementStatusFor, formatEngagementPlanText, formatEngagementStatusText, } from "./cli/engagement.js";
|
|
50
|
+
import { applyEngagementClose, applyEngagementSet, applyEngagementSetContext, buildNewEngagement, buildUnitIndex, effectiveTracking, engagementMembersFor, engagementPlanFor, engagementStatusFor, formatEngagementPlanText, formatEngagementStatusText, } from "./cli/engagement.js";
|
|
47
51
|
import { LEDGER_STATES, engagementPath, loadEngagement, readOpenEngagements } from "./cli/engagementFile.js";
|
|
48
|
-
import { buildEngagementFoundationJson, buildEngagementPlanJson, buildEngagementStatusJson } from "./emit/engagementJson.js";
|
|
52
|
+
import { buildEngagementBriefJson, buildEngagementFoundationJson, buildEngagementPlanJson, buildEngagementStatusJson } from "./emit/engagementJson.js";
|
|
49
53
|
import { checkFoundation, formatFoundationText } from "./cli/engagementFoundation.js";
|
|
54
|
+
import { engagementBriefFor, formatEngagementBriefText } from "./cli/engagementBrief.js";
|
|
50
55
|
import { detectSliceDocCollisions } from "./catalog/modelCollisionValidate.js";
|
|
51
56
|
import { checkLedger, readLedgerWaiverTrailers, applyLedgerWaivers } from "./cli/ledgerCheck.js";
|
|
52
57
|
import { planMigration, verifyMigration } from "./cli/migrateReactionShape.js";
|
|
@@ -68,7 +73,7 @@ import { checkSkillSyncBundle, checkAgentFiles } from "./cli/skillCheck.js";
|
|
|
68
73
|
import { buildSkillCheckJson } from "./emit/skillCheckJson.js";
|
|
69
74
|
import { checkPlugin, claudePluginsDir, detectPlugin, pluginInstallCommands } from "./cli/pluginPin.js";
|
|
70
75
|
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";
|
|
76
|
+
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
77
|
import { createServer as createMcpServer } from "./mcp/server.js";
|
|
73
78
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
74
79
|
import { syncAgentsMd } from "./cli/agentsMd.js";
|
|
@@ -95,13 +100,15 @@ import { runDefer, runDeferFromIssue } from "./cli/defer.js";
|
|
|
95
100
|
import { runSliceConform, runSliceConformPreview, writePreviewFindings, resolveFindingsForReport, findingsFilesOf, unruledAcrossFiles, } from "./cli/sliceConform.js";
|
|
96
101
|
import { checkPrdTriage, buildCheckPrdTriageJson, renderPrdTriageMarkdown, validatePrdTriage } from "./cli/prdTriage.js";
|
|
97
102
|
import { checkFindingsFile, buildCheckFindingsJson, FINDING_LOCI } from "./cli/findings.js";
|
|
103
|
+
import { runBindingsCheck, buildBindingsCheckJson, renderBindingsCheckText } from "./cli/bindings.js";
|
|
98
104
|
import { STATE_FILE_NAME, PHASES, isPhase, loadStateFile, parseState, setPhase, setConformance, setModelVersion, setCertified, setEmVersion, setReview, isValidDateString, modelPathMismatch, } from "./cli/stateFile.js";
|
|
99
105
|
import { findCertifiedVersion, latestModelVersionNumber, modelVersionDrift, runCertifyModelVersion, runModelVersionBump, } from "./cli/modelVersion.js";
|
|
100
|
-
import { STARTER_EM, starterEmFor, scaffoldReadme, scaffoldStateFile, scaffoldConstitution } from "./templates.js";
|
|
106
|
+
import { STARTER_EM, starterEmFor, scaffoldReadme, scaffoldStateFile, scaffoldConstitution, SLICES_DIR_README, CONTRACTS_DIR_README, MODEL_VERSIONS_DIR_README, CONFORMANCE_BINDINGS_DIR_README, } from "./templates.js";
|
|
101
107
|
import { kebabSlug } from "./util/slug.js";
|
|
102
108
|
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
109
|
import { resolveRepoRoot, detectUpgrade, applyUpgrade, checkUpgrade } from "./cli/upgrade.js";
|
|
104
110
|
import { buildStorylineSuggestion } from "./cli/storylineSuggest.js";
|
|
111
|
+
import { planStorylineSplit } from "./cli/storylineSplit.js";
|
|
105
112
|
import { buildUpgradeJson } from "./emit/upgradeJson.js";
|
|
106
113
|
import { isOlder } from "./util/semver.js";
|
|
107
114
|
import { RULES } from "./model/rules.js";
|
|
@@ -117,6 +124,8 @@ import { runCodeowners } from "./system/codeowners.js";
|
|
|
117
124
|
import { buildCodeownersJson } from "./emit/codeownersJson.js";
|
|
118
125
|
import { verifySystem } from "./system/verify.js";
|
|
119
126
|
import { buildSystemJson } from "./emit/systemJson.js";
|
|
127
|
+
import { docDirOf } from "./model/sliceDocPath.js";
|
|
128
|
+
import { docDirs } from "./catalog/docLocate.js";
|
|
120
129
|
/** GH #253: the trailing note `em slice defer` prints after `deferred: …` on a ratified/shipped
|
|
121
130
|
* doc — the item went into the signed body, outside `ratifiedHash`, and when the stamp was still
|
|
122
131
|
* in an older scope (pre-1.17 whole-body, or 1.17–1.18) it was re-stamped in the same edit (body unchanged). */
|
|
@@ -156,7 +165,10 @@ program
|
|
|
156
165
|
.command("scaffold")
|
|
157
166
|
.description("scaffold a full project: <slug>/<slug>.em, README.md, .event-modeling.md, and " +
|
|
158
167
|
"constitution.md (the implementation constitution — skipped, with a note, in a spec-kit " +
|
|
159
|
-
"project, where `.specify/memory/constitution.md` IS that document) " +
|
|
168
|
+
"project, where `.specify/memory/constitution.md` IS that document), plus the standard " +
|
|
169
|
+
"slices/, contracts/, model-versions/ and conformance/bindings/ directories (each seeded " +
|
|
170
|
+
"with a one-line README naming what lands there and which command writes it, instead of " +
|
|
171
|
+
"a .gitkeep) " +
|
|
160
172
|
"(see docs/cli.md — for just a starter .em, use `em init`; for a multi-model project, " +
|
|
161
173
|
"pass --under to nest it under a shared parent directory)")
|
|
162
174
|
.argument("<name>", "model display name — kebab-cased for the directory and file names, used as-is for titles/prose")
|
|
@@ -180,6 +192,20 @@ program
|
|
|
180
192
|
writeFileSync(join(dirPath, `${slugName}.em`), starterEmFor(name));
|
|
181
193
|
writeFileSync(join(dirPath, "README.md"), scaffoldReadme(name, slugName));
|
|
182
194
|
writeFileSync(join(dirPath, STATE_FILE_NAME), scaffoldStateFile(name, slugName, today, PKG_VERSION));
|
|
195
|
+
// GH #453: the full standardized model-directory family, not just the four files above —
|
|
196
|
+
// every doc-aware command eventually writes into one of these. Git doesn't track empty
|
|
197
|
+
// directories, so each gets a one-line README (never a .gitkeep) naming what lands there
|
|
198
|
+
// and which command writes it.
|
|
199
|
+
await mkdir(join(dirPath, "slices"), { recursive: true });
|
|
200
|
+
writeFileSync(join(dirPath, "slices", "README.md"), SLICES_DIR_README);
|
|
201
|
+
await mkdir(join(dirPath, "contracts"), { recursive: true });
|
|
202
|
+
writeFileSync(join(dirPath, "contracts", "README.md"), CONTRACTS_DIR_README);
|
|
203
|
+
await mkdir(join(dirPath, "model-versions"), { recursive: true });
|
|
204
|
+
writeFileSync(join(dirPath, "model-versions", "README.md"), MODEL_VERSIONS_DIR_README);
|
|
205
|
+
// conformance/bindings/ (GH #449) — creating it also creates its conformance/ parent, which
|
|
206
|
+
// otherwise only appears once the conform phase writes its first report/findings file.
|
|
207
|
+
await mkdir(join(dirPath, "conformance", "bindings"), { recursive: true });
|
|
208
|
+
writeFileSync(join(dirPath, "conformance", "bindings", "README.md"), CONFORMANCE_BINDINGS_DIR_README);
|
|
183
209
|
// MIL-202: the implementation constitution. One document per project, never two — in a
|
|
184
210
|
// spec-kit project `.specify/memory/constitution.md` already IS that slot, so em defers to it
|
|
185
211
|
// and writes nothing rather than seeding a second, competing copy beside the model. Same
|
|
@@ -201,12 +227,17 @@ program
|
|
|
201
227
|
.option("-T, --format <fmt>", "output format (svg, png, pdf, ...)")
|
|
202
228
|
.option("--slice <name>", "render only this slice, redrawn in its own canonical pattern shape " +
|
|
203
229
|
"(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`) " +
|
|
230
|
+
.option("--storyline <name>", "render only this storyline: its own slices in order, nothing from outside it " +
|
|
206
231
|
"(default out: storylines/<kebab-slug>.svg)")
|
|
232
|
+
.option("--map", "render every storyline to storylines/<kebab-slug>.svg plus a draggable canvas of them all, " +
|
|
233
|
+
"with the flow between storylines, at storylines/map.html (-o picks the html path)")
|
|
207
234
|
.option("--emit-dot", "print the generated DOT instead of rendering")
|
|
208
235
|
.option("--keep-empty-lanes", "keep the API lane even when empty")
|
|
209
236
|
.action(async (file, opts) => {
|
|
237
|
+
if (opts.map && (opts.slice !== undefined || opts.storyline !== undefined || opts.emitDot)) {
|
|
238
|
+
console.error("em render: --map cannot be combined with --slice, --storyline or --emit-dot");
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
210
241
|
if (opts.slice !== undefined && opts.emitDot) {
|
|
211
242
|
console.error("em render: --slice cannot be combined with --emit-dot");
|
|
212
243
|
process.exit(1);
|
|
@@ -239,6 +270,27 @@ program
|
|
|
239
270
|
console.error("not rendering: fix the errors above");
|
|
240
271
|
process.exit(1);
|
|
241
272
|
}
|
|
273
|
+
if (opts.map) {
|
|
274
|
+
const storylines = model.storylines ?? [];
|
|
275
|
+
if (storylines.length === 0) {
|
|
276
|
+
console.error("em render --map: this model declares no storylines");
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
const out = opts.out ?? join(dirname(file), "storylines", "map.html");
|
|
280
|
+
const outDir = dirname(out);
|
|
281
|
+
await mkdir(outDir, { recursive: true });
|
|
282
|
+
const frames = [];
|
|
283
|
+
for (const st of storylines) {
|
|
284
|
+
const sub = buildStorylineDiagram(model, st, { keepEmptyLanes: opts.keepEmptyLanes });
|
|
285
|
+
const svg = composeSvg(await layoutDot(sub.dot), sub.model, sub.grid, dirname(file), outDir);
|
|
286
|
+
const slug = kebabSlug(st.name);
|
|
287
|
+
await writeRendered(svg, join(outDir, `${slug}.svg`), "svg");
|
|
288
|
+
frames.push({ name: st.name, slug, sliceCount: st.sliceIndexes.length, svg });
|
|
289
|
+
}
|
|
290
|
+
await writeFile(out, buildStorylineMapHtml(model, frames, storylineMap(model)), "utf8");
|
|
291
|
+
console.log(`rendered ${out} (+ ${frames.length} storyline svg${frames.length === 1 ? "" : "s"})`);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
242
294
|
if (opts.storyline !== undefined) {
|
|
243
295
|
const found = resolveStorylineArg(model, opts.storyline);
|
|
244
296
|
if ("error" in found) {
|
|
@@ -261,7 +313,7 @@ program
|
|
|
261
313
|
console.error(lookup.error);
|
|
262
314
|
process.exit(1);
|
|
263
315
|
}
|
|
264
|
-
const out = opts.out ?? defaultSliceOut(file, opts.slice);
|
|
316
|
+
const out = opts.out ?? defaultSliceOut(file, opts.slice, docDirOf(model.slices[lookup.index]));
|
|
265
317
|
const fmt = opts.format ?? formatFromPath(out);
|
|
266
318
|
await mkdir(dirname(out), { recursive: true }); // slices/ may not exist yet
|
|
267
319
|
const { model: sliceModel, grid: sliceGrid, dot: sliceDot } = buildSliceDiagram(model, lookup.index, {
|
|
@@ -410,6 +462,25 @@ api
|
|
|
410
462
|
if (!outcome.report.current)
|
|
411
463
|
process.exitCode = 1;
|
|
412
464
|
});
|
|
465
|
+
api
|
|
466
|
+
.command("surface")
|
|
467
|
+
.description("the model's public commands, events, views and reachable declared types as a versioned " +
|
|
468
|
+
"JSON document for companion tools (GH #448, see docs/cli.md)")
|
|
469
|
+
.argument("<file>", "input .em file")
|
|
470
|
+
.option("--json", "print a JSON document instead of the text listing (see docs/cli.md)")
|
|
471
|
+
.action((file, opts) => {
|
|
472
|
+
const outcome = runApiSurface(file);
|
|
473
|
+
printDiagnostics(outcome.diagnostics);
|
|
474
|
+
if (!outcome.ok) {
|
|
475
|
+
console.error(outcome.message);
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
if (opts.json)
|
|
479
|
+
process.stdout.write(buildApiSurfaceJson(outcome.report) + "\n");
|
|
480
|
+
else
|
|
481
|
+
for (const line of formatApiSurfaceText(outcome.report))
|
|
482
|
+
console.log(line);
|
|
483
|
+
});
|
|
413
484
|
program
|
|
414
485
|
.command("typespec")
|
|
415
486
|
.description("DEPRECATED alias of `em api generate --stdout` (use em api generate)")
|
|
@@ -441,7 +512,10 @@ program
|
|
|
441
512
|
oldDoc: (key) => readSliceDoc(dirname(plan.oldFile), key),
|
|
442
513
|
newDoc: (key) => readSliceDoc(dirname(plan.newFile), key),
|
|
443
514
|
};
|
|
444
|
-
runDiff(oldSource, plan.oldFile, newSource, plan.newFile, opts.exitCode, opts.json, lineage
|
|
515
|
+
runDiff(oldSource, plan.oldFile, newSource, plan.newFile, opts.exitCode, opts.json, lineage, {
|
|
516
|
+
old: diskReader(dirname(plan.oldFile)),
|
|
517
|
+
new: diskReader(dirname(plan.newFile)),
|
|
518
|
+
});
|
|
445
519
|
return;
|
|
446
520
|
}
|
|
447
521
|
const oldSource = readAtRevision(plan.file, plan.from);
|
|
@@ -452,7 +526,10 @@ program
|
|
|
452
526
|
oldDoc: (key) => resolveDocAtRevision(plan.file, key, plan.from),
|
|
453
527
|
newDoc: (key) => plan.to ? resolveDocAtRevision(plan.file, key, plan.to) : readSliceDoc(dirname(plan.file), key),
|
|
454
528
|
};
|
|
455
|
-
runDiff(oldSource, oldLabel, newSource, newLabel, opts.exitCode, opts.json, lineage
|
|
529
|
+
runDiff(oldSource, oldLabel, newSource, newLabel, opts.exitCode, opts.json, lineage, {
|
|
530
|
+
old: gitReader(plan.file, plan.from),
|
|
531
|
+
new: plan.to ? gitReader(plan.file, plan.to) : diskReader(dirname(plan.file)),
|
|
532
|
+
});
|
|
456
533
|
});
|
|
457
534
|
program
|
|
458
535
|
.command("glossary")
|
|
@@ -475,7 +552,7 @@ program
|
|
|
475
552
|
let anyErrors = false;
|
|
476
553
|
for (const file of files) {
|
|
477
554
|
const source = readFileOrExit(file);
|
|
478
|
-
const result = compileSource(source, file);
|
|
555
|
+
const result = compileSource(source, file, diskReader(dirname(file)));
|
|
479
556
|
printDiagnosticsFor(file, result.diagnostics);
|
|
480
557
|
if (hasErrors(result.diagnostics))
|
|
481
558
|
anyErrors = true;
|
|
@@ -596,8 +673,12 @@ slice
|
|
|
596
673
|
process.exit(1);
|
|
597
674
|
}
|
|
598
675
|
const key = sliceDocKey(name);
|
|
599
|
-
|
|
600
|
-
|
|
676
|
+
// The note clause written into the model is always `slices/<key>.md`, relative to the file
|
|
677
|
+
// that declares the slice (GH #421). Where the FILE goes: `slices/` under the cwd, or — with
|
|
678
|
+
// --wire naming a model whose slice sits in a storyline folder — that folder's `slices/`.
|
|
679
|
+
const notePath = `slices/${key}.md`;
|
|
680
|
+
let dir = "slices";
|
|
681
|
+
let path = join(dir, `${key}.md`);
|
|
601
682
|
if (existsSync(path) && !opts.force) {
|
|
602
683
|
console.error(`refusing to overwrite ${path} (use --force)`);
|
|
603
684
|
process.exit(1);
|
|
@@ -617,12 +698,25 @@ slice
|
|
|
617
698
|
console.error(`em slice new --wire: not wiring — fix the errors in ${opts.wire} above`);
|
|
618
699
|
process.exit(1);
|
|
619
700
|
}
|
|
620
|
-
|
|
701
|
+
// GH #421: a slice declared in a storyline model file is wired in THAT file (its line
|
|
702
|
+
// numbers count there); a root slice in the root as before.
|
|
703
|
+
const sliceFile = model.slices[refs.sliceKeys.indexOf(key)]?.file;
|
|
704
|
+
const wireTarget = sliceFile ? resolve(dirname(opts.wire), sliceFile) : opts.wire;
|
|
705
|
+
const wireSource = sliceFile ? readFileSync(wireTarget, "utf8") : source;
|
|
706
|
+
if (sliceFile) {
|
|
707
|
+
dir = join(dirname(opts.wire), docDirOf(model.slices[refs.sliceKeys.indexOf(key)]), "slices");
|
|
708
|
+
path = join(dir, `${key}.md`);
|
|
709
|
+
if (existsSync(path) && !opts.force) {
|
|
710
|
+
console.error(`refusing to overwrite ${path} (use --force)`);
|
|
711
|
+
process.exit(1);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const result = wireSliceNote(wireSource, model, refs, key, opts.pattern, notePath);
|
|
621
715
|
if (!result.ok) {
|
|
622
716
|
console.error(`em slice new --wire: ${result.message}`);
|
|
623
717
|
process.exit(1);
|
|
624
718
|
}
|
|
625
|
-
wired = result;
|
|
719
|
+
wired = { ...result, target: wireTarget };
|
|
626
720
|
regions = buildSliceRegions(model, [refs.sliceKeys.indexOf(key)]);
|
|
627
721
|
const sliceEvents = model.slices[refs.sliceKeys.indexOf(key)].elements.filter((e) => e.kind === "event");
|
|
628
722
|
alternatives = sliceEvents.filter((e) => e.alternativeGroup !== undefined).map((e) => e.name);
|
|
@@ -634,8 +728,8 @@ slice
|
|
|
634
728
|
writeFileSync(path, content);
|
|
635
729
|
console.log(`wrote ${path}`);
|
|
636
730
|
if (wired) {
|
|
637
|
-
writeFileSync(
|
|
638
|
-
console.log(`wired ${path} onto ${wired.elementName} in slice "${wired.sliceName}" (${
|
|
731
|
+
writeFileSync(wired.target, wired.content);
|
|
732
|
+
console.log(`wired ${path} onto ${wired.elementName} in slice "${wired.sliceName}" (${wired.target})`);
|
|
639
733
|
}
|
|
640
734
|
else {
|
|
641
735
|
console.log(`add this to the slice's primary element in the .em file:`);
|
|
@@ -739,6 +833,7 @@ slice
|
|
|
739
833
|
"facts (key, pattern, doc status/implementedIn) — the hand-maintained table is deprecated")
|
|
740
834
|
.argument("<file>", "input .em file")
|
|
741
835
|
.option("--check", "verify the table is current; exit non-zero on drift without writing (CI)")
|
|
836
|
+
.option("--full", "every lifecycle column (reviewed/ratified/owner/tracking/implemented/conformed) instead of the reader's five")
|
|
742
837
|
.action((file, opts) => {
|
|
743
838
|
const { model, diagnostics, refs } = compileFile(file);
|
|
744
839
|
printDiagnostics(diagnostics);
|
|
@@ -746,7 +841,7 @@ slice
|
|
|
746
841
|
console.error("not indexing: fix the errors above");
|
|
747
842
|
process.exit(1);
|
|
748
843
|
}
|
|
749
|
-
const result = runSliceIndex(model, refs, file, !!opts.check);
|
|
844
|
+
const result = runSliceIndex(model, refs, file, !!opts.check, { full: !!opts.full });
|
|
750
845
|
printDiagnostics(newDiagnostics(result.table.diagnostics, diagnostics));
|
|
751
846
|
if (!result.ok) {
|
|
752
847
|
console.error(result.message);
|
|
@@ -758,6 +853,10 @@ slice
|
|
|
758
853
|
else {
|
|
759
854
|
console.log(`ok — ${result.readmePath}'s Slices table is already up to date`);
|
|
760
855
|
}
|
|
856
|
+
if (result.storylines === "absent") {
|
|
857
|
+
console.log(`note: ${result.readmePath} has no <!-- GENERATED:storylines:start --> / :end markers, so the storyline table ` +
|
|
858
|
+
"was not written — add the ## Storylines section from templates/model-readme.md to get it");
|
|
859
|
+
}
|
|
761
860
|
});
|
|
762
861
|
slice
|
|
763
862
|
.command("mark-implemented")
|
|
@@ -1445,8 +1544,8 @@ state
|
|
|
1445
1544
|
process.exit(1);
|
|
1446
1545
|
}
|
|
1447
1546
|
const phases = sortUsagePhases(rawPhases);
|
|
1448
|
-
const { model, diagnostics, refs,
|
|
1449
|
-
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics,
|
|
1547
|
+
const { model, diagnostics, refs, hashSource } = compileFile(file);
|
|
1548
|
+
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics, hashSource);
|
|
1450
1549
|
const categorySet = new Set(allDiagnostics.map((d) => RULES[d.code].usageCategory));
|
|
1451
1550
|
const categories = categorySet.size > 0 ? [...categorySet].sort() : ["none"];
|
|
1452
1551
|
const loaded = loadStateFile(dirname(file));
|
|
@@ -1489,13 +1588,13 @@ modelVersionCmd
|
|
|
1489
1588
|
}
|
|
1490
1589
|
const today = localIsoDate();
|
|
1491
1590
|
const on = opts.on ?? today;
|
|
1492
|
-
const { model: compiledModel, refs, diagnostics,
|
|
1591
|
+
const { model: compiledModel, refs, diagnostics, hashSource } = compileFile(file);
|
|
1493
1592
|
printDiagnostics(diagnostics);
|
|
1494
1593
|
if (hasErrors(diagnostics)) {
|
|
1495
1594
|
console.error(`em model version bump: "${file}" has errors — fix them first`);
|
|
1496
1595
|
process.exit(1);
|
|
1497
1596
|
}
|
|
1498
|
-
const result = runModelVersionBump(baseDir, basename(file), compiledModel, refs,
|
|
1597
|
+
const result = runModelVersionBump(baseDir, basename(file), compiledModel, refs, hashSource, opts.by, on, GENERATOR_VERSION, opts.force === true);
|
|
1499
1598
|
if (!result.ok) {
|
|
1500
1599
|
console.error(`em model version bump: ${result.message}`);
|
|
1501
1600
|
process.exit(1);
|
|
@@ -1679,6 +1778,44 @@ conformFindings
|
|
|
1679
1778
|
if (!result.ok)
|
|
1680
1779
|
process.exit(1);
|
|
1681
1780
|
});
|
|
1781
|
+
const conform = program.command("conform").description("file-contract conformance checks (advisory only, GH #449 — never wired into validate/--slice-ready)");
|
|
1782
|
+
const conformBindings = conform.command("bindings").description("per-slice bindings manifests (conformance/bindings/<slice-key>.yaml, see docs/bindings.md)");
|
|
1783
|
+
conformBindings
|
|
1784
|
+
.command("check")
|
|
1785
|
+
.description("verify every per-slice bindings manifest (conformance/bindings/<slice-key>.yaml, see " +
|
|
1786
|
+
"docs/bindings.md): each bound element's `ref` resolves against the compiled model, and " +
|
|
1787
|
+
"each bound `file` exists under --repo and contains its `identifier` as a plain substring " +
|
|
1788
|
+
"(grep-level only, never code-parsed — language-agnostic). Distinct failure kinds: an " +
|
|
1789
|
+
"unknown `--slice` key, a missing bindings file, malformed YAML, an unknown slice named " +
|
|
1790
|
+
"inside a file, and an unknown element ref. Advisory only — exit code stays 0 unless " +
|
|
1791
|
+
"--exit-code is given and a binding failed (the opt-in CI gate, same posture as `em refs " +
|
|
1792
|
+
"--reachable-from`).")
|
|
1793
|
+
.argument("<file>", "input .em file")
|
|
1794
|
+
.requiredOption("--repo <path>", "path to the target codebase (bound `file`s resolve relative to it; never required to be a git repository)")
|
|
1795
|
+
.option("--slice <key>", "check only this slice's bindings file (default: every conformance/bindings/*.yaml)")
|
|
1796
|
+
.option("--json", "print a JSON document instead of the text report")
|
|
1797
|
+
.option("--exit-code", "exit 1 if any binding failed (git-diff convention) — the opt-in CI gate")
|
|
1798
|
+
.action((file, opts) => {
|
|
1799
|
+
const { model, diagnostics, refs } = compileFile(file);
|
|
1800
|
+
printDiagnostics(diagnostics);
|
|
1801
|
+
if (hasErrors(diagnostics)) {
|
|
1802
|
+
console.error("em conform bindings check: fix the model's errors above first");
|
|
1803
|
+
process.exit(1);
|
|
1804
|
+
}
|
|
1805
|
+
const sliceKey = opts.slice !== undefined ? resolveSliceOrExit("em conform bindings check", model, refs, opts.slice) : undefined;
|
|
1806
|
+
const result = runBindingsCheck(refs, dirname(file), opts.repo, sliceKey);
|
|
1807
|
+
if (opts.json) {
|
|
1808
|
+
console.log(buildBindingsCheckJson(result));
|
|
1809
|
+
}
|
|
1810
|
+
else {
|
|
1811
|
+
console.log(renderBindingsCheckText(result));
|
|
1812
|
+
}
|
|
1813
|
+
// Same rationale as `em diff`/`em api check`'s --exit-code: set the code rather than
|
|
1814
|
+
// process.exit() so a large --json document piped downstream isn't truncated, and so this
|
|
1815
|
+
// advisory check NEVER fails a run by default (GH #449 — opt-in CI gate only).
|
|
1816
|
+
if (opts.exitCode && !result.ok)
|
|
1817
|
+
process.exitCode = 1;
|
|
1818
|
+
});
|
|
1682
1819
|
const discover = program.command("discover").description("checks for the discover skill's records (GH #295)");
|
|
1683
1820
|
discover
|
|
1684
1821
|
.command("triage-check")
|
|
@@ -1745,6 +1882,10 @@ program
|
|
|
1745
1882
|
// --storyline resolves, in which case build() below points it at that storyline's own
|
|
1746
1883
|
// subset file instead (GH #309).
|
|
1747
1884
|
let liveFile = basename(out);
|
|
1885
|
+
// GH #422: the storyline canvas (the page `em render --map` writes), rebuilt on every build
|
|
1886
|
+
// and served live at /map — undefined while the model declares no storylines, so /map 404s.
|
|
1887
|
+
let canvas;
|
|
1888
|
+
let hasStorylines = false;
|
|
1748
1889
|
// serializeBuilds: chokidar can fire several events for one save (and an
|
|
1749
1890
|
// editor's write can land while a slow png/pdf render is still in flight) —
|
|
1750
1891
|
// overlapping builds would race each other writing the same output file.
|
|
@@ -1787,14 +1928,26 @@ program
|
|
|
1787
1928
|
await writeRendered(fullSvg, out, "svg");
|
|
1788
1929
|
// Every declared storyline's own subset (GH #277's `em render --storyline` subset,
|
|
1789
1930
|
// 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.
|
|
1931
|
+
// --storyline, so the viewer's picker always has somewhere to navigate to. The same
|
|
1932
|
+
// subsets fill the live canvas's frames (GH #422), each frame opening into its
|
|
1933
|
+
// storyline's viewer page.
|
|
1934
|
+
const frames = [];
|
|
1791
1935
|
for (const st of model.storylines ?? []) {
|
|
1792
1936
|
const sub = buildStorylineDiagram(model, st, { keepEmptyLanes: opts.keepEmptyLanes });
|
|
1793
1937
|
const subFile = storylineFileFor(st.name);
|
|
1794
1938
|
const subRaw = await layoutDot(sub.dot);
|
|
1795
1939
|
const subSvg = embedStorylinePicker(composeSvg(subRaw, sub.model, sub.grid, baseDir, outDir), storylineVariants(model, outFile, subFile));
|
|
1796
1940
|
await writeRendered(subSvg, join(outDir, subFile), "svg");
|
|
1941
|
+
frames.push({
|
|
1942
|
+
name: st.name,
|
|
1943
|
+
slug: kebabSlug(st.name),
|
|
1944
|
+
sliceCount: st.sliceIndexes.length,
|
|
1945
|
+
svg: subSvg,
|
|
1946
|
+
href: `/?svg=${encodeURIComponent(subFile)}`,
|
|
1947
|
+
});
|
|
1797
1948
|
}
|
|
1949
|
+
hasStorylines = frames.length > 0;
|
|
1950
|
+
canvas = hasStorylines ? buildStorylineMapHtml(model, frames, storylineMap(model), { live: true }) : undefined;
|
|
1798
1951
|
}
|
|
1799
1952
|
else {
|
|
1800
1953
|
await renderDot(dot, model, grid, out, fmt, dirname(file));
|
|
@@ -1812,11 +1965,16 @@ program
|
|
|
1812
1965
|
try {
|
|
1813
1966
|
// Serve the directory the SVG is written to — that's what the browser
|
|
1814
1967
|
// fetches, and note "..." links inside the SVG resolve relative to it.
|
|
1815
|
-
server = await startLiveServer({ dir: dirname(resolve(out)), port: opts.port });
|
|
1968
|
+
server = await startLiveServer({ dir: dirname(resolve(out)), port: opts.port, canvas: () => canvas });
|
|
1816
1969
|
// The viewer inlines SVG only — for png/pdf watches there's nothing
|
|
1817
1970
|
// live to embed, so don't print a ?svg= URL that can't work.
|
|
1818
1971
|
if (fmt === "svg") {
|
|
1819
|
-
|
|
1972
|
+
const pinned = opts.storyline !== undefined;
|
|
1973
|
+
console.log(`→ live view: ${liveViewUrl(server.url, liveFile, hasStorylines, pinned)}`);
|
|
1974
|
+
if (hasStorylines && !pinned) {
|
|
1975
|
+
console.log(" the storyline canvas — click a storyline's “open ›” to step through it;");
|
|
1976
|
+
console.log(` the full timeline: ${server.url}/?svg=${encodeURIComponent(liveFile)}`);
|
|
1977
|
+
}
|
|
1820
1978
|
console.log(" open it in a browser and share your screen");
|
|
1821
1979
|
}
|
|
1822
1980
|
else {
|
|
@@ -1833,7 +1991,20 @@ program
|
|
|
1833
1991
|
// the .em file. Watching the directory (not a glob — chokidar 4 has none)
|
|
1834
1992
|
// still fires for docs authored after watch starts; the filter keeps it to .md.
|
|
1835
1993
|
const slicesDir = join(dirname(file), "slices");
|
|
1836
|
-
|
|
1994
|
+
// GH #421: a storyline model file is part of the model — watch each one the root declares
|
|
1995
|
+
// (resolved once at start; a storyline added to the root mid-session needs a restart).
|
|
1996
|
+
const storylineFiles = (() => {
|
|
1997
|
+
try {
|
|
1998
|
+
return compileModelFile(file).files.map((f) => resolve(dirname(file), f));
|
|
1999
|
+
}
|
|
2000
|
+
catch {
|
|
2001
|
+
return [];
|
|
2002
|
+
}
|
|
2003
|
+
})();
|
|
2004
|
+
const watched = new Set([resolve(file), ...storylineFiles]);
|
|
2005
|
+
// GH #421 step 2: docs live beside their declaring file — watch every existing doc folder
|
|
2006
|
+
const docFoldersNow = docDirs(dirname(file));
|
|
2007
|
+
watchFile([file, ...storylineFiles, slicesDir, ...docFoldersNow.filter((d) => resolve(d) !== resolve(slicesDir))], build, 80, (p) => watched.has(resolve(p)) || p.endsWith(".md"));
|
|
1837
2008
|
console.log(`watching ${file} … (ctrl-c to stop)`);
|
|
1838
2009
|
if (!opts.serve)
|
|
1839
2010
|
console.log("tip: add --serve for the live browser view");
|
|
@@ -1892,9 +2063,9 @@ program
|
|
|
1892
2063
|
console.error("em validate: --shape cannot be combined with --slice-ready, --list-*, --fail-on-issues or --check-signed-bodies");
|
|
1893
2064
|
process.exit(1);
|
|
1894
2065
|
}
|
|
1895
|
-
const { model, diagnostics, refs, source } = compileFile(file);
|
|
2066
|
+
const { model, diagnostics, refs, source, hashSource } = compileFile(file);
|
|
1896
2067
|
if (opts.shape) {
|
|
1897
|
-
const all = computeAllDiagnostics(file, model, refs, diagnostics,
|
|
2068
|
+
const all = computeAllDiagnostics(file, model, refs, diagnostics, hashSource);
|
|
1898
2069
|
const report = buildShapeReport(all, opts.waiveFile ? readWaiveFile(opts.waiveFile) : []);
|
|
1899
2070
|
const modelErrors = all.filter((d) => d.severity === "error").length;
|
|
1900
2071
|
for (const k of report.staleWaivers)
|
|
@@ -1908,7 +2079,7 @@ program
|
|
|
1908
2079
|
// GH #257: an opt-in addition to the unconditional set, so --json, the text report and the
|
|
1909
2080
|
// exit code all see it the same way every other diagnostic is seen.
|
|
1910
2081
|
const allDiagnostics = [
|
|
1911
|
-
...computeAllDiagnostics(file, model, refs, diagnostics,
|
|
2082
|
+
...computeAllDiagnostics(file, model, refs, diagnostics, hashSource),
|
|
1912
2083
|
...(opts.checkSignedBodies ? validateSignedBodies(model, refs, dirname(file)) : []),
|
|
1913
2084
|
];
|
|
1914
2085
|
// GH #293: --strict promotes advisory lints to errors (the --slice-ready branch below does
|
|
@@ -2310,11 +2481,11 @@ program
|
|
|
2310
2481
|
const compiled = [];
|
|
2311
2482
|
let anyErrors = false;
|
|
2312
2483
|
for (const file of files) {
|
|
2313
|
-
const { model, refs, diagnostics, source } = compileFile(file);
|
|
2484
|
+
const { model, refs, diagnostics, source, hashSource } = compileFile(file);
|
|
2314
2485
|
printDiagnosticsFor(file, diagnostics);
|
|
2315
2486
|
if (hasErrors(diagnostics))
|
|
2316
2487
|
anyErrors = true;
|
|
2317
|
-
compiled.push({ file, model, refs, source });
|
|
2488
|
+
compiled.push({ file, model, refs, source, hashSource });
|
|
2318
2489
|
}
|
|
2319
2490
|
if (anyErrors) {
|
|
2320
2491
|
console.error("em status: not reporting status — fix the errors above");
|
|
@@ -2368,11 +2539,11 @@ program
|
|
|
2368
2539
|
// merged `sliceFacts` above (which spans every input file when several are given).
|
|
2369
2540
|
const conformance = compiled.map(({ file }) => {
|
|
2370
2541
|
const facts = factsByFile.get(file) ?? [];
|
|
2371
|
-
const sliceDocFacts = facts.map((f) => ({ key: f.key, status: f.rawStatus, implementedIn: f.implementedIn, shipped: f.shipped }));
|
|
2542
|
+
const sliceDocFacts = facts.map((f) => ({ key: f.key, status: f.rawStatus, implementedIn: f.implementedIn, shipped: f.shipped, file: f.sliceFile }));
|
|
2372
2543
|
return resolveConformanceEntry(file, opts.repo, sliceDocFacts);
|
|
2373
2544
|
});
|
|
2374
2545
|
// MIL-218: one model-version entry per input file.
|
|
2375
|
-
const modelVersion = compiled.map(({ file, model, refs,
|
|
2546
|
+
const modelVersion = compiled.map(({ file, model, refs, hashSource }) => resolveModelVersionStatusEntry(file, model, refs, hashSource));
|
|
2376
2547
|
// MIL-219: one em-version entry per input file whose state file resolved.
|
|
2377
2548
|
const emVersion = compiled
|
|
2378
2549
|
.map(({ file }) => resolveEmVersionStatusEntry(file, PKG_VERSION))
|
|
@@ -2630,6 +2801,7 @@ engagement
|
|
|
2630
2801
|
.option("--by <name>", "who created the engagement (createdBy)")
|
|
2631
2802
|
.option("--tracking <key>", "an issue-tracker key or URL the slice PRs cite (a slice doc's own tracking: wins)")
|
|
2632
2803
|
.option("--pr-title <template>", "PR title template for the slice PRs; {key} = tracking key, {slice} = slice name, {sliceKey} = slice key (default \"{key}: {slice}\", or \"{slice}\" without a key)")
|
|
2804
|
+
.option("--context-doc <path>", "repo-relative path to a document implementers need beyond the slice doc — the constitution, an NFR doc, an ADR (repeatable; GH #455)", (value, previous) => [...previous, value], [])
|
|
2633
2805
|
.option("--force", "overwrite an existing engagement file with this slug")
|
|
2634
2806
|
.action((file, slug, opts) => {
|
|
2635
2807
|
const fail = (m) => {
|
|
@@ -2649,7 +2821,7 @@ engagement
|
|
|
2649
2821
|
const parallel = /^\d+$/.test(opts.parallel) ? Number(opts.parallel) : NaN;
|
|
2650
2822
|
const { model, refs } = compileFile(file);
|
|
2651
2823
|
const path = engagementPath(file, slug);
|
|
2652
|
-
const result = buildNewEngagement({ file, model, refs, baseDir: dirname(file) }, { slug, selector, parallel, createdBy: opts.by ?? null, tracking: opts.tracking ?? null, prTitle: opts.prTitle ?? null, created: localIsoDate() });
|
|
2824
|
+
const result = buildNewEngagement({ file, model, refs, baseDir: dirname(file) }, { slug, selector, parallel, createdBy: opts.by ?? null, tracking: opts.tracking ?? null, prTitle: opts.prTitle ?? null, context: opts.contextDoc, created: localIsoDate() });
|
|
2653
2825
|
if (!result.ok)
|
|
2654
2826
|
return fail(result.message);
|
|
2655
2827
|
if (existsSync(path) && !opts.force)
|
|
@@ -2680,8 +2852,8 @@ function resolveSliceOrExit(command, model, refs, input) {
|
|
|
2680
2852
|
/** Compile `file` with the full `em validate` diagnostic set — what `engagement plan` needs for
|
|
2681
2853
|
* each slice's `--slice-ready` verdict (R34). */
|
|
2682
2854
|
function engagementModelInput(file) {
|
|
2683
|
-
const { model, refs, diagnostics, source } = compileFile(file);
|
|
2684
|
-
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics,
|
|
2855
|
+
const { model, refs, diagnostics, source, hashSource } = compileFile(file);
|
|
2856
|
+
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics, hashSource);
|
|
2685
2857
|
return { file, model, refs, baseDir: dirname(file), contract: { file, source, diagnostics }, allDiagnostics };
|
|
2686
2858
|
}
|
|
2687
2859
|
engagement
|
|
@@ -2727,22 +2899,76 @@ engagement
|
|
|
2727
2899
|
if (!check.ok)
|
|
2728
2900
|
process.exitCode = 1;
|
|
2729
2901
|
});
|
|
2902
|
+
engagement
|
|
2903
|
+
.command("brief")
|
|
2904
|
+
.description("GH #454: a deterministic, self-contained per-slice work brief for implementers em does not " +
|
|
2905
|
+
"control (a ticket-driven cloud agent) — the bound slice doc verbatim (or the model's own " +
|
|
2906
|
+
"elements when there is none), model context from the edge list, the public-contract " +
|
|
2907
|
+
"surface, a definition-of-done (bindings manifest skeleton + conform check + isolation + PR " +
|
|
2908
|
+
"title), and the engagement's context: documents. One brief per engagement slice unless " +
|
|
2909
|
+
"--slice narrows it")
|
|
2910
|
+
.argument("<file>", "input .em file")
|
|
2911
|
+
.argument("<slug>", "engagement slug")
|
|
2912
|
+
.option("--slice <key>", "only this slice's brief (export key, display name, or unique prefix)")
|
|
2913
|
+
.option("--inline-context", "embed each context document's full text instead of just its path")
|
|
2914
|
+
.option("--json", "print a JSON document instead of text (same document as the MCP engagement_brief tool)")
|
|
2915
|
+
.action((file, slug, opts) => {
|
|
2916
|
+
const { model, refs } = compileFile(file);
|
|
2917
|
+
// GH #237: an input that resolves to no slice goes through as typed, so the engagement's own
|
|
2918
|
+
// "is not in engagement" refusal (which names what the Ledger holds) still speaks.
|
|
2919
|
+
let sliceKey = null;
|
|
2920
|
+
if (opts.slice !== undefined) {
|
|
2921
|
+
const resolved = resolveSliceKey(model, refs, opts.slice);
|
|
2922
|
+
sliceKey = resolved.ok ? resolved.key : opts.slice;
|
|
2923
|
+
}
|
|
2924
|
+
const result = engagementBriefFor({ file, model, refs, baseDir: dirname(file) }, slug, { sliceKey, inlineContext: opts.inlineContext === true });
|
|
2925
|
+
if (!result.ok) {
|
|
2926
|
+
console.error(`em engagement brief: ${result.message}`);
|
|
2927
|
+
process.exit(1);
|
|
2928
|
+
}
|
|
2929
|
+
process.stdout.write((opts.json ? buildEngagementBriefJson(result.brief) : formatEngagementBriefText(result.brief)) + "\n");
|
|
2930
|
+
});
|
|
2730
2931
|
engagement
|
|
2731
2932
|
.command("set")
|
|
2732
2933
|
.description("the only write path to the Ledger: set one slice's state (and optionally branch/base/pr); " +
|
|
2733
|
-
"idempotent; merged is terminal; --state held records a human hold"
|
|
2934
|
+
"idempotent; merged is terminal; --state held records a human hold. Or, on its own (no slice " +
|
|
2935
|
+
"key), --context-doc replaces the engagement-level context: list (GH #455)")
|
|
2734
2936
|
.argument("<file>", "input .em file")
|
|
2735
2937
|
.argument("<slug>", "engagement slug")
|
|
2736
|
-
.argument("
|
|
2737
|
-
.
|
|
2938
|
+
.argument("[key]", "slice export key (omit when using --context-doc alone)")
|
|
2939
|
+
.option("--state <state>", `one of: ${LEDGER_STATES.join(" | ")}`)
|
|
2738
2940
|
.option("--branch <name>", "the slice's branch")
|
|
2739
2941
|
.option("--base <name>", "the branch the slice's PR targets")
|
|
2740
2942
|
.option("--pr <url>", "the slice's PR URL")
|
|
2943
|
+
.option("--context-doc <path>", "repo-relative path to a document implementers need beyond the slice doc (repeatable; replaces the whole engagement-level context: list; GH #455)", (value, previous) => [...previous, value], [])
|
|
2741
2944
|
.action((file, slug, key, opts) => {
|
|
2742
2945
|
const fail = (m) => {
|
|
2743
2946
|
console.error(`em engagement set: ${m}`);
|
|
2744
2947
|
process.exit(1);
|
|
2745
2948
|
};
|
|
2949
|
+
const sliceFields = opts.state !== undefined || opts.branch !== undefined || opts.base !== undefined || opts.pr !== undefined;
|
|
2950
|
+
if (opts.contextDoc.length > 0) {
|
|
2951
|
+
if (key !== undefined || sliceFields) {
|
|
2952
|
+
return fail("--context-doc sets the engagement-level context: list on its own — pass it without a slice key or --state/--branch/--base/--pr");
|
|
2953
|
+
}
|
|
2954
|
+
const loaded = loadEngagement(file, slug);
|
|
2955
|
+
if (!loaded.ok)
|
|
2956
|
+
return fail(loaded.message);
|
|
2957
|
+
const result = applyEngagementSetContext(slug, loaded.text, loaded.file, opts.contextDoc);
|
|
2958
|
+
if (!result.ok)
|
|
2959
|
+
return fail(result.message);
|
|
2960
|
+
if (!result.changed) {
|
|
2961
|
+
console.log(`no change: context is already [${result.context.join(", ")}] (${loaded.path})`);
|
|
2962
|
+
return;
|
|
2963
|
+
}
|
|
2964
|
+
writeFileSync(loaded.path, result.text);
|
|
2965
|
+
console.log(`set context: ${result.context.length} path${result.context.length === 1 ? "" : "s"} (${loaded.path})`);
|
|
2966
|
+
return;
|
|
2967
|
+
}
|
|
2968
|
+
if (!key)
|
|
2969
|
+
return fail("a slice export key is required (or pass --context-doc to set the engagement-level context: list)");
|
|
2970
|
+
if (!opts.state)
|
|
2971
|
+
return fail("--state is required when setting a slice");
|
|
2746
2972
|
const loaded = loadEngagement(file, slug);
|
|
2747
2973
|
if (!loaded.ok)
|
|
2748
2974
|
return fail(loaded.message);
|
|
@@ -2752,7 +2978,7 @@ engagement
|
|
|
2752
2978
|
const resolved = resolveSliceKey(model, refs, key);
|
|
2753
2979
|
if (resolved.ok)
|
|
2754
2980
|
key = resolved.key;
|
|
2755
|
-
const result = applyEngagementSet({ file, model, refs, baseDir: dirname(file) }, slug, loaded.text, loaded.file, { key,
|
|
2981
|
+
const result = applyEngagementSet({ file, model, refs, baseDir: dirname(file) }, slug, loaded.text, loaded.file, { key, state: opts.state, branch: opts.branch, base: opts.base, pr: opts.pr });
|
|
2756
2982
|
if (!result.ok)
|
|
2757
2983
|
return fail(result.message);
|
|
2758
2984
|
if (!result.changed) {
|
|
@@ -3014,17 +3240,24 @@ const scopeCommand = systemCommand
|
|
|
3014
3240
|
function packagedSkillsRoot() {
|
|
3015
3241
|
return join(dirname(fileURLToPath(import.meta.url)), "..", ".claude", "skills");
|
|
3016
3242
|
}
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
// MIL-269 (R32): the four em sub-agent definitions live in
|
|
3243
|
+
// GH #412: the consumer side is `.claude/skills/` + `.claude/agents/` by default, `--skills-dir` /
|
|
3244
|
+
// `--agents-dir` when given, or `.agents/skills/` (+ `.agents/agents/`) when that neutral folder
|
|
3245
|
+
// exists and `.claude/skills/` does not — see resolveVendoredDirs (src/cli/skillDirs.ts).
|
|
3246
|
+
// MIL-269 (R32): the four em sub-agent definitions live in the agents dir, a directory shared
|
|
3021
3247
|
// with the consumer's own agents - install/sync/check only ever handle the fixed EM_AGENT_FILES.
|
|
3022
3248
|
function packagedAgentsRoot() {
|
|
3023
3249
|
return join(dirname(fileURLToPath(import.meta.url)), "..", ".claude", "agents");
|
|
3024
3250
|
}
|
|
3025
|
-
function
|
|
3026
|
-
|
|
3251
|
+
function vendoredDirs(repoRoot, opts) {
|
|
3252
|
+
const dirs = resolveVendoredDirs(repoRoot, opts);
|
|
3253
|
+
if (dirs.detected)
|
|
3254
|
+
console.log(`using ${relative(resolve(repoRoot), dirs.skillsRoot)}/ (no .claude/skills/ here; pass --skills-dir to choose)`);
|
|
3255
|
+
return dirs;
|
|
3027
3256
|
}
|
|
3257
|
+
const SKILLS_DIR_HELP = "where the vendored skill bundle lives, relative to the repo root (default .claude/skills; " +
|
|
3258
|
+
".agents/skills when that exists and .claude/skills does not — GH #412)";
|
|
3259
|
+
const AGENTS_DIR_HELP = "where the four em sub-agent definitions live, relative to the repo root (default .claude/agents, " +
|
|
3260
|
+
"or .agents/agents beside an .agents/skills bundle — GH #412)";
|
|
3028
3261
|
program
|
|
3029
3262
|
.command("contract")
|
|
3030
3263
|
.description("print the packaged implementation contract (reference/implement.md) to stdout — the " +
|
|
@@ -3066,9 +3299,8 @@ const skill = program
|
|
|
3066
3299
|
.description("manage Claude Code skills bundled with em");
|
|
3067
3300
|
// MIL-269: reconcile the four `.claude/agents/em-*.md` files (create the directory if absent);
|
|
3068
3301
|
// byte-exact restore, never touches any other file there.
|
|
3069
|
-
function installAgentFiles(
|
|
3302
|
+
function installAgentFiles(vendored) {
|
|
3070
3303
|
const packaged = packagedAgentsRoot();
|
|
3071
|
-
const vendored = vendoredAgentsRoot(repoRoot);
|
|
3072
3304
|
const plan = planAgentSync(packaged, vendored, EM_AGENT_FILES);
|
|
3073
3305
|
if (plan.changes.length === 0)
|
|
3074
3306
|
return;
|
|
@@ -3088,10 +3320,13 @@ skill
|
|
|
3088
3320
|
"bundle's own managed directories the same way `em skill sync` does — never touching an " +
|
|
3089
3321
|
"unrelated sibling skill (MIL-180)")
|
|
3090
3322
|
.option("--no-agents-md", "skip writing/updating the AGENTS.md agent-contract section (on by default, MIL-129)")
|
|
3323
|
+
.option("--skills-dir <dir>", SKILLS_DIR_HELP)
|
|
3324
|
+
.option("--agents-dir <dir>", AGENTS_DIR_HELP)
|
|
3091
3325
|
.action(async (opts) => {
|
|
3092
3326
|
warnVendoredDeprecated();
|
|
3093
3327
|
const srcRoot = packagedSkillsRoot();
|
|
3094
|
-
const
|
|
3328
|
+
const dirs = vendoredDirs(process.cwd(), opts);
|
|
3329
|
+
const destRoot = dirs.skillsRoot;
|
|
3095
3330
|
const anchor = join(destRoot, EM_SKILL_ANCHOR_DIR);
|
|
3096
3331
|
if (existsSync(anchor) && !opts.force) {
|
|
3097
3332
|
console.log(`skill already installed at ${anchor}`);
|
|
@@ -3121,7 +3356,7 @@ skill
|
|
|
3121
3356
|
`(${removedCount} removed), ${totalUnchanged} unchanged`);
|
|
3122
3357
|
}
|
|
3123
3358
|
console.log("in Claude Code, run /event-modeling to start a guided session");
|
|
3124
|
-
installAgentFiles(
|
|
3359
|
+
installAgentFiles(dirs.agentsRoot);
|
|
3125
3360
|
}
|
|
3126
3361
|
else {
|
|
3127
3362
|
await mkdir(destRoot, { recursive: true });
|
|
@@ -3130,7 +3365,7 @@ skill
|
|
|
3130
3365
|
}
|
|
3131
3366
|
console.log(`installed event-modeling skill bundle → ${destRoot}`);
|
|
3132
3367
|
console.log("in Claude Code, run /event-modeling to start a guided session");
|
|
3133
|
-
installAgentFiles(
|
|
3368
|
+
installAgentFiles(dirs.agentsRoot);
|
|
3134
3369
|
}
|
|
3135
3370
|
if (opts.agentsMd !== false) {
|
|
3136
3371
|
console.log(agentsMdMessage(syncAgentsMd(process.cwd())));
|
|
@@ -3142,15 +3377,18 @@ skill
|
|
|
3142
3377
|
"installed em package (overwrites unconditionally; local edits are never merged, MIL-93)")
|
|
3143
3378
|
.argument("[path]", "consumer repo root", ".")
|
|
3144
3379
|
.option("--no-agents-md", "skip writing/updating the AGENTS.md agent-contract section (on by default, MIL-129)")
|
|
3380
|
+
.option("--skills-dir <dir>", SKILLS_DIR_HELP)
|
|
3381
|
+
.option("--agents-dir <dir>", AGENTS_DIR_HELP)
|
|
3145
3382
|
.action(async (path, opts) => {
|
|
3146
3383
|
warnVendoredDeprecated();
|
|
3147
3384
|
const packagedRoot = packagedSkillsRoot();
|
|
3148
|
-
const
|
|
3385
|
+
const dirs = vendoredDirs(path, opts);
|
|
3386
|
+
const vendoredRoot = dirs.skillsRoot;
|
|
3149
3387
|
const bundlePlan = planSkillSyncBundle(packagedRoot, vendoredRoot, EM_ALL_SKILL_BUNDLE_DIRS);
|
|
3150
3388
|
const skillChanges = bundlePlan.reduce((n, { plan }) => n + plan.changes.length, 0);
|
|
3151
3389
|
// MIL-269 (R32): the fixed agent-file list, never the shared .claude/agents/ directory as a whole.
|
|
3152
3390
|
const packagedAgents = packagedAgentsRoot();
|
|
3153
|
-
const vendoredAgents =
|
|
3391
|
+
const vendoredAgents = dirs.agentsRoot;
|
|
3154
3392
|
const agentPlan = planAgentSync(packagedAgents, vendoredAgents, EM_AGENT_FILES);
|
|
3155
3393
|
const totalChanges = skillChanges + agentPlan.changes.length;
|
|
3156
3394
|
const totalUnchanged = bundlePlan.reduce((n, { plan }) => n + plan.unchangedCount, 0) + agentPlan.unchangedCount;
|
|
@@ -3181,9 +3419,13 @@ skill
|
|
|
3181
3419
|
.option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
|
|
3182
3420
|
.option("--ci", "also fail (exit 1) when the declared plugin is not registered on this machine (implied by CI=true in the environment); " +
|
|
3183
3421
|
"without it that finding prints the install commands on stderr and exits 0")
|
|
3422
|
+
.option("--skills-dir <dir>", SKILLS_DIR_HELP)
|
|
3423
|
+
.option("--agents-dir <dir>", AGENTS_DIR_HELP)
|
|
3184
3424
|
.action((path, opts) => {
|
|
3185
3425
|
const packagedRoot = packagedSkillsRoot();
|
|
3186
|
-
|
|
3426
|
+
// Detection is silent here: a check prints findings and one verdict line, nothing else.
|
|
3427
|
+
const dirs = resolveVendoredDirs(path, opts);
|
|
3428
|
+
const vendoredRoot = dirs.skillsRoot;
|
|
3187
3429
|
const ciMode = !!opts.ci || process.env.CI === "true";
|
|
3188
3430
|
// MIL-231: the plugin signal (a pinned marketplace in .claude/settings*.json) sits beside the
|
|
3189
3431
|
// vendored signal (the anchor directory). Plugin only -> only the plugin is checked; vendored
|
|
@@ -3196,7 +3438,7 @@ skill
|
|
|
3196
3438
|
// MIL-269 (R32): the four agent files, only when the vendored bundle is present (a plugin-only
|
|
3197
3439
|
// repo ships them via the plugin); checked by fixed name, never the shared directory.
|
|
3198
3440
|
const agents = vendoredPresent
|
|
3199
|
-
? checkAgentFiles(packagedAgentsRoot(),
|
|
3441
|
+
? checkAgentFiles(packagedAgentsRoot(), dirs.agentsRoot, EM_AGENT_FILES)
|
|
3200
3442
|
: { findings: [], ok: true };
|
|
3201
3443
|
const findings = [...vendored.findings, ...agents.findings, ...(pluginCheck?.findings ?? [])];
|
|
3202
3444
|
const failing = findings.filter((f) => f.code !== "plugin-not-installed-locally" || ciMode);
|
|
@@ -3373,7 +3615,74 @@ ci.command("init")
|
|
|
3373
3615
|
console.log(`${path} already up to date`);
|
|
3374
3616
|
}
|
|
3375
3617
|
});
|
|
3376
|
-
const storylineCmd = program.command("storyline").description("
|
|
3618
|
+
const storylineCmd = program.command("storyline").description("storylines (GH #277, #421): adopt storyline blocks, split them into storyline model files");
|
|
3619
|
+
storylineCmd
|
|
3620
|
+
.command("split")
|
|
3621
|
+
.description("move each inline `storyline \"X\" { … }` block into its own storyline model file <slug>/<slug>.em " +
|
|
3622
|
+
"(a `storyline \"X\" [source]` header, then the block's slices) and leave the one-line declaration " +
|
|
3623
|
+
"`storyline \"X\" [after \"Y\"]` in the root (GH #421). Slice docs stay under the root's slices/. " +
|
|
3624
|
+
"A storyline declared in several blocks is left alone (merge them first)")
|
|
3625
|
+
.argument("<file>", "the root .em model")
|
|
3626
|
+
.option("--storyline <name>", "split only this storyline")
|
|
3627
|
+
.option("--dry-run", "print what would be written without touching anything")
|
|
3628
|
+
.option("-f, --force", "overwrite a storyline model file that already exists")
|
|
3629
|
+
.action((file, opts) => {
|
|
3630
|
+
const { model, refs, diagnostics, source } = compileFile(file);
|
|
3631
|
+
printDiagnostics(diagnostics.filter((d) => d.severity === "error"));
|
|
3632
|
+
if (hasErrors(diagnostics)) {
|
|
3633
|
+
console.error("em storyline split: not splitting — fix the errors above");
|
|
3634
|
+
process.exit(1);
|
|
3635
|
+
}
|
|
3636
|
+
if (opts.storyline !== undefined && !(model.storylines ?? []).some((s) => s.name.trim().toLowerCase() === opts.storyline.trim().toLowerCase())) {
|
|
3637
|
+
console.error(`em storyline split: no storyline "${opts.storyline}" (declared: ${(model.storylines ?? []).map((s) => `"${s.name}"`).join(", ") || "none"})`);
|
|
3638
|
+
process.exit(1);
|
|
3639
|
+
}
|
|
3640
|
+
const plan = planStorylineSplit(model, source, opts.storyline);
|
|
3641
|
+
for (const s of plan.skipped)
|
|
3642
|
+
console.log(`skip "${s.storyline}" — ${s.reason}`);
|
|
3643
|
+
if (plan.files.length === 0) {
|
|
3644
|
+
console.log("nothing to split");
|
|
3645
|
+
return;
|
|
3646
|
+
}
|
|
3647
|
+
const rootDir = dirname(file);
|
|
3648
|
+
for (const f of plan.files) {
|
|
3649
|
+
const abs = resolve(rootDir, f.path);
|
|
3650
|
+
if (existsSync(abs) && !opts.force) {
|
|
3651
|
+
console.error(`em storyline split: ${f.path} already exists (use --force to overwrite)`);
|
|
3652
|
+
process.exit(1);
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
if (opts.dryRun) {
|
|
3656
|
+
for (const f of plan.files)
|
|
3657
|
+
console.log(`would write ${f.path} (storyline "${f.storyline}", ${f.content.split(/\r?\n/).filter((l) => /^slice\b/.test(l)).length} slice(s))`);
|
|
3658
|
+
console.log(`would rewrite ${file}`);
|
|
3659
|
+
return;
|
|
3660
|
+
}
|
|
3661
|
+
for (const f of plan.files) {
|
|
3662
|
+
const abs = resolve(rootDir, f.path);
|
|
3663
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
3664
|
+
writeFileSync(abs, f.content, "utf8");
|
|
3665
|
+
console.log(`wrote ${f.path} (storyline "${f.storyline}")`);
|
|
3666
|
+
// GH #421 step 2: the story's docs (and per-slice diagrams) move beside its file
|
|
3667
|
+
const st = (model.storylines ?? []).find((s) => s.name === f.storyline);
|
|
3668
|
+
const folder = dirname(abs);
|
|
3669
|
+
let moved = 0;
|
|
3670
|
+
for (const i of st?.sliceIndexes ?? []) {
|
|
3671
|
+
for (const ext of [".md", ".svg"]) {
|
|
3672
|
+
const src = join(rootDir, "slices", `${refs.sliceKeys[i]}${ext}`);
|
|
3673
|
+
if (!existsSync(src))
|
|
3674
|
+
continue;
|
|
3675
|
+
mkdirSync(join(folder, "slices"), { recursive: true });
|
|
3676
|
+
renameSync(src, join(folder, "slices", `${refs.sliceKeys[i]}${ext}`));
|
|
3677
|
+
moved++;
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
if (moved > 0)
|
|
3681
|
+
console.log(` moved ${moved} doc/diagram file(s) into ${relative(rootDir, join(folder, "slices"))}/`);
|
|
3682
|
+
}
|
|
3683
|
+
writeFileSync(file, plan.rootText, "utf8");
|
|
3684
|
+
console.log(`rewrote ${file}: ${plan.files.length} storyline declaration(s) now point at their files`);
|
|
3685
|
+
});
|
|
3377
3686
|
storylineCmd
|
|
3378
3687
|
.command("suggest")
|
|
3379
3688
|
.description("advisory (GH #290): propose `storyline` blocks for a model that has none — one per distinct slice " +
|
|
@@ -3516,8 +3825,8 @@ program
|
|
|
3516
3825
|
* stands, so an estate sees how many advisory shape findings it carries. Never writes a waive file:
|
|
3517
3826
|
* baselining shapes you accept is a decision (`--waive-file`, and a `shape-accepted:` Decisions-log bullet). */
|
|
3518
3827
|
function printUpgradeShapeSection(file) {
|
|
3519
|
-
const { model, diagnostics, refs,
|
|
3520
|
-
const all = computeAllDiagnostics(file, model, refs, diagnostics,
|
|
3828
|
+
const { model, diagnostics, refs, hashSource } = compileFile(file);
|
|
3829
|
+
const all = computeAllDiagnostics(file, model, refs, diagnostics, hashSource);
|
|
3521
3830
|
const report = buildShapeReport(all, []);
|
|
3522
3831
|
console.log("");
|
|
3523
3832
|
console.log("Shape (em validate --shape, advisory):");
|
|
@@ -3543,18 +3852,17 @@ function compileFile(file, opts = {},
|
|
|
3543
3852
|
// watcher (and the --serve live view with it) is a session-ending failure.
|
|
3544
3853
|
// One-shot commands keep the exit-with-message behavior.
|
|
3545
3854
|
{ survive = false } = {}) {
|
|
3546
|
-
|
|
3547
|
-
try {
|
|
3548
|
-
source = readFileSync(file, "utf8");
|
|
3549
|
-
}
|
|
3550
|
-
catch {
|
|
3855
|
+
if (!existsSync(file)) {
|
|
3551
3856
|
if (survive)
|
|
3552
3857
|
throw new Error(`cannot read ${file}`);
|
|
3553
3858
|
console.error(`cannot read ${file}`);
|
|
3554
3859
|
process.exit(1);
|
|
3555
3860
|
}
|
|
3556
3861
|
try {
|
|
3557
|
-
|
|
3862
|
+
// GH #421: the root plus every storyline model file it declares, compiled as one model.
|
|
3863
|
+
// `source` is the root text (line-based edits of root entities), `files`/`sources` the
|
|
3864
|
+
// storyline files, `hashSource` what a model-level hash covers.
|
|
3865
|
+
return compileModelFile(file, opts);
|
|
3558
3866
|
}
|
|
3559
3867
|
catch (e) {
|
|
3560
3868
|
if (e instanceof ParseError) {
|
|
@@ -3563,6 +3871,12 @@ function compileFile(file, opts = {},
|
|
|
3563
3871
|
console.error(`parse error in ${file} ${e.message}`);
|
|
3564
3872
|
process.exit(1);
|
|
3565
3873
|
}
|
|
3874
|
+
if (e instanceof ModelLoadError) {
|
|
3875
|
+
if (survive)
|
|
3876
|
+
throw new Error(e.message);
|
|
3877
|
+
console.error(e.message);
|
|
3878
|
+
process.exit(1);
|
|
3879
|
+
}
|
|
3566
3880
|
throw e;
|
|
3567
3881
|
}
|
|
3568
3882
|
}
|
|
@@ -3578,7 +3892,8 @@ function compileFile(file, opts = {},
|
|
|
3578
3892
|
* report, since its whole job is logging which categories fired. `source` is the `.em` file's
|
|
3579
3893
|
* own text — only `validateModelVersionStale` reads it (the modelHash comparison). */
|
|
3580
3894
|
function computeAllDiagnostics(file, model, refs, diagnostics, source) {
|
|
3581
|
-
|
|
3895
|
+
// GH #421: the fs-aware rules below point at elements too — give each its storyline file.
|
|
3896
|
+
return attributeDiagnosticFiles(model, refs, [
|
|
3582
3897
|
...diagnostics,
|
|
3583
3898
|
...validateLineage(model, refs, dirname(file)),
|
|
3584
3899
|
...validateFrontmatterCoherence(model, refs, dirname(file)),
|
|
@@ -3587,7 +3902,7 @@ function computeAllDiagnostics(file, model, refs, diagnostics, source) {
|
|
|
3587
3902
|
...validateOrphanedSliceDocs(model, refs, dirname(file)),
|
|
3588
3903
|
...validateModelVersionStale(model, refs, dirname(file), source),
|
|
3589
3904
|
...validateInvariants(model, refs, dirname(file)),
|
|
3590
|
-
];
|
|
3905
|
+
]);
|
|
3591
3906
|
}
|
|
3592
3907
|
/** Read a file's text, or exit with a clear error — shared by `em diff` (both forms) and
|
|
3593
3908
|
* `em glossary`, which each read one or more plain `.em` files off disk before compiling. */
|
|
@@ -3601,18 +3916,31 @@ function readFileOrExit(file) {
|
|
|
3601
3916
|
}
|
|
3602
3917
|
}
|
|
3603
3918
|
/** Parse+normalize source text for `em diff`, exiting with a labeled parse error on failure. */
|
|
3604
|
-
function compileSource(source, label) {
|
|
3919
|
+
function compileSource(source, label, read) {
|
|
3605
3920
|
try {
|
|
3606
|
-
|
|
3921
|
+
// GH #421: a split model's storyline files come through `read` (disk, or git at a revision).
|
|
3922
|
+
return compileModelText(source, read, label);
|
|
3607
3923
|
}
|
|
3608
3924
|
catch (e) {
|
|
3609
3925
|
if (e instanceof ParseError) {
|
|
3610
3926
|
console.error(`parse error in ${label} ${e.message}`);
|
|
3611
3927
|
process.exit(1);
|
|
3612
3928
|
}
|
|
3929
|
+
if (e instanceof ModelLoadError) {
|
|
3930
|
+
console.error(e.message);
|
|
3931
|
+
process.exit(1);
|
|
3932
|
+
}
|
|
3613
3933
|
throw e;
|
|
3614
3934
|
}
|
|
3615
3935
|
}
|
|
3936
|
+
/** GH #421: a reader for a text with no file behind it — a split root cannot be compiled from text alone. */
|
|
3937
|
+
const noStorylineFiles = (rel) => {
|
|
3938
|
+
throw new ModelLoadError(`storyline model file ${rel} is not available for this text`);
|
|
3939
|
+
};
|
|
3940
|
+
/** GH #421: storyline model files of `file` as of git revision `rev`. */
|
|
3941
|
+
function gitReader(file, rev) {
|
|
3942
|
+
return (rel) => readAtRevision(join(dirname(file), rel), rev);
|
|
3943
|
+
}
|
|
3616
3944
|
/** Resolve `file`'s content at a git revision, exiting with a clear error on any git failure. */
|
|
3617
3945
|
function readAtRevision(file, rev) {
|
|
3618
3946
|
const result = resolveRevision(file, rev);
|
|
@@ -3623,9 +3951,11 @@ function readAtRevision(file, rev) {
|
|
|
3623
3951
|
return result.content;
|
|
3624
3952
|
}
|
|
3625
3953
|
/** 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
|
-
|
|
3954
|
+
function runDiff(oldSource, oldLabel, newSource, newLabel, exitCode, json, lineage,
|
|
3955
|
+
// GH #421: how each side's storyline model files are read (disk, or git at a revision).
|
|
3956
|
+
readers = { old: noStorylineFiles, new: noStorylineFiles }) {
|
|
3957
|
+
const oldResult = compileSource(oldSource, oldLabel, readers.old);
|
|
3958
|
+
const newResult = compileSource(newSource, newLabel, readers.new);
|
|
3629
3959
|
printDiagnostics(oldResult.diagnostics);
|
|
3630
3960
|
printDiagnostics(newResult.diagnostics);
|
|
3631
3961
|
if (hasErrors(oldResult.diagnostics) || hasErrors(newResult.diagnostics)) {
|
|
@@ -3715,7 +4045,7 @@ function printDiagnostics(diags) {
|
|
|
3715
4045
|
if (diags.length === 0)
|
|
3716
4046
|
return;
|
|
3717
4047
|
for (const d of diags) {
|
|
3718
|
-
const line =
|
|
4048
|
+
const line = formatDiagnosticWithFile(d);
|
|
3719
4049
|
if (d.severity === "error")
|
|
3720
4050
|
console.error(line);
|
|
3721
4051
|
else
|