@milehimikey/em 1.4.0 → 1.6.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 (95) hide show
  1. package/.claude/skills/event-modeling/SKILL.md +121 -30
  2. package/.claude/skills/event-modeling/reference/conform.md +36 -7
  3. package/.claude/skills/event-modeling/reference/em-dsl.md +210 -48
  4. package/.claude/skills/event-modeling/reference/extract.md +24 -7
  5. package/.claude/skills/event-modeling/reference/methodology.md +129 -32
  6. package/.claude/skills/event-modeling/templates/conformance-report.md +9 -7
  7. package/.claude/skills/event-modeling/templates/model-readme.md +9 -0
  8. package/.claude/skills/event-modeling/templates/slice.md +20 -1
  9. package/.claude/skills/event-modeling/templates/state.md +8 -0
  10. package/README.md +1 -0
  11. package/dist/catalog/build.d.ts +33 -0
  12. package/dist/catalog/build.js +117 -0
  13. package/dist/catalog/build.js.map +1 -0
  14. package/dist/catalog/classify.d.ts +14 -0
  15. package/dist/catalog/classify.js +44 -0
  16. package/dist/catalog/classify.js.map +1 -0
  17. package/dist/catalog/pages.d.ts +45 -0
  18. package/dist/catalog/pages.js +174 -0
  19. package/dist/catalog/pages.js.map +1 -0
  20. package/dist/catalog/sliceDoc.d.ts +10 -0
  21. package/dist/catalog/sliceDoc.js +20 -0
  22. package/dist/catalog/sliceDoc.js.map +1 -0
  23. package/dist/cli/catalog-inputs.d.ts +16 -0
  24. package/dist/cli/catalog-inputs.js +18 -0
  25. package/dist/cli/catalog-inputs.js.map +1 -0
  26. package/dist/cli/glossary-inputs.d.ts +15 -0
  27. package/dist/cli/glossary-inputs.js +17 -0
  28. package/dist/cli/glossary-inputs.js.map +1 -0
  29. package/dist/cli/render-inputs.d.ts +27 -0
  30. package/dist/cli/render-inputs.js +41 -0
  31. package/dist/cli/render-inputs.js.map +1 -0
  32. package/dist/cli.js +188 -10
  33. package/dist/cli.js.map +1 -1
  34. package/dist/emit/diffJson.d.ts +1 -1
  35. package/dist/emit/diffJson.js +8 -1
  36. package/dist/emit/diffJson.js.map +1 -1
  37. package/dist/emit/dot.d.ts +7 -0
  38. package/dist/emit/dot.js +10 -4
  39. package/dist/emit/dot.js.map +1 -1
  40. package/dist/emit/glossaryJson.d.ts +13 -0
  41. package/dist/emit/glossaryJson.js +36 -0
  42. package/dist/emit/glossaryJson.js.map +1 -0
  43. package/dist/emit/json.d.ts +16 -2
  44. package/dist/emit/json.js +57 -6
  45. package/dist/emit/json.js.map +1 -1
  46. package/dist/emit/theme.d.ts +3 -0
  47. package/dist/emit/theme.js +19 -0
  48. package/dist/emit/theme.js.map +1 -1
  49. package/dist/model/diff.d.ts +36 -1
  50. package/dist/model/diff.js +198 -35
  51. package/dist/model/diff.js.map +1 -1
  52. package/dist/model/edges.d.ts +12 -1
  53. package/dist/model/edges.js +26 -24
  54. package/dist/model/edges.js.map +1 -1
  55. package/dist/model/glossary.d.ts +68 -0
  56. package/dist/model/glossary.js +138 -0
  57. package/dist/model/glossary.js.map +1 -0
  58. package/dist/model/model.d.ts +35 -0
  59. package/dist/model/model.js +41 -0
  60. package/dist/model/model.js.map +1 -1
  61. package/dist/model/validate.js +252 -1
  62. package/dist/model/validate.js.map +1 -1
  63. package/dist/parser/ast.d.ts +20 -0
  64. package/dist/parser/parser.js +158 -29
  65. package/dist/parser/parser.js.map +1 -1
  66. package/dist/render/drawEdges.d.ts +3 -1
  67. package/dist/render/drawEdges.js +119 -7
  68. package/dist/render/drawEdges.js.map +1 -1
  69. package/dist/render/drawNotes.d.ts +7 -3
  70. package/dist/render/drawNotes.js +75 -28
  71. package/dist/render/drawNotes.js.map +1 -1
  72. package/dist/render/render.d.ts +20 -1
  73. package/dist/render/render.js +57 -17
  74. package/dist/render/render.js.map +1 -1
  75. package/dist/render/serve.js +155 -0
  76. package/dist/render/serve.js.map +1 -1
  77. package/dist/render/sliceDiagram.d.ts +11 -0
  78. package/dist/render/sliceDiagram.js +198 -0
  79. package/dist/render/sliceDiagram.js.map +1 -0
  80. package/dist/render/sliceOverlay.d.ts +21 -0
  81. package/dist/render/sliceOverlay.js +111 -0
  82. package/dist/render/sliceOverlay.js.map +1 -0
  83. package/dist/render/sliceStatus.d.ts +5 -0
  84. package/dist/render/sliceStatus.js +26 -0
  85. package/dist/render/sliceStatus.js.map +1 -0
  86. package/dist/render/statusOverlay.d.ts +16 -0
  87. package/dist/render/statusOverlay.js +110 -0
  88. package/dist/render/statusOverlay.js.map +1 -0
  89. package/dist/render/svgGeometry.d.ts +43 -0
  90. package/dist/render/svgGeometry.js +69 -3
  91. package/dist/render/svgGeometry.js.map +1 -1
  92. package/dist/render/watch.d.ts +1 -1
  93. package/dist/render/watch.js +5 -2
  94. package/dist/render/watch.js.map +1 -1
  95. package/package.json +3 -2
