@vizzly-testing/cli 0.35.3-beta.1 → 0.35.3-beta.3

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 (34) hide show
  1. package/README.md +8 -3
  2. package/dist/api/endpoints.js +1 -46
  3. package/dist/api/index.js +1 -1
  4. package/dist/cli.js +5 -5
  5. package/dist/commands/api.js +3 -3
  6. package/dist/commands/builds.js +12 -15
  7. package/dist/commands/comparisons.js +7 -9
  8. package/dist/commands/context.js +10 -20
  9. package/dist/commands/init.js +62 -13
  10. package/dist/commands/review.js +122 -35
  11. package/dist/commands/run.js +1 -3
  12. package/dist/commands/status.js +35 -79
  13. package/dist/commands/upload.js +1 -1
  14. package/dist/context/local-workspace-provider.js +36 -126
  15. package/dist/reporter/reporter-bundle.iife.js +49 -81
  16. package/dist/server/handlers/tdd-handler.js +2 -7
  17. package/dist/server/routers/dashboard.js +5 -1
  18. package/dist/tdd/runtime-context.js +0 -2
  19. package/dist/tdd/services/comparison-service.js +36 -76
  20. package/dist/tdd/tdd-service.js +7 -205
  21. package/dist/types/index.d.ts +1 -1
  22. package/dist/uploader/core.js +7 -8
  23. package/dist/utils/visual-context-normalizers.js +86 -139
  24. package/package.json +4 -4
  25. package/skills/vizzly/SKILL.md +42 -52
  26. package/skills/vizzly/references/cli-context.md +49 -52
  27. package/skills/vizzly/references/dynamic-content.md +18 -29
  28. package/skills/vizzly/references/sdks.md +20 -28
  29. package/skills/vizzly/references/setup-ci.md +18 -42
  30. package/dist/tdd/core/hotspot-coverage.js +0 -112
  31. package/dist/tdd/core/region-coverage.js +0 -203
  32. package/dist/tdd/metadata/hotspot-metadata.js +0 -93
  33. package/dist/tdd/metadata/region-metadata.js +0 -56
  34. package/skills/vizzly/agents/openai.yaml +0 -4
@@ -1,44 +1,43 @@
1
1
  /**
2
- * Read the API's canonical visual-review state with legacy approval fallbacks.
2
+ * Read the API's visual-review state.
3
3
  *
4
4
  * @param {Object} record - Comparison or review record from the API.
5
5
  * @returns {string|null} The review state when the API supplied one.
6
6
  */
7
7
  export function getVisualReviewState(record = {}) {
8
- return record.visual_review?.state || record.review_state || record.approval_status || null;
8
+ return record.visual_review?.state || null;
9
9
  }
10
10
 
11
11
  /**
12
- * Keep visual outcome separate from processing status when both are available.
12
+ * Read the visual comparison result.
13
13
  *
14
14
  * @param {Object} comparison - Comparison record from the API.
15
- * @returns {string|null} The visual result, or the legacy status fallback.
15
+ * @returns {string|null} The visual result when the API supplied one.
16
16
  */
17
17
  export function getComparisonResult(comparison = {}) {
18
- return comparison.result || comparison.status || null;
18
+ return comparison.result || null;
19
19
  }
20
20
 
21
21
  /**
22
- * Resolve a screenshot name across current and legacy response shapes.
22
+ * Read the current screenshot name.
23
23
  *
24
24
  * @param {Object} comparison - Comparison record from the API.
25
- * @returns {string|null} The first API-provided name or identity fallback.
25
+ * @returns {string|null} The screenshot name when the API supplied one.
26
26
  */
27
27
  export function getComparisonName(comparison = {}) {
28
- return comparison.name || comparison.screenshot_name || comparison.current_screenshot?.name || comparison.screenshot?.name || comparison.current_name || comparison.id || null;
28
+ return comparison.screenshot?.name || null;
29
29
  }
30
30
 
31
31
  /**
32
- * Resolve CSS viewport dimensions without confusing them with bitmap size.
32
+ * Read CSS viewport dimensions without confusing them with bitmap size.
33
33
  *
34
34
  * @param {Object} comparison - Comparison record from the API.
35
35
  * @returns {{width: number|null, height: number|null}|null} Viewport dimensions.
36
36
  */
