@surlymochan/evalid 0.0.0-stage → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/LICENSE +21 -0
  3. package/README.md +315 -2
  4. package/RELEASE_NOTES.md +102 -0
  5. package/atoms/registry.json +758 -0
  6. package/bin/xflow.js +7208 -0
  7. package/docs/adoption/README.md +128 -0
  8. package/docs/adoption/openflow-release-hardening.md +61 -0
  9. package/docs/benchmark-openflow-vs-gstack-superpowers-openspec-2026-05-25.md +81 -0
  10. package/docs/compatibility.md +52 -0
  11. package/docs/competitive-benchmark.md +108 -0
  12. package/docs/corps-operator-guide.md +75 -0
  13. package/docs/demo-proof.md +126 -0
  14. package/docs/examples-gallery.md +115 -0
  15. package/docs/fixtures/tracker-item.json +7 -0
  16. package/docs/goal-vs-codex.md +129 -0
  17. package/docs/gstack-comparison.md +88 -0
  18. package/docs/install-upgrade.md +133 -0
  19. package/docs/integrations.md +49 -0
  20. package/docs/launch-demo.md +123 -0
  21. package/docs/launch-dossier.md +180 -0
  22. package/docs/methodology.md +149 -0
  23. package/docs/npm-publish-handoff.md +83 -0
  24. package/docs/openspec-migration.md +106 -0
  25. package/docs/public-benchmark.md +68 -0
  26. package/docs/public-release.md +145 -0
  27. package/docs/quality-assessment.md +139 -0
  28. package/docs/quickstart.md +156 -0
  29. package/docs/reviewer-guide.md +43 -0
  30. package/docs/spec-kit-benchmark.md +73 -0
  31. package/docs/super-assistant-comparison.md +106 -0
  32. package/docs/superpowers-comparison.md +82 -0
  33. package/docs/team-adoption.md +123 -0
  34. package/docs/tooling-matrix.md +91 -0
  35. package/docs/walkthrough.md +76 -0
  36. package/docs/workflow-manual.md +1125 -0
  37. package/package.json +83 -4
  38. package/prompts/pencil-draft.md +46 -0
  39. package/prompts/pencil-refine.md +46 -0
  40. package/prompts/phase-artifact.md +24 -0
  41. package/prompts/phase-execution.md +32 -0
  42. package/schemas/competitor-reconstruction-contract.schema.json +91 -0
  43. package/schemas/llm-design-review-aggregate.schema.json +83 -0
  44. package/schemas/visual-benchmark.schema.json +978 -0
  45. package/schemas/workflow.schema.json +100 -0
  46. package/scripts/run-tests.mjs +23 -0
  47. package/scripts/takein_digest.py +139 -0
  48. package/specs/README.md +118 -0
  49. package/specs/architecture.md +36 -0
  50. package/specs/product.md +33 -0
  51. package/specs/workflow.md +349 -0
  52. package/src/agent_team/atoms/codex-app-bridge.js +8 -0
  53. package/src/agent_team/atoms/design/competitor-reconstruction-review.js +44 -0
  54. package/src/agent_team/atoms/design/contract-freeze.js +8 -0
  55. package/src/agent_team/atoms/design/generation-contract.js +442 -0
  56. package/src/agent_team/atoms/design/reconstruction-pack.js +246 -0
  57. package/src/agent_team/atoms/design/reference-surface-lock.js +49 -0
  58. package/src/agent_team/atoms/patch-challenge.js +19 -0
  59. package/src/agent_team/atoms/pencil/accept.js +9 -0
  60. package/src/agent_team/atoms/pencil/draft.js +365 -0
  61. package/src/agent_team/atoms/pencil/refine.js +229 -0
  62. package/src/agent_team/atoms/phase-run-dispatch.js +8 -0
  63. package/src/agent_team/atoms/team-run.js +380 -0
  64. package/src/agent_team/atoms/visual/aesthetic-review.js +231 -0
  65. package/src/agent_team/atoms/visual/benchmark-repair-plan.js +291 -0
  66. package/src/agent_team/atoms/visual/benchmark.js +1652 -0
  67. package/src/agent_team/atoms/visual/design-selection.js +23 -0
  68. package/src/agent_team/atoms/visual/direction-synthesis.js +124 -0
  69. package/src/agent_team/atoms/visual/dom-rect-export.js +129 -0
  70. package/src/agent_team/atoms/visual/image-diff.js +186 -0
  71. package/src/agent_team/atoms/visual/layout-competition.js +8 -0
  72. package/src/agent_team/atoms/visual/page-evidence-capture.js +806 -0
  73. package/src/agent_team/atoms/visual/playwright-runtime.js +8 -0
  74. package/src/agent_team/atoms/visual/review-aggregate.js +87 -0
  75. package/src/agent_team/atoms/visual/review.js +77 -0
  76. package/src/agent_team/atoms/visual/visual-report.js +197 -0
  77. package/src/agent_team/atoms/visual/visual-token-export.js +417 -0
  78. package/src/agent_team/common.js +311 -0
  79. package/src/core/atoms/agent-invoke.js +684 -0
  80. package/src/core/atoms/event/ingest.js +30 -0
  81. package/src/core/atoms/experience/digest.js +55 -0
  82. package/src/core/atoms/gate/artifact-verify.js +83 -0
  83. package/src/core/atoms/gate/competitor-contract-validate.js +119 -0
  84. package/src/core/atoms/gate/evidence-collect.js +64 -0
  85. package/src/core/atoms/gate/payload-from-phase-run.js +78 -0
  86. package/src/core/atoms/gate/visual-benchmark-validate.js +183 -0
  87. package/src/core/atoms/gate/workbench.js +79 -0
  88. package/src/core/atoms/intake/classify.js +15 -0
  89. package/src/core/atoms/intake/design-skill-select.js +16 -0
  90. package/src/core/atoms/intake/preview-or-refine.js +19 -0
  91. package/src/core/atoms/intake/recommend.js +20 -0
  92. package/src/core/atoms/intake/research-same-category.js +19 -0
  93. package/src/core/atoms/intervention/create.js +30 -0
  94. package/src/core/atoms/memory/manage.js +27 -0
  95. package/src/core/atoms/memory/mount.js +13 -0
  96. package/src/core/atoms/mission/create.js +19 -0
  97. package/src/core/atoms/mission/list.js +14 -0
  98. package/src/core/atoms/mission/show.js +26 -0
  99. package/src/core/atoms/notify/checkpoint.js +46 -0
  100. package/src/core/atoms/phase-run/complete.js +42 -0
  101. package/src/core/atoms/phase-run/fail.js +40 -0
  102. package/src/core/atoms/phase-run/persist.js +39 -0
  103. package/src/core/atoms/phase-run/start.js +30 -0
  104. package/src/core/atoms/plugin/run.js +76 -0
  105. package/src/core/atoms/policy/gate.js +90 -0
  106. package/src/core/atoms/policy/resolve.js +11 -0
  107. package/src/core/atoms/storage/sync.js +57 -0
  108. package/src/core/atoms/workflow/advance.js +25 -0
  109. package/src/core/atoms/workflow/guard.js +127 -0
  110. package/src/core/atoms/workflow/state-load.js +37 -0
  111. package/src/core/bindings.js +37 -0
  112. package/src/core/change-artifacts.js +134 -0
  113. package/src/core/competitive-score.js +265 -0
  114. package/src/core/concurrency-policy.js +35 -0
  115. package/src/core/execution-log.js +108 -0
  116. package/src/core/intake-recommendation.js +134 -0
  117. package/src/core/memory-state.js +62 -0
  118. package/src/core/mission-state.js +780 -0
  119. package/src/core/pending-gates.js +66 -0
  120. package/src/core/phase-cost.js +116 -0
  121. package/src/core/phase-run-read.js +27 -0
  122. package/src/core/policy-overlay.js +180 -0
  123. package/src/core/state-store.js +201 -0
  124. package/src/core/workflow-executor.js +998 -0
  125. package/src/core/workflow-integrity.js +111 -0
  126. package/src/core/workflow-loader.js +163 -0
  127. package/src/core/workflow-rerun.js +84 -0
  128. package/src/core/workflow-state.js +119 -0
  129. package/src/server.js +1307 -0
  130. package/workflows/corps.yaml +479 -0
  131. package/workflows/yolo.yaml +233 -0
  132. package/xflow/README.md +76 -0
  133. package/xflow/aha/SKILL.md +82 -0
  134. package/xflow/aha/scripts/append_aha.py +50 -0
  135. package/xflow/archive/scripts/archive_change.py +338 -0
  136. package/xflow/archive/templates/merge-design.md +54 -0
  137. package/xflow/archive/templates/merge-workflow.md +26 -0
  138. package/xflow/atoms/__init__.py +6 -0
  139. package/xflow/atoms/a1_issue_create_or_patch.py +70 -0
  140. package/xflow/atoms/a2_branch_create_and_link.py +42 -0
  141. package/xflow/atoms/a3_repo_doctor.py +11 -0
  142. package/xflow/atoms/a4_project_set_status.py +11 -0
  143. package/xflow/atoms/a5_archive_commit_push_close.py +11 -0
  144. package/xflow/atoms/a6_pr_create.py +72 -0
  145. package/xflow/atoms/b1_change_scaffold.py +11 -0
  146. package/xflow/atoms/b2_status_read.py +16 -0
  147. package/xflow/atoms/b2b_status_write.py +20 -0
  148. package/xflow/atoms/b3_status_transition.py +27 -0
  149. package/xflow/atoms/b4_handoff_scaffold_or_refresh.py +11 -0
  150. package/xflow/atoms/b5_aha_append.py +11 -0
  151. package/xflow/atoms/b6_takein_digest.py +11 -0
  152. package/xflow/atoms/b6b_lookback_digest.py +11 -0
  153. package/xflow/atoms/b7_change_mission_bind.py +43 -0
  154. package/xflow/atoms/c1_mem.py +54 -0
  155. package/xflow/atoms/e6_gate_local_precheck.py +23 -0
  156. package/xflow/atoms/h1_design_lite_gate.py +74 -0
  157. package/xflow/atoms/i6a_tdd_run.py +104 -0
  158. package/xflow/atoms/i6b_tdd_proof_validate.py +165 -0
  159. package/xflow/atoms/i6c_tdd_quality_review.py +357 -0
  160. package/xflow/atoms/j1_tests_run.py +129 -0
  161. package/xflow/atoms/j4a_spec_consistency_structural.py +105 -0
  162. package/xflow/atoms/j4b_spec_delta_review.py +316 -0
  163. package/xflow/atoms/j4c_openspec_migration_map.py +121 -0
  164. package/xflow/atoms/j4d_spec_clarify_gate.py +373 -0
  165. package/xflow/atoms/j6_cr_review.py +443 -0
  166. package/xflow/atoms/k1_artifacts_complete_check.py +39 -0
  167. package/xflow/atoms/k2_merge_snippets_apply.py +11 -0
  168. package/xflow/atoms/k3_handoff_refresh.py +14 -0
  169. package/xflow/atoms/k4_mem_lesson_persist.py +65 -0
  170. package/xflow/atoms/k5_archive_publish.py +21 -0
  171. package/xflow/atoms/k6_aha_merge.py +42 -0
  172. package/xflow/atoms/tdd_lib.py +747 -0
  173. package/xflow/corps/SKILL.md +109 -0
  174. package/xflow/exec/scripts/set_issue_in_progress.py +159 -0
  175. package/xflow/goal/SKILL.md +150 -0
  176. package/xflow/handoff/SKILL.md +95 -0
  177. package/xflow/handoff/scripts/scaffold_handoff.py +304 -0
  178. package/xflow/lookback/SKILL.md +66 -0
  179. package/xflow/lookback/scripts/lookback_digest.py +46 -0
  180. package/xflow/openissue/scripts/open_issue_flow.py +438 -0
  181. package/xflow/plan/SKILL.md +38 -0
  182. package/xflow/ralph/SKILL.md +123 -0
  183. package/xflow/scripts/check_installed_xflow_skill_sync.sh +44 -0
  184. package/xflow/scripts/common.py +480 -0
  185. package/xflow/scripts/doctor.py +278 -0
  186. package/xflow/scripts/sync_installed_xflow_skill.sh +134 -0
  187. package/xflow/takein/SKILL.md +81 -0
  188. package/xflow/takein/scripts/takein_digest.py +88 -0
  189. package/xflow/yolo/SKILL.md +66 -0
  190. package/xflow/yolo/scripts/scaffold_specs_change.py +673 -0
  191. package/xflow/yolo/scripts/yolo_gate.py +312 -0
  192. package/xflow/yolo/templates/DESIGN.md +179 -0
