@milehimikey/em 1.6.1 → 1.7.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 (92) hide show
  1. package/.claude/skills/event-modeling/SKILL.md +60 -12
  2. package/.claude/skills/event-modeling/reference/conform.md +30 -8
  3. package/.claude/skills/event-modeling/reference/em-dsl.md +139 -23
  4. package/.claude/skills/event-modeling/reference/extract.md +2 -1
  5. package/.claude/skills/event-modeling/reference/implement.md +172 -0
  6. package/.claude/skills/event-modeling/templates/conformance-report.md +15 -9
  7. package/.claude/skills/event-modeling/templates/model-readme.md +3 -3
  8. package/.claude/skills/event-modeling/templates/slice.md +70 -4
  9. package/.claude/skills/event-modeling/templates/state.md +6 -5
  10. package/README.md +1 -0
  11. package/dist/catalog/build.d.ts +5 -0
  12. package/dist/catalog/build.js +1 -3
  13. package/dist/catalog/build.js.map +1 -1
  14. package/dist/catalog/docJoin.d.ts +39 -0
  15. package/dist/catalog/docJoin.js +86 -0
  16. package/dist/catalog/docJoin.js.map +1 -0
  17. package/dist/catalog/driftSignal.d.ts +16 -0
  18. package/dist/catalog/driftSignal.js +24 -0
  19. package/dist/catalog/driftSignal.js.map +1 -0
  20. package/dist/catalog/frontmatterCoherenceValidate.d.ts +8 -0
  21. package/dist/catalog/frontmatterCoherenceValidate.js +52 -0
  22. package/dist/catalog/frontmatterCoherenceValidate.js.map +1 -0
  23. package/dist/catalog/lineageValidate.d.ts +9 -0
  24. package/dist/catalog/lineageValidate.js +161 -0
  25. package/dist/catalog/lineageValidate.js.map +1 -0
  26. package/dist/catalog/readSliceDoc.d.ts +2 -0
  27. package/dist/catalog/readSliceDoc.js +21 -0
  28. package/dist/catalog/readSliceDoc.js.map +1 -0
  29. package/dist/catalog/sliceDoc.d.ts +94 -3
  30. package/dist/catalog/sliceDoc.js +157 -7
  31. package/dist/catalog/sliceDoc.js.map +1 -1
  32. package/dist/catalog/sliceReadyValidate.d.ts +11 -0
  33. package/dist/catalog/sliceReadyValidate.js +85 -0
  34. package/dist/catalog/sliceReadyValidate.js.map +1 -0
  35. package/dist/cli/diff-inputs.d.ts +20 -0
  36. package/dist/cli/diff-inputs.js +75 -18
  37. package/dist/cli/diff-inputs.js.map +1 -1
  38. package/dist/cli/ledgerCheck.d.ts +38 -0
  39. package/dist/cli/ledgerCheck.js +116 -0
  40. package/dist/cli/ledgerCheck.js.map +1 -0
  41. package/dist/cli/skillCheck.d.ts +18 -0
  42. package/dist/cli/skillCheck.js +63 -0
  43. package/dist/cli/skillCheck.js.map +1 -0
  44. package/dist/cli/skillSync.d.ts +22 -0
  45. package/dist/cli/skillSync.js +69 -0
  46. package/dist/cli/skillSync.js.map +1 -0
  47. package/dist/cli/skillVersionCheck.d.ts +32 -0
  48. package/dist/cli/skillVersionCheck.js +145 -0
  49. package/dist/cli/skillVersionCheck.js.map +1 -0
  50. package/dist/cli.d.ts +3 -1
  51. package/dist/cli.js +199 -32
  52. package/dist/cli.js.map +1 -1
  53. package/dist/emit/diffJson.d.ts +1 -1
  54. package/dist/emit/diffJson.js +18 -14
  55. package/dist/emit/diffJson.js.map +1 -1
  56. package/dist/emit/json.d.ts +10 -25
  57. package/dist/emit/json.js +70 -103
  58. package/dist/emit/json.js.map +1 -1
  59. package/dist/emit/ledgerJson.d.ts +5 -0
  60. package/dist/emit/ledgerJson.js +25 -0
  61. package/dist/emit/ledgerJson.js.map +1 -0
  62. package/dist/emit/skillCheckJson.d.ts +5 -0
  63. package/dist/emit/skillCheckJson.js +32 -0
  64. package/dist/emit/skillCheckJson.js.map +1 -0
  65. package/dist/model/diff.d.ts +44 -1
  66. package/dist/model/diff.js +51 -9
  67. package/dist/model/diff.js.map +1 -1
  68. package/dist/model/model.d.ts +1 -1
  69. package/dist/model/refs.d.ts +23 -0
  70. package/dist/model/refs.js +75 -0
  71. package/dist/model/refs.js.map +1 -0
  72. package/dist/model/rules.d.ts +252 -0
  73. package/dist/model/rules.js +253 -0
  74. package/dist/model/rules.js.map +1 -0
  75. package/dist/model/validate.d.ts +23 -1
  76. package/dist/model/validate.js +168 -47
  77. package/dist/model/validate.js.map +1 -1
  78. package/dist/parser/ast.d.ts +3 -2
  79. package/dist/parser/parser.js +23 -11
  80. package/dist/parser/parser.js.map +1 -1
  81. package/dist/pipeline.d.ts +9 -0
  82. package/dist/pipeline.js +4 -2
  83. package/dist/pipeline.js.map +1 -1
  84. package/dist/render/sliceStatus.js +1 -1
  85. package/dist/render/sliceStatus.js.map +1 -1
  86. package/dist/util/isMainModule.d.ts +14 -0
  87. package/dist/util/isMainModule.js +33 -0
  88. package/dist/util/isMainModule.js.map +1 -0
  89. package/dist/util/walkDir.d.ts +4 -0
  90. package/dist/util/walkDir.js +28 -0
  91. package/dist/util/walkDir.js.map +1 -0
  92. package/package.json +5 -2
