@milehimikey/em 1.9.1 → 1.11.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 (125) hide show
  1. package/.claude/skills/event-modeling/SKILL.md +1 -1
  2. package/.claude/skills/event-modeling-conform/SKILL.md +5 -1
  3. package/.claude/skills/event-modeling-design/SKILL.md +31 -12
  4. package/.claude/skills/event-modeling-discover/SKILL.md +1 -1
  5. package/.claude/skills/event-modeling-implement/SKILL.md +14 -3
  6. package/.claude/skills/event-modeling-implement/reference/implement.md +73 -6
  7. package/.claude/skills/event-modeling-review/SKILL.md +14 -3
  8. package/.claude/skills/event-modeling-shared/reference/em-dsl.md +110 -11
  9. package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +4 -1
  10. package/.claude/skills/event-modeling-shared/templates/constitution.md +111 -0
  11. package/.claude/skills/event-modeling-shared/templates/model-readme.md +2 -2
  12. package/.claude/skills/event-modeling-shared/templates/slice.md +4 -0
  13. package/README.md +8 -1
  14. package/dist/catalog/docJoin.d.ts +8 -0
  15. package/dist/catalog/docJoin.js +4 -0
  16. package/dist/catalog/docJoin.js.map +1 -1
  17. package/dist/catalog/sliceDoc.d.ts +8 -0
  18. package/dist/catalog/sliceDoc.js +2 -0
  19. package/dist/catalog/sliceDoc.js.map +1 -1
  20. package/dist/cli/ciInit.d.ts +10 -3
  21. package/dist/cli/ciInit.js +22 -14
  22. package/dist/cli/ciInit.js.map +1 -1
  23. package/dist/cli/ledgerCheck.d.ts +72 -0
  24. package/dist/cli/ledgerCheck.js +122 -1
  25. package/dist/cli/ledgerCheck.js.map +1 -1
  26. package/dist/cli/ratify.d.ts +37 -5
  27. package/dist/cli/ratify.js +106 -9
  28. package/dist/cli/ratify.js.map +1 -1
  29. package/dist/cli/reratify.js +14 -5
  30. package/dist/cli/reratify.js.map +1 -1
  31. package/dist/cli/review.d.ts +38 -0
  32. package/dist/cli/review.js +160 -0
  33. package/dist/cli/review.js.map +1 -0
  34. package/dist/cli/sliceIndex.d.ts +3 -0
  35. package/dist/cli/sliceIndex.js +5 -4
  36. package/dist/cli/sliceIndex.js.map +1 -1
  37. package/dist/cli/stateFile.d.ts +10 -0
  38. package/dist/cli/stateFile.js +14 -0
  39. package/dist/cli/stateFile.js.map +1 -1
  40. package/dist/cli/status.d.ts +44 -3
  41. package/dist/cli/status.js +75 -18
  42. package/dist/cli/status.js.map +1 -1
  43. package/dist/cli/systemInputs.d.ts +34 -0
  44. package/dist/cli/systemInputs.js +129 -0
  45. package/dist/cli/systemInputs.js.map +1 -0
  46. package/dist/cli.js +362 -23
  47. package/dist/cli.js.map +1 -1
  48. package/dist/emit/freshnessJson.d.ts +1 -1
  49. package/dist/emit/freshnessJson.js +5 -1
  50. package/dist/emit/freshnessJson.js.map +1 -1
  51. package/dist/emit/json.d.ts +37 -1
  52. package/dist/emit/json.js +67 -1
  53. package/dist/emit/json.js.map +1 -1
  54. package/dist/emit/ledgerJson.d.ts +1 -1
  55. package/dist/emit/ledgerJson.js +6 -1
  56. package/dist/emit/ledgerJson.js.map +1 -1
  57. package/dist/emit/queryJson.d.ts +6 -0
  58. package/dist/emit/queryJson.js +33 -0
  59. package/dist/emit/queryJson.js.map +1 -0
  60. package/dist/emit/statusJson.d.ts +1 -1
  61. package/dist/emit/statusJson.js +5 -1
  62. package/dist/emit/statusJson.js.map +1 -1
  63. package/dist/emit/systemJson.d.ts +8 -0
  64. package/dist/emit/systemJson.js +38 -0
  65. package/dist/emit/systemJson.js.map +1 -0
  66. package/dist/mcp/server.d.ts +1 -1
  67. package/dist/mcp/server.js +195 -9
  68. package/dist/mcp/server.js.map +1 -1
  69. package/dist/model/edges.d.ts +28 -1
  70. package/dist/model/edges.js +77 -15
  71. package/dist/model/edges.js.map +1 -1
  72. package/dist/model/model.d.ts +6 -0
  73. package/dist/model/model.js +2 -0
  74. package/dist/model/model.js.map +1 -1
  75. package/dist/model/qualifiedRef.d.ts +47 -0
  76. package/dist/model/qualifiedRef.js +100 -0
  77. package/dist/model/qualifiedRef.js.map +1 -0
  78. package/dist/model/queryIndex.d.ts +53 -0
  79. package/dist/model/queryIndex.js +114 -0
  80. package/dist/model/queryIndex.js.map +1 -0
  81. package/dist/model/rules.d.ts +88 -0
  82. package/dist/model/rules.js +101 -0
  83. package/dist/model/rules.js.map +1 -1
  84. package/dist/model/sliceDocIndex.d.ts +30 -0
  85. package/dist/model/sliceDocIndex.js +110 -0
  86. package/dist/model/sliceDocIndex.js.map +1 -0
  87. package/dist/model/validate.js +85 -11
  88. package/dist/model/validate.js.map +1 -1
  89. package/dist/parser/ast.d.ts +25 -0
  90. package/dist/parser/parser.d.ts +2 -0
  91. package/dist/parser/parser.js +104 -34
  92. package/dist/parser/parser.js.map +1 -1
  93. package/dist/query/format.d.ts +8 -0
  94. package/dist/query/format.js +80 -0
  95. package/dist/query/format.js.map +1 -0
  96. package/dist/query/pipeline.d.ts +17 -0
  97. package/dist/query/pipeline.js +38 -0
  98. package/dist/query/pipeline.js.map +1 -0
  99. package/dist/query/system.d.ts +60 -0
  100. package/dist/query/system.js +87 -0
  101. package/dist/query/system.js.map +1 -0
  102. package/dist/query/verbs.d.ts +100 -0
  103. package/dist/query/verbs.js +306 -0
  104. package/dist/query/verbs.js.map +1 -0
  105. package/dist/render/drawEdges.js +15 -4
  106. package/dist/render/drawEdges.js.map +1 -1
  107. package/dist/render/drawNotes.d.ts +13 -3
  108. package/dist/render/drawNotes.js +96 -20
  109. package/dist/render/drawNotes.js.map +1 -1
  110. package/dist/render/render.js +6 -5
  111. package/dist/render/render.js.map +1 -1
  112. package/dist/render/sliceDiagram.js +1 -0
  113. package/dist/render/sliceDiagram.js.map +1 -1
  114. package/dist/system/manifest.d.ts +45 -0
  115. package/dist/system/manifest.js +178 -0
  116. package/dist/system/manifest.js.map +1 -0
  117. package/dist/system/verify.d.ts +88 -0
  118. package/dist/system/verify.js +263 -0
  119. package/dist/system/verify.js.map +1 -0
  120. package/dist/templates.d.ts +14 -1
  121. package/dist/templates.js +134 -5
  122. package/dist/templates.js.map +1 -1
  123. package/dist/util/slug.js +16 -10
  124. package/dist/util/slug.js.map +1 -1
  125. package/package.json +10 -1