@@ -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
- - `em` shape: `slice { ui X @Persona command Y event Z @Context }`
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?"*
@@ -46,28 +77,38 @@ against invariants and records one or more events.
46
77
  **Event(s) → Read Model → UI.** Past events are projected into a read model that a screen
47
78
  displays. Read-only; changes no state.
48
79
  - `em` shape: `slice { view V from "Event A", "Event B" ui Screen @Persona }`
49
- - **Headless / API systems (no UI):** when there is no UI, a read model is consumed by an
50
- **external caller querying the API**, modeled as a **read translation** — the outbound analog of
51
- `→ ui`. Shape: `slice { view V from "..." translation <Caller> Read <V> }`. A read translation
52
- **triggers no command** (it returns data); it is *not* a reaction, so the
53
- `reaction → command → event` rule does **not** apply to it. (Contrast: a *reaction* translation in
54
- pattern 4 triggers a command.)
80
+ - **Headless / API systems:** still use `ui`/`persona` — declare a persona per external
81
+ caller/role (e.g. `IntegratorAPI`) and treat its `ui` boxes as API calls, not screens. Shape:
82
+ `slice { view V from "..." ui Read V @IntegratorAPI }`, same as any State View slice.
83
+ `translation` stays reserved for genuine reactions/external-system boundaries (pattern 4), not
84
+ for a synchronous request/response API call.
55
85
  - **Repeat read models across the timeline.** A read model is drawn fresh in **every** slice where
56
86
  it is read — after the events that update it, before the actions that consume it — so the diagram
57
87
  shows state flowing left-to-right (the information-completeness staircase). The same read-model
58
- name recurring is intentional and **renders cleanly**: `em validate` only warns about a duplicate
59
- name that is *also referenced* by a `from`/`arrow` (it resolves to the first), so a repeated read
60
- model that nothing references by name produces **no warning**. **Wire each event to a read model
88
+ name recurring is intentional and **renders cleanly**. Declare every instance after the first with
89
+ **`view V again from "..."`**: `again` instances are exempt from the duplicate-name warning even
90
+ when something references them, and each reference resolves to the right instance. (A plain repeat
91
+ only stays warning-free while nothing references it by name, and resolves to the *first*
92
+ declaration when something does.) **Wire each event to a read model
61
93
  exactly once:** a repeated instance's `from` lists only the **new** events since the previous
62
94
  instance (not cumulative) — otherwise an event draws a duplicate arrow to the same read model at
63
95
  every repeat. (An event may still feed several *different* read models, once each.)
96
+ - **Instances are never connected to one another.** There is no arrow between two appearances of one
97
+ read model, and an explicit one is a validation error. The repeat is an ergonomic device for
98
+ showing the same read model at successive points in time — continuity is implied by the shared
99
+ name, and the events arriving at each instance are what show it changing. An arrow between them
100
+ would say the read model feeds itself.
64
101
  - **Place each repeat immediately after its feeding event (span-1).** The cleanest staircase puts a
65
102
  read-model instance right after each event that updates it, sourcing **only that one adjacent
66
103
  event** — every `event → read model` arrow is then short and forward-flowing. A read model sitting
67
104
  far from its source events (e.g. a "list/queue" read placed early but fed by late events) draws
68
- long arrows that cross the read-model row and *look* like read→read connections — which Event
69
- Modeling forbids. The fix is never a `view → view` edge; it's to repeat the read model next to each
70
- event that feeds it.
105
+ long arrows that sweep across the diagram. The renderer routes them *around* intervening boxes
106
+ rather than through them, so they no longer *look* like a forbidden read→read link — but the
107
+ distance itself is the problem: the arrowhead lands columns away from the event that produced it,
108
+ so **the write slice reads as dangling**, as if nothing consumed its event. A reader has to trace
109
+ the line across the diagram to see otherwise. The fix is never a `view → view` edge; it's to
110
+ repeat the read model next to each event that feeds it, and to keep a sub-flow that detours into
111
+ another context together rather than parking it at the end of the model.
71
112
  - Socratic prompts: *"What does the consumer need to see to make their next decision? Which past
72
113
  events provide that information? Is this a screen or an API read? What's the freshness/consistency
73
114
  expectation?"*
@@ -76,10 +117,24 @@ displays. Read-only; changes no state.
76
117
  **Read Model → Processor → (next slice) Command → Event.** The system reacts on its own: a
77
118
  processor watches a read model (a "to-do list" of work) and **issues a command** — it never
78
119
  records an event directly.
79
- - `em` shape: **two slices.** First `slice { view "Todo" from "..." processor P }`, then the
80
- **next** `slice { command C event E @Context }`. The processor band holds no event; the command
81
- in the next slice produces it. Keeping the triggered command in the same slice as the processor
82
- is a validation warning — always split it.
120
+ - `em` shape: **two slices**, then the read slice that consumes the event — three in all:
121
+ ```em
122
+ slice "Todo" {
123
+ view Todo from "..."
124
+ processor P
125
+ }
126
+ slice "Do It" { # the triggered command goes in the NEXT slice
127
+ command C
128
+ event E @Context
129
+ }
130
+ slice "Todo — done" {
131
+ view Todo again from "E" # every event needs a reader; here it clears the to-do list
132
+ ui Work Board @Persona # ...and every read model needs a consumer
133
+ }
134
+ ```
135
+ The processor band holds no event; the command in the next slice produces it. Keeping the
136
+ triggered command in the same slice as the processor is a validation warning — always split it.
137
+ Forgetting the third slice leaves `E` unread, which is also a warning.
83
138
  - Socratic prompts: *"What should happen without a human? What condition triggers it? What work
