@milehimikey/em 1.1.0 → 1.2.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 +64 -81
- package/.claude/skills/event-modeling/reference/em-dsl.md +23 -18
- package/.claude/skills/event-modeling/reference/extract.md +123 -0
- package/.claude/skills/event-modeling/reference/methodology.md +23 -0
- package/.claude/skills/event-modeling/templates/live.html +65 -13
- package/.claude/skills/event-modeling/templates/model-readme.md +9 -6
- package/.claude/skills/event-modeling/templates/slice.md +2 -4
- package/.claude/skills/event-modeling/templates/state.md +16 -11
- package/README.md +44 -199
- package/dist/cli.js +21 -227
- package/dist/cli.js.map +1 -1
- package/dist/model/validate.d.ts +0 -2
- package/dist/model/validate.js +1 -1
- package/dist/model/validate.js.map +1 -1
- package/dist/render/serve.d.ts +20 -0
- package/dist/render/serve.js +202 -0
- package/dist/render/serve.js.map +1 -0
- package/package.json +4 -5
- package/.claude/skills/event-modeling/reference/frontmatter.md +0 -129
- package/dist/config.d.ts +0 -15
- package/dist/config.js +0 -60
- package/dist/config.js.map +0 -1
- package/dist/migrate/migrate.d.ts +0 -26
- package/dist/migrate/migrate.js +0 -276
- package/dist/migrate/migrate.js.map +0 -1
- package/dist/model/deriveSliceFields.d.ts +0 -21
- package/dist/model/deriveSliceFields.js +0 -60
- package/dist/model/deriveSliceFields.js.map +0 -1
- package/dist/model/frontmatter.d.ts +0 -21
- package/dist/model/frontmatter.js +0 -38
- package/dist/model/frontmatter.js.map +0 -1
- package/dist/model/sliceDoc.d.ts +0 -56
- package/dist/model/sliceDoc.js +0 -28
- package/dist/model/sliceDoc.js.map +0 -1
- package/dist/model/validateSliceDocs.d.ts +0 -7
- package/dist/model/validateSliceDocs.js +0 -195
- package/dist/model/validateSliceDocs.js.map +0 -1
- package/dist/slice/generate.d.ts +0 -17
- package/dist/slice/generate.js +0 -61
- package/dist/slice/generate.js.map +0 -1
- package/dist/slice/query.d.ts +0 -42
- package/dist/slice/query.js +0 -96
- package/dist/slice/query.js.map +0 -1
- package/dist/slice/sync.d.ts +0 -18
- package/dist/slice/sync.js +0 -104
- package/dist/slice/sync.js.map +0 -1
- package/dist/util/findModel.d.ts +0 -1
- package/dist/util/findModel.js +0 -27
- package/dist/util/findModel.js.map +0 -1
|
@@ -4,23 +4,28 @@ The skill reads this on `/event-modeling` (no arg) to resume where you left off.
|
|
|
4
4
|
Keep it current at the end of every working session.
|
|
5
5
|
-->
|
|
6
6
|
|
|
7
|
-
---
|
|
8
|
-
schemaVersion: 1
|
|
9
|
-
model: {{model-name}}.em
|
|
10
|
-
---
|
|
11
|
-
|
|
12
7
|
# Event Modeling Progress — {{Model Name}}
|
|
13
8
|
|
|
14
9
|
- **Model file:** `{{model-name}}.em`
|
|
15
|
-
- **Current phase:** {{discover | model | slice}}
|
|
16
|
-
- **Current step:** {{1–7, see methodology}}
|
|
10
|
+
- **Current phase:** {{extract | discover | model | slice}}
|
|
11
|
+
- **Current step:** {{1–7, see methodology; or extraction round R1–R7}}
|
|
17
12
|
- **Last updated:** {{YYYY-MM-DD}}
|
|
18
13
|
|
|
19
14
|
## Session inputs
|
|
20
15
|
- **Scope line:** {{one-line description of what's in/out of bounds for this model}}
|
|
21
16
|
- **PRD / spec reference:** {{path, link, or "none"}}
|
|
22
17
|
- **Headless/API model:** {{yes | no}}
|
|
23
|
-
- **
|
|
18
|
+
- **Source mode:** {{greenfield | extract-event-driven | extract-procedural}}
|
|
19
|
+
- **Existing system refs:** {{repo paths, event-schema/topic locations, docs — or "n/a"}}
|
|
20
|
+
|
|
21
|
+
## Extraction progress (existing-system models only — delete for greenfield)
|
|
22
|
+
- [ ] R1. Candidate events (extracted/synthesized, filtered, confirmed)
|
|
23
|
+
- [ ] R2. Timeline order (as-is narrative, actors/callers, first render)
|
|
24
|
+
- [ ] R3. Commands / inputs
|
|
25
|
+
- [ ] R4. Read models / outputs (validate from here)
|
|
26
|
+
- [ ] R5. Boundaries & reactions (two-slice splits)
|
|
27
|
+
- [ ] R6. Gap & TBD reconciliation
|
|
28
|
+
- [ ] R7. Convergence (render + validate clean, user confirmed as-is)
|
|
24
29
|
|
|
25
30
|
## Steps completed
|
|
26
31
|
- [ ] 1. Brainstorm events
|
|
@@ -40,6 +45,6 @@ model: {{model-name}}.em
|
|
|
40
45
|
- [ ] {{question}}
|
|
41
46
|
|
|
42
47
|
## Slice inventory
|
|
43
|
-
| Slice |
|
|
44
|
-
|
|
45
|
-
| {{Slice Name}} | {{
|
|
48
|
+
| Slice | Pattern | Doc status |
|
|
49
|
+
|-------|---------|------------|
|
|
50
|
+
| {{Slice Name}} | {{State Change/View/Automation/Translation}} | {{none / draft / reviewed / ready-to-implement}} |
|
package/README.md
CHANGED
|
@@ -1,214 +1,85 @@
|
|
|
1
|
-
# em —
|
|
1
|
+
# em — event modeling in plain text
|
|
2
2
|
|
|
3
3
|
`em` is a command-line tool for [Event Modeling](https://eventmodeling.org/). You write a
|
|
4
|
-
model in a small,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
is for a person to write by hand. Layout is a **strict grid**: swimlane rows and
|
|
8
|
-
time-ordered slice columns stay perfectly aligned with no manual offsets.
|
|
4
|
+
model in a small, slice-first DSL and `em` renders it as a clean, deterministic diagram.
|
|
5
|
+
Because the source is plain text — diff-able, reviewable, unambiguous — it's as easy for an
|
|
6
|
+
AI to write and edit as it is for a person, and `em validate` keeps either one honest.
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
examples/order-fulfillment.em -> em render -> order-fulfillment.svg
|
|
12
|
-
```
|
|
8
|
+

|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
rasterized in-process, so there's nothing to `apt`/`brew` install for SVG or PNG output.
|
|
10
|
+
The source for that diagram is [about 70 lines of text](examples/order-fulfillment.em).
|
|
16
11
|
|
|
17
12
|
## Install
|
|
18
13
|
|
|
19
14
|
```bash
|
|
20
|
-
npm install -g @milehimikey/em
|
|
15
|
+
npm install -g @milehimikey/em
|
|
21
16
|
```
|
|
22
17
|
|
|
23
|
-
Requires
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
Run without installing:
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
npx @milehimikey/em init model.em
|
|
30
|
-
npx @milehimikey/em render model.em
|
|
31
|
-
```
|
|
18
|
+
Requires Node ≥ 18. SVG and PNG rendering are fully self-contained (Graphviz runs as
|
|
19
|
+
bundled WebAssembly); nothing else to install. PDF is optional — see
|
|
20
|
+
[docs/dependencies.md](docs/dependencies.md).
|
|
32
21
|
|
|
33
22
|
## Quickstart
|
|
34
23
|
|
|
35
24
|
```bash
|
|
36
25
|
em init model.em # scaffold a starter model
|
|
37
26
|
em render model.em # -> model.svg (open it in a browser)
|
|
38
|
-
em watch model.em
|
|
27
|
+
em watch model.em --serve # live browser view, re-renders on every save
|
|
39
28
|
em validate model.em # check event-modeling rules
|
|
40
29
|
```
|
|
41
30
|
|
|
42
|
-
A
|
|
43
|
-
|
|
31
|
+
A model is a list of slices — vertical time steps, read left to right — whose elements land
|
|
32
|
+
in swimlane rows:
|
|
44
33
|
|
|
45
|
-
```
|
|
34
|
+
```em
|
|
46
35
|
model "Order Fulfillment"
|
|
47
36
|
|
|
48
|
-
persona Customer
|
|
49
|
-
context Order
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
customerId
|
|
56
|
-
total: Money # optional `: Type` annotation
|
|
57
|
-
}
|
|
58
|
-
event Order Placed @Order note "notes/order-placed.md" { # note "…" links docs
|
|
59
|
-
orderId
|
|
60
|
-
total: Money
|
|
61
|
-
}
|
|
37
|
+
persona Customer
|
|
38
|
+
context Order
|
|
39
|
+
|
|
40
|
+
slice "Browse Catalog" {
|
|
41
|
+
ui Product Catalog @Customer
|
|
42
|
+
command Place Order
|
|
43
|
+
event Order Placed @Order
|
|
62
44
|
}
|
|
63
45
|
|
|
64
46
|
slice "View Open Orders" {
|
|
65
|
-
view Open Orders from "Order Placed"
|
|
47
|
+
view Open Orders from "Order Placed"
|
|
66
48
|
ui Order List @Customer
|
|
67
49
|
}
|
|
68
50
|
```
|
|
69
51
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
Event Modeling is built from four patterns, all expressible as slices:
|
|
73
|
-
|
|
74
|
-
| Pattern | Flow | DSL elements |
|
|
75
|
-
|---|---|---|
|
|
76
|
-
| **Input** (state change) | UI → command → event | `ui`, `command`, `event` |
|
|
77
|
-
| **Output** (state view) | event → read model → UI | `event`, `view from "…"`, `ui` |
|
|
78
|
-
| **Automation** | read model (todo) → processor → command → event | `view from "…"` + `processor`, then `command` + `event` |
|
|
79
|
-
| **Translation** | external read model → translation → command → event | `view from "…"` + `translation`, then `command` + `event` |
|
|
80
|
-
|
|
81
|
-
Automation/translation slices are **not** triggered by a command — they react to a read
|
|
82
|
-
model (a "todo list") or a timer and *issue* a command when done. The automation slice
|
|
83
|
-
contains **only** the read model it reads and the processor/translation; the command it
|
|
84
|
-
triggers (and that command's event) is the **next** slice.
|
|
52
|
+
The [tutorial](docs/tutorial.md) builds a complete model from an empty file in about twenty
|
|
53
|
+
minutes.
|
|
85
54
|
|
|
86
|
-
##
|
|
55
|
+
## Model with AI
|
|
87
56
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
0. *(header)* — each slice name is rendered as a title cell in the top row
|
|
91
|
-
1. `automation` / `processor` / `saga` / `translation` — **Automation** band (only shown if used)
|
|
92
|
-
2. `ui` — **persona** rows (one per `persona`)
|
|
93
|
-
3. `command` + `view` — the **API** row (commands and read models share one lane)
|
|
94
|
-
4. `event` — **context/concept** rows (one per `context`)
|
|
95
|
-
|
|
96
|
-
Commands and read models share the **API** row, so a slice holds *either* a command *or* a
|
|
97
|
-
read model. The empty API lane is dropped when a model has neither; pass `--keep-empty-lanes`
|
|
98
|
-
to keep it.
|
|
99
|
-
|
|
100
|
-
`@Persona` / `@Context` choose the row within a band (undeclared tags create a new row).
|
|
101
|
-
`view <Name> again [from "Event", …]` declares a LATER INSTANCE of an already-declared read
|
|
102
|
-
model — the Event Modeling device for keeping the timeline forward-only as a view evolves.
|
|
103
|
-
Instances are one logical view: the first declaration owns the `note` doc; each instance lists
|
|
104
|
-
the events that reach it there; instances are linked left-to-right with a continuity arrow, and
|
|
105
|
-
reactions (`processor … from "View"`) read the nearest instance at-or-before their slice.
|
|
106
|
-
|
|
107
|
-
`view … from "Event"[, "Event2"]` declares which events feed a read model and draws the
|
|
108
|
-
data-flow arrow. Arrows within a slice are inferred from the pattern; use `arrow A -> B` for
|
|
109
|
-
anything extra (e.g. a read model feeding a different screen).
|
|
110
|
-
|
|
111
|
-
### Fields
|
|
112
|
-
|
|
113
|
-
Any element can declare **data fields** in a `{ … }` block — the data it accepts (command),
|
|
114
|
-
records (event), projects (read model), or shows (UI). Each field is `name` with an optional
|
|
115
|
-
`: Type`; write one per line, or inline and comma-separated:
|
|
116
|
-
|
|
117
|
-
```
|
|
118
|
-
command Place Order { # one per line
|
|
119
|
-
customerId
|
|
120
|
-
items: List<LineItem>
|
|
121
|
-
total: Money
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
event Payment Requested @Payment { orderId, amount: Money } # inline
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
Fields render **in the box**, UML-style: the name, a divider rule, then the fields. The box
|
|
128
|
-
grows to fit them (width stays fixed, so columns stay aligned), and arrows still anchor to
|
|
129
|
-
the box edges so the lines stay stable. A field block coexists with `note`/`from` clauses on
|
|
130
|
-
the same element. Fields are the foundation of the later event-modeling phases (the slicing /
|
|
131
|
-
information-completeness process); field-level validation across slices is planned.
|
|
132
|
-
|
|
133
|
-
### Notes
|
|
134
|
-
|
|
135
|
-
Any element can carry `note "path.md"` (valid on every keyword). The prose lives in the
|
|
136
|
-
markdown file — keeping the diagram uncluttered — and the box gets a small **numbered
|
|
137
|
-
folded-corner marker** in its top-right corner. A **legend** is appended below the diagram
|
|
138
|
-
mapping each number to its element and note file, so even a static export tells you which
|
|
139
|
-
note belongs to which box.
|
|
140
|
-
|
|
141
|
-
In **SVG** output the markers and legend rows are links; clicking one opens the markdown
|
|
142
|
-
file. Links resolve relative to the **output SVG's location**, so they keep working whether
|
|
143
|
-
the SVG is rendered beside the model or into another folder (as long as the notes travel
|
|
144
|
-
with it). Open the SVG in a **web browser** to use the links — image viewers like macOS
|
|
145
|
-
Preview/Quick Look show the markers but ignore SVG hyperlinks. Raster output (PNG/PDF) can't
|
|
146
|
-
carry links — that's what the footnote numbers + legend are for.
|
|
147
|
-
|
|
148
|
-
## Commands
|
|
57
|
+
`em` ships a Claude Code skill that runs a facilitated Event Modeling session: the AI asks
|
|
58
|
+
the questions, you supply the domain, and the model renders live as it grows.
|
|
149
59
|
|
|
150
60
|
```bash
|
|
151
|
-
em
|
|
152
|
-
em render <file> [-o out.svg] # render (format from -o extension or -T)
|
|
153
|
-
em render <file> -o out.png # PNG (in-process, no system deps)
|
|
154
|
-
em render <file> --emit-dot # print the generated DOT instead of rendering
|
|
155
|
-
em render <file> --keep-empty-lanes # don't collapse the API lane when empty
|
|
156
|
-
em watch <file> [-o out.svg] # re-render on every save
|
|
157
|
-
em validate <file> # check event-modeling rules + slice-doc frontmatter
|
|
158
|
-
em migrate <model-dir> # one-time upgrade of a pre-1.0 model to the frontmatter standard
|
|
61
|
+
em skill install # copy the skill into .claude/skills/event-modeling/
|
|
159
62
|
```
|
|
160
63
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
- two same-band elements in one slice (collision) → **error** (split into separate slices)
|
|
166
|
-
- a read model whose `from "Event"` doesn't exist → **error**
|
|
167
|
-
- **time flows left to right** (the Two Laws): an event feeding an EARLIER view instance,
|
|
168
|
-
a reaction reading a view that only exists later, or an explicit backward `arrow` → **error**
|
|
169
|
-
(fix: add `view X again` at the point on the timeline where the event lands)
|
|
170
|
-
- `again` on a view with no earlier declaration → **error**; `again` on non-views → parse error
|
|
171
|
-
- multi-event slices always render as a FAN from the command (never event→event); fanned
|
|
172
|
-
arrows route around the column gutter so they cannot read as a chain
|
|
173
|
-
- an automation/translation whose `from "<read model>"` doesn't exist → **error**
|
|
174
|
-
- an `arrow` endpoint that matches no element → **error**
|
|
175
|
-
- an automation/translation slice that also holds the command it triggers → **warning**
|
|
176
|
-
- a command that records no event → **warning**
|
|
177
|
-
- a read model with no source event → **warning**
|
|
178
|
-
- a name defined more than once and referenced → **warning** (resolves to first)
|
|
179
|
-
|
|
180
|
-
## Slice docs & frontmatter
|
|
181
|
-
|
|
182
|
-
Slice design docs (`slices/<id>.md`, written during the skill's `slice` phase or via
|
|
183
|
-
`em slice new`) carry a required YAML frontmatter block — a stable `id`, lifecycle `status`,
|
|
184
|
-
integer `version`, and fields generated from the `.em` (`commands`/`events`/`readModels`/
|
|
185
|
-
`contexts`/`personas`, plus `triggers`/`triggeredBy` for automation/translation) — so an AI agent
|
|
186
|
-
can search and filter across a model with hundreds of slices from frontmatter alone, without
|
|
187
|
-
reading every doc's body or re-parsing the `.em`:
|
|
188
|
-
|
|
189
|
-
```bash
|
|
190
|
-
em slice new "Place Order" --pattern state-change # scaffold slices/place-order.md
|
|
191
|
-
em slice sync place-order # after wiring note "slices/..." into the .em
|
|
192
|
-
em slice list --status ready-to-implement
|
|
193
|
-
em slice search --pattern automation --context Payment --format json
|
|
194
|
-
em slice search --status ready-to-implement --format json --full # every field, not just a summary
|
|
195
|
-
em slice update place-order --status reviewed --bump-version
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
The doc body itself is **yours to define** — point `em.config.json` at a custom template
|
|
199
|
-
(`{ "sliceTemplate": "path/to/template.md" }`) and `em` injects the frontmatter on top of it
|
|
200
|
-
regardless of what sections you ask for. Full schema, lifecycle model, and the handoff contract a
|
|
201
|
-
build/SDD tool should treat as authoritative once a slice is `ready-to-implement`:
|
|
202
|
-
[docs/1.0.0-spec.md](docs/1.0.0-spec.md).
|
|
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
|
|
66
|
+
[em-with-ai repository](https://github.com/milehimikey/em-with-ai) for a ~50-slice model
|
|
67
|
+
built this way.
|
|
203
68
|
|
|
204
69
|
## Documentation
|
|
205
70
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
71
|
+
| Doc | What it answers |
|
|
72
|
+
|---|---|
|
|
73
|
+
| [docs/tutorial.md](docs/tutorial.md) | Learn the tool by building a model from scratch |
|
|
74
|
+
| [docs/patterns.md](docs/patterns.md) | The four Event Modeling patterns and their DSL shapes |
|
|
75
|
+
| [docs/dsl.md](docs/dsl.md) | Full DSL reference: keywords, `from`, `again`, fields, notes |
|
|
76
|
+
| [docs/cli.md](docs/cli.md) | Every command and flag |
|
|
77
|
+
| [docs/validation.md](docs/validation.md) | Every rule `em validate` checks, and the fixes |
|
|
78
|
+
| [docs/timeline.md](docs/timeline.md) | The Two Laws of the Timeline |
|
|
79
|
+
| [docs/ai-workflow.md](docs/ai-workflow.md) | The Claude Code skill: install, phases, artifacts |
|
|
80
|
+
| [docs/dependencies.md](docs/dependencies.md) | What's bundled vs. what needs a system install |
|
|
81
|
+
| [docs/architecture.md](docs/architecture.md) | How rendering works; why Graphviz, not PlantUML |
|
|
82
|
+
| [docs/roadmap.md](docs/roadmap.md) | What's planned |
|
|
212
83
|
|
|
213
84
|
## Development
|
|
214
85
|
|
|
@@ -219,32 +90,6 @@ npm test # vitest
|
|
|
219
90
|
npx tsx src/cli.ts <command> ... # run straight from source
|
|
220
91
|
```
|
|
221
92
|
|
|
222
|
-
## AI Assistant (Claude Code)
|
|
223
|
-
|
|
224
|
-
`em` ships a **Claude Code skill** that guides you and your team through Event Modeling using the 7-step process and 4 design patterns, producing implementation-ready slice specifications. The AI drives the model — asking focused questions, never guessing domain facts — while `em validate` keeps the diagram honest.
|
|
225
|
-
|
|
226
|
-
**Install the skill into your project:**
|
|
227
|
-
|
|
228
|
-
```bash
|
|
229
|
-
em skill install # copies the skill into .claude/skills/event-modeling/
|
|
230
|
-
em skill install --force # overwrite an existing installation
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
Then, in Claude Code, run:
|
|
234
|
-
|
|
235
|
-
```
|
|
236
|
-
/event-modeling # start (or resume) a guided session
|
|
237
|
-
/event-modeling discover # steps 1–4: brainstorm events, timeline, commands, read models
|
|
238
|
-
/event-modeling model # steps 5–7: swimlanes, patterns, completeness check
|
|
239
|
-
/event-modeling slice # deep implementation specs, one per slice (frontmatter via `em slice new`)
|
|
240
|
-
/event-modeling watch # open a live browser view for team modeling sessions
|
|
241
|
-
/event-modeling validate # run em validate and resolve all diagnostics
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
Sessions are **resumable** — the skill checkpoints progress in `.event-modeling.md` so you can continue across conversations.
|
|
245
|
-
|
|
246
|
-
For a complete worked example (a headless CPQ system with ~50 slices), see the [em-with-ai repository](https://github.com/milehimikey/em-with-ai).
|
|
247
|
-
|
|
248
93
|
## License
|
|
249
94
|
|
|
250
95
|
[MIT](LICENSE) © milehimikey
|
package/dist/cli.js
CHANGED
|
@@ -4,24 +4,19 @@ import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
|
4
4
|
import { cp, mkdir } from "node:fs/promises";
|
|
5
5
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { Command
|
|
7
|
+
import { Command } from "commander";
|
|
8
8
|
import { compile } from "./pipeline.js";
|
|
9
9
|
import { ParseError } from "./parser/parser.js";
|
|
10
10
|
import { renderDot, formatFromPath } from "./render/render.js";
|
|
11
11
|
import { watchFile } from "./render/watch.js";
|
|
12
|
+
import { startLiveServer } from "./render/serve.js";
|
|
12
13
|
import { formatDiagnostic, hasErrors } from "./model/validate.js";
|
|
13
|
-
import { loadSliceDocs } from "./model/sliceDoc.js";
|
|
14
|
-
import { validateSliceDocs, VALID_PATTERNS, VALID_STATUSES } from "./model/validateSliceDocs.js";
|
|
15
|
-
import { newSlice } from "./slice/generate.js";
|
|
16
|
-
import { syncAll, syncSlice } from "./slice/sync.js";
|
|
17
|
-
import { listSliceRecords, listSlices, searchSliceRecords, searchSlices, showSlice, updateSlice, } from "./slice/query.js";
|
|
18
|
-
import { applyMigration, assertCleanWorkingTree, formatMigrationReport, planMigration, } from "./migrate/migrate.js";
|
|
19
14
|
import { STARTER_EM } from "./templates.js";
|
|
20
15
|
const program = new Command();
|
|
21
16
|
program
|
|
22
17
|
.name("em")
|
|
23
18
|
.description("Event Modeling CLI — slice-first DSL rendered as a strict Graphviz grid")
|
|
24
|
-
.version("1.
|
|
19
|
+
.version("1.2.0");
|
|
25
20
|
program
|
|
26
21
|
.command("init")
|
|
27
22
|
.description("scaffold a starter .em model")
|
|
@@ -75,9 +70,12 @@ program
|
|
|
75
70
|
.option("-o, --out <path>", "output path (extension picks the format)")
|
|
76
71
|
.option("-T, --format <fmt>", "output format (svg, png, pdf, ...)")
|
|
77
72
|
.option("--keep-empty-lanes", "keep the API lane even when empty")
|
|
73
|
+
.option("--serve", "serve a live viewer with instant push-reload (no polling)")
|
|
74
|
+
.option("--port <n>", "port for --serve (default 5173)", (v) => parseInt(v, 10))
|
|
78
75
|
.action(async (file, opts) => {
|
|
79
76
|
const out = opts.out ?? defaultOut(file, opts.format ?? "svg");
|
|
80
77
|
const fmt = opts.format ?? formatFromPath(out);
|
|
78
|
+
let server;
|
|
81
79
|
const build = async () => {
|
|
82
80
|
const started = Date.now();
|
|
83
81
|
try {
|
|
@@ -92,12 +90,25 @@ program
|
|
|
92
90
|
}
|
|
93
91
|
await renderDot(dot, model, out, fmt, dirname(file));
|
|
94
92
|
console.log(`rendered ${out} (${Date.now() - started}ms)`);
|
|
93
|
+
server?.notify();
|
|
95
94
|
}
|
|
96
95
|
catch (e) {
|
|
97
96
|
reportError(e);
|
|
98
97
|
}
|
|
99
98
|
};
|
|
100
99
|
await build();
|
|
100
|
+
if (opts.serve) {
|
|
101
|
+
try {
|
|
102
|
+
// Serve the directory the SVG is written to — that's what the browser
|
|
103
|
+
// fetches, and note "..." links inside the SVG resolve relative to it.
|
|
104
|
+
server = await startLiveServer({ dir: dirname(resolve(out)), port: opts.port });
|
|
105
|
+
console.log(`→ live view: ${server.url}/?svg=${encodeURIComponent(basename(out))}`);
|
|
106
|
+
console.log(" open it in a browser and share your screen");
|
|
107
|
+
}
|
|
108
|
+
catch (e) {
|
|
109
|
+
reportError(e);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
101
112
|
watchFile(file, build);
|
|
102
113
|
console.log(`watching ${file} … (ctrl-c to stop)`);
|
|
103
114
|
});
|
|
@@ -105,13 +116,8 @@ program
|
|
|
105
116
|
.command("validate")
|
|
106
117
|
.description("check a model against event-modeling rules")
|
|
107
118
|
.argument("<file>", "input .em file")
|
|
108
|
-
.
|
|
109
|
-
|
|
110
|
-
const { model, diagnostics } = compileFile(file);
|
|
111
|
-
if (!opts.skipSlices) {
|
|
112
|
-
const docs = loadSliceDocs(dirname(file));
|
|
113
|
-
diagnostics.push(...validateSliceDocs(model, file, docs));
|
|
114
|
-
}
|
|
119
|
+
.action((file) => {
|
|
120
|
+
const { diagnostics } = compileFile(file);
|
|
115
121
|
printDiagnostics(diagnostics);
|
|
116
122
|
if (hasErrors(diagnostics))
|
|
117
123
|
process.exit(1);
|
|
@@ -139,198 +145,6 @@ skill
|
|
|
139
145
|
console.log(`installed event-modeling skill → ${dest}`);
|
|
140
146
|
console.log("in Claude Code, run /event-modeling to start a guided session");
|
|
141
147
|
});
|
|
142
|
-
const slice = program.command("slice").description("manage slice design docs (slices/*.md)");
|
|
143
|
-
slice
|
|
144
|
-
.command("new")
|
|
145
|
-
.description("scaffold a new slice doc with injected frontmatter")
|
|
146
|
-
.argument("<name>", 'slice title, e.g. "Place Order"')
|
|
147
|
-
.option("--dir <path>", "model directory", ".")
|
|
148
|
-
.option("--model <path>", "explicit .em file (default: the only .em in --dir)")
|
|
149
|
-
.option("--config <path>", "explicit em.config.json path")
|
|
150
|
-
.addOption(new Option("--pattern <pattern>", "slice pattern").choices(VALID_PATTERNS).default("state-change"))
|
|
151
|
-
.option("--id <id>", "explicit slice id (default: kebab-case of the name)")
|
|
152
|
-
.option("--slice-element <id>", "the .em Element.id this doc documents, if already known")
|
|
153
|
-
.option("-f, --force", "overwrite an existing doc with this id")
|
|
154
|
-
.action((name, opts) => {
|
|
155
|
-
try {
|
|
156
|
-
const result = newSlice(name, {
|
|
157
|
-
dir: opts.dir,
|
|
158
|
-
modelPath: opts.model,
|
|
159
|
-
configPath: opts.config,
|
|
160
|
-
pattern: opts.pattern,
|
|
161
|
-
id: opts.id,
|
|
162
|
-
sliceElement: opts.sliceElement,
|
|
163
|
-
force: opts.force,
|
|
164
|
-
});
|
|
165
|
-
console.log(`wrote ${result.path} (id: ${result.id})`);
|
|
166
|
-
}
|
|
167
|
-
catch (e) {
|
|
168
|
-
reportError(e);
|
|
169
|
-
process.exit(1);
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
slice
|
|
173
|
-
.command("sync")
|
|
174
|
-
.description("recompute generated frontmatter fields (commands/events/readModels/contexts/personas/triggers) from the .em")
|
|
175
|
-
.argument("[id]", "slice id to sync (omit with --all)")
|
|
176
|
-
.option("--dir <path>", "model directory", ".")
|
|
177
|
-
.option("--model <path>", "explicit .em file")
|
|
178
|
-
.option("--all", "sync every slice doc in the directory")
|
|
179
|
-
.action((id, opts) => {
|
|
180
|
-
try {
|
|
181
|
-
if (opts.all) {
|
|
182
|
-
const { synced, skipped } = syncAll({ dir: opts.dir, modelPath: opts.model });
|
|
183
|
-
for (const s of synced)
|
|
184
|
-
console.log(`synced ${s.path}${s.changed ? "" : " (no changes)"}`);
|
|
185
|
-
for (const s of skipped)
|
|
186
|
-
console.warn(` skip ${s.file}: ${s.reason}`);
|
|
187
|
-
}
|
|
188
|
-
else {
|
|
189
|
-
if (!id) {
|
|
190
|
-
console.error("pass a slice id, or --all");
|
|
191
|
-
process.exit(1);
|
|
192
|
-
}
|
|
193
|
-
const result = syncSlice(id, { dir: opts.dir, modelPath: opts.model });
|
|
194
|
-
console.log(`synced ${result.path}${result.changed ? "" : " (no changes)"}`);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
catch (e) {
|
|
198
|
-
reportError(e);
|
|
199
|
-
process.exit(1);
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
slice
|
|
203
|
-
.command("list")
|
|
204
|
-
.description("list slice docs from frontmatter only (no .em parse, no body read)")
|
|
205
|
-
.option("--dir <path>", "model directory", ".")
|
|
206
|
-
.addOption(new Option("--status <status>", "filter by status").choices(VALID_STATUSES))
|
|
207
|
-
.addOption(new Option("--pattern <pattern>", "filter by pattern").choices(VALID_PATTERNS))
|
|
208
|
-
.option("--context <context>", "filter by context")
|
|
209
|
-
.option("--tag <tag>", "filter by tag")
|
|
210
|
-
.option("--format <fmt>", "table | json", "table")
|
|
211
|
-
.option("--full", "emit every frontmatter field per slice (requires --format json)")
|
|
212
|
-
.action((opts) => {
|
|
213
|
-
const filter = {
|
|
214
|
-
dir: opts.dir,
|
|
215
|
-
status: opts.status,
|
|
216
|
-
pattern: opts.pattern,
|
|
217
|
-
context: opts.context,
|
|
218
|
-
tag: opts.tag,
|
|
219
|
-
};
|
|
220
|
-
if (opts.full) {
|
|
221
|
-
requireJson(opts.format);
|
|
222
|
-
console.log(JSON.stringify(listSliceRecords(filter), null, 2));
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
printSlices(listSlices(filter), opts.format);
|
|
226
|
-
});
|
|
227
|
-
slice
|
|
228
|
-
.command("show")
|
|
229
|
-
.description("show one slice doc's frontmatter")
|
|
230
|
-
.argument("<id>", "slice id")
|
|
231
|
-
.option("--dir <path>", "model directory", ".")
|
|
232
|
-
.option("--format <fmt>", "table | json", "table")
|
|
233
|
-
.option("--body", "include the doc body")
|
|
234
|
-
.action((id, opts) => {
|
|
235
|
-
const doc = showSlice(id, { dir: opts.dir });
|
|
236
|
-
if (!doc) {
|
|
237
|
-
console.error(`no slice doc with id "${id}" found in ${opts.dir}`);
|
|
238
|
-
process.exit(1);
|
|
239
|
-
}
|
|
240
|
-
if (opts.format === "json") {
|
|
241
|
-
const payload = opts.body ? { ...doc.frontmatter, body: doc.body } : doc.frontmatter;
|
|
242
|
-
console.log(JSON.stringify(payload, null, 2));
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
console.log(`# ${doc.file}`);
|
|
246
|
-
for (const [k, v] of Object.entries(doc.frontmatter ?? {})) {
|
|
247
|
-
console.log(`${k}: ${JSON.stringify(v)}`);
|
|
248
|
-
}
|
|
249
|
-
if (opts.body) {
|
|
250
|
-
console.log("");
|
|
251
|
-
console.log(doc.body);
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
slice
|
|
255
|
-
.command("search")
|
|
256
|
-
.description("search slice docs by frontmatter only (title/tags/commands/events/readModels)")
|
|
257
|
-
.argument("[query]", "free-text query", "")
|
|
258
|
-
.option("--dir <path>", "model directory", ".")
|
|
259
|
-
.addOption(new Option("--status <status>", "filter by status").choices(VALID_STATUSES))
|
|
260
|
-
.addOption(new Option("--pattern <pattern>", "filter by pattern").choices(VALID_PATTERNS))
|
|
261
|
-
.option("--context <context>", "filter by context")
|
|
262
|
-
.option("--tag <tag>", "filter by tag")
|
|
263
|
-
.option("--format <fmt>", "table | json", "table")
|
|
264
|
-
.option("--full", "emit every frontmatter field per slice (requires --format json)")
|
|
265
|
-
.action((query, opts) => {
|
|
266
|
-
const filter = {
|
|
267
|
-
dir: opts.dir,
|
|
268
|
-
status: opts.status,
|
|
269
|
-
pattern: opts.pattern,
|
|
270
|
-
context: opts.context,
|
|
271
|
-
tag: opts.tag,
|
|
272
|
-
};
|
|
273
|
-
if (opts.full) {
|
|
274
|
-
requireJson(opts.format);
|
|
275
|
-
console.log(JSON.stringify(searchSliceRecords(query, filter), null, 2));
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
printSlices(searchSlices(query, filter), opts.format);
|
|
279
|
-
});
|
|
280
|
-
slice
|
|
281
|
-
.command("update")
|
|
282
|
-
.description("write status/version changes back into a slice doc's frontmatter")
|
|
283
|
-
.argument("<id>", "slice id")
|
|
284
|
-
.option("--dir <path>", "model directory", ".")
|
|
285
|
-
.addOption(new Option("--status <status>", "new status").choices(VALID_STATUSES))
|
|
286
|
-
.option("--bump-version", "increment version by 1")
|
|
287
|
-
.action((id, opts) => {
|
|
288
|
-
try {
|
|
289
|
-
const result = updateSlice(id, {
|
|
290
|
-
dir: opts.dir,
|
|
291
|
-
status: opts.status,
|
|
292
|
-
bumpVersion: opts.bumpVersion,
|
|
293
|
-
});
|
|
294
|
-
console.log(`updated ${result.path}`);
|
|
295
|
-
}
|
|
296
|
-
catch (e) {
|
|
297
|
-
reportError(e);
|
|
298
|
-
process.exit(1);
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
program
|
|
302
|
-
.command("migrate")
|
|
303
|
-
.description("upgrade a pre-1.0 model directory to the frontmatter standard (docs/1.0.0-spec.md)")
|
|
304
|
-
.argument("<model-dir>", "model directory")
|
|
305
|
-
.option("--model <path>", "explicit .em file")
|
|
306
|
-
.option("--dry-run", "print the plan/report without writing")
|
|
307
|
-
.option("--force", "proceed even with uncommitted changes or no git repo")
|
|
308
|
-
.option("--report <path>", "write the migration report to a file instead of stdout")
|
|
309
|
-
.action((dir, opts) => {
|
|
310
|
-
try {
|
|
311
|
-
if (!opts.dryRun)
|
|
312
|
-
assertCleanWorkingTree(dir, opts.force);
|
|
313
|
-
const plan = planMigration(dir, { modelPath: opts.model });
|
|
314
|
-
const report = formatMigrationReport(plan);
|
|
315
|
-
if (opts.report) {
|
|
316
|
-
writeFileSync(opts.report, report);
|
|
317
|
-
console.log(`wrote migration report -> ${opts.report}`);
|
|
318
|
-
}
|
|
319
|
-
else {
|
|
320
|
-
console.log(report);
|
|
321
|
-
}
|
|
322
|
-
if (opts.dryRun) {
|
|
323
|
-
console.log("dry run — no files written");
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
applyMigration(plan);
|
|
327
|
-
console.log("migration applied");
|
|
328
|
-
}
|
|
329
|
-
catch (e) {
|
|
330
|
-
reportError(e);
|
|
331
|
-
process.exit(1);
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
148
|
program.parseAsync().catch((e) => {
|
|
335
149
|
reportError(e);
|
|
336
150
|
process.exit(1);
|
|
@@ -383,24 +197,4 @@ function printDiagnostics(diags) {
|
|
|
383
197
|
function reportError(e) {
|
|
384
198
|
console.error(e instanceof Error ? e.message : String(e));
|
|
385
199
|
}
|
|
386
|
-
/** --full emits a shape only JSON can carry; fail loudly rather than silently ignoring it. */
|
|
387
|
-
function requireJson(format) {
|
|
388
|
-
if (format !== "json") {
|
|
389
|
-
console.error("--full requires --format json");
|
|
390
|
-
process.exit(1);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
function printSlices(results, format) {
|
|
394
|
-
if (format === "json") {
|
|
395
|
-
console.log(JSON.stringify(results, null, 2));
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
if (results.length === 0) {
|
|
399
|
-
console.log("no matching slices");
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
for (const s of results) {
|
|
403
|
-
console.log(`${s.id}\t${s.pattern ?? "?"}\t${s.status ?? "?"}\tv${s.version ?? "?"}\t${s.title}`);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
200
|
//# sourceMappingURL=cli.js.map
|