37
37
  export function getComparisonViewport(comparison = {}) {
38
- let current = comparison.current_screenshot || comparison.screenshot || {};
39
- let viewport = comparison.viewport || current.viewport || {};
40
- let width = viewport.width ?? comparison.viewport_width ?? current.viewport_width ?? comparison.current_viewport_width ?? null;
41
- let height = viewport.height ?? comparison.viewport_height ?? current.viewport_height ?? comparison.current_viewport_height ?? null;
38
+ let viewport = comparison.screenshot?.viewport || {};
39
+ let width = viewport.width ?? null;
40
+ let height = viewport.height ?? null;
42
41
  return width != null || height != null ? {
43
42
  width,
44
43
  height
@@ -46,26 +45,25 @@ export function getComparisonViewport(comparison = {}) {
46
45
  }
47
46
 
48
47
  /**
49
- * Resolve the browser from nested current screenshot or legacy metadata fields.
48
+ * Read the browser from the current screenshot.
50
49
  *
51
50
  * @param {Object} comparison - Comparison record from the API.
52
51
  * @returns {string|null} The browser name when present.
53
52
  */
54
53
  export function getComparisonBrowser(comparison = {}) {
55
- return comparison.browser || comparison.current_screenshot?.browser || comparison.screenshot?.browser || comparison.current_browser || comparison.current_metadata?.browser || comparison.current_metadata?.properties?.browser || null;
54
+ return comparison.screenshot?.browser || null;
56
55
  }
57
56
 
58
57
  /**
59
- * Resolve bitmap dimensions from nested or flat screenshot fields.
58
+ * Read bitmap dimensions from a screenshot record.
60
59
  *
61
- * @param {Object} record - Preferred screenshot record.
62
- * @param {Object} fallback - Legacy fields to use when the record is incomplete.
60
+ * @param {Object} record - Screenshot record from the API.
63
61
  * @returns {{width: number|null, height: number|null}|null} Bitmap dimensions.
64
62
  */
65
- function getBitmap(record = {}, fallback = {}) {
66
- let bitmap = record.bitmap || fallback.bitmap || {};
67
- let width = bitmap.width ?? record.bitmap_width ?? fallback.bitmap_width ?? null;
68
- let height = bitmap.height ?? record.bitmap_height ?? fallback.bitmap_height ?? null;
63
+ function getBitmap(record = {}) {
64
+ let bitmap = record.bitmap || {};
65
+ let width = bitmap.width ?? null;
66
+ let height = bitmap.height ?? null;
69
67
  return width != null || height != null ? {
70
68
  width,
71
69
  height
@@ -73,16 +71,15 @@ function getBitmap(record = {}, fallback = {}) {
73
71
  }
74
72
 
75
73
  /**
76
- * Resolve viewport dimensions from one screenshot-shaped record.
74
+ * Read viewport dimensions from a screenshot record.
77
75
  *
78
- * @param {Object} record - Preferred screenshot record.
79
- * @param {Object} fallback - Legacy fields to use when the record is incomplete.
76
+ * @param {Object} record - Screenshot record from the API.
80
77
  * @returns {{width: number|null, height: number|null}|null} Viewport dimensions.
81
78
  */
82
- function getRecordViewport(record = {}, fallback = {}) {
83
- let viewport = record.viewport || fallback.viewport || {};
84
- let width = viewport.width ?? record.viewport_width ?? fallback.viewport_width ?? null;
85
- let height = viewport.height ?? record.viewport_height ?? fallback.viewport_height ?? null;
79
+ function getRecordViewport(record = {}) {
80
+ let viewport = record.viewport || {};
81
+ let width = viewport.width ?? null;
82
+ let height = viewport.height ?? null;
86
83
  return width != null || height != null ? {
87
84
  width,
88
85
  height
@@ -93,30 +90,22 @@ function getRecordViewport(record = {}, fallback = {}) {
93
90
  * Normalize current screenshot identity, render facts, metadata, and asset URL.
94
91
  *
95
92
  * @param {Object} comparison - Comparison record from the API.
96
- * @returns {Object} A stable current screenshot projection.
93
+ * @returns {Object} Current screenshot details.
97
94
  */
98
95
  function getCurrentScreenshot(comparison = {}) {
99
- let screenshot = comparison.current_screenshot || comparison.screenshot || {};
96
+ let screenshot = comparison.screenshot || {};
100
97
  return {
101
- id: screenshot.id || comparison.current_screenshot_id || null,
102
- name: screenshot.name || getComparisonName(comparison),
103
- status: screenshot.status || comparison.screenshot_status || null,
104
- browser: screenshot.browser || getComparisonBrowser(comparison),
105
- device: screenshot.device || comparison.device || null,
106
- viewport: getRecordViewport(screenshot, {
107
- viewport: comparison.viewport,
108
- viewport_width: comparison.current_viewport_width ?? comparison.viewport_width,
109
- viewport_height: comparison.current_viewport_height ?? comparison.viewport_height
110
- }) || getComparisonViewport(comparison),
111
- bitmap: getBitmap(screenshot, {
112
- bitmap: comparison.bitmap,
113
- bitmap_width: comparison.current_width ?? comparison.bitmap_width,
114
- bitmap_height: comparison.current_height ?? comparison.bitmap_height
115
- }),
116
- metadata: screenshot.metadata || comparison.current_metadata || comparison.metadata || null,
117
- signature: screenshot.signature || comparison.current_signature || comparison.signature || null,
118
- url: screenshot.url || screenshot.original_url || comparison.current_url || comparison.current_original_url || comparison.current_screenshot_url || null,
119
- error_message: screenshot.error_message || comparison.error_message || null
98
+ id: screenshot.id || null,
99
+ name: screenshot.name || null,
100
+ status: screenshot.status || null,
101
+ browser: screenshot.browser || null,
102
+ device: screenshot.device || null,
103
+ viewport: getRecordViewport(screenshot),
104
+ bitmap: getBitmap(screenshot),
105
+ metadata: screenshot.metadata || null,
106
+ signature: screenshot.signature || null,
107
+ url: screenshot.url || null,
108
+ error_message: screenshot.error_message || null
120
109
  };
121
110
  }
122
111
 
@@ -124,31 +113,28 @@ function getCurrentScreenshot(comparison = {}) {
124
113
  * Normalize the baseline screenshot without inventing missing render facts.
125
114
  *
126
115
  * @param {Object} comparison - Comparison record from the API.
127
- * @returns {Object} A stable baseline screenshot projection.
116
+ * @returns {Object} Baseline screenshot details.
128
117
  */
129
118
  function getBaselineScreenshot(comparison = {}) {
130
- let current = comparison.current_screenshot || comparison.screenshot || {};
131
- let baseline = comparison.baseline_screenshot || comparison.baseline || current.baseline || {};
119
+ let baseline = comparison.screenshot?.baseline || {};
132
120
  return {
133
- id: baseline.id || comparison.baseline_screenshot_id || null,
134
- build_id: baseline.build_id || comparison.baseline_build_id || null,
135
- name: baseline.name || comparison.baseline_name || null,
121
+ id: baseline.id || null,
122
+ build_id: baseline.build_id || null,
123
+ name: baseline.name || null,
136
124
  browser: baseline.browser || null,
137
125
  viewport: getRecordViewport(baseline),
138
126
  bitmap: getBitmap(baseline),
139
- metadata: baseline.metadata || comparison.baseline_metadata || null,
140
- signature: baseline.signature || comparison.baseline_signature || null,
141
- url: baseline.url || baseline.original_url || comparison.baseline_url || comparison.baseline_original_url || comparison.baseline_screenshot_url || null
127
+ metadata: baseline.metadata || null,
128
+ signature: baseline.signature || null,
129
+ url: baseline.url || null
142
130
  };
143
131
  }
144
132
 
145
133
  /**
146
- * Project compact Honeydiff facts without making raw geometry the default.
134
+ * Keep the useful Honeydiff facts without including raw geometry by default.
147
135
  *
148
- * Agents need stable counts, fingerprints, URLs, the API projection, and the
149
- * server's authoritative artifact manifest for first-pass diagnosis. Keeping
150
- * that manifest intact prevents the CLI from becoming less authoritative than
151
- * the API. Raw regions and scoring details stay behind an explicit include
136
+ * Agents need counts, fingerprints, URLs, analysis details, and the server's
137
+ * artifact list for first-pass diagnosis. Raw regions and scoring details stay behind an explicit include
152
138
  * because they can dominate an otherwise bounded handoff.
153
139
  *
154
140
  * @param {Object} comparison - Comparison record from the API.
@@ -156,20 +142,19 @@ function getBaselineScreenshot(comparison = {}) {
156
142
  * @returns {Object} Compact diff evidence suitable for context summaries.
157
143
  */
158
144
  function getComparisonDiff(comparison = {}, includeDiffs = false) {
159
- let diff = comparison.diff || comparison.diff_image || comparison.analysis || {};
160
- let analysis = comparison.analysis || {};
161
- let regions = diff.regions || diff.diff_regions || analysis.diff_regions || comparison.diff_regions;
162
- let projection = diff.projection || diff.analysis_projection || analysis.projection || analysis.analysis_projection || comparison.analysis_projection || comparison.projection || null;
163
- let artifacts = diff.artifacts ?? analysis.artifacts ?? comparison.artifacts ?? null;
145
+ let diff = comparison.diff || {};
146
+ let regions = diff.regions;
147
+ let details = diff.details || null;
148
+ let artifacts = diff.artifacts ?? null;
164
149
  let compact = {
165
- percentage: diff.percentage ?? analysis.percentage ?? analysis.diff_percentage ?? comparison.diff_percentage ?? null,
166
- changed_pixels: diff.changed_pixels ?? analysis.changed_pixels ?? comparison.changed_pixels ?? null,
167
- total_pixels: diff.total_pixels ?? analysis.total_pixels ?? comparison.total_pixels ?? null,
168
- threshold: diff.threshold ?? analysis.threshold ?? comparison.threshold ?? null,
169
- fingerprint_hash: diff.fingerprint_hash || analysis.fingerprint_hash || comparison.fingerprint_hash || null,
170
- region_count: diff.region_count ?? analysis.region_count ?? comparison.region_count ?? projection?.clusters?.count ?? (Array.isArray(regions) ? regions.length : null),
171
- projection,
172
- image_url: diff.image_url || diff.url || analysis.diff_image_url || comparison.diff_url || comparison.diff_image_url || null
150
+ percentage: diff.percentage ?? null,
151
+ changed_pixels: diff.changed_pixels ?? null,
152
+ total_pixels: diff.total_pixels ?? null,
153
+ threshold: diff.threshold ?? null,
154
+ fingerprint_hash: diff.fingerprint_hash || null,
155
+ region_count: diff.region_count ?? details?.clusters?.count ?? (Array.isArray(regions) ? regions.length : null),
156
+ details,
157
+ image_url: diff.image_url || null
173
158
  };
174
159
 
175
160
  // Preserve server-owned evidence exactly so agents can verify and download
@@ -179,10 +164,10 @@ function getComparisonDiff(comparison = {}, includeDiffs = false) {
179
164
  }
180
165
  if (includeDiffs) {
181
166
  compact.regions = regions || [];
182
- compact.cluster_metadata = diff.cluster_metadata || analysis.cluster_metadata || comparison.cluster_metadata || null;
183
- compact.ssim_score = diff.ssim_score ?? analysis.ssim_score ?? comparison.ssim_score ?? null;
184
- compact.gmsd_score = diff.gmsd_score ?? analysis.gmsd_score ?? comparison.gmsd_score ?? null;
185
- compact.diff_lines = diff.diff_lines || analysis.diff_lines || comparison.diff_lines || [];
167
+ compact.cluster_metadata = diff.cluster_metadata || null;
168
+ compact.ssim_score = diff.ssim_score ?? null;
169
+ compact.gmsd_score = diff.gmsd_score ?? null;
170
+ compact.diff_lines = diff.diff_lines || [];
186
171
  }
187
172
  return compact;
188
173
  }
@@ -191,7 +176,7 @@ function getComparisonDiff(comparison = {}, includeDiffs = false) {
191
176
  * Prefer explicit review need and return null when the API supplied no review fact.
192
177
  *
193
178
  * @param {Object} comparison - Comparison record from the API.
194
- * @param {string|null} reviewState - Canonical review state for the record.
179
+ * @param {string|null} reviewState - Review state for the record.
195
180
  * @returns {boolean|null} Whether the comparison needs review, when known.
196
181
  */
197
182
  function comparisonNeedsReview(comparison = {}, reviewState = null) {
@@ -205,28 +190,24 @@ function comparisonNeedsReview(comparison = {}, reviewState = null) {
205
190
  }
206
191
 
207
192
  /**
208
- * Normalize one comparison across grouped and legacy flat API response shapes.
193
+ * Normalize one comparison from the current build-context API.
209
194
  *
210
195
  * @param {Object} comparison - Comparison record from the API.
211
196
  * @param {Object} options - Normalization options.
212
- * @param {string|null} [options.fallbackName] - Group name for unnamed variants.
213
197
  * @param {boolean} [options.includeDiffs] - Include raw Honeydiff diagnostics.
214
198
  * @returns {Object} A stable comparison evidence record.
215
199
  */
216
200
  export function normalizeComparisonRecord(comparison = {}, options = {}) {
217
201
  let reviewState = getVisualReviewState(comparison);
218
- let name = comparison.name || comparison.screenshot_name || comparison.current_screenshot?.name || comparison.screenshot?.name || comparison.current_name || options.fallbackName || comparison.id || null;
219
202
  return {
220
203
  id: comparison.id || null,
221
- name,
204
+ name: getComparisonName(comparison),
222
205
  result: getComparisonResult(comparison),
223
206
  status: comparison.status || null,
224
207
  review_state: reviewState,
225
208
  visual_review: comparison.visual_review || null,
226
- approval_status: comparison.approval_status || null,
227
209
  build_branch: comparison.build_branch || null,
228
210
  needs_review: comparisonNeedsReview(comparison, reviewState),
229
- is_flaky: comparison.is_flaky == null ? null : comparison.is_flaky === true,
230
211
  browser: getComparisonBrowser(comparison),
231
212
  viewport: getComparisonViewport(comparison),
232
213
  screenshot: getCurrentScreenshot(comparison),
@@ -236,13 +217,13 @@ export function normalizeComparisonRecord(comparison = {}, options = {}) {
236
217
  }
237
218
 
238
219
  /**
239
- * Read the server's total variant count across supported response shapes.
220
+ * Read the server's total variant count.
240
221
  *
241
222
  * @param {Object} group - Screenshot group from the API.
242
223
  * @returns {number|null} The explicit total when present.
243
224
  */
244
225
  function getExplicitVariantCount(group = {}) {
245
- return group.variant_count ?? group.total_variants ?? group.totalVariants ?? null;
226
+ return group.total_variants ?? null;
246
227
  }
247
228
 
248
229
  /**
@@ -250,13 +231,9 @@ function getExplicitVariantCount(group = {}) {
250
231
  *
251
232
  * @param {Object} group - Screenshot group from the API.
252
233
  * @param {Object[]} variants - Returned normalized variants.
253
- * @param {boolean} forcedComplete - Whether the caller owns the complete set.
254
234
  * @returns {boolean} Whether aggregate derivation is safe.
255
235
  */
256
- function hasCompleteVariants(group = {}, variants = [], forcedComplete = false) {
257
- if (forcedComplete || group.variants_complete === true) {
258
- return true;
259
- }
236
+ function hasCompleteVariants(group = {}, variants = []) {
260
237
  let explicitCount = getExplicitVariantCount(group);
261
238
  return explicitCount != null && explicitCount === variants.length;
262
239
  }
@@ -264,7 +241,7 @@ function hasCompleteVariants(group = {}, variants = [], forcedComplete = false)
264
241
  /**
265
242
  * Derive only facts backed by a complete variant set and complete source fields.
266
243
  *
267
- * Unknown review, flaky, result, or diff values stay null instead of becoming
244
+ * Unknown review, result, or diff values stay null instead of becoming
268
245
  * client-authored false or zero values.
269
246
  *
270
247
  * @param {Object[]} variants - Complete normalized variants for one screenshot.
@@ -278,12 +255,10 @@ function deriveAggregateFacts(variants = [], complete = false) {
278
255
  let results = variants.map(variant => variant.result);
279
256
  let reviewStates = variants.map(variant => variant.review_state);
280
257
  let needsReview = variants.map(variant => variant.needs_review);
281
- let flaky = variants.map(variant => variant.is_flaky);
282
258
  let percentages = variants.map(variant => variant.diff.percentage).filter(value => value != null);
283
259
  let allResultsKnown = results.every(value => value != null);
284
260
  let allReviewStatesKnown = reviewStates.every(value => value != null);
285
261
  let allNeedsReviewKnown = needsReview.every(value => value != null);
286
- let allFlakyKnown = flaky.every(value => value != null);
287
262
  return {
288
263
  has_changes: results.includes('changed') ? true : allResultsKnown ? false : null,
289
264
  has_new: results.includes('new') ? true : allResultsKnown ? false : null,
@@ -292,27 +267,24 @@ function deriveAggregateFacts(variants = [], complete = false) {
292
267
  needs_review_count: allNeedsReviewKnown ? needsReview.filter(value => value === true).length : null,
293
268
  failed_count: allResultsKnown ? results.filter(result => ['failed', 'error'].includes(result)).length : null,
294
269
  has_rejected: reviewStates.includes('rejected') ? true : allReviewStatesKnown ? false : null,
295
- has_flaky: flaky.includes(true) ? true : allFlakyKnown ? false : null,
296
270
  max_diff_percentage: percentages.length === variants.length && percentages.length > 0 ? Math.max(...percentages) : null
297
271
  };
298
272
  }
299
273
 
300
274
  /**
301
- * Normalize a screenshot group while keeping explicit server aggregates authoritative.
275
+ * Normalize a screenshot group while keeping explicit server totals unchanged.
302
276
  *
303
277
  * @param {Object} group - Grouped visual-review record from the API.
304
278
  * @param {Object} options - Normalization options.
305
- * @param {boolean} [options.variantsComplete] - Marks a caller-owned complete set.
306
279
  * @returns {Object} A normalized screenshot group and its variants.
307
280
  */
308
281
  export function normalizeComparisonGroup(group = {}, options = {}) {
309
- let rawVariants = group.variants || group.comparisons || [];
310
- let groupName = group.name || group.test_name || group.testName || null;
282
+ let rawVariants = group.comparisons || [];
283
+ let groupName = group.name || null;
311
284
  let variants = rawVariants.map(variant => normalizeComparisonRecord(variant, {
312
- fallbackName: groupName,
313
285
  includeDiffs: options.includeDiffs
314
286
  }));
315
- let complete = hasCompleteVariants(group, variants, options.variantsComplete === true);
287
+ let complete = hasCompleteVariants(group, variants);
316
288
  let derived = deriveAggregateFacts(variants, complete);
317
289
  let aggregate = group.aggregate_status || {};
318
290
  let needsReviewCount = aggregate.needs_review_count ?? derived.needs_review_count ?? null;
@@ -329,36 +301,11 @@ export function normalizeComparisonGroup(group = {}, options = {}) {
329
301
  needs_review_count: needsReviewCount,
330
302
  failed_count: aggregate.failed_count ?? derived.failed_count ?? null,
331
303
  has_rejected: aggregate.has_rejected ?? derived.has_rejected ?? null,
332
- has_flaky: aggregate.has_flaky ?? derived.has_flaky ?? null,
333
304
  max_diff_percentage: aggregate.max_diff_percentage ?? derived.max_diff_percentage ?? null
334
305
  }
335
306
  };
336
307
  }
337
308
 
338
- /**
339
- * Group a complete legacy flat comparison list by screenshot name.
340
- *
341
- * @param {Object[]} comparisons - Complete flat comparisons from build context.
342
- * @param {Object} options - Normalization options passed to each comparison.
343
- * @returns {Object[]} Normalized screenshot groups in API order.
344
- */
345
- function groupFlatComparisons(comparisons = [], options = {}) {
346
- let grouped = new Map();
347
- for (let comparison of comparisons) {
348
- let name = getComparisonName(comparison) || 'unknown screenshot';
349
- let group = grouped.get(name) || {
350
- name,
351
- variants: []
352
- };
353
- group.variants.push(comparison);
354
- grouped.set(name, group);
355
- }
356
- return [...grouped.values()].map(group => normalizeComparisonGroup(group, {
357
- ...options,
358
- variantsComplete: true
359
- }));
360
- }
361
-
362
309
  /**
363
310
  * Join compact group variants to the exact comparison records in the same API
364
311
  * response.
@@ -375,11 +322,10 @@ function groupFlatComparisons(comparisons = [], options = {}) {
375
322
  function joinGroupComparisons(groups = [], comparisons = []) {
376
323
  let comparisonsById = new Map(comparisons.filter(comparison => comparison?.id).map(comparison => [comparison.id, comparison]));
377
324
  return groups.map(group => {
378
- let variantKey = Array.isArray(group.variants) ? 'variants' : 'comparisons';
379
- let variants = Array.isArray(group[variantKey]) ? group[variantKey] : [];
325
+ let comparisons = Array.isArray(group.comparisons) ? group.comparisons : [];
380
326
  return {
381
327
  ...group,
382
- [variantKey]: variants.map(variant => ({
328
+ comparisons: comparisons.map(variant => ({
383
329
  ...variant,
384
330
  ...(comparisonsById.get(variant.id) || {})
385
331
  }))
@@ -399,9 +345,10 @@ function joinGroupComparisons(groups = [], comparisons = []) {
399
345
  */
400
346
  function normalizeFailedCapture(screenshot = {}, options = {}) {
401
347
  let normalized = normalizeComparisonRecord({
402
- ...screenshot,
403
348
  screenshot,
404
- result: screenshot.result || screenshot.status || 'failed'
349
+ result: 'failed',
350
+ status: screenshot.status || null,
351
+ visual_review: screenshot.comparison?.visual_review || null
405
352
  }, options);
406
353
  return {
407
354
  ...normalized,
@@ -410,14 +357,14 @@ function normalizeFailedCapture(screenshot = {}, options = {}) {
410
357
  }
411
358
 
412
359
  /**
413
- * Collect failed captures from explicit and screenshot-list response shapes.
360
+ * Collect failed captures from the build context response.
414
361
  *
415
362
  * @param {Object} context - Raw build context response.
416
363
  * @param {Object} options - Normalization options passed to each capture.
417
364
  * @returns {Object[]} Deduplicated failed captures in API order.
418
365
  */
419
366
  function getFailedCaptures(context = {}, options = {}) {
420
- let explicit = Array.isArray(context.failed_captures) ? context.failed_captures : Array.isArray(context.failed_screenshots) ? context.failed_screenshots : [];
367
+ let explicit = Array.isArray(context.failed_captures) ? context.failed_captures : [];
421
368
  let screenshots = (context.screenshots || []).filter(screenshot => ['failed', 'error'].includes(screenshot.status));
422
369
  let captures = [...explicit, ...screenshots];
423
370
  let seen = new Set();
@@ -432,7 +379,7 @@ function getFailedCaptures(context = {}, options = {}) {
432
379
  }
433
380
 
434
381
  /**
435
- * Adapt raw build context for human presentation across current and legacy APIs.
382
+ * Adapt raw build context for human presentation.
436
383
  *
437
384
  * Raw JSON output intentionally bypasses this lossy presentation boundary.
438
385
  *
@@ -445,7 +392,7 @@ export function normalizeBuildContext(context = {}, options = {}) {
445
392
  let rawComparisons = context.comparisons || [];
446
393
  let comparisons = rawComparisons.map(comparison => normalizeComparisonRecord(comparison, options));
447
394
  let joinedGroups = joinGroupComparisons(context.groups || [], rawComparisons);
448
- let groups = joinedGroups.length > 0 ? joinedGroups.map(group => normalizeComparisonGroup(group, options)) : groupFlatComparisons(rawComparisons, options);
395
+ let groups = joinedGroups.map(group => normalizeComparisonGroup(group, options));
449
396
  return {
450
397
  ...context,
451
398
  comparisons,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizzly-testing/cli",
3
- "version": "0.35.3-beta.1",
3
+ "version": "0.35.3-beta.3",
4
4
  "description": "Visual regression testing from your terminal",
5
5
  "keywords": [
6
6
  "llm-context",
@@ -94,7 +94,7 @@
94
94
  },
95
95
  "dependencies": {
96
96
  "@vizzly-testing/bear-den": "0.1.4",
97
- "@vizzly-testing/honeydiff": "^0.15.0",
97
+ "@vizzly-testing/honeydiff": "^0.16.0",
98
98
  "ansis": "^4.2.0",
99
99
  "commander": "^15.0.0",
100
100
  "cosmiconfig": "^9.0.0",
@@ -121,7 +121,7 @@
121
121
  "@babel/preset-env": "^8.0.2",
122
122
  "@babel/preset-react": "^8.0.1",
123
123
  "@babel/preset-typescript": "^8.0.1",
124
- "@biomejs/biome": "^2.3.8",
124
+ "@biomejs/biome": "^2.5.10",
125
125
  "@heroicons/react": "^2.2.0",
126
126
  "@modelcontextprotocol/sdk": "^1.0.4",
127
127
  "@playwright/test": "^1.55.1",
@@ -138,7 +138,7 @@
138
138
  "tsd": "^0.33.0",
139
139
  "tui-driver": "github:vizzly-testing/tui-driver",
140
140
  "typescript": "^7.0.2",
141
- "vite": "^8.0.0",
141
+ "vite": "^8.2.2",
142
142
  "wouter": "^3.7.1"
143
143
  }
144
144
  }
@@ -1,74 +1,64 @@
1
1
  ---
2
2
  name: vizzly
3
- description: "Inspect, explain, and debug Vizzly visual regression evidence; use an existing Vizzly workflow before and after UI changes; add or adjust screenshot capture; troubleshoot local TDD or cloud builds; or configure Vizzly CI. Use when a task mentions Vizzly, screenshot baselines, comparisons, Honeydiff, visual review, dynamic regions, or a repository's Vizzly setup."
3
+ description: Inspect and explain Vizzly visual regression evidence, verify UI changes, troubleshoot local or cloud builds, adjust screenshot capture, or configure Vizzly CI. Use when a task mentions Vizzly, screenshot baselines, comparisons, Honeydiff, visual review, or an existing Vizzly workflow.
4
4
  ---
5
5
 
6
6
  # Vizzly
7
7
 
8
- Use Vizzly as visual evidence for user-facing changes. Keep the repository's
9
- existing test workflow in charge of how the UI is exercised.
8
+ Use Vizzly as evidence for user-facing changes. Keep the repository's existing
9
+ test workflow in charge of how the UI is exercised.
10
10
 
11
- ## Start From The Repository
11
+ ## Prepare
12
12
 
13
- 1. Confirm that the repository uses Vizzly by checking its configuration,
14
- dependencies, scripts, or existing `.vizzly` data.
15
- 2. Use the repository's established CLI invocation. The examples in this skill
16
- use `vizzly`; substitute its package script, `pnpm exec vizzly`, or
17
- `npx vizzly` when that is how the repository runs local binaries.
18
- 3. Do not install, initialize, log in, or change credentials unless the task
19
- explicitly includes setup. Never print or persist a token.
13
+ 1. Confirm the repository uses Vizzly by checking its config, dependencies,
14
+ scripts, or `.vizzly` data.
15
+ 2. Use the repository's established CLI invocation exactly. Otherwise, replace
16
+ `vizzly` in these examples with `pnpm exec vizzly`; use `npx vizzly` when the
17
+ repository uses npm.
18
+ 3. Do not install, initialize, log in, or change credentials unless setup is in
19
+ scope. Never print or persist a token.
20
20
 
21
- ## Follow The Evidence Loop
21
+ ## Inspect And Verify
22
22
 
23
- 1. Choose the most relevant source:
24
- - Use a supplied cloud build or comparison ID when the task names one.
25
- - Use existing local context when `.vizzly` contains the run under review.
26
- - Generate fresh evidence through the repository's existing visual test
27
- workflow when stored evidence may be stale or absent.
28
- 2. Request the bounded, machine-readable build context:
23
+ 1. Choose the supplied cloud build or comparison when one is named. Otherwise,
24
+ use current local evidence or find the relevant cloud build.
25
+ 2. Request bounded JSON:
29
26
 
30
27
  ```bash
31
28
  vizzly context build current --source local --agent --json
32
29
  vizzly context build <build-id> --source cloud --agent --json
33
30
  ```
34
31
 
35
- 3. Check the build identity, source, branch, timestamps, baseline selection,
36
- review state, and truncation fields before interpreting the evidence. Treat
37
- omitted fields as unknown.
38
- 4. Inspect the current, baseline, and diff images together using whatever
39
- image, browser, URL, or local-file capability is available. If an image is
40
- inaccessible, say so and do not infer its visual contents from metadata.
41
- 5. Read viewport, browser, screenshot metadata, review state, and Honeydiff
42
- facts alongside the images. Follow the returned `suggested_commands` for
43
- exact comparison or screenshot drill-downs. Request raw diff regions only
44
- when the compact summary is insufficient.
45
- 6. Separate observations from explanations. State what the evidence shows,
46
- then label any proposed cause with appropriate confidence.
47
- 7. Make the smallest justified UI or test change, rerun the owning user
48
- workflow, and inspect the resulting evidence again.
32
+ 3. Confirm the build, source, branch, timestamps, baseline, review state, and
33
+ pagination before drawing conclusions. If `has_more` is true, run the
34
+ returned next-page command before concluding. Missing fields remain unknown.
35
+ 4. Follow `suggested_commands` to inspect a comparison. View its baseline,
36
+ current, and diff images together. If an image cannot be opened, label the
37
+ result metadata-only; do not call it visual verification.
38
+ 5. Read image dimensions, viewport, browser, diff regions, fingerprint, and
39
+ relevant history alongside the images. A prior approval is supporting
40
+ evidence, not permission to approve the current comparison.
41
+ 6. State observations before possible causes. Make the smallest justified
42
+ change, rerun the owning workflow, and inspect the new evidence.
49
43
 
50
44
  ## Guardrails
51
45
 
52
- - Treat an approved baseline as the accepted reference for that comparison,
53
- not infallible truth. Check its identity and selection reason.
54
- - Do not invent progress, counts, review state, visual causes, or missing API
46
+ - Do not invent progress, ranking, review state, visual causes, or missing API
55
47
  values.
56
- - Do not approve, reject, comment on, publish, or replace visual evidence
57
- unless the user explicitly asks for that mutation.
58
- - Preserve existing thresholds, cluster sizes, signature properties, and
59
- dynamic-region behavior unless the task and evidence justify changing them.
60
- - Prefer deterministic fixtures and existing end-to-end journeys over hiding a
61
- diff with broader tolerances or a new screenshot-only test.
62
- - Report the screenshot or comparison identity, the relevant build or link,
63
- the observed evidence, any access limitations, and the command used.
48
+ - Do not approve, reject, comment on, publish, or replace evidence unless the
49
+ task explicitly asks for that mutation.
50
+ - Preserve thresholds, cluster sizes, signature properties, and other capture
51
+ settings unless repeated evidence justifies a change.
52
+ - Prefer deterministic fixtures and existing user journeys over hiding a diff
53
+ with broader tolerances or a new screenshot-only test.
54
+ - Report the comparison or screenshot identity, observed evidence, access
55
+ limitations, and the command or link used.
64
56
 
65
- ## Load References Only When Needed
57
+ ## Load A Reference When Needed
66
58
 
67
- - Read [references/cli-context.md](references/cli-context.md) for local and
68
- cloud inspection commands, TDD lifecycle, and evidence drill-downs.
69
- - Read [references/sdks.md](references/sdks.md) only when adding or changing
70
- screenshot capture code.
71
- - Read [references/dynamic-content.md](references/dynamic-content.md) when a
72
- diff may involve unstable content, hotspots, or confirmed regions.
73
- - Read [references/setup-ci.md](references/setup-ci.md) only when the task asks
74
- to initialize Vizzly, change CI, or troubleshoot configuration.
59
+ - [CLI context](references/cli-context.md): local and cloud evidence, build
60
+ discovery, drill-downs, images, and TDD lifecycle.
61
+ - [SDK capture](references/sdks.md): add or change screenshot capture code.
62
+ - [Dynamic content](references/dynamic-content.md): investigate unstable
63
+ content and screenshot-specific tolerances.
64
+ - [Setup and CI](references/setup-ci.md): initialize Vizzly or change CI.