@milehimikey/em 1.7.2 → 1.8.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 (112) hide show
  1. package/.claude/skills/event-modeling/SKILL.md +92 -65
  2. package/.claude/skills/event-modeling/reference/conform.md +45 -20
  3. package/.claude/skills/event-modeling/reference/em-dsl.md +120 -55
  4. package/.claude/skills/event-modeling/reference/extract.md +5 -4
  5. package/.claude/skills/event-modeling/reference/implement.md +34 -22
  6. package/.claude/skills/event-modeling/templates/model-readme.md +8 -5
  7. package/.claude/skills/event-modeling/templates/slice.md +19 -5
  8. package/README.md +6 -4
  9. package/dist/catalog/docJoin.d.ts +28 -3
  10. package/dist/catalog/docJoin.js +82 -13
  11. package/dist/catalog/docJoin.js.map +1 -1
  12. package/dist/catalog/docModelConsistencyValidate.d.ts +8 -0
  13. package/dist/catalog/docModelConsistencyValidate.js +335 -0
  14. package/dist/catalog/docModelConsistencyValidate.js.map +1 -0
  15. package/dist/catalog/noteBindingValidate.d.ts +8 -0
  16. package/dist/catalog/noteBindingValidate.js +133 -0
  17. package/dist/catalog/noteBindingValidate.js.map +1 -0
  18. package/dist/catalog/pages.js +3 -3
  19. package/dist/catalog/pages.js.map +1 -1
  20. package/dist/catalog/sliceDoc.d.ts +14 -0
  21. package/dist/catalog/sliceDoc.js +22 -0
  22. package/dist/catalog/sliceDoc.js.map +1 -1
  23. package/dist/catalog/sliceReadyValidate.d.ts +18 -0
  24. package/dist/catalog/sliceReadyValidate.js +25 -1
  25. package/dist/catalog/sliceReadyValidate.js.map +1 -1
  26. package/dist/cli/agentsMd.d.ts +15 -0
  27. package/dist/cli/agentsMd.js +72 -0
  28. package/dist/cli/agentsMd.js.map +1 -0
  29. package/dist/cli/conformScope.d.ts +86 -0
  30. package/dist/cli/conformScope.js +156 -0
  31. package/dist/cli/conformScope.js.map +1 -0
  32. package/dist/cli/contract.d.ts +7 -0
  33. package/dist/cli/contract.js +27 -0
  34. package/dist/cli/contract.js.map +1 -0
  35. package/dist/cli/coverage.d.ts +69 -0
  36. package/dist/cli/coverage.js +157 -0
  37. package/dist/cli/coverage.js.map +1 -0
  38. package/dist/cli/markImplemented.d.ts +34 -0
  39. package/dist/cli/markImplemented.js +180 -0
  40. package/dist/cli/markImplemented.js.map +1 -0
  41. package/dist/cli/migrateReactionShape.d.ts +62 -0
  42. package/dist/cli/migrateReactionShape.js +283 -0
  43. package/dist/cli/migrateReactionShape.js.map +1 -0
  44. package/dist/cli/sliceIndex.d.ts +49 -0
  45. package/dist/cli/sliceIndex.js +115 -0
  46. package/dist/cli/sliceIndex.js.map +1 -0
  47. package/dist/cli/sliceNew.d.ts +18 -0
  48. package/dist/cli/sliceNew.js +53 -0
  49. package/dist/cli/sliceNew.js.map +1 -0
  50. package/dist/cli/stateFile.d.ts +74 -0
  51. package/dist/cli/stateFile.js +191 -0
  52. package/dist/cli/stateFile.js.map +1 -0
  53. package/dist/cli.js +535 -43
  54. package/dist/cli.js.map +1 -1
  55. package/dist/emit/coverageJson.d.ts +8 -0
  56. package/dist/emit/coverageJson.js +30 -0
  57. package/dist/emit/coverageJson.js.map +1 -0
  58. package/dist/emit/dot.js +41 -10
  59. package/dist/emit/dot.js.map +1 -1
  60. package/dist/emit/json.d.ts +72 -4
  61. package/dist/emit/json.js +73 -4
  62. package/dist/emit/json.js.map +1 -1
  63. package/dist/emit/validateJson.d.ts +52 -0
  64. package/dist/emit/validateJson.js +120 -0
  65. package/dist/emit/validateJson.js.map +1 -0
  66. package/dist/mcp/main.d.ts +2 -0
  67. package/dist/mcp/main.js +20 -0
  68. package/dist/mcp/main.js.map +1 -0
  69. package/dist/mcp/server.d.ts +11 -0
  70. package/dist/mcp/server.js +260 -0
  71. package/dist/mcp/server.js.map +1 -0
  72. package/dist/model/model.d.ts +30 -1
  73. package/dist/model/model.js +26 -0
  74. package/dist/model/model.js.map +1 -1
  75. package/dist/model/rules.d.ts +114 -0
  76. package/dist/model/rules.js +116 -4
  77. package/dist/model/rules.js.map +1 -1
  78. package/dist/model/validate.js +83 -7
  79. package/dist/model/validate.js.map +1 -1
  80. package/dist/parser/ast.d.ts +41 -0
  81. package/dist/parser/lexer.d.ts +16 -0
  82. package/dist/parser/lexer.js +44 -2
  83. package/dist/parser/lexer.js.map +1 -1
  84. package/dist/parser/parser.js +340 -20
  85. package/dist/parser/parser.js.map +1 -1
  86. package/dist/render/drawNotes.js +8 -1
  87. package/dist/render/drawNotes.js.map +1 -1
  88. package/dist/render/render.js +85 -17
  89. package/dist/render/render.js.map +1 -1
  90. package/dist/render/serve.d.ts +4 -0
  91. package/dist/render/serve.js +23 -239
  92. package/dist/render/serve.js.map +1 -1
  93. package/dist/render/sliceStatus.d.ts +1 -1
  94. package/dist/render/sliceStatus.js +35 -6
  95. package/dist/render/sliceStatus.js.map +1 -1
  96. package/dist/render/viewerHtml.d.ts +1 -0
  97. package/dist/render/viewerHtml.js +5 -0
  98. package/dist/render/viewerHtml.js.map +1 -0
  99. package/dist/render/watch.d.ts +10 -0
  100. package/dist/render/watch.js +29 -0
  101. package/dist/render/watch.js.map +1 -1
  102. package/dist/templates.d.ts +19 -0
  103. package/dist/templates.js +169 -0
  104. package/dist/templates.js.map +1 -1
  105. package/dist/util/markers.d.ts +13 -0
  106. package/dist/util/markers.js +30 -0
  107. package/dist/util/markers.js.map +1 -0
  108. package/dist/util/walkDir.d.ts +10 -1
  109. package/dist/util/walkDir.js +11 -7
  110. package/dist/util/walkDir.js.map +1 -1
  111. package/package.json +12 -3
  112. package/.claude/skills/event-modeling/templates/live.html +0 -117