package/dist/cli.js CHANGED
@@ -28,12 +28,12 @@ import { validateDocModelConsistency } from "./catalog/docModelConsistencyValida
28
28
  import { validateOrphanedSliceDocs } from "./catalog/orphanedSliceDocValidate.js";
29
29
  import { validateSliceReady, computeSliceReadyGates } from "./catalog/sliceReadyValidate.js";
30
30
  import { detectSliceDocCollisions } from "./catalog/modelCollisionValidate.js";
31
- import { checkLedger } from "./cli/ledgerCheck.js";
31
+ import { checkLedger, readLedgerWaiverTrailers, applyLedgerWaivers } from "./cli/ledgerCheck.js";
32
32
  import { planMigration, verifyMigration } from "./cli/migrateReactionShape.js";
33
33
  import { buildLedgerJson } from "./emit/ledgerJson.js";
34
34
  import { buildCoverageReport } from "./cli/coverage.js";
35
35
  import { buildCoverageJson } from "./emit/coverageJson.js";
36
- import { resolveSliceStatusFacts, countOpenIssues, resolveConformanceEntry, aggregateInvariantTotals, buildStatusReport, formatStatusText, formatStatusMarkdown, buildStatusBadge, formatConformancePart, } from "./cli/status.js";
36
+ import { resolveSliceStatusFacts, countOpenIssues, resolveConformanceEntry, aggregateInvariantTotals, buildStatusReport, formatStatusText, formatStatusMarkdown, buildStatusBadge, formatConformancePart, findSpecifyRoot, } from "./cli/status.js";
37
37
  import { buildStatusJson } from "./emit/statusJson.js";
38
38
  import { buildFreshnessJson } from "./emit/freshnessJson.js";
39
39
  import { planSkillSyncBundle, applySkillSyncBundle } from "./cli/skillSync.js";
@@ -52,6 +52,7 @@ import { planCatalogArgs } from "./cli/catalog-inputs.js";
52
52
  import { runSliceIndex } from "./cli/sliceIndex.js";
53
53
  import { runMarkImplemented } from "./cli/markImplemented.js";
54
54
  import { runRatify } from "./cli/ratify.js";
55
+ import { runReview } from "./cli/review.js";
55
56
  import { runConformSupersede } from "./cli/conformSupersede.js";
56
57
  import { buildConformScope, changedPathsSince, resolveSliceDocFacts, seedAsisModel } from "./cli/conformScope.js";
57
58
  import { buildSliceDocContent, isSlicePattern, sliceDocKey, SLICE_PATTERNS } from "./cli/sliceNew.js";
@@ -59,13 +60,21 @@ import { wireSliceNote } from "./cli/sliceLink.js";
59
60
  import { listModelCommits } from "./cli/changelog-git.js";
60
61
  import { buildChangelogDoc } from "./cli/changelogBuild.js";
61
62
  import { runReratify } from "./cli/reratify.js";
62
- import { STATE_FILE_NAME, PHASES, isPhase, loadStateFile, parseState, setPhase, setConformance, setReview, isValidDateString, } from "./cli/stateFile.js";
63
- import { STARTER_EM, starterEmFor, scaffoldReadme, scaffoldStateFile } from "./templates.js";
63
+ import { STATE_FILE_NAME, PHASES, isPhase, loadStateFile, parseState, setPhase, setConformance, setReview, isValidDateString, modelPathMismatch, } from "./cli/stateFile.js";
64
+ import { STARTER_EM, starterEmFor, scaffoldReadme, scaffoldStateFile, scaffoldConstitution } from "./templates.js";
64
65
  import { kebabSlug } from "./util/slug.js";
65
66
  import { ciWorkflowPath, conformWorkflowPath, buildCiWorkflowFile, buildConformWorkflowFile, ciManagedBody, conformManagedBody, planCiFile, applyCiFile, findUnsafeCiInitArg, CI_WORKFLOW_MARKER, CONFORM_WORKFLOW_MARKER, } from "./cli/ciInit.js";
