@milehimikey/em 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/.claude/skills/event-modeling/SKILL.md +1 -1
  2. package/.claude/skills/event-modeling-conform/SKILL.md +1 -1
  3. package/.claude/skills/event-modeling-design/SKILL.md +46 -5
  4. package/.claude/skills/event-modeling-discover/SKILL.md +43 -8
  5. package/.claude/skills/event-modeling-discover/reference/extract.md +6 -1
  6. package/.claude/skills/event-modeling-discover/reference/prd-rubric.md +102 -0
  7. package/.claude/skills/event-modeling-discover/reference/prd-spec-path.md +171 -0
  8. package/.claude/skills/event-modeling-engagement/SKILL.md +8 -3
  9. package/.claude/skills/event-modeling-implement/SKILL.md +1 -1
  10. package/.claude/skills/event-modeling-implement/reference/implement.md +8 -0
  11. package/.claude/skills/event-modeling-review/SKILL.md +1 -1
  12. package/.claude/skills/event-modeling-shared/reference/em-dsl.md +90 -5
  13. package/.claude/skills/event-modeling-shared/reference/methodology.md +268 -0
  14. package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +96 -2
  15. package/.claude/skills/event-modeling-shared/templates/slice.md +11 -3
  16. package/.claude-plugin/marketplace.json +2 -2
  17. package/dist/catalog/docModelConsistencyValidate.js +160 -0
  18. package/dist/catalog/docModelConsistencyValidate.js.map +1 -1
  19. package/dist/catalog/invariantsValidate.d.ts +2 -0
  20. package/dist/catalog/invariantsValidate.js +67 -1
  21. package/dist/catalog/invariantsValidate.js.map +1 -1
  22. package/dist/catalog/scenarioQualityValidate.d.ts +4 -0
  23. package/dist/catalog/scenarioQualityValidate.js +148 -0
  24. package/dist/catalog/scenarioQualityValidate.js.map +1 -0
  25. package/dist/catalog/scenarios.d.ts +5 -0
  26. package/dist/catalog/scenarios.js +80 -0
  27. package/dist/catalog/scenarios.js.map +1 -1
  28. package/dist/catalog/sliceReadyValidate.d.ts +8 -0
  29. package/dist/catalog/sliceReadyValidate.js +35 -3
  30. package/dist/catalog/sliceReadyValidate.js.map +1 -1
  31. package/dist/catalog/sliceSections.d.ts +3 -2
  32. package/dist/catalog/sliceSections.js +99 -9
  33. package/dist/catalog/sliceSections.js.map +1 -1
  34. package/dist/cli/ciInit.d.ts +4 -11
  35. package/dist/cli/ciInit.js +24 -6
  36. package/dist/cli/ciInit.js.map +1 -1
  37. package/dist/cli/coverage.d.ts +9 -0
  38. package/dist/cli/coverage.js +18 -0
  39. package/dist/cli/coverage.js.map +1 -1
  40. package/dist/cli/defer.d.ts +28 -0
  41. package/dist/cli/defer.js +87 -4
  42. package/dist/cli/defer.js.map +1 -1
  43. package/dist/cli/engagement.d.ts +2 -2
  44. package/dist/cli/engagement.js +24 -1
  45. package/dist/cli/engagement.js.map +1 -1
  46. package/dist/cli/markImplemented.d.ts +14 -10
  47. package/dist/cli/markImplemented.js +49 -1
  48. package/dist/cli/markImplemented.js.map +1 -1
  49. package/dist/cli/prdTriage.d.ts +109 -0
  50. package/dist/cli/prdTriage.js +378 -0
  51. package/dist/cli/prdTriage.js.map +1 -0
  52. package/dist/cli/render-inputs.d.ts +2 -0
  53. package/dist/cli/render-inputs.js +4 -0
  54. package/dist/cli/render-inputs.js.map +1 -1
  55. package/dist/cli/revise.d.ts +2 -0
  56. package/dist/cli/revise.js +7 -2
  57. package/dist/cli/revise.js.map +1 -1
  58. package/dist/cli/shape.d.ts +27 -0
  59. package/dist/cli/shape.js +77 -0
  60. package/dist/cli/shape.js.map +1 -0
  61. package/dist/cli/sliceIndex.d.ts +6 -1
  62. package/dist/cli/sliceIndex.js +28 -13
  63. package/dist/cli/sliceIndex.js.map +1 -1
  64. package/dist/cli/sliceNew.d.ts +4 -1
  65. package/dist/cli/sliceNew.js +26 -9
  66. package/dist/cli/sliceNew.js.map +1 -1
  67. package/dist/cli/status.d.ts +21 -1
  68. package/dist/cli/status.js +25 -3
  69. package/dist/cli/status.js.map +1 -1
  70. package/dist/cli/storylineSuggest.d.ts +26 -0
  71. package/dist/cli/storylineSuggest.js +252 -0
  72. package/dist/cli/storylineSuggest.js.map +1 -0
  73. package/dist/cli/upgrade.d.ts +15 -3
  74. package/dist/cli/upgrade.js +426 -7
  75. package/dist/cli/upgrade.js.map +1 -1
  76. package/dist/cli/upgradeReshape.d.ts +60 -0
  77. package/dist/cli/upgradeReshape.js +425 -0
  78. package/dist/cli/upgradeReshape.js.map +1 -0
  79. package/dist/cli.js +286 -29
  80. package/dist/cli.js.map +1 -1
  81. package/dist/emit/dot.d.ts +2 -0
  82. package/dist/emit/dot.js +44 -8
  83. package/dist/emit/dot.js.map +1 -1
  84. package/dist/emit/engagementJson.d.ts +1 -1
  85. package/dist/emit/engagementJson.js +3 -1
  86. package/dist/emit/engagementJson.js.map +1 -1
  87. package/dist/emit/json.d.ts +21 -1
  88. package/dist/emit/json.js +26 -2
  89. package/dist/emit/json.js.map +1 -1
  90. package/dist/emit/queryJson.d.ts +1 -1
  91. package/dist/emit/queryJson.js +3 -1
  92. package/dist/emit/queryJson.js.map +1 -1
  93. package/dist/emit/statusJson.d.ts +1 -1
  94. package/dist/emit/statusJson.js +6 -1
  95. package/dist/emit/statusJson.js.map +1 -1
  96. package/dist/emit/theme.d.ts +1 -0
  97. package/dist/emit/theme.js +13 -0
  98. package/dist/emit/theme.js.map +1 -1
  99. package/dist/emit/validateJson.d.ts +5 -2
  100. package/dist/emit/validateJson.js +36 -2
  101. package/dist/emit/validateJson.js.map +1 -1
  102. package/dist/layout/grid.d.ts +5 -1
  103. package/dist/layout/grid.js +93 -2
  104. package/dist/layout/grid.js.map +1 -1
  105. package/dist/mcp/server.js +46 -11
  106. package/dist/mcp/server.js.map +1 -1
  107. package/dist/model/edges.d.ts +7 -0
  108. package/dist/model/edges.js +33 -16
  109. package/dist/model/edges.js.map +1 -1
  110. package/dist/model/model.d.ts +33 -0
  111. package/dist/model/model.js +87 -3
  112. package/dist/model/model.js.map +1 -1
  113. package/dist/model/queryIndex.js +2 -2
  114. package/dist/model/queryIndex.js.map +1 -1
  115. package/dist/model/refs.js +8 -5
  116. package/dist/model/refs.js.map +1 -1
  117. package/dist/model/rules.d.ts +149 -0
  118. package/dist/model/rules.js +144 -0
  119. package/dist/model/rules.js.map +1 -1
  120. package/dist/model/shapeLint.d.ts +12 -0
  121. package/dist/model/shapeLint.js +170 -0
  122. package/dist/model/shapeLint.js.map +1 -0
  123. package/dist/model/storylines.d.ts +51 -0
  124. package/dist/model/storylines.js +188 -0
  125. package/dist/model/storylines.js.map +1 -0
  126. package/dist/model/validate.d.ts +6 -1
  127. package/dist/model/validate.js +317 -14
  128. package/dist/model/validate.js.map +1 -1
  129. package/dist/parser/againSugar.d.ts +30 -0
  130. package/dist/parser/againSugar.js +105 -0
  131. package/dist/parser/againSugar.js.map +1 -0
  132. package/dist/parser/ast.d.ts +33 -0
  133. package/dist/parser/lexer.d.ts +2 -0
  134. package/dist/parser/lexer.js +4 -0
  135. package/dist/parser/lexer.js.map +1 -1
  136. package/dist/parser/parser.js +284 -4
  137. package/dist/parser/parser.js.map +1 -1
  138. package/dist/query/format.js +1 -0
  139. package/dist/query/format.js.map +1 -1
  140. package/dist/query/verbs.d.ts +4 -0
  141. package/dist/query/verbs.js +30 -3
  142. package/dist/query/verbs.js.map +1 -1
  143. package/dist/render/drawEdges.js +100 -4
  144. package/dist/render/drawEdges.js.map +1 -1
  145. package/dist/render/render.js +20 -2
  146. package/dist/render/render.js.map +1 -1
  147. package/dist/render/sliceDiagram.js +5 -5
  148. package/dist/render/sliceDiagram.js.map +1 -1
  149. package/dist/render/storylineBands.d.ts +11 -0
  150. package/dist/render/storylineBands.js +80 -0
  151. package/dist/render/storylineBands.js.map +1 -0
  152. package/dist/render/storylineDiagram.d.ts +14 -0
  153. package/dist/render/storylineDiagram.js +142 -0
  154. package/dist/render/storylineDiagram.js.map +1 -0
  155. package/dist/roster/check.d.ts +32 -0
  156. package/dist/roster/check.js +92 -0
  157. package/dist/roster/check.js.map +1 -0
  158. package/dist/roster/context.d.ts +3 -0
  159. package/dist/roster/context.js +23 -0
  160. package/dist/roster/context.js.map +1 -0
  161. package/dist/roster/init.d.ts +15 -0
  162. package/dist/roster/init.js +52 -0
  163. package/dist/roster/init.js.map +1 -0
  164. package/dist/roster/roster.d.ts +79 -0
  165. package/dist/roster/roster.js +298 -0
  166. package/dist/roster/roster.js.map +1 -0
  167. package/package.json +1 -1
  168. package/plugin/.claude-plugin/plugin.json +2 -2
  169. package/plugin/shared/reference/em-dsl.md +90 -5
  170. package/plugin/shared/reference/methodology.md +268 -0
  171. package/plugin/shared/reference/slice-doc-schema.md +96 -2
  172. package/plugin/shared/templates/slice.md +11 -3
  173. package/plugin/skills/conform/SKILL.md +1 -1
  174. package/plugin/skills/design/SKILL.md +46 -5
  175. package/plugin/skills/discover/SKILL.md +43 -8
  176. package/plugin/skills/discover/reference/extract.md +6 -1
  177. package/plugin/skills/discover/reference/prd-rubric.md +103 -0
  178. package/plugin/skills/discover/reference/prd-spec-path.md +172 -0
  179. package/plugin/skills/engagement/SKILL.md +8 -3
  180. package/plugin/skills/event-modeling/SKILL.md +1 -1
  181. package/plugin/skills/implement/SKILL.md +1 -1
  182. package/plugin/skills/implement/reference/implement.md +8 -0
  183. package/plugin/skills/review/SKILL.md +1 -1
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from "node:fs";
4
4
  import { cp, mkdir } from "node:fs/promises";
