@zenuml/core 3.47.8 → 3.48.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 (204) hide show
  1. package/dist/cloud-icons-eHuugVSv.js.map +1 -0
  2. package/dist/zenuml.esm.mjs +2153 -2156
  3. package/dist/zenuml.esm.mjs.map +1 -0
  4. package/dist/zenuml.js +82 -82
  5. package/dist/zenuml.js.map +1 -0
  6. package/package.json +11 -1
  7. package/src/cli/zenuml.ts +1164 -0
  8. package/.agents/skills/babysit-pr/SKILL.md +0 -223
  9. package/.agents/skills/babysit-pr/agents/openai.yaml +0 -7
  10. package/.agents/skills/dia-scoring/SKILL.md +0 -139
  11. package/.agents/skills/dia-scoring/agents/openai.yaml +0 -7
  12. package/.agents/skills/dia-scoring/references/selectors-and-keys.md +0 -253
  13. package/.agents/skills/land-pr/SKILL.md +0 -120
  14. package/.agents/skills/propagate-core-release/SKILL.md +0 -205
  15. package/.agents/skills/propagate-core-release/agents/openai.yaml +0 -7
  16. package/.agents/skills/propagate-core-release/references/downstreams.md +0 -42
  17. package/.agents/skills/ship-branch/SKILL.md +0 -105
  18. package/.agents/skills/submit-branch/SKILL.md +0 -76
  19. package/.agents/skills/validate-branch/SKILL.md +0 -72
  20. package/.claude/commands/README.md +0 -162
  21. package/.claude/commands/analyze.md +0 -101
  22. package/.claude/commands/clarify.md +0 -158
  23. package/.claude/commands/code-review.md +0 -322
  24. package/.claude/commands/constitution.md +0 -73
  25. package/.claude/commands/create-docs.md +0 -309
  26. package/.claude/commands/full-context.md +0 -121
  27. package/.claude/commands/gemini-consult.md +0 -164
  28. package/.claude/commands/handoff.md +0 -146
  29. package/.claude/commands/implement.md +0 -56
  30. package/.claude/commands/plan.md +0 -43
  31. package/.claude/commands/refactor.md +0 -188
  32. package/.claude/commands/specify.md +0 -21
  33. package/.claude/commands/tasks.md +0 -62
  34. package/.claude/commands/update-docs.md +0 -314
  35. package/.claude/hooks/README.md +0 -270
  36. package/.claude/hooks/config/sensitive-patterns.json +0 -86
  37. package/.claude/hooks/gemini-context-injector.sh +0 -129
  38. package/.claude/hooks/mcp-security-scan.sh +0 -147
  39. package/.claude/hooks/notify.sh +0 -103
  40. package/.claude/hooks/setup/hook-setup.md +0 -96
  41. package/.claude/hooks/setup/settings.json.template +0 -63
  42. package/.claude/hooks/sounds/complete.wav +0 -0
  43. package/.claude/hooks/sounds/input-needed.wav +0 -0
  44. package/.claude/hooks/subagent-context-injector.sh +0 -65
  45. package/.claude/skills/babysit-pr/SKILL.md +0 -223
  46. package/.claude/skills/babysit-pr/agents/openai.yaml +0 -7
  47. package/.claude/skills/dia-scoring/SKILL.md +0 -139
  48. package/.claude/skills/dia-scoring/agents/openai.yaml +0 -7
  49. package/.claude/skills/dia-scoring/references/selectors-and-keys.md +0 -253
  50. package/.claude/skills/emoji-eval/SKILL.md +0 -187
  51. package/.claude/skills/land-pr/SKILL.md +0 -120
  52. package/.claude/skills/propagate-core-release/SKILL.md +0 -205
  53. package/.claude/skills/propagate-core-release/agents/openai.yaml +0 -7
  54. package/.claude/skills/propagate-core-release/references/downstreams.md +0 -42
  55. package/.claude/skills/ship-branch/SKILL.md +0 -105
  56. package/.claude/skills/submit-branch/SKILL.md +0 -76
  57. package/.claude/skills/validate-branch/SKILL.md +0 -72
  58. package/.claude/skills/zenuml-ux-research/SKILL.md +0 -183
  59. package/.claude/skills/zenuml-ux-research/references/assertion-catalog.md +0 -261
  60. package/.claude/skills/zenuml-ux-research/references/best-practices-overview.md +0 -56
  61. package/.claude/skills/zenuml-ux-research/references/report-template.md +0 -89
  62. package/.claude/skills/zenuml-ux-research/references/scenarios/edit-message-label.md +0 -37
  63. package/.claude/skills/zenuml-ux-research/references/scenarios/insert-message.md +0 -36
  64. package/.claude/skills/zenuml-ux-research/references/scenarios/insert-participant.md +0 -31
  65. package/.claude/skills/zenuml-ux-research/references/scenarios/rename-participant.md +0 -33
  66. package/.claude/skills/zenuml-ux-research/references/scenarios/undo-insert.md +0 -35
  67. package/.devcontainer/devcontainer.json +0 -21
  68. package/.dockerignore +0 -19
  69. package/.eslintrc.js +0 -39
  70. package/.git-blame-ignore-revs +0 -6
  71. package/.kiro/hooks/README.md +0 -38
  72. package/.kiro/hooks/session-sound-notification.js +0 -44
  73. package/.kiro/hooks/session-sound-notification.json +0 -23
  74. package/.mcp.json.example +0 -17
  75. package/.nvmrc +0 -1
  76. package/.prettierignore +0 -4
  77. package/.prettierrc +0 -1
  78. package/.specify/memory/constitution.md +0 -33
  79. package/.specify/scripts/bash/check-prerequisites.sh +0 -166
  80. package/.specify/scripts/bash/common.sh +0 -113
  81. package/.specify/scripts/bash/create-new-feature.sh +0 -97
  82. package/.specify/scripts/bash/setup-plan.sh +0 -60
  83. package/.specify/scripts/bash/update-agent-context.sh +0 -728
  84. package/.specify/templates/agent-file-template.md +0 -23
  85. package/.specify/templates/plan-template.md +0 -219
  86. package/.specify/templates/spec-template.md +0 -116
  87. package/.specify/templates/tasks-template.md +0 -127
  88. package/.storybook/main.ts +0 -25
  89. package/.storybook/preview.ts +0 -29
  90. package/.watchmanconfig +0 -3
  91. package/AGENTS.md +0 -26
  92. package/CLAUDE.md +0 -124
  93. package/DEPLOYMENT.md +0 -62
  94. package/Dockerfile +0 -36
  95. package/IMPLEMENTATION_PLAN.md +0 -163
  96. package/Integration/vanilla-js/index.html +0 -42
  97. package/MCP-ASSISTANT-RULES.md +0 -85
  98. package/README_CN.md +0 -15
  99. package/TUTORIAL.md +0 -116
  100. package/antlr/antlr-4.11.1-complete.jar +0 -0
  101. package/bun.lock +0 -1544
  102. package/bunfig.toml +0 -52
  103. package/docs/UNICODE_SUPPORT.md +0 -179
  104. package/docs/ai-context/deployment-infrastructure.md +0 -21
  105. package/docs/ai-context/docs-overview.md +0 -89
  106. package/docs/ai-context/handoff.md +0 -174
  107. package/docs/ai-context/project-structure.md +0 -160
  108. package/docs/ai-context/system-integration.md +0 -21
  109. package/docs/asciidoc/contributor.adoc +0 -54
  110. package/docs/asciidoc/create-my-own-theme.adoc +0 -149
  111. package/docs/asciidoc/images/creation-component.png +0 -0
  112. package/docs/asciidoc/images/creation-rtl.png +0 -0
  113. package/docs/asciidoc/images/message-arrow-rtl.png +0 -0
  114. package/docs/asciidoc/images/occurrence.png +0 -0
  115. package/docs/asciidoc/images/return-message-conflict.png +0 -0
  116. package/docs/asciidoc/images/shift-up-half-the-height.png +0 -0
  117. package/docs/asciidoc/images/three-layer-info-arch.png +0 -0
  118. package/docs/asciidoc/images/vertical-alignment.svg +0 -1
  119. package/docs/asciidoc/images/vertically-aligning.png +0 -0
  120. package/docs/asciidoc/index.adoc +0 -277
  121. package/docs/asciidoc/theme-debug-web-app.png +0 -0
  122. package/docs/asciidoc/tutorial.adoc +0 -22
  123. package/docs/asciidoc/user-css.png +0 -0
  124. package/docs/async-vs-sync-parser-rules.md +0 -81
  125. package/docs/divider-parser-allow-spaces.md +0 -38
  126. package/docs/highlighting-messages.md +0 -52
  127. package/docs/images/editor-sample.png +0 -0
  128. package/docs/inherited-vs-provided-from.md +0 -64
  129. package/docs/parser/Assignment.md +0 -8
  130. package/docs/parser/PARSER_IMPROVEMENTS_CC.md +0 -425
  131. package/docs/parser/grammar_review_gemini.md +0 -116
  132. package/docs/participants-function.md +0 -25
  133. package/docs/responsive-participant-margin.md +0 -52
  134. package/docs/starter.md +0 -9
  135. package/docs/superpowers/plans/2026-03-27-e2e-test-reorg.md +0 -698
  136. package/docs/superpowers/plans/2026-03-30-emoji-support.md +0 -1220
  137. package/docs/superpowers/plans/2026-03-30-self-correcting-scoring.md +0 -206
  138. package/docs/superpowers/plans/2026-04-15-keyboard-editing-on-diagram.md +0 -1992
  139. package/docs/superpowers/plans/2026-04-15-zenuml-ux-research-skill.md +0 -1452
  140. package/docs/ux-research/.gitkeep +0 -0
  141. package/docs/ux-research/2026-04-15-rename-participant.md +0 -156
  142. package/docs/ux-research/2026-04-18-insert-participant.md +0 -151
  143. package/docs/width-translate-and-offsets.md +0 -62
  144. package/docs/xss.md +0 -59
  145. package/e2e/data/compare-cases.js +0 -1090
  146. package/e2e/data/diff-algorithm.js +0 -199
  147. package/e2e/fixtures/create-message.html +0 -26
  148. package/e2e/fixtures/editable-label.html +0 -35
  149. package/e2e/fixtures/editable-span.html +0 -122
  150. package/e2e/fixtures/empty-diagram.html +0 -23
  151. package/e2e/fixtures/fixture.html +0 -31
  152. package/e2e/fixtures/insert-participant.html +0 -23
  153. package/e2e/fixtures/reorder-cross-fragment.html +0 -31
  154. package/e2e/fixtures/reorder-fragment.html +0 -29
  155. package/e2e/fixtures/reorder-message.html +0 -27
  156. package/e2e/fixtures/svg-test.html +0 -21
  157. package/e2e/fixtures/type-switch.html +0 -29
  158. package/e2e/tools/canonical-history.html +0 -908
  159. package/e2e/tools/compare-case.html +0 -371
  160. package/e2e/tools/compare.html +0 -35
  161. package/e2e/tools/native-diff-ext/background.js +0 -60
  162. package/e2e/tools/native-diff-ext/bridge.js +0 -26
  163. package/e2e/tools/native-diff-ext/content.js +0 -194
  164. package/e2e/tools/svg-preview.html +0 -56
  165. package/embed.html +0 -193
  166. package/eslint.config.mjs +0 -35
  167. package/firebase-debug.log +0 -108
  168. package/iframe-container-demo/diagram.html +0 -124
  169. package/iframe-container-demo/host.html +0 -817
  170. package/index.html +0 -771
  171. package/mermaid-zenuml-async-spa-auth.png +0 -0
  172. package/mermaid-zenuml-async-spa-auth.snapshot.md +0 -96
  173. package/newsletter/unicode-support-announcement.md +0 -134
  174. package/playground/creation.html +0 -53
  175. package/playground/message.html +0 -63
  176. package/playwright.config.ts +0 -40
  177. package/renderer.html +0 -366
  178. package/scripts/analyze-compare-case/collect-data.mjs +0 -1134
  179. package/scripts/analyze-compare-case/config.mjs +0 -102
  180. package/scripts/analyze-compare-case/geometry.mjs +0 -101
  181. package/scripts/analyze-compare-case/native-diff.mjs +0 -224
  182. package/scripts/analyze-compare-case/output.mjs +0 -74
  183. package/scripts/analyze-compare-case/panel-diff.mjs +0 -114
  184. package/scripts/analyze-compare-case/report.mjs +0 -162
  185. package/scripts/analyze-compare-case/residual-scopes.mjs +0 -347
  186. package/scripts/analyze-compare-case/scoring.mjs +0 -829
  187. package/scripts/analyze-compare-case.mjs +0 -149
  188. package/scripts/bump-version.js +0 -117
  189. package/scripts/snapshot-dual.js +0 -173
  190. package/scripts/update-snapshots.js +0 -70
  191. package/skills/dia-scoring/SKILL.md +0 -129
  192. package/skills/dia-scoring/agents/openai.yaml +0 -7
  193. package/skills/dia-scoring/references/selectors-and-keys.md +0 -253
  194. package/tailwind.config.js +0 -126
  195. package/test-compression.html +0 -274
  196. package/test-mermaid-zenuml.html +0 -57
  197. package/test-setup.ts +0 -124
  198. package/test-url-params.html +0 -192
  199. package/tsconfig.app.json +0 -31
  200. package/tsconfig.node.json +0 -24
  201. package/tsconfig.test.json +0 -9
  202. package/vite.config.lib.ts +0 -93
  203. package/vite.config.ts +0 -84
  204. package/wrangler.toml +0 -18
