@milehimikey/em 1.8.0 → 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.
Files changed (127) hide show
  1. package/.claude/skills/event-modeling/SKILL.md +58 -404
  2. package/.claude/skills/event-modeling-conform/SKILL.md +77 -0
  3. package/.claude/skills/{event-modeling → event-modeling-conform}/reference/conform.md +39 -13
  4. package/.claude/skills/event-modeling-design/SKILL.md +139 -0
  5. package/.claude/skills/event-modeling-discover/SKILL.md +89 -0
  6. package/.claude/skills/{event-modeling → event-modeling-discover}/reference/extract.md +3 -2
  7. package/.claude/skills/event-modeling-implement/SKILL.md +40 -0
  8. package/.claude/skills/{event-modeling → event-modeling-implement}/reference/implement.md +3 -2
  9. package/.claude/skills/event-modeling-review/SKILL.md +58 -0
  10. package/.claude/skills/{event-modeling → event-modeling-shared}/reference/em-dsl.md +128 -84
  11. package/.claude/skills/{event-modeling → event-modeling-shared}/reference/methodology.md +6 -5
  12. package/.claude/skills/event-modeling-shared/reference/operating-principles.md +147 -0
  13. package/.claude/skills/{event-modeling → event-modeling-shared}/templates/model-readme.md +2 -2
  14. package/.claude/skills/{event-modeling → event-modeling-shared}/templates/slice.md +51 -8
  15. package/README.md +24 -4
  16. package/dist/catalog/build.js +9 -0
  17. package/dist/catalog/build.js.map +1 -1
  18. package/dist/catalog/docJoin.d.ts +15 -0
  19. package/dist/catalog/docJoin.js +8 -0
  20. package/dist/catalog/docJoin.js.map +1 -1
  21. package/dist/catalog/modelCollisionValidate.d.ts +22 -0
  22. package/dist/catalog/modelCollisionValidate.js +67 -0
  23. package/dist/catalog/modelCollisionValidate.js.map +1 -0
  24. package/dist/catalog/orphanedSliceDocValidate.d.ts +11 -0
  25. package/dist/catalog/orphanedSliceDocValidate.js +98 -0
  26. package/dist/catalog/orphanedSliceDocValidate.js.map +1 -0
  27. package/dist/catalog/sliceDoc.d.ts +15 -0
  28. package/dist/catalog/sliceDoc.js +18 -0
  29. package/dist/catalog/sliceDoc.js.map +1 -1
  30. package/dist/cli/changelogBuild.d.ts +12 -0
  31. package/dist/cli/changelogBuild.js +89 -0
  32. package/dist/cli/changelogBuild.js.map +1 -0
  33. package/dist/cli/ciInit.d.ts +42 -0
  34. package/dist/cli/ciInit.js +298 -0
  35. package/dist/cli/ciInit.js.map +1 -0
  36. package/dist/cli/conformSupersede.d.ts +32 -0
  37. package/dist/cli/conformSupersede.js +102 -0
  38. package/dist/cli/conformSupersede.js.map +1 -0
  39. package/dist/cli/coverage.d.ts +17 -10
  40. package/dist/cli/coverage.js +61 -16
  41. package/dist/cli/coverage.js.map +1 -1
  42. package/dist/cli/frontmatterSurgery.d.ts +28 -0
  43. package/dist/cli/frontmatterSurgery.js +66 -0
  44. package/dist/cli/frontmatterSurgery.js.map +1 -0
  45. package/dist/cli/markImplemented.js +6 -45
  46. package/dist/cli/markImplemented.js.map +1 -1
  47. package/dist/cli/ratify.d.ts +41 -0
  48. package/dist/cli/ratify.js +151 -0
  49. package/dist/cli/ratify.js.map +1 -0
  50. package/dist/cli/reratify.d.ts +33 -0
  51. package/dist/cli/reratify.js +137 -0
  52. package/dist/cli/reratify.js.map +1 -0
  53. package/dist/cli/skillCheck.d.ts +6 -0
  54. package/dist/cli/skillCheck.js +46 -0
  55. package/dist/cli/skillCheck.js.map +1 -1
  56. package/dist/cli/skillDirs.d.ts +15 -0
  57. package/dist/cli/skillDirs.js +34 -0
  58. package/dist/cli/skillDirs.js.map +1 -0
  59. package/dist/cli/skillSync.d.ts +10 -0
  60. package/dist/cli/skillSync.js +15 -0
  61. package/dist/cli/skillSync.js.map +1 -1
  62. package/dist/cli/sliceIndex.d.ts +3 -0
  63. package/dist/cli/sliceIndex.js +6 -2
  64. package/dist/cli/sliceIndex.js.map +1 -1
  65. package/dist/cli/sliceLink.d.ts +52 -0
  66. package/dist/cli/sliceLink.js +152 -0
  67. package/dist/cli/sliceLink.js.map +1 -0
  68. package/dist/cli/status.d.ts +261 -0
  69. package/dist/cli/status.js +536 -0
  70. package/dist/cli/status.js.map +1 -0
  71. package/dist/cli/usageLog.d.ts +75 -0
  72. package/dist/cli/usageLog.js +193 -0
  73. package/dist/cli/usageLog.js.map +1 -0
  74. package/dist/cli.js +501 -130
  75. package/dist/cli.js.map +1 -1
  76. package/dist/emit/diffJson.d.ts +9 -1
  77. package/dist/emit/diffJson.js +44 -1
  78. package/dist/emit/diffJson.js.map +1 -1
  79. package/dist/emit/freshnessJson.d.ts +6 -0
  80. package/dist/emit/freshnessJson.js +24 -0
  81. package/dist/emit/freshnessJson.js.map +1 -0
  82. package/dist/emit/json.d.ts +67 -8
  83. package/dist/emit/json.js +19 -3
  84. package/dist/emit/json.js.map +1 -1
  85. package/dist/emit/statusJson.d.ts +7 -0
  86. package/dist/emit/statusJson.js +39 -0
  87. package/dist/emit/statusJson.js.map +1 -0
  88. package/dist/emit/typespec.d.ts +26 -0
  89. package/dist/emit/typespec.js +356 -0
  90. package/dist/emit/typespec.js.map +1 -0
  91. package/dist/emit/usageReportJson.d.ts +3 -0
  92. package/dist/emit/usageReportJson.js +23 -0
  93. package/dist/emit/usageReportJson.js.map +1 -0
  94. package/dist/mcp/server.d.ts +1 -1
  95. package/dist/mcp/server.js +301 -11
  96. package/dist/mcp/server.js.map +1 -1
  97. package/dist/model/rules.d.ts +13 -0
  98. package/dist/model/rules.js +20 -0
  99. package/dist/model/rules.js.map +1 -1
  100. package/dist/model/validate.js +9 -1
  101. package/dist/model/validate.js.map +1 -1
  102. package/dist/parser/ast.d.ts +6 -0
  103. package/dist/parser/parser.js +20 -6
  104. package/dist/parser/parser.js.map +1 -1
  105. package/dist/render/drawNotes.d.ts +11 -1
  106. package/dist/render/drawNotes.js +20 -2
  107. package/dist/render/drawNotes.js.map +1 -1
  108. package/dist/render/render.js +31 -6
  109. package/dist/render/render.js.map +1 -1
  110. package/dist/render/serve.js +4 -0
  111. package/dist/render/serve.js.map +1 -1
  112. package/dist/render/sliceOverlay.d.ts +7 -2
  113. package/dist/render/sliceOverlay.js +19 -7
  114. package/dist/render/sliceOverlay.js.map +1 -1
  115. package/dist/render/sliceStatus.d.ts +7 -3
  116. package/dist/render/sliceStatus.js +21 -14
  117. package/dist/render/sliceStatus.js.map +1 -1
  118. package/dist/render/viewerHtml.js +1 -1
  119. package/dist/render/viewerHtml.js.map +1 -1
  120. package/dist/templates.d.ts +2 -2
  121. package/dist/templates.js +5 -5
  122. package/dist/util/markers.d.ts +16 -4
  123. package/dist/util/markers.js +23 -10
  124. package/dist/util/markers.js.map +1 -1
  125. package/package.json +2 -1
  126. /package/.claude/skills/{event-modeling → event-modeling-shared}/templates/conformance-report.md +0 -0
  127. /package/.claude/skills/{event-modeling → event-modeling-shared}/templates/state.md +0 -0
