@milehimikey/em 1.13.0 → 1.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/.claude/skills/event-modeling/SKILL.md +3 -2
  2. package/.claude/skills/event-modeling-conform/SKILL.md +1 -1
  3. package/.claude/skills/event-modeling-conform/reference/conform.md +1 -1
  4. package/.claude/skills/event-modeling-design/SKILL.md +17 -3
  5. package/.claude/skills/event-modeling-discover/SKILL.md +1 -1
  6. package/.claude/skills/event-modeling-implement/SKILL.md +7 -4
  7. package/.claude/skills/event-modeling-implement/reference/implement.md +14 -2
  8. package/.claude/skills/event-modeling-review/SKILL.md +1 -1
  9. package/.claude/skills/event-modeling-shared/reference/em-dsl.md +6 -5
  10. package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +9 -0
  11. package/dist/cli/ciInit.d.ts +24 -0
  12. package/dist/cli/ciInit.js +70 -17
  13. package/dist/cli/ciInit.js.map +1 -1
  14. package/dist/cli/coverage.d.ts +16 -2
  15. package/dist/cli/coverage.js +26 -4
  16. package/dist/cli/coverage.js.map +1 -1
  17. package/dist/cli/reratify.d.ts +8 -2
  18. package/dist/cli/reratify.js +54 -22
  19. package/dist/cli/reratify.js.map +1 -1
  20. package/dist/cli/upgrade.d.ts +14 -4
  21. package/dist/cli/upgrade.js +69 -21
  22. package/dist/cli/upgrade.js.map +1 -1
  23. package/dist/cli.js +90 -19
  24. package/dist/cli.js.map +1 -1
  25. package/dist/emit/coverageJson.d.ts +5 -2
  26. package/dist/emit/coverageJson.js +6 -2
  27. package/dist/emit/coverageJson.js.map +1 -1
  28. package/dist/emit/upgradeJson.d.ts +1 -1
  29. package/dist/emit/upgradeJson.js +4 -1
  30. package/dist/emit/upgradeJson.js.map +1 -1
  31. package/dist/mcp/server.js +23 -6
  32. package/dist/mcp/server.js.map +1 -1
  33. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -34,7 +34,7 @@ import { detectSliceDocCollisions } from "./catalog/modelCollisionValidate.js";
34
34
  import { checkLedger, readLedgerWaiverTrailers, applyLedgerWaivers } from "./cli/ledgerCheck.js";
35
35
  import { planMigration, verifyMigration } from "./cli/migrateReactionShape.js";
36
36
  import { buildLedgerJson } from "./emit/ledgerJson.js";
37
- import { buildCoverageReport, resolveScopedSlices } from "./cli/coverage.js";
37
+ import { buildCoverageReport, resolveScopedSlices, resolveCoverageSliceKey } from "./cli/coverage.js";
38
38
  import { buildCoverageJson } from "./emit/coverageJson.js";
39
39
  import { resolveSliceStatusFacts, countOpenIssues, resolveConformanceEntry, aggregateInvariantTotals, buildStatusReport, formatStatusText, formatStatusMarkdown, buildStatusBadge, formatConformancePart, findSpecifyRoot, resolveModelVersionStatusEntry, resolveEmVersionStatusEntry, } from "./cli/status.js";
40
40
  import { buildStatusJson } from "./emit/statusJson.js";
@@ -646,8 +646,12 @@ slice
646
646
  .command("ratify")
647
647
  .description("flip a slice doc's frontmatter to `status: ready-to-implement` and record `ratifiedBy:`/" +
648
648
  "`ratifiedOn:` — the handoff sign-off (MIL-165, docs/process.md#the-slice-lifecycle-gates) " +
649
- "that makes who ratified, and when, a first-class recorded fact. Refuses a doc that never " +
650
- "passed the review gate (`em slice review`) unless --skip-review is passed. Idempotent on " +
649
+ "that makes who ratified, and when, a first-class recorded fact. The review gate is a check " +
650
+ "on the doc's current `status:`, not on whether `reviewedBy:`/`reviewedOn:` are populated: " +
651
+ "a doc at `reviewed` or `ready-to-implement` passes (the latter is where `em slice " +
652
+ "reratify` leaves a doc, so the follow-up ratify needs no --skip-review); any other status " +
653
+ "— a `draft` that never went through `em slice review` — is refused unless --skip-review " +
654
+ "is passed. Idempotent on " +
651
655
  "the same --by/--on pair; refuses to overwrite a different one already recorded; never " +
652
656
  "touches `version:` or the doc body")
653
657
  .argument("<file>", "input .em file")
@@ -697,7 +701,10 @@ slice
697
701
  .command("reratify")
