@zenuml/core 3.46.0 → 3.46.2

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 (60) hide show
  1. package/.claude/skills/dia-scoring/SKILL.md +139 -0
  2. package/.claude/skills/dia-scoring/agents/openai.yaml +7 -0
  3. package/.claude/skills/dia-scoring/references/selectors-and-keys.md +253 -0
  4. package/.claude/skills/land-pr/SKILL.md +98 -0
  5. package/.claude/skills/ship-branch/SKILL.md +81 -0
  6. package/.claude/skills/submit-branch/SKILL.md +76 -0
  7. package/.claude/skills/validate-branch/SKILL.md +54 -0
  8. package/CLAUDE.md +1 -1
  9. package/bun.lock +25 -11
  10. package/cy/canonical-history.html +908 -0
  11. package/cy/compare-case.html +357 -0
  12. package/cy/compare-cases.js +824 -0
  13. package/cy/compare.html +35 -0
  14. package/cy/diff-algorithm.js +199 -0
  15. package/cy/element-report.html +705 -0
  16. package/cy/icons-test.html +29 -0
  17. package/cy/legacy-vs-html.html +291 -0
  18. package/cy/native-diff-ext/background.js +60 -0
  19. package/cy/native-diff-ext/bridge.js +26 -0
  20. package/cy/native-diff-ext/content.js +194 -0
  21. package/cy/parity-test.html +122 -0
  22. package/cy/return-in-nested-if.html +29 -0
  23. package/cy/svg-preview.html +56 -0
  24. package/cy/svg-test.html +21 -0
  25. package/cy/theme-default-test.html +28 -0
  26. package/dist/stats.html +1 -1
  27. package/dist/zenuml.esm.mjs +16352 -15223
  28. package/dist/zenuml.js +701 -575
  29. package/docs/ship-branch-skill-plan.md +134 -0
  30. package/docs/superpowers/plans/2026-03-23-svg-parity-features.md +283 -0
  31. package/index.html +568 -73
  32. package/package.json +15 -4
  33. package/scripts/analyze-compare-case/collect-data.mjs +991 -0
  34. package/scripts/analyze-compare-case/config.mjs +102 -0
  35. package/scripts/analyze-compare-case/geometry.mjs +101 -0
  36. package/scripts/analyze-compare-case/native-diff.mjs +224 -0
  37. package/scripts/analyze-compare-case/output.mjs +74 -0
  38. package/scripts/analyze-compare-case/panel-diff.mjs +114 -0
  39. package/scripts/analyze-compare-case/report.mjs +157 -0
  40. package/scripts/analyze-compare-case/residual-scopes.mjs +325 -0
  41. package/scripts/analyze-compare-case/scoring.mjs +816 -0
  42. package/scripts/analyze-compare-case.mjs +149 -0
  43. package/scripts/snapshot-dual.js +34 -34
  44. package/skills/dia-scoring/SKILL.md +129 -0
  45. package/skills/dia-scoring/agents/openai.yaml +7 -0
  46. package/skills/dia-scoring/references/selectors-and-keys.md +253 -0
  47. package/test-setup.ts +8 -0
  48. package/types/index.d.ts +56 -0
  49. package/vite.config.ts +4 -0
  50. package/dist/10029-icon-service-Function-Apps-ObflOLuF.js +0 -5
  51. package/dist/Res_AWS-Identity-Access-Management_IAM-Access-Analyzer_48-BPq60XMY.js +0 -11
  52. package/dist/Res_AWS-Lambda_Lambda-Function_48-Co38UB_2.js +0 -12
  53. package/dist/Res_Amazon-EC2_Instance_48-CRaqbNUl.js +0 -12
  54. package/dist/Res_Amazon-Simple-Notification-Service_Topic_48-q13mxUeM.js +0 -11
  55. package/dist/Res_Amazon-Simple-Queue-Service_Queue_48-D2-8gbFw.js +0 -11
  56. package/dist/Robustness_Diagram_Boundary-nYnmTPs8.js +0 -10
  57. package/dist/Robustness_Diagram_Control-DLNLoMxd.js +0 -11
  58. package/dist/Robustness_Diagram_Entity-Be3kcbIE.js +0 -11
  59. package/dist/actor-BMj_HFpo.js +0 -11
  60. package/dist/database-BKHQQWQK.js +0 -8