84
139
  list does the processor watch? What command does it fire? What if it fails or retries?"*
85
140
 
@@ -87,14 +142,42 @@ records an event directly.
87
142
  **Boundary crossing → command → event.** An adapter translates data across a boundary (an external
88
143
  system, or another bounded context) into the model's own language. Like an automation, a
89
144
  translation is a *reaction*: it **triggers a command**, never records an event directly. Two
90
- trigger forms:
91
- - **Externally triggered:** `external input → translation → command → event`. The trigger comes
92
- from outside the model, so the translation has no internal `from`.
93
- - **Internally triggered** (reacting to the modeled system's own state, e.g. pushing data out):
94
- `read model → translation → command → event`. The translation reads a **view** via `from`.
95
- - `em` shape: **two slices**, exactly like an automation. First
96
- `slice { [view "Source" from "..."] translation T [from "Source"] }`, then the **next**
97
- `slice { command C event E @Context }`. The translation band never carries an event.
145
+ independent questions shape which form it takes — don't conflate them:
146
+ - **Trigger source:** does the input come from outside the model, or from the model's own state
147
+ pushed back out?
148
+ - **Durable artifact:** is there a queryable, persisted thing behind the trigger (a queue, topic,
149
+ or log), or is it an ephemeral call with nothing to query afterward?
150
+
151
+ The `em` shape only tracks the second question — trigger source doesn't change it:
152
+ - **No durable artifact:** `external input → translation → command → event`. No internal `from`.
153
+ Typically (not necessarily) externally triggered — a bare webhook call with nothing persisted
154
+ behind it.
155
+ - **Durable artifact:** `read model → translation → command → event`. The translation reads a
156
+ **view** via `from`. This shape is the same whether the view was filled by the model's own event
157
+ (internally triggered, e.g. pushing data out) or by an external system's persisted queue
158
+ (externally triggered, e.g. a webhook whose inbound message is stored first for retries,
159
+ ordering, or audit) — an externally triggered translation backed by a real queue is
160
+ architecturally closer to this case than to the no-artifact one above.
161
+ - **Event legitimacy:** a received external message counts as a legitimate domain `event` when
162
+ it's scoped to the context/lane whose fact it represents, not by who committed it — the same
163
+ move as an Anti-Corruption-Layer boundary event in DDD — and stays legitimate as long as it
164
+ doesn't leak into the model's own domain vocabulary.
165
+ - `em` shape: **two slices**, exactly like an automation, plus the read slice for the event:
166
+ ```em
167
+ slice "Boundary" {
168
+ view Source from "..." # durable-artifact form only; omit if there's nothing persisted
169
+ translation T from "Source"
170
+ }
171
+ slice "Record It" {
172
+ command C
173
+ event E @Context
174
+ }
175
+ slice "Source — recorded" {
176
+ view Source again from "E" # every event needs a reader
177
+ ui Status Screen @Persona # ...and every read model needs a consumer
178
+ }
179
+ ```
180
+ The translation band never carries an event.
98
181
  - Socratic prompts: *"What boundary are we crossing, and which way? What outside system or context
99
182
  feeds us (or do we feed)? In what format? How do we know its data is trustworthy/complete? What
100
183
  internal **command** does it trigger, and what event does that command record?"*
@@ -170,17 +253,31 @@ dedicated `slice` phase writes the full rich spec (see `templates/slice.md`).
170
253
 
171
254
  ### Step 7 — Evaluate Completeness *(model)*
172
255
  Walk the whole model with stakeholders and check for loose ends:
256
+ - Every **command** has something that **triggers** it — a `ui` in its slice, or a reaction in the
257
+ slice before it. A command nothing points at is a write nobody can start. `em validate` warns.
173
258
  - Every **command** produces at least one **event**.
174
- - Every **view** has at least one source event.
175
- - Every **event** is shown somewhere or consumed by something (no orphan facts).
259
+ - Every **view** has at least one source event, **and a consumer** — a `ui` (headless: the `ui`
260
+ tagged to the API-caller persona), or a reaction watching it. Every *instance* of a repeated
261
+ view, not just the last one.
262
+ A read model nothing displays is information projected out and then dropped. `em validate` warns.
263
+ - Every **event** is **read by a read model** — a `view` naming it in `from` (any `again` instance
264
+ counts), or a `view` with no `from` sitting in its slice. A reaction consuming it does **not**
265
+ count: reactions read views, not events. An event nothing projects is a write nobody can see, so
266
+ there was no point recording it. `em validate` warns on each one.
176
267
  - Every **UI** is reachable and leads somewhere.
268
+ - Every connection is one of the six legal pairs (`ui → command`, `command → event`,
269
+ `event → read model`, `read model → ui`, `read model → reaction`, `reaction → command`). No
270
+ arrow between two instances of one read model.
177
271
  - Automations **and** translations are split correctly (reaction → command → event, never a
178
272
  reaction wired straight to an event); translations cover every external input.
179
- - Run `em validate` and resolve all errors and warnings. Note: `em validate` does **not** catch a
180
- translation/automation that emits an event directly — verify the two-slice split by hand.
181
- - Prompts: *"Is there any event nobody sees? Any screen with no way in or out? Any command that
182
- records nothing? Any external system we haven't translated? Any translation or automation that
183
- records an event instead of triggering a command?"*
273
+ - Run `em validate` and resolve all errors and warnings. It now catches illegal `arrow` kind pairs
274
+ (error) and unread events (warning), but it still does **not** catch a translation/automation that
275
+ emits an event directly — verify the two-slice split by hand.
276
+ - Prompts: *"Is there any event nobody reads — and if the business genuinely never looks at it, why
277
+ are we recording it? Any command that just happens, with nobody and nothing asking for it? Any
278
+ screen with no way in or out? Any command that records nothing? Any
279
+ external system we haven't translated? Any translation or automation that records an event instead
280
+ of triggering a command?"*
184
281
 
185
282
  ---
186
283
 
@@ -16,11 +16,11 @@ Replace bracketed placeholders; delete guidance comments before finishing.
16
16
 
17
17
  ## Summary
18
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}} |
19
+ | Surface | Real drift | Model gap | Internal inconsistency | Accepted divergence | Uncertainty |
20
+ |---|---|---|---|---|---|
21
+ | Structural | {{n}} | {{n}} | — | {{n}} | {{n}} |
22
+ | Spec | {{n}} | {{n}} | — | {{n}} | {{n}} |
23
+ | Internal | — | — | {{n}} | {{n}} | {{n}} |
24
24
 
