@vizzly-testing/cli 0.35.3-beta.2 → 0.35.3-beta.4

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 (36) hide show
  1. package/README.md +3 -4
  2. package/dist/api/core.js +12 -5
  3. package/dist/api/endpoints.js +5 -50
  4. package/dist/api/index.js +1 -1
  5. package/dist/cli.js +4 -4
  6. package/dist/client/index.js +2 -1
  7. package/dist/commands/api.js +3 -3
  8. package/dist/commands/builds.js +12 -15
  9. package/dist/commands/comparisons.js +7 -9
  10. package/dist/commands/context.js +3 -24
  11. package/dist/commands/review.js +122 -35
  12. package/dist/commands/run.js +1 -3
  13. package/dist/commands/status.js +35 -79
  14. package/dist/commands/upload.js +1 -1
  15. package/dist/context/local-workspace-provider.js +36 -126
  16. package/dist/reporter/reporter-bundle.iife.js +49 -81
  17. package/dist/screenshot-server/core.js +5 -1
  18. package/dist/sdk/index.js +2 -1
  19. package/dist/server/handlers/api-handler.js +18 -2
  20. package/dist/server/handlers/tdd-handler.js +12 -11
  21. package/dist/server/routers/dashboard.js +5 -1
  22. package/dist/server/routers/screenshot.js +25 -2
  23. package/dist/tdd/runtime-context.js +0 -2
  24. package/dist/tdd/services/comparison-service.js +4 -68
  25. package/dist/tdd/tdd-service.js +10 -208
  26. package/dist/types/index.d.ts +6 -2
  27. package/dist/uploader/core.js +7 -8
  28. package/dist/utils/screenshot-options.js +61 -22
  29. package/dist/utils/visual-context-normalizers.js +86 -139
  30. package/package.json +1 -1
  31. package/skills/vizzly/SKILL.md +4 -4
  32. package/skills/vizzly/references/dynamic-content.md +2 -2
  33. package/dist/tdd/core/hotspot-coverage.js +0 -112
  34. package/dist/tdd/core/region-coverage.js +0 -203
  35. package/dist/tdd/metadata/hotspot-metadata.js +0 -93
  36. package/dist/tdd/metadata/region-metadata.js +0 -56
@@ -49,67 +49,38 @@ async function fetchOptionalPreviewInfo(getPreviewInfo, client, buildId) {
49
49
  }
50
50
  }
51
51
  export function normalizeBuildStatus(buildStatus) {
52
- return buildStatus.build || buildStatus;
52
+ return buildStatus.build;
53
53
  }
54
54
 
55
55
  /**
56
- * Read exact processing counts from canonical or legacy status responses.
56
+ * Read the processing counts from the status response.
57
57
  *
58
- * Legacy `pending_screenshots` is review state, not queue state, so it is
59
- * deliberately excluded. Missing processing fields remain missing instead of
60
- * becoming client-authored zeroes.
61
- *
62
- * @param {Object} status - Canonical status bundle or legacy build record.
63
- * @returns {Object|undefined} API-provided processing facts when available.
58
+ * @param {Object} status - Status response.
59
+ * @returns {Object} API-provided processing facts.
64
60
  */
65
61
  export function getProcessingStatus(status = {}) {
66
- if (status.processing) {
67
- return status.processing;
68
- }
69
- let build = normalizeBuildStatus(status);
70
- let processing = {};
71
- let fields = [['total', build.screenshot_count], ['completed', build.completed_jobs], ['failed', build.failed_jobs], ['active', build.processing_screenshots]];
72
- for (let [name, value] of fields) {
73
- if (value != null) {
74
- processing[name] = value;
75
- }
76
- }
77
- return Object.keys(processing).length > 0 ? processing : undefined;
62
+ return status.processing;
78
63
  }
79
64
 
80
65
  /**
81
66
  * Read comparison totals without deriving them from screenshot counts.
82
67
  *
83
- * @param {Object} status - Canonical status bundle or legacy build record.
84
- * @returns {Object|undefined} API-provided comparison facts when available.
68
+ * @param {Object} status - Status response.
69
+ * @returns {Object} API-provided comparison facts.
85
70
  */
