@milehimikey/em 1.11.0 → 1.13.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 (154) hide show
  1. package/.claude/skills/event-modeling/SKILL.md +4 -2
  2. package/.claude/skills/event-modeling-conform/SKILL.md +20 -9
  3. package/.claude/skills/event-modeling-conform/reference/conform.md +107 -10
  4. package/.claude/skills/event-modeling-design/SKILL.md +22 -6
  5. package/.claude/skills/event-modeling-discover/SKILL.md +1 -1
  6. package/.claude/skills/event-modeling-implement/SKILL.md +12 -6
  7. package/.claude/skills/event-modeling-implement/reference/implement.md +165 -18
  8. package/.claude/skills/event-modeling-review/SKILL.md +5 -1
  9. package/.claude/skills/event-modeling-shared/reference/em-dsl.md +57 -8
  10. package/.claude/skills/event-modeling-shared/reference/operating-principles.md +13 -3
  11. package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +69 -0
  12. package/.claude/skills/event-modeling-shared/templates/conformance-report.md +8 -1
  13. package/.claude/skills/event-modeling-shared/templates/constitution.md +8 -1
  14. package/.claude/skills/event-modeling-shared/templates/state.md +3 -0
  15. package/README.md +1 -0
  16. package/dist/catalog/build.js +42 -1
  17. package/dist/catalog/build.js.map +1 -1
  18. package/dist/catalog/coverage.d.ts +15 -0
  19. package/dist/catalog/coverage.js +34 -0
  20. package/dist/catalog/coverage.js.map +1 -0
  21. package/dist/catalog/docJoin.d.ts +20 -2
  22. package/dist/catalog/docJoin.js +32 -4
  23. package/dist/catalog/docJoin.js.map +1 -1
  24. package/dist/catalog/docModelConsistencyValidate.js +1 -1
  25. package/dist/catalog/docModelConsistencyValidate.js.map +1 -1
  26. package/dist/catalog/driftSignal.d.ts +11 -5
  27. package/dist/catalog/driftSignal.js +18 -4
  28. package/dist/catalog/driftSignal.js.map +1 -1
  29. package/dist/catalog/modelVersionValidate.d.ts +9 -0
  30. package/dist/catalog/modelVersionValidate.js +38 -0
  31. package/dist/catalog/modelVersionValidate.js.map +1 -0
  32. package/dist/catalog/noteBindingValidate.js +18 -0
  33. package/dist/catalog/noteBindingValidate.js.map +1 -1
  34. package/dist/catalog/orphanedSliceDocValidate.d.ts +8 -1
  35. package/dist/catalog/orphanedSliceDocValidate.js +33 -6
  36. package/dist/catalog/orphanedSliceDocValidate.js.map +1 -1
  37. package/dist/catalog/pages.d.ts +16 -1
  38. package/dist/catalog/pages.js +16 -4
  39. package/dist/catalog/pages.js.map +1 -1
  40. package/dist/catalog/sliceDoc.d.ts +12 -0
  41. package/dist/catalog/sliceDoc.js +15 -0
  42. package/dist/catalog/sliceDoc.js.map +1 -1
  43. package/dist/catalog/sliceReadyValidate.d.ts +12 -1
  44. package/dist/catalog/sliceReadyValidate.js +4 -4
  45. package/dist/catalog/sliceReadyValidate.js.map +1 -1
  46. package/dist/cli/agentsMd.js +9 -3
  47. package/dist/cli/agentsMd.js.map +1 -1
  48. package/dist/cli/ciInit.js +20 -0
  49. package/dist/cli/ciInit.js.map +1 -1
  50. package/dist/cli/conformScope.d.ts +1 -0
  51. package/dist/cli/conformScope.js +13 -4
  52. package/dist/cli/conformScope.js.map +1 -1
  53. package/dist/cli/conformSupersede.d.ts +24 -2
  54. package/dist/cli/conformSupersede.js +44 -3
  55. package/dist/cli/conformSupersede.js.map +1 -1
  56. package/dist/cli/coverage.d.ts +34 -9
  57. package/dist/cli/coverage.js +61 -24
  58. package/dist/cli/coverage.js.map +1 -1
  59. package/dist/cli/findings.d.ts +134 -0
  60. package/dist/cli/findings.js +355 -0
  61. package/dist/cli/findings.js.map +1 -0
  62. package/dist/cli/markImplemented.js +12 -1
  63. package/dist/cli/markImplemented.js.map +1 -1
  64. package/dist/cli/metrics.d.ts +93 -0
  65. package/dist/cli/metrics.js +331 -0
  66. package/dist/cli/metrics.js.map +1 -0
  67. package/dist/cli/modelVersion.d.ts +139 -0
  68. package/dist/cli/modelVersion.js +256 -0
  69. package/dist/cli/modelVersion.js.map +1 -0
  70. package/dist/cli/ratify.js +17 -1
  71. package/dist/cli/ratify.js.map +1 -1
  72. package/dist/cli/reratify.d.ts +17 -0
  73. package/dist/cli/reratify.js +37 -2
  74. package/dist/cli/reratify.js.map +1 -1
  75. package/dist/cli/review.js +12 -1
  76. package/dist/cli/review.js.map +1 -1
  77. package/dist/cli/skillCheck.js +2 -2
  78. package/dist/cli/skillCheck.js.map +1 -1
  79. package/dist/cli/sliceConform.d.ts +50 -0
  80. package/dist/cli/sliceConform.js +206 -0
  81. package/dist/cli/sliceConform.js.map +1 -0
  82. package/dist/cli/sliceIndex.d.ts +2 -0
  83. package/dist/cli/sliceIndex.js +24 -7
  84. package/dist/cli/sliceIndex.js.map +1 -1
  85. package/dist/cli/sliceLink.js +16 -0
  86. package/dist/cli/sliceLink.js.map +1 -1
  87. package/dist/cli/sliceNew.d.ts +12 -0
  88. package/dist/cli/sliceNew.js +23 -0
  89. package/dist/cli/sliceNew.js.map +1 -1
  90. package/dist/cli/sliceStubAll.d.ts +76 -0
  91. package/dist/cli/sliceStubAll.js +189 -0
  92. package/dist/cli/sliceStubAll.js.map +1 -0
  93. package/dist/cli/stateFile.d.ts +63 -7
  94. package/dist/cli/stateFile.js +179 -18
  95. package/dist/cli/stateFile.js.map +1 -1
  96. package/dist/cli/status.d.ts +90 -1
  97. package/dist/cli/status.js +151 -16
  98. package/dist/cli/status.js.map +1 -1
  99. package/dist/cli/upgrade.d.ts +122 -0
  100. package/dist/cli/upgrade.js +476 -0
  101. package/dist/cli/upgrade.js.map +1 -0
  102. package/dist/cli.js +586 -66
  103. package/dist/cli.js.map +1 -1
  104. package/dist/emit/coverageJson.d.ts +2 -2
  105. package/dist/emit/coverageJson.js +6 -2
  106. package/dist/emit/coverageJson.js.map +1 -1
  107. package/dist/emit/freshnessJson.d.ts +1 -1
  108. package/dist/emit/freshnessJson.js +3 -1
  109. package/dist/emit/freshnessJson.js.map +1 -1
  110. package/dist/emit/json.d.ts +34 -1
  111. package/dist/emit/json.js +84 -2
  112. package/dist/emit/json.js.map +1 -1
  113. package/dist/emit/metricsJson.d.ts +7 -0
  114. package/dist/emit/metricsJson.js +27 -0
  115. package/dist/emit/metricsJson.js.map +1 -0
  116. package/dist/emit/statusJson.d.ts +1 -1
  117. package/dist/emit/statusJson.js +22 -1
  118. package/dist/emit/statusJson.js.map +1 -1
  119. package/dist/emit/upgradeJson.d.ts +5 -0
  120. package/dist/emit/upgradeJson.js +26 -0
  121. package/dist/emit/upgradeJson.js.map +1 -0
  122. package/dist/emit/validateJson.d.ts +10 -5
  123. package/dist/emit/validateJson.js +22 -8
  124. package/dist/emit/validateJson.js.map +1 -1
  125. package/dist/mcp/server.d.ts +1 -1
  126. package/dist/mcp/server.js +151 -22
  127. package/dist/mcp/server.js.map +1 -1
  128. package/dist/model/continuation.d.ts +23 -0
  129. package/dist/model/continuation.js +49 -0
  130. package/dist/model/continuation.js.map +1 -0
  131. package/dist/model/queryIndex.js +1 -1
  132. package/dist/model/queryIndex.js.map +1 -1
  133. package/dist/model/rules.d.ts +13 -0
  134. package/dist/model/rules.js +18 -0
  135. package/dist/model/rules.js.map +1 -1
  136. package/dist/model/sliceDocIndex.d.ts +12 -5
  137. package/dist/model/sliceDocIndex.js +18 -7
  138. package/dist/model/sliceDocIndex.js.map +1 -1
  139. package/dist/model/validate.d.ts +20 -0
  140. package/dist/model/validate.js +94 -45
  141. package/dist/model/validate.js.map +1 -1
  142. package/dist/render/sliceStatus.d.ts +3 -3
  143. package/dist/render/sliceStatus.js +27 -29
  144. package/dist/render/sliceStatus.js.map +1 -1
  145. package/dist/templates.d.ts +3 -3
  146. package/dist/templates.js +15 -2
  147. package/dist/templates.js.map +1 -1
  148. package/dist/util/localDate.d.ts +2 -0
  149. package/dist/util/localDate.js +17 -0
  150. package/dist/util/localDate.js.map +1 -0
  151. package/dist/util/semver.d.ts +19 -0
  152. package/dist/util/semver.js +54 -0
  153. package/dist/util/semver.js.map +1 -0
  154. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -7,14 +7,16 @@ import { fileURLToPath } from "node:url";
