@milehimikey/em 1.7.2 → 1.8.1

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 (119) hide show
  1. package/.claude/skills/event-modeling/SKILL.md +92 -65
  2. package/.claude/skills/event-modeling/reference/conform.md +45 -20
  3. package/.claude/skills/event-modeling/reference/em-dsl.md +132 -55
  4. package/.claude/skills/event-modeling/reference/extract.md +5 -4
  5. package/.claude/skills/event-modeling/reference/implement.md +34 -22
  6. package/.claude/skills/event-modeling/templates/model-readme.md +8 -5
  7. package/.claude/skills/event-modeling/templates/slice.md +19 -5
  8. package/README.md +6 -4
  9. package/dist/catalog/docJoin.d.ts +28 -3
  10. package/dist/catalog/docJoin.js +82 -13
  11. package/dist/catalog/docJoin.js.map +1 -1
  12. package/dist/catalog/docModelConsistencyValidate.d.ts +8 -0
  13. package/dist/catalog/docModelConsistencyValidate.js +335 -0
  14. package/dist/catalog/docModelConsistencyValidate.js.map +1 -0
  15. package/dist/catalog/noteBindingValidate.d.ts +8 -0
  16. package/dist/catalog/noteBindingValidate.js +133 -0
  17. package/dist/catalog/noteBindingValidate.js.map +1 -0
  18. package/dist/catalog/pages.js +3 -3
  19. package/dist/catalog/pages.js.map +1 -1
  20. package/dist/catalog/sliceDoc.d.ts +14 -0
  21. package/dist/catalog/sliceDoc.js +22 -0
  22. package/dist/catalog/sliceDoc.js.map +1 -1
  23. package/dist/catalog/sliceReadyValidate.d.ts +18 -0
  24. package/dist/catalog/sliceReadyValidate.js +25 -1
  25. package/dist/catalog/sliceReadyValidate.js.map +1 -1
  26. package/dist/cli/agentsMd.d.ts +15 -0
  27. package/dist/cli/agentsMd.js +72 -0
  28. package/dist/cli/agentsMd.js.map +1 -0
  29. package/dist/cli/conformScope.d.ts +86 -0
  30. package/dist/cli/conformScope.js +156 -0
  31. package/dist/cli/conformScope.js.map +1 -0
  32. package/dist/cli/contract.d.ts +7 -0
  33. package/dist/cli/contract.js +27 -0
  34. package/dist/cli/contract.js.map +1 -0
  35. package/dist/cli/coverage.d.ts +71 -0
  36. package/dist/cli/coverage.js +177 -0
  37. package/dist/cli/coverage.js.map +1 -0
  38. package/dist/cli/markImplemented.d.ts +34 -0
  39. package/dist/cli/markImplemented.js +180 -0
  40. package/dist/cli/markImplemented.js.map +1 -0
  41. package/dist/cli/migrateReactionShape.d.ts +62 -0
  42. package/dist/cli/migrateReactionShape.js +283 -0
  43. package/dist/cli/migrateReactionShape.js.map +1 -0
  44. package/dist/cli/sliceIndex.d.ts +49 -0
  45. package/dist/cli/sliceIndex.js +115 -0
  46. package/dist/cli/sliceIndex.js.map +1 -0
  47. package/dist/cli/sliceNew.d.ts +18 -0
  48. package/dist/cli/sliceNew.js +53 -0
  49. package/dist/cli/sliceNew.js.map +1 -0
  50. package/dist/cli/stateFile.d.ts +74 -0
  51. package/dist/cli/stateFile.js +191 -0
  52. package/dist/cli/stateFile.js.map +1 -0
  53. package/dist/cli.js +535 -43
  54. package/dist/cli.js.map +1 -1
  55. package/dist/emit/coverageJson.d.ts +8 -0
  56. package/dist/emit/coverageJson.js +30 -0
  57. package/dist/emit/coverageJson.js.map +1 -0
  58. package/dist/emit/diffJson.d.ts +9 -1
  59. package/dist/emit/diffJson.js +44 -1
  60. package/dist/emit/diffJson.js.map +1 -1
  61. package/dist/emit/dot.js +41 -10
  62. package/dist/emit/dot.js.map +1 -1
  63. package/dist/emit/json.d.ts +77 -4
  64. package/dist/emit/json.js +78 -4
  65. package/dist/emit/json.js.map +1 -1
  66. package/dist/emit/validateJson.d.ts +52 -0
  67. package/dist/emit/validateJson.js +120 -0
  68. package/dist/emit/validateJson.js.map +1 -0
  69. package/dist/mcp/main.d.ts +2 -0
  70. package/dist/mcp/main.js +20 -0
  71. package/dist/mcp/main.js.map +1 -0
  72. package/dist/mcp/server.d.ts +11 -0
  73. package/dist/mcp/server.js +260 -0
  74. package/dist/mcp/server.js.map +1 -0
  75. package/dist/model/model.d.ts +30 -1
  76. package/dist/model/model.js +26 -0
  77. package/dist/model/model.js.map +1 -1
  78. package/dist/model/rules.d.ts +114 -0
  79. package/dist/model/rules.js +116 -4
  80. package/dist/model/rules.js.map +1 -1
  81. package/dist/model/validate.js +92 -8
  82. package/dist/model/validate.js.map +1 -1
  83. package/dist/parser/ast.d.ts +47 -0
  84. package/dist/parser/lexer.d.ts +16 -0
  85. package/dist/parser/lexer.js +44 -2
  86. package/dist/parser/lexer.js.map +1 -1
  87. package/dist/parser/parser.js +354 -20
  88. package/dist/parser/parser.js.map +1 -1
  89. package/dist/render/drawNotes.d.ts +11 -1
  90. package/dist/render/drawNotes.js +28 -3
  91. package/dist/render/drawNotes.js.map +1 -1
  92. package/dist/render/render.js +116 -23
  93. package/dist/render/render.js.map +1 -1
  94. package/dist/render/serve.d.ts +4 -0
  95. package/dist/render/serve.js +27 -239
  96. package/dist/render/serve.js.map +1 -1
  97. package/dist/render/sliceOverlay.d.ts +7 -2
  98. package/dist/render/sliceOverlay.js +19 -7
  99. package/dist/render/sliceOverlay.js.map +1 -1
  100. package/dist/render/sliceStatus.d.ts +7 -3
  101. package/dist/render/sliceStatus.js +48 -12
  102. package/dist/render/sliceStatus.js.map +1 -1
  103. package/dist/render/viewerHtml.d.ts +1 -0
  104. package/dist/render/viewerHtml.js +5 -0
  105. package/dist/render/viewerHtml.js.map +1 -0
  106. package/dist/render/watch.d.ts +10 -0
  107. package/dist/render/watch.js +29 -0
  108. package/dist/render/watch.js.map +1 -1
  109. package/dist/templates.d.ts +20 -1
  110. package/dist/templates.js +172 -3
  111. package/dist/templates.js.map +1 -1
  112. package/dist/util/markers.d.ts +13 -0
  113. package/dist/util/markers.js +30 -0
  114. package/dist/util/markers.js.map +1 -0
  115. package/dist/util/walkDir.d.ts +10 -1
  116. package/dist/util/walkDir.js +11 -7
  117. package/dist/util/walkDir.js.map +1 -1
  118. package/package.json +12 -3
  119. package/.claude/skills/event-modeling/templates/live.html +0 -117
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // SPDX-License-Identifier: MIT
3
- import { readFileSync, writeFileSync, existsSync } from "node:fs";
3
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from "node:fs";
4
4
  import { cp, mkdir } from "node:fs/promises";