5
- import { basename, dirname, extname, join, resolve } from "node:path";
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
8
  import { compile } from "./pipeline.js";
@@ -12,7 +12,8 @@ import { continuationOf } from "./model/continuation.js";
12
12
  import { ParseError } from "./parser/parser.js";
13
13
  import { renderDot, layoutDot, composeSvg, writeRendered, formatFromPath } from "./render/render.js";
14
14
  import { buildSliceDiagram } from "./render/sliceDiagram.js";
15
- import { resolveSliceArg, defaultSliceOut } from "./cli/render-inputs.js";
15
+ import { resolveSliceArg, defaultSliceOut, defaultStorylineOut } from "./cli/render-inputs.js";
16
+ import { buildStorylineDiagram, resolveStorylineArg } from "./render/storylineDiagram.js";
16
17
  import { resolveSliceKey } from "./model/resolveSlice.js";
17
18
  import { serializeBuilds, watchFile } from "./render/watch.js";
18
19
  import { startLiveServer } from "./render/serve.js";
@@ -20,7 +21,7 @@ import { formatDiagnostic, hasErrors, publicViewsWithoutInModelReader } from "./
20
21
  import { buildExport, buildSliceExport, GENERATOR_VERSION } from "./emit/json.js";
21
22
  import { generateContract, runApiCheck, formatApiCheckText } from "./cli/api.js";
22
23
  import { buildApiCheckJson } from "./emit/apiCheckJson.js";
23
- import { buildValidateJson, buildSliceReadyJson, buildValidateListJson, collectMarkers } from "./emit/validateJson.js";
24
+ import { buildValidateJson, buildShapeJson, buildSliceReadyJson, buildValidateListJson, collectMarkers } from "./emit/validateJson.js";
24
25
  import { buildDiffJson } from "./emit/diffJson.js";
25
26
  import { diffModels, formatModelDiff, hasChanges } from "./model/diff.js";
26
27
  import { planDiffArgs, realGit, resolveRevision, resolveDocAtRevision } from "./cli/diff-inputs.js";
@@ -34,6 +35,11 @@ import { validateOrphanedSliceDocs } from "./catalog/orphanedSliceDocValidate.js
34
35
  import { validateModelVersionStale } from "./catalog/modelVersionValidate.js";
35
36
  import { validateInvariants } from "./catalog/invariantsValidate.js";
36
37
  import { computeSliceReadiness } from "./catalog/sliceReadyValidate.js";