25
25
  {{One or two sentences: overall read — clean, mostly clean with N items, or notably drifted.}}
26
26
 
@@ -32,12 +32,14 @@ Replace bracketed placeholders; delete guidance comments before finishing.
32
32
  ### {{n}}. {{Short finding title}}
33
33
 
34
34
  - **Surface:** {{structural | spec | internal}}
35
- - **Classification:** {{real drift | model gap | internal inconsistency}}
35
+ - **Classification:** {{real drift | model gap | internal inconsistency | accepted divergence}}
36
36
  - **Slice:** {{slice name}}
37
- - **Evidence:** {{file path(s) and line/symbol, or the `em diff --json` entry (type + name)}}
37
+ - **Evidence:** {{file path(s) and line/symbol, or the `em diff --json` entry (type + name) —
38
+ for accepted divergence, the `divergence "..."` annotation text itself}}
38
39
  - **Claim:** {{what the model/doc says vs. what the code/doc shows — the actual disagreement}}
39
40
  - **Proposed red note:** `issue "conformance: {{text}}"` on {{element}} (slice "{{slice}}")
40
41
  <!-- Omit this line entirely for internal-inconsistency findings with no clear side to flag,
42
+ for any accepted-divergence finding (already ratified — don't propose re-ratifying it),
41
43
  or when the finding doesn't warrant a red note (say why instead). -->
42
44
 
43
45
  ## Scenario / invariant coverage
@@ -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
@@ -3,10 +3,15 @@ Rich slice design document. One per slice, stored in <model>/slices/<slice-name>
3
3
  linked from the .em model with: note "slices/<slice-name>.md" on the slice's defining element.
4
4
  Fill every section through Socratic questioning. Leave "Open Questions" rather than guessing.
5
5
  Replace the bracketed placeholders; delete guidance comments before finishing.
6
+
7
+ The diagram below is generated, not hand-drawn: `em render <model>.em --slice "{{Slice Name}}"
8
+ -o slices/{{slice-name}}.svg` (kebab-case the slice name to match this doc's own filename).
6
9
  -->
7
10
 
8
11
  # Slice: {{Slice Name}}
9
12
 
13
+ ![Diagram](./{{slice-name}}.svg)
14
+
10
15
  - **Pattern:** {{State Change | State View | Automation | Translation}}
11
16
  - **Swimlane:** {{Persona/Actor}} → {{Context/Aggregate}}
12
17
  - **Status:** {{draft | reviewed | ready-to-implement | implemented}}
@@ -32,9 +37,20 @@ an event directly.}}
32
37
  |-------|------|----------|--------------------|
33
38
  | {{field}} | {{Type}} | {{yes/no}} | {{constraints, formats, ranges}} |
34
39
 
40
+ ## Trigger
41
+ <!-- What issues this slice's command. Required: a command nothing points at is a write nobody
42
+ can start. Either the screen the user acts on (a `ui` in this slice), or the reaction that
43
+ issues it (an automation/processor/translation in the PREVIOUS slice). -->
44
+ **Triggered by:** {{screen `X` @Persona | processor `Y` in slice "Z"}}
45
+
35
46
  ## Event(s) Emitted
36
47
  <!-- The immutable facts recorded. List each event and its payload. -->
37
48
  **Event:** `{{Event Name}}` → context `{{Context}}`
49
+ **Read by:** {{which read model projects this event, and in which slice}}
50
+ <!-- Required, not optional. Every event must be read by a read model — an event nothing
51
+ projects is a write nobody can see, and `em validate` warns on it. A reaction consuming
52
+ it does NOT count: reactions read views, not events. If the honest answer is "nothing
53
+ reads it", that's a question for the business, not a field to leave blank. -->
38
54
 
39
55
  | Field | Type | Immutable Fact? | Source / Notes |
40
56
  |-------|------|-----------------|----------------|
@@ -43,7 +59,10 @@ an event directly.}}
43
59
  ## Read Model / View
44
60
  <!-- For State View slices, and any read model this slice produces or feeds. -->
45
61
  - **View:** `{{View Name}}` built from events: {{"Event A", "Event B"}}
46
- - **Consumed by:** {{which UI screen or automation}}
62
+ - **Consumed by:** {{which UI screen (or API-caller persona), or reaction}}
63
+ <!-- "Consumed by" is required, not optional. A read model nothing displays or watches is
64
+ information projected out of the system and then dropped, and `em validate` warns. Every
65
+ instance of a repeated view needs its own consumer, not just the last one. -->
47
66
  - **Freshness / consistency expectation:** {{real-time | eventual | on-demand}}
