@milehimikey/em 1.2.0 → 1.4.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 (44) hide show
  1. package/.claude/skills/event-modeling/SKILL.md +66 -16
  2. package/.claude/skills/event-modeling/reference/conform.md +228 -0
  3. package/.claude/skills/event-modeling/reference/em-dsl.md +22 -5
  4. package/.claude/skills/event-modeling/reference/extract.md +5 -4
  5. package/.claude/skills/event-modeling/reference/methodology.md +11 -3
  6. package/.claude/skills/event-modeling/templates/conformance-report.md +69 -0
  7. package/.claude/skills/event-modeling/templates/model-readme.md +6 -4
  8. package/.claude/skills/event-modeling/templates/slice.md +11 -2
  9. package/.claude/skills/event-modeling/templates/state.md +17 -7
  10. package/README.md +17 -3
  11. package/dist/cli/changelog-git.d.ts +53 -0
  12. package/dist/cli/changelog-git.js +112 -0
  13. package/dist/cli/changelog-git.js.map +1 -0
  14. package/dist/cli/diff-inputs.d.ts +44 -0
  15. package/dist/cli/diff-inputs.js +62 -0
  16. package/dist/cli/diff-inputs.js.map +1 -0
  17. package/dist/cli.js +231 -7
  18. package/dist/cli.js.map +1 -1
  19. package/dist/emit/changelog.d.ts +57 -0
  20. package/dist/emit/changelog.js +143 -0
  21. package/dist/emit/changelog.js.map +1 -0
  22. package/dist/emit/diffJson.d.ts +17 -0
  23. package/dist/emit/diffJson.js +74 -0
  24. package/dist/emit/diffJson.js.map +1 -0
  25. package/dist/emit/json.d.ts +29 -0
  26. package/dist/emit/json.js +121 -0
  27. package/dist/emit/json.js.map +1 -0
  28. package/dist/model/diff.d.ts +52 -0
  29. package/dist/model/diff.js +351 -0
  30. package/dist/model/diff.js.map +1 -0
  31. package/dist/model/model.d.ts +2 -0
  32. package/dist/model/model.js +1 -0
  33. package/dist/model/model.js.map +1 -1
  34. package/dist/model/validate.js +74 -0
  35. package/dist/model/validate.js.map +1 -1
  36. package/dist/parser/ast.d.ts +2 -0
  37. package/dist/parser/parser.js +8 -0
  38. package/dist/parser/parser.js.map +1 -1
  39. package/dist/render/drawNotes.d.ts +8 -2
  40. package/dist/render/drawNotes.js +106 -42
  41. package/dist/render/drawNotes.js.map +1 -1
  42. package/dist/render/render.js +5 -4
  43. package/dist/render/render.js.map +1 -1
  44. package/package.json +3 -2
@@ -10,6 +10,7 @@ Keep it current at the end of every working session.
10
10
  - **Current phase:** {{extract | discover | model | slice}}
11
11
  - **Current step:** {{1–7, see methodology; or extraction round R1–R7}}
12
12
  - **Last updated:** {{YYYY-MM-DD}}
13
+ - **Last conformance:** {{YYYY-MM-DD @ <target-repo revision> — report: conformance/<date>-report.md | never}}
13
14
 
14
15
  ## Session inputs
