ui-design-workbench-cli 0.5.0__py3-none-any.whl

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 (73) hide show
  1. android_resource_resolver.py +370 -0
  2. android_xml_support.py +230 -0
  3. apple_resource_resolver.py +141 -0
  4. coverage_report.py +134 -0
  5. fidelity_adapter_api.py +89 -0
  6. fidelity_adapters.py +508 -0
  7. fidelity_core.py +241 -0
  8. fidelity_platform_adapters.py +902 -0
  9. ir_contracts.py +76 -0
  10. native_render_android.py +160 -0
  11. native_render_apple.py +165 -0
  12. native_render_contracts.py +148 -0
  13. native_render_registry.py +86 -0
  14. quality_common.py +217 -0
  15. render_preview.py +1797 -0
  16. scan_ui.py +1485 -0
  17. scoped_context.py +458 -0
  18. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/SKILL.md +52 -0
  19. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/adapter-api.md +20 -0
  20. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/agent-integrations.md +39 -0
  21. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/cache-protocol.md +61 -0
  22. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/design-modes.md +109 -0
  23. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/fidelity.md +53 -0
  24. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/ir-schema.md +412 -0
  25. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/native-rendering.md +40 -0
  26. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/platform-profiles.json +84 -0
  27. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/platform-standards.md +131 -0
  28. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/platforms.md +40 -0
  29. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/quality-automation.md +77 -0
  30. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/review-workflow.md +135 -0
  31. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/ui-guidance-mode.md +22 -0
  32. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/ui-policy-schema.md +83 -0
  33. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/ui-reviewer.md +175 -0
  34. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/validation.md +34 -0
  35. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/references/workbench-ui.md +108 -0
  36. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/native-render-state.schema.json +71 -0
  37. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/ui-agent-job.schema.json +36 -0
  38. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/ui-graph.schema.json +46 -0
  39. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/ui-ir.patch.schema.json +36 -0
  40. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/ui-ir.schema.json +296 -0
  41. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/schemas/uidw-config.schema.json +68 -0
  42. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/android_resource_resolver.py +370 -0
  43. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/android_xml_support.py +230 -0
  44. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/apple_resource_resolver.py +141 -0
  45. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/coverage_report.py +134 -0
  46. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/fidelity_adapter_api.py +89 -0
  47. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/fidelity_adapters.py +508 -0
  48. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/fidelity_core.py +241 -0
  49. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/fidelity_platform_adapters.py +902 -0
  50. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/generate_interaction_matrix.py +166 -0
  51. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/ir_contracts.py +76 -0
  52. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/merge_review_state.py +267 -0
  53. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/native_render_android.py +160 -0
  54. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/native_render_apple.py +165 -0
  55. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/native_render_contracts.py +148 -0
  56. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/native_render_registry.py +86 -0
  57. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/quality_common.py +217 -0
  58. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/render_preview.py +1797 -0
  59. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/scan_ui.py +1485 -0
  60. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/scoped_context.py +458 -0
  61. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/smoke_preview.js +978 -0
  62. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/uidw.py +2821 -0
  63. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/uidw_mcp.py +282 -0
  64. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/validate_platform_profiles.py +93 -0
  65. ui_design_workbench_cli-0.5.0.data/data/share/ui-design-workbench/scripts/visual_regression.py +141 -0
  66. ui_design_workbench_cli-0.5.0.dist-info/METADATA +133 -0
  67. ui_design_workbench_cli-0.5.0.dist-info/RECORD +73 -0
  68. ui_design_workbench_cli-0.5.0.dist-info/WHEEL +5 -0
  69. ui_design_workbench_cli-0.5.0.dist-info/entry_points.txt +3 -0
  70. ui_design_workbench_cli-0.5.0.dist-info/top_level.txt +20 -0
  71. uidw.py +2821 -0
  72. uidw_mcp.py +282 -0
  73. validate_platform_profiles.py +93 -0