66
67
  import { RULES } from "./model/rules.js";
67
68
  import { USAGE_PHASES, isUsagePhase, sortUsagePhases, appendUsageLogEntry, findStateFiles, aggregateUsageReport, formatUsageReportText, } from "./cli/usageLog.js";
68
69
  import { buildUsageReportJson } from "./emit/usageReportJson.js";
70
+ import { compileForQuery } from "./query/pipeline.js";
71
+ import { buildQuerySystem } from "./query/system.js";
72
+ import { queryConsumers, queryProducers, queryDownstream, queryUpstream, querySlices, queryInvariant, queryField, queryPath, } from "./query/verbs.js";
73
+ import { formatConsumers, formatProducers, formatClosure, formatSlices, formatInvariant, formatField, formatPath, } from "./query/format.js";
74
+ import { buildQueryJson } from "./emit/queryJson.js";
75
+ import { loadSystem } from "./cli/systemInputs.js";
76
+ import { verifySystem } from "./system/verify.js";
77
+ import { buildSystemJson } from "./emit/systemJson.js";
69
78
  const program = new Command();
70
79
  // Single source of truth for the version (also read by src/emit/json.ts for
71
80
  // `generator.version` in `em export`): package.json, one level up from both
@@ -90,7 +99,9 @@ program
90
99
  });
91
100
  program
92
101
  .command("scaffold")
93
- .description("scaffold a full project: <slug>/<slug>.em, README.md, .event-modeling.md " +
102
+ .description("scaffold a full project: <slug>/<slug>.em, README.md, .event-modeling.md, and " +
103
+ "constitution.md (the implementation constitution — skipped, with a note, in a spec-kit " +
104
+ "project, where `.specify/memory/constitution.md` IS that document) " +
94
105
  "(see docs/cli.md — for just a starter .em, use `em init`; for a multi-model project, " +
95
106
  "pass --under to nest it under a shared parent directory)")
96
107
  .argument("<name>", "model display name — kebab-cased for the directory and file names, used as-is for titles/prose")
@@ -114,7 +125,18 @@ program
114
125
  writeFileSync(join(dirPath, `${slugName}.em`), starterEmFor(name));
115
126
  writeFileSync(join(dirPath, "README.md"), scaffoldReadme(name, slugName));
116
127
  writeFileSync(join(dirPath, STATE_FILE_NAME), scaffoldStateFile(name, slugName, today));
128
+ // MIL-202: the implementation constitution. One document per project, never two — in a
129
+ // spec-kit project `.specify/memory/constitution.md` already IS that slot, so em defers to it
130
+ // and writes nothing rather than seeding a second, competing copy beside the model. Same
131
+ // walk-up resolution `em status` reports with (src/cli/status.ts's resolveConstitution).
132
+ const specifyRoot = findSpecifyRoot(dirPath);
133
+ if (!specifyRoot)
134
+ writeFileSync(join(dirPath, "constitution.md"), scaffoldConstitution(name));
117
135
  console.log(`scaffolded ${dirPath}/`);
136
+ if (specifyRoot) {
137
+ console.log("note: .specify/ found — the implementation constitution is .specify/memory/constitution.md " +
138
+ "(spec-kit's file); em writes no second copy");
139
+ }
118
140
  });
119
141
  program
120
142
  .command("render")
@@ -512,17 +534,56 @@ slice
512
534
  ? `marked implemented: ${result.path} (implementedIn: ${prUrl})`
513
535
  : `already implemented (no-op): ${result.path}`);
514
536
  });
