@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.
- package/dist/cloud-icons-eHuugVSv.js.map +1 -0
- package/dist/zenuml.esm.mjs +2153 -2156
- package/dist/zenuml.esm.mjs.map +1 -0
- package/dist/zenuml.js +82 -82
- package/dist/zenuml.js.map +1 -0
- package/package.json +11 -1
- package/src/cli/zenuml.ts +1164 -0
- package/.agents/skills/babysit-pr/SKILL.md +0 -223
- package/.agents/skills/babysit-pr/agents/openai.yaml +0 -7
- package/.agents/skills/dia-scoring/SKILL.md +0 -139
- package/.agents/skills/dia-scoring/agents/openai.yaml +0 -7
- package/.agents/skills/dia-scoring/references/selectors-and-keys.md +0 -253
- package/.agents/skills/land-pr/SKILL.md +0 -120
- package/.agents/skills/propagate-core-release/SKILL.md +0 -205
- package/.agents/skills/propagate-core-release/agents/openai.yaml +0 -7
- package/.agents/skills/propagate-core-release/references/downstreams.md +0 -42
- package/.agents/skills/ship-branch/SKILL.md +0 -105
- package/.agents/skills/submit-branch/SKILL.md +0 -76
- package/.agents/skills/validate-branch/SKILL.md +0 -72
- package/.claude/commands/README.md +0 -162
- package/.claude/commands/analyze.md +0 -101
- package/.claude/commands/clarify.md +0 -158
- package/.claude/commands/code-review.md +0 -322
- package/.claude/commands/constitution.md +0 -73
- package/.claude/commands/create-docs.md +0 -309
- package/.claude/commands/full-context.md +0 -121
- package/.claude/commands/gemini-consult.md +0 -164
- package/.claude/commands/handoff.md +0 -146
- package/.claude/commands/implement.md +0 -56
- package/.claude/commands/plan.md +0 -43
- package/.claude/commands/refactor.md +0 -188
- package/.claude/commands/specify.md +0 -21
- package/.claude/commands/tasks.md +0 -62
- package/.claude/commands/update-docs.md +0 -314
- package/.claude/hooks/README.md +0 -270
- package/.claude/hooks/config/sensitive-patterns.json +0 -86
- package/.claude/hooks/gemini-context-injector.sh +0 -129
- package/.claude/hooks/mcp-security-scan.sh +0 -147
- package/.claude/hooks/notify.sh +0 -103
- package/.claude/hooks/setup/hook-setup.md +0 -96
- package/.claude/hooks/setup/settings.json.template +0 -63
- package/.claude/hooks/sounds/complete.wav +0 -0
- package/.claude/hooks/sounds/input-needed.wav +0 -0
- package/.claude/hooks/subagent-context-injector.sh +0 -65
- package/.claude/skills/babysit-pr/SKILL.md +0 -223
- package/.claude/skills/babysit-pr/agents/openai.yaml +0 -7
- package/.claude/skills/dia-scoring/SKILL.md +0 -139
- package/.claude/skills/dia-scoring/agents/openai.yaml +0 -7
- package/.claude/skills/dia-scoring/references/selectors-and-keys.md +0 -253
- package/.claude/skills/emoji-eval/SKILL.md +0 -187
- package/.claude/skills/land-pr/SKILL.md +0 -120
- package/.claude/skills/propagate-core-release/SKILL.md +0 -205
- package/.claude/skills/propagate-core-release/agents/openai.yaml +0 -7
- package/.claude/skills/propagate-core-release/references/downstreams.md +0 -42
- package/.claude/skills/ship-branch/SKILL.md +0 -105
- package/.claude/skills/submit-branch/SKILL.md +0 -76
- package/.claude/skills/validate-branch/SKILL.md +0 -72
- package/.claude/skills/zenuml-ux-research/SKILL.md +0 -183
- package/.claude/skills/zenuml-ux-research/references/assertion-catalog.md +0 -261
- package/.claude/skills/zenuml-ux-research/references/best-practices-overview.md +0 -56
- package/.claude/skills/zenuml-ux-research/references/report-template.md +0 -89
- package/.claude/skills/zenuml-ux-research/references/scenarios/edit-message-label.md +0 -37
- package/.claude/skills/zenuml-ux-research/references/scenarios/insert-message.md +0 -36
- package/.claude/skills/zenuml-ux-research/references/scenarios/insert-participant.md +0 -31
- package/.claude/skills/zenuml-ux-research/references/scenarios/rename-participant.md +0 -33
- package/.claude/skills/zenuml-ux-research/references/scenarios/undo-insert.md +0 -35
- package/.devcontainer/devcontainer.json +0 -21
- package/.dockerignore +0 -19
- package/.eslintrc.js +0 -39
- package/.git-blame-ignore-revs +0 -6
- package/.kiro/hooks/README.md +0 -38
- package/.kiro/hooks/session-sound-notification.js +0 -44
- package/.kiro/hooks/session-sound-notification.json +0 -23
- package/.mcp.json.example +0 -17
- package/.nvmrc +0 -1
- package/.prettierignore +0 -4
- package/.prettierrc +0 -1
- package/.specify/memory/constitution.md +0 -33
- package/.specify/scripts/bash/check-prerequisites.sh +0 -166
- package/.specify/scripts/bash/common.sh +0 -113
- package/.specify/scripts/bash/create-new-feature.sh +0 -97
- package/.specify/scripts/bash/setup-plan.sh +0 -60
- package/.specify/scripts/bash/update-agent-context.sh +0 -728
- package/.specify/templates/agent-file-template.md +0 -23
- package/.specify/templates/plan-template.md +0 -219
- package/.specify/templates/spec-template.md +0 -116
- package/.specify/templates/tasks-template.md +0 -127
- package/.storybook/main.ts +0 -25
- package/.storybook/preview.ts +0 -29
- package/.watchmanconfig +0 -3
- package/AGENTS.md +0 -26
- package/CLAUDE.md +0 -124
- package/DEPLOYMENT.md +0 -62
- package/Dockerfile +0 -36
- package/IMPLEMENTATION_PLAN.md +0 -163
- package/Integration/vanilla-js/index.html +0 -42
- package/MCP-ASSISTANT-RULES.md +0 -85
- package/README_CN.md +0 -15
- package/TUTORIAL.md +0 -116
- package/antlr/antlr-4.11.1-complete.jar +0 -0
- package/bun.lock +0 -1544
- package/bunfig.toml +0 -52
- package/docs/UNICODE_SUPPORT.md +0 -179
- package/docs/ai-context/deployment-infrastructure.md +0 -21
- package/docs/ai-context/docs-overview.md +0 -89
- package/docs/ai-context/handoff.md +0 -174
- package/docs/ai-context/project-structure.md +0 -160
- package/docs/ai-context/system-integration.md +0 -21
- package/docs/asciidoc/contributor.adoc +0 -54
- package/docs/asciidoc/create-my-own-theme.adoc +0 -149
- package/docs/asciidoc/images/creation-component.png +0 -0
- package/docs/asciidoc/images/creation-rtl.png +0 -0
- package/docs/asciidoc/images/message-arrow-rtl.png +0 -0
- package/docs/asciidoc/images/occurrence.png +0 -0
- package/docs/asciidoc/images/return-message-conflict.png +0 -0
- package/docs/asciidoc/images/shift-up-half-the-height.png +0 -0
- package/docs/asciidoc/images/three-layer-info-arch.png +0 -0
- package/docs/asciidoc/images/vertical-alignment.svg +0 -1
- package/docs/asciidoc/images/vertically-aligning.png +0 -0
- package/docs/asciidoc/index.adoc +0 -277
- package/docs/asciidoc/theme-debug-web-app.png +0 -0
- package/docs/asciidoc/tutorial.adoc +0 -22
- package/docs/asciidoc/user-css.png +0 -0
- package/docs/async-vs-sync-parser-rules.md +0 -81
- package/docs/divider-parser-allow-spaces.md +0 -38
- package/docs/highlighting-messages.md +0 -52
- package/docs/images/editor-sample.png +0 -0
- package/docs/inherited-vs-provided-from.md +0 -64
- package/docs/parser/Assignment.md +0 -8
- package/docs/parser/PARSER_IMPROVEMENTS_CC.md +0 -425
- package/docs/parser/grammar_review_gemini.md +0 -116
- package/docs/participants-function.md +0 -25
- package/docs/responsive-participant-margin.md +0 -52
- package/docs/starter.md +0 -9
- package/docs/superpowers/plans/2026-03-27-e2e-test-reorg.md +0 -698
- package/docs/superpowers/plans/2026-03-30-emoji-support.md +0 -1220
- package/docs/superpowers/plans/2026-03-30-self-correcting-scoring.md +0 -206
- package/docs/superpowers/plans/2026-04-15-keyboard-editing-on-diagram.md +0 -1992
- package/docs/superpowers/plans/2026-04-15-zenuml-ux-research-skill.md +0 -1452
- package/docs/ux-research/.gitkeep +0 -0
- package/docs/ux-research/2026-04-15-rename-participant.md +0 -156
- package/docs/ux-research/2026-04-18-insert-participant.md +0 -151
- package/docs/width-translate-and-offsets.md +0 -62
- package/docs/xss.md +0 -59
- package/e2e/data/compare-cases.js +0 -1090
- package/e2e/data/diff-algorithm.js +0 -199
- package/e2e/fixtures/create-message.html +0 -26
- package/e2e/fixtures/editable-label.html +0 -35
- package/e2e/fixtures/editable-span.html +0 -122
- package/e2e/fixtures/empty-diagram.html +0 -23
- package/e2e/fixtures/fixture.html +0 -31
- package/e2e/fixtures/insert-participant.html +0 -23
- package/e2e/fixtures/reorder-cross-fragment.html +0 -31
- package/e2e/fixtures/reorder-fragment.html +0 -29
- package/e2e/fixtures/reorder-message.html +0 -27
- package/e2e/fixtures/svg-test.html +0 -21
- package/e2e/fixtures/type-switch.html +0 -29
- package/e2e/tools/canonical-history.html +0 -908
- package/e2e/tools/compare-case.html +0 -371
- package/e2e/tools/compare.html +0 -35
- package/e2e/tools/native-diff-ext/background.js +0 -60
- package/e2e/tools/native-diff-ext/bridge.js +0 -26
- package/e2e/tools/native-diff-ext/content.js +0 -194
- package/e2e/tools/svg-preview.html +0 -56
- package/embed.html +0 -193
- package/eslint.config.mjs +0 -35
- package/firebase-debug.log +0 -108
- package/iframe-container-demo/diagram.html +0 -124
- package/iframe-container-demo/host.html +0 -817
- package/index.html +0 -771
- package/mermaid-zenuml-async-spa-auth.png +0 -0
- package/mermaid-zenuml-async-spa-auth.snapshot.md +0 -96
- package/newsletter/unicode-support-announcement.md +0 -134
- package/playground/creation.html +0 -53
- package/playground/message.html +0 -63
- package/playwright.config.ts +0 -40
- package/renderer.html +0 -366
- package/scripts/analyze-compare-case/collect-data.mjs +0 -1134
- package/scripts/analyze-compare-case/config.mjs +0 -102
- package/scripts/analyze-compare-case/geometry.mjs +0 -101
- package/scripts/analyze-compare-case/native-diff.mjs +0 -224
- package/scripts/analyze-compare-case/output.mjs +0 -74
- package/scripts/analyze-compare-case/panel-diff.mjs +0 -114
- package/scripts/analyze-compare-case/report.mjs +0 -162
- package/scripts/analyze-compare-case/residual-scopes.mjs +0 -347
- package/scripts/analyze-compare-case/scoring.mjs +0 -829
- package/scripts/analyze-compare-case.mjs +0 -149
- package/scripts/bump-version.js +0 -117
- package/scripts/snapshot-dual.js +0 -173
- package/scripts/update-snapshots.js +0 -70
- package/skills/dia-scoring/SKILL.md +0 -129
- package/skills/dia-scoring/agents/openai.yaml +0 -7
- package/skills/dia-scoring/references/selectors-and-keys.md +0 -253
- package/tailwind.config.js +0 -126
- package/test-compression.html +0 -274
- package/test-mermaid-zenuml.html +0 -57
- package/test-setup.ts +0 -124
- package/test-url-params.html +0 -192
- package/tsconfig.app.json +0 -31
- package/tsconfig.node.json +0 -24
- package/tsconfig.test.json +0 -9
- package/vite.config.lib.ts +0 -93
- package/vite.config.ts +0 -84
- package/wrangler.toml +0 -18
|
@@ -1,371 +0,0 @@
|
|
|
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-btn { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; background: #334155; border: none; border-radius: 4px; color: white; font-size: 16px; cursor: pointer; text-decoration: none; line-height: 1; }
|
|
21
|
-
.nav-btn:hover { background: #475569; }
|
|
22
|
-
.nav-btn[style*="hidden"] { visibility: hidden; }
|
|
23
|
-
.nav .match-badge { font-size: 11px; color: #94a3b8; }
|
|
24
|
-
.container { display: flex; gap: 0; }
|
|
25
|
-
.container.stacked { flex-direction: column; }
|
|
26
|
-
.panel { flex: 1; overflow: auto; }
|
|
27
|
-
.panel > h3 { margin: 0; padding: 8px 16px; background: #334155; color: white; font-size: 13px; }
|
|
28
|
-
.panel-content { padding: 10px; background: white; }
|
|
29
|
-
.diff-panel { display: none; }
|
|
30
|
-
.diff-panel.visible { display: block; }
|
|
31
|
-
.diff-panel .panel-content canvas { max-width: 100%; }
|
|
32
|
-
#html-output .footer { display: none !important; }
|
|
33
|
-
#html-output .privacy { visibility: hidden !important; width: 0 !important; overflow: hidden !important; }
|
|
34
|
-
</style>
|
|
35
|
-
</head>
|
|
36
|
-
<body>
|
|
37
|
-
<div class="compare-bar">
|
|
38
|
-
<a href="/e2e/tools/compare.html">← All cases</a>
|
|
39
|
-
<h2 id="case-name"></h2>
|
|
40
|
-
<div class="nav">
|
|
41
|
-
<a id="prev-link" class="nav-btn" href="#" title="Previous case">‹</a>
|
|
42
|
-
<a id="next-link" class="nav-btn" href="#" title="Next case">›</a>
|
|
43
|
-
<span class="match-badge" id="match-badge"></span>
|
|
44
|
-
</div>
|
|
45
|
-
</div>
|
|
46
|
-
<div class="container">
|
|
47
|
-
<div class="panel">
|
|
48
|
-
<h3>HTML/React Renderer (theme-default)</h3>
|
|
49
|
-
<div class="panel-content" id="html-output">
|
|
50
|
-
<div id="diagram"><pre class="zenuml" style="margin: 0"></pre></div>
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
<div class="panel">
|
|
54
|
-
<h3>Native SVG Renderer</h3>
|
|
55
|
-
<div class="panel-content" id="svg-output"></div>
|
|
56
|
-
</div>
|
|
57
|
-
<div class="panel diff-panel" id="diff-panel">
|
|
58
|
-
<h3>Pixel Diff</h3>
|
|
59
|
-
<div class="panel-content" id="diff-output"></div>
|
|
60
|
-
</div>
|
|
61
|
-
</div>
|
|
62
|
-
<script type="module">
|
|
63
|
-
import ZenUml from "/src/core.tsx";
|
|
64
|
-
import { renderToSvg } from "/src/svg/renderToSvg.ts";
|
|
65
|
-
|
|
66
|
-
import { CASES } from "../data/compare-cases.js";
|
|
67
|
-
import { diffImages, DEFAULTS } from "../data/diff-algorithm.js";
|
|
68
|
-
|
|
69
|
-
const caseNames = Object.keys(CASES);
|
|
70
|
-
const params = new URLSearchParams(window.location.search);
|
|
71
|
-
const caseName = params.get("case") || caseNames[0];
|
|
72
|
-
const code = CASES[caseName];
|
|
73
|
-
|
|
74
|
-
// Update title and header
|
|
75
|
-
document.title = `Compare: ${caseName}`;
|
|
76
|
-
document.getElementById("case-name").textContent = caseName;
|
|
77
|
-
|
|
78
|
-
// Prev/Next navigation
|
|
79
|
-
const idx = caseNames.indexOf(caseName);
|
|
80
|
-
const prevLink = document.getElementById("prev-link");
|
|
81
|
-
const nextLink = document.getElementById("next-link");
|
|
82
|
-
if (idx > 0) {
|
|
83
|
-
prevLink.href = `?case=${encodeURIComponent(caseNames[idx - 1])}`;
|
|
84
|
-
} else {
|
|
85
|
-
prevLink.style.visibility = "hidden";
|
|
86
|
-
}
|
|
87
|
-
if (idx < caseNames.length - 1) {
|
|
88
|
-
nextLink.href = `?case=${encodeURIComponent(caseNames[idx + 1])}`;
|
|
89
|
-
} else {
|
|
90
|
-
nextLink.style.visibility = "hidden";
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Keyboard navigation
|
|
94
|
-
document.addEventListener("keydown", (e) => {
|
|
95
|
-
if (e.key === "ArrowLeft" && prevLink.style.visibility !== "hidden") {
|
|
96
|
-
window.location.href = prevLink.href;
|
|
97
|
-
} else if (e.key === "ArrowRight" && nextLink.style.visibility !== "hidden") {
|
|
98
|
-
window.location.href = nextLink.href;
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
// Expose DSL for the extension's batch mode to read
|
|
103
|
-
window.__currentDSL = code || "";
|
|
104
|
-
|
|
105
|
-
if (code === undefined) {
|
|
106
|
-
const errP = document.createElement("p");
|
|
107
|
-
errP.style.color = "red";
|
|
108
|
-
errP.textContent = "Unknown case: " + caseName;
|
|
109
|
-
document.getElementById("svg-output").appendChild(errP);
|
|
110
|
-
} else {
|
|
111
|
-
// SVG side
|
|
112
|
-
const result = renderToSvg(code);
|
|
113
|
-
document.getElementById("svg-output").innerHTML = result.svg;
|
|
114
|
-
|
|
115
|
-
// HTML side — render with theme-default
|
|
116
|
-
const diagram = document.getElementById("diagram");
|
|
117
|
-
const pre = diagram.querySelector("pre.zenuml");
|
|
118
|
-
pre.textContent = code;
|
|
119
|
-
const zenUml = new ZenUml(pre);
|
|
120
|
-
await zenUml.render(code, { theme: "theme-default" });
|
|
121
|
-
|
|
122
|
-
// Hide footer in HTML output (SVG has no footer equivalent)
|
|
123
|
-
requestAnimationFrame(() => {
|
|
124
|
-
const footer = document.querySelector("#html-output .footer");
|
|
125
|
-
if (footer) footer.style.display = "none";
|
|
126
|
-
|
|
127
|
-
// Stack panels vertically when diagram is wider than half the viewport.
|
|
128
|
-
// This prevents overflow clipping in CDP screenshots for wide diagrams.
|
|
129
|
-
const frame = document.querySelector("#html-output .frame, #html-output .sequence-diagram");
|
|
130
|
-
const svgEl = document.querySelector("#svg-output > svg");
|
|
131
|
-
const maxW = Math.max(frame?.offsetWidth || 0, svgEl?.offsetWidth || 0);
|
|
132
|
-
if (maxW > window.innerWidth / 2 - 40) {
|
|
133
|
-
document.querySelector(".container").classList.add("stacked");
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ——— Element extraction for structural comparison ———
|
|
139
|
-
|
|
140
|
-
function extractHtmlElements(container) {
|
|
141
|
-
const elements = { participants: [], messages: [], selfCalls: [], creations: [], returns: [], fragments: [] };
|
|
142
|
-
|
|
143
|
-
// Participants
|
|
144
|
-
container.querySelectorAll("[data-participant-id]").forEach(el => {
|
|
145
|
-
const name = el.getAttribute("data-participant-id");
|
|
146
|
-
if (name) elements.participants.push(name);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// Messages (non-self, non-return, non-creation interactions)
|
|
150
|
-
container.querySelectorAll('.interaction:not(.return):not(.creation)').forEach(el => {
|
|
151
|
-
const isSelf = el.classList.contains("self-invocation") ||
|
|
152
|
-
el.querySelector(".self-invocation") !== null ||
|
|
153
|
-
el.classList.contains("self");
|
|
154
|
-
if (isSelf) return;
|
|
155
|
-
const sig = el.getAttribute("data-signature") || el.getAttribute("data-to") || "";
|
|
156
|
-
if (sig) elements.messages.push(sig);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
// Self-calls
|
|
160
|
-
container.querySelectorAll('.self-invocation').forEach(el => {
|
|
161
|
-
const interaction = el.closest(".interaction");
|
|
162
|
-
const sig = interaction?.getAttribute("data-signature") || "";
|
|
163
|
-
if (sig) elements.selfCalls.push(sig);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// Creations
|
|
167
|
-
container.querySelectorAll('.interaction.creation').forEach(el => {
|
|
168
|
-
const sig = el.getAttribute("data-signature") || "";
|
|
169
|
-
if (sig) elements.creations.push(sig);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
// Returns — match .interaction.return (some have data-type="return", some don't)
|
|
173
|
-
container.querySelectorAll('.interaction.return').forEach(el => {
|
|
174
|
-
const sig = el.getAttribute("data-signature") || "";
|
|
175
|
-
const msgDiv = el.querySelector(".message");
|
|
176
|
-
const label = sig || (msgDiv ? msgDiv.textContent.trim().replace(/[\d.]+$/, "").trim() : "");
|
|
177
|
-
if (label) elements.returns.push(label);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
// Fragments
|
|
181
|
-
container.querySelectorAll(".fragment").forEach(el => {
|
|
182
|
-
const kind = [...el.classList].find(c => c.startsWith("fragment-"))?.replace("fragment-", "") ||
|
|
183
|
-
[...el.classList].find(c => ["alt", "loop", "opt", "par", "tcf", "critical", "section", "ref"].includes(c)) || "unknown";
|
|
184
|
-
elements.fragments.push(kind);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
return elements;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function extractSvgElements(svgElement) {
|
|
191
|
-
const elements = { participants: [], messages: [], selfCalls: [], creations: [], returns: [], fragments: [] };
|
|
192
|
-
if (!svgElement) return elements;
|
|
193
|
-
|
|
194
|
-
// Participants
|
|
195
|
-
svgElement.querySelectorAll("g.participant, g.participant-starter").forEach(g => {
|
|
196
|
-
const name = g.getAttribute("data-participant");
|
|
197
|
-
if (name) elements.participants.push(name);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
// Messages (non-self)
|
|
201
|
-
svgElement.querySelectorAll("g.message").forEach(g => {
|
|
202
|
-
if (g.classList.contains("self-call")) return;
|
|
203
|
-
const text = g.querySelector("text.message-label");
|
|
204
|
-
elements.messages.push(text ? text.textContent.trim() : "");
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
// Self-calls
|
|
208
|
-
svgElement.querySelectorAll("g.message.self-call").forEach(g => {
|
|
209
|
-
const text = g.querySelector("text.message-label");
|
|
210
|
-
elements.selfCalls.push(text ? text.textContent.trim() : "");
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// Creations
|
|
214
|
-
svgElement.querySelectorAll("g.creation").forEach(g => {
|
|
215
|
-
const text = g.querySelector("text.message-label");
|
|
216
|
-
elements.creations.push(text ? text.textContent.trim() : "");
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
// Returns
|
|
220
|
-
svgElement.querySelectorAll("g.return").forEach(g => {
|
|
221
|
-
const text = g.querySelector("text.return-label");
|
|
222
|
-
elements.returns.push(text ? text.textContent.trim() : "");
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// Fragments
|
|
226
|
-
svgElement.querySelectorAll("g.fragment, g[class*='fragment-']").forEach(g => {
|
|
227
|
-
const kind = ([...g.classList].find(c => c.startsWith("fragment-")) || "").replace("fragment-", "") || "unknown";
|
|
228
|
-
elements.fragments.push(kind);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
return elements;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function computeStructuralMatch(htmlElements, svgElements) {
|
|
235
|
-
const types = ["participants", "messages", "selfCalls", "creations", "returns", "fragments"];
|
|
236
|
-
let totalScore = 0;
|
|
237
|
-
let scoredTypes = 0;
|
|
238
|
-
let totalElements = 0;
|
|
239
|
-
let matchedElements = 0;
|
|
240
|
-
const details = {};
|
|
241
|
-
|
|
242
|
-
for (const type of types) {
|
|
243
|
-
const htmlList = htmlElements[type] || [];
|
|
244
|
-
const svgList = svgElements[type] || [];
|
|
245
|
-
const maxLen = Math.max(htmlList.length, svgList.length);
|
|
246
|
-
|
|
247
|
-
if (maxLen === 0) continue; // skip types with no elements in either
|
|
248
|
-
|
|
249
|
-
// Count matches by order
|
|
250
|
-
let matched = 0;
|
|
251
|
-
const minLen = Math.min(htmlList.length, svgList.length);
|
|
252
|
-
for (let i = 0; i < minLen; i++) {
|
|
253
|
-
matched++; // count as matched if present in both at same index
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const score = matched / maxLen;
|
|
257
|
-
totalScore += score;
|
|
258
|
-
scoredTypes++;
|
|
259
|
-
totalElements += maxLen;
|
|
260
|
-
matchedElements += matched;
|
|
261
|
-
|
|
262
|
-
details[type] = { html: htmlList.length, svg: svgList.length, matched, score };
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return {
|
|
266
|
-
score: scoredTypes > 0 ? totalScore / scoredTypes : 1,
|
|
267
|
-
totalElements,
|
|
268
|
-
matchedElements,
|
|
269
|
-
details,
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// --- Reusable capture/diff API (also exposed on window for external tools) ---
|
|
274
|
-
|
|
275
|
-
// State for hide/restore cycle
|
|
276
|
-
let _hiddenEls = [];
|
|
277
|
-
let _savedStyles = [];
|
|
278
|
-
|
|
279
|
-
function _hideEl(el) {
|
|
280
|
-
if (el) { _hiddenEls.push({ el, prev: el.style.display }); el.style.display = "none"; }
|
|
281
|
-
}
|
|
282
|
-
function _stripMargin(el) {
|
|
283
|
-
if (el) { _savedStyles.push({ el, prev: el.style.margin }); el.style.margin = "0"; }
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Prepare HTML panel for screenshot: hide chrome, numbering, icons
|
|
287
|
-
window.prepareHtmlForCapture = function() {
|
|
288
|
-
_hiddenEls = [];
|
|
289
|
-
_savedStyles = [];
|
|
290
|
-
|
|
291
|
-
const htmlEl = document.getElementById("html-output");
|
|
292
|
-
const htmlFrame = htmlEl.querySelector(".frame");
|
|
293
|
-
const seqDiagram = htmlEl.querySelector(".sequence-diagram");
|
|
294
|
-
const svgElement = document.getElementById("svg-output").querySelector("svg");
|
|
295
|
-
|
|
296
|
-
_hideEl(htmlEl.querySelector(".footer"));
|
|
297
|
-
_stripMargin(htmlFrame);
|
|
298
|
-
|
|
299
|
-
// Hide bottom participant labels (SVG no longer renders them)
|
|
300
|
-
const captureEl = htmlFrame || seqDiagram || htmlEl;
|
|
301
|
-
const captureRect = captureEl.getBoundingClientRect();
|
|
302
|
-
const captureMidY = captureRect.top + captureRect.height * 0.7;
|
|
303
|
-
captureEl.querySelectorAll(".participant").forEach(el => {
|
|
304
|
-
const r = el.getBoundingClientRect();
|
|
305
|
-
if (r.top > captureMidY) _hideEl(el);
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
if (htmlFrame) {
|
|
309
|
-
const frameRect2 = htmlFrame.getBoundingClientRect();
|
|
310
|
-
// Hide SVG icons in the header area (top 35px) — checkbox, etc.
|
|
311
|
-
// Skip group outline overlays (data-group-overlay) which are functional rendering elements.
|
|
312
|
-
htmlFrame.querySelectorAll("svg").forEach(svg => {
|
|
313
|
-
if (svg.hasAttribute("data-group-overlay")) return;
|
|
314
|
-
const r = svg.getBoundingClientRect();
|
|
315
|
-
if (r.width > 0 && (r.top - frameRect2.top) < 35) _hideEl(svg);
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
return { htmlSelector: "#html-output .frame, #html-output .sequence-diagram", svgSelector: "#svg-output svg" };
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
// Restore HTML panel after screenshot
|
|
323
|
-
window.restoreHtmlAfterCapture = function() {
|
|
324
|
-
for (const { el, prev } of _hiddenEls) el.style.display = prev;
|
|
325
|
-
for (const { el, prev } of _savedStyles) el.style.margin = prev;
|
|
326
|
-
_hiddenEls = [];
|
|
327
|
-
_savedStyles = [];
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
// Run pixel diff from two PNG data URLs and display results on the page.
|
|
331
|
-
// Returns { pixelPct, posPct, structPct, matched, total, htmlOnly, svgOnly, colorDiff }
|
|
332
|
-
window.diffFromImages = async function(htmlDataUrl, svgDataUrl) {
|
|
333
|
-
const htmlEl = document.getElementById("html-output");
|
|
334
|
-
const svgEl = document.getElementById("svg-output");
|
|
335
|
-
const seqDiagram = htmlEl.querySelector(".sequence-diagram");
|
|
336
|
-
const svgElement = svgEl.querySelector("svg");
|
|
337
|
-
|
|
338
|
-
// Structural comparison
|
|
339
|
-
const htmlElements = extractHtmlElements(seqDiagram || htmlEl);
|
|
340
|
-
const svgElements = extractSvgElements(svgElement);
|
|
341
|
-
const structural = computeStructuralMatch(htmlElements, svgElements);
|
|
342
|
-
|
|
343
|
-
// Allow URL param overrides for debugging
|
|
344
|
-
const urlParams = new URLSearchParams(window.location.search);
|
|
345
|
-
const opts = {
|
|
346
|
-
LUMA_THRESHOLD: parseInt(urlParams.get('luma') || DEFAULTS.LUMA_THRESHOLD, 10),
|
|
347
|
-
CHANNEL_TOLERANCE: parseInt(urlParams.get('ctol') || DEFAULTS.CHANNEL_TOLERANCE, 10),
|
|
348
|
-
POSITION_TOLERANCE: parseInt(urlParams.get('ptol') || DEFAULTS.POSITION_TOLERANCE, 10),
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
const { canvas: diffCanvas, stats, badgeHtml } = await diffImages(htmlDataUrl, svgDataUrl, opts);
|
|
352
|
-
|
|
353
|
-
const diffOutput = document.getElementById("diff-output");
|
|
354
|
-
diffOutput.innerHTML = "";
|
|
355
|
-
diffOutput.appendChild(diffCanvas);
|
|
356
|
-
|
|
357
|
-
const panel = document.getElementById("diff-panel");
|
|
358
|
-
panel.classList.add("visible");
|
|
359
|
-
|
|
360
|
-
const structPct = (structural.score * 100).toFixed(0);
|
|
361
|
-
document.getElementById("match-badge").innerHTML =
|
|
362
|
-
`${structPct}% structural · ${badgeHtml}`;
|
|
363
|
-
|
|
364
|
-
return { pixelPct: stats.pixelPct, posPct: stats.posPct, structPct: parseFloat(structPct), ...stats };
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
</script>
|
|
370
|
-
</body>
|
|
371
|
-
</html>
|
package/e2e/tools/compare.html
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html>
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<title>SVG vs HTML Comparison</title>
|
|
6
|
-
<style>
|
|
7
|
-
body { margin: 0; font-family: sans-serif; background: #0f172a; color: #e2e8f0; }
|
|
8
|
-
h1 { margin: 0; padding: 16px 24px; font-size: 20px; font-weight: 600; border-bottom: 1px solid #334155; }
|
|
9
|
-
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; padding: 16px 24px; }
|
|
10
|
-
.card {
|
|
11
|
-
display: block; padding: 14px 18px; background: #1e293b; border-radius: 8px;
|
|
12
|
-
text-decoration: none; color: #e2e8f0; border: 1px solid #334155;
|
|
13
|
-
transition: border-color 0.15s, background 0.15s;
|
|
14
|
-
}
|
|
15
|
-
.card:hover { border-color: #60a5fa; background: #1e3a5f; }
|
|
16
|
-
.card .name { font-size: 15px; font-weight: 500; }
|
|
17
|
-
</style>
|
|
18
|
-
</head>
|
|
19
|
-
<body>
|
|
20
|
-
<h1>SVG vs HTML Comparison</h1>
|
|
21
|
-
<div class="grid" id="grid"></div>
|
|
22
|
-
<script type="module">
|
|
23
|
-
import { CASES } from "../data/compare-cases.js";
|
|
24
|
-
|
|
25
|
-
const grid = document.getElementById("grid");
|
|
26
|
-
for (const name of Object.keys(CASES)) {
|
|
27
|
-
const a = document.createElement("a");
|
|
28
|
-
a.href = `/e2e/tools/compare-case.html?case=${encodeURIComponent(name)}`;
|
|
29
|
-
a.className = "card";
|
|
30
|
-
a.innerHTML = `<div class="name">${name}</div>`;
|
|
31
|
-
grid.appendChild(a);
|
|
32
|
-
}
|
|
33
|
-
</script>
|
|
34
|
-
</body>
|
|
35
|
-
</html>
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
// Background service worker: uses chrome.debugger + CDP for element screenshots
|
|
2
|
-
|
|
3
|
-
chrome.action.onClicked.addListener(async (tab) => {
|
|
4
|
-
if (!tab.url || !tab.url.includes("compare-case.html")) return;
|
|
5
|
-
try {
|
|
6
|
-
const response = await chrome.tabs.sendMessage(tab.id, { action: "native-diff" });
|
|
7
|
-
console.log("[native-diff-ext bg]", response?.error || `Done: ${response?.pixelPct}%`);
|
|
8
|
-
} catch (err) {
|
|
9
|
-
console.error("[native-diff-ext bg] Failed:", err);
|
|
10
|
-
}
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
|
14
|
-
if (msg.action === "cdp-screenshot") {
|
|
15
|
-
cdpScreenshot(sender.tab.id, msg.selector)
|
|
16
|
-
.then((dataUrl) => sendResponse({ dataUrl }))
|
|
17
|
-
.catch((err) => sendResponse({ error: err.message }));
|
|
18
|
-
return true;
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
// Take a single element screenshot
|
|
23
|
-
async function cdpScreenshot(tabId, selector) {
|
|
24
|
-
const target = { tabId };
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
await chrome.debugger.attach(target, "1.3");
|
|
28
|
-
const { root } = await cdp(target, "DOM.getDocument", {});
|
|
29
|
-
return await screenshotNode(target, root.nodeId, selector);
|
|
30
|
-
} finally {
|
|
31
|
-
try { await chrome.debugger.detach(target); } catch (_) {}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function screenshotNode(target, rootNodeId, selector) {
|
|
36
|
-
const { nodeId } = await cdp(target, "DOM.querySelector", {
|
|
37
|
-
nodeId: rootNodeId,
|
|
38
|
-
selector,
|
|
39
|
-
});
|
|
40
|
-
if (!nodeId) throw new Error(`Element not found: ${selector}`);
|
|
41
|
-
|
|
42
|
-
const { model } = await cdp(target, "DOM.getBoxModel", { nodeId });
|
|
43
|
-
const border = model.border;
|
|
44
|
-
const x = border[0];
|
|
45
|
-
const y = border[1];
|
|
46
|
-
const width = Math.ceil(border[2] - border[0]);
|
|
47
|
-
const height = Math.ceil(border[5] - border[1]);
|
|
48
|
-
|
|
49
|
-
const { data } = await cdp(target, "Page.captureScreenshot", {
|
|
50
|
-
format: "png",
|
|
51
|
-
clip: { x, y, width, height, scale: 1 },
|
|
52
|
-
captureBeyondViewport: true,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
return "data:image/png;base64," + data;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function cdp(target, method, params) {
|
|
59
|
-
return chrome.debugger.sendCommand(target, method, params);
|
|
60
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
// Bridge script: runs in ISOLATED world
|
|
2
|
-
// Relays messages between content.js (MAIN world) and background.js (service worker)
|
|
3
|
-
|
|
4
|
-
// Content.js → bridge → background.js: screenshot requests
|
|
5
|
-
window.addEventListener("message", (event) => {
|
|
6
|
-
if (event.data && event.data.type === "native-diff-screenshot") {
|
|
7
|
-
console.log("[native-diff-ext bridge] Screenshot request:", event.data.selector);
|
|
8
|
-
chrome.runtime.sendMessage(
|
|
9
|
-
{ action: "cdp-screenshot", selector: event.data.selector },
|
|
10
|
-
(response) => {
|
|
11
|
-
window.postMessage({
|
|
12
|
-
type: "native-diff-screenshot-response",
|
|
13
|
-
dataUrl: response?.dataUrl,
|
|
14
|
-
error: response?.error,
|
|
15
|
-
}, "*");
|
|
16
|
-
}
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
// Icon click → content.js: trigger diff
|
|
22
|
-
chrome.runtime.onMessage.addListener((msg) => {
|
|
23
|
-
if (msg.action === "native-diff") {
|
|
24
|
-
window.postMessage({ type: "native-diff-trigger" }, "*");
|
|
25
|
-
}
|
|
26
|
-
});
|
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
// Content script: runs in MAIN world (has access to page's window functions)
|
|
2
|
-
// Uses CDP element screenshots via bridge.js → background.js
|
|
3
|
-
// Hides neighboring panels before each screenshot to prevent overlap.
|
|
4
|
-
//
|
|
5
|
-
// Diff algorithm lives in e2e/data/diff-algorithm.js, loaded by compare-case.html
|
|
6
|
-
// and exposed as window.diffFromImages. This script only handles screenshots
|
|
7
|
-
// and orchestration.
|
|
8
|
-
|
|
9
|
-
// Auto-run on page load
|
|
10
|
-
window.addEventListener("load", () => {
|
|
11
|
-
console.log("[native-diff-ext] Page loaded, waiting 1s for renderers...");
|
|
12
|
-
setTimeout(() => {
|
|
13
|
-
console.log("[native-diff-ext] Starting native diff...");
|
|
14
|
-
runNativeDiff();
|
|
15
|
-
}, 1000);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
// Icon click trigger from bridge.js
|
|
19
|
-
window.addEventListener("message", (event) => {
|
|
20
|
-
if (event.data && event.data.type === "native-diff-trigger") {
|
|
21
|
-
console.log("[native-diff-ext] Triggered by icon click");
|
|
22
|
-
runNativeDiff();
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
// Request a single CDP element screenshot via bridge
|
|
27
|
-
function screenshotOne(selector) {
|
|
28
|
-
return new Promise((resolve) => {
|
|
29
|
-
function handler(event) {
|
|
30
|
-
if (event.data && event.data.type === "native-diff-screenshot-response") {
|
|
31
|
-
window.removeEventListener("message", handler);
|
|
32
|
-
resolve(event.data);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
window.addEventListener("message", handler);
|
|
36
|
-
window.postMessage({ type: "native-diff-screenshot", selector }, "*");
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function runNativeDiff() {
|
|
41
|
-
if (typeof window.prepareHtmlForCapture !== "function") {
|
|
42
|
-
console.error("[native-diff-ext] prepareHtmlForCapture not found");
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
if (typeof window.diffFromImages !== "function") {
|
|
46
|
-
console.error("[native-diff-ext] diffFromImages not found — compare-case.html must load diff-algorithm.js");
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// 1. Prepare: hide HTML chrome (same as skill's page.evaluate step)
|
|
51
|
-
console.log("[native-diff-ext] Preparing HTML for capture...");
|
|
52
|
-
window.prepareHtmlForCapture();
|
|
53
|
-
|
|
54
|
-
// 2. Determine selectors
|
|
55
|
-
const htmlSelector = document.querySelector("#html-output .frame")
|
|
56
|
-
? "#html-output .frame"
|
|
57
|
-
: "#html-output .sequence-diagram";
|
|
58
|
-
const svgSelector = "#svg-output > svg";
|
|
59
|
-
|
|
60
|
-
// 3. Screenshot HTML (CDP clip isolates the element — no need to hide siblings)
|
|
61
|
-
console.log("[native-diff-ext] Taking HTML screenshot...");
|
|
62
|
-
const htmlCapture = await screenshotOne(htmlSelector);
|
|
63
|
-
|
|
64
|
-
if (htmlCapture.error) {
|
|
65
|
-
console.error("[native-diff-ext] HTML screenshot failed:", htmlCapture.error);
|
|
66
|
-
window.restoreHtmlAfterCapture();
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// 4. Screenshot SVG
|
|
71
|
-
console.log("[native-diff-ext] Taking SVG screenshot...");
|
|
72
|
-
const svgCapture = await screenshotOne(svgSelector);
|
|
73
|
-
|
|
74
|
-
// 5. Restore HTML chrome
|
|
75
|
-
window.restoreHtmlAfterCapture();
|
|
76
|
-
|
|
77
|
-
if (svgCapture.error) {
|
|
78
|
-
console.error("[native-diff-ext] SVG screenshot failed:", svgCapture.error);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
console.log("[native-diff-ext] Screenshots captured. Running diff...");
|
|
83
|
-
|
|
84
|
-
// 6. Run diff via shared algorithm (exposed by compare-case.html)
|
|
85
|
-
const result = await window.diffFromImages(htmlCapture.dataUrl, svgCapture.dataUrl);
|
|
86
|
-
console.log("[native-diff-ext] Done!", result.pixelPct + "% pixel match", result.posPct + "% position-only match");
|
|
87
|
-
|
|
88
|
-
// Post result back for icon-click flow
|
|
89
|
-
window.postMessage({ type: "native-diff-result", result }, "*");
|
|
90
|
-
|
|
91
|
-
// Batch mode: if __cr_cases is set in localStorage, save result and auto-advance
|
|
92
|
-
try {
|
|
93
|
-
const batchCases = localStorage.getItem("__cr_cases");
|
|
94
|
-
if (batchCases) {
|
|
95
|
-
const cases = JSON.parse(batchCases);
|
|
96
|
-
const results = JSON.parse(localStorage.getItem("__cr_results") || "{}");
|
|
97
|
-
const currentCase = new URLSearchParams(window.location.search).get("case");
|
|
98
|
-
if (currentCase && !results[currentCase]) {
|
|
99
|
-
// Get the DSL for this case from the page (exposed by compare-case.html)
|
|
100
|
-
const dsl = window.__currentDSL || "";
|
|
101
|
-
|
|
102
|
-
results[currentCase] = { score: result.pixelPct, posScore: result.posPct, dsl };
|
|
103
|
-
localStorage.setItem("__cr_results", JSON.stringify(results));
|
|
104
|
-
const doneCount = Object.keys(results).length;
|
|
105
|
-
console.log(`[native-diff-ext] Batch: ${currentCase}=${result.pixelPct}% px / ${result.posPct}% pos (${doneCount}/${cases.length})`);
|
|
106
|
-
if (doneCount < cases.length) {
|
|
107
|
-
const idx = cases.indexOf(currentCase);
|
|
108
|
-
if (idx >= 0 && idx + 1 < cases.length) {
|
|
109
|
-
setTimeout(() => {
|
|
110
|
-
window.location.href = `/e2e/tools/compare-case.html?case=${cases[idx + 1]}`;
|
|
111
|
-
}, 200);
|
|
112
|
-
}
|
|
113
|
-
} else {
|
|
114
|
-
const elapsed = ((Date.now() - parseInt(localStorage.getItem("__cr_start") || "0")) / 1000).toFixed(1);
|
|
115
|
-
console.log(`[native-diff-ext] Batch DONE in ${elapsed}s`);
|
|
116
|
-
// Save to IndexedDB for history tracking
|
|
117
|
-
saveBatchToHistory(results, elapsed);
|
|
118
|
-
// Clear batch vars so extension doesn't re-trigger on subsequent page loads
|
|
119
|
-
localStorage.removeItem("__cr_cases");
|
|
120
|
-
localStorage.removeItem("__cr_results");
|
|
121
|
-
localStorage.removeItem("__cr_start");
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
} catch (e) { /* batch mode is optional, don't break normal flow */ }
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// ---- IndexedDB history storage ----
|
|
129
|
-
// Database: "canonical-history", store: "runs"
|
|
130
|
-
// Each run: { timestamp, elapsed, cases: { name: { score, dsl } }, average }
|
|
131
|
-
|
|
132
|
-
function openHistoryDB() {
|
|
133
|
-
return new Promise((resolve, reject) => {
|
|
134
|
-
const req = indexedDB.open("canonical-history", 1);
|
|
135
|
-
req.onupgradeneeded = () => {
|
|
136
|
-
const db = req.result;
|
|
137
|
-
if (!db.objectStoreNames.contains("runs")) {
|
|
138
|
-
db.createObjectStore("runs", { keyPath: "timestamp" });
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
req.onsuccess = () => resolve(req.result);
|
|
142
|
-
req.onerror = () => reject(req.error);
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async function saveBatchToHistory(results, elapsed) {
|
|
147
|
-
try {
|
|
148
|
-
const db = await openHistoryDB();
|
|
149
|
-
const scores = Object.values(results).map(r => typeof r === "object" ? r.score : r);
|
|
150
|
-
const posScores = Object.values(results).map(r => {
|
|
151
|
-
if (typeof r === "object" && typeof r.posScore === "number") return r.posScore;
|
|
152
|
-
return typeof r === "object" ? r.score : r;
|
|
153
|
-
});
|
|
154
|
-
const average = scores.length > 0
|
|
155
|
-
? parseFloat((scores.reduce((a, b) => a + b, 0) / scores.length).toFixed(1))
|
|
156
|
-
: 0;
|
|
157
|
-
const averagePos = posScores.length > 0
|
|
158
|
-
? parseFloat((posScores.reduce((a, b) => a + b, 0) / posScores.length).toFixed(1))
|
|
159
|
-
: average;
|
|
160
|
-
|
|
161
|
-
const record = {
|
|
162
|
-
timestamp: new Date().toISOString(),
|
|
163
|
-
elapsed,
|
|
164
|
-
cases: results,
|
|
165
|
-
average,
|
|
166
|
-
averagePos,
|
|
167
|
-
caseCount: Object.keys(results).length,
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const tx = db.transaction("runs", "readwrite");
|
|
171
|
-
tx.objectStore("runs").add(record);
|
|
172
|
-
await new Promise((resolve, reject) => {
|
|
173
|
-
tx.oncomplete = resolve;
|
|
174
|
-
tx.onerror = () => reject(tx.error);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
console.log(`[native-diff-ext] History saved: avg=${average}% px / ${averagePos}% pos, ${Object.keys(results).length} cases`);
|
|
178
|
-
db.close();
|
|
179
|
-
} catch (e) {
|
|
180
|
-
console.error("[native-diff-ext] Failed to save history:", e);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Expose history reader for dashboard pages
|
|
185
|
-
window.__getCanonicalHistory = async function() {
|
|
186
|
-
const db = await openHistoryDB();
|
|
187
|
-
const tx = db.transaction("runs", "readonly");
|
|
188
|
-
const store = tx.objectStore("runs");
|
|
189
|
-
return new Promise((resolve, reject) => {
|
|
190
|
-
const req = store.getAll();
|
|
191
|
-
req.onsuccess = () => { db.close(); resolve(req.result); };
|
|
192
|
-
req.onerror = () => { db.close(); reject(req.error); };
|
|
193
|
-
});
|
|
194
|
-
};
|