15
16
  - **Scope line:** {{one-line description of what's in/out of bounds for this model}}
@@ -18,6 +19,11 @@ Keep it current at the end of every working session.
18
19
  - **Source mode:** {{greenfield | extract-event-driven | extract-procedural}}
19
20
  - **Existing system refs:** {{repo paths, event-schema/topic locations, docs — or "n/a"}}
20
21
 
22
+ ## Participants
23
+ <!-- Populate at session start. Live workshop: one human proxy relays questions to the room;
24
+ attribute every answer/decision in the Decisions log to a named participant here. -->
25
+ - {{Name}} — {{role}} — {{domain area}}
26
+
21
27
  ## Extraction progress (existing-system models only — delete for greenfield)
22
28
  - [ ] R1. Candidate events (extracted/synthesized, filtered, confirmed)
23
29
  - [ ] R2. Timeline order (as-is narrative, actors/callers, first render)
@@ -37,14 +43,18 @@ Keep it current at the end of every working session.
37
43
  - [ ] 7. Evaluate completeness (`em validate` clean)
38
44
 
39
45
  ## Decisions log
40
- <!-- Resolved choices, with the reasoning, so they aren't re-litigated. -->
41
- - {{YYYY-MM-DD}}: {{decision}} — {{why}}
46
+ <!-- Resolved choices, with the reasoning, so they aren't re-litigated. In a live workshop,
47
+ attribute each entry to the participant who made the call (see Participants above). -->
48
+ - {{YYYY-MM-DD}}: {{decision}} — {{why}} — by {{participant, if a live workshop}}
42
49
 
43
50
  ## Open questions / parking lot
44
- <!-- Unresolved items to bring back to the user. Never guess these. -->
45
- - [ ] {{question}}
51
+ <!-- Unresolved items to bring back to the user. Never guess these. Metadata is optional and
52
+ flat — add only what's known: source (ticket/conversation that spawned it), blocked on
53
+ (who/what), revisit (when). -->
54
+ - [ ] {{question}} — source: {{ticket/conversation link, optional}} — blocked on: {{who/what, optional}} — revisit: {{when, optional}}
46
55
 
47
56
  ## Slice inventory
48
- | Slice | Pattern | Doc status |
49
- |-------|---------|------------|
50
- | {{Slice Name}} | {{State Change/View/Automation/Translation}} | {{none / draft / reviewed / ready-to-implement}} |
57
+ <!-- Deliberately NOT a table: the canonical slice index (slice, pattern, status, doc link)
58
+ lives in README.md's "Slices" section — one source of truth, updated there. This file
59
+ only tracks resumable session state. -->
60
+ See `README.md` → **Slices** for the slice index and per-slice doc status.
package/README.md CHANGED
@@ -28,6 +28,14 @@ em watch model.em --serve # live browser view, re-renders on every save
28
28
  em validate model.em # check event-modeling rules
29
29
  ```
30
30
 
31
+ Once a model is real and committed, `em` keeps working on it over time:
32
+
33
+ ```bash
34
+ em export model.em # versioned JSON, for agents and tooling
35
+ em diff model.em --from HEAD~1 # what this change did to the model
36
+ em changelog model.em # the model's git history as a business ledger
37
+ ```
38
+
31
39
  A model is a list of slices — vertical time steps, read left to right — whose elements land
32
40
  in swimlane rows:
33
41
 
@@ -50,7 +58,9 @@ slice "View Open Orders" {
50
58
  ```
51
59
 
52
60
  The [tutorial](docs/tutorial.md) builds a complete model from an empty file in about twenty
53
- minutes.
61
+ minutes, and [docs/workflow.md](docs/workflow.md) picks up where it leaves off: how a model
62
+ gets specified, gated in CI, handed to implementation, and checked against the code that
63
+ implements it.
54
64
 
55
65
  ## Model with AI
56
66
 
@@ -61,8 +71,10 @@ the questions, you supply the domain, and the model renders live as it grows.
61
71
  em skill install # copy the skill into .claude/skills/event-modeling/
62
72
  ```
63
73
 
64
- Then run `/event-modeling` in Claude Code. See [docs/ai-workflow.md](docs/ai-workflow.md)
65
- for the phases and what a session produces, and the
74
+ Then run `/event-modeling` in Claude Code. The same skill also runs the reverse direction:
75
+ `extract` derives a model from a system that already exists, and `conform` checks a model
76
+ against the code implementing it and reports where they've drifted. See
77
+ [docs/ai-workflow.md](docs/ai-workflow.md) for the phases and what a session produces, and the
66
78
  [em-with-ai repository](https://github.com/milehimikey/em-with-ai) for a ~50-slice model
67
79
  built this way.
68
80
 
@@ -71,10 +83,12 @@ built this way.
71
83
  | Doc | What it answers |
72
84
  |---|---|
73
85
  | [docs/tutorial.md](docs/tutorial.md) | Learn the tool by building a model from scratch |
86
+ | [docs/workflow.md](docs/workflow.md) | The model lifecycle: specify, gate, hand off, track change, detect drift |
74
87
  | [docs/patterns.md](docs/patterns.md) | The four Event Modeling patterns and their DSL shapes |
75
88
  | [docs/dsl.md](docs/dsl.md) | Full DSL reference: keywords, `from`, `again`, fields, notes |
76
89
  | [docs/cli.md](docs/cli.md) | Every command and flag |
77
90
  | [docs/validation.md](docs/validation.md) | Every rule `em validate` checks, and the fixes |
91
+ | [docs/ci.md](docs/ci.md) | Copy-paste CI recipes: validate `.em` changes, run conformance on a schedule |
78
92
  | [docs/timeline.md](docs/timeline.md) | The Two Laws of the Timeline |
79
93
  | [docs/ai-workflow.md](docs/ai-workflow.md) | The Claude Code skill: install, phases, artifacts |
80
94
  | [docs/dependencies.md](docs/dependencies.md) | What's bundled vs. what needs a system install |
@@ -0,0 +1,53 @@
1
+ import { GitRunner } from "./diff-inputs.js";
2
+ export interface CommitInfo {
3
+ /** Full commit hash — what `readFileAtCommit`/`git show` reads content at. */
4
+ hash: string;
5
+ shortHash: string;
6
+ /** YYYY-MM-DD author date (`--date=short`). */
7
+ date: string;
8
+ subject: string;
9
+ /**
10
+ * Repo-relative path of the file AS OF this commit, taken from the same
11
+ * `git log --follow --name-only` walk — pre-rename commits carry the old
12
+ * path, the one `git show <hash>:<path>` can actually read.
13
+ */
14
+ path: string;
15
+ }
16
+ export type CommitsResult = {
17
+ ok: true;
18
+ repoRoot: string;
19
+ commits: CommitInfo[];
20
+ } | {
21
+ ok: false;
22
+ message: string;
23
+ };
24
+ /**
25
+ * List every commit that touched `file` (rename-following via `--follow`),
26
+ * oldest -> newest, each carrying the file's path as of that commit.
27
+ *
28
+ * `--from`/`--to` bound the walk **inclusive of both boundary commits**: the
29
+ * range compiles to `<from>^..<to>` (git's standard idiom for an
30
+ * inclusive-from range), so `--from` itself is the oldest commit in the
31
+ * output and `--to` the newest. `--to` defaults to `HEAD`. `--from` on a
32
+ * root commit (no parent to exclude) falls back to an unbounded walk up to
33
+ * `<to>`, which is equivalent — there's nothing before a root commit to
34
+ * exclude anyway.
35
+ */
36
+ export declare function listModelCommits(file: string, opts?: {
37
+ from?: string;
38
+ to?: string;
39
+ }, runGit?: GitRunner): CommitsResult;
40
+ /** File content at a commit, or a note-ready `message` (no command prefix — it lands inside a changelog section). */
41
+ export type FileAtCommitResult = {
42
+ ok: true;
43
+ content: string;
44
+ } | {
45
+ ok: false;
46
+ message: string;
47
+ };
48
+ /**
49
+ * Read the model's content at `commit`, using `commit.path` — the path the
50
+ * file had AT that commit — so reads keep working across renames, where the
51
+ * current path doesn't exist in older revisions.
52
+ */
53
+ export declare function readFileAtCommit(repoRoot: string, commit: CommitInfo, runGit?: GitRunner): FileAtCommitResult;
@@ -0,0 +1,112 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Git interaction for `em changelog`, isolated like diff-inputs.ts: listing
3
+ // the commits that touched a model file and reading a revision's content
4
+ // (`readFileAtCommit`) are the only impure operations here — everything
5
+ // formattable/parseable (markdown assembly, decisions parsing) lives in
6
+ // ../emit/changelog.ts, unit-tested without git.
7
+ import { dirname, resolve } from "node:path";
8
+ import { realGit } from "./diff-inputs.js";
9
+ // A control character, not a valid character in a commit subject — safe as a
10
+ // field separator in `git log --format`.
11
+ const FS = "\x01";
12
+ /**
13
+ * List every commit that touched `file` (rename-following via `--follow`),
14
+ * oldest -> newest, each carrying the file's path as of that commit.
15
+ *
16
+ * `--from`/`--to` bound the walk **inclusive of both boundary commits**: the
17
+ * range compiles to `<from>^..<to>` (git's standard idiom for an
18
+ * inclusive-from range), so `--from` itself is the oldest commit in the
19
+ * output and `--to` the newest. `--to` defaults to `HEAD`. `--from` on a
20
+ * root commit (no parent to exclude) falls back to an unbounded walk up to
21
+ * `<to>`, which is equivalent — there's nothing before a root commit to
22
+ * exclude anyway.
23
+ */
24
+ export function listModelCommits(file, opts = {}, runGit = realGit) {
25
+ const abs = resolve(file);
26
+ const toplevel = runGit(["-C", dirname(abs), "rev-parse", "--show-toplevel"]);
27
+ if (toplevel.status !== 0) {
28
+ return { ok: false, message: `em changelog: ${file} is not inside a git repository` };
29
+ }
30
+ const repoRoot = toplevel.stdout.trim();
31
+ const lsFiles = runGit(["-C", repoRoot, "ls-files", "--full-name", "--", abs]);
32
+ const relPath = lsFiles.stdout.trim().split("\n")[0];
33
+ if (!relPath) {
34
+ return { ok: false, message: `em changelog: ${file} is not tracked by git in ${repoRoot}` };
35
+ }
36
+ const upRev = opts.to ?? "HEAD";
37
+ if (opts.to) {
38
+ const verifyTo = runGit(["-C", repoRoot, "rev-parse", "--verify", "--end-of-options", opts.to]);
39
+ if (verifyTo.status !== 0) {
40
+ return { ok: false, message: `em changelog: unknown revision "${opts.to}" for --to` };
41
+ }
42
+ }
43
+ let range = upRev;
44
+ if (opts.from) {
45
+ const verifyFrom = runGit(["-C", repoRoot, "rev-parse", "--verify", "--end-of-options", opts.from]);
46
+ if (verifyFrom.status !== 0) {
47
+ return { ok: false, message: `em changelog: unknown revision "${opts.from}" for --from` };
48
+ }
49
+ const verifyParent = runGit(["-C", repoRoot, "rev-parse", "--verify", "--end-of-options", `${opts.from}^`]);
50
+ range = verifyParent.status === 0 ? `${opts.from}^..${upRev}` : upRev;
51
+ }
52
+ // --name-only prints, after each commit's format line, the file's path as
53
+ // of that commit (--follow makes that the historical, pre-rename path when
54
+ // applicable). core.quotepath=false keeps non-ASCII paths literal instead
55
+ // of octal-escaped, so the printed path is usable in `git show`.
56
+ const log = runGit([
57
+ "-C",
58
+ repoRoot,
59
+ "-c",
60
+ "core.quotepath=false",
61
+ "log",
62
+ "--follow",
63
+ "--name-only",
64
+ `--format=%H${FS}%h${FS}%ad${FS}%s`,
65
+ "--date=short",
66
+ range,
67
+ "--",
68
+ relPath,
69
+ ]);
70
+ if (log.status !== 0) {
71
+ return {
72
+ ok: false,
73
+ message: `em changelog: git log failed: ${(log.stderr || "").trim() || "unknown git error"}`,
74
+ };
75
+ }
76
+ // Output alternates: a format line (contains FS), a blank line, then the
77
+ // path line for that commit. A commit with no path line (e.g. a merge,
78
+ // whose file list git omits by default) falls back to the current path.
79
+ const commits = [];
80
+ for (const line of log.stdout.split("\n")) {
81
+ if (line.length === 0)
82
+ continue;
83
+ if (line.includes(FS)) {
84
+ const [hash, shortHash, date, ...subjectParts] = line.split(FS);
85
+ commits.push({ hash, shortHash, date, subject: subjectParts.join(FS), path: relPath });
86
+ }
87
+ else if (commits.length > 0) {
88
+ commits[commits.length - 1].path = line;
89
+ }
90
+ }
91
+ commits.reverse(); // git log is newest-first; changelog building wants oldest->newest
92
+ if (commits.length === 0) {
93
+ return { ok: false, message: `em changelog: no commits found for ${relPath} in the given range` };
94
+ }
95
+ return { ok: true, repoRoot, commits };
96
+ }
97
+ /**
98
+ * Read the model's content at `commit`, using `commit.path` — the path the
99
+ * file had AT that commit — so reads keep working across renames, where the
100
+ * current path doesn't exist in older revisions.
101
+ */
102
+ export function readFileAtCommit(repoRoot, commit, runGit = realGit) {
103
+ const show = runGit(["-C", repoRoot, "show", `${commit.hash}:${commit.path}`]);
104
+ if (show.status !== 0) {
105
+ return {
106
+ ok: false,
107
+ message: `cannot read ${commit.path} at ${commit.shortHash}: ${(show.stderr || "").trim() || "unknown git error"}`,
108
+ };
109
+ }
110
+ return { ok: true, content: show.stdout };
111
+ }
112
+ //# sourceMappingURL=changelog-git.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"changelog-git.js","sourceRoot":"","sources":["../../src/cli/changelog-git.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,44 @@
1
+ /** Resolved `em diff` invocation, or a user-facing `error` for an invalid form. */
2
+ export type DiffPlan = {
3
+ error: string;
4
+ } | {
5
+ form: "files";
6
+ oldFile: string;
7
+ newFile: string;
8
+ } | {
9
+ form: "git";
10
+ file: string;
11
+ from: string;
12
+ to?: string;
13
+ };
14
+ /**
15
+ * Validate the `em diff` argument/flag combination. Pure — no I/O. The two forms
16
+ * (two files, or one file across git revisions) are mutually exclusive.
17
+ */
18
+ export declare function planDiffArgs(oldFile: string, newFile: string | undefined, opts: {
19
+ from?: string;
20
+ to?: string;
21
+ }): DiffPlan;
22
+ /** The slice of a `spawnSync` result `resolveRevision` reads; lets tests inject a fake git. */
23
+ export interface GitResult {
24
+ status: number | null;
25
+ stdout: string;
26
+ stderr: string;
27
+ }
28
+ export type GitRunner = (args: string[]) => GitResult;
29
+ /** Default runner: really invokes `git` (array args — no shell, no injection). */
30
+ export declare const realGit: GitRunner;
31
+ /** File content at a git revision, or a user-facing `message` for each failure mode. */
32
+ export type RevisionResult = {
33
+ ok: true;
34
+ content: string;
35
+ } | {
36
+ ok: false;
37
+ message: string;
38
+ };
39
+ /**
40
+ * Resolve `file`'s content at git revision `rev` via `git show <rev>:<repo-relative-path>`.
41
+ * The git runner is injectable so the not-a-repo / not-tracked / bad-rev branches are
42
+ * unit-testable without a real repository.
43
+ */
44
+ export declare function resolveRevision(file: string, rev: string, runGit?: GitRunner): RevisionResult;
@@ -0,0 +1,62 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Testable input handling for `em diff`: argument-form validation and
3
+ // git-revision resolution, factored out of cli.ts so the branching logic can be
4
+ // unit-tested without spawning a subprocess or a real git repo (cli.ts keeps the
5
+ // process.exit/console wiring; see test/diff-inputs.test.ts).
6
+ import { spawnSync } from "node:child_process";
7
+ import { dirname, resolve } from "node:path";
8
+ /**
9
+ * Validate the `em diff` argument/flag combination. Pure — no I/O. The two forms
10
+ * (two files, or one file across git revisions) are mutually exclusive.
11
+ */
12
+ export function planDiffArgs(oldFile, newFile, opts) {
13
+ if (opts.from) {
14
+ if (newFile) {
15
+ return {
16
+ error: "em diff: cannot combine two file arguments with --from/--to — use one form or the other",
17
+ };
18
+ }
19
+ return { form: "git", file: oldFile, from: opts.from, to: opts.to };
20
+ }
21
+ if (opts.to) {
22
+ return { error: "em diff: --to requires --from" };
23
+ }
24
+ if (!newFile) {
25
+ return {
26
+ error: "em diff: provide two files (`em diff old.em new.em`) or one file with --from <rev>",
27
+ };
28
+ }
29
+ return { form: "files", oldFile, newFile };
30
+ }
31
+ /** Default runner: really invokes `git` (array args — no shell, no injection). */
32
+ export const realGit = (args) => {
33
+ const r = spawnSync("git", args, { encoding: "utf8" });
34
+ return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
35
+ };
36
+ /**
37
+ * Resolve `file`'s content at git revision `rev` via `git show <rev>:<repo-relative-path>`.
38
+ * The git runner is injectable so the not-a-repo / not-tracked / bad-rev branches are
39
+ * unit-testable without a real repository.
40
+ */
41
+ export function resolveRevision(file, rev, runGit = realGit) {
42
+ const abs = resolve(file);
43
+ const toplevel = runGit(["-C", dirname(abs), "rev-parse", "--show-toplevel"]);
44
+ if (toplevel.status !== 0) {
45
+ return { ok: false, message: `em diff: ${file} is not inside a git repository (needed for --from/--to)` };
46
+ }
47
+ const repoRoot = toplevel.stdout.trim();
48
+ const lsFiles = runGit(["-C", repoRoot, "ls-files", "--full-name", "--", abs]);
49
+ const relPath = lsFiles.stdout.trim().split("\n")[0];
50
+ if (!relPath) {
51
+ return { ok: false, message: `em diff: ${file} is not tracked by git in ${repoRoot}` };
52
+ }
53
+ const show = runGit(["-C", repoRoot, "show", `${rev}:${relPath}`]);
54
+ if (show.status !== 0) {
55
+ return {
56
+ ok: false,
57
+ message: `em diff: cannot read ${relPath} at revision "${rev}": ${(show.stderr || "").trim() || "unknown git error"}`,
58
+ };
59
+ }
60
+ return { ok: true, content: show.stdout };
61
+ }
62
+ //# sourceMappingURL=diff-inputs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff-inputs.js","sourceRoot":"","sources":["../../src/cli/diff-inputs.ts"],"names":[],"mappings":"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"}