537
+ slice
538
+ .command("review")
539
+ .description("flip a slice doc's frontmatter to `status: reviewed` and record `reviewedBy:`/`reviewedOn:` " +
540
+ "— the FIRST of the two human gates (MIL-201, docs/process.md#the-slice-lifecycle-gates): " +
541
+ "the room walked this slice and every open question it raised is resolved. Ratification is " +
542
+ "a separate, later gate (`em slice ratify`). Legal from `status: draft`; idempotent on the " +
543
+ "same --by/--on pair; refuses to overwrite a different one already recorded; refuses a doc " +
544
+ "already `ready-to-implement`/`implemented`; never touches `version:` or the doc body")
545
+ .argument("<file>", "input .em file")
546
+ .argument("<slice-key>", "slice export key (kebab-case)")
547
+ .requiredOption("--by <name>", "the reviewer's (or facilitator's) name")
548
+ .option("--on <date>", "review date, YYYY-MM-DD (default: today)")
549
+ .action((file, sliceKey, opts) => {
550
+ const { model, refs, diagnostics } = compileFile(file);
551
+ printDiagnostics(diagnostics);
552
+ // Scoped the same way `em slice ratify`/`em slice mark-implemented`/`em export --slice`/
553
+ // `em validate --slice-ready` are: only an error concerning THIS slice refuses.
554
+ const scopedErrors = diagnostics.filter((d) => d.severity === "error" && d.refs?.some((r) => r === sliceKey || r.startsWith(`${sliceKey}/`)));
555
+ if (scopedErrors.length > 0) {
556
+ console.error(`em slice review: slice "${sliceKey}" has errors — fix them first`);
557
+ process.exit(1);
558
+ }
559
+ const today = new Date().toISOString().slice(0, 10);
560
+ const reviewedOn = opts.on ?? today;
561
+ if (opts.on !== undefined && !isValidDateString(opts.on)) {
562
+ console.error(`em slice review: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
563
+ process.exit(1);
564
+ }
565
+ const result = runReview(model, refs, dirname(file), sliceKey, opts.by, reviewedOn);
566
+ if (!result.ok) {
567
+ console.error(`em slice review: ${result.message}`);
568
+ process.exit(1);
569
+ }
570
+ console.log(result.changed
571
+ ? `reviewed: ${result.path} (reviewedBy: ${opts.by}, reviewedOn: ${reviewedOn})`
572
+ : `already reviewed (no-op): ${result.path}`);
573
+ });
515
574
  slice
516
575
  .command("ratify")
517
576
  .description("flip a slice doc's frontmatter to `status: ready-to-implement` and record `ratifiedBy:`/" +
518
- "`ratifiedOn:` — the handoff sign-off (MIL-165, docs/process.md#what-ratified-means) that " +
519
- "makes who ratified, and when, a first-class recorded fact. Idempotent on the same " +
520
- "--by/--on pair; refuses to overwrite a different one already recorded; never touches " +
521
- "`version:` or the doc body")
577
+ "`ratifiedOn:` — the handoff sign-off (MIL-165, docs/process.md#the-slice-lifecycle-gates) " +
578
+ "that makes who ratified, and when, a first-class recorded fact. Refuses a doc that never " +
579
+ "passed the review gate (`em slice review`) unless --skip-review is passed. Idempotent on " +
580
+ "the same --by/--on pair; refuses to overwrite a different one already recorded; never " +
581
+ "touches `version:` or the doc body")
522
582
  .argument("<file>", "input .em file")
523
583
  .argument("<slice-key>", "slice export key (kebab-case)")
524
584
  .requiredOption("--by <name>", "the ratifier's name")
525
585
  .option("--on <date>", "ratification date, YYYY-MM-DD (default: today)")
586
+ .option("--skip-review", "ratify without a recorded review — prints a loud notice on stderr")
526
587
  .action((file, sliceKey, opts) => {
527
588
  const { model, refs, diagnostics } = compileFile(file);
528
589
  printDiagnostics(diagnostics);
@@ -539,11 +600,23 @@ slice
539
600
  console.error(`em slice ratify: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
540
601
  process.exit(1);
541
602
  }
542
- const result = runRatify(model, refs, dirname(file), sliceKey, opts.by, ratifiedOn);
603
+ const result = runRatify(model, refs, dirname(file), sliceKey, opts.by, ratifiedOn, opts.skipReview === true);
543
604
  if (!result.ok) {
544
605
  console.error(`em slice ratify: ${result.message}`);
545
606
  process.exit(1);
546
607
  }
608
+ // Loud, on stderr, exactly when --skip-review is what let this through — the skip is a
609
+ // visible act in the terminal and in CI logs, and deliberately leaves no trace in the doc.
610
+ if (result.skippedReviewFrom !== null) {
611
+ console.error(`notice: --skip-review — ratifying "${sliceKey}" without a recorded review ` +
612
+ `(status was ${result.skippedReviewFrom})`);
613
+ }
614
+ // MIL-198: advisory only, never refuses — ratifying ahead of an upstream slice the model's
615
+ // own timeline puts before this one (query index `in` adjacency, one hop, other slices only)
616
+ // is sometimes deliberate; this just makes it visible in the terminal and CI logs.
617
+ for (const upstream of result.upstreamWarnings) {
618
+ console.error(`warn: ratifying "${sliceKey}" ahead of upstream slice "${upstream.sliceKey}" (status: ${upstream.status})`);
619
+ }
547
620
  console.log(result.changed
548
621
  ? `ratified: ${result.path} (ratifiedBy: ${opts.by}, ratifiedOn: ${ratifiedOn})`
549
622
  : `already ratified (no-op): ${result.path}`);
@@ -553,8 +626,9 @@ slice
553
626
  .description("bump `version:` and flip a shipped slice doc's frontmatter back to `status: " +
554
627
  "ready-to-implement` — the re-ratification mechanical edit (MIL-161, mirrors `em slice " +
555
628
  "mark-implemented`). Only applies to a doc at `status: implemented`; clears any stale " +
556
- "`ratifiedBy:`/`ratifiedOn:` (they describe the PRIOR version's sign-off) so a follow-up " +
557
- "`em slice ratify --by` applies cleanly; never touches `implementedIn:` or the doc body")
629
+ "`ratifiedBy:`/`ratifiedOn:`/`reviewedBy:`/`reviewedOn:` (they describe the PRIOR version's " +
630
+ "review and sign-off) so a follow-up `em slice ratify --by` applies cleanly — and needs no " +
631
+ "fresh review; never touches `implementedIn:` or the doc body")
558
632
  .argument("<file>", "input .em file")
559
633
  .argument("<slice-key>", "slice export key (kebab-case)")
560
634
  .action((file, sliceKey) => {
@@ -757,7 +831,14 @@ program
757
831
  console.error(`em conform-scope: ${parsed.message}`);
758
832
  process.exit(1);
759
833
  }
760
- const { lastConformance } = parsed.state;
834
+ // MIL-179: the state file names exactly one model in `Model file:`; a sibling this run
835
+ // isn't describing (e.g. a `--seed-asis` scratch copy) must not inherit its conformance
836
+ // record. Same check `em status`'s resolveConformanceEntry runs (PR #116).
837
+ const stateFileMismatch = modelPathMismatch(parsed.state.modelPath, file);
838
+ if (stateFileMismatch) {
839
+ console.error(stateFileMismatch);
840
+ }
841
+ const lastConformance = stateFileMismatch ? null : parsed.state.lastConformance;
761
842
  const { facts, diagnostics: docDiags } = resolveSliceDocFacts(model, refs, dirname(file));
762
843
  printDiagnostics(newDiagnostics(docDiags, diagnostics));
763
844
  const skipGit = !!opts.full || lastConformance === null;
@@ -772,6 +853,9 @@ program
772
853
  }
773
854
  const scope = buildConformScope(facts, lastConformance, changedPaths, !!opts.full);
774
855
  const output = { ...scope };
856
+ if (stateFileMismatch) {
857
+ output.stateFile = { error: stateFileMismatch };
858
+ }
775
859
  if (opts.seedAsis) {
776
860
  output.seeded = seedAsisModel(file);
777
861
  }
@@ -1023,6 +1107,9 @@ program
1023
1107
  if (plan.refusals.length > 0)
1024
1108
  process.exitCode = 1;
1025
1109
  });