7
7
  import { Command } from "commander";
8
8
  import { compile } from "./pipeline.js";
9
9
  import { isMainModule } from "./util/isMainModule.js";
10
+ import { localIsoDate } from "./util/localDate.js";
11
+ import { continuationOf } from "./model/continuation.js";
10
12
  import { ParseError } from "./parser/parser.js";
11
13
  import { renderDot, layoutDot, composeSvg, writeRendered, formatFromPath } from "./render/render.js";
12
14
  import { buildSliceDiagram } from "./render/sliceDiagram.js";
13
15
  import { resolveSliceArg, defaultSliceOut } from "./cli/render-inputs.js";
14
16
  import { serializeBuilds, watchFile } from "./render/watch.js";
15
17
  import { startLiveServer } from "./render/serve.js";
16
- import { formatDiagnostic, hasErrors } from "./model/validate.js";
17
- import { buildExport, buildSliceExport } from "./emit/json.js";
18
+ import { formatDiagnostic, hasErrors, publicViewsWithoutInModelReader } from "./model/validate.js";
19
+ import { buildExport, buildSliceExport, GENERATOR_VERSION } from "./emit/json.js";
18
20
  import { buildTypeSpec } from "./emit/typespec.js";
19
21
  import { buildValidateJson, buildSliceReadyJson, buildValidateListJson, collectMarkers } from "./emit/validateJson.js";
20
22
  import { buildDiffJson } from "./emit/diffJson.js";
@@ -26,16 +28,19 @@ import { validateFrontmatterCoherence } from "./catalog/frontmatterCoherenceVali
26
28
  import { validateNoteBindings } from "./catalog/noteBindingValidate.js";
27
29
  import { validateDocModelConsistency } from "./catalog/docModelConsistencyValidate.js";
28
30
  import { validateOrphanedSliceDocs } from "./catalog/orphanedSliceDocValidate.js";
31
+ import { validateModelVersionStale } from "./catalog/modelVersionValidate.js";
29
32
  import { validateSliceReady, computeSliceReadyGates } from "./catalog/sliceReadyValidate.js";
30
33
  import { detectSliceDocCollisions } from "./catalog/modelCollisionValidate.js";
31
34
  import { checkLedger, readLedgerWaiverTrailers, applyLedgerWaivers } from "./cli/ledgerCheck.js";
32
35
  import { planMigration, verifyMigration } from "./cli/migrateReactionShape.js";
33
36
  import { buildLedgerJson } from "./emit/ledgerJson.js";
34
- import { buildCoverageReport } from "./cli/coverage.js";
37
+ import { buildCoverageReport, resolveScopedSlices } from "./cli/coverage.js";
35
38
  import { buildCoverageJson } from "./emit/coverageJson.js";
36
- import { resolveSliceStatusFacts, countOpenIssues, resolveConformanceEntry, aggregateInvariantTotals, buildStatusReport, formatStatusText, formatStatusMarkdown, buildStatusBadge, formatConformancePart, findSpecifyRoot, } from "./cli/status.js";
39
+ import { resolveSliceStatusFacts, countOpenIssues, resolveConformanceEntry, aggregateInvariantTotals, buildStatusReport, formatStatusText, formatStatusMarkdown, buildStatusBadge, formatConformancePart, findSpecifyRoot, resolveModelVersionStatusEntry, resolveEmVersionStatusEntry, } from "./cli/status.js";
37
40
  import { buildStatusJson } from "./emit/statusJson.js";
38
41
  import { buildFreshnessJson } from "./emit/freshnessJson.js";
42
+ import { computeMetrics, formatMetricsText } from "./cli/metrics.js";
43
+ import { buildMetricsJson } from "./emit/metricsJson.js";
39
44
  import { planSkillSyncBundle, applySkillSyncBundle } from "./cli/skillSync.js";
40
45
  import { checkSkillSyncBundle } from "./cli/skillCheck.js";
41
46
  import { buildSkillCheckJson } from "./emit/skillCheckJson.js";
@@ -55,15 +60,22 @@ import { runRatify } from "./cli/ratify.js";
55
60
  import { runReview } from "./cli/review.js";
56
61
  import { runConformSupersede } from "./cli/conformSupersede.js";
57
62
  import { buildConformScope, changedPathsSince, resolveSliceDocFacts, seedAsisModel } from "./cli/conformScope.js";
58
- import { buildSliceDocContent, isSlicePattern, sliceDocKey, SLICE_PATTERNS } from "./cli/sliceNew.js";
63
+ import { buildSliceDocContent, buildStubDocContent, isSlicePattern, sliceDocKey, SLICE_PATTERNS } from "./cli/sliceNew.js";
59
64
  import { wireSliceNote } from "./cli/sliceLink.js";
65
+ import { isStubStatus, runStubAll, STUB_STATUSES } from "./cli/sliceStubAll.js";
60
66
  import { listModelCommits } from "./cli/changelog-git.js";
61
67
  import { buildChangelogDoc } from "./cli/changelogBuild.js";
62
68
  import { runReratify } from "./cli/reratify.js";
63
- import { STATE_FILE_NAME, PHASES, isPhase, loadStateFile, parseState, setPhase, setConformance, setReview, isValidDateString, modelPathMismatch, } from "./cli/stateFile.js";
69
+ import { runSliceConform } from "./cli/sliceConform.js";
70
+ import { checkFindingsFile, buildCheckFindingsJson, lookupFindingsBesideReport, unruledFindingsInScope, FINDING_LOCI } from "./cli/findings.js";
71
+ import { STATE_FILE_NAME, PHASES, isPhase, loadStateFile, parseState, setPhase, setConformance, setModelVersion, setCertified, setEmVersion, setReview, isValidDateString, modelPathMismatch, } from "./cli/stateFile.js";
72
+ import { findCertifiedVersion, latestModelVersionNumber, modelVersionDrift, runCertifyModelVersion, runModelVersionBump, } from "./cli/modelVersion.js";
64
73
  import { STARTER_EM, starterEmFor, scaffoldReadme, scaffoldStateFile, scaffoldConstitution } from "./templates.js";
65
74
  import { kebabSlug } from "./util/slug.js";
66
75
  import { ciWorkflowPath, conformWorkflowPath, buildCiWorkflowFile, buildConformWorkflowFile, ciManagedBody, conformManagedBody, planCiFile, applyCiFile, findUnsafeCiInitArg, CI_WORKFLOW_MARKER, CONFORM_WORKFLOW_MARKER, } from "./cli/ciInit.js";
76
+ import { resolveRepoRoot, detectUpgrade, applyUpgrade, checkUpgrade } from "./cli/upgrade.js";
77
+ import { buildUpgradeJson } from "./emit/upgradeJson.js";
78
+ import { isOlder } from "./util/semver.js";
67
79
  import { RULES } from "./model/rules.js";
68
80
  import { USAGE_PHASES, isUsagePhase, sortUsagePhases, appendUsageLogEntry, findStateFiles, aggregateUsageReport, formatUsageReportText, } from "./cli/usageLog.js";
69
81
  import { buildUsageReportJson } from "./emit/usageReportJson.js";
@@ -121,10 +133,10 @@ program
121
133
  process.exit(1);
122
134
  }
123
135
  await mkdir(dirPath, { recursive: true });
124
- const today = new Date().toISOString().slice(0, 10);
136
+ const today = localIsoDate();
125
137
  writeFileSync(join(dirPath, `${slugName}.em`), starterEmFor(name));
126
138
  writeFileSync(join(dirPath, "README.md"), scaffoldReadme(name, slugName));
127
- writeFileSync(join(dirPath, STATE_FILE_NAME), scaffoldStateFile(name, slugName, today));
139
+ writeFileSync(join(dirPath, STATE_FILE_NAME), scaffoldStateFile(name, slugName, today, PKG_VERSION));
128
140
  // MIL-202: the implementation constitution. One document per project, never two — in a
129
141
  // spec-kit project `.specify/memory/constitution.md` already IS that slot, so em defers to it
130
142
  // and writes nothing rather than seeding a second, competing copy beside the model. Same
@@ -435,6 +447,9 @@ slice
435
447
  .option("-f, --force", "overwrite the file if it already exists")
436
448
  .option("--wire <model-file>", "also insert the `note \"slices/<key>.md\"` line onto the slice's primary element in this " +
437
449
  ".em file (matched by export key), instead of just printing it to paste by hand (MIL-161)")