@@ -0,0 +1,145 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // MIL-92's version-stamp release gate: checks that package.json's `version` and SKILL.md's
3
+ // `em-version:` frontmatter stamp move together across two revisions. Repo-internal — unlike
4
+ // `em ledger` (MIL-89, src/cli/ledgerCheck.ts), which consumer repos run against their own
5
+ // slice docs, this checks *this repo's own* package version against *this repo's own* bundled
6
+ // skill, so it's never a shipped `em` subcommand — see scripts/check-skill-version-stamp.ts for
7
+ // the thin CLI shim, and docs/ci.md's precedent framing for why a version/content-agreement
8
+ // check needs two revisions rather than being folded into `em validate`.
9
+ //
10
+ // Both sides here are version strings compared for "did it move," not ledger's version-vs-
11
+ // freeform-content comparison — the meaningful case is package.json's version changing without
12
+ // the stamp following it (`skill-version-stamp-not-bumped`). Two more are flagged as cheap extra
13
+ // coverage: a stray stamp edit with no matching package.json move (`-stray-bump`), and the
14
+ // stamp being deleted outright (`-removed`, unconditional — regardless of whether package.json
15
+ // also changed, since losing the mechanism entirely is a problem on its own; a code review on
16
+ // this very PR caught an earlier version that silently skipped the pkgChanged+deleted-stamp
17
+ // combination). The one case that stays clean rather than becoming a finding: the stamp's
18
+ // first-ever introduction (no prior value to compare against) — the normal one-time bootstrap
19
+ // of the field, not a stray edit (this check's own introducing PR, confirmed on CI, first).
20
+ import { readFileSync } from "node:fs";
21
+ import { realGit, resolveRevision } from "./diff-inputs.js";
22
+ /** `to: null` reads the working tree directly (fs); a revision string resolves via git — same
23
+ * `to: null` = working-tree convention `em diff`/`em ledger` use. */
24
+ function readAt(path, rev, runGit) {
25
+ if (rev === null) {
26
+ try {
27
+ return { ok: true, content: readFileSync(path, "utf8") };
28
+ }
29
+ catch {
30
+ return { ok: false };
31
+ }
32
+ }
33
+ const result = resolveRevision(path, rev, runGit);
34
+ return result.ok ? { ok: true, content: result.content } : { ok: false };
35
+ }
36
+ function extractPkgVersion(content) {
37
+ try {
38
+ const parsed = JSON.parse(content);
39
+ const version = parsed?.version;
40
+ return typeof version === "string" ? version : null;
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ /** Pull `em-version: X` out of a `---\n...\n---` frontmatter block. Minimal and single-purpose
47
+ * — SKILL.md's frontmatter is two plain scalar keys plus a folded `description:` block, not
48
+ * the richer schema src/catalog/sliceDoc.ts parses, so this doesn't reuse that parser.
49
+ * Exported for reuse by src/cli/skillCheck.ts (MIL-93), which needs the same extraction to
50
+ * compare a vendored copy's stamp against the installed em's own version — no reason to
51
+ * duplicate the regex a second time. */
52
+ export function extractEmVersionStamp(content) {
53
+ const frontmatter = content.match(/^---\n([\s\S]*?)\n---/);
54
+ if (!frontmatter)
55
+ return null;
56
+ const line = frontmatter[1].split("\n").find((l) => /^em-version:\s*/.test(l));
57
+ if (!line)
58
+ return null;
59
+ return line
60
+ .replace(/^em-version:\s*/, "")
61
+ .trim()
62
+ .replace(/^["']|["']$/g, "");
63
+ }
64
+ /**
65
+ * Compare `pkgJsonPath`'s version and `skillMdPath`'s `em-version:` stamp between revisions
66
+ * `from` and `to` (`to: null` = current working tree, matching `em diff`/`em ledger`). Pure
67
+ * aside from the injected `GitRunner` and fs reads — no process.exit, no console.
68
+ */
69
+ export function checkSkillVersionStamp(pkgJsonPath, skillMdPath, from, to, runGit = realGit) {
70
+ const oldPkg = readAt(pkgJsonPath, from, runGit);
71
+ const newPkg = readAt(pkgJsonPath, to, runGit);
72
+ if (!oldPkg.ok || !newPkg.ok)
73
+ return { finding: null, skipped: "package-json-unreadable" };
74
+ const oldPkgVersion = extractPkgVersion(oldPkg.content);
75
+ const newPkgVersion = extractPkgVersion(newPkg.content);
76
+ if (oldPkgVersion === null || newPkgVersion === null)
77
+ return { finding: null, skipped: "package-json-unreadable" };
78
+ const oldSkill = readAt(skillMdPath, from, runGit);
79
+ const newSkill = readAt(skillMdPath, to, runGit);
80
+ if (!oldSkill.ok || !newSkill.ok)
81
+ return { finding: null, skipped: "skill-md-unreadable" };
82
+ const oldStamp = extractEmVersionStamp(oldSkill.content);
83
+ const newStamp = extractEmVersionStamp(newSkill.content);
84
+ // Never adopted anywhere in this revision span — nothing to compare. Distinct from the stamp
85
+ // being *removed* below: that's a regression, this is simply "the field doesn't exist yet."
86
+ if (oldStamp === null && newStamp === null)
87
+ return { finding: null, skipped: "stamp-missing" };
88
+ // The stamp's first-ever introduction (no em-version: key at `from`, one at `to`) has nothing
89
+ // to compare against either — not a stray edit, the normal one-time bootstrap of the field
90
+ // itself (e.g. this very check landing). Treat it as clean rather than a permanent
91
+ // false-positive on whichever PR happens to add the key.
92
+ if (oldStamp === null)
93
+ return { finding: null, skipped: null };
94
+ const pkgChanged = oldPkgVersion !== newPkgVersion;
95
+ // The stamp existed and is now gone entirely — a stronger signal than merely "left stale"
96
+ // (below), and one a naive "newStamp === null -> skip" early-out would otherwise swallow
97
+ // silently, including the worst case: package.json bumped in the very same change that
98
+ // deleted the stamp (caught in code review, MIL-92 PR #85 — no prior test combined
99
+ // pkgChanged: true with a deleted stamp). Always a finding, regardless of pkgChanged: removing
100
+ // the mechanism entirely is a problem on its own, not just when it coincides with a release.
101
+ if (newStamp === null) {
102
+ return {
103
+ skipped: null,
104
+ finding: {
105
+ code: "skill-version-stamp-removed",
106
+ message: pkgChanged
107
+ ? `SKILL.md's em-version: stamp (was ${oldStamp}) was removed entirely while package.json bumped ${oldPkgVersion} -> ${newPkgVersion}`
108
+ : `SKILL.md's em-version: stamp (was ${oldStamp}) was removed entirely`,
109
+ oldPkgVersion,
110
+ newPkgVersion,
111
+ oldStamp,
112
+ newStamp: null,
113
+ },
114
+ };
115
+ }
116
+ const stampChanged = oldStamp !== newStamp;
117
+ if (pkgChanged && !stampChanged) {
118
+ return {
119
+ skipped: null,
120
+ finding: {
121
+ code: "skill-version-stamp-not-bumped",
122
+ message: `package.json bumped ${oldPkgVersion} -> ${newPkgVersion} but SKILL.md's em-version: stamp stayed at ${newStamp}`,
123
+ oldPkgVersion,
124
+ newPkgVersion,
125
+ oldStamp,
126
+ newStamp,
127
+ },
128
+ };
129
+ }
130
+ if (!pkgChanged && stampChanged) {
131
+ return {
132
+ skipped: null,
133
+ finding: {
134
+ code: "skill-version-stamp-stray-bump",
135
+ message: `SKILL.md's em-version: stamp changed (${oldStamp ?? "(none)"} -> ${newStamp}) but package.json's version didn't move (still ${newPkgVersion})`,
136
+ oldPkgVersion,
137
+ newPkgVersion,
138
+ oldStamp,
139
+ newStamp,
140
+ },
141
+ };
142
+ }
143
+ return { finding: null, skipped: null };
144
+ }
145
+ //# sourceMappingURL=skillVersionCheck.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skillVersionCheck.js","sourceRoot":"","sources":["../../src/cli/skillVersionCheck.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ import { Command } from "commander";
3
+ declare const program: Command;
4
+ export { program };
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import { basename, dirname, extname, join, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { Command } from "commander";
8
8
  import { compile } from "./pipeline.js";
9
+ import { isMainModule } from "./util/isMainModule.js";
9
10
  import { ParseError } from "./parser/parser.js";
10
11
  import { renderDot, layoutDot, composeSvg, writeRendered, formatFromPath } from "./render/render.js";
11
12
  import { buildSliceDiagram } from "./render/sliceDiagram.js";
@@ -16,7 +17,16 @@ import { formatDiagnostic, hasErrors } from "./model/validate.js";
16
17
  import { buildExport } from "./emit/json.js";
17
18
  import { buildDiffJson } from "./emit/diffJson.js";
18
19
  import { diffModels, formatModelDiff, hasChanges } from "./model/diff.js";
19
- import { planDiffArgs, resolveRevision } from "./cli/diff-inputs.js";
20
+ import { planDiffArgs, resolveRevision, resolveDocAtRevision } from "./cli/diff-inputs.js";
21
+ import { readSliceDoc } from "./catalog/readSliceDoc.js";
22
+ import { validateLineage } from "./catalog/lineageValidate.js";
23
+ import { validateFrontmatterCoherence } from "./catalog/frontmatterCoherenceValidate.js";
24
+ import { validateSliceReady } from "./catalog/sliceReadyValidate.js";
25
+ import { checkLedger } from "./cli/ledgerCheck.js";
26
+ import { buildLedgerJson } from "./emit/ledgerJson.js";
27
+ import { planSkillSync, applySkillSync } from "./cli/skillSync.js";
28
+ import { checkSkillSync } from "./cli/skillCheck.js";
29
+ import { buildSkillCheckJson } from "./emit/skillCheckJson.js";
20
30
  import { buildGlossary, detectKindConflicts, detectFieldTypeConflicts, hasConflicts, formatGlossarySummary, formatConflictLine, } from "./model/glossary.js";
21
31
  import { buildGlossaryJson } from "./emit/glossaryJson.js";
22
32
  import { planGlossaryArgs } from "./cli/glossary-inputs.js";
@@ -110,14 +120,14 @@ program
110
120
  .argument("<file>", "input .em file")
111
121
  .option("-o, --out <path>", "write to a file instead of stdout")
112
122
  .action((file, opts) => {
113
- const { model, diagnostics, source } = compileFile(file);
123
+ const { model, refs, diagnostics, source } = compileFile(file);
114
124
  printDiagnostics(diagnostics);
115
125
  if (hasErrors(diagnostics)) {
116
126
  console.error("not exporting: fix the errors above");
117
127
  process.exit(1);
118
128
  }
119
- const exported = buildExport(model, diagnostics, source, file);
120
- printDiagnostics(exported.diagnostics.filter((d) => !diagnostics.includes(d)));
129
+ const exported = buildExport(model, refs, diagnostics, source, file);
130
+ printDiagnostics(newDiagnostics(exported.diagnostics, diagnostics));
121
131
  if (opts.out) {
122
132
  writeFileSync(opts.out, exported.text + "\n");
123
133
  console.log(`wrote ${opts.out}`);
@@ -144,14 +154,22 @@ program
144
154
  if (plan.form === "files") {
145
155
  const oldSource = readFileOrExit(plan.oldFile);
146
156
  const newSource = readFileOrExit(plan.newFile);
147
- runDiff(oldSource, plan.oldFile, newSource, plan.newFile, opts.exitCode, opts.json);
157
+ const lineage = {
158
+ oldDoc: (key) => readSliceDoc(dirname(plan.oldFile), key),
159
+ newDoc: (key) => readSliceDoc(dirname(plan.newFile), key),
160
+ };
161
+ runDiff(oldSource, plan.oldFile, newSource, plan.newFile, opts.exitCode, opts.json, lineage);
148
162
  return;
149
163
  }
150
164
  const oldSource = readAtRevision(plan.file, plan.from);
151
165
  const oldLabel = `${plan.file}@${plan.from}`;
152
166
  const newSource = plan.to ? readAtRevision(plan.file, plan.to) : readFileOrExit(plan.file);
153
167
  const newLabel = plan.to ? `${plan.file}@${plan.to}` : plan.file;
154
- runDiff(oldSource, oldLabel, newSource, newLabel, opts.exitCode, opts.json);
168
+ const lineage = {
169
+ oldDoc: (key) => resolveDocAtRevision(plan.file, key, plan.from),
170
+ newDoc: (key) => plan.to ? resolveDocAtRevision(plan.file, key, plan.to) : readSliceDoc(dirname(plan.file), key),
171
+ };
172
+ runDiff(oldSource, oldLabel, newSource, newLabel, opts.exitCode, opts.json, lineage);
155
173
  });
156
174
  program
157
175
  .command("glossary")
@@ -226,14 +244,20 @@ program
226
244
  process.exit(1);
227
245
  }
228
246
  const inputs = [];
247
+ // Tracked so the post-buildCatalog print below can skip diagnostics already printed
248
+ // here (ref-collision warnings now live in compileFile()'s own `diagnostics`, and
249
+ // buildCatalog() forwards those same, reference-identical objects rather than
250
+ // recomputing them — printing both lists unfiltered would double-print them).
251
+ const diagnosticsByFile = new Map();
229
252
  let anyErrors = false;
230
253
  for (const file of files) {
231
- const { dot, model, grid, diagnostics } = compileFile(file, { keepEmptyLanes: opts.keepEmptyLanes });
254
+ const { dot, model, grid, diagnostics, refs } = compileFile(file, { keepEmptyLanes: opts.keepEmptyLanes });
232
255
  printDiagnosticsFor(file, diagnostics);
256
+ diagnosticsByFile.set(file, diagnostics);
233
257
  warnMissingNotes(file, model);
234
258
  if (hasErrors(diagnostics))
235
259
  anyErrors = true;
236
- inputs.push({ file, model, grid, dot });
260
+ inputs.push({ file, model, grid, dot, refs });
237
261
  }
238
262
  if (anyErrors) {
239
263
  console.error("not building catalog: fix the errors above");
@@ -245,8 +269,10 @@ program
245
269
  title: opts.title,
246
270
  keepEmptyLanes: opts.keepEmptyLanes,
247
271
  });
248
- for (const d of result.diagnostics)
249
- printDiagnosticsFor(d.file, d.diagnostics);
272
+ for (const d of result.diagnostics) {
273
+ const already = diagnosticsByFile.get(d.file) ?? [];
274
+ printDiagnosticsFor(d.file, newDiagnostics(d.diagnostics, already));
275
+ }
250
276
  const modelWord = result.models === 1 ? "model" : "models";
251
277
  const sliceWord = result.slices === 1 ? "slice" : "slices";
252
278
  console.log(`wrote ${opts.out}/ (${result.models} ${modelWord}, ${result.slices} ${sliceWord})`);
@@ -331,31 +357,109 @@ program
331
357
  .argument("<file>", "input .em file")
332
358
  .option("--list-issues", "print only open `issue` diagnostics (slice, element, line, text)")
333
359
  .option("--list-divergences", "print only accepted-divergence annotations (slice, element, line, text) — never fails the build")
334
- .option("--list-public", "print only events marked `public` (slice, name, line) — an integration-surface audit, never fails the build")
360
+ .option("--list-public", "print only events and views marked `public` (slice, kind, name, line) — an integration-surface audit, never fails the build")
335
361
  .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)")
362
+ .option("--slice-ready <key>", "readiness gate for one slice (export key): status ready-to-implement, doc resolvable via " +
363
+ "note binding, zero unchecked Open Questions — exits non-zero if not ready (MIL-87)")
336
364
  .action((file, opts) => {
337
- const { model, diagnostics } = compileFile(file);
365
+ const { model, diagnostics, refs } = compileFile(file);
366
+ // Lineage-ref resolution (MIL-84) and frontmatter-coherence (MIL-85) are validate's
367
+ // fs-aware rules — every other check above is a pure function of the .em source. Both
368
+ // read slices/*.md alongside the model.
369
+ const allDiagnostics = [
370
+ ...diagnostics,
371
+ ...validateLineage(model, refs, dirname(file)),
372
+ ...validateFrontmatterCoherence(model, refs, dirname(file)),
373
+ ];
374
+ if (opts.sliceReady) {
375
+ // MIL-87: a targeted, single-slice readiness gate, not part of the unconditional
376
+ // diagnostic set above (see sliceReadyValidate.ts's header for why). Folds in MIL-85's
377
+ // frontmatter-coherence findings for free by filtering allDiagnostics' own refs, rather
378
+ // than re-deriving that classification here.
379
+ //
380
+ // "Concerns this slice" means a ref that either IS the bare slice key (lineage,
381
+ // frontmatter-coherence, and this module's own diagnostics all tag that way) OR starts
382
+ // with `<sliceKey>/` (every element-level ref from computeRefs()/model/validate.ts,
383
+ // e.g. an unknown-event error on a view inside this slice). An earlier version gated on
384
+ // "any error anywhere in the model," which silently failed the check — with zero
385
+ // diagnostics printed — on an unrelated slice's breakage; scoping to this slice alone
386
+ // matches the ticket's own scenario (check one slice while the rest of a large model is
387
+ // still WIP) and this module's own "single-slice" framing.
388
+ const readyDiagnostics = validateSliceReady(model, refs, dirname(file), opts.sliceReady);
389
+ const combined = [...allDiagnostics, ...readyDiagnostics];
390
+ const key = opts.sliceReady;
391
+ const scoped = combined.filter((d) => d.refs?.some((r) => r === key || r.startsWith(`${key}/`)));
392
+ printDiagnostics(scoped);
393
+ const ready = scoped.length === 0;
394
+ console.log(ready ? `slice "${key}" is ready-to-implement` : `slice "${key}" is NOT ready-to-implement`);
395
+ if (!ready)
396
+ process.exit(1);
397
+ return;
398
+ }
338
399
  if (opts.listIssues || opts.listDivergences || opts.listPublic) {
339
400
  if (opts.listIssues)
340
401
  printIssues(model);
341
402
  if (opts.listDivergences)
342
403
  printDivergences(model);
343
404
  if (opts.listPublic)
344
- printPublicEvents(model);
405
+ printPublicElements(model);
345
406
  // Errors still fail the run below — surface them rather than exiting
346
407
  // non-zero with nothing but the list on screen.
347
- printDiagnostics(diagnostics.filter((d) => d.severity === "error"));
408
+ printDiagnostics(allDiagnostics.filter((d) => d.severity === "error"));
348
409
  }
349
410
  else {
350
- printDiagnostics(diagnostics);
351
- if (diagnostics.length === 0)
411
+ printDiagnostics(allDiagnostics);
412
+ if (allDiagnostics.length === 0)
352
413
  console.log("ok — no issues");
353
414
  }
354
- if (hasErrors(diagnostics))
415
+ if (hasErrors(allDiagnostics))
355
416
  process.exit(1);
356
417
  if (opts.failOnIssues && model.elements.some((el) => el.issue))
357
418
  process.exit(1);
358
419
  });
420
+ program
421
+ .command("ledger")
422
+ .description("check slice docs' version: field agrees with their content across two git revisions " +
423
+ "(opt-in CI check, MIL-89 — never part of `em validate`, see docs/ci.md)")
424
+ .argument("<file>", "anchor .em file — locates slices/ relative to it; never parsed")
425
+ .option("--from <rev>", "baseline revision")
426
+ .option("--to <rev>", "compare revision (default: current working tree)")
427
+ .option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
428
+ .action((file, opts) => {
429
+ if (!opts.from) {
430
+ console.error("em ledger: --from <rev> is required");
431
+ process.exit(1);
432
+ }
433
+ const to = opts.to ?? null;
434
+ const result = checkLedger(file, opts.from, to);
435
+ if (opts.json) {
436
+ process.stdout.write(buildLedgerJson(result, opts.from, to) + "\n");
437
+ }
438
+ else {
439
+ for (const f of result.findings)
440
+ console.log(f.message);
441
+ if (result.findings.length === 0) {
442
+ console.log(`ok — ledger agrees (${result.checkedCount} slice doc(s) checked)`);
443
+ }
444
+ else {
445
+ console.log(`${result.findings.length} ledger mismatch(es)`);
446
+ }
447
+ }
448
+ // Set the code rather than process.exit(): same rationale as em diff/em glossary — stdout
449
+ // to a pipe (a --json document) shouldn't risk truncation.
450
+ if (result.findings.length > 0)
451
+ process.exitCode = 1;
452
+ });
453
+ // Shared by install/sync/check: the skill directory bundled with whatever em package is
454
+ // actually running (works whether em was installed from npm or run from a checkout, and
455
+ // regardless of a symlinked global install — see pkgDir's own resolution above for
456
+ // PKG_VERSION) and the skill directory a consumer repo vendors it into.
457
+ function packagedSkillDir() {
458
+ return join(dirname(fileURLToPath(import.meta.url)), "..", ".claude", "skills", "event-modeling");
459
+ }
460
+ function vendoredSkillDir(repoRoot) {
461
+ return join(resolve(repoRoot), ".claude", "skills", "event-modeling");
462
+ }
359
463
  const skill = program
360
464
  .command("skill")
361
465
  .description("manage Claude Code skills bundled with em");
@@ -364,9 +468,8 @@ skill
364
468
  .description("copy the event-modeling skill into .claude/skills/event-modeling/")
365
469
  .option("-f, --force", "overwrite an existing installation")
366
470
  .action(async (opts) => {
367
- const pkgDir = dirname(fileURLToPath(import.meta.url));
368
- const src = join(pkgDir, "..", ".claude", "skills", "event-modeling");
369
- const dest = join(process.cwd(), ".claude", "skills", "event-modeling");
471
+ const src = packagedSkillDir();
472
+ const dest = vendoredSkillDir(process.cwd());
370
473
  if (existsSync(dest) && !opts.force) {
371
474
  console.log(`skill already installed at ${dest}`);
372
475
  console.log("re-run with --force to overwrite");
@@ -377,10 +480,59 @@ skill
377
480
  console.log(`installed event-modeling skill → ${dest}`);
378
481
  console.log("in Claude Code, run /event-modeling to start a guided session");
379
482
  });
380
- program.parseAsync().catch((e) => {
381
- reportError(e);
382
- process.exit(1);
483
+ skill
484
+ .command("sync")
485
+ .description("update the vendored .claude/skills/event-modeling/ copy in [path] to match the installed " +
486
+ "em package (overwrites unconditionally; local edits are never merged, MIL-93)")
487
+ .argument("[path]", "consumer repo root", ".")
488
+ .action(async (path) => {
489
+ const packagedDir = packagedSkillDir();
490
+ const vendoredDir = vendoredSkillDir(path);
491
+ const plan = planSkillSync(packagedDir, vendoredDir);
492
+ if (plan.changes.length === 0) {
493
+ console.log(`up to date — ${vendoredDir} already matches the installed skill (${plan.unchangedCount} file(s))`);
494
+ return;
495
+ }
496
+ applySkillSync(plan, packagedDir, vendoredDir);
497
+ for (const c of plan.changes)
498
+ console.log(`${c.kind}: ${c.relPath}`);
499
+ console.log(`synced ${vendoredDir} — ${plan.changes.length} file(s) changed, ${plan.unchangedCount} unchanged`);
500
+ });
501
+ skill
502
+ .command("check")
503
+ .description("check the vendored .claude/skills/event-modeling/ copy in [path] for drift against the " +
504
+ "installed em package; exits non-zero on any mismatch (CI-ready, MIL-93)")
505
+ .argument("[path]", "consumer repo root", ".")
506
+ .option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
507
+ .action((path, opts) => {
508
+ const packagedDir = packagedSkillDir();
509
+ const vendoredDir = vendoredSkillDir(path);
510
+ const result = checkSkillSync(packagedDir, vendoredDir, PKG_VERSION);
511
+ if (opts.json) {
512
+ process.stdout.write(buildSkillCheckJson(result, vendoredDir, PKG_VERSION) + "\n");
513
+ }
514
+ else {
515
+ for (const f of result.findings)
516
+ console.log(f.message);
517
+ console.log(result.ok ? `ok — vendored skill matches em ${PKG_VERSION}` : `${result.findings.length} mismatch(es)`);
518
+ }
519
+ // Set the code rather than process.exit(): same rationale as em ledger/em diff — stdout to
520
+ // a pipe (a --json document) shouldn't risk truncation.
521
+ if (!result.ok)
522
+ process.exitCode = 1;
383
523
  });
524
+ // Exported so dev tooling (e.g. scripts/generate-skill-docs.ts) can introspect the registered
525
+ // commands/options without triggering a real CLI run. Guarded the same way Node's CJS
526
+ // `require.main === module` used to: only actually parse argv when this file is the process's
527
+ // entry module, not merely `import`ed. See util/isMainModule.ts for why this has to
528
+ // realpath-resolve argv[1] — a naive path comparison breaks the `npm i -g` symlink case.
529
+ export { program };
530
+ if (isMainModule(import.meta.url)) {
531
+ program.parseAsync().catch((e) => {
532
+ reportError(e);
533
+ process.exit(1);
534
+ });
535
+ }
384
536
  // ---- helpers ----
385
537
  function compileFile(file, opts = {}) {
386
538
  let source;
@@ -448,27 +600,32 @@ function readAtRevision(file, rev) {
448
600
  */
449
601
  function buildChangelogDoc(file, repoRoot, commits) {
450
602
  const models = [];
603
+ const refsList = [];
451
604
  const errors = [];
452
605
  for (const c of commits) {
453
606
  const rev = readFileAtCommit(repoRoot, c);
454
607
  if (!rev.ok) {
455
608
  models.push(null);
609
+ refsList.push(null);
456
610
  errors.push(rev.message);
457
611
  continue;
458
612
  }
459
613
  try {
460
- const { model, diagnostics } = compile(rev.content);
614
+ const { model, refs, diagnostics } = compile(rev.content);
461
615
  if (hasErrors(diagnostics)) {
462
616
  models.push(null);
617
+ refsList.push(null);
463
618
  errors.push(`validation errors at ${c.shortHash} — fix with \`em validate\` at that revision`);
464
619
  }
465
620
  else {
466
621
  models.push(model);
622
+ refsList.push(refs);
467
623
  errors.push(null);
468
624
  }
469
625
  }
470
626
  catch (e) {
471
627
  models.push(null);
628
+ refsList.push(null);
472
629
  errors.push(e instanceof ParseError ? `parse error: ${e.message}` : e instanceof Error ? e.message : String(e));
473
630
  }
474
631
  }
@@ -486,7 +643,10 @@ function buildChangelogDoc(file, repoRoot, commits) {
486
643
  entries.push({ ...base, diff: null, error: `no earlier parseable revision to diff against (${commits[0].shortHash}: ${errors[0]})` });
487
644
  }
488
645
  else {
489
- entries.push({ ...base, diff: diffModels(models[prevParseable], models[i]) });
646
+ entries.push({
647
+ ...base,
648
+ diff: diffModels(models[prevParseable], models[i], refsList[prevParseable], refsList[i]),
649
+ });
490
650
  }
491
651
  if (models[i])
492
652
  prevParseable = i;
@@ -498,7 +658,7 @@ function buildChangelogDoc(file, repoRoot, commits) {
498
658
  return buildChangelog(entries, decisions, { file, intro, introError });
499
659
  }
500
660
  /** Shared body for both `em diff` forms: compile both sides, gate on errors, print, exit-code. */
501
- function runDiff(oldSource, oldLabel, newSource, newLabel, exitCode, json) {
661
+ function runDiff(oldSource, oldLabel, newSource, newLabel, exitCode, json, lineage) {
502
662
  const oldResult = compileSource(oldSource, oldLabel);
503
663
  const newResult = compileSource(newSource, newLabel);
504
664
  printDiagnostics(oldResult.diagnostics);
@@ -507,7 +667,7 @@ function runDiff(oldSource, oldLabel, newSource, newLabel, exitCode, json) {
507
667
  console.error("not diffing: fix the errors above");
508
668
  process.exit(1);
509
669
  }
510
- const diff = diffModels(oldResult.model, newResult.model);
670
+ const diff = diffModels(oldResult.model, newResult.model, oldResult.refs, newResult.refs, lineage);
511
671
  if (json) {
512
672
  const oldSide = { label: oldLabel, source: oldSource, diagnostics: oldResult.diagnostics };
513
673
  const newSide = { label: newLabel, source: newSource, diagnostics: newResult.diagnostics };
@@ -561,18 +721,25 @@ function printDivergences(model) {
561
721
  console.log(` divergence :${el.line} slice "${slice.name}" ${el.kind} "${el.name}": ${el.divergence}`);
562
722
  }
563
723
  }
564
- /** Print only events marked `public`: slice, name, line — for an integration-surface audit. */
565
- function printPublicEvents(model) {
566
- const pub = model.elements.filter((el) => el.kind === "event" && el.public);
724
+ /** Print only elements marked `public`: slice, kind, name, line — for an integration-surface audit. */
725
+ function printPublicElements(model) {
726
+ const pub = model.elements.filter((el) => el.public && (el.kind === "event" || el.kind === "view"));
567
727
  if (pub.length === 0) {
568
- console.log("no public events");
728
+ console.log("no public elements");
569
729
  return;
570
730
  }
571
731
  for (const el of pub) {
572
732
  const slice = model.slices[el.sliceIndex];
573
- console.log(` public :${el.line} slice "${slice.name}" event "${el.name}"`);
733
+ console.log(` public :${el.line} slice "${slice.name}" ${el.kind} "${el.name}"`);
574
734
  }
575
735
  }
736
+ /** Diagnostics in `all` not already present (by reference identity) in `already` — avoids
737
+ * double-printing diagnostic objects forwarded unchanged from an earlier compile step (e.g.
738
+ * ref-collision warnings `buildExport()`/`buildCatalog()` fold in from the same `RefsResult`
739
+ * `compileFile()` already printed once). */
740
+ function newDiagnostics(all, already) {
741
+ return all.filter((d) => !already.includes(d));
742
+ }
576
743
  function printDiagnostics(diags) {
577
744
  if (diags.length === 0)
578
745
  return;