@vizzly-testing/cli 0.35.2 → 0.35.3-beta.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 (43) hide show
  1. package/README.md +8 -5
  2. package/dist/api/client.js +20 -5
  3. package/dist/api/core.js +17 -3
  4. package/dist/api/endpoints.js +24 -3
  5. package/dist/api/index.js +1 -1
  6. package/dist/cli.js +25 -65
  7. package/dist/commands/baselines.js +0 -7
  8. package/dist/commands/comparisons.js +52 -45
  9. package/dist/commands/config-cmd.js +0 -7
  10. package/dist/commands/context.js +444 -238
  11. package/dist/commands/doctor.js +0 -8
  12. package/dist/commands/init.js +7 -12
  13. package/dist/commands/login.js +0 -13
  14. package/dist/commands/logout.js +0 -12
  15. package/dist/commands/orgs.js +0 -9
  16. package/dist/commands/run.js +84 -29
  17. package/dist/commands/status.js +276 -59
  18. package/dist/commands/tdd.js +19 -3
  19. package/dist/commands/whoami.js +0 -12
  20. package/dist/context/local-workspace-provider.js +293 -37
  21. package/dist/plugin-loader.js +1 -1
  22. package/dist/reporter/reporter-bundle.css +1 -1
  23. package/dist/reporter/reporter-bundle.iife.js +17 -17
  24. package/dist/server/routers/events.js +4 -4
  25. package/dist/test-runner/operations.js +70 -28
  26. package/dist/types/index.d.ts +4 -2
  27. package/dist/uploader/core.js +6 -6
  28. package/dist/uploader/operations.js +4 -1
  29. package/dist/utils/visual-context-normalizers.js +455 -0
  30. package/package.json +13 -13
  31. package/skills/vizzly/SKILL.md +68 -101
  32. package/skills/vizzly/agents/openai.yaml +2 -2
  33. package/skills/vizzly/references/cli-context.md +79 -27
  34. package/skills/vizzly/references/dynamic-content.md +28 -33
  35. package/skills/vizzly/references/sdks.md +26 -39
  36. package/skills/vizzly/references/setup-ci.md +44 -20
  37. package/dist/config/index.js +0 -13
  38. package/dist/project/index.js +0 -13
  39. package/dist/tdd/index.js +0 -19
  40. package/dist/tdd/services/baseline-downloader.js +0 -151
  41. package/dist/tdd/services/hotspot-service.js +0 -58
  42. package/dist/utils/environment.js +0 -109
  43. package/skills/vizzly/references/public-screenshots.md +0 -44
