@milehimikey/em 1.7.1 → 1.8.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.
- package/.claude/skills/event-modeling/SKILL.md +92 -65
- package/.claude/skills/event-modeling/reference/conform.md +45 -20
- package/.claude/skills/event-modeling/reference/em-dsl.md +120 -55
- package/.claude/skills/event-modeling/reference/extract.md +5 -4
- package/.claude/skills/event-modeling/reference/implement.md +34 -22
- package/.claude/skills/event-modeling/templates/model-readme.md +8 -5
- package/.claude/skills/event-modeling/templates/slice.md +19 -5
- package/README.md +6 -4
- package/dist/catalog/docJoin.d.ts +28 -3
- package/dist/catalog/docJoin.js +82 -13
- package/dist/catalog/docJoin.js.map +1 -1
- package/dist/catalog/docModelConsistencyValidate.d.ts +8 -0
- package/dist/catalog/docModelConsistencyValidate.js +335 -0
- package/dist/catalog/docModelConsistencyValidate.js.map +1 -0
- package/dist/catalog/noteBindingValidate.d.ts +8 -0
- package/dist/catalog/noteBindingValidate.js +133 -0
- package/dist/catalog/noteBindingValidate.js.map +1 -0
- package/dist/catalog/pages.js +3 -3
- package/dist/catalog/pages.js.map +1 -1
- package/dist/catalog/sliceDoc.d.ts +14 -0
- package/dist/catalog/sliceDoc.js +22 -0
- package/dist/catalog/sliceDoc.js.map +1 -1
- package/dist/catalog/sliceReadyValidate.d.ts +18 -0
- package/dist/catalog/sliceReadyValidate.js +25 -1
- package/dist/catalog/sliceReadyValidate.js.map +1 -1
- package/dist/cli/agentsMd.d.ts +15 -0
- package/dist/cli/agentsMd.js +72 -0
- package/dist/cli/agentsMd.js.map +1 -0
- package/dist/cli/conformScope.d.ts +86 -0
- package/dist/cli/conformScope.js +156 -0
- package/dist/cli/conformScope.js.map +1 -0
- package/dist/cli/contract.d.ts +7 -0
- package/dist/cli/contract.js +27 -0
- package/dist/cli/contract.js.map +1 -0
- package/dist/cli/coverage.d.ts +69 -0
- package/dist/cli/coverage.js +157 -0
- package/dist/cli/coverage.js.map +1 -0
- package/dist/cli/markImplemented.d.ts +34 -0
- package/dist/cli/markImplemented.js +180 -0
- package/dist/cli/markImplemented.js.map +1 -0
- package/dist/cli/migrateReactionShape.d.ts +62 -0
- package/dist/cli/migrateReactionShape.js +283 -0
- package/dist/cli/migrateReactionShape.js.map +1 -0
- package/dist/cli/sliceIndex.d.ts +49 -0
- package/dist/cli/sliceIndex.js +115 -0
- package/dist/cli/sliceIndex.js.map +1 -0
- package/dist/cli/sliceNew.d.ts +18 -0
- package/dist/cli/sliceNew.js +53 -0
- package/dist/cli/sliceNew.js.map +1 -0
- package/dist/cli/stateFile.d.ts +74 -0
- package/dist/cli/stateFile.js +191 -0
- package/dist/cli/stateFile.js.map +1 -0
- package/dist/cli.js +535 -43
- package/dist/cli.js.map +1 -1
- package/dist/emit/coverageJson.d.ts +8 -0
- package/dist/emit/coverageJson.js +30 -0
- package/dist/emit/coverageJson.js.map +1 -0
- package/dist/emit/dot.js +41 -10
- package/dist/emit/dot.js.map +1 -1
- package/dist/emit/json.d.ts +72 -4
- package/dist/emit/json.js +73 -4
- package/dist/emit/json.js.map +1 -1
- package/dist/emit/validateJson.d.ts +52 -0
- package/dist/emit/validateJson.js +120 -0
- package/dist/emit/validateJson.js.map +1 -0
- package/dist/mcp/main.d.ts +2 -0
- package/dist/mcp/main.js +20 -0
- package/dist/mcp/main.js.map +1 -0
- package/dist/mcp/server.d.ts +11 -0
- package/dist/mcp/server.js +260 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/model/model.d.ts +30 -1
- package/dist/model/model.js +26 -0
- package/dist/model/model.js.map +1 -1
- package/dist/model/rules.d.ts +114 -0
- package/dist/model/rules.js +116 -4
- package/dist/model/rules.js.map +1 -1
- package/dist/model/validate.js +83 -7
- package/dist/model/validate.js.map +1 -1
- package/dist/parser/ast.d.ts +41 -0
- package/dist/parser/lexer.d.ts +16 -0
- package/dist/parser/lexer.js +44 -2
- package/dist/parser/lexer.js.map +1 -1
- package/dist/parser/parser.js +340 -20
- package/dist/parser/parser.js.map +1 -1
- package/dist/render/drawNotes.js +8 -1
- package/dist/render/drawNotes.js.map +1 -1
- package/dist/render/render.js +85 -17
- package/dist/render/render.js.map +1 -1
- package/dist/render/serve.d.ts +4 -0
- package/dist/render/serve.js +23 -227
- package/dist/render/serve.js.map +1 -1
- package/dist/render/sliceStatus.d.ts +1 -1
- package/dist/render/sliceStatus.js +35 -6
- package/dist/render/sliceStatus.js.map +1 -1
- package/dist/render/viewerHtml.d.ts +1 -0
- package/dist/render/viewerHtml.js +5 -0
- package/dist/render/viewerHtml.js.map +1 -0
- package/dist/render/watch.d.ts +10 -0
- package/dist/render/watch.js +29 -0
- package/dist/render/watch.js.map +1 -1
- package/dist/templates.d.ts +19 -0
- package/dist/templates.js +169 -0
- package/dist/templates.js.map +1 -1
- package/dist/util/markers.d.ts +13 -0
- package/dist/util/markers.js +30 -0
- package/dist/util/markers.js.map +1 -0
- package/dist/util/walkDir.d.ts +10 -1
- package/dist/util/walkDir.js +11 -7
- package/dist/util/walkDir.js.map +1 -1
- package/package.json +12 -3
- package/.claude/skills/event-modeling/templates/live.html +0 -105
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// SPDX-License-Identifier: MIT
|
|
3
|
-
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from "node:fs";
|
|
4
4
|
import { cp, mkdir } from "node:fs/promises";
|
|
5
5
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -11,30 +11,46 @@ import { ParseError } from "./parser/parser.js";
|
|
|
11
11
|
import { renderDot, layoutDot, composeSvg, writeRendered, formatFromPath } from "./render/render.js";
|
|
12
12
|
import { buildSliceDiagram } from "./render/sliceDiagram.js";
|
|
13
13
|
import { resolveSliceArg, defaultSliceOut } from "./cli/render-inputs.js";
|
|
14
|
-
import { watchFile } from "./render/watch.js";
|
|
14
|
+
import { serializeBuilds, watchFile } from "./render/watch.js";
|
|
15
15
|
import { startLiveServer } from "./render/serve.js";
|
|
16
16
|
import { formatDiagnostic, hasErrors } from "./model/validate.js";
|
|
17
|
-
import { buildExport } from "./emit/json.js";
|
|
17
|
+
import { buildExport, buildSliceExport } from "./emit/json.js";
|
|
18
|
+
import { buildValidateJson, buildSliceReadyJson, buildValidateListJson, collectMarkers } from "./emit/validateJson.js";
|
|
18
19
|
import { buildDiffJson } from "./emit/diffJson.js";
|
|
19
20
|
import { diffModels, formatModelDiff, hasChanges } from "./model/diff.js";
|
|
20
21
|
import { planDiffArgs, resolveRevision, resolveDocAtRevision } from "./cli/diff-inputs.js";
|
|
21
22
|
import { readSliceDoc } from "./catalog/readSliceDoc.js";
|
|
22
23
|
import { validateLineage } from "./catalog/lineageValidate.js";
|
|
23
24
|
import { validateFrontmatterCoherence } from "./catalog/frontmatterCoherenceValidate.js";
|
|
24
|
-
import {
|
|
25
|
+
import { validateNoteBindings } from "./catalog/noteBindingValidate.js";
|
|
26
|
+
import { validateDocModelConsistency } from "./catalog/docModelConsistencyValidate.js";
|
|
27
|
+
import { validateSliceReady, computeSliceReadyGates } from "./catalog/sliceReadyValidate.js";
|
|
25
28
|
import { checkLedger } from "./cli/ledgerCheck.js";
|
|
29
|
+
import { planMigration, verifyMigration } from "./cli/migrateReactionShape.js";
|
|
26
30
|
import { buildLedgerJson } from "./emit/ledgerJson.js";
|
|
31
|
+
import { buildCoverageReport } from "./cli/coverage.js";
|
|
32
|
+
import { buildCoverageJson } from "./emit/coverageJson.js";
|
|
27
33
|
import { planSkillSync, applySkillSync } from "./cli/skillSync.js";
|
|
28
34
|
import { checkSkillSync } from "./cli/skillCheck.js";
|
|
29
35
|
import { buildSkillCheckJson } from "./emit/skillCheckJson.js";
|
|
36
|
+
import { readContract } from "./cli/contract.js";
|
|
37
|
+
import { createServer as createMcpServer } from "./mcp/server.js";
|
|
38
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
39
|
+
import { syncAgentsMd } from "./cli/agentsMd.js";
|
|
30
40
|
import { buildGlossary, detectKindConflicts, detectFieldTypeConflicts, hasConflicts, formatGlossarySummary, formatConflictLine, } from "./model/glossary.js";
|
|
31
41
|
import { buildGlossaryJson } from "./emit/glossaryJson.js";
|
|
32
42
|
import { planGlossaryArgs } from "./cli/glossary-inputs.js";
|
|
33
43
|
import { buildCatalog } from "./catalog/build.js";
|
|
34
44
|
import { planCatalogArgs } from "./cli/catalog-inputs.js";
|
|
45
|
+
import { runSliceIndex } from "./cli/sliceIndex.js";
|
|
46
|
+
import { runMarkImplemented } from "./cli/markImplemented.js";
|
|
47
|
+
import { buildConformScope, changedPathsSince, resolveSliceDocFacts, seedAsisModel } from "./cli/conformScope.js";
|
|
48
|
+
import { buildSliceDocContent, isSlicePattern, sliceDocKey, SLICE_PATTERNS } from "./cli/sliceNew.js";
|
|
35
49
|
import { listModelCommits, readFileAtCommit } from "./cli/changelog-git.js";
|
|
36
50
|
import { buildChangelog, parseDecisionsLog } from "./emit/changelog.js";
|
|
37
|
-
import {
|
|
51
|
+
import { STATE_FILE_NAME, PHASES, isPhase, loadStateFile, parseState, setPhase, setConformance, setReview, isValidDateString, } from "./cli/stateFile.js";
|
|
52
|
+
import { STARTER_EM, starterEmFor, scaffoldReadme, scaffoldStateFile } from "./templates.js";
|
|
53
|
+
import { kebabSlug } from "./util/slug.js";
|
|
38
54
|
const program = new Command();
|
|
39
55
|
// Single source of truth for the version (also read by src/emit/json.ts for
|
|
40
56
|
// `generator.version` in `em export`): package.json, one level up from both
|
|
@@ -57,6 +73,29 @@ program
|
|
|
57
73
|
writeFileSync(file, STARTER_EM);
|
|
58
74
|
console.log(`wrote ${file}`);
|
|
59
75
|
});
|
|
76
|
+
program
|
|
77
|
+
.command("scaffold")
|
|
78
|
+
.description("scaffold a full project: <slug>/<slug>.em, README.md, .event-modeling.md " +
|
|
79
|
+
"(see docs/cli.md — for just a starter .em, use `em init`)")
|
|
80
|
+
.argument("<name>", "model display name — kebab-cased for the directory and file names, used as-is for titles/prose")
|
|
81
|
+
.option("-f, --force", "overwrite the directory's contents if it already exists")
|
|
82
|
+
.action(async (name, opts) => {
|
|
83
|
+
if (name.includes('"') || name.includes("{{")) {
|
|
84
|
+
console.error(`em scaffold: name must not contain '"' or '{{' (breaks the generated .em/README/state files): ${name}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
const dirName = kebabSlug(name);
|
|
88
|
+
if (existsSync(dirName) && !opts.force) {
|
|
89
|
+
console.error(`refusing to overwrite ${dirName}/ (use --force)`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
await mkdir(dirName, { recursive: true });
|
|
93
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
94
|
+
writeFileSync(join(dirName, `${dirName}.em`), starterEmFor(name));
|
|
95
|
+
writeFileSync(join(dirName, "README.md"), scaffoldReadme(name, dirName));
|
|
96
|
+
writeFileSync(join(dirName, STATE_FILE_NAME), scaffoldStateFile(name, dirName, today));
|
|
97
|
+
console.log(`scaffolded ${dirName}/`);
|
|
98
|
+
});
|
|
60
99
|
program
|
|
61
100
|
.command("render")
|
|
62
101
|
.description("transpile a model and render it (or emit DOT)")
|
|
@@ -72,10 +111,15 @@ program
|
|
|
72
111
|
console.error("em render: --slice cannot be combined with --emit-dot");
|
|
73
112
|
process.exit(1);
|
|
74
113
|
}
|
|
75
|
-
const { dot, model, grid, diagnostics } = compileFile(file, {
|
|
114
|
+
const { dot, model, grid, diagnostics, refs } = compileFile(file, {
|
|
76
115
|
keepEmptyLanes: opts.keepEmptyLanes,
|
|
77
116
|
});
|
|
78
|
-
|
|
117
|
+
// MIL-126: note-binding mismatches read slices/*.md alongside the model, so (like
|
|
118
|
+
// validateLineage/validateFrontmatterCoherence in `em validate`) they're computed here
|
|
119
|
+
// rather than living in compile()'s pure diagnostics. Always warning-severity — folding
|
|
120
|
+
// them in never changes whether rendering proceeds below.
|
|
121
|
+
const allDiagnostics = [...diagnostics, ...validateNoteBindings(model, refs, dirname(file))];
|
|
122
|
+
printDiagnostics(allDiagnostics);
|
|
79
123
|
warnMissingNotes(file, model);
|
|
80
124
|
if (opts.emitDot) {
|
|
81
125
|
if (opts.out) {
|
|
@@ -87,7 +131,7 @@ program
|
|
|
87
131
|
}
|
|
88
132
|
return;
|
|
89
133
|
}
|
|
90
|
-
if (hasErrors(
|
|
134
|
+
if (hasErrors(allDiagnostics)) {
|
|
91
135
|
console.error("not rendering: fix the errors above");
|
|
92
136
|
process.exit(1);
|
|
93
137
|
}
|
|
@@ -119,9 +163,40 @@ program
|
|
|
119
163
|
.description("export a versioned JSON snapshot of the normalized model")
|
|
120
164
|
.argument("<file>", "input .em file")
|
|
121
165
|
.option("-o, --out <path>", "write to a file instead of stdout")
|
|
166
|
+
.option("--slice <key>", "export only this slice's object (pattern/fields/doc) instead of the whole model (export " +
|
|
167
|
+
"key, MIL-128) — refuses only if THIS slice has an error; an unrelated slice's breakage " +
|
|
168
|
+
"elsewhere in the model doesn't block it (see docs/cli.md)")
|
|
122
169
|
.action((file, opts) => {
|
|
123
170
|
const { model, refs, diagnostics, source } = compileFile(file);
|
|
124
171
|
printDiagnostics(diagnostics);
|
|
172
|
+
if (opts.slice) {
|
|
173
|
+
// Scoped export deliberately does NOT reuse the whole-model error guard below: the
|
|
174
|
+
// ticket's own motivation for `--slice` is an agent implementing one already-ratified
|
|
175
|
+
// slice while the rest of a large, still-WIP model has unrelated errors — the same
|
|
176
|
+
// "don't gate on breakage elsewhere" call `--slice-ready` already made (see its own
|
|
177
|
+
// scoping comment above). Only an error that concerns THIS slice (bare key, or an
|
|
178
|
+
// element ref prefixed `<key>/`) refuses.
|
|
179
|
+
const key = opts.slice;
|
|
180
|
+
const scopedErrors = diagnostics.filter((d) => d.severity === "error" && d.refs?.some((r) => r === key || r.startsWith(`${key}/`)));
|
|
181
|
+
if (scopedErrors.length > 0) {
|
|
182
|
+
console.error(`not exporting: slice "${key}" has errors — fix them first`);
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
const sliceExport = buildSliceExport(model, refs, diagnostics, source, file, key);
|
|
186
|
+
if (!sliceExport.found) {
|
|
187
|
+
console.error(`em export --slice: no slice with export key "${key}" in this model`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
printDiagnostics(newDiagnostics(sliceExport.diagnostics, diagnostics));
|
|
191
|
+
if (opts.out) {
|
|
192
|
+
writeFileSync(opts.out, sliceExport.text + "\n");
|
|
193
|
+
console.log(`wrote ${opts.out}`);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
process.stdout.write(sliceExport.text + "\n");
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
125
200
|
if (hasErrors(diagnostics)) {
|
|
126
201
|
console.error("not exporting: fix the errors above");
|
|
127
202
|
process.exit(1);
|
|
@@ -277,6 +352,95 @@ program
|
|
|
277
352
|
const sliceWord = result.slices === 1 ? "slice" : "slices";
|
|
278
353
|
console.log(`wrote ${opts.out}/ (${result.models} ${modelWord}, ${result.slices} ${sliceWord})`);
|
|
279
354
|
});
|
|
355
|
+
// Namespace for slice-doc authoring/maintenance subcommands: `new` (MIL-97, scaffolds a fresh
|
|
356
|
+
// slice doc's frontmatter) and `index` (MIL-98, regenerates the model README's Slices table)
|
|
357
|
+
// share the `em slice <verb>` shape rather than living as separate top-level commands.
|
|
358
|
+
const slice = program.command("slice").description("author and maintain slice docs");
|
|
359
|
+
slice
|
|
360
|
+
.command("new")
|
|
361
|
+
.description("scaffold a fresh slices/<key>.md doc — the 5 frontmatter keys required at `status: draft` " +
|
|
362
|
+
"plus the `# Slice:` heading and diagram-image stub; judgment sections (Intent, " +
|
|
363
|
+
"Scenarios, Open Questions, ...) stay hand-authored (see docs/slice-doc-schema.md, " +
|
|
364
|
+
"templates/slice.md)")
|
|
365
|
+
.argument("<name>", "slice display name (e.g. \"Request Payment\") — kebab-cased for the filename")
|
|
366
|
+
.requiredOption("--pattern <pattern>", `slice pattern: ${SLICE_PATTERNS.join(" | ")}`)
|
|
367
|
+
.requiredOption("--swimlane <swimlane>", 'swimlane, e.g. "Persona → Context"')
|
|
368
|
+
.option("-f, --force", "overwrite the file if it already exists")
|
|
369
|
+
.action((name, opts) => {
|
|
370
|
+
if (!isSlicePattern(opts.pattern)) {
|
|
371
|
+
console.error(`em slice new: invalid --pattern "${opts.pattern}" — expected one of: ${SLICE_PATTERNS.join(", ")}`);
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
const key = sliceDocKey(name);
|
|
375
|
+
const dir = "slices";
|
|
376
|
+
const path = join(dir, `${key}.md`);
|
|
377
|
+
if (existsSync(path) && !opts.force) {
|
|
378
|
+
console.error(`refusing to overwrite ${path} (use --force)`);
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
mkdirSync(dir, { recursive: true });
|
|
382
|
+
writeFileSync(path, buildSliceDocContent(name, key, opts.pattern, opts.swimlane));
|
|
383
|
+
console.log(`wrote ${path}`);
|
|
384
|
+
console.log(`add this to the slice's primary element in the .em file:`);
|
|
385
|
+
console.log(` note "${path}"`);
|
|
386
|
+
});
|
|
387
|
+
slice
|
|
388
|
+
.command("index")
|
|
389
|
+
.description("rewrite the model's sibling README.md's GENERATED Slices table from `em export`'s slice " +
|
|
390
|
+
"facts (key, pattern, doc status/implementedIn) — the hand-maintained table is deprecated")
|
|
391
|
+
.argument("<file>", "input .em file")
|
|
392
|
+
.option("--check", "verify the table is current; exit non-zero on drift without writing (CI)")
|
|
393
|
+
.action((file, opts) => {
|
|
394
|
+
const { model, diagnostics, refs } = compileFile(file);
|
|
395
|
+
printDiagnostics(diagnostics);
|
|
396
|
+
if (hasErrors(diagnostics)) {
|
|
397
|
+
console.error("not indexing: fix the errors above");
|
|
398
|
+
process.exit(1);
|
|
399
|
+
}
|
|
400
|
+
const result = runSliceIndex(model, refs, file, !!opts.check);
|
|
401
|
+
printDiagnostics(newDiagnostics(result.table.diagnostics, diagnostics));
|
|
402
|
+
if (!result.ok) {
|
|
403
|
+
console.error(result.message);
|
|
404
|
+
process.exit(1);
|
|
405
|
+
}
|
|
406
|
+
if (result.wrote) {
|
|
407
|
+
console.log(`wrote ${result.readmePath}`);
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
console.log(`ok — ${result.readmePath}'s Slices table is already up to date`);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
slice
|
|
414
|
+
.command("mark-implemented")
|
|
415
|
+
.description("flip a slice doc's frontmatter to `status: implemented` / `implementedIn: <pr-url>` — the " +
|
|
416
|
+
"one edit an implementing agent makes to a ratified doc at merge (MIL-103, replaces the " +
|
|
417
|
+
"em-sdd-bridge `em-sdd-mark-implemented` script; see reference/implement.md §6). " +
|
|
418
|
+
"Idempotent on the same URL; refuses to overwrite a different one; never touches `version:` " +
|
|
419
|
+
"or the doc body")
|
|
420
|
+
.argument("<file>", "input .em file")
|
|
421
|
+
.argument("<slice-key>", "slice export key (kebab-case)")
|
|
422
|
+
.argument("<pr-url>", "merged PR (or commit) URL")
|
|
423
|
+
.action((file, sliceKey, prUrl) => {
|
|
424
|
+
const { model, refs, diagnostics } = compileFile(file);
|
|
425
|
+
printDiagnostics(diagnostics);
|
|
426
|
+
// Scoped the same way `em export --slice`/`em validate --slice-ready` are: only an error
|
|
427
|
+
// concerning THIS slice (bare key, or an element ref prefixed `<key>/`) refuses — an
|
|
428
|
+
// unrelated slice's breakage elsewhere in a large, still-WIP model doesn't block marking
|
|
429
|
+
// this one implemented.
|
|
430
|
+
const scopedErrors = diagnostics.filter((d) => d.severity === "error" && d.refs?.some((r) => r === sliceKey || r.startsWith(`${sliceKey}/`)));
|
|
431
|
+
if (scopedErrors.length > 0) {
|
|
432
|
+
console.error(`em slice mark-implemented: slice "${sliceKey}" has errors — fix them first`);
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
const result = runMarkImplemented(model, refs, dirname(file), sliceKey, prUrl);
|
|
436
|
+
if (!result.ok) {
|
|
437
|
+
console.error(`em slice mark-implemented: ${result.message}`);
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
console.log(result.changed
|
|
441
|
+
? `marked implemented: ${result.path} (implementedIn: ${prUrl})`
|
|
442
|
+
: `already implemented (no-op): ${result.path}`);
|
|
443
|
+
});
|
|
280
444
|
program
|
|
281
445
|
.command("changelog")
|
|
282
446
|
.description("render a model's git history as a business-readable ledger (see docs/cli.md)")
|
|
@@ -299,6 +463,124 @@ program
|
|
|
299
463
|
process.stdout.write(markdown + "\n");
|
|
300
464
|
}
|
|
301
465
|
});
|
|
466
|
+
const STATE_DIR_HELP = "model directory containing .event-modeling.md, or a direct path to that file (default: current directory)";
|
|
467
|
+
/** Shared by every `em state` writer: load, apply the pure patch, write, report — the only
|
|
468
|
+
* difference between set-phase/set-conformance/set-review is which `mutate` closure they pass. */
|
|
469
|
+
function writeStateUpdate(dirOrFile, cmdLabel, mutate) {
|
|
470
|
+
const loaded = loadStateFile(dirOrFile);
|
|
471
|
+
if (!loaded.ok) {
|
|
472
|
+
console.error(loaded.message);
|
|
473
|
+
process.exit(1);
|
|
474
|
+
}
|
|
475
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
476
|
+
const result = mutate(loaded.text, today);
|
|
477
|
+
if (!result.ok) {
|
|
478
|
+
console.error(`${cmdLabel}: ${result.message}`);
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
writeFileSync(loaded.path, result.text);
|
|
482
|
+
console.log(`wrote ${loaded.path}`);
|
|
483
|
+
}
|
|
484
|
+
const state = program
|
|
485
|
+
.command("state")
|
|
486
|
+
.description("read/write the state file's mechanical fields deterministically (see docs/cli.md)");
|
|
487
|
+
state
|
|
488
|
+
.command("read")
|
|
489
|
+
.description("print the state file's mechanical fields as JSON")
|
|
490
|
+
.argument("[dir]", STATE_DIR_HELP, ".")
|
|
491
|
+
.action((dir) => {
|
|
492
|
+
const loaded = loadStateFile(dir);
|
|
493
|
+
if (!loaded.ok) {
|
|
494
|
+
console.error(loaded.message);
|
|
495
|
+
process.exit(1);
|
|
496
|
+
}
|
|
497
|
+
const parsed = parseState(loaded.text);
|
|
498
|
+
if (!parsed.ok) {
|
|
499
|
+
console.error(`em state read: ${parsed.message}`);
|
|
500
|
+
process.exit(1);
|
|
501
|
+
}
|
|
502
|
+
console.log(JSON.stringify(parsed.state, null, 2));
|
|
503
|
+
});
|
|
504
|
+
state
|
|
505
|
+
.command("set-phase")
|
|
506
|
+
.description("rewrite Current phase: (and Last updated:); --step also rewrites Current step:")
|
|
507
|
+
.argument("<phase>", `phase: ${PHASES.join(" | ")}`)
|
|
508
|
+
.argument("[dir]", STATE_DIR_HELP, ".")
|
|
509
|
+
.option("--step <n>", "also set Current step: to this value")
|
|
510
|
+
.action((phase, dir, opts) => {
|
|
511
|
+
if (!isPhase(phase)) {
|
|
512
|
+
console.error(`em state set-phase: invalid phase "${phase}" — expected one of: ${PHASES.join(", ")}`);
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}
|
|
515
|
+
writeStateUpdate(dir, "em state set-phase", (text, today) => setPhase(text, phase, today, opts.step));
|
|
516
|
+
});
|
|
517
|
+
state
|
|
518
|
+
.command("set-conformance")
|
|
519
|
+
.description("rewrite Last conformance: (and Last updated:) in the exact format reference/conform.md parses")
|
|
520
|
+
.argument("<revision>", "target-repo revision just diffed against")
|
|
521
|
+
.argument("[dir]", STATE_DIR_HELP, ".")
|
|
522
|
+
.requiredOption("--report <path>", "path to the conformance report just written")
|
|
523
|
+
.action((revision, dir, opts) => {
|
|
524
|
+
writeStateUpdate(dir, "em state set-conformance", (text, today) => setConformance(text, revision, opts.report, today));
|
|
525
|
+
});
|
|
526
|
+
state
|
|
527
|
+
.command("set-review")
|
|
528
|
+
.description("rewrite Last stakeholder review: (and Last updated:)")
|
|
529
|
+
.argument("<date>", "review date, YYYY-MM-DD")
|
|
530
|
+
.argument("[dir]", STATE_DIR_HELP, ".")
|
|
531
|
+
.action((date, dir) => {
|
|
532
|
+
if (!isValidDateString(date)) {
|
|
533
|
+
console.error(`em state set-review: invalid date "${date}" — expected YYYY-MM-DD`);
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
writeStateUpdate(dir, "em state set-review", (text, today) => setReview(text, date, today));
|
|
537
|
+
});
|
|
538
|
+
program
|
|
539
|
+
.command("conform-scope")
|
|
540
|
+
.description("mechanize conform phase step 1 (reference/conform.md): map the target repo's changed paths " +
|
|
541
|
+
"since Last conformance: to slices via each slice doc's implementedIn, JSON to stdout — " +
|
|
542
|
+
"--seed-asis also seeds the <model>-asis.em scratch model (see docs/cli.md)")
|
|
543
|
+
.argument("<file>", "input .em file")
|
|
544
|
+
.requiredOption("--repo <path>", "path to (or inside) the target codebase's git repository")
|
|
545
|
+
.option("--full", "ignore Last conformance:/changed paths; scope every implemented slice")
|
|
546
|
+
.option("--seed-asis", "write <model>-asis.em as a byte copy of the canonical model and ensure it's gitignored")
|
|
547
|
+
.action((file, opts) => {
|
|
548
|
+
const { model, diagnostics, refs } = compileFile(file);
|
|
549
|
+
printDiagnostics(diagnostics);
|
|
550
|
+
if (hasErrors(diagnostics)) {
|
|
551
|
+
console.error("not scoping: fix the errors above");
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
const loaded = loadStateFile(dirname(file));
|
|
555
|
+
if (!loaded.ok) {
|
|
556
|
+
console.error(loaded.message);
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
const parsed = parseState(loaded.text);
|
|
560
|
+
if (!parsed.ok) {
|
|
561
|
+
console.error(`em conform-scope: ${parsed.message}`);
|
|
562
|
+
process.exit(1);
|
|
563
|
+
}
|
|
564
|
+
const { lastConformance } = parsed.state;
|
|
565
|
+
const { facts, diagnostics: docDiags } = resolveSliceDocFacts(model, refs, dirname(file));
|
|
566
|
+
printDiagnostics(newDiagnostics(docDiags, diagnostics));
|
|
567
|
+
const skipGit = !!opts.full || lastConformance === null;
|
|
568
|
+
let changedPaths = [];
|
|
569
|
+
if (!skipGit) {
|
|
570
|
+
const result = changedPathsSince(opts.repo, lastConformance.revision);
|
|
571
|
+
if (!result.ok) {
|
|
572
|
+
console.error(result.message);
|
|
573
|
+
process.exit(1);
|
|
574
|
+
}
|
|
575
|
+
changedPaths = result.paths;
|
|
576
|
+
}
|
|
577
|
+
const scope = buildConformScope(facts, lastConformance, changedPaths, !!opts.full);
|
|
578
|
+
const output = { ...scope };
|
|
579
|
+
if (opts.seedAsis) {
|
|
580
|
+
output.seeded = seedAsisModel(file);
|
|
581
|
+
}
|
|
582
|
+
console.log(JSON.stringify(output, null, 2));
|
|
583
|
+
});
|
|
302
584
|
program
|
|
303
585
|
.command("watch")
|
|
304
586
|
.description("re-render on every save")
|
|
@@ -312,16 +594,24 @@ program
|
|
|
312
594
|
const out = opts.out ?? defaultOut(file, opts.format ?? "svg");
|
|
313
595
|
const fmt = opts.format ?? formatFromPath(out);
|
|
314
596
|
let server;
|
|
315
|
-
|
|
597
|
+
// serializeBuilds: chokidar can fire several events for one save (and an
|
|
598
|
+
// editor's write can land while a slow png/pdf render is still in flight) —
|
|
599
|
+
// overlapping builds would race each other writing the same output file.
|
|
600
|
+
const build = serializeBuilds(async () => {
|
|
316
601
|
const started = Date.now();
|
|
317
602
|
try {
|
|
318
|
-
const { dot, model, grid, diagnostics } = compileFile(file, {
|
|
319
|
-
|
|
320
|
-
|
|
603
|
+
const { dot, model, grid, diagnostics } = compileFile(file, { keepEmptyLanes: opts.keepEmptyLanes },
|
|
604
|
+
// A bad save must not kill the watcher: the error is reported below
|
|
605
|
+
// and pushed to the live view's banner instead.
|
|
606
|
+
{ survive: true });
|
|
321
607
|
printDiagnostics(diagnostics);
|
|
322
608
|
warnMissingNotes(file, model);
|
|
323
609
|
if (hasErrors(diagnostics)) {
|
|
324
610
|
console.error("skipped render (errors above)");
|
|
611
|
+
// The shared screen must say WHY it's stale, not silently show an
|
|
612
|
+
// outdated model — push the first error into the viewer's banner.
|
|
613
|
+
const first = diagnostics.find((d) => d.severity === "error");
|
|
614
|
+
server?.notifyError(first ? formatDiagnostic(first).trim() : "model has errors — see terminal");
|
|
325
615
|
return;
|
|
326
616
|
}
|
|
327
617
|
await renderDot(dot, model, grid, out, fmt, dirname(file));
|
|
@@ -330,16 +620,25 @@ program
|
|
|
330
620
|
}
|
|
331
621
|
catch (e) {
|
|
332
622
|
reportError(e);
|
|
623
|
+
server?.notifyError(e instanceof Error ? e.message : String(e));
|
|
333
624
|
}
|
|
334
|
-
};
|
|
625
|
+
});
|
|
335
626
|
await build();
|
|
336
627
|
if (opts.serve) {
|
|
337
628
|
try {
|
|
338
629
|
// Serve the directory the SVG is written to — that's what the browser
|
|
339
630
|
// fetches, and note "..." links inside the SVG resolve relative to it.
|
|
340
631
|
server = await startLiveServer({ dir: dirname(resolve(out)), port: opts.port });
|
|
341
|
-
|
|
342
|
-
|
|
632
|
+
// The viewer inlines SVG only — for png/pdf watches there's nothing
|
|
633
|
+
// live to embed, so don't print a ?svg= URL that can't work.
|
|
634
|
+
if (fmt === "svg") {
|
|
635
|
+
console.log(`→ live view: ${server.url}/?svg=${encodeURIComponent(basename(out))}`);
|
|
636
|
+
console.log(" open it in a browser and share your screen");
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
console.log(`→ serving ${dirname(resolve(out))} at ${server.url}`);
|
|
640
|
+
console.log(" (the live viewer needs an .svg output — add -o <name>.svg to use it)");
|
|
641
|
+
}
|
|
343
642
|
}
|
|
344
643
|
catch (e) {
|
|
345
644
|
reportError(e);
|
|
@@ -350,6 +649,8 @@ program
|
|
|
350
649
|
// the .em file. The glob still fires for docs authored after watch starts.
|
|
351
650
|
watchFile([file, join(dirname(file), "slices", "*.md")], build);
|
|
352
651
|
console.log(`watching ${file} … (ctrl-c to stop)`);
|
|
652
|
+
if (!opts.serve)
|
|
653
|
+
console.log("tip: add --serve for the live browser view");
|
|
353
654
|
});
|
|
354
655
|
program
|
|
355
656
|
.command("validate")
|
|
@@ -365,25 +666,33 @@ program
|
|
|
365
666
|
.option("--fail-on-issues", "exit non-zero if the model has any open `issue`s (opt-in — issues are warnings and don't block by default)")
|
|
366
667
|
.option("--slice-ready <key>", "readiness gate for one slice (export key): status ready-to-implement, doc resolvable via " +
|
|
367
668
|
"note binding, zero unchecked Open Questions — exits non-zero if not ready (MIL-87)")
|
|
669
|
+
.option("--json", "print a JSON document instead of text — works on a model WITH errors, unlike `em export` " +
|
|
670
|
+
"(MIL-128, see docs/cli.md); exit codes are unchanged")
|
|
368
671
|
.action((file, opts) => {
|
|
369
672
|
const { model, diagnostics, refs } = compileFile(file);
|
|
370
|
-
// Lineage-ref resolution (MIL-84)
|
|
371
|
-
//
|
|
372
|
-
//
|
|
673
|
+
// Lineage-ref resolution (MIL-84), frontmatter-coherence (MIL-85), note-binding
|
|
674
|
+
// mismatches (MIL-126), and doc↔model consistency (MIL-124) are validate's fs-aware
|
|
675
|
+
// rules — every other check above is a pure function of the .em source. All four read
|
|
676
|
+
// slices/*.md alongside the model. Doc↔model consistency is deliberately validate-only
|
|
677
|
+
// (unlike note-binding, which `em render` also folds in) — it's a conform-phase concern,
|
|
678
|
+
// not something every render needs to recheck.
|
|
373
679
|
const allDiagnostics = [
|
|
374
680
|
...diagnostics,
|
|
375
681
|
...validateLineage(model, refs, dirname(file)),
|
|
376
682
|
...validateFrontmatterCoherence(model, refs, dirname(file)),
|
|
683
|
+
...validateNoteBindings(model, refs, dirname(file)),
|
|
684
|
+
...validateDocModelConsistency(model, refs, dirname(file)),
|
|
377
685
|
];
|
|
378
686
|
if (opts.sliceReady) {
|
|
379
687
|
// MIL-87: a targeted, single-slice readiness gate, not part of the unconditional
|
|
380
688
|
// diagnostic set above (see sliceReadyValidate.ts's header for why). Folds in MIL-85's
|
|
381
|
-
// frontmatter-coherence findings
|
|
382
|
-
//
|
|
689
|
+
// frontmatter-coherence findings, MIL-126's note-binding-mismatch findings, AND MIL-124's
|
|
690
|
+
// doc-model-consistency findings for free by filtering allDiagnostics' own refs, rather
|
|
691
|
+
// than re-deriving any of those classifications here.
|
|
383
692
|
//
|
|
384
693
|
// "Concerns this slice" means a ref that either IS the bare slice key (lineage,
|
|
385
|
-
// frontmatter-coherence, and this module's own diagnostics all tag that
|
|
386
|
-
// with `<sliceKey>/` (every element-level ref from computeRefs()/model/validate.ts,
|
|
694
|
+
// frontmatter-coherence, note-binding, and this module's own diagnostics all tag that
|
|
695
|
+
// way) OR starts with `<sliceKey>/` (every element-level ref from computeRefs()/model/validate.ts,
|
|
387
696
|
// e.g. an unknown-event error on a view inside this slice). An earlier version gated on
|
|
388
697
|
// "any error anywhere in the model," which silently failed the check — with zero
|
|
389
698
|
// diagnostics printed — on an unrelated slice's breakage; scoping to this slice alone
|
|
@@ -395,21 +704,46 @@ program
|
|
|
395
704
|
const scoped = combined.filter((d) => d.refs?.some((r) => r === key || r.startsWith(`${key}/`)));
|
|
396
705
|
printDiagnostics(scoped);
|
|
397
706
|
const ready = scoped.length === 0;
|
|
398
|
-
|
|
707
|
+
if (opts.json) {
|
|
708
|
+
// MIL-128: the 4 named gates (see computeSliceReadyGates) plus the same `scoped`
|
|
709
|
+
// diagnostics and `ready` verdict driving the exit code below — replaces both the
|
|
710
|
+
// scraped warning prose and the two hand-parsed English sentences.
|
|
711
|
+
const gates = computeSliceReadyGates(model, refs, dirname(file), key);
|
|
712
|
+
process.stdout.write(buildSliceReadyJson(file, key, gates, scoped, ready) + "\n");
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
console.log(ready ? `slice "${key}" is ready-to-implement` : `slice "${key}" is NOT ready-to-implement`);
|
|
716
|
+
}
|
|
399
717
|
if (!ready)
|
|
400
718
|
process.exit(1);
|
|
401
719
|
return;
|
|
402
720
|
}
|
|
403
721
|
if (opts.listIssues || opts.listDivergences || opts.listPublic) {
|
|
404
|
-
if (opts.listIssues)
|
|
405
|
-
printIssues(model);
|
|
406
|
-
if (opts.listDivergences)
|
|
407
|
-
printDivergences(model);
|
|
408
|
-
if (opts.listPublic)
|
|
409
|
-
printPublicElements(model);
|
|
410
722
|
// Errors still fail the run below — surface them rather than exiting
|
|
411
723
|
// non-zero with nothing but the list on screen.
|
|
412
|
-
|
|
724
|
+
const errorsOnly = allDiagnostics.filter((d) => d.severity === "error");
|
|
725
|
+
if (opts.json) {
|
|
726
|
+
const markers = collectMarkers(model, refs, {
|
|
727
|
+
issues: opts.listIssues,
|
|
728
|
+
divergences: opts.listDivergences,
|
|
729
|
+
public: opts.listPublic,
|
|
730
|
+
});
|
|
731
|
+
printDiagnostics(errorsOnly);
|
|
732
|
+
process.stdout.write(buildValidateListJson(file, markers, errorsOnly) + "\n");
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
if (opts.listIssues)
|
|
736
|
+
printIssues(model);
|
|
737
|
+
if (opts.listDivergences)
|
|
738
|
+
printDivergences(model);
|
|
739
|
+
if (opts.listPublic)
|
|
740
|
+
printPublicElements(model);
|
|
741
|
+
printDiagnostics(errorsOnly);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
else if (opts.json) {
|
|
745
|
+
printDiagnostics(allDiagnostics);
|
|
746
|
+
process.stdout.write(buildValidateJson(file, allDiagnostics) + "\n");
|
|
413
747
|
}
|
|
414
748
|
else {
|
|
415
749
|
printDiagnostics(allDiagnostics);
|
|
@@ -421,6 +755,61 @@ program
|
|
|
421
755
|
if (opts.failOnIssues && model.elements.some((el) => el.issue))
|
|
422
756
|
process.exit(1);
|
|
423
757
|
});
|
|
758
|
+
program
|
|
759
|
+
.command("migrate")
|
|
760
|
+
.description("rewrite the old two-slice Automation/Translation shape into the merged single-slice " +
|
|
761
|
+
"shape MIL-120 made canonical (see docs/cli.md)")
|
|
762
|
+
.argument("<file>", "input .em file")
|
|
763
|
+
.option("--write", "apply the rewrite to the file (default: dry run — report only, write nothing)")
|
|
764
|
+
.action((file, opts) => {
|
|
765
|
+
const source = readFileOrExit(file);
|
|
766
|
+
let plan;
|
|
767
|
+
try {
|
|
768
|
+
plan = planMigration(source);
|
|
769
|
+
}
|
|
770
|
+
catch (e) {
|
|
771
|
+
if (e instanceof ParseError) {
|
|
772
|
+
console.error(`parse error in ${file} ${e.message}`);
|
|
773
|
+
process.exit(1);
|
|
774
|
+
}
|
|
775
|
+
throw e;
|
|
776
|
+
}
|
|
777
|
+
if (plan.changes.length === 0 && plan.refusals.length === 0) {
|
|
778
|
+
console.log(`nothing to migrate — ${file} has no old two-slice Automation/Translation shape`);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
if (plan.changes.length > 0) {
|
|
782
|
+
const verify = verifyMigration(source, plan.rewritten);
|
|
783
|
+
if (!verify.ok) {
|
|
784
|
+
console.error(`em migrate: aborting — the rewrite would introduce ${verify.newErrors.length} new error(s):`);
|
|
785
|
+
for (const d of verify.newErrors)
|
|
786
|
+
console.error(` ${formatDiagnostic(d)}`);
|
|
787
|
+
console.error(`${file} left untouched`);
|
|
788
|
+
process.exit(1);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
for (const c of plan.changes)
|
|
792
|
+
console.log(c.message);
|
|
793
|
+
for (const r of plan.refusals)
|
|
794
|
+
console.log(r.message);
|
|
795
|
+
if (plan.changes.length === 0) {
|
|
796
|
+
console.log(`0 site(s) migrated, ${plan.refusals.length} refused`);
|
|
797
|
+
}
|
|
798
|
+
else if (opts.write) {
|
|
799
|
+
writeFileSync(file, plan.rewritten);
|
|
800
|
+
const refusedNote = plan.refusals.length > 0 ? `, ${plan.refusals.length} refused` : "";
|
|
801
|
+
console.log(`wrote ${file} — ${plan.changes.length} site(s) migrated${refusedNote}`);
|
|
802
|
+
}
|
|
803
|
+
else {
|
|
804
|
+
const refusedNote = plan.refusals.length > 0 ? `; ${plan.refusals.length} refused` : "";
|
|
805
|
+
console.log(`${plan.changes.length} site(s) would migrate (dry run — re-run with --write to apply)${refusedNote}`);
|
|
806
|
+
}
|
|
807
|
+
// A refusal always means the file still needs a human, whether or not other sites in the
|
|
808
|
+
// same run migrated cleanly — same "set exitCode, don't truncate stdout" rationale as
|
|
809
|
+
// em ledger/em diff, even though migrate's own output is never large.
|
|
810
|
+
if (plan.refusals.length > 0)
|
|
811
|
+
process.exitCode = 1;
|
|
812
|
+
});
|
|
424
813
|
program
|
|
425
814
|
.command("ledger")
|
|
426
815
|
.description("check slice docs' version: field agrees with their content across two git revisions " +
|
|
@@ -454,6 +843,62 @@ program
|
|
|
454
843
|
if (result.findings.length > 0)
|
|
455
844
|
process.exitCode = 1;
|
|
456
845
|
});
|
|
846
|
+
program
|
|
847
|
+
.command("coverage")
|
|
848
|
+
.description("check that every INV-* invariant ID cited in a ready-to-implement/implemented slice doc " +
|
|
849
|
+
"is cited by a test under --tests <dir> (MIL-130) — mechanizes reference/implement.md's " +
|
|
850
|
+
"definition-of-done citation check; advisory by default, --strict for CI")
|
|
851
|
+
.argument("<file>", "input .em file")
|
|
852
|
+
.requiredOption("--tests <dir>", "directory to scan recursively for test files citing invariant IDs")
|
|
853
|
+
.option("--strict", "exit non-zero if any invariant ID has zero citations (CI)")
|
|
854
|
+
.option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
|
|
855
|
+
.action((file, opts) => {
|
|
856
|
+
const { model, diagnostics, refs } = compileFile(file);
|
|
857
|
+
printDiagnostics(diagnostics);
|
|
858
|
+
if (hasErrors(diagnostics)) {
|
|
859
|
+
console.error("em coverage: model has errors — fix them first");
|
|
860
|
+
process.exit(1);
|
|
861
|
+
}
|
|
862
|
+
if (!existsSync(opts.tests)) {
|
|
863
|
+
console.error(`em coverage: --tests directory not found: ${opts.tests}`);
|
|
864
|
+
process.exit(1);
|
|
865
|
+
}
|
|
866
|
+
if (!statSync(opts.tests).isDirectory()) {
|
|
867
|
+
console.error(`em coverage: --tests is not a directory: ${opts.tests}`);
|
|
868
|
+
process.exit(1);
|
|
869
|
+
}
|
|
870
|
+
const report = buildCoverageReport(model, refs, dirname(file), opts.tests);
|
|
871
|
+
if (opts.json) {
|
|
872
|
+
process.stdout.write(buildCoverageJson(file, opts.tests, report) + "\n");
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
for (const slice of report.slices) {
|
|
876
|
+
if (!slice.inScope)
|
|
877
|
+
continue;
|
|
878
|
+
console.log(`slice "${slice.key}" (${slice.status}):`);
|
|
879
|
+
if (slice.invariants.length === 0) {
|
|
880
|
+
console.log(` (no INV-* invariant IDs found in the doc body)`);
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
for (const inv of slice.invariants) {
|
|
884
|
+
if (inv.cited) {
|
|
885
|
+
console.log(` cited ${inv.id}`);
|
|
886
|
+
for (const c of inv.citations)
|
|
887
|
+
console.log(` ${c.file}:${c.line}`);
|
|
888
|
+
}
|
|
889
|
+
else {
|
|
890
|
+
console.log(` uncovered ${inv.id}`);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
console.log(`${report.totalInvariants} invariant(s) checked, ${report.uncoveredCount} uncovered`);
|
|
895
|
+
}
|
|
896
|
+
// Advisory by default (exit 0 even with uncovered IDs) — --strict is the opt-in CI gate,
|
|
897
|
+
// same "set exitCode, don't truncate stdout" rationale as em ledger/em diff for the --json
|
|
898
|
+
// form, and consistent for the text form too.
|
|
899
|
+
if (opts.strict && report.uncoveredCount > 0)
|
|
900
|
+
process.exitCode = 1;
|
|
901
|
+
});
|
|
457
902
|
// Shared by install/sync/check: the skill directory bundled with whatever em package is
|
|
458
903
|
// actually running (works whether em was installed from npm or run from a checkout, and
|
|
459
904
|
// regardless of a symlinked global install — see pkgDir's own resolution above for
|
|
@@ -464,6 +909,35 @@ function packagedSkillDir() {
|
|
|
464
909
|
function vendoredSkillDir(repoRoot) {
|
|
465
910
|
return join(resolve(repoRoot), ".claude", "skills", "event-modeling");
|
|
466
911
|
}
|
|
912
|
+
program
|
|
913
|
+
.command("contract")
|
|
914
|
+
.description("print the packaged implementation contract (reference/implement.md) to stdout — the " +
|
|
915
|
+
"agent-neutral discovery path for any agent that can run a shell, not just Claude Code " +
|
|
916
|
+
"(MIL-129); see docs/cli.md")
|
|
917
|
+
.action(() => {
|
|
918
|
+
process.stdout.write(readContract(packagedSkillDir()));
|
|
919
|
+
});
|
|
920
|
+
program
|
|
921
|
+
.command("mcp")
|
|
922
|
+
.description("start an MCP (Model Context Protocol) server over stdio, exposing validate/slice_ready/" +
|
|
923
|
+
"list_markers/export_model/export_slice/coverage/contract as tools (MIL-21) — a " +
|
|
924
|
+
"structured, agent-facing alternative to shelling out to `em`; see docs/mcp.md. " +
|
|
925
|
+
"Equivalent to running the `em-mcp` bin directly")
|
|
926
|
+
.action(async () => {
|
|
927
|
+
// A 3-line wrapper only, for discoverability — the server itself (src/mcp/) never imports
|
|
928
|
+
// this file or commander, matching the ticket's "outside the CLI core" constraint. The
|
|
929
|
+
// `em-mcp` bin (package.json) starts the identical server the same way.
|
|
930
|
+
await createMcpServer().connect(new StdioServerTransport());
|
|
931
|
+
});
|
|
932
|
+
/** One line reporting what `syncAgentsMd` did, shared by `skill install`/`skill sync` so both
|
|
933
|
+
* commands describe the AGENTS.md write in the same words. */
|
|
934
|
+
function agentsMdMessage(result) {
|
|
935
|
+
if (result.created)
|
|
936
|
+
return `created ${result.path} with the agent-contract section (MIL-129)`;
|
|
937
|
+
if (result.wrote)
|
|
938
|
+
return `updated the agent-contract section in ${result.path}`;
|
|
939
|
+
return `${result.path} already has an up-to-date agent-contract section`;
|
|
940
|
+
}
|
|
467
941
|
const skill = program
|
|
468
942
|
.command("skill")
|
|
469
943
|
.description("manage Claude Code skills bundled with em");
|
|
@@ -471,36 +945,46 @@ skill
|
|
|
471
945
|
.command("install")
|
|
472
946
|
.description("copy the event-modeling skill into .claude/skills/event-modeling/")
|
|
473
947
|
.option("-f, --force", "overwrite an existing installation")
|
|
948
|
+
.option("--no-agents-md", "skip writing/updating the AGENTS.md agent-contract section (on by default, MIL-129)")
|
|
474
949
|
.action(async (opts) => {
|
|
475
950
|
const src = packagedSkillDir();
|
|
476
951
|
const dest = vendoredSkillDir(process.cwd());
|
|
477
952
|
if (existsSync(dest) && !opts.force) {
|
|
478
953
|
console.log(`skill already installed at ${dest}`);
|
|
479
954
|
console.log("re-run with --force to overwrite");
|
|
480
|
-
return;
|
|
481
955
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
956
|
+
else {
|
|
957
|
+
await mkdir(join(process.cwd(), ".claude", "skills"), { recursive: true });
|
|
958
|
+
await cp(src, dest, { recursive: true });
|
|
959
|
+
console.log(`installed event-modeling skill → ${dest}`);
|
|
960
|
+
console.log("in Claude Code, run /event-modeling to start a guided session");
|
|
961
|
+
}
|
|
962
|
+
if (opts.agentsMd !== false) {
|
|
963
|
+
console.log(agentsMdMessage(syncAgentsMd(process.cwd())));
|
|
964
|
+
}
|
|
486
965
|
});
|
|
487
966
|
skill
|
|
488
967
|
.command("sync")
|
|
489
968
|
.description("update the vendored .claude/skills/event-modeling/ copy in [path] to match the installed " +
|
|
490
969
|
"em package (overwrites unconditionally; local edits are never merged, MIL-93)")
|
|
491
970
|
.argument("[path]", "consumer repo root", ".")
|
|
492
|
-
.
|
|
971
|
+
.option("--no-agents-md", "skip writing/updating the AGENTS.md agent-contract section (on by default, MIL-129)")
|
|
972
|
+
.action(async (path, opts) => {
|
|
493
973
|
const packagedDir = packagedSkillDir();
|
|
494
974
|
const vendoredDir = vendoredSkillDir(path);
|
|
495
975
|
const plan = planSkillSync(packagedDir, vendoredDir);
|
|
496
976
|
if (plan.changes.length === 0) {
|
|
497
977
|
console.log(`up to date — ${vendoredDir} already matches the installed skill (${plan.unchangedCount} file(s))`);
|
|
498
|
-
return;
|
|
499
978
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
979
|
+
else {
|
|
980
|
+
applySkillSync(plan, packagedDir, vendoredDir);
|
|
981
|
+
for (const c of plan.changes)
|
|
982
|
+
console.log(`${c.kind}: ${c.relPath}`);
|
|
983
|
+
console.log(`synced ${vendoredDir} — ${plan.changes.length} file(s) changed, ${plan.unchangedCount} unchanged`);
|
|
984
|
+
}
|
|
985
|
+
if (opts.agentsMd !== false) {
|
|
986
|
+
console.log(agentsMdMessage(syncAgentsMd(resolve(path))));
|
|
987
|
+
}
|
|
504
988
|
});
|
|
505
989
|
skill
|
|
506
990
|
.command("check")
|
|
@@ -538,12 +1022,18 @@ if (isMainModule(import.meta.url)) {
|
|
|
538
1022
|
});
|
|
539
1023
|
}
|
|
540
1024
|
// ---- helpers ----
|
|
541
|
-
function compileFile(file, opts = {}
|
|
1025
|
+
function compileFile(file, opts = {},
|
|
1026
|
+
// `em watch` must OUTLIVE a bad save — a mid-edit syntax error killing the
|
|
1027
|
+
// watcher (and the --serve live view with it) is a session-ending failure.
|
|
1028
|
+
// One-shot commands keep the exit-with-message behavior.
|
|
1029
|
+
{ survive = false } = {}) {
|
|
542
1030
|
let source;
|
|
543
1031
|
try {
|
|
544
1032
|
source = readFileSync(file, "utf8");
|
|
545
1033
|
}
|
|
546
1034
|
catch {
|
|
1035
|
+
if (survive)
|
|
1036
|
+
throw new Error(`cannot read ${file}`);
|
|
547
1037
|
console.error(`cannot read ${file}`);
|
|
548
1038
|
process.exit(1);
|
|
549
1039
|
}
|
|
@@ -552,6 +1042,8 @@ function compileFile(file, opts = {}) {
|
|
|
552
1042
|
}
|
|
553
1043
|
catch (e) {
|
|
554
1044
|
if (e instanceof ParseError) {
|
|
1045
|
+
if (survive)
|
|
1046
|
+
throw new Error(`parse error in ${file} ${e.message}`);
|
|
555
1047
|
console.error(`parse error in ${file} ${e.message}`);
|
|
556
1048
|
process.exit(1);
|
|
557
1049
|
}
|
|
@@ -657,7 +1149,7 @@ function buildChangelogDoc(file, repoRoot, commits) {
|
|
|
657
1149
|
});
|
|
658
1150
|
const intro = models[0] ? { slices: models[0].slices.length, elements: models[0].elements.length } : null;
|
|
659
1151
|
const introError = models[0] ? undefined : (errors[0] ?? undefined);
|
|
660
|
-
const stateFile = join(dirname(file),
|
|
1152
|
+
const stateFile = join(dirname(file), STATE_FILE_NAME);
|
|
661
1153
|
const decisions = existsSync(stateFile) ? parseDecisionsLog(readFileSync(stateFile, "utf8")) : [];
|
|
662
1154
|
return buildChangelog(entries, decisions, { file, intro, introError });
|
|
663
1155
|
}
|