snapdrift 0.14.0 → 0.15.0

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,43 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.15.0 - 2026-09-22
6
+
7
+ Workspace releases this version:
8
+
9
+ - `@snapdrift/compare-core` 1.6.0 — vertical-offset alignment when exact row
10
+ matching hits its edit cap, including alpha-aware highlighting and the
11
+ search memory caps.
12
+ - `@snapdrift/manifest` 1.7.0 — `comparedOffset` on comparison row mappings.
13
+ - `@snapdrift/adapter-fs` 1.6.0 — depends on that compare-core so local policy
14
+ v2 comparisons use the offset-run engine.
15
+
16
+ `@snapdrift/adapter-report-md` stays 1.4.0. Root floors move to
17
+ `@snapdrift/compare-core ^1.6.0`, `@snapdrift/manifest ^1.7.0`, and
18
+ `@snapdrift/adapter-fs ^1.6.0`. The dispatcher pins `7b7b913`, the #179 merge
19
+ commit on `main`, which contains the offset-run runtime loaded by both wrapper
20
+ actions.
21
+
22
+ ### Features
23
+
24
+ - Comparison policy v2, after exact row alignment hits its edit cap, maps long
25
+ vertical-offset runs so an insertion does not highlight the rest of a shifted
26
+ page. A matched row on that path was mapped and fell below the highlight
27
+ rule; it does not have to be byte-identical. When the compared row is one
28
+ pixel above or below the recorded current row, the mapping includes
29
+ `comparedOffset` (`-1` or `1`).
30
+ - Offset highlighting includes alpha. Scoring ignores fully transparent pixels
31
+ and treats a large opacity change as a structured mismatch, so a small alpha
32
+ delta stays on the offset path.
33
+ - The offset search rejects a page before allocating its score matrices when
34
+ those matrices would exceed 64 MB. It also samples every 32nd row first and
35
+ stops that sample once the page cannot be a correspondence, so a dissimilar
36
+ page falls back without scoring every offset. A dissimilar 1440×5622 pair
37
+ fell back in about 0.9s; scoring every offset on that pair took about 3.5s.
38
+ - An offset canvas that would exceed `maxPixels` falls back to coordinate
39
+ comparison instead of throwing. The coordinate union can still throw when it
40
+ itself exceeds the limit.
41
+
5
42
  ## 0.14.0 - 2026-09-21
6
43
 
7
44
  Workspace releases this version:
package/README.md CHANGED
@@ -48,7 +48,7 @@ You keep ownership of checkout, build, startup, readiness, and teardown. SnapDri
48
48
 
49
49
  ```yaml
50
50
  - name: SnapDrift Baseline
51
- uses: ranacseruet/snapdrift@v0.14.0
51
+ uses: ranacseruet/snapdrift@v0.15.0
52
52
  with:
53
53
  mode: baseline
54
54
  repo-config-path: .github/snapdrift.json
@@ -60,7 +60,7 @@ With `provider: "snap"`, baseline publication is a complete, fail-closed snapsho
60
60
 
61
61
  ```yaml
62
62
  - name: SnapDrift Report
63
- uses: ranacseruet/snapdrift@v0.14.0
63
+ uses: ranacseruet/snapdrift@v0.15.0
64
64
  with:
65
65
  mode: pr-diff
66
66
  github-token: ${{ secrets.GITHUB_TOKEN }}
package/action.yml CHANGED
@@ -155,7 +155,7 @@ runs:
155
155
  - name: SnapDrift baseline
156
156
  id: baseline
157
157
  if: inputs.mode == 'baseline'
158
- uses: ranacseruet/snapdrift/actions/baseline@576e774845c29288a0061365e4905009533e0e01 # v0.14.0
158
+ uses: ranacseruet/snapdrift/actions/baseline@7b7b9138416fd16631c5d648b2ddfa9fe4a6a783 # v0.15.0
159
159
  with:
160
160
  repo-config-path: ${{ inputs.repo-config-path }}
161
161
  route-ids: ${{ inputs.route-ids }}
@@ -165,7 +165,7 @@ runs:
165
165
  - name: SnapDrift pull request diff
166
166
  id: pr-diff
167
167
  if: inputs.mode == 'pr-diff'
168
- uses: ranacseruet/snapdrift/actions/pr-diff@576e774845c29288a0061365e4905009533e0e01 # v0.14.0
168
+ uses: ranacseruet/snapdrift/actions/pr-diff@7b7b9138416fd16631c5d648b2ddfa9fe4a6a783 # v0.15.0
169
169
  with:
170
170
  github-token: ${{ inputs.github-token }}
171
171
  repo-config-path: ${{ inputs.repo-config-path }}
package/docs/contracts.md CHANGED
@@ -293,10 +293,14 @@ the diff output bundle (for example `diffs/home.png`).
293
293
  Policy v2 comparison metadata additionally includes `policyVersion: 2` and