@@ -0,0 +1,89 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // `em changelog`'s document builder (MIL-100), extracted out of cli.ts (MIL-167) so the MCP
3
+ // server's `changelog` tool can call the exact same function the CLI command does — same
4
+ // "one builder, two callers" pattern as buildDiffJson/buildGlossaryJson, just markdown instead
5
+ // of JSON (the CLI command itself has no `--json` flag; the document IS the markdown text).
6
+ //
7
+ // Already pure enough to share as-is: no process.exit, no console — a per-revision compile
8
+ // failure becomes that entry's `error` field, never a crash (see the docstring below). The only
9
+ // impure bit (`readFileAtCommit`, a `git show`) is injected in, same convention as
10
+ // changelog-git.ts/diff-inputs.ts's GitRunner.
11
+ import { dirname, join } from "node:path";
12
+ import { existsSync, readFileSync } from "node:fs";
13
+ import { compile } from "../pipeline.js";
14
+ import { ParseError } from "../parser/parser.js";
15
+ import { hasErrors } from "../model/validate.js";
16
+ import { diffModels } from "../model/diff.js";
17
+ import { buildChangelog, parseDecisionsLog } from "../emit/changelog.js";
18
+ import { readFileAtCommit } from "./changelog-git.js";
19
+ import { STATE_FILE_NAME } from "./stateFile.js";
20
+ /**
21
+ * Build the `em changelog` markdown document for an already-resolved commit list
22
+ * (oldest -> newest, see `listModelCommits`). Compiles every revision once; per-revision
23
+ * warnings are deliberately never surfaced (historical revisions can be noisy) — only a compile
24
+ * *failure* (parse error or validation error, same threshold `em diff` uses) surfaces, as that
25
+ * entry's error note, never a crash. Diffs are computed against the previous *parseable*
26
+ * revision, so a single bad revision in the middle of the walk doesn't break every entry after
27
+ * it. Content is read at each commit's own path (`readFileAtCommit`), so the walk survives
28
+ * renames.
29
+ */
30
+ export function buildChangelogDoc(file, repoRoot, commits) {
31
+ const models = [];
32
+ const refsList = [];
33
+ const errors = [];
34
+ for (const c of commits) {
35
+ const rev = readFileAtCommit(repoRoot, c);
36
+ if (!rev.ok) {
37
+ models.push(null);
38
+ refsList.push(null);
39
+ errors.push(rev.message);
40
+ continue;
41
+ }
42
+ try {
43
+ const { model, refs, diagnostics } = compile(rev.content);
44
+ if (hasErrors(diagnostics)) {
45
+ models.push(null);
46
+ refsList.push(null);
47
+ errors.push(`validation errors at ${c.shortHash} — fix with \`em validate\` at that revision`);
48
+ }
49
+ else {
50
+ models.push(model);
51
+ refsList.push(refs);
52
+ errors.push(null);
53
+ }
54
+ }
55
+ catch (e) {
56
+ models.push(null);
57
+ refsList.push(null);
58
+ errors.push(e instanceof ParseError ? `parse error: ${e.message}` : e instanceof Error ? e.message : String(e));
59
+ }
60
+ }
61
+ const entries = [];
62
+ let prevParseable = -1;
63
+ commits.forEach((c, i) => {
64
+ const base = { shortHash: c.shortHash, date: c.date, subject: c.subject };
65
+ if (i === 0) {
66
+ entries.push({ ...base, diff: null });
67
+ }
68
+ else if (!models[i]) {
69
+ entries.push({ ...base, diff: null, error: errors[i] });
70
+ }
71
+ else if (prevParseable === -1) {
72
+ entries.push({ ...base, diff: null, error: `no earlier parseable revision to diff against (${commits[0].shortHash}: ${errors[0]})` });
73
+ }
74
+ else {
75
+ entries.push({
76
+ ...base,
77
+ diff: diffModels(models[prevParseable], models[i], refsList[prevParseable], refsList[i]),
78
+ });
79
+ }
80
+ if (models[i])
81
+ prevParseable = i;
82
+ });
83
+ const intro = models[0] ? { slices: models[0].slices.length, elements: models[0].elements.length } : null;
84
+ const introError = models[0] ? undefined : (errors[0] ?? undefined);
85
+ const stateFile = join(dirname(file), STATE_FILE_NAME);
86
+ const decisions = existsSync(stateFile) ? parseDecisionsLog(readFileSync(stateFile, "utf8")) : [];
87
+ return buildChangelog(entries, decisions, { file, intro, introError });
88
+ }
89
+ //# sourceMappingURL=changelogBuild.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"changelogBuild.js","sourceRoot":"","sources":["../../src/cli/changelogBuild.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,4FAA4F;AAC5F,yFAAyF;AACzF,+FAA+F;AAC/F,4FAA4F;AAC5F,EAAE;AACF,2FAA2F;AAC3F,gGAAgG;AAChG,mFAAmF;AACnF,+CAA+C;AAE/C,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAGjD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAkC,MAAM,sBAAsB,CAAC;AACzG,OAAO,EAAE,gBAAgB,EAAc,MAAM,oBAAoB,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAEjD;;;;;;;;;GASG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,QAAgB,EAAE,OAAqB;IACrF,MAAM,MAAM,GAA+B,EAAE,CAAC;IAC9C,MAAM,QAAQ,GAA0B,EAAE,CAAC;IAC3C,MAAM,MAAM,GAAsB,EAAE,CAAC;IAErC,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,GAAG,GAAG,gBAAgB,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;QAC1C,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAClB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACpB,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACzB,SAAS;QACX,CAAC;QACD,IAAI,CAAC;YACH,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YAC1D,IAAI,SAAS,CAAC,WAAW,CAAC,EAAE,CAAC;gBAC3B,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAClB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACpB,MAAM,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC,SAAS,8CAA8C,CAAC,CAAC;YACjG,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACnB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACpB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACpB,CAAC;QACH,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAClB,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACpB,MAAM,CAAC,IAAI,CAAC,CAAC,YAAY,UAAU,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAClH,CAAC;IACH,CAAC;IAED,MAAM,OAAO,GAAqB,EAAE,CAAC;IACrC,IAAI,aAAa,GAAG,CAAC,CAAC,CAAC;IACvB,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACvB,MAAM,IAAI,GAAG,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;QAC1E,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACZ,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACxC,CAAC;aAAM,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;YACtB,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAE,EAAE,CAAC,CAAC;QAC3D,CAAC;aAAM,IAAI,aAAa,KAAK,CAAC,CAAC,EAAE,CAAC;YAChC,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,kDAAkD,OAAO,CAAC,CAAC,CAAC,CAAC,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;QACxI,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,IAAI,CAAC;gBACX,GAAG,IAAI;gBACP,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC,aAAa,CAAE,EAAE,MAAM,CAAC,CAAC,CAAE,EAAE,QAAQ,CAAC,aAAa,CAAE,EAAE,QAAQ,CAAC,CAAC,CAAE,CAAC;aAC7F,CAAC,CAAC;QACL,CAAC;QACD,IAAI,MAAM,CAAC,CAAC,CAAC;YAAE,aAAa,GAAG,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IAEH,MAAM,KAAK,GAA0B,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACjI,MAAM,UAAU,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC,CAAC;IAEpE,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,eAAe,CAAC,CAAC;IACvD,MAAM,SAAS,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAElG,OAAO,cAAc,CAAC,OAAO,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC;AACzE,CAAC"}
@@ -0,0 +1,42 @@
1
+ export declare const CI_WORKFLOW_MARKER = "em-ci";
2
+ export declare const CONFORM_WORKFLOW_MARKER = "em-conform";
3
+ export declare function ciWorkflowPath(repoRoot: string): string;
4
+ export declare function conformWorkflowPath(repoRoot: string): string;
5
+ export declare function findUnsafeCiInitArg(value: string): string | null;
6
+ /** The managed block only (no header/`on:`/`jobs:` scaffolding) — what gets written between
7
+ * the marker pair, both for a fresh file and to patch an existing marked one in place. */
8
+ export declare function ciManagedBody(model: string, testsDir: string): string;
9
+ export declare function buildCiWorkflowFile(model: string, testsDir: string, emVersion: string): string;
10
+ /** The managed block only (no header/`on:`/`permissions:`/`jobs:` scaffolding) — what gets
11
+ * written between the marker pair, both for a fresh file and to patch an existing marked one
12
+ * in place. */
13
+ export declare function conformManagedBody(model: string): string;
14
+ export declare function buildConformWorkflowFile(model: string, emVersion: string): string;
15
+ export type CiFileStatus = {
16
+ kind: "create";
17
+ content: string;
18
+ } | {
19
+ kind: "ok";
20
+ content: string;
21
+ } | {
22
+ kind: "stale";
23
+ content: string;
24
+ current: string;
25
+ } | {
26
+ kind: "missing-markers";
27
+ } | {
28
+ kind: "would-replace";
29
+ content: string;
30
+ };
31
+ /**
32
+ * Decide what to do with one generated file: create it if missing, patch the managed block if
33
+ * the marker pair is present, or (without `force`) leave an existing unmarked file alone. Pure
34
+ * — never touches disk; `applyCiFile` below does the actual write.
35
+ *
36
+ * `generated` is the full from-scratch file content (header + `on:`/`jobs:` scaffolding +
37
+ * marker-wrapped body) — written verbatim for `create`/`would-replace`. `managedBody` is just
38
+ * the text between the markers, reused to patch an existing marked file in place without
39
+ * touching whatever a repo added around it.
40
+ */
41
+ export declare function planCiFile(path: string, generated: string, managedBody: string, markerName: string, force: boolean): CiFileStatus;
42
+ export declare function applyCiFile(path: string, status: CiFileStatus): void;
@@ -0,0 +1,298 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // `em ci init` (MIL-166): scaffolds the CI enforcement preset docs/ci.md describes as a
3
+ // copy-paste cookbook into two installed, plain GitHub Actions workflow files — converting
4
+ // "machines check this" from available (a recipe someone has to know to copy) to default (a
5
+ // command that installs it). Every check the preset wires already exists as its own `em`
6
+ // command (docs/ci.md); this module only owns the YAML wiring around them.
7
+ //
8
+ // Same install discipline as `em skill install`/`em slice index` (see their own modules):
9
+ // - marker-delimited (src/util/markers.ts's "hash" style, since YAML has no `<!-- -->`
10
+ // comment syntax) — the managed job block sits between `# GENERATED:em-ci:start` /
11
+ // `# GENERATED:em-ci:end`, so a repo can add its own jobs above or below the markers, at
12
+ // the same indent under `jobs:`, and keep them across a future re-run.
13
+ // - idempotent — re-running with nothing changed leaves both files byte-identical; a file
14
+ // that already exists without our markers is left alone unless `--force` says to replace
15
+ // it wholesale (matching `em skill install`: existing-and-no-force is reported, not an
16
+ // error).
17
+ // - `--check` for CI self-verification — never writes; reports whether the managed block in
18
+ // each file still matches what the current preset would generate (missing / no-markers /
19
+ // stale / ok), CI-ready the same way `em slice index --check` is.
20
+ //
21
+ // The generated files are handed to the repo, not owned by `em` forever: past the initial
22
+ // `em ci init`, edit them freely (see each file's own header comment). `--check` is there for a
23
+ // team that would rather pin the vanilla preset and gate on drift — opt-in, the same posture
24
+ // docs/ci.md already recommends for `em skill check` ("if you'd rather pin ... add `em skill
25
+ // check` as its own gate instead of silently overwriting").
26
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
27
+ import { dirname, join } from "node:path";
28
+ import { applyMarker, markerPair } from "../util/markers.js";
29
+ export const CI_WORKFLOW_MARKER = "em-ci";
30
+ export const CONFORM_WORKFLOW_MARKER = "em-conform";
31
+ const CI_WORKFLOW_RELPATH = join(".github", "workflows", "em-ci.yml");
32
+ const CONFORM_WORKFLOW_RELPATH = join(".github", "workflows", "em-conform.yml");
33
+ export function ciWorkflowPath(repoRoot) {
34
+ return join(repoRoot, CI_WORKFLOW_RELPATH);
35
+ }
36
+ export function conformWorkflowPath(repoRoot) {
37
+ return join(repoRoot, CONFORM_WORKFLOW_RELPATH);
38
+ }
39
+ /** Characters that would break out of the double-quoted shell strings the generated workflow
40
+ * embeds `model`/`testsDir` into (`"model.em"`, `"test"`) — same validate-the-input-not-the-
41
+ * output posture as `em scaffold`'s name check (src/cli.ts). */
42
+ const UNSAFE_ARG_CHARS = /["`$\n]/;
43
+ export function findUnsafeCiInitArg(value) {
44
+ return UNSAFE_ARG_CHARS.test(value) ? value : null;
45
+ }
46
+ // ---- em-ci.yml (PR gates + push-triggered badge rebuild) ----
47
+ /** The managed block only (no header/`on:`/`jobs:` scaffolding) — what gets written between
48
+ * the marker pair, both for a fresh file and to patch an existing marked one in place. */
49
+ export function ciManagedBody(model, testsDir) {
50
+ return ` validate:
51
+ name: em validate (changed .em files)
52
+ if: github.event_name == 'pull_request'
53
+ runs-on: ubuntu-latest
54
+ steps:
55
+ - uses: actions/checkout@v4
56
+ with:
57
+ fetch-depth: 0
58
+ - uses: actions/setup-node@v4
59
+ with:
60
+ node-version: 20
61
+ - name: Validate changed models
62
+ run: |
63
+ set -e
64
+ base="\${{ github.event.pull_request.base.sha }}"
65
+ head="\${{ github.event.pull_request.head.sha }}"
66
+ changed=$(git diff --name-only "$base" "$head" -- '*.em')
67
+ if [ -z "$changed" ]; then
68
+ echo "no .em files changed"
69
+ exit 0
70
+ fi
71
+ status=0
72
+ for f in $changed; do
73
+ echo "::group::em validate $f"
74
+ npx @milehimikey/em validate "$f" || status=1
75
+ npx @milehimikey/em validate "$f" --list-issues
76
+ echo "::endgroup::"
77
+ done
78
+ exit $status
79
+
80
+ slice-index:
81
+ name: "em slice index --check (README table drift)"
82
+ if: github.event_name == 'pull_request'
83
+ runs-on: ubuntu-latest
84
+ steps:
85
+ - uses: actions/checkout@v4
86
+ - uses: actions/setup-node@v4
87
+ with:
88
+ node-version: 20
89
+ - name: Check README Slices table is current
90
+ run: npx @milehimikey/em slice index "${model}" --check
91
+
92
+ coverage:
93
+ name: "em coverage --strict (invariant citations)"
94
+ if: github.event_name == 'pull_request'
95
+ runs-on: ubuntu-latest
96
+ steps:
97
+ - uses: actions/checkout@v4
98
+ - uses: actions/setup-node@v4
99
+ with:
100
+ node-version: 20
101
+ - name: Check invariant test coverage
102
+ run: npx @milehimikey/em coverage "${model}" --tests "${testsDir}" --strict
103
+
104
+ ledger:
105
+ name: em ledger (slice doc version/content agreement)
106
+ if: github.event_name == 'pull_request'
107
+ runs-on: ubuntu-latest
108
+ steps:
109
+ - uses: actions/checkout@v4
110
+ with:
111
+ fetch-depth: 0
112
+ - uses: actions/setup-node@v4
113
+ with:
114
+ node-version: 20
115
+ - name: Check slice doc version/content agreement
116
+ run: npx @milehimikey/em ledger "${model}" --from "\${{ github.event.pull_request.base.sha }}"
117
+
118
+ skill-check:
119
+ name: em skill check (vendored skill drift)
120
+ if: github.event_name == 'pull_request'
121
+ runs-on: ubuntu-latest
122
+ steps:
123
+ - uses: actions/checkout@v4
124
+ - uses: actions/setup-node@v4
125
+ with:
126
+ node-version: 20
127
+ - name: Check vendored skill matches installed em
128
+ run: |
129
+ if [ -d .claude/skills/event-modeling ]; then
130
+ npx @milehimikey/em skill check
131
+ else
132
+ echo "no vendored skill at .claude/skills/event-modeling — skipping (run \`em skill install\` to opt in)"
133
+ fi
134
+
135
+ glossary:
136
+ name: "em glossary --fail-on-conflicts (cross-model vocabulary)"
137
+ if: github.event_name == 'pull_request'
138
+ runs-on: ubuntu-latest
139
+ steps:
140
+ - uses: actions/checkout@v4
141
+ - uses: actions/setup-node@v4
142
+ with:
143
+ node-version: 20
144
+ - name: Check glossary consistency across models
145
+ run: npx @milehimikey/em glossary $(git ls-files '*.em') --fail-on-conflicts
146
+
147
+ status-badge:
148
+ name: rebuild status badge (advisory — publish only, never a gate)
149
+ if: github.event_name == 'push'
150
+ runs-on: ubuntu-latest
151
+ permissions:
152
+ contents: write
153
+ steps:
154
+ - uses: actions/checkout@v4
155
+ - uses: actions/setup-node@v4
156
+ with:
157
+ node-version: 20
158
+ - name: Rebuild status badge
159
+ run: npx @milehimikey/em status "${model}" --tests "${testsDir}" --badge -o status-badge.svg
160
+ - name: Commit badge if changed
161
+ run: |
162
+ if git diff --quiet -- status-badge.svg; then
163
+ echo "status-badge.svg unchanged"
164
+ exit 0
165
+ fi
166
+ git config user.name "em-ci"
167
+ git config user.email "em-ci@users.noreply.github.com"
168
+ git add status-badge.svg
169
+ git commit -m "em ci: rebuild status badge [skip ci]"
170
+ git push`;
171
+ }
172
+ function ciWorkflowHeader(model, emVersion) {
173
+ return `# Generated once by \`em ci init ${model}\` (em ${emVersion}) — docs/ci.md
174
+ #
175
+ # This file is yours from here: edit it, add jobs, remove ones you don't want. The content
176
+ # between the GENERATED:${CI_WORKFLOW_MARKER} markers below is what a future \`em ci init\`
177
+ # (e.g. after upgrading em) refreshes in place — add your own jobs above or below the markers,
178
+ # at the same indent under \`jobs:\`, and they survive a re-run untouched. Every gate here fails
179
+ # the PR the same way any other required check does; the status-badge job only ever publishes.
180
+ # \`em ci init ${model} --check\` reports drift in the managed block — advisory unless you wire
181
+ # it into a gate yourself (docs/ci.md).
182
+ name: em ci
183
+
184
+ on:
185
+ pull_request:
186
+ paths:
187
+ - "**/*.em"
188
+ - "**/slices/**"
189
+ - "**/README.md"
190
+ push:
191
+ branches: [main]
192
+
193
+ jobs:
194
+ `;
195
+ }
196
+ // Marker comment lines sit at column 0, not indented to the 2-space job-key column: YAML
197
+ // comments are whitespace-agnostic (indentation only matters for actual content nodes), and
198
+ // `applyMarker`'s regex only ever recognizes the marker text itself, not any indentation before
199
+ // it on the same line — a fixed indent here would get silently swallowed into the discarded
200
+ // middle capture group on the very first re-patch (was a real bug during development: the
201
+ // closing marker's leading spaces vanished on a second `em ci init` run).
202
+ export function buildCiWorkflowFile(model, testsDir, emVersion) {
203
+ const { start, end } = markerPair(CI_WORKFLOW_MARKER, "hash");
204
+ return `${ciWorkflowHeader(model, emVersion)}${start}\n${ciManagedBody(model, testsDir)}\n${end}\n`;
205
+ }
206
+ // ---- em-conform.yml (scheduled, advisory-only conformance cadence) ----
207
+ /** The managed block only (no header/`on:`/`permissions:`/`jobs:` scaffolding) — what gets
208
+ * written between the marker pair, both for a fresh file and to patch an existing marked one
209
+ * in place. */
210
+ export function conformManagedBody(model) {
211
+ const modelDir = dirname(model) === "." ? "." : dirname(model);
212
+ return ` conform:
213
+ runs-on: ubuntu-latest
214
+ steps:
215
+ - uses: actions/checkout@v4
216
+ - uses: actions/setup-node@v4
217
+ with:
218
+ node-version: 20
219
+
220
+ - name: Note reports already present
221
+ run: ls "$MODEL_DIR"/conformance/*-report.md 2>/dev/null | sort > /tmp/reports-before
222
+
223
+ - name: Run conform phase
224
+ run: |
225
+ npm i -g @milehimikey/em@1 @anthropic-ai/claude-code
226
+ em skill install --force
227
+ claude -p "/event-modeling conform" \\
228
+ --allowedTools "Bash(em:*),Bash(git:*),Read,Grep,Glob,Write,Edit"
229
+ env:
230
+ ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
231
+
232
+ - name: Post report
233
+ run: |
234
+ ls "$MODEL_DIR"/conformance/*-report.md 2>/dev/null | sort > /tmp/reports-after
235
+ report=$(comm -13 /tmp/reports-before /tmp/reports-after | tail -1)
236
+ if [ -z "$report" ]; then
237
+ echo "the run produced no new report — nothing to post"
238
+ exit 0
239
+ fi
240
+ gh issue create --title "Model conformance report $(date +%F)" \\
241
+ --body-file "$report"
242
+ env:
243
+ GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
244
+ env:
245
+ MODEL_DIR: ${modelDir}`;
246
+ }
247
+ function conformWorkflowHeader(model, emVersion) {
248
+ return `# Generated once by \`em ci init ${model}\` (em ${emVersion}) — docs/ci.md#conformance-cadence-advisory
249
+ #
250
+ # This file is yours from here: edit it freely. It is advisory-only by construction — the job
251
+ # never fails the build on drift (findings become a GitHub issue for a human to ratify), and
252
+ # the state file's \`Last conformance:\` marker only advances when a human ratifies the run's
253
+ # outcome locally and commits that update. Resist wiring this to every push; cadence, not
254
+ # trigger, is the intended shape (docs/ci.md).
255
+ name: model-conformance
256
+
257
+ on:
258
+ schedule:
259
+ - cron: "0 6 * * 1" # weekly, Monday 06:00 UTC
260
+ workflow_dispatch: {} # and on demand
261
+
262
+ permissions:
263
+ contents: read
264
+ issues: write # the Post report step opens an issue
265
+
266
+ jobs:
267
+ `;
268
+ }
269
+ export function buildConformWorkflowFile(model, emVersion) {
270
+ const { start, end } = markerPair(CONFORM_WORKFLOW_MARKER, "hash");
271
+ return `${conformWorkflowHeader(model, emVersion)}${start}\n${conformManagedBody(model)}\n${end}\n`;
272
+ }
273
+ /**
274
+ * Decide what to do with one generated file: create it if missing, patch the managed block if
275
+ * the marker pair is present, or (without `force`) leave an existing unmarked file alone. Pure
276
+ * — never touches disk; `applyCiFile` below does the actual write.
277
+ *
278
+ * `generated` is the full from-scratch file content (header + `on:`/`jobs:` scaffolding +
279
+ * marker-wrapped body) — written verbatim for `create`/`would-replace`. `managedBody` is just
280
+ * the text between the markers, reused to patch an existing marked file in place without
281
+ * touching whatever a repo added around it.
282
+ */
283
+ export function planCiFile(path, generated, managedBody, markerName, force) {
284
+ if (!existsSync(path))
285
+ return { kind: "create", content: generated };
286
+ const original = readFileSync(path, "utf8");
287
+ const updated = applyMarker(original, markerName, managedBody, "hash");
288
+ if (updated === null) {
289
+ return force ? { kind: "would-replace", content: generated } : { kind: "missing-markers" };
290
+ }
291
+ return updated === original ? { kind: "ok", content: original } : { kind: "stale", content: updated, current: original };
292
+ }
293
+ export function applyCiFile(path, status) {
294
+ if (status.kind === "create" || status.kind === "stale" || status.kind === "would-replace") {
295
+ writeFileSync(path, status.content, "utf8");
296
+ }
297
+ }
298
+ //# sourceMappingURL=ciInit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ciInit.js","sourceRoot":"","sources":["../../src/cli/ciInit.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ export type ApplySupersedeResult = {
2
+ ok: true;
3
+ content: string;
4
+ changed: boolean;
5
+ } | {
6
+ ok: false;
7
+ message: string;
8
+ };
9
+ /**
10
+ * Pure text transform: inserts (or, if already present, no-ops on) a `> **Superseded as of
11
+ * `<revision>`** — findings <findings> since ruled (<on>).` banner line directly under `raw`'s
12
+ * first line. Refuses when `raw` has no first line to anchor under, or when any input carries
13
+ * control characters / an unsafe `findings` value that could corrupt the report or smuggle
14
+ * unintended markdown into it. No fs access — the caller reads/writes; see `runConformSupersede`
15
+ * below.
16
+ */
17
+ export declare function applySupersededBanner(raw: string, revision: string, findings: string, on: string): ApplySupersedeResult;
18
+ export type RunConformSupersedeResult = {
19
+ ok: true;
20
+ path: string;
21
+ changed: boolean;
22
+ } | {
23
+ ok: false;
24
+ message: string;
25
+ };
26
+ /**
27
+ * Resolves `reportPath` (relative to `baseDir` — the `.em` file's directory, same convention
28
+ * every doc/note/report path in `em` uses) and applies the banner. Refuses cleanly when the
29
+ * report doesn't exist rather than creating one — `em conform-supersede` stamps an existing
30
+ * report, it never authors one.
31
+ */
32
+ export declare function runConformSupersede(baseDir: string, reportPath: string, revision: string, findings: string, on: string): RunConformSupersedeResult;
@@ -0,0 +1,102 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // `em conform-supersede` (MIL-164): stamps a conformance report with a "this describes an
3
+ // ancestor" banner once its findings have been ruled on (docs/process.md's "Ratifying
4
+ // conformance findings" — stage 7 of workflow.md, the companion step to run alongside/after
5
+ // `em slice ratify` once a report's findings are ruled). A reader following a superseded
6
+ // report's file:line citations needs to know, before trusting them, that the report was
7
+ // written against an older revision — the exact problem a stale-but-uncaptioned report causes
8
+ // (Meridian Goods' 2026-08-23 report citing a file a later ruling renamed: correct as history,
9
+ // misleading as a current view).
10
+ //
11
+ // Write strategy: additive-only text splice, never a parse+re-serialize of the report — same
12
+ // surgical discipline ratify.ts/markImplemented.ts hold for slice-doc frontmatter, applied here
13
+ // to a plain-markdown report instead. The banner is inserted as a blockquote line directly under
14
+ // the report's title (its first line); every byte after that — the metadata bullets, Summary,
15
+ // Findings, everything — is copied through verbatim. Repeated calls (a report ruled on
16
+ // incrementally, across more than one session) accumulate one banner line per call rather than
17
+ // overwriting the last — each stamp names its own revision/findings/date, so the accumulated
18
+ // block reads as a small history of rulings, not a single mutable fact. Calling again with the
19
+ // EXACT same revision/findings/date is a no-op (`changed: false`), same idempotency convention
20
+ // every other `em` write command holds.
21
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
22
+ import { join } from "node:path";
23
+ import { isValidDateString } from "./stateFile.js";
24
+ const BANNER_PREFIX = "> **Superseded as of `";
25
+ /** Conservative charset for `--findings`: digits, commas, spaces, and either dash style (ASCII
26
+ * hyphen or an en/em dash pasted from a report) — enough to write "1-3", "1, 2, 4", or "1–3",
27
+ * nothing that could smuggle markdown/HTML into the banner it gets spliced into. */
28
+ const FINDINGS_RE = /^[\d,\s\-–—]+$/;
29
+ const CONTROL_CHARS_RE = /[\x00-\x1f\x7f]/;
30
+ /**
31
+ * Pure text transform: inserts (or, if already present, no-ops on) a `> **Superseded as of
32
+ * `<revision>`** — findings <findings> since ruled (<on>).` banner line directly under `raw`'s
33
+ * first line. Refuses when `raw` has no first line to anchor under, or when any input carries
34
+ * control characters / an unsafe `findings` value that could corrupt the report or smuggle
35
+ * unintended markdown into it. No fs access — the caller reads/writes; see `runConformSupersede`
36
+ * below.
37
+ */
38
+ export function applySupersededBanner(raw, revision, findings, on) {
39
+ const trimmedRev = revision.trim();
40
+ if (!trimmedRev)
41
+ return { ok: false, message: "a revision is required (--as-of)" };
42
+ if (CONTROL_CHARS_RE.test(trimmedRev) || trimmedRev.includes("`")) {
43
+ return { ok: false, message: "revision must not contain control characters or a backtick" };
44
+ }
45
+ const trimmedFindings = findings.trim();
46
+ if (!trimmedFindings)
47
+ return { ok: false, message: "a findings spec is required (--findings)" };
48
+ if (!FINDINGS_RE.test(trimmedFindings)) {
49
+ return {
50
+ ok: false,
51
+ message: `--findings "${findings}" must be a plain list/range of numbers (e.g. "1-3" or "1, 2, 4")`,
52
+ };
53
+ }
54
+ if (!isValidDateString(on)) {
55
+ return { ok: false, message: `invalid date "${on}" — expected YYYY-MM-DD` };
56
+ }
57
+ const firstNl = raw.indexOf("\n");
58
+ if (firstNl === -1) {
59
+ return { ok: false, message: "report has no title line to anchor the banner after" };
60
+ }
61
+ const eol = raw[firstNl - 1] === "\r" ? "\r\n" : "\n";
62
+ const titleEnd = firstNl - (eol === "\r\n" ? 1 : 0);
63
+ const titleLine = raw.slice(0, titleEnd);
64
+ const rest = raw.slice(firstNl + 1);
65
+ const newLine = `${BANNER_PREFIX}${trimmedRev}\`** — findings ${trimmedFindings} since ruled (${on}). This report describes an ancestor of the current model; verify file:line citations against the current code before relying on them.`;
66
+ const restLines = rest.split(eol);
67
+ const bannerLines = [];
68
+ let i = 0;
69
+ while (i < restLines.length && restLines[i].startsWith(BANNER_PREFIX)) {
70
+ bannerLines.push(restLines[i]);
71
+ i++;
72
+ }
73
+ if (bannerLines.includes(newLine)) {
74
+ return { ok: true, content: raw, changed: false }; // idempotent no-op — exact stamp already recorded
75
+ }
76
+ const remainder = restLines.slice(i).join(eol);
77
+ const newBannerBlock = [...bannerLines, newLine];
78
+ const content = `${titleLine}${eol}${newBannerBlock.join(eol)}${eol}${remainder}`;
79
+ return { ok: true, content, changed: true };
80
+ }
81
+ /**
82
+ * Resolves `reportPath` (relative to `baseDir` — the `.em` file's directory, same convention
83
+ * every doc/note/report path in `em` uses) and applies the banner. Refuses cleanly when the
84
+ * report doesn't exist rather than creating one — `em conform-supersede` stamps an existing
85
+ * report, it never authors one.
86
+ */
87
+ export function runConformSupersede(baseDir, reportPath, revision, findings, on) {
88
+ const absPath = join(baseDir, reportPath);
89
+ if (!existsSync(absPath)) {
90
+ return { ok: false, message: `no such report: ${reportPath}` };
91
+ }
92
+ const raw = readFileSync(absPath, "utf8");
93
+ const result = applySupersededBanner(raw, revision, findings, on);
94
+ if (!result.ok) {
95
+ return { ok: false, message: `${reportPath}: ${result.message}` };
96
+ }
97
+ if (result.changed) {
98
+ writeFileSync(absPath, result.content, "utf8");
99
+ }
100
+ return { ok: true, path: reportPath, changed: result.changed };
101
+ }
102
+ //# sourceMappingURL=conformSupersede.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conformSupersede.js","sourceRoot":"","sources":["../../src/cli/conformSupersede.ts"],"names":[],"mappings":"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"}
@@ -1,17 +1,24 @@
1
1
  import { NormalizedModel } from "../model/model.js";
