@milehimikey/em 1.19.6 → 1.20.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 (102) 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-conform/reference/conform.md +32 -4
  4. package/.claude/skills/event-modeling-design/SKILL.md +14 -3
  5. package/.claude/skills/event-modeling-discover/SKILL.md +1 -1
  6. package/.claude/skills/event-modeling-discover/reference/extract.md +8 -0
  7. package/.claude/skills/event-modeling-engagement/SKILL.md +1 -1
  8. package/.claude/skills/event-modeling-implement/SKILL.md +7 -2
  9. package/.claude/skills/event-modeling-implement/reference/implement.md +73 -6
  10. package/.claude/skills/event-modeling-review/SKILL.md +1 -1
  11. package/.claude/skills/event-modeling-shared/reference/em-dsl.md +24 -4
  12. package/.claude/skills/event-modeling-shared/reference/methodology.md +16 -0
  13. package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +1 -1
  14. package/.claude/skills/event-modeling-shared/templates/slice.md +6 -1
  15. package/.claude-plugin/marketplace.json +2 -2
  16. package/README.md +11 -1
  17. package/dist/catalog/docBadge.d.ts +16 -0
  18. package/dist/catalog/docBadge.js +50 -0
  19. package/dist/catalog/docBadge.js.map +1 -0
  20. package/dist/catalog/docModelConsistencyValidate.js +9 -2
  21. package/dist/catalog/docModelConsistencyValidate.js.map +1 -1
  22. package/dist/catalog/invTokens.d.ts +49 -0
  23. package/dist/catalog/invTokens.js +94 -0
  24. package/dist/catalog/invTokens.js.map +1 -0
  25. package/dist/catalog/pages.js +48 -1
  26. package/dist/catalog/pages.js.map +1 -1
  27. package/dist/catalog/scenarios.d.ts +4 -0
  28. package/dist/catalog/scenarios.js +4 -1
  29. package/dist/catalog/scenarios.js.map +1 -1
  30. package/dist/catalog/sliceDoc.d.ts +46 -1
  31. package/dist/catalog/sliceDoc.js +90 -2
  32. package/dist/catalog/sliceDoc.js.map +1 -1
  33. package/dist/catalog/sliceDocRender.d.ts +3 -0
  34. package/dist/catalog/sliceDocRender.js +295 -0
  35. package/dist/catalog/sliceDocRender.js.map +1 -0
  36. package/dist/cli/conformSupersede.js +13 -3
  37. package/dist/cli/conformSupersede.js.map +1 -1
  38. package/dist/cli/coverage.d.ts +13 -23
  39. package/dist/cli/coverage.js +51 -91
  40. package/dist/cli/coverage.js.map +1 -1
  41. package/dist/cli/markImplemented.js +21 -3
  42. package/dist/cli/markImplemented.js.map +1 -1
  43. package/dist/cli/refsCheck.d.ts +52 -0
  44. package/dist/cli/refsCheck.js +127 -1
  45. package/dist/cli/refsCheck.js.map +1 -1
  46. package/dist/cli/sliceConform.d.ts +100 -1
  47. package/dist/cli/sliceConform.js +182 -9
  48. package/dist/cli/sliceConform.js.map +1 -1
  49. package/dist/cli/status.js +4 -3
  50. package/dist/cli/status.js.map +1 -1
  51. package/dist/cli/watchStoryline.d.ts +27 -0
  52. package/dist/cli/watchStoryline.js +55 -0
  53. package/dist/cli/watchStoryline.js.map +1 -0
  54. package/dist/cli.js +184 -24
  55. package/dist/cli.js.map +1 -1
  56. package/dist/emit/docJson.d.ts +99 -0
  57. package/dist/emit/docJson.js +197 -0
  58. package/dist/emit/docJson.js.map +1 -0
  59. package/dist/emit/refsJson.d.ts +5 -3
  60. package/dist/emit/refsJson.js +5 -2
  61. package/dist/emit/refsJson.js.map +1 -1
  62. package/dist/mcp/server.js +23 -3
  63. package/dist/mcp/server.js.map +1 -1
  64. package/dist/model/continuation.d.ts +9 -0
  65. package/dist/model/continuation.js +30 -0
  66. package/dist/model/continuation.js.map +1 -1
  67. package/dist/model/rules.d.ts +8 -0
  68. package/dist/model/rules.js +8 -0
  69. package/dist/model/rules.js.map +1 -1
  70. package/dist/model/shapeLint.js +48 -0
  71. package/dist/model/shapeLint.js.map +1 -1
  72. package/dist/model/validate.js +119 -0
  73. package/dist/model/validate.js.map +1 -1
  74. package/dist/parser/ast.d.ts +8 -0
  75. package/dist/parser/parser.js +51 -1
  76. package/dist/parser/parser.js.map +1 -1
  77. package/dist/render/render.js +1 -1
  78. package/dist/render/render.js.map +1 -1
  79. package/dist/render/sliceOverlay.d.ts +7 -2
  80. package/dist/render/sliceOverlay.js +28 -3
  81. package/dist/render/sliceOverlay.js.map +1 -1
  82. package/dist/render/storylinePicker.d.ts +9 -0
  83. package/dist/render/storylinePicker.js +17 -0
  84. package/dist/render/storylinePicker.js.map +1 -0
  85. package/dist/render/viewerHtml.js +1 -1
  86. package/dist/render/viewerHtml.js.map +1 -1
  87. package/package.json +1 -1
  88. package/plugin/.claude-plugin/plugin.json +2 -2
  89. package/plugin/shared/reference/em-dsl.md +24 -4
  90. package/plugin/shared/reference/methodology.md +16 -0
  91. package/plugin/shared/reference/slice-doc-schema.md +1 -1
  92. package/plugin/shared/templates/slice.md +6 -1
  93. package/plugin/skills/conform/SKILL.md +1 -1
  94. package/plugin/skills/conform/reference/conform.md +32 -4
  95. package/plugin/skills/design/SKILL.md +14 -3
  96. package/plugin/skills/discover/SKILL.md +1 -1
  97. package/plugin/skills/discover/reference/extract.md +8 -0
  98. package/plugin/skills/engagement/SKILL.md +1 -1
  99. package/plugin/skills/event-modeling/SKILL.md +1 -1
  100. package/plugin/skills/implement/SKILL.md +7 -2
  101. package/plugin/skills/implement/reference/implement.md +73 -6
  102. package/plugin/skills/review/SKILL.md +1 -1
