@milehimikey/em 1.19.7 → 1.20.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 (93) hide show
  1. package/.claude/skills/event-modeling/SKILL.md +1 -1
  2. package/.claude/skills/event-modeling-conform/SKILL.md +1 -1
  3. package/.claude/skills/event-modeling-conform/reference/conform.md +29 -1
  4. package/.claude/skills/event-modeling-design/SKILL.md +14 -3
  5. package/.claude/skills/event-modeling-discover/SKILL.md +1 -1
  6. package/.claude/skills/event-modeling-discover/reference/extract.md +8 -0
  7. package/.claude/skills/event-modeling-engagement/SKILL.md +1 -1
  8. package/.claude/skills/event-modeling-implement/SKILL.md +7 -2
  9. package/.claude/skills/event-modeling-implement/reference/implement.md +73 -6
  10. package/.claude/skills/event-modeling-review/SKILL.md +1 -1
  11. package/.claude/skills/event-modeling-shared/reference/em-dsl.md +22 -2
  12. package/.claude/skills/event-modeling-shared/reference/methodology.md +16 -0
  13. package/.claude/skills/event-modeling-shared/templates/slice.md +6 -1
  14. package/.claude-plugin/marketplace.json +2 -2
  15. package/dist/catalog/docBadge.d.ts +16 -0
  16. package/dist/catalog/docBadge.js +50 -0
  17. package/dist/catalog/docBadge.js.map +1 -0
  18. package/dist/catalog/docModelConsistencyValidate.js +9 -2
  19. package/dist/catalog/docModelConsistencyValidate.js.map +1 -1
  20. package/dist/catalog/invTokens.d.ts +49 -0
  21. package/dist/catalog/invTokens.js +94 -0
  22. package/dist/catalog/invTokens.js.map +1 -0
  23. package/dist/catalog/pages.js +48 -1
  24. package/dist/catalog/pages.js.map +1 -1
  25. package/dist/catalog/scenarios.d.ts +4 -0
  26. package/dist/catalog/scenarios.js +4 -1
  27. package/dist/catalog/scenarios.js.map +1 -1
  28. package/dist/catalog/sliceDoc.d.ts +46 -1
  29. package/dist/catalog/sliceDoc.js +90 -2
  30. package/dist/catalog/sliceDoc.js.map +1 -1
  31. package/dist/catalog/sliceDocRender.d.ts +3 -0
  32. package/dist/catalog/sliceDocRender.js +295 -0
  33. package/dist/catalog/sliceDocRender.js.map +1 -0
  34. package/dist/cli/coverage.d.ts +13 -23
  35. package/dist/cli/coverage.js +51 -91
  36. package/dist/cli/coverage.js.map +1 -1
  37. package/dist/cli/refsCheck.d.ts +52 -0
  38. package/dist/cli/refsCheck.js +127 -1
  39. package/dist/cli/refsCheck.js.map +1 -1
  40. package/dist/cli/sliceConform.d.ts +57 -1
  41. package/dist/cli/sliceConform.js +109 -9
  42. package/dist/cli/sliceConform.js.map +1 -1
  43. package/dist/cli/watchStoryline.d.ts +27 -0
  44. package/dist/cli/watchStoryline.js +55 -0
  45. package/dist/cli/watchStoryline.js.map +1 -0
  46. package/dist/cli.js +161 -7
  47. package/dist/cli.js.map +1 -1
  48. package/dist/emit/docJson.d.ts +99 -0
  49. package/dist/emit/docJson.js +197 -0
  50. package/dist/emit/docJson.js.map +1 -0
  51. package/dist/emit/refsJson.d.ts +5 -3
  52. package/dist/emit/refsJson.js +5 -2
  53. package/dist/emit/refsJson.js.map +1 -1
  54. package/dist/mcp/server.js +23 -3
  55. package/dist/mcp/server.js.map +1 -1
  56. package/dist/model/continuation.d.ts +9 -0
  57. package/dist/model/continuation.js +30 -0
  58. package/dist/model/continuation.js.map +1 -1
  59. package/dist/model/rules.d.ts +8 -0
  60. package/dist/model/rules.js +8 -0
  61. package/dist/model/rules.js.map +1 -1
  62. package/dist/model/shapeLint.js +48 -0
  63. package/dist/model/shapeLint.js.map +1 -1
  64. package/dist/model/validate.js +119 -0
  65. package/dist/model/validate.js.map +1 -1
  66. package/dist/parser/ast.d.ts +8 -0
  67. package/dist/parser/parser.js +51 -1
  68. package/dist/parser/parser.js.map +1 -1
  69. package/dist/render/render.js +1 -1
  70. package/dist/render/render.js.map +1 -1
  71. package/dist/render/sliceOverlay.d.ts +7 -2
  72. package/dist/render/sliceOverlay.js +28 -3
  73. package/dist/render/sliceOverlay.js.map +1 -1
  74. package/dist/render/storylinePicker.d.ts +9 -0
  75. package/dist/render/storylinePicker.js +17 -0
  76. package/dist/render/storylinePicker.js.map +1 -0
  77. package/dist/render/viewerHtml.js +1 -1
  78. package/dist/render/viewerHtml.js.map +1 -1
  79. package/package.json +1 -1
  80. package/plugin/.claude-plugin/plugin.json +2 -2
  81. package/plugin/shared/reference/em-dsl.md +22 -2
  82. package/plugin/shared/reference/methodology.md +16 -0
  83. package/plugin/shared/templates/slice.md +6 -1
  84. package/plugin/skills/conform/SKILL.md +1 -1
  85. package/plugin/skills/conform/reference/conform.md +29 -1
  86. package/plugin/skills/design/SKILL.md +14 -3
  87. package/plugin/skills/discover/SKILL.md +1 -1
  88. package/plugin/skills/discover/reference/extract.md +8 -0
  89. package/plugin/skills/engagement/SKILL.md +1 -1
  90. package/plugin/skills/event-modeling/SKILL.md +1 -1
  91. package/plugin/skills/implement/SKILL.md +7 -2
  92. package/plugin/skills/implement/reference/implement.md +73 -6
  93. package/plugin/skills/review/SKILL.md +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/render/render.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/render/render.ts"],"names":[],"mappings":"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"}
@@ -22,5 +22,10 @@ export declare function tagSliceAttrs(svg: string, model: NormalizedModel): stri
22
22
  *
23
23
  * `docs` is each slice's resolved doc (or null), same order as `grid`'s columns —
24
24
  * see render/sliceStatus.ts's `readSliceDocs`, the same resolution that colors the
25
- * slice's header by status. */
26
- export declare function buildSliceOverlay(svg: string, grid: Grid, rects: Map<string, Rect>, docs: (SliceDoc | null)[]): string;
25
+ * slice's header by status.
26
+ *
27
+ * GH #309: a stub column (`Slice.stub`, `em render --storyline`'s subset diagram) is excluded
28
+ * from the embedded list entirely — it's still tagged/dimmed in the SVG itself (dimStubColumns)
29
+ * so its arrows still have a source, but it isn't one of "this storyline's own slices," so
30
+ * Review mode's Prev/Next and filmstrip must never step onto it. */
31
+ export declare function buildSliceOverlay(svg: string, model: NormalizedModel, grid: Grid, rects: Map<string, Rect>, docs: (SliceDoc | null)[]): string;
@@ -26,6 +26,8 @@
26
26
  // its one caller that needs it.
27
27
  import { headerCellId } from "../layout/grid.js";
28
28
  import { rowLabelId } from "../emit/dot.js";
29
+ import { classifyImplementationDrift } from "../catalog/driftSignal.js";
30
+ import { renderFrontmatterBadgeRow } from "../catalog/docBadge.js";
29
31
  import { decode, readGraphTransform, NODE_GROUP, TITLE } from "./svgGeometry.js";
30
32
  const TEXT = /<text\b([^>]*)>([^<]*)<\/text>/;