38
+ import { checkRosterForWrite, findRosterAbove, loadRoster, ROSTER_FILE } from "./roster/roster.js";
39
+ import { observeSigners, renderRoster } from "./roster/init.js";
40
+ import { rosterContextFor } from "./roster/context.js";
41
+ import { computeModelKey } from "./model/qualifiedRef.js";
42
+ import { checkRoster, formatRosterCheck, buildRosterCheckJson, grandfatheredNotice } from "./roster/check.js";
37
43
  import { applyEngagementClose, applyEngagementSet, buildNewEngagement, engagementMembersFor, engagementPlanFor, engagementStatusFor, formatEngagementPlanText, formatEngagementStatusText, } from "./cli/engagement.js";
38
44
  import { LEDGER_STATES, engagementPath, loadEngagement, readOpenEngagements } from "./cli/engagementFile.js";
39
45
  import { buildEngagementFoundationJson, buildEngagementPlanJson, buildEngagementStatusJson } from "./emit/engagementJson.js";
@@ -43,6 +49,7 @@ import { checkLedger, readLedgerWaiverTrailers, applyLedgerWaivers } from "./cli
43
49
  import { planMigration, verifyMigration } from "./cli/migrateReactionShape.js";
44
50
  import { buildLedgerJson } from "./emit/ledgerJson.js";
45
51
  import { buildRefsJson } from "./emit/refsJson.js";
52
+ import { buildShapeReport, formatShapeText } from "./cli/shape.js";
46
53
  import { applyRefWaivers, checkRefs, readWaiveFile } from "./cli/refsCheck.js";
47
54
  import { buildCoverageReport, resolveScopedSlices, resolveCoverageSliceKey } from "./cli/coverage.js";
48
55
  import { buildCoverageJson } from "./emit/coverageJson.js";
@@ -68,7 +75,7 @@ import { planGlossaryArgs } from "./cli/glossary-inputs.js";
68
75
  import { buildCatalog } from "./catalog/build.js";
69
76
  import { planCatalogArgs } from "./cli/catalog-inputs.js";
70
77
  import { runSliceIndex } from "./cli/sliceIndex.js";
71
- import { runMarkImplemented } from "./cli/markImplemented.js";
78
+ import { formatNextDraftNotice, runMarkImplemented } from "./cli/markImplemented.js";
72
79
  import { runRatify, parseMeaningFlags, meaningConfirmationLine } from "./cli/ratify.js";
73
80
  import { runReview } from "./cli/review.js";
74
81
  import { runConformSupersede } from "./cli/conformSupersede.js";
@@ -83,6 +90,7 @@ import { buildChangelogDoc } from "./cli/changelogBuild.js";
83
90
  import { runRevise } from "./cli/revise.js";
84
91
  import { runDefer, runDeferFromIssue } from "./cli/defer.js";
85
92
  import { runSliceConform } from "./cli/sliceConform.js";
93
+ import { checkPrdTriage, buildCheckPrdTriageJson, renderPrdTriageMarkdown, validatePrdTriage } from "./cli/prdTriage.js";
86
94
  import { checkFindingsFile, buildCheckFindingsJson, lookupFindingsBesideReport, unruledFindingsInScope, FINDING_LOCI } from "./cli/findings.js";
87
95
  import { STATE_FILE_NAME, PHASES, isPhase, loadStateFile, parseState, setPhase, setConformance, setModelVersion, setCertified, setEmVersion, setReview, isValidDateString, modelPathMismatch, } from "./cli/stateFile.js";
88
96
  import { findCertifiedVersion, latestModelVersionNumber, modelVersionDrift, runCertifyModelVersion, runModelVersionBump, } from "./cli/modelVersion.js";