@@ -0,0 +1,978 @@
1
+ #!/usr/bin/env node
2
+ /* Run the standalone preview Diagnostics Runner in headless Chromium without app servers. */
3
+
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+ const { pathToFileURL } = require('url');
8
+ const { spawn } = require('child_process');
9
+
10
+ function parseArgs(argv) {
11
+ const result = { input: null, output: null, screenshot: null, geometryOutput: null, captureView: null, captureScreen: null, captureLeftPanel: null, captureRightPanel: null, captureInspectorTab: null, captureReviewSection: null, viewportWidth: 1440, viewportHeight: 960, mode: 'projection', failOnFindings: false, captureOnly: false, timeoutMs: 60000 };
12
+ for (let index = 0; index < argv.length; index += 1) {
13
+ const value = argv[index];
14
+ if (value === '--output') result.output = argv[++index];
15
+ else if (value === '--screenshot') result.screenshot = argv[++index];
16
+ else if (value === '--geometry-output') result.geometryOutput = argv[++index];
17
+ else if (value === '--capture-view') result.captureView = argv[++index];
18
+ else if (value === '--capture-screen') result.captureScreen = argv[++index];
19
+ else if (value === '--capture-left-panel') result.captureLeftPanel = argv[++index];
20
+ else if (value === '--capture-right-panel') result.captureRightPanel = argv[++index];
21
+ else if (value === '--capture-inspector-tab') result.captureInspectorTab = argv[++index];
22
+ else if (value === '--capture-review-section') result.captureReviewSection = argv[++index];
23
+ else if (value === '--viewport-width') result.viewportWidth = Number(argv[++index]);
24
+ else if (value === '--viewport-height') result.viewportHeight = Number(argv[++index]);
25
+ else if (value === '--mode') result.mode = argv[++index];
26
+ else if (value === '--fail-on-findings') result.failOnFindings = true;
27
+ else if (value === '--capture-only') result.captureOnly = true;
28
+ else if (value === '--timeout-ms') result.timeoutMs = Number(argv[++index]);
29
+ else if (!result.input) result.input = value;
30
+ else throw new Error(`Unexpected argument: ${value}`);
31
+ }
32
+ if (!result.input) throw new Error('Usage: smoke_preview.js <ui-preview.html> [--mode projection|review] [--output diagnostics.json] [--screenshot preview.png] [--geometry-output geometry.json] [--capture-only] [--capture-view overview|prototype|single|states|compare] [--capture-screen ID] [--capture-left-panel open|closed] [--capture-right-panel open|closed] [--capture-inspector-tab inspect|review|comments] [--capture-review-section summary|problems|changes] [--viewport-width 1440] [--viewport-height 960] [--fail-on-findings]');
33
+ if (!['projection', 'review'].includes(result.mode)) throw new Error('--mode must be projection or review');
34
+ if (result.captureView && !['overview', 'prototype', 'single', 'states', 'compare'].includes(result.captureView)) throw new Error('--capture-view must be overview, prototype, single, states, or compare');
35
+ if (result.captureLeftPanel && !['open', 'closed'].includes(result.captureLeftPanel)) throw new Error('--capture-left-panel must be open or closed');
36
+ if (result.captureRightPanel && !['open', 'closed'].includes(result.captureRightPanel)) throw new Error('--capture-right-panel must be open or closed');
37
+ if (result.captureInspectorTab && !['inspect', 'review', 'comments'].includes(result.captureInspectorTab)) throw new Error('--capture-inspector-tab must be inspect, review, or comments');
38
+ if (result.captureReviewSection && !['summary', 'problems', 'changes'].includes(result.captureReviewSection)) throw new Error('--capture-review-section must be summary, problems, or changes');
39
+ if (!Number.isFinite(result.viewportWidth) || result.viewportWidth < 320 || !Number.isFinite(result.viewportHeight) || result.viewportHeight < 320) throw new Error('Viewport width and height must be at least 320');
40
+ return result;
41
+ }
42
+
43
+ function chromeCandidates() {
44
+ const values = [process.env.CHROME_PATH];
45
+ if (process.platform === 'win32') {
46
+ values.push(
47
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
48
+ 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
49
+ 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
50
+ );
51
+ } else if (process.platform === 'darwin') {
52
+ values.push('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge');
53
+ } else {
54
+ values.push('/usr/bin/google-chrome', '/usr/bin/chromium', '/usr/bin/chromium-browser', '/usr/bin/microsoft-edge');
55
+ }
56
+ return values.filter(Boolean);
57
+ }
58
+
59
+ function findChrome() {
60
+ const executable = chromeCandidates().find(candidate => fs.existsSync(candidate));
61
+ if (!executable) throw new Error('Chromium browser not found. Set CHROME_PATH to Chrome, Edge, or Chromium.');
62
+ return executable;
63
+ }
64
+
65
+ const delay = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));
66
+
67
+ async function waitForFile(file, timeoutMs) {
68
+ const deadline = Date.now() + timeoutMs;
69
+ while (Date.now() < deadline) {
70
+ if (fs.existsSync(file) && fs.statSync(file).size > 0) return;
71
+ await delay(100);
72
+ }
73
+ throw new Error(`Timed out waiting for ${file}`);
74
+ }
75
+
76
+ async function connectCdp(webSocketUrl) {
77
+ const socket = new WebSocket(webSocketUrl);
78
+ await new Promise((resolve, reject) => {
79
+ socket.addEventListener('open', resolve, { once: true });
80
+ socket.addEventListener('error', () => reject(new Error('Cannot connect to Chromium DevTools')), { once: true });
81
+ });
82
+ let nextId = 1;
83
+ const pending = new Map();
84
+ socket.addEventListener('message', event => {
85
+ const payload = JSON.parse(String(event.data));
86
+ if (!payload.id || !pending.has(payload.id)) return;
87
+ const { resolve, reject } = pending.get(payload.id);
88
+ pending.delete(payload.id);
89
+ if (payload.error) reject(new Error(payload.error.message));
90
+ else resolve(payload.result);
91
+ });
92
+ const call = (method, params = {}) => new Promise((resolve, reject) => {
93
+ const id = nextId++;
94
+ pending.set(id, { resolve, reject });
95
+ socket.send(JSON.stringify({ id, method, params }));
96
+ });
97
+ return { socket, call };
98
+ }
99
+
100
+ async function main() {
101
+ const args = parseArgs(process.argv.slice(2));
102
+ const input = path.resolve(args.input);
103
+ if (!fs.existsSync(input)) throw new Error(`Preview not found: ${input}`);
104
+ const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'ui-design-workbench-smoke-'));
105
+ const activePort = path.join(profile, 'DevToolsActivePort');
106
+ const url = pathToFileURL(input);
107
+ url.searchParams.set('diagnostics', 'run');
108
+ url.searchParams.set('diagnostic-mode', args.mode);
109
+ url.searchParams.set('lang', 'ru');
110
+ const browser = spawn(findChrome(), [
111
+ '--headless=new',
112
+ '--disable-gpu',
113
+ '--no-first-run',
114
+ '--disable-background-networking',
115
+ '--remote-debugging-port=0',
116
+ `--user-data-dir=${profile}`,
117
+ url.href,
118
+ ], { stdio: 'ignore', windowsHide: true });
119
+ let socket;
120
+ let cdp;
121
+ try {
122
+ await waitForFile(activePort, Math.min(args.timeoutMs, 15000));
123
+ const [port] = fs.readFileSync(activePort, 'utf8').trim().split(/\r?\n/);
124
+ let targets = [];
125
+ const targetDeadline = Date.now() + 15000;
126
+ while (Date.now() < targetDeadline) {
127
+ targets = await fetch(`http://127.0.0.1:${port}/json/list`).then(response => response.json());
128
+ if (targets.some(target => target.type === 'page' && target.url.startsWith('file:'))) break;
129
+ await delay(100);
130
+ }
131
+ const target = targets.find(item => item.type === 'page' && item.url.startsWith('file:'));
132
+ if (!target) throw new Error('Preview page was not created by Chromium');
133
+ cdp = await connectCdp(target.webSocketDebuggerUrl);
134
+ socket = cdp.socket;
135
+ await cdp.call('Runtime.enable');
136
+ await cdp.call('Emulation.setDeviceMetricsOverride', { width: args.viewportWidth, height: args.viewportHeight, deviceScaleFactor: 1, mobile: false });
137
+ const deadline = Date.now() + args.timeoutMs;
138
+ let status = 'idle';
139
+ while (Date.now() < deadline) {
140
+ const response = await cdp.call('Runtime.evaluate', { expression: "document.documentElement.dataset.diagnosticsStatus || 'idle'", returnByValue: true });
141
+ status = response.result?.value || 'idle';
142
+ if (status === 'complete') break;
143
+ await delay(200);
144
+ }
145
+ if (status !== 'complete') throw new Error(`Diagnostics did not finish within ${args.timeoutMs}ms (status=${status})`);
146
+ const response = await cdp.call('Runtime.evaluate', { expression: "document.getElementById('runtime-diagnostics-report').textContent", returnByValue: true });
147
+ const report = JSON.parse(response.result?.value || 'null');
148
+ if (!report || report.status !== 'complete') throw new Error('Diagnostics report is missing or incomplete');
149
+ const initialVisualStateResponse = await cdp.call('Runtime.evaluate', {
150
+ expression: `({
151
+ view: state.view,
152
+ screen: state.screen,
153
+ activeVersion: state.activeVersion,
154
+ compareBaseVersion: state.compareBaseVersion,
155
+ compareTargetVersion: state.compareTargetVersion,
156
+ versionIds: versions.map(version => version.id),
157
+ versionDecision: state.versionDecision,
158
+ findingDecisions: JSON.parse(JSON.stringify(state.findingDecisions)),
159
+ findingFilter: state.findingFilter,
160
+ findingSource: state.findingSource,
161
+ findingScreen: state.findingScreen,
162
+ findingFocus: JSON.parse(JSON.stringify(state.findingFocus)),
163
+ runtimeFindings: JSON.parse(JSON.stringify(state.runtimeFindings)),
164
+ reviewSection: state.reviewSection,
165
+ reviewScope: state.reviewScope,
166
+ reviewRuns: JSON.parse(JSON.stringify(state.reviewRuns)),
167
+ importedReview: JSON.parse(JSON.stringify(state.importedReview)),
168
+ sidebarOpen: state.sidebarOpen,
169
+ inspectorOpen: state.inspectorOpen,
170
+ inspectorTab: state.inspectorTab,
171
+ zoomMode: state.zoomMode,
172
+ zoom: state.zoom,
173
+ selectedNodeId: state.selectedNodeId,
174
+ selectedScreenId: state.selectedScreenId,
175
+ focusedFindingId: state.focusedFindingId,
176
+ openFindingId: state.openFindingId,
177
+ showFindings: state.showFindings,
178
+ showBefore: state.showBefore,
179
+ showAfter: state.showAfter,
180
+ layerReturnView: state.layerReturnView,
181
+ diagnosticTargetIds: JSON.parse(JSON.stringify(state.diagnosticTargetIds)),
182
+ stageScroll: JSON.parse(JSON.stringify(state.stageScroll)),
183
+ screenScrolls: JSON.parse(JSON.stringify(state.screenScrolls)),
184
+ })`,
185
+ returnByValue: true,
186
+ });
187
+ const initialVisualState = initialVisualStateResponse.result?.value || {};
188
+ const workflowResponse = args.captureOnly ? { result: { value: {} } } : await cdp.call('Runtime.evaluate', {
189
+ expression: `(async () => {
190
+ const totalBefore = Number(document.querySelector('.finding-total')?.textContent || 0);
191
+ const auditLink = document.querySelector('[data-audit-findings]');
192
+ if (auditLink) auditLink.click();
193
+ const linkedVisible = !document.querySelector('.finding-context')?.hidden;
194
+ document.querySelector('.finding-context button')?.click();
195
+ const diagnosticAction = [...document.querySelectorAll('[data-accept-diagnostic]')]
196
+ .find(button => button.closest('.diagnostic-card')?.dataset.result !== 'pass');
197
+ diagnosticAction?.click();
198
+ const diagnosticFindingCreated = Boolean(diagnosticAction);
199
+ const staleRuntimeId = 'runtime-stale-smoke';
200
+ state.runtimeFindings.push({
201
+ id: staleRuntimeId, title: 'Stale runtime finding', category: 'runtime-diagnostic', severity: 'medium', confidence: 'high',
202
+ screenId: screens[0].id, nodeId: null, observation: 'Old engine result.', impact: 'Noise.', recommendation: 'Recheck.',
203
+ evidence: [{ type: 'heuristic', ref: 'runtime-diagnostics.old', note: 'Synthetic stale result.' }], effort: 'small', runtimeDiagnosticId: 'old-check'
204
+ });
205
+ state.findingDecisions[staleRuntimeId] = 'accepted';
206
+ let diagnosticDownloads = 0;
207
+ const originalAnchorClick = HTMLAnchorElement.prototype.click;
208
+ HTMLAnchorElement.prototype.click = function smokeDownloadGuard() {
209
+ if (this.download) diagnosticDownloads += 1;
210
+ return originalAnchorClick.call(this);
211
+ };
212
+ try {
213
+ await runAutomatedReview();
214
+ } finally {
215
+ HTMLAnchorElement.prototype.click = originalAnchorClick;
216
+ }
217
+ const diagnosticsDoNotDownload = diagnosticDownloads === 0;
218
+ const staleRuntimePruned = !state.runtimeFindings.some(item => item.id === staleRuntimeId)
219
+ && !Object.prototype.hasOwnProperty.call(state.findingDecisions, staleRuntimeId);
220
+ const decisionsBeforeBulk = JSON.parse(JSON.stringify(state.findingDecisions));
221
+ if (document.querySelector('.review-next-action')?.dataset.action === 'problems') document.querySelector('.review-next-action')?.click();
222
+ if (document.querySelector('.review-next-action')?.dataset.action === 'select-all') document.querySelector('.review-next-action')?.click();
223
+ const bulkSelected = selectedFindings().length;
224
+ const bulkSelectionPathWorks = bulkSelected === findings.filter(item => !findingResolved(item)).length
225
+ && state.reviewSection === 'problems'
226
+ && document.querySelector('.review-next-action')?.dataset.action === 'changes'
227
+ && document.querySelector('.review-next-action')?.textContent?.includes(String(bulkSelected));
228
+ state.findingDecisions = decisionsBeforeBulk;
229
+ renderFindings();
230
+ const isProductFinding = item => item.result !== 'pass' && item.metrics?.scope !== 'workbench';
231
+ const actionableChecks = (state.diagnostics?.checks || []).filter(isProductFinding).length;
232
+ const actionableGroups = groupDiagnosticChecks((state.diagnostics?.checks || []).filter(isProductFinding)).length;
233
+ const totalAfter = Number(document.querySelector('.finding-total')?.textContent || 0);
234
+ const autoReviewFindings = state.runtimeFindings.length;
235
+ const selected = selectedFindings().length;
236
+ const selectionNextStep = selected === 0
237
+ || (state.reviewSection === 'problems'
238
+ && document.querySelector('.review-next-action')?.dataset.action === 'changes'
239
+ && document.querySelector('.review-next-action')?.textContent?.includes(String(selected)));
240
+ const request = selected && typeof fixRequestPayload === 'function' ? fixRequestPayload() : null;
241
+ const expertRequest = typeof expertReviewRequestPayload === 'function' ? expertReviewRequestPayload() : null;
242
+ const expertHandoff = window.__uiPreviewDiagnostics?.agentHandoff?.('expert');
243
+ const proposalHandoff = window.__uiPreviewDiagnostics?.agentHandoff?.('proposal');
244
+ state.agentHandoff = {
245
+ kind: 'proposal', status: 'prepared', createdAt: new Date().toISOString(),
246
+ acceptedFindingIds: proposalHandoff?.context?.acceptedFindingIds || [],
247
+ screenIds: proposalHandoff?.context?.screenIds || [], artifactDir: proposalHandoff?.path || ''
248
+ };
249
+ renderAgentHandoff();
250
+ const handoffPanelWorks = document.querySelector('.agent-handoff-panel')?.hidden === false
251
+ && Boolean(document.querySelector('.copy-agent-task'))
252
+ && Boolean(document.querySelector('.refresh-preview'))
253
+ && document.querySelector('.agent-handoff-meta')?.textContent?.includes('выбранном AI-агенте');
254
+ state.agentHandoff = null;
255
+ renderAgentHandoff();
256
+ const runtimeActionable = Boolean(request?.findings?.some(item => item.runtimeDiagnosticId));
257
+ const contextProposalAction = ['proposal', 'compare-selected', 'approve', 'source'].includes(document.querySelector('.review-next-action')?.dataset.action);
258
+ const smokeFinding = {
259
+ id: 'smoke-imported-finding', title: 'Smoke imported finding', category: 'smoke', severity: 'low', confidence: 'high',
260
+ screenId: screens[0].id, nodeId: nodeIdsForScreen(screens[0])[0] || null,
261
+ observation: 'Imported during smoke.', impact: 'None.', recommendation: 'None.',
262
+ evidence: [{ type: 'source', ref: 'smoke', note: 'Synthetic smoke fixture.' }], effort: 'small', proposalVersionId: 'smoke-imported-proposal'
263
+ };
264
+ importExpertReviewData({
265
+ type: 'ui-design-workbench-expert-review-result', requestRevision: reviewRevision, project: ir.project?.name,
266
+ summary: 'Smoke import', findings: [smokeFinding],
267
+ versions: [{ id: 'smoke-imported-proposal', label: 'Smoke proposal', kind: 'proposal', parent: baselineVersion, findingIds: [smokeFinding.id], resolvedFindingIds: [smokeFinding.id], nodeOverrides: {} }]
268
+ });
269
+ const importWorks = state.importedReview?.addedVersions === 1
270
+ && findings.some(item => item.id === smokeFinding.id)
271
+ && versionById['smoke-imported-proposal'];
272
+ state.compareBaseVersion = versions[1]?.id || baselineVersion;
273
+ state.compareTargetVersion = 'smoke-imported-proposal';
274
+ state.findingFocus = [];
275
+ state.findingFilter = 'all';
276
+ state.findingSource = 'all';
277
+ state.findingScreen = 'all';
278
+ state.showFindings = true;
279
+ const smokeAnnotation = {
280
+ id: 'smoke-canvas-comment', screenId: screens[0].id, nodeId: null,
281
+ versionId: 'smoke-imported-proposal', text: 'Smoke user comment.', priority: 'medium', status: 'new'
282
+ };
283
+ state.annotations.push(smokeAnnotation);
284
+ renderQueue();
285
+ renderCompareVersionOptions();
286
+ setScreen(screens[0].id, 'compare');
287
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
288
+ const compareVariantsWork = document.querySelectorAll('.compare-panel').length === 2
289
+ && getComputedStyle(document.querySelector('.header-version-picker')).display === 'none'
290
+ && document.querySelectorAll('.compare-version-select').length === 2
291
+ && getComputedStyle(document.querySelector('.compare-workspace-controls')).display === 'flex';
292
+ const compareSelectorsMatchCanvas = document.querySelector('.compare-base-select')?.value === state.compareBaseVersion
293
+ && document.querySelector('.compare-target-select')?.value === state.compareTargetVersion
294
+ && Boolean(document.querySelector('.device[data-version-id="' + CSS.escape(state.compareBaseVersion) + '"]'))
295
+ && Boolean(document.querySelector('.device[data-version-id="' + CSS.escape(state.compareTargetVersion) + '"]'));
296
+ const smokeMarker = document.querySelector('[data-open-finding="' + CSS.escape(smokeFinding.id) + '"]');
297
+ const smokeCommentMarker = document.querySelector('[data-open-annotation="' + CSS.escape(smokeAnnotation.id) + '"]');
298
+ const baseDeviceRect = document.querySelector('.device[data-version-id="' + CSS.escape(state.compareBaseVersion) + '"]')?.getBoundingClientRect();
299
+ const targetDeviceRect = document.querySelector('.device[data-version-id="' + CSS.escape(state.compareTargetVersion) + '"]')?.getBoundingClientRect();
300
+ const findingMarkerRect = smokeMarker?.getBoundingClientRect();
301
+ const commentMarkerRect = smokeCommentMarker?.getBoundingClientRect();
302
+ const markersFollowComparedViews = Boolean(baseDeviceRect && targetDeviceRect && findingMarkerRect && commentMarkerRect
303
+ && findingMarkerRect.left + findingMarkerRect.width / 2 < baseDeviceRect.left
304
+ && commentMarkerRect.left + commentMarkerRect.width / 2 > targetDeviceRect.right);
305
+ const userCommentHasDistinctColor = Boolean(smokeCommentMarker
306
+ && getComputedStyle(smokeCommentMarker).backgroundColor === 'rgb(124, 58, 237)');
307
+ smokeCommentMarker?.click();
308
+ await new Promise(resolve => requestAnimationFrame(resolve));
309
+ const commentPopoverOpens = Boolean(document.querySelector('[data-annotation-popover="' + CSS.escape(smokeAnnotation.id) + '"]'));
310
+ document.querySelector('[data-collapse-annotation="' + CSS.escape(smokeAnnotation.id) + '"]')?.click();
311
+ await new Promise(resolve => requestAnimationFrame(resolve));
312
+ const commentPopoverCollapses = !document.querySelector('[data-annotation-popover="' + CSS.escape(smokeAnnotation.id) + '"]')
313
+ && Boolean(document.querySelector('[data-open-annotation="' + CSS.escape(smokeAnnotation.id) + '"]'));
314
+ const smokeListNumber = document.querySelector('.finding-card[data-finding-id="' + CSS.escape(smokeFinding.id) + '"] .finding-list-index')?.textContent?.trim();
315
+ const markerNumberMatches = Boolean(smokeMarker && smokeMarker.textContent.trim() === smokeListNumber);
316
+ smokeMarker?.click();
317
+ await new Promise(resolve => requestAnimationFrame(resolve));
318
+ const markerPopoverOpens = Boolean(document.querySelector('[data-finding-popover="' + CSS.escape(smokeFinding.id) + '"]'));
319
+ document.querySelector('[data-collapse-finding="' + CSS.escape(smokeFinding.id) + '"]')?.click();
320
+ await new Promise(resolve => requestAnimationFrame(resolve));
321
+ const markerPopoverCollapses = !document.querySelector('[data-finding-popover="' + CSS.escape(smokeFinding.id) + '"]')
322
+ && Boolean(document.querySelector('[data-open-finding="' + CSS.escape(smokeFinding.id) + '"]'));
323
+ const findingsToggle = document.querySelector('[data-canvas-layer="findings"]');
324
+ findingsToggle?.click();
325
+ await new Promise(resolve => requestAnimationFrame(resolve));
326
+ const markersHide = document.querySelectorAll('.finding-pin,.finding-pin-card').length === 0
327
+ && findingsToggle?.getAttribute('aria-pressed') === 'false'
328
+ && Boolean(document.querySelector('[data-open-annotation="' + CSS.escape(smokeAnnotation.id) + '"]'));
329
+ findingsToggle?.click();
330
+ await new Promise(resolve => requestAnimationFrame(resolve));
331
+ const markersRestore = Boolean(document.querySelector('[data-open-finding="' + CSS.escape(smokeFinding.id) + '"]'))
332
+ && findingsToggle?.getAttribute('aria-pressed') === 'true';
333
+ state.activeVersion = 'smoke-imported-proposal';
334
+ versionSelect.value = state.activeVersion;
335
+ const navigationSource = screens.find(screen => nodeIdsForScreen(screen).some(nodeId => nodes[nodeId]?.action?.type === 'navigate' && nodes[nodeId]?.action?.target !== screen.id));
336
+ let prototypeNavigationWorks = true;
337
+ let prototypeTreePreservesMode = true;
338
+ let prototypeForcesInteraction = true;
339
+ if (navigationSource) {
340
+ const navigationNodeId = nodeIdsForScreen(navigationSource).find(nodeId => nodes[nodeId]?.action?.type === 'navigate' && nodes[nodeId]?.action?.target !== navigationSource.id);
341
+ const navigationTarget = nodes[navigationNodeId]?.action?.target;
342
+ state.interaction = 'inspect';
343
+ setScreen(navigationSource.id, 'single');
344
+ setView('prototype');
345
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
346
+ prototypeForcesInteraction = state.interaction === 'interact';
347
+ document.querySelector('.stage [data-node-id="' + CSS.escape(navigationNodeId) + '"]')?.click();
348
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
349
+ prototypeNavigationWorks = state.view === 'prototype' && state.screen === navigationTarget;
350
+ const treeTarget = screens.find(screen => screen.id !== state.screen)?.id;
351
+ if (treeTarget) {
352
+ document.querySelector('.screen-link[data-screen="' + CSS.escape(treeTarget) + '"]')?.click();
353
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
354
+ prototypeTreePreservesMode = state.view === 'prototype' && state.screen === treeTarget;
355
+ }
356
+ }
357
+
358
+ const scenarioScreen = screens[0];
359
+ const scenarioNodeId = scenarioScreen ? nodeIdsForScreen(scenarioScreen).find(nodeId => nodes[nodeId]?.type === 'text') : null;
360
+ let screenScenarioControlsWork = true;
361
+ let stateGalleryWorks = true;
362
+ if (scenarioScreen && scenarioNodeId) {
363
+ const originalScenarios = scenarioScreen.scenarios;
364
+ scenarioScreen.scenarios = [...(Array.isArray(originalScenarios) ? originalScenarios : []), { id: 'smoke-fixture', label: 'Smoke fixture', nodeOverrides: { [scenarioNodeId]: { text: 'Scenario fixture visible' } } }];
365
+ setScreen(scenarioScreen.id, 'single');
366
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
367
+ const scenarioSelect = document.querySelector('.screen-state-select[data-screen-scenario]');
368
+ const initialControlWidth = scenarioSelect?.getBoundingClientRect().width || 0;
369
+ if (scenarioSelect) {
370
+ scenarioSelect.value = 'smoke-fixture';
371
+ scenarioSelect.dispatchEvent(new Event('change', { bubbles: true }));
372
+ }
373
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
374
+ const updatedScenarioSelect = document.querySelector('.screen-state-select[data-screen-scenario]');
375
+ screenScenarioControlsWork = Boolean(scenarioSelect)
376
+ && state.screenScenarioIds[scenarioScreen.id] === 'smoke-fixture'
377
+ && document.querySelector('.stage [data-node-id="' + CSS.escape(scenarioNodeId) + '"]')?.textContent === 'Scenario fixture visible'
378
+ && updatedScenarioSelect?.value === 'smoke-fixture'
379
+ && updatedScenarioSelect?.closest('.screen-control-group')?.querySelector('.screen-state-label')
380
+ && Math.abs((updatedScenarioSelect?.getBoundingClientRect().width || 0) - initialControlWidth) < 1
381
+ && document.activeElement === updatedScenarioSelect;
382
+ setView('states');
383
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
384
+ stateGalleryWorks = state.view === 'states'
385
+ && document.querySelectorAll('.state-gallery .state-panel').length === screenScenarios(scenarioScreen).length
386
+ && document.querySelectorAll('.state-gallery .device').length === screenScenarios(scenarioScreen).length
387
+ && Boolean(document.querySelector('.screen-state-select[data-gallery-scenario]'))
388
+ && (themes.length < 2 || Boolean(document.querySelector('.screen-state-select[data-gallery-theme]')))
389
+ && document.querySelector('.variant-gallery-toolbar')?.style.zoom === ''
390
+ && Number.parseFloat(document.querySelector('.variant-canvas')?.style.zoom || '0') > 0;
391
+ const matrixButton = document.querySelector('[data-variant-axis="matrix"]');
392
+ if (matrixButton) {
393
+ matrixButton.click();
394
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
395
+ stateGalleryWorks = stateGalleryWorks
396
+ && state.variantAxis === 'matrix'
397
+ && Boolean(document.querySelector('.screen-state-select[data-gallery-scenario]'))
398
+ && Boolean(document.querySelector('.screen-state-select[data-gallery-theme]'))
399
+ && document.querySelectorAll('.state-gallery .device').length === themes.length * screenScenarios(scenarioScreen).length;
400
+ }
401
+ if (originalScenarios === undefined) delete scenarioScreen.scenarios; else scenarioScreen.scenarios = originalScenarios;
402
+ delete state.screenScenarioIds[scenarioScreen.id];
403
+ }
404
+
405
+ setScreen(screens[0].id, 'single');
406
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
407
+ const resolvedMarkerHidden = !document.querySelector('[data-open-finding="' + CSS.escape(smokeFinding.id) + '"]');
408
+ const proposalMarkersHidden = document.querySelectorAll('[data-open-finding],[data-finding-popover]').length === 0;
409
+ state.activeVersion = baselineVersion;
410
+ versionSelect.value = state.activeVersion;
411
+ renderView();
412
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
413
+ const baselineMarkerRestored = Boolean(document.querySelector('[data-open-finding="' + CSS.escape(smokeFinding.id) + '"]'));
414
+ setView('compare');
415
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
416
+ const resolvedMarkersStayOnBaseline = document.querySelectorAll('[data-open-finding="' + CSS.escape(smokeFinding.id) + '"]').length === 1;
417
+ const compareFindingMarkersBaselineOnly = [...document.querySelectorAll('[data-open-finding],[data-finding-popover]')]
418
+ .every(marker => marker.dataset.markerDevice?.includes(':' + state.compareBaseVersion + ':'));
419
+ const versionArchitectureWorks = resolvedMarkerHidden && proposalMarkersHidden && baselineMarkerRestored
420
+ && resolvedMarkersStayOnBaseline && compareFindingMarkersBaselineOnly;
421
+ state.activeVersion = 'smoke-imported-proposal';
422
+ versionSelect.value = state.activeVersion;
423
+ setReviewSection('changes');
424
+ renderFindings();
425
+ const addressedCard = document.querySelector('.finding-card[data-finding-id="' + CSS.escape(smokeFinding.id) + '"]');
426
+ const addressedIsNotVerified = findingAddressedInProposal(smokeFinding)
427
+ && !findingResolved(smokeFinding)
428
+ && addressedCard?.querySelector('.finding-card-status')?.textContent?.trim() === 'Учтено в новом макете'
429
+ && document.querySelector('.version-application-status')?.textContent?.trim() === 'Ещё не применён к проекту'
430
+ && !document.querySelector('[data-review-section="changes"]')?.classList.contains('complete');
431
+ state.compareBaseVersion = baselineVersion;
432
+ state.compareTargetVersion = 'smoke-imported-proposal';
433
+ setView('compare');
434
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
435
+ const markerBeforeZoom = document.querySelector('[data-open-finding="' + CSS.escape(smokeFinding.id) + '"]')?.getBoundingClientRect();
436
+ const deviceBeforeZoom = document.querySelector('.device[data-version-id="' + CSS.escape(state.compareBaseVersion) + '"]')?.getBoundingClientRect();
437
+ const anchorBeforeZoom = document.querySelector('.device[data-version-id="' + CSS.escape(state.compareBaseVersion) + '"] .device-content')?.getBoundingClientRect();
438
+ const markerOffsetBeforeZoom = markerBeforeZoom && deviceBeforeZoom && anchorBeforeZoom ? (() => {
439
+ const center = markerBeforeZoom.left + markerBeforeZoom.width / 2;
440
+ const side = center < deviceBeforeZoom.left ? 'left' : 'right';
441
+ const centerY = markerBeforeZoom.top + markerBeforeZoom.height / 2;
442
+ return { side, x: center - (side === 'left' ? deviceBeforeZoom.left : deviceBeforeZoom.right), y: centerY - (anchorBeforeZoom.top + Math.min(12, anchorBeforeZoom.height / 2)), withinDeviceY: centerY >= deviceBeforeZoom.top - 1 && centerY <= deviceBeforeZoom.bottom + 1 };
443
+ })() : null;
444
+ setZoom(.8);
445
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
446
+ const zoomedMarker = document.querySelector('[data-open-finding="' + CSS.escape(smokeFinding.id) + '"]');
447
+ const zoomedDevice = document.querySelector('.device[data-version-id="' + CSS.escape(state.compareBaseVersion) + '"]');
448
+ const zoomedAnchor = zoomedDevice?.querySelector('.device-content');
449
+ const markerRect = zoomedMarker?.getBoundingClientRect();
450
+ const deviceRect = zoomedDevice?.getBoundingClientRect();
451
+ const anchorRect = zoomedAnchor?.getBoundingClientRect();
452
+ const markerOffsetAfterZoom = markerRect && deviceRect && anchorRect ? (() => {
453
+ const center = markerRect.left + markerRect.width / 2;
454
+ const side = center < deviceRect.left ? 'left' : 'right';
455
+ const centerY = markerRect.top + markerRect.height / 2;
456
+ return { side, x: center - (side === 'left' ? deviceRect.left : deviceRect.right), y: centerY - (anchorRect.top + Math.min(12, anchorRect.height / 2)), withinDeviceY: centerY >= deviceRect.top - 1 && centerY <= deviceRect.bottom + 1 };
457
+ })() : null;
458
+ const markerTracksZoom = Boolean(markerOffsetBeforeZoom && markerOffsetAfterZoom
459
+ && markerOffsetBeforeZoom.side === markerOffsetAfterZoom.side
460
+ && Math.abs(markerOffsetBeforeZoom.x) <= 70 && Math.abs(markerOffsetAfterZoom.x) <= 70
461
+ && markerOffsetBeforeZoom.withinDeviceY && markerOffsetAfterZoom.withinDeviceY);
462
+ const markerTrackMetrics = { before: markerOffsetBeforeZoom, after: markerOffsetAfterZoom };
463
+ const markerGroups = new Map();
464
+ for (const marker of document.querySelectorAll('.finding-pin[data-marker-device],.annotation-pin[data-marker-device]')) {
465
+ const group = markerGroups.get(marker.dataset.markerDevice) || [];
466
+ group.push(marker.getBoundingClientRect());
467
+ markerGroups.set(marker.dataset.markerDevice, group);
468
+ }
469
+ let markerOverlaps = 0;
470
+ for (const rects of markerGroups.values()) for (let first = 0; first < rects.length; first += 1) {
471
+ for (let second = first + 1; second < rects.length; second += 1) {
472
+ const x = Math.max(0, Math.min(rects[first].right, rects[second].right) - Math.max(rects[first].left, rects[second].left));
473
+ const y = Math.max(0, Math.min(rects[first].bottom, rects[second].bottom) - Math.max(rects[first].top, rects[second].top));
474
+ if (x * y > 1) markerOverlaps += 1;
475
+ }
476
+ }
477
+ const markerLayoutStable = markerGroups.size > 0 && markerOverlaps === 0;
478
+ document.querySelectorAll('[data-collapse-finding]').forEach(button => button.click());
479
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
480
+ const linkedMarker = document.querySelector('.finding-pin-leader + [data-open-finding="' + CSS.escape(smokeFinding.id) + '"]')
481
+ || document.querySelector('.finding-pin-leader + .finding-pin');
482
+ const leadersHiddenByDefault = [...document.querySelectorAll('.finding-pin-leader')]
483
+ .every(leader => leader.nextElementSibling?.matches(':hover') || Number(getComputedStyle(leader).opacity) === 0);
484
+ linkedMarker?.click();
485
+ await new Promise(resolve => setTimeout(resolve, 180));
486
+ const openLeader = document.querySelector('.finding-pin-leader + [data-finding-popover="' + CSS.escape(smokeFinding.id) + '"]')
487
+ || document.querySelector('.finding-pin-leader + .finding-pin-card');
488
+ const leaderRevealsForOpenCard = !linkedMarker || Boolean(openLeader && Number(getComputedStyle(openLeader).opacity) === 1);
489
+ document.querySelector('[data-collapse-finding]')?.click();
490
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
491
+ const markerLeaderWorks = leadersHiddenByDefault && leaderRevealsForOpenCard;
492
+ const markerLeaderMetrics = { leadersHiddenByDefault, leaderRevealsForOpenCard, linkedMarker: Boolean(linkedMarker), openLeader: Boolean(openLeader), openLeaderOpacity: openLeader ? getComputedStyle(openLeader).opacity : null };
493
+ const railCommandsWork = ['inspect', 'review', 'comments'].every(tab => Boolean(document.querySelector('.workbench-rail [data-inspector-tab="' + tab + '"]')))
494
+ && document.querySelectorAll('.workbench-rail .rail-menu').length === 2
495
+ && Boolean(document.querySelector('.workbench-rail .rail-menu.language-menu .locale-current'))
496
+ && Boolean(document.querySelector('.workbench-rail .rail-menu .export-feedback'))
497
+ && document.querySelectorAll('.canvas-tools [data-interaction]').length === 1
498
+ && Boolean(document.querySelector('.canvas-tools [data-interaction="interact"]'))
499
+ && !document.querySelector('.canvas-tools [data-canvas-layer]');
500
+ const fileRailMenu = document.querySelector('.workbench-rail .rail-menu:not(.language-menu)');
501
+ const languageRailMenu = document.querySelector('.workbench-rail .rail-menu.language-menu');
502
+ const fileRailSummaryRect = fileRailMenu?.querySelector('summary')?.getBoundingClientRect();
503
+ const fileRailIconRect = fileRailMenu?.querySelector('summary .icon')?.getBoundingClientRect();
504
+ const railFileGeometryWorks = Boolean(fileRailMenu && fileRailSummaryRect && fileRailIconRect
505
+ && document.querySelector('.workbench-rail')?.firstElementChild === fileRailMenu
506
+ && Math.abs((fileRailSummaryRect.left + fileRailSummaryRect.right - fileRailIconRect.left - fileRailIconRect.right) / 2) <= .5
507
+ && Math.abs((fileRailSummaryRect.top + fileRailSummaryRect.bottom - fileRailIconRect.top - fileRailIconRect.bottom) / 2) <= .5);
508
+ fileRailMenu?.querySelector('summary')?.click();
509
+ await new Promise(resolve => requestAnimationFrame(resolve));
510
+ const fileRailMenuOpens = fileRailMenu?.open === true;
511
+ languageRailMenu?.querySelector('summary')?.click();
512
+ await new Promise(resolve => requestAnimationFrame(resolve));
513
+ const railMenusAreExclusive = languageRailMenu?.open === true && fileRailMenu?.open === false;
514
+ document.body.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, button: 0 }));
515
+ await new Promise(resolve => requestAnimationFrame(resolve));
516
+ const railMenusDismiss = languageRailMenu?.open === false && fileRailMenu?.open === false;
517
+ const railMenusWork = fileRailMenuOpens && railMenusAreExclusive && railMenusDismiss;
518
+ state.annotations = state.annotations.filter(item => item.id !== smokeAnnotation.id);
519
+ state.openAnnotationId = null;
520
+ renderQueue();
521
+ renderFindingMarkers();
522
+ setWorkbenchLocale('en', { remember: false, updateLocation: false });
523
+ const localeMetrics = { englishCanvas: document.querySelector('.canvas-tools')?.getAttribute('aria-label'), englishVersionTitle: document.querySelector('.header-version-picker')?.getAttribute('title'), englishCompare: document.querySelector('.mode-button[data-view="compare"] .button-label')?.textContent?.trim() };
524
+ const englishLocaleWorks = document.documentElement.lang === 'en'
525
+ && document.querySelector('.canvas-tools')?.getAttribute('aria-label') === 'Canvas tools'
526
+ && document.querySelector('.header-version-picker')?.getAttribute('title') === 'Currently displayed mockup'
527
+ && document.querySelector('.mode-button[data-view="compare"] .button-label')?.textContent?.trim() === 'Compare';
528
+ setWorkbenchLocale('ru', { remember: false, updateLocation: false });
529
+ Object.assign(localeMetrics, { russianCanvas: document.querySelector('.canvas-tools')?.getAttribute('aria-label'), russianVersionTitle: document.querySelector('.header-version-picker')?.getAttribute('title'), russianCompare: document.querySelector('.mode-button[data-view="compare"] .button-label')?.textContent?.trim() });
530
+ const russianLocaleRestores = document.documentElement.lang === 'ru'
531
+ && document.querySelector('.canvas-tools')?.getAttribute('aria-label') === 'Инструменты холста'
532
+ && document.querySelector('.header-version-picker')?.getAttribute('title') === 'Какой макет сейчас показан'
533
+ && document.querySelector('.mode-button[data-view="compare"] .button-label')?.textContent?.trim() === 'Сравнить';
534
+ for (const id of Object.keys(state.findingDecisions)) state.findingDecisions[id] = 'pending';
535
+ state.findingDecisions[smokeFinding.id] = 'accepted';
536
+ renderFindings();
537
+ const sourceBeforeApproval = document.querySelector('.review-next-action')?.dataset.action !== 'source';
538
+ approveActiveVersion();
539
+ setReviewSection('summary');
540
+ const sourceHiddenOnSummary = document.querySelector('.review-next-action')?.dataset.action !== 'source';
541
+ setReviewSection('problems');
542
+ const sourceHiddenOnProblems = document.querySelector('.review-next-action')?.dataset.action !== 'source';
543
+ setReviewSection('changes');
544
+ const sourceAfterApproval = document.querySelector('.review-next-action')?.dataset.action === 'source';
545
+ const applyOnlyOnFixStep = sourceHiddenOnSummary && sourceHiddenOnProblems && sourceAfterApproval;
546
+ const sourceRequest = typeof sourceRequestPayload === 'function' ? sourceRequestPayload() : null;
547
+ const implementationHandoff = window.__uiPreviewDiagnostics?.agentHandoff?.('implement');
548
+ const directFixEntryPoint = Boolean(document.querySelector('.fix-all-source'));
549
+ const revisionVisible = Boolean(document.querySelector('.revision-badge')?.textContent?.trim());
550
+ const declaredScenarioCases = screens.flatMap(screen => screenScenarios(screen).slice(1).map(scenario => [screen.id, scenario.id]));
551
+ const screenScenarioDiagnosticsWork = declaredScenarioCases.every(([screenId, scenarioId]) => (state.diagnostics?.checks || []).some(check => check.scenarioId === 'state-matrix' && check.screenId === screenId && check.metrics?.screenScenarioId === scenarioId));
552
+
553
+ setScreen(screens[0].id, 'single');
554
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
555
+ const firstNode = document.querySelector('.stage .ui-node[data-node-id]');
556
+ const selectedNodeId = firstNode?.dataset.nodeId || null;
557
+ if (firstNode) selectNode(firstNode);
558
+ setZoom(.9);
559
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
560
+ const selectionPreserved = !selectedNodeId || document.querySelector('[data-node-id="' + CSS.escape(selectedNodeId) + '"]')?.dataset.selected === 'true';
561
+
562
+ setView('overview');
563
+ setZoom(2);
564
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
565
+ const panStage = document.querySelector('.stage');
566
+ panStage.scrollLeft = 0;
567
+ panStage.scrollTop = 0;
568
+ panStage.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, pointerId: 77, button: 1, buttons: 4, clientX: 420, clientY: 360 }));
569
+ panStage.dispatchEvent(new PointerEvent('pointermove', { bubbles: true, pointerId: 77, button: -1, buttons: 4, clientX: 350, clientY: 310 }));
570
+ const middlePanWorks = panStage.scrollLeft >= 60
571
+ && (panStage.scrollHeight <= panStage.clientHeight + 1 || panStage.scrollTop >= 40)
572
+ && panStage.classList.contains('canvas-panning');
573
+ const middlePanMetrics = { left: panStage.scrollLeft, top: panStage.scrollTop, scrollWidth: panStage.scrollWidth, scrollHeight: panStage.scrollHeight, clientWidth: panStage.clientWidth, clientHeight: panStage.clientHeight, active: panStage.classList.contains('canvas-panning') };
574
+ panStage.dispatchEvent(new PointerEvent('pointerup', { bubbles: true, pointerId: 77, button: 1, buttons: 0, clientX: 350, clientY: 310 }));
575
+ const middlePanReleases = !panStage.classList.contains('canvas-panning');
576
+ setZoom(1);
577
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
578
+ const stage = document.querySelector('.stage');
579
+ const requestedScroll = Math.min(40, Math.max(0, stage.scrollWidth - stage.clientWidth));
580
+ stage.scrollLeft = requestedScroll;
581
+ renderView();
582
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
583
+ const renderedStage = document.querySelector('.stage');
584
+ const expectedScroll = Math.min(requestedScroll, Math.max(0, renderedStage.scrollWidth - renderedStage.clientWidth));
585
+ const scrollPreserved = Math.abs(renderedStage.scrollLeft - expectedScroll) <= 1;
586
+
587
+ setInspectorTab('inspect');
588
+ const inspectTabWorks = document.querySelector('[data-inspector-pane="inspect"]')?.hidden === false
589
+ && [...document.querySelectorAll('[data-inspector-pane]')].filter(pane => !pane.hidden).length === 1;
590
+ setInspectorTab('comments');
591
+ const commentsTabWorks = document.querySelector('[data-inspector-pane="comments"]')?.hidden === false
592
+ && [...document.querySelectorAll('[data-inspector-pane]')].filter(pane => !pane.hidden).length === 1;
593
+ setInspectorTab('review');
594
+ const reviewTabWorks = document.querySelector('[data-inspector-pane="review"]')?.hidden === false
595
+ && [...document.querySelectorAll('[data-inspector-pane]')].filter(pane => !pane.hidden).length === 1;
596
+
597
+ const search = document.querySelector('.screen-search');
598
+ const searchProbe = [...document.querySelectorAll('.screen-link')].at(-1)?.textContent?.trim() || '';
599
+ if (search) {
600
+ search.value = searchProbe;
601
+ search.dispatchEvent(new Event('input', { bubbles: true }));
602
+ }
603
+ const treeSearchWorks = [...document.querySelectorAll('.screen-link:not([hidden])')]
604
+ .some(button => button.textContent.trim() === searchProbe);
605
+ if (search) {
606
+ search.value = '';
607
+ search.dispatchEvent(new Event('input', { bubbles: true }));
608
+ }
609
+ const evidenceEdges = (ir.navigationGraph?.edges || []).filter(edge => edge.kind === 'open-logical-view');
610
+ const nestedTreeWorks = evidenceEdges.every(edge => (treePaths[edge.to]?.length || 0) > (treePaths[edge.from]?.length || 0));
611
+
612
+ let compactPanelsExclusive = true;
613
+ if (innerWidth <= 980) {
614
+ state.sidebarOpen = true;
615
+ state.inspectorOpen = false;
616
+ applyPanelState();
617
+ document.querySelector('.inspector-toggle')?.click();
618
+ const rightOpenedAlone = state.inspectorOpen && !state.sidebarOpen;
619
+ document.querySelector('.sidebar-toggle')?.click();
620
+ const leftOpenedAlone = state.sidebarOpen && !state.inspectorOpen;
621
+ compactPanelsExclusive = rightOpenedAlone && leftOpenedAlone;
622
+ setInspectorTab('review');
623
+ }
624
+ const reviewBackup = feedbackPayload();
625
+ const expectedAnnotationCount = state.annotations.length;
626
+ const expectedDecisionCount = Object.keys(state.findingDecisions).length;
627
+ state.annotations = [];
628
+ state.findingDecisions = {};
629
+ restoreReviewStateData(reviewBackup);
630
+ const reviewBackupWorks = reviewBackup.type === 'ui-design-workbench-review-state'
631
+ && reviewBackup.diagnosticsEngineVersion === diagnosticsEngineVersion
632
+ && state.annotations.length === expectedAnnotationCount
633
+ && Object.keys(state.findingDecisions).length === expectedDecisionCount;
634
+ return {
635
+ totalBefore,
636
+ totalAfter,
637
+ actionableChecks,
638
+ actionableGroups,
639
+ diagnosticFindingCreated,
640
+ diagnosticsDoNotDownload,
641
+ staleRuntimePruned,
642
+ autoReviewFindings,
643
+ autoReviewButton: Boolean(document.querySelector('.review-next-action')),
644
+ agentReviewButtonEnabled: document.querySelector('.open-agent-review')?.disabled === false,
645
+ expertReviewButtonEnabled: document.querySelector('.export-review-request')?.disabled === false,
646
+ expertRequestValid: expertRequest?.type === 'ui-design-workbench-expert-review-request'
647
+ && expertRequest?.diagnosticsSummary?.status === 'complete'
648
+ && expertRequest?.activeVersion === baselineVersion
649
+ && expertRequest?.reviewVersionId === baselineVersion
650
+ && expertRequest?.screenIds?.length === screens.length
651
+ && expertRequest?.contextFile?.endsWith('ui-agent-context.json')
652
+ && expertRequest?.patchFile?.endsWith('ui-ir.patch.json')
653
+ && expertRequest?.requiredChatReport?.includes('plain numbered list')
654
+ && expertRequest?.requiredChatReport?.includes('Do not duplicate it as plain text in HTML')
655
+ && expertRequest?.expectedResult?.type === 'ui-design-workbench-ir-patch',
656
+ expertHandoffValid: expertHandoff?.supported === true
657
+ && expertHandoff?.provider === 'generic'
658
+ && expertHandoff?.url === null
659
+ && expertHandoff?.path === previewContext.artifactDir
660
+ && expertHandoff?.context?.activeVersion === baselineVersion
661
+ && expertHandoff?.context?.reviewVersionId === baselineVersion
662
+ && expertHandoff?.prompt?.includes('ui-design-workbench')
663
+ && expertHandoff?.prompt?.includes('Не изменяй исходный проект'),
664
+ proposalHandoffValid: selected === 0 || (proposalHandoff?.supported === true
665
+ && proposalHandoff?.context?.acceptedFindingIds?.length === selected
666
+ && proposalHandoff?.context?.sourceChangeAllowed === false),
667
+ implementationHandoffValid: implementationHandoff?.supported === true
668
+ && implementationHandoff?.context?.sourceChangeAllowed === true
669
+ && Boolean(implementationHandoff?.context?.projectRoot)
670
+ && implementationHandoff?.context?.contextFile?.endsWith('ui-agent-context.json')
671
+ && implementationHandoff?.prompt?.includes('contextFile')
672
+ && implementationHandoff?.prompt?.includes('не запускай новое полное ревью'),
673
+ directFixEntryPoint,
674
+ handoffPanelWorks,
675
+ contextProposalAction,
676
+ importWorks: Boolean(importWorks),
677
+ compareVariantsWork,
678
+ compareSelectorsMatchCanvas,
679
+ layerControlsWork: document.querySelectorAll('[data-canvas-layer]').length === 1
680
+ && document.querySelectorAll('.mode-button[data-view]').length === 5
681
+ && document.querySelectorAll('.compare-version-select').length === 2,
682
+ prototypeForcesInteraction,
683
+ prototypeNavigationWorks,
684
+ prototypeTreePreservesMode,
685
+ screenScenarioControlsWork,
686
+ stateGalleryWorks,
687
+ screenScenarioDiagnosticsWork,
688
+ markerNumberMatches,
689
+ markerPopoverOpens,
690
+ markerPopoverCollapses,
691
+ commentPopoverOpens,
692
+ commentPopoverCollapses,
693
+ userCommentHasDistinctColor,
694
+ markersFollowComparedViews,
695
+ railCommandsWork,
696
+ railMenusWork,
697
+ railFileGeometryWorks,
698
+ markersToggleWork: markersHide && markersRestore,
699
+ versionArchitectureWorks,
700
+ addressedIsNotVerified,
701
+ markerTracksZoom,
702
+ markerTrackMetrics,
703
+ markerLayoutStable,
704
+ markerOverlaps,
705
+ markerLeaderWorks,
706
+ markerLeaderMetrics,
707
+ middleMousePanWorks: middlePanWorks && middlePanReleases,
708
+ middlePanMetrics,
709
+ localeSwitchWorks: englishLocaleWorks && russianLocaleRestores,
710
+ localeMetrics,
711
+ reviewSectionsWork: ['summary', 'problems', 'changes'].every(section => Boolean(document.querySelector('[data-review-section="' + section + '"]'))),
712
+ simplifiedReviewFlow: document.querySelectorAll('.review-phase').length === 0
713
+ && !document.querySelector('.review-context-status')?.textContent?.includes('Шаг 2 из 3'),
714
+ reviewBackupWorks,
715
+ reviewBackupControls: Boolean(document.querySelector('.export-feedback') && document.querySelector('.import-feedback') && document.querySelector('.import-feedback-file')),
716
+ auditLinks: document.querySelectorAll('[data-audit-findings]').length,
717
+ linkedVisible,
718
+ selected,
719
+ selectionNextStep,
720
+ bulkSelectionPathWorks,
721
+ requestFindings: request?.acceptedFindingIds?.length || 0,
722
+ runtimeActionable,
723
+ reviewEntryPoint: Boolean(document.querySelector('[data-inspector-tab="review"]')),
724
+ sourceBeforeApproval,
725
+ sourceAfterApproval,
726
+ applyOnlyOnFixStep,
727
+ sourceApprovalGuard: sourceRequest?.sourceChangeAllowed === true
728
+ && Boolean(sourceRequest?.projectRoot)
729
+ && sourceRequest?.contextFile?.endsWith('ui-agent-context.json')
730
+ && Array.isArray(sourceRequest?.requiredChatReport)
731
+ && sourceRequest?.requestedAction?.includes('never repeat the full AI review automatically'),
732
+ revisionVisible,
733
+ selectionPreserved,
734
+ scrollPreserved,
735
+ inspectTabWorks,
736
+ commentsTabWorks,
737
+ reviewTabWorks,
738
+ treeSearchWorks,
739
+ nestedTreeWorks,
740
+ compactPanelsExclusive,
741
+ };
742
+ })()`,
743
+ returnByValue: true,
744
+ awaitPromise: true,
745
+ });
746
+ if (!args.captureOnly && workflowResponse.exceptionDetails) {
747
+ throw new Error(`Review workflow evaluation failed: ${workflowResponse.exceptionDetails.exception?.description || workflowResponse.exceptionDetails.text || 'unknown error'}`);
748
+ }
749
+ const workflow = workflowResponse.result?.value || {};
750
+ if (!args.captureOnly && !workflow.reviewEntryPoint) throw new Error('Review workflow entry point is missing');
751
+ const expectedRuntimeFindings = workflow.actionableGroups;
752
+ const actionableWorkflowFailed = workflow.diagnosticFindingCreated
753
+ ? (!workflow.contextProposalAction || workflow.selected !== 1 || workflow.requestFindings !== 1 || !workflow.runtimeActionable)
754
+ : (workflow.selected !== 0 || workflow.requestFindings !== 0 || workflow.runtimeActionable);
755
+ if (!args.captureOnly && workflow.totalBefore > 0 && (
756
+ !workflow.auditLinks || !workflow.linkedVisible || workflow.totalAfter !== workflow.totalBefore + expectedRuntimeFindings
757
+ || !workflow.diagnosticsDoNotDownload || !workflow.staleRuntimePruned
758
+ || workflow.autoReviewFindings !== expectedRuntimeFindings || !workflow.autoReviewButton
759
+ || !workflow.agentReviewButtonEnabled || !workflow.expertReviewButtonEnabled || !workflow.expertRequestValid
760
+ || !workflow.expertHandoffValid || !workflow.proposalHandoffValid || !workflow.implementationHandoffValid || !workflow.directFixEntryPoint
761
+ || !workflow.handoffPanelWorks
762
+ || actionableWorkflowFailed || !workflow.importWorks || !workflow.compareVariantsWork || !workflow.compareSelectorsMatchCanvas || !workflow.reviewSectionsWork
763
+ || !workflow.simplifiedReviewFlow || !workflow.reviewBackupWorks || !workflow.reviewBackupControls
764
+ || !workflow.layerControlsWork || !workflow.prototypeForcesInteraction || !workflow.prototypeNavigationWorks || !workflow.prototypeTreePreservesMode
765
+ || !workflow.screenScenarioControlsWork || !workflow.stateGalleryWorks || !workflow.screenScenarioDiagnosticsWork || !workflow.markerNumberMatches || !workflow.markerPopoverOpens
766
+ || !workflow.markerPopoverCollapses || !workflow.commentPopoverOpens || !workflow.commentPopoverCollapses
767
+ || !workflow.userCommentHasDistinctColor || !workflow.markersFollowComparedViews || !workflow.railCommandsWork || !workflow.railMenusWork || !workflow.railFileGeometryWorks
768
+ || !workflow.markersToggleWork || !workflow.versionArchitectureWorks || !workflow.addressedIsNotVerified
769
+ || !workflow.markerTracksZoom || !workflow.markerLayoutStable || !workflow.markerLeaderWorks || !workflow.middleMousePanWorks || !workflow.localeSwitchWorks
770
+ || !workflow.selectionNextStep || !workflow.bulkSelectionPathWorks || !workflow.sourceBeforeApproval || !workflow.sourceAfterApproval || !workflow.applyOnlyOnFixStep
771
+ || !workflow.sourceApprovalGuard || !workflow.revisionVisible
772
+ || !workflow.selectionPreserved || !workflow.scrollPreserved
773
+ || !workflow.inspectTabWorks || !workflow.commentsTabWorks || !workflow.reviewTabWorks
774
+ || !workflow.treeSearchWorks || !workflow.nestedTreeWorks || !workflow.compactPanelsExclusive
775
+ )) {
776
+ throw new Error(`Review workflow smoke failed: ${JSON.stringify(workflow)}`);
777
+ }
778
+ const fixedProfiles = (report.profiles || []).filter(item => item.viewport && item.viewport !== 'current');
779
+ const measuredProfiles = new Set((report.checks || []).map(item => item.metrics?.profileId).filter(Boolean));
780
+ if (fixedProfiles.length < 2 || fixedProfiles.some(item => !measuredProfiles.has(item.id))) {
781
+ throw new Error(`Viewport profile smoke failed: configured=${fixedProfiles.map(item => item.id)} measured=${[...measuredProfiles]}`);
782
+ }
783
+ const captureState = { ...initialVisualState };
784
+ captureState.findingFocus = [];
785
+ captureState.findingFilter = 'all';
786
+ captureState.findingSource = 'all';
787
+ captureState.findingScreen = 'all';
788
+ if (args.captureView) captureState.view = args.captureView;
789
+ if (args.captureScreen) captureState.screen = args.captureScreen;
790
+ if (args.captureLeftPanel) captureState.sidebarOpen = args.captureLeftPanel === 'open';
791
+ if (args.captureRightPanel) captureState.inspectorOpen = args.captureRightPanel === 'open';
792
+ if (args.captureInspectorTab) captureState.inspectorTab = args.captureInspectorTab;
793
+ if (args.captureReviewSection) captureState.reviewSection = args.captureReviewSection;
794
+ if (args.viewportWidth <= 980 && captureState.sidebarOpen && captureState.inspectorOpen) captureState.sidebarOpen = false;
795
+ await cdp.call('Runtime.evaluate', {
796
+ expression: `(() => {
797
+ const snapshot = ${JSON.stringify(captureState)};
798
+ const originalVersionIds = new Set(snapshot.versionIds || []);
799
+ for (let index = versions.length - 1; index >= 0; index -= 1) {
800
+ if (originalVersionIds.has(versions[index].id)) continue;
801
+ delete versionById[versions[index].id];
802
+ versions.splice(index, 1);
803
+ }
804
+ document.querySelectorAll('.version-select option,.compare-version-select option').forEach(option => {
805
+ if (!originalVersionIds.has(option.value)) option.remove();
806
+ });
807
+ delete snapshot.versionIds;
808
+ if (!screens.some(item => item.id === snapshot.screen)) snapshot.screen = screens[0]?.id;
809
+ Object.assign(state, snapshot);
810
+ versionSelect.value = state.activeVersion;
811
+ writeLocation(false);
812
+ applyPanelState();
813
+ syncInteractionButtons();
814
+ renderFindings();
815
+ renderQueue();
816
+ renderFixQueue();
817
+ renderReviewSections();
818
+ renderCoverage();
819
+ renderReviewHistory();
820
+ renderImportStatus();
821
+ renderAgentHandoff();
822
+ renderView();
823
+ document.querySelector('[data-inspector-pane="review"]')?.scrollTo({ top: 0, behavior: 'instant' });
824
+ const toast = document.querySelector('.workbench-toast');
825
+ if (toast) { toast.classList.remove('visible'); toast.textContent = ''; }
826
+ })()`,
827
+ returnByValue: true,
828
+ });
829
+ await delay(100);
830
+ if (args.screenshot) {
831
+ await cdp.call('Page.enable');
832
+ const shot = await cdp.call('Page.captureScreenshot', { format: 'png', fromSurface: true, captureBeyondViewport: false });
833
+ fs.writeFileSync(path.resolve(args.screenshot), Buffer.from(shot.data, 'base64'));
834
+ }
835
+ if (args.geometryOutput) {
836
+ const geometryResponse = await cdp.call('Runtime.evaluate', {
837
+ expression: `(() => {
838
+ const rect = element => {
839
+ if (!element) return null;
840
+ const value = element.getBoundingClientRect();
841
+ const style = getComputedStyle(element);
842
+ return {
843
+ x: Math.round(value.x * 100) / 100,
844
+ y: Math.round(value.y * 100) / 100,
845
+ width: Math.round(value.width * 100) / 100,
846
+ height: Math.round(value.height * 100) / 100,
847
+ visible: style.display !== 'none' && style.visibility !== 'hidden' && value.width > 0 && value.height > 0,
848
+ overflowX: style.overflowX,
849
+ overflowY: style.overflowY,
850
+ };
851
+ };
852
+ const selectors = {
853
+ rail: '.workbench-rail',
854
+ leftPanel: '.sidebar',
855
+ documentBar: '.workbench-header',
856
+ stage: '.stage',
857
+ canvasTools: '.canvas-tools',
858
+ zoomControls: '.floating-zoom',
859
+ rightPanel: '.inspector',
860
+ activeInspectorPane: '[data-inspector-pane]:not([hidden])',
861
+ reviewFooter: '.review-context-footer',
862
+ };
863
+ const elements = Object.fromEntries(Object.entries(selectors).map(([key, selector]) => [key, rect(document.querySelector(selector))]));
864
+ document.querySelectorAll('[data-screen-card]').forEach((element, index) => { elements['screenCard:' + (element.dataset.screenCard || index)] = rect(element); });
865
+ const cards = Object.entries(elements).filter(([key, value]) => key.startsWith('screenCard:') && value?.visible);
866
+ const cardOverlaps = [];
867
+ for (let first = 0; first < cards.length; first += 1) for (let second = first + 1; second < cards.length; second += 1) {
868
+ const [aKey, a] = cards[first], [bKey, b] = cards[second];
869
+ if (a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y) cardOverlaps.push([aKey, bKey]);
870
+ }
871
+ const controlPairs = [['canvasTools', 'zoomControls'], ['canvasTools', 'rightPanel'], ['zoomControls', 'rightPanel'], ['canvasTools', 'leftPanel'], ['zoomControls', 'leftPanel']];
872
+ const controlOverlaps = controlPairs.filter(([aKey, bKey]) => {
873
+ const a = elements[aKey], b = elements[bKey];
874
+ return a?.visible && b?.visible && a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
875
+ });
876
+ return { version: 2, viewport: { width: innerWidth, height: innerHeight }, view: state.view, screen: state.screen, zoom: state.computedZoom, sidebarOpen: state.sidebarOpen, inspectorOpen: state.inspectorOpen, inspectorTab: state.inspectorTab, elements, overlaps: [...cardOverlaps, ...controlOverlaps], cardOverlaps, controlOverlaps };
877
+ })()`,
878
+ returnByValue: true,
879
+ });
880
+ fs.writeFileSync(path.resolve(args.geometryOutput), JSON.stringify(geometryResponse.result?.value || {}, null, 2) + '\n');
881
+ }
882
+ if (!args.captureOnly) {
883
+ await cdp.call('Page.enable');
884
+ const storageKeyResponse = await cdp.call('Runtime.evaluate', { expression: '({storageKey,reviewBaseRevision})', returnByValue: true });
885
+ const reviewStorageKey = storageKeyResponse.result?.value?.storageKey;
886
+ const baseRevision = storageKeyResponse.result?.value?.reviewBaseRevision;
887
+ if (!reviewStorageKey) throw new Error('Review storage key is unavailable');
888
+ const legacyShellInjection = await cdp.call('Page.addScriptToEvaluateOnNewDocument', {
889
+ source: `localStorage.setItem(${JSON.stringify(reviewStorageKey)}, JSON.stringify({revision:${JSON.stringify(`${baseRevision}-legacy-shell`)},diagnosticsEngineVersion:1,diagnostics:{engineVersion:1,mode:'review',checks:[],summary:{}},runtimeFindings:[{id:'legacy-runtime',category:'runtime-diagnostic',screenId:'legacy',runtimeDiagnosticId:'legacy-check'}],reviewRuns:[{id:'legacy-shell-run',engineVersion:1}],findingDecisions:{'legacy-runtime':'accepted'}}));`,
890
+ });
891
+ await cdp.call('Page.navigate', { url: pathToFileURL(input).href });
892
+ const legacyMigrationDeadline = Date.now() + 15000;
893
+ let legacyShellStateRestored = false;
894
+ while (Date.now() < legacyMigrationDeadline) {
895
+ const migrationResponse = await cdp.call('Runtime.evaluate', {
896
+ expression: `(() => {
897
+ if (document.readyState !== 'complete') return false;
898
+ const stored = JSON.parse(localStorage.getItem(storageKey) || '{}');
899
+ return document.querySelector('.revision-notice')?.hidden === true
900
+ && state.reviewRuns.some(item => item.id === 'legacy-shell-run')
901
+ && state.diagnostics === null
902
+ && !state.runtimeFindings.some(item => item.id === 'legacy-runtime')
903
+ && !Object.prototype.hasOwnProperty.call(state.findingDecisions, 'legacy-runtime')
904
+ && stored.targetRevision === reviewTargetRevision
905
+ && stored.workbenchSchema === workbenchSchema;
906
+ })()`,
907
+ returnByValue: true,
908
+ });
909
+ legacyShellStateRestored = migrationResponse.result?.value === true;
910
+ if (legacyShellStateRestored) break;
911
+ await delay(100);
912
+ }
913
+ await cdp.call('Page.removeScriptToEvaluateOnNewDocument', { identifier: legacyShellInjection.identifier });
914
+ if (!legacyShellStateRestored) throw new Error('Renderer-only review state migration failed');
915
+ const staleInjection = await cdp.call('Page.addScriptToEvaluateOnNewDocument', {
916
+ source: `localStorage.setItem(${JSON.stringify(reviewStorageKey)}, JSON.stringify({revision:'stale-smoke',annotations:[],findingDecisions:{}}));`,
917
+ });
918
+ await cdp.call('Page.navigate', { url: pathToFileURL(input).href });
919
+ const revisionDeadline = Date.now() + 15000;
920
+ let staleNoticeVisible = false;
921
+ while (Date.now() < revisionDeadline) {
922
+ const staleResponse = await cdp.call('Runtime.evaluate', {
923
+ expression: "document.readyState === 'complete' && document.querySelector('.revision-notice')?.hidden === false",
924
+ returnByValue: true,
925
+ });
926
+ staleNoticeVisible = staleResponse.result?.value === true;
927
+ if (staleNoticeVisible) break;
928
+ await delay(100);
929
+ }
930
+ await cdp.call('Page.removeScriptToEvaluateOnNewDocument', { identifier: staleInjection.identifier });
931
+ if (!staleNoticeVisible) throw new Error('Stale revision notice did not appear');
932
+ const stalePreservedResponse = await cdp.call('Runtime.evaluate', {
933
+ expression: `(() => { persist(); return JSON.parse(localStorage.getItem(storageKey) || '{}').revision === 'stale-smoke'; })()`,
934
+ returnByValue: true,
935
+ });
936
+ if (stalePreservedResponse.result?.value !== true) throw new Error('Stale review snapshot was overwritten before a migration decision');
937
+ const resetResponse = await cdp.call('Runtime.evaluate', {
938
+ expression: `(() => {
939
+ document.querySelector('.discard-review-state')?.click();
940
+ const stored = JSON.parse(localStorage.getItem(storageKey) || '{}');
941
+ return document.querySelector('.revision-notice')?.hidden === true && stored.revision !== 'stale-smoke';
942
+ })()`,
943
+ returnByValue: true,
944
+ });
945
+ if (resetResponse.result?.value !== true) throw new Error('Stale revision reset failed');
946
+ }
947
+ if (args.output) fs.writeFileSync(path.resolve(args.output), `${JSON.stringify(report, null, 2)}\n`, 'utf8');
948
+ const summary = report.summary || {};
949
+ console.log(`${args.mode === 'review' ? 'Review' : 'Projection'} diagnostics complete: product-warning=${summary.productWarning ?? summary.warning ?? 0} product-fail=${summary.productFail ?? summary.fail ?? 0} tool-warning=${summary.toolWarning || 0} tool-fail=${summary.toolFail || 0} checks=${report.checks?.length || 0}`);
950
+ if (args.mode === 'review' && !args.captureOnly) console.log(`Review workflow: findings=${workflow.totalAfter || 0} auditLinks=${workflow.auditLinks || 0} selected=${workflow.selected || 0} requestFindings=${workflow.requestFindings || 0} profiles=${fixedProfiles.length}`);
951
+ if (args.failOnFindings && (summary.productFail ?? summary.fail ?? 0) > 0) process.exitCode = 1;
952
+ } finally {
953
+ try { await Promise.race([cdp?.call('Browser.close'), delay(1500)]); } catch (_) {}
954
+ try { socket?.close(); } catch (_) {}
955
+ if (browser.exitCode === null) await Promise.race([
956
+ new Promise(resolve => browser.once('exit', resolve)),
957
+ delay(5000),
958
+ ]);
959
+ if (browser.exitCode === null) {
960
+ try { browser.kill('SIGKILL'); } catch (_) {}
961
+ await Promise.race([
962
+ new Promise(resolve => browser.once('exit', resolve)),
963
+ delay(3000),
964
+ ]);
965
+ }
966
+ let cleanupError = null;
967
+ for (let attempt = 0; attempt < 10; attempt += 1) {
968
+ try { fs.rmSync(profile, { recursive: true, force: true }); cleanupError = null; break; }
969
+ catch (error) { cleanupError = error; await delay(200 * (attempt + 1)); }
970
+ }
971
+ if (cleanupError) console.warn(`Temporary browser profile could not be removed: ${cleanupError.message}`);
972
+ }
973
+ }
974
+
975
+ main().catch(error => {
976
+ console.error(error.message);
977
+ process.exitCode = 2;
978
+ });