@zenuml/core 3.47.1 → 3.47.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.
- package/.agents/skills/babysit-pr/SKILL.md +223 -0
- package/.agents/skills/babysit-pr/agents/openai.yaml +7 -0
- package/.agents/skills/dia-scoring/SKILL.md +139 -0
- package/.agents/skills/dia-scoring/agents/openai.yaml +7 -0
- package/.agents/skills/dia-scoring/references/selectors-and-keys.md +253 -0
- package/.agents/skills/land-pr/SKILL.md +120 -0
- package/.agents/skills/propagate-core-release/SKILL.md +205 -0
- package/.agents/skills/propagate-core-release/agents/openai.yaml +7 -0
- package/.agents/skills/propagate-core-release/references/downstreams.md +42 -0
- package/.agents/skills/ship-branch/SKILL.md +105 -0
- package/.agents/skills/submit-branch/SKILL.md +76 -0
- package/.agents/skills/validate-branch/SKILL.md +72 -0
- package/.claude/skills/emoji-eval/SKILL.md +187 -0
- package/.claude/skills/propagate-core-release/SKILL.md +81 -76
- package/.claude/skills/propagate-core-release/agents/openai.yaml +2 -2
- package/AGENTS.md +1 -1
- package/dist/stats.html +1 -1
- package/dist/zenuml.esm.mjs +16092 -15337
- package/dist/zenuml.js +540 -535
- package/docs/superpowers/plans/2026-03-30-emoji-support.md +1220 -0
- package/docs/superpowers/plans/2026-03-30-self-correcting-scoring.md +206 -0
- package/e2e/data/compare-cases.js +233 -0
- package/e2e/tools/compare-case.html +16 -2
- package/package.json +3 -3
- package/playwright.config.ts +1 -1
- package/scripts/analyze-compare-case/collect-data.mjs +139 -16
- package/scripts/analyze-compare-case/config.mjs +1 -1
- package/scripts/analyze-compare-case/report.mjs +3 -0
- package/scripts/analyze-compare-case/residual-scopes.mjs +23 -1
- package/scripts/analyze-compare-case/scoring.mjs +1 -0
|
@@ -414,6 +414,21 @@ export async function collectLabelData(page) {
|
|
|
414
414
|
}
|
|
415
415
|
|
|
416
416
|
for (const fragmentEl of root.querySelectorAll("g.fragment")) {
|
|
417
|
+
// Detect direct text.fragment-section-label children (e.g. [else] rendered without a <g> wrapper)
|
|
418
|
+
for (const directLabel of fragmentEl.querySelectorAll(":scope > text.fragment-section-label")) {
|
|
419
|
+
const text = (directLabel.textContent ?? "").trim();
|
|
420
|
+
if (!text) continue;
|
|
421
|
+
const measured = measureTextEntry(directLabel, rootRect);
|
|
422
|
+
labels.push({
|
|
423
|
+
side: "svg",
|
|
424
|
+
kind: text.startsWith("[") ? "fragment-condition" : "fragment-section",
|
|
425
|
+
text,
|
|
426
|
+
ownerText: textOrEmpty(fragmentEl, ":scope > text.fragment-label") || null,
|
|
427
|
+
box: measured.box,
|
|
428
|
+
font: measured.font,
|
|
429
|
+
letters: measured.letters,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
417
432
|
for (const groupEl of fragmentEl.querySelectorAll(":scope > g")) {
|
|
418
433
|
const conditionTextEls = Array.from(groupEl.querySelectorAll(":scope > text.fragment-condition"));
|
|
419
434
|
if (conditionTextEls.length > 0) {
|
|
@@ -461,18 +476,23 @@ export async function collectLabelData(page) {
|
|
|
461
476
|
|
|
462
477
|
const rowEl = participantEl.querySelector(":scope > .flex.items-center.justify-center, :scope > div:last-child");
|
|
463
478
|
const firstChild = rowEl?.firstElementChild ?? null;
|
|
464
|
-
|
|
479
|
+
// Find emoji span (span.mr-1.flex-shrink-0 containing emoji text)
|
|
480
|
+
const emojiSpan = participantEl.querySelector("span.mr-1.flex-shrink-0, span[data-testid='participant-emoji']");
|
|
481
|
+
const emojiText = emojiSpan ? emojiSpan.textContent.trim() : null;
|
|
482
|
+
// Find type icon div (div with aria-description or h-6.w-6 containing an SVG icon)
|
|
483
|
+
const typeIconDiv = firstChild && (
|
|
465
484
|
firstChild.matches("[aria-description]") ||
|
|
466
485
|
firstChild.querySelector("svg") ||
|
|
467
486
|
/\bh-6\b/.test(firstChild.className || "")
|
|
468
|
-
)
|
|
469
|
-
|
|
470
|
-
: null;
|
|
487
|
+
) ? firstChild : null;
|
|
488
|
+
const iconHost = typeIconDiv || emojiSpan || null;
|
|
471
489
|
const labelEl = Array.from(participantEl.querySelectorAll(".name")).at(-1) ?? null;
|
|
472
490
|
const measuredLabel = labelEl ? measureTextEntry(labelEl, rootRect) : null;
|
|
473
491
|
const stereotypeEl = participantEl.querySelector("label.interface");
|
|
474
492
|
const measuredStereotype = stereotypeEl ? measureTextEntry(stereotypeEl, rootRect) : null;
|
|
475
|
-
|
|
493
|
+
// For icon measurement: prefer type icon (so we compare type icon vs type icon across renderers).
|
|
494
|
+
// Fall back to emoji span for emoji-only participants.
|
|
495
|
+
const iconPaintRoot = typeIconDiv ? (typeIconDiv.querySelector("svg") ?? typeIconDiv) : (emojiSpan || null);
|
|
476
496
|
const participantStyle = getComputedStyle(participantEl);
|
|
477
497
|
|
|
478
498
|
participants.push({
|
|
@@ -488,6 +508,7 @@ export async function collectLabelData(page) {
|
|
|
488
508
|
stereotypeFont: measuredStereotype?.font ?? null,
|
|
489
509
|
stereotypeLetters: measuredStereotype?.letters ?? [],
|
|
490
510
|
iconBox: paintedBox(iconPaintRoot, rootRect),
|
|
511
|
+
emojiText: emojiText || null,
|
|
491
512
|
anchorKind: measuredLabel?.box ? "label" : "participant-box",
|
|
492
513
|
anchorBox: measuredLabel?.box ?? participantBox,
|
|
493
514
|
backgroundColor: normalizeColorValue(participantStyle.backgroundColor),
|
|
@@ -518,7 +539,20 @@ export async function collectLabelData(page) {
|
|
|
518
539
|
.sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top)[0]
|
|
519
540
|
|| null;
|
|
520
541
|
const measuredStereotype = stereotypeEl ? measureTextEntry(stereotypeEl, rootRect) : null;
|
|
521
|
-
const iconEl = participantEl.querySelector(":scope > g[transform]");
|
|
542
|
+
const iconEl = participantEl.querySelector(":scope > g.participant-icon[transform]");
|
|
543
|
+
// Detect emoji: separate text.participant-emoji element (present for both emoji-only AND icon+emoji participants)
|
|
544
|
+
// or first tspan in participant-label containing emoji codepoints (legacy inline-tspan format)
|
|
545
|
+
const emojiPattern = /[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{FE00}-\u{FE0F}\u{200D}]/u;
|
|
546
|
+
const emojiTextEl = participantEl.querySelector(":scope > text.participant-emoji") ?? null;
|
|
547
|
+
const emojiTspan = !emojiTextEl && labelEl
|
|
548
|
+
? Array.from(labelEl.querySelectorAll("tspan")).find((ts) => emojiPattern.test(ts.textContent))
|
|
549
|
+
: null;
|
|
550
|
+
const svgEmojiText = (emojiTextEl || emojiTspan)
|
|
551
|
+
? (emojiTextEl || emojiTspan).textContent.trim()
|
|
552
|
+
: null;
|
|
553
|
+
// For icon measurement: prefer type icon so we compare type icon vs type icon across renderers.
|
|
554
|
+
// Fall back to emoji element for emoji-only participants.
|
|
555
|
+
const iconTarget = iconEl || emojiTextEl || emojiTspan;
|
|
522
556
|
const participantBoxStyle = participantBoxEl ? getComputedStyle(participantBoxEl) : null;
|
|
523
557
|
|
|
524
558
|
participants.push({
|
|
@@ -533,7 +567,8 @@ export async function collectLabelData(page) {
|
|
|
533
567
|
stereotypeBox: measuredStereotype?.box ?? null,
|
|
534
568
|
stereotypeFont: measuredStereotype?.font ?? null,
|
|
535
569
|
stereotypeLetters: measuredStereotype?.letters ?? [],
|
|
536
|
-
iconBox: paintedBox(
|
|
570
|
+
iconBox: paintedBox(iconTarget, rootRect),
|
|
571
|
+
emojiText: svgEmojiText || null,
|
|
537
572
|
anchorKind: measuredLabel?.box ? "label" : "participant-box",
|
|
538
573
|
anchorBox: measuredLabel?.box ?? participantBox,
|
|
539
574
|
backgroundColor: normalizeColorValue(participantBoxStyle?.fill || participantBoxEl?.getAttribute("fill")),
|
|
@@ -583,20 +618,36 @@ export async function collectLabelData(page) {
|
|
|
583
618
|
}
|
|
584
619
|
|
|
585
620
|
function collectHtmlGroups(root, rootRect) {
|
|
586
|
-
|
|
621
|
+
// Group containers appear twice: once in participant layer (has name label, no overlay)
|
|
622
|
+
// and once in lifeline layer (has overlay rect, no name label).
|
|
623
|
+
// Collect name data from participant-layer containers and outline boxes from
|
|
624
|
+
// lifeline-layer containers, then merge by index order.
|
|
625
|
+
const nameEntries = [];
|
|
626
|
+
const boxEntries = [];
|
|
587
627
|
for (const groupEl of root.querySelectorAll(".lifeline-group-container")) {
|
|
588
628
|
const nameEl = groupEl.querySelector(".text-skin-lifeline-group-name");
|
|
589
|
-
const
|
|
590
|
-
|
|
629
|
+
const outlineRect = groupEl.querySelector("[data-group-overlay] rect");
|
|
630
|
+
if (nameEl) {
|
|
631
|
+
nameEntries.push({
|
|
632
|
+
name: textContentNormalized(nameEl),
|
|
633
|
+
measuredName: measureTextEntry(nameEl, rootRect),
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
if (outlineRect) {
|
|
637
|
+
boxEntries.push(boxOrNull(relRect(outlineRect.getBoundingClientRect(), rootRect)));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
const groups = [];
|
|
641
|
+
for (let i = 0; i < nameEntries.length; i++) {
|
|
642
|
+
const box = boxEntries[i] || null;
|
|
591
643
|
if (!box) continue;
|
|
592
|
-
const measuredName = nameEl ? measureTextEntry(nameEl, rootRect) : null;
|
|
593
644
|
groups.push({
|
|
594
645
|
side: "html",
|
|
595
|
-
name,
|
|
646
|
+
name: nameEntries[i].name,
|
|
596
647
|
box,
|
|
597
|
-
nameBox: measuredName?.box ?? null,
|
|
598
|
-
nameFont: measuredName?.font ?? null,
|
|
599
|
-
nameLetters: measuredName?.letters ?? [],
|
|
648
|
+
nameBox: nameEntries[i].measuredName?.box ?? null,
|
|
649
|
+
nameFont: nameEntries[i].measuredName?.font ?? null,
|
|
650
|
+
nameLetters: nameEntries[i].measuredName?.letters ?? [],
|
|
600
651
|
});
|
|
601
652
|
}
|
|
602
653
|
return groups;
|
|
@@ -607,7 +658,10 @@ export async function collectLabelData(page) {
|
|
|
607
658
|
for (const groupEl of root.querySelectorAll("g.participant-group")) {
|
|
608
659
|
const nameEl = groupEl.querySelector(":scope > text");
|
|
609
660
|
const name = textContentNormalized(nameEl);
|
|
610
|
-
|
|
661
|
+
// Measure the outline <rect> directly — consistent with HTML side measurement.
|
|
662
|
+
const outlineRect = groupEl.querySelector("rect.group-outline");
|
|
663
|
+
const measureEl = outlineRect || groupEl;
|
|
664
|
+
const box = boxOrNull(relRect(measureEl.getBoundingClientRect(), rootRect));
|
|
611
665
|
if (!box) continue;
|
|
612
666
|
const measuredName = nameEl ? measureTextEntry(nameEl, rootRect) : null;
|
|
613
667
|
groups.push({
|
|
@@ -966,6 +1020,73 @@ export async function collectLabelData(page) {
|
|
|
966
1020
|
return dividers;
|
|
967
1021
|
}
|
|
968
1022
|
|
|
1023
|
+
/**
|
|
1024
|
+
* Critical fragment header color calibration.
|
|
1025
|
+
* HTML renders .fragment-critical .header::before with border-bottom: 2px solid
|
|
1026
|
+
* (the unique thick header separator not present on other fragment types).
|
|
1027
|
+
* SVG renders no extra border for critical kind.
|
|
1028
|
+
* This collects the header bounding box + color style from both sides for comparison.
|
|
1029
|
+
*/
|
|
1030
|
+
function collectHtmlCriticalFragmentHeaders(root, rootRect) {
|
|
1031
|
+
const results = [];
|
|
1032
|
+
for (const frag of root.querySelectorAll(".fragment.fragment-critical")) {
|
|
1033
|
+
const header = frag.querySelector(":scope > .header");
|
|
1034
|
+
if (!header) continue;
|
|
1035
|
+
const r = header.getBoundingClientRect();
|
|
1036
|
+
const box = {
|
|
1037
|
+
x: Math.round(r.left - rootRect.left),
|
|
1038
|
+
y: Math.round(r.top - rootRect.top),
|
|
1039
|
+
w: Math.round(r.width),
|
|
1040
|
+
h: Math.round(r.height),
|
|
1041
|
+
};
|
|
1042
|
+
// ::before pseudo-element carries the border-bottom: 2px solid style
|
|
1043
|
+
const pseudoStyle = window.getComputedStyle(header, "::before");
|
|
1044
|
+
const borderBottomWidth = parseFloat(pseudoStyle.borderBottomWidth || "0") || 0;
|
|
1045
|
+
const borderBottomColor = pseudoStyle.borderBottomColor || "";
|
|
1046
|
+
results.push({
|
|
1047
|
+
side: "html",
|
|
1048
|
+
idx: results.length,
|
|
1049
|
+
box,
|
|
1050
|
+
headerBottomY: box.y + box.h,
|
|
1051
|
+
borderBottomWidth,
|
|
1052
|
+
borderBottomColor,
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
return results;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function collectSvgCriticalFragmentHeaders(root, rootRect) {
|
|
1059
|
+
const results = [];
|
|
1060
|
+
for (const frag of root.querySelectorAll("g.fragment.fragment-critical")) {
|
|
1061
|
+
const headerRect_el = frag.querySelector("rect.fragment-header");
|
|
1062
|
+
if (!headerRect_el) continue;
|
|
1063
|
+
const r = headerRect_el.getBoundingClientRect();
|
|
1064
|
+
const box = {
|
|
1065
|
+
x: Math.round(r.left - rootRect.left),
|
|
1066
|
+
y: Math.round(r.top - rootRect.top),
|
|
1067
|
+
w: Math.round(r.width),
|
|
1068
|
+
h: Math.round(r.height),
|
|
1069
|
+
};
|
|
1070
|
+
const headerBottomY = box.y + box.h;
|
|
1071
|
+
// Look for a line element at the header bottom edge (within 2px)
|
|
1072
|
+
const lines = Array.from(frag.querySelectorAll("line"));
|
|
1073
|
+
const borderLine = lines.find(l => {
|
|
1074
|
+
const lr = l.getBoundingClientRect();
|
|
1075
|
+
return Math.abs((lr.top - rootRect.top) - headerBottomY) < 3;
|
|
1076
|
+
});
|
|
1077
|
+
results.push({
|
|
1078
|
+
side: "svg",
|
|
1079
|
+
idx: results.length,
|
|
1080
|
+
box,
|
|
1081
|
+
headerBottomY,
|
|
1082
|
+
hasHeaderBottomLine: !!borderLine,
|
|
1083
|
+
headerBottomLineColor: borderLine ? (borderLine.getAttribute("stroke") || getComputedStyle(borderLine).stroke || "") : null,
|
|
1084
|
+
headerBottomLineWidth: borderLine ? (parseFloat(borderLine.getAttribute("stroke-width") || getComputedStyle(borderLine).strokeWidth || "0") || 0) : null,
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
return results;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
969
1090
|
const prepared = typeof window.prepareHtmlForCapture === "function"
|
|
970
1091
|
? window.prepareHtmlForCapture()
|
|
971
1092
|
: null;
|
|
@@ -1006,6 +1127,8 @@ export async function collectLabelData(page) {
|
|
|
1006
1127
|
svgFragmentDividers: collectSvgFragmentDividers(svgRoot, svgRootRect),
|
|
1007
1128
|
htmlDividers: collectHtmlDividers(htmlRoot, htmlRootRect),
|
|
1008
1129
|
svgDividers: collectSvgDividers(svgRoot, svgRootRect),
|
|
1130
|
+
htmlCriticalFragmentHeaders: collectHtmlCriticalFragmentHeaders(htmlRoot, htmlRootRect),
|
|
1131
|
+
svgCriticalFragmentHeaders: collectSvgCriticalFragmentHeaders(svgRoot, svgRootRect),
|
|
1009
1132
|
};
|
|
1010
1133
|
});
|
|
1011
1134
|
}
|
|
@@ -235,6 +235,22 @@ function buildDiffClusters(diffImage, targetClass) {
|
|
|
235
235
|
return clusters.sort((a, b) => b.size - a.size);
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
function normalizeClusterToFrameSpace(cluster, scaleX, scaleY) {
|
|
239
|
+
return {
|
|
240
|
+
...cluster,
|
|
241
|
+
bbox: {
|
|
242
|
+
x: cluster.bbox.x / scaleX,
|
|
243
|
+
y: cluster.bbox.y / scaleY,
|
|
244
|
+
w: cluster.bbox.w / scaleX,
|
|
245
|
+
h: cluster.bbox.h / scaleY,
|
|
246
|
+
},
|
|
247
|
+
centroid: {
|
|
248
|
+
x: cluster.centroid.x / scaleX,
|
|
249
|
+
y: cluster.centroid.y / scaleY,
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
238
254
|
function pickScopeTarget(cluster, items) {
|
|
239
255
|
const centroid = cluster.centroid;
|
|
240
256
|
let best = null;
|
|
@@ -287,10 +303,16 @@ function formatResidualScopeSummary(scope) {
|
|
|
287
303
|
export function buildResidualScopes(extracted, diffImage) {
|
|
288
304
|
const htmlItems = buildScopeItems("html", extracted);
|
|
289
305
|
const svgItems = buildScopeItems("svg", extracted);
|
|
306
|
+
const frameWidth = extracted.htmlRootBox?.w || extracted.svgRootBox?.w || diffImage.width;
|
|
307
|
+
const frameHeight = extracted.htmlRootBox?.h || extracted.svgRootBox?.h || diffImage.height;
|
|
308
|
+
const scaleX = frameWidth > 0 ? diffImage.width / frameWidth : 1;
|
|
309
|
+
const scaleY = frameHeight > 0 ? diffImage.height / frameHeight : 1;
|
|
290
310
|
const clusters = [
|
|
291
311
|
...buildDiffClusters(diffImage, 2),
|
|
292
312
|
...buildDiffClusters(diffImage, 3),
|
|
293
|
-
]
|
|
313
|
+
]
|
|
314
|
+
.map((cluster) => normalizeClusterToFrameSpace(cluster, scaleX, scaleY))
|
|
315
|
+
.sort((a, b) => b.size - a.size);
|
|
294
316
|
|
|
295
317
|
const residualScopes = clusters.map((cluster, index) => ({
|
|
296
318
|
rank: index + 1,
|
|
@@ -376,6 +376,7 @@ function scoreParticipantIcon(htmlParticipant, svgParticipant, diffImage) {
|
|
|
376
376
|
const participant = {
|
|
377
377
|
name: base?.name ?? "",
|
|
378
378
|
label_text: htmlParticipant?.labelText || svgParticipant?.labelText || null,
|
|
379
|
+
emoji: htmlParticipant?.emojiText || svgParticipant?.emojiText || null,
|
|
379
380
|
presence: {
|
|
380
381
|
html: iconPresentHtml,
|
|
381
382
|
svg: iconPresentSvg,
|