@zenuml/core 3.46.0 → 3.46.1

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 (55) 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.md +1 -1
  5. package/bun.lock +25 -11
  6. package/cy/canonical-history.html +908 -0
  7. package/cy/compare-case.html +357 -0
  8. package/cy/compare-cases.js +824 -0
  9. package/cy/compare.html +35 -0
  10. package/cy/diff-algorithm.js +199 -0
  11. package/cy/element-report.html +705 -0
  12. package/cy/icons-test.html +29 -0
  13. package/cy/legacy-vs-html.html +291 -0
  14. package/cy/native-diff-ext/background.js +60 -0
  15. package/cy/native-diff-ext/bridge.js +26 -0
  16. package/cy/native-diff-ext/content.js +194 -0
  17. package/cy/parity-test.html +122 -0
  18. package/cy/return-in-nested-if.html +29 -0
  19. package/cy/svg-preview.html +56 -0
  20. package/cy/svg-test.html +21 -0
  21. package/cy/theme-default-test.html +28 -0
  22. package/dist/stats.html +1 -1
  23. package/dist/zenuml.esm.mjs +16352 -15223
  24. package/dist/zenuml.js +701 -575
  25. package/docs/superpowers/plans/2026-03-23-svg-parity-features.md +283 -0
  26. package/index.html +568 -73
  27. package/package.json +15 -4
  28. package/scripts/analyze-compare-case/collect-data.mjs +991 -0
  29. package/scripts/analyze-compare-case/config.mjs +102 -0
  30. package/scripts/analyze-compare-case/geometry.mjs +101 -0
  31. package/scripts/analyze-compare-case/native-diff.mjs +224 -0
  32. package/scripts/analyze-compare-case/output.mjs +74 -0
  33. package/scripts/analyze-compare-case/panel-diff.mjs +114 -0
  34. package/scripts/analyze-compare-case/report.mjs +157 -0
  35. package/scripts/analyze-compare-case/residual-scopes.mjs +325 -0
  36. package/scripts/analyze-compare-case/scoring.mjs +816 -0
  37. package/scripts/analyze-compare-case.mjs +149 -0
  38. package/scripts/snapshot-dual.js +34 -34
  39. package/skills/dia-scoring/SKILL.md +129 -0
  40. package/skills/dia-scoring/agents/openai.yaml +7 -0
  41. package/skills/dia-scoring/references/selectors-and-keys.md +253 -0
  42. package/test-setup.ts +8 -0
  43. package/types/index.d.ts +56 -0
  44. package/vite.config.ts +4 -0
  45. package/dist/10029-icon-service-Function-Apps-ObflOLuF.js +0 -5
  46. package/dist/Res_AWS-Identity-Access-Management_IAM-Access-Analyzer_48-BPq60XMY.js +0 -11
  47. package/dist/Res_AWS-Lambda_Lambda-Function_48-Co38UB_2.js +0 -12
  48. package/dist/Res_Amazon-EC2_Instance_48-CRaqbNUl.js +0 -12
  49. package/dist/Res_Amazon-Simple-Notification-Service_Topic_48-q13mxUeM.js +0 -11
  50. package/dist/Res_Amazon-Simple-Queue-Service_Queue_48-D2-8gbFw.js +0 -11
  51. package/dist/Robustness_Diagram_Boundary-nYnmTPs8.js +0 -10
  52. package/dist/Robustness_Diagram_Control-DLNLoMxd.js +0 -11
  53. package/dist/Robustness_Diagram_Entity-Be3kcbIE.js +0 -11
  54. package/dist/actor-BMj_HFpo.js +0 -11
  55. package/dist/database-BKHQQWQK.js +0 -8
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env node
2
+
3
+ /*
4
+ * What this file does:
5
+ * Runs the compare-case analyzer end to end from the command line.
6
+ *
7
+ * High-level flow:
8
+ * 1. Parse CLI flags such as case name and diff tolerances.
9
+ * 2. Open compare-case.html in Playwright.
10
+ * 3. Extract semantic geometry from the live HTML and SVG renderers.
11
+ * 4. Capture native screenshots of both sides and build the analyzer's local diff.
12
+ * 5. Build a structured report and optionally write artifacts to disk.
13
+ * 6. Print either JSON, summaries, or both.
14
+ *
15
+ * This file is intentionally thin. The detailed work lives in focused modules:
16
+ * config, browser extraction, diffing, scoring, residual attribution, and output.
17
+ *
18
+ * Example input:
19
+ * `node scripts/analyze-compare-case.mjs --case async-2a --user-data-dir "/Users/pengxiao/Library/Application Support/Google/Chrome" --profile-directory "Profile 8" --channel chrome --headed --json`
20
+ *
21
+ * Example output:
22
+ * A report object printed as JSON, with top-level sections such as `labels`,
23
+ * `numbers`, `arrows`, `participant_labels`, `participant_icons`,
24
+ * `participant_boxes`, `residual_scopes`, `diff`, and `capture`.
25
+ */
26
+
27
+ import process from "node:process";
28
+ import { resolve } from "node:path";
29
+ import { pathToFileURL } from "node:url";
30
+
31
+ import { chromium } from "playwright";
32
+
33
+ import { parseArgs } from "./analyze-compare-case/config.mjs";
34
+ import { collectLabelData } from "./analyze-compare-case/collect-data.mjs";
35
+ import { maybeWriteArtifacts, writeReportOutput } from "./analyze-compare-case/output.mjs";
36
+ import { renderAndReadDiffPanel } from "./analyze-compare-case/panel-diff.mjs";
37
+ import { buildReport } from "./analyze-compare-case/report.mjs";
38
+
39
+ export async function main(argv = process.argv.slice(2), stdout = process.stdout) {
40
+ const args = parseArgs(argv);
41
+ if (args.profileDirectory && !args.userDataDir) {
42
+ throw new Error("--profile-directory requires --user-data-dir");
43
+ }
44
+ const compareUrl = `${args.baseUrl.replace(/\/$/, "")}/cy/compare-case.html?case=${encodeURIComponent(args.caseName)}`;
45
+ const chromiumArgs = args.profileDirectory
46
+ ? [`--profile-directory=${args.profileDirectory}`]
47
+ : [];
48
+ const launchOptions = {
49
+ channel: args.browserChannel || undefined,
50
+ headless: args.headless,
51
+ viewport: args.viewport,
52
+ deviceScaleFactor: 2,
53
+ args: chromiumArgs,
54
+ };
55
+ const persistentContext = args.userDataDir
56
+ ? await chromium.launchPersistentContext(args.userDataDir, launchOptions)
57
+ : null;
58
+ const browser = persistentContext ? null : await chromium.launch({
59
+ channel: args.browserChannel || undefined,
60
+ headless: args.headless,
61
+ args: chromiumArgs,
62
+ });
63
+ const context = persistentContext || await browser.newContext({
64
+ viewport: args.viewport,
65
+ deviceScaleFactor: 2,
66
+ });
67
+ const page = persistentContext
68
+ ? context.pages()[0] || await context.newPage()
69
+ : await context.newPage();
70
+
71
+ try {
72
+ await page.goto(compareUrl, { waitUntil: "networkidle" });
73
+ await page.waitForSelector("#html-output .interaction, #html-output .frame, #html-output .sequence-diagram");
74
+ await page.waitForSelector("#svg-output svg");
75
+
76
+ const extracted = await collectLabelData(page);
77
+
78
+ // Use CDP screenshots to match native-diff-ext (source of truth).
79
+ // The extension uses DOM.getBoxModel border-box + Page.captureScreenshot
80
+ // with clip and scale:1. Playwright's locator.screenshot() differs subtly
81
+ // in how it clips elements, so we replicate the extension's exact logic.
82
+ const cdpSession = await page.context().newCDPSession(page);
83
+ async function cdpScreenshotElement(selector) {
84
+ const { root } = await cdpSession.send("DOM.getDocument", {});
85
+ const { nodeId } = await cdpSession.send("DOM.querySelector", {
86
+ nodeId: root.nodeId,
87
+ selector,
88
+ });
89
+ if (!nodeId) throw new Error(`Element not found: ${selector}`);
90
+ const { model } = await cdpSession.send("DOM.getBoxModel", { nodeId });
91
+ const border = model.border;
92
+ const x = border[0];
93
+ const y = border[1];
94
+ const width = Math.ceil(border[2] - border[0]);
95
+ const height = Math.ceil(border[5] - border[1]);
96
+ const { data } = await cdpSession.send("Page.captureScreenshot", {
97
+ format: "png",
98
+ clip: { x, y, width, height, scale: 1 },
99
+ captureBeyondViewport: true,
100
+ });
101
+ return Buffer.from(data, "base64");
102
+ }
103
+
104
+ const htmlBuffer = await cdpScreenshotElement(extracted.htmlRootSelector);
105
+ const svgBuffer = await cdpScreenshotElement(extracted.svgRootSelector);
106
+ await cdpSession.detach();
107
+
108
+ await page.evaluate(() => {
109
+ if (typeof window.restoreHtmlAfterCapture === "function") {
110
+ window.restoreHtmlAfterCapture();
111
+ }
112
+ });
113
+
114
+ const diffImage = await renderAndReadDiffPanel(page, htmlBuffer, svgBuffer);
115
+ const report = buildReport(extracted.caseName || args.caseName, extracted, diffImage);
116
+ report.diff = diffImage.stats;
117
+ report.capture = {
118
+ url: compareUrl,
119
+ html_root: extracted.htmlRoot,
120
+ svg_root: extracted.svgRoot,
121
+ diff_badge: diffImage.badgeText,
122
+ panel_stats: diffImage.panelStats,
123
+ };
124
+
125
+ const artifactPaths = await maybeWriteArtifacts(args.outputDir, htmlBuffer, svgBuffer, diffImage, report);
126
+ if (artifactPaths) {
127
+ report.artifacts = artifactPaths;
128
+ }
129
+
130
+ writeReportOutput(stdout, report, args);
131
+ return report;
132
+ } finally {
133
+ await context.close();
134
+ if (browser) {
135
+ await browser.close();
136
+ }
137
+ }
138
+ }
139
+
140
+ const isDirectRun = process.argv[1]
141
+ ? pathToFileURL(resolve(process.argv[1])).href === import.meta.url
142
+ : false;
143
+
144
+ if (isDirectRun) {
145
+ main().catch((error) => {
146
+ console.error(error.stack || error.message || String(error));
147
+ process.exit(1);
148
+ });
149
+ }
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // Generate two sets of Playwright snapshots (server & browser modes) and a diff overlay.
2
+ // Generate two sets of Playwright snapshots (html & legacy modes) and a diff overlay.
3
3
  // Usage: node scripts/snapshot-dual.js -- tests/creation.spec.ts