@@ -0,0 +1,455 @@
1
+ /**
2
+ * Read the API's canonical visual-review state with legacy approval fallbacks.
3
+ *
4
+ * @param {Object} record - Comparison or review record from the API.
5
+ * @returns {string|null} The review state when the API supplied one.
6
+ */
7
+ export function getVisualReviewState(record = {}) {
8
+ return record.visual_review?.state || record.review_state || record.approval_status || null;
9
+ }
10
+
11
+ /**
12
+ * Keep visual outcome separate from processing status when both are available.
13
+ *
14
+ * @param {Object} comparison - Comparison record from the API.
15
+ * @returns {string|null} The visual result, or the legacy status fallback.
16
+ */
17
+ export function getComparisonResult(comparison = {}) {
18
+ return comparison.result || comparison.status || null;
19
+ }
20
+
21
+ /**
22
+ * Resolve a screenshot name across current and legacy response shapes.
23
+ *
24
+ * @param {Object} comparison - Comparison record from the API.
25
+ * @returns {string|null} The first API-provided name or identity fallback.
26
+ */
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;
29
+ }
30
+
31
+ /**
32
+ * Resolve CSS viewport dimensions without confusing them with bitmap size.
33
+ *
34
+ * @param {Object} comparison - Comparison record from the API.
35
+ * @returns {{width: number|null, height: number|null}|null} Viewport dimensions.
36
+ */
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;
42
+ return width != null || height != null ? {
43
+ width,
44
+ height
45
+ } : null;
46
+ }
47
+
48
+ /**
49
+ * Resolve the browser from nested current screenshot or legacy metadata fields.
50
+ *
51
+ * @param {Object} comparison - Comparison record from the API.
52
+ * @returns {string|null} The browser name when present.
53
+ */
54
+ 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;
56
+ }
57
+
58
+ /**
59
+ * Resolve bitmap dimensions from nested or flat screenshot fields.
60
+ *
61
+ * @param {Object} record - Preferred screenshot record.
62
+ * @param {Object} fallback - Legacy fields to use when the record is incomplete.
63
+ * @returns {{width: number|null, height: number|null}|null} Bitmap dimensions.
64
+ */
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;
69
+ return width != null || height != null ? {
70
+ width,
71
+ height
72
+ } : null;
73
+ }
74
+
75
+ /**
76
+ * Resolve viewport dimensions from one screenshot-shaped record.
77
+ *
78
+ * @param {Object} record - Preferred screenshot record.
79
+ * @param {Object} fallback - Legacy fields to use when the record is incomplete.
80
+ * @returns {{width: number|null, height: number|null}|null} Viewport dimensions.
81
+ */
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;
86
+ return width != null || height != null ? {
87
+ width,
88
+ height
89
+ } : null;
90
+ }
91
+
92
+ /**
93
+ * Normalize current screenshot identity, render facts, metadata, and asset URL.
94
+ *
95
+ * @param {Object} comparison - Comparison record from the API.
96
+ * @returns {Object} A stable current screenshot projection.
97
+ */
98
+ function getCurrentScreenshot(comparison = {}) {
99
+ let screenshot = comparison.current_screenshot || comparison.screenshot || {};
100
+ 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
120
+ };
121
+ }
122
+
123
+ /**
124
+ * Normalize the baseline screenshot without inventing missing render facts.
125
+ *
126
+ * @param {Object} comparison - Comparison record from the API.
127
+ * @returns {Object} A stable baseline screenshot projection.
128
+ */
129
+ function getBaselineScreenshot(comparison = {}) {
130
+ let current = comparison.current_screenshot || comparison.screenshot || {};
131
+ let baseline = comparison.baseline_screenshot || comparison.baseline || current.baseline || {};
132
+ 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,
136
+ browser: baseline.browser || null,
137
+ viewport: getRecordViewport(baseline),
138
+ 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
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Project compact Honeydiff facts without making raw geometry the default.
147
+ *
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
152
+ * because they can dominate an otherwise bounded handoff.
153
+ *
154
+ * @param {Object} comparison - Comparison record from the API.
155
+ * @param {boolean} includeDiffs - Whether to include raw Honeydiff diagnostics.
156
+ * @returns {Object} Compact diff evidence suitable for context summaries.
157
+ */
158
+ 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;
164
+ 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
173
+ };
174
+
175
+ // Preserve server-owned evidence exactly so agents can verify and download
176
+ // artifacts without the CLI recreating identities, digests, or availability.
177
+ if (artifacts != null) {
178
+ compact.artifacts = artifacts;
179
+ }
180
+ if (includeDiffs) {
181
+ 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 || [];
186
+ }
187
+ return compact;
188
+ }
189
+
190
+ /**
191
+ * Prefer explicit review need and return null when the API supplied no review fact.
192
+ *
193
+ * @param {Object} comparison - Comparison record from the API.
194
+ * @param {string|null} reviewState - Canonical review state for the record.
195
+ * @returns {boolean|null} Whether the comparison needs review, when known.
196
+ */
197
+ function comparisonNeedsReview(comparison = {}, reviewState = null) {
198
+ if (comparison.needs_review != null) {
199
+ return comparison.needs_review === true;
200
+ }
201
+ if (reviewState == null) {
202
+ return null;
203
+ }
204
+ return reviewState === 'pending';
205
+ }
206
+
207
+ /**
208
+ * Normalize one comparison across grouped and legacy flat API response shapes.
209
+ *
210
+ * @param {Object} comparison - Comparison record from the API.
211
+ * @param {Object} options - Normalization options.
212
+ * @param {string|null} [options.fallbackName] - Group name for unnamed variants.
213
+ * @param {boolean} [options.includeDiffs] - Include raw Honeydiff diagnostics.
214
+ * @returns {Object} A stable comparison evidence record.
215
+ */
216
+ export function normalizeComparisonRecord(comparison = {}, options = {}) {
217
+ 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
+ return {
220
+ id: comparison.id || null,
221
+ name,
222
+ result: getComparisonResult(comparison),
223
+ status: comparison.status || null,
224
+ review_state: reviewState,
225
+ visual_review: comparison.visual_review || null,
226
+ approval_status: comparison.approval_status || null,
227
+ build_branch: comparison.build_branch || null,
228
+ needs_review: comparisonNeedsReview(comparison, reviewState),
229
+ is_flaky: comparison.is_flaky == null ? null : comparison.is_flaky === true,
230
+ browser: getComparisonBrowser(comparison),
231
+ viewport: getComparisonViewport(comparison),
232
+ screenshot: getCurrentScreenshot(comparison),
233
+ baseline: getBaselineScreenshot(comparison),
234
+ diff: getComparisonDiff(comparison, options.includeDiffs === true)
235
+ };
236
+ }
237
+
238
+ /**
239
+ * Read the server's total variant count across supported response shapes.
240
+ *
241
+ * @param {Object} group - Screenshot group from the API.
242
+ * @returns {number|null} The explicit total when present.
243
+ */
244
+ function getExplicitVariantCount(group = {}) {
245
+ return group.variant_count ?? group.total_variants ?? group.totalVariants ?? null;
246
+ }
247
+
248
+ /**
249
+ * Prove whether every variant is available before deriving aggregate facts.
250
+ *
251
+ * @param {Object} group - Screenshot group from the API.
252
+ * @param {Object[]} variants - Returned normalized variants.
253
+ * @param {boolean} forcedComplete - Whether the caller owns the complete set.
254
+ * @returns {boolean} Whether aggregate derivation is safe.
255
+ */
256
+ function hasCompleteVariants(group = {}, variants = [], forcedComplete = false) {
257
+ if (forcedComplete || group.variants_complete === true) {
258
+ return true;
259
+ }
260
+ let explicitCount = getExplicitVariantCount(group);
261
+ return explicitCount != null && explicitCount === variants.length;
262
+ }
263
+
264
+ /**
265
+ * Derive only facts backed by a complete variant set and complete source fields.
266
+ *
267
+ * Unknown review, flaky, result, or diff values stay null instead of becoming
268
+ * client-authored false or zero values.
269
+ *
270
+ * @param {Object[]} variants - Complete normalized variants for one screenshot.
271
+ * @param {boolean} complete - Whether every variant is present.
272
+ * @returns {Object} Exact derived aggregate facts, or an empty object.
273
+ */
274
+ function deriveAggregateFacts(variants = [], complete = false) {
275
+ if (!complete) {
276
+ return {};
277
+ }
278
+ let results = variants.map(variant => variant.result);
279
+ let reviewStates = variants.map(variant => variant.review_state);
280
+ let needsReview = variants.map(variant => variant.needs_review);
281
+ let flaky = variants.map(variant => variant.is_flaky);
282
+ let percentages = variants.map(variant => variant.diff.percentage).filter(value => value != null);
283
+ let allResultsKnown = results.every(value => value != null);
284
+ let allReviewStatesKnown = reviewStates.every(value => value != null);
285
+ let allNeedsReviewKnown = needsReview.every(value => value != null);
286
+ let allFlakyKnown = flaky.every(value => value != null);
287
+ return {
288
+ has_changes: results.includes('changed') ? true : allResultsKnown ? false : null,
289
+ has_new: results.includes('new') ? true : allResultsKnown ? false : null,
290
+ all_approved: variants.length > 0 && allReviewStatesKnown ? reviewStates.every(state => state === 'approved') : null,
291
+ needs_review: allNeedsReviewKnown ? needsReview.some(value => value === true) : null,
292
+ needs_review_count: allNeedsReviewKnown ? needsReview.filter(value => value === true).length : null,
293
+ failed_count: allResultsKnown ? results.filter(result => ['failed', 'error'].includes(result)).length : null,
294
+ has_rejected: reviewStates.includes('rejected') ? true : allReviewStatesKnown ? false : null,
295
+ has_flaky: flaky.includes(true) ? true : allFlakyKnown ? false : null,
296
+ max_diff_percentage: percentages.length === variants.length && percentages.length > 0 ? Math.max(...percentages) : null
297
+ };
298
+ }
299
+
300
+ /**
301
+ * Normalize a screenshot group while keeping explicit server aggregates authoritative.
302
+ *
303
+ * @param {Object} group - Grouped visual-review record from the API.
304
+ * @param {Object} options - Normalization options.
305
+ * @param {boolean} [options.variantsComplete] - Marks a caller-owned complete set.
306
+ * @returns {Object} A normalized screenshot group and its variants.
307
+ */
308
+ export function normalizeComparisonGroup(group = {}, options = {}) {
309
+ let rawVariants = group.variants || group.comparisons || [];
310
+ let groupName = group.name || group.test_name || group.testName || null;
311
+ let variants = rawVariants.map(variant => normalizeComparisonRecord(variant, {
312
+ fallbackName: groupName,
313
+ includeDiffs: options.includeDiffs
314
+ }));
315
+ let complete = hasCompleteVariants(group, variants, options.variantsComplete === true);
316
+ let derived = deriveAggregateFacts(variants, complete);
317
+ let aggregate = group.aggregate_status || {};
318
+ let needsReviewCount = aggregate.needs_review_count ?? derived.needs_review_count ?? null;
319
+ return {
320
+ name: groupName || variants[0]?.name || 'unknown screenshot',
321
+ variant_count: getExplicitVariantCount(group) ?? (complete ? variants.length : null),
322
+ variants_complete: complete,
323
+ variants,
324
+ aggregate_status: {
325
+ has_changes: aggregate.has_changes ?? derived.has_changes ?? null,
326
+ has_new: aggregate.has_new ?? derived.has_new ?? null,
327
+ all_approved: aggregate.all_approved ?? derived.all_approved ?? null,
328
+ needs_review: aggregate.needs_review ?? (aggregate.needs_review_count != null ? aggregate.needs_review_count > 0 : derived.needs_review ?? null),
329
+ needs_review_count: needsReviewCount,
330
+ failed_count: aggregate.failed_count ?? derived.failed_count ?? null,
331
+ has_rejected: aggregate.has_rejected ?? derived.has_rejected ?? null,
332
+ has_flaky: aggregate.has_flaky ?? derived.has_flaky ?? null,
333
+ max_diff_percentage: aggregate.max_diff_percentage ?? derived.max_diff_percentage ?? null
334
+ }
335
+ };
336
+ }
337
+
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
+ /**
363
+ * Join compact group variants to the exact comparison records in the same API
364
+ * response.
365
+ *
366
+ * The build context API deliberately keeps group variants small while also
367
+ * returning complete comparison evidence at the top level. Joining by the
368
+ * server-owned comparison ID keeps the useful group ordering and aggregates
369
+ * without dropping screenshot identity, baseline facts, or Honeydiff data.
370
+ *
371
+ * @param {Object[]} groups - Raw comparison groups from build context.
372
+ * @param {Object[]} comparisons - Exact top-level comparison records.
373
+ * @returns {Object[]} Groups whose variants include exact comparison evidence.
374
+ */
375
+ function joinGroupComparisons(groups = [], comparisons = []) {
376
+ let comparisonsById = new Map(comparisons.filter(comparison => comparison?.id).map(comparison => [comparison.id, comparison]));
377
+ return groups.map(group => {
378
+ let variantKey = Array.isArray(group.variants) ? 'variants' : 'comparisons';
379
+ let variants = Array.isArray(group[variantKey]) ? group[variantKey] : [];
380
+ return {
381
+ ...group,
382
+ [variantKey]: variants.map(variant => ({
383
+ ...variant,
384
+ ...(comparisonsById.get(variant.id) || {})
385
+ }))
386
+ };
387
+ });
388
+ }
389
+
390
+ /**
391
+ * Keep failed capture identity, render evidence, and the API error together.
392
+ *
393
+ * A capture can fail before any comparison exists, so it must remain useful
394
+ * evidence without inheriting comparison-only assumptions.
395
+ *
396
+ * @param {Object} screenshot - Failed screenshot record from the API.
397
+ * @param {Object} options - Normalization options passed to the record.
398
+ * @returns {Object} A normalized failed-capture record.
399
+ */
400
+ function normalizeFailedCapture(screenshot = {}, options = {}) {
401
+ let normalized = normalizeComparisonRecord({
402
+ ...screenshot,
403
+ screenshot,
404
+ result: screenshot.result || screenshot.status || 'failed'
405
+ }, options);
406
+ return {
407
+ ...normalized,
408
+ error_message: screenshot.error_message || normalized.screenshot.error_message || null
409
+ };
410
+ }
411
+
412
+ /**
413
+ * Collect failed captures from explicit and screenshot-list response shapes.
414
+ *
415
+ * @param {Object} context - Raw build context response.
416
+ * @param {Object} options - Normalization options passed to each capture.
417
+ * @returns {Object[]} Deduplicated failed captures in API order.
418
+ */
419
+ function getFailedCaptures(context = {}, options = {}) {
420
+ let explicit = Array.isArray(context.failed_captures) ? context.failed_captures : Array.isArray(context.failed_screenshots) ? context.failed_screenshots : [];
421
+ let screenshots = (context.screenshots || []).filter(screenshot => ['failed', 'error'].includes(screenshot.status));
422
+ let captures = [...explicit, ...screenshots];
423
+ let seen = new Set();
424
+ return captures.map(capture => normalizeFailedCapture(capture, options)).filter(capture => {
425
+ let key = capture.id || capture.screenshot.signature || capture.name;
426
+ if (seen.has(key)) {
427
+ return false;
428
+ }
429
+ seen.add(key);
430
+ return true;
431
+ });
432
+ }
433
+
434
+ /**
435
+ * Adapt raw build context for human presentation across current and legacy APIs.
436
+ *
437
+ * Raw JSON output intentionally bypasses this lossy presentation boundary.
438
+ *
439
+ * @param {Object} context - Raw build context response from the provider.
440
+ * @param {Object} options - Normalization options.
441
+ * @param {boolean} [options.includeDiffs] - Include raw Honeydiff diagnostics.
442
+ * @returns {Object} Normalized comparisons, groups, and failed captures.
443
+ */
444
+ export function normalizeBuildContext(context = {}, options = {}) {
445
+ let rawComparisons = context.comparisons || [];
446
+ let comparisons = rawComparisons.map(comparison => normalizeComparisonRecord(comparison, options));
447
+ let joinedGroups = joinGroupComparisons(context.groups || [], rawComparisons);
448
+ let groups = joinedGroups.length > 0 ? joinedGroups.map(group => normalizeComparisonGroup(group, options)) : groupFlatComparisons(rawComparisons, options);
449
+ return {
450
+ ...context,
451
+ comparisons,
452
+ groups,
453
+ failed_captures: getFailedCaptures(context, options)
454
+ };
455
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vizzly-testing/cli",
3
- "version": "0.35.2",
3
+ "version": "0.35.3-beta.1",
4
4
  "description": "Visual regression testing from your terminal",
5
5
  "keywords": [
6
6
  "llm-context",
@@ -60,7 +60,8 @@
60
60
  "LICENSE"
61
61
  ],
62
62
  "scripts": {
63
- "cli": "source .envrc && node bin/vizzly.js",
63
+ "cli": "node scripts/run-development-cli.js production",
64
+ "cli:local": "node scripts/run-development-cli.js local",
64
65
  "start": "node src/index.js",
65
66
  "build": "pnpm run clean && pnpm run compile && pnpm run build:reporter && pnpm run build:reporter-ssr && pnpm run copy-types",
66
67
  "clean": "rimraf dist",
@@ -78,7 +79,6 @@
78
79
  "test:swift:e2e": "pnpm run build && node clients/swift/scripts/run-e2e.js",
79
80
  "test:tui": "node --test --test-reporter=spec tests/tui/*.test.js",
80
81
  "test:tui:docker": "./tests/tui/run-tui-tests.sh",
81
- "smoke:prod:pack": "node scripts/smoke-prod-pack.js",
82
82
  "lint": "biome check src tests clients/storybook/src clients/storybook/tests clients/static-site/src clients/static-site/tests clients/vitest/src clients/vitest/tests clients/ember/src clients/ember/tests clients/ember/bin",
83
83
  "lint:fix": "biome check --write --unsafe src tests clients/storybook/src clients/storybook/tests clients/static-site/src clients/static-site/tests clients/vitest/src clients/vitest/tests clients/ember/src clients/ember/tests clients/ember/bin",
84
84
  "format": "biome format --write src tests clients/storybook/src clients/storybook/tests clients/static-site/src clients/static-site/tests clients/vitest/src clients/vitest/tests clients/ember/src clients/ember/tests clients/ember/bin",
@@ -93,10 +93,10 @@
93
93
  "registry": "https://registry.npmjs.org/"
94
94
  },
95
95
  "dependencies": {
96
- "@vizzly-testing/bear-den": "0.1.2",
97
- "@vizzly-testing/honeydiff": "^0.11.1",
96
+ "@vizzly-testing/bear-den": "0.1.4",
97
+ "@vizzly-testing/honeydiff": "^0.15.0",
98
98
  "ansis": "^4.2.0",
99
- "commander": "^14.0.0",
99
+ "commander": "^15.0.0",
100
100
  "cosmiconfig": "^9.0.0",
101
101
  "dotenv": "^17.2.1",
102
102
  "form-data": "^4.0.0",
@@ -116,18 +116,18 @@
116
116
  }
117
117
  },