294
294
  `mode`. An aligned result has `mode: "vertical-aligned"` and a compact
295
295
  `rowMapping` describing matched, changed, inserted, and deleted row runs. A
296
- `mode: "coordinate-fallback"` result includes `fallbackReason` when widths,
297
- ignore regions, ambiguity, or the alignment resource limit prevent safe row
298
- alignment. The aligned canvas may be taller than either source image because
299
- inserted and deleted rows each occupy an output row.
296
+ matched offset-run was mapped and fell below the highlight rule, so its rows
297
+ need not be byte-identical. A segment may include `comparedOffset` of `-1` or
298
+ `1` when the compared current row is one pixel away from `currentStart`; output
299
+ row `i` was compared to current row `currentStart + i + comparedOffset`. A `mode: "coordinate-fallback"` result
300
+ includes `fallbackReason` when widths, ignore regions, ambiguity, or the
301
+ alignment resource limit prevent safe row alignment. The aligned canvas may be
302
+ taller than either source image because inserted and deleted rows each occupy
303
+ an output row.
300
304
 
301
305
  When a changed item has a generated local `diffImagePath`, its PR comment cell
302
306
  links to the uploaded report artifact (or the workflow run when no artifact URL
@@ -332,7 +336,7 @@ Additional missing-baseline fields: `baselineAvailable`, `currentResultsPath`.
332
336
  - `diff.threshold` applies per screenshot
333
337
  - Missing captures are counted separately from drift signals
334
338
  - Comparison policy v1 `{ "version": 1, "threshold": number }` is deprecated but remains the compatibility default. Images are top-left aligned on a max-dimension union canvas with no scaling. Overlap pixels are compared, one-sided pixels count as changed (including transparent pixels), and empty union corners do not enter the denominator.
335
- - Comparison policy v2 `{ "version": 2, "threshold": number }` is opt-in. When source widths match and no ignore regions are requested, rows are fingerprinted and aligned with a bounded Myers sequence diff. Unchanged content that moves after an insertion/deletion remains matched; inserted current rows are green, deleted baseline rows are red, and actual pixel edits are orange. Width changes, ignore regions, ambiguous row matches, and resource limits fall back to v1 coordinate comparison and record the fallback reason. This is a raster row alignment aid, not DOM or arbitrary component matching.
339
+ - Comparison policy v2 `{ "version": 2, "threshold": number }` is opt-in. When source widths match and no ignore regions are requested, rows are fingerprinted and aligned with a bounded Myers sequence diff. If that search hits its edit cap, long vertical-offset runs are mapped instead. Inside a run, a row may be compared one pixel above or below the recorded current row when that neighbor matches. `currentStart` still consumes each current row once, so that neighbor is not a separate insertion or deletion. When the compared row differs, the segment includes `comparedOffset` of `-1` or `1`: output row `i` was compared to current row `currentStart + i + comparedOffset`. On that offset-run path, a pixel whose red, green, blue, or alpha channel differs by 32 or less stays unhighlighted, so rendering noise and soft edges remain matched. A short cluster of larger channel deltas is an orange edit. A wide band of those pixels is a local mismatch: it is highlighted in place and does not discard the rest of the page. Unmatched baseline rows are red deletions. Current rows with no baseline partner are green insertions. `matched` on this path means the row was mapped and fell below this highlight rule. Exact fingerprint matches remain byte-identical. Width changes, ignore regions, ambiguous exact-row matches, and resource limits fall back to v1 coordinate comparison and record the fallback reason. This is a raster row alignment aid, not DOM or arbitrary component matching.
336
340
  - Both policies preserve the explicit dimension-change signal: a source height/width change remains a changed route even when the aligned mismatch ratio is at or below the threshold. Empty output corners do not enter the denominator.
337
341
  - Local diff images are rendered with a semantic palette: orange = changed pixels, green = added or inserted pixels, red = removed or deleted pixels. Equal-dimension strict diffs (`generateDiffImage`) use orange for changed pixels and cannot contain added/removed pixels. Reports render a legend whenever a local diff image is embedded; summaries that carry only comparison metadata (Snap provider runs, which render diffs hosted-side) render no legend.
338
342
  - `dimensionChanges[]` remains part of the summary contract but is empty for local comparisons in v1. Strict same-dimension comparison (`compareBuffers`) remains available to direct `@snapdrift/compare-core`/`comparePngs` callers that pass no policy.
@@ -46,7 +46,7 @@ SnapDrift owns route selection, capture, comparison, skipped-report generation,
46
46
 
47
47
  ```yaml
48
48
  - name: SnapDrift Baseline
49
- uses: ranacseruet/snapdrift@v0.14.0
49
+ uses: ranacseruet/snapdrift@v0.15.0
50
50
  with:
51
51
  mode: baseline
52
52
  repo-config-path: .github/snapdrift.json
@@ -71,7 +71,7 @@ Then add SnapDrift after the app is running:
71
71
 
72
72
  ```yaml
73
73
  - name: SnapDrift Report
74
- uses: ranacseruet/snapdrift@v0.14.0
74
+ uses: ranacseruet/snapdrift@v0.15.0
75
75
  with:
76
76
  mode: pr-diff
77
77
  github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -86,8 +86,8 @@ The wrappers remain published in their own right, and are equivalent to the disp
86
86
 
87
87
  | Entry point | Equivalent to |
88
88
  |-------------|---------------|
89
- | `ranacseruet/snapdrift@v0.14.0` with `mode: baseline` | `ranacseruet/snapdrift/actions/baseline@v0.14.0` |
90
- | `ranacseruet/snapdrift@v0.14.0` with `mode: pr-diff` | `ranacseruet/snapdrift/actions/pr-diff@v0.14.0` |
89
+ | `ranacseruet/snapdrift@v0.15.0` with `mode: baseline` | `ranacseruet/snapdrift/actions/baseline@v0.15.0` |
90
+ | `ranacseruet/snapdrift@v0.15.0` with `mode: pr-diff` | `ranacseruet/snapdrift/actions/pr-diff@v0.15.0` |
91
91
 
92
92
  Use the wrappers directly when you want to skip the dispatcher's input indirection, and the lower-level `capture`, `compare`, `scope`, `resolve-baseline`, `stage`, `comment`, and `enforce` actions when you need to orchestrate the stages yourself.
93
93
 
@@ -130,7 +130,7 @@ jobs:
130
130
  done
131
131
 
132
132
  - name: SnapDrift Report
133
- uses: ranacseruet/snapdrift@v0.14.0
133
+ uses: ranacseruet/snapdrift@v0.15.0
134
134
  with:
135
135
  mode: pr-diff
136
136
  github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -170,7 +170,7 @@ jobs:
170
170
  done
171
171
 
172
172
  - name: SnapDrift Report
173
- uses: ranacseruet/snapdrift@v0.14.0
173
+ uses: ranacseruet/snapdrift@v0.15.0
174
174
  with:
175
175
  mode: pr-diff
176
176
  github-token: ${{ secrets.GITHUB_TOKEN }}
@@ -82,7 +82,7 @@ jobs:
82
82
 
83
83
  - name: Publish SnapDrift baseline
84
84
  if: steps.baseline_candidate.outputs.current == 'true'
85
- uses: ranacseruet/snapdrift@v0.14.0
85
+ uses: ranacseruet/snapdrift@v0.15.0
86
86
  with:
87
87
  mode: baseline
88
88
  repo-config-path: .github/snapdrift.json
@@ -226,6 +226,13 @@ function isComparisonMetadata(value) {
226
226
  }
227
227
  if (hasBaseline && (segment.baselineStart < 0 || segment.baselineStart + segment.length > baseline.height)) return false;
228
228
  if (hasCurrent && (segment.currentStart < 0 || segment.currentStart + segment.length > current.height)) return false;
229
+ if (segment.comparedOffset !== undefined) {
230
+ if (!hasBaseline || !hasCurrent || (segment.comparedOffset !== -1 && segment.comparedOffset !== 0 && segment.comparedOffset !== 1)) {
231
+ return false;
232
+ }
233
+ const comparedStart = segment.currentStart + segment.comparedOffset;
234
+ if (comparedStart < 0 || comparedStart + segment.length > current.height) return false;
235
+ }
229
236
  outputStart += segment.length;
230
237
  if (hasBaseline) baselineStart += segment.length;
231
238
  if (hasCurrent) currentStart += segment.length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snapdrift",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "SnapDrift captures, compares, and reports UI drift for Playwright-based app checks.",
@@ -103,10 +103,10 @@
103
103
  "registry": "https://registry.npmjs.org"
104
104
  },
105
105
  "dependencies": {
106
- "@snapdrift/adapter-fs": "^1.5.0",
106
+ "@snapdrift/adapter-fs": "^1.6.0",
107
107
  "@snapdrift/adapter-report-md": "^1.4.0",
108
- "@snapdrift/compare-core": "^1.5.0",
109
- "@snapdrift/manifest": "^1.6.0",
108
+ "@snapdrift/compare-core": "^1.6.0",
109
+ "@snapdrift/manifest": "^1.7.0",
110
110
  "js-yaml": "^5.4.0",
111
111
  "playwright": "^1.59.1",
112
112
  "pngjs": "^7.0.0"
@@ -55,6 +55,12 @@ export interface ComparisonRowMapping {
55
55
  kind: ComparisonRowKind;
56
56
  baselineStart?: number;
57
57
  currentStart?: number;
58
+ /**
59
+ * Present when an offset-run row was compared one pixel above or below the
60
+ * recorded current row. Output row i was compared to current row
61
+ * `currentStart + i + comparedOffset`.
62
+ */
63
+ comparedOffset?: -1 | 0 | 1;
58
64
  }
59
65
 
60
66
  export interface VisualRegressionRouteConfig {