@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.
- package/.claude/skills/event-modeling/SKILL.md +3 -2
- package/.claude/skills/event-modeling-conform/SKILL.md +1 -1
- package/.claude/skills/event-modeling-conform/reference/conform.md +1 -1
- package/.claude/skills/event-modeling-design/SKILL.md +17 -3
- package/.claude/skills/event-modeling-discover/SKILL.md +1 -1
- package/.claude/skills/event-modeling-implement/SKILL.md +7 -4
- package/.claude/skills/event-modeling-implement/reference/implement.md +14 -2
- package/.claude/skills/event-modeling-review/SKILL.md +1 -1
- package/.claude/skills/event-modeling-shared/reference/em-dsl.md +6 -5
- package/.claude/skills/event-modeling-shared/reference/slice-doc-schema.md +9 -0
- package/dist/cli/ciInit.d.ts +24 -0
- package/dist/cli/ciInit.js +70 -17
- package/dist/cli/ciInit.js.map +1 -1
- package/dist/cli/coverage.d.ts +16 -2
- package/dist/cli/coverage.js +26 -4
- package/dist/cli/coverage.js.map +1 -1
- package/dist/cli/reratify.d.ts +8 -2
- package/dist/cli/reratify.js +54 -22
- package/dist/cli/reratify.js.map +1 -1
- package/dist/cli/upgrade.d.ts +14 -4
- package/dist/cli/upgrade.js +69 -21
- package/dist/cli/upgrade.js.map +1 -1
- package/dist/cli.js +90 -19
- package/dist/cli.js.map +1 -1
- package/dist/emit/coverageJson.d.ts +5 -2
- package/dist/emit/coverageJson.js +6 -2
- package/dist/emit/coverageJson.js.map +1 -1
- package/dist/emit/upgradeJson.d.ts +1 -1
- package/dist/emit/upgradeJson.js +4 -1
- package/dist/emit/upgradeJson.js.map +1 -1
- package/dist/mcp/server.js +23 -6
- package/dist/mcp/server.js.map +1 -1
- 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.
|
|
650
|
-
"
|
|
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`).
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 === "
|
|
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 === "
|
|
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
|
-
|
|
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;
|