450
+ .option("--stub", "write a near-free stub instead: same 5 frontmatter keys, but a one-line placeholder body " +
451
+ "instead of the diagram-image stub and every judgment section (MIL-184) — deepen it later " +
452
+ "by re-running without --stub and -f")
438
453
  .action((name, opts) => {
439
454
  if (!isSlicePattern(opts.pattern)) {
440
455
  console.error(`em slice new: invalid --pattern "${opts.pattern}" — expected one of: ${SLICE_PATTERNS.join(", ")}`);
@@ -466,7 +481,10 @@ slice
466
481
  wired = result;
467
482
  }
468
483
  mkdirSync(dir, { recursive: true });
469
- writeFileSync(path, buildSliceDocContent(name, key, opts.pattern, opts.swimlane));
484
+ const content = opts.stub
485
+ ? buildStubDocContent(name, opts.pattern, opts.swimlane)
486
+ : buildSliceDocContent(name, key, opts.pattern, opts.swimlane);
487
+ writeFileSync(path, content);
470
488
  console.log(`wrote ${path}`);
471
489
  if (wired) {
472
490
  writeFileSync(opts.wire, wired.content);
@@ -477,6 +495,59 @@ slice
477
495
  console.log(` note "${path}"`);
478
496
  }
479
497
  });
498
+ slice
499
+ .command("stub-all")
500
+ .description("scaffold + wire a near-free stub (`em slice new --stub`'s content, MIL-184) for every " +
501
+ "slice in <file> with no resolvable doc — the fast path to status coloring for an " +
502
+ "exploratory/backbone model without hand-running `slice new` per slice. Skips a " +
503
+ "continuation slice (MIL-208, it has no doc of its own), an already-documented slice, and " +
504
+ "a slice whose pattern can't be classified")
505
+ .argument("<file>", "input .em file")
506
+ .option("--status <status>", `target status for every stub: ${STUB_STATUSES.join(" | ")}`, "draft")
507
+ .option("--by <name>", "identity for reviewedBy/ratifiedBy — required unless --status draft")
508
+ .option("--implemented-in <url>", "PR/commit URL for implementedIn — required with --status implemented")
509
+ .option("--dry-run", "list what would be stubbed/wired without writing anything")
510
+ .action((file, opts) => {
511
+ if (!isStubStatus(opts.status)) {
512
+ console.error(`em slice stub-all: invalid --status "${opts.status}" — expected one of: ${STUB_STATUSES.join(", ")}`);
513
+ process.exit(1);
514
+ }
515
+ if (opts.status !== "draft" && !opts.by) {
516
+ console.error(`em slice stub-all: --status ${opts.status} requires --by <name>`);
517
+ process.exit(1);
518
+ }
519
+ if (opts.status === "implemented" && !opts.implementedIn) {
520
+ console.error("em slice stub-all: --status implemented requires --implemented-in <url>");
521
+ process.exit(1);
522
+ }
523
+ const { model, refs, diagnostics, source } = compileFile(file);
524
+ printDiagnostics(diagnostics);
525
+ if (hasErrors(diagnostics)) {
526
+ console.error("em slice stub-all: not stubbing — fix the errors above");
527
+ process.exit(1);
528
+ }
529
+ mkdirSync(join(dirname(file), "slices"), { recursive: true });
530
+ const result = runStubAll(model, refs, dirname(file), source, {
531
+ status: opts.status,
532
+ by: opts.by ?? null,
533
+ on: localIsoDate(),
534
+ implementedInUrl: opts.implementedIn ?? null,
535
+ dryRun: opts.dryRun === true,
536
+ });
537
+ for (const outcome of result.outcomes) {
538
+ if (outcome.kind === "skip") {
539
+ console.log(`skip ${outcome.sliceKey} — ${outcome.reason}`);
540
+ continue;
541
+ }
542
+ const verb = opts.dryRun ? "would stub" : "stubbed";
543
+ const wireNote = outcome.wired ? `wired onto ${outcome.elementName}` : "already wired";
544
+ console.log(`${verb} ${outcome.path} (${outcome.sliceKey}, pattern: ${outcome.pattern}, swimlane: "${outcome.swimlane}", ` +
545
+ `status: ${outcome.status}, ${wireNote})`);
546
+ }
547
+ if (!opts.dryRun && result.source !== source) {
548
+ writeFileSync(file, result.source);
549
+ }
550
+ });
480
551
  slice
481
552
  .command("index")
482
553
  .description("rewrite the model's sibling README.md's GENERATED Slices table from `em export`'s slice " +
@@ -545,7 +616,7 @@ slice
545
616
  .argument("<file>", "input .em file")
546
617
  .argument("<slice-key>", "slice export key (kebab-case)")
547
618
  .requiredOption("--by <name>", "the reviewer's (or facilitator's) name")
548
- .option("--on <date>", "review date, YYYY-MM-DD (default: today)")
619
+ .option("--on <date>", "review date, YYYY-MM-DD (default: today, local date)")
549
620
  .action((file, sliceKey, opts) => {
550
621
  const { model, refs, diagnostics } = compileFile(file);
551
622
  printDiagnostics(diagnostics);
@@ -556,7 +627,7 @@ slice
556
627
  console.error(`em slice review: slice "${sliceKey}" has errors — fix them first`);
557
628
  process.exit(1);
558
629
  }
559
- const today = new Date().toISOString().slice(0, 10);
630
+ const today = localIsoDate();
560
631
  const reviewedOn = opts.on ?? today;
561
632
  if (opts.on !== undefined && !isValidDateString(opts.on)) {
562
633
  console.error(`em slice review: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
@@ -582,10 +653,10 @@ slice
582
653
  .argument("<file>", "input .em file")
583
654
  .argument("<slice-key>", "slice export key (kebab-case)")
584
655
  .requiredOption("--by <name>", "the ratifier's name")
585
- .option("--on <date>", "ratification date, YYYY-MM-DD (default: today)")
656
+ .option("--on <date>", "ratification date, YYYY-MM-DD (default: today, local date)")
586
657
  .option("--skip-review", "ratify without a recorded review — prints a loud notice on stderr")
587
658
  .action((file, sliceKey, opts) => {
588
- const { model, refs, diagnostics } = compileFile(file);
659
+ const { model, refs, diagnostics, source } = compileFile(file);
589
660
  printDiagnostics(diagnostics);
590
661
  // Scoped the same way `em slice mark-implemented`/`em export --slice`/`em validate
591
662
  // --slice-ready` are: only an error concerning THIS slice refuses.
@@ -594,7 +665,7 @@ slice
594
665
  console.error(`em slice ratify: slice "${sliceKey}" has errors — fix them first`);
595
666
  process.exit(1);
596
667
  }
597
- const today = new Date().toISOString().slice(0, 10);
668
+ const today = localIsoDate();
598
669
  const ratifiedOn = opts.on ?? today;
599
670
  if (opts.on !== undefined && !isValidDateString(opts.on)) {
600
671
  console.error(`em slice ratify: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
@@ -620,6 +691,7 @@ slice
620
691
  console.log(result.changed
621
692
  ? `ratified: ${result.path} (ratifiedBy: ${opts.by}, ratifiedOn: ${ratifiedOn})`
622
693
  : `already ratified (no-op): ${result.path}`);
694
+ warnModelVersionDrift(dirname(file), model, refs, source, `ratifying "${sliceKey}"`);
623
695
  });
624
696
  slice
625
697
  .command("reratify")
@@ -632,7 +704,7 @@ slice
632
704
  .argument("<file>", "input .em file")
633
705
  .argument("<slice-key>", "slice export key (kebab-case)")
634
706
  .action((file, sliceKey) => {
635
- const { model, refs, diagnostics } = compileFile(file);
707
+ const { model, refs, diagnostics, source } = compileFile(file);
636
708
  printDiagnostics(diagnostics);
637
709
  // Scoped the same way `em slice ratify`/`em slice mark-implemented`/`em export --slice`/
638
710
  // `em validate --slice-ready` are: only an error concerning THIS slice refuses.
@@ -646,7 +718,60 @@ slice
646
718
  console.error(`em slice reratify: ${result.message}`);
647
719
  process.exit(1);
648
720
  }
721
+ // MIL-214: advisory only, never refuses — see reratify.ts's reratifyAdvisory. Both warnings
722
+ // can fire together (a version that was never certified AND still carries unruled findings).
723
+ if (result.advisory.neverCertified) {
724
+ console.error(`warn: reratifying "${sliceKey}" whose v${result.newVersion - 1} was never certified`);
725
+ }
726
+ if (result.advisory.unruledFindingsCount > 0) {
727
+ console.error(`warn: reratifying "${sliceKey}" has ${result.advisory.unruledFindingsCount} unruled conformance finding(s)`);
728
+ }
649
729
  console.log(`reratified: ${result.path} (version: ${result.newVersion}, status: ready-to-implement)`);
730
+ warnModelVersionDrift(dirname(file), model, refs, source, `reratifying "${sliceKey}"`);
731
+ });
732
+ slice
733
+ .command("conform")
734
+ .description("record per-slice-per-version conformance certification: sets `conformedVersion:`/" +
735
+ "`conformedAt:`/`conformedOn:` on a slice doc (MIL-214) — the fact `driftSignal: in-sync` " +
736
+ "now depends on. Legal only for `status: implemented` with an `implementedIn:` link. " +
737
+ "Idempotent on the same (version, --at) pair; a different --at simply overwrites (a " +
738
+ "later re-certification is normal — there's no way for em to tell 'later' from 'earlier' " +
739
+ "for an arbitrary revision string, so there's no --force to reach for). Refuses when the " +
740
+ "newest conformance/*-findings.json matching --at still has an unruled finding in scope " +
741
+ "for this slice; --skip-findings-check overrides with a loud notice.")
742
+ .argument("<file>", "input .em file")
743
+ .argument("<slice-key>", "slice export key (kebab-case)")
744
+ .requiredOption("--at <rev>", "the target-repo revision this certification sweep diffed against")
745
+ .option("--on <date>", "certification date, YYYY-MM-DD (default: today, local date)")
746
+ .option("--skip-findings-check", "certify even with unruled findings in scope — prints a loud notice on stderr")
747
+ .action((file, sliceKey, opts) => {
748
+ const { model, refs, diagnostics } = compileFile(file);
749
+ printDiagnostics(diagnostics);
750
+ // Scoped the same way `em slice ratify`/`em slice reratify`/`em slice mark-implemented` are:
751
+ // only an error concerning THIS slice refuses.
752
+ const scopedErrors = diagnostics.filter((d) => d.severity === "error" && d.refs?.some((r) => r === sliceKey || r.startsWith(`${sliceKey}/`)));
753
+ if (scopedErrors.length > 0) {
754
+ console.error(`em slice conform: slice "${sliceKey}" has errors — fix them first`);
755
+ process.exit(1);
756
+ }
757
+ const today = localIsoDate();
758
+ const on = opts.on ?? today;
759
+ if (opts.on !== undefined && !isValidDateString(opts.on)) {
760
+ console.error(`em slice conform: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
761
+ process.exit(1);
762
+ }
763
+ const result = runSliceConform(model, refs, dirname(file), sliceKey, opts.at, on, opts.skipFindingsCheck === true);
764
+ if (!result.ok) {
765
+ console.error(`em slice conform: ${result.message}`);
766
+ process.exit(1);
767
+ }
768
+ if (result.skippedFindingsCheck !== null) {
769
+ console.error(`notice: --skip-findings-check — certifying "${sliceKey}" with ${result.skippedFindingsCheck.length} ` +
770
+ `unruled conformance finding(s) still in scope (id ${result.skippedFindingsCheck.join(", ")})`);
771
+ }
772
+ console.log(result.changed
773
+ ? `certified: ${result.path} (conformedVersion: ${result.version}, conformedAt: ${opts.at}, conformedOn: ${on})`
774
+ : `already certified (no-op): ${result.path}`);
650
775
  });
651
776
  program
652
777
  .command("changelog")
@@ -671,23 +796,45 @@ program
671
796
  }
672
797
  });
