@milehimikey/em 1.5.0 → 1.6.1

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 (91) hide show
  1. package/.claude/skills/event-modeling/SKILL.md +62 -17
  2. package/.claude/skills/event-modeling/reference/conform.md +33 -6
  3. package/.claude/skills/event-modeling/reference/em-dsl.md +144 -43
  4. package/.claude/skills/event-modeling/reference/extract.md +12 -2
  5. package/.claude/skills/event-modeling/reference/methodology.md +34 -14
  6. package/.claude/skills/event-modeling/templates/conformance-report.md +9 -7
  7. package/.claude/skills/event-modeling/templates/slice.md +6 -1
  8. package/.claude/skills/event-modeling/templates/state.md +8 -0
  9. package/README.md +1 -0
  10. package/dist/catalog/build.d.ts +33 -0
  11. package/dist/catalog/build.js +117 -0
  12. package/dist/catalog/build.js.map +1 -0
  13. package/dist/catalog/classify.d.ts +14 -0
  14. package/dist/catalog/classify.js +44 -0
  15. package/dist/catalog/classify.js.map +1 -0
  16. package/dist/catalog/pages.d.ts +45 -0
  17. package/dist/catalog/pages.js +174 -0
  18. package/dist/catalog/pages.js.map +1 -0
  19. package/dist/catalog/sliceDoc.d.ts +10 -0
  20. package/dist/catalog/sliceDoc.js +20 -0
  21. package/dist/catalog/sliceDoc.js.map +1 -0
  22. package/dist/cli/catalog-inputs.d.ts +16 -0
  23. package/dist/cli/catalog-inputs.js +18 -0
  24. package/dist/cli/catalog-inputs.js.map +1 -0
  25. package/dist/cli/glossary-inputs.d.ts +15 -0
  26. package/dist/cli/glossary-inputs.js +17 -0
  27. package/dist/cli/glossary-inputs.js.map +1 -0
  28. package/dist/cli/render-inputs.d.ts +27 -0
  29. package/dist/cli/render-inputs.js +41 -0
  30. package/dist/cli/render-inputs.js.map +1 -0
  31. package/dist/cli.js +188 -10
  32. package/dist/cli.js.map +1 -1
  33. package/dist/emit/diffJson.d.ts +1 -1
  34. package/dist/emit/diffJson.js +8 -1
  35. package/dist/emit/diffJson.js.map +1 -1
  36. package/dist/emit/dot.d.ts +7 -0
  37. package/dist/emit/dot.js +10 -4
  38. package/dist/emit/dot.js.map +1 -1
  39. package/dist/emit/glossaryJson.d.ts +13 -0
  40. package/dist/emit/glossaryJson.js +36 -0
  41. package/dist/emit/glossaryJson.js.map +1 -0
  42. package/dist/emit/json.d.ts +16 -2
  43. package/dist/emit/json.js +57 -6
  44. package/dist/emit/json.js.map +1 -1
  45. package/dist/emit/theme.d.ts +3 -0
  46. package/dist/emit/theme.js +19 -0
  47. package/dist/emit/theme.js.map +1 -1
  48. package/dist/model/diff.d.ts +36 -1
  49. package/dist/model/diff.js +198 -35
  50. package/dist/model/diff.js.map +1 -1
  51. package/dist/model/edges.d.ts +12 -1
  52. package/dist/model/edges.js +19 -8
  53. package/dist/model/edges.js.map +1 -1
  54. package/dist/model/glossary.d.ts +68 -0
  55. package/dist/model/glossary.js +138 -0
  56. package/dist/model/glossary.js.map +1 -0
  57. package/dist/model/model.d.ts +35 -0
  58. package/dist/model/model.js +41 -0
  59. package/dist/model/model.js.map +1 -1
  60. package/dist/model/validate.js +114 -3
  61. package/dist/model/validate.js.map +1 -1
  62. package/dist/parser/ast.d.ts +20 -0
  63. package/dist/parser/parser.js +170 -31
  64. package/dist/parser/parser.js.map +1 -1
  65. package/dist/render/drawNotes.d.ts +7 -3
  66. package/dist/render/drawNotes.js +75 -28
  67. package/dist/render/drawNotes.js.map +1 -1
  68. package/dist/render/render.d.ts +20 -1
  69. package/dist/render/render.js +52 -14
  70. package/dist/render/render.js.map +1 -1
  71. package/dist/render/serve.js +155 -0
  72. package/dist/render/serve.js.map +1 -1
  73. package/dist/render/sliceDiagram.d.ts +11 -0
  74. package/dist/render/sliceDiagram.js +198 -0
  75. package/dist/render/sliceDiagram.js.map +1 -0
  76. package/dist/render/sliceOverlay.d.ts +21 -0
  77. package/dist/render/sliceOverlay.js +111 -0
  78. package/dist/render/sliceOverlay.js.map +1 -0
  79. package/dist/render/sliceStatus.d.ts +5 -0
  80. package/dist/render/sliceStatus.js +26 -0
  81. package/dist/render/sliceStatus.js.map +1 -0
  82. package/dist/render/statusOverlay.d.ts +16 -0
  83. package/dist/render/statusOverlay.js +110 -0
  84. package/dist/render/statusOverlay.js.map +1 -0
  85. package/dist/render/svgGeometry.d.ts +30 -0
  86. package/dist/render/svgGeometry.js +33 -14
  87. package/dist/render/svgGeometry.js.map +1 -1
  88. package/dist/render/watch.d.ts +1 -1
  89. package/dist/render/watch.js +5 -2
  90. package/dist/render/watch.js.map +1 -1
  91. package/package.json +3 -2