@@ -0,0 +1,357 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>Compare</title>
6
+ <style>
7
+ body { margin: 0; font-family: sans-serif; }
8
+ .compare-bar {
9
+ display: flex; align-items: center; gap: 12px;
10
+ padding: 8px 16px; background: #1e293b; color: white;
11
+ }
12
+ .compare-bar a { color: #93c5fd; text-decoration: none; font-size: 13px; }
13
+ .compare-bar a:hover { text-decoration: underline; }
14
+ .compare-bar h2 { margin: 0; font-size: 15px; font-weight: 500; }
15
+ .nav { display: flex; gap: 8px; margin-left: auto; align-items: center; }
16
+ .nav a { padding: 4px 10px; background: #334155; border-radius: 4px; font-size: 12px; }
17
+ .nav button { padding: 4px 12px; background: #7c3aed; border: none; border-radius: 4px; color: white; font-size: 12px; cursor: pointer; }
18
+ .nav button:hover { background: #6d28d9; }
19
+ .nav button:disabled { background: #64748b; cursor: wait; }
20
+ .nav .match-badge { font-size: 11px; color: #94a3b8; }
21
+ .container { display: flex; gap: 0; }
22
+ .container.stacked { flex-direction: column; }
23
+ .panel { flex: 1; overflow: auto; }
24
+ .panel > h3 { margin: 0; padding: 8px 16px; background: #334155; color: white; font-size: 13px; }
25
+ .panel-content { padding: 10px; background: white; }
26
+ .diff-panel { display: none; }
27
+ .diff-panel.visible { display: block; }
28
+ .diff-panel .panel-content canvas { max-width: 100%; }
29
+ #html-output .footer { display: none !important; }
30
+ #html-output .privacy { visibility: hidden !important; }
31
+ </style>
32
+ </head>
33
+ <body>
34
+ <div class="compare-bar">
35
+ <a href="/cy/compare.html">&larr; All cases</a>
36
+ <h2 id="case-name"></h2>
37
+ <div class="nav">
38
+ <a id="prev-link" href="#">&larr; Prev</a>
39
+ <a id="next-link" href="#">Next &rarr;</a>
40
+ <span class="match-badge" id="match-badge"></span>
41
+ </div>
42
+ </div>
43
+ <div class="container">
44
+ <div class="panel">
45
+ <h3>HTML/React Renderer (theme-default)</h3>
46
+ <div class="panel-content" id="html-output">
47
+ <div id="diagram"><pre class="zenuml" style="margin: 0"></pre></div>
48
+ </div>
49
+ </div>
50
+ <div class="panel">
51
+ <h3>Native SVG Renderer</h3>
52
+ <div class="panel-content" id="svg-output"></div>
53
+ </div>
54
+ <div class="panel diff-panel" id="diff-panel">
55
+ <h3>Pixel Diff</h3>
56
+ <div class="panel-content" id="diff-output"></div>
57
+ </div>
58
+ </div>
59
+ <script type="module">
60
+ import ZenUml from "/src/core.tsx";
61
+ import { renderToSvg } from "/src/svg/renderToSvg.ts";
62
+
63
+ import { CASES } from "./compare-cases.js";
64
+ import { diffImages, DEFAULTS } from "./diff-algorithm.js";
65
+
66
+ const caseNames = Object.keys(CASES);
67
+ const params = new URLSearchParams(window.location.search);
68
+ const caseName = params.get("case") || caseNames[0];
69
+ const code = CASES[caseName];
70
+
71
+ // Update title and header
72
+ document.title = `Compare: ${caseName}`;
73
+ document.getElementById("case-name").textContent = caseName;
74
+
75
+ // Prev/Next navigation
76
+ const idx = caseNames.indexOf(caseName);
77
+ const prevLink = document.getElementById("prev-link");
78
+ const nextLink = document.getElementById("next-link");
79
+ if (idx > 0) {
80
+ prevLink.href = `?case=${encodeURIComponent(caseNames[idx - 1])}`;
81
+ } else {
82
+ prevLink.style.visibility = "hidden";
83
+ }
84
+ if (idx < caseNames.length - 1) {
85
+ nextLink.href = `?case=${encodeURIComponent(caseNames[idx + 1])}`;
86
+ } else {
87
+ nextLink.style.visibility = "hidden";
88
+ }
89
+
90
+ // Expose DSL for the extension's batch mode to read
91
+ window.__currentDSL = code || "";
92
+
93
+ if (code === undefined) {
94
+ const errP = document.createElement("p");
95
+ errP.style.color = "red";
96
+ errP.textContent = "Unknown case: " + caseName;
97
+ document.getElementById("svg-output").appendChild(errP);
98
+ } else {
99
+ // SVG side
100
+ const result = renderToSvg(code);
101
+ document.getElementById("svg-output").innerHTML = result.svg;
102
+
103
+ // HTML side — render with theme-default
104
+ const diagram = document.getElementById("diagram");
105
+ const pre = diagram.querySelector("pre.zenuml");
106
+ pre.textContent = code;
107
+ const zenUml = new ZenUml(pre);
108
+ await zenUml.render(code, { theme: "theme-default" });
109
+
110
+ // Hide footer in HTML output (SVG has no footer equivalent)
111
+ requestAnimationFrame(() => {
112
+ const footer = document.querySelector("#html-output .footer");
113
+ if (footer) footer.style.display = "none";
114
+
115
+ // Stack panels vertically when diagram is wider than half the viewport.
116
+ // This prevents overflow clipping in CDP screenshots for wide diagrams.
117
+ const frame = document.querySelector("#html-output .frame, #html-output .sequence-diagram");
118
+ const svgEl = document.querySelector("#svg-output > svg");
119
+ const maxW = Math.max(frame?.offsetWidth || 0, svgEl?.offsetWidth || 0);
120
+ if (maxW > window.innerWidth / 2 - 40) {
121
+ document.querySelector(".container").classList.add("stacked");
122
+ }
123
+ });
124
+ }
125
+
126
+ // ——— Element extraction for structural comparison ———
127
+
128
+ function extractHtmlElements(container) {
129
+ const elements = { participants: [], messages: [], selfCalls: [], creations: [], returns: [], fragments: [] };
130
+
131
+ // Participants
132
+ container.querySelectorAll("[data-participant-id]").forEach(el => {
133
+ const name = el.getAttribute("data-participant-id");
134
+ if (name) elements.participants.push(name);
135
+ });
136
+
137
+ // Messages (non-self, non-return, non-creation interactions)
138
+ container.querySelectorAll('.interaction:not(.return):not(.creation)').forEach(el => {
139
+ const isSelf = el.classList.contains("self-invocation") ||
140
+ el.querySelector(".self-invocation") !== null ||
141
+ el.classList.contains("self");
142
+ if (isSelf) return;
143
+ const sig = el.getAttribute("data-signature") || el.getAttribute("data-to") || "";
144
+ if (sig) elements.messages.push(sig);
145
+ });
146
+
147
+ // Self-calls
148
+ container.querySelectorAll('.self-invocation').forEach(el => {
149
+ const interaction = el.closest(".interaction");
150
+ const sig = interaction?.getAttribute("data-signature") || "";
151
+ if (sig) elements.selfCalls.push(sig);
152
+ });
153
+
154
+ // Creations
155
+ container.querySelectorAll('.interaction.creation').forEach(el => {
156
+ const sig = el.getAttribute("data-signature") || "";
157
+ if (sig) elements.creations.push(sig);
158
+ });
159
+
160
+ // Returns — match .interaction.return (some have data-type="return", some don't)
161
+ container.querySelectorAll('.interaction.return').forEach(el => {
162
+ const sig = el.getAttribute("data-signature") || "";
163
+ const msgDiv = el.querySelector(".message");
164
+ const label = sig || (msgDiv ? msgDiv.textContent.trim().replace(/[\d.]+$/, "").trim() : "");
165
+ if (label) elements.returns.push(label);
166
+ });
167
+
168
+ // Fragments
169
+ container.querySelectorAll(".fragment").forEach(el => {
170
+ const kind = [...el.classList].find(c => c.startsWith("fragment-"))?.replace("fragment-", "") ||
171
+ [...el.classList].find(c => ["alt", "loop", "opt", "par", "tcf", "critical", "section", "ref"].includes(c)) || "unknown";
172
+ elements.fragments.push(kind);
173
+ });
174
+
175
+ return elements;
176
+ }
177
+
178
+ function extractSvgElements(svgElement) {
179
+ const elements = { participants: [], messages: [], selfCalls: [], creations: [], returns: [], fragments: [] };
180
+ if (!svgElement) return elements;
181
+
182
+ // Participants
183
+ svgElement.querySelectorAll("g.participant, g.participant-starter").forEach(g => {
184
+ const name = g.getAttribute("data-participant");
185
+ if (name) elements.participants.push(name);
186
+ });
187
+
188
+ // Messages (non-self)
189
+ svgElement.querySelectorAll("g.message").forEach(g => {
190
+ if (g.classList.contains("self-call")) return;
191
+ const text = g.querySelector("text.message-label");
192
+ elements.messages.push(text ? text.textContent.trim() : "");
193
+ });
194
+
195
+ // Self-calls
196
+ svgElement.querySelectorAll("g.message.self-call").forEach(g => {
197
+ const text = g.querySelector("text.message-label");
198
+ elements.selfCalls.push(text ? text.textContent.trim() : "");
199
+ });
200
+
201
+ // Creations
202
+ svgElement.querySelectorAll("g.creation").forEach(g => {
203
+ const text = g.querySelector("text.message-label");
204
+ elements.creations.push(text ? text.textContent.trim() : "");
205
+ });
206
+
207
+ // Returns
208
+ svgElement.querySelectorAll("g.return").forEach(g => {
209
+ const text = g.querySelector("text.return-label");
210
+ elements.returns.push(text ? text.textContent.trim() : "");
211
+ });
212
+
213
+ // Fragments
214
+ svgElement.querySelectorAll("g.fragment, g[class*='fragment-']").forEach(g => {
215
+ const kind = ([...g.classList].find(c => c.startsWith("fragment-")) || "").replace("fragment-", "") || "unknown";
216
+ elements.fragments.push(kind);
217
+ });
218
+
219
+ return elements;
220
+ }
221
+
222
+ function computeStructuralMatch(htmlElements, svgElements) {
223
+ const types = ["participants", "messages", "selfCalls", "creations", "returns", "fragments"];
224
+ let totalScore = 0;
225
+ let scoredTypes = 0;
226
+ let totalElements = 0;
227
+ let matchedElements = 0;
228
+ const details = {};
229
+
230
+ for (const type of types) {
231
+ const htmlList = htmlElements[type] || [];
232
+ const svgList = svgElements[type] || [];
233
+ const maxLen = Math.max(htmlList.length, svgList.length);
234
+
235
+ if (maxLen === 0) continue; // skip types with no elements in either
236
+
237
+ // Count matches by order
238
+ let matched = 0;
239
+ const minLen = Math.min(htmlList.length, svgList.length);
240
+ for (let i = 0; i < minLen; i++) {
241
+ matched++; // count as matched if present in both at same index
242
+ }
243
+
244
+ const score = matched / maxLen;
245
+ totalScore += score;
246
+ scoredTypes++;
247
+ totalElements += maxLen;
248
+ matchedElements += matched;
249
+
250
+ details[type] = { html: htmlList.length, svg: svgList.length, matched, score };
251
+ }
252
+
253
+ return {
254
+ score: scoredTypes > 0 ? totalScore / scoredTypes : 1,
255
+ totalElements,
256
+ matchedElements,
257
+ details,
258
+ };
259
+ }
260
+
261
+ // --- Reusable capture/diff API (also exposed on window for external tools) ---
262
+
263
+ // State for hide/restore cycle
264
+ let _hiddenEls = [];
265
+ let _savedStyles = [];
266
+
267
+ function _hideEl(el) {
268
+ if (el) { _hiddenEls.push({ el, prev: el.style.display }); el.style.display = "none"; }
269
+ }
270
+ function _stripMargin(el) {
271
+ if (el) { _savedStyles.push({ el, prev: el.style.margin }); el.style.margin = "0"; }
272
+ }
273
+
274
+ // Prepare HTML panel for screenshot: hide chrome, numbering, icons
275
+ window.prepareHtmlForCapture = function() {
276
+ _hiddenEls = [];
277
+ _savedStyles = [];
278
+
279
+ const htmlEl = document.getElementById("html-output");
280
+ const htmlFrame = htmlEl.querySelector(".frame");
281
+ const seqDiagram = htmlEl.querySelector(".sequence-diagram");
282
+ const svgElement = document.getElementById("svg-output").querySelector("svg");
283
+
284
+ _hideEl(htmlEl.querySelector(".footer"));
285
+ _stripMargin(htmlFrame);
286
+
287
+ // Hide bottom participant labels (SVG no longer renders them)
288
+ const captureEl = htmlFrame || seqDiagram || htmlEl;
289
+ const captureRect = captureEl.getBoundingClientRect();
290
+ const captureMidY = captureRect.top + captureRect.height * 0.7;
291
+ captureEl.querySelectorAll(".participant").forEach(el => {
292
+ const r = el.getBoundingClientRect();
293
+ if (r.top > captureMidY) _hideEl(el);
294
+ });
295
+
296
+ if (htmlFrame) {
297
+ const frameRect2 = htmlFrame.getBoundingClientRect();
298
+ // Hide SVG icons in the header area (top 35px) — checkbox, etc.
299
+ htmlFrame.querySelectorAll("svg").forEach(svg => {
300
+ const r = svg.getBoundingClientRect();
301
+ if (r.width > 0 && (r.top - frameRect2.top) < 35) _hideEl(svg);
302
+ });
303
+ }
304
+
305
+ return { htmlSelector: "#html-output .frame, #html-output .sequence-diagram", svgSelector: "#svg-output svg" };
306
+ };
307
+
308
+ // Restore HTML panel after screenshot
309
+ window.restoreHtmlAfterCapture = function() {
310
+ for (const { el, prev } of _hiddenEls) el.style.display = prev;
311
+ for (const { el, prev } of _savedStyles) el.style.margin = prev;
312
+ _hiddenEls = [];
313
+ _savedStyles = [];
314
+ };
315
+
316
+ // Run pixel diff from two PNG data URLs and display results on the page.
317
+ // Returns { pixelPct, posPct, structPct, matched, total, htmlOnly, svgOnly, colorDiff }
318
+ window.diffFromImages = async function(htmlDataUrl, svgDataUrl) {
319
+ const htmlEl = document.getElementById("html-output");
320
+ const svgEl = document.getElementById("svg-output");
321
+ const seqDiagram = htmlEl.querySelector(".sequence-diagram");
322
+ const svgElement = svgEl.querySelector("svg");
323
+
324
+ // Structural comparison
325
+ const htmlElements = extractHtmlElements(seqDiagram || htmlEl);
326
+ const svgElements = extractSvgElements(svgElement);
327
+ const structural = computeStructuralMatch(htmlElements, svgElements);
328
+
329
+ // Allow URL param overrides for debugging
330
+ const urlParams = new URLSearchParams(window.location.search);
331
+ const opts = {
332
+ LUMA_THRESHOLD: parseInt(urlParams.get('luma') || DEFAULTS.LUMA_THRESHOLD, 10),
333
+ CHANNEL_TOLERANCE: parseInt(urlParams.get('ctol') || DEFAULTS.CHANNEL_TOLERANCE, 10),
334
+ POSITION_TOLERANCE: parseInt(urlParams.get('ptol') || DEFAULTS.POSITION_TOLERANCE, 10),
335
+ };
336
+
337
+ const { canvas: diffCanvas, stats, badgeHtml } = await diffImages(htmlDataUrl, svgDataUrl, opts);
338
+
339
+ const diffOutput = document.getElementById("diff-output");
340
+ diffOutput.innerHTML = "";
341
+ diffOutput.appendChild(diffCanvas);
342
+
343
+ const panel = document.getElementById("diff-panel");
344
+ panel.classList.add("visible");
345
+
346
+ const structPct = (structural.score * 100).toFixed(0);
347
+ document.getElementById("match-badge").innerHTML =
348
+ `${structPct}% structural · ${badgeHtml}`;
349
+
350
+ return { pixelPct: stats.pixelPct, posPct: stats.posPct, structPct: parseFloat(structPct), ...stats };
351
+ };
352
+
353
+
354
+
355
+ </script>
356
+ </body>
357
+ </html>