@@ -1 +1 @@
1
- {"version":3,"file":"serve.js","sourceRoot":"","sources":["../../src/render/serve.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"serve.js","sourceRoot":"","sources":["../../src/render/serve.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { NormalizedModel } from "../model/model.js";
2
2
  /** One status per slice, same order as model.slices (and so the same order as
3
3
  * the header row's columns — see layout/grid.ts's sliceNames). null means no
4
- * doc was found, or the doc has no `- **Status:** ...` line. */
4
+ * doc was found (own or covering), or the doc has no `- **Status:** ...` line. */
5
5
  export declare function readSliceStatuses(model: NormalizedModel, baseDir: string): (string | null)[];
@@ -7,20 +7,49 @@
7
7
  // the *deduped* export key from computeRefs(), not a fresh kebabSlug(name) —
8
8
  // so two same-named slices resolve statuses the same way `em catalog` resolves
9
9
  // docs for them (first one gets the real file, the second honestly gets null).
10
- import { existsSync, readFileSync } from "node:fs";
10
+ //
11
+ // MIL-121 fallback: when a slice has no own `slices/<key>.md`, scan the `slices/` directory for
12
+ // a sibling doc whose frontmatter `covers:` names this slice's key, and borrow ITS status. This
13
+ // is filename+frontmatter discovery only — same convention as the direct lookup above, and
14
+ // still never reads `note` (the tested invariant test/catalog.e2e.test.ts pins for `em catalog`'s
15
+ // parallel doc discovery) — a slice's `note "slices/<other>.md"` cross-binding declaration is
16
+ // docJoin.ts's concern (`em export`/`em validate --slice-ready`), not this legend's.
17
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
11
18
  import { join } from "node:path";
12
19
  import { computeRefs } from "../model/refs.js";
13
20
  import { parseSliceDoc } from "../catalog/sliceDoc.js";
14
21
  /** One status per slice, same order as model.slices (and so the same order as
15
22
  * the header row's columns — see layout/grid.ts's sliceNames). null means no
16
- * doc was found, or the doc has no `- **Status:** ...` line. */
23
+ * doc was found (own or covering), or the doc has no `- **Status:** ...` line. */
17
24
  export function readSliceStatuses(model, baseDir) {
18
25
  const { sliceKeys } = computeRefs(model);
26
+ const slicesDir = join(baseDir, "slices");
27
+ let coverage = null; // built lazily, only if ever needed
19
28
  return model.slices.map((_slice, i) => {
20
- const docPath = join(baseDir, "slices", `${sliceKeys[i]}.md`);
21
- if (!existsSync(docPath))
22
- return null;
23
- return parseSliceDoc(readFileSync(docPath, "utf8")).status;
29
+ const sliceKey = sliceKeys[i];
30
+ const docPath = join(slicesDir, `${sliceKey}.md`);
31
+ if (existsSync(docPath))
32
+ return parseSliceDoc(readFileSync(docPath, "utf8")).status;
33
+ coverage ??= readCoverageStatuses(slicesDir);
34
+ return coverage.get(sliceKey) ?? null;
24
35
  });
25
36
  }
37
+ /** Scans every `slices/*.md` file (sorted, for deterministic first-wins on an overlapping
38
+ * `covers:` claim) and maps each covered slice key to that doc's status. Directory-scan, not
39
+ * the single-file existsSync check every other doc lookup in em uses — the one precedent for
40
+ * it is `em ledger`'s (src/cli/ledgerCheck.ts) own `readdirSync(slicesDir)` sweep. */
41
+ function readCoverageStatuses(slicesDir) {
42
+ const coverage = new Map();
43
+ if (!existsSync(slicesDir))
44
+ return coverage;
45
+ const files = readdirSync(slicesDir).filter((f) => f.endsWith(".md")).sort();
46
+ for (const file of files) {
47
+ const doc = parseSliceDoc(readFileSync(join(slicesDir, file), "utf8"));
48
+ for (const coveredKey of doc.covers) {
49
+ if (!coverage.has(coveredKey))
50
+ coverage.set(coveredKey, doc.status);
51
+ }
52
+ }
53
+ return coverage;
54
+ }
26
55
  //# sourceMappingURL=sliceStatus.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sliceStatus.js","sourceRoot":"","sources":["../../src/render/sliceStatus.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,oEAAoE;AACpE,kFAAkF;AAClF,yEAAyE;AACzE,EAAE;AACF,6EAA6E;AAC7E,6EAA6E;AAC7E,+EAA+E;AAC/E,+EAA+E;AAE/E,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAEvD;;iEAEiE;AACjE,MAAM,UAAU,iBAAiB,CAAC,KAAsB,EAAE,OAAe;IACvE,MAAM,EAAE,SAAS,EAAE,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IACzC,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;QACpC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,GAAG,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAC9D,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;YAAE,OAAO,IAAI,CAAC;QACtC,OAAO,aAAa,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;IAC7D,CAAC,CAAC,CAAC;AACL,CAAC"}
1
+ {"version":3,"file":"sliceStatus.js","sourceRoot":"","sources":["../../src/render/sliceStatus.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,oEAAoE;AACpE,kFAAkF;AAClF,yEAAyE;AACzE,EAAE;AACF,6EAA6E;AAC7E,6EAA6E;AAC7E,+EAA+E;AAC/E,+EAA+E;AAC/E,EAAE;AACF,gGAAgG;AAChG,gGAAgG;AAChG,2FAA2F;AAC3F,kGAAkG;AAClG,8FAA8F;AAC9F,qFAAqF;AAErF,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAChE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAEvD;;mFAEmF;AACnF,MAAM,UAAU,iBAAiB,CAAC,KAAsB,EAAE,OAAe;IACvE,MAAM,EAAE,SAAS,EAAE,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;IACzC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IAC1C,IAAI,QAAQ,GAAsC,IAAI,CAAC,CAAC,oCAAoC;IAE5F,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;QACpC,MAAM,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;QAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,EAAE,GAAG,QAAQ,KAAK,CAAC,CAAC;QAClD,IAAI,UAAU,CAAC,OAAO,CAAC;YAAE,OAAO,aAAa,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;QACpF,QAAQ,KAAK,oBAAoB,CAAC,SAAS,CAAC,CAAC;QAC7C,OAAO,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC;IACxC,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;uFAGuF;AACvF,SAAS,oBAAoB,CAAC,SAAiB;IAC7C,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAyB,CAAC;IAClD,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,QAAQ,CAAC;IAC5C,MAAM,KAAK,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC7E,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,GAAG,GAAG,aAAa,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;QACvE,KAAK,MAAM,UAAU,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;YACpC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC;gBAAE,QAAQ,CAAC,GAAG,CAAC,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;QACtE,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1 @@
1
+ export declare const VIEWER_HTML: string;
@@ -0,0 +1,5 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // GENERATED from src/render/viewer.html by scripts/generate-viewer-html.ts — do not edit.
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<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 </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\"></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\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}], from this load's <metadata>\n let rowLabels = null; // {x0, x1} | null, the swimlane label column's range\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 }\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 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 let x0 = rowLabels ? Math.min(rowLabels.x0, slice.x0) : slice.x0;\n let x1 = slice.x1;\n x0 = Math.max(natural.x, x0 - PAD);\n x1 = Math.min(natural.x + natural.w, x1 + PAD);\n return frameRect(x0, natural.y, Math.max(1, x1 - x0), natural.h);\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 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\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 stage.addEventListener(\"pointerdown\", function (e) {\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 stage.addEventListener(\"wheel\", function (e) {\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 (!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\" && reviewMode) { exitReview(); }\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
+ //# sourceMappingURL=viewerHtml.js.map
@@ -0,0 +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,0l2BAA0l2B,CAAC"}
@@ -1,2 +1,12 @@
1
1
  import chokidar from "chokidar";
2
+ /**
3
+ * Wrap an async build fn so overlapping triggers never run it concurrently.
4
+ * A trigger that lands mid-build marks it dirty and returns; the running call
5
+ * re-runs the fn exactly once after it finishes, however many triggers piled
6
+ * up — a boolean, not a queue, because every build reads the current file
7
+ * state, so one trailing run always reflects the latest save. A rejection
8
+ * propagates to the first caller and drops any pending re-run; `em watch`'s
9
+ * build catches its own errors, so in practice the chain never breaks.
10
+ */
11
+ export declare function serializeBuilds(fn: () => void | Promise<void>): () => Promise<void>;
2
12
  export declare function watchFile(paths: string | string[], onChange: () => void | Promise<void>, debounceMs?: number): chokidar.FSWatcher;
@@ -1,6 +1,35 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // Debounced file watcher for `em watch`.
3
3
  import chokidar from "chokidar";
4
+ /**
5
+ * Wrap an async build fn so overlapping triggers never run it concurrently.
6
+ * A trigger that lands mid-build marks it dirty and returns; the running call
7
+ * re-runs the fn exactly once after it finishes, however many triggers piled
8
+ * up — a boolean, not a queue, because every build reads the current file
9
+ * state, so one trailing run always reflects the latest save. A rejection
10
+ * propagates to the first caller and drops any pending re-run; `em watch`'s
11
+ * build catches its own errors, so in practice the chain never breaks.
12
+ */
13
+ export function serializeBuilds(fn) {
14
+ let running = false;
15
+ let dirty = false;
16
+ return async () => {
17
+ if (running) {
18
+ dirty = true;
19
+ return;
20
+ }
21
+ running = true;
22
+ try {
23
+ do {
24
+ dirty = false;
25
+ await fn();
26
+ } while (dirty);
27
+ }
28
+ finally {
29
+ running = false;
30
+ }
31
+ };
32
+ }
4
33
  export function watchFile(paths, onChange, debounceMs = 80) {
5
34
  // A glob path (e.g. a slices/*.md pattern) picks up files created after the
6
35
  // watcher starts, not just ones that already exist — needed so authoring a
@@ -1 +1 @@
1
- {"version":3,"file":"watch.js","sourceRoot":"","sources":["../../src/render/watch.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,yCAAyC;AAEzC,OAAO,QAAQ,MAAM,UAAU,CAAC;AAEhC,MAAM,UAAU,SAAS,CACvB,KAAwB,EACxB,QAAoC,EACpC,UAAU,GAAG,EAAE;IAEf,4EAA4E;IAC5E,2EAA2E;IAC3E,mEAAmE;IACnE,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/D,IAAI,KAAK,GAA0B,IAAI,CAAC;IACxC,MAAM,OAAO,GAAG,GAAG,EAAE;QACnB,IAAI,KAAK;YAAE,YAAY,CAAC,KAAK,CAAC,CAAC;QAC/B,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YACtB,KAAK,QAAQ,EAAE,CAAC;QAClB,CAAC,EAAE,UAAU,CAAC,CAAC;IACjB,CAAC,CAAC;IACF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC9B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAC3B,OAAO,OAAO,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"watch.js","sourceRoot":"","sources":["../../src/render/watch.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,yCAAyC;AAEzC,OAAO,QAAQ,MAAM,UAAU,CAAC;AAEhC;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,EAA8B;IAC5D,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,IAAI,KAAK,GAAG,KAAK,CAAC;IAClB,OAAO,KAAK,IAAI,EAAE;QAChB,IAAI,OAAO,EAAE,CAAC;YACZ,KAAK,GAAG,IAAI,CAAC;YACb,OAAO;QACT,CAAC;QACD,OAAO,GAAG,IAAI,CAAC;QACf,IAAI,CAAC;YACH,GAAG,CAAC;gBACF,KAAK,GAAG,KAAK,CAAC;gBACd,MAAM,EAAE,EAAE,CAAC;YACb,CAAC,QAAQ,KAAK,EAAE;QAClB,CAAC;gBAAS,CAAC;YACT,OAAO,GAAG,KAAK,CAAC;QAClB,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,SAAS,CACvB,KAAwB,EACxB,QAAoC,EACpC,UAAU,GAAG,EAAE;IAEf,4EAA4E;IAC5E,2EAA2E;IAC3E,mEAAmE;IACnE,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/D,IAAI,KAAK,GAA0B,IAAI,CAAC;IACxC,MAAM,OAAO,GAAG,GAAG,EAAE;QACnB,IAAI,KAAK;YAAE,YAAY,CAAC,KAAK,CAAC,CAAC;QAC/B,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YACtB,KAAK,QAAQ,EAAE,CAAC;QAClB,CAAC,EAAE,UAAU,CAAC,CAAC;IACjB,CAAC,CAAC;IACF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC9B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAC3B,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -1 +1,20 @@
1
1
  export declare const STARTER_EM = "model \"Order Fulfillment\"\n\npersona Customer\npersona Manager\n\ncontext Order\ncontext Payment\n\n# --- Command pattern: UI -> command -> event ---\nslice \"Browse Catalog\" {\n ui Product Catalog @Customer\n command Place Order\n event Order Placed @Order\n}\n\n# --- View pattern: event -> read model -> UI ---\nslice \"View Open Orders\" {\n view Open Orders from \"Order Placed\"\n ui Order List @Customer\n}\n\nslice \"Checkout\" {\n ui Checkout Screen @Customer\n command Submit Payment\n event Payment Requested @Payment\n}\n\n# --- View pattern: a human view of pending payments ---\nslice \"Manager Review\" {\n view Pending Payments from \"Payment Requested\"\n ui Payment Dashboard @Manager\n}\n\n# --- Automation slice: only the read model it reads + the processor ---\nslice \"Payments To Process\" {\n view Payments To Process from \"Payment Requested\"\n processor Payment Gateway\n}\n\n# --- Command slice: the command the automation triggers + its event ---\nslice \"Capture Payment\" {\n command Capture Payment\n event Payment Captured @Payment\n}\n\nslice \"Show Receipt\" {\n view Receipts from \"Payment Captured\"\n ui Receipt Screen @Customer\n}\n";
2
+ /** Raw copy of templates/model-readme.md, \`{{...}}\` placeholders intact — see scaffoldReadme(). */
3
+ export declare const MODEL_README_TEMPLATE = "# {{Model Name}}\n\n{{One-paragraph description of the business process(es) this event model covers.}}\n\n## Live view\nWhile modeling, run the live view so the team can watch the diagram update:\n\n```bash\nem watch {{model-name}}.em -o {{model-name}}.svg --serve # re-render + instant push-reload\n# then open the URL it prints (http://localhost:5173/?svg={{model-name}}.svg) and share the screen\n```\n\nPan/zoom to navigate the diagram (drag, scroll; **Fit** resets), and click **Review mode** in\nthe header for a slice-by-slice walkthrough. If a save fails to render, the viewer keeps the\nlast good diagram and shows an error banner until the next successful load.\n\nStatic render: `em render {{model-name}}.em -o {{model-name}}.svg`\n\n## Patterns legend\n- **State Change** \u2014 UI \u2192 Command \u2192 Event\n- **State View** \u2014 Event(s) \u2192 Read Model \u2192 UI\n- **Automation** \u2014 Read Model (slice before) \u2192 Processor + Command \u2192 Event, together\n- **Translation** \u2014 External input (or Read Model, slice before) \u2192 Translation + Command \u2192 Event, together\n\nBetween them these are the only legal connections: `ui \u2192 command`, `command \u2192 event`,\n`event \u2192 read model`, `read model \u2192 ui`, `read model \u2192 reaction`, `reaction \u2192 command`. A command\nnever reaches a read model directly \u2014 the event goes between them. Every slice is joined up at\nboth ends: something triggers each command (the screen it's issued from, or the reaction that\ntriggers it, also in this slice), and every event a command records is read by some read model \u2014 so each State\nChange slice is paired with the State View slice that projects its event. A read model repeated\nalong the timeline (`view X again`) shows the same projection at a later point; the instances are\nnever connected to one another.\n\n## Slices\n<!-- The canonical slice index \u2014 the ONE place slices are enumerated (the state file\n points here rather than keeping its own copy). Generated \u2014 run\n `em slice index {{model-name}}.em` to (re)write the table below from the model and its\n slice docs; never hand-edit between the markers. -->\n<!-- GENERATED:slices:start -->\n| # | Slice | Pattern | Status | Implemented in | Design doc |\n|---|-------|---------|--------|----------------|------------|\n<!-- GENERATED:slices:end -->\n\n## Status\nSee [`.event-modeling.md`](.event-modeling.md) for current phase, decisions, and open questions.\n";
4
+ /** Raw copy of templates/state.md, \`{{...}}\` placeholders intact — see scaffoldStateFile(). */
5
+ export declare const STATE_TEMPLATE = "<!--\nResumable progress state for an event model. Stored as <model>/.event-modeling.md\nThe skill reads this on `/event-modeling` (no arg) to resume where you left off.\nKeep it current at the end of every working session.\n-->\n\n# Event Modeling Progress \u2014 {{Model Name}}\n\n- **Model file:** `{{model-name}}.em`\n- **Current phase:** {{discover | extract | model | slice | implement | conform | review | validate}}\n- **Current step:** {{1\u20137, see methodology; or extraction round R1\u2013R7}}\n- **Last updated:** {{YYYY-MM-DD}}\n- **Last conformance:** {{YYYY-MM-DD @ <target-repo revision> \u2014 report: conformance/<date>-report.md | never}}\n- **Last stakeholder review:** {{YYYY-MM-DD \u2014 attendees: see Participants | never}}\n\n## Session inputs\n- **Scope line:** {{one-line description of what's in/out of bounds for this model}}\n- **PRD / spec reference:** {{path, link, or \"none\"}}\n- **Headless/API model:** {{yes | no}}\n- **Source mode:** {{greenfield | extract-event-driven | extract-procedural}}\n- **Existing system refs:** {{repo paths, event-schema/topic locations, docs \u2014 or \"n/a\"}}\n\n## Participants\n<!-- Populate at session start. Live workshop: one human proxy relays questions to the room;\n attribute every answer/decision in the Decisions log to a named participant here. -->\n- {{Name}} \u2014 {{role}} \u2014 {{domain area}}\n\n## Extraction progress (existing-system models only \u2014 delete for greenfield)\n- [ ] R1. Candidate events (extracted/synthesized, filtered, confirmed)\n- [ ] R2. Timeline order (as-is narrative, actors/callers, first render)\n- [ ] R3. Commands / inputs\n- [ ] R4. Read models / outputs (validate from here)\n- [ ] R5. Boundaries & reactions (reaction shares its slice with the command it triggers)\n- [ ] R6. Gap & TBD reconciliation\n- [ ] R7. Convergence (render + validate clean, user confirmed as-is)\n\n## Steps completed\n- [ ] 1. Brainstorm events\n- [ ] 2. Plot / storyboard (personas + UI)\n- [ ] 3. Inputs (commands)\n- [ ] 4. Outputs (read models)\n- [ ] 5. Swimlanes & apply patterns\n- [ ] 6. Elaborate scenarios\n- [ ] 7. Evaluate completeness (`em validate` clean)\n\n## Decisions log\n<!-- Resolved choices, with the reasoning, so they aren't re-litigated. In a live workshop,\n attribute each entry to the participant who made the call (see Participants above). -->\n- {{YYYY-MM-DD}}: {{decision}} \u2014 {{why}} \u2014 by {{participant, if a live workshop}}\n\n## Usage log\n<!-- The team's only usage signal today (see docs/usage-data.md) \u2014 cheap and coarse on purpose.\n One line per session: phase(s) touched, and validate diagnostic *categories* hit (one of\n the exact fixed strings in docs/usage-data.md#categories, e.g. \"read model has no\n consumer\" \u2014 never the full instance message or any domain content). Append, never edit\n past entries. -->\n- {{YYYY-MM-DD}}: phases: {{discover | extract | model | slice | implement | conform | review | validate | watch, ...}} \u2014 validate: {{diagnostic category (docs/usage-data.md#categories), ... | none}}\n\n## Open questions / parking lot\n<!-- Unresolved items to bring back to the user. Never guess these. Metadata is optional and\n flat \u2014 add only what's known: source (ticket/conversation that spawned it), blocked on\n (who/what), revisit (when). -->\n- [ ] {{question}} \u2014 source: {{ticket/conversation link, optional}} \u2014 blocked on: {{who/what, optional}} \u2014 revisit: {{when, optional}}\n\n## Slice inventory\n<!-- Deliberately NOT a table: the canonical slice index (slice, pattern, status, doc link)\n lives in README.md's \"Slices\" section \u2014 one source of truth, updated there. This file\n only tracks resumable session state. -->\nSee `README.md` \u2192 **Slices** for the slice index and per-slice doc status.\n";
6
+ /** Same STARTER_EM content `em init` writes, titled from an arbitrary display name — used by
7
+ * `em scaffold`, which (unlike `em init`) knows a name to title the model with. */
8
+ export declare function starterEmFor(title: string): string;
9
+ /** Fill MODEL_README_TEMPLATE's Model Name / model-name placeholders from `em scaffold`'s
10
+ * arguments. The still-unknown one-paragraph description drops to a guidance comment rather
11
+ * than a fabricated sentence — never leaves \`{{...}}\` in the result. The GENERATED:slices
12
+ * marker block is untouched: it's already header-only in the template (MIL-98), so there's
13
+ * nothing to fill and no placeholder row to hand-write. */
14
+ export declare function scaffoldReadme(displayName: string, slugName: string): string;
15
+ /** Fill STATE_TEMPLATE's mechanical fields (model path, phase, step, dates) with real values.
16
+ * Every judgment section's placeholder bullet (Session inputs, Participants, Decisions log,
17
+ * Usage log, Open questions) is dropped rather than filled, leaving a real empty heading with
18
+ * the template's own guidance comment intact where it has one — per the skill's "don't guess,
19
+ * park it" principle, never a fabricated example. Never leaves \`{{...}}\` in the result. */
20
+ export declare function scaffoldStateFile(displayName: string, slugName: string, today: string): string;
package/dist/templates.js CHANGED
@@ -50,4 +50,173 @@ slice "Show Receipt" {
50
50
  ui Receipt Screen @Customer
51
51
  }
52
52
  `;
53
+ // ---- Templates for `em scaffold` (MIL-97 item 2) ----
54
+ //
55
+ // Embedded verbatim (backticks escaped) from the bundled skill's own templates/ directory,
56
+ // mirroring STARTER_EM above — the compiled CLI never reads .claude/skills/ at runtime to
57
+ // produce these, so `em scaffold` behaves identically whether em is run from a checkout or
58
+ // installed from npm. test/scaffoldTemplatesSync.test.ts asserts each constant below stays
59
+ // byte-identical to its source file in .claude/skills/event-modeling/templates/, so the two
60
+ // copies can't silently drift — regenerate by hand (or re-run the one-off script noted there)
61
+ // whenever a template file changes.
62
+ /** Raw copy of templates/model-readme.md, \`{{...}}\` placeholders intact — see scaffoldReadme(). */
63
+ export const MODEL_README_TEMPLATE = `# {{Model Name}}
64
+
65
+ {{One-paragraph description of the business process(es) this event model covers.}}
66
+
67
+ ## Live view
68
+ While modeling, run the live view so the team can watch the diagram update:
69
+
70
+ \`\`\`bash
71
+ em watch {{model-name}}.em -o {{model-name}}.svg --serve # re-render + instant push-reload
72
+ # then open the URL it prints (http://localhost:5173/?svg={{model-name}}.svg) and share the screen
73
+ \`\`\`
74
+
75
+ Pan/zoom to navigate the diagram (drag, scroll; **Fit** resets), and click **Review mode** in
76
+ the header for a slice-by-slice walkthrough. If a save fails to render, the viewer keeps the
77
+ last good diagram and shows an error banner until the next successful load.
78
+
79
+ Static render: \`em render {{model-name}}.em -o {{model-name}}.svg\`
80
+
81
+ ## Patterns legend
82
+ - **State Change** — UI → Command → Event
83
+ - **State View** — Event(s) → Read Model → UI
84
+ - **Automation** — Read Model (slice before) → Processor + Command → Event, together
85
+ - **Translation** — External input (or Read Model, slice before) → Translation + Command → Event, together
86
+
87
+ Between them these are the only legal connections: \`ui → command\`, \`command → event\`,
88
+ \`event → read model\`, \`read model → ui\`, \`read model → reaction\`, \`reaction → command\`. A command
89
+ never reaches a read model directly — the event goes between them. Every slice is joined up at
90
+ both ends: something triggers each command (the screen it's issued from, or the reaction that
91
+ triggers it, also in this slice), and every event a command records is read by some read model — so each State
92
+ Change slice is paired with the State View slice that projects its event. A read model repeated
93
+ along the timeline (\`view X again\`) shows the same projection at a later point; the instances are
94
+ never connected to one another.
95
+
96
+ ## Slices
97
+ <!-- The canonical slice index — the ONE place slices are enumerated (the state file
98
+ points here rather than keeping its own copy). Generated — run
99
+ \`em slice index {{model-name}}.em\` to (re)write the table below from the model and its
100
+ slice docs; never hand-edit between the markers. -->
101
+ <!-- GENERATED:slices:start -->
102
+ | # | Slice | Pattern | Status | Implemented in | Design doc |
103
+ |---|-------|---------|--------|----------------|------------|
104
+ <!-- GENERATED:slices:end -->
105
+
106
+ ## Status
107
+ See [\`.event-modeling.md\`](.event-modeling.md) for current phase, decisions, and open questions.
108
+ `;
109
+ /** Raw copy of templates/state.md, \`{{...}}\` placeholders intact — see scaffoldStateFile(). */
110
+ export const STATE_TEMPLATE = `<!--
111
+ Resumable progress state for an event model. Stored as <model>/.event-modeling.md
112
+ The skill reads this on \`/event-modeling\` (no arg) to resume where you left off.
113
+ Keep it current at the end of every working session.
114
+ -->
115
+
116
+ # Event Modeling Progress — {{Model Name}}
117
+
118
+ - **Model file:** \`{{model-name}}.em\`
119
+ - **Current phase:** {{discover | extract | model | slice | implement | conform | review | validate}}
120
+ - **Current step:** {{1–7, see methodology; or extraction round R1–R7}}
121
+ - **Last updated:** {{YYYY-MM-DD}}
122
+ - **Last conformance:** {{YYYY-MM-DD @ <target-repo revision> — report: conformance/<date>-report.md | never}}
123
+ - **Last stakeholder review:** {{YYYY-MM-DD — attendees: see Participants | never}}
124
+
125
+ ## Session inputs
126
+ - **Scope line:** {{one-line description of what's in/out of bounds for this model}}
127
+ - **PRD / spec reference:** {{path, link, or "none"}}
128
+ - **Headless/API model:** {{yes | no}}
129
+ - **Source mode:** {{greenfield | extract-event-driven | extract-procedural}}
130
+ - **Existing system refs:** {{repo paths, event-schema/topic locations, docs — or "n/a"}}
131
+
132
+ ## Participants
133
+ <!-- Populate at session start. Live workshop: one human proxy relays questions to the room;
134
+ attribute every answer/decision in the Decisions log to a named participant here. -->
135
+ - {{Name}} — {{role}} — {{domain area}}
136
+
137
+ ## Extraction progress (existing-system models only — delete for greenfield)
138
+ - [ ] R1. Candidate events (extracted/synthesized, filtered, confirmed)
139
+ - [ ] R2. Timeline order (as-is narrative, actors/callers, first render)
140
+ - [ ] R3. Commands / inputs
141
+ - [ ] R4. Read models / outputs (validate from here)
142
+ - [ ] R5. Boundaries & reactions (reaction shares its slice with the command it triggers)
143
+ - [ ] R6. Gap & TBD reconciliation
144
+ - [ ] R7. Convergence (render + validate clean, user confirmed as-is)
145
+
146
+ ## Steps completed
147
+ - [ ] 1. Brainstorm events
148
+ - [ ] 2. Plot / storyboard (personas + UI)
149
+ - [ ] 3. Inputs (commands)
150
+ - [ ] 4. Outputs (read models)
151
+ - [ ] 5. Swimlanes & apply patterns
152
+ - [ ] 6. Elaborate scenarios
153
+ - [ ] 7. Evaluate completeness (\`em validate\` clean)
154
+
155
+ ## Decisions log
156
+ <!-- Resolved choices, with the reasoning, so they aren't re-litigated. In a live workshop,
157
+ attribute each entry to the participant who made the call (see Participants above). -->
158
+ - {{YYYY-MM-DD}}: {{decision}} — {{why}} — by {{participant, if a live workshop}}
159
+
160
+ ## Usage log
161
+ <!-- The team's only usage signal today (see docs/usage-data.md) — cheap and coarse on purpose.
162
+ One line per session: phase(s) touched, and validate diagnostic *categories* hit (one of
163
+ the exact fixed strings in docs/usage-data.md#categories, e.g. "read model has no
164
+ consumer" — never the full instance message or any domain content). Append, never edit
165
+ past entries. -->
166
+ - {{YYYY-MM-DD}}: phases: {{discover | extract | model | slice | implement | conform | review | validate | watch, ...}} — validate: {{diagnostic category (docs/usage-data.md#categories), ... | none}}
167
+
168
+ ## Open questions / parking lot
169
+ <!-- Unresolved items to bring back to the user. Never guess these. Metadata is optional and
170
+ flat — add only what's known: source (ticket/conversation that spawned it), blocked on
171
+ (who/what), revisit (when). -->
172
+ - [ ] {{question}} — source: {{ticket/conversation link, optional}} — blocked on: {{who/what, optional}} — revisit: {{when, optional}}
173
+
174
+ ## Slice inventory
175
+ <!-- Deliberately NOT a table: the canonical slice index (slice, pattern, status, doc link)
176
+ lives in README.md's "Slices" section — one source of truth, updated there. This file
177
+ only tracks resumable session state. -->
178
+ See \`README.md\` → **Slices** for the slice index and per-slice doc status.
179
+ `;
180
+ /** Same STARTER_EM content `em init` writes, titled from an arbitrary display name — used by
181
+ * `em scaffold`, which (unlike `em init`) knows a name to title the model with. */
182
+ export function starterEmFor(title) {
183
+ // Replacer function, not a string pattern — a string replacement would let `$&`/`$$`/etc.
184
+ // in `title` expand against the match instead of being inserted literally (the CLI layer
185
+ // separately rejects a `"` in the name, since that would break the .em string literal here).
186
+ return STARTER_EM.replace('model "Order Fulfillment"', () => `model "${title}"`);
187
+ }
188
+ /** Fill MODEL_README_TEMPLATE's Model Name / model-name placeholders from `em scaffold`'s
189
+ * arguments. The still-unknown one-paragraph description drops to a guidance comment rather
190
+ * than a fabricated sentence — never leaves \`{{...}}\` in the result. The GENERATED:slices
191
+ * marker block is untouched: it's already header-only in the template (MIL-98), so there's
192
+ * nothing to fill and no placeholder row to hand-write. */
193
+ export function scaffoldReadme(displayName, slugName) {
194
+ // Replacer functions, not string patterns — a string replacement would let `$&`/`$$`/etc. in
195
+ // displayName expand against the match instead of being inserted literally.
196
+ return MODEL_README_TEMPLATE.replace(/\{\{Model Name\}\}/g, () => displayName)
197
+ .replace(/\{\{model-name\}\}/g, () => slugName)
198
+ .replace("{{One-paragraph description of the business process(es) this event model covers.}}", "<!-- One-paragraph description of the business process(es) this event model covers. -->");
199
+ }
200
+ /** Fill STATE_TEMPLATE's mechanical fields (model path, phase, step, dates) with real values.
201
+ * Every judgment section's placeholder bullet (Session inputs, Participants, Decisions log,
202
+ * Usage log, Open questions) is dropped rather than filled, leaving a real empty heading with
203
+ * the template's own guidance comment intact where it has one — per the skill's "don't guess,
204
+ * park it" principle, never a fabricated example. Never leaves \`{{...}}\` in the result. */
205
+ export function scaffoldStateFile(displayName, slugName, today) {
206
+ // Replacer functions for the display-name/slug fills, not string patterns — a string
207
+ // replacement would let `$&`/`$$`/etc. in the value expand against the match instead of
208
+ // being inserted literally.
209
+ const filled = STATE_TEMPLATE.replace("{{Model Name}}", () => displayName)
210
+ .replace("{{model-name}}", () => slugName)
211
+ .replace("{{discover | extract | model | slice | implement | conform | review | validate}}", "discover")
212
+ .replace("{{1\u20137, see methodology; or extraction round R1\u2013R7}}", "1")
213
+ .replace("{{YYYY-MM-DD}}", today) // "Last updated" — the first occurrence; later ones belong to
214
+ // judgment-section placeholder bullets and are dropped whole below.
215
+ .replace("{{YYYY-MM-DD @ <target-repo revision> \u2014 report: conformance/<date>-report.md | never}}", "never")
216
+ .replace("{{YYYY-MM-DD \u2014 attendees: see Participants | never}}", "never");
217
+ return filled
218
+ .split("\n")
219
+ .filter((line) => !line.includes("{{"))
220
+ .join("\n");
221
+ }
53
222
  //# sourceMappingURL=templates.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"templates.js","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,mCAAmC;AAEnC,MAAM,CAAC,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiDzB,CAAC"}
1
+ {"version":3,"file":"templates.js","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,mCAAmC;AAEnC,MAAM,CAAC,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiDzB,CAAC;AAEF,wDAAwD;AACxD,EAAE;AACF,2FAA2F;AAC3F,0FAA0F;AAC1F,2FAA2F;AAC3F,2FAA2F;AAC3F,4FAA4F;AAC5F,8FAA8F;AAC9F,oCAAoC;AAEpC,qGAAqG;AACrG,MAAM,CAAC,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6CpC,CAAC;AAEF,iGAAiG;AACjG,MAAM,CAAC,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqE7B,CAAC;AAEF;oFACoF;AACpF,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,0FAA0F;IAC1F,yFAAyF;IACzF,6FAA6F;IAC7F,OAAO,UAAU,CAAC,OAAO,CAAC,2BAA2B,EAAE,GAAG,EAAE,CAAC,UAAU,KAAK,GAAG,CAAC,CAAC;AACnF,CAAC;AAED;;;;4DAI4D;AAC5D,MAAM,UAAU,cAAc,CAAC,WAAmB,EAAE,QAAgB;IAClE,6FAA6F;IAC7F,4EAA4E;IAC5E,OAAO,qBAAqB,CAAC,OAAO,CAAC,qBAAqB,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC;SAC3E,OAAO,CAAC,qBAAqB,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC;SAC9C,OAAO,CACN,oFAAoF,EACpF,yFAAyF,CAC1F,CAAC;AACN,CAAC;AAED;;;;8FAI8F;AAC9F,MAAM,UAAU,iBAAiB,CAAC,WAAmB,EAAE,QAAgB,EAAE,KAAa;IACpF,qFAAqF;IACrF,wFAAwF;IACxF,4BAA4B;IAC5B,MAAM,MAAM,GAAG,cAAc,CAAC,OAAO,CAAC,gBAAgB,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC;SACvE,OAAO,CAAC,gBAAgB,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC;SACzC,OAAO,CAAC,kFAAkF,EAAE,UAAU,CAAC;SACvG,OAAO,CAAC,+DAA+D,EAAE,GAAG,CAAC;SAC7E,OAAO,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAC,8DAA8D;QAChG,oEAAoE;SACnE,OAAO,CAAC,6FAA6F,EAAE,OAAO,CAAC;SAC/G,OAAO,CAAC,2DAA2D,EAAE,OAAO,CAAC,CAAC;IAEjF,OAAO,MAAM;SACV,KAAK,CAAC,IAAI,CAAC;SACX,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;SACtC,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC"}
@@ -0,0 +1,13 @@
1
+ /** Captures the exact start/end marker lines separately from whatever sits between them, so
2
+ * replacement never depends on how much whitespace happened to separate them before — matters
3
+ * for a freshly-added, still-empty marker pair (start line immediately followed by end line,
4
+ * nothing in between to non-greedily match a leading "\n" against). */
5
+ export declare function markerRegex(name: string): RegExp;
6
+ /**
7
+ * Replace the body between the named marker pair in `content` with `newBody`, preserving the
8
+ * marker lines themselves verbatim. Returns `null` (never throws) when the marker pair isn't
9
+ * found in `content` — callers decide how to report that: a dev-tooling throw
10
+ * (scripts/generate-skill-docs.ts), or a user-facing CLI error suggesting how to add the
11
+ * markers (`em slice index`, src/cli/sliceIndex.ts).
12
+ */
13
+ export declare function applyMarker(content: string, markerName: string, newBody: string): string | null;
@@ -0,0 +1,30 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Shared marker-delimited region patcher: `<!-- GENERATED:<name>:start -->...
3
+ // <!-- GENERATED:<name>:end -->`. Originally scripts/generate-skill-docs.ts's own local helper
4
+ // (MIL-92, docs-generation only); extracted here so a real runtime command — `em slice index`
5
+ // (MIL-98) — can reuse the exact same marker convention instead of re-deriving its own regex.
6
+ // Pure string in, string (or null) out — no fs access, so both a dev script and a CLI command
7
+ // can layer their own file-reading and error reporting on top.
8
+ /** Captures the exact start/end marker lines separately from whatever sits between them, so
9
+ * replacement never depends on how much whitespace happened to separate them before — matters
10
+ * for a freshly-added, still-empty marker pair (start line immediately followed by end line,
11
+ * nothing in between to non-greedily match a leading "\n" against). */
12
+ export function markerRegex(name) {
13
+ const start = `<!-- GENERATED:${name}:start[^\\n]*-->`;
14
+ const end = `<!-- GENERATED:${name}:end -->`;
15
+ return new RegExp(`(${start})([\\s\\S]*?)(${end})`);
16
+ }
17
+ /**
18
+ * Replace the body between the named marker pair in `content` with `newBody`, preserving the
19
+ * marker lines themselves verbatim. Returns `null` (never throws) when the marker pair isn't
20
+ * found in `content` — callers decide how to report that: a dev-tooling throw
21
+ * (scripts/generate-skill-docs.ts), or a user-facing CLI error suggesting how to add the
22
+ * markers (`em slice index`, src/cli/sliceIndex.ts).
23
+ */
24
+ export function applyMarker(content, markerName, newBody) {
25
+ const re = markerRegex(markerName);
26
+ if (!re.test(content))
27
+ return null;
28
+ return content.replace(re, (_m, open, _old, close) => `${open}\n${newBody}\n${close}`);
29
+ }
30
+ //# sourceMappingURL=markers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markers.js","sourceRoot":"","sources":["../../src/util/markers.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,8EAA8E;AAC9E,+FAA+F;AAC/F,8FAA8F;AAC9F,8FAA8F;AAC9F,8FAA8F;AAC9F,+DAA+D;AAE/D;;;wEAGwE;AACxE,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,MAAM,KAAK,GAAG,kBAAkB,IAAI,kBAAkB,CAAC;IACvD,MAAM,GAAG,GAAG,kBAAkB,IAAI,UAAU,CAAC;IAC7C,OAAO,IAAI,MAAM,CAAC,IAAI,KAAK,iBAAiB,GAAG,GAAG,CAAC,CAAC;AACtD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,OAAe,EAAE,UAAkB,EAAE,OAAe;IAC9E,MAAM,EAAE,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;IACnC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,IAAI,KAAK,OAAO,KAAK,KAAK,EAAE,CAAC,CAAC;AACzF,CAAC"}
@@ -1,4 +1,13 @@
1
+ export interface WalkDirOptions {
2
+ /** When given, called with each directory entry's bare name before descending into it; a
3
+ * `true` return prunes the whole subtree (never read via `readdirSync`, never descended) —
4
+ * cheaper and safer than walking everything and filtering the flat result list afterward
5
+ * (skips permission errors / huge trees under a pruned dir too, e.g. `node_modules`). Omitted
6
+ * entirely by every existing caller (skillSync.ts/skillCheck.ts), which still walk the whole
7
+ * tree with no behavior change. */
8
+ skipDir?: (name: string) => boolean;
9
+ }
1
10
  /** Every regular file under `dir`, recursively, as paths relative to `dir` — sorted, POSIX
2
11
  * forward slashes regardless of platform (so hashes/change-reports are stable and testable
3
12
  * across OSes). Symlinks and other non-file/non-directory entries are skipped. */
4
- export declare function walkDir(dir: string): string[];
13
+ export declare function walkDir(dir: string, opts?: WalkDirOptions): string[];
@@ -2,7 +2,7 @@
2
2
  // Recursive file-tree listing shared by `em skill sync`/`em skill check` (src/cli/skillSync.ts,
3
3
  // src/cli/skillCheck.ts) to enumerate every file in the bundled skill directory — not just the
4
4
  // `.md` subset test/skillExamples.test.ts's skillFiles() walks for doctesting, which deliberately
5
- // skips non-`.md` files like templates/live.html. Sync/check must cover the whole tree.
5
+ // skips any non-`.md` file the skill may bundle. Sync/check must cover the whole tree.
6
6
  //
7
7
  // Manual walk rather than `readdirSync(dir, { recursive: true })` — that overload is Node
8
8
  // 20.1+ only; package.json declares `"engines": { "node": ">=18" }`.
@@ -11,15 +11,19 @@ import { join, relative, sep } from "node:path";
11
11
  /** Every regular file under `dir`, recursively, as paths relative to `dir` — sorted, POSIX
12
12
  * forward slashes regardless of platform (so hashes/change-reports are stable and testable
13
13
  * across OSes). Symlinks and other non-file/non-directory entries are skipped. */
14
- export function walkDir(dir) {
14
+ export function walkDir(dir, opts) {
15
15
  const out = [];
16
+ const skipDir = opts?.skipDir;
16
17
  function recurse(current) {
17
18
  for (const entry of readdirSync(current, { withFileTypes: true })) {
18
- const full = join(current, entry.name);
19
- if (entry.isDirectory())
20
- recurse(full);
21
- else if (entry.isFile())
22
- out.push(relative(dir, full).split(sep).join("/"));
19
+ if (entry.isDirectory()) {
20
+ if (skipDir?.(entry.name))
21
+ continue;
22
+ recurse(join(current, entry.name));
23
+ }
24
+ else if (entry.isFile()) {
25
+ out.push(relative(dir, join(current, entry.name)).split(sep).join("/"));
26
+ }
23
27
  }
24
28
  }
25
29
  recurse(dir);
@@ -1 +1 @@
1
- {"version":3,"file":"walkDir.js","sourceRoot":"","sources":["../../src/util/walkDir.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,gGAAgG;AAChG,+FAA+F;AAC/F,kGAAkG;AAClG,wFAAwF;AACxF,EAAE;AACF,0FAA0F;AAC1F,qEAAqE;AAErE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACtC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAEhD;;mFAEmF;AACnF,MAAM,UAAU,OAAO,CAAC,GAAW;IACjC,MAAM,GAAG,GAAa,EAAE,CAAC;IAEzB,SAAS,OAAO,CAAC,OAAe;QAC9B,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,OAAO,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;YAClE,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;YACvC,IAAI,KAAK,CAAC,WAAW,EAAE;gBAAE,OAAO,CAAC,IAAI,CAAC,CAAC;iBAClC,IAAI,KAAK,CAAC,MAAM,EAAE;gBAAE,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QAC9E,CAAC;IACH,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,CAAC;IACb,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC;AACpB,CAAC"}
1
+ {"version":3,"file":"walkDir.js","sourceRoot":"","sources":["../../src/util/walkDir.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,gGAAgG;AAChG,+FAA+F;AAC/F,kGAAkG;AAClG,uFAAuF;AACvF,EAAE;AACF,0FAA0F;AAC1F,qEAAqE;AAErE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACtC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAYhD;;mFAEmF;AACnF,MAAM,UAAU,OAAO,CAAC,GAAW,EAAE,IAAqB;IACxD,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,MAAM,OAAO,GAAG,IAAI,EAAE,OAAO,CAAC;IAE9B,SAAS,OAAO,CAAC,OAAe;QAC9B,KAAK,MAAM,KAAK,IAAI,WAAW,CAAC,OAAO,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;YAClE,IAAI,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;gBACxB,IAAI,OAAO,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC;oBAAE,SAAS;gBACpC,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;YACrC,CAAC;iBAAM,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;gBAC1B,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;YAC1E,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,CAAC;IACb,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC;AACpB,CAAC"}
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@milehimikey/em",
3
- "version": "1.7.2",
3
+ "version": "1.8.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": {
7
- "em": "./dist/cli.js"
7
+ "em": "./dist/cli.js",
8
+ "em-mcp": "./dist/mcp/main.js"
8
9
  },
9
10
  "main": "./dist/cli.js",
10
11
  "files": [
@@ -21,6 +22,8 @@
21
22
  "test": "vitest run",
22
23
  "test:watch": "vitest",
23
24
  "docs:generate": "tsx scripts/generate-skill-docs.ts",
25
+ "viewer:generate": "tsx scripts/generate-viewer-html.ts",
26
+ "viewer:check": "tsx scripts/generate-viewer-html.ts --check",
24
27
  "docs:check": "tsx scripts/generate-skill-docs.ts --check",
25
28
  "check:skill-version": "tsx scripts/check-skill-version-stamp.ts",
26
29
  "prepublishOnly": "npm run build && npm run typecheck && npm test"
@@ -53,13 +56,19 @@
53
56
  },
54
57
  "dependencies": {
55
58
  "@hpcc-js/wasm-graphviz": "^1.22.0",
59
+ "@modelcontextprotocol/sdk": "^1.30.0",
56
60
  "@resvg/resvg-js": "^2.6.2",
57
61
  "chokidar": "^3.6.0",
58
62
  "commander": "^12.1.0",
59
- "marked": "^18.0.0"
63
+ "marked": "^18.0.0",
64
+ "pdfkit": "^0.19.1",
65
+ "svg-to-pdfkit": "^0.1.8",
66
+ "zod": "^4.4.3"
60
67
  },
61
68
  "devDependencies": {
62
69
  "@types/node": "^22.0.0",
70
+ "@types/pdfkit": "^0.17.6",
71
+ "jsdom": "^26.1.0",
63
72
  "tsx": "^4.19.0",
64
73
  "typescript": "^5.6.0",
65
74
  "vitest": "^4.1.10"