86
71
  export function getComparisonStatus(status = {}) {
87
- if (status.comparisons) {
88
- return status.comparisons;
89
- }
90
- let build = normalizeBuildStatus(status);
91
- let comparisons = {};
92
- let fields = [['total', build.total_comparisons], ['new', build.new_comparisons], ['changed', build.changed_comparisons], ['identical', build.identical_comparisons]];
93
- for (let [name, value] of fields) {
94
- if (value != null) {
95
- comparisons[name] = value;
96
- }
97
- }
98
- return Object.keys(comparisons).length > 0 ? comparisons : undefined;
72
+ return status.comparisons;
99
73
  }
100
74
 
101
75
  /**
102
- * Prefer canonical Cricket review state while retaining the legacy fallback.
103
- *
104
- * Processing status stays separate: a pending review never means a screenshot
105
- * is still being processed.
76
+ * Read the build review state without mixing it with processing status.
106
77
  *
107
- * @param {Object} status - Canonical status bundle or legacy build record.
78
+ * @param {Object} status - Status response.
108
79
  * @returns {string|null} API-provided review state when available.
109
80
  */
110
81
  export function getBuildReviewState(status = {}) {
111
82
  let build = normalizeBuildStatus(status);
112
- return getVisualReviewState(build) || getVisualReviewState(status.visualReview?.build || status.visual_review) || null;
83
+ return getVisualReviewState(build);
113
84
  }
114
85
 
115
86
  /**
@@ -132,13 +103,10 @@ export function createStatusSuggestedCommands(build = {}) {
132
103
  }
133
104
 
134
105
  /**
135
- * Preserve the established JSON fields while exposing canonical status facts.
136
- *
137
106
  * Every lifecycle, processing, comparison, and review value comes directly
138
- * from the API. Undefined values intentionally disappear during JSON encoding
139
- * rather than being presented as false certainty.
107
+ * from the API.
140
108
  *
141
- * @param {Object} status - Canonical status bundle or legacy build record.
109
+ * @param {Object} status - Status response.
142
110
  * @param {Object|null} previewInfo - Optional preview response.
143
111
  * @returns {Object} Machine-readable status payload.
144
112
  */