5
5
  import { basename, dirname, extname, join, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
@@ -11,30 +11,46 @@ import { ParseError } from "./parser/parser.js";
11
11
  import { renderDot, layoutDot, composeSvg, writeRendered, formatFromPath } from "./render/render.js";
12
12
  import { buildSliceDiagram } from "./render/sliceDiagram.js";
13
13
  import { resolveSliceArg, defaultSliceOut } from "./cli/render-inputs.js";
14
- import { watchFile } from "./render/watch.js";
14
+ import { serializeBuilds, watchFile } from "./render/watch.js";
15
15
  import { startLiveServer } from "./render/serve.js";
16
16
  import { formatDiagnostic, hasErrors } from "./model/validate.js";
17
- import { buildExport } from "./emit/json.js";
17
+ import { buildExport, buildSliceExport } from "./emit/json.js";
18
+ import { buildValidateJson, buildSliceReadyJson, buildValidateListJson, collectMarkers } from "./emit/validateJson.js";
18
19
  import { buildDiffJson } from "./emit/diffJson.js";
19
20
  import { diffModels, formatModelDiff, hasChanges } from "./model/diff.js";
20
21
  import { planDiffArgs, resolveRevision, resolveDocAtRevision } from "./cli/diff-inputs.js";
21
22
  import { readSliceDoc } from "./catalog/readSliceDoc.js";
22
23
  import { validateLineage } from "./catalog/lineageValidate.js";
23
24
  import { validateFrontmatterCoherence } from "./catalog/frontmatterCoherenceValidate.js";
24
- import { validateSliceReady } from "./catalog/sliceReadyValidate.js";
25
+ import { validateNoteBindings } from "./catalog/noteBindingValidate.js";
26
+ import { validateDocModelConsistency } from "./catalog/docModelConsistencyValidate.js";
27
+ import { validateSliceReady, computeSliceReadyGates } from "./catalog/sliceReadyValidate.js";
25
28
  import { checkLedger } from "./cli/ledgerCheck.js";
29
+ import { planMigration, verifyMigration } from "./cli/migrateReactionShape.js";
26
30
  import { buildLedgerJson } from "./emit/ledgerJson.js";
31
+ import { buildCoverageReport } from "./cli/coverage.js";
32
+ import { buildCoverageJson } from "./emit/coverageJson.js";
27
33
  import { planSkillSync, applySkillSync } from "./cli/skillSync.js";
28
34
  import { checkSkillSync } from "./cli/skillCheck.js";
29
35
  import { buildSkillCheckJson } from "./emit/skillCheckJson.js";
36
+ import { readContract } from "./cli/contract.js";
37
+ import { createServer as createMcpServer } from "./mcp/server.js";
38
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
39
+ import { syncAgentsMd } from "./cli/agentsMd.js";
30
40
  import { buildGlossary, detectKindConflicts, detectFieldTypeConflicts, hasConflicts, formatGlossarySummary, formatConflictLine, } from "./model/glossary.js";
31
41
  import { buildGlossaryJson } from "./emit/glossaryJson.js";
32
42
  import { planGlossaryArgs } from "./cli/glossary-inputs.js";
33
43
  import { buildCatalog } from "./catalog/build.js";
34
44
  import { planCatalogArgs } from "./cli/catalog-inputs.js";
45
+ import { runSliceIndex } from "./cli/sliceIndex.js";
46
+ import { runMarkImplemented } from "./cli/markImplemented.js";
47
+ import { buildConformScope, changedPathsSince, resolveSliceDocFacts, seedAsisModel } from "./cli/conformScope.js";
48
+ import { buildSliceDocContent, isSlicePattern, sliceDocKey, SLICE_PATTERNS } from "./cli/sliceNew.js";
35
49
  import { listModelCommits, readFileAtCommit } from "./cli/changelog-git.js";
36
50
  import { buildChangelog, parseDecisionsLog } from "./emit/changelog.js";
37
- import { STARTER_EM } from "./templates.js";
51
+ import { STATE_FILE_NAME, PHASES, isPhase, loadStateFile, parseState, setPhase, setConformance, setReview, isValidDateString, } from "./cli/stateFile.js";
52
+ import { STARTER_EM, starterEmFor, scaffoldReadme, scaffoldStateFile } from "./templates.js";
53
+ import { kebabSlug } from "./util/slug.js";
38
54
  const program = new Command();
39
55
  // Single source of truth for the version (also read by src/emit/json.ts for
40
56
  // `generator.version` in `em export`): package.json, one level up from both
@@ -57,6 +73,29 @@ program
57
73
  writeFileSync(file, STARTER_EM);
58
74
  console.log(`wrote ${file}`);
59
75
  });
76
+ program
77
+ .command("scaffold")
78
+ .description("scaffold a full project: <slug>/<slug>.em, README.md, .event-modeling.md " +
79
+ "(see docs/cli.md — for just a starter .em, use `em init`)")
80
+ .argument("<name>", "model display name — kebab-cased for the directory and file names, used as-is for titles/prose")
81
+ .option("-f, --force", "overwrite the directory's contents if it already exists")
82
+ .action(async (name, opts) => {
83
+ if (name.includes('"') || name.includes("{{")) {
84
+ console.error(`em scaffold: name must not contain '"' or '{{' (breaks the generated .em/README/state files): ${name}`);
85
+ process.exit(1);
86
+ }
87
+ const dirName = kebabSlug(name);
88
+ if (existsSync(dirName) && !opts.force) {
89
+ console.error(`refusing to overwrite ${dirName}/ (use --force)`);
90
+ process.exit(1);
91
+ }
92
+ await mkdir(dirName, { recursive: true });
93
+ const today = new Date().toISOString().slice(0, 10);
94
+ writeFileSync(join(dirName, `${dirName}.em`), starterEmFor(name));
95
+ writeFileSync(join(dirName, "README.md"), scaffoldReadme(name, dirName));
96
+ writeFileSync(join(dirName, STATE_FILE_NAME), scaffoldStateFile(name, dirName, today));
97
+ console.log(`scaffolded ${dirName}/`);
98
+ });
60
99
  program