698
702
  .description("bump `version:` and flip a shipped slice doc's frontmatter back to `status: " +
699
703
  "ready-to-implement` — the re-ratification mechanical edit (MIL-161, mirrors `em slice " +
700
- "mark-implemented`). Only applies to a doc at `status: implemented`; clears any stale " +
704
+ "mark-implemented`). Applies to a doc at `status: implemented`, or (MIL-258) to a " +
705
+ "`ready-to-implement` doc that is ratified but not yet shipped — there it bumps `version:` " +
706
+ "and clears the sign-off but leaves `status:` alone; a `ready-to-implement` doc with no " +
707
+ "`ratifiedBy:` refuses (awaiting `em slice ratify --by`). Clears any stale " +
701
708
  "`ratifiedBy:`/`ratifiedOn:`/`reviewedBy:`/`reviewedOn:` (they describe the PRIOR version's " +
702
709
  "review and sign-off) so a follow-up `em slice ratify --by` applies cleanly — and needs no " +
703
710
  "fresh review; never touches `implementedIn:` or the doc body")
@@ -720,13 +727,20 @@ slice
720
727
  }
721
728
  // MIL-214: advisory only, never refuses — see reratify.ts's reratifyAdvisory. Both warnings
722
729
  // can fire together (a version that was never certified AND still carries unruled findings).
723
- if (result.advisory.neverCertified) {
730
+ if (result.advisory?.neverCertified) {
724
731
  console.error(`warn: reratifying "${sliceKey}" whose v${result.newVersion - 1} was never certified`);
725
732
  }
726
- if (result.advisory.unruledFindingsCount > 0) {
733
+ if (result.advisory && result.advisory.unruledFindingsCount > 0) {
727
734
  console.error(`warn: reratifying "${sliceKey}" has ${result.advisory.unruledFindingsCount} unruled conformance finding(s)`);
728
735
  }
729
- console.log(`reratified: ${result.path} (version: ${result.newVersion}, status: ready-to-implement)`);
736
+ if (result.kind === "unshipped") {
737
+ // MIL-258: nothing was flipped — say what DID happen, and that the new version is unsigned.
738
+ console.log(`reratified: ${result.path} (version: ${result.newVersion - 1} -> ${result.newVersion}, sign-off cleared, ` +
739
+ `status unchanged: ready-to-implement) — not ratified until \`em slice ratify --by <name>\` records the new sign-off`);
740
+ }
741
+ else {
742
+ console.log(`reratified: ${result.path} (version: ${result.newVersion}, status: ready-to-implement)`);
743
+ }
730
744
  warnModelVersionDrift(dirname(file), model, refs, source, `reratifying "${sliceKey}"`);
731
745
  });
732
746
  slice
@@ -1509,9 +1523,13 @@ program
1509
1523
  "definition-of-done citation check; advisory by default, --strict for CI; " +
1510
1524
  "--include-ready also counts ready-to-implement docs (forward-looking report); a " +
1511
1525
  "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")
1526
+ "from the report — its invariants, if any, live in the originating slice's own doc; " +
1527
+ "--slice <key> scopes the report to one slice whatever its status (MIL-255), the " +
1528
+ "pre-merge definition-of-done check")
1513
1529
  .argument("<file>", "input .em file")
1514
1530
  .requiredOption("--tests <dir>", "directory to scan recursively for test files citing invariant IDs")
1531
+ .option("--slice <key>", "only this slice (export key), whether ready-to-implement or implemented — no --include-ready needed; " +
1532
+ "--strict then fails on this slice's uncited invariants, or if the slice is not in scope (draft/reviewed/unbound: nothing checked) (MIL-255)")
1515
1533
  .option("--strict", "exit non-zero if any invariant ID has zero citations (CI)")
1516
1534
  .option("--include-ready", "also count ready-to-implement docs, not just implemented (MIL-207)")
1517
1535
  .option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
@@ -1524,11 +1542,25 @@ program
1524
1542
  }
1525
1543
  const baseDir = dirname(file);
1526
1544
  const includeReady = !!opts.includeReady;
1545
+ // MIL-255: resolve --slice before anything else — an unknown key is an error (never a
1546
+ // vacuously passing empty report), and a continuation key (MIL-208) scopes to its
1547
+ // originating slice, which the output names (matching `em validate --slice-ready`).
1548
+ let onlyKey;
1549
+ let continuationOfKey = null;
1550
+ if (opts.slice !== undefined) {
1551
+ const target = resolveCoverageSliceKey(model, refs, opts.slice);
1552
+ if (!target) {
1553
+ console.error(`em coverage --slice: no slice with export key "${opts.slice}" in this model`);
1554
+ process.exit(1);
1555
+ }
1556
+ onlyKey = target.key;
1557
+ continuationOfKey = target.continuationOf;
1558
+ }
1527
1559
  // MIL-207: a missing --tests dir is only a defect when something is actually in scope to