@@ -0,0 +1,55 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Testable core of `em watch --serve --storyline <name>` (GH #309, deferred from #277):
3
+ // resolving the --storyline argument against the model (the exact same resolution `em render
4
+ // --storyline` and `em engagement`'s own `--storyline` selector use — findStoryline, GH #348's
5
+ // case/whitespace-insensitive match) and the live viewer's storyline-picker entries. Factored
6
+ // out of cli.ts's `watch` action, which starts a long-running file watcher and isn't itself
7
+ // unit-testable, mirroring src/cli/render-inputs.ts's existing split for `em render`'s own
8
+ // `--slice`/`--storyline`.
9
+ import { kebabSlug } from "../util/slug.js";
10
+ import { findStoryline } from "../model/storylines.js";
11
+ /** Flat (no subdirectory) filename for a storyline's live-serve subset SVG, written alongside
12
+ * the full model's own output file. Flat, unlike `em render --storyline`'s `storylines/`
13
+ * convention, because the live viewer's `?svg=` allowlist (serve.ts / viewer.html) accepts a
14
+ * bare filename only — never a path with a separator. */
15
+ export function storylineFileFor(name) {
16
+ return `storyline-${kebabSlug(name)}.svg`;
17
+ }
18
+ /** The live viewer's storyline-picker entries (GH #309): "Full model" plus one per declared
19
+ * storyline, `current` marking whichever file THIS render is — so the same list, computed
20
+ * once per build, embeds into every output file with only `current` differing. `[]` (no
21
+ * picker) when the model declares none. */
22
+ export function storylineVariants(model, outFile, currentFile) {
23
+ const declared = model.storylines ?? [];
24
+ if (declared.length === 0)
25
+ return [];
26
+ return [
27
+ { name: "Full model", file: outFile, current: currentFile === outFile },
28
+ ...declared.map((st) => {
29
+ const file = storylineFileFor(st.name);
30
+ return { name: st.name, file, current: currentFile === file };
31
+ }),
32
+ ];
33
+ }
34
+ /** Resolve `--storyline <name>` against the model — `{ none: true }` when the flag wasn't
35
+ * given at all. Same validation `em render --storyline` reports on an unknown name: lists
36
+ * every declared storyline, so a typo is correctable without opening the .em file. Uses
37
+ * findStoryline directly (not render/storylineDiagram.ts's resolveStorylineArg) only because
38
+ * that helper's error text is hardcoded to "em render --storyline" — the lookup itself (GH
39
+ * #348's case/whitespace-insensitive match) is the one `em render`, `em engagement
40
+ * --storyline` and this all share; this is not a second resolution. */
41
+ export function resolveWatchStoryline(model, name) {
42
+ if (name === undefined)
43
+ return { none: true };
44
+ const storyline = findStoryline(model, name);
45
+ if (storyline)
46
+ return { storyline };
47
+ const declared = model.storylines ?? [];
48
+ const known = declared.map((s) => `"${s.name}"`).join(", ");
49
+ return {
50
+ error: declared.length === 0
51
+ ? `em watch --storyline: this model declares no storylines`
52
+ : `em watch --storyline: no storyline "${name}" (declared: ${known})`,
53
+ };
54
+ }
55
+ //# sourceMappingURL=watchStoryline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watchStoryline.js","sourceRoot":"","sources":["../../src/cli/watchStoryline.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,wFAAwF;AACxF,6FAA6F;AAC7F,+FAA+F;AAC/F,8FAA8F;AAC9F,4FAA4F;AAC5F,2FAA2F;AAC3F,2BAA2B;AAG3B,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAGvD;;;0DAG0D;AAC1D,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,aAAa,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC;AAC5C,CAAC;AAED;;;4CAG4C;AAC5C,MAAM,UAAU,iBAAiB,CAAC,KAAsB,EAAE,OAAe,EAAE,WAAmB;IAC5F,MAAM,QAAQ,GAAG,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC;IACxC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IACrC,OAAO;QACL,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,KAAK,OAAO,EAAE;QACvE,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;YACrB,MAAM,IAAI,GAAG,gBAAgB,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;YACvC,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,KAAK,IAAI,EAAE,CAAC;QAChE,CAAC,CAAC;KACH,CAAC;AACJ,CAAC;AAID;;;;;;wEAMwE;AACxE,MAAM,UAAU,qBAAqB,CAAC,KAAsB,EAAE,IAAwB;IACpF,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC9C,MAAM,SAAS,GAAG,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAC7C,IAAI,SAAS;QAAE,OAAO,EAAE,SAAS,EAAE,CAAC;IACpC,MAAM,QAAQ,GAAG,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC;IACxC,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5D,OAAO;QACL,KAAK,EACH,QAAQ,CAAC,MAAM,KAAK,CAAC;YACnB,CAAC,CAAC,yDAAyD;YAC3D,CAAC,CAAC,uCAAuC,IAAI,gBAAgB,KAAK,GAAG;KAC1E,CAAC;AACJ,CAAC"}
package/dist/cli.js CHANGED
@@ -14,11 +14,14 @@ import { renderDot, layoutDot, composeSvg, writeRendered, formatFromPath } from
14
14
  import { buildSliceDiagram } from "./render/sliceDiagram.js";
15
15
  import { resolveSliceArg, defaultSliceOut, defaultStorylineOut } from "./cli/render-inputs.js";
16
16
  import { buildStorylineDiagram, resolveStorylineArg } from "./render/storylineDiagram.js";
17
+ import { embedStorylinePicker } from "./render/storylinePicker.js";
18
+ import { resolveWatchStoryline, storylineFileFor, storylineVariants } from "./cli/watchStoryline.js";
17
19
  import { resolveSliceKey } from "./model/resolveSlice.js";
18
20
  import { serializeBuilds, watchFile } from "./render/watch.js";
19
21
  import { startLiveServer } from "./render/serve.js";
20
22
  import { formatDiagnostic, hasErrors, publicViewsWithoutInModelReader } from "./model/validate.js";
21
23
  import { buildExport, buildSliceExport, GENERATOR_VERSION } from "./emit/json.js";
24
+ import { buildDocJsonExport } from "./emit/docJson.js";
22
25
  import { generateContract, runApiCheck, formatApiCheckText } from "./cli/api.js";
23
26
  import { buildApiCheckJson } from "./emit/apiCheckJson.js";
24
27
  import { buildValidateJson, buildShapeJson, buildSliceReadyJson, buildValidateListJson, collectMarkers } from "./emit/validateJson.js";
@@ -50,7 +53,7 @@ import { planMigration, verifyMigration } from "./cli/migrateReactionShape.js";
50
53
  import { buildLedgerJson } from "./emit/ledgerJson.js";
51
54
  import { buildRefsJson } from "./emit/refsJson.js";
52
55
  import { buildShapeReport, formatShapeText } from "./cli/shape.js";
53
- import { applyRefWaivers, checkRefs, readWaiveFile } from "./cli/refsCheck.js";
56
+ import { applyRefWaivers, checkRatificationOrder, checkRefs, readWaiveFile } from "./cli/refsCheck.js";
54
57
  import { buildCoverageReport, resolveScopedSlices, resolveCoverageSliceKey } from "./cli/coverage.js";
55
58
  import { buildCoverageJson } from "./emit/coverageJson.js";
56
59
  import { resolveSliceStatusFacts, countOpenIssues, resolveConformanceEntry, aggregateInvariantTotals, buildStatusReport, formatStatusText, formatStatusMarkdown, buildStatusBadge, formatConformancePart, findSpecifyRoot, resolveModelVersionStatusEntry, resolveEmVersionStatusEntry, } from "./cli/status.js";
@@ -89,9 +92,9 @@ import { listModelCommits } from "./cli/changelog-git.js";
89
92
  import { buildChangelogDoc } from "./cli/changelogBuild.js";
90
93
  import { runRevise } from "./cli/revise.js";
91
94
  import { runDefer, runDeferFromIssue } from "./cli/defer.js";
92
- import { runSliceConform } from "./cli/sliceConform.js";
95
+ import { runSliceConform, runSliceConformPreview, writePreviewFindings, resolveFindingsForReport, findingsFilesOf, unruledAcrossFiles, } from "./cli/sliceConform.js";
93
96
  import { checkPrdTriage, buildCheckPrdTriageJson, renderPrdTriageMarkdown, validatePrdTriage } from "./cli/prdTriage.js";
94
- import { checkFindingsFile, buildCheckFindingsJson, lookupFindingsBesideReport, unruledFindingsInScope, FINDING_LOCI } from "./cli/findings.js";
97
+ import { checkFindingsFile, buildCheckFindingsJson, FINDING_LOCI } from "./cli/findings.js";
95
98
  import { STATE_FILE_NAME, PHASES, isPhase, loadStateFile, parseState, setPhase, setConformance, setModelVersion, setCertified, setEmVersion, setReview, isValidDateString, modelPathMismatch, } from "./cli/stateFile.js";
96
99
  import { findCertifiedVersion, latestModelVersionNumber, modelVersionDrift, runCertifyModelVersion, runModelVersionBump, } from "./cli/modelVersion.js";
97
100
  import { STARTER_EM, starterEmFor, scaffoldReadme, scaffoldStateFile, scaffoldConstitution } from "./templates.js";
@@ -283,9 +286,35 @@ program
283
286
  .option("--slice <key>", "export only this slice's object (pattern/fields/doc) instead of the whole model (export " +
284
287
  "key, MIL-128) — refuses only if THIS slice has an error; an unrelated slice's breakage " +
285
288
  "elsewhere in the model doesn't block it (see docs/cli.md)")
289
+ .option("--doc-json", "emit the structured slice-doc export (GH #401) instead of the model snapshot: per slice the " +
290
+ "frontmatter join plus the parsed sections as data (fields, invariants, scenarios, error " +
291
+ "flows, open questions) and a model-level invariant catalog. Own schema version; whole " +
292
+ "model only (does not combine with --slice)")
293
+ .option("--include-html", "with --doc-json, include each slice doc's rendered HTML body (excluded by default; the " +
294
+ "authored body never appears in a plain export)")
286
295
  .action((file, opts) => {
287
296
  const { model, refs, diagnostics, source } = compileFile(file);
288
297
  printDiagnostics(diagnostics);
298
+ if (opts.docJson) {
299
+ if (opts.slice) {
300
+ console.error("em export --doc-json: exports the whole model and does not combine with --slice");
301
+ process.exit(1);
302
+ }
303
+ if (hasErrors(diagnostics)) {
304
+ console.error("not exporting: fix the errors above");
305
+ process.exit(1);
306
+ }
307
+ const exportedDoc = buildDocJsonExport(model, refs, diagnostics, source, file, { includeHtml: opts.includeHtml });
308
+ printDiagnostics(newDiagnostics(exportedDoc.diagnostics, diagnostics));
309
+ if (opts.out) {
310
+ writeFileSync(opts.out, exportedDoc.text + "\n");
311
+ console.log(`wrote ${opts.out}`);
312
+ }
313
+ else {
314
+ process.stdout.write(exportedDoc.text + "\n");
315
+ }
316
+ return;
317
+ }
289
318
  if (opts.slice) {
290
319
  // Scoped export deliberately does NOT reuse the whole-model error guard below: the
291
320
  // ticket's own motivation for `--slice` is an agent implementing one already-ratified
@@ -1088,7 +1117,10 @@ slice
1088
1117
  "later re-certification is normal — there's no way for em to tell 'later' from 'earlier' " +
1089
1118
  "for an arbitrary revision string, so there's no --force to reach for). Refuses when the " +
1090
1119
  "conformance/<date>[-<name>]-findings.json file matching --at still has an unruled finding in scope " +
1091
- "for this slice; --skip-findings-check overrides with a loud notice.")
1120
+ "for this slice; --skip-findings-check overrides with a loud notice. GH #400: --preview runs the " +
1121
+ "same doc-resolution and findings-check read for a slice that hasn't shipped yet — the shipped/" +
1122
+ "implementedIn gates are waived and NOTHING is ever written to the doc; output is ephemeral " +
1123
+ "(text or --json) unless --write-findings opts into persisting a normal findings file.")
1092
1124
  .argument("<file>", "input .em file")
1093
1125
  .argument("<slice-key>", "slice export key (kebab-case)")
1094
1126
  .requiredOption("--at <rev>", "the target-repo revision this certification sweep diffed against — or, when what was exercised is not a commit " +
@@ -1096,12 +1128,31 @@ slice
1096
1128
  "`<kind>:<id>` such as build:1234 or deploy:eu-1-2026-10-15 (GH #261); recorded verbatim, skipped by git-reachability checks")
1097
1129
  .option("--on <date>", "certification date, YYYY-MM-DD (default: today, local date)")
1098
1130
  .option("--skip-findings-check", "certify even with unruled findings in scope — prints a loud notice on stderr")
1131
+ .option("--preview", "GH #400: pre-ship advisory mode — waives the shipped-status/`implementedIn:` gates, writes NOTHING to the " +
1132
+ "doc (no conformedVersion/conformedAt/conformedOn, ever); ephemeral by default, --write-findings opts " +
1133
+ "into persisting a normal findings file. --skip-findings-check has no effect in this mode (preview " +
1134
+ "never refuses on findings)")
1135
+ .option("--write-findings", "with --preview: persist the preview's own unruled findings as conformance/<on>-<slice-key>-preview-findings.json")
1136
+ .option("--json", "with --preview: print a JSON document instead of the text report")
1099
1137
  .action((file, sliceKey, opts) => {
1138
+ if (opts.writeFindings === true && opts.preview !== true) {
1139
+ console.error("em slice conform: --write-findings only applies with --preview");
1140
+ process.exit(1);
1141
+ }
1142
+ if (opts.json === true && opts.preview !== true) {
1143
+ console.error("em slice conform: --json only applies with --preview");
1144
+ process.exit(1);
1145
+ }
1146
+ if (opts.preview === true && opts.skipFindingsCheck === true) {
1147
+ console.error("em slice conform: --skip-findings-check has no effect with --preview (preview never blocks on findings)");
1148
+ process.exit(1);
1149
+ }
1100
1150
  const { model, refs, diagnostics } = compileFile(file);
1101
1151
  printDiagnostics(diagnostics);
1102
1152
  sliceKey = resolveSliceOrExit("em slice conform", model, refs, sliceKey);
1103
1153
  // Scoped the same way `em slice ratify`/`em slice revise`/`em slice mark-implemented` are:
1104
- // only an error concerning THIS slice refuses.
1154
+ // only an error concerning THIS slice refuses. Not waived by --preview — an erroring model
1155
+ // isn't a fact worth previewing against either.
1105
1156
  const scopedErrors = diagnostics.filter((d) => d.severity === "error" && d.refs?.some((r) => r === sliceKey || r.startsWith(`${sliceKey}/`)));
1106
1157
  if (scopedErrors.length > 0) {
1107
1158
  console.error(`em slice conform: slice "${sliceKey}" has errors — fix them first`);
@@ -1113,6 +1164,44 @@ slice
1113
1164
  console.error(`em slice conform: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
1114
1165
  process.exit(1);
1115
1166
  }
1167
+ if (opts.preview === true) {
1168
+ const baseDir = dirname(file);
1169
+ const result = runSliceConformPreview(model, refs, baseDir, sliceKey, opts.at);
1170
+ if (!result.ok) {
1171
+ if (opts.json) {
1172
+ console.log(JSON.stringify({ ok: false, message: result.message }, null, 2));
1173
+ }
1174
+ else {
1175
+ console.error(`em slice conform --preview: ${result.message}`);
1176
+ }
1177
+ process.exit(1);
1178
+ }
1179
+ const writtenTo = opts.writeFindings === true ? writePreviewFindings(baseDir, file, sliceKey, result.at, on, result.unruledFindings) : null;
1180
+ if (opts.json) {
1181
+ console.log(JSON.stringify({
1182
+ ok: true,
1183
+ preview: true,
1184
+ slice: sliceKey,
1185
+ path: result.path,
1186
+ status: result.status,
1187
+ version: result.version,
1188
+ at: result.at,
1189
+ findingsFiles: result.findingsFiles,
1190
+ unruledFindings: result.unruledFindings,
1191
+ writtenTo,
1192
+ }, null, 2));
1193
+ }
1194
+ else {
1195
+ const findingsNote = result.unruledFindings.length === 0
1196
+ ? result.findingsFiles.length === 0
1197
+ ? `no conformance findings file matches --at ${result.at} (conformance/<date>[-<name>]-findings.json with that revision)`
1198
+ : `no unruled conformance findings in scope (checked: ${result.findingsFiles.join(", ")})`
1199
+ : `${result.unruledFindings.length} unruled conformance finding(s) in scope (id ${result.unruledFindings.map((f) => f.id).join(", ")}) in ${result.findingsFiles.join(", ")}`;
1200
+ console.log(`preview: ${result.path} (status: ${result.status ?? "(empty)"}) @ ${result.at} — ${findingsNote}`);
1201
+ console.log(writtenTo !== null ? `wrote: ${writtenTo}` : "ephemeral — nothing written; pass --write-findings to persist a snapshot");
1202
+ }
1203
+ return;
1204
+ }
1116
1205
  const result = runSliceConform(model, refs, dirname(file), sliceKey, opts.at, on, opts.skipFindingsCheck === true);
1117
1206
  if (!result.ok) {
1118
1207
  console.error(`em slice conform: ${result.message}`);
@@ -1228,9 +1317,10 @@ state
1228
1317
  .command("set-conformance")
1229
1318
  .description("rewrite Last conformance: (and Last updated:) in the exact format reference/conform.md " +
1230
1319
  "parses. Refuses (MIL-214) while any `implemented` slice has an unruled conformance " +
1231
- "finding in the findings JSON beside --report (a `slice: null` finding blocks every " +
1320
+ "finding in the findings JSON beside --report, or, when that is absent, in every findings " +
1321
+ "file at the report's own revision (a `slice: null` finding blocks every " +
1232
1322
  "slice); --partial records the marker with a ` (partial)` suffix instead, with a loud " +
1233
- "notice. Without a findings JSON beside --report: warns once and records the marker as " +
1323
+ "notice. Without a findings JSON beside --report and none at the report's revision: warns once and records the marker as " +
1234
1324
  "given (migration path for a report predating `em conform-findings`)")
1235
1325
  .argument("<revision>", "target-repo revision just diffed against")
1236
1326
  .argument("[dir]", STATE_DIR_HELP, ".")
@@ -1266,28 +1356,31 @@ state
1266
1356
  compiledForCertify = { model: compiled.model, refs: compiled.refs, source: compiled.source };
1267
1357
  }
1268
1358
  }
1269
- let unruled = [];
1270
- const lookup = lookupFindingsBesideReport(modelDir, opts.report);
1271
- if (lookup.kind === "invalid") {
1272
- console.error(`em state set-conformance: ${lookup.message}`);
1359
+ let unruledTotal = 0;
1360
+ let unruledDescribe = "";
1361
+ const resolved = resolveFindingsForReport(modelDir, opts.report, revision);
1362
+ if (resolved.kind === "invalid") {
1363
+ console.error(`em state set-conformance: ${resolved.message}`);
1273
1364
  process.exit(1);
1274
1365
  }
1275
- else if (lookup.kind === "absent") {
1276
- console.error(`warn: no findings JSON found beside ${opts.report} — recording the marker without verifying every ` +
1366
+ else if (resolved.kind === "none") {
1367
+ console.error(`warn: no findings JSON found beside ${opts.report} (and no same-revision file) — recording the marker without verifying every ` +
1277
1368
  "finding is ruled (migration path for a report written before `em conform-findings`)");
1278
1369
  }
1279
1370
  else {
1280
- unruled = unruledFindingsInScope(lookup.doc.findings, inScope);
1371
+ const counted = unruledAcrossFiles(findingsFilesOf(resolved), inScope);
1372
+ unruledTotal = counted.total;
1373
+ unruledDescribe = counted.describe;
1281
1374
  }
1282
1375
  const partialFlag = opts.partial === true;
1283
- if (unruled.length > 0 && !partialFlag) {
1284
- console.error(`em state set-conformance: ${unruled.length} unruled conformance finding(s) among implemented slices ` +
1285
- `(id ${unruled.map((f) => f.id).join(", ")}) — rule on them (\`em conform-supersede --locus --by\`) or pass --partial`);
1376
+ if (unruledTotal > 0 && !partialFlag) {
1377
+ console.error(`em state set-conformance: ${unruledTotal} unruled conformance finding(s) among implemented slices ` +
1378
+ `in ${unruledDescribe} — rule on them (\`em conform-supersede --locus --by\`) or pass --partial`);
1286
1379
  process.exit(1);
1287
1380
  }
1288
- const recordPartial = partialFlag && unruled.length > 0;
1381
+ const recordPartial = partialFlag && unruledTotal > 0;
1289
1382
  if (recordPartial) {
1290
- console.error(`notice: conformance marker recorded as PARTIAL — ${unruled.length} finding(s) unruled`);
1383
+ console.error(`notice: conformance marker recorded as PARTIAL — ${unruledTotal} finding(s) unruled`);
1291
1384
  }
1292
1385
  const today = localIsoDate();
1293
1386
  // MIL-218 ruling D: a FULL (non-`--partial`) marker also certifies the current design
@@ -1302,7 +1395,8 @@ state
1302
1395
  // is where that problem gets its own diagnostic.
1303
1396
  let certifiedVersion = null;
1304
1397
  if (!recordPartial && compiledForCertify) {
1305
- const findingsPath = lookup.kind === "found" ? lookup.path : null;
1398
+ const readFiles = findingsFilesOf(resolved);
1399
+ const findingsPath = readFiles ? readFiles.map((f) => f.path).join(", ") : null;
1306
1400
  const certifyResult = runCertifyModelVersion(modelDir, compiledForCertify.model, compiledForCertify.refs, compiledForCertify.source, revision, today, opts.report, findingsPath);
1307
1401
  if (!certifyResult.ok) {
1308
1402
  console.error(`em state set-conformance: ${certifyResult.message}`);
@@ -1517,7 +1611,8 @@ program
1517
1611
  "report; idempotent on the same --as-of/--findings/--on stamp; refuses if the report " +
1518
1612
  "doesn't exist. --locus/--by (MIL-214, both required together) additionally record the " +
1519
1613
  "ruling itself — locus/resolvedBy/resolvedOn — on the findings named by --findings in the " +
1520
- "sibling conformance/<date>-findings.json, when one exists (warns once and stamps the " +
1614
+ "sibling conformance/<date>-findings.json, when one exists (refuses, naming them, when only " +
1615
+ "other findings files exist at the report's revision; warns once and stamps the " +
1521
1616
  "banner only when it doesn't — the migration path for a report predating em conform-findings)")
1522
1617
  .argument("<file>", "input .em file (used only to resolve the report path's base directory)")
1523
1618
  .argument("<report-path>", "path to the conformance report, relative to the model's directory")
@@ -1632,10 +1727,24 @@ program
1632
1727
  .option("--keep-empty-lanes", "keep the API lane even when empty")
1633
1728
  .option("--serve", "serve a live viewer with instant push-reload (no polling)")
1634
1729
  .option("--port <n>", "port for --serve (default 5173)", (v) => parseInt(v, 10))
1730
+ .option("--storyline <name>", "GH #309 (deferred from #277): filter the live view to one storyline's subset — the same " +
1731
+ "subset `em render --storyline` draws (its own slices in order, plus dimmed stub columns " +
1732
+ "for what they read from outside it and the `after` end state). Whenever the model " +
1733
+ "declares any storylines the viewer also offers a picker to switch to the full model or " +
1734
+ "another declared storyline, live, without restarting. An unknown name errors, listing " +
1735
+ "the declared storylines; requires --serve")
1635
1736
  .action(async (file, opts) => {
1636
1737
  const out = opts.out ?? defaultOut(file, opts.format ?? "svg");
1637
1738
  const fmt = opts.format ?? formatFromPath(out);
1739
+ if (opts.storyline !== undefined && !opts.serve) {
1740
+ console.error("em watch: --storyline applies only with --serve");
1741
+ process.exit(1);
1742
+ }
1638
1743
  let server;
1744
+ // The file the printed live-view URL points at — basename(out) (the full model) unless
1745
+ // --storyline resolves, in which case build() below points it at that storyline's own
1746
+ // subset file instead (GH #309).
1747
+ let liveFile = basename(out);
1639
1748
  // serializeBuilds: chokidar can fire several events for one save (and an
1640
1749
  // editor's write can land while a slow png/pdf render is still in flight) —
1641
1750
  // overlapping builds would race each other writing the same output file.
@@ -1656,7 +1765,40 @@ program
1656
1765
  server?.notifyError(first ? formatDiagnostic(first).trim() : "model has errors — see terminal");
1657
1766
  return;
1658
1767
  }
1659
- await renderDot(dot, model, grid, out, fmt, dirname(file));
1768
+ // GH #309: --storyline is validated on every build, same as `em render --storyline`
1769
+ // (resolveWatchStoryline shares findStoryline's resolution) — a save that renames or
1770
+ // drops the storyline must not kill the watcher.
1771
+ const resolved = resolveWatchStoryline(model, opts.storyline);
1772
+ if ("error" in resolved) {
1773
+ console.error(resolved.error);
1774
+ server?.notifyError(resolved.error);
1775
+ return;
1776
+ }
1777
+ if (opts.serve && fmt === "svg") {
1778
+ const baseDir = dirname(file);
1779
+ const outDir = dirname(out);
1780
+ const outFile = basename(out);
1781
+ liveFile = "storyline" in resolved ? storylineFileFor(resolved.storyline.name) : outFile;
1782
+ // The full model, always rendered to `out` (unchanged default) — carries the
1783
+ // storyline-picker metadata (a no-op when the model declares none) so the viewer can
1784
+ // offer it regardless of whether --storyline was passed.
1785
+ const fullRaw = await layoutDot(dot);
1786
+ const fullSvg = embedStorylinePicker(composeSvg(fullRaw, model, grid, baseDir, outDir), storylineVariants(model, outFile, liveFile));
1787
+ await writeRendered(fullSvg, out, "svg");
1788
+ // Every declared storyline's own subset (GH #277's `em render --storyline` subset,
1789
+ // 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.
1791
+ for (const st of model.storylines ?? []) {
1792
+ const sub = buildStorylineDiagram(model, st, { keepEmptyLanes: opts.keepEmptyLanes });
1793
+ const subFile = storylineFileFor(st.name);
1794
+ const subRaw = await layoutDot(sub.dot);
1795
+ const subSvg = embedStorylinePicker(composeSvg(subRaw, sub.model, sub.grid, baseDir, outDir), storylineVariants(model, outFile, subFile));
1796
+ await writeRendered(subSvg, join(outDir, subFile), "svg");
1797
+ }
1798
+ }
1799
+ else {
1800
+ await renderDot(dot, model, grid, out, fmt, dirname(file));
1801
+ }
1660
1802
  console.log(`rendered ${out} (${Date.now() - started}ms)`);
1661
1803
  server?.notify();
1662
1804
  }
@@ -1674,7 +1816,7 @@ program
1674
1816
  // The viewer inlines SVG only — for png/pdf watches there's nothing
1675
1817
  // live to embed, so don't print a ?svg= URL that can't work.
1676
1818
  if (fmt === "svg") {
1677
- console.log(`→ live view: ${server.url}/?svg=${encodeURIComponent(basename(out))}`);
1819
+ console.log(`→ live view: ${server.url}/?svg=${encodeURIComponent(liveFile)}`);
1678
1820
  console.log(" open it in a browser and share your screen");
1679
1821
  }
1680
1822
  else {
@@ -1932,6 +2074,10 @@ program
1932
2074
  .requiredOption("--reachable-from <rev>", "the branch or commit every ref must be an ancestor of (e.g. origin/main)")
1933
2075
  .option("--waive <slice-key>", "excuse this slice's non-ancestor/unresolved/undecidable results from the exit code (repeatable); still reported", (value, previous) => [...previous, value], [])
1934
2076
  .option("--waive-file <path>", "a baseline file of slice keys to waive, one per line (`#` comments allowed); missing file = no waivers")
2077
+ .option("--check-order", "advisory-only (GH #399): for every slice doc with a ratifiedRef, report whether a commit between the branch's " +
2078
+ "fork point and ratifiedRef already touched code other than the doc itself — a retrofit signal for inline " +
2079
+ "ratification (spec written to describe code that already exists). History is rewritable, so this NEVER " +
2080
+ "affects the exit code or `ok`, and is never a validate gate — see docs/process.md's inline-ratification section")
1935
2081
  .option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
1936
2082
  .action((file, opts) => {
1937
2083
  const { model, refs } = compileFile(file);
@@ -1947,8 +2093,11 @@ program
1947
2093
  }
1948
2094
  if (result.shallow)
1949
2095
  console.error("warn: shallow clone — every ref is undecidable; check out with fetch-depth: 0");
2096
+ // GH #399: computed only with --check-order, and never folded into `result`/`result.ok` —
2097
+ // it is a sibling report, not a new ref field, and must never move the exit code (R5).
2098
+ const orderResults = opts.checkOrder ? checkRatificationOrder(model, refs, dirname(file), result.target, result.shallow) : null;
1950
2099
  if (opts.json) {
1951
- process.stdout.write(buildRefsJson(result, file) + "\n");
2100
+ process.stdout.write(buildRefsJson(result, file, orderResults) + "\n");
1952
2101
  }
1953
2102
  else {
1954
2103
  for (const r of result.results) {
@@ -1965,8 +2114,19 @@ program
1965
2114
  else {
1966
2115
  console.log(`${bad} ref(s) not reachable from ${opts.reachableFrom} (${result.checkedRefs} checked on ${result.checkedDocs} doc(s)${waivedNote})`);
1967
2116
  }
2117
+ if (orderResults) {
2118
+ console.log("");
2119
+ console.log("ratification order (advisory — never affects exit code):");
2120
+ if (orderResults.length === 0) {
2121
+ console.log(" no slice doc carries a ratifiedRef to check");
2122
+ }
2123
+ for (const o of orderResults) {
2124
+ console.log(` ${o.status.padEnd(23)} ${o.sliceKey} ratifiedRef: ${o.ratifiedRef}${o.detail ? ` — ${o.detail}` : ""}`);
2125
+ }
2126
+ }
1968
2127
  }
1969
2128
  // Set the code rather than process.exit(): stdout to a pipe shouldn't risk truncation.
2129
+ // `orderResults` never participates — advisory only, per R5.
1970
2130
  if (!result.ok)
1971
2131
  process.exitCode = 1;
1972
2132
  });