2
2
  import { RefsResult } from "../model/refs.js";
3
- /** The stable INV token format, scanned across a slice doc's whole body (not just the
4
- * `## Invariants / Business Rules` section — a `## Delta` section's Added/Modified/Renamed
5
- * requirement IDs are just as citable, see templates/slice.md). `INV-` then one or more
6
- * alphanumeric/hyphen segments — matches both `INV-1`/`INV-2` (the template's bare numbering)
7
- * and `INV-CHK-4`/`INV-CHK-3a` (the per-slice mnemonic style reference/conform.md's own example
8
- * uses). Exported so tests can assert against the exact grammar. */
3
+ /** The stable INV token format. `INV-` then one or more alphanumeric/hyphen segments — matches
4
+ * both `INV-1`/`INV-2` (the template's bare numbering) and `INV-CHK-4`/`INV-CHK-3a` (the
5
+ * per-slice mnemonic style reference/conform.md's own example uses). Exported so tests can
6
+ * assert against the exact grammar. */
9
7
  export declare const INV_TOKEN_RE: RegExp;
10
8
  /**
11
- * Extract every distinct `INV-*` token from a slice doc's body text, in first-occurrence order
12
- * (a rule stated once in `## Invariants` and mentioned again in a later `## Delta` section dedupes
13
- * to its first appearance — order is for stable, readable output, not a claim about which mention
14
- * is canonical).
9
+ * Extract every distinct `INV-*` token this slice doc *defines*, in first-occurrence order (a
10
+ * rule stated once in `## Invariants` and re-declared in a later `## Delta` section dedupes to
11
+ * its first appearance — order is for stable, readable output, not a claim about which mention is
12
+ * canonical). Scoped to `OWNERSHIP_HEADING_RE`'s sections only, so a doc's prose narratively
13
+ * citing another slice's ID elsewhere (Dependencies, Open Questions, ...) doesn't cross-credit
14
+ * that ID as this slice's own (MIL-149) — the section ends at the next `#`/`##` heading or EOF,
15
+ * same boundary rule sliceDoc.ts's countOpenQuestions() uses, so a `### Added`/`#### Requirement`
16
+ * subheading under `## Delta` stays in scope while a sibling `## Scenarios` does not. Within that
17
+ * scope, only `STRUCTURAL_LINE_RE`'s bullet/subheading lines contribute — MIL-155 — so a wrapped
18
+ * continuation line that legitimately *cites* a sibling slice's ID while explaining this doc's own
19
+ * (e.g. an Invariants bullet's exactly-once rule wrapping onto a line naming another slice's
20
+ * queue-draining ID) doesn't cross-credit that citation just because it sits inside the ownership
21
+ * section too.
15
22
  */
16
23
  export declare function extractInvariantIds(body: string): string[];
17
24
  export interface Citation {