1110
+ function describeLedgerWaiveSource(source) {
1111
+ return source.source === "flag" ? "--waive" : `trailer ${source.commit.slice(0, 7)}`;
1112
+ }
1026
1113
  program
1027
1114
  .command("ledger")
1028
1115
  .description("check slice docs' version: field agrees with their content across two git revisions " +
@@ -1030,6 +1117,8 @@ program
1030
1117
  .argument("<file>", "anchor .em file — locates slices/ relative to it; never parsed")
1031
1118
  .option("--from <rev>", "baseline revision")
1032
1119
  .option("--to <rev>", "compare revision (default: current working tree)")
1120
+ .option("--waive <slice-key>", "excuse a doc-content-without-version-bump finding for this slice key (repeatable; MIL-185, " +
1121
+ "see docs/cli.md) — never waives a version regression or bump-without-content-change", (value, previous) => [...previous, value], [])
1033
1122
  .option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
1034
1123
  .action((file, opts) => {
1035
1124
  if (!opts.from) {
@@ -1037,18 +1126,30 @@ program
1037
1126
  process.exit(1);
1038
1127
  }
1039
1128
  const to = opts.to ?? null;
1040
- const result = checkLedger(file, opts.from, to);
1129
+ const rawResult = checkLedger(file, opts.from, to);
1130
+ // Commit trailers apply only to CI/manual runs against a real range — the working-tree
1131
+ // comparison itself (`to === null`) still resolves the trailer range as `from..HEAD`, since
1132
+ // an uncommitted change has no commit to carry a trailer; that gap is `--waive`'s job (see
1133
+ // docs/cli.md).
1134
+ const trailerWaivers = readLedgerWaiverTrailers(file, opts.from, to);
1135
+ const { result, unknownWaivers } = applyLedgerWaivers(rawResult, opts.waive, trailerWaivers);
1136
+ for (const note of unknownWaivers) {
1137
+ console.error(`note: waiver for unknown slice "${note.sliceKey}" ignored`);
1138
+ }
1041
1139
  if (opts.json) {
1042
1140
  process.stdout.write(buildLedgerJson(result, opts.from, to) + "\n");
1043
1141
  }
1044
1142
  else {
1045
1143
  for (const f of result.findings)
1046
1144
  console.log(f.message);
1145
+ for (const w of result.waived)
1146
+ console.log(`waived: ${w.message} (waived by ${describeLedgerWaiveSource(w.waivedBy)})`);
1147
+ const waivedNote = result.waived.length > 0 ? `, ${result.waived.length} waived` : "";
1047
1148
  if (result.findings.length === 0) {
1048
- console.log(`ok — ledger agrees (${result.checkedCount} slice doc(s) checked)`);
1149
+ console.log(`ok — ledger agrees (${result.checkedCount} slice doc(s) checked${waivedNote})`);
1049
1150
  }
1050
1151
  else {
1051
- console.log(`${result.findings.length} ledger mismatch(es)`);
1152
+ console.log(`${result.findings.length} ledger mismatch(es)${waivedNote}`);
1052
1153
  }
1053
1154
  }
1054
1155
  // Set the code rather than process.exit(): same rationale as em diff/em glossary — stdout
@@ -1240,6 +1341,207 @@ program
1240
1341
  console.log(formatConformancePart(entry));
1241
1342
  }
1242
1343
  });