673
798
  const STATE_DIR_HELP = "model directory containing .event-modeling.md, or a direct path to that file (default: current directory)";
674
- /** Shared by every `em state` writer: load, apply the pure patch, write, report — the only
675
- * difference between set-phase/set-conformance/set-review is which `mutate` closure they pass. */
676
- function writeStateUpdate(dirOrFile, cmdLabel, mutate) {
799
+ /** Shared by every `em state` writer: load, apply the pure patch, stamp `Em version:` to the
800
+ * running em (MIL-219 ruling A — every state-file writer refreshes it, since each is already
801
+ * rewriting the file), write, report — the only difference between set-phase/set-conformance/
802
+ * set-review/set-model-version is which `mutate` closure they pass. `today` defaults to
803
+ * `localIsoDate()`; a caller that also needs to stamp a SECOND artifact (MIL-218: `em state
804
+ * set-conformance`'s model-version certify step) with the exact same date passes it in
805
+ * explicitly, computed once, rather than each call independently reading the clock and risking
806
+ * a midnight-rollover mismatch between the two. The `Em version:` stamp is best-effort — a
807
+ * failure there (should never happen: `Model file:` is one of the six bullets a successful
808
+ * `loadStateFile`+`mutate` already proves is present) falls back to `mutate`'s own output rather
809
+ * than losing the write the caller actually asked for. */
810
+ function writeStateUpdate(dirOrFile, cmdLabel, mutate, today = localIsoDate()) {
677
811
  const loaded = loadStateFile(dirOrFile);
678
812
  if (!loaded.ok) {
679
813
  console.error(loaded.message);
680
814
  process.exit(1);
681
815
  }
682
- const today = new Date().toISOString().slice(0, 10);
683
816
  const result = mutate(loaded.text, today);
684
817
  if (!result.ok) {
685
818
  console.error(`${cmdLabel}: ${result.message}`);
686
819
  process.exit(1);
687
820
  }
688
- writeFileSync(loaded.path, result.text);
821
+ const stamped = setEmVersion(result.text, PKG_VERSION, today);
822
+ writeFileSync(loaded.path, stamped.ok ? stamped.text : result.text);
689
823
  console.log(`wrote ${loaded.path}`);
690
824
  }
825
+ /** MIL-218 ruling C: the one-line advisory `em slice ratify`/`reratify` print when the write
826
+ * they just made pushed the slice-version vector (or, in principle, the `.em` content hash —
827
+ * never true for these two commands, which only ever edit a slice doc's frontmatter, but
828
+ * checked anyway so this helper stays the one true predicate) past the last bumped design
829
+ * version. Silent when no manifest exists at all (`drift.current === null`) — a repo that
830
+ * never bumped a design version has nothing to be stale against, same silence
831
+ * `model-version-stale` (rules.ts) holds. Never refuses; advisory only. */
832
+ function warnModelVersionDrift(baseDir, model, refs, source, cmdLabel) {
833
+ const drift = modelVersionDrift(baseDir, model, refs, source);
834
+ if (drift.current === null || (!drift.hashChanged && drift.slicesChanged.length === 0))
835
+ return;
836
+ console.error(`warn: ${cmdLabel} moved the model past v${drift.current} — run \`em model version bump\` to record it`);
837
+ }
691
838
  const state = program
692
839
  .command("state")
693
840
  .description("read/write the state file's mechanical fields deterministically (see docs/cli.md)");
@@ -723,12 +870,95 @@ state
723
870
  });
724
871
  state
725
872
  .command("set-conformance")
726
- .description("rewrite Last conformance: (and Last updated:) in the exact format reference/conform.md parses")
873
+ .description("rewrite Last conformance: (and Last updated:) in the exact format reference/conform.md " +
874
+ "parses. Refuses (MIL-214) while any `implemented` slice has an unruled conformance " +
875
+ "finding in the findings JSON beside --report (a `slice: null` finding blocks every " +
876
+ "slice); --partial records the marker with a ` (partial)` suffix instead, with a loud " +
877
+ "notice. Without a findings JSON beside --report: warns once and records the marker as " +
878
+ "given (migration path for a report predating `em conform-findings`)")
727
879
  .argument("<revision>", "target-repo revision just diffed against")
728
880
  .argument("[dir]", STATE_DIR_HELP, ".")
729
881
  .requiredOption("--report <path>", "path to the conformance report just written")
