@variance-authority/core 0.14.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 (147) hide show
  1. package/CHANGELOG.md +878 -0
  2. package/README.md +38 -17
  3. package/dist/attribute/component-hash.d.ts +14 -5
  4. package/dist/attribute/component-hash.js +14 -5
  5. package/dist/attribute/component-hash.js.map +1 -1
  6. package/dist/attribute/composition.d.ts +16 -0
  7. package/dist/attribute/composition.js +4 -6
  8. package/dist/attribute/composition.js.map +1 -1
  9. package/dist/attribute/index.d.ts +3 -1
  10. package/dist/attribute/index.js +2 -1
  11. package/dist/attribute/index.js.map +1 -1
  12. package/dist/attribute/movement.d.ts +1 -1
  13. package/dist/attribute/movement.js.map +1 -1
  14. package/dist/attribute/region.d.ts +47 -4
  15. package/dist/attribute/region.js +91 -22
  16. package/dist/attribute/region.js.map +1 -1
  17. package/dist/attribute/source.d.ts +10 -24
  18. package/dist/attribute/source.js +4 -38
  19. package/dist/attribute/source.js.map +1 -1
  20. package/dist/attribute/subject-pieces.d.ts +37 -0
  21. package/dist/attribute/subject-pieces.js +122 -0
  22. package/dist/attribute/subject-pieces.js.map +1 -0
  23. package/dist/compare/band.d.ts +8 -2
  24. package/dist/compare/band.js +7 -2
  25. package/dist/compare/band.js.map +1 -1
  26. package/dist/compare/diff/attribution.js +6 -4
  27. package/dist/compare/diff/attribution.js.map +1 -1
  28. package/dist/compare/diff/compare-nodes.js +9 -1
  29. package/dist/compare/diff/compare-nodes.js.map +1 -1
  30. package/dist/compare/diff/match.d.ts +2 -1
  31. package/dist/compare/diff/match.js +52 -0
  32. package/dist/compare/diff/match.js.map +1 -1
  33. package/dist/compare/parting.js +6 -4
  34. package/dist/compare/parting.js.map +1 -1
  35. package/dist/format/capture.d.ts +25 -0
  36. package/dist/format/capture.js +6 -0
  37. package/dist/format/capture.js.map +1 -1
  38. package/dist/format/document.d.ts +28 -5
  39. package/dist/format/document.js +8 -2
  40. package/dist/format/document.js.map +1 -1
  41. package/dist/format/drive.d.ts +191 -0
  42. package/dist/format/drive.js +343 -0
  43. package/dist/format/drive.js.map +1 -0
  44. package/dist/format/environment.d.ts +20 -2
  45. package/dist/format/environment.js +7 -0
  46. package/dist/format/environment.js.map +1 -1
  47. package/dist/format/index.d.ts +13 -3
  48. package/dist/format/index.js +8 -2
  49. package/dist/format/index.js.map +1 -1
  50. package/dist/format/intervention.d.ts +79 -0
  51. package/dist/format/intervention.js.map +1 -1
  52. package/dist/format/origin.d.ts +32 -0
  53. package/dist/format/origin.js +54 -0
  54. package/dist/format/origin.js.map +1 -0
  55. package/dist/format/packed.d.ts +32 -0
  56. package/dist/format/packed.js +23 -0
  57. package/dist/format/packed.js.map +1 -0
  58. package/dist/format/shape.d.ts +31 -0
  59. package/dist/format/shape.js +269 -0
  60. package/dist/format/shape.js.map +1 -0
  61. package/dist/format/snapshot.d.ts +2 -0
  62. package/dist/format/snapshot.js.map +1 -1
  63. package/dist/format/stabilize.d.ts +35 -6
  64. package/dist/format/stabilize.js +58 -103
  65. package/dist/format/stabilize.js.map +1 -1
  66. package/dist/format/steps.d.ts +44 -0
  67. package/dist/format/steps.js +67 -0
  68. package/dist/format/steps.js.map +1 -0
  69. package/dist/format/waits.d.ts +12 -0
  70. package/dist/format/waits.js +156 -0
  71. package/dist/format/waits.js.map +1 -0
  72. package/dist/format/width.d.ts +35 -0
  73. package/dist/format/width.js +38 -0
  74. package/dist/format/width.js.map +1 -0
  75. package/dist/index.d.ts +2 -1
  76. package/dist/index.js +2 -1
  77. package/dist/index.js.map +1 -1
  78. package/dist/judge/docket.js +1 -0
  79. package/dist/judge/docket.js.map +1 -1
  80. package/dist/judge/index.d.ts +1 -0
  81. package/dist/judge/index.js +1 -0
  82. package/dist/judge/index.js.map +1 -1
  83. package/dist/judge/restamp.d.ts +20 -0
  84. package/dist/judge/restamp.js +20 -0
  85. package/dist/judge/restamp.js.map +1 -0
  86. package/dist/nearest.d.ts +46 -0
  87. package/dist/nearest.js +102 -0
  88. package/dist/nearest.js.map +1 -0
  89. package/dist/relate/before.d.ts +19 -24
  90. package/dist/relate/before.js +37 -39
  91. package/dist/relate/before.js.map +1 -1
  92. package/dist/relate/beyond.d.ts +59 -0
  93. package/dist/relate/beyond.js +98 -0
  94. package/dist/relate/beyond.js.map +1 -0
  95. package/dist/relate/carried.d.ts +122 -0
  96. package/dist/relate/carried.js +279 -0
  97. package/dist/relate/carried.js.map +1 -0
  98. package/dist/relate/graph.d.ts +14 -12
  99. package/dist/relate/graph.js +12 -11
  100. package/dist/relate/graph.js.map +1 -1
  101. package/dist/relate/index.d.ts +3 -1
  102. package/dist/relate/index.js +3 -1
  103. package/dist/relate/index.js.map +1 -1
  104. package/dist/relate/merkle.js +2 -11
  105. package/dist/relate/merkle.js.map +1 -1
  106. package/dist/relate/narrow.d.ts +2 -2
  107. package/dist/relate/narrow.js +26 -73
  108. package/dist/relate/narrow.js.map +1 -1
  109. package/dist/relate/reach.d.ts +6 -6
  110. package/dist/relate/reach.js +10 -53
  111. package/dist/relate/reach.js.map +1 -1
  112. package/dist/relate/records.d.ts +6 -5
  113. package/dist/relate/records.js +3 -2
  114. package/dist/relate/records.js.map +1 -1
  115. package/dist/rules/index.d.ts +3 -3
  116. package/dist/rules/index.js +3 -3
  117. package/dist/rules/index.js.map +1 -1
  118. package/dist/rules/normalize/cascade.d.ts +12 -0
  119. package/dist/rules/normalize/cascade.js +34 -16
  120. package/dist/rules/normalize/cascade.js.map +1 -1
  121. package/dist/rules/normalize/fixture.d.ts +3 -0
  122. package/dist/rules/normalize/fixture.js +3 -1
  123. package/dist/rules/normalize/fixture.js.map +1 -1
  124. package/dist/rules/normalize/index.d.ts +2 -2
  125. package/dist/rules/normalize/index.js +9 -2
  126. package/dist/rules/normalize/index.js.map +1 -1
  127. package/dist/rules/normalize/value.d.ts +13 -2
  128. package/dist/rules/normalize/value.js +42 -8
  129. package/dist/rules/normalize/value.js.map +1 -1
  130. package/dist/rules/normalize/wrapper.js +5 -0
  131. package/dist/rules/normalize/wrapper.js.map +1 -1
  132. package/dist/rules/ruleset.d.ts +24 -4
  133. package/dist/rules/ruleset.js +24 -4
  134. package/dist/rules/ruleset.js.map +1 -1
  135. package/dist/segment/index.d.ts +20 -4
  136. package/dist/segment/index.js +28 -7
  137. package/dist/segment/index.js.map +1 -1
  138. package/dist/shard/index.d.ts +11 -0
  139. package/dist/shard/index.js +11 -0
  140. package/dist/shard/index.js.map +1 -0
  141. package/dist/shard/placement.d.ts +119 -0
  142. package/dist/shard/placement.js +156 -0
  143. package/dist/shard/placement.js.map +1 -0
  144. package/dist/share/index.d.ts +1 -1
  145. package/dist/share/index.js +1 -1
  146. package/dist/share/index.js.map +1 -1
  147. package/package.json +8 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,883 @@
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
+
427
+ ## 0.15.0
428
+
429
+ ### Minor Changes
430
+
431
+ - 8dbc834: The components a module declares are read from its code. A declaration written
432
+ in a block comment, a JSDoc example or a template literal no longer names a
433
+ component, so a commented-out `function Retired()` no longer makes its file
434
+ the one that declares `Retired`. A name is declared by a statement of the
435
+ module itself: a function, class or binding inside a function body belongs to
436
+ that function and no longer counts. An `export async function Page()`, an
437
+ abstract class, a generator, a second name in `const A = 1, B = 2`, and a name
438
+ that starts with a non-ASCII capital or carries a `$` are now declared too.
439
+
440
+ `variance run`, `variance collect`, the Storybook collector and the route
441
+ collector resolve a component to its `file:line` from the same reading, through
442
+ `indexDeclarations`, which `sense` now exports. What that changes in the index
443
+ they build from `source.dirs`:
444
+
445
+ - The line is the first line of the declaring statement. A decorated class
446
+ sits at its first decorator, or at its `export` when the decorator is written
447
+ above the `export`, and a second name in a multi-line `const` at the line of
448
+ the `const`.
449
+ - `.d.ts` files, and test, spec and story files your own `exclude` lets
450
+ through, declare nothing.
451
+ - A file the parser cannot read declares nothing, where the text scan found
452
+ the components in it. That covers a Flow-annotated `.js` file, and a `.vue`,
453
+ `.svelte` or `.mdx` file you add to a collector's `extensions`, which is
454
+ parsed as TSX and fails. The index built from `source.dirs` or a collector's
455
+ `source` does not say so: the file's components are missing, and nothing
456
+ names the file. Only `sense`'s source index, for a file it records, names the
457
+ parse error in that file's record.
458
+ - Building the index needs `sense`'s native addon, which ships for macOS on
459
+ arm64, Linux on x64 and arm64 with glibc, and Windows on x64. On any other
460
+ machine, including an Intel Mac, an Alpine image and Windows on arm64, a
461
+ configured `source.dirs` fails at its first file and names why the addon did
462
+ not load, where the text scan ran anywhere. The route collector depends on
463
+ `sense` for it.
464
+
465
+ `indexSource` is removed from `@variance-authority/core/attribute`. Build a
466
+ `SourceIndex` with `indexDeclarations` from `@variance-authority/sense`, or
467
+ write one as plain data: a map from component name to `{ file, line, via }`.
468
+ `SourceRef['via']` loses `'declared'`, which nothing produced, and
469
+ `readCapture` in `@variance-authority/unit-test` refuses a capture whose source
470
+ index carries it.
471
+
472
+ The source index format moves to version 18, so an index written before this
473
+ release is rebuilt once instead of keeping the names it read from comments.
474
+ - 6755276: `variance distill --file <path>` lists each module no case entered under the
475
+ import that made the test file load it: the topmost import every path from the
476
+ test file to the module runs through, with nothing behind it that a case
477
+ entered, often in a module the test used rather than in the test file. A module
478
+ two imports reach is listed as shared, with the nearest file every path to it
479
+ runs through; one no static import reaches, such as a dynamic import's, is
480
+ listed apart. The file graph is read from the checkout only when the reading
481
+ has a module no case entered. A module the file mocked with a factory was never
482
+ evaluated, and its import is not walked.
483
+
484
+ `@variance-authority/core/relate` exports `dominatorsOf`, each node's immediate
485
+ dominator from a root. `distillFile` takes an optional `imports` lookup and
486
+ sets `cause` on each module no case entered; `@variance-authority/distill`
487
+ exports `LoadCause`.
488
+ - b065364: `::before` and `::after` are read as children of the element they hang from
489
+
490
+ Restyling or rewording a `::before` or `::after` box used to repaint the page
491
+ and move no hash: the rules written for it were matched against the element,
492
+ failed, and were dropped from both the snapshot and the render document. A
493
+ generated box now enters the capture as a child node tagged `::before` (first)
494
+ or `::after` (last), carrying the rules written for it. In a browser it carries
495
+ its computed style, projected onto the allowlist, and the text its `content`
496
+ says as a `#text` child; a box whose `content` is `none` or `normal`, or whose
497
+ `display` is `none`, is not in the tree. Strings are joined and `attr()` reads
498
+ the element's attribute; a part such as `counter()` or `open-quote` is carried
499
+ as written.
500
+
501
+ Under JSDOM, which computes no generated content, the declared rules answer,
502
+ picked by the same cascade order normalization uses: a box is in the tree when
503
+ the winning `content` gives it one and the winning `display` is not `none`, with the rules that style it and no computed style.
504
+ When that `content` is strings and `attr()`, the box says its words as a
505
+ `#text` child, as in a browser, so rewording it moves the snapshot. When it
506
+ holds a `counter()`, a quote, a `var()` or a `url()`, the box has no text and
507
+ carries `unread` on `RawNode` and `SemanticNode`, naming the parts it could not
508
+ resolve; `unread` is outside every hash. JSDOM's own parser drops a `content`
509
+ that is a single `attr()` or `counter()` and nothing else, so no box is read
510
+ for one.
511
+
512
+ This is part of `RULESET_VERSION` `r2`, the same `r2` that keeps the served
513
+ port out of a render. A subject with a `::before` or `::after` box has a new
514
+ render document, so after this upgrade its image shows the box and reads
515
+ `changed`; `variance accept --all` and Playwright's
516
+ `--update-snapshots=changed` adopt it.
517
+ - 1a0b845: A subject is read with nothing hovered, nothing focused, motion reduced and `Date` fixed
518
+
519
+ Every layout and raster baseline made before this release is compared under a
520
+ moved recipe once; a semantic baseline, read in jsdom, does not move. The
521
+ default recipes gained tricks, so the stabilization digest every reading records
522
+ has changed, and each subject names that recipe as the cause of whatever it
523
+ reports. Review the changes and adopt them with `variance accept --all`, as for
524
+ any recipe change.
525
+
526
+ The standard collection recipe now also holds:
527
+
528
+ - `no-hover`: the pointer is moved to `(-1, -1)`, off the page, once per page
529
+ before it loads. On the `variance` fixture's page it is moved again before
530
+ every observation, a frame each, and not put back: a menu a script opened on
531
+ hover is closed for your test's next step.
532
+ - `no-focus`: whatever holds focus is blurred before the subject is read,
533
+ including an element inside a shadow root. On a page your test or agent goes
534
+ on using, focus goes back to that element once the reading and any screenshot
535
+ of it are taken, so a test types where it left off. The blur still ran the
536
+ page's blur handlers, and what they did, such as a validate-on-blur error,
537
+ stays.
538
+ - `reduced-motion`: the browser reports `prefers-reduced-motion: reduce`, which
539
+ stops the script animations of a page that honours it, and the reading
540
+ declares the same preference among the conditions its `@media` rules are
541
+ resolved against. A `prefers-reduced-motion` condition you declare through
542
+ `createHarness`'s `features` wins, and the browser is asked for that value;
543
+ a declared `no-preference` is not recorded as `reduced-motion`. Rules under
544
+ `@media (prefers-reduced-motion: no-preference)` are not read by default;
545
+ leave `reduced-motion` out to read them.
546
+ - `freeze-clock`: `Date` reads one instant. A script added before the page's
547
+ own replaces `new Date()`, `Date()` and `Date.now()`, and gives the same
548
+ instant to `Intl.DateTimeFormat`'s `format()` and `formatToParts()` called
549
+ without a date. Timers, animation frames and `performance.now()` are the
550
+ browser's own, so a page that settles on a timer still settles. No Playwright
551
+ clock is installed, so `page.clock` stays yours, and a clock your test
552
+ installs replaces the fixed `Date`; the reading still records `freeze-clock`,
553
+ so leave it out of the list when your test owns the clock. `Temporal.Now` and
554
+ `Date` inside a worker are not fixed. The default instant is in the past, and
555
+ the browser's cookie store keeps real time: a cookie your page's script sets
556
+ with an expiry counted from `Date.now()` is already expired and is not
557
+ stored, so a dismissed consent banner can come back after the next
558
+ navigation. Set `varianceNow` or `now` near today, or leave `freeze-clock`
559
+ out.
560
+
561
+ The render and layout recipes gain `no-focus` alone, because a painted document
562
+ still honours a collected `autofocus`. The other three have nothing to act on
563
+ there: the date the page printed is already text in the document, and its
564
+ `@media` rules were resolved when it was read. A `<script>` inside a subject
565
+ root is carried into the render document and runs there under the real clock.
566
+ Every document is painted under the render recipe, so a reading taken without
567
+ `no-focus` is still painted blurred.
568
+
569
+ The instant defaults to `DEFAULT_NOW`, `2024-06-15T12:00:00.000Z`, exported
570
+ from `@variance-authority/core/format`. `routeCollector` and
571
+ `storybookCollector` take a `now` option to choose another. It must be an ISO
572
+ 8601 instant with its zone; anything else is refused before a browser opens.
573
+ An instant other than the default is spelled in the clock's id,
574
+ `freeze-clock@<instant>`, so it is part of the environment key, and a baseline taken at one instant is compared
575
+ with a run at another with the moved instant named as the cause.
576
+
577
+ To read a subject hovered, focused, animated or at the real time, pass your own
578
+ `stabilize` list without that trick: `DEFAULT_STABILIZE`, the ids of the
579
+ default recipe, filtered, as in
580
+ `DEFAULT_STABILIZE.filter((id) => id !== 'freeze-clock')`. It is exported from
581
+ `@variance-authority/core/format`, `@variance-authority/route-collector`,
582
+ `@variance-authority/storybook-collector` and
583
+ `@variance-authority/playwright-test`. `storybookCollector` and `observe` in
584
+ `@variance-authority/playwright-test` now take `stabilize`, as `routeCollector`
585
+ already did.
586
+
587
+ A trick needs a browser driver to apply it, and is recorded only where one did.
588
+ The collectors apply all four. So does the `variance` fixture in
589
+ `@variance-authority/playwright-test`: it sets `reduced-motion` and
590
+ `freeze-clock` on the `page` it hands your test, before the test body runs, and
591
+ takes the pointer off the page and releases focus before every observation,
592
+ giving focus back after it. Two new fixture options, `varianceStabilize` and
593
+ `varianceNow`, choose what it sets and the instant, and are set with
594
+ `test.use`; `varianceDriven` holds the ids it set. A fixed `Date` stops code
595
+ that times itself with `Date.now()`: a debounce or a throttle, such as lodash's
596
+ `debounce`, sees no time pass and never fires, so a test that types into a
597
+ debounced search box can stop getting results after this upgrade. A loop that
598
+ waits for `Date.now()` to pass a mark hangs the page, an animation timed by
599
+ `Date`, such as jQuery's `animate`, never calls its `complete`, and a client
600
+ that checks a token's `nbf` or `iat` against `Date.now()` can refuse a token
601
+ issued today. None of these failures names the clock. Leave
602
+ `freeze-clock` out for such a test, and for one that sets `page.clock` itself:
603
+
604
+ ```ts
605
+ import { DEFAULT_STABILIZE } from '@variance-authority/playwright-test';
606
+
607
+ test.use({ varianceStabilize: DEFAULT_STABILIZE.filter((id) => id !== 'freeze-clock') });
608
+ ```
609
+
610
+ On a page the fixture never handed your test,
611
+ such as one opened from your own browser context, call the new
612
+ `driveBeforeTest(page)` before `page.goto()` to set the same two; `observe` and
613
+ `createVariance` there apply `no-hover` and `no-focus`.
614
+ `@variance-authority/vitest-browser` reads from inside your page and has no
615
+ driver; `@variance-authority/presentation` reads through the Playwright `Page`
616
+ you hand it, a test's or an agent's live browser, and does not drive it. Both
617
+ apply `no-focus` only, giving focus back once the reading is taken. A trick left out is absent from the
618
+ reading's ids and digest, as a trick a tier cannot reach already is.
619
+ `stabilizeForObservation` in `@variance-authority/dom` takes `driven`, the ids
620
+ your driver applied, and returns `refocus()`, which gives focus back to the
621
+ element the reading released. `stabilizeAsRequested` takes the recipe by id, as
622
+ a page agent receives it. `createHarness` in `@variance-authority/playwright`
623
+ takes a `stabilize` recipe to drive its page before the first navigation, and
624
+ reports the ids it applied as `driven`. `createObservedHarness` opens one with
625
+ `observeNetwork` started before its first navigation, as both collectors do.
626
+
627
+ `createPlaywrightRenderer` no longer couples `no-focus` to `waitForFonts`:
628
+ `waitForFonts: false` turns off the waits alone.
629
+
630
+ `@variance-authority/core/format` exports `DEFAULT_STABILIZE`, `DEFAULT_NOW`,
631
+ `driveBeforeLoad` and `driveBeforeRead`, which apply the driver tricks to a
632
+ Playwright `Page`, `forDriver`, which drops the ones a host did not apply,
633
+ `featuresOf`, which declares the motion preference a recipe drove, and
634
+ `recipeToDrive`, which resolves a `stabilize` list and checks `now` before a
635
+ browser opens.
636
+ - d432339: A page reads its clock in UTC and formats for en-US, not for the host
637
+
638
+ The harness the route and Storybook collectors open, and the pages the renderer
639
+ paints, took their timezone and locale from the machine. A date the page
640
+ formatted read one way on a laptop in Sydney and another on a runner in
641
+ Virginia, from one build, and the two compared against one baseline as
642
+ `changed`.
643
+
644
+ Both are pinned: `timezoneId` to `UTC` and `locale` to `en-US`. Declare either
645
+ in `variance.config.json` (`"timezoneId": "Europe/Berlin"`, `"locale":
646
+ "de-DE"`) and the collectors and the renderer pin that instead; an unknown
647
+ timezone or a malformed language tag is refused by its field.
648
+ `createHarness` and `createPlaywrightRenderer` take the same two options.
649
+ `@variance-authority/core/format` exports their type, `PageRegion`, and
650
+ `@variance-authority/playwright` exports `pinnedRegion`, which fills in `UTC`
651
+ and `en-US` for a field left out.
652
+
653
+ Both are part of the environment key, and the renderer's pair is part of
654
+ `RenderIdentity.rasterization`. A capture read in another timezone or language
655
+ is another environment, and an image painted in one is compared with a
656
+ baseline painted in another, with `rasterization` named as the cause of
657
+ whatever moved.
658
+
659
+ `@variance-authority/playwright-test` paints each test in the timezone and
660
+ locale its page runs in: whatever the Playwright project or `test.use` sets.
661
+ When neither sets a `timezoneId`, `varianceFixtures` opens the page in `UTC`
662
+ rather than the host's, so a laptop and a runner in UTC read it under one key;
663
+ Playwright Test already opens it in `en-US` when nothing sets a `locale`. That
664
+ reaches every test of a suite extended with `varianceFixtures`, including tests
665
+ that never call `variance`, so a test that asserts a date formatted in your
666
+ machine's timezone reads it in `UTC` after this upgrade; set `timezoneId` in
667
+ `use` to keep your own. A page
668
+ you hand to `createVariance` is read and painted in whatever its own context
669
+ opened it in, which is the host's timezone when that context set none: open the
670
+ context with `timezoneId: 'UTC'` to match. `varianceRenderer` is now a test fixture, chosen
671
+ by the page, and `varianceRenderers` is the worker's set of renderers, one per
672
+ timezone and locale; a project that overrode `varianceRenderer` at worker scope
673
+ overrides it at test scope instead.
674
+
675
+ **Every image painted by the Playwright renderer is compared under a moved
676
+ recipe once after this upgrade**, because its recipe now carries the timezone
677
+ and locale. Each subject names `rasterization` as the cause of what it reports,
678
+ and one whose pixels moved is `changed`:
679
+
680
+ - With `variance run`: the run exits `1` for each `changed` subject. Review
681
+ them in `variance report --format html` and adopt them with
682
+ `variance accept --all`. A rerun reports `unchanged`.
683
+ - With `@variance-authority/playwright-test`: the suite fails each `changed`
684
+ test, and `--update-snapshots=changed` adopts them.
685
+ - With `@variance-authority/vitest-browser`: `vitest --update` adopts them, as
686
+ `variance accept --all` does.
687
+
688
+ `@variance-authority/vitest-browser` reads no `variance.config.json` and takes no
689
+ timezone or locale, so it paints in `UTC` and `en-US` whatever its tab ran in,
690
+ and neither it nor `@variance-authority/unit-test` writes a timezone or locale
691
+ to the environment key.
692
+ - 14aefbf: Ask what one test, or one story, checks that the narrower ones inside it do
693
+ not.
694
+
695
+ - `variance ask test-composition --file <test> --name <words>`, and
696
+ `docs_test_composition`, read one recorded test as the smaller tests whose
697
+ regions sit inside its own, the larger tests holding it, and the regions no
698
+ smaller test entered: modules only it enters, and paths through a smaller
699
+ test's module only it takes.
700
+ - `variance_composition {subject}` closes with the same reading for a story:
701
+ the smaller stories inside it, the larger ones holding it, and what no
702
+ smaller story renders — components only it mounts, and components a smaller
703
+ story renders another way. The run carries it per subject in the report's
704
+ structure section; `piecesOf` in `@variance-authority/core/attribute`
705
+ computes it.
706
+ - A component mounted by more than half the suite's subjects is structure. A
707
+ subject that mounts a component many times now counts once, where every
708
+ mount used to count: a chip story is an example of the chip again when the
709
+ pages around it mount chips many times over.
710
+ - f1c1b64: A bumped package and a moved manifest are read as the repository files they
711
+ change, once, before anything else is asked. `beyondReach` in
712
+ `@variance-authority/core/relate` walks a bump through the install to the first
713
+ files whose runtime imports load it, and stops there: a test that entered
714
+ anything further along evaluated that file on the way. A moved manifest becomes
715
+ every file beside it. `variance select`, `variance reach` and `variance run
716
+ --since` hand those files on as changed whole, so the suite's `before` and the
717
+ record answer for them as they do for an edited file.
718
+
719
+ `before` now holds files alone. A bump of a package your setup imports runs the
720
+ whole suite and names the setup file, not the package. `changedBefore` takes the
721
+ changed files only, and `BeforeReach.packages` is gone.
722
+
723
+ The `packages` option of `narrowByJourneys`, `narrowByExecution` and
724
+ `selectJourneyFile` is removed: pass the files `beyondReach` returns, each with
725
+ no line ranges.
726
+ - a4f55c1: One walk carries a change to every file that loads it
727
+
728
+ `@variance-authority/core/relate` exports `carried`, the walk against the
729
+ arrows that `dependentsOf`, its narrowed form and test selection's two importer
730
+ walks now all run. You pass seeds, each with what its change carries — the
731
+ whole file, or the names that moved — and a step that says what one importer
732
+ does with what reached it. The walk owns the worklist, enters each file once
733
+ per name, ends on any cycle, and treats a step with no answer as the whole
734
+ file. A walk you write over the graph gets the same answers on cycles,
735
+ self-imports and re-exports as the ones the selector runs, with no loop of your
736
+ own.
737
+ - bd98c3a: `--shard` in Vitest and Jest is placed by recorded time, and `variance shards` says how many to start
738
+
739
+ A Vitest or Jest config wrapped by `withTestSelection` now places the files of
740
+ `--shard k/n` by the time each took on the last recorded run, rather than
741
+ leaving the runner to divide them by count. Each shard computes the whole
742
+ placement from the same record and keeps its own part: files go longest first
743
+ to the shard with the least time so far, and a file the record has not seen is
744
+ priced at the median of the ones it has. The run says on stderr which record
745
+ placed it and what each shard was given:
746
+
747
+ ```text
748
+ variance-authority: shard 1/2 by the times recorded at 8e3b0f0e9028: 1 of 5 files, 9.0 s (shards 400 ms to 9.0 s)
749
+ ```
750
+
751
+ When the times cannot be read, the runner's own split runs instead, and the
752
+ line says why. A failed read never fails the run. A Jest config that names no
753
+ `testSequencer` finds Jest's own through Jest, so a layout that does not hoist
754
+ `@jest/test-sequencer`, as pnpm does not, runs as before. Under
755
+ `VARIANCE_AUTHORITY_SINCE`, only the files the selection keeps are placed.
756
+
757
+ `variance shards --setup <seconds>` prints how many shards a recorded suite is
758
+ worth, and with `--format json` a `matrix` of `k/n` strings for a CI job to
759
+ start. Every shard is charged its setup, so a shard is added only when it
760
+ shortens the wait by more than it spends, and the count stops at the slowest
761
+ test file, which the answer names with its slowest case. `--budget` asks for
762
+ the fewest shards that finish within it, `--workers` divides a shard's share
763
+ among its runner's workers, and `--since` leaves out what the change lets the
764
+ run skip, so a change that reaches no test answers `0 shards`. `--collected
765
+ <file>`, the runner's list of test files, counts a test file the record has not
766
+ seen, priced at the median.
767
+
768
+ It always answers, and names what it could not weigh, in `notes` under
769
+ `--format json`. Without `--setup` the count weighs no setup. A budget no
770
+ longer than the setup is out of reach at any count, and the answer is the count
771
+ that finishes soonest, with `why` set to `setup over budget`. `--since` without
772
+ `--collected` counts only recorded files and never answers fewer than one
773
+ shard, and `--at-distance` without `--since` counts the whole suite. With
774
+ nothing recorded it answers one shard until `--unrecorded <n>` names the count
775
+ to start.
776
+
777
+ `@variance-authority/core/shard` is the placement and the count, which
778
+ `variance run --shard` places stories and routes by too.
779
+ `@variance-authority/sense` exports `recordedTimes` and `fileCases`, and
780
+ `@variance-authority/cli` exports `suiteTimes`.
781
+ - af6b022: `before` moves out of `source` and is declared where the suite is. The top-level
782
+ `before` is what every suite rests on — a CI workflow, a `.nvmrc` — and a
783
+ suite's own `before`, beside its `kind`, is its runner config and its setup
784
+ files, each listed, since a config names its setup as a string and a string is
785
+ not an import. `variance select` now reads both for the suite it reads: each
786
+ entry is walked down the file graph, and a change to any file it reaches, a
787
+ `package.json` whose `exports`, `main` or `type` moved over one of those files,
788
+ or a bump of a package those files import, runs the whole suite and names what
789
+ moved. A directory in either list is walked from every file under it. `select
790
+ --execution` reads the same lists for the suite `--suite` names, the only one
791
+ declared, or every declared suite when none is named; beside a snapshot
792
+ `--execution`, `--suite` is refused, since both name the record. A manifest move
793
+ is read when the diff leaves every lockfile alone. A
794
+ config below the repository root inherits the root's `before`, as it inherits
795
+ its suites. A suite that declares nothing has nothing before its reach, and
796
+ `select` says so.
797
+ `variance run --since` reads the top-level list as it read `source.before`.
798
+
799
+ `source.before` is refused: move it to the top level.
800
+ - 754d04c: The port a build is served on is no part of a render
801
+
802
+ The route and Storybook collectors serve a build on a free port, and that port
803
+ reached three identities. An asset the page loaded was keyed by its absolute
804
+ URL, a `url()` in a computed style was the absolute URL the engine resolved,
805
+ and the document digest read both. One build served twice therefore read as two
806
+ environment keys and two documents: each run missed the render cache, painted
807
+ again, and read its document as changed against its own baseline.
808
+
809
+ An asset the page's own origin serves is keyed by its path, and the page's
810
+ origin is dropped from a `url()` before it is hashed. An asset another origin
811
+ serves keeps its absolute URL. A capture carries the page's `baseUrl` so core
812
+ knows which origin is the page's, and the renderer resolves a path-keyed asset
813
+ against the document's `baseUrl`. `documentDigest` reads the page's own base
814
+ and resources as paths, so one build served on two ports addresses one render
815
+ and the render cache hits; the document keeps them absolute for the renderer,
816
+ and a resource another origin serves stays absolute in the digest too.
817
+
818
+ A `@variance-authority/unit-test` capture keys its assets the same way, so a
819
+ `@variance-authority/vitest-browser` observation, which captures through it on
820
+ the port the runner had free, keeps the port out of its environment key.
821
+ `readCapture` reads back a capture whose asset is keyed by path, resolving the
822
+ key against the document's `baseUrl` as the renderer does; `assetUrl` in
823
+ `@variance-authority/core/format` is that one resolution.
824
+
825
+ **The document of every page served over `http` moves once after this
826
+ upgrade.** `documentDigest` now reads a document's base and resources without
827
+ the page's origin, so the digest of every document served over `http` or
828
+ `https` changes: every route, every story, every Vitest browser page, and every
829
+ Playwright page opened by URL. A moved document misses the render cache once,
830
+ and its image is still compared with the baseline: a page whose pixels did not
831
+ move reads `unchanged`, and one whose pixels did reads `changed`, which
832
+ `variance accept --all` and Playwright's `--update-snapshots=changed` adopt.
833
+ `RULESET_VERSION` moves from `r1` to `r2`. The ruleset is part of a snapshot's
834
+ environment key, not of the render recipe, so no image names it as a moved
835
+ identity; a snapshot diffed against one recorded under `r1` names the ruleset as its
836
+ environment root.
837
+ - 8187fb1: `didYouMean` and `nearest` are exported from `@variance-authority/core` and no
838
+ longer from `@variance-authority/mcp/tools`. Import them from
839
+ `@variance-authority/core`. A mistyped command or flag is refused, with the
840
+ nearest right one, without loading the MCP server's tools.
841
+ - ca7813e: `intersectionArea` is exported from `@variance-authority/core/format`
842
+
843
+ The area two `Rect`s share, zero when they only touch or do not meet. It sits
844
+ beside `Rect`, where the attribution already used it, so the review service can
845
+ ask whether two builds changed the same place.
846
+
847
+ ### Patch Changes
848
+
849
+ - 85e1a40: `wait-for-fonts` gives up after 15 seconds and names the font
850
+
851
+ A web font whose request never answered kept `document.fonts.ready` pending, and
852
+ the capture waited forever. `wait-for-fonts` now waits at most 15 seconds, the
853
+ same bound as `wait-for-images`, then fails naming every font still loading:
854
+ its family, and the `src` that requested it when the stylesheet declaring it is
855
+ same-origin. A face declared in a cross-origin stylesheet is named by family
856
+ alone, because the page cannot read that sheet's rules.
857
+ - cb8a52f: A source index another release wrote is no longer reported as damaged
858
+
859
+ After an upgrade, the first command that read the source index said it was
860
+ damaged and read only up to its first bad segment, and in CI it refused with
861
+ the same words. The segments were whole: an earlier release had written them in
862
+ another format version. Now the reader names both versions, for example that
863
+ the index was written in format version 15 and this release reads version 18.
864
+ On a workstation it rebuilds the index and says so, and `variance index` prints
865
+ `source index rebuilt over one written in format version 15`. In CI the refusal
866
+ names both versions and asks you to run `variance index` with this release
867
+ before the command that reads it. `ask orient --files` over such an index says
868
+ which version wrote it and that `variance index` rebuilds it.
869
+ - ef496ad: READMEs name what the program does
870
+
871
+ The package READMEs, and the `@variance-authority/vantage` and
872
+ `@variance-authority/playwright-test` descriptions, no longer write a test, a
873
+ record or a run as something that says, asks or knows. Each sentence names what
874
+ the program does: a command prints, a record holds a field, a test runs or
875
+ covers. Where a value is filled in from configuration or a default rather than
876
+ recorded, the README says so. Four renamed headings change their anchors:
877
+ `help`'s "Where the declaration is undocumented", `playwright`'s "Where a
878
+ component is declared, read from the engine", `storybook`'s "What a pass sends
879
+ the preview" and `sense`'s "Correct what a file's text declares it imports".
880
+
3
881
  ## 0.14.0
4
882
 
5
883
  Lockstep release — nothing in this package changed. Every `@variance-authority/*` package shares one version.