1344
+ // `em query` (MIL-168): deterministic graph queries over the compiled model — scoped, token-
1345
+ // cheap answers instead of a whole-model `em export`. Compiles every input file through the
1346
+ // LIGHTER query sub-pipeline (query/pipeline.ts: parse -> normalize -> refs -> validate against
1347
+ // an empty grid -> ModelIndex — no layout allocation, no DOT emission), same "refuse on compile
1348
+ // errors" gate every JSON surface uses, then hands the built `QuerySystem` to whichever verb
1349
+ // function ran (query/verbs.ts) — the exact same functions the MCP `query` tool calls (MCP
1350
+ // parity, docs/mcp.md). `<files...>` variadic, multi-model, same as `em status`; refs in output
1351
+ // are `<modelKey>:<sliceKey>/<kind>.<slug>`-qualified whenever more than one file is given (see
1352
+ // query/system.ts's header for why), bare/unqualified otherwise.
1353
+ function compileFilesForQuery(files) {
1354
+ const entries = files.map((file) => {
1355
+ const source = readFileOrExit(file);
1356
+ let compiled;
1357
+ try {
1358
+ compiled = compileForQuery(source, dirname(file));
1359
+ }
1360
+ catch (e) {
1361
+ if (e instanceof ParseError) {
1362
+ console.error(`parse error in ${file} ${e.message}`);
1363
+ process.exit(1);
1364
+ }
1365
+ throw e;
1366
+ }
1367
+ printDiagnosticsFor(file, compiled.diagnostics);
1368
+ if (hasErrors(compiled.diagnostics)) {
1369
+ console.error(`em query: not querying — "${file}" has errors — fix them first`);
1370
+ process.exit(1);
1371
+ }
1372
+ return { file, model: compiled.model, refs: compiled.refs, index: compiled.index };
1373
+ });
1374
+ const system = buildQuerySystem(entries);
1375
+ // MIL-193: a `duplicate-model-key` collision is attributed to the file that had to take the
1376
+ // `~n` suffix (its key is `refs[0]`), printed exactly like that file's own compile warnings.
1377
+ for (const d of system.diagnostics) {
1378
+ const owner = system.entries.find((e) => e.modelKey === d.refs?.[0]);
1379
+ printDiagnosticsFor(owner?.file ?? files.join(", "), [d]);
1380
+ }
1381
+ return system;
1382
+ }
1383
+ /** Runs one verb end to end: compile every file, call `fn`, then print — text by default,
1384
+ * `--json` for `emit/queryJson.ts`'s versioned document. A verb-level error (bad/ambiguous ref,
1385
+ * unknown invariant ID, ...) is `console.error` + exit 1; a legitimately empty result set still
1386
+ * exits 0 (MIL-168's own contract — "an event with no consumers prints 'none' and exits 0"). */
1387
+ function runQueryVerb(files, verb, args, json, fn, formatFn) {
1388
+ const system = compileFilesForQuery(files);
1389
+ const result = fn(system);
1390
+ if (!result.ok) {
1391
+ console.error(result.error);
1392
+ process.exit(1);
1393
+ }
1394
+ if (json)
1395
+ process.stdout.write(buildQueryJson(verb, files, args, result.results) + "\n");
1396
+ else
1397
+ console.log(formatFn(result.results));
1398
+ }
1399
+ function parseDepthOpt(depth, verb) {
1400
+ if (depth === undefined)
1401
+ return undefined;
1402
+ // Decimal digits only — `Number()` would also accept "1e1", "0x10", " 5", which are not what
1403
+ // a hop count looks like on a command line.
1404
+ if (!/^[1-9][0-9]*$/.test(depth)) {
1405
+ console.error(`em query ${verb}: --depth must be a positive integer`);
1406
+ process.exit(1);
1407
+ }
1408
+ return Number(depth);
1409
+ }
1410
+ const query = program.command("query").description("deterministic graph queries over the compiled model (MIL-168, see docs/cli.md)");
1411
+ query
1412
+ .command("consumers")
1413
+ .description("views/reactions consuming an event, plus their slices")
1414
+ .argument("<files...>", "input .em files")
1415
+ .requiredOption("--event <ref-or-name>", "the event's export ref or display name")
1416
+ .option("--json", "print a JSON document instead of the text report")
1417
+ .action((files, opts) => {
1418
+ runQueryVerb(files, "consumers", { event: opts.event }, opts.json, (s) => queryConsumers(s, opts.event), formatConsumers);
1419
+ });
1420
+ query
1421
+ .command("producers")
1422
+ .description("commands producing an event, plus their slices and ui triggers")
1423
+ .argument("<files...>", "input .em files")
1424
+ .requiredOption("--event <ref-or-name>", "the event's export ref or display name")
1425
+ .option("--json", "print a JSON document instead of the text report")
1426
+ .action((files, opts) => {
1427
+ runQueryVerb(files, "producers", { event: opts.event }, opts.json, (s) => queryProducers(s, opts.event), formatProducers);
1428
+ });
1429
+ query
1430
+ .command("downstream")
1431
+ .description("transitive closure along legal edges from an element — impact analysis")
1432
+ .argument("<files...>", "input .em files")
1433
+ .requiredOption("--of <ref-or-name>", "the starting element's export ref or display name")
1434
+ .option("--depth <n>", "limit traversal to n hops (default: unlimited)")
1435
+ .option("--json", "print a JSON document instead of the text report")
1436
+ .action((files, opts) => {
1437
+ const depth = parseDepthOpt(opts.depth, "downstream");
1438
+ runQueryVerb(files, "downstream", { of: opts.of, depth: depth ?? null }, opts.json, (s) => queryDownstream(s, opts.of, depth), formatClosure);
1439
+ });
1440
+ query
1441
+ .command("upstream")
1442
+ .description("transitive closure against legal-edge direction from an element")
1443
+ .argument("<files...>", "input .em files")
1444
+ .requiredOption("--of <ref-or-name>", "the starting element's export ref or display name")
1445
+ .option("--depth <n>", "limit traversal to n hops (default: unlimited)")
1446
+ .option("--json", "print a JSON document instead of the text report")
1447
+ .action((files, opts) => {
1448
+ const depth = parseDepthOpt(opts.depth, "upstream");
1449
+ runQueryVerb(files, "upstream", { of: opts.of, depth: depth ?? null }, opts.json, (s) => queryUpstream(s, opts.of, depth), formatClosure);
1450
+ });
1451
+ query
1452
+ .command("slices")
1453
+ .description("filtered slice list — pattern/status/context/persona/tag filters AND-combine")
1454
+ .argument("<files...>", "input .em files")
1455
+ .option("--pattern <p>", "state-change | state-view | automation | translation | unclassified")
1456
+ .option("--status <s>", "the slice's joined doc status (draft, reviewed, ready-to-implement, implemented, ...)")
1457
+ .option("--context <c>", "match a slice with an event in this @Context")
1458
+ .option("--persona <p>", "match a slice with a ui in this @Persona")
1459
+ .option("--tag <t>", "match a slice with an event carrying this tag key")
1460
+ .option("--json", "print a JSON document instead of the text report")
1461
+ .action((files, opts) => {
1462
+ const filters = { pattern: opts.pattern, status: opts.status, context: opts.context, persona: opts.persona, tag: opts.tag };
1463
+ runQueryVerb(files, "slices", filters, opts.json, (s) => querySlices(s, filters), formatSlices);
1464
+ });
1465
+ query
1466
+ .command("invariant")
1467
+ .description("declaring slice + doc facts for one INV-* id, and (with --tests) its test citations")
1468
+ .argument("<files...>", "input .em files")
1469
+ .requiredOption("--id <inv-id>", "the INV-* id to look up")
1470
+ .option("--tests <dir>", "directory to scan for test files citing this id")
1471
+ .option("--json", "print a JSON document instead of the text report")
1472
+ .action((files, opts) => {
1473
+ if (opts.tests) {
1474
+ if (!existsSync(opts.tests)) {
1475
+ console.error(`em query invariant: --tests directory not found: ${opts.tests}`);
1476
+ process.exit(1);
1477
+ }
1478
+ if (!statSync(opts.tests).isDirectory()) {
1479
+ console.error(`em query invariant: --tests is not a directory: ${opts.tests}`);
1480
+ process.exit(1);
1481
+ }
1482
+ }
1483
+ runQueryVerb(files, "invariant", { id: opts.id, tests: opts.tests ?? null }, opts.json, (s) => queryInvariant(s, opts.id, opts.tests), formatInvariant);
1484
+ });
1485
+ query
1486
+ .command("field")
1487
+ .description("one field's facts on an element — type, tag/assigned markers, renamed-from chain")
1488
+ .argument("<files...>", "input .em files")
1489
+ .requiredOption("--of <element-ref>", "the element's export ref or display name")
1490
+ .requiredOption("--name <field>", "the field's name")
1491
+ .option("--json", "print a JSON document instead of the text report")
1492
+ .action((files, opts) => {
1493
+ runQueryVerb(files, "field", { of: opts.of, name: opts.name }, opts.json, (s) => queryField(s, opts.of, opts.name), formatField);
1494
+ });
1495
+ query
1496
+ .command("path")
1497
+ .description("shortest path between two elements through the six legal connection types")
1498
+ .argument("<files...>", "input .em files")
1499
+ .requiredOption("--from <ref-or-name>", "the starting element's export ref or display name")
1500
+ .requiredOption("--to <ref-or-name>", "the ending element's export ref or display name")
1501
+ .option("--json", "print a JSON document instead of the text report")
1502
+ .action((files, opts) => {
1503
+ runQueryVerb(files, "path", { from: opts.from, to: opts.to }, opts.json, (s) => queryPath(s, opts.from, opts.to), formatPath);
1504
+ });
1505
+ program
1506
+ .command("system")
1507
+ .description("verify a seam manifest (system.yaml) against its models' exports — every `public` event/view " +
1508
+ "bound to a reaction in another model, the cross-model half of \"both ends of a flow\" (MIL-194, see docs/cli.md)")
1509
+ .argument("<manifest>", "seam manifest path (YAML, or JSON); each model's `source` resolves relative to it")
1510
+ // Same reasoning as `em validate`'s allowExcessArguments(false) (MIL-123): a second positional
1511
+ // would be silently dropped, and "one manifest per run" is the whole contract here.
1512
+ .allowExcessArguments(false)
1513
+ .option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
1514
+ .action((manifest, opts) => {
1515
+ const loaded = loadSystem(manifest);
1516
+ if (!loaded.ok) {
1517
+ // Same refusal posture as `em export`/`em status`: a manifest that can't be parsed, or a
1518
+ // source that can't be read/compiled, is not a system to verify — no document, exit 1.
1519
+ printSystemDiagnostics(loaded.diagnostics);
1520
+ console.error(`em system: not verifying — ${manifest} could not be loaded; fix the above first`);
1521
+ process.exit(1);
1522
+ }
1523
+ const report = verifySystem(loaded.manifest, loaded.models, manifest);
1524
+ if (opts.json) {
1525
+ // `em validate --json`'s convention: diagnostics still go to stderr in the human format,
1526
+ // the document (which carries the same diagnostics) to stdout; exit code unchanged.
1527
+ printSystemDiagnostics(report.diagnostics);
1528
+ process.stdout.write(buildSystemJson(manifest, loaded.manifestText, report) + "\n");
1529
+ }
1530
+ else {
1531
+ const verified = report.seams.filter((s) => s.status === "verified").length;
1532
+ const failing = report.seams.length - verified;
1533
+ const label = report.name === null ? "system" : `system "${report.name}"`;
1534
+ console.log(`${label}: ${report.models.length} model${report.models.length === 1 ? "" : "s"}, ` +
1535
+ `${report.seams.length} seam${report.seams.length === 1 ? "" : "s"} (${verified} verified, ${failing} failing)`);
1536
+ printSystemDiagnostics(report.diagnostics);
1537
+ if (report.diagnostics.length === 0)
1538
+ console.log("ok — no issues");
1539
+ }
1540
+ // Set the code rather than process.exit(): same rationale as em diff/em ledger — stdout to
1541
+ // a pipe (a --json document) shouldn't risk truncation.
1542
+ if (hasErrors(report.diagnostics))
1543
+ process.exitCode = 1;
1544
+ });
1243
1545
  // Shared by install/sync/check: the `.claude/skills/` root bundled with whatever em package is