882
+ .option("--partial", "record the marker as PARTIAL even though some in-scope findings are still unruled")
730
883
  .action((revision, dir, opts) => {
731
- writeStateUpdate(dir, "em state set-conformance", (text, today) => setConformance(text, revision, opts.report, today));
884
+ const loaded = loadStateFile(dir);
885
+ if (!loaded.ok) {
886
+ console.error(loaded.message);
887
+ process.exit(1);
888
+ }
889
+ const parsed = parseState(loaded.text);
890
+ if (!parsed.ok) {
891
+ console.error(`em state set-conformance: ${parsed.message}`);
892
+ process.exit(1);
893
+ }
894
+ const modelDir = dirname(loaded.path);
895
+ const modelFile = join(modelDir, parsed.state.modelPath);
896
+ // The "in scope" set for the findings gate below — every `implemented` slice, per the
897
+ // ruling. Best-effort: a model that doesn't exist or doesn't compile cleanly can't tell us
898
+ // its implemented slices, so the gate is skipped (not refused) in that case rather than
899
+ // blocking a state-file operation on an unrelated model problem — `em validate` is where
900
+ // that problem gets its own diagnostic. Also the compiled model MIL-218's certify step
901
+ // needs below (kept, not just the derived `inScope` set, since certify needs the model/refs
902
+ // themselves — never re-compiled a second time).
903
+ let inScope = new Set();
904
+ let compiledForCertify = null;
905
+ if (existsSync(modelFile)) {
906
+ const compiled = compileFile(modelFile);
907
+ if (!hasErrors(compiled.diagnostics)) {
908
+ const { facts } = resolveSliceDocFacts(compiled.model, compiled.refs, modelDir);
909
+ inScope = new Set(facts.filter((f) => f.status === "implemented").map((f) => f.key));
910
+ compiledForCertify = { model: compiled.model, refs: compiled.refs, source: compiled.source };
911
+ }
912
+ }
913
+ let unruled = [];
914
+ const lookup = lookupFindingsBesideReport(modelDir, opts.report);
915
+ if (lookup.kind === "invalid") {
916
+ console.error(`em state set-conformance: ${lookup.message}`);
917
+ process.exit(1);
918
+ }
919
+ else if (lookup.kind === "absent") {
920
+ console.error(`warn: no findings JSON found beside ${opts.report} — recording the marker without verifying every ` +
921
+ "finding is ruled (migration path for a report written before `em conform-findings`)");
922
+ }
923
+ else {
924
+ unruled = unruledFindingsInScope(lookup.doc.findings, inScope);
925
+ }
926
+ const partialFlag = opts.partial === true;
927
+ if (unruled.length > 0 && !partialFlag) {
928
+ console.error(`em state set-conformance: ${unruled.length} unruled conformance finding(s) among implemented slices ` +
929
+ `(id ${unruled.map((f) => f.id).join(", ")}) — rule on them (\`em conform-supersede --locus --by\`) or pass --partial`);
930
+ process.exit(1);
931
+ }
932
+ const recordPartial = partialFlag && unruled.length > 0;
933
+ if (recordPartial) {
934
+ console.error(`notice: conformance marker recorded as PARTIAL — ${unruled.length} finding(s) unruled`);
935
+ }
936
+ const today = localIsoDate();
937
+ // MIL-218 ruling D: a FULL (non-`--partial`) marker also certifies the current design
938
+ // version's model-versions/*.json manifest. Checked (and, on success, WRITTEN) before the
939
+ // `Last conformance:` bullet below — a refusal here (no manifest yet, or the vector has
940
+ // drifted since it was bumped) must leave the state file untouched, same "refuse before any
941
+ // write" discipline every other em command holds; never leaves `Last conformance:` updated
942
+ // with no matching `Certified:` to show for it. Best-effort, same posture as the `inScope`
943
+ // computation above, for exactly one case: a model that doesn't compile cleanly can't be
944
+ // certified against, so certification is silently SKIPPED (not a `set-conformance` failure)
945
+ // rather than blocking the conformance marker on an unrelated model problem — `em validate`
946
+ // is where that problem gets its own diagnostic.
947
+ let certifiedVersion = null;
948
+ if (!recordPartial && compiledForCertify) {
949
+ const findingsPath = lookup.kind === "found" ? lookup.path : null;
950
+ const certifyResult = runCertifyModelVersion(modelDir, compiledForCertify.model, compiledForCertify.refs, compiledForCertify.source, revision, today, opts.report, findingsPath);
951
+ if (!certifyResult.ok) {
952
+ console.error(`em state set-conformance: ${certifyResult.message}`);
953
+ process.exit(1);
954
+ }
955
+ certifiedVersion = certifyResult.version;
956
+ console.log(`certified: ${certifyResult.path} (v${certifyResult.version} @ ${revision})`);
957
+ }
958
+ writeStateUpdate(dir, "em state set-conformance", (text, t) => setConformance(text, revision, opts.report, t, recordPartial), today);
959
+ if (certifiedVersion !== null) {
960
+ writeStateUpdate(dir, "em state set-conformance", (text, t) => setCertified(text, certifiedVersion, revision, t, t), today);
961
+ }
732
962
  });
733
963
  state
734
964
  .command("set-review")
@@ -765,8 +995,8 @@ state
765
995
  process.exit(1);
766
996
  }
767
997
  const phases = sortUsagePhases(rawPhases);
768
- const { model, diagnostics, refs } = compileFile(file);
769
- const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics);
998
+ const { model, diagnostics, refs, source } = compileFile(file);
999
+ const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics, source);
770
1000
  const categorySet = new Set(allDiagnostics.map((d) => RULES[d.code].usageCategory));
771
1001
  const categories = categorySet.size > 0 ? [...categorySet].sort() : ["none"];
772
1002
  const loaded = loadStateFile(dirname(file));
@@ -774,7 +1004,7 @@ state
774
1004
  console.error(loaded.message);
775
1005
  process.exit(1);
776
1006
  }
777
- const today = new Date().toISOString().slice(0, 10);
1007
+ const today = localIsoDate();
778
1008
  const result = appendUsageLogEntry(loaded.text, today, phases, categories);
779
1009
  if (!result.ok) {
780
1010
  console.error(`em state log-usage: ${result.message}`);
@@ -783,6 +1013,67 @@ state
783
1013
  writeFileSync(loaded.path, result.text);
784
1014
  console.log(`wrote ${loaded.path} — phases: ${phases.join(", ")} — validate: ${categories.join(", ")}`);
785
1015
  });