118
118
  "devDependencies": {
119
- "@babel/cli": "^7.28.0",
120
- "@babel/core": "^7.23.6",
121
- "@babel/preset-env": "^7.23.6",
122
- "@babel/preset-react": "^7.27.1",
123
- "@babel/preset-typescript": "^7.23.6",
119
+ "@babel/cli": "^8.0.1",
120
+ "@babel/core": "^8.0.1",
121
+ "@babel/preset-env": "^8.0.2",
122
+ "@babel/preset-react": "^8.0.1",
123
+ "@babel/preset-typescript": "^8.0.1",
124
124
  "@biomejs/biome": "^2.3.8",
125
125
  "@heroicons/react": "^2.2.0",
126
126
  "@modelcontextprotocol/sdk": "^1.0.4",
127
127
  "@playwright/test": "^1.55.1",
128
128
  "@tailwindcss/postcss": "^4.1.13",
129
129
  "@tanstack/react-query": "^5.90.11",
130
- "@types/node": "^25.0.2",
130
+ "@types/node": "^26.0.0",
131
131
  "@vitejs/plugin-react": "^6.0.1",
132
132
  "autoprefixer": "^10.4.21",
133
133
  "babel-plugin-react-compiler": "^1.0.0",
@@ -137,7 +137,7 @@
137
137
  "tailwindcss": "^4.1.13",
138
138
  "tsd": "^0.33.0",
139
139
  "tui-driver": "github:vizzly-testing/tui-driver",
140
- "typescript": "^6.0.3",
140
+ "typescript": "^7.0.2",
141
141
  "vite": "^8.0.0",
142
142
  "wouter": "^3.7.1"
143
143
  }