1244
1546
  // actually running (works whether em was installed from npm or run from a checkout, and
1245
1547
  // regardless of a symlinked global install — see pkgDir's own resolution above for
@@ -1264,9 +1566,9 @@ program
1264
1566
  });
1265
1567
  program
1266
1568
  .command("mcp")
1267
- .description("start an MCP (Model Context Protocol) server over stdio, exposing validate/slice_ready/" +
1268
- "list_markers/export_model/export_slice/coverage/contract as tools (MIL-21) — a " +
1269
- "structured, agent-facing alternative to shelling out to `em`; see docs/mcp.md. " +
1569
+ .description("start an MCP (Model Context Protocol) server over stdio (MIL-21) — a structured, agent-" +
1570
+ "facing alternative to shelling out to `em`; see docs/mcp.md for the full, current tool " +
1571
+ "table (the list changes as commands gain MCP parity, so it's not repeated here — MIL-187). " +
1270
1572
  "Equivalent to running the `em-mcp` bin directly")
1271
1573
  .action(async () => {
1272
1574
  // A 3-line wrapper only, for discoverability — the server itself (src/mcp/) never imports
@@ -1288,8 +1590,14 @@ const skill = program
1288
1590
  .description("manage Claude Code skills bundled with em");
1289
1591
  skill
1290
1592
  .command("install")
1291
- .description("copy the event-modeling skill bundle into .claude/skills/ (event-modeling, event-modeling-discover/-design/-implement/-conform/-review, event-modeling-shared)")
1292
- .option("-f, --force", "overwrite an existing installation")
1593
+ .description("copy the event-modeling skill bundle into .claude/skills/ (event-modeling, event-modeling-" +
1594
+ "discover/-design/-implement/-conform/-review, event-modeling-shared); across a structural " +
1595
+ "bundle change (e.g. the MIL-157 split of the old single event-modeling/ directory into " +
1596
+ "this six-directory bundle), `em skill sync` is the migration path — --force now performs " +
1597
+ "the same reconcile (MIL-180)")
1598
+ .option("-f, --force", "overwrite an existing installation, reconciling added/updated/removed files within the " +
1599
+ "bundle's own managed directories the same way `em skill sync` does — never touching an " +
1600
+ "unrelated sibling skill (MIL-180)")
1293
1601
  .option("--no-agents-md", "skip writing/updating the AGENTS.md agent-contract section (on by default, MIL-129)")
1294
1602
  .action(async (opts) => {
1295
1603
  const srcRoot = packagedSkillsRoot();
@@ -1299,6 +1607,31 @@ skill
1299
1607
  console.log(`skill already installed at ${anchor}`);
1300
1608
  console.log("re-run with --force to overwrite");
1301
1609
  }
1610
+ else if (existsSync(anchor)) {
1611
+ // --force on an existing installation (MIL-180): reconcile via the same plan/apply
1612
+ // `em skill sync` uses instead of a raw recursive copy, so a file that no longer exists
1613
+ // in the packaged bundle (e.g. left behind by the MIL-157 6-directory split) is removed
1614
+ // from the vendored copy too. Scoped to EM_ALL_SKILL_BUNDLE_DIRS only, exactly like sync
1615
+ // — an unrelated sibling skill under .claude/skills/ is never walked, never touched.
1616
+ await mkdir(destRoot, { recursive: true });
1617
+ const bundlePlan = planSkillSyncBundle(srcRoot, destRoot, EM_ALL_SKILL_BUNDLE_DIRS);
1618
+ const totalChanges = bundlePlan.reduce((n, { plan }) => n + plan.changes.length, 0);
1619
+ const totalUnchanged = bundlePlan.reduce((n, { plan }) => n + plan.unchangedCount, 0);
1620
+ const removedCount = bundlePlan.reduce((n, { plan }) => n + plan.changes.filter((c) => c.kind === "removed").length, 0);
1621
+ applySkillSyncBundle(bundlePlan, srcRoot, destRoot);
1622
+ for (const { dirName, plan } of bundlePlan) {
1623
+ for (const c of plan.changes)
1624
+ console.log(`${c.kind}: ${dirName}/${c.relPath}`);
1625
+ }
1626
+ if (totalChanges === 0) {
1627
+ console.log(`installed event-modeling skill bundle → ${destRoot} — already up to date (${totalUnchanged} file(s))`);
1628
+ }
1629
+ else {
1630
+ console.log(`installed event-modeling skill bundle → ${destRoot} — ${totalChanges} file(s) changed ` +
1631
+ `(${removedCount} removed), ${totalUnchanged} unchanged`);
1632
+ }
1633
+ console.log("in Claude Code, run /event-modeling to start a guided session");
1634
+ }
1302
1635
  else {
1303
1636
  await mkdir(destRoot, { recursive: true });
1304
1637
  for (const name of EM_ALL_SKILL_BUNDLE_DIRS) {
@@ -1385,11 +1718,11 @@ ci.command("init")
1385
1718
  const files = [
1386
1719
  [
1387
1720
  ciPath,
1388
- planCiFile(ciPath, buildCiWorkflowFile(model, opts.tests, PKG_VERSION), ciManagedBody(model, opts.tests), CI_WORKFLOW_MARKER, !!opts.force),
1721
+ planCiFile(ciPath, buildCiWorkflowFile(model, opts.tests, PKG_VERSION), ciManagedBody(model, opts.tests, PKG_VERSION), CI_WORKFLOW_MARKER, !!opts.force),
1389
1722
  ],
1390
1723
  [
1391
1724
  conformPath,
1392
- planCiFile(conformPath, buildConformWorkflowFile(model, PKG_VERSION), conformManagedBody(model), CONFORM_WORKFLOW_MARKER, !!opts.force),
1725
+ planCiFile(conformPath, buildConformWorkflowFile(model, PKG_VERSION), conformManagedBody(model, PKG_VERSION), CONFORM_WORKFLOW_MARKER, !!opts.force),
1393
1726
  ],
1394
1727
  ];
1395
1728
  if (opts.check) {
@@ -1618,6 +1951,12 @@ function printDiagnostics(diags) {
1618
1951
  console.warn(line);
1619
1952
  }
1620
1953
  }
1954
+ /** `em system`'s diagnostics carry their own `file` (manifest or model source) — print each in
1955
+ * the same `<file>: <severity>:<line> <message>` shape printDiagnosticsFor uses. */
1956
+ function printSystemDiagnostics(diags) {
1957
+ for (const d of diags)
1958
+ printDiagnosticsFor(d.file, [d]);
1959
+ }
1621
1960
  /** Same as printDiagnostics, prefixed with the file each diagnostic came from —
1622
1961
  * worth doing once N input files can be more than the two `em diff` compares,
1623
1962
  * where ambiguity about which side a diagnostic belongs to gets materially worse. */