61
100
  .command("render")
62
101
  .description("transpile a model and render it (or emit DOT)")
@@ -72,10 +111,15 @@ program
72
111
  console.error("em render: --slice cannot be combined with --emit-dot");
73
112
  process.exit(1);
74
113
  }
75
- const { dot, model, grid, diagnostics } = compileFile(file, {
114
+ const { dot, model, grid, diagnostics, refs } = compileFile(file, {
76
115
  keepEmptyLanes: opts.keepEmptyLanes,
77
116
  });
78
- printDiagnostics(diagnostics);
117
+ // MIL-126: note-binding mismatches read slices/*.md alongside the model, so (like
118
+ // validateLineage/validateFrontmatterCoherence in `em validate`) they're computed here
119
+ // rather than living in compile()'s pure diagnostics. Always warning-severity — folding
120
+ // them in never changes whether rendering proceeds below.
121
+ const allDiagnostics = [...diagnostics, ...validateNoteBindings(model, refs, dirname(file))];
122
+ printDiagnostics(allDiagnostics);
79
123
  warnMissingNotes(file, model);
80
124
  if (opts.emitDot) {
81
125
  if (opts.out) {
@@ -87,7 +131,7 @@ program
87
131
  }
88
132
  return;
89
133
  }
90
- if (hasErrors(diagnostics)) {
134
+ if (hasErrors(allDiagnostics)) {
91
135
  console.error("not rendering: fix the errors above");
92
136
  process.exit(1);
93
137
  }
@@ -119,9 +163,40 @@ program
119
163
  .description("export a versioned JSON snapshot of the normalized model")
120
164
  .argument("<file>", "input .em file")
121
165
  .option("-o, --out <path>", "write to a file instead of stdout")
166
+ .option("--slice <key>", "export only this slice's object (pattern/fields/doc) instead of the whole model (export " +
167
+ "key, MIL-128) — refuses only if THIS slice has an error; an unrelated slice's breakage " +
168
+ "elsewhere in the model doesn't block it (see docs/cli.md)")
122
169
  .action((file, opts) => {
123
170
  const { model, refs, diagnostics, source } = compileFile(file);
124
171
  printDiagnostics(diagnostics);
172
+ if (opts.slice) {
173
+ // Scoped export deliberately does NOT reuse the whole-model error guard below: the
174
+ // ticket's own motivation for `--slice` is an agent implementing one already-ratified
175
+ // slice while the rest of a large, still-WIP model has unrelated errors — the same
176
+ // "don't gate on breakage elsewhere" call `--slice-ready` already made (see its own
177
+ // scoping comment above). Only an error that concerns THIS slice (bare key, or an
178
+ // element ref prefixed `<key>/`) refuses.
179
+ const key = opts.slice;
180
+ const scopedErrors = diagnostics.filter((d) => d.severity === "error" && d.refs?.some((r) => r === key || r.startsWith(`${key}/`)));
181
+ if (scopedErrors.length > 0) {
182
+ console.error(`not exporting: slice "${key}" has errors — fix them first`);
183
+ process.exit(1);
184
+ }
185
+ const sliceExport = buildSliceExport(model, refs, diagnostics, source, file, key);
186
+ if (!sliceExport.found) {
187
+ console.error(`em export --slice: no slice with export key "${key}" in this model`);
188
+ process.exit(1);
189
+ }
190
+ printDiagnostics(newDiagnostics(sliceExport.diagnostics, diagnostics));
191
+ if (opts.out) {
192
+ writeFileSync(opts.out, sliceExport.text + "\n");
193
+ console.log(`wrote ${opts.out}`);
194
+ }
195
+ else {
196
+ process.stdout.write(sliceExport.text + "\n");
197
+ }
198
+ return;
199
+ }
125
200
  if (hasErrors(diagnostics)) {
126
201
  console.error("not exporting: fix the errors above");
127
202
  process.exit(1);
@@ -277,6 +352,95 @@ program
277
352
  const sliceWord = result.slices === 1 ? "slice" : "slices";
278
353
  console.log(`wrote ${opts.out}/ (${result.models} ${modelWord}, ${result.slices} ${sliceWord})`);
279
354
  });
355
+ // Namespace for slice-doc authoring/maintenance subcommands: `new` (MIL-97, scaffolds a fresh
356
+ // slice doc's frontmatter) and `index` (MIL-98, regenerates the model README's Slices table)
357
+ // share the `em slice <verb>` shape rather than living as separate top-level commands.
358
+ const slice = program.command("slice").description("author and maintain slice docs");
359
+ slice
360
+ .command("new")
361
+ .description("scaffold a fresh slices/<key>.md doc — the 5 frontmatter keys required at `status: draft` " +
362
+ "plus the `# Slice:` heading and diagram-image stub; judgment sections (Intent, " +
363
+ "Scenarios, Open Questions, ...) stay hand-authored (see docs/slice-doc-schema.md, " +
364
+ "templates/slice.md)")
365
+ .argument("<name>", "slice display name (e.g. \"Request Payment\") — kebab-cased for the filename")
366
+ .requiredOption("--pattern <pattern>", `slice pattern: ${SLICE_PATTERNS.join(" | ")}`)
367
+ .requiredOption("--swimlane <swimlane>", 'swimlane, e.g. "Persona → Context"')
368
+ .option("-f, --force", "overwrite the file if it already exists")
369
+ .action((name, opts) => {
370
+ if (!isSlicePattern(opts.pattern)) {
371
+ console.error(`em slice new: invalid --pattern "${opts.pattern}" — expected one of: ${SLICE_PATTERNS.join(", ")}`);
372
+ process.exit(1);
373
+ }
374
+ const key = sliceDocKey(name);
375
+ const dir = "slices";
376
+ const path = join(dir, `${key}.md`);
377
+ if (existsSync(path) && !opts.force) {
378
+ console.error(`refusing to overwrite ${path} (use --force)`);
379
+ process.exit(1);
380
+ }
381
+ mkdirSync(dir, { recursive: true });
382
+ writeFileSync(path, buildSliceDocContent(name, key, opts.pattern, opts.swimlane));
383
+ console.log(`wrote ${path}`);
384
+ console.log(`add this to the slice's primary element in the .em file:`);
385
+ console.log(` note "${path}"`);
386
+ });
387
+ slice
388
+ .command("index")
389
+ .description("rewrite the model's sibling README.md's GENERATED Slices table from `em export`'s slice " +
390
+ "facts (key, pattern, doc status/implementedIn) — the hand-maintained table is deprecated")
391
+ .argument("<file>", "input .em file")
392
+ .option("--check", "verify the table is current; exit non-zero on drift without writing (CI)")
393
+ .action((file, opts) => {
394
+ const { model, diagnostics, refs } = compileFile(file);
395
+ printDiagnostics(diagnostics);
396
+ if (hasErrors(diagnostics)) {
397
+ console.error("not indexing: fix the errors above");
398
+ process.exit(1);
399
+ }
400
+ const result = runSliceIndex(model, refs, file, !!opts.check);
401
+ printDiagnostics(newDiagnostics(result.table.diagnostics, diagnostics));
402
+ if (!result.ok) {
403
+ console.error(result.message);
404
+ process.exit(1);
405
+ }
406
+ if (result.wrote) {
407
+ console.log(`wrote ${result.readmePath}`);
408
+ }
409
+ else {
410
+ console.log(`ok — ${result.readmePath}'s Slices table is already up to date`);
411
+ }
412
+ });
413
+ slice
414
+ .command("mark-implemented")
415
+ .description("flip a slice doc's frontmatter to `status: implemented` / `implementedIn: <pr-url>` — the " +
416
+ "one edit an implementing agent makes to a ratified doc at merge (MIL-103, replaces the " +
417
+ "em-sdd-bridge `em-sdd-mark-implemented` script; see reference/implement.md §6). " +
418
+ "Idempotent on the same URL; refuses to overwrite a different one; never touches `version:` " +
419
+ "or the doc body")
420
+ .argument("<file>", "input .em file")
421
+ .argument("<slice-key>", "slice export key (kebab-case)")
422
+ .argument("<pr-url>", "merged PR (or commit) URL")
423
+ .action((file, sliceKey, prUrl) => {
424
+ const { model, refs, diagnostics } = compileFile(file);
425
+ printDiagnostics(diagnostics);
426
+ // Scoped the same way `em export --slice`/`em validate --slice-ready` are: only an error
427
+ // concerning THIS slice (bare key, or an element ref prefixed `<key>/`) refuses — an
428
+ // unrelated slice's breakage elsewhere in a large, still-WIP model doesn't block marking
429
+ // this one implemented.
430
+ const scopedErrors = diagnostics.filter((d) => d.severity === "error" && d.refs?.some((r) => r === sliceKey || r.startsWith(`${sliceKey}/`)));
431
+ if (scopedErrors.length > 0) {
432
+ console.error(`em slice mark-implemented: slice "${sliceKey}" has errors — fix them first`);
433
+ process.exit(1);
434
+ }
435
+ const result = runMarkImplemented(model, refs, dirname(file), sliceKey, prUrl);
436
+ if (!result.ok) {
437
+ console.error(`em slice mark-implemented: ${result.message}`);
438
+ process.exit(1);
439
+ }
440
+ console.log(result.changed
441
+ ? `marked implemented: ${result.path} (implementedIn: ${prUrl})`
442
+ : `already implemented (no-op): ${result.path}`);
443
+ });
280
444
  program
281
445
  .command("changelog")
282
446
  .description("render a model's git history as a business-readable ledger (see docs/cli.md)")
@@ -299,6 +463,124 @@ program
299
463
  process.stdout.write(markdown + "\n");
300
464
  }
301
465
  });
466
+ const STATE_DIR_HELP = "model directory containing .event-modeling.md, or a direct path to that file (default: current directory)";
467
+ /** Shared by every `em state` writer: load, apply the pure patch, write, report — the only
468
+ * difference between set-phase/set-conformance/set-review is which `mutate` closure they pass. */
469
+ function writeStateUpdate(dirOrFile, cmdLabel, mutate) {
470
+ const loaded = loadStateFile(dirOrFile);
471
+ if (!loaded.ok) {
472
+ console.error(loaded.message);
473
+ process.exit(1);
474
+ }
475
+ const today = new Date().toISOString().slice(0, 10);
476
+ const result = mutate(loaded.text, today);
477
+ if (!result.ok) {
478
+ console.error(`${cmdLabel}: ${result.message}`);
479
+ process.exit(1);
480
+ }
481
+ writeFileSync(loaded.path, result.text);
482
+ console.log(`wrote ${loaded.path}`);
483
+ }
484
+ const state = program
485
+ .command("state")
486
+ .description("read/write the state file's mechanical fields deterministically (see docs/cli.md)");
487
+ state
488
+ .command("read")
489
+ .description("print the state file's mechanical fields as JSON")
490
+ .argument("[dir]", STATE_DIR_HELP, ".")
491
+ .action((dir) => {
492
+ const loaded = loadStateFile(dir);
493
+ if (!loaded.ok) {
494
+ console.error(loaded.message);
495
+ process.exit(1);
496
+ }
497
+ const parsed = parseState(loaded.text);
498
+ if (!parsed.ok) {
499
+ console.error(`em state read: ${parsed.message}`);
500
+ process.exit(1);
501
+ }
502
+ console.log(JSON.stringify(parsed.state, null, 2));
503
+ });
504
+ state
505
+ .command("set-phase")
506
+ .description("rewrite Current phase: (and Last updated:); --step also rewrites Current step:")
507
+ .argument("<phase>", `phase: ${PHASES.join(" | ")}`)
508
+ .argument("[dir]", STATE_DIR_HELP, ".")
509
+ .option("--step <n>", "also set Current step: to this value")
510
+ .action((phase, dir, opts) => {
511
+ if (!isPhase(phase)) {
512
+ console.error(`em state set-phase: invalid phase "${phase}" — expected one of: ${PHASES.join(", ")}`);
513
+ process.exit(1);
514
+ }
515
+ writeStateUpdate(dir, "em state set-phase", (text, today) => setPhase(text, phase, today, opts.step));
516
+ });
517
+ state
518
+ .command("set-conformance")
519
+ .description("rewrite Last conformance: (and Last updated:) in the exact format reference/conform.md parses")
520
+ .argument("<revision>", "target-repo revision just diffed against")
521
+ .argument("[dir]", STATE_DIR_HELP, ".")
522
+ .requiredOption("--report <path>", "path to the conformance report just written")
523
+ .action((revision, dir, opts) => {
524
+ writeStateUpdate(dir, "em state set-conformance", (text, today) => setConformance(text, revision, opts.report, today));
525
+ });
526
+ state
527
+ .command("set-review")
528
+ .description("rewrite Last stakeholder review: (and Last updated:)")
529
+ .argument("<date>", "review date, YYYY-MM-DD")
530
+ .argument("[dir]", STATE_DIR_HELP, ".")
531
+ .action((date, dir) => {
532
+ if (!isValidDateString(date)) {
533
+ console.error(`em state set-review: invalid date "${date}" — expected YYYY-MM-DD`);
534
+ process.exit(1);
535
+ }
536
+ writeStateUpdate(dir, "em state set-review", (text, today) => setReview(text, date, today));
537
+ });
538
+ program
539
+ .command("conform-scope")
540
+ .description("mechanize conform phase step 1 (reference/conform.md): map the target repo's changed paths " +
541
+ "since Last conformance: to slices via each slice doc's implementedIn, JSON to stdout — " +
542
+ "--seed-asis also seeds the <model>-asis.em scratch model (see docs/cli.md)")
543
+ .argument("<file>", "input .em file")
544
+ .requiredOption("--repo <path>", "path to (or inside) the target codebase's git repository")
545
+ .option("--full", "ignore Last conformance:/changed paths; scope every implemented slice")
546
+ .option("--seed-asis", "write <model>-asis.em as a byte copy of the canonical model and ensure it's gitignored")
547
+ .action((file, opts) => {
548
+ const { model, diagnostics, refs } = compileFile(file);
549
+ printDiagnostics(diagnostics);
550
+ if (hasErrors(diagnostics)) {
551
+ console.error("not scoping: fix the errors above");
552
+ process.exit(1);
553
+ }
554
+ const loaded = loadStateFile(dirname(file));
555
+ if (!loaded.ok) {
556
+ console.error(loaded.message);
557
+ process.exit(1);
558
+ }
559
+ const parsed = parseState(loaded.text);
560
+ if (!parsed.ok) {
561
+ console.error(`em conform-scope: ${parsed.message}`);
562
+ process.exit(1);
563
+ }
564
+ const { lastConformance } = parsed.state;
565
+ const { facts, diagnostics: docDiags } = resolveSliceDocFacts(model, refs, dirname(file));
566
+ printDiagnostics(newDiagnostics(docDiags, diagnostics));
567
+ const skipGit = !!opts.full || lastConformance === null;
568
+ let changedPaths = [];
569
+ if (!skipGit) {
570
+ const result = changedPathsSince(opts.repo, lastConformance.revision);
571
+ if (!result.ok) {
572
+ console.error(result.message);
573
+ process.exit(1);
574
+ }
575
+ changedPaths = result.paths;
576
+ }
577
+ const scope = buildConformScope(facts, lastConformance, changedPaths, !!opts.full);
578
+ const output = { ...scope };
579
+ if (opts.seedAsis) {
580
+ output.seeded = seedAsisModel(file);
581
+ }
582
+ console.log(JSON.stringify(output, null, 2));
583
+ });
302
584
  program
303
585
  .command("watch")
304
586
  .description("re-render on every save")
@@ -312,16 +594,24 @@ program
312
594
  const out = opts.out ?? defaultOut(file, opts.format ?? "svg");
313
595
  const fmt = opts.format ?? formatFromPath(out);
314
596
  let server;
315
- const build = async () => {
597
+ // serializeBuilds: chokidar can fire several events for one save (and an
598
+ // editor's write can land while a slow png/pdf render is still in flight) —
599
+ // overlapping builds would race each other writing the same output file.
600
+ const build = serializeBuilds(async () => {
316
601
  const started = Date.now();
317
602
  try {
318
- const { dot, model, grid, diagnostics } = compileFile(file, {
319
- keepEmptyLanes: opts.keepEmptyLanes,
320
- });
603
+ const { dot, model, grid, diagnostics } = compileFile(file, { keepEmptyLanes: opts.keepEmptyLanes },
604
+ // A bad save must not kill the watcher: the error is reported below
605
+ // and pushed to the live view's banner instead.
606
+ { survive: true });
321
607
  printDiagnostics(diagnostics);
322
608
  warnMissingNotes(file, model);
323
609
  if (hasErrors(diagnostics)) {
324
610
  console.error("skipped render (errors above)");
611
+ // The shared screen must say WHY it's stale, not silently show an
612
+ // outdated model — push the first error into the viewer's banner.
613
+ const first = diagnostics.find((d) => d.severity === "error");
614
+ server?.notifyError(first ? formatDiagnostic(first).trim() : "model has errors — see terminal");
325
615
  return;
326
616
  }
327
617
  await renderDot(dot, model, grid, out, fmt, dirname(file));
@@ -330,16 +620,25 @@ program
330
620
  }
331
621
  catch (e) {
332
622
  reportError(e);
623
+ server?.notifyError(e instanceof Error ? e.message : String(e));
333
624
  }
334
- };
625
+ });
335
626
  await build();
336
627
  if (opts.serve) {
337
628
  try {
338
629
  // Serve the directory the SVG is written to — that's what the browser
339
630
  // fetches, and note "..." links inside the SVG resolve relative to it.
340
631
  server = await startLiveServer({ dir: dirname(resolve(out)), port: opts.port });
341
- console.log(`→ live view: ${server.url}/?svg=${encodeURIComponent(basename(out))}`);
342
- console.log(" open it in a browser and share your screen");
632
+ // The viewer inlines SVG only — for png/pdf watches there's nothing
633
+ // live to embed, so don't print a ?svg= URL that can't work.
634
+ if (fmt === "svg") {
635
+ console.log(`→ live view: ${server.url}/?svg=${encodeURIComponent(basename(out))}`);
636
+ console.log(" open it in a browser and share your screen");
637
+ }
638
+ else {
639
+ console.log(`→ serving ${dirname(resolve(out))} at ${server.url}`);
640
+ console.log(" (the live viewer needs an .svg output — add -o <name>.svg to use it)");
641
+ }
343
642
  }
344
643
  catch (e) {
345
644
  reportError(e);
@@ -350,6 +649,8 @@ program
350
649
  // the .em file. The glob still fires for docs authored after watch starts.
351
650
  watchFile([file, join(dirname(file), "slices", "*.md")], build);
352
651
  console.log(`watching ${file} … (ctrl-c to stop)`);
652
+ if (!opts.serve)
653
+ console.log("tip: add --serve for the live browser view");
353
654
  });
354
655
  program
355
656
  .command("validate")
@@ -365,25 +666,33 @@ program
365
666
  .option("--fail-on-issues", "exit non-zero if the model has any open `issue`s (opt-in — issues are warnings and don't block by default)")
366
667
  .option("--slice-ready <key>", "readiness gate for one slice (export key): status ready-to-implement, doc resolvable via " +
367
668
  "note binding, zero unchecked Open Questions — exits non-zero if not ready (MIL-87)")
669
+ .option("--json", "print a JSON document instead of text — works on a model WITH errors, unlike `em export` " +
670
+ "(MIL-128, see docs/cli.md); exit codes are unchanged")
368
671
  .action((file, opts) => {
369
672
  const { model, diagnostics, refs } = compileFile(file);
370
- // Lineage-ref resolution (MIL-84) and frontmatter-coherence (MIL-85) are validate's
371
- // fs-aware rules — every other check above is a pure function of the .em source. Both
372
- // read slices/*.md alongside the model.
673
+ // Lineage-ref resolution (MIL-84), frontmatter-coherence (MIL-85), note-binding
674
+ // mismatches (MIL-126), and doc↔model consistency (MIL-124) are validate's fs-aware
675
+ // rules — every other check above is a pure function of the .em source. All four read
676
+ // slices/*.md alongside the model. Doc↔model consistency is deliberately validate-only
677
+ // (unlike note-binding, which `em render` also folds in) — it's a conform-phase concern,
678
+ // not something every render needs to recheck.
373
679
  const allDiagnostics = [
374
680
  ...diagnostics,
375
681
  ...validateLineage(model, refs, dirname(file)),
376
682
  ...validateFrontmatterCoherence(model, refs, dirname(file)),
683
+ ...validateNoteBindings(model, refs, dirname(file)),
684
+ ...validateDocModelConsistency(model, refs, dirname(file)),
377
685
  ];
378
686
  if (opts.sliceReady) {
379
687
  // MIL-87: a targeted, single-slice readiness gate, not part of the unconditional
380
688
  // diagnostic set above (see sliceReadyValidate.ts's header for why). Folds in MIL-85's
381
- // frontmatter-coherence findings for free by filtering allDiagnostics' own refs, rather
382
- // than re-deriving that classification here.
689
+ // frontmatter-coherence findings, MIL-126's note-binding-mismatch findings, AND MIL-124's
690
+ // doc-model-consistency findings for free by filtering allDiagnostics' own refs, rather
691
+ // than re-deriving any of those classifications here.
383
692
  //
384
693
  // "Concerns this slice" means a ref that either IS the bare slice key (lineage,
385
- // frontmatter-coherence, and this module's own diagnostics all tag that way) OR starts
386
- // with `<sliceKey>/` (every element-level ref from computeRefs()/model/validate.ts,
694
+ // frontmatter-coherence, note-binding, and this module's own diagnostics all tag that
695
+ // way) OR starts with `<sliceKey>/` (every element-level ref from computeRefs()/model/validate.ts,
387
696
  // e.g. an unknown-event error on a view inside this slice). An earlier version gated on
388
697
  // "any error anywhere in the model," which silently failed the check — with zero
389
698
  // diagnostics printed — on an unrelated slice's breakage; scoping to this slice alone
@@ -395,21 +704,46 @@ program
395
704
  const scoped = combined.filter((d) => d.refs?.some((r) => r === key || r.startsWith(`${key}/`)));
396
705
  printDiagnostics(scoped);
397
706
  const ready = scoped.length === 0;
398
- console.log(ready ? `slice "${key}" is ready-to-implement` : `slice "${key}" is NOT ready-to-implement`);
707
+ if (opts.json) {
708
+ // MIL-128: the 4 named gates (see computeSliceReadyGates) plus the same `scoped`
709
+ // diagnostics and `ready` verdict driving the exit code below — replaces both the
710
+ // scraped warning prose and the two hand-parsed English sentences.
711
+ const gates = computeSliceReadyGates(model, refs, dirname(file), key);
712
+ process.stdout.write(buildSliceReadyJson(file, key, gates, scoped, ready) + "\n");
713
+ }
714
+ else {
715
+ console.log(ready ? `slice "${key}" is ready-to-implement` : `slice "${key}" is NOT ready-to-implement`);
716
+ }
399
717
  if (!ready)
400
718
  process.exit(1);
401
719
  return;
402
720
  }
403
721
  if (opts.listIssues || opts.listDivergences || opts.listPublic) {
404
- if (opts.listIssues)
405
- printIssues(model);
406
- if (opts.listDivergences)
407
- printDivergences(model);
408
- if (opts.listPublic)
409
- printPublicElements(model);
410
722
  // Errors still fail the run below — surface them rather than exiting
411
723
  // non-zero with nothing but the list on screen.
412
- printDiagnostics(allDiagnostics.filter((d) => d.severity === "error"));
724
+ const errorsOnly = allDiagnostics.filter((d) => d.severity === "error");
725
+ if (opts.json) {
726
+ const markers = collectMarkers(model, refs, {
727
+ issues: opts.listIssues,
728
+ divergences: opts.listDivergences,
729
+ public: opts.listPublic,
730
+ });
731
+ printDiagnostics(errorsOnly);
732
+ process.stdout.write(buildValidateListJson(file, markers, errorsOnly) + "\n");
733
+ }
734
+ else {
735
+ if (opts.listIssues)
736
+ printIssues(model);
737
+ if (opts.listDivergences)
738
+ printDivergences(model);
739
+ if (opts.listPublic)
740
+ printPublicElements(model);
741
+ printDiagnostics(errorsOnly);
742
+ }
743
+ }
744
+ else if (opts.json) {
745
+ printDiagnostics(allDiagnostics);
746
+ process.stdout.write(buildValidateJson(file, allDiagnostics) + "\n");
413
747
  }
414
748
  else {
415
749
  printDiagnostics(allDiagnostics);
@@ -421,6 +755,61 @@ program
421
755
  if (opts.failOnIssues && model.elements.some((el) => el.issue))
422
756
  process.exit(1);
423
757
  });
758
+ program
759
+ .command("migrate")
760
+ .description("rewrite the old two-slice Automation/Translation shape into the merged single-slice " +
761
+ "shape MIL-120 made canonical (see docs/cli.md)")
762
+ .argument("<file>", "input .em file")
763
+ .option("--write", "apply the rewrite to the file (default: dry run — report only, write nothing)")
764
+ .action((file, opts) => {
765
+ const source = readFileOrExit(file);
766
+ let plan;
767
+ try {
768
+ plan = planMigration(source);
769
+ }
770
+ catch (e) {
771
+ if (e instanceof ParseError) {
772
+ console.error(`parse error in ${file} ${e.message}`);
773
+ process.exit(1);
774
+ }
775
+ throw e;
776
+ }
777
+ if (plan.changes.length === 0 && plan.refusals.length === 0) {
778
+ console.log(`nothing to migrate — ${file} has no old two-slice Automation/Translation shape`);
779
+ return;
780
+ }
781
+ if (plan.changes.length > 0) {
782
+ const verify = verifyMigration(source, plan.rewritten);
783
+ if (!verify.ok) {
784
+ console.error(`em migrate: aborting — the rewrite would introduce ${verify.newErrors.length} new error(s):`);
785
+ for (const d of verify.newErrors)
786
+ console.error(` ${formatDiagnostic(d)}`);
787
+ console.error(`${file} left untouched`);
788
+ process.exit(1);
789
+ }
790
+ }
791
+ for (const c of plan.changes)
792
+ console.log(c.message);
793
+ for (const r of plan.refusals)
794
+ console.log(r.message);
795
+ if (plan.changes.length === 0) {
796
+ console.log(`0 site(s) migrated, ${plan.refusals.length} refused`);
797
+ }
798
+ else if (opts.write) {
799
+ writeFileSync(file, plan.rewritten);
800
+ const refusedNote = plan.refusals.length > 0 ? `, ${plan.refusals.length} refused` : "";
801
+ console.log(`wrote ${file} — ${plan.changes.length} site(s) migrated${refusedNote}`);
802
+ }
803
+ else {
804
+ const refusedNote = plan.refusals.length > 0 ? `; ${plan.refusals.length} refused` : "";
805
+ console.log(`${plan.changes.length} site(s) would migrate (dry run — re-run with --write to apply)${refusedNote}`);
806
+ }
807
+ // A refusal always means the file still needs a human, whether or not other sites in the
808
+ // same run migrated cleanly — same "set exitCode, don't truncate stdout" rationale as
809
+ // em ledger/em diff, even though migrate's own output is never large.
810
+ if (plan.refusals.length > 0)
811
+ process.exitCode = 1;
812
+ });
424
813
  program
425
814
  .command("ledger")
426
815
  .description("check slice docs' version: field agrees with their content across two git revisions " +
@@ -454,6 +843,62 @@ program
454
843
  if (result.findings.length > 0)
455
844
  process.exitCode = 1;
456
845
  });
846
+ program
847
+ .command("coverage")
848
+ .description("check that every INV-* invariant ID cited in a ready-to-implement/implemented slice doc " +
849
+ "is cited by a test under --tests <dir> (MIL-130) — mechanizes reference/implement.md's " +
850
+ "definition-of-done citation check; advisory by default, --strict for CI")
851
+ .argument("<file>", "input .em file")
852
+ .requiredOption("--tests <dir>", "directory to scan recursively for test files citing invariant IDs")
853
+ .option("--strict", "exit non-zero if any invariant ID has zero citations (CI)")
854
+ .option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
855
+ .action((file, opts) => {
856
+ const { model, diagnostics, refs } = compileFile(file);
857
+ printDiagnostics(diagnostics);
858
+ if (hasErrors(diagnostics)) {
859
+ console.error("em coverage: model has errors — fix them first");
860
+ process.exit(1);
861
+ }
862
+ if (!existsSync(opts.tests)) {
863
+ console.error(`em coverage: --tests directory not found: ${opts.tests}`);
864
+ process.exit(1);
865
+ }
866
+ if (!statSync(opts.tests).isDirectory()) {
867
+ console.error(`em coverage: --tests is not a directory: ${opts.tests}`);
868
+ process.exit(1);
869
+ }
870
+ const report = buildCoverageReport(model, refs, dirname(file), opts.tests);
871
+ if (opts.json) {
872
+ process.stdout.write(buildCoverageJson(file, opts.tests, report) + "\n");
873
+ }
874
+ else {
875
+ for (const slice of report.slices) {
876
+ if (!slice.inScope)
877
+ continue;
878
+ console.log(`slice "${slice.key}" (${slice.status}):`);
879
+ if (slice.invariants.length === 0) {
880
+ console.log(` (no INV-* invariant IDs found in the doc body)`);
881
+ continue;
882
+ }
883
+ for (const inv of slice.invariants) {
884
+ if (inv.cited) {
885
+ console.log(` cited ${inv.id}`);
886
+ for (const c of inv.citations)
887
+ console.log(` ${c.file}:${c.line}`);
888
+ }
889
+ else {
890
+ console.log(` uncovered ${inv.id}`);
891
+ }
892
+ }
893
+ }
894
+ console.log(`${report.totalInvariants} invariant(s) checked, ${report.uncoveredCount} uncovered`);
895
+ }
896
+ // Advisory by default (exit 0 even with uncovered IDs) — --strict is the opt-in CI gate,
897
+ // same "set exitCode, don't truncate stdout" rationale as em ledger/em diff for the --json
898
+ // form, and consistent for the text form too.
899
+ if (opts.strict && report.uncoveredCount > 0)
900
+ process.exitCode = 1;
901
+ });
457
902
  // Shared by install/sync/check: the skill directory bundled with whatever em package is
458
903
  // actually running (works whether em was installed from npm or run from a checkout, and
459
904
  // regardless of a symlinked global install — see pkgDir's own resolution above for
@@ -464,6 +909,35 @@ function packagedSkillDir() {
464
909
  function vendoredSkillDir(repoRoot) {
465
910
  return join(resolve(repoRoot), ".claude", "skills", "event-modeling");
466
911
  }
912
+ program
913
+ .command("contract")
914
+ .description("print the packaged implementation contract (reference/implement.md) to stdout — the " +
915
+ "agent-neutral discovery path for any agent that can run a shell, not just Claude Code " +
916
+ "(MIL-129); see docs/cli.md")
917
+ .action(() => {
918
+ process.stdout.write(readContract(packagedSkillDir()));
919
+ });
920
+ program
921
+ .command("mcp")
922
+ .description("start an MCP (Model Context Protocol) server over stdio, exposing validate/slice_ready/" +
923
+ "list_markers/export_model/export_slice/coverage/contract as tools (MIL-21) — a " +
924
+ "structured, agent-facing alternative to shelling out to `em`; see docs/mcp.md. " +
925
+ "Equivalent to running the `em-mcp` bin directly")
926
+ .action(async () => {
927
+ // A 3-line wrapper only, for discoverability — the server itself (src/mcp/) never imports
928
+ // this file or commander, matching the ticket's "outside the CLI core" constraint. The
929
+ // `em-mcp` bin (package.json) starts the identical server the same way.
930
+ await createMcpServer().connect(new StdioServerTransport());
931
+ });
932
+ /** One line reporting what `syncAgentsMd` did, shared by `skill install`/`skill sync` so both
933
+ * commands describe the AGENTS.md write in the same words. */
934
+ function agentsMdMessage(result) {
935
+ if (result.created)
936
+ return `created ${result.path} with the agent-contract section (MIL-129)`;
937
+ if (result.wrote)
938
+ return `updated the agent-contract section in ${result.path}`;
939
+ return `${result.path} already has an up-to-date agent-contract section`;
940
+ }
467
941
  const skill = program
468
942
  .command("skill")
469
943
  .description("manage Claude Code skills bundled with em");
@@ -471,36 +945,46 @@ skill
471
945
  .command("install")
472
946
  .description("copy the event-modeling skill into .claude/skills/event-modeling/")
473
947
  .option("-f, --force", "overwrite an existing installation")
948
+ .option("--no-agents-md", "skip writing/updating the AGENTS.md agent-contract section (on by default, MIL-129)")
474
949
  .action(async (opts) => {
475
950
  const src = packagedSkillDir();
476
951
  const dest = vendoredSkillDir(process.cwd());
477
952
  if (existsSync(dest) && !opts.force) {
478
953
  console.log(`skill already installed at ${dest}`);
479
954
  console.log("re-run with --force to overwrite");
480
- return;
481
955
  }
482
- await mkdir(join(process.cwd(), ".claude", "skills"), { recursive: true });
483
- await cp(src, dest, { recursive: true });
484
- console.log(`installed event-modeling skill → ${dest}`);
485
- console.log("in Claude Code, run /event-modeling to start a guided session");
956
+ else {
957
+ await mkdir(join(process.cwd(), ".claude", "skills"), { recursive: true });
958
+ await cp(src, dest, { recursive: true });
959
+ console.log(`installed event-modeling skill → ${dest}`);
960
+ console.log("in Claude Code, run /event-modeling to start a guided session");
961
+ }
962
+ if (opts.agentsMd !== false) {
963
+ console.log(agentsMdMessage(syncAgentsMd(process.cwd())));
964
+ }
486
965
  });
487
966
  skill
488
967
  .command("sync")
489
968
  .description("update the vendored .claude/skills/event-modeling/ copy in [path] to match the installed " +
490
969
  "em package (overwrites unconditionally; local edits are never merged, MIL-93)")
491
970
  .argument("[path]", "consumer repo root", ".")
492
- .action(async (path) => {
971
+ .option("--no-agents-md", "skip writing/updating the AGENTS.md agent-contract section (on by default, MIL-129)")
972
+ .action(async (path, opts) => {
493
973
  const packagedDir = packagedSkillDir();
494
974
  const vendoredDir = vendoredSkillDir(path);
495
975
  const plan = planSkillSync(packagedDir, vendoredDir);
496
976
  if (plan.changes.length === 0) {
497
977
  console.log(`up to date — ${vendoredDir} already matches the installed skill (${plan.unchangedCount} file(s))`);
498
- return;
499
978
  }
500
- applySkillSync(plan, packagedDir, vendoredDir);
501
- for (const c of plan.changes)
502
- console.log(`${c.kind}: ${c.relPath}`);
503
- console.log(`synced ${vendoredDir} — ${plan.changes.length} file(s) changed, ${plan.unchangedCount} unchanged`);
979
+ else {
980
+ applySkillSync(plan, packagedDir, vendoredDir);
981
+ for (const c of plan.changes)
982
+ console.log(`${c.kind}: ${c.relPath}`);
983
+ console.log(`synced ${vendoredDir} — ${plan.changes.length} file(s) changed, ${plan.unchangedCount} unchanged`);
984
+ }
985
+ if (opts.agentsMd !== false) {
986
+ console.log(agentsMdMessage(syncAgentsMd(resolve(path))));
987
+ }
504
988
  });
505
989
  skill
506
990
  .command("check")
@@ -538,12 +1022,18 @@ if (isMainModule(import.meta.url)) {
538
1022
  });
539
1023
  }
540
1024
  // ---- helpers ----
541
- function compileFile(file, opts = {}) {
1025
+ function compileFile(file, opts = {},
1026
+ // `em watch` must OUTLIVE a bad save — a mid-edit syntax error killing the
1027
+ // watcher (and the --serve live view with it) is a session-ending failure.
1028
+ // One-shot commands keep the exit-with-message behavior.
1029
+ { survive = false } = {}) {
542
1030
  let source;
543
1031
  try {
544
1032
  source = readFileSync(file, "utf8");
545
1033
  }
546
1034
  catch {
1035
+ if (survive)
1036
+ throw new Error(`cannot read ${file}`);
547
1037
  console.error(`cannot read ${file}`);
548
1038
  process.exit(1);
549
1039
  }
@@ -552,6 +1042,8 @@ function compileFile(file, opts = {}) {
552
1042
  }
553
1043
  catch (e) {
554
1044
  if (e instanceof ParseError) {
1045
+ if (survive)
1046
+ throw new Error(`parse error in ${file} ${e.message}`);
555
1047
  console.error(`parse error in ${file} ${e.message}`);
556
1048
  process.exit(1);
557
1049
  }
@@ -657,7 +1149,7 @@ function buildChangelogDoc(file, repoRoot, commits) {
657
1149
  });
658
1150
  const intro = models[0] ? { slices: models[0].slices.length, elements: models[0].elements.length } : null;
659
1151
  const introError = models[0] ? undefined : (errors[0] ?? undefined);
660
- const stateFile = join(dirname(file), ".event-modeling.md");
1152
+ const stateFile = join(dirname(file), STATE_FILE_NAME);
661
1153
  const decisions = existsSync(stateFile) ? parseDecisionsLog(readFileSync(stateFile, "utf8")) : [];
662
1154
  return buildChangelog(entries, decisions, { file, intro, introError });
663
1155
  }