@milehimikey/em 1.3.0 → 1.5.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 +99 -22
- package/.claude/skills/event-modeling/reference/conform.md +230 -0
- package/.claude/skills/event-modeling/reference/em-dsl.md +93 -26
- package/.claude/skills/event-modeling/reference/extract.md +12 -5
- package/.claude/skills/event-modeling/reference/methodology.md +103 -21
- package/.claude/skills/event-modeling/templates/conformance-report.md +69 -0
- package/.claude/skills/event-modeling/templates/model-readme.md +9 -0
- package/.claude/skills/event-modeling/templates/slice.md +15 -1
- package/.claude/skills/event-modeling/templates/state.md +1 -0
- package/README.md +17 -4
- package/dist/cli/changelog-git.d.ts +53 -0
- package/dist/cli/changelog-git.js +112 -0
- package/dist/cli/changelog-git.js.map +1 -0
- package/dist/cli.js +103 -5
- package/dist/cli.js.map +1 -1
- package/dist/emit/changelog.d.ts +57 -0
- package/dist/emit/changelog.js +143 -0
- package/dist/emit/changelog.js.map +1 -0
- package/dist/emit/diffJson.d.ts +17 -0
- package/dist/emit/diffJson.js +74 -0
- package/dist/emit/diffJson.js.map +1 -0
- package/dist/emit/json.d.ts +1 -0
- package/dist/emit/json.js +1 -1
- package/dist/emit/json.js.map +1 -1
- package/dist/model/edges.js +7 -16
- package/dist/model/edges.js.map +1 -1
- package/dist/model/validate.js +154 -0
- package/dist/model/validate.js.map +1 -1
- package/dist/render/drawEdges.d.ts +3 -1
- package/dist/render/drawEdges.js +119 -7
- package/dist/render/drawEdges.js.map +1 -1
- package/dist/render/render.js +5 -3
- package/dist/render/render.js.map +1 -1
- package/dist/render/svgGeometry.d.ts +13 -0
- package/dist/render/svgGeometry.js +47 -0
- package/dist/render/svgGeometry.js.map +1 -1
- package/package.json +3 -2
|
@@ -70,10 +70,16 @@ it is fine to finish a round with `# TBD`s still parked.
|
|
|
70
70
|
*actual* actors/callers and the *real* UI or API surfaces (headless systems: no
|
|
71
71
|
`ui`/`persona` — use write/read translations per `em-dsl.md`). **First `em render` here.**
|
|
72
72
|
- **R3 — Commands / inputs.** For each event, the request that actually produces it today
|
|
73
|
-
(imperative name)
|
|
73
|
+
(imperative name), *and what triggers it* — the real screen, or the scheduler/listener that
|
|
74
|
+
fires it (model that as a reaction in the preceding slice, per R5). Unclear trigger → `# TBD`,
|
|
75
|
+
not a plausible guess; expect a *command has nothing that triggers it* warning until R5 fills
|
|
76
|
+
it in, and don't invent a screen to silence it.
|
|
74
77
|
- **R4 — Read models / outputs.** The projections/queries the system actually serves; wire
|
|
75
|
-
`from "Event"` (repeat read models span-1 per `em-dsl.md`
|
|
76
|
-
each round from here.
|
|
78
|
+
`from "Event"` (repeat read models span-1 per `em-dsl.md`, using `view X again` after the
|
|
79
|
+
first). **Start running `em validate`** each round from here. Expect *event is not read by any
|
|
80
|
+
read model* warnings: real systems do record fire-and-forget events. In an as-is model that is
|
|
81
|
+
a **finding about the system**, not a modelling mistake — never invent a reader to silence it
|
|
82
|
+
(that would be desired state). Park it in the Decisions log and carry the warning.
|
|
77
83
|
- **R5 — Boundaries & reactions.** Current automations and integrations as-is: schedulers,
|
|
78
84
|
listeners, outbound syncs, inbound webhooks. Model each as a reaction with the two-slice
|
|
79
85
|
`reaction → command → event` split and the slice-ordering rules in `em-dsl.md` — even when
|
|
@@ -103,8 +109,9 @@ one for a sprawling one. Record any deviation in the Decisions log.
|
|
|
103
109
|
## Completion & handoff
|
|
104
110
|
|
|
105
111
|
Extraction is complete when: the model renders; `em validate` is clean (or remaining warnings
|
|
106
|
-
are consciously accepted in the Decisions log
|
|
107
|
-
|
|
112
|
+
are consciously accepted in the Decisions log — unread events in an as-is model are the usual
|
|
113
|
+
accepted case, and each one is worth naming explicitly as a finding); every unknown is a parked
|
|
114
|
+
`# TBD`; and the user has confirmed the model reflects today's behavior. Then:
|
|
108
115
|
|
|
109
116
|
1. State file: set phase progress (extraction rounds checked), log the completion decision.
|
|
110
117
|
Seed `README.md`'s **slice index** (every slice, pattern, status `none`).
|
|
@@ -16,6 +16,20 @@ three ways:
|
|
|
16
16
|
3. There is **no other way** for information to flow. Every box on the diagram is one of:
|
|
17
17
|
UI screen, command, event, read model, or automation/processor.
|
|
18
18
|
|
|
19
|
+
Concretely, that leaves exactly six legal connections, and `em` infers only these:
|
|
20
|
+
`ui → command`, `command → event`, `event → read model`, `read model → ui`,
|
|
21
|
+
`read model → reaction`, `reaction → command`. Every other pair is a rule violation, and an
|
|
22
|
+
explicit `arrow` writing one is a validation error. The two worth naming, because they are the
|
|
23
|
+
tempting shortcuts:
|
|
24
|
+
|
|
25
|
+
- **`command → read model`** — the CQRS violation. A write is only ever visible to a reader
|
|
26
|
+
through the event it recorded. Put the event between them.
|
|
27
|
+
- **`read model → command`** — reads never drive a write directly. A processor or translation
|
|
28
|
+
watches the read model and issues the command, across two slices.
|
|
29
|
+
|
|
30
|
+
If you find yourself wanting an arrow the patterns don't allow, the model is missing an
|
|
31
|
+
**element**, not an arrow.
|
|
32
|
+
|
|
19
33
|
**Automations and translations are not exceptions to rule 1.** They are *reactions* — a processor
|
|
20
34
|
or an adapter that wakes up, decides something must change, and **issues a command** to do it. They
|
|
21
35
|
never record an event themselves. A reaction box always points at a **command** (in the next
|
|
@@ -37,7 +51,24 @@ Every slice is exactly one of these. In `em`, each maps to a specific shape (see
|
|
|
37
51
|
### 1. State Change (Command pattern)
|
|
38
52
|
**UI → Command → Event.** A user (or automation) submits a command; the system validates it
|
|
39
53
|
against invariants and records one or more events.
|
|
40
|
-
-
|
|
54
|
+
- **A State Change never travels alone, and it's joined at both ends.** Something must *trigger*
|
|
55
|
+
the command — the `ui` it's issued from, or (patterns 3 and 4) the reaction in the slice before
|
|
56
|
+
it — and its event needs a read model that projects it. So the unit of work is a State Change
|
|
57
|
+
slice *plus* the State View slice that reads its event. Write them together; a command nothing
|
|
58
|
+
points at is a write nobody can start, an unread event is a write nobody can see, and
|
|
59
|
+
`em validate` warns on each.
|
|
60
|
+
- `em` shape (one element per line — there is no one-line slice form):
|
|
61
|
+
```em
|
|
62
|
+
slice "Do The Thing" {
|
|
63
|
+
ui X @Persona
|
|
64
|
+
command Y
|
|
65
|
+
event Z @Context
|
|
66
|
+
}
|
|
67
|
+
slice "See The Result" { # the State View slice that reads Z — pattern 2, but required here
|
|
68
|
+
view V from "Z"
|
|
69
|
+
ui Screen @Persona
|
|
70
|
+
}
|
|
71
|
+
```
|
|
41
72
|
- This is where **invariants** live — the command is rejected if a rule is violated.
|
|
42
73
|
- Socratic prompts: *"What does the user do here? What request are they making? What must be
|
|
43
74
|
true for it to succeed? What fact gets recorded when it does? What gets rejected and why?"*
|
|
@@ -55,19 +86,30 @@ displays. Read-only; changes no state.
|
|
|
55
86
|
- **Repeat read models across the timeline.** A read model is drawn fresh in **every** slice where
|
|
56
87
|
it is read — after the events that update it, before the actions that consume it — so the diagram
|
|
57
88
|
shows state flowing left-to-right (the information-completeness staircase). The same read-model
|
|
58
|
-
name recurring is intentional and **renders cleanly
|
|
59
|
-
|
|
60
|
-
|
|
89
|
+
name recurring is intentional and **renders cleanly**. Declare every instance after the first with
|
|
90
|
+
**`view V again from "..."`**: `again` instances are exempt from the duplicate-name warning even
|
|
91
|
+
when something references them, and each reference resolves to the right instance. (A plain repeat
|
|
92
|
+
only stays warning-free while nothing references it by name, and resolves to the *first*
|
|
93
|
+
declaration when something does.) **Wire each event to a read model
|
|
61
94
|
exactly once:** a repeated instance's `from` lists only the **new** events since the previous
|
|
62
95
|
instance (not cumulative) — otherwise an event draws a duplicate arrow to the same read model at
|
|
63
96
|
every repeat. (An event may still feed several *different* read models, once each.)
|
|
97
|
+
- **Instances are never connected to one another.** There is no arrow between two appearances of one
|
|
98
|
+
read model, and an explicit one is a validation error. The repeat is an ergonomic device for
|
|
99
|
+
showing the same read model at successive points in time — continuity is implied by the shared
|
|
100
|
+
name, and the events arriving at each instance are what show it changing. An arrow between them
|
|
101
|
+
would say the read model feeds itself.
|
|
64
102
|
- **Place each repeat immediately after its feeding event (span-1).** The cleanest staircase puts a
|
|
65
103
|
read-model instance right after each event that updates it, sourcing **only that one adjacent
|
|
66
104
|
event** — every `event → read model` arrow is then short and forward-flowing. A read model sitting
|
|
67
105
|
far from its source events (e.g. a "list/queue" read placed early but fed by late events) draws
|
|
68
|
-
long arrows that
|
|
69
|
-
|
|
70
|
-
event that
|
|
106
|
+
long arrows that sweep across the diagram. The renderer routes them *around* intervening boxes
|
|
107
|
+
rather than through them, so they no longer *look* like a forbidden read→read link — but the
|
|
108
|
+
distance itself is the problem: the arrowhead lands columns away from the event that produced it,
|
|
109
|
+
so **the write slice reads as dangling**, as if nothing consumed its event. A reader has to trace
|
|
110
|
+
the line across the diagram to see otherwise. The fix is never a `view → view` edge; it's to
|
|
111
|
+
repeat the read model next to each event that feeds it, and to keep a sub-flow that detours into
|
|
112
|
+
another context together rather than parking it at the end of the model.
|
|
71
113
|
- Socratic prompts: *"What does the consumer need to see to make their next decision? Which past
|
|
72
114
|
events provide that information? Is this a screen or an API read? What's the freshness/consistency
|
|
73
115
|
expectation?"*
|
|
@@ -76,10 +118,24 @@ displays. Read-only; changes no state.
|
|
|
76
118
|
**Read Model → Processor → (next slice) Command → Event.** The system reacts on its own: a
|
|
77
119
|
processor watches a read model (a "to-do list" of work) and **issues a command** — it never
|
|
78
120
|
records an event directly.
|
|
79
|
-
- `em` shape: **two slices
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
121
|
+
- `em` shape: **two slices**, then the read slice that consumes the event — three in all:
|
|
122
|
+
```em
|
|
123
|
+
slice "Todo" {
|
|
124
|
+
view Todo from "..."
|
|
125
|
+
processor P
|
|
126
|
+
}
|
|
127
|
+
slice "Do It" { # the triggered command goes in the NEXT slice
|
|
128
|
+
command C
|
|
129
|
+
event E @Context
|
|
130
|
+
}
|
|
131
|
+
slice "Todo — done" {
|
|
132
|
+
view Todo again from "E" # every event needs a reader; here it clears the to-do list
|
|
133
|
+
ui Work Board @Persona # ...and every read model needs a consumer
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
The processor band holds no event; the command in the next slice produces it. Keeping the
|
|
137
|
+
triggered command in the same slice as the processor is a validation warning — always split it.
|
|
138
|
+
Forgetting the third slice leaves `E` unread, which is also a warning.
|
|
83
139
|
- Socratic prompts: *"What should happen without a human? What condition triggers it? What work
|
|
84
140
|
list does the processor watch? What command does it fire? What if it fails or retries?"*
|
|
85
141
|
|
|
@@ -92,9 +148,22 @@ trigger forms:
|
|
|
92
148
|
from outside the model, so the translation has no internal `from`.
|
|
93
149
|
- **Internally triggered** (reacting to the modeled system's own state, e.g. pushing data out):
|
|
94
150
|
`read model → translation → command → event`. The translation reads a **view** via `from`.
|
|
95
|
-
- `em` shape: **two slices**, exactly like an automation
|
|
96
|
-
|
|
97
|
-
|
|
151
|
+
- `em` shape: **two slices**, exactly like an automation, plus the read slice for the event:
|
|
152
|
+
```em
|
|
153
|
+
slice "Boundary" {
|
|
154
|
+
view Source from "..." # internally triggered only; omit for an external trigger
|
|
155
|
+
translation T from "Source"
|
|
156
|
+
}
|
|
157
|
+
slice "Record It" {
|
|
158
|
+
command C
|
|
159
|
+
event E @Context
|
|
160
|
+
}
|
|
161
|
+
slice "Source — recorded" {
|
|
162
|
+
view Source again from "E" # every event needs a reader
|
|
163
|
+
ui Status Screen @Persona # ...and every read model needs a consumer
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
The translation band never carries an event.
|
|
98
167
|
- Socratic prompts: *"What boundary are we crossing, and which way? What outside system or context
|
|
99
168
|
feeds us (or do we feed)? In what format? How do we know its data is trustworthy/complete? What
|
|
100
169
|
internal **command** does it trigger, and what event does that command record?"*
|
|
@@ -170,17 +239,30 @@ dedicated `slice` phase writes the full rich spec (see `templates/slice.md`).
|
|
|
170
239
|
|
|
171
240
|
### Step 7 — Evaluate Completeness *(model)*
|
|
172
241
|
Walk the whole model with stakeholders and check for loose ends:
|
|
242
|
+
- Every **command** has something that **triggers** it — a `ui` in its slice, or a reaction in the
|
|
243
|
+
slice before it. A command nothing points at is a write nobody can start. `em validate` warns.
|
|
173
244
|
- Every **command** produces at least one **event**.
|
|
174
|
-
- Every **view** has at least one source event
|
|
175
|
-
|
|
245
|
+
- Every **view** has at least one source event, **and a consumer** — a `ui`, a reaction watching it,
|
|
246
|
+
or (headless) a read translation. Every *instance* of a repeated view, not just the last one.
|
|
247
|
+
A read model nothing displays is information projected out and then dropped. `em validate` warns.
|
|
248
|
+
- Every **event** is **read by a read model** — a `view` naming it in `from` (any `again` instance
|
|
249
|
+
counts), or a `view` with no `from` sitting in its slice. A reaction consuming it does **not**
|
|
250
|
+
count: reactions read views, not events. An event nothing projects is a write nobody can see, so
|
|
251
|
+
there was no point recording it. `em validate` warns on each one.
|
|
176
252
|
- Every **UI** is reachable and leads somewhere.
|
|
253
|
+
- Every connection is one of the six legal pairs (`ui → command`, `command → event`,
|
|
254
|
+
`event → read model`, `read model → ui`, `read model → reaction`, `reaction → command`). No
|
|
255
|
+
arrow between two instances of one read model.
|
|
177
256
|
- Automations **and** translations are split correctly (reaction → command → event, never a
|
|
178
257
|
reaction wired straight to an event); translations cover every external input.
|
|
179
|
-
- Run `em validate` and resolve all errors and warnings.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
258
|
+
- Run `em validate` and resolve all errors and warnings. It now catches illegal `arrow` kind pairs
|
|
259
|
+
(error) and unread events (warning), but it still does **not** catch a translation/automation that
|
|
260
|
+
emits an event directly — verify the two-slice split by hand.
|
|
261
|
+
- Prompts: *"Is there any event nobody reads — and if the business genuinely never looks at it, why
|
|
262
|
+
are we recording it? Any command that just happens, with nobody and nothing asking for it? Any
|
|
263
|
+
screen with no way in or out? Any command that records nothing? Any
|
|
264
|
+
external system we haven't translated? Any translation or automation that records an event instead
|
|
265
|
+
of triggering a command?"*
|
|
184
266
|
|
|
185
267
|
---
|
|
186
268
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Conformance report skeleton. One per conform run, stored at
|
|
3
|
+
<model-name>/conformance/<YYYY-MM-DD>-report.md (see reference/conform.md).
|
|
4
|
+
Fill every section; leave "none" rather than deleting a section with nothing to report.
|
|
5
|
+
Replace bracketed placeholders; delete guidance comments before finishing.
|
|
6
|
+
-->
|
|
7
|
+
|
|
8
|
+
# Conformance Report — {{Model Name}} — {{YYYY-MM-DD}}
|
|
9
|
+
|
|
10
|
+
- **Model:** `{{model-name}}.em`
|
|
11
|
+
- **Target repo:** {{repo path or URL}} @ `{{revision}}`
|
|
12
|
+
- **Previous conformance revision:** {{prior target-repo revision, or "never (first run)"}}
|
|
13
|
+
- **Scope:** {{full | diff-scoped since <prior revision>}}
|
|
14
|
+
- **Slices checked:** {{comma-separated slice names, or "all implemented slices"}}
|
|
15
|
+
- **Slices skipped (not yet implemented):** {{list, or "none"}}
|
|
16
|
+
|
|
17
|
+
## Summary
|
|
18
|
+
|
|
19
|
+
| Surface | Real drift | Model gap | Internal inconsistency | Uncertainty |
|
|
20
|
+
|---|---|---|---|---|
|
|
21
|
+
| Structural | {{n}} | {{n}} | — | {{n}} |
|
|
22
|
+
| Spec | {{n}} | {{n}} | — | {{n}} |
|
|
23
|
+
| Internal | — | — | {{n}} | {{n}} |
|
|
24
|
+
|
|
25
|
+
{{One or two sentences: overall read — clean, mostly clean with N items, or notably drifted.}}
|
|
26
|
+
|
|
27
|
+
## Findings
|
|
28
|
+
|
|
29
|
+
<!-- One subsection per finding. Group however reads clearest (by slice, or by surface) but keep
|
|
30
|
+
every field below for every finding. Order doesn't matter; classification does. -->
|
|
31
|
+
|
|
32
|
+
### {{n}}. {{Short finding title}}
|
|
33
|
+
|
|
34
|
+
- **Surface:** {{structural | spec | internal}}
|
|
35
|
+
- **Classification:** {{real drift | model gap | internal inconsistency}}
|
|
36
|
+
- **Slice:** {{slice name}}
|
|
37
|
+
- **Evidence:** {{file path(s) and line/symbol, or the `em diff --json` entry (type + name)}}
|
|
38
|
+
- **Claim:** {{what the model/doc says vs. what the code/doc shows — the actual disagreement}}
|
|
39
|
+
- **Proposed red note:** `issue "conformance: {{text}}"` on {{element}} (slice "{{slice}}")
|
|
40
|
+
<!-- Omit this line entirely for internal-inconsistency findings with no clear side to flag,
|
|
41
|
+
or when the finding doesn't warrant a red note (say why instead). -->
|
|
42
|
+
|
|
43
|
+
## Scenario / invariant coverage
|
|
44
|
+
|
|
45
|
+
<!-- Per in-scope slice: every named invariant and every GWT scenario the doc claims, mapped to
|
|
46
|
+
its enforcement/test site. Leave a cell explicitly blank/"none found" rather than omitting
|
|
47
|
+
the row — gaps are the point of this table. -->
|
|
48
|
+
|
|
49
|
+
### {{Slice Name}}
|
|
50
|
+
|
|
51
|
+
| Claim | Type | Enforcement site | Test site | Status |
|
|
52
|
+
|---|---|---|---|---|
|
|
53
|
+
| {{INV-XXX-1 or scenario title}} | {{invariant \| scenario}} | {{file path / symbol, or "none found"}} | {{file path / symbol, or "none found"}} | {{covered \| gap \| uncertain}} |
|
|
54
|
+
|
|
55
|
+
## Uncertainties
|
|
56
|
+
|
|
57
|
+
<!-- Listed separately from Findings — these are NOT drift. No evidence either way; say what was
|
|
58
|
+
missing (time, clarity, an ambiguous test) so a follow-up run can pick it up. -->
|
|
59
|
+
|
|
60
|
+
- **{{structural | spec | internal}}** — {{what's uncertain}} — {{slice}} — {{why: e.g. "couldn't locate the test for INV-CHK-3, only its enforcement site"}}
|
|
61
|
+
|
|
62
|
+
<!-- Tag each with its surface so these reconcile against the Summary table's Uncertainty column. -->
|
|
63
|
+
|
|
64
|
+
## Applied this run
|
|
65
|
+
|
|
66
|
+
<!-- Filled in AFTER the user reviews Findings and says what to apply. Leave "none yet — awaiting
|
|
67
|
+
ratification" until that conversation happens. -->
|
|
68
|
+
|
|
69
|
+
{{List of ratified proposals actually applied to the `.em`/slice docs, or "none yet — awaiting ratification".}}
|
|
@@ -21,6 +21,15 @@ Static render: `em render {{model-name}}.em -o {{model-name}}.svg`
|
|
|
21
21
|
- **Automation** — Read Model → Processor → (next slice) Command → Event
|
|
22
22
|
- **Translation** — External input (or Read Model) → Translation → (next slice) Command → Event
|
|
23
23
|
|
|
24
|
+
Between them these are the only legal connections: `ui → command`, `command → event`,
|
|
25
|
+
`event → read model`, `read model → ui`, `read model → reaction`, `reaction → command`. A command
|
|
26
|
+
never reaches a read model directly — the event goes between them. Every slice is joined up at
|
|
27
|
+
both ends: something triggers each command (the screen it's issued from, or the reaction in the
|
|
28
|
+
slice before it), and every event a command records is read by some read model — so each State
|
|
29
|
+
Change slice is paired with the State View slice that projects its event. A read model repeated
|
|
30
|
+
along the timeline (`view X again`) shows the same projection at a later point; the instances are
|
|
31
|
+
never connected to one another.
|
|
32
|
+
|
|
24
33
|
## Slices
|
|
25
34
|
<!-- The canonical slice index — the ONE place slices are enumerated (the state file
|
|
26
35
|
points here rather than keeping its own copy). Keep it in sync as slice docs are
|
|
@@ -32,9 +32,20 @@ an event directly.}}
|
|
|
32
32
|
|-------|------|----------|--------------------|
|
|
33
33
|
| {{field}} | {{Type}} | {{yes/no}} | {{constraints, formats, ranges}} |
|
|
34
34
|
|
|
35
|
+
## Trigger
|
|
36
|
+
<!-- What issues this slice's command. Required: a command nothing points at is a write nobody
|
|
37
|
+
can start. Either the screen the user acts on (a `ui` in this slice), or the reaction that
|
|
38
|
+
issues it (an automation/processor/translation in the PREVIOUS slice). -->
|
|
39
|
+
**Triggered by:** {{screen `X` @Persona | processor `Y` in slice "Z"}}
|
|
40
|
+
|
|
35
41
|
## Event(s) Emitted
|
|
36
42
|
<!-- The immutable facts recorded. List each event and its payload. -->
|
|
37
43
|
**Event:** `{{Event Name}}` → context `{{Context}}`
|
|
44
|
+
**Read by:** {{which read model projects this event, and in which slice}}
|
|
45
|
+
<!-- Required, not optional. Every event must be read by a read model — an event nothing
|
|
46
|
+
projects is a write nobody can see, and `em validate` warns on it. A reaction consuming
|
|
47
|
+
it does NOT count: reactions read views, not events. If the honest answer is "nothing
|
|
48
|
+
reads it", that's a question for the business, not a field to leave blank. -->
|
|
38
49
|
|
|
39
50
|
| Field | Type | Immutable Fact? | Source / Notes |
|
|
40
51
|
|-------|------|-----------------|----------------|
|
|
@@ -43,7 +54,10 @@ an event directly.}}
|
|
|
43
54
|
## Read Model / View
|
|
44
55
|
<!-- For State View slices, and any read model this slice produces or feeds. -->
|
|
45
56
|
- **View:** `{{View Name}}` built from events: {{"Event A", "Event B"}}
|
|
46
|
-
- **Consumed by:** {{which UI screen or
|
|
57
|
+
- **Consumed by:** {{which UI screen, reaction, or read translation}}
|
|
58
|
+
<!-- "Consumed by" is required, not optional. A read model nothing displays or watches is
|
|
59
|
+
information projected out of the system and then dropped, and `em validate` warns. Every
|
|
60
|
+
instance of a repeated view needs its own consumer, not just the last one. -->
|
|
47
61
|
- **Freshness / consistency expectation:** {{real-time | eventual | on-demand}}
|
|
48
62
|
|
|
49
63
|
## Invariants / Business Rules
|
|
@@ -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}}
|
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.
|
|
65
|
-
|
|
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,11 +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 |
|
|
78
|
-
| [docs/ci.md](docs/ci.md) | Copy-paste
|
|
91
|
+
| [docs/ci.md](docs/ci.md) | Copy-paste CI recipes: validate `.em` changes, run conformance on a schedule |
|
|
79
92
|
| [docs/timeline.md](docs/timeline.md) | The Two Laws of the Timeline |
|
|
80
93
|
| [docs/ai-workflow.md](docs/ai-workflow.md) | The Claude Code skill: install, phases, artifacts |
|
|
81
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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"}
|