4
4
 
5
5
  const { spawnSync } = require("node:child_process");
@@ -14,8 +14,8 @@ const testsArg = process.argv.slice(2);
14
14
  const tmpRoot = path.join(repoRoot, "tmp", "snapshots-dual");
15
15
  const paths = {
16
16
  original: path.join(tmpRoot, "original"),
17
- server: path.join(tmpRoot, "server"),
18
- browser: path.join(tmpRoot, "browser"),
17
+ html: path.join(tmpRoot, "html"),
18
+ legacy: path.join(tmpRoot, "legacy"),
19
19
  diff: path.join(tmpRoot, "diff"),
20
20
  };
21
21
 
@@ -58,48 +58,48 @@ function runPlaywright(mode) {
58
58
  stdio: "inherit",
59
59
  });
60
60
  if (result.status !== 0) {
61
- throw new Error(`Playwright failed in ${mode} mode`);
61
+ throw new Error(`Playwright failed in "${mode}" mode`);
62
62
  }
63
63
  }
64
64
 
65
65
  async function diffImages() {
66
66
  const differences = [];
67
67
  await ensureDir(paths.diff);
68
- const serverDirs = await fs.readdir(paths.server, { withFileTypes: true });
69
- for (const dirent of serverDirs) {
68
+ const htmlDirs = await fs.readdir(paths.html, { withFileTypes: true });
69
+ for (const dirent of htmlDirs) {
70
70
  if (!dirent.isDirectory()) continue;
71
71
  const baseName = dirent.name;
72
- const serverDir = path.join(paths.server, baseName);
73
- const browserDir = path.join(paths.browser, baseName);
74
- const files = await fs.readdir(serverDir);
72
+ const htmlDir = path.join(paths.html, baseName);
73
+ const legacyDir = path.join(paths.legacy, baseName);
74
+ const files = await fs.readdir(htmlDir);
75
75
  for (const file of files) {
76
76
  if (!file.endsWith(".png")) continue;
77
- const serverPngPath = path.join(serverDir, file);
78
- const browserPngPath = path.join(browserDir, file);
77
+ const htmlPngPath = path.join(htmlDir, file);
78
+ const legacyPngPath = path.join(legacyDir, file);
79
79
  try {
80
- const [serverBuf, browserBuf] = await Promise.all([
81
- fs.readFile(serverPngPath),
82
- fs.readFile(browserPngPath),
80
+ const [htmlBuf, legacyBuf] = await Promise.all([
81
+ fs.readFile(htmlPngPath),
82
+ fs.readFile(legacyPngPath),
83
83
  ]);
84
- const serverImg = PNG.sync.read(serverBuf);
85
- const browserImg = PNG.sync.read(browserBuf);
84
+ const htmlImg = PNG.sync.read(htmlBuf);
85
+ const legacyImg = PNG.sync.read(legacyBuf);
86
86
  if (
87
- serverImg.width !== browserImg.width ||
88
- serverImg.height !== browserImg.height
87
+ htmlImg.width !== legacyImg.width ||
88
+ htmlImg.height !== legacyImg.height
89
89
  ) {
90
90
  console.warn(`Skipping ${file}: dimension mismatch`);
91
91
  continue;
92
92
  }
93
93
  const diff = new PNG({
94
- width: serverImg.width,
95
- height: serverImg.height,
94
+ width: htmlImg.width,
95
+ height: htmlImg.height,
96
96
  });
97
97
  const diffPixels = pixelmatch(
98
- serverImg.data,
99
- browserImg.data,
98
+ htmlImg.data,
99
+ legacyImg.data,
100
100
  diff.data,
101
- serverImg.width,
102
- serverImg.height,
101
+ htmlImg.width,
102
+ htmlImg.height,
103
103
  { threshold: 0.1 },
104
104
  );
105
105
  const outDir = path.join(paths.diff, baseName);
@@ -135,22 +135,22 @@ async function main() {
135
135
  // backup existing snapshots
136
136
  // await copySnapshotDirs(paths.original);
137
137
 
138
- // browser mode
139
- runPlaywright("browser");
140
- await fs.rm(paths.browser, { recursive: true, force: true });
141
- await copySnapshotDirs(paths.browser);
138
+ // legacy mode
139
+ runPlaywright("legacy");
140
+ await fs.rm(paths.legacy, { recursive: true, force: true });
141
+ await copySnapshotDirs(paths.legacy);
142
142
 
143
- // server mode
144
- runPlaywright("server");
145
- await fs.rm(paths.server, { recursive: true, force: true });
146
- await copySnapshotDirs(paths.server);
143
+ // html mode
144
+ runPlaywright("html");
145
+ await fs.rm(paths.html, { recursive: true, force: true });
146
+ await copySnapshotDirs(paths.html);
147
147
 
148
148
  // generate diffs
149
149
  const differences = await diffImages();
150
- console.log(`Snapshots saved under ${paths.server} and ${paths.browser}`);
150
+ console.log(`Snapshots saved under ${paths.html} and ${paths.legacy}`);
151
151
  console.log(`Diff overlays saved under ${paths.diff}`);
152
152
  if (differences.length === 0) {
153
- console.log("All snapshots match between server and browser modes.");
153
+ console.log("All snapshots match between html and legacy modes.");
154
154
  } else {
155
155
  console.log("Snapshots with differences:");
156
156
  const sorted = differences.sort((a, b) => {
@@ -0,0 +1,129 @@
1
+ ---
2
+ name: dia-scoring
3
+ description: Score HTML-vs-SVG diagram parity in compare-case pages, including message labels, fragment labels, sequence numbers, arrows, participant headers, icons, stereotypes, participant colors, participant groups, comments, and residual diff scopes. Use Playwright for page inspection and semantic attribution; use the live `#diff-panel canvas` as the sole pixel-diff source of truth.
4
+ ---
5
+
6
+ # Dia Scoring
7
+
8
+ Use this skill when the task is to measure **message labels, fragment labels, sequence numbers, message arrows, participant labels, participant boxes, participant icons, stereotypes, participant colors, participant groups, inline comments, and residual diff hotspots** between the HTML renderer and the native SVG renderer on `compare-case.html`. Use Playwright page inspection only to inspect the page and semantically attribute diffs to letters or elements. Use the live `#diff-panel canvas` as the sole pixel-diff source of truth.
9
+
10
+ The workflow is browser-native:
11
+
12
+ 1. Open `http://localhost:8080/cy/compare-case.html?case=<name>`.
13
+ 2. Treat the `native-diff-ext` extension as required for pixel diff work: it generates the live `#diff-panel canvas` on page load.
14
+ 3. Use the analyzer script at [../../scripts/analyze-compare-case.mjs](../../scripts/analyze-compare-case.mjs).
15
+ 4. Prefer `--json` when the next step is automated processing.
16
+ 5. Prefer `--output-dir <dir>` when you need saved `html.png`, `svg.png`, `diff.png`, and `report.json`.
17
+ 6. Treat all pixel-diff comparison and residual scoping as live-panel work sourced from `#diff-panel canvas`.
18
+ 7. When recalibrating or correcting this skill itself, use the live `#diff-panel canvas` to calibrate the skill's measurement rules and reporting language.
19
+
20
+ ## Offset Anchor
21
+
22
+ All reported offsets must use the **outermost frame's top-left corner** as the anchor.
23
+
24
+ - HTML anchor: the compare-case HTML frame root
25
+ - SVG anchor: the compare-case SVG root / outer frame root
26
+ - Do not report alternate offset systems
27
+ - Do not anchor offsets to participant boxes, label boxes, stereotype boxes, or local containers
28
+ - If a local-container-relative reading differs from the frame-anchor reading, prefer the frame-anchor reading in all reporting
29
+
30
+ ## Browser Requirement
31
+
32
+ Use **Playwright browser tools only** for browser interaction in this workflow.
33
+
34
+ - Preferred tools: `browser_navigate`, `browser_snapshot`, `browser_evaluate`, `browser_take_screenshot`, `browser_click`, `browser_wait_for`
35
+ - Do not use Chrome DevTools browser tools for scoring, DOM inspection, screenshot capture, or residual validation
36
+ - Do not build your own pixel diff from HTML/SVG screenshots. For pixel comparison, use only the extension-rendered `#diff-panel canvas`
37
+
38
+ ## Rules
39
+
40
+ - Do not use `html-to-image` for capture.
41
+ - Use browser-native screenshots only.
42
+ - Use Playwright for browser-native screenshots and page inspection.
43
+ - Use the extension-generated live `#diff-panel canvas` as the sole source for pixel diff comparison and residual validation.
44
+ - All offset calculations must be anchored to the outermost frame's top-left corner.
45
+ - When re-checking, recalibrating, or correcting `dia-scoring` itself, calibrate the skill against the live `#diff-panel canvas`, not against a separately-built diff or memory of prior results.
46
+ - If `#diff-panel canvas` is absent, do not recalibrate or correct `dia-scoring` itself.
47
+ - Never build or trust a local screenshot-to-screenshot pixel diff when `#diff-panel canvas` is the question.
48
+ - Do not use Chrome DevTools browser tools for this workflow.
49
+ - Scope:
50
+ - normal messages
51
+ - self messages
52
+ - returns
53
+ - fragment conditions such as `[cond]`, `[else]`
54
+ - fragment section labels such as `catch`, `finally`
55
+ - participant label text and participant box geometry
56
+ - participant icons (actor, database, ec2, lambda, azurefunction, sqs, sns, iam, boundary, control, entity)
57
+ - participant stereotypes such as `«BFF»`, `«Interface»`
58
+ - participant background colors (`#FFEBE6`, `#0747A6`, etc.) and computed text contrast
59
+ - participant groups (dashed outline containers with title bar)
60
+ - inline comments (`// text`) above messages and fragments, including styled comments (`// [red] text`)
61
+ - residual `html-only` and `svg-only` diff clusters scoped back to nearby elements
62
+ - For each supported message, include:
63
+ - label text
64
+ - fragment condition / section label text when present
65
+ - sequence number text, including fragment sequence numbers when present
66
+ - arrow geometry keyed by sequence number
67
+ - normal/return arrow endpoint deltas: `left_dx`, `right_dx`, `width_dx`
68
+ - self-arrow loop geometry from the painted loop path plus arrowhead, not the outer `svg` viewport
69
+ - self-arrow vertical deltas: `top_dy`, `bottom_dy`, `height_dy`
70
+ - For participant icons, include:
71
+ - icon presence (HTML vs SVG)
72
+ - participant label text when the participant has an icon
73
+ - icon position relative to participant label
74
+ - icon visual match confirmation from diff image
75
+ - For participant stereotypes, include:
76
+ - stereotype text presence (HTML vs SVG), e.g. `«BFF»`
77
+ - stereotype position relative to participant label (above label, smaller font)
78
+ - stereotype offset must be measured with per-letter glyph-box comparison relative to the outermost frame anchor
79
+ - do not use participant-box-relative or other local-container-relative deltas in final reporting
80
+ - do not mark a stereotype as clean from glyph boxes alone; also check the live `#diff-panel canvas` in the stereotype row
81
+ - if glyph-box deltas are `0/0` but the panel still shows localized red/blue pixels overlapping the stereotype glyph union, report the stereotype as `ambiguous` or `paint-level residual`, not clean
82
+ - stereotype text color matching participant background contrast
83
+ - For participant colors, include:
84
+ - background fill color (hex value) on participant rect
85
+ - text color contrast (dark text on light bg, white text on dark bg)
86
+ - color application to both top and bottom participant boxes
87
+ - For participant groups, include:
88
+ - group name text presence and position (centered title bar)
89
+ - dashed outline rect enclosing grouped participants
90
+ - group bounds: leftmost to rightmost participant with margin
91
+ - group height extending to diagram bottom
92
+ - For inline comments, include:
93
+ - comment text presence and position (above the associated statement)
94
+ - comment Y offset from the message/fragment it belongs to
95
+ - fragment-level comments (e.g. `// comment 4` before `if(...)`) positioned above fragment header
96
+ - styled comment color application (e.g. `// [red] text`)
97
+ - For participant boxes, include:
98
+ - `html_box` and `svg_box` with `x`, `y`, `w`, `h`
99
+ - box deltas `dx`, `dy`, `dw`, `dh`
100
+ - SVG measurement based on the painted outer bounds of the stroked box, not the inset rect geometry
101
+ - For residual scopes, include:
102
+ - connected `html-only` and `svg-only` diff clusters from `#diff-panel canvas`
103
+ - cluster `size`, `bbox`, and `centroid`
104
+ - nearest scoped HTML and SVG targets at that position
105
+ - summaries that explain which element a remaining positional diff most likely belongs to
106
+ - live native diff panel confirmation before claiming a hotspot is real
107
+ - the largest confirmed live-panel `html-only` and `svg-only` clusters with approximate positions
108
+ - grouped summaries of where the panel's red and blue pixels are concentrated
109
+ - Do not report a residual hotspot as real if it is absent from the live `#diff-panel canvas`.
110
+ - Do not stop at totals like `HTML-only (44)` or `SVG-only (55)` when residuals matter; report where those pixels are.
111
+ - Each reported letter must be backed by:
112
+ - direct HTML-vs-SVG browser layout positions
113
+ - pixel-panel confirmation from `#diff-panel canvas`
114
+ - Participant stereotypes are first-class targets, not just part of `participant-box` or `participant-label`.
115
+ - If the evidence is weak or contradictory, keep the letter `ambiguous`.
116
+
117
+ ## Commands
118
+
119
+ Run from [../..](../..):
120
+
121
+ ```bash
122
+ node scripts/analyze-compare-case.mjs --case async-2a
123
+ node scripts/analyze-compare-case.mjs --case async-2a --json
124
+ node scripts/analyze-compare-case.mjs --case async-2a --output-dir tmp/message-elements/async-2a
125
+ ```
126
+
127
+ ## References
128
+
129
+ - Selector and pairing details: [references/selectors-and-keys.md](references/selectors-and-keys.md)
@@ -0,0 +1,7 @@
1
+ interface:
2
+ display_name: "Dia Scoring"
3
+ short_description: "Diagram label, number, and arrow offsets"
4
+ default_prompt: "Use $dia-scoring to measure message label, sequence-number, and arrow parity for a compare-case page."
5
+
6
+ policy:
7
+ allow_implicit_invocation: true
@@ -0,0 +1,253 @@
1
+ # Selectors And Keys
2
+
3
+ The analyzer uses these roots:
4
+
5
+ - HTML root: `#html-output .frame`, fallback `#html-output .sequence-diagram`
6
+ - SVG root: `#svg-output > svg`
7
+
8
+ Offset anchor:
9
+
10
+ - All reported offsets use the outermost frame root's top-left corner
11
+ - HTML side: `#html-output .frame`, fallback `#html-output .sequence-diagram`
12
+ - SVG side: `#svg-output > svg`
13
+ - Do not emit final `dx` / `dy` values from participant-local or other nested-container anchors
14
+
15
+ HTML label extraction:
16
+
17
+ - Normal messages: iterate `.interaction`, skip `.return`, `.creation`, and self interactions, then read `.message .editable-span-base`
18
+ - Self messages: `.self-invocation .label .editable-span-base`
19
+ - Returns: `.interaction.return .message .editable-span-base`, fallback `.interaction.return .name`
20
+ - Fragment conditions: `.fragment .segment > .text-skin-fragment:not(.finally)`, using only visible child spans when conditional branches are stacked
21
+ - Fragment sections:
22
+ - `.fragment.fragment-tcf .segment > .header.inline-block.bg-skin-frame.opacity-65`
23
+ - `.fragment.fragment-tcf .segment > .header.finally`
24
+
25
+ SVG label extraction:
26
+
27
+ - Normal messages: `g.message:not(.self-call) > text.message-label`
28
+ - Self messages: `g.message.self-call > text.message-label`
29
+ - Returns: `g.return > text.return-label`
30
+ - Fragment conditions: `g.fragment > text.fragment-condition`
31
+ - Fragment condition / section groups: `g.fragment > g` containing `text.fragment-section-label`
32
+ - texts starting with `[` are treated as `fragment-condition`
33
+ - other texts are treated as `fragment-section`
34
+
35
+ Pairing key:
36
+
37
+ - Semantic grouping is by `kind + text`
38
+ - Duplicate labels are paired by top-to-bottom order within that group
39
+ - Output key is:
40
+ - `kind`
41
+ - `text`
42
+ - `y_order`
43
+ - Fragment labels also include `owner=<fragment header>` in the human-readable summary when available
44
+
45
+ Per-letter scoring:
46
+
47
+ - Grapheme segmentation uses `Intl.Segmenter`, fallback `Array.from`
48
+ - Glyph boxes come from browser layout ranges, not whole-word centroids
49
+ - Numeric `dx` and `dy` are only emitted when direct layout evidence and diff-image evidence agree
50
+
51
+ Arrow extraction:
52
+
53
+ - HTML normal/return messages:
54
+ - line: direct child `svg` line strip inside `.message`
55
+ - head: direct child arrowhead `svg` inside `.message`
56
+ - HTML self messages:
57
+ - loop: painted geometry inside `svg.arrow`
58
+ - parts: outer loop path plus nested arrowhead path
59
+ - SVG normal messages:
60
+ - line: `line.message-line`
61
+ - head: `svg.arrow-head`
62
+ - SVG returns:
63
+ - line: `line.return-line`
64
+ - head: `polyline.return-arrow`
65
+ - SVG self messages:
66
+ - loop: painted geometry inside the outer `svg` under `g.message.self-call`
67
+ - parts: outer loop path plus nested arrowhead path
68
+
69
+ Arrow scoring:
70
+
71
+ - Arrows are keyed by sequence number when numbering is available, for example `arrow:1.2.3`
72
+ - Normal and return arrows are measured as one combined geometry item:
73
+ - line + arrow head together
74
+ - Self arrows are measured as one loop geometry item
75
+ - Self arrows use the union of the painted loop path and arrowhead path, not the outer viewport box
76
+ - Arrow output is endpoint-based, not box-centroid-based
77
+ - For normal and return arrows, report:
78
+ - `left_dx`
79
+ - `right_dx`
80
+ - `width_dx`
81
+ - For self arrows, also report:
82
+ - `top_dy`
83
+ - `bottom_dy`
84
+ - `height_dy`
85
+ - Do not report `dy` for horizontal message arrows
86
+
87
+ HTML sequence number extraction:
88
+
89
+ - Normal messages: `.interaction:not(.return):not(.creation):not(.self-invocation):not(.self) > .message > .absolute.text-xs`
90
+ - Self messages: `.interaction.self-invocation > .message .absolute.text-xs`
91
+ - Returns: `.interaction.return > .message > .absolute.text-xs`
92
+ - Fragments: `.fragment > .header > .absolute.text-xs`
93
+
94
+ SVG sequence number extraction:
95
+
96
+ - Normal messages: `g.message:not(.self-call) > text.seq-number`
97
+ - Self messages: `g.message.self-call > text.seq-number`
98
+ - Returns: `g.return > text.seq-number`
99
+ - Fragments: `g.fragment > text.seq-number`
100
+
101
+ ## Participant Icon Extraction
102
+
103
+ ## Participant Header Extraction
104
+
105
+ HTML participant header extraction:
106
+
107
+ - Participant root: `.participant[data-participant-id]`
108
+ - Participant box: outer border box from the participant root element
109
+ - Participant stereotype: `label.interface`, when present
110
+ - Participant label: last `.name` descendant, measured by glyph boxes
111
+
112
+ SVG participant header extraction:
113
+
114
+ - Participant root: `g.participant[data-participant]`
115
+ - Skip `g.participant-bottom`
116
+ - Participant box element: `:scope > rect.participant-box`
117
+ - Participant box measurement must use the painted outer bounds of the stroked rect, not the inset rect geometry
118
+ - Participant stereotype:
119
+ - prefer `:scope > text.stereotype-label`
120
+ - fallback: top-most direct `text` child above `text.participant-label`
121
+ - Participant label: `:scope > text.participant-label`
122
+
123
+ Participant stereotype pairing and scoring:
124
+
125
+ - Pair by participant name
126
+ - Validate text equality, for example `«BFF»`
127
+ - Measure stereotype offset by per-letter glyph boxes relative to the outermost frame root, not by participant-local anchors or whole-word box centroids
128
+ - Report:
129
+ - `letter_deltas`
130
+ - concise aggregate only when the per-letter evidence agrees
131
+ - Do not mark the stereotype clean from glyph boxes alone
132
+ - Also check the live `#diff-panel canvas` over the union of the HTML and SVG stereotype glyph boxes
133
+ - If localized red or blue pixels persist in that stereotype region while glyph-box deltas are `0/0`, classify it as `ambiguous` or `paint-level residual`
134
+
135
+ Participant box pairing and scoring:
136
+
137
+ - Pair by participant name
138
+ - Report `html_box` and `svg_box` with `x`, `y`, `w`, `h`
139
+ - Box `x` / `y` values are frame-anchor-relative
140
+ - Report box deltas:
141
+ - `dx`
142
+ - `dy`
143
+ - `dw`
144
+ - `dh`
145
+
146
+ HTML icon extraction:
147
+
148
+ - Participant root: `.participant[data-participant-id]`
149
+ - Top-row participant only: keep the top-most entry for each participant id
150
+ - Icon host: first child inside the centered participant row when it is an async icon host
151
+ - `[aria-description]`
152
+ - or contains `svg`
153
+ - or has `h-6` sizing class from `AsyncIcon`
154
+ - Icon box: union of painted SVG shapes when available, fallback to the host box
155
+ - Participant label: last `.name` descendant, measured by glyph boxes
156
+
157
+ SVG icon extraction:
158
+
159
+ - Participant root: `g.participant[data-participant]`
160
+ - Skip `g.participant-bottom`
161
+ - Icon element: `:scope > g[transform]`
162
+ - Icon box: union of painted shapes within that transformed group
163
+ - Participant label: `:scope > text.participant-label`
164
+
165
+ Icon pairing:
166
+
167
+ - Pair by participant name
168
+ - Only report participant icon rows for participants where at least one side has an icon
169
+ - Participant labels for icon-bearing participants are paired by participant name, not raw label text
170
+
171
+ Icon scoring:
172
+
173
+ - Absolute icon drift:
174
+ - `icon_dx`
175
+ - `icon_dy`
176
+ - Absolute icon drift is measured from the outermost frame anchor
177
+ - Relative icon drift against the participant label anchor:
178
+ - `relative_dx`
179
+ - `relative_dy`
180
+ - If there is no participant label on one side, use the participant box center as the anchor
181
+ - Report presence mismatch if one renderer has an icon and the other does not
182
+ - Diff confirmation is taken from `#diff-panel canvas`, scoped to the union of the HTML and SVG icon boxes
183
+
184
+ ## Residual Scope Attribution
185
+
186
+ Residual scope extraction:
187
+
188
+ - Build connected clusters from the live `#diff-panel canvas` colors:
189
+ - red = `html-only`
190
+ - blue = `svg-only`
191
+ - Ignore green `match` and magenta `color diff` pixels for positional scoping
192
+ - Each cluster reports:
193
+ - `size`
194
+ - `bbox`
195
+ - `centroid`
196
+ - These panel-derived clusters are the source of truth for residual hotspots.
197
+
198
+ Residual scope candidates:
199
+
200
+ - HTML side:
201
+ - labels
202
+ - numbers
203
+ - arrows
204
+ - participant stereotypes
205
+ - participant labels
206
+ - participant icons
207
+ - participant boxes
208
+ - diagram root fallback
209
+ - SVG side:
210
+ - labels
211
+ - numbers
212
+ - arrows
213
+ - participant stereotypes
214
+ - participant labels
215
+ - participant icons
216
+ - participant boxes
217
+ - `rect.frame-border-inner`, fallback `rect.frame-border` / `rect.frame-box`
218
+ - diagram root fallback
219
+
220
+ Residual scope attribution:
221
+
222
+ - Pick the closest candidate to the cluster centroid on each side
223
+ - Prefer targets that contain the centroid
224
+ - Prefer more specific categories over large containers:
225
+ - `participant-icon`
226
+ - `participant-stereotype`
227
+ - `label`, `number`, `participant-label`
228
+ - `arrow`
229
+ - `participant-box`
230
+ - `frame-border`
231
+ - `diagram-root`
232
+ - Use cluster/target overlap and centroid distance as tie-breakers
233
+
234
+ Residual scope output:
235
+
236
+ - `residual_scopes`: all attributed clusters
237
+ - `residual_scope_summary`: top 20 concise lines for terminal use
238
+ - `residual_scope_html_only_top`: top 10 `html-only` clusters
239
+ - `residual_scope_svg_only_top`: top 10 `svg-only` clusters
240
+ - When answering from the live panel, also report:
241
+ - the largest red clusters from `#diff-panel canvas`
242
+ - the largest blue clusters from `#diff-panel canvas`
243
+ - approximate diagram-space positions or bounding boxes
244
+ - attributed HTML and SVG targets for those clusters
245
+ - a short grouped summary of where the red and blue pixels are concentrated
246
+
247
+ Live panel validation:
248
+
249
+ - Source of truth for residual hotspots is `#diff-panel canvas`
250
+ - Confirm the hotspot by reading the panel's actual red and blue pixels at that area
251
+ - If the panel shows no red or blue pixels there, do not report that hotspot as a real residual diff
252
+ - If the panel shows non-zero red or blue totals, do not stop at the totals alone; locate the dominant clusters and report them
253
+ - Do not build or rely on a separate screenshot-to-screenshot diff for pixel comparison when `#diff-panel canvas` is available on the page
package/test-setup.ts CHANGED
@@ -97,6 +97,14 @@ global.ResizeObserver = class ResizeObserver {
97
97
  unobserve() {}
98
98
  };
99
99
 
100
+ // Inject @napi-rs/canvas for accurate text measurement in all tests.
101
+ // happy-dom's canvas doesn't do real text measurement, which causes
102
+ // WidthProviderOnCanvas to return inaccurate widths.
103
+ import { createCanvas } from "@napi-rs/canvas";
104
+ import { setCanvasContext } from "./src/positioning/WidthProviderFunc";
105
+ const _napiCanvas = createCanvas(1, 1);
106
+ setCanvasContext(_napiCanvas.getContext("2d") as unknown as CanvasRenderingContext2D);
107
+
100
108
  // Add custom matchers or global test utilities here
101
109
  // For example:
102
110
  // expect.extend({