1528
1560
  // check — cheap to know without touching the test tree (resolveScopedSlices never reads
1529
1561
  // --tests). A fresh scaffold (or a doc-only ratification PR, with the MIL-207 default scope)
1530
1562
  // 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);
1563
+ const anyInScope = resolveScopedSlices(model, refs, baseDir, includeReady, onlyKey).some((s) => s.inScope);
1532
1564
  if (!existsSync(opts.tests)) {
1533
1565
  if (anyInScope) {
1534
1566
  console.error(`em coverage: --tests directory not found: ${opts.tests}`);
@@ -1540,17 +1572,36 @@ program
1540
1572
  console.error(`em coverage: --tests is not a directory: ${opts.tests}`);
1541
1573
  process.exit(1);
1542
1574
  }
1543
- const report = buildCoverageReport(model, refs, baseDir, opts.tests, includeReady);
1575
+ const report = buildCoverageReport(model, refs, baseDir, opts.tests, includeReady, onlyKey);
1576
+ // MIL-255: a scoped slice whose doc is draft/reviewed/unbound/unusable is out of scope — the
1577
+ // same way the unscoped report treats it — but under --strict "nothing was checked" must not
1578
+ // exit 0, or the pre-merge check passes vacuously (the very bug --slice exists to fix). So:
1579
+ // advisory (no --strict) prints the note on stdout and exits 0; --strict prints it on stderr
1580
+ // and exits 1 (also in --json mode, whose document shape is unchanged — consumers read the
1581
+ // slice's `inScope` alongside `ok`). An in-scope doc with zero invariants stays a pass.
1582
+ const unchecked = onlyKey === undefined ? undefined : report.slices.find((sl) => !sl.inScope);
1583
+ const uncheckedNote = unchecked
1584
+ ? `slice "${unchecked.key}" is not in scope (${unchecked.status ?? unchecked.docReason ?? "no doc"}) — nothing checked; only ready-to-implement/implemented docs are checked`
1585
+ : null;
1586
+ if (uncheckedNote !== null && (opts.strict || !opts.json)) {
1587
+ (opts.strict ? console.error : console.log)(uncheckedNote);
1588
+ }
1544
1589
  if (opts.json) {
1545
- process.stdout.write(buildCoverageJson(file, opts.tests, report, includeReady) + "\n");
1590
+ const sliceScope = opts.slice === undefined ? null : { requested: opts.slice, continuationOf: continuationOfKey };
1591
+ process.stdout.write(buildCoverageJson(file, opts.tests, report, includeReady, sliceScope) + "\n");
1546
1592
  }
1547
1593
  else {
1594
+ if (continuationOfKey !== null) {
1595
+ console.log(`slice "${opts.slice}" is a continuation of "${continuationOfKey}" — checking "${continuationOfKey}"`);
1596
+ }
1548
1597
  for (const slice of report.slices) {
1549
1598
  if (!slice.inScope)
1550
1599
  continue;
1551
1600
  console.log(`slice "${slice.key}" (${slice.status}):`);
1552
1601
  if (slice.invariants.length === 0) {
1553
- console.log(` (no INV-* invariant IDs found in the doc body)`);
1602
+ // MIL-255: said as "checked" so an in-scope doc defining zero invariants (a legitimate
1603
+ // pass) is distinguishable from the "not in scope — nothing checked" case above.
1604
+ console.log(` (checked — the doc defines 0 INV-* invariant IDs)`);
1554
1605
  continue;
1555
1606
  }
1556
1607
  for (const inv of slice.invariants) {
@@ -1569,7 +1620,7 @@ program
1569
1620
  // Advisory by default (exit 0 even with uncovered IDs) — --strict is the opt-in CI gate,
1570
1621
  // same "set exitCode, don't truncate stdout" rationale as em ledger/em diff for the --json
1571
1622
  // form, and consistent for the text form too.
1572
- if (opts.strict && report.uncoveredCount > 0)
1623
+ if (opts.strict && (report.uncoveredCount > 0 || uncheckedNote !== null))
1573
1624
  process.exitCode = 1;
1574
1625
  });
1575
1626
  program
@@ -2115,7 +2166,7 @@ ci.command("init")
2115
2166
  "idempotent, --check for CI self-verification (MIL-166, see docs/ci.md)")
2116
2167
  .argument("<model>", "anchor .em file the coverage/ledger/slice-index/status steps point at")
2117
2168
  .option("--tests <dir>", "test directory the coverage/status steps scan for INV-* citations", "test")
2118
- .option("-f, --force", "replace an existing workflow file that has no GENERATED markers")
2169
+ .option("-f, --force", "replace an existing workflow file that has no GENERATED markers, or a managed block generated for a different model")
2119
2170
  .option("--check", "verify both files match the current preset; exit non-zero on drift without writing (CI)")
2120
2171
  .action((model, opts) => {
2121
2172
  const unsafe = findUnsafeCiInitArg(model) ?? findUnsafeCiInitArg(opts.tests);
@@ -2137,10 +2188,17 @@ ci.command("init")
2137
2188
  planCiFile(conformPath, buildConformWorkflowFile(model, PKG_VERSION), conformManagedBody(model, PKG_VERSION), CONFORM_WORKFLOW_MARKER, !!opts.force),
2138
2189
  ],
2139
2190
  ];
2191
+ // MIL-256 (#174): the managed block is single-model (multi-model: MIL-233). Another model's
2192
+ // block is its own condition - never `stale`, never silently replaced.
2193
+ const otherModel = (previous, path) => `${path} was generated for ${previous}, not ${model} - the managed block is single-model until multi-model support lands (MIL-233)`;
2140
2194
  if (opts.check) {
2141
2195
  let drift = false;
2142
2196
  for (const [path, status] of files) {
2143
- if (status.kind === "create") {
2197
+ if (status.kind === "other-model" || status.kind === "replace-model") {
2198
+ console.log(`different model: ${otherModel(status.previous, path)} (re-run \`em ci init ${model} --force\` to replace it)`);
2199
+ drift = true;
2200
+ }
2201
+ else if (status.kind === "create") {
2144
2202
  console.log(`missing: ${path} — run \`em ci init ${model}\` to create it`);
2145
2203
  drift = true;
2146
2204
  }
@@ -2161,10 +2219,22 @@ ci.command("init")
2161
2219
  process.exitCode = 1;
2162
2220
  return;
2163
2221
  }
2222
+ // Refuse before writing anything, so a refusal on one file never leaves the other updated.
2223
+ const refused = files.filter(([, s]) => s.kind === "other-model");
2224
+ if (refused.length > 0) {
2225
+ for (const [path, status] of refused) {
2226
+ if (status.kind === "other-model")
2227
+ console.error(`em ci init: ${otherModel(status.previous, path)}; re-run with --force to replace it`);
2228
+ }
2229
+ process.exitCode = 1;
2230
+ return;
2231
+ }
2164
2232
  mkdirSync(dirname(ciPath), { recursive: true });
2165
2233
  for (const [path, status] of files) {
2166
2234
  applyCiFile(path, status);
2167
- if (status.kind === "create")
2235
+ if (status.kind === "replace-model")
2236
+ console.log(`replaced ${path} (--force; was generated for ${status.previous})`);
2237
+ else if (status.kind === "create")
2168
2238
  console.log(`installed ${path}`);
2169
2239
  else if (status.kind === "stale")
2170
2240
  console.log(`updated ${path}`);
@@ -2185,7 +2255,7 @@ program
2185
2255
  "makes one git commit per applicable step, refusing on a dirty working tree")
2186
2256
  .argument("<file>", "input .em model file")
2187
2257
  .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")
2258
+ .option("--check", "exit non-zero only on a hard incompatibility (an existing but unparseable state file, an un-migratable .em shape) — writes nothing; what CI runs; the final line says why")
2189
2259
  .option("--json", "print a JSON document instead of text (dry-run/--check only, never with --apply)")
2190
2260
  .action((file, opts) => {
2191
2261
  if (opts.apply && opts.check) {
@@ -2223,14 +2293,15 @@ program
2223
2293
  refs,
2224
2294
  };
2225
2295
  if (opts.check) {
2226
- const { ok, report } = checkUpgrade(ctx);
2296
+ const { ok, report, reasons } = checkUpgrade(ctx);
2227
2297
  console.error(`em upgrade --check: from ${report.from.version}${report.from.inferred ? ` (inferred: ${report.from.basis})` : ""} → to ${report.to}`);
2228
2298
  for (const s of report.steps) {
2229
2299
  console.error(` [${s.applicable ? "x" : " "}] ${s.id} (since ${s.sinceVersion}): ${s.reason}`);
2230
2300
  }
2231
2301
  for (const h of report.human)
2232
2302
  console.error(` human: ${h.id}: ${h.reason}`);
2233
- console.error(ok ? "ok — no hard incompatibility" : "em upgrade --check: hard incompatibility found");
2303
+ // MIL-257: the final line always says why (previously visible only via --json).
2304
+ console.error(ok ? "ok — no hard incompatibility" : `em upgrade --check: hard incompatibility found: ${reasons.join("; ")}`);
2234
2305
  if (!ok)
2235
2306
  process.exitCode = 1;
2236
2307
  return;