@variance-authority/core 0.15.0 → 0.17.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 (82) hide show
  1. package/CHANGELOG.md +424 -0
  2. package/README.md +25 -7
  3. package/dist/attribute/component-hash.d.ts +11 -3
  4. package/dist/attribute/component-hash.js +11 -3
  5. package/dist/attribute/component-hash.js.map +1 -1
  6. package/dist/attribute/movement.d.ts +1 -1
  7. package/dist/attribute/movement.js.map +1 -1
  8. package/dist/attribute/region.d.ts +47 -4
  9. package/dist/attribute/region.js +90 -17
  10. package/dist/attribute/region.js.map +1 -1
  11. package/dist/attribute/source.d.ts +1 -1
  12. package/dist/attribute/source.js +1 -1
  13. package/dist/attribute/source.js.map +1 -1
  14. package/dist/compare/band.d.ts +8 -2
  15. package/dist/compare/band.js +7 -2
  16. package/dist/compare/band.js.map +1 -1
  17. package/dist/compare/diff/attribution.js +6 -4
  18. package/dist/compare/diff/attribution.js.map +1 -1
  19. package/dist/compare/diff/compare-nodes.js +9 -1
  20. package/dist/compare/diff/compare-nodes.js.map +1 -1
  21. package/dist/compare/diff/match.d.ts +2 -1
  22. package/dist/compare/diff/match.js +52 -0
  23. package/dist/compare/diff/match.js.map +1 -1
  24. package/dist/compare/parting.js +6 -4
  25. package/dist/compare/parting.js.map +1 -1
  26. package/dist/format/document.d.ts +26 -3
  27. package/dist/format/document.js.map +1 -1
  28. package/dist/format/index.d.ts +7 -0
  29. package/dist/format/index.js +4 -0
  30. package/dist/format/index.js.map +1 -1
  31. package/dist/format/packed.d.ts +32 -0
  32. package/dist/format/packed.js +23 -0
  33. package/dist/format/packed.js.map +1 -0
  34. package/dist/format/shape.d.ts +31 -0
  35. package/dist/format/shape.js +269 -0
  36. package/dist/format/shape.js.map +1 -0
  37. package/dist/format/stabilize.d.ts +7 -1
  38. package/dist/format/stabilize.js +10 -3
  39. package/dist/format/stabilize.js.map +1 -1
  40. package/dist/format/steps.d.ts +44 -0
  41. package/dist/format/steps.js +67 -0
  42. package/dist/format/steps.js.map +1 -0
  43. package/dist/format/width.d.ts +35 -0
  44. package/dist/format/width.js +38 -0
  45. package/dist/format/width.js.map +1 -0
  46. package/dist/judge/docket.js +1 -0
  47. package/dist/judge/docket.js.map +1 -1
  48. package/dist/judge/index.d.ts +1 -0
  49. package/dist/judge/index.js +1 -0
  50. package/dist/judge/index.js.map +1 -1
  51. package/dist/judge/restamp.d.ts +20 -0
  52. package/dist/judge/restamp.js +20 -0
  53. package/dist/judge/restamp.js.map +1 -0
  54. package/dist/relate/before.js +2 -2
  55. package/dist/relate/before.js.map +1 -1
  56. package/dist/relate/carried.d.ts +1 -1
  57. package/dist/relate/carried.js +5 -5
  58. package/dist/relate/carried.js.map +1 -1
  59. package/dist/relate/graph.d.ts +14 -12
  60. package/dist/relate/graph.js +12 -11
  61. package/dist/relate/graph.js.map +1 -1
  62. package/dist/relate/index.d.ts +1 -2
  63. package/dist/relate/index.js +1 -2
  64. package/dist/relate/index.js.map +1 -1
  65. package/dist/relate/reach.d.ts +5 -5
  66. package/dist/relate/reach.js.map +1 -1
  67. package/dist/relate/records.d.ts +5 -4
  68. package/dist/relate/records.js +3 -2
  69. package/dist/relate/records.js.map +1 -1
  70. package/dist/rules/normalize/fixture.d.ts +2 -0
  71. package/dist/rules/normalize/fixture.js +2 -1
  72. package/dist/rules/normalize/fixture.js.map +1 -1
  73. package/dist/shard/placement.d.ts +10 -8
  74. package/dist/shard/placement.js +7 -21
  75. package/dist/shard/placement.js.map +1 -1
  76. package/dist/share/index.d.ts +1 -1
  77. package/dist/share/index.js +1 -1
  78. package/dist/share/index.js.map +1 -1
  79. package/package.json +2 -2
  80. package/dist/relate/dominate.d.ts +0 -19
  81. package/dist/relate/dominate.js +0 -77
  82. package/dist/relate/dominate.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,429 @@
1
1
  # @variance-authority/core
2
2
 