1016
+ const model = program.command("model").description("model-level version: design version + certified version (see docs/model-versions.md)");
1017
+ const modelVersionCmd = model.command("version").description("bump/inspect the model's design version (MIL-218)");
1018
+ modelVersionCmd
1019
+ .command("bump")
1020
+ .description("bump the model's design version: write model-versions/v<N+1>.json (sibling of slices/, " +
1021
+ "conformance/) and rewrite the state file's `Model version:` bullet (MIL-218). Refuses " +
1022
+ "when the state file is missing (run `em scaffold` first), when --by is empty, and when " +
1023
+ "nothing has changed since the current version (same model content hash AND same " +
1024
+ "slice-version vector) unless --force — a no-op bump is the one case --force is right for.")
1025
+ .argument("<file>", "input .em file")
1026
+ .requiredOption("--by <name>", "the bumper's name")
1027
+ .option("--on <date>", "bump date, YYYY-MM-DD (default: today, local date)")
1028
+ .option("--force", "bump even though nothing has changed since the current version")
1029
+ .action((file, opts) => {
1030
+ const baseDir = dirname(file);
1031
+ const loaded = loadStateFile(baseDir);
1032
+ if (!loaded.ok) {
1033
+ console.error(`em model version bump: no state file — run \`em scaffold\` first (${loaded.message})`);
1034
+ process.exit(1);
1035
+ }
1036
+ if (opts.on !== undefined && !isValidDateString(opts.on)) {
1037
+ console.error(`em model version bump: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
1038
+ process.exit(1);
1039
+ }
1040
+ const today = localIsoDate();
1041
+ const on = opts.on ?? today;
1042
+ const { model: compiledModel, refs, diagnostics, source } = compileFile(file);
1043
+ printDiagnostics(diagnostics);
1044
+ if (hasErrors(diagnostics)) {
1045
+ console.error(`em model version bump: "${file}" has errors — fix them first`);
1046
+ process.exit(1);
1047
+ }
1048
+ const result = runModelVersionBump(baseDir, basename(file), compiledModel, refs, source, opts.by, on, GENERATOR_VERSION, opts.force === true);
1049
+ if (!result.ok) {
1050
+ console.error(`em model version bump: ${result.message}`);
1051
+ process.exit(1);
1052
+ }
1053
+ writeStateUpdate(baseDir, "em model version bump", (text, t) => setModelVersion(text, result.version, t), today);
1054
+ console.log(`bumped: ${result.path} (v${result.version})`);
1055
+ });
1056
+ modelVersionCmd
1057
+ .command("show")
1058
+ .description("print the model's current design version and the most recently certified version, if any " +
1059
+ "(model-versions/*.json — MIL-218)")
1060
+ .argument("<file>", "input .em file")
1061
+ .option("--json", "print a JSON document instead of text")
1062
+ .action((file, opts) => {
1063
+ const baseDir = dirname(file);
1064
+ const design = latestModelVersionNumber(baseDir);
1065
+ const certifiedFound = findCertifiedVersion(baseDir);
1066
+ const certified = certifiedFound
1067
+ ? { version: certifiedFound.version, at: certifiedFound.certified.at, on: certifiedFound.certified.on }
1068
+ : null;
1069
+ if (opts.json) {
1070
+ console.log(JSON.stringify({ file, design, certified }, null, 2));
1071
+ }
1072
+ else {
1073
+ console.log(`design version: ${design ?? "none"}`);
1074
+ console.log(`certified: ${certified ? `v${certified.version} @ ${certified.at} (${certified.on})` : "never"}`);
1075
+ }
1076
+ });
786
1077
  program
787
1078
  .command("usage-report")
788
1079
  .description("aggregate every .event-modeling.md's Usage log under [root] into phase/diagnostic-category " +
@@ -868,28 +1159,75 @@ program
868
1159
  "at ratification time so a reader following the report's file:line citations knows they " +
869
1160
  "describe an ancestor of the current model. Additive-only splice, never a rewrite of the " +
870
1161
  "report; idempotent on the same --as-of/--findings/--on stamp; refuses if the report " +
871
- "doesn't exist")
1162
+ "doesn't exist. --locus/--by (MIL-214, both required together) additionally record the " +
1163
+ "ruling itself — locus/resolvedBy/resolvedOn — on the findings named by --findings in the " +
1164
+ "sibling conformance/<date>-findings.json, when one exists (warns once and stamps the " +
1165
+ "banner only when it doesn't — the migration path for a report predating em conform-findings)")
872
1166
  .argument("<file>", "input .em file (used only to resolve the report path's base directory)")
873
1167
  .argument("<report-path>", "path to the conformance report, relative to the model's directory")
874
1168
  .requiredOption("--as-of <rev>", "the revision this ruling was made against — same value passed to `em state set-conformance`")
875
1169
  .requiredOption("--findings <spec>", 'which finding number(s) this stamps as ruled, e.g. "1-3" or "1,2,4"')
876
- .option("--on <date>", "ruling date, YYYY-MM-DD (default: today)")
1170
+ .option("--on <date>", "ruling date, YYYY-MM-DD (default: today, local date)")
1171
+ .option("--locus <locus>", `who/what the named finding(s) say is wrong: ${FINDING_LOCI.join(" | ")} (requires --by)`)
1172
+ .option("--by <name>", "who ruled on the named finding(s) (requires --locus)")
877
1173
  .action((file, reportPath, opts) => {
878
- const today = new Date().toISOString().slice(0, 10);
1174
+ const today = localIsoDate();
879
1175
  const on = opts.on ?? today;
880
1176
  if (opts.on !== undefined && !isValidDateString(opts.on)) {
881
1177
  console.error(`em conform-supersede: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
882
1178
  process.exit(1);
883
1179
  }
884
- const result = runConformSupersede(dirname(file), reportPath, opts.asOf, opts.findings, on);
1180
+ if ((opts.locus !== undefined) !== (opts.by !== undefined)) {
1181
+ console.error("em conform-supersede: --locus and --by must be given together");
1182
+ process.exit(1);
1183
+ }
1184
+ if (opts.locus !== undefined && !FINDING_LOCI.includes(opts.locus)) {
1185
+ console.error(`em conform-supersede: invalid --locus "${opts.locus}" — expected one of: ${FINDING_LOCI.join(", ")}`);
1186
+ process.exit(1);
1187
+ }
1188
+ const ruling = opts.locus !== undefined && opts.by !== undefined ? { locus: opts.locus, by: opts.by } : undefined;
1189
+ const result = runConformSupersede(dirname(file), reportPath, opts.asOf, opts.findings, on, ruling);
885
1190
  if (!result.ok) {
886
1191
  console.error(`em conform-supersede: ${result.message}`);
887
1192
  process.exit(1);
888
1193
  }
1194
+ if (result.findingsRuling.kind === "no-findings-file") {
1195
+ console.error(`warn: ${result.findingsRuling.warning}`);
1196
+ }
1197
+ else if (result.findingsRuling.kind === "applied") {
1198
+ console.log(result.findingsRuling.changed
1199
+ ? `ruled: ${result.findingsRuling.path} (finding(s) ${result.findingsRuling.ids.join(", ")} -> locus: ${opts.locus}, by: ${opts.by})`
1200
+ : `already ruled (no-op): ${result.findingsRuling.path}`);
1201
+ }
889
1202
  console.log(result.changed
890
1203
  ? `stamped superseded: ${result.path} (as of ${opts.asOf}, findings ${opts.findings}, on ${on})`
891
1204
  : `already stamped (no-op): ${result.path}`);
892
1205
  });
1206
+ const conformFindings = program.command("conform-findings").description("read-only checks for the conformance findings record (MIL-214)");
1207
+ conformFindings
1208
+ .command("check")
1209
+ .description("shape-validate a conformance/<date>-findings.json file (findingsSchemaVersion, required " +
1210
+ "fields, enum values, ids sorted+unique, a ruled finding carries resolvedBy+resolvedOn) — " +
1211
+ "exit 1 on shape errors. The conform skill writes this file directly alongside its report; " +
1212
+ "this is how a headless run verifies what it just wrote.")
1213
+ .argument("<path>", "path to a conformance/<date>-findings.json file")
1214
+ .option("--json", "print a JSON document instead of the text report")
1215
+ .action((path, opts) => {
1216
+ const result = checkFindingsFile(path);
1217
+ if (opts.json) {
1218
+ console.log(buildCheckFindingsJson(result));
1219
+ }
1220
+ else if (result.ok) {
1221
+ console.log(`ok — ${result.path}: ${result.findingsCount} finding(s), shape valid`);
1222
+ }
1223
+ else {
1224
+ console.error(`em conform-findings check: ${result.path} — shape errors:`);
1225
+ for (const e of result.errors)
1226
+ console.error(` - ${e}`);
1227
+ }
1228
+ if (!result.ok)
1229
+ process.exit(1);
1230
+ });
893
1231
  program
894
1232
  .command("watch")
895
1233
  .description("re-render on every save")
@@ -978,8 +1316,8 @@ program
978
1316
  .option("--json", "print a JSON document instead of text — works on a model WITH errors, unlike `em export` " +
979
1317
  "(MIL-128, see docs/cli.md); exit codes are unchanged")
980
1318
  .action((file, opts) => {
981
- const { model, diagnostics, refs } = compileFile(file);
982
- const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics);
1319
+ const { model, diagnostics, refs, source } = compileFile(file);
1320
+ const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics, source);
983
1321
  if (opts.sliceReady) {
984
1322
  // MIL-87: a targeted, single-slice readiness gate, not part of the unconditional
985
1323
  // diagnostic set above (see sliceReadyValidate.ts's header for why). Folds in MIL-85's
@@ -1005,11 +1343,18 @@ program
1005
1343
  // MIL-128: the 4 named gates (see computeSliceReadyGates) plus the same `scoped`
1006
1344
  // diagnostics and `ready` verdict driving the exit code below — replaces both the
1007
1345
  // scraped warning prose and the two hand-parsed English sentences.
1008
- const gates = computeSliceReadyGates(model, refs, dirname(file), key);
1009
- process.stdout.write(buildSliceReadyJson(file, key, gates, scoped, ready) + "\n");
1346
+ const result = computeSliceReadyGates(model, refs, dirname(file), key);
1347
+ process.stdout.write(buildSliceReadyJson(file, key, result?.gates ?? null, scoped, ready, result?.continuationOf ?? null) + "\n");
1010
1348
  }
1011
1349
  else {
1012
1350
  console.log(ready ? `slice "${key}" is ready-to-implement` : `slice "${key}" is NOT ready-to-implement`);
1351
+ // MIL-208: `--slice-ready` on a continuation key transparently reports the
1352
+ // originating slice's own verdict (the doc join resolves straight through) — name
1353
+ // that slice so the text mode doesn't look like this key has a doc of its own.
1354
+ const sliceIdx = refs.sliceKeys.indexOf(key);
1355
+ const continuation = sliceIdx === -1 ? null : continuationOf(model, refs, sliceIdx);
1356
+ if (continuation)
1357
+ console.log(` (continuation of "${continuation.sliceKey}")`);
1013
1358
  }
1014
1359
  if (!ready)
1015
1360
  process.exit(1);
@@ -1159,12 +1504,16 @@ program
1159
1504
  });
1160
1505
  program
1161
1506
  .command("coverage")
1162
- .description("check that every INV-* invariant ID cited in a ready-to-implement/implemented slice doc " +
1163
- "is cited by a test under --tests <dir> (MIL-130) — mechanizes reference/implement.md's " +
1164
- "definition-of-done citation check; advisory by default, --strict for CI")
1507
+ .description("check that every INV-* invariant ID cited in an implemented slice doc is cited by a test " +
1508
+ "under --tests <dir> (MIL-130/MIL-207) — mechanizes reference/implement.md's " +
1509
+ "definition-of-done citation check; advisory by default, --strict for CI; " +
1510
+ "--include-ready also counts ready-to-implement docs (forward-looking report); a " +
1511
+ "continuation slice (an `again` view instance with no doc of its own, MIL-208) is excluded " +
1512
+ "from the report — its invariants, if any, live in the originating slice's own doc")
1165
1513
  .argument("<file>", "input .em file")
1166
1514
  .requiredOption("--tests <dir>", "directory to scan recursively for test files citing invariant IDs")
1167
1515
  .option("--strict", "exit non-zero if any invariant ID has zero citations (CI)")
1516
+ .option("--include-ready", "also count ready-to-implement docs, not just implemented (MIL-207)")
1168
1517
  .option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
