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