3
+ ## 0.17.1
4
+
5
+ Lockstep release — nothing in this package changed. Every `@variance-authority/*` package shares one version.
6
+
7
+ ## 0.17.0
8
+
9
+ Lockstep release — nothing in this package changed. Every `@variance-authority/*` package shares one version.
10
+
11
+ ## 0.16.0
12
+
13
+ ### Minor Changes
14
+
15
+ - 40a19b9: A committed baseline is named for its story or test
16
+
17
+ Under `directory` and `lfs`, a baseline was written to
18
+ `<root>/<identity digest>/<percent-encoded subject id>.png`, so a pull request
19
+ that accepted one listed a 32-character hex directory nobody could check against
20
+ the change, and each machine that accepted wrote its own copy. `baselines.path`
21
+ is now a template rendered from what the subject is: `{dir}` and `{file}` (the
22
+ file that declares it), `{title}`, `{name}`, `{id}`, `{project}` (the
23
+ Playwright project's name, in the fixture's `varianceBaselinePath` only) and
24
+ `{engine}` (the engine's family). The default, `{title}/{name}-{engine}`,
25
+ writes `Ui/Button/Primary-chromium.png`;
26
+ `{dir}/__baselines__/{file}/{name}-{engine}` with `root: "."` writes
27
+ `src/ui/Button/__baselines__/Button.stories.tsx/Primary-chromium.png` beside the
28
+ code. A label is spelled `Primary__dark-chromium.png`.
29
+
30
+ The machine that painted a baseline is read from the record beside it. One file
31
+ holds each subject per engine family, the last accept writes it, and a run on
32
+ another platform, scale factor or engine version compares against it, reading
33
+ unchanged or changed as the pixels say, with the moved field named as the cause.
34
+ A laptop and CI that take turns accepting rewrite the record each time; set
35
+ `baselines.records` to keep the records out of version control. The tribunal's
36
+ bucket store answers the same way: a lookup reads the last accept on the
37
+ engine family, where it used to prefer this machine's own row.
38
+
39
+ Two subjects that render one path, compared case-folded, stop the run with an
40
+ error naming both subjects and the template. Nothing is renumbered. The record
41
+ names the subject it was accepted for, so a shard, a Playwright worker or a
42
+ later run that renders the same path for another subject stops too. Two
43
+ Playwright projects that differ only in scale factor, colour scheme or viewport
44
+ render one path; the error names both projects and `{project}`. A record left by
45
+ a project the config no longer runs is a renamed project's, and the first run
46
+ under the new name takes it over.
47
+
48
+ The Vitest plugin reads a test's `describe` path from the runner, so a title
49
+ that contains ` > ` stays one directory under `{title}`.
50
+
51
+ **Breaking:** `baselines.layout` is removed, and a config that sets it is
52
+ refused with an error naming `path`. Baselines under the identity directories
53
+ are still read: the subject is compared against them, unchanged or changed as
54
+ the pixels say, with the move from the old path to the new one given as a cause,
55
+ and the next accept writes the template's path and deletes the old pair, so the
56
+ pull request shows a rename. An accept deletes only a pair this machine's
57
+ identity or engine line painted; another machine's stays until that machine
58
+ accepts. `BaselineKey` carries `file`, `title`, `name` and
59
+ `project` in place of `path`; the Playwright fixture, the Vitest plugin and
60
+ Storybook's index supply them, and `varianceBaselinePath` and `baselinePath`
61
+ set the template for a suite run without the CLI.
62
+
63
+ The git-LFS store's `.gitattributes` pattern and the action's commit-back
64
+ pathspecs are the template's glob, so a collocated baseline is tracked and
65
+ committed where it is written. An `lfs` config whose root is `.` and whose
66
+ template has no directory of its own is refused, because its glob would track
67
+ every hyphenated image in the repository; give the template a directory such
68
+ as `__baselines__`, give the baselines a root of their own, or set
69
+ `baselines.pattern`. A `directory` config at root `.` with such a template is
70
+ refused for the same reason: the action would commit every hyphenated image
71
+ back. An existing LFS root gets the template's pattern as a new
72
+ `.gitattributes` line the next time a run opens it; commit it with the baselines.
73
+ - 09f60c8: A reach walks type-only imports unless you name the runtime edges
74
+
75
+ `dependentsOf`, `dependenciesOf`, `affectedBy` and `carried` in
76
+ `@variance-authority/core/relate` now walk every edge kind, `type` included,
77
+ when you pass no `through`. A type-only import is still a dependency: a changed
78
+ type can break the file that imports it, and the walk said nothing about that
79
+ file. Test selection asks what a run loads, so every selection walk in Sense and
80
+ the CLI passes `RUNTIME_EDGES`, and what it selects does not change.
81
+
82
+ If you called one of these and relied on `type` edges being left out, pass
83
+ `{ through: RUNTIME_EDGES }`.
84
+ - 0b6af81: A changed region is joined against the baseline's nodes as well as this render's
85
+
86
+ A change that replaces an element paints over the old element's box and the new
87
+ one's, and `attributeRegions` read only one snapshot. On todomvc's broken toggle,
88
+ 71% of the changed region is inside the checkbox that left and 88.75% inside the
89
+ div that arrived. Neither reaches the 0.9 containment, so the region named the
90
+ row around both.
91
+
92
+ `AttributionOptions.before` is now required: the snapshot of the revision the
93
+ pixels are compared against, or `undefined`. Each node is joined against the
94
+ area its own box and the box of the node the differ's matcher pairs it with in
95
+ `before` cover together: the two places, not the rectangle that holds both.
96
+ A box that left is placed by where it was in its own image, whose corner is the
97
+ new `AttributionOptions.beforeOrigin`: by default the before snapshot's root, so
98
+ a subject the page moved between two clipped shots joins where its pixels are.
99
+ A call that states `origin` with a `before` snapshot and no `beforeOrigin`
100
+ throws, since a corner guessed from the other image is right only when the page
101
+ did not move.
102
+
103
+ A baseline keeps that snapshot. `Raster.snapshot` is a `PackedSnapshot`, the
104
+ tree gzipped and in base64, written by the Playwright fixture, its promotion,
105
+ the Vitest browser provider and the CLI's image sidecars. `packSnapshot` and
106
+ `unpackSnapshot` are exported from `@variance-authority/observe`. Every store
107
+ carries it as text: `sidecarFrom` keeps it, `variance push` forwards it, and
108
+ tribunal stores it in new `baselines.snapshot` and
109
+ `build_subjects.candidate_snapshot` columns (schema 21, migration
110
+ `0019_baseline-snapshot.sql`) and promotes it on approval. On
111
+ `cases/storybook-case` it adds 3.2–5.4 KB to each sidecar.
112
+
113
+ A baseline without a snapshot says so. When a changed verdict has regions and
114
+ its baseline kept no snapshot, or one that will not unpack, the reason ends
115
+ *the baseline records no snapshot of its nodes, so regions were joined against
116
+ this render's boxes alone: an accepting run records one on a revision where
117
+ this subject is unchanged*. An accepting run — `--update-snapshots`, Vitest's
118
+ `--update`, `variance accept` — now re-stamps an unchanged baseline that kept no
119
+ snapshot, as it re-stamps one painted under another identity, and the
120
+ observation marks it `signals.snapshot: 'unrecorded'`; `restamps` in
121
+ `@variance-authority/core/judge` is the one rule all three ask. Every other
122
+ verdict the mark is on ends *the baseline records no snapshot of its nodes, and
123
+ an accepting run records this one*. Two images
124
+ compared directly get the same clause, naming the before image, without the
125
+ remedy, and are never marked.
126
+
127
+ A subject whose document has not moved settles on its digest and builds no
128
+ observation to mark, so `settle` now renders a pictured baseline that kept no
129
+ snapshot when this run collected one, with the reason *the baseline records no
130
+ snapshot of its nodes, so this run renders it for an accepting run to record
131
+ one*. A suite of baselines accepted before this release renders them on every
132
+ run until an accepting run re-stamps them. Tribunal records one on ingest
133
+ instead: a pushed candidate's snapshot is written onto a baseline that has an
134
+ image and kept none, when it was painted from the same document under the same
135
+ identity, and never over one it kept, so such a subject renders once. The
136
+ snapshot's runtime fields (`provenance` and its `createdBy`, `wiring`, `holding`,
137
+ `styleProvenance`, `styleTokens`, `ignoredBy`) are then the recording run's: the
138
+ first push, from a build on any branch, that drew that document, as they are the
139
+ accepting run's on a re-stamp. `Described.keepsSnapshot`
140
+ is the new required field it reads:
141
+ the durable, LFS and tribunal stores derive it from the sidecar with
142
+ `keepsSnapshot` from `@variance-authority/core/format`, and the remote store
143
+ refuses a description without it. A packed snapshot now opens with a `v1:`
144
+ header naming its format version, `PACKED_SNAPSHOT_HEADER`, and a snapshot
145
+ without it counts as not kept. `unpackSnapshot` reads one as not recorded
146
+ unless every node in it is one, with `isSemanticSnapshot`, new in
147
+ `@variance-authority/core/format` beside `isRenderDocument` and
148
+ `isSubjectRef`: the checks `@variance-authority/unit-test` already made of a
149
+ capture file's snapshot, document and subject, which it now asks. Tribunal serves API 6, which describes a
150
+ baseline with `keepsSnapshot`; `variance push` against an older deployment names
151
+ the refused descriptions when the project's `remote` baselines are stored there,
152
+ and the remote store's refusal names tribunal before API 6 and a
153
+ `serveRasterStore` from an earlier `@variance-authority/remote`: upgrade the
154
+ server, tribunal or your own, before the client. `settle` takes this run's snapshot as a
155
+ fifth argument. `variance accept` and the MCP changelog call these subjects
156
+ *unchanged and re-stamped* rather than re-stamped under this run's identity,
157
+ which was untrue of them. The render cache no longer hands on a snapshot an
158
+ entry kept.
159
+ - 18c70d3: A run collects in parallel by default, and the report says where each subject's time went
160
+
161
+ Without `workers` in the config, a run collected in one browser world, so a
162
+ route suite read its pages one after another on any machine. It now collects in
163
+ half the machine's logical cores, at least one and never more worlds than there
164
+ are files: two on a 4-vCPU CI runner, eight on a 16-core laptop. A `concurrency`
165
+ you set holds it lower, since every world keeps a page and a subject in flight.
166
+ A `workers` you set still wins over both. Set `workers: 1` for a server under
167
+ test that cannot take concurrent pages. A collector with no `openWorker` still
168
+ collects in one world, and the run warns about it only when you set `workers`. Verdicts and the order
169
+ of observations do not depend on the count, and the report's new `workers` field
170
+ records it. `variance collect` takes the same width.
171
+
172
+ A changed subject read in a second world is now run alone like one read in the
173
+ first. A worker opens no clean world of its own, so the run asked the worker and
174
+ reported "not re-run alone: collector has no `collectAlone`"; it now asks the
175
+ collector that can open one.
176
+
177
+ The `alone.limit` budget now goes to the first subjects to change in the order
178
+ the run takes them. It went to whichever changed subjects reached it first, so
179
+ with more changed subjects than the limit, which ones carried `again` and
180
+ `alone` evidence depended on the number of worlds and on timing.
181
+
182
+ Each observation in the report also carries `steps` beside `costMs`: every
183
+ phase the subject went through, in the order it ran, with its wall time. The
184
+ collector's phases are named `collect.*` (`collect.navigate`,
185
+ `collect.wait-for-fonts`, `collect.capture`), the run's own are `compose`,
186
+ `inspect`, `settle`, `baseline`, `cache.get`, `cache.put`, `decode`, `compare`,
187
+ `write`, `again.collect` and `alone.collect`, and a painted subject adds the
188
+ renderer's under `render.*`. The report's `steps` sums each phase over the
189
+ subjects that ran it and counts them. A phase a subject skipped is absent, not
190
+ zero. Timings are part of no digest, so images, records and baselines do not
191
+ move when only timing does.
192
+
193
+ `settleRecipe`, `stabilizeForObservation`, `compareRasters` and the raster
194
+ `Renderer.render` take an optional step clock and report the steps they ran on
195
+ it. The route page agent and `readRoute` always return the steps they took.
196
+ `stepClock`, `timeOn` and `totalsOf` are exported from
197
+ `@variance-authority/core/format`.
198
+
199
+ A run that is not sharded now reads the mainline's subject costs at every
200
+ width, as a sharded run always did, and takes its slowest files first. One
201
+ world takes its subjects in the same order as several, so the budget above
202
+ reaches the same subjects.
203
+
204
+ `concurrency` is split evenly across the worlds and rounded up, at least one
205
+ per world, so by default the raster tier is as wide as the number of worlds.
206
+
207
+ The frame above a subject carries its classes in code-unit order. Storybook
208
+ swaps its layout class on a page it reuses, so their order said which story the
209
+ page showed before, and a run in parallel lanes gave the same story a different
210
+ document, and a render-cache miss, with the same pixels.
211
+ - 9b95dc1: A shard takes whole files in every runner, and is balanced by file count without times
212
+
213
+ Under `--shard k/n`, a Playwright config wrapped by `withTestSelection` now
214
+ places each spec file whole in one shard, by the time it took on the last
215
+ recorded run, as Vitest and Jest shards are placed. Playwright's own split,
216
+ which under `fullyParallel` divides a file's tests between shards, does not run.
217
+ The reporter places the shard in `preprocess`, after `--project`, `--grep` and
218
+ `.only`, and prints one line on stderr:
219
+ `variance-authority: shard 1/2 by file count: 2 of 3 files; no times at …`.
220
+ A file several projects run, or `repeatEach` repeats, goes with all its copies
221
+ to one shard; setup and teardown projects are not placed, and a project with no
222
+ file in the shard is left out whole. Playwright runs a setup project in every
223
+ shard that needs it, so combining the shards' records refuses its files as run
224
+ by two shards. `PWTEST_SHARD_WEIGHTS` is ignored for placement, though
225
+ Playwright still validates it.
226
+
227
+ This needs Playwright 1.62 or newer. `withTestSelection` refuses a run that asks
228
+ for a shard, by `--shard` or by `shard` in the configuration, when the
229
+ configuration is evaluated, before any test starts: on an older Playwright with
230
+ `variance-authority: refusing shard k/n: Playwright <version> would split it
231
+ test by test, …; placing whole files needs Playwright 1.62 or newer`, and when
232
+ a `--reporter` on the command line replaces the configuration's reporters
233
+ without naming `@variance-authority/playwright-test/reporter`, by that
234
+ specifier or by a path that resolves to it. The version is the one the
235
+ configuration's directory resolves, from `-c`/`--config` or the working
236
+ directory. A configuration that names the reporter without `withTestSelection`
237
+ is not refused. A run with no shard runs as before.
238
+
239
+ When the record has no times, or none for the files collected, a Vitest, Jest
240
+ or Playwright shard prices every file the same and places them by the same
241
+ rule, so the shards are balanced by file count and none is left empty while
242
+ there are as many files as shards. The line reads
243
+ `shard k/n by file count: <m> of <n> files; <why>`, in place of
244
+ `split by the runner, by count: <why>`. A file the runner hands in more than
245
+ once, for each project that runs it, is placed once at its recorded time, which
246
+ already sums every project, with every copy in one shard.
247
+
248
+ `place` in `@variance-authority/core/shard` is where that rule lives: with
249
+ nothing recorded, every group weighs one and is placed longest first on the
250
+ least-loaded shard like a timed one, ties broken by key in code-unit order and
251
+ then by the lowest shard. Its `by` reads `count` rather than `checksum`, and
252
+ its `cost` and `load` are always present, one per group when placed by count.
253
+ `variance run --shard` and `variance collect --shard` place stories and routes
254
+ by the same rule, and a subject another shard owns reads
255
+ `assigned to shard k/n by count, …`.
256
+
257
+ A Jest or Vitest configuration wrapped by `withTestSelection` and evaluated
258
+ under `--shard` gets `passWithNoTests: true` unless it sets `passWithNoTests`
259
+ itself, so a shard left no file, because there are fewer files than shards,
260
+ passes as it does under Playwright. Vitest 4 refused such a run before any file
261
+ was placed. An unsharded run that finds no file still fails.
262
+
263
+ `shardOf(files, shard, times)`, `landingTimes(record, options)` and
264
+ `shardArgument(argv)`, which reads the `--shard k/n` a command line asks for, are exported
265
+ from `@variance-authority/sense/test-selection`, for a seam that places a shard
266
+ by the same rule.
267
+
268
+ A Vitest, Jest or Playwright shard that cannot read the record its times come
269
+ from now fails before any test runs, with
270
+ `variance-authority: the times this shard is placed by could not be read, …`
271
+ and the reason: a share that did not answer or answered with what does not
272
+ read, a record that is there and does not decode, or a CLI that throws as it
273
+ loads. It used to place by file count, beside shards that may have read the
274
+ times. A record that is not there, a share that holds none or is not
275
+ configured, and a CLI that is not installed still place by count. An unsharded
276
+ run reads no times and runs as before. `variance shards` fails the same way.
277
+
278
+ **Breaking:**
279
+
280
+ - A Vitest or Jest shard with no times is placed by file count, in code-unit
281
+ order of path, not by your sequencer's `shard`. Your sequencer still sorts
282
+ each shard.
283
+ - A Playwright run below 1.62 that asks for a shard is refused. Upgrade
284
+ Playwright, or run without `--shard`.
285
+ - A sharded Playwright run whose `--reporter` leaves out
286
+ `@variance-authority/playwright-test/reporter` is refused. Name it in
287
+ `--reporter` too.
288
+ - `variance run --shard` and `variance collect --shard` with no recorded costs
289
+ place each file by count, not by a checksum of the file and the shard. The
290
+ shards are balanced, but adding a file can move others to another shard.
291
+ A collection part's `assignment`, and the `assignment` in the provenance of
292
+ the suite index merged from it, read `count` where they read `checksum`. An
293
+ index that reads `checksum` still opens. `variance collect merge` refuses
294
+ sharded parts an earlier version cut by checksum; collect every shard again.
295
+ - A sharded run whose share does not answer fails rather than placing by
296
+ file count. Run the job again, or hand every shard the record fetched once.
297
+ - 402ce9f: A story's parameters are read when the story is reached, so no story file is imported before its story comes up
298
+
299
+ Your Storybook run used to read every story's `parameters.variance` before the
300
+ first story, through the preview's `extract()`. That imported every story file
301
+ up front: on a large Storybook you paid for the whole build before seeing one
302
+ story, and each file's first story then timed its import as 0 in `collect`.
303
+
304
+ The run now plans from `index.json` alone. When it reaches a story, it asks the
305
+ preview for that story's merged parameters, which imports only that story's
306
+ file, the import showing the story would have made anyway. That import is
307
+ charged to the story's `collect`, so the first story of each file carries what
308
+ its file costs. A story that excludes itself is still skipped and listed in the
309
+ report with its reason, and a story whose parameters cannot be read is read at
310
+ your run's `viewport` with a warning naming it. A preview too old to read one
311
+ story's parameters says so once per run, not once per story.
312
+
313
+ A story that declares `widths` is still read as `<story id>@<width>` at each
314
+ width, with its own baseline, and all its widths are read on the same page in
315
+ one visit: nothing is opened, booted or loaded again for a second width. A
316
+ run that does not read such a story, because `--subjects`, `--since` or a shard
317
+ left it out, because its visit failed, or because its parameters could not be read, still counts its width baselines as
318
+ planned rather than naming them as baselines your plan no longer holds. A
319
+ story that excludes itself when it is reached holds no subject of the run, so
320
+ every baseline it left, under its own id and at each width, is named. A
321
+ shard prices the story at the sum of the widths its last visit read.
322
+
323
+ If you write a collector, `collect` may now return a `Visit` instead of a
324
+ `Collected`: every subject the visit to the planned one produced, each with its own
325
+ result, each id the planned id or the planned id at a width (`<id>@<width>`),
326
+ and any warnings the visit raised. A visit that could not learn every subject
327
+ it realizes, such as a story read without its parameters, says `incomplete: true`.
328
+ A visit that produced no subject is
329
+ reported as a failed one, and so is a visit that produced any other id. The run and `variance collect` report each subject
330
+ at the position the plan gave the planned subject, and evidence parts record
331
+ the planned id as `of`; a merge refuses a part whose subject is neither. A
332
+ `--subjects` glob that names one width, such as `*@375`, now reaches the story
333
+ it comes from, and only a width suffix extends a story id: `button--primary`
334
+ no longer reaches `button--primary-large`.
335
+
336
+ `@variance-authority/core/format` exports `atWidth`, which writes a subject's
337
+ id and name at a width, and `widenedFrom`, which reads the planned id back
338
+ from a width id, and `realizes`, which says whether a visit to one id may
339
+ produce another.
340
+
341
+ `readParameters(page)` is now `readParameters(page, storyId)` and returns one
342
+ story's parameters; the up-front read of every story is gone.
343
+ - f18c00c: The differ pairs the one element its author replaced, and says what it became
344
+
345
+ When, under one matched parent, exactly one element left and exactly one
346
+ arrived, and the same component's `createdBy` wrote both, `matchTrees` pairs
347
+ them. The pair is compared like any other and leads with a new `tag-changed`
348
+ delta, in the geometry band and counted as structural, followed by whatever else
349
+ differs: role, name, state, attributes, styles and box. Before, the same change
350
+ was a `node-removed` and a `node-added`. Two or more replaced by one author under
351
+ one parent stay unpaired, because either could be either, and so do two that
352
+ each carry an alias or a role and name of their own: every `<Button>` is created
353
+ by `Button`, and a Save button replaced by an Export link is two elements.
354
+
355
+ `tag-changed` is reported on every pair across two tags, so a pair that a
356
+ declared alias or a shared role and name joined — a `<button>` and a
357
+ `<div role="button">` with one name — now says its tag changed, where before it
358
+ reported nothing for it.
359
+
360
+ A `style-changed` or `token-changed` delta on a pair across two tags is banded
361
+ `geometry`, with the `tag-changed` it follows, rather than `token`: the style is
362
+ the new element's, and the structure a `token` delta says held did not. A
363
+ `bands: ['token']` ignore rule no longer reads it. `bandOf` owns the rule, as
364
+ `bandOf(kind, { retagged })`, and a parting row's bands now read each delta's
365
+ own band, so the row and the docket agree.
366
+ - 4aa7b16: Content inserted into a page is the inserted rows, not everything under them
367
+
368
+ Two images of the same width are compared row by row after their rows are
369
+ aligned: each row is hashed, runs of equal rows are one token, and Myers finds
370
+ the shortest edit between the two sequences. Each after row is then compared
371
+ with the before row that shows the same content, and `pixelmatch` still judges
372
+ every pairing; a row that moved, came or went is a change in itself, so no
373
+ pairing makes a page read as unchanged. A section added to a long page marks that
374
+ section, and the content it pushed down matches. A pairing is kept only when it
375
+ leaves fewer pixels differing than position does, a row it leaves unpaired
376
+ charged in full, so a line that changed in place is not shifted against its
377
+ neighbours. The rows a gap grew by are inserted, as the rows it lost are
378
+ removed, so a button whose padding grew has a region where it grew. Images of
379
+ different widths, or an edit longer than 2,000 runs of rows, are compared
380
+ position by position on the union box, as before.
381
+
382
+ An inserted row is marked edge to edge, since nothing says where its content
383
+ begins, so a region inside one is joined by its rows rather than its area: to
384
+ the tightest box whose rows span it. `attributeRegions` takes the inserted rows
385
+ as `inserted`. Joined by area, every such band went to the subject's root, and
386
+ a card whose button grew named the card rather than the button.
387
+
388
+ `RasterComparison.alignment` carries the pairing as `RowAlignment`: the after
389
+ rows `inserted`, the before rows `removed` with the after row they left from,
390
+ and the rows `shifted` with how far. A comparison without it was not aligned.
391
+
392
+ Content that moved is a change. A gap that grew in one place and shrank in
393
+ another moves what lies between them and leaves the page as tall as it was, and
394
+ every moved row matches the row it came from; such a subject reports `changed`,
395
+ never `unchanged` or `ignored`. The reason names the edit:
396
+ *120 pixels differ across 1 region; 3 pixel rows inserted in 1 place, moving
397
+ the content under them by up to 3 rows, and the subject resized from 40×5 to
398
+ 40×8*. The Playwright fixture's evidence now writes a diff for a subject that
399
+ resized, drawn on the after image.
400
+ - 16e7c76: `dominatorsOf` is removed from `@variance-authority/core/relate`
401
+
402
+ Nothing ran it once distill read its records in the sense addon, which finds
403
+ the import every path from a test file runs through with its own dominator
404
+ pass. A caller that needs immediate dominators of a file graph computes them
405
+ over `Relations` directly.
406
+
407
+ ### Patch Changes
408
+
409
+ - 8d1bcc1: A link in a doc comment opens from `node_modules`
410
+
411
+ Doc comments linked to documentation by repository paths such as
412
+ `../../../../docs/flakiness.md`, which resolve to nothing in an installed
413
+ package. They now link to the page on variance-authority.dev, or to the file on
414
+ GitHub when the site does not publish it, so the link works on hover in your
415
+ editor and in what `variance ask symbol` prints.
416
+
417
+ The MCP server's instructions and its observability answer linked to
418
+ `/start-cli` and `/presentation`, which the site does not serve. They now link
419
+ to `/start/cli` and `/docs/presentation`, and the `@variance-authority/cli`,
420
+ `@variance-authority/help`, `@variance-authority/mcp` and
421
+ `@variance-authority/vitest-browser` READMEs link to the guides where the site
422
+ publishes them.
423
+
424
+ `variancePrecondition`, `wiringOf`, `tapCommits` and `provenanceOf` carry an
425
+ `@see` link to the section of the guide that documents them.
426
+
3
427
  ## 0.15.0