@@ -0,0 +1,198 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Renders a single slice's own diagram, matching the canonical shape of
3
+ // whichever of the 4 Event Modeling patterns it is (State Change, State View,
4
+ // Automation, Translation) — instead of cropping the full multi-slice diagram
5
+ // down to one column (which forces a narrow crop to still show a wide swimlane
6
+ // label, and can pull in most of a neighboring slice; see git history).
7
+ //
8
+ // The key trick: every piece of "immediate context" a slice needs to draw its
9
+ // own shape is already one hop away in the compiled model — a view's upstream
10
+ // event(s) resolve via `from` (resolveFromSource, shared with semanticEdges),
11
+ // and an automation/translation's watched view is just whatever `view` sits in
12
+ // the same slice. So this builds a tiny *synthetic* NormalizedModel — reusing
13
+ // the real, already-normalized Element objects (same ids, same fields, same
14
+ // notes), shallow-cloned only to reassign which of a handful of new columns
15
+ // they land in — and runs it through the exact same layout()/emitDot() the
16
+ // full model uses, unmodified. Edge-drawing comes free too: semanticEdges()
17
+ // resolves `from` via byName, so as long as the synthetic model's byName map
18
+ // is populated, the arrows get drawn by the existing inference with zero new
19
+ // edge-drawing code.
20
+ //
21
+ // This never chases more than one hop away (previous slice for a triggerless
22
+ // State Change; a view's own `from` sources for State View; the next slice for
23
+ // Automation/Translation — semanticEdges itself never looks further than i+1),
24
+ // so a slice diagram can never balloon into showing most of the model, the way
25
+ // a naive crop or a naive "just extract this slice and recompile" both could.
26
+ import { AUTOMATION_KINDS } from "../parser/ast.js";
27
+ import { normalizeName } from "../model/model.js";
28
+ import { resolveFromSource } from "../model/edges.js";
29
+ import { layout } from "../layout/grid.js";
30
+ import { emitDot } from "../emit/dot.js";
31
+ import { classifySlicePattern } from "../catalog/classify.js";
32
+ /** Build a small, standalone diagram for one slice — real Graphviz layout, not a crop.
33
+ * `opts` is forwarded to layout() unchanged, so `--keep-empty-lanes` behaves the same
34
+ * way here as it does for the full model instead of being silently dropped. */
35
+ export function buildSliceDiagram(model, sliceIndex, opts = {}) {
36
+ const slice = model.slices[sliceIndex];
37
+ const pattern = classifySlicePattern(slice);
38
+ const columns = mergeColumns(collectColumns(model, sliceIndex, pattern));
39
+ const synthetic = assembleSyntheticModel(model, columns, slice.name);
40
+ const grid = layout(synthetic, opts);
41
+ const dot = emitDot(synthetic, grid);
42
+ return { model: synthetic, grid, dot };
43
+ }
44
+ function collectColumns(model, sliceIndex, pattern) {
45
+ switch (pattern) {
46
+ case "state-change":
47
+ return columnsForStateChange(model, sliceIndex);
48
+ case "state-view":
49
+ return columnsForStateView(model, sliceIndex);
50
+ case "automation":
51
+ case "translation":
52
+ return columnsForReaction(model, sliceIndex);
53
+ default: // "unclassified"
54
+ return [{ sourceSliceIndex: sliceIndex, elements: model.slices[sliceIndex].elements }];
55
+ }
56
+ }
57
+ /** State Change: UI -> Command -> Event, all in this slice. If there's no local
58
+ * UI, the command's real trigger is the previous slice's automation/translation
59
+ * reaction — pull in just that one element as earlier context. */
60
+ function columnsForStateChange(model, i) {
61
+ const slice = model.slices[i];
62
+ const hasUi = slice.elements.some((e) => e.kind === "ui");
63
+ const cols = [{ sourceSliceIndex: i, elements: slice.elements }];
64
+ if (!hasUi) {
65
+ const prev = model.slices[i - 1];
66
+ const trigger = prev?.elements.find((e) => AUTOMATION_KINDS.has(e.kind));
67
+ if (trigger)
68
+ cols.push({ sourceSliceIndex: i - 1, elements: [trigger] });
69
+ }
70
+ return cols;
71
+ }
72
+ /** State View: Event(s) -> View -> UI. Each view's `from` names resolve to their
73
+ * real source Elements (same rule semanticEdges uses) — one column per distinct
74
+ * origin slice, so a multi-source view (e.g. a dashboard reading two events from
75
+ * two different, possibly far-apart slices) naturally gets one column per source,
76
+ * not a single blown-out column. */
77
+ function columnsForStateView(model, i) {
78
+ const slice = model.slices[i];
79
+ const cols = [];
80
+ for (const view of slice.elements.filter((e) => e.kind === "view")) {
81
+ for (const name of view.from ?? []) {
82
+ const src = resolveFromSource(model, view, name);
83
+ if (src)
84
+ cols.push({ sourceSliceIndex: src.sliceIndex, elements: [src] });
85
+ }
86
+ }
87
+ cols.push({ sourceSliceIndex: i, elements: slice.elements });
88
+ return cols;
89
+ }
90
+ /** Automation/Translation share one shape: [View ->] Automation (this slice),
91
+ * then Command -> Event (the immediately next slice — never further, mirroring
92
+ * semanticEdges' own i+1-only rule). The watched view is same-slice adjacency,
93
+ * not a `from` hop, so it's already included via this slice's own elements —
94
+ * deliberately not chased any further back. Only command/event are pulled from
95
+ * the next slice; a stray `ui` there belongs to *that* slice's own diagram. */
96
+ function columnsForReaction(model, i) {
97
+ const slice = model.slices[i];
98
+ const cols = [{ sourceSliceIndex: i, elements: slice.elements }];
99
+ const next = model.slices[i + 1];
100
+ const forward = next?.elements.filter((e) => e.kind === "command" || e.kind === "event") ?? [];
101
+ if (forward.length > 0)
102
+ cols.push({ sourceSliceIndex: i + 1, elements: forward });
103
+ return cols;
104
+ }
105
+ /** Merge columns sharing a source slice (two `from` names can resolve to the same
106
+ * origin slice), dedupe elements by id, then sort chronologically. `from`/reaction
107
+ * wiring only ever points backward or to the immediate next slice, so a plain
108
+ * ascending sort by real slice index is sufficient for correct left-to-right
109
+ * placement across every pattern — no pattern-specific ordering code needed. */
110
+ function mergeColumns(columns) {
111
+ const bySource = new Map();
112
+ for (const col of columns) {
113
+ let elements = bySource.get(col.sourceSliceIndex);
114
+ if (!elements) {
115
+ elements = new Map();
116
+ bySource.set(col.sourceSliceIndex, elements);
117
+ }
118
+ for (const el of col.elements)
119
+ elements.set(el.id, el);
120
+ }
121
+ return [...bySource.entries()]
122
+ .map(([sourceSliceIndex, elements]) => ({ sourceSliceIndex, elements: [...elements.values()] }))
123
+ .sort((a, b) => a.sourceSliceIndex - b.sourceSliceIndex);
124
+ }
125
+ /**
126
+ * Assemble a small, valid NormalizedModel from the given columns — real Element
127
+ * objects, shallow-cloned only to reassign `sliceIndex` to their new synthetic
128
+ * column (never mutating the real model). Two details matter for correctness:
129
+ *
130
+ * - `byId` keeps each clone's *original* id — everything downstream (emitDot's
131
+ * node ids, styleFor, note markers, parseNodeRects) keys off Element.id, so
132
+ * preserving it is what keeps the whole rest of the render pipeline working
133
+ * completely unmodified.
134
+ * - `byName` is populated from the clones actually included, using their
135
+ * untouched `name`/`from` strings — this is the entire trick that makes edge
136
+ * drawing free: semanticEdges() (called later, unmodified, inside
137
+ * composeSvg) resolves `from` via byName exactly as it does for the full
138
+ * model, so it finds the clones and draws the arrows automatically.
139
+ *
140
+ * `personas`/`contexts` are derived from only the included elements, not
141
+ * copied from the full model — copying them was the exact class of bug that
142
+ * made the previous crop-based approach barely narrower than the full diagram
143
+ * (an unused row still reserves its own swimlane).
144
+ */
145
+ function assembleSyntheticModel(model, columns, title) {
146
+ const slices = [];
147
+ const elements = [];
148
+ const byId = new Map();
149
+ const byName = new Map();
150
+ columns.forEach((col, syntheticIndex) => {
151
+ const origin = model.slices[col.sourceSliceIndex];
152
+ const cloned = col.elements.map((el) => ({ ...el, sliceIndex: syntheticIndex }));
153
+ slices.push({ name: origin.name, index: syntheticIndex, source: origin.source, elements: cloned, line: origin.line });
154
+ for (const el of cloned) {
155
+ elements.push(el);
156
+ byId.set(el.id, el);
157
+ const key = normalizeName(el.name);
158
+ const bucket = byName.get(key);
159
+ if (bucket)
160
+ bucket.push(el);
161
+ else
162
+ byName.set(key, [el]);
163
+ }
164
+ });
165
+ return {
166
+ name: title,
167
+ personas: dedupeDefined(elements.filter((e) => e.kind === "ui").map((e) => e.persona)),
168
+ contexts: dedupeDefined(elements.filter((e) => e.kind === "event").map((e) => e.context)),
169
+ hasAutomation: elements.some((e) => AUTOMATION_KINDS.has(e.kind)),
170
+ slices,
171
+ elements,
172
+ byId,
173
+ byName,
174
+ // v1 gap, deliberate: explicit DSL `arrow` statements are rare (they exist for
175
+ // relationships semanticEdges' pattern inference can't derive on its own) and
176
+ // dropping them from a single-slice extraction is narrow, not silent — a
177
+ // follow-up could remap model.arrows entries whose both endpoints resolve
178
+ // through an original-id -> cloned-id map, but it's not needed to ship this.
179
+ arrows: [],
180
+ // Unused by layout()/emitDot() — fieldLabel() prints a field's type as a raw
181
+ // string, never resolves it — so passthrough is free and avoids handing back
182
+ // broken/empty maps for no benefit.
183
+ types: model.types,
184
+ typesByName: model.typesByName,
185
+ };
186
+ }
187
+ function dedupeDefined(values) {
188
+ const seen = new Set();
189
+ const out = [];
190
+ for (const v of values) {
191
+ if (v && !seen.has(v)) {
192
+ seen.add(v);
193
+ out.push(v);
194
+ }
195
+ }
196
+ return out;
197
+ }
198
+ //# sourceMappingURL=sliceDiagram.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sliceDiagram.js","sourceRoot":"","sources":["../../src/render/sliceDiagram.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import { NormalizedModel } from "../model/model.js";
2
+ import { Grid } from "../layout/grid.js";
3
+ import { Rect } from "./svgGeometry.js";
4
+ /** Every non-element node id the slice overlay needs a rect for (header cells),
5
+ * to fold into the same parseNodeRects() call as element ids. */
6
+ export declare function sliceOverlayIds(grid: Grid): string[];
7
+ /** Splice `data-slice="<index>"` onto every element's node group. */
8
+ export declare function tagSliceAttrs(svg: string, model: NormalizedModel): string;
9
+ /** `<metadata>` (slice ranges, as JSON) + `<style>` (the dim class) block, spliced
10
+ * just inside the root `<svg>`. `svg` is scanned (post-fitCanvas/tagSliceAttrs is
11
+ * fine — neither touches `<text>` elements) for the row-label column's x-range.
12
+ * Returns "" if no slice has a rect — the model is empty, or the SVG shape wasn't
13
+ * what parseNodeRects expected.
14
+ *
15
+ * rects (from parseNodeRects) and the row-label `<text>` x positions scanned below
16
+ * are both in graph-space — the coordinate system *inside* Graphviz's
17
+ * `<g class="graph" transform="scale(...) translate(...)">` wrapper — not the root
18
+ * SVG/viewBox space the served viewer sets `viewBox` in. Every x0/x1 here goes
19
+ * through readGraphTransform()'s toRoot() before it's embedded, the same
20
+ * conversion fitCanvas() applies to the edge-detour bbox for the same reason. */
21
+ export declare function buildSliceOverlay(svg: string, grid: Grid, rects: Map<string, Rect>): string;
@@ -0,0 +1,111 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Tags each element's rendered node group with its slice index, and embeds a
3
+ // <metadata> block describing every slice's name and x-range in the SVG.
4
+ //
5
+ // This is what lets the storyboard mode in the live viewer (`em watch --serve`,
6
+ // see src/render/serve.ts) highlight and pan/zoom to one slice at a time
7
+ // entirely client-side, with no separate data fetch — everything the browser
8
+ // needs travels inside the SVG it already loads.
9
+ //
10
+ // A slice's x-range comes from its header-cell rect (headerCellId), not from
11
+ // element rects — a slice can have empty rows, which would give an element-based
12
+ // bbox a wrong or missing range.
13
+ //
14
+ // The row-label (swimlane) column has no rect at all in parseNodeRects' sense:
15
+ // row labels render as bare `shape=plaintext` Graphviz nodes, which emit only a
16
+ // `<text>` element (no `<polygon>`/`<path>`), so they fall outside
17
+ // parseNodeRects' polygon/path-based extraction entirely. Their x-range is
18
+ // instead estimated straight from each `<text>` element: an exact center
19
+ // (Graphviz always emits `text-anchor="middle"` here) and a width approximated
20
+ // from character count and font size. That's a deliberate approximation local
21
+ // to this file — good enough to keep the swimlane labels in frame when the
22
+ // storyboard viewer pans/zooms to a slice, not meant to be pixel-accurate, and
23
+ // not worth teaching the shared parseNodeRects() a text-measuring fallback for
24
+ // its one caller that needs it.
25
+ import { headerCellId } from "../layout/grid.js";
26
+ import { rowLabelId } from "../emit/dot.js";
27
+ import { decode, readGraphTransform, NODE_GROUP, TITLE } from "./svgGeometry.js";
28
+ const TEXT = /<text\b([^>]*)>([^<]*)<\/text>/;
29
+ /** Every non-element node id the slice overlay needs a rect for (header cells),
30
+ * to fold into the same parseNodeRects() call as element ids. */
31
+ export function sliceOverlayIds(grid) {
32
+ const ids = [];
33
+ for (let c = 0; c < grid.cols; c++)
34
+ ids.push(headerCellId(c));
35
+ return ids;
36
+ }
37
+ /** Splice `data-slice="<index>"` onto every element's node group. */
38
+ export function tagSliceAttrs(svg, model) {
39
+ return svg.replace(NODE_GROUP, (block) => {
40
+ const id = decode(TITLE.exec(block)?.[1] ?? "");
41
+ const el = model.byId.get(id);
42
+ if (!el)
43
+ return block; // header cell, row label, or placeholder — leave untagged
44
+ return block.replace(/^<g\b/, `<g data-slice="${el.sliceIndex}"`);
45
+ });
46
+ }
47
+ /** `<metadata>` (slice ranges, as JSON) + `<style>` (the dim class) block, spliced
48
+ * just inside the root `<svg>`. `svg` is scanned (post-fitCanvas/tagSliceAttrs is
49
+ * fine — neither touches `<text>` elements) for the row-label column's x-range.
50
+ * Returns "" if no slice has a rect — the model is empty, or the SVG shape wasn't
51
+ * what parseNodeRects expected.
52
+ *
53
+ * rects (from parseNodeRects) and the row-label `<text>` x positions scanned below
54
+ * are both in graph-space — the coordinate system *inside* Graphviz's
55
+ * `<g class="graph" transform="scale(...) translate(...)">` wrapper — not the root
56
+ * SVG/viewBox space the served viewer sets `viewBox` in. Every x0/x1 here goes
57
+ * through readGraphTransform()'s toRoot() before it's embedded, the same
58
+ * conversion fitCanvas() applies to the edge-detour bbox for the same reason. */
59
+ export function buildSliceOverlay(svg, grid, rects) {
60
+ const transform = readGraphTransform(svg);
61
+ const toRootX = (x) => transform.toRoot(x, 0).x;
62
+ const slices = [];
63
+ for (let c = 0; c < grid.cols; c++) {
64
+ const r = rects.get(headerCellId(c));
65
+ if (!r)
66
+ continue;
67
+ slices.push({ index: c, name: grid.sliceNames[c], x0: toRootX(r.left), x1: toRootX(r.right) });
68
+ }
69
+ if (slices.length === 0)
70
+ return "";
71
+ const rowLabels = rowLabelXRange(svg, grid.rows.length, toRootX);
72
+ const payload = { slices, rowLabels };
73
+ const metadata = `<metadata id="em-slices">${escXml(JSON.stringify(payload))}</metadata>`;
74
+ const style = `<style>.em-slice-dim{opacity:.15;transition:opacity .15s ease}</style>`;
75
+ return metadata + style;
76
+ }
77
+ /** Estimate the row-label column's x-range from its `<text>` elements — see the
78
+ * file header comment for why this can't go through parseNodeRects. `toRootX`
79
+ * converts the graph-space text position (and the estimated half-width around
80
+ * it) into root/viewBox space — see buildSliceOverlay's comment. Returns null
81
+ * if no row label rendered any text (e.g. an empty model). */
82
+ function rowLabelXRange(svg, rowCount, toRootX) {
83
+ let range = null;
84
+ for (let r = 0; r < rowCount; r++) {
85
+ // Scoped to this row's own node group (up to its closing </g>) rather than a
86
+ // fixed-length lookahead — the header row's label is always "" (no <text> of
87
+ // its own), and a fixed window would otherwise pick up the *next* row's text.
88
+ const body = new RegExp(`<title>${rowLabelId(r)}<\\/title>([\\s\\S]*?)<\\/g>`).exec(svg)?.[1];
89
+ if (!body)
90
+ continue;
91
+ const m = TEXT.exec(body);
92
+ if (!m)
93
+ continue; // the header row's label is "" — plaintext emits no <text> at all
94
+ const x = +(attr(m[1], "x") ?? NaN);
95
+ if (Number.isNaN(x))
96
+ continue;
97
+ const fontSize = +(attr(m[1], "font-size") ?? 10);
98
+ const halfWidth = (decode(m[2]).length * fontSize) / 2 + 4; // + a little breathing room
99
+ const x0 = toRootX(x - halfWidth);
100
+ const x1 = toRootX(x + halfWidth);
101
+ range = range ? { x0: Math.min(range.x0, x0), x1: Math.max(range.x1, x1) } : { x0, x1 };
102
+ }
103
+ return range;
104
+ }
105
+ function attr(attrs, name) {
106
+ return new RegExp(`\\b${name}="(-?[\\d.]+)"`).exec(attrs)?.[1];
107
+ }
108
+ function escXml(s) {
109
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
110
+ }
111
+ //# sourceMappingURL=sliceOverlay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sliceOverlay.js","sourceRoot":"","sources":["../../src/render/sliceOverlay.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import { NormalizedModel } from "../model/model.js";
2
+ /** One status per slice, same order as model.slices (and so the same order as
3
+ * the header row's columns — see layout/grid.ts's sliceNames). null means no
4
+ * doc was found, or the doc has no `- **Status:** ...` line. */
5
+ export declare function readSliceStatuses(model: NormalizedModel, baseDir: string): (string | null)[];
@@ -0,0 +1,26 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Reads each slice's development status from its sibling design doc
3
+ // (slices/<kebab-slug>.md, see .claude/skills/event-modeling/templates/slice.md),
4
+ // for the main render pipeline (composeSvg) to color slice headers with.
5
+ //
6
+ // Same doc-path convention `em catalog` already uses (src/catalog/build.ts):
7
+ // the *deduped* export key from computeRefs(), not a fresh kebabSlug(name) —
8
+ // so two same-named slices resolve statuses the same way `em catalog` resolves
9
+ // docs for them (first one gets the real file, the second honestly gets null).
10
+ import { existsSync, readFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { computeRefs } from "../emit/json.js";
13
+ import { parseSliceDoc } from "../catalog/sliceDoc.js";
14
+ /** One status per slice, same order as model.slices (and so the same order as
15
+ * the header row's columns — see layout/grid.ts's sliceNames). null means no
16
+ * doc was found, or the doc has no `- **Status:** ...` line. */
17
+ export function readSliceStatuses(model, baseDir) {
18
+ const { sliceKeys } = computeRefs(model);
19
+ return model.slices.map((_slice, i) => {
20
+ const docPath = join(baseDir, "slices", `${sliceKeys[i]}.md`);
21
+ if (!existsSync(docPath))
22
+ return null;
23
+ return parseSliceDoc(readFileSync(docPath, "utf8")).status;
24
+ });
25
+ }
26
+ //# sourceMappingURL=sliceStatus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sliceStatus.js","sourceRoot":"","sources":["../../src/render/sliceStatus.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,oEAAoE;AACpE,kFAAkF;AAClF,yEAAyE;AACzE,EAAE;AACF,6EAA6E;AAC7E,6EAA6E;AAC7E,+EAA+E;AAC/E,+EAA+E;AAE/E,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAEvD;;iEAEiE;AACjE,MAAM,UAAU,iBAAiB,CAAC,KAAsB,EAAE,OAAe;IACvE,MAAM,EAAE,SAAS,EAAE,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IACzC,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;QACpC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAC9D,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC;QACtC,OAAO,aAAa,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;IAC7D,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { Grid } from "../layout/grid.js";
2
+ /** Recolor each header cell whose slice resolved to a known status (see
3
+ * statusStyle()). Slices with no doc, `draft`, or an unrecognized status
4
+ * string are left with the default header colors, untouched. */
5
+ export declare function applyStatusColors(svg: string, grid: Grid, statuses: (string | null)[]): string;
6
+ /**
7
+ * Grow the canvas and append a "Slice Status" legend below the diagram, one
8
+ * swatch+label row per distinct known status actually present among
9
+ * `statuses`. Returns the SVG unchanged if no slice resolved to a known
10
+ * status (nobody's using slice docs yet, or none set a recognized value), or
11
+ * if the SVG's dimensions can't be parsed. Independent of appendNoteLegend
12
+ * (src/render/drawNotes.ts) — safe to call after it since it recomputes the
13
+ * (possibly already-grown) viewBox itself, same as every other
14
+ * append-below-diagram step in the render pipeline.
15
+ */
16
+ export declare function appendStatusLegend(svg: string, grid: Grid, statuses: (string | null)[]): string;
@@ -0,0 +1,110 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Recolors each slice header cell's fill/stroke/font in a Graphviz-rendered SVG
3
+ // based on that slice's development status (see src/render/sliceStatus.ts),
4
+ // and appends a small legend explaining the colors actually used.
5
+ //
6
+ // This is a post-layout overlay, the same pattern as drawEdges.ts/drawNotes.ts:
7
+ // Graphviz always lays out every header cell identically (status never affects
8
+ // size, only color), so recoloring is a scoped string replace on the already-
9
+ // rendered header node group rather than anything that needs new geometry.
10
+ import { headerCellId } from "../layout/grid.js";
11
+ import { HEADER_FILL, HEADER_BORDER, HEADER_FONT } from "../emit/dot.js";
12
+ import { statusStyle } from "../emit/theme.js";
13
+ // Graphviz lowercases hex colors in its SVG output (e.g. our "#E3E7EB" comes
14
+ // back as "#e3e7eb"), so matching must be case-insensitive even though we
15
+ // authored the DOT-side constants in uppercase.
16
+ function attrRe(name, value) {
17
+ return new RegExp(`${name}="${value}"`, "gi");
18
+ }
19
+ /** Recolor each header cell whose slice resolved to a known status (see
20
+ * statusStyle()). Slices with no doc, `draft`, or an unrecognized status
21
+ * string are left with the default header colors, untouched. */
22
+ export function applyStatusColors(svg, grid, statuses) {
23
+ let out = svg;
24
+ for (let c = 0; c < grid.cols; c++) {
25
+ const style = statusStyle(statuses[c] ?? null);
26
+ if (!style)
27
+ continue;
28
+ const id = headerCellId(c);
29
+ const re = new RegExp(`(<title>${id}</title>)([\\s\\S]*?)(</g>)`);
30
+ out = out.replace(re, (_whole, title, body, close) => {
31
+ const recolored = body
32
+ .replace(attrRe("fill", HEADER_FILL), `fill="${style.fill}"`)
33
+ .replace(attrRe("stroke", HEADER_BORDER), `stroke="${style.stroke}"`)
34
+ .replace(attrRe("fill", HEADER_FONT), `fill="${style.fontColor}"`);
35
+ return title + recolored + close;
36
+ });
37
+ }
38
+ return out;
39
+ }
40
+ // ---- legend ----
41
+ const PAD_X = 16;
42
+ const PAD_TOP = 14;
43
+ const HEAD_H = 22;
44
+ const LINE_H = 18;
45
+ const PAD_BOTTOM = 14;
46
+ const SWATCH = 11;
47
+ /** Canonical display order, independent of column order — a model's slices can
48
+ * land in any status in any order, but the legend should read the same way
49
+ * (earliest-to-latest lifecycle stage) every time. */
50
+ const STATUS_ORDER = ["reviewed", "ready-to-implement", "implemented"];
51
+ function humanizeStatus(status) {
52
+ return status.charAt(0).toUpperCase() + status.slice(1).replace(/-/g, " ");
53
+ }
54
+ /**
55
+ * Grow the canvas and append a "Slice Status" legend below the diagram, one
56
+ * swatch+label row per distinct known status actually present among
57
+ * `statuses`. Returns the SVG unchanged if no slice resolved to a known
58
+ * status (nobody's using slice docs yet, or none set a recognized value), or
59
+ * if the SVG's dimensions can't be parsed. Independent of appendNoteLegend
60
+ * (src/render/drawNotes.ts) — safe to call after it since it recomputes the
61
+ * (possibly already-grown) viewBox itself, same as every other
62
+ * append-below-diagram step in the render pipeline.
63
+ */
64
+ export function appendStatusLegend(svg, grid, statuses) {
65
+ const present = new Set(statuses.slice(0, grid.cols).map((s) => s?.toLowerCase()).filter((s) => !!s));
66
+ const rows = STATUS_ORDER.filter((s) => present.has(s));
67
+ if (rows.length === 0)
68
+ return svg;
69
+ const vb = /viewBox="([\d.eE+-]+)\s+([\d.eE+-]+)\s+([\d.eE+-]+)\s+([\d.eE+-]+)"/.exec(svg);
70
+ const hpt = /height="([\d.eE+-]+)pt"/.exec(svg);
71
+ if (!vb || !hpt)
72
+ return svg;
73
+ const minX = +vb[1];
74
+ const minY = +vb[2];
75
+ const vw = +vb[3];
76
+ const vh = +vb[4];
77
+ const legendH = PAD_TOP + HEAD_H + rows.length * LINE_H + PAD_BOTTOM;
78
+ const newVh = vh + legendH;
79
+ const newHpt = +hpt[1] * (newVh / vh);
80
+ const top = minY + vh;
81
+ let out = svg;
82
+ out = out.replace(hpt[0], `height="${n(newHpt)}pt"`);
83
+ out = out.replace(vb[0], `viewBox="${vb[1]} ${vb[2]} ${vb[3]} ${n(newVh)}"`);
84
+ const sectionY = top + PAD_TOP;
85
+ let sections = `<text x="${n(minX + PAD_X)}" y="${n(sectionY + 14)}" font-family="Helvetica" ` +
86
+ `font-weight="bold" font-size="12" fill="#202124">Slice Status</text>`;
87
+ sections += rows
88
+ .map((status, i) => {
89
+ const style = statusStyle(status);
90
+ const y = sectionY + HEAD_H + i * LINE_H;
91
+ return (`<rect x="${n(minX + PAD_X)}" y="${n(y)}" width="${SWATCH}" height="${SWATCH}" ` +
92
+ `fill="${style.fill}" stroke="${style.stroke}"/>` +
93
+ `<text x="${n(minX + PAD_X + SWATCH + 6)}" y="${n(y + SWATCH - 1)}" font-family="Helvetica" ` +
94
+ `font-size="11" fill="#3C4043">${esc(humanizeStatus(status))}</text>`);
95
+ })
96
+ .join("");
97
+ const legend = `<g class="em-status-legend">` +
98
+ `<rect x="${n(minX)}" y="${n(top)}" width="${n(vw)}" height="${n(legendH)}" fill="#FFFFFF"/>` +
99
+ `<line x1="${n(minX)}" y1="${n(top)}" x2="${n(minX + vw)}" y2="${n(top)}" stroke="#D0D0D0" stroke-width="1"/>` +
100
+ sections +
101
+ `</g>`;
102
+ return out.replace(/(<\/g>\s*)(<\/svg>)/, `$1${legend}$2`);
103
+ }
104
+ function n(v) {
105
+ return String(Math.round(v * 100) / 100);
106
+ }
107
+ function esc(s) {
108
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
109
+ }
110
+ //# sourceMappingURL=statusOverlay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"statusOverlay.js","sourceRoot":"","sources":["../../src/render/statusOverlay.ts"],"names":[],"mappings":"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"}
@@ -6,6 +6,8 @@ export interface Rect {
6
6
  cx: number;
7
7
  cy: number;
8
8
  }
9
+ export declare const NODE_GROUP: RegExp;
10
+ export declare const TITLE: RegExp;
9
11
  /** Map each requested node id to its rectangle in SVG coordinates. */
10
12
  export declare function parseNodeRects(svg: string, ids: Set<string>): Map<string, Rect>;
11
13
  /** A bounding box in the graph's own (post-transform) coordinate space. */
@@ -15,9 +17,37 @@ export interface Box {
15
17
  maxX: number;
16
18
  maxY: number;
17
19
  }
20
+ /** scale/translate read from the graph's own transform, and the root-space
21
+ * conversion it implies — see readGraphTransform() for where this comes from. */
22
+ export interface GraphTransform {
23
+ sx: number;
24
+ sy: number;
25
+ tx: number;
26
+ ty: number;
27
+ /** Convert a graph-space (pre-transform) point — e.g. a rect from
28
+ * parseNodeRects(), or a raw `<text>` x/y — into root/viewBox space. */
29
+ toRoot(x: number, y: number): {
30
+ x: number;
31
+ y: number;
32
+ };
33
+ }
34
+ /**
35
+ * Graphviz wraps the graph in `transform="scale(sx sy) rotate(0) translate(tx ty)"`,
36
+ * applied right to left, so a graph-space point lands at `scale * (p + translate)`.
37
+ * Anything computed from parseNodeRects()'s box coordinates (or a `<text>` element's
38
+ * raw x/y) is in that same pre-transform graph space — it must go through
39
+ * `toRoot()` before it's valid as a viewBox coordinate or compared against one.
40
+ * Defaults to the identity transform (scale 1, translate 0) if the graph group or
41
+ * its transform attribute isn't present, matching what Graphviz emits when neither
42
+ * scaling nor an offset was needed.
43
+ */
44
+ export declare function readGraphTransform(svg: string): GraphTransform;
18
45
  /**
19
46
  * Grow the canvas so content drawn in graph space isn't clipped by the root viewBox.
20
47
  * Edge detours can legitimately run outside the box grid Graphviz sized the canvas for
21
48
  * (an arc under the bottom lane, say), and anything past the viewBox is simply lost.
22
49
  */
23
50
  export declare function fitCanvas(svg: string, box: Box | null): string;
51
+ /** Decode the handful of entities Graphviz emits in `<title>` text. Shared with
52
+ * sliceOverlay.ts, which also needs to recover an element id from a title. */
53
+ export declare function decode(s: string): string;