@@ -90,6 +98,7 @@ import { STARTER_EM, starterEmFor, scaffoldReadme, scaffoldStateFile, scaffoldCo
90
98
  import { kebabSlug } from "./util/slug.js";
91
99
  import { ciWorkflowPath, conformWorkflowPath, buildCiWorkflowFile, buildConformWorkflowFile, buildCiWorkflowFileMulti, buildConformWorkflowFileMulti, ciManagedBody, ciManagedBodyMulti, conformManagedBody, conformManagedBodyMulti, ciModelsFromManifest, describeOtherModels, describeSetChange, fileBlockModels, planCiFile, applyCiFile, findUnsafeCiInitArg, CI_WORKFLOW_MARKER, CONFORM_WORKFLOW_MARKER, } from "./cli/ciInit.js";
92
100
  import { resolveRepoRoot, detectUpgrade, applyUpgrade, checkUpgrade } from "./cli/upgrade.js";
101
+ import { buildStorylineSuggestion } from "./cli/storylineSuggest.js";
93
102
  import { buildUpgradeJson } from "./emit/upgradeJson.js";
94
103
  import { isOlder } from "./util/semver.js";
95
104
  import { RULES } from "./model/rules.js";
@@ -189,6 +198,9 @@ program
189
198
  .option("-T, --format <fmt>", "output format (svg, png, pdf, ...)")
190
199
  .option("--slice <name>", "render only this slice, redrawn in its own canonical pattern shape " +
191
200
  "(default out: slices/<kebab-slug>.svg)")
201
+ .option("--storyline <name>", "render only this storyline: its slices in order, preceded by dimmed stub columns for what " +
202
+ "they read from outside it (and the end state of a storyline it comes `after`) " +
203
+ "(default out: storylines/<kebab-slug>.svg)")
192
204
  .option("--emit-dot", "print the generated DOT instead of rendering")
193
205
  .option("--keep-empty-lanes", "keep the API lane even when empty")
194
206
  .action(async (file, opts) => {
@@ -196,6 +208,10 @@ program
196
208
  console.error("em render: --slice cannot be combined with --emit-dot");
197
209
  process.exit(1);
198
210
  }
211
+ if (opts.storyline !== undefined && (opts.slice !== undefined || opts.emitDot)) {
212
+ console.error(`em render: --storyline cannot be combined with ${opts.slice !== undefined ? "--slice" : "--emit-dot"}`);
213
+ process.exit(1);
214
+ }
199
215
  const { dot, model, grid, diagnostics, refs } = compileFile(file, {
200
216
  keepEmptyLanes: opts.keepEmptyLanes,
201
217
  });
@@ -220,6 +236,22 @@ program
220
236
  console.error("not rendering: fix the errors above");
221
237
  process.exit(1);
222
238
  }
239
+ if (opts.storyline !== undefined) {
240
+ const found = resolveStorylineArg(model, opts.storyline);
241
+ if ("error" in found) {
242
+ console.error(found.error);
243
+ process.exit(1);
244
+ }
245
+ const out = opts.out ?? defaultStorylineOut(file, found.storyline.name);
246
+ const fmt = opts.format ?? formatFromPath(out);
247
+ await mkdir(dirname(out), { recursive: true }); // storylines/ may not exist yet
248
+ const sub = buildStorylineDiagram(model, found.storyline, { keepEmptyLanes: opts.keepEmptyLanes });
249
+ const raw = await layoutDot(sub.dot);
250
+ const svg = composeSvg(raw, sub.model, sub.grid, dirname(file), dirname(out));
251
+ await writeRendered(svg, out, fmt);
252
+ console.log(`rendered ${out}`);
253
+ return;
254
+ }
223
255
  if (opts.slice !== undefined) {
224
256
  const lookup = resolveSliceArg(opts.slice, model.slices);
225
257
  if ("error" in lookup) {
@@ -539,6 +571,7 @@ slice
539
571
  // MIL-266: with a model in hand (--wire), the doc's generated regions are filled from it;
540
572
  // without one they carry the template's placeholders until `em slice sync` fills them.
541
573
  let regions;
574
+ let alternatives = [];
542
575
  if (opts.wire) {
543
576
  const { model, refs, diagnostics, source } = compileFile(opts.wire);
544
577
  printDiagnostics(diagnostics);
@@ -553,11 +586,13 @@ slice
553
586
  }
554
587
  wired = result;
555
588
  regions = buildSliceRegions(model, [refs.sliceKeys.indexOf(key)]);
589
+ const sliceEvents = model.slices[refs.sliceKeys.indexOf(key)].elements.filter((e) => e.kind === "event");
590
+ alternatives = sliceEvents.filter((e) => e.alternativeGroup !== undefined).map((e) => e.name);
556
591
  }
557
592
  mkdirSync(dir, { recursive: true });
558
593
  const content = opts.stub
559
594
  ? buildStubDocContent(name, opts.pattern, opts.swimlane)
560
- : buildSliceDocContent(name, key, opts.pattern, opts.swimlane, regions);
595
+ : buildSliceDocContent(name, key, opts.pattern, opts.swimlane, regions, alternatives);
561
596
  writeFileSync(path, content);
562
597
  console.log(`wrote ${path}`);
563
598
  if (wired) {
@@ -698,6 +733,7 @@ slice
698
733
  .argument("<slice-key>", "slice export key (kebab-case)")
699
734
  .argument("<pr-url>", "merged PR (or commit) URL")
700
735
  .option("--on <date>", "merge date recorded as `shippedOn:`, YYYY-MM-DD (default: today, local date; MIL-284)")
736
+ .option("--json", "print a JSON document instead of text: { path, changed, implementedIn, nextDraft: { version, reopened[] } | null } (GH #285)")
701
737
  .action((file, sliceKey, prUrl, opts) => {
702
738
  if (opts.on !== undefined && !isValidDateString(opts.on)) {
703
739
  console.error(`em slice mark-implemented: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
@@ -720,13 +756,21 @@ slice
720
756
  console.error(`em slice mark-implemented: ${result.message}`);
721
757
  process.exit(1);
722
758
  }
759
+ if (opts.json) {
760
+ console.log(JSON.stringify({
761
+ path: result.path,
762
+ changed: result.changed,
763
+ implementedIn: prUrl,
764
+ nextDraft: result.openedDraft ? { version: result.openedDraft.version, reopened: result.openedDraft.titles } : null,
765
+ }, null, 2));
766
+ return;
767
+ }
723
768
  console.log(result.changed
724
769
  ? `marked implemented: ${result.path} (implementedIn: ${prUrl})`
725
770
  : `already implemented (no-op): ${result.path}`);
726
- if (result.ok && result.openedDraft) {
727
- console.log(`opened draft v${result.openedDraft.version}: ${result.path} — re-opened ${result.openedDraft.reopened} deferred question(s)` +
728
- (result.openedDraft.injected > 0 ? `, injected ${result.openedDraft.injected} queued question(s) (GH #236)` : "") +
729
- `; v${result.openedDraft.version - 1} stays shipped`);
771
+ if (result.openedDraft) {
772
+ console.log("");
773
+ console.log(formatNextDraftNotice(result.path, result.openedDraft, prUrl));
730
774
  }
731
775
  });
732
776
  slice
@@ -741,6 +785,7 @@ slice
741
785
  .argument("<slice-key>", "slice export key (kebab-case)")
742
786
  .requiredOption("--by <name>", "the reviewer's (or facilitator's) name")
743
787
  .option("--on <date>", "review date, YYYY-MM-DD (default: today, local date)")
788
+ .option("--roster <path>", "ratifier roster to check --by against (GH #259; default: the nearest ratifiers.yaml above the model, none = unrestricted)")
744
789
  .action((file, sliceKey, opts) => {
745
790
  const { model, refs, diagnostics } = compileFile(file);
746
791
  printDiagnostics(diagnostics);
@@ -752,6 +797,11 @@ slice
752
797
  console.error(`em slice review: slice "${sliceKey}" has errors — fix them first`);
753
798
  process.exit(1);
754
799
  }
800
+ const rosterRefused = checkRosterForWrite(loadRoster(dirname(file), opts.roster, rosterContextFor(model, file)), "reviewers", opts.by, model, refs.sliceKeys.indexOf(sliceKey), computeModelKey(model, file));
801
+ if (rosterRefused !== null) {
802
+ console.error(`em slice review: ${rosterRefused}`);
803
+ process.exit(1);
804
+ }
755
805
  const today = localIsoDate();
756
806
  const reviewedOn = opts.on ?? today;
757
807
  if (opts.on !== undefined && !isValidDateString(opts.on)) {
@@ -794,6 +844,7 @@ slice
794
844
  .option("--no-ref", "record ratifiedHash: only, no ratifiedRef: (GH #255) — the signed content keeps its identity, nothing " +
795
845
  "points at a commit a squash or rebase merge would orphan; the uncommitted-changes check does not apply")
796
846
  .option("--on <date>", "ratification date, YYYY-MM-DD (default: today, local date)")
847
+ .option("--roster <path>", "ratifier roster to check --by against (GH #259; default: the nearest ratifiers.yaml above the model, none = unrestricted)")
797
848
  .option("--skip-review", "ratify without a recorded review — prints a loud notice on stderr and records `reviewSkipped: <status>` on the doc " +
798
849
  "(GH #260) so the bypass is visible to later readers, `em export` and `--slice-ready --json`; cleared by `em slice revise`")
799
850
  .option("--meaning-unchanged", "API-first sign-off (MIL-238): this version does not change what the public contract means — " +
@@ -818,6 +869,11 @@ slice
818
869
  console.error(`em slice ratify: slice "${sliceKey}" has errors — fix them first`);
819
870
  process.exit(1);
820
871
  }
872
+ const rosterRefused = checkRosterForWrite(loadRoster(dirname(file), opts.roster, rosterContextFor(model, file)), "ratifiers", opts.by, model, refs.sliceKeys.indexOf(sliceKey), computeModelKey(model, file));
873
+ if (rosterRefused !== null) {
874
+ console.error(`em slice ratify: ${rosterRefused}`);
875
+ process.exit(1);
876
+ }
821
877
  const today = localIsoDate();
822
878
  const ratifiedOn = opts.on ?? today;
823
879
  if (opts.on !== undefined && !isValidDateString(opts.on)) {
@@ -884,9 +940,13 @@ function reviseAction(commandName) {
884
940
  const what = result.kind === "shipped"
885
941
  ? `v${result.newVersion - 1} stays shipped; v${result.newVersion} opened as draft`
886
942
  : result.kind === "unshipped"
887
- ? `ratification of v${result.newVersion - 1} withdrawn (never shipped); v${result.newVersion} opened as draft`
943
+ ? result.shippedVersion !== null
944
+ ? `ratification of v${result.newVersion - 1} withdrawn; v${result.shippedVersion} stays shipped; v${result.newVersion} opened as draft`
945
+ : `ratification of v${result.newVersion - 1} withdrawn (never shipped); v${result.newVersion} opened as draft`
888
946
  : result.kind === "unshipped-kept"
889
- ? `ratification of v${result.newVersion} withdrawn (never shipped); reopened as draft at the same version (--keep-version)`
947
+ ? result.shippedVersion !== null
948
+ ? `ratification of v${result.newVersion} withdrawn; v${result.shippedVersion} stays shipped; reopened as draft at the same version (--keep-version)`
949
+ : `ratification of v${result.newVersion} withdrawn (never shipped); reopened as draft at the same version (--keep-version)`
890
950
  : `v${result.newVersion} was never signed; reopened as draft at the same version`;
891
951
  console.log(`revised: ${result.path} (status: draft, version: ${result.newVersion} — ${what})`);
892
952
  if (result.reopened > 0)
@@ -1495,6 +1555,45 @@ conformFindings
1495
1555
  if (!result.ok)
1496
1556
  process.exit(1);
1497
1557
  });
1558
+ const discover = program.command("discover").description("checks for the discover skill's records (GH #295)");
1559
+ discover
1560
+ .command("triage-check")
1561
+ .description("validate a discovery/prd-triage.json (prdTriageSchemaVersion 1.0): shape, the shipped rubric " +
1562
+ "version, every statement/storyline quote appearing verbatim in the input document, and every " +
1563
+ "statement carrying a category, an outcome and (modeled/rewritten) a group + storyline — exit 1 " +
1564
+ "on errors. The discover skill writes this file directly; this is how a headless run verifies it.")
1565
+ .argument("<file>", "path to a discovery/prd-triage.json file")
1566
+ .option("--source <path>", "the input PRD/spec to check quotes against (default: the file's own `source`, relative to it)")
1567
+ .option("--markdown", "after a passing check, print the Markdown view of the record instead of the summary")
1568
+ .option("--json", "print a JSON document instead of the text report")
1569
+ .action((file, opts) => {
1570
+ const result = checkPrdTriage(file, { source: opts.source });
1571
+ if (opts.json) {
1572
+ console.log(buildCheckPrdTriageJson(result));
1573
+ }
1574
+ else if (result.ok) {
1575
+ if (opts.markdown) {
1576
+ const shape = validatePrdTriage(JSON.parse(readFileSync(file, "utf8")));
1577
+ if (shape.ok)
1578
+ process.stdout.write(renderPrdTriageMarkdown(shape.doc, shape.referenceOnlyGroups));
1579
+ }
1580
+ else {
1581
+ console.log(`ok — ${result.path}: ${result.storylines} storyline(s), ${result.groups} group(s), ${result.statements} statement(s), quotes verbatim in ${relative(process.cwd(), result.source) || result.source}`);
1582
+ if (result.referenceOnlyGroups.length > 0) {
1583
+ console.log("reference-only groups — operations no walkthrough touches; check these are not CRUD fill:");
1584
+ for (const g of result.referenceOnlyGroups)
1585
+ console.log(` - ${g}`);
1586
+ }
1587
+ }
1588
+ }
1589
+ else {
1590
+ console.error(`em discover triage-check: ${result.path} — errors:`);
1591
+ for (const e of result.errors)
1592
+ console.error(` - ${e}`);
1593
+ }
1594
+ if (!result.ok)
1595
+ process.exit(1);
1596
+ });
1498
1597
  program
1499
1598
  .command("watch")
1500
1599
  .description("re-render on every save")
@@ -1587,24 +1686,65 @@ program
1587
1686
  .option("--require-ratified-hash", "with --slice-ready (GH #256): strict mode — a ready-to-implement doc with no `ratifiedHash:` is NOT ready " +
1588
1687
  "(`slice-ready-missing-ratified-hash`, error; `ready: false` in --json). Off by default so docs ratified " +
1589
1688
  "before em 1.15 keep passing; turn it on once legacy docs are re-ratified")
1689
+ .option("--strict", "promote advisory lints (GH #293: scenario-quality warnings) to errors: they fail the run and, with --slice-ready, block readiness")
1690
+ .option("--roster <path>", "with --slice-ready (GH #259): the ratifier roster to check `ratifiedBy` against (default: the nearest " +
1691
+ "ratifiers.yaml above the model; none = unrestricted). A name the roster does not permit for the slice's " +
1692
+ "context is `ratified-by-not-in-roster` (error)")
1590
1693
  .option("--check-signed-bodies", "opt-in (GH #257): compare every slice doc that records a `ratifiedHash:` — ready, implemented, any status — " +
1591
1694
  "to its body; a mismatch is `slice-body-changed-since-ratification` (error). A pre-1.17 whole-body hash with an " +
1592
1695
  "intact body passes; a deferral written by `em slice defer` is outside the hash. The one check that still " +
1593
1696
  "watches a shipped doc's signed content")
1697
+ .option("--shape", "GH #282: run only the advisory shape-family lints (storyline shape) and print rule / count / first ref / docs " +
1698
+ "anchor; exit 0 always. With --json: `shapeSchemaVersion` 1.0 with per-rule finding arrays")
1699
+ .option("--waive-file <path>", "with --shape: a baseline file (`<rule-code> <slice-key>` per line, or a bare slice key for every shape rule; `#` comments) whose shape findings are listed " +
1700
+ "as waived and not counted; missing file = no waivers")
1594
1701
  .option("--json", "print a JSON document instead of text — works on a model WITH errors, unlike `em export` " +
1595
1702
  "(MIL-128, see docs/cli.md); exit codes are unchanged")
1596
1703
  .action((file, opts) => {
1704
+ if (opts.roster !== undefined && !opts.sliceReady) {
1705
+ console.error("em validate: --roster applies only with --slice-ready <key>");
1706
+ process.exit(1);
1707
+ }
1597
1708
  if (opts.requireRatifiedHash && !opts.sliceReady) {
1598
1709
  console.error("em validate: --require-ratified-hash applies only with --slice-ready <key>");
1599
1710
  process.exit(1);
1600
1711
  }
1712
+ if (opts.waiveFile && !opts.shape) {
1713
+ console.error("em validate: --waive-file applies only with --shape");
1714
+ process.exit(1);
1715
+ }
1716
+ if (opts.shape && opts.strict) {
1717
+ console.error("em validate: --strict applies only without --shape (shape findings never gate)");
1718
+ process.exit(1);
1719
+ }
1720
+ if (opts.shape && (opts.sliceReady || opts.listIssues || opts.listDivergences || opts.listPublic || opts.failOnIssues || opts.checkSignedBodies)) {
1721
+ console.error("em validate: --shape cannot be combined with --slice-ready, --list-*, --fail-on-issues or --check-signed-bodies");
1722
+ process.exit(1);
1723
+ }
1601
1724
  const { model, diagnostics, refs, source } = compileFile(file);
1725
+ if (opts.shape) {
1726
+ const all = computeAllDiagnostics(file, model, refs, diagnostics, source);
1727
+ const report = buildShapeReport(all, opts.waiveFile ? readWaiveFile(opts.waiveFile) : []);
1728
+ const modelErrors = all.filter((d) => d.severity === "error").length;
1729
+ for (const k of report.staleWaivers)
1730
+ console.error(`note: waiver "${k}" (${opts.waiveFile}) excuses no shape finding — ignored`);
1731
+ if (opts.json)
1732
+ process.stdout.write(buildShapeJson(file, report, modelErrors) + "\n");
1733
+ else
1734
+ console.log(formatShapeText(report, modelErrors));
1735
+ return;
1736
+ }
1602
1737
  // GH #257: an opt-in addition to the unconditional set, so --json, the text report and the
1603
1738
  // exit code all see it the same way every other diagnostic is seen.
1604
1739
  const allDiagnostics = [
1605
1740
  ...computeAllDiagnostics(file, model, refs, diagnostics, source),
1606
1741
  ...(opts.checkSignedBodies ? validateSignedBodies(model, refs, dirname(file)) : []),
1607
1742
  ];
1743
+ // GH #293: --strict promotes advisory lints to errors (the --slice-ready branch below does
1744
+ // its own promotion inside computeSliceReadiness, from the unpromoted set).
1745
+ const reported = opts.strict && !opts.sliceReady
1746
+ ? allDiagnostics.map((d) => RULES[d.code]?.advisory ? { ...d, severity: "error" } : d)
1747
+ : allDiagnostics;
1608
1748
  if (opts.sliceReady) {
1609
1749
  // MIL-87: a targeted, single-slice readiness gate, not part of the unconditional
1610
1750
  // diagnostic set above (see sliceReadyValidate.ts's header for why). Folds in MIL-85's
@@ -1629,15 +1769,17 @@ program
1629
1769
  // `slice-ready-unknown-slice` error (carrying the resolver's message).
1630
1770
  const resolved = resolveSliceKey(model, refs, opts.sliceReady);
1631
1771
  const key = resolved.ok ? resolved.key : opts.sliceReady;
1632
- const { scoped, ready, result } = computeSliceReadiness(model, refs, dirname(file), key, contractInput, allDiagnostics, {
1772
+ const { scoped, advisories, ready, result } = computeSliceReadiness(model, refs, dirname(file), key, contractInput, allDiagnostics, {
1633
1773
  requireRatifiedHash: opts.requireRatifiedHash === true,
1774
+ strict: opts.strict === true,
1775
+ rosterPath: opts.roster,
1634
1776
  });
1635
- printDiagnostics(scoped);
1777
+ printDiagnostics([...scoped, ...advisories]);
1636
1778
  if (opts.json) {
1637
1779
  // MIL-128: the named gates (see computeSliceReadyGates) plus the same `scoped`
1638
1780
  // diagnostics and `ready` verdict driving the exit code below — replaces both the
1639
1781
  // scraped warning prose and the two hand-parsed English sentences.
1640
- process.stdout.write(buildSliceReadyJson(file, key, result?.gates ?? null, scoped, ready, result?.continuationOf ?? null, result?.reviewSkipped ?? null) + "\n");
1782
+ process.stdout.write(buildSliceReadyJson(file, key, result?.gates ?? null, scoped, ready, result?.continuationOf ?? null, result?.reviewSkipped ?? null, advisories) + "\n");
1641
1783
  }
1642
1784
  else {
1643
1785
  console.log(ready ? `slice "${key}" is ready-to-implement` : `slice "${key}" is NOT ready-to-implement`);
@@ -1656,7 +1798,7 @@ program
1656
1798
  if (opts.listIssues || opts.listDivergences || opts.listPublic) {
1657
1799
  // Errors still fail the run below — surface them rather than exiting
1658
1800
  // non-zero with nothing but the list on screen.
1659
- const errorsOnly = allDiagnostics.filter((d) => d.severity === "error");
1801
+ const errorsOnly = reported.filter((d) => d.severity === "error");
1660
1802
  if (opts.json) {
1661
1803
  const markers = collectMarkers(model, refs, {
1662
1804
  issues: opts.listIssues,
@@ -1677,15 +1819,15 @@ program
1677
1819
  }
1678
1820
  }
1679
1821
  else if (opts.json) {
1680
- printDiagnostics(allDiagnostics);
1681
- process.stdout.write(buildValidateJson(file, allDiagnostics) + "\n");
1822
+ printDiagnostics(reported);
1823
+ process.stdout.write(buildValidateJson(file, reported) + "\n");
1682
1824
  }
1683
1825
  else {
1684
- printDiagnostics(allDiagnostics);
1685
- if (allDiagnostics.length === 0)
1826
+ printDiagnostics(reported);
1827
+ if (reported.length === 0)
1686
1828
  console.log("ok — no issues");
1687
1829
  }
1688
- if (hasErrors(allDiagnostics))
1830
+ if (hasErrors(reported))
1689
1831
  process.exit(1);
1690
1832
  if (opts.failOnIssues && model.elements.some((el) => el.issue))
1691
1833
  process.exit(1);
@@ -2046,7 +2188,7 @@ program
2046
2188
  .filter((e) => e !== null);
2047
2189
  // MIL-239 (R11): the nearest system.yaml above the first input file, consumer-adaptation counted.
2048
2190
  const system = statusSystemBlock(files[0], realGit, (m) => console.error(`warn: em status: ${m}`));
2049
- const report = buildStatusReport(files, sliceFacts, openIssuesCount, invariants, conformance, statusDiagnostics, modelVersion, emVersion, system, readOpenEngagements(files));
2191
+ const report = buildStatusReport(files, sliceFacts, openIssuesCount, invariants, conformance, statusDiagnostics, modelVersion, emVersion, system, readOpenEngagements(files), loadRoster(dirname(files[0])).state);
2050
2192
  let output;
2051
2193
  if (opts.json)
2052
2194
  output = buildStatusJson(report);
@@ -2233,9 +2375,10 @@ query
2233
2375
  .option("--context <c>", "match a slice with an event in this @Context")
2234
2376
  .option("--persona <p>", "match a slice with a ui in this @Persona")
2235
2377
  .option("--tag <t>", "match a slice with an event carrying this tag key")
2378
+ .option("--storyline <name>", "match a slice declared in this `storyline` block")
2236
2379
  .option("--json", "print a JSON document instead of the text report")
2237
2380
  .action((files, opts) => {
2238
- const filters = { pattern: opts.pattern, status: opts.status, context: opts.context, persona: opts.persona, tag: opts.tag };
2381
+ const filters = { pattern: opts.pattern, status: opts.status, context: opts.context, persona: opts.persona, tag: opts.tag, storyline: opts.storyline };
2239
2382
  runQueryVerb(files, "slices", filters, opts.json, (s) => querySlices(s, filters), formatSlices);
2240
2383
  });
2241
2384
  query
@@ -2459,6 +2602,69 @@ engagement
2459
2602
  writeFileSync(loaded.path, result.text);
2460
2603
  console.log(`closed engagement "${slug}" (${loaded.path})`);
2461
2604
  });
2605
+ const rosterCommand = program
2606
+ .command("roster")
2607
+ .description("the optional ratifier roster (ratifiers.yaml, GH #259) -- who may sign slice docs");
2608
+ rosterCommand
2609
+ .command("init")
2610
+ .description("write ratifiers.yaml beside the model, seeded from the names already recorded in the slice docs' " +
2611
+ "`ratifiedBy:`/`reviewedBy:` (whole-model entries; narrow them with `contexts:` by hand). Refuses " +
2612
+ "to overwrite an existing file unless --force; `em upgrade`'s grandfathered placeholder is not a name")
2613
+ .argument("<file>", "input .em file")
2614
+ .option("--roster <path>", "write the roster here instead of <model dir>/ratifiers.yaml")
2615
+ .option("--force", "overwrite an existing roster file")
2616
+ .action((file, opts) => {
2617
+ const { model, refs, diagnostics } = compileFile(file);
2618
+ printDiagnostics(diagnostics);
2619
+ const target = opts.roster ?? join(dirname(file), ROSTER_FILE);
2620
+ if (existsSync(target) && !opts.force) {
2621
+ console.error(`em roster init: ${target} already exists -- pass --force to overwrite it`);
2622
+ process.exit(1);
2623
+ }
2624
+ // A roster already governing this model from higher up would be shadowed by a nearer one.
2625
+ const governing = findRosterAbove(dirname(file));
2626
+ if (governing !== null && resolve(governing) !== resolve(target) && !opts.force) {
2627
+ console.error(`em roster init: ${governing} already governs this model -- writing ${target} would shadow it; pass --force to write it anyway`);
2628
+ process.exit(1);
2629
+ }
2630
+ const observed = observeSigners(model, refs, dirname(file));
2631
+ if (observed.ratifiers.length === 0 && observed.reviewers.length === 0) {
2632
+ const gfNone = grandfatheredNotice(observed.grandfathered);
2633
+ if (gfNone)
2634
+ console.log(gfNone);
2635
+ console.error("em roster init: no ratifiedBy/reviewedBy names found in the slice docs -- write ratifiers.yaml by hand (docs/slice-doc-schema.md#who-may-ratify)");
2636
+ process.exit(1);
2637
+ }
2638
+ writeFileSync(target, renderRoster(observed), "utf8");
2639
+ console.log(`wrote ${target} (${observed.ratifiers.length} ratifier(s), ${observed.reviewers.length} reviewer(s))`);
2640
+ const gf = grandfatheredNotice(observed.grandfathered);
2641
+ if (gf)
2642
+ console.log(gf);
2643
+ });
2644
+ rosterCommand
2645
+ .command("check")
2646
+ .description("check EVERY slice doc's recorded signers against the ratifier roster (GH #259) - `ratifiedBy` on " +
2647
+ "ready-to-implement and implemented docs, `reviewedBy` on any doc - the whole-model CI gate that " +
2648
+ "`--slice-ready <key>` (one ready slice) is not. Exit 1 on any violation or an invalid roster; no " +
2649
+ "roster file = nothing to check, exit 0. The `grandfathered ...` placeholder is exempt (counted)")
2650
+ .argument("<file>", "input .em file")
2651
+ .option("--roster <path>", "roster to check against (default: the nearest ratifiers.yaml above the model)")
2652
+ .option("--json", "print a JSON document (per-doc results, diagnostics with refs); exit codes unchanged")
2653
+ .action((file, opts) => {
2654
+ const { model, refs } = compileFile(file);
2655
+ const loaded = loadRoster(dirname(file), opts.roster, rosterContextFor(model, file));
2656
+ const report = checkRoster(model, refs, dirname(file), file, loaded);
2657
+ printDiagnostics(report.diagnostics);
2658
+ if (opts.json) {
2659
+ process.stdout.write(buildRosterCheckJson(file, report) + "\n");
2660
+ }
2661
+ else {
2662
+ for (const line of formatRosterCheck(report))
2663
+ console.log(line);
2664
+ }
2665
+ if (!report.ok)
2666
+ process.exitCode = 1;
2667
+ });
2462
2668
  const systemCommand = program
2463
2669
  .command("system")
2464
2670
  // `--json` exists on both `em system` and `em system codeowners`: parent options are only
@@ -2885,7 +3091,7 @@ ci.command("init")
2885
3091
  files = [
2886
3092
  [
2887
3093
  ciPath,
2888
- planCiFile(ciPath, buildCiWorkflowFile(model, opts.tests, PKG_VERSION), ciManagedBody(model, opts.tests, PKG_VERSION), CI_WORKFLOW_MARKER, !!opts.force),
3094
+ planCiFile(ciPath, buildCiWorkflowFile(model, opts.tests, PKG_VERSION, findRosterAbove(dirname(resolve(model))) !== null), ciManagedBody(model, opts.tests, PKG_VERSION, findRosterAbove(dirname(resolve(model))) !== null), CI_WORKFLOW_MARKER, !!opts.force),
2889
3095
  ],
2890
3096
  [
2891
3097
  conformPath,
@@ -2956,6 +3162,30 @@ ci.command("init")
2956
3162
  console.log(`${path} already up to date`);
2957
3163
  }
2958
3164
  });
3165
+ const storylineCmd = program.command("storyline").description("storyline blocks (GH #277): advisory helpers for adopting them");
3166
+ storylineCmd
3167
+ .command("suggest")
3168
+ .description("advisory (GH #290): propose `storyline` blocks for a model that has none — one per distinct slice " +
3169
+ "`source \"url\"`, the remaining slices grouped by the view-instance chains (`again`) they feed — " +
3170
+ "printed as a unified diff on stdout (apply with `patch -p1` or `git apply`); never writes the model")
3171
+ .argument("<file>", "input .em model file")
3172
+ .action((file) => {
3173
+ const { model, diagnostics, source: text } = compileFile(file);
3174
+ printDiagnostics(diagnostics);
3175
+ const s = buildStorylineSuggestion(model, text, file);
3176
+ if (s.patch === "") {
3177
+ console.error(`em storyline suggest: nothing to propose for ${file}`);
3178
+ return;
3179
+ }
3180
+ for (const st of s.storylines) {
3181
+ console.error(`storyline "${st.name}" — ${st.sliceIndexes.length} slice(s), from ${st.basis === "source" ? `source ${st.source}` : "a view-instance chain"}`);
3182
+ }
3183
+ if (s.ungrouped.length > 0) {
3184
+ console.error(`left outside every storyline (${s.ungrouped.length}): ${s.ungrouped.map((i) => model.slices[i].name).join(", ")}`);
3185
+ }
3186
+ console.error("review the patch below and apply it by hand — nothing was written");
3187
+ process.stdout.write(s.patch);
3188
+ });
2959
3189
  program
2960
3190
  .command("upgrade")
2961
3191
  .description("bring a model repo authored under an older em (1.6 forward) up to the installed version: " +
@@ -2967,7 +3197,13 @@ program
2967
3197
  .option("--apply", "apply every applicable mechanical step, one git commit each, then a final `Em version:` commit")
2968
3198
  .option("--check", "exit non-zero only on a hard incompatibility (an existing but unparseable state file, an un-migratable .em shape) — writes nothing; what CI runs; the final line says why")
2969
3199
  .option("--json", "print a JSON document instead of text (dry-run/--check only, never with --apply)")
3200
+ .option("--fold-continuations", "also run the opt-in fold-continuations step (stack consecutive continuations of one view into the first; continuationOf, doc join and index rows unchanged)")
3201
+ .option("--to-sugar", "(implies --fold-continuations; without --apply it is a dry run listing exactly what would fold): fold continuations into their host command slice as `view X again … shown on` lines — CHANGES continuationOf and the doc join to `<host>--again` and removes the folded slices' index rows")
2970
3202
  .action((file, opts) => {
3203
+ if ((opts.foldContinuations || opts.toSugar) && opts.check) {
3204
+ console.error("em upgrade: --fold-continuations / --to-sugar do not combine with --check");
3205
+ process.exit(1);
3206
+ }
2971
3207
  if (opts.apply && opts.check) {
2972
3208
  console.error("em upgrade: --apply and --check are mutually exclusive");
2973
3209
  process.exit(1);
@@ -3002,12 +3238,14 @@ program
3002
3238
  packagedAgentsRoot: packagedAgentsRoot(),
3003
3239
  model,
3004
3240
  refs,
3241
+ ...(opts.foldContinuations ? { foldContinuations: true } : {}),
3242
+ ...(opts.toSugar ? { toSugar: true } : {}),
3005
3243
  };
3006
3244
  if (opts.check) {
3007
3245
  const { ok, report, reasons } = checkUpgrade(ctx);
3008
3246
  console.error(`em upgrade --check: from ${report.from.version}${report.from.inferred ? ` (inferred: ${report.from.basis})` : ""} → to ${report.to}`);
3009
3247
  for (const s of report.steps) {
3010
- console.error(` [${s.applicable ? "x" : " "}] ${s.id} (since ${s.sinceVersion}): ${s.reason}`);
3248
+ console.error(` [${s.optIn ? "~" : s.applicable ? "x" : " "}] ${s.id} (since ${s.sinceVersion}): ${s.reason}`);
3011
3249
  }
3012
3250
  for (const h of report.human)
3013
3251
  console.error(` human: ${h.id}: ${h.reason}`);
@@ -3020,10 +3258,13 @@ program
3020
3258
  if (opts.apply) {
3021
3259
  const result = applyUpgrade(ctx);
3022
3260
  for (const s of result.applied) {
3023
- if (s.applied)
3261
+ if (s.applied) {
3024
3262
  console.log(`applied: ${s.id} — ${s.changedFiles.join(", ")} (${s.commit})`);
3263
+ for (const n of s.notes ?? [])
3264
+ console.log(` note: ${n}`);
3265
+ }
3025
3266
  else
3026
- console.log(`skipped: ${s.id}`);
3267
+ console.log(`skipped: ${s.id}${result.report.steps.find((x) => x.id === s.id)?.optIn ? " (opt-in: not requested by a flag)" : ""}`);
3027
3268
  }
3028
3269
  if (!result.ok) {
3029
3270
  console.error(result.message);
@@ -3035,6 +3276,7 @@ program
3035
3276
  console.log("Em version: already up to date — no commit needed");
3036
3277
  for (const h of result.report.human)
3037
3278
  console.log(`human: ${h.id}: ${h.reason}`);
3279
+ printUpgradeShapeSection(file);
3038
3280
  return;
3039
3281
  }
3040
3282
  const report = detectUpgrade(ctx);
@@ -3047,16 +3289,31 @@ program
3047
3289
  console.log(`state file: ${report.stateFileError}`);
3048
3290
  console.log("Steps:");
3049
3291
  for (const s of report.steps) {
3050
- console.log(` [${s.applicable ? "x" : " "}] ${s.id} (since ${s.sinceVersion}): ${s.reason}`);
3292
+ console.log(` [${s.optIn ? "~" : s.applicable ? "x" : " "}] ${s.id} (since ${s.sinceVersion}): ${s.reason}`);
3051
3293
  }
3052
3294
  console.log("Human:");
3053
3295
  if (report.human.length === 0)
3054
3296
  console.log(" (none)");
3055
3297
  for (const h of report.human)
3056
3298
  console.log(` ${h.id}: ${h.reason}`);
3057
- const applicableCount = report.steps.filter((s) => s.applicable).length;
3058
- console.log(`${applicableCount} step(s) applicable, ${report.human.length} human item(s) — re-run with --apply to apply mechanical steps`);
3299
+ const applicableCount = report.steps.filter((s) => s.applicable && !s.optIn).length;
3300
+ const optInCount = report.steps.filter((s) => s.optIn).length;
3301
+ console.log(`${applicableCount} step(s) applicable${optInCount > 0 ? ` (+${optInCount} opt-in, marked [~], not applied by --apply)` : ""}, ${report.human.length} human item(s) — re-run with --apply to apply mechanical steps`);
3302
+ printUpgradeShapeSection(file);
3059
3303
  });
3304
+ /** GH #290: the closing section of `em upgrade`'s text report — `em validate --shape` over the model as it
3305
+ * stands, so an estate sees how many advisory shape findings it carries. Never writes a waive file:
3306
+ * baselining shapes you accept is a decision (`--waive-file`, and a `shape-accepted:` Decisions-log bullet). */
3307
+ function printUpgradeShapeSection(file) {
3308
+ const { model, diagnostics, refs, source } = compileFile(file);
3309
+ const all = computeAllDiagnostics(file, model, refs, diagnostics, source);
3310
+ const report = buildShapeReport(all, []);
3311
+ console.log("");
3312
+ console.log("Shape (em validate --shape, advisory):");
3313
+ console.log(formatShapeText(report, all.filter((d) => d.severity === "error").length));
3314
+ console.log("Record each shape you accept as `shape-accepted: <rule> <refs> — <reason>` in `.event-modeling.md` `## Decisions log`; " +
3315
+ "to baseline an estate, run `em validate " + file + " --shape --waive-file <path>` (em upgrade writes no waive file).");
3316
+ }
3060
3317
  // Exported so dev tooling (e.g. scripts/generate-skill-docs.ts) can introspect the registered
3061
3318
  // commands/options without triggering a real CLI run. Guarded the same way Node's CJS
3062
3319
  // `require.main === module` used to: only actually parse argv when this file is the process's