@vizzly-testing/cli 0.35.3-beta.2 → 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.
@@ -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.2",
3
+ "version": "0.35.3-beta.3",
4
4
  "description": "Visual regression testing from your terminal",
5
5
  "keywords": [
6
6
  "llm-context",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: vizzly
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, dynamic regions, or an existing Vizzly workflow.
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
@@ -47,8 +47,8 @@ test workflow in charge of how the UI is exercised.
47
47
  values.
48
48
  - Do not approve, reject, comment on, publish, or replace evidence unless the
49
49
  task explicitly asks for that mutation.
50
- - Preserve thresholds, cluster sizes, signature properties, and dynamic-region
51
- behavior unless repeated evidence justifies a change.
50
+ - Preserve thresholds, cluster sizes, signature properties, and other capture
51
+ settings unless repeated evidence justifies a change.
52
52
  - Prefer deterministic fixtures and existing user journeys over hiding a diff
53
53
  with broader tolerances or a new screenshot-only test.
54
54
  - Report the comparison or screenshot identity, observed evidence, access
@@ -60,5 +60,5 @@ test workflow in charge of how the UI is exercised.
60
60
  discovery, drill-downs, images, and TDD lifecycle.
61
61
  - [SDK capture](references/sdks.md): add or change screenshot capture code.
62
62
  - [Dynamic content](references/dynamic-content.md): investigate unstable
63
- content, hotspots, or confirmed regions.
63
+ content and screenshot-specific tolerances.
64
64
  - [Setup and CI](references/setup-ci.md): initialize Vizzly or change CI.
@@ -14,8 +14,8 @@ can cause a diff. They are possible explanations, not conclusions.
14
14
  2. Record the visible region, recurrence, render metadata, and whether the same
15
15
  change appears across builds or variants.
16
16
  3. Prefer deterministic fixtures when they can remove irrelevant variation.
17
- 4. Treat hotspots and confirmed regions as server- or user-authored evidence.
18
- This CLI exposes them for inspection, not authoring.
17
+ 4. Use diff regions only to locate changed pixels; they do not approve a
18
+ comparison or explain why it changed.
19
19
  5. Preserve `threshold` and `minClusterSize` unless repeated evidence
20
20
  justifies a change.
21
21
 
@@ -1,112 +0,0 @@
1
- /**
2
- * Hotspot Coverage Calculation
3
- *
4
- * Pure functions for calculating how much of a visual diff falls within
5
- * "hotspot" regions - areas of the UI that frequently change due to dynamic
6
- * content (timestamps, animations, etc.).
7
- *
8
- * Uses 1D Y-coordinate matching (same algorithm as cloud).
9
- */
10
-
11
- /**
12
- * Calculate what percentage of diff falls within hotspot regions
13
- *
14
- * @param {Array} diffClusters - Array of diff clusters from honeydiff
15
- * @param {Object} hotspotAnalysis - Hotspot data with regions array
16
- * @returns {{ coverage: number, linesInHotspots: number, totalLines: number }}
17
- */
18
- export function calculateHotspotCoverage(diffClusters, hotspotAnalysis) {
19
- if (!diffClusters || diffClusters.length === 0) {
20
- return {
21
- coverage: 0,
22
- linesInHotspots: 0,
23
- totalLines: 0
24
- };
25
- }
26
- if (!hotspotAnalysis?.regions || hotspotAnalysis.regions.length === 0) {
27
- return {
28
- coverage: 0,
29
- linesInHotspots: 0,
30
- totalLines: 0
31
- };
32
- }
33
-
34
- // Extract Y-coordinates (diff lines) from clusters
35
- // Each cluster has a boundingBox with y and height
36
- let diffLines = [];
37
- for (let cluster of diffClusters) {
38
- if (cluster.boundingBox) {
39
- let {
40
- y,
41
- height
42
- } = cluster.boundingBox;
43
- // Add all Y lines covered by this cluster
44
- for (let line = y; line < y + height; line++) {
45
- diffLines.push(line);
46
- }
47
- }
48
- }
49
- if (diffLines.length === 0) {
50
- return {
51
- coverage: 0,
52
- linesInHotspots: 0,
53
- totalLines: 0
54
- };
55
- }
56
-
57
- // Remove duplicates and sort
58
- diffLines = [...new Set(diffLines)].sort((a, b) => a - b);
59
-
60
- // Check how many diff lines fall within hotspot regions
61
- let linesInHotspots = 0;
62
- for (let line of diffLines) {
63
- let inHotspot = hotspotAnalysis.regions.some(region => line >= region.y1 && line <= region.y2);
64
- if (inHotspot) {
65
- linesInHotspots++;
66
- }
67
- }
68
- let coverage = linesInHotspots / diffLines.length;
69
- return {
70
- coverage,
71
- linesInHotspots,
72
- totalLines: diffLines.length
73
- };
74
- }
75
-
76
- /**
77
- * Determine if a comparison should be filtered as "passed" based on hotspot coverage
78
- *
79
- * A diff is filtered when:
80
- * 1. Coverage is >= 80% (most diff in hotspot regions)
81
- * 2. Confidence is "high" or confidence score > 0.7
82
- *
83
- * @param {Object} hotspotAnalysis - Hotspot data with confidence info
84
- * @param {{ coverage: number }} coverageResult - Result from calculateHotspotCoverage
85
- * @returns {boolean} True if diff should be filtered as hotspot noise
86
- */
87
- export function shouldFilterAsHotspot(hotspotAnalysis, coverageResult) {
88
- if (!hotspotAnalysis || !coverageResult) {
89
- return false;
90
- }
91
- let {
92
- coverage
93
- } = coverageResult;
94
-
95
- // Need at least 80% of diff in hotspot regions
96
- if (coverage < 0.8) {
97
- return false;
98
- }
99
-
100
- // Need high confidence in the hotspot analysis
101
- let {
102
- confidence,
103
- confidenceScore
104
- } = hotspotAnalysis;
105
- if (confidence === 'high') {
106
- return true;
107
- }
108
- if (confidenceScore !== undefined && confidenceScore > 0.7) {
109
- return true;
110
- }
111
- return false;
112
- }