4
428
 
5
429
  ### Minor Changes
package/README.md CHANGED
@@ -5,6 +5,7 @@
5
5
  > The Variance Authority format, rules, comparison, attribution and verdicts. Pure data in, pure data out, no DOM and no I/O.
6
6
 
7
7
  Part of [Variance Authority](https://variance-authority.dev).
8
+ Point your coding agent at [`llms.txt`](https://variance-authority.dev/llms.txt).
8
9
 
9
10
  ## What this is for
10
11
 
@@ -187,13 +188,13 @@ are five, loudest first:
187
188
  | band | what changed | example delta kinds |
188
189
  |---|---|---|
189
190
  | `a11y` | a role, accessible name, description or ARIA state | `role-changed`, `name-changed`, `state-changed` |
190
- | `geometry` | boxes appeared, vanished, moved or resized | `node-added`, `node-moved`, `rect-changed`, `attribute-changed` |
191
- | `token` | style values changed while structure stayed the same | `style-changed`, `token-changed` |
191
+ | `geometry` | boxes appeared, vanished, moved, resized or were replaced | `node-added`, `node-moved`, `tag-changed`, `rect-changed`, `attribute-changed` |
192
+ | `token` | style values changed while structure stayed the same | `style-changed`, `token-changed`, except on a pair across two tags, where they are `geometry` |
192
193
  | `content` | text changed and nothing else did | `text-changed` |
193
194
  | `texture` | sub-semantic rendering variance | `raster-residue` |
194
195
 
195
- `bandOf(kind)` performs that assignment and is the default rather than the
196
- answer — policy may promote or demote per project, subject, region or
196
+ `bandOf(kind, { retagged })` performs that assignment and is the default
197
+ rather than the answer — policy may promote or demote per project, subject, region or
197
198
  component. `loudestBand(bands)` collapses a set to the loudest one present, and
198
199
  returns `null` for an empty set so that *nothing changed* and *something changed
199
200
  at the quietest band* stay apart.
@@ -231,7 +232,10 @@ const after = normalize(recapture);
231
232
  const diff = diffSnapshots(before, after); // deltas and roots, no verdict
232
233
 
233
234
  const places = isolateRegions(mask, { cell: 8 }); // pixels → regions
234
- const named = attributeRegions(places.regions, after, { scale: 2 });
235
+ const named = attributeRegions(places.regions, after, {
236
+ scale: 2,
237
+ before, // where the pixels it replaced were
238
+ });
235
239
  ```
236
240
 
237
241
  `diffSnapshots` returns `identical`, the flat `deltas`, the `roots` a docket
@@ -245,6 +249,20 @@ runner.
245
249
  screenshot at 1x and every region lands in the top-left quadrant with the wrong
246
250
  component named — a full, plausible, entirely wrong report.
247
251
 
252
+ `before` is required too: the snapshot of the revision the pixels are compared
253
+ against, or `undefined` when nobody kept it. A change that replaces an element
254
+ paints over the old box and the new one, so each node is joined against its own
255
+ box and the box of the node the differ's matcher pairs it with. A stored baseline
256
+ carries that snapshot packed, as its `snapshot`; `unpackSnapshot` in
257
+ `@variance-authority/observe` reads it back.
258
+
259
+ Each box that left is placed by where it was in its own image. A screenshot
260
+ clipped to the subject starts at the subject's root, and `beforeOrigin` is the
261
+ before image's top-left corner in page CSS pixels. It defaults to the before
262
+ snapshot's root, as `origin` defaults to this render's. When you pass `origin`
263
+ and a `before` snapshot, pass `beforeOrigin` too, or the call throws: the page
264
+ can move what you clipped to between the two shots.
265
+
248
266
  Rendered comparisons throw when the two subjects differ, or when their
249
267
  observation profiles differ.
250
268
 
@@ -253,7 +271,7 @@ observation profiles differ.
253
271
  | call | useful controls |
254
272
  |---|---|
255
273
  | `normalize` | `collapseWrappers` removes layout-only wrapper boundaries, `digestText` includes text content, and `sourceRoot` relativizes source locations |
256
- | `isolateRegions` / `attributeRegions` | `cell` and `limit` bound mask work; `origin` and `containment` describe the coordinate origin and how much a node must contain a region |
274
+ | `isolateRegions` / `attributeRegions` | `cell` and `limit` bound mask work; `origin` and `containment` describe the coordinate origin and how much a node must contain a region; `inserted` names the rows the after image added, and a region inside them goes to the tightest box whose rows span it |
257
275
  | `fingerprintOfMask` | `grid` controls the shape sample and `coverage` the minimum occupied share |
258
276
  | `compareLocales` / ignore validation | `slack` permits a declared locale distance; `sites` supplies resolved ignore locations and `now` evaluates expiry |
259
277
  | `buildDocket` / dependency reach | `sampleSize` limits review examples; `through` selects graph edge kinds, `avoid` names nodes a walk never covers, and `shadows` lists per file the modules its run never loads, so `affectedBy` leaves out a file every trail to which crosses one of its own shadows; `depends` adds the install — which package rests on which — so a dependency bump is a seed like any other and affects only the files that import it; `uses` names what each import binds and each re-export republishes, and `moved` lists per seed the exports its change changed, so `affectedBy` covers only the files that import one of them |
@@ -364,4 +382,4 @@ the comparison is refused rather than reported as a difference in the product.
364
382
 
365
383
  ---
366
384
 
367
- **[@variance-authority/core](https://variance-authority.dev/reference/packages/core)** is part of [Variance Authority](https://variance-authority.dev) — [documentation](https://variance-authority.dev/docs) · MIT
385
+ **[@variance-authority/core](https://variance-authority.dev/reference/packages/core)** is part of [Variance Authority](https://variance-authority.dev) — [documentation](https://variance-authority.dev/docs) · [GitHub](https://github.com/Variance-Authority/variance-authority/tree/main/packages/core) · MIT
@@ -49,7 +49,7 @@ export declare function hashComponents(snapshot: SemanticSnapshot): readonly Com
49
49
  * The arithmetic behind cause-first ranking, and the reason a baseline carries
50
50
  * its component hashes at all. `rankRegions` takes a list of cause names and,
51
51
  * given none, falls back to area — which
52
- * [journal 0013](../../../../docs/context/journal/0013-observability.md)
52
+ * [journal 0013](https://github.com/Variance-Authority/variance-authority/blob/main/docs/context/journal/0013-observability.md)
53
53
  * measured as backwards by 6×, because area measures displacement rather than
54
54
  * cause. This is where that list comes from on a path with no second document.
55
55
  *
@@ -81,12 +81,20 @@ export declare function causesBetween(before: readonly ComponentHash[], after: r
81
81
  * and a boolean cannot serve a route-level test, whose entire request is *tell
82
82
  * me when the page stops assembling and never when it is repainted*.
83
83
  *
84
- * The mapping is exact and it is the same one `bandOf` applies to a delta, which
85
- * is the property that matters: a subject relaxed to `layout` must absorb the
84
+ * The mapping is the same one `bandOf` applies to a delta, but for the one case
85
+ * below, and that is the property that matters: a subject relaxed to `layout` must absorb the
86
86
  * same things whether the run held two documents or two sidecars. Two mappings
87
87
  * would be one drift away from a config key meaning different things on the two
88
88
  * paths, discovered as a regression somebody let through.
89
89
  *
90
+ * One delta the digests cannot band as `bandOf` does: the style of an element
91
+ * its author replaced, which `bandOf` bands `geometry` with the tag change. Two
92
+ * hashes do not say the tag changed under the style, so the `style` digest still
93
+ * reads `token` beside it. The `structure` digest moves with the tag, so both
94
+ * paths report `geometry`, and the one level that asserts `token`, `strict`,
95
+ * asserts `geometry` too: the verdict agrees. What differs is the list a
96
+ * relaxed reason gives as absorbed, which names `token` on this path only.
97
+ *
90
98
  * | digest | band | what it covers |
91
99
  * |---|---|---|
92
100
  * | `semantics` | `a11y` | role, accessible name, ARIA state |
@@ -92,7 +92,7 @@ function byCodeUnit(a, b) {
92
92
  * The arithmetic behind cause-first ranking, and the reason a baseline carries
93
93
  * its component hashes at all. `rankRegions` takes a list of cause names and,
94
94
  * given none, falls back to area — which
95
- * [journal 0013](../../../../docs/context/journal/0013-observability.md)
95
+ * [journal 0013](https://github.com/Variance-Authority/variance-authority/blob/main/docs/context/journal/0013-observability.md)
96
96
  * measured as backwards by 6×, because area measures displacement rather than
97
97
  * cause. This is where that list comes from on a path with no second document.
98
98
  *
@@ -176,12 +176,20 @@ function ownContentMoved(before, after) {
176
176
  * and a boolean cannot serve a route-level test, whose entire request is *tell
177
177
  * me when the page stops assembling and never when it is repainted*.
178
178
  *
179
- * The mapping is exact and it is the same one `bandOf` applies to a delta, which
180
- * is the property that matters: a subject relaxed to `layout` must absorb the
179
+ * The mapping is the same one `bandOf` applies to a delta, but for the one case
180
+ * below, and that is the property that matters: a subject relaxed to `layout` must absorb the
181
181
  * same things whether the run held two documents or two sidecars. Two mappings
182
182
  * would be one drift away from a config key meaning different things on the two
183
183
  * paths, discovered as a regression somebody let through.
184
184
  *
185
+ * One delta the digests cannot band as `bandOf` does: the style of an element
186
+ * its author replaced, which `bandOf` bands `geometry` with the tag change. Two
187
+ * hashes do not say the tag changed under the style, so the `style` digest still
188
+ * reads `token` beside it. The `structure` digest moves with the tag, so both
189
+ * paths report `geometry`, and the one level that asserts `token`, `strict`,
190
+ * asserts `geometry` too: the verdict agrees. What differs is the list a
191
+ * relaxed reason gives as absorbed, which names `token` on this path only.
192
+ *
185
193
  * | digest | band | what it covers |
186
194
  * |---|---|---|
187
195
  * | `semantics` | `a11y` | role, accessible name, ARIA state |