48
67
 
49
68
  ## Invariants / Business Rules
@@ -11,6 +11,7 @@ Keep it current at the end of every working session.
11
11
  - **Current step:** {{1–7, see methodology; or extraction round R1–R7}}
12
12
  - **Last updated:** {{YYYY-MM-DD}}
13
13
  - **Last conformance:** {{YYYY-MM-DD @ <target-repo revision> — report: conformance/<date>-report.md | never}}
14
+ - **Last stakeholder review:** {{YYYY-MM-DD — attendees: see Participants | never}}
14
15
 
15
16
  ## Session inputs
16
17
  - **Scope line:** {{one-line description of what's in/out of bounds for this model}}
@@ -47,6 +48,13 @@ Keep it current at the end of every working session.
47
48
  attribute each entry to the participant who made the call (see Participants above). -->
48
49
  - {{YYYY-MM-DD}}: {{decision}} — {{why}} — by {{participant, if a live workshop}}
49
50
 
51
+ ## Usage log
52
+ <!-- The team's only usage signal today (see docs/usage-data.md) — cheap and coarse on purpose.
53
+ One line per session: phase(s) touched, and validate diagnostic *categories* hit (the rule
54
+ name from docs/validation.md's tables, e.g. "read model nothing consumes" — never the full
55
+ instance message or any domain content). Append, never edit past entries. -->
56
+ - {{YYYY-MM-DD}}: phases: {{discover | extract | model | slice | conform | review | validate | watch, ...}} — validate: {{diagnostic category (docs/usage-data.md#categories), ... | none}}
57
+
50
58
  ## Open questions / parking lot
51
59
  <!-- Unresolved items to bring back to the user. Never guess these. Metadata is optional and
52
60
  flat — add only what's known: source (ticket/conversation that spawned it), blocked on
package/README.md CHANGED
@@ -92,6 +92,7 @@ built this way.
92
92
  | [docs/timeline.md](docs/timeline.md) | The Two Laws of the Timeline |
93
93
  | [docs/ai-workflow.md](docs/ai-workflow.md) | The Claude Code skill: install, phases, artifacts |
94
94
  | [docs/dependencies.md](docs/dependencies.md) | What's bundled vs. what needs a system install |
95
+ | [docs/usage-data.md](docs/usage-data.md) | What usage data em captures, and how to roll it up for a retro |
95
96
  | [docs/architecture.md](docs/architecture.md) | How rendering works; why Graphviz, not PlantUML |
96
97
  | [docs/roadmap.md](docs/roadmap.md) | What's planned |
97
98
 
@@ -0,0 +1,33 @@
1
+ import { NormalizedModel } from "../model/model.js";
2
+ import { Grid } from "../layout/grid.js";
3
+ import { Diagnostic } from "../model/validate.js";
4
+ export interface CatalogModelInput {
5
+ /** The .em file as given on the command line — used for the doc-discovery path and display. */
6
+ file: string;
7
+ model: NormalizedModel;
8
+ grid: Grid;
9
+ dot: string;
10
+ }
11
+ export interface CatalogOptions {
12
+ outDir: string;
13
+ format?: "svg" | "png";
14
+ title?: string;
15
+ /** Forwarded to buildSliceDiagram's fallback build so `--keep-empty-lanes`
16
+ * behaves the same way for a built-on-the-fly slice diagram as it does for
17
+ * the main one (which already gets it via the per-file compileFile call
18
+ * that produced `model`/`grid` in the first place). */
19
+ keepEmptyLanes?: boolean;
20
+ }
21
+ export interface CatalogBuildResult {
22
+ models: number;
23
+ slices: number;
24
+ /** Ref-collision warnings raised while assigning slice/element keys (same diagnostics
25
+ * `em export`/`em diff` surface via computeRefs), tagged by the input file they came from —
26
+ * a duplicate slice/element name still gets a stable `~2` key here, but the caller should
27
+ * print these the same way every other command does rather than build silently. */
28
+ diagnostics: {
29
+ file: string;
30
+ diagnostics: Diagnostic[];
31
+ }[];
32
+ }
33
+ export declare function buildCatalog(inputs: CatalogModelInput[], opts: CatalogOptions): Promise<CatalogBuildResult>;
@@ -0,0 +1,117 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Orchestrates `em catalog`'s static-site build — the only I/O in
3
+ // src/catalog/*. One output directory per input model (so slice keys from
4
+ // different files can never collide, even without a cross-file dedup pass):
5
+ //
6
+ // <outDir>/
7
+ // index.html
8
+ // <model-key>/
9
+ // diagram.svg (or .png)
10
+ // slices/
11
+ // <slice-key>.html
12
+ // <slice-key>.svg (this slice's own diagram — copied in if authored, else built)
13
+ import { existsSync, readFileSync } from "node:fs";
14
+ import { copyFile, mkdir, writeFile } from "node:fs/promises";
15
+ import { basename, dirname, extname, join } from "node:path";
16
+ import { layoutDot, composeSvg, writeRendered } from "../render/render.js";
17
+ import { buildSliceDiagram } from "../render/sliceDiagram.js";
18
+ import { computeRefs } from "../emit/json.js";
19
+ import { dedupe, kebabSlug } from "../util/slug.js";
20
+ import { classifySlicePattern } from "./classify.js";
21
+ import { parseSliceDoc } from "./sliceDoc.js";
22
+ import { renderIndexPage, renderSlicePage } from "./pages.js";
23
+ export async function buildCatalog(inputs, opts) {
24
+ const format = opts.format ?? "svg";
25
+ const title = opts.title ?? "Event Model Catalog";
26
+ const outDir = opts.outDir;
27
+ await mkdir(outDir, { recursive: true });
28
+ const usedModelKeys = new Set();
29
+ const modelSummaries = [];
30
+ const diagnostics = [];
31
+ let sliceCount = 0;
32
+ for (const { file, model, grid, dot } of inputs) {
33
+ const modelKey = dedupe(kebabSlug(basename(file, extname(file))), usedModelKeys, "~");
34
+ const modelDir = join(outDir, modelKey);
35
+ const slicesDir = join(modelDir, "slices");
36
+ await mkdir(slicesDir, { recursive: true });
37
+ const diagramFile = `diagram.${format}`;
38
+ // baseDir = dirname(file), same as `em render`/`em watch` — this is why note hrefs
39
+ // embedded in the diagram still resolve correctly from the catalog's output location.
40
+ const raw = await layoutDot(dot);
41
+ const mainSvg = composeSvg(raw, model, grid, dirname(file), modelDir);
42
+ await writeRendered(mainSvg, join(modelDir, diagramFile), format);
43
+ const refs = computeRefs(model);
44
+ if (refs.diagnostics.length > 0)
45
+ diagnostics.push({ file, diagnostics: refs.diagnostics });
46
+ const sliceSummaries = [];
47
+ for (let i = 0; i < model.slices.length; i++) {
48
+ const slice = model.slices[i];
49
+ const sliceKey = refs.sliceKeys[i];
50
+ const pattern = classifySlicePattern(slice);
51
+ // Look up the doc at the *deduped* key, not a fresh kebabSlug(slice.name): for the
52
+ // (overwhelmingly common) case of a unique slice name they're identical, but if two
53
+ // slices share a name (see the diagnostics above), only the first gets sliceKey ==
54
+ // kebabSlug(name) — the doc a human actually authored on disk. The second slice's key
55
+ // carries a "~2" suffix no doc file will ever match, so it honestly reports "no doc"
56
+ // instead of silently rendering the first slice's doc content on both pages.
57
+ const docRelPath = join("slices", `${sliceKey}.md`);
58
+ const docPath = join(dirname(file), docRelPath);
59
+ const doc = existsSync(docPath) ? parseSliceDoc(readFileSync(docPath, "utf8")) : null;
60
+ // Same sibling-file convention as the doc lookup above, one extension over: a
61
+ // slice diagram authored by `em render --slice` (per the event-modeling skill)
62
+ // is copied in as-is so the catalog stays self-contained; otherwise built
63
+ // fresh via buildSliceDiagram, written only into the catalog's own output
64
+ // tree — `em catalog` never writes back into the source tree.
65
+ const sliceSvgSrcPath = join(dirname(file), "slices", `${sliceKey}.svg`);
66
+ const sliceDiagramFile = `${sliceKey}.svg`;
67
+ if (existsSync(sliceSvgSrcPath)) {
68
+ await copyFile(sliceSvgSrcPath, join(slicesDir, sliceDiagramFile));
69
+ }
70
+ else {
71
+ // One Graphviz WASM layout pass per slice lacking an authored sibling — unlike
72
+ // the single shared pass the old crop-based approach paid for once per model.
73
+ // Each pass is on a tiny 1-3-column graph, so this is fast in absolute terms
74
+ // and the WASM module itself is loaded once and memoized (render.ts); a real,
75
+ // deliberate trade-off, not an oversight — worth revisiting only if catalog
76
+ // build time on a very-many-slice model actually becomes a complaint.
77
+ const { model: sliceModel, grid: sliceGrid, dot: sliceDot } = buildSliceDiagram(model, i, {
78
+ keepEmptyLanes: opts.keepEmptyLanes,
79
+ });
80
+ const sliceRaw = await layoutDot(sliceDot);
81
+ const sliceSvg = composeSvg(sliceRaw, sliceModel, sliceGrid, dirname(file), slicesDir);
82
+ await writeFile(join(slicesDir, sliceDiagramFile), sliceSvg, "utf8");
83
+ }
84
+ const elementRefs = new Map();
85
+ for (const el of slice.elements) {
86
+ const ref = refs.refById.get(el.id);
87
+ if (ref)
88
+ elementRefs.set(el.id, ref);
89
+ }
90
+ const page = renderSlicePage({
91
+ modelName: model.name,
92
+ diagramFile: `../${diagramFile}`,
93
+ sliceDiagramFile,
94
+ slice,
95
+ sliceKey,
96
+ pattern,
97
+ elementRefs,
98
+ doc,
99
+ docExpectedPath: docRelPath,
100
+ });
101
+ await writeFile(join(slicesDir, `${sliceKey}.html`), page, "utf8");
102
+ sliceSummaries.push({
103
+ key: sliceKey,
104
+ name: slice.name,
105
+ pattern,
106
+ hasDoc: doc !== null,
107
+ status: doc?.status ?? null,
108
+ });
109
+ sliceCount++;
110
+ }
111
+ modelSummaries.push({ key: modelKey, name: model.name, file, diagramFile, slices: sliceSummaries });
112
+ }
113
+ const indexHtml = renderIndexPage(modelSummaries, title, format);
114
+ await writeFile(join(outDir, "index.html"), indexHtml, "utf8");
115
+ return { models: modelSummaries.length, slices: sliceCount, diagnostics };
116
+ }
117
+ //# sourceMappingURL=build.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build.js","sourceRoot":"","sources":["../../src/catalog/build.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,kEAAkE;AAClE,0EAA0E;AAC1E,4EAA4E;AAC5E,EAAE;AACF,cAAc;AACd,iBAAiB;AACjB,mBAAmB;AACnB,8BAA8B;AAC9B,gBAAgB;AAChB,2BAA2B;AAC3B,2FAA2F;AAE3F,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAG7D,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAC3E,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9C,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,eAAe,EAA4C,MAAM,YAAY,CAAC;AA+BxG,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,MAA2B,EAC3B,IAAoB;IAEpB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC;IACpC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,qBAAqB,CAAC;IAClD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAE3B,MAAM,KAAK,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAEzC,MAAM,aAAa,GAAG,IAAI,GAAG,EAAU,CAAC;IACxC,MAAM,cAAc,GAA0B,EAAE,CAAC;IACjD,MAAM,WAAW,GAAkD,EAAE,CAAC;IACtE,IAAI,UAAU,GAAG,CAAC,CAAC;IAEnB,KAAK,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,MAAM,EAAE,CAAC;QAChD,MAAM,QAAQ,GAAG,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,GAAG,CAAC,CAAC;QACtF,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QACxC,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAC3C,MAAM,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAE5C,MAAM,WAAW,GAAG,WAAW,MAAM,EAAE,CAAC;QACxC,mFAAmF;QACnF,sFAAsF;QACtF,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,CAAC;QACjC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAC;QACtE,MAAM,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,EAAE,MAAM,CAAC,CAAC;QAElE,MAAM,IAAI,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;QAChC,IAAI,IAAI,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC;YAAE,WAAW,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;QAC3F,MAAM,cAAc,GAA0B,EAAE,CAAC;QAEjD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAC7C,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YAC9B,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;YACnC,MAAM,OAAO,GAAG,oBAAoB,CAAC,KAAK,CAAC,CAAC;YAE5C,mFAAmF;YACnF,oFAAoF;YACpF,mFAAmF;YACnF,sFAAsF;YACtF,qFAAqF;YACrF,6EAA6E;YAC7E,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,EAAE,GAAG,QAAQ,KAAK,CAAC,CAAC;YACpD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC,CAAC;YAChD,MAAM,GAAG,GAAG,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAEtF,8EAA8E;YAC9E,+EAA+E;YAC/E,0EAA0E;YAC1E,0EAA0E;YAC1E,8DAA8D;YAC9D,MAAM,eAAe,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,GAAG,QAAQ,MAAM,CAAC,CAAC;YACzE,MAAM,gBAAgB,GAAG,GAAG,QAAQ,MAAM,CAAC;YAC3C,IAAI,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;gBAChC,MAAM,QAAQ,CAAC,eAAe,EAAE,IAAI,CAAC,SAAS,EAAE,gBAAgB,CAAC,CAAC,CAAC;YACrE,CAAC;iBAAM,CAAC;gBACN,+EAA+E;gBAC/E,8EAA8E;gBAC9E,6EAA6E;gBAC7E,8EAA8E;gBAC9E,4EAA4E;gBAC5E,sEAAsE;gBACtE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,iBAAiB,CAAC,KAAK,EAAE,CAAC,EAAE;oBACxF,cAAc,EAAE,IAAI,CAAC,cAAc;iBACpC,CAAC,CAAC;gBACH,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,QAAQ,CAAC,CAAC;gBAC3C,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,CAAC;gBACvF,MAAM,SAAS,CAAC,IAAI,CAAC,SAAS,EAAE,gBAAgB,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;YACvE,CAAC;YAED,MAAM,WAAW,GAAG,IAAI,GAAG,EAAkB,CAAC;YAC9C,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;gBAChC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;gBACpC,IAAI,GAAG;oBAAE,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;YACvC,CAAC;YAED,MAAM,IAAI,GAAG,eAAe,CAAC;gBAC3B,SAAS,EAAE,KAAK,CAAC,IAAI;gBACrB,WAAW,EAAE,MAAM,WAAW,EAAE;gBAChC,gBAAgB;gBAChB,KAAK;gBACL,QAAQ;gBACR,OAAO;gBACP,WAAW;gBACX,GAAG;gBACH,eAAe,EAAE,UAAU;aAC5B,CAAC,CAAC;YACH,MAAM,SAAS,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,QAAQ,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;YAEnE,cAAc,CAAC,IAAI,CAAC;gBAClB,GAAG,EAAE,QAAQ;gBACb,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,OAAO;gBACP,MAAM,EAAE,GAAG,KAAK,IAAI;gBACpB,MAAM,EAAE,GAAG,EAAE,MAAM,IAAI,IAAI;aAC5B,CAAC,CAAC;YACH,UAAU,EAAE,CAAC;QACf,CAAC;QAED,cAAc,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC;IACtG,CAAC;IAED,MAAM,SAAS,GAAG,eAAe,CAAC,cAAc,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;IACjE,MAAM,SAAS,CAAC,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;IAE/D,OAAO,EAAE,MAAM,EAAE,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC;AAC5E,CAAC"}
@@ -0,0 +1,14 @@
1
+ import { Slice } from "../model/model.js";
2
+ export type SlicePattern = "translation" | "automation" | "state-change" | "state-view" | "unclassified";
3
+ /**
4
+ * Classify a single slice by its element kinds. Known simplification, not a
5
+ * bug: docs/patterns.md describes Automation and Translation as spanning
6
+ * *two* slices — the reaction slice, then a following `command`+`event`
7
+ * slice. That second slice has the exact same kind-signature as a State
8
+ * Change slice, so per-slice classification reads it as State Change. This
9
+ * matches "derived from which element kinds appear in the slice" and keeps
10
+ * the classifier a pure function of one slice at a time; don't "fix" this by
11
+ * trying to look at neighboring slices.
12
+ */
13
+ export declare function classifySlicePattern(slice: Slice): SlicePattern;
14
+ export declare function slicePatternLabel(pattern: SlicePattern): string;
@@ -0,0 +1,44 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Derives a slice's Event Modeling pattern from which element `kind`s it
3
+ // contains — the pattern itself is never stored in the AST/model (see
4
+ // docs/patterns.md), so `em catalog` is the first consumer that needs it
5
+ // computed. Rules follow docs/patterns.md's "DSL elements" table exactly,
6
+ // checked in priority order (a slice can carry a translation and a view
7
+ // together, for instance, so order matters).
8
+ /**
9
+ * Classify a single slice by its element kinds. Known simplification, not a
10
+ * bug: docs/patterns.md describes Automation and Translation as spanning
11
+ * *two* slices — the reaction slice, then a following `command`+`event`
12
+ * slice. That second slice has the exact same kind-signature as a State
13
+ * Change slice, so per-slice classification reads it as State Change. This
14
+ * matches "derived from which element kinds appear in the slice" and keeps
15
+ * the classifier a pure function of one slice at a time; don't "fix" this by
16
+ * trying to look at neighboring slices.
17
+ */
18
+ export function classifySlicePattern(slice) {
19
+ const kinds = new Set(slice.elements.map((el) => el.kind));
20
+ if (kinds.has("translation"))
21
+ return "translation";
22
+ if (kinds.has("processor") || kinds.has("automation") || kinds.has("saga"))
23
+ return "automation";
24
+ if (kinds.has("command") || kinds.has("event"))
25
+ return "state-change";
26
+ if (kinds.has("view"))
27
+ return "state-view";
28
+ return "unclassified";
29
+ }
30
+ export function slicePatternLabel(pattern) {
31
+ switch (pattern) {
32
+ case "translation":
33
+ return "Translation";
34
+ case "automation":
35
+ return "Automation";
36
+ case "state-change":
37
+ return "State Change";
38
+ case "state-view":
39
+ return "State View";
40
+ case "unclassified":
41
+ return "Unclassified";
42
+ }
43
+ }
44
+ //# sourceMappingURL=classify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.js","sourceRoot":"","sources":["../../src/catalog/classify.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,yEAAyE;AACzE,sEAAsE;AACtE,yEAAyE;AACzE,0EAA0E;AAC1E,wEAAwE;AACxE,6CAA6C;AAM7C;;;;;;;;;GASG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAY;IAC/C,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAC3D,IAAI,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC;QAAE,OAAO,aAAa,CAAC;IACnD,IAAI,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC;QAAE,OAAO,YAAY,CAAC;IAChG,IAAI,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC;QAAE,OAAO,cAAc,CAAC;IACtE,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC;QAAE,OAAO,YAAY,CAAC;IAC3C,OAAO,cAAc,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,OAAqB;IACrD,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,aAAa;YAChB,OAAO,aAAa,CAAC;QACvB,KAAK,YAAY;YACf,OAAO,YAAY,CAAC;QACtB,KAAK,cAAc;YACjB,OAAO,cAAc,CAAC;QACxB,KAAK,YAAY;YACf,OAAO,YAAY,CAAC;QACtB,KAAK,cAAc;YACjB,OAAO,cAAc,CAAC;IAC1B,CAAC;AACH,CAAC"}
@@ -0,0 +1,45 @@
1
+ import { Slice } from "../model/model.js";
2
+ import { SlicePattern } from "./classify.js";
3
+ import { SliceDoc } from "./sliceDoc.js";
4
+ export declare function escapeHtml(s: string): string;
5
+ /** A slice's row in the index page's per-model table. */
6
+ export interface CatalogSliceSummary {
7
+ key: string;
8
+ name: string;
9
+ pattern: SlicePattern;
10
+ /** Whether a slices/<key>.md doc was found at all — distinct from `status` being null,
11
+ * which can also mean "doc exists but has no recognizable Status line". */
12
+ hasDoc: boolean;
13
+ /** Lowercased Status value from the doc, or null (no doc, or doc has no Status line). */
14
+ status: string | null;
15
+ }
16
+ export interface CatalogModelSummary {
17
+ key: string;
18
+ name: string;
19
+ /** Input .em file path, as given on the command line. */
20
+ file: string;
21
+ /** Diagram filename, relative to <outDir>/<model-key>/. */
22
+ diagramFile: string;
23
+ slices: CatalogSliceSummary[];
24
+ }
25
+ export declare function renderIndexPage(models: CatalogModelSummary[], title: string, format: "svg" | "png"): string;
26
+ export interface SlicePageArgs {
27
+ modelName: string;
28
+ /** Full model diagram path relative to this page (which lives in
29
+ * <outDir>/<model-key>/slices/) — shown as a secondary "view full diagram" link
30
+ * (a plain anchor, not an embed, so no MIME type is needed here). */
31
+ diagramFile: string;
32
+ /** This slice's own diagram, relative to this page (same directory) — always
33
+ * svg (buildSliceDiagram writes svg regardless of the main diagram's chosen
34
+ * format). Shown as the page's primary diagram. */
35
+ sliceDiagramFile: string;
36
+ slice: Slice;
37
+ sliceKey: string;
38
+ pattern: SlicePattern;
39
+ /** Internal Element.id -> em export ref, for stable per-row anchors. */
40
+ elementRefs: Map<string, string>;
41
+ doc: SliceDoc | null;
42
+ /** slices/<slug>.md, relative to the source .em file — shown when no doc was found. */
43
+ docExpectedPath: string;
44
+ }
45
+ export declare function renderSlicePage(args: SlicePageArgs): string;