@@ -1,162 +0,0 @@
1
- /*
2
- * What this file does:
3
- * Assembles the final report object from scored sections and residual attribution.
4
- *
5
- * High-level flow:
6
- * - Pulls in semantic scoring results and residual hotspot attribution.
7
- * - Builds the JSON report shape consumed by scripts and humans.
8
- * - Produces concise summary lines for labels, numbers, arrows, icons, boxes,
9
- * and residual scopes so the CLI can print a readable digest.
10
- *
11
- * This module is the boundary between raw analysis results and final report shape.
12
- *
13
- * Example input:
14
- * `caseName`, extracted geometry, and the analyzer's local diff image.
15
- *
16
- * Example output:
17
- * A single report object with JSON-friendly sections plus terminal summary arrays
18
- * such as `summary`, `arrow_summary`, and `residual_scope_summary`.
19
- */
20
- import { buildResidualScopes } from "./residual-scopes.mjs";
21
- import { buildScoredSections } from "./scoring.mjs";
22
-
23
- function formatLetterSummary(letter) {
24
- if (letter.dx == null || letter.dy == null) {
25
- return `${letter.grapheme}: ambiguous`;
26
- }
27
- const dx = letter.dx.toFixed(2);
28
- const dy = letter.dy.toFixed(2);
29
- return `${letter.grapheme}: dx=${dx}px dy=${dy}px`;
30
- }
31
-
32
- function formatSectionSummary(prefix, item) {
33
- const notes = [];
34
- if (item.owner_text) {
35
- notes.push(`owner=${item.owner_text}`);
36
- }
37
- if (item.html_text && item.svg_text && item.html_text !== item.svg_text) {
38
- notes.push(`text_mismatch(html=${item.html_text} svg=${item.svg_text})`);
39
- }
40
- const noteSuffix = notes.length > 0 ? ` [${notes.join("; ")}]` : "";
41
- if (!item.letters || item.letters.length === 0) {
42
- return `${prefix}:${item.key.kind}:${item.key.text}${noteSuffix} -> ambiguous`;
43
- }
44
- return `${prefix}:${item.key.kind}:${item.key.text}${noteSuffix} -> ${item.letters.map(formatLetterSummary).join(", ")}`;
45
- }
46
-
47
- function formatArrowSummary(arrow) {
48
- if (arrow.status !== "ok") {
49
- return "ambiguous";
50
- }
51
- const parts = [
52
- `left_dx=${arrow.left_dx.toFixed(2)}px`,
53
- `right_dx=${arrow.right_dx.toFixed(2)}px`,
54
- `width_dx=${arrow.width_dx.toFixed(2)}px`,
55
- ];
56
- if (arrow.key?.kind === "self") {
57
- parts.push(`top_dy=${arrow.top_dy.toFixed(2)}px`);
58
- parts.push(`bottom_dy=${arrow.bottom_dy.toFixed(2)}px`);
59
- parts.push(`height_dy=${arrow.height_dy.toFixed(2)}px`);
60
- }
61
- return parts.join(" ");
62
- }
63
-
64
- function formatParticipantIconSummary(icon) {
65
- const notes = [];
66
- if (icon.emoji) {
67
- notes.push(`emoji=${icon.emoji}`);
68
- }
69
- if (icon.label_text) {
70
- notes.push(`label=${icon.label_text}`);
71
- }
72
- if (icon.anchor_kind) {
73
- notes.push(`anchor=${icon.anchor_kind}`);
74
- }
75
- if (icon.presence && icon.presence.html !== icon.presence.svg) {
76
- notes.push(`presence_mismatch(html=${icon.presence.html} svg=${icon.presence.svg})`);
77
- }
78
- const noteSuffix = notes.length > 0 ? ` [${notes.join("; ")}]` : "";
79
- if (icon.status !== "ok") {
80
- return `icon:${icon.name}${noteSuffix} -> ambiguous`;
81
- }
82
- return `icon:${icon.name}${noteSuffix} -> icon_dx=${icon.icon_dx.toFixed(2)}px icon_dy=${icon.icon_dy.toFixed(2)}px relative_dx=${icon.relative_dx.toFixed(2)}px relative_dy=${icon.relative_dy.toFixed(2)}px`;
83
- }
84
-
85
- function formatParticipantBoxSummary(box) {
86
- if (box.status !== "ok") {
87
- return `participant-box:${box.name} -> ambiguous`;
88
- }
89
- return `participant-box:${box.name} -> dx=${box.dx.toFixed(2)}px dy=${box.dy.toFixed(2)}px dw=${box.dw.toFixed(2)}px dh=${box.dh.toFixed(2)}px`;
90
- }
91
-
92
- function formatParticipantColorSummary(color) {
93
- if (color.status !== "ok") {
94
- return `participant-color:${color.name} -> ambiguous`;
95
- }
96
- return `participant-color:${color.name} -> bg(html=${color.html_background_color} svg=${color.svg_background_color}) text(html=${color.html_text_color} svg=${color.svg_text_color}) stereotype(html=${color.html_stereotype_color} svg=${color.svg_stereotype_color})`;
97
- }
98
-
99
- function formatGroupSummary(group) {
100
- if (group.status !== "ok") {
101
- return `participant-group:${group.name} -> ambiguous`;
102
- }
103
- const namePart = group.name_dx === null || group.name_dy === null
104
- ? "name=ambiguous"
105
- : `name_dx=${group.name_dx.toFixed(2)}px name_dy=${group.name_dy.toFixed(2)}px`;
106
- return `participant-group:${group.name} -> dx=${group.dx.toFixed(2)}px dy=${group.dy.toFixed(2)}px dw=${group.dw.toFixed(2)}px dh=${group.dh.toFixed(2)}px ${namePart}`;
107
- }
108
-
109
- function formatOccurrenceSummary(occ) {
110
- if (occ.status !== "ok") {
111
- return `occurrence:${occ.participant}#${occ.idx} -> ambiguous`;
112
- }
113
- return `occurrence:${occ.participant}#${occ.idx} -> dx=${occ.dx.toFixed(2)}px dy=${occ.dy.toFixed(2)}px dw=${occ.dw.toFixed(2)}px dh=${occ.dh.toFixed(2)}px`;
114
- }
115
-
116
- function formatFragmentDividerSummary(div) {
117
- if (div.status !== "ok") {
118
- return `fragment-divider:#${div.idx} -> ambiguous`;
119
- }
120
- return `fragment-divider:#${div.idx} -> dx=${div.dx.toFixed(2)}px dy=${div.dy.toFixed(2)}px dw=${div.dw.toFixed(2)}px`;
121
- }
122
-
123
- export function buildReport(caseName, extracted, diffImage) {
124
- const sections = buildScoredSections(extracted, diffImage);
125
- const residualScopes = buildResidualScopes(extracted, diffImage);
126
-
127
- return {
128
- case: caseName,
129
- title: sections.title,
130
- labels: sections.labels,
131
- numbers: sections.numbers,
132
- arrows: sections.arrows,
133
- participant_labels: sections.participantLabels,
134
- participant_stereotypes: sections.participantStereotypes,
135
- participant_icons: sections.participantIcons,
136
- participant_boxes: sections.participantBoxes,
137
- participant_colors: sections.participantColors,
138
- comments: sections.comments,
139
- participant_groups: sections.groups,
140
- occurrences: sections.occurrences,
141
- fragment_dividers: sections.fragmentDividers,
142
- dividers: sections.dividers,
143
- residual_scopes: residualScopes.scopes,
144
- title_summary: sections.title ? formatSectionSummary("title", sections.title) : null,
145
- summary: sections.labels.map((label) => formatSectionSummary("label", label)),
146
- number_summary: sections.numbers.map((number) => formatSectionSummary("number", number)),
147
- arrow_summary: sections.arrows.map((arrow) => `arrow:${arrow.key.text} -> ${formatArrowSummary(arrow)}`),
148
- participant_label_summary: sections.participantLabels.map((label) => formatSectionSummary("participant-label", label)),
149
- participant_stereotype_summary: sections.participantStereotypes.map((label) => formatSectionSummary("participant-stereotype", label)),
150
- participant_icon_summary: sections.participantIcons.map((icon) => formatParticipantIconSummary(icon)),
151
- participant_box_summary: sections.participantBoxes.map((box) => formatParticipantBoxSummary(box)),
152
- participant_color_summary: sections.participantColors.map((color) => formatParticipantColorSummary(color)),
153
- comment_summary: sections.comments.map((comment) => formatSectionSummary("comment", comment)),
154
- participant_group_summary: sections.groups.map((group) => formatGroupSummary(group)),
155
- occurrence_summary: sections.occurrences.map((occ) => formatOccurrenceSummary(occ)),
156
- fragment_divider_summary: sections.fragmentDividers.map((div) => formatFragmentDividerSummary(div)),
157
- divider_summary: sections.dividers.map((d) => `divider:"${d.label}" dy=${d.dy ?? "?"} ${d.status}`),
158
- residual_scope_summary: residualScopes.summary,
159
- residual_scope_html_only_top: residualScopes.html_only_top,
160
- residual_scope_svg_only_top: residualScopes.svg_only_top,
161
- };
162
- }
@@ -1,347 +0,0 @@
1
- /*
2
- * What this file does:
3
- * Groups remaining diff pixels into residual hotspots and attributes them to nearby elements.
4
- *
5
- * High-level flow:
6
- * - Scans the analyzer's local diff for connected html-only and svg-only clusters.
7
- * - Builds candidate scope items from labels, arrows, participant headers, icons,
8
- * boxes, and frame-level containers.
9
- * - Picks the nearest and most specific target on each side for each cluster.
10
- * - Produces concise residual summaries for terminal and JSON output.
11
- *
12
- * This module explains "where the remaining diff seems to belong" after the
13
- * semantic scoring step.
14
- *
15
- * Example input:
16
- * Extracted geometry plus a diff image containing `html-only` and `svg-only` pixels.
17
- *
18
- * Example output:
19
- * `{ scopes, summary, html_only_top, svg_only_top }`
20
- * with each scope carrying a bbox, centroid, and nearest HTML/SVG targets.
21
- */
22
- import { area, intersectionArea, rectBottom, rectRight, round } from "./geometry.mjs";
23
-
24
- function scopePriority(category) {
25
- switch (category) {
26
- case "participant-icon":
27
- return 8;
28
- case "participant-stereotype":
29
- case "label":
30
- case "number":
31
- case "comment":
32
- case "participant-label":
33
- return 7;
34
- case "arrow":
35
- case "fragment-divider":
36
- return 6;
37
- case "occurrence":
38
- case "participant-box":
39
- case "participant-group":
40
- return 5;
41
- case "frame-border":
42
- return 2;
43
- case "diagram-root":
44
- return 1;
45
- default:
46
- return 0;
47
- }
48
- }
49
-
50
- function pointInRect(point, rect) {
51
- return point.x >= rect.x && point.x <= rectRight(rect) && point.y >= rect.y && point.y <= rectBottom(rect);
52
- }
53
-
54
- function distanceToRect(point, rect) {
55
- const dx = point.x < rect.x ? rect.x - point.x : point.x > rectRight(rect) ? point.x - rectRight(rect) : 0;
56
- const dy = point.y < rect.y ? rect.y - point.y : point.y > rectBottom(rect) ? point.y - rectBottom(rect) : 0;
57
- return Math.hypot(dx, dy);
58
- }
59
-
60
- function formatScopeName(item) {
61
- if (item.owner_text && item.text && item.owner_text !== item.text) {
62
- return `${item.owner_text}:${item.text}`;
63
- }
64
- return item.name ?? item.text ?? item.kind ?? item.category ?? "unknown";
65
- }
66
-
67
- function buildScopeItems(side, extracted) {
68
- const items = [];
69
-
70
- function push(category, name, box, extra = {}) {
71
- if (!box || box.w <= 0 || box.h <= 0) {
72
- return;
73
- }
74
- items.push({
75
- side,
76
- category,
77
- name,
78
- box,
79
- ...extra,
80
- });
81
- }
82
-
83
- const sideKey = side === "html" ? "html" : "svg";
84
- const labels = side === "html" ? extracted.htmlLabels : extracted.svgLabels;
85
- const numbers = side === "html" ? extracted.htmlNumbers : extracted.svgNumbers;
86
- const arrows = side === "html" ? extracted.htmlArrows : extracted.svgArrows;
87
- const participants = side === "html" ? extracted.htmlParticipants : extracted.svgParticipants;
88
- const comments = side === "html" ? extracted.htmlComments : extracted.svgComments;
89
- const groups = side === "html" ? extracted.htmlGroups : extracted.svgGroups;
90
- const occurrences = side === "html" ? (extracted.htmlOccurrences || []) : (extracted.svgOccurrences || []);
91
- const fragmentDividers = side === "html" ? (extracted.htmlFragmentDividers || []) : (extracted.svgFragmentDividers || []);
92
-
93
- for (const label of labels) {
94
- push("label", formatScopeName(label), label.box, {
95
- kind: label.kind,
96
- text: label.text,
97
- owner_text: label.ownerText ?? null,
98
- });
99
- }
100
- for (const number of numbers) {
101
- push("number", formatScopeName(number), number.box, {
102
- kind: number.kind,
103
- text: number.text,
104
- owner_text: number.ownerText ?? null,
105
- });
106
- }
107
- for (const arrow of arrows) {
108
- push("arrow", formatScopeName(arrow), arrow.box, {
109
- kind: arrow.kind,
110
- text: arrow.text,
111
- owner_text: arrow.labelText ?? null,
112
- });
113
- }
114
- for (const comment of comments) {
115
- push("comment", formatScopeName(comment), comment.box, {
116
- kind: comment.kind,
117
- text: comment.text,
118
- });
119
- }
120
- for (const participant of participants) {
121
- push("participant-box", participant.name, participant.participantBox, {
122
- kind: "participant",
123
- text: participant.labelText ?? participant.name,
124
- owner_text: participant.name,
125
- });
126
- push("participant-label", participant.name, participant.labelBox, {
127
- kind: "participant",
128
- text: participant.labelText ?? participant.name,
129
- owner_text: participant.name,
130
- });
131
- push("participant-stereotype", participant.name, participant.stereotypeBox, {
132
- kind: "participant",
133
- text: participant.stereotypeText ?? participant.name,
134
- owner_text: participant.name,
135
- });
136
- push("participant-icon", participant.name, participant.iconBox, {
137
- kind: "participant",
138
- text: participant.labelText ?? participant.name,
139
- owner_text: participant.name,
140
- });
141
- }
142
- for (const group of groups) {
143
- push("participant-group", group.name || "group", group.box, {
144
- kind: "group",
145
- text: group.name,
146
- });
147
- }
148
- for (const occ of occurrences) {
149
- push("occurrence", `${occ.participant}#${occ.idx}`, occ.box, {
150
- kind: "occurrence",
151
- text: occ.participant,
152
- owner_text: occ.participant,
153
- });
154
- }
155
- for (const div of fragmentDividers) {
156
- push("fragment-divider", `divider#${div.idx}`, { x: div.x, y: div.y, w: div.width, h: 1 }, {
157
- kind: "divider",
158
- text: div.label || `divider#${div.idx}`,
159
- });
160
- }
161
-
162
- if (sideKey === "html") {
163
- push("diagram-root", "html-root", extracted.htmlRootBox, { kind: "root" });
164
- } else {
165
- push("frame-border", "frame-border", extracted.svgFrameBorderBox, { kind: "frame" });
166
- push("diagram-root", "svg-root", extracted.svgRootBox, { kind: "root" });
167
- }
168
-
169
- return items;
170
- }
171
-
172
- function buildDiffClusters(diffImage, targetClass) {
173
- const visited = new Uint8Array(diffImage.width * diffImage.height);
174
- const clusters = [];
175
- const offsets = [-1, 0, 1, 0, -1];
176
-
177
- for (let index = 0; index < diffImage.classData.length; index++) {
178
- if (visited[index] || diffImage.classData[index] !== targetClass) {
179
- continue;
180
- }
181
- visited[index] = 1;
182
- const queue = [index];
183
- let head = 0;
184
- let size = 0;
185
- let sumX = 0;
186
- let sumY = 0;
187
- let left = diffImage.width;
188
- let top = diffImage.height;
189
- let right = -1;
190
- let bottom = -1;
191
-
192
- while (head < queue.length) {
193
- const current = queue[head++];
194
- const x = current % diffImage.width;
195
- const y = Math.floor(current / diffImage.width);
196
- size++;
197
- sumX += x + 0.5;
198
- sumY += y + 0.5;
199
- left = Math.min(left, x);
200
- top = Math.min(top, y);
201
- right = Math.max(right, x);
202
- bottom = Math.max(bottom, y);
203
-
204
- for (let dir = 0; dir < 4; dir++) {
205
- const nx = x + offsets[dir];
206
- const ny = y + offsets[dir + 1];
207
- if (nx < 0 || nx >= diffImage.width || ny < 0 || ny >= diffImage.height) {
208
- continue;
209
- }
210
- const nextIndex = ny * diffImage.width + nx;
211
- if (visited[nextIndex] || diffImage.classData[nextIndex] !== targetClass) {
212
- continue;
213
- }
214
- visited[nextIndex] = 1;
215
- queue.push(nextIndex);
216
- }
217
- }
218
-
219
- clusters.push({
220
- class: targetClass === 2 ? "html-only" : "svg-only",
221
- size,
222
- bbox: {
223
- x: left,
224
- y: top,
225
- w: right - left + 1,
226
- h: bottom - top + 1,
227
- },
228
- centroid: {
229
- x: sumX / size,
230
- y: sumY / size,
231
- },
232
- });
233
- }
234
-
235
- return clusters.sort((a, b) => b.size - a.size);
236
- }
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
-
254
- function pickScopeTarget(cluster, items) {
255
- const centroid = cluster.centroid;
256
- let best = null;
257
- let bestScore = -Infinity;
258
-
259
- for (const item of items) {
260
- const contains = pointInRect(centroid, item.box);
261
- const distance = distanceToRect(centroid, item.box);
262
- const overlap = intersectionArea(cluster.bbox, item.box);
263
- const score = (contains ? 10000 : 0)
264
- + overlap * 10
265
- - distance * 100
266
- + scopePriority(item.category) * 1000
267
- - area(item.box) * 0.01;
268
-
269
- if (score > bestScore) {
270
- bestScore = score;
271
- best = {
272
- category: item.category,
273
- name: item.name,
274
- kind: item.kind ?? null,
275
- text: item.text ?? null,
276
- owner_text: item.owner_text ?? null,
277
- contains_centroid: contains,
278
- overlap_area: round(overlap),
279
- distance: round(distance),
280
- box: {
281
- x: round(item.box.x),
282
- y: round(item.box.y),
283
- w: round(item.box.w),
284
- h: round(item.box.h),
285
- },
286
- };
287
- }
288
- }
289
-
290
- return best;
291
- }
292
-
293
- function formatResidualScopeSummary(scope) {
294
- const htmlTarget = scope.html_target
295
- ? `${scope.html_target.category}:${scope.html_target.name}`
296
- : "none";
297
- const svgTarget = scope.svg_target
298
- ? `${scope.svg_target.category}:${scope.svg_target.name}`
299
- : "none";
300
- return `${scope.class}:${scope.size}px @ (${scope.centroid.x.toFixed(1)},${scope.centroid.y.toFixed(1)}) -> html=${htmlTarget} svg=${svgTarget}`;
301
- }
302
-
303
- export function buildResidualScopes(extracted, diffImage) {
304
- const htmlItems = buildScopeItems("html", extracted);
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;
310
- const clusters = [
311
- ...buildDiffClusters(diffImage, 2),
312
- ...buildDiffClusters(diffImage, 3),
313
- ]
314
- .map((cluster) => normalizeClusterToFrameSpace(cluster, scaleX, scaleY))
315
- .sort((a, b) => b.size - a.size);
316
-
317
- const residualScopes = clusters.map((cluster, index) => ({
318
- rank: index + 1,
319
- class: cluster.class,
320
- size: cluster.size,
321
- centroid: {
322
- x: round(cluster.centroid.x),
323
- y: round(cluster.centroid.y),
324
- },
325
- bbox: {
326
- x: round(cluster.bbox.x),
327
- y: round(cluster.bbox.y),
328
- w: round(cluster.bbox.w),
329
- h: round(cluster.bbox.h),
330
- },
331
- html_target: pickScopeTarget(cluster, htmlItems),
332
- svg_target: pickScopeTarget(cluster, svgItems),
333
- }));
334
-
335
- const byClass = residualScopes.reduce((acc, scope) => {
336
- acc[scope.class] = acc[scope.class] || [];
337
- acc[scope.class].push(scope);
338
- return acc;
339
- }, {});
340
-
341
- return {
342
- scopes: residualScopes,
343
- summary: residualScopes.slice(0, 20).map((scope) => formatResidualScopeSummary(scope)),
344
- html_only_top: (byClass["html-only"] || []).slice(0, 10),
345
- svg_only_top: (byClass["svg-only"] || []).slice(0, 10),
346
- };
347
- }