1169
1518
  .action((file, opts) => {
1170
1519
  const { model, diagnostics, refs } = compileFile(file);
@@ -1173,17 +1522,27 @@ program
1173
1522
  console.error("em coverage: model has errors — fix them first");
1174
1523
  process.exit(1);
1175
1524
  }
1525
+ const baseDir = dirname(file);
1526
+ const includeReady = !!opts.includeReady;
1527
+ // MIL-207: a missing --tests dir is only a defect when something is actually in scope to
1528
+ // check — cheap to know without touching the test tree (resolveScopedSlices never reads
1529
+ // --tests). A fresh scaffold (or a doc-only ratification PR, with the MIL-207 default scope)
1530
+ // has zero implemented docs, so there's nothing yet for a test to cite.
1531
+ const anyInScope = resolveScopedSlices(model, refs, baseDir, includeReady).some((s) => s.inScope);
1176
1532
  if (!existsSync(opts.tests)) {
1177
- console.error(`em coverage: --tests directory not found: ${opts.tests}`);
1178
- process.exit(1);
1533
+ if (anyInScope) {
1534
+ console.error(`em coverage: --tests directory not found: ${opts.tests}`);
1535
+ process.exit(1);
1536
+ }
1537
+ console.error(`warn: em coverage: --tests directory not found: ${opts.tests} — nothing in scope yet`);
1179
1538
  }
1180
- if (!statSync(opts.tests).isDirectory()) {
1539
+ else if (!statSync(opts.tests).isDirectory()) {
1181
1540
  console.error(`em coverage: --tests is not a directory: ${opts.tests}`);
1182
1541
  process.exit(1);
1183
1542
  }
1184
- const report = buildCoverageReport(model, refs, dirname(file), opts.tests);
1543
+ const report = buildCoverageReport(model, refs, baseDir, opts.tests, includeReady);
1185
1544
  if (opts.json) {
1186
- process.stdout.write(buildCoverageJson(file, opts.tests, report) + "\n");
1545
+ process.stdout.write(buildCoverageJson(file, opts.tests, report, includeReady) + "\n");
1187
1546
  }
1188
1547
  else {
1189
1548
  for (const slice of report.slices) {
@@ -1232,29 +1591,36 @@ program
1232
1591
  console.error("em status: --json, --md, and --badge are mutually exclusive");
1233
1592
  process.exit(1);
1234
1593
  }
1235
- if (opts.tests) {
1236
- if (!existsSync(opts.tests)) {
1237
- console.error(`em status: --tests directory not found: ${opts.tests}`);
1238
- process.exit(1);
1239
- }
1240
- if (!statSync(opts.tests).isDirectory()) {
1241
- console.error(`em status: --tests is not a directory: ${opts.tests}`);
1242
- process.exit(1);
1243
- }
1594
+ // The "directory not found" half of the --tests check is deferred until after compiling
1595
+ // (below) — MIL-207: whether that's an error depends on whether anything is actually in
1596
+ // scope across the given models, which needs the compiled model+refs to answer.
1597
+ if (opts.tests && existsSync(opts.tests) && !statSync(opts.tests).isDirectory()) {
1598
+ console.error(`em status: --tests is not a directory: ${opts.tests}`);
1599
+ process.exit(1);
1244
1600
  }
1245
1601
  const compiled = [];
1246
1602
  let anyErrors = false;
1247
1603
  for (const file of files) {
1248
- const { model, refs, diagnostics } = compileFile(file);
1604
+ const { model, refs, diagnostics, source } = compileFile(file);
1249
1605
  printDiagnosticsFor(file, diagnostics);
1250
1606
  if (hasErrors(diagnostics))
1251
1607
  anyErrors = true;
1252
- compiled.push({ file, model, refs });
1608
+ compiled.push({ file, model, refs, source });
1253
1609
  }
1254
1610
  if (anyErrors) {
1255
1611
  console.error("em status: not reporting status — fix the errors above");
1256
1612
  process.exit(1);
1257
1613
  }
1614
+ if (opts.tests && !existsSync(opts.tests)) {
1615
+ // MIL-207: same leniency as `em coverage` — a missing --tests dir is only a defect when
1616
+ // at least one model has something in scope (`implemented`) to check.
1617
+ const anyInScope = compiled.some(({ model, refs, file }) => resolveScopedSlices(model, refs, dirname(file), false).some((s) => s.inScope));
1618
+ if (anyInScope) {
1619
+ console.error(`em status: --tests directory not found: ${opts.tests}`);
1620
+ process.exit(1);
1621
+ }
1622
+ console.error(`warn: em status: --tests directory not found: ${opts.tests} — nothing in scope yet`);
1623
+ }
1258
1624
  const sliceFacts = [];
1259
1625
  const factsByFile = new Map();
1260
1626
  const statusDiagnostics = [];
@@ -1296,7 +1662,13 @@ program
1296
1662
  const sliceDocFacts = facts.map((f) => ({ key: f.key, status: f.rawStatus, implementedIn: f.implementedIn }));
1297
1663
  return resolveConformanceEntry(file, opts.repo, sliceDocFacts);
1298
1664
  });
1299
- const report = buildStatusReport(files, sliceFacts, openIssuesCount, invariants, conformance, statusDiagnostics);
1665
+ // MIL-218: one model-version entry per input file.
1666
+ const modelVersion = compiled.map(({ file, model, refs, source }) => resolveModelVersionStatusEntry(file, model, refs, source));
1667
+ // MIL-219: one em-version entry per input file whose state file resolved.
1668
+ const emVersion = compiled
1669
+ .map(({ file }) => resolveEmVersionStatusEntry(file, PKG_VERSION))
1670
+ .filter((e) => e !== null);
1671
+ const report = buildStatusReport(files, sliceFacts, openIssuesCount, invariants, conformance, statusDiagnostics, modelVersion, emVersion);
1300
1672
  let output;
1301
1673
  if (opts.json)
1302
1674
  output = buildStatusJson(report);
@@ -1341,6 +1713,32 @@ program
1341
1713
  console.log(formatConformancePart(entry));
1342
1714
  }
1343
1715
  });
1716
+ program
1717
+ .command("metrics")
1718
+ .description("the pilot metrics named in advance by the register, computed from git history alone " +
1719
+ "(MIL-170): ratification turnaround (reviewed -> ratified -> implemented), conform-cycle " +
1720
+ "cadence + finding counts, and status-vs-reality disagreement over time — plus a fourth, " +
1721
+ "reported as not computable from history (see docs/usage-data.md). `<file>` is an anchor " +
1722
+ ".em file, used only to locate slices/, conformance/, and .event-modeling.md relative to " +
1723
+ "it — same convention as em ledger; never parsed or compiled")
1724
+ .argument("<file>", "anchor .em file")
1725
+ .requiredOption("--from <rev>", "baseline revision")
1726
+ .option("--to <rev>", "compare revision (default: HEAD)")
1727
+ .option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
1728
+ .action((file, opts) => {
1729
+ const to = opts.to ?? "HEAD";
1730
+ const computed = computeMetrics(file, opts.from, to);
1731
+ if (!computed.ok) {
1732
+ console.error(computed.message);
1733
+ process.exit(1);
1734
+ }
1735
+ if (opts.json) {
1736
+ process.stdout.write(buildMetricsJson(computed.result) + "\n");
1737
+ }
1738
+ else {
1739
+ console.log(formatMetricsText(computed.result));
1740
+ }
1741
+ });
1344
1742
  // `em query` (MIL-168): deterministic graph queries over the compiled model — scoped, token-
1345
1743
  // cheap answers instead of a whole-model `em export`. Compiles every input file through the
1346
1744
  // LIGHTER query sub-pipeline (query/pipeline.ts: parse -> normalize -> refs -> validate against
@@ -1567,8 +1965,10 @@ program
1567
1965
  program
1568
1966
  .command("mcp")
1569
1967
  .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). " +
1968
+ "facing alternative to shelling out to `em`; every tool mirrors a CLI `--json` surface " +
1969
+ "byte-for-byte. docs/mcp.md isn't vendored into the skill bundle — for the handshake and " +
1970
+ "the full, current tool table (the list changes as commands gain MCP parity, so it's not " +
1971
+ "repeated here — MIL-187) see https://github.com/milehimikey/em/blob/main/docs/mcp.md. " +
1572
1972
  "Equivalent to running the `em-mcp` bin directly")
1573
1973
  .action(async () => {
1574
1974
  // A 3-line wrapper only, for discoverability — the server itself (src/mcp/) never imports
@@ -1689,6 +2089,18 @@ skill
1689
2089
  console.log(f.message);
1690
2090
  console.log(result.ok ? `ok — vendored skill matches em ${PKG_VERSION}` : `${result.findings.length} mismatch(es)`);
1691
2091
  }
2092
+ // MIL-219 ruling A: `em skill check` also warns when [path]'s own state file records an
2093
+ // `Em version:` behind the installed em — best-effort (no state file, or an unparseable
2094
+ // one, is silently skipped; that's `em upgrade`'s own hard-incompatibility case to report,
2095
+ // not this command's). Always stderr, independent of --json/exit code — advisory only, and
2096
+ // this command's own exit code stays about skill-bundle drift, never about this.
2097
+ const loaded = loadStateFile(resolve(path));
2098
+ if (loaded.ok) {
2099
+ const parsed = parseState(loaded.text);
2100
+ if (parsed.ok && isOlder(parsed.state.emVersion, PKG_VERSION)) {
2101
+ console.error(`warn: recorded Em version (${parsed.state.emVersion}) is behind installed em (${PKG_VERSION}) — run \`em upgrade\``);
2102
+ }
2103
+ }
1692
2104
  // Set the code rather than process.exit(): same rationale as em ledger/em diff — stdout to
1693
2105
  // a pipe (a --json document) shouldn't risk truncation.
1694
2106
  if (!result.ok)
@@ -1764,6 +2176,105 @@ ci.command("init")
1764
2176
  console.log(`${path} already up to date`);
1765
2177
  }
1766
2178
  });