@@ -146,7 +114,6 @@ export function createStatusData(status, previewInfo = null) {
146
114
  let build = normalizeBuildStatus(status);
147
115
  let processing = getProcessingStatus(status);
148
116
  let comparisons = getComparisonStatus(status);
149
- let visualReview = build.visual_review || status.visualReview?.build || status.visual_review;
150
117
  return {
151
118
  resource: status.resource,
152
119
  schemaVersion: status.schema_version,
@@ -161,18 +128,14 @@ export function createStatusData(status, previewInfo = null) {
161
128
  branch: build.branch,
162
129
  commit: build.commit_sha,
163
130
  commitMessage: build.commit_message,
164
- screenshotsTotal: processing?.total ?? build.screenshot_count,
131
+ screenshotsTotal: processing.total,
165
132
  processing,
166
- comparisonsTotal: comparisons?.total ?? build.total_comparisons,
133
+ comparisonsTotal: comparisons.total,
167
134
  comparisons,
168
- newComparisons: comparisons?.new ?? build.new_comparisons,
169
- changedComparisons: comparisons?.changed ?? build.changed_comparisons,
170
- identicalComparisons: comparisons?.identical ?? build.identical_comparisons,
171
- reviewState: getBuildReviewState(status),
172
- review: status.review,
173
- reviewFlow: status.reviewFlow,
174
- visualReview,
175
- approvalStatus: build.approval_status,
135
+ newComparisons: comparisons.new,
136
+ changedComparisons: comparisons.changed,
137
+ identicalComparisons: comparisons.identical,
138
+ visual_review: build.visual_review || null,
176
139
  executionTime: build.execution_time_ms,
177
140
  isBaseline: build.is_baseline,
178
141
  userAgent: build.user_agent,
@@ -203,19 +166,18 @@ export function createBuildInfo(build) {
203
166
  }
204
167
 
205
168
  /**
206
- * Format API comparison outcomes without filling missing buckets with zeroes.
169
+ * Format API comparison outcomes.
207
170
  *
208
- * @param {Object} status - Canonical status bundle or legacy build record.
171
+ * @param {Object} status - Status response.
209
172
  * @param {Object} colors - Output color helpers.
210
173
  * @returns {string} Human-readable comparison summary.
211
174
  */
212
175
  export function createComparisonStats(status, colors) {
213
- let build = normalizeBuildStatus(status);
214
- let comparisons = getComparisonStatus(status) || {};
176
+ let comparisons = getComparisonStatus(status);
215
177
  let stats = [];
216
- let newCount = comparisons.new ?? build.new_comparisons;
217
- let changedCount = comparisons.changed ?? build.changed_comparisons;
218
- let identicalCount = comparisons.identical ?? build.identical_comparisons;
178
+ let newCount = comparisons.new;
179
+ let changedCount = comparisons.changed;
180
+ let identicalCount = comparisons.identical;
219
181
  if (newCount > 0) {
220
182
  stats.push(`${colors.brand.info(newCount)} new`);
221
183
  }
@@ -229,45 +191,39 @@ export function createComparisonStats(status, colors) {
229
191
  }
230
192
 
231
193
  /**
232
- * Build the best available legacy link when the API did not return one.
233
- *
234
- * Slug routes match the current app, while the project-ID route remains as a
235
- * compatibility fallback for older build responses.
194
+ * Build the scoped app link when the API did not return one.
236
195
  *
237
196
  * @param {string} baseUrl - API or app base URL.
238
197
  * @param {Object} build - Build record.
239
- * @param {Object|null} scope - Canonical organization and project scope.
198
+ * @param {Object|null} scope - Organization and project details.
240
199
  * @returns {string|null} Build URL when the response has enough identity.
241
200
  */
242
201
  export function createBuildUrl(baseUrl, build, scope = null) {
243
202
  if (!baseUrl) {
244
203
  return null;
245
204
  }
246
- let organizationSlug = scope?.organization?.slug || build.organization_slug || build.org_slug;
247
- let projectSlug = scope?.project?.slug || build.project_slug;
205
+ let organizationSlug = scope?.organization?.slug;
206
+ let projectSlug = scope?.project?.slug;
248
207
  let appBaseUrl = getAppBaseUrl(baseUrl);
249
208
  if (organizationSlug && projectSlug) {
250
209
  return `${appBaseUrl}/${organizationSlug}/${projectSlug}/builds/${build.id}`;
251
210
  }
252
- if (build.project_id) {
253
- return `${appBaseUrl}/projects/${build.project_id}/builds/${build.id}`;
254
- }
255
211
  return null;
256
212
  }
257
213
 
258
214
  /**
259
- * Preserve status failure behavior while accepting the canonical conclusion.
215
+ * Preserve status failure behavior while using the server's conclusion.
260
216
  *
261
217
  * Review-required and rejected builds keep their existing successful command
262
218
  * exit behavior. Only build or processing failures produce a failing status.
263
219
  *
264
- * @param {Object} status - Canonical status bundle or legacy build record.
220
+ * @param {Object} status - Status response.
265
221
  * @returns {boolean} Whether human status should exit non-zero.
266
222
  */
267
223
  export function shouldFailStatus(status) {
268
224
  let build = normalizeBuildStatus(status);
269
225
  let processing = getProcessingStatus(status);
270
- return build.status === 'failed' || ['build_failed', 'processing_failed'].includes(status.conclusion) || (processing?.failed ?? 0) > 0;
226
+ return build.status === 'failed' || ['build_failed', 'processing_failed'].includes(status.conclusion) || processing.failed > 0;
271
227
  }
272
228
  function writeStatusTiming({
273
229
  build,
@@ -314,7 +270,7 @@ function writeSuggestedCommands({
314
270
  /**
315
271
  * Add optional API facts without inventing placeholders for missing metadata.
316
272
  *
317
- * @param {Object} status - Canonical status bundle or legacy build record.
273
+ * @param {Object} status - Status response.
318
274
  * @returns {Object} Available verbose fields.
319
275
  */
320
276
  function createVerboseInfo(status) {
@@ -348,7 +304,7 @@ function createVerboseInfo(status) {
348
304
  /**
349
305
  * Format only the processing facts supplied by the API.
350
306
  *
351
- * @param {Object|undefined} processing - Canonical processing counts.
307
+ * @param {Object|undefined} processing - Processing counts.
352
308
  * @returns {string} Human-readable processing summary.
353
309
  */
354
310
  function formatProcessingStatus(processing) {
@@ -387,7 +343,7 @@ function writeHumanStatus({
387
343
  let colors = output.getColors();
388
344
  let comparisonStats = createComparisonStats(status, colors);
389
345
  let processing = getProcessingStatus(status);
390
- let screenshotsTotal = processing?.total ?? build.screenshot_count;
346
+ let screenshotsTotal = processing.total;
391
347
  let processingSummary = formatProcessingStatus(processing);
392
348
  if (screenshotsTotal != null) {
393
349
  output.labelValue('Screenshots', String(screenshotsTotal));
@@ -262,7 +262,7 @@ export async function uploadCommand(screenshotsPath, options = {}, globalOptions
262
262
  failed: buildResult.failedComparisons || 0,
263
263
  new: buildResult.newComparisons || 0
264
264
  },
265
- approvalStatus: buildResult.approvalStatus || 'pending',
265
+ visual_review: buildResult.visual_review,
266
266
  executionTimeMs
267
267
  });
268
268
  output.cleanup();
@@ -43,7 +43,7 @@ function mapComparisonResult(status) {
43
43
  }
44
44
  return status || 'unknown';
45
45
  }
46
- function mapApprovalStatus(status) {
46
+ function mapReviewState(status) {
47
47
  if (status === 'failed' || status === 'new') {
48
48
  return 'pending';
49
49
  }
@@ -88,50 +88,6 @@ function resolveAssetReference(assetPath, snapshot) {
88
88
  }
89
89
  return assetPath;
90
90
  }
91
- function normalizeConfirmedRegions(regions = []) {
92
- return regions.map((region, index) => ({
93
- id: region.id || `local-region-${index}`,
94
- x1: region.x1 ?? region.x ?? null,
95
- y1: region.y1 ?? region.y ?? null,
96
- x2: region.x2 ?? (region.x != null && region.width != null ? region.x + region.width : null),
97
- y2: region.y2 ?? (region.y != null && region.height != null ? region.y + region.height : null),
98
- label: region.label || null
99
- }));
100
- }
101
- function mergeConfirmedRegions(snapshot, comparisonName, details = {}) {
102
- let workspaceRegions = snapshot.regions?.[comparisonName]?.confirmed || [];
103
- let detailRegions = details.confirmedRegions || [];
104
- let merged = [...workspaceRegions, ...detailRegions];
105
- let seen = new Set();
106
- return normalizeConfirmedRegions(merged).filter(region => {
107
- let key = `${region.label || ''}:${region.x1}:${region.y1}:${region.x2}:${region.y2}`;
108
- if (seen.has(key)) {
109
- return false;
110
- }
111
- seen.add(key);
112
- return true;
113
- });
114
- }
115
- function buildHotspotAnalysis(snapshot, comparisonName, details = {}) {
116
- let hotspotMetadata = snapshot.hotspots?.[comparisonName] || null;
117
- let hotspotAnalysis = details.hotspotAnalysis || null;
118
- if (!hotspotMetadata && !hotspotAnalysis) {
119
- return {
120
- regions: [],
121
- total_builds_analyzed: 0,
122
- confidence: 'no_data',
123
- confidence_score: null,
124
- data_source: 'local_workspace'
125
- };
126
- }
127
- return {
128
- regions: hotspotMetadata?.regions || [],
129
- total_builds_analyzed: 1,
130
- confidence: hotspotAnalysis?.confidence || hotspotMetadata?.confidence || 'workspace',
131
- confidence_score: hotspotAnalysis?.confidenceScore || null,
132
- data_source: 'local_workspace'
133
- };
134
- }
135
91
  function buildComparisonLinks(snapshot, comparisonId) {
136
92
  if (!snapshot.serverInfo?.port) {
137
93
  return {};
@@ -160,7 +116,7 @@ function buildBuildSnapshot(snapshot) {
160
116
  branch: 'local',
161
117
  commit_sha: null,
162
118
  commit_message: null,
163
- approval_status: snapshot.serverInfo ? 'pending' : 'approved',
119
+ review_state: snapshot.serverInfo ? 'pending' : 'approved',
164
120
  status: snapshot.serverInfo ? 'running' : 'completed',
165
121
  created_at: null
166
122
  };
@@ -181,7 +137,7 @@ function buildBaselineSnapshot(snapshot) {
181
137
  branch: metadata.branch || 'local',
182
138
  commit_sha: metadata.buildInfo?.commitSha || null,
183
139
  commit_message: metadata.buildInfo?.commitMessage || null,
184
- approval_status: metadata.buildInfo?.approvalStatus || 'approved',
140
+ review_state: metadata.buildInfo?.visual_review?.state || 'approved',
185
141
  status: metadata.buildInfo?.completedAt ? 'completed' : 'local',
186
142
  created_at: metadata.createdAt || null,
187
143
  completed_at: metadata.buildInfo?.completedAt || null
@@ -192,7 +148,7 @@ function buildBaselineSnapshot(snapshot) {
192
148
  }
193
149
  function buildReviewState(build, reviewSummary) {
194
150
  let reasons = [];
195
- if (build.approval_status === 'pending') {
151
+ if (build.review_state === 'pending') {
196
152
  reasons.push('build_pending_approval');
197
153
  }
198
154
  if (reviewSummary.pending > 0) {
@@ -208,12 +164,10 @@ function buildReviewState(build, reviewSummary) {
208
164
  function mapLocalComparison(snapshot, comparison) {
209
165
  let details = snapshot.comparisonDetails[comparison.id] || {};
210
166
  let comparisonName = comparison.originalName || comparison.name;
211
- let confirmedRegions = mergeConfirmedRegions(snapshot, comparisonName, details);
212
- let hotspotAnalysis = buildHotspotAnalysis(snapshot, comparisonName, details);
213
167
  let properties = comparison.properties || {};
214
168
  let buildSnapshot = buildBuildSnapshot(snapshot);
215
169
  let result = mapComparisonResult(comparison.status);
216
- let approvalStatus = mapApprovalStatus(comparison.status);
170
+ let reviewState = mapReviewState(comparison.status);
217
171
  let baselineBuildId = snapshot.baselineMetadata?.buildId || null;
218
172
  let diffImageUrl = resolveAssetReference(comparison.diff, snapshot);
219
173
  let diffRegions = details.diffClusters || [];
@@ -223,8 +177,8 @@ function mapLocalComparison(snapshot, comparison) {
223
177
  screenshot_name: comparisonName,
224
178
  status: comparison.status,
225
179
  result,
226
- approval_status: approvalStatus,
227
- needs_review: approvalStatus === 'pending' && ['changed', 'new'].includes(result),
180
+ review_state: reviewState,
181
+ needs_review: reviewState === 'pending' && ['changed', 'new'].includes(result),
228
182
  build_id: buildSnapshot.id,
229
183
  build_name: buildSnapshot.name,
230
184
  build_branch: buildSnapshot.branch,
@@ -275,14 +229,11 @@ function mapLocalComparison(snapshot, comparison) {
275
229
  } : null,
276
230
  diff_lines: null,
277
231
  fingerprint_hash: null,
278
- fingerprint_data: null,
279
- hotspot_analysis: hotspotAnalysis,
280
- region_analysis: details.regionAnalysis || null,
281
- confirmed_regions: confirmedRegions
232
+ fingerprint_data: null
282
233
  }
283
234
  };
284
235
  }
285
- function projectLocalScreenshot(screenshot, baseline) {
236
+ function formatLocalScreenshot(screenshot, baseline) {
286
237
  if (!screenshot) return null;
287
238
  return {
288
239
  id: screenshot.id,
@@ -306,8 +257,8 @@ function projectLocalScreenshot(screenshot, baseline) {
306
257
  } : null
307
258
  };
308
259
  }
309
- function projectLocalDiff(diff, includeDiffs) {
310
- let projected = {
260
+ function formatLocalDiff(diff, includeDiffs) {
261
+ let formatted = {
311
262
  percentage: diff?.percentage ?? null,
312
263
  changed_pixels: diff?.changed_pixels ?? null,
313
264
  total_pixels: diff?.total_pixels ?? null,
@@ -317,32 +268,32 @@ function projectLocalDiff(diff, includeDiffs) {
317
268
  region_count: Array.isArray(diff?.regions) ? diff.regions.length : null
318
269
  };
319
270
  if (includeDiffs) {
320
- projected.regions = diff?.regions || [];
321
- projected.cluster_metadata = diff?.cluster_metadata ?? null;
322
- projected.fingerprint_data = diff?.fingerprint_data ?? null;
323
- projected.diff_lines = diff?.diff_lines ?? [];
271
+ formatted.regions = diff?.regions || [];
272
+ formatted.cluster_metadata = diff?.cluster_metadata ?? null;
273
+ formatted.fingerprint_data = diff?.fingerprint_data ?? null;
274
+ formatted.diff_lines = diff?.diff_lines ?? [];
324
275
  }
325
- return projected;
276
+ return formatted;
326
277
  }
327
- function projectLocalEvidence(comparison, includeDiffs) {
278
+ function formatLocalEvidence(comparison, includeDiffs) {
328
279
  return {
329
280
  type: 'comparison',
330
281
  id: comparison.id,
331
282
  screenshot_name: comparison.screenshot_name,
332
283
  status: comparison.status,
333
284
  result: comparison.result,
334
- approval_status: comparison.approval_status,
285
+ review_state: comparison.review_state,
335
286
  needs_review: comparison.needs_review,
336
287
  build_id: comparison.build_id,
337
288
  build_name: comparison.build_name,
338
289
  build_branch: comparison.build_branch,
339
290
  build_commit_sha: comparison.build_commit_sha,
340
291
  build_created_at: comparison.build_created_at,
341
- screenshot: projectLocalScreenshot(comparison.screenshot, comparison.baseline),
342
- diff: projectLocalDiff(comparison.diff, includeDiffs)
292
+ screenshot: formatLocalScreenshot(comparison.screenshot, comparison.baseline),
293
+ diff: formatLocalDiff(comparison.diff, includeDiffs)
343
294
  };
344
295
  }
345
- function projectLocalFocusedComparison(comparison, includeDiffs) {
296
+ function formatLocalFocusedComparison(comparison, includeDiffs) {
346
297
  if (includeDiffs) return comparison;
347
298
  let analysis = comparison.analysis || {};
348
299
  let {
@@ -354,11 +305,11 @@ function projectLocalFocusedComparison(comparison, includeDiffs) {
354
305
  } = analysis;
355
306
  return {
356
307
  ...comparison,
357
- diff: projectLocalDiff(comparison.diff, false),
308
+ diff: formatLocalDiff(comparison.diff, false),
358
309
  analysis: summaryAnalysis
359
310
  };
360
311
  }
361
- function projectLocalHistoryItem(comparison) {
312
+ function formatLocalHistoryItem(comparison) {
362
313
  return {
363
314
  id: comparison.id,
364
315
  screenshot_name: comparison.screenshot_name,
@@ -369,7 +320,7 @@ function projectLocalHistoryItem(comparison) {
369
320
  build_name: comparison.build_name,
370
321
  build_branch: comparison.build_branch,
371
322
  build_created_at: comparison.build_created_at,
372
- screenshot: projectLocalScreenshot(comparison.screenshot, comparison.baseline),
323
+ screenshot: formatLocalScreenshot(comparison.screenshot, comparison.baseline),
373
324
  diff: {
374
325
  percentage: comparison.diff?.percentage ?? null,
375
326
  fingerprint_hash: comparison.diff?.fingerprint_hash ?? null,
@@ -384,43 +335,19 @@ function compactLocalCollection(items) {
384
335
  details_available: Array.isArray(items) && items.length > 0
385
336
  };
386
337
  }
387
- function createLocalDynamicRegions(comparison = null) {
388
- let confirmedRegions = comparison?.analysis?.confirmed_regions;
389
- let hotspotAnalysis = comparison?.analysis?.hotspot_analysis;
390
- return {
391
- decision: null,
392
- patterns: compactLocalCollection([]),
393
- confirmed_regions: compactLocalCollection(confirmedRegions),
394
- exclusions: {
395
- total: null,
396
- included: false,
397
- details_available: false
398
- },
399
- hotspot_analysis: hotspotAnalysis ? {
400
- total_builds_analyzed: hotspotAnalysis.total_builds_analyzed ?? null,
401
- confidence: hotspotAnalysis.confidence ?? null,
402
- confidence_score: hotspotAnalysis.confidence_score ?? null,
403
- data_source: hotspotAnalysis.data_source ?? null,
404
- coverage: hotspotAnalysis.coverage ?? null,
405
- confirmed_region_coverage: hotspotAnalysis.confirmed_region_coverage ?? null
406
- } : null
407
- };
408
- }
409
338
  function createLocalSnapshotRevision(snapshot) {
410
339
  let revisionInput = {
411
340
  serverInfo: snapshot.serverInfo,
412
341
  reportData: snapshot.reportData,
413
342
  comparisonDetails: snapshot.comparisonDetails,
414
- baselineMetadata: snapshot.baselineMetadata,
415
- hotspotFile: snapshot.hotspotFile,
416
- regionFile: snapshot.regionFile
343
+ baselineMetadata: snapshot.baselineMetadata
417
344
  };
418
345
  return createHash('sha256').update(JSON.stringify(revisionInput)).digest('base64url');
419
346
  }
420
347
  function buildReviewSummary(comparisons = []) {
421
- let approved = comparisons.filter(comparison => mapApprovalStatus(comparison.status) === 'approved').length;
422
- let rejected = comparisons.filter(comparison => mapApprovalStatus(comparison.status) === 'rejected').length;
423
- let pending = comparisons.filter(comparison => mapApprovalStatus(comparison.status) === 'pending').length;
348
+ let approved = comparisons.filter(comparison => mapReviewState(comparison.status) === 'approved').length;
349
+ let rejected = comparisons.filter(comparison => mapReviewState(comparison.status) === 'rejected').length;
350
+ let pending = comparisons.filter(comparison => mapReviewState(comparison.status) === 'pending').length;
424
351
  return {
425
352
  total: comparisons.length,
426
353
  pending,
@@ -457,12 +384,8 @@ export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
457
384
  serverInfo: readJson(join(vizzlyDir, 'server.json')),
458
385
  reportData: normalizeReportData(readJson(join(vizzlyDir, 'report-data.json')) || createEmptyReportData()),
459
386
  comparisonDetails: readJson(join(vizzlyDir, 'comparison-details.json')) || {},
460
- baselineMetadata: readJson(join(vizzlyDir, 'baselines', 'metadata.json')),
461
- hotspotFile: readJson(join(vizzlyDir, 'hotspots.json')),
462
- regionFile: readJson(join(vizzlyDir, 'regions.json'))
387
+ baselineMetadata: readJson(join(vizzlyDir, 'baselines', 'metadata.json'))
463
388
  };
464
- snapshotCache.hotspots = snapshotCache.hotspotFile?.hotspots || null;
465
- snapshotCache.regions = snapshotCache.regionFile?.regions || null;
466
389
  return snapshotCache;
467
390
  }
468
391
  function isAvailable(snapshot = loadSnapshot()) {
@@ -486,7 +409,7 @@ export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
486
409
  return Boolean(findComparison(snapshot, target));
487
410
  }
488
411
  if (command === 'screenshot') {
489
- return Boolean(snapshot.reportData.comparisons.some(comparison => (comparison.originalName || comparison.name) === target) || snapshot.regions?.[target] || snapshot.hotspots?.[target]);
412
+ return Boolean(snapshot.reportData.comparisons.some(comparison => (comparison.originalName || comparison.name) === target));
490
413
  }
491
414
  if (command === 'review-queue') {
492
415
  return snapshot.reportData.comparisons.length > 0;
@@ -572,7 +495,7 @@ export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
572
495
  throw createLocalWorkspaceError(`Local workspace context is only available for the active local build (${resolvedBuild.id})`);
573
496
  }
574
497
  let mappedComparisons = snapshot.reportData.comparisons.map(comparison => mapLocalComparison(snapshot, comparison));
575
- let mappedScreenshots = mappedComparisons.map(comparison => projectLocalScreenshot(comparison.screenshot, comparison.baseline));
498
+ let mappedScreenshots = mappedComparisons.map(comparison => formatLocalScreenshot(comparison.screenshot, comparison.baseline));
576
499
  let reviewSummary = buildReviewSummary(snapshot.reportData.comparisons);
577
500
  let reviewState = buildReviewState(resolvedBuild, reviewSummary);
578
501
  let context = {
@@ -612,7 +535,7 @@ export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
612
535
  let cursorQuery = `${query.details}:${limit}`;
613
536
  let cursor = readLocalCursor(query.cursor, 'build_context', resolvedBuild.id, cursorQuery, revision, ['evidence']);
614
537
  let includeDiffs = query.details === 'diffs';
615
- let evidence = mappedComparisons.map(comparison => projectLocalEvidence(comparison, includeDiffs));
538
+ let evidence = mappedComparisons.map(comparison => formatLocalEvidence(comparison, includeDiffs));
616
539
  return {
617
540
  resource: 'build_context',
618
541
  source: LOCAL_CONTEXT_SOURCE,
@@ -623,14 +546,6 @@ export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
623
546
  summary: context.summary,
624
547
  preview: null,
625
548
  signature_properties: [],
626
- dynamic_regions: {
627
- exclusions: {
628
- total: null,
629
- included: false,
630
- details_available: false
631
- },
632
- item_details_included: false
633
- },
634
549
  evidence: createLocalPage(evidence, {
635
550
  limit,
636
551
  offset: cursor?.offset || 0,
@@ -664,9 +579,7 @@ export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
664
579
  comparison: mappedComparison,
665
580
  history: {
666
581
  similar_by_fingerprint: [],
667
- recent_by_name: history,
668
- hotspot_analysis: buildHotspotAnalysis(snapshot, comparisonName, snapshot.comparisonDetails[comparison.id] || {}),
669
- confirmed_regions: mergeConfirmedRegions(snapshot, comparisonName, snapshot.comparisonDetails[comparison.id] || {})
582
+ recent_by_name: history
670
583
  },
671
584
  review: {
672
585
  review_summary: {
@@ -699,8 +612,7 @@ export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
699
612
  scope: context.scope,
700
613
  build: context.build,
701
614
  signature_properties: [],
702
- comparison: projectLocalFocusedComparison(context.comparison, query.details === 'diffs'),
703
- dynamic_regions: createLocalDynamicRegions(context.comparison),
615
+ comparison: formatLocalFocusedComparison(context.comparison, query.details === 'diffs'),
704
616
  history: {
705
617
  active_stream: activeStream || null,
706
618
  similar_by_fingerprint: createLocalPage([], {
@@ -712,7 +624,7 @@ export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
712
624
  stream: 'similar_by_fingerprint',
713
625
  revision
714
626
  }),
715
- recent_by_name: createLocalPage(history.map(projectLocalHistoryItem), {
627
+ recent_by_name: createLocalPage(history.map(formatLocalHistoryItem), {
716
628
  limit,
717
629
  offset: recentOffset,
718
630
  resource: 'comparison_context',
@@ -735,7 +647,7 @@ export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
735
647
  function getScreenshotContext(screenshotName) {
736
648
  let snapshot = loadSnapshot();
737
649
  let matches = snapshot.reportData.comparisons.filter(comparison => (comparison.originalName || comparison.name) === screenshotName).map(comparison => mapLocalComparison(snapshot, comparison));
738
- if (matches.length === 0 && !snapshot.regions?.[screenshotName] && !snapshot.hotspots?.[screenshotName]) {
650
+ if (matches.length === 0) {
739
651
  throw createLocalWorkspaceError(`No local screenshot context found for "${screenshotName}"`);
740
652
  }
741
653
  return {
@@ -745,8 +657,6 @@ export function createLocalWorkspaceContextProvider(options = {}, deps = {}) {
745
657
  screenshot: {
746
658
  name: screenshotName
747
659
  },
748
- hotspot_analysis: buildHotspotAnalysis(snapshot, screenshotName),
749
- confirmed_regions: mergeConfirmedRegions(snapshot, screenshotName),
750
660
  history: {
751
661
  recent_comparisons: matches
752
662
  }