31
33
  /** Every non-element node id the slice overlay needs a rect for (header cells),
@@ -46,6 +48,22 @@ export function tagSliceAttrs(svg, model) {
46
48
  return block.replace(/^<g\b/, `<g data-slice="${el.sliceIndex}"`);
47
49
  });
48
50
  }
51
+ /** GH #402: `doc.html` prefixed with its frontmatter badge row (pattern/version/status/
52
+ * implementation-drift), or null when there's no doc. A doc with no frontmatter fence at all
53
+ * (a legacy freeform doc) gets no badge row, same as catalog/pages.ts's own gate. */
54
+ function docHtmlFor(doc) {
55
+ if (!doc)
56
+ return null;
57
+ if (!doc.frontmatterPresent)
58
+ return doc.html;
59
+ const badge = renderFrontmatterBadgeRow({
60
+ pattern: doc.pattern,
61
+ version: doc.version,
62
+ status: doc.status,
63
+ driftSignal: classifyImplementationDrift(doc),
64
+ });
65
+ return badge + doc.html;
66
+ }
49
67
  /** `<metadata>` (slice ranges, as JSON) + `<style>` (the dim class) block, spliced
50
68
  * just inside the root `<svg>`. `svg` is scanned (post-fitCanvas/tagSliceAttrs is
51
69
  * fine — neither touches `<text>` elements) for the row-label column's x-range.
@@ -61,12 +79,19 @@ export function tagSliceAttrs(svg, model) {
61
79
  *
62
80
  * `docs` is each slice's resolved doc (or null), same order as `grid`'s columns —
63
81
  * see render/sliceStatus.ts's `readSliceDocs`, the same resolution that colors the
64
- * slice's header by status. */
65
- export function buildSliceOverlay(svg, grid, rects, docs) {
82
+ * slice's header by status.
83
+ *
84
+ * GH #309: a stub column (`Slice.stub`, `em render --storyline`'s subset diagram) is excluded
85
+ * from the embedded list entirely — it's still tagged/dimmed in the SVG itself (dimStubColumns)
86
+ * so its arrows still have a source, but it isn't one of "this storyline's own slices," so
87
+ * Review mode's Prev/Next and filmstrip must never step onto it. */
88
+ export function buildSliceOverlay(svg, model, grid, rects, docs) {
66
89
  const transform = readGraphTransform(svg);
67
90
  const toRootX = (x) => transform.toRoot(x, 0).x;
68
91
  const slices = [];
69
92
  for (let c = 0; c < grid.cols; c++) {
93
+ if (model.slices[c]?.stub)
94
+ continue;
70
95
  const r = rects.get(headerCellId(c));
71
96
  if (!r)
72
97
  continue;
@@ -75,7 +100,7 @@ export function buildSliceOverlay(svg, grid, rects, docs) {
75
100
  name: grid.sliceNames[c],
76
101
  x0: toRootX(r.left),
77
102
  x1: toRootX(r.right),
78
- docHtml: docs[c]?.html ?? null,
103
+ docHtml: docHtmlFor(docs[c] ?? null),
79
104
  });
80
105
  }
81
106
  if (slices.length === 0)
@@ -1 +1 @@
1
- {"version":3,"file":"sliceOverlay.js","sourceRoot":"","sources":["../../src/render/sliceOverlay.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sliceOverlay.js","sourceRoot":"","sources":["../../src/render/sliceOverlay.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /** One entry of the live viewer's storyline picker: a file this `em watch --serve` session
2
+ * writes (the full model, or one storyline's subset — see src/cli/watchStoryline.ts), and
3
+ * whether it's the one currently loaded. */
4
+ export interface StorylineVariant {
5
+ name: string;
6
+ file: string;
7
+ current: boolean;
8
+ }
9
+ export declare function embedStorylinePicker(svg: string, variants: StorylineVariant[]): string;
@@ -0,0 +1,17 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Embeds the live viewer's storyline-picker metadata (GH #309, deferred from #277) into a
3
+ // composed SVG: a flat `<metadata id="em-storylines">` JSON array, read by viewer.html to
4
+ // render a <select> that switches between the full model and any declared storyline without
5
+ // restarting `em watch --serve`. Spliced the same way sliceOverlay.ts's own
6
+ // `<metadata id="em-slices">` is — just inside the root `<svg>` — and a no-op (returns `svg`
7
+ // unchanged) when there's nothing to pick between, i.e. the model declares no storylines.
8
+ export function embedStorylinePicker(svg, variants) {
9
+ if (variants.length === 0)
10
+ return svg;
11
+ const metadata = `<metadata id="em-storylines">${escXml(JSON.stringify(variants))}</metadata>`;
12
+ return svg.replace(/(<svg\b[^>]*>)/, `$1${metadata}`);
13
+ }
14
+ function escXml(s) {
15
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
16
+ }
17
+ //# sourceMappingURL=storylinePicker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storylinePicker.js","sourceRoot":"","sources":["../../src/render/storylinePicker.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,0FAA0F;AAC1F,0FAA0F;AAC1F,4FAA4F;AAC5F,4EAA4E;AAC5E,6FAA6F;AAC7F,0FAA0F;AAW1F,MAAM,UAAU,oBAAoB,CAAC,GAAW,EAAE,QAA4B;IAC5E,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IACtC,MAAM,QAAQ,GAAG,gCAAgC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,aAAa,CAAC;IAC/F,OAAO,GAAG,CAAC,OAAO,CAAC,gBAAgB,EAAE,KAAK,QAAQ,EAAE,CAAC,CAAC;AACxD,CAAC;AAED,SAAS,MAAM,CAAC,CAAS;IACvB,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC9E,CAAC"}
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // GENERATED from src/render/viewer.html by scripts/generate-viewer-html.ts — do not edit.
3
3
  // After editing viewer.html, run: npm run viewer:generate
4
- export const VIEWER_HTML = "<!doctype html>\n<!--\n The live viewer page, served at / by `em watch --serve` (src/render/serve.ts).\n\n This file is the SOURCE OF TRUTH for the viewer. It ships as the generated\n constant in src/render/viewerHtml.ts - after editing this file, run\n `npm run viewer:generate` to regenerate it (a sync test fails otherwise).\n\n Self-contained: no external assets, all state in the one script below, driven\n by SSE push - the browser reloads only when the server says the SVG changed.\n\n The SVG is inlined (fetch, DOMParser, validate, then swap) rather than\n embedded via an object tag: a truncated or malformed file is detected BEFORE\n touching the DOM, so a bad render can never blank the shared screen - the\n last good frame stays up behind an error banner while the viewer retries\n with backoff. A save that fails to render entirely (parse error) arrives as\n an SSE \"renderError\" and lands in the same banner over the intact diagram.\n Inline SVG also keeps note links clickable and hands the camera a real\n viewBox to drive.\n\n The camera IS the svg viewBox (cam = x/y/w/h): drag and wheel pan,\n ctrl+wheel (what a trackpad pinch emits) and two-pointer pinch zoom about\n the cursor, and flyTo() animates slice navigation and fit. Driving viewBox\n directly (not CSS transforms) keeps text crisp and makes the client-to-model\n coordinate math a single exact mapping.\n\n Review mode: a Prev/Next storyboard that highlights and flies to one slice\n at a time, reading the data-slice attributes and the em-slices metadata that\n src/render/sliceOverlay.ts embeds in every rendered SVG. Review state lives\n outside the load pipeline and is re-applied after every SSE-triggered swap -\n a facilitator hand-editing the model live must not lose their place.\n\n Doc flyout (MIL-153): clicking any element belonging to a slice opens a\n side panel with that slice's own design doc, rendered server-side to HTML\n and carried in the same em-slices metadata (no separate fetch — see\n src/render/sliceOverlay.ts's docHtml field). Independent of review mode: a\n slice click never moves the camera, and stepping/navigating never touches\n the flyout. Like review state, which slice's doc is open lives outside the\n load pipeline, refreshed (or closed, if the slice vanished) after every\n reload rather than reset by one.\n-->\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <title>Event Model — Live</title>\n <style>\n :root { color-scheme: light dark; }\n html, body { height: 100%; }\n body {\n margin: 0; font-family: system-ui, sans-serif; background: #fafafa;\n display: flex; flex-direction: column; overflow: hidden;\n }\n header {\n display: flex; align-items: center; gap: .75rem; flex: none;\n padding: .5rem .9rem; background: #1f2933; color: #fff; font-size: 14px;\n }\n header .dot { width: 9px; height: 9px; border-radius: 50%; background: #34c759; transition: background .3s; flex: none; }\n header .dot.stale { background: #f0a020; }\n header .stamp { margin-left: auto; opacity: .7; font-variant-numeric: tabular-nums; }\n header .zoom { display: flex; align-items: center; gap: .3rem; }\n #zoomPct { font-size: 12px; opacity: .7; min-width: 3.2em; text-align: center; font-variant-numeric: tabular-nums; }\n .btn {\n background: #3b4754; color: #fff; border: 0; border-radius: 4px;\n padding: .3rem .7rem; font-size: 13px; cursor: pointer; font-family: inherit;\n }\n .btn:hover { background: #4b5763; }\n .btn:disabled { opacity: .4; cursor: default; }\n .btn.active { background: #2f6fed; }\n #errorBanner {\n display: flex; align-items: center; gap: .6rem; flex: none;\n padding: .45rem .9rem; background: #8c2f28; color: #fff; font-size: 13px;\n }\n #errorBanner[hidden] { display: none; }\n #errorBanner button {\n margin-left: auto; background: none; border: 0; color: #fff;\n font-size: 16px; line-height: 1; cursor: pointer; opacity: .8;\n }\n #storyboard {\n display: flex; align-items: center; gap: .5rem; padding: .4rem .9rem; flex: none;\n background: #e8eaed; border-bottom: 1px solid #d0d3d7;\n }\n #storyboard[hidden] { display: none; }\n #storyboard .nav { background: none; border: 0; font-size: 18px; line-height: 1; cursor: pointer; color: #3b4754; padding: 0 .3rem; }\n #storyboard .nav:disabled { opacity: .3; cursor: default; }\n #filmstrip { display: flex; gap: .35rem; overflow-x: auto; }\n #filmstrip .slice {\n padding: .25rem .6rem; border-radius: 12px; background: #fff; border: 1px solid #d0d3d7;\n font-size: 12px; white-space: nowrap; cursor: pointer; color: #3b4754; font-family: inherit;\n }\n #filmstrip .slice.active { background: #2f6fed; border-color: #2f6fed; color: #fff; font-weight: 600; }\n #stage {\n flex: 1 1 auto; min-height: 0; position: relative; overflow: hidden;\n touch-action: none; cursor: grab; user-select: none; -webkit-user-select: none;\n }\n #stage.dragging { cursor: grabbing; }\n #stage svg { width: 100%; height: 100%; display: block; }\n #docFlyout {\n position: absolute; top: 0; right: 0; bottom: 0; width: min(640px, 94%);\n background: #fff; border-left: 1px solid #d0d3d7; box-shadow: -2px 0 10px rgba(0, 0, 0, .15);\n display: flex; flex-direction: column; z-index: 5;\n transform: translateX(100%); transition: transform .2s ease;\n /* #stage sets these for its own drag-to-pan surface; the flyout is a normal\n panel — its text should be selectable and its cursor should look normal. */\n user-select: text; -webkit-user-select: text; cursor: auto; touch-action: auto;\n }\n #docFlyout.open { transform: translateX(0); }\n #docFlyout .docHead {\n display: flex; align-items: center; gap: .5rem; flex: none;\n padding: .55rem .9rem; background: #1f2933; color: #fff; font-size: 14px; font-weight: 600;\n }\n #docFlyout .docHead span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n #docFlyout .docHead button {\n margin-left: auto; background: none; border: 0; color: #fff;\n font-size: 18px; line-height: 1; cursor: pointer; opacity: .8; flex: none;\n }\n #docFlyout .docBody {\n /* min-height: 0 overrides a flex item's default min-height: auto, which would\n otherwise let this grow to fit all its content instead of scrolling — the\n actual cause of \"no way to scroll it\": without it the body just spills past\n the flyout's bottom edge, clipped invisibly by #stage's own overflow:hidden. */\n flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden;\n padding: .9rem 1.1rem; font-size: 13px; line-height: 1.55; color: #202124;\n }\n #docFlyout .docBody.empty { color: #5f6368; font-style: italic; }\n #docFlyout .docBody img { max-width: 100%; height: auto; display: block; margin: .5em 0; }\n #docFlyout .docBody :is(h1, h2, h3, h4, h5, h6) { margin: 1em 0 .35em; line-height: 1.3; }\n #docFlyout .docBody :is(h1, h2, h3, h4, h5, h6):first-child { margin-top: 0; }\n #docFlyout .docBody p, #docFlyout .docBody ul, #docFlyout .docBody ol { margin: .5em 0; }\n #docFlyout .docBody ul, #docFlyout .docBody ol { padding-left: 1.3em; }\n #docFlyout .docBody a { color: #2f6fed; }\n #docFlyout .docBody code { background: #f0f1f3; padding: .1em .3em; border-radius: 3px; font-size: 12px; }\n #docFlyout .docBody pre { background: #f0f1f3; padding: .6rem; border-radius: 4px; overflow-x: auto; }\n #docFlyout .docBody pre code { background: none; padding: 0; }\n #docFlyout .docBody blockquote {\n margin: .5em 0; padding-left: .8em; border-left: 3px solid #d0d3d7; color: #5f6368;\n }\n #docFlyout .docBody table { border-collapse: collapse; margin: .5em 0; }\n #docFlyout .docBody :is(th, td) { border: 1px solid #d0d3d7; padding: .3em .5em; }\n </style>\n</head>\n<body>\n <header>\n <span class=\"dot\" id=\"dot\"></span>\n <span>Event Model — live</span>\n <button class=\"btn\" id=\"reviewToggle\" title=\"Step through one slice at a time\">Review mode</button>\n <span class=\"zoom\">\n <button class=\"btn\" id=\"zoomOut\" aria-label=\"Zoom out\" title=\"Zoom out (-)\">−</button>\n <span id=\"zoomPct\">100%</span>\n <button class=\"btn\" id=\"zoomIn\" aria-label=\"Zoom in\" title=\"Zoom in (+)\">+</button>\n <button class=\"btn\" id=\"fitBtn\" title=\"Fit the whole diagram (0)\">Fit</button>\n </span>\n <span class=\"stamp\" id=\"stamp\">connecting…</span>\n </header>\n <div id=\"errorBanner\" hidden>\n <span id=\"errorText\"></span>\n <button id=\"errorDismiss\" aria-label=\"Dismiss\">×</button>\n </div>\n <nav id=\"storyboard\" hidden>\n <button class=\"nav\" id=\"prevSlice\" aria-label=\"Previous slice\">‹</button>\n <div id=\"filmstrip\"></div>\n <button class=\"nav\" id=\"nextSlice\" aria-label=\"Next slice\">›</button>\n </nav>\n <div id=\"stage\">\n <aside id=\"docFlyout\">\n <div class=\"docHead\">\n <span id=\"docFlyoutTitle\"></span>\n <button id=\"docFlyoutClose\" aria-label=\"Close\">×</button>\n </div>\n <div class=\"docBody\" id=\"docFlyoutBody\"></div>\n </aside>\n </div>\n <script>\n // ?svg=<name>.svg picks the model file (default model.svg), so one server\n // serves any model without editing this page. The allowlist keeps the\n // value a bare *.svg filename inside the served directory — no path\n // separator, no drive/scheme colon — so it is safe to fetch (always\n // URI-encoded) and to display (via textContent only, never markup).\n // Spaces and unicode are fine: em watch prints whatever the output file\n // is called, and that URL has to work.\n const svgParam = new URLSearchParams(location.search).get(\"svg\");\n const SVG_FILE = svgParam && /^[^/\\\\:]+\\.svg$/.test(svgParam) ? svgParam : \"model.svg\";\n\n const stage = document.getElementById(\"stage\");\n const stamp = document.getElementById(\"stamp\");\n const dot = document.getElementById(\"dot\");\n const banner = document.getElementById(\"errorBanner\");\n const bannerText = document.getElementById(\"errorText\");\n const reviewToggle = document.getElementById(\"reviewToggle\");\n const storyboard = document.getElementById(\"storyboard\");\n const filmstrip = document.getElementById(\"filmstrip\");\n const prevBtn = document.getElementById(\"prevSlice\");\n const nextBtn = document.getElementById(\"nextSlice\");\n const zoomPct = document.getElementById(\"zoomPct\");\n const docFlyout = document.getElementById(\"docFlyout\");\n const docFlyoutTitle = document.getElementById(\"docFlyoutTitle\");\n const docFlyoutBody = document.getElementById(\"docFlyoutBody\");\n const docFlyoutClose = document.getElementById(\"docFlyoutClose\");\n\n // --- review-mode state: outside the load pipeline on purpose, see the\n // file-level comment above — a reload re-applies it, never resets it. ---\n let reviewMode = false;\n let activeSlice = null; // slice index, or null for \"show all\"\n let slices = []; // [{index, name, x0, x1, docHtml}], from this load's <metadata>\n let rowLabels = null; // {x0, x1} | null, the swimlane label column's range\n\n // --- doc flyout state: independent of review mode, outside the load\n // pipeline for the same reason review state is — see the file comment. ---\n let openSliceIndex = null; // slice index whose doc the flyout shows, or null when closed\n\n // --- camera state ---\n let svgEl = null; // the inline <svg>, null until the first good load\n let natural = null; // the loaded SVG's own viewBox: its full-fit frame\n let cam = null; // the current viewBox {x, y, w, h}\n // True while the user has never panned/zoomed (or pressed Fit last): a\n // reload then re-fits to the new natural box instead of pinning the old\n // absolute view onto a diagram that may have grown or shrunk.\n let atFit = true;\n\n function stageRect() { return stage.getBoundingClientRect(); }\n\n // Uniform px→model scale of the current view. The svg keeps the default\n // preserveAspectRatio (xMidYMid meet): one scale factor, centered.\n function scale() {\n const r = stageRect();\n return Math.max(cam.w / Math.max(1, r.width), cam.h / Math.max(1, r.height));\n }\n\n // Exact client-px → model-coords inverse of the \"meet\" mapping, so\n // zoom-about-cursor keeps the model point under the cursor exactly.\n function toSvg(cx, cy) {\n const r = stageRect();\n const s = scale();\n const ox = cam.x - (r.width * s - cam.w) / 2;\n const oy = cam.y - (r.height * s - cam.h) / 2;\n return { x: ox + (cx - r.left) * s, y: oy + (cy - r.top) * s };\n }\n\n // Fit a model-space rect into the stage aspect, centered.\n function frameRect(x, y, w, h) {\n const r = stageRect();\n const aspect = Math.max(1, r.width) / Math.max(1, r.height);\n let fw = w, fh = h;\n if (fw / fh < aspect) fw = fh * aspect; else fh = fw / aspect;\n return { x: x + (w - fw) / 2, y: y + (h - fh) / 2, w: fw, h: fh };\n }\n\n function fitCam() { return frameRect(natural.x, natural.y, natural.w, natural.h); }\n\n // New cam zoomed by the given factor about model point p, clamped to\n // 0.5×–30× of fit so the diagram is neither lost nor over-magnified.\n function zoomedCam(p, factor) {\n const fitW = fitCam().w;\n const w = Math.min(fitW * 2, Math.max(fitW / 30, cam.w / factor));\n const f = cam.w / w;\n return clampPanOf({\n x: p.x - (p.x - cam.x) / f,\n y: p.y - (p.y - cam.y) / f,\n w: cam.w / f,\n h: cam.h / f,\n });\n }\n\n // Keep at least half of min(view, diagram) in frame on each axis, so the\n // diagram can never be panned fully out of view.\n function clampPanOf(c) {\n if (!natural) return c;\n const kx = Math.min(c.w, natural.w) / 2;\n const ky = Math.min(c.h, natural.h) / 2;\n c.x = Math.max(natural.x - c.w + kx, Math.min(natural.x + natural.w - kx, c.x));\n c.y = Math.max(natural.y - c.h + ky, Math.min(natural.y + natural.h - ky, c.y));\n return c;\n }\n function clampPan() { cam = clampPanOf(cam); }\n\n function applyCam() {\n if (!svgEl || !cam) return;\n svgEl.setAttribute(\"viewBox\", cam.x + \" \" + cam.y + \" \" + cam.w + \" \" + cam.h);\n if (natural) zoomPct.textContent = Math.round((fitCam().w / cam.w) * 100) + \"%\";\n }\n\n function markMoved() { atFit = false; }\n\n // Animated camera move; any direct manipulation (drag/wheel/pinch) cancels\n // it and takes over from wherever the animation got to.\n let flyHandle = null;\n function flyTo(target) {\n cancelFly();\n if (!cam) { cam = target; applyCam(); return; }\n const from = { x: cam.x, y: cam.y, w: cam.w, h: cam.h };\n const t0 = performance.now();\n const DUR = 280;\n function ease(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; }\n function frame(now) {\n const t = Math.min(1, (now - t0) / DUR);\n const k = ease(t);\n cam = {\n x: from.x + (target.x - from.x) * k,\n y: from.y + (target.y - from.y) * k,\n w: from.w + (target.w - from.w) * k,\n h: from.h + (target.h - from.h) * k,\n };\n applyCam();\n flyHandle = t < 1 ? requestAnimationFrame(frame) : null;\n }\n flyHandle = requestAnimationFrame(frame);\n }\n function cancelFly() {\n if (flyHandle !== null) { cancelAnimationFrame(flyHandle); flyHandle = null; }\n }\n\n function goFit() {\n if (!natural) return;\n atFit = true;\n flyTo(fitCam());\n }\n\n function zoomStep(factor) {\n if (!cam) return;\n const r = stageRect();\n markMoved();\n flyTo(zoomedCam(toSvg(r.left + r.width / 2, r.top + r.height / 2), factor));\n }\n\n // --- load pipeline: fetch → parse → validate → swap. A failure at any\n // step keeps the last good frame on screen and retries with backoff. ---\n let loadId = 0; // only the latest load's result may touch the DOM\n let retryTimer = null;\n let retryAttempt = 0;\n const RETRY_MS = [500, 1000, 2000, 4000]; // then every 4s\n\n function reload() {\n const id = ++loadId; // supersedes any in-flight load and pending retry\n if (retryTimer !== null) { clearTimeout(retryTimer); retryTimer = null; }\n dot.classList.add(\"stale\");\n fetch(encodeURIComponent(SVG_FILE) + \"?t=\" + Date.now(), { cache: \"no-store\" })\n .then(function (res) {\n if (!res.ok) throw new Error(\"HTTP \" + res.status);\n return res.text();\n })\n .then(function (text) {\n if (id !== loadId) return;\n const doc = new DOMParser().parseFromString(text, \"image/svg+xml\");\n const root = doc.documentElement;\n // Validate before touching the DOM — a truncated write mid-render or\n // a non-SVG response must not blank the shared screen.\n if (!root || root.localName !== \"svg\" || doc.querySelector(\"parsererror\")) {\n fail(\"SVG parse error\");\n return;\n }\n applyLoad(root);\n })\n .catch(function (err) {\n if (id !== loadId) return;\n fail(err && err.message ? err.message : String(err));\n });\n }\n\n function applyLoad(root) {\n const next = document.importNode(root, true);\n const vb = (next.getAttribute(\"viewBox\") || \"\").trim().split(/[\\s,]+/).map(Number);\n // CSS sizes the element; the viewBox is the camera.\n next.removeAttribute(\"width\");\n next.removeAttribute(\"height\");\n if (svgEl) svgEl.replaceWith(next); else stage.appendChild(next);\n svgEl = next;\n natural = vb.length === 4 && vb.every(isFinite) && vb[2] > 0 && vb[3] > 0\n ? { x: vb[0], y: vb[1], w: vb[2], h: vb[3] }\n : null;\n if (!natural) {\n // No usable viewBox (not something our renderer emits): frame the ink.\n const bb = next.getBBox();\n natural = { x: bb.x, y: bb.y, w: Math.max(1, bb.width), h: Math.max(1, bb.height) };\n }\n\n const metaEl = document.getElementById(\"em-slices\");\n let meta = null;\n if (metaEl) { try { meta = JSON.parse(metaEl.textContent); } catch (err) { meta = null; } }\n slices = (meta && meta.slices) || [];\n rowLabels = (meta && meta.rowLabels) || null;\n // The model may have changed shape (slice removed) between loads. But a\n // reload with NO metadata at all (a transient bad render) must not eject\n // the facilitator from review mode or lose their place — review state\n // survives every reload; the position is only remapped when there are\n // real slices to remap onto.\n if (slices.length) {\n if (activeSlice !== null && !slices.some(function (s) { return s.index === activeSlice; })) {\n activeSlice = slices[0].index;\n }\n if (reviewMode && activeSlice === null) activeSlice = slices[0].index;\n // Same survives-a-reload treatment for the doc flyout: refresh its\n // content (the doc may have changed) if its slice is still here,\n // close it if the slice is gone — but only on a reload that actually\n // carries real slice metadata, never on a transient bad render.\n if (openSliceIndex !== null) {\n if (slices.some(function (s) { return s.index === openSliceIndex; })) renderDocFlyout();\n else closeDoc();\n }\n }\n renderChrome();\n applyDim();\n\n // Camera across reloads: review mode re-flies to the (possibly moved)\n // active slice; an untouched/fitted camera re-fits to the new natural\n // box; otherwise the absolute view is preserved (including while the\n // active slice is temporarily missing from the metadata).\n cancelFly();\n if (reviewMode && activeSlice !== null &&\n slices.some(function (s) { return s.index === activeSlice; })) {\n flyTo(sliceFrame(activeSlice));\n } else if (atFit || !cam) {\n cam = fitCam();\n applyCam();\n } else {\n clampPan();\n applyCam();\n }\n\n banner.hidden = true;\n retryAttempt = 0;\n stamp.textContent = new Date().toLocaleTimeString();\n dot.classList.remove(\"stale\");\n }\n\n function fail(msg) {\n bannerText.textContent = \"Couldn't load \" + SVG_FILE + \" — \" + msg + \" · retrying…\";\n banner.hidden = false;\n const delay = RETRY_MS[Math.min(retryAttempt, RETRY_MS.length - 1)];\n retryAttempt += 1;\n retryTimer = setTimeout(reload, delay);\n }\n\n // --- review mode / storyboard ---\n\n // The union, in natural/model coordinates, of every element actually\n // belonging to slice `index` — lets sliceFrame zoom tightly around what a\n // slice contains instead of always keeping the model's FULL height in\n // frame. Always requiring the full height meant a model with few slices\n // but several swimlane rows could never zoom in tighter than Fit: letting\n // frameRect grow the frame's width to preserve that full height, on a\n // landscape stage, routinely produced a frame wider than the whole\n // diagram. getBoundingClientRect() + toSvg() reuses the exact camera math\n // already trusted for zoom-about-cursor, so this needs no new server-side\n // geometry or metadata. Returns null when there's no usable geometry yet\n // (nothing tagged, or a non-layout test environment) — sliceFrame falls\n // back to the old full-height framing in that case.\n function sliceContentBox(index) {\n if (!svgEl || !cam) return null;\n const nodes = svgEl.querySelectorAll('[data-slice=\"' + index + '\"]');\n let box = null;\n nodes.forEach(function (node) {\n const r = node.getBoundingClientRect();\n if (r.width <= 0 || r.height <= 0) return;\n const p0 = toSvg(r.left, r.top);\n const p1 = toSvg(r.right, r.bottom);\n const nx0 = Math.min(p0.x, p1.x), nx1 = Math.max(p0.x, p1.x);\n const ny0 = Math.min(p0.y, p1.y), ny1 = Math.max(p0.y, p1.y);\n box = box\n ? { x0: Math.min(box.x0, nx0), x1: Math.max(box.x1, nx1), y0: Math.min(box.y0, ny0), y1: Math.max(box.y1, ny1) }\n : { x0: nx0, x1: nx1, y0: ny0, y1: ny1 };\n });\n return box;\n }\n\n function sliceFrame(index) {\n let slice = null;\n for (const s of slices) if (s.index === index) slice = s;\n if (!slice || !natural) return fitCam();\n const PAD = 24; // breathing room around the framed slice\n const content = sliceContentBox(index);\n let x0 = rowLabels ? Math.min(rowLabels.x0, slice.x0) : slice.x0;\n let x1 = slice.x1;\n // Vertically, frame just the slice's own content when we can measure it;\n // otherwise fall back to the model's full height (every swimlane row).\n let y0 = natural.y, y1 = natural.y + natural.h;\n if (content) {\n x0 = Math.min(x0, content.x0);\n x1 = Math.max(x1, content.x1);\n y0 = content.y0;\n y1 = content.y1;\n }\n x0 = Math.max(natural.x, x0 - PAD);\n x1 = Math.min(natural.x + natural.w, x1 + PAD);\n y0 = Math.max(natural.y, y0 - PAD);\n y1 = Math.min(natural.y + natural.h, y1 + PAD);\n return frameRect(x0, y0, Math.max(1, x1 - x0), Math.max(1, y1 - y0));\n }\n\n function renderChrome() {\n storyboard.hidden = slices.length === 0;\n reviewToggle.classList.toggle(\"active\", reviewMode);\n reviewToggle.disabled = slices.length === 0;\n filmstrip.innerHTML = \"\";\n for (const s of slices) {\n const b = document.createElement(\"button\");\n b.className = \"slice\" + (s.index === activeSlice ? \" active\" : \"\");\n b.textContent = s.name;\n b.addEventListener(\"click\", function () { goTo(s.index); });\n filmstrip.appendChild(b);\n }\n const first = slices[0], last = slices[slices.length - 1];\n prevBtn.disabled = activeSlice === null || !first || activeSlice <= first.index;\n nextBtn.disabled = activeSlice === null || !last || activeSlice >= last.index;\n }\n\n function applyDim() {\n if (!svgEl) return;\n svgEl.querySelectorAll(\"[data-slice]\").forEach(function (node) {\n const dim = reviewMode && activeSlice !== null &&\n Number(node.getAttribute(\"data-slice\")) !== activeSlice;\n node.classList.toggle(\"em-slice-dim\", dim);\n });\n }\n\n // --- doc flyout: click a slice's own node to see its design doc, purely\n // an overlay — it never moves the camera or touches review state. ---\n function sliceByIndex(index) {\n for (const s of slices) if (s.index === index) return s;\n return null;\n }\n\n function renderDocFlyout() {\n const s = sliceByIndex(openSliceIndex);\n if (!s) { docFlyout.classList.remove(\"open\"); return; }\n docFlyoutTitle.textContent = s.name;\n docFlyoutBody.classList.toggle(\"empty\", !s.docHtml);\n if (s.docHtml) docFlyoutBody.innerHTML = s.docHtml;\n else docFlyoutBody.textContent = \"No design doc yet for this slice.\";\n docFlyout.classList.add(\"open\");\n }\n\n function openDoc(index) {\n openSliceIndex = index;\n renderDocFlyout();\n }\n\n function closeDoc() {\n openSliceIndex = null;\n docFlyout.classList.remove(\"open\");\n }\n\n function goTo(index) {\n activeSlice = index;\n renderChrome();\n applyDim();\n if (reviewMode) flyTo(sliceFrame(index));\n }\n\n function step(delta) {\n if (!slices.length) return;\n if (activeSlice === null) { goTo(slices[0].index); return; }\n const i = slices.findIndex(function (s) { return s.index === activeSlice; });\n const next = slices[Math.min(slices.length - 1, Math.max(0, i + delta))];\n if (next) goTo(next.index);\n }\n\n function exitReview() {\n reviewMode = false;\n renderChrome();\n applyDim();\n goFit();\n }\n\n reviewToggle.addEventListener(\"click\", function () {\n if (reviewMode) { exitReview(); return; }\n reviewMode = true;\n if (activeSlice === null && slices.length) activeSlice = slices[0].index;\n renderChrome();\n applyDim();\n if (activeSlice !== null) flyTo(sliceFrame(activeSlice));\n });\n prevBtn.addEventListener(\"click\", function () { step(-1); });\n nextBtn.addEventListener(\"click\", function () { step(1); });\n document.getElementById(\"zoomIn\").addEventListener(\"click\", function () { zoomStep(1.5); });\n document.getElementById(\"zoomOut\").addEventListener(\"click\", function () { zoomStep(1 / 1.5); });\n document.getElementById(\"fitBtn\").addEventListener(\"click\", goFit);\n document.getElementById(\"errorDismiss\").addEventListener(\"click\", function () {\n banner.hidden = true; // retries keep going quietly\n });\n docFlyoutClose.addEventListener(\"click\", closeDoc);\n\n // --- direct manipulation: drag pan, wheel pan, ctrl+wheel / pinch zoom ---\n const pointers = new Map(); // pointerId -> last client {x, y}\n let drag = null; // { id, startX, startY, active } — single-pointer pan\n let suppressClick = false; // swallow the click that ends a drag/pinch\n\n // The doc flyout is a normal interactive DOM panel stacked over the stage\n // (see #docFlyout's z-index) — its own scrolling, text selection, and link\n // clicks must behave normally, not get hijacked by the stage's direct-\n // manipulation gestures (drag-pan, wheel-pan/zoom, double-click-to-zoom).\n function overFlyout(e) {\n return !!(e.target.closest && e.target.closest(\"#docFlyout\"));\n }\n\n stage.addEventListener(\"pointerdown\", function (e) {\n if (overFlyout(e)) return;\n if (e.button !== 0) return;\n cancelFly(); // direct manipulation takes over from any animation\n pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });\n if (pointers.size === 1) {\n suppressClick = false;\n drag = { id: e.pointerId, startX: e.clientX, startY: e.clientY, active: false };\n } else {\n // Second pointer: pinch. Capture both so the gesture survives leaving\n // the stage; a pinch must never end in a note-link click.\n drag = null;\n suppressClick = true;\n stage.classList.add(\"dragging\");\n for (const id of pointers.keys()) {\n try { stage.setPointerCapture(id); } catch (err) { /* pointer already gone */ }\n }\n }\n });\n\n stage.addEventListener(\"pointermove\", function (e) {\n const pt = pointers.get(e.pointerId);\n if (!pt) return;\n if (pointers.size >= 2) { pinchMove(e, pt); return; }\n if (!drag || drag.id !== e.pointerId || !cam) { pt.x = e.clientX; pt.y = e.clientY; return; }\n if (!drag.active) {\n // Movement threshold before treating it as a drag — capturing the\n // pointer immediately would retarget the eventual click away from the\n // SVG's note links. Until then pt stays at the pointerdown position,\n // so the first panning move applies the full accumulated delta.\n if (Math.hypot(e.clientX - drag.startX, e.clientY - drag.startY) < 4) return;\n drag.active = true;\n suppressClick = true;\n stage.setPointerCapture(e.pointerId);\n stage.classList.add(\"dragging\");\n }\n const s = scale();\n cam.x -= (e.clientX - pt.x) * s;\n cam.y -= (e.clientY - pt.y) * s;\n pt.x = e.clientX; pt.y = e.clientY;\n clampPan();\n applyCam();\n markMoved();\n });\n\n // Incremental pinch: pan by the midpoint's movement, then zoom about the\n // new midpoint by the ratio of pointer distances.\n function pinchMove(e, pt) {\n let other = null;\n for (const entry of pointers) if (entry[0] !== e.pointerId) other = entry[1];\n if (!other || !cam) { pt.x = e.clientX; pt.y = e.clientY; return; }\n const oldMid = { x: (pt.x + other.x) / 2, y: (pt.y + other.y) / 2 };\n const oldDist = Math.hypot(pt.x - other.x, pt.y - other.y);\n pt.x = e.clientX; pt.y = e.clientY;\n const newMid = { x: (pt.x + other.x) / 2, y: (pt.y + other.y) / 2 };\n const newDist = Math.hypot(pt.x - other.x, pt.y - other.y);\n const s = scale();\n cam.x -= (newMid.x - oldMid.x) * s;\n cam.y -= (newMid.y - oldMid.y) * s;\n if (oldDist > 0 && newDist > 0) cam = zoomedCam(toSvg(newMid.x, newMid.y), newDist / oldDist);\n clampPan();\n applyCam();\n markMoved();\n }\n\n function endPointer(e) {\n if (!pointers.has(e.pointerId)) return; // not a pointer we were tracking\n pointers.delete(e.pointerId);\n if (stage.hasPointerCapture && stage.hasPointerCapture(e.pointerId)) {\n stage.releasePointerCapture(e.pointerId);\n }\n if (drag && drag.id === e.pointerId) drag = null;\n if (pointers.size === 1) {\n // Pinch collapsed to one finger: continue as an already-active pan.\n const rest = pointers.entries().next().value;\n drag = { id: rest[0], startX: rest[1].x, startY: rest[1].y, active: true };\n }\n if (pointers.size === 0) stage.classList.remove(\"dragging\");\n }\n // On window, not the stage: a pointer that goes down on the stage but is\n // released elsewhere (before the drag threshold captures it) must still be\n // cleared, or its stale Map entry would misclassify every later gesture as\n // a pinch. endPointer ignores pointers it never tracked.\n window.addEventListener(\"pointerup\", endPointer);\n window.addEventListener(\"pointercancel\", endPointer);\n\n stage.addEventListener(\"click\", function (e) {\n if (suppressClick) { suppressClick = false; e.preventDefault(); e.stopPropagation(); }\n }, true);\n\n // A plain click landing on an element's own node group (not a note/issue/\n // divergence marker — those live in separate groups with no data-slice\n // ancestor) opens that slice's doc flyout. A drag/pinch's trailing click\n // never reaches here: the capturing listener above stops it first.\n stage.addEventListener(\"click\", function (e) {\n const node = e.target.closest && e.target.closest(\"[data-slice]\");\n if (node) openDoc(Number(node.getAttribute(\"data-slice\")));\n });\n\n stage.addEventListener(\"wheel\", function (e) {\n if (overFlyout(e)) return; // let the flyout's own overflow-y:auto scroll natively\n e.preventDefault(); // the page itself must never scroll or browser-zoom\n if (!cam) return;\n cancelFly();\n const k = e.deltaMode === 1 ? 16 : 1; // line-mode deltas (Firefox) → ~px\n if (e.ctrlKey) {\n // ctrl+wheel is also what a trackpad pinch emits.\n cam = zoomedCam(toSvg(e.clientX, e.clientY), Math.exp(-e.deltaY * k * 0.01));\n } else {\n const s = scale();\n cam.x += e.deltaX * k * s;\n cam.y += e.deltaY * k * s;\n clampPan();\n }\n applyCam();\n markMoved();\n }, { passive: false });\n\n stage.addEventListener(\"dblclick\", function (e) {\n if (overFlyout(e)) return;\n if (!cam) return;\n e.preventDefault();\n markMoved();\n flyTo(zoomedCam(toSvg(e.clientX, e.clientY), 2));\n });\n\n document.addEventListener(\"keydown\", function (e) {\n // Modified keys stay with the browser; shift is allowed (\"+\" needs it\n // on many layouts). Guard against future text inputs, too.\n if (e.ctrlKey || e.metaKey || e.altKey) return;\n const t = e.target;\n if (t && (t.tagName === \"INPUT\" || t.tagName === \"TEXTAREA\" || t.isContentEditable)) return;\n if (e.key === \"+\" || e.key === \"=\") { e.preventDefault(); zoomStep(1.5); }\n else if (e.key === \"-\") { e.preventDefault(); zoomStep(1 / 1.5); }\n else if (e.key === \"0\" || e.key === \"f\") { e.preventDefault(); goFit(); }\n else if (e.key === \"Escape\") {\n if (openSliceIndex !== null) closeDoc();\n else if (reviewMode) exitReview();\n }\n else if (reviewMode) {\n if (e.key === \"ArrowLeft\") { e.preventDefault(); step(-1); }\n else if (e.key === \"ArrowRight\") { e.preventDefault(); step(1); }\n else if (e.key === \"Home\") { e.preventDefault(); if (slices.length) goTo(slices[0].index); }\n else if (e.key === \"End\") { e.preventDefault(); if (slices.length) goTo(slices[slices.length - 1].index); }\n }\n });\n\n window.addEventListener(\"resize\", function () {\n if (!natural || !cam) return;\n // Keep what the presenter framed: an active slice re-frames to the new\n // aspect; a fitted view re-fits; a hand-placed camera stays put.\n if (reviewMode && activeSlice !== null) { cam = sliceFrame(activeSlice); applyCam(); }\n else if (atFit) { cam = fitCam(); applyCam(); }\n });\n\n reload();\n\n const es = new EventSource(\"/__events\");\n // onopen reloads only on REconnect (laptop sleep, server restart): pushes\n // sent in the meantime were missed. The initial connect skips it — the\n // eager reload() above is already in flight, and doubling it would fetch\n // and parse every page open twice.\n let firstOpen = true;\n es.onopen = function () {\n retryAttempt = 0;\n if (firstOpen) { firstOpen = false; return; }\n reload();\n };\n es.onmessage = function () { retryAttempt = 0; reload(); };\n es.onerror = function () { dot.classList.add(\"stale\"); };\n // A save that failed to render (parse/render error): the server pushes the\n // message, the last good diagram stays up, and the banner says why the\n // screen is stale — it must never silently show an outdated model. No\n // retry here: refetching can only return the old file; the next successful\n // render pushes a reload that clears the banner.\n es.addEventListener(\"renderError\", function (e) {\n if (retryTimer !== null) { clearTimeout(retryTimer); retryTimer = null; }\n bannerText.textContent = \"Model didn't render — \" + e.data + \" — showing last good diagram\";\n banner.hidden = false;\n dot.classList.add(\"stale\");\n });\n </script>\n</body>\n</html>\n";
4
+ export const VIEWER_HTML = "<!doctype html>\n<!--\n The live viewer page, served at / by `em watch --serve` (src/render/serve.ts).\n\n This file is the SOURCE OF TRUTH for the viewer. It ships as the generated\n constant in src/render/viewerHtml.ts - after editing this file, run\n `npm run viewer:generate` to regenerate it (a sync test fails otherwise).\n\n Self-contained: no external assets, all state in the one script below, driven\n by SSE push - the browser reloads only when the server says the SVG changed.\n\n The SVG is inlined (fetch, DOMParser, validate, then swap) rather than\n embedded via an object tag: a truncated or malformed file is detected BEFORE\n touching the DOM, so a bad render can never blank the shared screen - the\n last good frame stays up behind an error banner while the viewer retries\n with backoff. A save that fails to render entirely (parse error) arrives as\n an SSE \"renderError\" and lands in the same banner over the intact diagram.\n Inline SVG also keeps note links clickable and hands the camera a real\n viewBox to drive.\n\n The camera IS the svg viewBox (cam = x/y/w/h): drag and wheel pan,\n ctrl+wheel (what a trackpad pinch emits) and two-pointer pinch zoom about\n the cursor, and flyTo() animates slice navigation and fit. Driving viewBox\n directly (not CSS transforms) keeps text crisp and makes the client-to-model\n coordinate math a single exact mapping.\n\n Review mode: a Prev/Next storyboard that highlights and flies to one slice\n at a time, reading the data-slice attributes and the em-slices metadata that\n src/render/sliceOverlay.ts embeds in every rendered SVG. Review state lives\n outside the load pipeline and is re-applied after every SSE-triggered swap -\n a facilitator hand-editing the model live must not lose their place.\n\n Doc flyout (MIL-153): clicking any element belonging to a slice opens a\n side panel with that slice's own design doc, rendered server-side to HTML\n and carried in the same em-slices metadata (no separate fetch — see\n src/render/sliceOverlay.ts's docHtml field). Independent of review mode: a\n slice click never moves the camera, and stepping/navigating never touches\n the flyout. Like review state, which slice's doc is open lives outside the\n load pipeline, refreshed (or closed, if the slice vanished) after every\n reload rather than reset by one.\n\n Storyline picker (GH #309, deferred from #277): whenever the model declares\n storylines, `em watch --serve` writes the full model plus every storyline's own\n `em render --storyline` subset side by side (src/cli.ts's watch action), each\n carrying the same `<metadata id=\"em-storylines\">` list of {name, file, current}\n (src/render/storylinePicker.ts). The picker reads that list and lets a facilitator\n switch which one is loaded — a plain navigation to this same page with a new `?svg=`,\n so it needs no new fetch/reload machinery of its own. `--storyline <name>` just picks\n which file is loaded first; the picker works the same with or without it.\n-->\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n <title>Event Model — Live</title>\n <style>\n :root { color-scheme: light dark; }\n html, body { height: 100%; }\n body {\n margin: 0; font-family: system-ui, sans-serif; background: #fafafa;\n display: flex; flex-direction: column; overflow: hidden;\n }\n header {\n display: flex; align-items: center; gap: .75rem; flex: none;\n padding: .5rem .9rem; background: #1f2933; color: #fff; font-size: 14px;\n }\n header .dot { width: 9px; height: 9px; border-radius: 50%; background: #34c759; transition: background .3s; flex: none; }\n header .dot.stale { background: #f0a020; }\n header .stamp { margin-left: auto; opacity: .7; font-variant-numeric: tabular-nums; }\n header .zoom { display: flex; align-items: center; gap: .3rem; }\n #zoomPct { font-size: 12px; opacity: .7; min-width: 3.2em; text-align: center; font-variant-numeric: tabular-nums; }\n .btn {\n background: #3b4754; color: #fff; border: 0; border-radius: 4px;\n padding: .3rem .7rem; font-size: 13px; cursor: pointer; font-family: inherit;\n }\n .btn:hover { background: #4b5763; }\n .btn:disabled { opacity: .4; cursor: default; }\n .btn.active { background: #2f6fed; }\n #storylinePicker {\n background: #3b4754; color: #fff; border: 0; border-radius: 4px;\n padding: .3rem .5rem; font-size: 13px; cursor: pointer; font-family: inherit;\n }\n #storylinePicker[hidden] { display: none; }\n #storylinePicker option { color: #000; }\n #errorBanner {\n display: flex; align-items: center; gap: .6rem; flex: none;\n padding: .45rem .9rem; background: #8c2f28; color: #fff; font-size: 13px;\n }\n #errorBanner[hidden] { display: none; }\n #errorBanner button {\n margin-left: auto; background: none; border: 0; color: #fff;\n font-size: 16px; line-height: 1; cursor: pointer; opacity: .8;\n }\n #storyboard {\n display: flex; align-items: center; gap: .5rem; padding: .4rem .9rem; flex: none;\n background: #e8eaed; border-bottom: 1px solid #d0d3d7;\n }\n #storyboard[hidden] { display: none; }\n #storyboard .nav { background: none; border: 0; font-size: 18px; line-height: 1; cursor: pointer; color: #3b4754; padding: 0 .3rem; }\n #storyboard .nav:disabled { opacity: .3; cursor: default; }\n #filmstrip { display: flex; gap: .35rem; overflow-x: auto; }\n #filmstrip .slice {\n padding: .25rem .6rem; border-radius: 12px; background: #fff; border: 1px solid #d0d3d7;\n font-size: 12px; white-space: nowrap; cursor: pointer; color: #3b4754; font-family: inherit;\n }\n #filmstrip .slice.active { background: #2f6fed; border-color: #2f6fed; color: #fff; font-weight: 600; }\n #stage {\n flex: 1 1 auto; min-height: 0; position: relative; overflow: hidden;\n touch-action: none; cursor: grab; user-select: none; -webkit-user-select: none;\n }\n #stage.dragging { cursor: grabbing; }\n #stage svg { width: 100%; height: 100%; display: block; }\n #docFlyout {\n position: absolute; top: 0; right: 0; bottom: 0; width: min(640px, 94%);\n background: #fff; border-left: 1px solid #d0d3d7; box-shadow: -2px 0 10px rgba(0, 0, 0, .15);\n display: flex; flex-direction: column; z-index: 5;\n transform: translateX(100%); transition: transform .2s ease;\n /* #stage sets these for its own drag-to-pan surface; the flyout is a normal\n panel — its text should be selectable and its cursor should look normal. */\n user-select: text; -webkit-user-select: text; cursor: auto; touch-action: auto;\n }\n #docFlyout.open { transform: translateX(0); }\n #docFlyout .docHead {\n display: flex; align-items: center; gap: .5rem; flex: none;\n padding: .55rem .9rem; background: #1f2933; color: #fff; font-size: 14px; font-weight: 600;\n }\n #docFlyout .docHead span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n #docFlyout .docHead button {\n margin-left: auto; background: none; border: 0; color: #fff;\n font-size: 18px; line-height: 1; cursor: pointer; opacity: .8; flex: none;\n }\n #docFlyout .docBody {\n /* min-height: 0 overrides a flex item's default min-height: auto, which would\n otherwise let this grow to fit all its content instead of scrolling — the\n actual cause of \"no way to scroll it\": without it the body just spills past\n the flyout's bottom edge, clipped invisibly by #stage's own overflow:hidden. */\n flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden;\n padding: .9rem 1.1rem; font-size: 13px; line-height: 1.55; color: #202124;\n }\n #docFlyout .docBody.empty { color: #5f6368; font-style: italic; }\n #docFlyout .docBody img { max-width: 100%; height: auto; display: block; margin: .5em 0; }\n #docFlyout .docBody :is(h1, h2, h3, h4, h5, h6) { margin: 1em 0 .35em; line-height: 1.3; }\n #docFlyout .docBody :is(h1, h2, h3, h4, h5, h6):first-child { margin-top: 0; }\n #docFlyout .docBody p, #docFlyout .docBody ul, #docFlyout .docBody ol { margin: .5em 0; }\n #docFlyout .docBody ul, #docFlyout .docBody ol { padding-left: 1.3em; }\n #docFlyout .docBody a { color: #2f6fed; }\n #docFlyout .docBody code { background: #f0f1f3; padding: .1em .3em; border-radius: 3px; font-size: 12px; }\n #docFlyout .docBody pre { background: #f0f1f3; padding: .6rem; border-radius: 4px; overflow-x: auto; }\n #docFlyout .docBody pre code { background: none; padding: 0; }\n #docFlyout .docBody blockquote {\n margin: .5em 0; padding-left: .8em; border-left: 3px solid #d0d3d7; color: #5f6368;\n }\n #docFlyout .docBody table { border-collapse: collapse; margin: .5em 0; }\n #docFlyout .docBody :is(th, td) { border: 1px solid #d0d3d7; padding: .3em .5em; }\n /* GH #402: section-aware slice-doc rendering — the frontmatter badge row, Given/When/Then\n scenario cards, INV-* chips, and field-table column styling catalog/sliceDocRender.ts and\n catalog/docBadge.ts build; see catalog/pages.ts for the static catalog's matching rules. */\n #docFlyout .docBody .doc-badge-row { display: flex; flex-wrap: wrap; gap: .4rem; margin: 0 0 1rem; }\n #docFlyout .docBody .doc-badge {\n display: inline-block; padding: .15rem .55rem; border-radius: 999px; font-size: 11px;\n font-weight: 600; letter-spacing: .01em; background: #e4e7eb; color: #3b4754;\n }\n #docFlyout .docBody .doc-badge--pattern { background: #e1ecff; color: #1d4ed8; }\n #docFlyout .docBody .doc-badge--version { background: #eef0f3; color: #52606d; font-variant-numeric: tabular-nums; }\n #docFlyout .docBody .doc-badge--status { background: #fef3cf; color: #8a6116; text-transform: capitalize; }\n #docFlyout .docBody .doc-badge--drift-in-sync { background: #d8f3dc; color: #1b7a3d; }\n #docFlyout .docBody .doc-badge--drift-uncertified, #docFlyout .docBody .doc-badge--drift-unpropagated-delta { background: #ffe9c7; color: #8a5300; }\n #docFlyout .docBody .doc-badge--drift-implemented-without-link { background: #fbd8d4; color: #9b2c20; }\n #docFlyout .docBody .doc-badge--drift-never-implemented { background: #e4e7eb; color: #52606d; }\n #docFlyout .docBody .scenario-card { border: 1px solid #d0d3d7; border-radius: 8px; margin: 1em 0; overflow: hidden; background: #fff; }\n #docFlyout .docBody .scenario-card-title { margin: 0; padding: .5rem .75rem; background: #f0f2f5; font-size: .95rem; border-bottom: 1px solid #d0d3d7; }\n #docFlyout .docBody .scenario-step { display: flex; gap: .6rem; padding: .45rem .75rem; border-top: 1px solid #eceef1; align-items: flex-start; }\n #docFlyout .docBody .scenario-step:first-of-type { border-top: 0; }\n #docFlyout .docBody .scenario-step-label {\n flex: none; min-width: 4.2em; font-size: 11px; font-weight: 700; text-transform: uppercase;\n letter-spacing: .04em; color: #52606d; padding-top: .15em;\n }\n #docFlyout .docBody .scenario-step--given .scenario-step-label { color: #1d4ed8; }\n #docFlyout .docBody .scenario-step--when .scenario-step-label { color: #8a5300; }\n #docFlyout .docBody .scenario-step--then .scenario-step-label { color: #1b7a3d; }\n #docFlyout .docBody .scenario-step-body { flex: 1 1 auto; }\n #docFlyout .docBody .scenario-step-body ul { margin: .2em 0; padding-left: 1.1em; }\n #docFlyout .docBody .inv-chip { display: inline-block; padding: .05em .4em; border-radius: 4px; font-size: .85em; font-family: ui-monospace, Menlo, monospace; }\n #docFlyout .docBody .inv-chip--def { background: #1f2933; color: #fff; scroll-margin-top: 1rem; }\n #docFlyout .docBody .inv-chip--ref { background: #eef0f3; color: #1d4ed8; text-decoration: none; }\n #docFlyout .docBody .inv-chip--ref:hover { text-decoration: underline; }\n #docFlyout .docBody .inv-chip--external { background: #eef0f3; color: #52606d; }\n #docFlyout .docBody table.field-table th.col-type, #docFlyout .docBody table.field-table td.col-type { font-family: ui-monospace, Menlo, monospace; font-size: .85em; color: #3b4754; white-space: nowrap; }\n #docFlyout .docBody table.field-table th.col-notes, #docFlyout .docBody table.field-table td.col-notes { color: #52606d; }\n #docFlyout .docBody table.field-table th.col-flag, #docFlyout .docBody table.field-table td.col-flag { text-align: center; white-space: nowrap; }\n </style>\n</head>\n<body>\n <header>\n <span class=\"dot\" id=\"dot\"></span>\n <span>Event Model — live</span>\n <button class=\"btn\" id=\"reviewToggle\" title=\"Step through one slice at a time\">Review mode</button>\n <select id=\"storylinePicker\" title=\"Filter the live view to one storyline\" hidden></select>\n <span class=\"zoom\">\n <button class=\"btn\" id=\"zoomOut\" aria-label=\"Zoom out\" title=\"Zoom out (-)\">−</button>\n <span id=\"zoomPct\">100%</span>\n <button class=\"btn\" id=\"zoomIn\" aria-label=\"Zoom in\" title=\"Zoom in (+)\">+</button>\n <button class=\"btn\" id=\"fitBtn\" title=\"Fit the whole diagram (0)\">Fit</button>\n </span>\n <span class=\"stamp\" id=\"stamp\">connecting…</span>\n </header>\n <div id=\"errorBanner\" hidden>\n <span id=\"errorText\"></span>\n <button id=\"errorDismiss\" aria-label=\"Dismiss\">×</button>\n </div>\n <nav id=\"storyboard\" hidden>\n <button class=\"nav\" id=\"prevSlice\" aria-label=\"Previous slice\">‹</button>\n <div id=\"filmstrip\"></div>\n <button class=\"nav\" id=\"nextSlice\" aria-label=\"Next slice\">›</button>\n </nav>\n <div id=\"stage\">\n <aside id=\"docFlyout\">\n <div class=\"docHead\">\n <span id=\"docFlyoutTitle\"></span>\n <button id=\"docFlyoutClose\" aria-label=\"Close\">×</button>\n </div>\n <div class=\"docBody\" id=\"docFlyoutBody\"></div>\n </aside>\n </div>\n <script>\n // ?svg=<name>.svg picks the model file (default model.svg), so one server\n // serves any model without editing this page. The allowlist keeps the\n // value a bare *.svg filename inside the served directory — no path\n // separator, no drive/scheme colon — so it is safe to fetch (always\n // URI-encoded) and to display (via textContent only, never markup).\n // Spaces and unicode are fine: em watch prints whatever the output file\n // is called, and that URL has to work.\n const svgParam = new URLSearchParams(location.search).get(\"svg\");\n const SVG_FILE = svgParam && /^[^/\\\\:]+\\.svg$/.test(svgParam) ? svgParam : \"model.svg\";\n\n const stage = document.getElementById(\"stage\");\n const stamp = document.getElementById(\"stamp\");\n const dot = document.getElementById(\"dot\");\n const banner = document.getElementById(\"errorBanner\");\n const bannerText = document.getElementById(\"errorText\");\n const reviewToggle = document.getElementById(\"reviewToggle\");\n const storylinePicker = document.getElementById(\"storylinePicker\");\n const storyboard = document.getElementById(\"storyboard\");\n const filmstrip = document.getElementById(\"filmstrip\");\n const prevBtn = document.getElementById(\"prevSlice\");\n const nextBtn = document.getElementById(\"nextSlice\");\n const zoomPct = document.getElementById(\"zoomPct\");\n const docFlyout = document.getElementById(\"docFlyout\");\n const docFlyoutTitle = document.getElementById(\"docFlyoutTitle\");\n const docFlyoutBody = document.getElementById(\"docFlyoutBody\");\n const docFlyoutClose = document.getElementById(\"docFlyoutClose\");\n\n // --- review-mode state: outside the load pipeline on purpose, see the\n // file-level comment above — a reload re-applies it, never resets it. ---\n let reviewMode = false;\n let activeSlice = null; // slice index, or null for \"show all\"\n let slices = []; // [{index, name, x0, x1, docHtml}], from this load's <metadata>\n let rowLabels = null; // {x0, x1} | null, the swimlane label column's range\n\n // --- storyline picker (GH #309): outside the load pipeline isn't needed here — unlike\n // review/doc-flyout state, switching storylines navigates to a new page (see below), so\n // there's nothing to preserve across a reload. ---\n let storylineVariants = []; // [{name, file, current}], from this load's <metadata id=\"em-storylines\">, or []\n\n // --- doc flyout state: independent of review mode, outside the load\n // pipeline for the same reason review state is — see the file comment. ---\n let openSliceIndex = null; // slice index whose doc the flyout shows, or null when closed\n\n // --- camera state ---\n let svgEl = null; // the inline <svg>, null until the first good load\n let natural = null; // the loaded SVG's own viewBox: its full-fit frame\n let cam = null; // the current viewBox {x, y, w, h}\n // True while the user has never panned/zoomed (or pressed Fit last): a\n // reload then re-fits to the new natural box instead of pinning the old\n // absolute view onto a diagram that may have grown or shrunk.\n let atFit = true;\n\n function stageRect() { return stage.getBoundingClientRect(); }\n\n // Uniform px→model scale of the current view. The svg keeps the default\n // preserveAspectRatio (xMidYMid meet): one scale factor, centered.\n function scale() {\n const r = stageRect();\n return Math.max(cam.w / Math.max(1, r.width), cam.h / Math.max(1, r.height));\n }\n\n // Exact client-px → model-coords inverse of the \"meet\" mapping, so\n // zoom-about-cursor keeps the model point under the cursor exactly.\n function toSvg(cx, cy) {\n const r = stageRect();\n const s = scale();\n const ox = cam.x - (r.width * s - cam.w) / 2;\n const oy = cam.y - (r.height * s - cam.h) / 2;\n return { x: ox + (cx - r.left) * s, y: oy + (cy - r.top) * s };\n }\n\n // Fit a model-space rect into the stage aspect, centered.\n function frameRect(x, y, w, h) {\n const r = stageRect();\n const aspect = Math.max(1, r.width) / Math.max(1, r.height);\n let fw = w, fh = h;\n if (fw / fh < aspect) fw = fh * aspect; else fh = fw / aspect;\n return { x: x + (w - fw) / 2, y: y + (h - fh) / 2, w: fw, h: fh };\n }\n\n function fitCam() { return frameRect(natural.x, natural.y, natural.w, natural.h); }\n\n // New cam zoomed by the given factor about model point p, clamped to\n // 0.5×–30× of fit so the diagram is neither lost nor over-magnified.\n function zoomedCam(p, factor) {\n const fitW = fitCam().w;\n const w = Math.min(fitW * 2, Math.max(fitW / 30, cam.w / factor));\n const f = cam.w / w;\n return clampPanOf({\n x: p.x - (p.x - cam.x) / f,\n y: p.y - (p.y - cam.y) / f,\n w: cam.w / f,\n h: cam.h / f,\n });\n }\n\n // Keep at least half of min(view, diagram) in frame on each axis, so the\n // diagram can never be panned fully out of view.\n function clampPanOf(c) {\n if (!natural) return c;\n const kx = Math.min(c.w, natural.w) / 2;\n const ky = Math.min(c.h, natural.h) / 2;\n c.x = Math.max(natural.x - c.w + kx, Math.min(natural.x + natural.w - kx, c.x));\n c.y = Math.max(natural.y - c.h + ky, Math.min(natural.y + natural.h - ky, c.y));\n return c;\n }\n function clampPan() { cam = clampPanOf(cam); }\n\n function applyCam() {\n if (!svgEl || !cam) return;\n svgEl.setAttribute(\"viewBox\", cam.x + \" \" + cam.y + \" \" + cam.w + \" \" + cam.h);\n if (natural) zoomPct.textContent = Math.round((fitCam().w / cam.w) * 100) + \"%\";\n }\n\n function markMoved() { atFit = false; }\n\n // Animated camera move; any direct manipulation (drag/wheel/pinch) cancels\n // it and takes over from wherever the animation got to.\n let flyHandle = null;\n function flyTo(target) {\n cancelFly();\n if (!cam) { cam = target; applyCam(); return; }\n const from = { x: cam.x, y: cam.y, w: cam.w, h: cam.h };\n const t0 = performance.now();\n const DUR = 280;\n function ease(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; }\n function frame(now) {\n const t = Math.min(1, (now - t0) / DUR);\n const k = ease(t);\n cam = {\n x: from.x + (target.x - from.x) * k,\n y: from.y + (target.y - from.y) * k,\n w: from.w + (target.w - from.w) * k,\n h: from.h + (target.h - from.h) * k,\n };\n applyCam();\n flyHandle = t < 1 ? requestAnimationFrame(frame) : null;\n }\n flyHandle = requestAnimationFrame(frame);\n }\n function cancelFly() {\n if (flyHandle !== null) { cancelAnimationFrame(flyHandle); flyHandle = null; }\n }\n\n function goFit() {\n if (!natural) return;\n atFit = true;\n flyTo(fitCam());\n }\n\n function zoomStep(factor) {\n if (!cam) return;\n const r = stageRect();\n markMoved();\n flyTo(zoomedCam(toSvg(r.left + r.width / 2, r.top + r.height / 2), factor));\n }\n\n // --- load pipeline: fetch → parse → validate → swap. A failure at any\n // step keeps the last good frame on screen and retries with backoff. ---\n let loadId = 0; // only the latest load's result may touch the DOM\n let retryTimer = null;\n let retryAttempt = 0;\n const RETRY_MS = [500, 1000, 2000, 4000]; // then every 4s\n\n function reload() {\n const id = ++loadId; // supersedes any in-flight load and pending retry\n if (retryTimer !== null) { clearTimeout(retryTimer); retryTimer = null; }\n dot.classList.add(\"stale\");\n fetch(encodeURIComponent(SVG_FILE) + \"?t=\" + Date.now(), { cache: \"no-store\" })\n .then(function (res) {\n if (!res.ok) throw new Error(\"HTTP \" + res.status);\n return res.text();\n })\n .then(function (text) {\n if (id !== loadId) return;\n const doc = new DOMParser().parseFromString(text, \"image/svg+xml\");\n const root = doc.documentElement;\n // Validate before touching the DOM — a truncated write mid-render or\n // a non-SVG response must not blank the shared screen.\n if (!root || root.localName !== \"svg\" || doc.querySelector(\"parsererror\")) {\n fail(\"SVG parse error\");\n return;\n }\n applyLoad(root);\n })\n .catch(function (err) {\n if (id !== loadId) return;\n fail(err && err.message ? err.message : String(err));\n });\n }\n\n function applyLoad(root) {\n const next = document.importNode(root, true);\n const vb = (next.getAttribute(\"viewBox\") || \"\").trim().split(/[\\s,]+/).map(Number);\n // CSS sizes the element; the viewBox is the camera.\n next.removeAttribute(\"width\");\n next.removeAttribute(\"height\");\n if (svgEl) svgEl.replaceWith(next); else stage.appendChild(next);\n svgEl = next;\n natural = vb.length === 4 && vb.every(isFinite) && vb[2] > 0 && vb[3] > 0\n ? { x: vb[0], y: vb[1], w: vb[2], h: vb[3] }\n : null;\n if (!natural) {\n // No usable viewBox (not something our renderer emits): frame the ink.\n const bb = next.getBBox();\n natural = { x: bb.x, y: bb.y, w: Math.max(1, bb.width), h: Math.max(1, bb.height) };\n }\n\n const metaEl = document.getElementById(\"em-slices\");\n let meta = null;\n if (metaEl) { try { meta = JSON.parse(metaEl.textContent); } catch (err) { meta = null; } }\n slices = (meta && meta.slices) || [];\n rowLabels = (meta && meta.rowLabels) || null;\n\n // GH #309: the storyline-picker list travels the same way em-slices does — read fresh\n // on every load (no survives-a-reload state to preserve; picking an option navigates).\n const storyMetaEl = document.getElementById(\"em-storylines\");\n let storyMeta = null;\n if (storyMetaEl) { try { storyMeta = JSON.parse(storyMetaEl.textContent); } catch (err) { storyMeta = null; } }\n storylineVariants = storyMeta || [];\n renderStorylinePicker();\n // The model may have changed shape (slice removed) between loads. But a\n // reload with NO metadata at all (a transient bad render) must not eject\n // the facilitator from review mode or lose their place — review state\n // survives every reload; the position is only remapped when there are\n // real slices to remap onto.\n if (slices.length) {\n if (activeSlice !== null && !slices.some(function (s) { return s.index === activeSlice; })) {\n activeSlice = slices[0].index;\n }\n if (reviewMode && activeSlice === null) activeSlice = slices[0].index;\n // Same survives-a-reload treatment for the doc flyout: refresh its\n // content (the doc may have changed) if its slice is still here,\n // close it if the slice is gone — but only on a reload that actually\n // carries real slice metadata, never on a transient bad render.\n if (openSliceIndex !== null) {\n if (slices.some(function (s) { return s.index === openSliceIndex; })) renderDocFlyout();\n else closeDoc();\n }\n }\n renderChrome();\n applyDim();\n\n // Camera across reloads: review mode re-flies to the (possibly moved)\n // active slice; an untouched/fitted camera re-fits to the new natural\n // box; otherwise the absolute view is preserved (including while the\n // active slice is temporarily missing from the metadata).\n cancelFly();\n if (reviewMode && activeSlice !== null &&\n slices.some(function (s) { return s.index === activeSlice; })) {\n flyTo(sliceFrame(activeSlice));\n } else if (atFit || !cam) {\n cam = fitCam();\n applyCam();\n } else {\n clampPan();\n applyCam();\n }\n\n banner.hidden = true;\n retryAttempt = 0;\n stamp.textContent = new Date().toLocaleTimeString();\n dot.classList.remove(\"stale\");\n }\n\n function fail(msg) {\n bannerText.textContent = \"Couldn't load \" + SVG_FILE + \" — \" + msg + \" · retrying…\";\n banner.hidden = false;\n const delay = RETRY_MS[Math.min(retryAttempt, RETRY_MS.length - 1)];\n retryAttempt += 1;\n retryTimer = setTimeout(reload, delay);\n }\n\n // --- review mode / storyboard ---\n\n // The union, in natural/model coordinates, of every element actually\n // belonging to slice `index` — lets sliceFrame zoom tightly around what a\n // slice contains instead of always keeping the model's FULL height in\n // frame. Always requiring the full height meant a model with few slices\n // but several swimlane rows could never zoom in tighter than Fit: letting\n // frameRect grow the frame's width to preserve that full height, on a\n // landscape stage, routinely produced a frame wider than the whole\n // diagram. getBoundingClientRect() + toSvg() reuses the exact camera math\n // already trusted for zoom-about-cursor, so this needs no new server-side\n // geometry or metadata. Returns null when there's no usable geometry yet\n // (nothing tagged, or a non-layout test environment) — sliceFrame falls\n // back to the old full-height framing in that case.\n function sliceContentBox(index) {\n if (!svgEl || !cam) return null;\n const nodes = svgEl.querySelectorAll('[data-slice=\"' + index + '\"]');\n let box = null;\n nodes.forEach(function (node) {\n const r = node.getBoundingClientRect();\n if (r.width <= 0 || r.height <= 0) return;\n const p0 = toSvg(r.left, r.top);\n const p1 = toSvg(r.right, r.bottom);\n const nx0 = Math.min(p0.x, p1.x), nx1 = Math.max(p0.x, p1.x);\n const ny0 = Math.min(p0.y, p1.y), ny1 = Math.max(p0.y, p1.y);\n box = box\n ? { x0: Math.min(box.x0, nx0), x1: Math.max(box.x1, nx1), y0: Math.min(box.y0, ny0), y1: Math.max(box.y1, ny1) }\n : { x0: nx0, x1: nx1, y0: ny0, y1: ny1 };\n });\n return box;\n }\n\n function sliceFrame(index) {\n let slice = null;\n for (const s of slices) if (s.index === index) slice = s;\n if (!slice || !natural) return fitCam();\n const PAD = 24; // breathing room around the framed slice\n const content = sliceContentBox(index);\n let x0 = rowLabels ? Math.min(rowLabels.x0, slice.x0) : slice.x0;\n let x1 = slice.x1;\n // Vertically, frame just the slice's own content when we can measure it;\n // otherwise fall back to the model's full height (every swimlane row).\n let y0 = natural.y, y1 = natural.y + natural.h;\n if (content) {\n x0 = Math.min(x0, content.x0);\n x1 = Math.max(x1, content.x1);\n y0 = content.y0;\n y1 = content.y1;\n }\n x0 = Math.max(natural.x, x0 - PAD);\n x1 = Math.min(natural.x + natural.w, x1 + PAD);\n y0 = Math.max(natural.y, y0 - PAD);\n y1 = Math.min(natural.y + natural.h, y1 + PAD);\n return frameRect(x0, y0, Math.max(1, x1 - x0), Math.max(1, y1 - y0));\n }\n\n // Populates the storyline <select> from this load's em-storylines metadata — \"Full\n // model\" plus one option per declared storyline, pre-selected on whichever is current.\n // Hidden entirely when the model declares none (storylineVariants is []).\n function renderStorylinePicker() {\n storylinePicker.hidden = storylineVariants.length === 0;\n storylinePicker.innerHTML = \"\";\n for (const v of storylineVariants) {\n const opt = document.createElement(\"option\");\n opt.value = v.file;\n opt.textContent = v.name;\n opt.selected = !!v.current;\n storylinePicker.appendChild(opt);\n }\n }\n\n function renderChrome() {\n storyboard.hidden = slices.length === 0;\n reviewToggle.classList.toggle(\"active\", reviewMode);\n reviewToggle.disabled = slices.length === 0;\n filmstrip.innerHTML = \"\";\n for (const s of slices) {\n const b = document.createElement(\"button\");\n b.className = \"slice\" + (s.index === activeSlice ? \" active\" : \"\");\n b.textContent = s.name;\n b.addEventListener(\"click\", function () { goTo(s.index); });\n filmstrip.appendChild(b);\n }\n const first = slices[0], last = slices[slices.length - 1];\n prevBtn.disabled = activeSlice === null || !first || activeSlice <= first.index;\n nextBtn.disabled = activeSlice === null || !last || activeSlice >= last.index;\n }\n\n function applyDim() {\n if (!svgEl) return;\n svgEl.querySelectorAll(\"[data-slice]\").forEach(function (node) {\n const dim = reviewMode && activeSlice !== null &&\n Number(node.getAttribute(\"data-slice\")) !== activeSlice;\n node.classList.toggle(\"em-slice-dim\", dim);\n });\n }\n\n // --- doc flyout: click a slice's own node to see its design doc, purely\n // an overlay — it never moves the camera or touches review state. ---\n function sliceByIndex(index) {\n for (const s of slices) if (s.index === index) return s;\n return null;\n }\n\n function renderDocFlyout() {\n const s = sliceByIndex(openSliceIndex);\n if (!s) { docFlyout.classList.remove(\"open\"); return; }\n docFlyoutTitle.textContent = s.name;\n docFlyoutBody.classList.toggle(\"empty\", !s.docHtml);\n if (s.docHtml) docFlyoutBody.innerHTML = s.docHtml;\n else docFlyoutBody.textContent = \"No design doc yet for this slice.\";\n docFlyout.classList.add(\"open\");\n }\n\n function openDoc(index) {\n openSliceIndex = index;\n renderDocFlyout();\n }\n\n function closeDoc() {\n openSliceIndex = null;\n docFlyout.classList.remove(\"open\");\n }\n\n function goTo(index) {\n activeSlice = index;\n renderChrome();\n applyDim();\n if (reviewMode) flyTo(sliceFrame(index));\n }\n\n function step(delta) {\n if (!slices.length) return;\n if (activeSlice === null) { goTo(slices[0].index); return; }\n const i = slices.findIndex(function (s) { return s.index === activeSlice; });\n const next = slices[Math.min(slices.length - 1, Math.max(0, i + delta))];\n if (next) goTo(next.index);\n }\n\n function exitReview() {\n reviewMode = false;\n renderChrome();\n applyDim();\n goFit();\n }\n\n reviewToggle.addEventListener(\"click\", function () {\n if (reviewMode) { exitReview(); return; }\n reviewMode = true;\n if (activeSlice === null && slices.length) activeSlice = slices[0].index;\n renderChrome();\n applyDim();\n if (activeSlice !== null) flyTo(sliceFrame(activeSlice));\n });\n prevBtn.addEventListener(\"click\", function () { step(-1); });\n nextBtn.addEventListener(\"click\", function () { step(1); });\n // GH #309: switching storylines is a plain navigation to this same page with a new\n // ?svg= — the chosen file is already on disk (em watch --serve writes every variant on\n // every build), so there's nothing to fetch or reload by hand.\n storylinePicker.addEventListener(\"change\", function () {\n location.search = \"svg=\" + encodeURIComponent(storylinePicker.value);\n });\n document.getElementById(\"zoomIn\").addEventListener(\"click\", function () { zoomStep(1.5); });\n document.getElementById(\"zoomOut\").addEventListener(\"click\", function () { zoomStep(1 / 1.5); });\n document.getElementById(\"fitBtn\").addEventListener(\"click\", goFit);\n document.getElementById(\"errorDismiss\").addEventListener(\"click\", function () {\n banner.hidden = true; // retries keep going quietly\n });\n docFlyoutClose.addEventListener(\"click\", closeDoc);\n\n // --- direct manipulation: drag pan, wheel pan, ctrl+wheel / pinch zoom ---\n const pointers = new Map(); // pointerId -> last client {x, y}\n let drag = null; // { id, startX, startY, active } — single-pointer pan\n let suppressClick = false; // swallow the click that ends a drag/pinch\n\n // The doc flyout is a normal interactive DOM panel stacked over the stage\n // (see #docFlyout's z-index) — its own scrolling, text selection, and link\n // clicks must behave normally, not get hijacked by the stage's direct-\n // manipulation gestures (drag-pan, wheel-pan/zoom, double-click-to-zoom).\n function overFlyout(e) {\n return !!(e.target.closest && e.target.closest(\"#docFlyout\"));\n }\n\n stage.addEventListener(\"pointerdown\", function (e) {\n if (overFlyout(e)) return;\n if (e.button !== 0) return;\n cancelFly(); // direct manipulation takes over from any animation\n pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });\n if (pointers.size === 1) {\n suppressClick = false;\n drag = { id: e.pointerId, startX: e.clientX, startY: e.clientY, active: false };\n } else {\n // Second pointer: pinch. Capture both so the gesture survives leaving\n // the stage; a pinch must never end in a note-link click.\n drag = null;\n suppressClick = true;\n stage.classList.add(\"dragging\");\n for (const id of pointers.keys()) {\n try { stage.setPointerCapture(id); } catch (err) { /* pointer already gone */ }\n }\n }\n });\n\n stage.addEventListener(\"pointermove\", function (e) {\n const pt = pointers.get(e.pointerId);\n if (!pt) return;\n if (pointers.size >= 2) { pinchMove(e, pt); return; }\n if (!drag || drag.id !== e.pointerId || !cam) { pt.x = e.clientX; pt.y = e.clientY; return; }\n if (!drag.active) {\n // Movement threshold before treating it as a drag — capturing the\n // pointer immediately would retarget the eventual click away from the\n // SVG's note links. Until then pt stays at the pointerdown position,\n // so the first panning move applies the full accumulated delta.\n if (Math.hypot(e.clientX - drag.startX, e.clientY - drag.startY) < 4) return;\n drag.active = true;\n suppressClick = true;\n stage.setPointerCapture(e.pointerId);\n stage.classList.add(\"dragging\");\n }\n const s = scale();\n cam.x -= (e.clientX - pt.x) * s;\n cam.y -= (e.clientY - pt.y) * s;\n pt.x = e.clientX; pt.y = e.clientY;\n clampPan();\n applyCam();\n markMoved();\n });\n\n // Incremental pinch: pan by the midpoint's movement, then zoom about the\n // new midpoint by the ratio of pointer distances.\n function pinchMove(e, pt) {\n let other = null;\n for (const entry of pointers) if (entry[0] !== e.pointerId) other = entry[1];\n if (!other || !cam) { pt.x = e.clientX; pt.y = e.clientY; return; }\n const oldMid = { x: (pt.x + other.x) / 2, y: (pt.y + other.y) / 2 };\n const oldDist = Math.hypot(pt.x - other.x, pt.y - other.y);\n pt.x = e.clientX; pt.y = e.clientY;\n const newMid = { x: (pt.x + other.x) / 2, y: (pt.y + other.y) / 2 };\n const newDist = Math.hypot(pt.x - other.x, pt.y - other.y);\n const s = scale();\n cam.x -= (newMid.x - oldMid.x) * s;\n cam.y -= (newMid.y - oldMid.y) * s;\n if (oldDist > 0 && newDist > 0) cam = zoomedCam(toSvg(newMid.x, newMid.y), newDist / oldDist);\n clampPan();\n applyCam();\n markMoved();\n }\n\n function endPointer(e) {\n if (!pointers.has(e.pointerId)) return; // not a pointer we were tracking\n pointers.delete(e.pointerId);\n if (stage.hasPointerCapture && stage.hasPointerCapture(e.pointerId)) {\n stage.releasePointerCapture(e.pointerId);\n }\n if (drag && drag.id === e.pointerId) drag = null;\n if (pointers.size === 1) {\n // Pinch collapsed to one finger: continue as an already-active pan.\n const rest = pointers.entries().next().value;\n drag = { id: rest[0], startX: rest[1].x, startY: rest[1].y, active: true };\n }\n if (pointers.size === 0) stage.classList.remove(\"dragging\");\n }\n // On window, not the stage: a pointer that goes down on the stage but is\n // released elsewhere (before the drag threshold captures it) must still be\n // cleared, or its stale Map entry would misclassify every later gesture as\n // a pinch. endPointer ignores pointers it never tracked.\n window.addEventListener(\"pointerup\", endPointer);\n window.addEventListener(\"pointercancel\", endPointer);\n\n stage.addEventListener(\"click\", function (e) {\n if (suppressClick) { suppressClick = false; e.preventDefault(); e.stopPropagation(); }\n }, true);\n\n // A plain click landing on an element's own node group (not a note/issue/\n // divergence marker — those live in separate groups with no data-slice\n // ancestor) opens that slice's doc flyout. A drag/pinch's trailing click\n // never reaches here: the capturing listener above stops it first.\n stage.addEventListener(\"click\", function (e) {\n const node = e.target.closest && e.target.closest(\"[data-slice]\");\n if (node) openDoc(Number(node.getAttribute(\"data-slice\")));\n });\n\n stage.addEventListener(\"wheel\", function (e) {\n if (overFlyout(e)) return; // let the flyout's own overflow-y:auto scroll natively\n e.preventDefault(); // the page itself must never scroll or browser-zoom\n if (!cam) return;\n cancelFly();\n const k = e.deltaMode === 1 ? 16 : 1; // line-mode deltas (Firefox) → ~px\n if (e.ctrlKey) {\n // ctrl+wheel is also what a trackpad pinch emits.\n cam = zoomedCam(toSvg(e.clientX, e.clientY), Math.exp(-e.deltaY * k * 0.01));\n } else {\n const s = scale();\n cam.x += e.deltaX * k * s;\n cam.y += e.deltaY * k * s;\n clampPan();\n }\n applyCam();\n markMoved();\n }, { passive: false });\n\n stage.addEventListener(\"dblclick\", function (e) {\n if (overFlyout(e)) return;\n if (!cam) return;\n e.preventDefault();\n markMoved();\n flyTo(zoomedCam(toSvg(e.clientX, e.clientY), 2));\n });\n\n document.addEventListener(\"keydown\", function (e) {\n // Modified keys stay with the browser; shift is allowed (\"+\" needs it\n // on many layouts). Guard against future text inputs, too.\n if (e.ctrlKey || e.metaKey || e.altKey) return;\n const t = e.target;\n if (t && (t.tagName === \"INPUT\" || t.tagName === \"TEXTAREA\" || t.isContentEditable)) return;\n if (e.key === \"+\" || e.key === \"=\") { e.preventDefault(); zoomStep(1.5); }\n else if (e.key === \"-\") { e.preventDefault(); zoomStep(1 / 1.5); }\n else if (e.key === \"0\" || e.key === \"f\") { e.preventDefault(); goFit(); }\n else if (e.key === \"Escape\") {\n if (openSliceIndex !== null) closeDoc();\n else if (reviewMode) exitReview();\n }\n else if (reviewMode) {\n if (e.key === \"ArrowLeft\") { e.preventDefault(); step(-1); }\n else if (e.key === \"ArrowRight\") { e.preventDefault(); step(1); }\n else if (e.key === \"Home\") { e.preventDefault(); if (slices.length) goTo(slices[0].index); }\n else if (e.key === \"End\") { e.preventDefault(); if (slices.length) goTo(slices[slices.length - 1].index); }\n }\n });\n\n window.addEventListener(\"resize\", function () {\n if (!natural || !cam) return;\n // Keep what the presenter framed: an active slice re-frames to the new\n // aspect; a fitted view re-fits; a hand-placed camera stays put.\n if (reviewMode && activeSlice !== null) { cam = sliceFrame(activeSlice); applyCam(); }\n else if (atFit) { cam = fitCam(); applyCam(); }\n });\n\n reload();\n\n const es = new EventSource(\"/__events\");\n // onopen reloads only on REconnect (laptop sleep, server restart): pushes\n // sent in the meantime were missed. The initial connect skips it — the\n // eager reload() above is already in flight, and doubling it would fetch\n // and parse every page open twice.\n let firstOpen = true;\n es.onopen = function () {\n retryAttempt = 0;\n if (firstOpen) { firstOpen = false; return; }\n reload();\n };\n es.onmessage = function () { retryAttempt = 0; reload(); };\n es.onerror = function () { dot.classList.add(\"stale\"); };\n // A save that failed to render (parse/render error): the server pushes the\n // message, the last good diagram stays up, and the banner says why the\n // screen is stale — it must never silently show an outdated model. No\n // retry here: refetching can only return the old file; the next successful\n // render pushes a reload that clears the banner.\n es.addEventListener(\"renderError\", function (e) {\n if (retryTimer !== null) { clearTimeout(retryTimer); retryTimer = null; }\n bannerText.textContent = \"Model didn't render — \" + e.data + \" — showing last good diagram\";\n banner.hidden = false;\n dot.classList.add(\"stale\");\n });\n </script>\n</body>\n</html>\n";
5
5
  //# sourceMappingURL=viewerHtml.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"viewerHtml.js","sourceRoot":"","sources":["../../src/render/viewerHtml.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,0FAA0F;AAC1F,0DAA0D;AAC1D,MAAM,CAAC,MAAM,WAAW,GAAW,w9nCAAw9nC,CAAC"}
1
+ {"version":3,"file":"viewerHtml.js","sourceRoot":"","sources":["../../src/render/viewerHtml.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,0FAA0F;AAC1F,0DAA0D;AAC1D,MAAM,CAAC,MAAM,WAAW,GAAW,y80CAAy80C,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milehimikey/em",
3
- "version": "1.19.7",
3
+ "version": "1.20.0",
4
4
  "description": "AI-friendly Event Modeling — a slice-first text DSL rendered to a strict, self-contained Graphviz grid (SVG/PNG)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "em",
3
- "version": "1.19.7",
3
+ "version": "1.20.0",
4
4
  "description": "Event Modeling with em: discover, design, implement, conform, review and engagement skills over a slice-first text DSL, plus the em MCP server.",
5
5
  "author": {
6
6
  "name": "Mike Key"
@@ -10,7 +10,7 @@
10
10
  "command": "npx",
11
11
  "args": [
12
12
  "-y",
13
- "@milehimikey/em@1.19.7",
13
+ "@milehimikey/em@1.20.0",
14
14
  "mcp"
15
15
  ]
16
16
  }
@@ -29,6 +29,8 @@ em render <file> --keep-empty-lanes # keep the A
29
29
  em export <file> # export a versioned JSON snapshot of the normalized model
30
30
  em export <file> -o, --out <path> # write to a file instead of stdout
31
31
  em export <file> --slice <key> # export only this slice's object (pattern/fields/doc) instead of the whole model (export key, MIL-128) — refuses only if THIS slice has an error; an unrelated slice's breakage elsewhere in the model doesn't block it (see docs/cli.md)
32
+ em export <file> --doc-json # emit the structured slice-doc export (GH #401) instead of the model snapshot: per slice the frontmatter join plus the parsed sections as data (fields, invariants, scenarios, error flows, open questions) and a model-level invariant catalog. Own schema version; whole model only (does not combine with --slice)
33
+ em export <file> --include-html # with --doc-json, include each slice doc's rendered HTML body (excluded by default; the authored body never appears in a plain export)
32
34
  em api generate <file> # write the TypeSpec contract for the model's public commands, events and views to <model dir>/contracts/<model key>.tsp
33
35
  em api generate <file> -o, --out <path> # write to this path instead of <model dir>/contracts/<model key>.tsp
34
36
  em api generate <file> --stdout # print the contract instead of writing it
@@ -99,10 +101,13 @@ em slice defer <file> <slice-key> [question] --decision <text> # what this
99
101
  em slice defer <file> <slice-key> [question] --from-issue <text> # defer a captured `issue "..."` on this slice instead of a doc Open Question (case-sensitive substring of its text)
100
102
  em slice defer <file> <slice-key> [question] --by <name> # who deferred it
101
103
  em slice defer <file> <slice-key> [question] --on <date> # deferral date, YYYY-MM-DD (default: today, local date)
102
- em slice conform <file> <slice-key> # record per-slice-per-version conformance certification: sets `conformedVersion:`/`conformedAt:`/`conformedOn:` on a slice doc (MIL-214) — the fact `driftSignal: in-sync` now depends on. Legal only for a shipped slice (`status: implemented`, or any status with a `shippedVersion:` — MIL-283) with an `implementedIn:` link; `conformedVersion:` is the shipped version. Idempotent on the same (version, --at) pair; a different --at simply overwrites (a later re-certification is normal — there's no way for em to tell 'later' from 'earlier' for an arbitrary revision string, so there's no --force to reach for). Refuses when the conformance/<date>[-<name>]-findings.json file matching --at still has an unruled finding in scope for this slice; --skip-findings-check overrides with a loud notice.
104
+ em slice conform <file> <slice-key> # record per-slice-per-version conformance certification: sets `conformedVersion:`/`conformedAt:`/`conformedOn:` on a slice doc (MIL-214) — the fact `driftSignal: in-sync` now depends on. Legal only for a shipped slice (`status: implemented`, or any status with a `shippedVersion:` — MIL-283) with an `implementedIn:` link; `conformedVersion:` is the shipped version. Idempotent on the same (version, --at) pair; a different --at simply overwrites (a later re-certification is normal — there's no way for em to tell 'later' from 'earlier' for an arbitrary revision string, so there's no --force to reach for). Refuses when the conformance/<date>[-<name>]-findings.json file matching --at still has an unruled finding in scope for this slice; --skip-findings-check overrides with a loud notice. GH #400: --preview runs the same doc-resolution and findings-check read for a slice that hasn't shipped yet — the shipped/implementedIn gates are waived and NOTHING is ever written to the doc; output is ephemeral (text or --json) unless --write-findings opts into persisting a normal findings file.
103
105
  em slice conform <file> <slice-key> --at <rev> # the target-repo revision this certification sweep diffed against — or, when what was exercised is not a commit that stays reachable (a per-PR environment under squash/rebase merges, a build or image), an opaque `<kind>:<id>` such as build:1234 or deploy:eu-1-2026-10-15 (GH #261); recorded verbatim, skipped by git-reachability checks
104
106
  em slice conform <file> <slice-key> --on <date> # certification date, YYYY-MM-DD (default: today, local date)
105
107
  em slice conform <file> <slice-key> --skip-findings-check # certify even with unruled findings in scope — prints a loud notice on stderr
108
+ em slice conform <file> <slice-key> --preview # GH #400: pre-ship advisory mode — waives the shipped-status/`implementedIn:` gates, writes NOTHING to the doc (no conformedVersion/conformedAt/conformedOn, ever); ephemeral by default, --write-findings opts into persisting a normal findings file. --skip-findings-check has no effect in this mode (preview never refuses on findings)
109
+ em slice conform <file> <slice-key> --write-findings # with --preview: persist the preview's own unruled findings as conformance/<on>-<slice-key>-preview-findings.json
110
+ em slice conform <file> <slice-key> --json # with --preview: print a JSON document instead of the text report
106
111
  em changelog <file> # render a model's git history as a business-readable ledger (see docs/cli.md)
107
112
  em changelog <file> --from <rev> # start the walk at this revision (inclusive)
108
113
  em changelog <file> --to <rev> # end the walk at this revision (inclusive; default HEAD)
@@ -146,6 +151,7 @@ em watch <file> -T, --format <fmt> # output for
146
151
  em watch <file> --keep-empty-lanes # keep the API lane even when empty
147
152
  em watch <file> --serve # serve a live viewer with instant push-reload (no polling)
148
153
  em watch <file> --port <n> # port for --serve (default 5173)
154
+ em watch <file> --storyline <name> # GH #309 (deferred from #277): filter the live view to one storyline's subset — the same subset `em render --storyline` draws (its own slices in order, plus dimmed stub columns for what they read from outside it and the `after` end state). Whenever the model declares any storylines the viewer also offers a picker to switch to the full model or another declared storyline, live, without restarting. An unknown name errors, listing the declared storylines; requires --serve
149
155
  em validate <file> # check a model against event-modeling rules
150
156
  em validate <file> --list-issues # print only open `issue` diagnostics (slice, element, line, text)
151
157
  em validate <file> --list-divergences # print only accepted-divergence annotations (slice, element, line, text) — never fails the build
@@ -165,6 +171,7 @@ em refs <file> # check that
165
171
  em refs <file> --reachable-from <rev> # the branch or commit every ref must be an ancestor of (e.g. origin/main)
166
172
  em refs <file> --waive <slice-key> # excuse this slice's non-ancestor/unresolved/undecidable results from the exit code (repeatable); still reported
167
173
  em refs <file> --waive-file <path> # a baseline file of slice keys to waive, one per line (`#` comments allowed); missing file = no waivers
174
+ em refs <file> --check-order # advisory-only (GH #399): for every slice doc with a ratifiedRef, report whether a commit between the branch's fork point and ratifiedRef already touched code other than the doc itself — a retrofit signal for inline ratification (spec written to describe code that already exists). History is rewritable, so this NEVER affects the exit code or `ok`, and is never a validate gate — see docs/process.md's inline-ratification section
168
175
  em refs <file> --json # print a JSON document instead of the text report (see docs/cli.md)
169
176
  em ledger <file> # check slice docs' version: field agrees with their content across two git revisions (opt-in CI check, MIL-89 — never part of `em validate`, see docs/ci.md)
170
177
  em ledger <file> --from <rev> # baseline revision
@@ -490,7 +497,19 @@ type Name { field: Type, ... } # named structured type, reusable from any f
490
497
  declared `type T`, with `derived from "Child Created", "Child Deleted"`, embeds a child entity:
491
498
  each field of `T` must trace to a named event (`fields-completeness/view-field-no-source`
492
499
  otherwise); a field also traces one level through the declared element type of an array field
493
- on the event (`optionValues: VariantOptionValue[]`), no deeper and with no path syntax. An
500
+ on the event (`optionValues: VariantOptionValue[]`), no deeper by same name. When the event
501
+ renamed or flattened the field, declare the element field's source by **path** into the named
502
+ event, as the unquoted form of the same clause (GH #383): `attributeValueIds: string[] derived
503
+ from values[].attributeValueId` — a `.`-joined chain of identifiers, each `[]`-suffixed exactly
504
+ when it steps through an array field (the marker is enforced against the event field's actual
505
+ arrayness). The path must resolve on a named event — every named event is tried; non-leaf
506
+ segments must be `type`-valued fields — and the leaf field's type must match the element
507
+ field's (array markers stripped); an unresolved path or leaf-type mismatch is
508
+ `fields-completeness/view-field-no-source` (implicit same-name nested tracing matches names
509
+ only; the explicit path form earns the stronger leaf-type check). The quoted event-list form
510
+ and the unquoted path form are mutually exclusive on one field — both at once is a parse error.
511
+ A plain (non-collection) view field may carry the same path form, checked the same way against
512
+ the view's source events — never silently exempt like a bare `derived` field. An
494
513
  element field the projection computes rather than copies (an audit stamp from event metadata,
495
514
  say) is marked `derived` on the `type` field itself — bare or `derived from "<events>"`, same
496
515
  syntax as a view field (GH #382): it is then skipped by `view-field-no-source`, and a traced
@@ -955,6 +974,7 @@ not the prose above has caught up yet. `--slice-ready <key>`-only codes are excl
955
974
  | `view-again-without-earlier` | error | `again` without an earlier declaration | Declare the view plainly the first time it appears. |
956
975
  | `view-from-future-event` | error | Backward timeline (view reads a future event) | Move the source to a later `view X again` instance. |
957
976
  | `view-from-unresolved` | error | Unknown event source | Fix the `from` reference to name an existing event. |
977
+ | `view-lookup-source` | warning | View reads an event only as a lookup | Carry the field on the event that creates this view's own row instead; if this is a genuine cross-context row change, accept it on the shape exit gate (docs/validation.md#the-shape-exit-gate) rather than fixing it (docs/patterns.md#state-view). |
958
978
  | `view-no-source` | warning | Read model without source | Add `from "Event"`, or place the view in a slice with an event. |
959
979
  <!-- GENERATED:validate-rules:end -->
960
980
 
@@ -317,6 +317,22 @@ drift into **CRUD laid on a timeline**; these rules stop that. Quoted lines are
317
317
  view carries ids; child with its own data → embedded in the parent view; child queried without the
318
318
  parent id → its own view. Aggregate rules decide write-side consistency, never read models. *(em
319
319
  ruling)*
320
+ - **Views as lookups: an event that creates a row carries what the row shows.** A view reads another
321
+ event only when that event adds, changes or removes a row the view already owns — never merely to
322
+ fill a column on a row some *other* event created. **Per-view check:** for each `from "E"` on a view,
323
+ ask *does E add, change or remove a row here? If it only fills a column, put the column on the event
324
+ that creates the row and drop E.* Generated models routinely come out wrong here — a wide fan-out of
325
+ "reference" events into every view that shows a name, and views keeping a lookup table ("every
326
+ neighbor's name and current email, every tool's name") beside their own rows. Fix: the decider
327
+ already read what it needed to enforce its own invariants (e.g. a checkout decider that reads Tool
328
+ Added and Neighbor Registered to validate the request already has `toolName`/`neighborName` in hand),
329
+ so have it assign those fields onto the event it records — the view then needs nothing beyond that
330
+ one event. A slice doc that needs a "Bookkeeping" paragraph (the view keeps, beyond its own rows, a
331
+ lookup of some other entity's fields) has this smell; see `docs/patterns.md#state-view` for the
332
+ worked overdue-notice example.
333
+ `view-lookup-source` is the mechanical, advisory half — it catches the cross-context cases with a few
334
+ false positives; a within-context lookup (`Tool Checked Out` read only to learn whose loan a later
335
+ event concerns) is invisible to it, so this check still has to be done by eye.
320
336
  - **Write side: children belong in the parent's context** when the child's invariants are checks against
321
337
  the parent. A sub-resource schema shared across parent types is one event per parent on the parent's
322
338
  context, not a shared aggregate — check the spec's event reference, not its resource section. Where
@@ -165,7 +165,12 @@ an event directly.}}
165
165
  line and field table are a GENERATED region (one per read model; `em-slice-view-<view-slug>`
166
166
  when there are several): Source / Notes names the source event a field is copied from, or
167
167
  `Derived` for a field marked `derived` in the `.em` (docs/dsl.md#derived-fields) — say how
168
- it is derived in prose below the region. `{{ }}` marks a field the model cannot trace. -->
168
+ it is derived in prose below the region. `{{ }}` marks a field the model cannot trace.
169
+ If describing a field here needs a "Bookkeeping" paragraph — this view also keeps, beyond its
170
+ own rows, a lookup of some other entity's name/email/etc. so it can display it — that is the
171
+ views-as-lookups smell (methodology reference, "Views as lookups"): the field belongs on the
172
+ event that creates this view's row instead, copied from state the command/decider already
173
+ reads to enforce its own invariants. Fix the `.em`, not the prose. -->
169
174
  <!-- GENERATED:em-slice-view:start — generated from the model by `em slice sync`; do not hand-edit -->
170
175
  - **View:** `{{View Name}}` built from events: {{"Event A", "Event B"}}
171
176
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: conform
3
- em-version: 1.19.7
3
+ em-version: 1.20.0
4
4
  description: >-
5
5
  Use when checking a ratified event model (and its slice docs) for drift against the codebase
6
6
  that implements it — advisory only, never a gate, never an unprompted edit — or when walking
@@ -40,7 +40,9 @@ difference is what you do with what you find: extract *builds* a model from the
40
40
  (or the status the user says corresponds to shipped code) for whichever slices are in scope.
41
41
  Checking a `draft` or `ready-to-implement` slice against code is a wasted walk — ratified or
42
42
  not, nobody has built to it yet; tell the user and skip it rather than reporting "drift"
43
- against it.
43
+ against it. (GH #400: the ONE exception is a deliberate pre-ship advisory run — see
44
+ `--preview` under step 5 below — not a reason to treat this precondition as optional for an
45
+ ordinary post-ship sweep.)
44
46
  3. The codebase to check: the state file's **Existing system refs** (populated during
45
47
  `extract`, if this model came from one) or ask the user for the repo path if not set. Record
46
48
  it if missing. In a headless/scheduled run with no user to ask, default to the repository
@@ -336,6 +338,32 @@ been through this) instead of just trusting `implementedIn`. Legal only for `sta
336
338
  with a link, and itself refuses if an unruled finding for that slice still exists at this
337
339
  revision — the mechanical proof this step comes only after rulings, not before.
338
340
 
341
+ **Pre-ship advisory mode: `--preview` (GH #400).** The certify step above is legal only for a
342
+ **shipped** slice (`status: implemented`/`shippedVersion:` + `implementedIn:`) — precondition 2
343
+ reflects that this phase ordinarily walks shipped code. `--preview` is the deliberate exception:
344
+ the SAME doc-resolution and findings-check read, legal for a slice that hasn't shipped yet
345
+ (the shipped/`implementedIn:` preconditions are waived), and it **never writes anything** —
346
+ no `conformedVersion`/`conformedAt`/`conformedOn`, ever, regardless of outcome:
347
+
348
+ ```
349
+ em slice conform <model-name>.em <slice-key> --at <target-repo revision> --preview
350
+ ```
351
+
352
+ Use this to run a conform sweep against your OWN implementing branch before it merges — catching
353
+ spec/code drift while you can still fix it on the same branch, rather than after shipping. The
354
+ walk itself (steps 1-4 above) is unchanged; only the certify step's preconditions differ. Output
355
+ is ephemeral by default (nothing is written, not even to `conformance/`); pass
356
+ `--write-findings` to persist the run's own unruled findings as a normal
357
+ `conformance/<date>-<slice-key>-preview-findings.json` (this is deliberate — `docs/cli.md`'s
358
+ `em slice conform --preview` section documents the exact shape and naming). Know before opting
359
+ in: the written file has no sibling report (`report` is synthetic), so `em conform-supersede`
360
+ refuses to rule on it — unruled entries in it gate certify/`set-conformance` at that revision
361
+ until you rule them in the file by hand, delete the snapshot, or author the sibling report.
362
+ `em state set-conformance` and the unruled-findings gates are unaffected by
363
+ this mode either way: a default preview run writes nothing for them to read, so it can never
364
+ block a later certify or `set-conformance` call. There is no `--slice-ready` integration for this
365
+ mode yet.
366
+
339
367
  End of run: update the state file's `Last conformance:` marker with
340
368
 
341
369
  ```