@milehimikey/em 1.8.1 → 1.9.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 +58 -404
- package/.claude/skills/event-modeling-conform/SKILL.md +77 -0
- package/.claude/skills/{event-modeling → event-modeling-conform}/reference/conform.md +39 -13
- package/.claude/skills/event-modeling-design/SKILL.md +139 -0
- package/.claude/skills/event-modeling-discover/SKILL.md +89 -0
- package/.claude/skills/{event-modeling → event-modeling-discover}/reference/extract.md +3 -2
- package/.claude/skills/event-modeling-implement/SKILL.md +40 -0
- package/.claude/skills/{event-modeling → event-modeling-implement}/reference/implement.md +3 -2
- package/.claude/skills/event-modeling-review/SKILL.md +58 -0
- package/.claude/skills/{event-modeling → event-modeling-shared}/reference/em-dsl.md +115 -83
- package/.claude/skills/{event-modeling → event-modeling-shared}/reference/methodology.md +6 -5
- package/.claude/skills/event-modeling-shared/reference/operating-principles.md +147 -0
- package/.claude/skills/{event-modeling → event-modeling-shared}/templates/model-readme.md +2 -2
- package/.claude/skills/{event-modeling → event-modeling-shared}/templates/slice.md +51 -8
- package/README.md +24 -4
- package/dist/catalog/build.js +9 -0
- package/dist/catalog/build.js.map +1 -1
- package/dist/catalog/docJoin.d.ts +15 -0
- package/dist/catalog/docJoin.js +8 -0
- package/dist/catalog/docJoin.js.map +1 -1
- package/dist/catalog/modelCollisionValidate.d.ts +22 -0
- package/dist/catalog/modelCollisionValidate.js +67 -0
- package/dist/catalog/modelCollisionValidate.js.map +1 -0
- package/dist/catalog/orphanedSliceDocValidate.d.ts +11 -0
- package/dist/catalog/orphanedSliceDocValidate.js +98 -0
- package/dist/catalog/orphanedSliceDocValidate.js.map +1 -0
- package/dist/catalog/sliceDoc.d.ts +15 -0
- package/dist/catalog/sliceDoc.js +18 -0
- package/dist/catalog/sliceDoc.js.map +1 -1
- package/dist/cli/changelogBuild.d.ts +12 -0
- package/dist/cli/changelogBuild.js +89 -0
- package/dist/cli/changelogBuild.js.map +1 -0
- package/dist/cli/ciInit.d.ts +42 -0
- package/dist/cli/ciInit.js +298 -0
- package/dist/cli/ciInit.js.map +1 -0
- package/dist/cli/conformSupersede.d.ts +32 -0
- package/dist/cli/conformSupersede.js +102 -0
- package/dist/cli/conformSupersede.js.map +1 -0
- package/dist/cli/coverage.d.ts +6 -1
- package/dist/cli/coverage.js +28 -3
- package/dist/cli/coverage.js.map +1 -1
- package/dist/cli/frontmatterSurgery.d.ts +28 -0
- package/dist/cli/frontmatterSurgery.js +66 -0
- package/dist/cli/frontmatterSurgery.js.map +1 -0
- package/dist/cli/markImplemented.js +6 -45
- package/dist/cli/markImplemented.js.map +1 -1
- package/dist/cli/ratify.d.ts +41 -0
- package/dist/cli/ratify.js +151 -0
- package/dist/cli/ratify.js.map +1 -0
- package/dist/cli/reratify.d.ts +33 -0
- package/dist/cli/reratify.js +137 -0
- package/dist/cli/reratify.js.map +1 -0
- package/dist/cli/skillCheck.d.ts +6 -0
- package/dist/cli/skillCheck.js +46 -0
- package/dist/cli/skillCheck.js.map +1 -1
- package/dist/cli/skillDirs.d.ts +15 -0
- package/dist/cli/skillDirs.js +34 -0
- package/dist/cli/skillDirs.js.map +1 -0
- package/dist/cli/skillSync.d.ts +10 -0
- package/dist/cli/skillSync.js +15 -0
- package/dist/cli/skillSync.js.map +1 -1
- package/dist/cli/sliceIndex.d.ts +3 -0
- package/dist/cli/sliceIndex.js +6 -2
- package/dist/cli/sliceIndex.js.map +1 -1
- package/dist/cli/sliceLink.d.ts +52 -0
- package/dist/cli/sliceLink.js +152 -0
- package/dist/cli/sliceLink.js.map +1 -0
- package/dist/cli/status.d.ts +261 -0
- package/dist/cli/status.js +536 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/cli/usageLog.d.ts +75 -0
- package/dist/cli/usageLog.js +193 -0
- package/dist/cli/usageLog.js.map +1 -0
- package/dist/cli.js +501 -130
- package/dist/cli.js.map +1 -1
- package/dist/emit/freshnessJson.d.ts +6 -0
- package/dist/emit/freshnessJson.js +24 -0
- package/dist/emit/freshnessJson.js.map +1 -0
- package/dist/emit/json.d.ts +62 -8
- package/dist/emit/json.js +14 -3
- package/dist/emit/json.js.map +1 -1
- package/dist/emit/statusJson.d.ts +7 -0
- package/dist/emit/statusJson.js +39 -0
- package/dist/emit/statusJson.js.map +1 -0
- package/dist/emit/typespec.d.ts +26 -0
- package/dist/emit/typespec.js +356 -0
- package/dist/emit/typespec.js.map +1 -0
- package/dist/emit/usageReportJson.d.ts +3 -0
- package/dist/emit/usageReportJson.js +23 -0
- package/dist/emit/usageReportJson.js.map +1 -0
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +301 -11
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/rules.d.ts +13 -0
- package/dist/model/rules.js +20 -0
- package/dist/model/rules.js.map +1 -1
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +2 -2
- package/dist/util/markers.d.ts +16 -4
- package/dist/util/markers.js +23 -10
- package/dist/util/markers.js.map +1 -1
- package/package.json +2 -1
- /package/.claude/skills/{event-modeling → event-modeling-shared}/templates/conformance-report.md +0 -0
- /package/.claude/skills/{event-modeling → event-modeling-shared}/templates/state.md +0 -0
package/dist/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ 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
17
|
import { buildExport, buildSliceExport } from "./emit/json.js";
|
|
18
|
+
import { buildTypeSpec } from "./emit/typespec.js";
|
|
18
19
|
import { buildValidateJson, buildSliceReadyJson, buildValidateListJson, collectMarkers } from "./emit/validateJson.js";
|
|
19
20
|
import { buildDiffJson } from "./emit/diffJson.js";
|
|
20
21
|
import { diffModels, formatModelDiff, hasChanges } from "./model/diff.js";
|
|
@@ -24,16 +25,22 @@ import { validateLineage } from "./catalog/lineageValidate.js";
|
|
|
24
25
|
import { validateFrontmatterCoherence } from "./catalog/frontmatterCoherenceValidate.js";
|
|
25
26
|
import { validateNoteBindings } from "./catalog/noteBindingValidate.js";
|
|
26
27
|
import { validateDocModelConsistency } from "./catalog/docModelConsistencyValidate.js";
|
|
28
|
+
import { validateOrphanedSliceDocs } from "./catalog/orphanedSliceDocValidate.js";
|
|
27
29
|
import { validateSliceReady, computeSliceReadyGates } from "./catalog/sliceReadyValidate.js";
|
|
30
|
+
import { detectSliceDocCollisions } from "./catalog/modelCollisionValidate.js";
|
|
28
31
|
import { checkLedger } from "./cli/ledgerCheck.js";
|
|
29
32
|
import { planMigration, verifyMigration } from "./cli/migrateReactionShape.js";
|
|
30
33
|
import { buildLedgerJson } from "./emit/ledgerJson.js";
|
|
31
34
|
import { buildCoverageReport } from "./cli/coverage.js";
|
|
32
35
|
import { buildCoverageJson } from "./emit/coverageJson.js";
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
36
|
+
import { resolveSliceStatusFacts, countOpenIssues, resolveConformanceEntry, aggregateInvariantTotals, buildStatusReport, formatStatusText, formatStatusMarkdown, buildStatusBadge, formatConformancePart, } from "./cli/status.js";
|
|
37
|
+
import { buildStatusJson } from "./emit/statusJson.js";
|
|
38
|
+
import { buildFreshnessJson } from "./emit/freshnessJson.js";
|
|
39
|
+
import { planSkillSyncBundle, applySkillSyncBundle } from "./cli/skillSync.js";
|
|
40
|
+
import { checkSkillSyncBundle } from "./cli/skillCheck.js";
|
|
35
41
|
import { buildSkillCheckJson } from "./emit/skillCheckJson.js";
|
|
36
42
|
import { readContract } from "./cli/contract.js";
|
|
43
|
+
import { EM_SKILL_DIR_NAMES, EM_SHARED_DIR_NAMES, EM_ALL_SKILL_BUNDLE_DIRS, EM_SKILL_ANCHOR_DIR } from "./cli/skillDirs.js";
|
|
37
44
|
import { createServer as createMcpServer } from "./mcp/server.js";
|
|
38
45
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
39
46
|
import { syncAgentsMd } from "./cli/agentsMd.js";
|
|
@@ -44,13 +51,21 @@ import { buildCatalog } from "./catalog/build.js";
|
|
|
44
51
|
import { planCatalogArgs } from "./cli/catalog-inputs.js";
|
|
45
52
|
import { runSliceIndex } from "./cli/sliceIndex.js";
|
|
46
53
|
import { runMarkImplemented } from "./cli/markImplemented.js";
|
|
54
|
+
import { runRatify } from "./cli/ratify.js";
|
|
55
|
+
import { runConformSupersede } from "./cli/conformSupersede.js";
|
|
47
56
|
import { buildConformScope, changedPathsSince, resolveSliceDocFacts, seedAsisModel } from "./cli/conformScope.js";
|
|
48
57
|
import { buildSliceDocContent, isSlicePattern, sliceDocKey, SLICE_PATTERNS } from "./cli/sliceNew.js";
|
|
49
|
-
import {
|
|
50
|
-
import {
|
|
58
|
+
import { wireSliceNote } from "./cli/sliceLink.js";
|
|
59
|
+
import { listModelCommits } from "./cli/changelog-git.js";
|
|
60
|
+
import { buildChangelogDoc } from "./cli/changelogBuild.js";
|
|
61
|
+
import { runReratify } from "./cli/reratify.js";
|
|
51
62
|
import { STATE_FILE_NAME, PHASES, isPhase, loadStateFile, parseState, setPhase, setConformance, setReview, isValidDateString, } from "./cli/stateFile.js";
|
|
52
63
|
import { STARTER_EM, starterEmFor, scaffoldReadme, scaffoldStateFile } from "./templates.js";
|
|
53
64
|
import { kebabSlug } from "./util/slug.js";
|
|
65
|
+
import { ciWorkflowPath, conformWorkflowPath, buildCiWorkflowFile, buildConformWorkflowFile, ciManagedBody, conformManagedBody, planCiFile, applyCiFile, findUnsafeCiInitArg, CI_WORKFLOW_MARKER, CONFORM_WORKFLOW_MARKER, } from "./cli/ciInit.js";
|
|
66
|
+
import { RULES } from "./model/rules.js";
|
|
67
|
+
import { USAGE_PHASES, isUsagePhase, sortUsagePhases, appendUsageLogEntry, findStateFiles, aggregateUsageReport, formatUsageReportText, } from "./cli/usageLog.js";
|
|
68
|
+
import { buildUsageReportJson } from "./emit/usageReportJson.js";
|
|
54
69
|
const program = new Command();
|
|
55
70
|
// Single source of truth for the version (also read by src/emit/json.ts for
|
|
56
71
|
// `generator.version` in `em export`): package.json, one level up from both
|
|
@@ -76,25 +91,30 @@ program
|
|
|
76
91
|
program
|
|
77
92
|
.command("scaffold")
|
|
78
93
|
.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
|
|
94
|
+
"(see docs/cli.md — for just a starter .em, use `em init`; for a multi-model project, " +
|
|
95
|
+
"pass --under to nest it under a shared parent directory)")
|
|
80
96
|
.argument("<name>", "model display name — kebab-cased for the directory and file names, used as-is for titles/prose")
|
|
81
97
|
.option("-f, --force", "overwrite the directory's contents if it already exists")
|
|
98
|
+
.option("--under <dir>", "parent directory to scaffold into — writes <dir>/<slug>/ instead of ./<slug>/, the " +
|
|
99
|
+
"supported multi-model layout (docs/cli.md, \"Multi-model projects\"): one directory per " +
|
|
100
|
+
"model, so each model's slices/ never collides with a sibling model's")
|
|
82
101
|
.action(async (name, opts) => {
|
|
83
102
|
if (name.includes('"') || name.includes("{{")) {
|
|
84
103
|
console.error(`em scaffold: name must not contain '"' or '{{' (breaks the generated .em/README/state files): ${name}`);
|
|
85
104
|
process.exit(1);
|
|
86
105
|
}
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
106
|
+
const slugName = kebabSlug(name);
|
|
107
|
+
const dirPath = opts.under ? join(opts.under, slugName) : slugName;
|
|
108
|
+
if (existsSync(dirPath) && !opts.force) {
|
|
109
|
+
console.error(`refusing to overwrite ${dirPath}/ (use --force)`);
|
|
90
110
|
process.exit(1);
|
|
91
111
|
}
|
|
92
|
-
await mkdir(
|
|
112
|
+
await mkdir(dirPath, { recursive: true });
|
|
93
113
|
const today = new Date().toISOString().slice(0, 10);
|
|
94
|
-
writeFileSync(join(
|
|
95
|
-
writeFileSync(join(
|
|
96
|
-
writeFileSync(join(
|
|
97
|
-
console.log(`scaffolded ${
|
|
114
|
+
writeFileSync(join(dirPath, `${slugName}.em`), starterEmFor(name));
|
|
115
|
+
writeFileSync(join(dirPath, "README.md"), scaffoldReadme(name, slugName));
|
|
116
|
+
writeFileSync(join(dirPath, STATE_FILE_NAME), scaffoldStateFile(name, slugName, today));
|
|
117
|
+
console.log(`scaffolded ${dirPath}/`);
|
|
98
118
|
});
|
|
99
119
|
program
|
|
100
120
|
.command("render")
|
|
@@ -211,6 +231,31 @@ program
|
|
|
211
231
|
process.stdout.write(exported.text + "\n");
|
|
212
232
|
}
|
|
213
233
|
});
|
|
234
|
+
program
|
|
235
|
+
.command("typespec")
|
|
236
|
+
.description("EXPERIMENTAL/POC (MIL-159): generate a TypeSpec contract for a model's commands, public " +
|
|
237
|
+
"events, and public views (see docs/cli.md)")
|
|
238
|
+
.argument("<file>", "input .em file")
|
|
239
|
+
.option("-o, --out <path>", "write to a file instead of stdout")
|
|
240
|
+
.action((file, opts) => {
|
|
241
|
+
const { model, refs, diagnostics, source } = compileFile(file);
|
|
242
|
+
printDiagnostics(diagnostics);
|
|
243
|
+
if (hasErrors(diagnostics)) {
|
|
244
|
+
console.error("not generating: fix the errors above");
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
const generated = buildTypeSpec(model, refs, diagnostics, source, file);
|
|
248
|
+
printDiagnostics(newDiagnostics(generated.diagnostics, diagnostics));
|
|
249
|
+
for (const note of generated.unmappedTypes)
|
|
250
|
+
console.error(`note: ${note}`);
|
|
251
|
+
if (opts.out) {
|
|
252
|
+
writeFileSync(opts.out, generated.text + "\n");
|
|
253
|
+
console.log(`wrote ${opts.out}`);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
process.stdout.write(generated.text + "\n");
|
|
257
|
+
}
|
|
258
|
+
});
|
|
214
259
|
program
|
|
215
260
|
.command("diff")
|
|
216
261
|
.description("compare two models structurally (two files, or one file across git revisions)")
|
|
@@ -366,6 +411,8 @@ slice
|
|
|
366
411
|
.requiredOption("--pattern <pattern>", `slice pattern: ${SLICE_PATTERNS.join(" | ")}`)
|
|
367
412
|
.requiredOption("--swimlane <swimlane>", 'swimlane, e.g. "Persona → Context"')
|
|
368
413
|
.option("-f, --force", "overwrite the file if it already exists")
|
|
414
|
+
.option("--wire <model-file>", "also insert the `note \"slices/<key>.md\"` line onto the slice's primary element in this " +
|
|
415
|
+
".em file (matched by export key), instead of just printing it to paste by hand (MIL-161)")
|
|
369
416
|
.action((name, opts) => {
|
|
370
417
|
if (!isSlicePattern(opts.pattern)) {
|
|
371
418
|
console.error(`em slice new: invalid --pattern "${opts.pattern}" — expected one of: ${SLICE_PATTERNS.join(", ")}`);
|
|
@@ -378,11 +425,35 @@ slice
|
|
|
378
425
|
console.error(`refusing to overwrite ${path} (use --force)`);
|
|
379
426
|
process.exit(1);
|
|
380
427
|
}
|
|
428
|
+
// Resolve (never write anything yet) BEFORE touching the doc file, so a --wire failure
|
|
429
|
+
// (no matching slice, ambiguous primary element, already wired) leaves nothing written at
|
|
430
|
+
// all rather than a doc with no wiring and a non-zero exit.
|
|
431
|
+
let wired = null;
|
|
432
|
+
if (opts.wire) {
|
|
433
|
+
const { model, refs, diagnostics, source } = compileFile(opts.wire);
|
|
434
|
+
printDiagnostics(diagnostics);
|
|
435
|
+
if (hasErrors(diagnostics)) {
|
|
436
|
+
console.error(`em slice new --wire: not wiring — fix the errors in ${opts.wire} above`);
|
|
437
|
+
process.exit(1);
|
|
438
|
+
}
|
|
439
|
+
const result = wireSliceNote(source, model, refs, key, opts.pattern, path);
|
|
440
|
+
if (!result.ok) {
|
|
441
|
+
console.error(`em slice new --wire: ${result.message}`);
|
|
442
|
+
process.exit(1);
|
|
443
|
+
}
|
|
444
|
+
wired = result;
|
|
445
|
+
}
|
|
381
446
|
mkdirSync(dir, { recursive: true });
|
|
382
447
|
writeFileSync(path, buildSliceDocContent(name, key, opts.pattern, opts.swimlane));
|
|
383
448
|
console.log(`wrote ${path}`);
|
|
384
|
-
|
|
385
|
-
|
|
449
|
+
if (wired) {
|
|
450
|
+
writeFileSync(opts.wire, wired.content);
|
|
451
|
+
console.log(`wired ${path} onto ${wired.elementName} in slice "${wired.sliceName}" (${opts.wire})`);
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
console.log(`add this to the slice's primary element in the .em file:`);
|
|
455
|
+
console.log(` note "${path}"`);
|
|
456
|
+
}
|
|
386
457
|
});
|
|
387
458
|
slice
|
|
388
459
|
.command("index")
|
|
@@ -441,6 +512,68 @@ slice
|
|
|
441
512
|
? `marked implemented: ${result.path} (implementedIn: ${prUrl})`
|
|
442
513
|
: `already implemented (no-op): ${result.path}`);
|
|
443
514
|
});
|
|
515
|
+
slice
|
|
516
|
+
.command("ratify")
|
|
517
|
+
.description("flip a slice doc's frontmatter to `status: ready-to-implement` and record `ratifiedBy:`/" +
|
|
518
|
+
"`ratifiedOn:` — the handoff sign-off (MIL-165, docs/process.md#what-ratified-means) that " +
|
|
519
|
+
"makes who ratified, and when, a first-class recorded fact. Idempotent on the same " +
|
|
520
|
+
"--by/--on pair; refuses to overwrite a different one already recorded; never touches " +
|
|
521
|
+
"`version:` or the doc body")
|
|
522
|
+
.argument("<file>", "input .em file")
|
|
523
|
+
.argument("<slice-key>", "slice export key (kebab-case)")
|
|
524
|
+
.requiredOption("--by <name>", "the ratifier's name")
|
|
525
|
+
.option("--on <date>", "ratification date, YYYY-MM-DD (default: today)")
|
|
526
|
+
.action((file, sliceKey, opts) => {
|
|
527
|
+
const { model, refs, diagnostics } = compileFile(file);
|
|
528
|
+
printDiagnostics(diagnostics);
|
|
529
|
+
// Scoped the same way `em slice mark-implemented`/`em export --slice`/`em validate
|
|
530
|
+
// --slice-ready` are: only an error concerning THIS slice refuses.
|
|
531
|
+
const scopedErrors = diagnostics.filter((d) => d.severity === "error" && d.refs?.some((r) => r === sliceKey || r.startsWith(`${sliceKey}/`)));
|
|
532
|
+
if (scopedErrors.length > 0) {
|
|
533
|
+
console.error(`em slice ratify: slice "${sliceKey}" has errors — fix them first`);
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
537
|
+
const ratifiedOn = opts.on ?? today;
|
|
538
|
+
if (opts.on !== undefined && !isValidDateString(opts.on)) {
|
|
539
|
+
console.error(`em slice ratify: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
|
|
540
|
+
process.exit(1);
|
|
541
|
+
}
|
|
542
|
+
const result = runRatify(model, refs, dirname(file), sliceKey, opts.by, ratifiedOn);
|
|
543
|
+
if (!result.ok) {
|
|
544
|
+
console.error(`em slice ratify: ${result.message}`);
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
console.log(result.changed
|
|
548
|
+
? `ratified: ${result.path} (ratifiedBy: ${opts.by}, ratifiedOn: ${ratifiedOn})`
|
|
549
|
+
: `already ratified (no-op): ${result.path}`);
|
|
550
|
+
});
|
|
551
|
+
slice
|
|
552
|
+
.command("reratify")
|
|
553
|
+
.description("bump `version:` and flip a shipped slice doc's frontmatter back to `status: " +
|
|
554
|
+
"ready-to-implement` — the re-ratification mechanical edit (MIL-161, mirrors `em slice " +
|
|
555
|
+
"mark-implemented`). Only applies to a doc at `status: implemented`; clears any stale " +
|
|
556
|
+
"`ratifiedBy:`/`ratifiedOn:` (they describe the PRIOR version's sign-off) so a follow-up " +
|
|
557
|
+
"`em slice ratify --by` applies cleanly; never touches `implementedIn:` or the doc body")
|
|
558
|
+
.argument("<file>", "input .em file")
|
|
559
|
+
.argument("<slice-key>", "slice export key (kebab-case)")
|
|
560
|
+
.action((file, sliceKey) => {
|
|
561
|
+
const { model, refs, diagnostics } = compileFile(file);
|
|
562
|
+
printDiagnostics(diagnostics);
|
|
563
|
+
// Scoped the same way `em slice ratify`/`em slice mark-implemented`/`em export --slice`/
|
|
564
|
+
// `em validate --slice-ready` are: only an error concerning THIS slice refuses.
|
|
565
|
+
const scopedErrors = diagnostics.filter((d) => d.severity === "error" && d.refs?.some((r) => r === sliceKey || r.startsWith(`${sliceKey}/`)));
|
|
566
|
+
if (scopedErrors.length > 0) {
|
|
567
|
+
console.error(`em slice reratify: slice "${sliceKey}" has errors — fix them first`);
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
const result = runReratify(model, refs, dirname(file), sliceKey);
|
|
571
|
+
if (!result.ok) {
|
|
572
|
+
console.error(`em slice reratify: ${result.message}`);
|
|
573
|
+
process.exit(1);
|
|
574
|
+
}
|
|
575
|
+
console.log(`reratified: ${result.path} (version: ${result.newVersion}, status: ready-to-implement)`);
|
|
576
|
+
});
|
|
444
577
|
program
|
|
445
578
|
.command("changelog")
|
|
446
579
|
.description("render a model's git history as a business-readable ledger (see docs/cli.md)")
|
|
@@ -535,6 +668,69 @@ state
|
|
|
535
668
|
}
|
|
536
669
|
writeStateUpdate(dir, "em state set-review", (text, today) => setReview(text, date, today));
|
|
537
670
|
});
|
|
671
|
+
state
|
|
672
|
+
.command("log-usage")
|
|
673
|
+
.description("append one Usage log line — phase(s) touched + em validate's diagnostic categories hit, " +
|
|
674
|
+
"deduped and canonically formatted (MIL-161, docs/usage-data.md) — the mechanical half of " +
|
|
675
|
+
"'save state at the end of every session' that used to be run-validate-then-hand-format; " +
|
|
676
|
+
"state file resolved next to <file>, same convention as em conform-scope")
|
|
677
|
+
.argument("<file>", "input .em file")
|
|
678
|
+
.requiredOption("--phases <list>", `comma-separated phase(s) touched this session: ${USAGE_PHASES.join(", ")}`)
|
|
679
|
+
.action((file, opts) => {
|
|
680
|
+
const rawPhases = opts.phases
|
|
681
|
+
.split(",")
|
|
682
|
+
.map((s) => s.trim())
|
|
683
|
+
.filter((s) => s.length > 0);
|
|
684
|
+
if (rawPhases.length === 0) {
|
|
685
|
+
console.error("em state log-usage: --phases must name at least one phase");
|
|
686
|
+
process.exit(1);
|
|
687
|
+
}
|
|
688
|
+
const invalid = rawPhases.filter((p) => !isUsagePhase(p));
|
|
689
|
+
if (invalid.length > 0) {
|
|
690
|
+
console.error(`em state log-usage: invalid phase(s) ${invalid.map((p) => `"${p}"`).join(", ")} — expected one of: ${USAGE_PHASES.join(", ")}`);
|
|
691
|
+
process.exit(1);
|
|
692
|
+
}
|
|
693
|
+
const phases = sortUsagePhases(rawPhases);
|
|
694
|
+
const { model, diagnostics, refs } = compileFile(file);
|
|
695
|
+
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics);
|
|
696
|
+
const categorySet = new Set(allDiagnostics.map((d) => RULES[d.code].usageCategory));
|
|
697
|
+
const categories = categorySet.size > 0 ? [...categorySet].sort() : ["none"];
|
|
698
|
+
const loaded = loadStateFile(dirname(file));
|
|
699
|
+
if (!loaded.ok) {
|
|
700
|
+
console.error(loaded.message);
|
|
701
|
+
process.exit(1);
|
|
702
|
+
}
|
|
703
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
704
|
+
const result = appendUsageLogEntry(loaded.text, today, phases, categories);
|
|
705
|
+
if (!result.ok) {
|
|
706
|
+
console.error(`em state log-usage: ${result.message}`);
|
|
707
|
+
process.exit(1);
|
|
708
|
+
}
|
|
709
|
+
writeFileSync(loaded.path, result.text);
|
|
710
|
+
console.log(`wrote ${loaded.path} — phases: ${phases.join(", ")} — validate: ${categories.join(", ")}`);
|
|
711
|
+
});
|
|
712
|
+
program
|
|
713
|
+
.command("usage-report")
|
|
714
|
+
.description("aggregate every .event-modeling.md's Usage log under [root] into phase/diagnostic-category " +
|
|
715
|
+
"tallies (MIL-161) — replaces docs/usage-data.md's hand-rolled grep/awk/sort pipeline; a " +
|
|
716
|
+
"logged line that doesn't match the canonical em state log-usage format is reported under " +
|
|
717
|
+
"unparseableLines rather than silently mistallied or dropped")
|
|
718
|
+
.argument("[root]", "directory to search recursively for .event-modeling.md files", ".")
|
|
719
|
+
.option("--json", "print a JSON document instead of the text report")
|
|
720
|
+
.action((root, opts) => {
|
|
721
|
+
const files = findStateFiles(root);
|
|
722
|
+
const entries = files.map((relPath) => ({
|
|
723
|
+
file: relPath,
|
|
724
|
+
text: readFileSync(join(root, relPath), "utf8"),
|
|
725
|
+
}));
|
|
726
|
+
const report = aggregateUsageReport(root, entries);
|
|
727
|
+
if (opts.json) {
|
|
728
|
+
process.stdout.write(buildUsageReportJson(report) + "\n");
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
console.log(formatUsageReportText(report));
|
|
732
|
+
}
|
|
733
|
+
});
|
|
538
734
|
program
|
|
539
735
|
.command("conform-scope")
|
|
540
736
|
.description("mechanize conform phase step 1 (reference/conform.md): map the target repo's changed paths " +
|
|
@@ -581,6 +777,35 @@ program
|
|
|
581
777
|
}
|
|
582
778
|
console.log(JSON.stringify(output, null, 2));
|
|
583
779
|
});
|
|
780
|
+
program
|
|
781
|
+
.command("conform-supersede")
|
|
782
|
+
.description("stamp a conformance report with a \"superseded as of <rev>\" banner once its findings have " +
|
|
783
|
+
"been ruled on (MIL-164, docs/process.md#what-ratified-means) — the companion step to run " +
|
|
784
|
+
"at ratification time so a reader following the report's file:line citations knows they " +
|
|
785
|
+
"describe an ancestor of the current model. Additive-only splice, never a rewrite of the " +
|
|
786
|
+
"report; idempotent on the same --as-of/--findings/--on stamp; refuses if the report " +
|
|
787
|
+
"doesn't exist")
|
|
788
|
+
.argument("<file>", "input .em file (used only to resolve the report path's base directory)")
|
|
789
|
+
.argument("<report-path>", "path to the conformance report, relative to the model's directory")
|
|
790
|
+
.requiredOption("--as-of <rev>", "the revision this ruling was made against — same value passed to `em state set-conformance`")
|
|
791
|
+
.requiredOption("--findings <spec>", 'which finding number(s) this stamps as ruled, e.g. "1-3" or "1,2,4"')
|
|
792
|
+
.option("--on <date>", "ruling date, YYYY-MM-DD (default: today)")
|
|
793
|
+
.action((file, reportPath, opts) => {
|
|
794
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
795
|
+
const on = opts.on ?? today;
|
|
796
|
+
if (opts.on !== undefined && !isValidDateString(opts.on)) {
|
|
797
|
+
console.error(`em conform-supersede: invalid --on date "${opts.on}" — expected YYYY-MM-DD`);
|
|
798
|
+
process.exit(1);
|
|
799
|
+
}
|
|
800
|
+
const result = runConformSupersede(dirname(file), reportPath, opts.asOf, opts.findings, on);
|
|
801
|
+
if (!result.ok) {
|
|
802
|
+
console.error(`em conform-supersede: ${result.message}`);
|
|
803
|
+
process.exit(1);
|
|
804
|
+
}
|
|
805
|
+
console.log(result.changed
|
|
806
|
+
? `stamped superseded: ${result.path} (as of ${opts.asOf}, findings ${opts.findings}, on ${on})`
|
|
807
|
+
: `already stamped (no-op): ${result.path}`);
|
|
808
|
+
});
|
|
584
809
|
program
|
|
585
810
|
.command("watch")
|
|
586
811
|
.description("re-render on every save")
|
|
@@ -670,19 +895,7 @@ program
|
|
|
670
895
|
"(MIL-128, see docs/cli.md); exit codes are unchanged")
|
|
671
896
|
.action((file, opts) => {
|
|
672
897
|
const { model, diagnostics, refs } = compileFile(file);
|
|
673
|
-
|
|
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.
|
|
679
|
-
const allDiagnostics = [
|
|
680
|
-
...diagnostics,
|
|
681
|
-
...validateLineage(model, refs, dirname(file)),
|
|
682
|
-
...validateFrontmatterCoherence(model, refs, dirname(file)),
|
|
683
|
-
...validateNoteBindings(model, refs, dirname(file)),
|
|
684
|
-
...validateDocModelConsistency(model, refs, dirname(file)),
|
|
685
|
-
];
|
|
898
|
+
const allDiagnostics = computeAllDiagnostics(file, model, refs, diagnostics);
|
|
686
899
|
if (opts.sliceReady) {
|
|
687
900
|
// MIL-87: a targeted, single-slice readiness gate, not part of the unconditional
|
|
688
901
|
// diagnostic set above (see sliceReadyValidate.ts's header for why). Folds in MIL-85's
|
|
@@ -899,15 +1112,147 @@ program
|
|
|
899
1112
|
if (opts.strict && report.uncoveredCount > 0)
|
|
900
1113
|
process.exitCode = 1;
|
|
901
1114
|
});
|
|
902
|
-
|
|
1115
|
+
program
|
|
1116
|
+
.command("status")
|
|
1117
|
+
.description("deterministic state-of-the-system rollup over one or more .em models: slices by " +
|
|
1118
|
+
"lifecycle status, driftSignal breakdown, invariant coverage totals (with --tests), " +
|
|
1119
|
+
"open issue markers + unchecked Open Questions, and last-conformance commits-behind-HEAD " +
|
|
1120
|
+
"(MIL-163, see docs/cli.md)")
|
|
1121
|
+
.argument("<files...>", "input .em files")
|
|
1122
|
+
.option("--tests <dir>", "directory to scan for INV-* test citations — enables invariant coverage totals")
|
|
1123
|
+
.option("--repo <path>", "git repo to compute commits-behind-HEAD in (default: each model's own directory)")
|
|
1124
|
+
.option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
|
|
1125
|
+
.option("--md", "print a markdown block suited for README embedding")
|
|
1126
|
+
.option("--badge", "print a generated SVG badge")
|
|
1127
|
+
.option("-o, --out <path>", "write output to a file instead of stdout")
|
|
1128
|
+
.action((files, opts) => {
|
|
1129
|
+
const modesSelected = [opts.json, opts.md, opts.badge].filter(Boolean).length;
|
|
1130
|
+
if (modesSelected > 1) {
|
|
1131
|
+
console.error("em status: --json, --md, and --badge are mutually exclusive");
|
|
1132
|
+
process.exit(1);
|
|
1133
|
+
}
|
|
1134
|
+
if (opts.tests) {
|
|
1135
|
+
if (!existsSync(opts.tests)) {
|
|
1136
|
+
console.error(`em status: --tests directory not found: ${opts.tests}`);
|
|
1137
|
+
process.exit(1);
|
|
1138
|
+
}
|
|
1139
|
+
if (!statSync(opts.tests).isDirectory()) {
|
|
1140
|
+
console.error(`em status: --tests is not a directory: ${opts.tests}`);
|
|
1141
|
+
process.exit(1);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
const compiled = [];
|
|
1145
|
+
let anyErrors = false;
|
|
1146
|
+
for (const file of files) {
|
|
1147
|
+
const { model, refs, diagnostics } = compileFile(file);
|
|
1148
|
+
printDiagnosticsFor(file, diagnostics);
|
|
1149
|
+
if (hasErrors(diagnostics))
|
|
1150
|
+
anyErrors = true;
|
|
1151
|
+
compiled.push({ file, model, refs });
|
|
1152
|
+
}
|
|
1153
|
+
if (anyErrors) {
|
|
1154
|
+
console.error("em status: not reporting status — fix the errors above");
|
|
1155
|
+
process.exit(1);
|
|
1156
|
+
}
|
|
1157
|
+
const sliceFacts = [];
|
|
1158
|
+
const factsByFile = new Map();
|
|
1159
|
+
const statusDiagnostics = [];
|
|
1160
|
+
// MIL-160: a multi-model run is exactly the case where two `.em` files could share a
|
|
1161
|
+
// directory and collide on a slice key — checked once, up front, over every compiled
|
|
1162
|
+
// input (never fatal, same "warn, don't block" posture as the doc-join diagnostics below).
|
|
1163
|
+
const collisions = detectSliceDocCollisions(compiled.map(({ file, refs }) => ({ file, sliceKeys: refs.sliceKeys })));
|
|
1164
|
+
for (const d of collisions) {
|
|
1165
|
+
const file = d.refs?.[2] ?? files[0];
|
|
1166
|
+
printDiagnosticsFor(file, [d]);
|
|
1167
|
+
statusDiagnostics.push({ file, ...d });
|
|
1168
|
+
}
|
|
1169
|
+
let openIssuesCount = 0;
|
|
1170
|
+
const coverageReports = [];
|
|
1171
|
+
for (const { file, model, refs } of compiled) {
|
|
1172
|
+
const baseDir = dirname(file);
|
|
1173
|
+
const { facts, diagnostics: docDiags } = resolveSliceStatusFacts(file, model, refs, baseDir);
|
|
1174
|
+
printDiagnosticsFor(file, docDiags);
|
|
1175
|
+
sliceFacts.push(...facts);
|
|
1176
|
+
factsByFile.set(file, facts);
|
|
1177
|
+
for (const d of docDiags)
|
|
1178
|
+
statusDiagnostics.push({ file, ...d });
|
|
1179
|
+
// MIL-183: a slices/*.md file left behind by a rename/removal, matching no current
|
|
1180
|
+
// slice's key or `covers:` declaration — same "fold into status's diagnostics list" as
|
|
1181
|
+
// every other doc-join warning above.
|
|
1182
|
+
const orphanDiags = validateOrphanedSliceDocs(model, refs, baseDir);
|
|
1183
|
+
printDiagnosticsFor(file, orphanDiags);
|
|
1184
|
+
for (const d of orphanDiags)
|
|
1185
|
+
statusDiagnostics.push({ file, ...d });
|
|
1186
|
+
openIssuesCount += countOpenIssues(model);
|
|
1187
|
+
if (opts.tests)
|
|
1188
|
+
coverageReports.push(buildCoverageReport(model, refs, baseDir, opts.tests));
|
|
1189
|
+
}
|
|
1190
|
+
const invariants = opts.tests ? aggregateInvariantTotals(opts.tests, coverageReports) : null;
|
|
1191
|
+
// MIL-164: slice-PRs-behind-HEAD is scoped per model — THIS model's own facts, not the
|
|
1192
|
+
// merged `sliceFacts` above (which spans every input file when several are given).
|
|
1193
|
+
const conformance = compiled.map(({ file }) => {
|
|
1194
|
+
const facts = factsByFile.get(file) ?? [];
|
|
1195
|
+
const sliceDocFacts = facts.map((f) => ({ key: f.key, status: f.rawStatus, implementedIn: f.implementedIn }));
|
|
1196
|
+
return resolveConformanceEntry(file, opts.repo, sliceDocFacts);
|
|
1197
|
+
});
|
|
1198
|
+
const report = buildStatusReport(files, sliceFacts, openIssuesCount, invariants, conformance, statusDiagnostics);
|
|
1199
|
+
let output;
|
|
1200
|
+
if (opts.json)
|
|
1201
|
+
output = buildStatusJson(report);
|
|
1202
|
+
else if (opts.md)
|
|
1203
|
+
output = formatStatusMarkdown(report);
|
|
1204
|
+
else if (opts.badge)
|
|
1205
|
+
output = buildStatusBadge(report);
|
|
1206
|
+
else
|
|
1207
|
+
output = formatStatusText(report);
|
|
1208
|
+
if (opts.out) {
|
|
1209
|
+
writeFileSync(opts.out, output + "\n");
|
|
1210
|
+
console.log(`wrote ${opts.out}`);
|
|
1211
|
+
}
|
|
1212
|
+
else {
|
|
1213
|
+
process.stdout.write(output + "\n");
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
program
|
|
1217
|
+
.command("freshness")
|
|
1218
|
+
.description("standalone freshness signal for one model's conformance record (MIL-164): \"last conformed " +
|
|
1219
|
+
"<rev> — N commits and M slice-PRs behind HEAD\", computed from the same conform-scope " +
|
|
1220
|
+
"machinery `em status`'s conformance clause uses — for when you want just this fact, no " +
|
|
1221
|
+
"full state-of-the-system rollup (see docs/cli.md)")
|
|
1222
|
+
.argument("<file>", "input .em file")
|
|
1223
|
+
.option("--repo <path>", "git repo to compute behind-HEAD in (default: the model's own directory)")
|
|
1224
|
+
.option("--json", "print a JSON document instead of the text line")
|
|
1225
|
+
.action((file, opts) => {
|
|
1226
|
+
const { model, refs, diagnostics } = compileFile(file);
|
|
1227
|
+
printDiagnostics(diagnostics);
|
|
1228
|
+
if (hasErrors(diagnostics)) {
|
|
1229
|
+
console.error("em freshness: not reporting freshness — fix the errors above");
|
|
1230
|
+
process.exit(1);
|
|
1231
|
+
}
|
|
1232
|
+
const baseDir = dirname(file);
|
|
1233
|
+
const { facts, diagnostics: docDiags } = resolveSliceDocFacts(model, refs, baseDir);
|
|
1234
|
+
printDiagnostics(docDiags);
|
|
1235
|
+
const entry = resolveConformanceEntry(file, opts.repo, facts);
|
|
1236
|
+
if (opts.json) {
|
|
1237
|
+
process.stdout.write(buildFreshnessJson(entry) + "\n");
|
|
1238
|
+
}
|
|
1239
|
+
else {
|
|
1240
|
+
console.log(formatConformancePart(entry));
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
// Shared by install/sync/check: the `.claude/skills/` root bundled with whatever em package is
|
|
903
1244
|
// actually running (works whether em was installed from npm or run from a checkout, and
|
|
904
1245
|
// regardless of a symlinked global install — see pkgDir's own resolution above for
|
|
905
|
-
// PKG_VERSION) and the
|
|
906
|
-
|
|
907
|
-
|
|
1246
|
+
// PKG_VERSION) and the `.claude/skills/` root a consumer repo vendors it into. The em skill
|
|
1247
|
+
// itself lives under several named directories inside this root (MIL-157, src/cli/skillDirs.ts)
|
|
1248
|
+
// rather than being this whole root's only occupant — install/sync/check only ever touch those
|
|
1249
|
+
// named directories, never the root wholesale, so an unrelated skill already vendored alongside
|
|
1250
|
+
// em's is never at risk.
|
|
1251
|
+
function packagedSkillsRoot() {
|
|
1252
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", ".claude", "skills");
|
|
908
1253
|
}
|
|
909
|
-
function
|
|
910
|
-
return join(resolve(repoRoot), ".claude", "skills"
|
|
1254
|
+
function vendoredSkillsRoot(repoRoot) {
|
|
1255
|
+
return join(resolve(repoRoot), ".claude", "skills");
|
|
911
1256
|
}
|
|
912
1257
|
program
|
|
913
1258
|
.command("contract")
|
|
@@ -915,7 +1260,7 @@ program
|
|
|
915
1260
|
"agent-neutral discovery path for any agent that can run a shell, not just Claude Code " +
|
|
916
1261
|
"(MIL-129); see docs/cli.md")
|
|
917
1262
|
.action(() => {
|
|
918
|
-
process.stdout.write(readContract(
|
|
1263
|
+
process.stdout.write(readContract(join(packagedSkillsRoot(), "event-modeling-implement")));
|
|
919
1264
|
});
|
|
920
1265
|
program
|
|
921
1266
|
.command("mcp")
|
|
@@ -943,20 +1288,23 @@ const skill = program
|
|
|
943
1288
|
.description("manage Claude Code skills bundled with em");
|
|
944
1289
|
skill
|
|
945
1290
|
.command("install")
|
|
946
|
-
.description("copy the event-modeling skill into .claude/skills/event-modeling
|
|
1291
|
+
.description("copy the event-modeling skill bundle into .claude/skills/ (event-modeling, event-modeling-discover/-design/-implement/-conform/-review, event-modeling-shared)")
|
|
947
1292
|
.option("-f, --force", "overwrite an existing installation")
|
|
948
1293
|
.option("--no-agents-md", "skip writing/updating the AGENTS.md agent-contract section (on by default, MIL-129)")
|
|
949
1294
|
.action(async (opts) => {
|
|
950
|
-
const
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
|
|
1295
|
+
const srcRoot = packagedSkillsRoot();
|
|
1296
|
+
const destRoot = vendoredSkillsRoot(process.cwd());
|
|
1297
|
+
const anchor = join(destRoot, EM_SKILL_ANCHOR_DIR);
|
|
1298
|
+
if (existsSync(anchor) && !opts.force) {
|
|
1299
|
+
console.log(`skill already installed at ${anchor}`);
|
|
954
1300
|
console.log("re-run with --force to overwrite");
|
|
955
1301
|
}
|
|
956
1302
|
else {
|
|
957
|
-
await mkdir(
|
|
958
|
-
|
|
959
|
-
|
|
1303
|
+
await mkdir(destRoot, { recursive: true });
|
|
1304
|
+
for (const name of EM_ALL_SKILL_BUNDLE_DIRS) {
|
|
1305
|
+
await cp(join(srcRoot, name), join(destRoot, name), { recursive: true });
|
|
1306
|
+
}
|
|
1307
|
+
console.log(`installed event-modeling skill bundle → ${destRoot}`);
|
|
960
1308
|
console.log("in Claude Code, run /event-modeling to start a guided session");
|
|
961
1309
|
}
|
|
962
1310
|
if (opts.agentsMd !== false) {
|
|
@@ -965,22 +1313,26 @@ skill
|
|
|
965
1313
|
});
|
|
966
1314
|
skill
|
|
967
1315
|
.command("sync")
|
|
968
|
-
.description("update the vendored .claude/skills/event-modeling
|
|
969
|
-
"em package (overwrites unconditionally; local edits are never merged, MIL-93)")
|
|
1316
|
+
.description("update the vendored .claude/skills/ event-modeling skill bundle in [path] to match the " +
|
|
1317
|
+
"installed em package (overwrites unconditionally; local edits are never merged, MIL-93)")
|
|
970
1318
|
.argument("[path]", "consumer repo root", ".")
|
|
971
1319
|
.option("--no-agents-md", "skip writing/updating the AGENTS.md agent-contract section (on by default, MIL-129)")
|
|
972
1320
|
.action(async (path, opts) => {
|
|
973
|
-
const
|
|
974
|
-
const
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
1321
|
+
const packagedRoot = packagedSkillsRoot();
|
|
1322
|
+
const vendoredRoot = vendoredSkillsRoot(path);
|
|
1323
|
+
const bundlePlan = planSkillSyncBundle(packagedRoot, vendoredRoot, EM_ALL_SKILL_BUNDLE_DIRS);
|
|
1324
|
+
const totalChanges = bundlePlan.reduce((n, { plan }) => n + plan.changes.length, 0);
|
|
1325
|
+
const totalUnchanged = bundlePlan.reduce((n, { plan }) => n + plan.unchangedCount, 0);
|
|
1326
|
+
if (totalChanges === 0) {
|
|
1327
|
+
console.log(`up to date — ${vendoredRoot} already matches the installed skill bundle (${totalUnchanged} file(s))`);
|
|
978
1328
|
}
|
|
979
1329
|
else {
|
|
980
|
-
|
|
981
|
-
for (const
|
|
982
|
-
|
|
983
|
-
|
|
1330
|
+
applySkillSyncBundle(bundlePlan, packagedRoot, vendoredRoot);
|
|
1331
|
+
for (const { dirName, plan } of bundlePlan) {
|
|
1332
|
+
for (const c of plan.changes)
|
|
1333
|
+
console.log(`${c.kind}: ${dirName}/${c.relPath}`);
|
|
1334
|
+
}
|
|
1335
|
+
console.log(`synced ${vendoredRoot} — ${totalChanges} file(s) changed, ${totalUnchanged} unchanged`);
|
|
984
1336
|
}
|
|
985
1337
|
if (opts.agentsMd !== false) {
|
|
986
1338
|
console.log(agentsMdMessage(syncAgentsMd(resolve(path))));
|
|
@@ -988,16 +1340,16 @@ skill
|
|
|
988
1340
|
});
|
|
989
1341
|
skill
|
|
990
1342
|
.command("check")
|
|
991
|
-
.description("check the vendored .claude/skills/event-modeling
|
|
992
|
-
"installed em package; exits non-zero on any mismatch (CI-ready, MIL-93)")
|
|
1343
|
+
.description("check the vendored .claude/skills/ event-modeling skill bundle in [path] for drift against " +
|
|
1344
|
+
"the installed em package; exits non-zero on any mismatch (CI-ready, MIL-93)")
|
|
993
1345
|
.argument("[path]", "consumer repo root", ".")
|
|
994
1346
|
.option("--json", "print a JSON document instead of the text report (see docs/cli.md)")
|
|
995
1347
|
.action((path, opts) => {
|
|
996
|
-
const
|
|
997
|
-
const
|
|
998
|
-
const result =
|
|
1348
|
+
const packagedRoot = packagedSkillsRoot();
|
|
1349
|
+
const vendoredRoot = vendoredSkillsRoot(path);
|
|
1350
|
+
const result = checkSkillSyncBundle(packagedRoot, vendoredRoot, PKG_VERSION, EM_SKILL_DIR_NAMES, EM_SHARED_DIR_NAMES);
|
|
999
1351
|
if (opts.json) {
|
|
1000
|
-
process.stdout.write(buildSkillCheckJson(result,
|
|
1352
|
+
process.stdout.write(buildSkillCheckJson(result, vendoredRoot, PKG_VERSION) + "\n");
|
|
1001
1353
|
}
|
|
1002
1354
|
else {
|
|
1003
1355
|
for (const f of result.findings)
|
|
@@ -1009,6 +1361,76 @@ skill
|
|
|
1009
1361
|
if (!result.ok)
|
|
1010
1362
|
process.exitCode = 1;
|
|
1011
1363
|
});
|
|
1364
|
+
const ci = program.command("ci").description("scaffold/check the CI enforcement preset (MIL-166, see docs/ci.md)");
|
|
1365
|
+
ci.command("init")
|
|
1366
|
+
.description("install .github/workflows/em-ci.yml (PR gates: em validate, em slice index --check, em " +
|
|
1367
|
+
"coverage --strict, em ledger, em skill check, em glossary --fail-on-conflicts, plus a " +
|
|
1368
|
+
"push-triggered status-badge rebuild) and em-conform.yml (scheduled, advisory-only " +
|
|
1369
|
+
"conformance cadence) — same install discipline as `em skill install`: marker-delimited, " +
|
|
1370
|
+
"idempotent, --check for CI self-verification (MIL-166, see docs/ci.md)")
|
|
1371
|
+
.argument("<model>", "anchor .em file the coverage/ledger/slice-index/status steps point at")
|
|
1372
|
+
.option("--tests <dir>", "test directory the coverage/status steps scan for INV-* citations", "test")
|
|
1373
|
+
.option("-f, --force", "replace an existing workflow file that has no GENERATED markers")
|
|
1374
|
+
.option("--check", "verify both files match the current preset; exit non-zero on drift without writing (CI)")
|
|
1375
|
+
.action((model, opts) => {
|
|
1376
|
+
const unsafe = findUnsafeCiInitArg(model) ?? findUnsafeCiInitArg(opts.tests);
|
|
1377
|
+
if (unsafe) {
|
|
1378
|
+
console.error(`em ci init: argument must not contain '"', '\`', '$', or a newline (breaks the ` +
|
|
1379
|
+
`generated workflow's shell steps): ${unsafe}`);
|
|
1380
|
+
process.exit(1);
|
|
1381
|
+
}
|
|
1382
|
+
const repoRoot = process.cwd();
|
|
1383
|
+
const ciPath = ciWorkflowPath(repoRoot);
|
|
1384
|
+
const conformPath = conformWorkflowPath(repoRoot);
|
|
1385
|
+
const files = [
|
|
1386
|
+
[
|
|
1387
|
+
ciPath,
|
|
1388
|
+
planCiFile(ciPath, buildCiWorkflowFile(model, opts.tests, PKG_VERSION), ciManagedBody(model, opts.tests), CI_WORKFLOW_MARKER, !!opts.force),
|
|
1389
|
+
],
|
|
1390
|
+
[
|
|
1391
|
+
conformPath,
|
|
1392
|
+
planCiFile(conformPath, buildConformWorkflowFile(model, PKG_VERSION), conformManagedBody(model), CONFORM_WORKFLOW_MARKER, !!opts.force),
|
|
1393
|
+
],
|
|
1394
|
+
];
|
|
1395
|
+
if (opts.check) {
|
|
1396
|
+
let drift = false;
|
|
1397
|
+
for (const [path, status] of files) {
|
|
1398
|
+
if (status.kind === "create") {
|
|
1399
|
+
console.log(`missing: ${path} — run \`em ci init ${model}\` to create it`);
|
|
1400
|
+
drift = true;
|
|
1401
|
+
}
|
|
1402
|
+
else if (status.kind === "missing-markers") {
|
|
1403
|
+
console.log(`can't verify: ${path} exists without GENERATED markers (re-run with --force to replace it)`);
|
|
1404
|
+
drift = true;
|
|
1405
|
+
}
|
|
1406
|
+
else if (status.kind === "stale") {
|
|
1407
|
+
console.log(`stale: ${path} — run \`em ci init ${model}\` and commit the result`);
|
|
1408
|
+
drift = true;
|
|
1409
|
+
}
|
|
1410
|
+
else {
|
|
1411
|
+
console.log(`ok: ${path}`);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
console.log(drift ? "em ci init --check: drift found" : "ok — both workflow files match the current preset");
|
|
1415
|
+
if (drift)
|
|
1416
|
+
process.exitCode = 1;
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
mkdirSync(dirname(ciPath), { recursive: true });
|
|
1420
|
+
for (const [path, status] of files) {
|
|
1421
|
+
applyCiFile(path, status);
|
|
1422
|
+
if (status.kind === "create")
|
|
1423
|
+
console.log(`installed ${path}`);
|
|
1424
|
+
else if (status.kind === "stale")
|
|
1425
|
+
console.log(`updated ${path}`);
|
|
1426
|
+
else if (status.kind === "would-replace")
|
|
1427
|
+
console.log(`replaced ${path} (--force)`);
|
|
1428
|
+
else if (status.kind === "missing-markers")
|
|
1429
|
+
console.log(`${path} already exists — re-run with --force to overwrite`);
|
|
1430
|
+
else
|
|
1431
|
+
console.log(`${path} already up to date`);
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1012
1434
|
// Exported so dev tooling (e.g. scripts/generate-skill-docs.ts) can introspect the registered
|
|
1013
1435
|
// commands/options without triggering a real CLI run. Guarded the same way Node's CJS
|
|
1014
1436
|
// `require.main === module` used to: only actually parse argv when this file is the process's
|
|
@@ -1050,6 +1472,25 @@ function compileFile(file, opts = {},
|
|
|
1050
1472
|
throw e;
|
|
1051
1473
|
}
|
|
1052
1474
|
}
|
|
1475
|
+
/** Lineage-ref resolution (MIL-84), frontmatter-coherence (MIL-85), note-binding mismatches
|
|
1476
|
+
* (MIL-126), doc↔model consistency (MIL-124), and orphaned slice docs (MIL-183) are `em
|
|
1477
|
+
* validate`'s fs-aware rules — every other diagnostic in `diagnostics` (from `compileFile`) is a
|
|
1478
|
+
* pure function of the .em source. All five read `slices/*.md` alongside the model. Doc↔model
|
|
1479
|
+
* consistency and orphaned-slice-doc are deliberately validate-only (unlike note-binding, which
|
|
1480
|
+
* `em render` also folds in) — both are conform-phase concerns, not something every render needs
|
|
1481
|
+
* to recheck. Shared by `em validate` and `em state log-usage` (MIL-161) — the latter needs the
|
|
1482
|
+
* exact same diagnostic set `em validate --json` would report, since its whole job is logging
|
|
1483
|
+
* which categories fired. */
|
|
1484
|
+
function computeAllDiagnostics(file, model, refs, diagnostics) {
|
|
1485
|
+
return [
|
|
1486
|
+
...diagnostics,
|
|
1487
|
+
...validateLineage(model, refs, dirname(file)),
|
|
1488
|
+
...validateFrontmatterCoherence(model, refs, dirname(file)),
|
|
1489
|
+
...validateNoteBindings(model, refs, dirname(file)),
|
|
1490
|
+
...validateDocModelConsistency(model, refs, dirname(file)),
|
|
1491
|
+
...validateOrphanedSliceDocs(model, refs, dirname(file)),
|
|
1492
|
+
];
|
|
1493
|
+
}
|
|
1053
1494
|
/** Read a file's text, or exit with a clear error — shared by `em diff` (both forms) and
|
|
1054
1495
|
* `em glossary`, which each read one or more plain `.em` files off disk before compiling. */
|
|
1055
1496
|
function readFileOrExit(file) {
|
|
@@ -1083,76 +1524,6 @@ function readAtRevision(file, rev) {
|
|
|
1083
1524
|
}
|
|
1084
1525
|
return result.content;
|
|
1085
1526
|
}
|
|
1086
|
-
/**
|
|
1087
|
-
* Build the `em changelog` markdown document for an already-resolved commit
|
|
1088
|
-
* list (oldest -> newest). Compiles every revision once; per-revision
|
|
1089
|
-
* warnings are deliberately never printed (historical revisions can be noisy)
|
|
1090
|
-
* — only a compile *failure* (parse error or validation error, same
|
|
1091
|
-
* threshold `em diff` uses) surfaces, as that entry's error note, never a
|
|
1092
|
-
* crash. Diffs are computed against the previous *parseable* revision, so a
|
|
1093
|
-
* single bad revision in the middle of the walk doesn't break every entry
|
|
1094
|
-
* after it. Content is read at each commit's own path (`readFileAtCommit`),
|
|
1095
|
-
* so the walk survives renames.
|
|
1096
|
-
*/
|
|
1097
|
-
function buildChangelogDoc(file, repoRoot, commits) {
|
|
1098
|
-
const models = [];
|
|
1099
|
-
const refsList = [];
|
|
1100
|
-
const errors = [];
|
|
1101
|
-
for (const c of commits) {
|
|
1102
|
-
const rev = readFileAtCommit(repoRoot, c);
|
|
1103
|
-
if (!rev.ok) {
|
|
1104
|
-
models.push(null);
|
|
1105
|
-
refsList.push(null);
|
|
1106
|
-
errors.push(rev.message);
|
|
1107
|
-
continue;
|
|
1108
|
-
}
|
|
1109
|
-
try {
|
|
1110
|
-
const { model, refs, diagnostics } = compile(rev.content);
|
|
1111
|
-
if (hasErrors(diagnostics)) {
|
|
1112
|
-
models.push(null);
|
|
1113
|
-
refsList.push(null);
|
|
1114
|
-
errors.push(`validation errors at ${c.shortHash} — fix with \`em validate\` at that revision`);
|
|
1115
|
-
}
|
|
1116
|
-
else {
|
|
1117
|
-
models.push(model);
|
|
1118
|
-
refsList.push(refs);
|
|
1119
|
-
errors.push(null);
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
catch (e) {
|
|
1123
|
-
models.push(null);
|
|
1124
|
-
refsList.push(null);
|
|
1125
|
-
errors.push(e instanceof ParseError ? `parse error: ${e.message}` : e instanceof Error ? e.message : String(e));
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
const entries = [];
|
|
1129
|
-
let prevParseable = -1;
|
|
1130
|
-
commits.forEach((c, i) => {
|
|
1131
|
-
const base = { shortHash: c.shortHash, date: c.date, subject: c.subject };
|
|
1132
|
-
if (i === 0) {
|
|
1133
|
-
entries.push({ ...base, diff: null });
|
|
1134
|
-
}
|
|
1135
|
-
else if (!models[i]) {
|
|
1136
|
-
entries.push({ ...base, diff: null, error: errors[i] });
|
|
1137
|
-
}
|
|
1138
|
-
else if (prevParseable === -1) {
|
|
1139
|
-
entries.push({ ...base, diff: null, error: `no earlier parseable revision to diff against (${commits[0].shortHash}: ${errors[0]})` });
|
|
1140
|
-
}
|
|
1141
|
-
else {
|
|
1142
|
-
entries.push({
|
|
1143
|
-
...base,
|
|
1144
|
-
diff: diffModels(models[prevParseable], models[i], refsList[prevParseable], refsList[i]),
|
|
1145
|
-
});
|
|
1146
|
-
}
|
|
1147
|
-
if (models[i])
|
|
1148
|
-
prevParseable = i;
|
|
1149
|
-
});
|
|
1150
|
-
const intro = models[0] ? { slices: models[0].slices.length, elements: models[0].elements.length } : null;
|
|
1151
|
-
const introError = models[0] ? undefined : (errors[0] ?? undefined);
|
|
1152
|
-
const stateFile = join(dirname(file), STATE_FILE_NAME);
|
|
1153
|
-
const decisions = existsSync(stateFile) ? parseDecisionsLog(readFileSync(stateFile, "utf8")) : [];
|
|
1154
|
-
return buildChangelog(entries, decisions, { file, intro, introError });
|
|
1155
|
-
}
|
|
1156
1527
|
/** Shared body for both `em diff` forms: compile both sides, gate on errors, print, exit-code. */
|
|
1157
1528
|
function runDiff(oldSource, oldLabel, newSource, newLabel, exitCode, json, lineage) {
|
|
1158
1529
|
const oldResult = compileSource(oldSource, oldLabel);
|