2179
+ program
2180
+ .command("upgrade")
2181
+ .description("bring a model repo authored under an older em (1.6 forward) up to the installed version: " +
2182
+ "an ordered list of mechanical steps (skill bundle, reaction shape, state-file bullets, " +
2183
+ "generated CI blocks, constitution scaffold) plus a human list of things no command can " +
2184
+ "safely decide by itself (MIL-219, see docs/upgrading.md). Dry-run by default; --apply " +
2185
+ "makes one git commit per applicable step, refusing on a dirty working tree")
2186
+ .argument("<file>", "input .em model file")
2187
+ .option("--apply", "apply every applicable mechanical step, one git commit each, then a final `Em version:` commit")
2188
+ .option("--check", "exit non-zero only on a hard incompatibility (unparseable state file, an un-migratable .em shape) — writes nothing; what CI runs")
2189
+ .option("--json", "print a JSON document instead of text (dry-run/--check only, never with --apply)")
2190
+ .action((file, opts) => {
2191
+ if (opts.apply && opts.check) {
2192
+ console.error("em upgrade: --apply and --check are mutually exclusive");
2193
+ process.exit(1);
2194
+ }
2195
+ const { model, refs, diagnostics, source } = compileFile(file);
2196
+ printDiagnostics(diagnostics);
2197
+ // Deliberately does NOT refuse on hasErrors(diagnostics), unlike every other command that
2198
+ // compiles a model first: the old pre-1.7.1 two-slice reaction shape `reaction-shape`
2199
+ // migrates is itself a validation ERROR under the current rules (MIL-120 changed what
2200
+ // `em validate` accepts) — refusing here would make that step, and the `predates-1.6` human
2201
+ // item, unreachable for exactly the repos that need them. Every step/human-detector either
2202
+ // reads the raw source directly (reaction-shape/predates-1.6, via planMigration) or a
2203
+ // best-effort compiled model that's still structurally usable even with validation errors
2204
+ // present (compile() never throws for a semantic error, only a parse error — already
2205
+ // handled by compileFile() above).
2206
+ if (hasErrors(diagnostics)) {
2207
+ console.error(`em upgrade: "${file}" has validation errors — proceeding anyway (this is often exactly what needs upgrading)`);
2208
+ }
2209
+ void source; // compileFile's own source read; steps re-read from disk themselves (module header)
2210
+ const baseDir = dirname(file);
2211
+ const repoRootResult = resolveRepoRoot(baseDir);
2212
+ if (!repoRootResult.ok) {
2213
+ console.error(repoRootResult.message);
2214
+ process.exit(1);
2215
+ }
2216
+ const ctx = {
2217
+ modelFile: file,
2218
+ baseDir,
2219
+ repoRoot: repoRootResult.repoRoot,
2220
+ installedVersion: PKG_VERSION,
2221
+ packagedSkillsRoot: packagedSkillsRoot(),
2222
+ model,
2223
+ refs,
2224
+ };
2225
+ if (opts.check) {
2226
+ const { ok, report } = checkUpgrade(ctx);
2227
+ console.error(`em upgrade --check: from ${report.from.version}${report.from.inferred ? ` (inferred: ${report.from.basis})` : ""} → to ${report.to}`);
2228
+ for (const s of report.steps) {
2229
+ console.error(` [${s.applicable ? "x" : " "}] ${s.id} (since ${s.sinceVersion}): ${s.reason}`);
2230
+ }
2231
+ for (const h of report.human)
2232
+ console.error(` human: ${h.id}: ${h.reason}`);
2233
+ console.error(ok ? "ok — no hard incompatibility" : "em upgrade --check: hard incompatibility found");
2234
+ if (!ok)
2235
+ process.exitCode = 1;
2236
+ return;
2237
+ }
2238
+ if (opts.apply) {
2239
+ const result = applyUpgrade(ctx);
2240
+ for (const s of result.applied) {
2241
+ if (s.applied)
2242
+ console.log(`applied: ${s.id} — ${s.changedFiles.join(", ")} (${s.commit})`);
2243
+ else
2244
+ console.log(`skipped: ${s.id}`);
2245
+ }
2246
+ if (!result.ok) {
2247
+ console.error(result.message);
2248
+ process.exit(1);
2249
+ }
2250
+ if (result.emVersionCommit)
2251
+ console.log(`recorded: ${result.emVersionCommit}`);
2252
+ else
2253
+ console.log("Em version: already up to date — no commit needed");
2254
+ for (const h of result.report.human)
2255
+ console.log(`human: ${h.id}: ${h.reason}`);
2256
+ return;
2257
+ }
2258
+ const report = detectUpgrade(ctx);
2259
+ if (opts.json) {
2260
+ console.log(buildUpgradeJson(file, report));
2261
+ return;
2262
+ }
2263
+ console.log(`em upgrade: from ${report.from.version}${report.from.inferred ? ` (inferred: ${report.from.basis})` : ""} → to ${report.to}`);
2264
+ if (report.stateFileError)
2265
+ console.log(`state file: ${report.stateFileError}`);
2266
+ console.log("Steps:");
2267
+ for (const s of report.steps) {
2268
+ console.log(` [${s.applicable ? "x" : " "}] ${s.id} (since ${s.sinceVersion}): ${s.reason}`);
2269
+ }
2270
+ console.log("Human:");
2271
+ if (report.human.length === 0)
2272
+ console.log(" (none)");
2273
+ for (const h of report.human)
2274
+ console.log(` ${h.id}: ${h.reason}`);
2275
+ const applicableCount = report.steps.filter((s) => s.applicable).length;
2276
+ console.log(`${applicableCount} step(s) applicable, ${report.human.length} human item(s) — re-run with --apply to apply mechanical steps`);
2277
+ });
1767
2278
  // Exported so dev tooling (e.g. scripts/generate-skill-docs.ts) can introspect the registered
1768
2279
  // commands/options without triggering a real CLI run. Guarded the same way Node's CJS
1769
2280
  // `require.main === module` used to: only actually parse argv when this file is the process's
@@ -1806,15 +2317,17 @@ function compileFile(file, opts = {},
1806
2317
  }
1807
2318
  }
1808
2319
  /** Lineage-ref resolution (MIL-84), frontmatter-coherence (MIL-85), note-binding mismatches
1809
- * (MIL-126), doc↔model consistency (MIL-124), and orphaned slice docs (MIL-183) are `em
1810
- * validate`'s fs-aware rules — every other diagnostic in `diagnostics` (from `compileFile`) is a
1811
- * pure function of the .em source. All five read `slices/*.md` alongside the model. Doc↔model
1812
- * consistency and orphaned-slice-doc are deliberately validate-only (unlike note-binding, which
1813
- * `em render` also folds in) — both are conform-phase concerns, not something every render needs
1814
- * to recheck. Shared by `em validate` and `em state log-usage` (MIL-161) — the latter needs the
1815
- * exact same diagnostic set `em validate --json` would report, since its whole job is logging
1816
- * which categories fired. */
1817
- function computeAllDiagnostics(file, model, refs, diagnostics) {
2320
+ * (MIL-126), doc↔model consistency (MIL-124), orphaned slice docs (MIL-183), and model-version
2321
+ * staleness (MIL-218) are `em validate`'s fs-aware rules — every other diagnostic in
2322
+ * `diagnostics` (from `compileFile`) is a pure function of the .em source. All six read
2323
+ * `slices/*.md`/`model-versions/*.json` alongside the model. Doc↔model consistency,
2324
+ * orphaned-slice-doc, and model-version-stale are deliberately validate-only (unlike
2325
+ * note-binding, which `em render` also folds in) — all three are conform-phase concerns, not
2326
+ * something every render needs to recheck. Shared by `em validate` and `em state log-usage`
2327
+ * (MIL-161) — the latter needs the exact same diagnostic set `em validate --json` would
2328
+ * report, since its whole job is logging which categories fired. `source` is the `.em` file's
2329
+ * own text — only `validateModelVersionStale` reads it (the modelHash comparison). */
2330
+ function computeAllDiagnostics(file, model, refs, diagnostics, source) {
1818
2331
  return [
1819
2332
  ...diagnostics,
1820
2333
  ...validateLineage(model, refs, dirname(file)),
@@ -1822,6 +2335,7 @@ function computeAllDiagnostics(file, model, refs, diagnostics) {
1822
2335
  ...validateNoteBindings(model, refs, dirname(file)),
1823
2336
  ...validateDocModelConsistency(model, refs, dirname(file)),
1824
2337
  ...validateOrphanedSliceDocs(model, refs, dirname(file)),
2338
+ ...validateModelVersionStale(model, refs, dirname(file), source),
1825
2339
  ];
1826
2340
  }
1827
2341
  /** Read a file's text, or exit with a clear error — shared by `em diff` (both forms) and
@@ -1921,16 +2435,22 @@ function printDivergences(model) {
1921
2435
  console.log(` divergence :${el.line} slice "${slice.name}" ${el.kind} "${el.name}": ${el.divergence}`);
1922
2436
  }
1923
2437
  }
1924
- /** Print only elements marked `public`: slice, kind, name, line — for an integration-surface audit. */
2438
+ /** Print only elements marked `public`: slice, kind, name, line — for an integration-surface
2439
+ * audit. A `public` view also gets a "(no in-model reader)" note (MIL-215) when no `ui`/
2440
+ * reaction anywhere on its timeline reads it — an audit hint, not a warning: a genuinely
2441
+ * cross-model `public` view's reader legitimately lives outside this file (see
2442
+ * `publicViewsWithoutInModelReader`'s own header for why this stays advisory). */
1925
2443
  function printPublicElements(model) {
1926
2444
  const pub = model.elements.filter((el) => el.public && (el.kind === "event" || el.kind === "view"));
1927
2445
  if (pub.length === 0) {
1928
2446
  console.log("no public elements");
1929
2447
  return;
1930
2448
  }
2449
+ const noReader = publicViewsWithoutInModelReader(model);
1931
2450
  for (const el of pub) {
1932
2451
  const slice = model.slices[el.sliceIndex];
1933
- console.log(` public :${el.line} slice "${slice.name}" ${el.kind} "${el.name}"`);
2452
+ const note = el.kind === "view" && noReader.has(el.logicalId) ? " (no in-model reader)" : "";
2453
+ console.log(` public :${el.line} slice "${slice.name}" ${el.kind} "${el.name}"${note}`);
1934
2454
  }
1935
2455
  }
1936
2456
  /** Diagnostics in `all` not already present (by reference identity) in `already` — avoids