@@ -0,0 +1,806 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { dirname, extname } from 'node:path';
3
+
4
+ import { loadPlaywright } from './playwright-runtime.js';
5
+
6
+ const PLATFORM_PROFILES = {
7
+ ios_phone: {
8
+ viewport: { width: 390, height: 844 },
9
+ userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
10
+ isMobile: true,
11
+ hasTouch: true,
12
+ deviceScaleFactor: 3,
13
+ waitMs: 300,
14
+ },
15
+ android_phone: {
16
+ viewport: { width: 412, height: 915 },
17
+ userAgent: 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Mobile Safari/537.36',
18
+ isMobile: true,
19
+ hasTouch: true,
20
+ deviceScaleFactor: 2.625,
21
+ waitMs: 300,
22
+ },
23
+ mobile_h5: {
24
+ viewport: { width: 393, height: 852 },
25
+ userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
26
+ isMobile: true,
27
+ hasTouch: true,
28
+ deviceScaleFactor: 3,
29
+ waitMs: 250,
30
+ },
31
+ };
32
+
33
+ function resolvePlatformProfile(platformProfile) {
34
+ if (typeof platformProfile !== 'string') return null;
35
+ return PLATFORM_PROFILES[platformProfile] || null;
36
+ }
37
+
38
+ function toNumber(value, fallback) {
39
+ const parsed = Number(value);
40
+ return Number.isFinite(parsed) ? parsed : fallback;
41
+ }
42
+
43
+ function deriveStateScreenshotPath(basePath, stateId, index) {
44
+ const ext = extname(basePath);
45
+ const suffix = `${stateId || `state-${index + 1}`}`.replace(/[^a-z0-9_-]+/gi, '-');
46
+ if (!ext) return `${basePath}-${suffix}`;
47
+ return `${basePath.slice(0, -ext.length)}-${suffix}${ext}`;
48
+ }
49
+
50
+ function normalizeCaptureStates(captureStates) {
51
+ if (!Array.isArray(captureStates)) return [];
52
+ return captureStates
53
+ .filter((state) => state && typeof state === 'object')
54
+ .map((state, index) => ({
55
+ id: typeof state.id === 'string' && state.id.trim() ? state.id.trim() : `state-${index + 1}`,
56
+ label: typeof state.label === 'string' && state.label.trim() ? state.label.trim() : null,
57
+ hover_selector: typeof state.hover_selector === 'string' ? state.hover_selector : null,
58
+ click_selector: typeof state.click_selector === 'string' ? state.click_selector : null,
59
+ focus_selector: typeof state.focus_selector === 'string' ? state.focus_selector : null,
60
+ swipe_selector: typeof state.swipe_selector === 'string' ? state.swipe_selector : null,
61
+ swipe_direction: typeof state.swipe_direction === 'string' ? state.swipe_direction : null,
62
+ swipe_distance: Number.isFinite(Number(state.swipe_distance)) ? Number(state.swipe_distance) : null,
63
+ drag_selector: typeof state.drag_selector === 'string' ? state.drag_selector : null,
64
+ drag_to_selector: typeof state.drag_to_selector === 'string' ? state.drag_to_selector : null,
65
+ drag_dx: Number.isFinite(Number(state.drag_dx)) ? Number(state.drag_dx) : null,
66
+ drag_dy: Number.isFinite(Number(state.drag_dy)) ? Number(state.drag_dy) : null,
67
+ scroll_selector: typeof state.scroll_selector === 'string' ? state.scroll_selector : null,
68
+ scroll_dx: Number.isFinite(Number(state.scroll_dx)) ? Number(state.scroll_dx) : null,
69
+ scroll_dy: Number.isFinite(Number(state.scroll_dy)) ? Number(state.scroll_dy) : null,
70
+ press_key: typeof state.press_key === 'string' ? state.press_key : null,
71
+ wait_ms: Number.isFinite(Number(state.wait_ms)) ? Number(state.wait_ms) : null,
72
+ expect_visual_change: state.expect_visual_change !== false,
73
+ min_pixel_diff_ratio: Number.isFinite(Number(state.min_pixel_diff_ratio)) ? Number(state.min_pixel_diff_ratio) : null,
74
+ reference_image: typeof state.reference_image === 'string' ? state.reference_image : null,
75
+ priority_score: Number.isFinite(Number(state.priority_score)) ? Number(state.priority_score) : 100,
76
+ priority_reason: typeof state.priority_reason === 'string' ? state.priority_reason : 'manual',
77
+ source: typeof state.source === 'string' ? state.source : 'manual',
78
+ workbench_state_type: typeof state.workbench_state_type === 'string' && state.workbench_state_type.trim()
79
+ ? state.workbench_state_type.trim()
80
+ : null,
81
+ state_tags: Array.isArray(state.state_tags)
82
+ ? state.state_tags.filter((tag) => typeof tag === 'string' && tag.trim()).map((tag) => tag.trim())
83
+ : [],
84
+ component_family: typeof state.component_family === 'string' && state.component_family.trim()
85
+ ? state.component_family.trim()
86
+ : null,
87
+ state_variant: typeof state.state_variant === 'string' && state.state_variant.trim()
88
+ ? state.state_variant.trim()
89
+ : null,
90
+ expect_motion: state.expect_motion === true,
91
+ sample_frames: Number.isFinite(Number(state.sample_frames)) ? Number(state.sample_frames) : 0,
92
+ frame_interval_ms: Number.isFinite(Number(state.frame_interval_ms)) ? Number(state.frame_interval_ms) : null,
93
+ min_motion_changed_frames: Number.isFinite(Number(state.min_motion_changed_frames)) ? Number(state.min_motion_changed_frames) : null,
94
+ actions: Array.isArray(state.actions) ? state.actions : [],
95
+ }));
96
+ }
97
+
98
+ async function captureViewportMetrics(page) {
99
+ return page.evaluate(() => {
100
+ function envInset(name) {
101
+ const probe = document.createElement('div');
102
+ probe.style.position = 'fixed';
103
+ probe.style.pointerEvents = 'none';
104
+ probe.style.opacity = '0';
105
+ probe.style.paddingTop = name === 'top' ? 'env(safe-area-inset-top)' : '0px';
106
+ probe.style.paddingBottom = name === 'bottom' ? 'env(safe-area-inset-bottom)' : '0px';
107
+ document.body.appendChild(probe);
108
+ const computed = window.getComputedStyle(probe);
109
+ const raw = name === 'top' ? computed.paddingTop : computed.paddingBottom;
110
+ probe.remove();
111
+ const parsed = Number.parseFloat(raw || '0');
112
+ return Number.isFinite(parsed) ? parsed : 0;
113
+ }
114
+
115
+ const vv = window.visualViewport;
116
+ const innerHeight = window.innerHeight || document.documentElement.clientHeight || 0;
117
+ const innerWidth = window.innerWidth || document.documentElement.clientWidth || 0;
118
+ const visualHeight = vv?.height || innerHeight;
119
+ const visualWidth = vv?.width || innerWidth;
120
+ const offsetTop = vv?.offsetTop || 0;
121
+ const offsetLeft = vv?.offsetLeft || 0;
122
+ const keyboardInsetBottom = Math.max(0, innerHeight - (visualHeight + offsetTop));
123
+
124
+ return {
125
+ safe_area_top: envInset('top'),
126
+ safe_area_bottom: envInset('bottom'),
127
+ visual_viewport_height: Number(visualHeight),
128
+ visual_viewport_width: Number(visualWidth),
129
+ visual_viewport_offset_top: Number(offsetTop),
130
+ visual_viewport_offset_left: Number(offsetLeft),
131
+ keyboard_inset_bottom: Number(keyboardInsetBottom),
132
+ };
133
+ });
134
+ }
135
+
136
+ function inferWorkbenchStateMetadata({ label = '', attributes = {} } = {}) {
137
+ const text = `${label} ${attributes['aria-label'] || ''} ${attributes['data-state-target'] || ''}`.trim().toLowerCase();
138
+ const stateTags = [];
139
+ let workbenchStateType = null;
140
+ let priorityReason = null;
141
+ let priorityBonus = 0;
142
+
143
+ if (attributes['aria-expanded'] === 'true' || /detail|open detail|drawer|expand|show more|more/.test(text)) {
144
+ workbenchStateType = 'detail_open';
145
+ priorityReason = 'primary-detail-open';
146
+ priorityBonus += 22;
147
+ stateTags.push('detail', 'open');
148
+ }
149
+
150
+ if (/filter|filters|refine|segment/.test(text)) {
151
+ if (!workbenchStateType) workbenchStateType = 'active_filter';
152
+ if (!priorityReason) priorityReason = 'primary-filter-active';
153
+ priorityBonus += 18;
154
+ stateTags.push('filters');
155
+ }
156
+
157
+ if (/select all|bulk|multi-select|batch/.test(text)) {
158
+ workbenchStateType = 'bulk_select_mode';
159
+ priorityReason = 'primary-bulk-select';
160
+ priorityBonus += 20;
161
+ stateTags.push('bulk-select');
162
+ } else if (/select|selected|row|item|card/.test(text)) {
163
+ if (!workbenchStateType) workbenchStateType = 'selected_item';
164
+ if (!priorityReason) priorityReason = 'primary-selected-item';
165
+ priorityBonus += 14;
166
+ stateTags.push('selection');
167
+ }
168
+
169
+ return {
170
+ workbench_state_type: workbenchStateType,
171
+ state_tags: Array.from(new Set(stateTags)),
172
+ priority_reason: priorityReason,
173
+ priority_bonus: priorityBonus,
174
+ };
175
+ }
176
+
177
+ async function discoverCaptureStates(page, limit = 4) {
178
+ const discovered = await page.evaluate((maxStates) => {
179
+ function selectorFor(element) {
180
+ if (element.id) return `#${element.id}`;
181
+ if (element.getAttribute('data-state-target')) return `[data-state-target="${element.getAttribute('data-state-target')}"]`;
182
+ if (element.getAttribute('data-panel')) return `[data-panel="${element.getAttribute('data-panel')}"]`;
183
+ if (element.getAttribute('aria-controls')) return `[aria-controls="${element.getAttribute('aria-controls')}"]`;
184
+ return null;
185
+ }
186
+
187
+ function inferComponentFamily(element) {
188
+ const panel = element.closest('[data-panel]')?.getAttribute('data-panel');
189
+ if (panel === 'toolbar') return 'toolbar_controls';
190
+ if (panel === 'list') return 'list_rows';
191
+ if (panel === 'detail') return 'detail_blocks';
192
+ return panel ? `${panel}_controls` : null;
193
+ }
194
+
195
+ function basePriority(element) {
196
+ let score = 40;
197
+ if (element.hasAttribute('aria-expanded')) score += 30;
198
+ if (element.hasAttribute('data-state-target')) score += 24;
199
+ if (element.hasAttribute('aria-controls')) score += 20;
200
+ if (element.tagName.toLowerCase() === 'summary') score += 22;
201
+ if (element.id) score += 10;
202
+ if (element.getAttribute('data-panel') === 'toolbar') score += 14;
203
+ const text = (element.textContent || element.getAttribute('aria-label') || '').trim().toLowerCase();
204
+ if (/detail|open|expand|more|filter|select|show/.test(text)) score += 18;
205
+ if (/row|item|card/.test(text)) score += 8;
206
+ if ((element.textContent || '').trim()) score += 6;
207
+ return score;
208
+ }
209
+
210
+ const candidates = Array.from(document.querySelectorAll('button,[role="button"],summary,[aria-expanded],[data-state-target]'));
211
+ const states = [];
212
+ for (const element of candidates) {
213
+ if (states.length >= maxStates) break;
214
+ const selector = selectorFor(element);
215
+ if (!selector) continue;
216
+ const label = (element.textContent || element.getAttribute('aria-label') || element.id || element.tagName || '').trim().slice(0, 32) || 'interactive';
217
+ const baseScore = basePriority(element);
218
+ const attrs = {};
219
+ for (const name of element.getAttributeNames()) attrs[name] = element.getAttribute(name);
220
+ const semantic = (() => {
221
+ const text = `${label} ${attrs['aria-label'] || ''} ${attrs['data-state-target'] || ''}`.trim().toLowerCase();
222
+ const stateTags = [];
223
+ let workbenchStateType = null;
224
+ let priorityReason = null;
225
+ let priorityBonus = 0;
226
+ if (attrs['aria-expanded'] === 'true' || /detail|open detail|drawer|expand|show more|more/.test(text)) {
227
+ workbenchStateType = 'detail_open';
228
+ priorityReason = 'primary-detail-open';
229
+ priorityBonus += 22;
230
+ stateTags.push('detail', 'open');
231
+ }
232
+ if (/filter|filters|refine|segment/.test(text)) {
233
+ if (!workbenchStateType) workbenchStateType = 'active_filter';
234
+ if (!priorityReason) priorityReason = 'primary-filter-active';
235
+ priorityBonus += 18;
236
+ stateTags.push('filters');
237
+ }
238
+ if (/select all|bulk|multi-select|batch/.test(text)) {
239
+ workbenchStateType = 'bulk_select_mode';
240
+ priorityReason = 'primary-bulk-select';
241
+ priorityBonus += 20;
242
+ stateTags.push('bulk-select');
243
+ } else if (/select|selected|row|item|card/.test(text)) {
244
+ if (!workbenchStateType) workbenchStateType = 'selected_item';
245
+ if (!priorityReason) priorityReason = 'primary-selected-item';
246
+ priorityBonus += 14;
247
+ stateTags.push('selection');
248
+ }
249
+ return {
250
+ workbench_state_type: workbenchStateType,
251
+ state_tags: Array.from(new Set(stateTags)),
252
+ priority_reason: priorityReason,
253
+ priority_bonus: priorityBonus,
254
+ };
255
+ })();
256
+ states.push({
257
+ id: `auto-hover-${states.length + 1}`,
258
+ label: `hover:${label}`,
259
+ hover_selector: selector,
260
+ wait_ms: 80,
261
+ priority_score: baseScore + semantic.priority_bonus,
262
+ priority_reason: semantic.priority_reason || 'interactive-hover-candidate',
263
+ source: 'auto_discovered',
264
+ workbench_state_type: semantic.workbench_state_type,
265
+ state_tags: semantic.state_tags,
266
+ component_family: inferComponentFamily(element),
267
+ state_variant: 'hover',
268
+ });
269
+ if (states.length >= maxStates) break;
270
+ states.push({
271
+ id: `auto-click-${states.length + 1}`,
272
+ label: `click:${label}`,
273
+ click_selector: selector,
274
+ wait_ms: 120,
275
+ priority_score: baseScore + 8 + semantic.priority_bonus,
276
+ priority_reason: semantic.priority_reason || 'interactive-click-candidate',
277
+ source: 'auto_discovered',
278
+ workbench_state_type: semantic.workbench_state_type,
279
+ state_tags: semantic.state_tags,
280
+ component_family: inferComponentFamily(element),
281
+ state_variant: 'active',
282
+ });
283
+ }
284
+ return states
285
+ .sort((left, right) => (right.priority_score || 0) - (left.priority_score || 0))
286
+ .slice(0, maxStates);
287
+ }, Number.isFinite(Number(limit)) ? Number(limit) : 4);
288
+ return normalizeCaptureStates(discovered);
289
+ }
290
+
291
+ async function applyCaptureState(page, state = {}) {
292
+ const actions = [];
293
+ if (state.hover_selector) actions.push({ type: 'hover', selector: state.hover_selector });
294
+ if (state.click_selector) actions.push({ type: 'click', selector: state.click_selector });
295
+ if (state.focus_selector) actions.push({ type: 'focus', selector: state.focus_selector });
296
+ if (state.swipe_selector) {
297
+ actions.push({
298
+ type: 'swipe',
299
+ selector: state.swipe_selector,
300
+ direction: state.swipe_direction || 'up',
301
+ distance: state.swipe_distance,
302
+ });
303
+ }
304
+ if (state.drag_selector) {
305
+ actions.push({
306
+ type: 'drag',
307
+ selector: state.drag_selector,
308
+ to_selector: state.drag_to_selector || null,
309
+ dx: state.drag_dx,
310
+ dy: state.drag_dy,
311
+ });
312
+ }
313
+ if (state.scroll_selector || Number.isFinite(state.scroll_dx) || Number.isFinite(state.scroll_dy)) {
314
+ actions.push({
315
+ type: 'scroll',
316
+ selector: state.scroll_selector || null,
317
+ dx: state.scroll_dx,
318
+ dy: state.scroll_dy,
319
+ });
320
+ }
321
+ if (state.press_key) actions.push({ type: 'press', key: state.press_key });
322
+ if (Array.isArray(state.actions)) actions.push(...state.actions);
323
+
324
+ for (const action of actions) {
325
+ if (!action || typeof action !== 'object') continue;
326
+ switch (action.type) {
327
+ case 'hover':
328
+ if (typeof action.selector === 'string') await page.locator(action.selector).first().hover();
329
+ break;
330
+ case 'click':
331
+ if (typeof action.selector === 'string') await page.locator(action.selector).first().click();
332
+ break;
333
+ case 'focus':
334
+ if (typeof action.selector === 'string') await page.locator(action.selector).first().focus();
335
+ break;
336
+ case 'swipe': {
337
+ if (typeof action.selector === 'string') {
338
+ const locator = page.locator(action.selector).first();
339
+ const box = await locator.boundingBox();
340
+ if (box) {
341
+ const centerX = box.x + (box.width / 2);
342
+ const centerY = box.y + (box.height / 2);
343
+ const distance = Number.isFinite(Number(action.distance))
344
+ ? Number(action.distance)
345
+ : Math.max(48, Math.min(box.width, box.height) * 0.6);
346
+ let endX = centerX;
347
+ let endY = centerY;
348
+ switch ((action.direction || 'up').toLowerCase()) {
349
+ case 'down':
350
+ endY += distance;
351
+ break;
352
+ case 'left':
353
+ endX -= distance;
354
+ break;
355
+ case 'right':
356
+ endX += distance;
357
+ break;
358
+ case 'up':
359
+ default:
360
+ endY -= distance;
361
+ break;
362
+ }
363
+ await page.mouse.move(centerX, centerY);
364
+ await page.mouse.down();
365
+ await page.mouse.move(endX, endY, { steps: 8 });
366
+ await page.mouse.up();
367
+ }
368
+ }
369
+ break;
370
+ }
371
+ case 'drag': {
372
+ if (typeof action.selector === 'string') {
373
+ const locator = page.locator(action.selector).first();
374
+ const fromBox = await locator.boundingBox();
375
+ if (fromBox) {
376
+ const startX = fromBox.x + (fromBox.width / 2);
377
+ const startY = fromBox.y + (fromBox.height / 2);
378
+ let endX = startX + (Number.isFinite(Number(action.dx)) ? Number(action.dx) : 0);
379
+ let endY = startY + (Number.isFinite(Number(action.dy)) ? Number(action.dy) : 0);
380
+ if (typeof action.to_selector === 'string') {
381
+ const targetBox = await page.locator(action.to_selector).first().boundingBox();
382
+ if (targetBox) {
383
+ endX = targetBox.x + (targetBox.width / 2);
384
+ endY = targetBox.y + (targetBox.height / 2);
385
+ }
386
+ }
387
+ await page.mouse.move(startX, startY);
388
+ await page.mouse.down();
389
+ await page.mouse.move(endX, endY, { steps: 10 });
390
+ await page.mouse.up();
391
+ }
392
+ }
393
+ break;
394
+ }
395
+ case 'scroll':
396
+ if (typeof action.selector === 'string') {
397
+ const locator = page.locator(action.selector).first();
398
+ await locator.evaluate((element, payload) => {
399
+ element.scrollBy(payload.dx || 0, payload.dy || 0);
400
+ }, {
401
+ dx: Number.isFinite(Number(action.dx)) ? Number(action.dx) : 0,
402
+ dy: Number.isFinite(Number(action.dy)) ? Number(action.dy) : 0,
403
+ });
404
+ } else {
405
+ await page.mouse.wheel(
406
+ Number.isFinite(Number(action.dx)) ? Number(action.dx) : 0,
407
+ Number.isFinite(Number(action.dy)) ? Number(action.dy) : 0,
408
+ );
409
+ }
410
+ break;
411
+ case 'press':
412
+ if (typeof action.key === 'string') await page.keyboard.press(action.key);
413
+ break;
414
+ default:
415
+ break;
416
+ }
417
+ if (Number.isFinite(Number(action.wait_ms)) && Number(action.wait_ms) > 0) {
418
+ await page.waitForTimeout(Number(action.wait_ms));
419
+ }
420
+ }
421
+
422
+ if (Number.isFinite(state.wait_ms) && state.wait_ms > 0) {
423
+ await page.waitForTimeout(state.wait_ms);
424
+ }
425
+ }
426
+
427
+ function deriveFrameScreenshotPath(basePath, stateId, frameIndex) {
428
+ const ext = extname(basePath);
429
+ const suffix = `${stateId || 'state'}-frame-${frameIndex + 1}`.replace(/[^a-z0-9_-]+/gi, '-');
430
+ if (!ext) return `${basePath}-${suffix}`;
431
+ return `${basePath.slice(0, -ext.length)}-${suffix}${ext}`;
432
+ }
433
+
434
+ async function captureNodes(page, { panelAttr, panelSelector, stateId = null, stateLabel = null }) {
435
+ const effectivePanelSelector = panelSelector || `[${panelAttr}]`;
436
+ const nodes = await page.evaluate(({ attr, selector, captureStateId, captureStateLabel }) => {
437
+ function captureStyleSummary(element) {
438
+ const computed = window.getComputedStyle(element);
439
+ return {
440
+ fontSize: computed.fontSize,
441
+ lineHeight: computed.lineHeight,
442
+ borderWidth: computed.borderWidth,
443
+ borderRadius: computed.borderRadius,
444
+ boxShadow: computed.boxShadow,
445
+ paddingTop: computed.paddingTop,
446
+ paddingBottom: computed.paddingBottom,
447
+ paddingLeft: computed.paddingLeft,
448
+ paddingRight: computed.paddingRight,
449
+ };
450
+ }
451
+
452
+ const elements = Array.from(document.querySelectorAll(selector));
453
+ return elements.map((element) => {
454
+ const rect = element.getBoundingClientRect();
455
+ const styles = window.getComputedStyle(element);
456
+ const panelId = element.getAttribute(attr) || element.id || null;
457
+ const attrs = {};
458
+ for (const name of element.getAttributeNames()) attrs[name] = element.getAttribute(name);
459
+ const tag = element.tagName.toLowerCase();
460
+ const selectorHint = panelId ? `${tag}[${attr}="${panelId}"]` : tag;
461
+ const iconElements = Array.from(element.querySelectorAll('svg, [data-icon], i, .icon, [class*="icon"]'));
462
+ const rowElements = Array.from(element.querySelectorAll('[data-row],[role="row"],li,tr'));
463
+ const controlElements = Array.from(element.querySelectorAll('button,input,select,textarea,[role="button"],[aria-expanded]'));
464
+ const directChildren = Array.from(element.children || []);
465
+ const iconRects = iconElements
466
+ .map((icon) => {
467
+ const iconRect = icon.getBoundingClientRect();
468
+ return iconRect.width > 0 && iconRect.height > 0 ? { width: iconRect.width, height: iconRect.height } : null;
469
+ })
470
+ .filter(Boolean);
471
+ const rowRects = rowElements
472
+ .map((row) => {
473
+ const rowRect = row.getBoundingClientRect();
474
+ return rowRect.width > 0 && rowRect.height > 0 ? { width: rowRect.width, height: rowRect.height } : null;
475
+ })
476
+ .filter(Boolean);
477
+ const controlRects = controlElements
478
+ .map((control) => {
479
+ const controlRect = control.getBoundingClientRect();
480
+ return controlRect.width > 0 && controlRect.height > 0 ? { width: controlRect.width, height: controlRect.height } : null;
481
+ })
482
+ .filter(Boolean);
483
+ const floatingPrimaryActionPresent = controlElements.some((control) => {
484
+ const controlRect = control.getBoundingClientRect();
485
+ if (!(controlRect.width > 0 && controlRect.height > 0)) return false;
486
+ const computed = window.getComputedStyle(control);
487
+ const radius = Number.parseFloat(computed.borderRadius || '0') || 0;
488
+ const isRoundish = radius >= Math.min(controlRect.width, controlRect.height) * 0.3;
489
+ const inBottomRight = (controlRect.left + controlRect.width / 2) >= window.innerWidth * 0.55
490
+ && (controlRect.top + controlRect.height / 2) >= window.innerHeight * 0.55;
491
+ const sizeOk = controlRect.width >= 40 && controlRect.width <= 96 && controlRect.height >= 40 && controlRect.height <= 96;
492
+ const position = (computed.position || '').toLowerCase();
493
+ const floatingPosition = ['fixed', 'absolute', 'sticky'].includes(position);
494
+ return isRoundish && inBottomRight && sizeOk && floatingPosition;
495
+ });
496
+ const segmentedControls = Array.from(element.querySelectorAll('[role="tab"],[aria-selected],[data-active],[data-segment],[data-segmented-control]'));
497
+ const segmentedActiveStatePresent = segmentedControls.length >= 2 && segmentedControls.some((control) => {
498
+ const className = typeof control.className === 'string' ? control.className.toLowerCase() : '';
499
+ return control.getAttribute('aria-selected') === 'true'
500
+ || control.getAttribute('aria-current') === 'page'
501
+ || control.getAttribute('aria-current') === 'true'
502
+ || control.getAttribute('data-active') === 'true'
503
+ || /\b(active|selected|current)\b/.test(className);
504
+ });
505
+ const childRects = directChildren
506
+ .map((child) => child.getBoundingClientRect())
507
+ .filter((childRect) => childRect.width > 0 && childRect.height > 0)
508
+ .sort((left, right) => left.top - right.top);
509
+ const bottomSheetHandlePresent = directChildren.some((child) => {
510
+ const childRect = child.getBoundingClientRect();
511
+ if (!(childRect.width > 0 && childRect.height > 0)) return false;
512
+ const centered = Math.abs((childRect.left + (childRect.width / 2)) - (rect.left + (rect.width / 2))) <= Math.max(24, rect.width * 0.2);
513
+ const nearTop = (childRect.top - rect.top) <= 48;
514
+ return centered && nearTop && childRect.width >= 24 && childRect.width <= 120 && childRect.height >= 2 && childRect.height <= 12;
515
+ });
516
+ const activeTabPresent = controlElements.some((control) => {
517
+ const className = typeof control.className === 'string' ? control.className.toLowerCase() : '';
518
+ return control.getAttribute('aria-selected') === 'true'
519
+ || control.getAttribute('aria-current') === 'page'
520
+ || control.getAttribute('aria-current') === 'true'
521
+ || control.getAttribute('data-active') === 'true'
522
+ || /\b(active|selected|current)\b/.test(className);
523
+ });
524
+ const area = rect.width * rect.height;
525
+ const iconDensity = area > 0 ? (iconRects.length / area) * 10000 : 0;
526
+ const controlDensity = area > 0 ? (controlRects.length / area) * 10000 : 0;
527
+ const className = typeof element.className === 'string' ? element.className.toLowerCase() : '';
528
+ const loadingNodes = Array.from(element.querySelectorAll('[aria-busy="true"],[data-loading],[class*="loading"],[class*="spinner"],[role="progressbar"],[role="status"]'));
529
+ const skeletonNodes = Array.from(element.querySelectorAll('[data-skeleton],[class*="skeleton"],[class*="shimmer"],[class*="placeholder"]'));
530
+ const errorNodes = Array.from(element.querySelectorAll('[role="alert"],[aria-invalid="true"],[data-error],[class*="error"],[class*="danger"]'));
531
+ const searchNodes = Array.from(element.querySelectorAll('input[type="search"],[role="searchbox"],[data-search],[aria-label*="search" i]'));
532
+ const panelName = (panelId || '').toLowerCase();
533
+ const textContent = (element.textContent || '').trim().toLowerCase();
534
+ const loadingStatePresent = loadingNodes.length > 0
535
+ || panelName.includes('loading')
536
+ || /\bloading\b|\bsyncing\b|\bfetching\b/.test(textContent)
537
+ || element.getAttribute('aria-busy') === 'true';
538
+ const skeletonStatePresent = skeletonNodes.length > 0
539
+ || panelName.includes('skeleton')
540
+ || /\bshimmer\b|\bplaceholder\b/.test(className);
541
+ const errorStatePresent = errorNodes.length > 0
542
+ || panelName.includes('error')
543
+ || /\berror\b|\bfailed\b|\bretry\b|\bproblem\b/.test(textContent);
544
+ const searchActivePresent = searchNodes.some((searchNode) => {
545
+ const activeElement = document.activeElement;
546
+ const value = typeof searchNode.value === 'string' ? searchNode.value.trim() : '';
547
+ return activeElement === searchNode
548
+ || value.length > 0
549
+ || searchNode.getAttribute('aria-expanded') === 'true'
550
+ || searchNode.closest('[aria-expanded="true"]');
551
+ });
552
+ const detailBlockGaps = [];
553
+ for (let i = 1; i < childRects.length; i += 1) {
554
+ const gap = childRects[i].top - childRects[i - 1].bottom;
555
+ if (gap > 0) detailBlockGaps.push(gap);
556
+ }
557
+ const toolbarControlStyles = controlElements.map((control) => captureStyleSummary(control));
558
+ const listRowStyles = rowElements.map((row) => captureStyleSummary(row));
559
+ const detailBlockStyles = directChildren.map((child) => captureStyleSummary(child));
560
+ return {
561
+ panel_id: panelId,
562
+ selector: selectorHint,
563
+ capture_state_id: captureStateId,
564
+ capture_state_label: captureStateLabel,
565
+ attributes: attrs,
566
+ boundingClientRect: {
567
+ left: rect.left,
568
+ top: rect.top,
569
+ width: rect.width,
570
+ height: rect.height,
571
+ },
572
+ descendantMetrics: {
573
+ icon_count: iconRects.length,
574
+ icon_density: Number(iconDensity.toFixed(4)),
575
+ icon_rects: iconRects,
576
+ row_rects: rowRects,
577
+ control_count: controlRects.length,
578
+ control_density: Number(controlDensity.toFixed(4)),
579
+ control_rects: controlRects,
580
+ detail_block_gaps: detailBlockGaps.map((gap) => Number(gap.toFixed(2))),
581
+ bottom_sheet_handle_present: bottomSheetHandlePresent,
582
+ active_tab_present: activeTabPresent,
583
+ floating_primary_action_present: floatingPrimaryActionPresent,
584
+ segmented_control_active_present: segmentedActiveStatePresent,
585
+ loading_state_present: loadingStatePresent,
586
+ skeleton_state_present: skeletonStatePresent,
587
+ error_state_present: errorStatePresent,
588
+ search_active_present: searchActivePresent,
589
+ search_bar_present: searchNodes.length > 0 || panelName.includes('search'),
590
+ toolbar_control_styles: toolbarControlStyles,
591
+ list_row_styles: listRowStyles,
592
+ detail_block_styles: detailBlockStyles,
593
+ },
594
+ computedStyle: {
595
+ fontFamily: styles.fontFamily,
596
+ fontWeight: styles.fontWeight,
597
+ fontSize: styles.fontSize,
598
+ lineHeight: styles.lineHeight,
599
+ letterSpacing: styles.letterSpacing,
600
+ color: styles.color,
601
+ backgroundColor: styles.backgroundColor,
602
+ borderColor: styles.borderColor,
603
+ borderWidth: styles.borderWidth,
604
+ borderStyle: styles.borderStyle,
605
+ borderRadius: styles.borderRadius,
606
+ borderTopLeftRadius: styles.borderTopLeftRadius,
607
+ borderTopRightRadius: styles.borderTopRightRadius,
608
+ borderBottomLeftRadius: styles.borderBottomLeftRadius,
609
+ borderBottomRightRadius: styles.borderBottomRightRadius,
610
+ boxShadow: styles.boxShadow,
611
+ transitionDuration: styles.transitionDuration,
612
+ transitionTimingFunction: styles.transitionTimingFunction,
613
+ animationDuration: styles.animationDuration,
614
+ paddingTop: styles.paddingTop,
615
+ paddingRight: styles.paddingRight,
616
+ paddingBottom: styles.paddingBottom,
617
+ paddingLeft: styles.paddingLeft,
618
+ marginTop: styles.marginTop,
619
+ marginRight: styles.marginRight,
620
+ marginBottom: styles.marginBottom,
621
+ marginLeft: styles.marginLeft,
622
+ rowGap: styles.rowGap,
623
+ columnGap: styles.columnGap,
624
+ gap: styles.gap,
625
+ },
626
+ };
627
+ });
628
+ }, { attr: panelAttr, selector: effectivePanelSelector, captureStateId: stateId, captureStateLabel: stateLabel });
629
+ return { nodes, effectivePanelSelector };
630
+ }
631
+
632
+ export async function capturePageEvidence({
633
+ url,
634
+ screenshotPath,
635
+ snapshotPath,
636
+ width = 1440,
637
+ height = 900,
638
+ waitMs = 250,
639
+ platformProfile = null,
640
+ panelAttr = 'data-panel',
641
+ panelSelector,
642
+ captureStates = [],
643
+ autoDiscoverStates = false,
644
+ stateLimit = 4,
645
+ }) {
646
+ if (!url) throw new Error('url is required for capturePageEvidence');
647
+ if (!screenshotPath) throw new Error('screenshotPath is required for capturePageEvidence');
648
+ if (!snapshotPath) throw new Error('snapshotPath is required for capturePageEvidence');
649
+
650
+ const playwright = await loadPlaywright();
651
+ const browser = await playwright.chromium.launch({ headless: true });
652
+ try {
653
+ const profile = resolvePlatformProfile(platformProfile);
654
+ const openPage = async () => {
655
+ const page = await browser.newPage({
656
+ viewport: {
657
+ width: toNumber(width, profile?.viewport?.width ?? 1440),
658
+ height: toNumber(height, profile?.viewport?.height ?? 900),
659
+ },
660
+ userAgent: profile?.userAgent,
661
+ isMobile: profile?.isMobile === true,
662
+ hasTouch: profile?.hasTouch === true,
663
+ deviceScaleFactor: profile?.deviceScaleFactor,
664
+ });
665
+ await page.goto(url, { waitUntil: 'domcontentloaded' });
666
+ const effectiveWaitMs = toNumber(waitMs, profile?.waitMs ?? 0);
667
+ if (effectiveWaitMs > 0) await page.waitForTimeout(effectiveWaitMs);
668
+ return page;
669
+ };
670
+
671
+ const page = await openPage();
672
+ const { nodes, effectivePanelSelector } = await captureNodes(page, { panelAttr, panelSelector });
673
+ const viewportMetrics = await captureViewportMetrics(page);
674
+
675
+ mkdirSync(dirname(screenshotPath), { recursive: true });
676
+ mkdirSync(dirname(snapshotPath), { recursive: true });
677
+ await page.screenshot({ path: screenshotPath, fullPage: false });
678
+ await page.close();
679
+
680
+ const normalizedStates = normalizeCaptureStates(captureStates);
681
+ let discoveredStates = [];
682
+ if (autoDiscoverStates) {
683
+ const discoveryPage = await openPage();
684
+ try {
685
+ discoveredStates = await discoverCaptureStates(discoveryPage, stateLimit);
686
+ } finally {
687
+ await discoveryPage.close();
688
+ }
689
+ }
690
+ const effectiveStates = [...normalizedStates];
691
+ for (const discovered of discoveredStates) {
692
+ if (!effectiveStates.some((state) => state.id === discovered.id)) effectiveStates.push(discovered);
693
+ }
694
+ effectiveStates.sort((left, right) => (right.priority_score || 0) - (left.priority_score || 0));
695
+ const stateSnapshots = [];
696
+ for (let index = 0; index < effectiveStates.length; index += 1) {
697
+ const state = effectiveStates[index];
698
+ const statePage = await openPage();
699
+ await applyCaptureState(statePage, state);
700
+ const stateCapture = await captureNodes(statePage, {
701
+ panelAttr,
702
+ panelSelector,
703
+ stateId: state.id,
704
+ stateLabel: state.label,
705
+ });
706
+ const stateScreenshotPath = deriveStateScreenshotPath(screenshotPath, state.id, index);
707
+ mkdirSync(dirname(stateScreenshotPath), { recursive: true });
708
+ await statePage.screenshot({ path: stateScreenshotPath, fullPage: false });
709
+ const frameScreenshotFiles = [];
710
+ const sampleFrames = Number.isFinite(Number(state.sample_frames)) ? Math.max(0, Number(state.sample_frames)) : 0;
711
+ const frameIntervalMs = Number.isFinite(Number(state.frame_interval_ms)) ? Number(state.frame_interval_ms) : 40;
712
+ for (let frameIndex = 0; frameIndex < sampleFrames; frameIndex += 1) {
713
+ if (frameIntervalMs > 0) await statePage.waitForTimeout(frameIntervalMs);
714
+ const frameScreenshotPath = deriveFrameScreenshotPath(screenshotPath, state.id, frameIndex);
715
+ mkdirSync(dirname(frameScreenshotPath), { recursive: true });
716
+ await statePage.screenshot({ path: frameScreenshotPath, fullPage: false });
717
+ frameScreenshotFiles.push(frameScreenshotPath);
718
+ }
719
+ await statePage.close();
720
+ stateSnapshots.push({
721
+ id: state.id,
722
+ label: state.label,
723
+ expect_visual_change: state.expect_visual_change,
724
+ min_pixel_diff_ratio: state.min_pixel_diff_ratio,
725
+ reference_image: state.reference_image,
726
+ priority_score: state.priority_score,
727
+ priority_reason: state.priority_reason,
728
+ source: state.source,
729
+ workbench_state_type: state.workbench_state_type,
730
+ state_tags: state.state_tags,
731
+ component_family: state.component_family,
732
+ state_variant: state.state_variant,
733
+ expect_motion: state.expect_motion === true,
734
+ min_motion_changed_frames: Number.isFinite(Number(state.min_motion_changed_frames)) ? Number(state.min_motion_changed_frames) : null,
735
+ frame_screenshot_files: frameScreenshotFiles,
736
+ screenshot_file: stateScreenshotPath,
737
+ nodes_captured: stateCapture.nodes.length,
738
+ nodes: stateCapture.nodes,
739
+ });
740
+ }
741
+
742
+ const snapshot = {
743
+ url,
744
+ panel_attr: panelAttr,
745
+ panel_selector: effectivePanelSelector,
746
+ viewport: {
747
+ width: toNumber(width, profile?.viewport?.width ?? 1440),
748
+ height: toNumber(height, profile?.viewport?.height ?? 900),
749
+ },
750
+ platform_profile: typeof platformProfile === 'string' ? platformProfile : null,
751
+ viewport_metrics: viewportMetrics,
752
+ nodes,
753
+ states: stateSnapshots.map((state) => ({
754
+ id: state.id,
755
+ label: state.label,
756
+ expect_visual_change: state.expect_visual_change,
757
+ min_pixel_diff_ratio: state.min_pixel_diff_ratio,
758
+ reference_image: state.reference_image,
759
+ priority_score: state.priority_score,
760
+ priority_reason: state.priority_reason,
761
+ source: state.source,
762
+ workbench_state_type: state.workbench_state_type,
763
+ state_tags: state.state_tags,
764
+ component_family: state.component_family,
765
+ state_variant: state.state_variant,
766
+ expect_motion: state.expect_motion === true,
767
+ min_motion_changed_frames: state.min_motion_changed_frames,
768
+ frame_screenshot_files: state.frame_screenshot_files,
769
+ screenshot_file: state.screenshot_file,
770
+ nodes: state.nodes,
771
+ })),
772
+ };
773
+ writeFileSync(snapshotPath, JSON.stringify(snapshot, null, 2));
774
+ return {
775
+ screenshot_file: screenshotPath,
776
+ snapshot_file: snapshotPath,
777
+ nodes_captured: nodes.length,
778
+ platform_profile: typeof platformProfile === 'string' ? platformProfile : null,
779
+ panel_attr: panelAttr,
780
+ panel_selector: effectivePanelSelector,
781
+ viewport_metrics: viewportMetrics,
782
+ states: stateSnapshots.map((state) => ({
783
+ id: state.id,
784
+ label: state.label,
785
+ expect_visual_change: state.expect_visual_change,
786
+ min_pixel_diff_ratio: state.min_pixel_diff_ratio,
787
+ reference_image: state.reference_image,
788
+ priority_score: state.priority_score,
789
+ priority_reason: state.priority_reason,
790
+ source: state.source,
791
+ workbench_state_type: state.workbench_state_type,
792
+ state_tags: state.state_tags,
793
+ component_family: state.component_family,
794
+ state_variant: state.state_variant,
795
+ expect_motion: state.expect_motion,
796
+ min_motion_changed_frames: state.min_motion_changed_frames,
797
+ frame_screenshot_files: state.frame_screenshot_files,
798
+ screenshot_file: state.screenshot_file,
799
+ nodes_captured: state.nodes_captured,
800
+ })),
801
+ snapshot,
802
+ };
803
+ } finally {
804
+ await browser.close();
805
+ }
806
+ }