@variance-authority/core 0.1.1 → 0.3.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 (96) hide show
  1. package/CHANGELOG.md +245 -10
  2. package/README.md +279 -110
  3. package/dist/attribute/call-site.d.ts +20 -2
  4. package/dist/attribute/call-site.js +17 -20
  5. package/dist/attribute/call-site.js.map +1 -1
  6. package/dist/attribute/closure.d.ts +105 -0
  7. package/dist/attribute/closure.js +180 -0
  8. package/dist/attribute/closure.js.map +1 -0
  9. package/dist/attribute/composition.d.ts +5 -3
  10. package/dist/attribute/composition.js +67 -11
  11. package/dist/attribute/composition.js.map +1 -1
  12. package/dist/attribute/divergence.js +7 -1
  13. package/dist/attribute/divergence.js.map +1 -1
  14. package/dist/attribute/index.d.ts +7 -1
  15. package/dist/attribute/index.js +4 -1
  16. package/dist/attribute/index.js.map +1 -1
  17. package/dist/attribute/landmark.d.ts +97 -0
  18. package/dist/attribute/landmark.js +144 -0
  19. package/dist/attribute/landmark.js.map +1 -0
  20. package/dist/attribute/lexicon.d.ts +112 -0
  21. package/dist/attribute/lexicon.js +210 -0
  22. package/dist/attribute/lexicon.js.map +1 -0
  23. package/dist/attribute/source.d.ts +19 -2
  24. package/dist/attribute/source.js +12 -0
  25. package/dist/attribute/source.js.map +1 -1
  26. package/dist/attribute/stack.d.ts +62 -1
  27. package/dist/attribute/stack.js +116 -5
  28. package/dist/attribute/stack.js.map +1 -1
  29. package/dist/compare/derive.d.ts +17 -0
  30. package/dist/compare/derive.js.map +1 -1
  31. package/dist/compare/diff/index.d.ts +25 -1
  32. package/dist/compare/diff/index.js +8 -5
  33. package/dist/compare/diff/index.js.map +1 -1
  34. package/dist/compare/explain.d.ts +1 -1
  35. package/dist/compare/explain.js +15 -8
  36. package/dist/compare/explain.js.map +1 -1
  37. package/dist/compare/index.d.ts +2 -2
  38. package/dist/compare/index.js +1 -1
  39. package/dist/compare/index.js.map +1 -1
  40. package/dist/compare/observability.d.ts +25 -1
  41. package/dist/compare/observability.js +54 -0
  42. package/dist/compare/observability.js.map +1 -1
  43. package/dist/compare/parting.d.ts +24 -2
  44. package/dist/compare/parting.js +14 -9
  45. package/dist/compare/parting.js.map +1 -1
  46. package/dist/compare/slice.d.ts +15 -2
  47. package/dist/compare/slice.js +6 -2
  48. package/dist/compare/slice.js.map +1 -1
  49. package/dist/format/capture.d.ts +15 -0
  50. package/dist/format/capture.js +30 -1
  51. package/dist/format/capture.js.map +1 -1
  52. package/dist/format/document.d.ts +65 -5
  53. package/dist/format/document.js +34 -0
  54. package/dist/format/document.js.map +1 -1
  55. package/dist/format/hash.d.ts +26 -0
  56. package/dist/format/hash.js +41 -0
  57. package/dist/format/hash.js.map +1 -1
  58. package/dist/format/index.d.ts +4 -3
  59. package/dist/format/index.js +3 -2
  60. package/dist/format/index.js.map +1 -1
  61. package/dist/format/sha256.d.ts +10 -2
  62. package/dist/format/sha256.js +42 -9
  63. package/dist/format/sha256.js.map +1 -1
  64. package/dist/format/stabilize.d.ts +15 -0
  65. package/dist/format/stabilize.js +22 -1
  66. package/dist/format/stabilize.js.map +1 -1
  67. package/dist/index.d.ts +3 -11
  68. package/dist/index.js +3 -11
  69. package/dist/index.js.map +1 -1
  70. package/dist/judge/inspect.js +57 -4
  71. package/dist/judge/inspect.js.map +1 -1
  72. package/dist/relate/before.d.ts +137 -0
  73. package/dist/relate/before.js +155 -0
  74. package/dist/relate/before.js.map +1 -0
  75. package/dist/relate/graph.d.ts +53 -16
  76. package/dist/relate/graph.js +33 -15
  77. package/dist/relate/graph.js.map +1 -1
  78. package/dist/relate/index.d.ts +3 -2
  79. package/dist/relate/index.js +2 -1
  80. package/dist/relate/index.js.map +1 -1
  81. package/dist/relate/merkle.d.ts +14 -5
  82. package/dist/relate/merkle.js +14 -6
  83. package/dist/relate/merkle.js.map +1 -1
  84. package/dist/relate/reach.d.ts +19 -5
  85. package/dist/relate/reach.js +10 -8
  86. package/dist/relate/reach.js.map +1 -1
  87. package/dist/relate/records.d.ts +95 -4
  88. package/dist/relate/records.js +93 -13
  89. package/dist/relate/records.js.map +1 -1
  90. package/dist/segment/index.d.ts +123 -0
  91. package/dist/segment/index.js +265 -0
  92. package/dist/segment/index.js.map +1 -0
  93. package/dist/share/index.d.ts +148 -0
  94. package/dist/share/index.js +190 -0
  95. package/dist/share/index.js.map +1 -0
  96. package/package.json +10 -2
package/README.md CHANGED
@@ -1,38 +1,153 @@
1
- <p align="center"><img src="./mark.svg" alt="Variance Authority mark" width="72"></p>
1
+ <p align="center"><img src="https://variance-authority.dev/mark.svg" alt="Variance Authority mark" width="72"></p>
2
2
 
3
3
  # @variance-authority/core
4
4
 
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
- A **subject** is whatever is under test — a component, a page, or a plain
8
- value. A **capture** is the raw material a collector records from it once,
9
- before anything is compared. Collectors extract captures; this package
10
- normalizes and adjudicates them, and never captures anything itself.
7
+ Part of [Variance Authority](https://variance-authority.dev).
8
+
9
+ ## What this is for
10
+
11
+ You already have a reading of something — a DOM recorded by a collector, a
12
+ rendered document, a per-pixel diff mask, or a plain JSON value — and you need
13
+ to know what changed in it, which component and file the change lands on, and
14
+ whether anyone should mind. This package does that part and only that part. It
15
+ opens no DOM, reads no files, makes no network calls, and has no runtime
16
+ dependencies: what you hand it is everything it sees.
17
+
18
+ A **subject** is one named UI state you asked for and can ask for again — a
19
+ component in a given state, a page at a given route, or a plain value. A
20
+ **capture** is the raw material a collector records from a subject once, before
21
+ anything is compared. Collectors produce captures; this package normalizes and
22
+ adjudicates them, and captures nothing itself.
23
+
24
+ ## Requirements
25
+
26
+ Node 22 or newer, and ESM. There is no CommonJS build, so `require()` will not
27
+ load it. No runtime dependencies — every input arrives from a package you
28
+ install separately, named at each example below.
29
+
30
+ ## Install, and compare a value
31
+
32
+ The shortest complete path needs no browser and nothing else installed.
11
33
 
12
34
  ```bash
13
35
  npm install --save-dev @variance-authority/core
14
36
  ```
37
+
38
+ ```ts
39
+ import { shapeValue } from '@variance-authority/core/format';
40
+ import { compareValues } from '@variance-authority/core/compare';
41
+
42
+ const shaping = { arrayKey: { '/rows': 'id' } };
43
+
44
+ const baseline = shapeValue(
45
+ { rows: [{ id: 'checkout', total: 10 }, { id: 'cart', total: 4 }] },
46
+ shaping,
47
+ );
48
+ const candidate = shapeValue(
49
+ { rows: [{ id: 'checkout', total: 12 }, { id: 'cart', total: 4 }, { id: 'search', total: 1 }] },
50
+ shaping,
51
+ );
52
+
53
+ console.log(baseline);
54
+ console.log(compareValues(baseline, candidate));
55
+ ```
56
+
57
+ ### What you get
58
+
59
+ ```
60
+ {
61
+ dialect: 'json',
62
+ text: '{"rows":{"cart":{"id":"cart","total":4},"checkout":{"id":"checkout","total":10}}}',
63
+ digest: 'v1:e19dcb366e2321d0c7890dd69629ac39',
64
+ recipe: 'value/1',
65
+ keyed: [ '/rows' ]
66
+ }
67
+ [
68
+ {
69
+ change: 'value-changed',
70
+ pointer: '/rows/checkout/total',
71
+ fingerprint: 'v1:444e8245747e15412c5142a5a44fe321'
72
+ },
73
+ {
74
+ change: 'added',
75
+ pointer: '/rows/search',
76
+ fingerprint: 'v1:a9bd40864996d0b80d37c034f3f62109'
77
+ }
78
+ ]
79
+ ```
80
+
81
+ Three things in that output do work you will want later.
82
+
83
+ `text` is the canonical serialization, byte for byte what `digest` was taken
84
+ over — keys sorted, numbers written portably, `undefined` omitted. It is what a
85
+ baseline stores and what a person reads in a pull request.
86
+
87
+ `arrayKey` turned the `rows` array into an object keyed by `id` before
88
+ canonicalization, which is why inserting `search` at the end reports one
89
+ `added` delta. Compared by index instead, an insertion at the top of a
90
+ two-thousand-row list reports two thousand rows as changed.
91
+
92
+ `fingerprint` is the shape of the difference with the row identity and the
93
+ values removed. Change `cart.total` instead of `checkout.total` and you get the
94
+ same `v1:444e8245747e15412c5142a5a44fe321`, so one decision can settle the same
95
+ edit wherever it occurred. Deltas record no values at all: the two canonical
96
+ texts are the evidence, and a delta that quoted them would put payload into
97
+ every record that accumulates.
98
+
99
+ `compareValues` throws rather than guessing when the two sides disagree about
100
+ how they were made — different dialects, different shaping rules, or different
101
+ keyed arrays. `shapeValue` throws on anything that cannot be canonical text,
102
+ naming the JSON Pointer:
103
+
104
+ ```
105
+ the value at /at is a Date, which is not a value. Its state is not in its own
106
+ enumerable keys, so it would serialize to `{}` and two different ones would
107
+ compare as unchanged.
108
+ ```
109
+
110
+ Functions, `Date`, `bigint` and non-finite numbers all fail this way. The
111
+ alternative is `JSON.stringify`'s: drop the key silently, which the next run
112
+ reads as a key that was removed.
113
+
114
+ ### Shaping options
115
+
116
+ | option | effect |
117
+ |---|---|
118
+ | `arrayKey` | `{ '/rows': 'id' }` — the member that identifies a row, so an array becomes an object and order stops being a fact about it |
119
+ | `drop` | records a pointer's value as the token `[dropped]` — present, never compared |
120
+ | `replace` | puts a stable token of your choosing at a pointer |
121
+ | `dialect` | a label saved with the text, `'json'` by default; two values with different dialects refuse to compare |
122
+ | `generator` | `{ name, version }` of whatever emitted the value |
123
+
124
+ Every key is a JSON Pointer (RFC 6901). A `-` token matches any array index, so
125
+ `/items/-/updatedAt` names that member of every item. Neither `dialect` nor
126
+ `generator` counts toward the digest, so labelling a value later does not orphan
127
+ its baseline.
128
+
15
129
  ## Use this package when
16
130
 
17
131
  Install `@variance-authority/core` when the input is already produced: a
18
- capture, a semantic snapshot, a render document (serialized subject markup,
19
- ready to be rasterized elsewhere), a raster mask (`ChangeMask`: a per-pixel
20
- changed/unchanged bitmap, not an image), or a plain value. The package does
21
- not collect a DOM, read PNG bytes, launch a renderer, or select a test
22
- runner — for that, install `@variance-authority/dom` for a live DOM,
23
- `@variance-authority/react` for React provenance (the chain of component
24
- ownership attached to each node), and `@variance-authority/png` when the
25
- input is a PNG.
132
+ capture, a **semantic snapshot** (a normalized tree of nodes, digests and
133
+ boxes), a **render document** (serialized subject markup, ready to be
134
+ rasterized elsewhere), a **change mask** (`ChangeMask`: a per-pixel
135
+ changed/unchanged bitmap, not an image), or a plain value.
136
+
137
+ To produce those inputs, install a collector beside it:
138
+
139
+ ```bash
140
+ npm install --save-dev @variance-authority/dom # collect() a live DOM into a capture
141
+ npm install --save-dev @variance-authority/react # React component ownership per node
142
+ npm install --save-dev @variance-authority/png # comparePngs() two PNGs into a ChangeMask
143
+ ```
26
144
 
27
145
  ## Entrypoints
28
146
 
29
- Seven groups. Five of them are the order an answer travels through; `core/plan`
30
- and `core/relate` sit outside that line, because both are asked *before* anything
31
- is captured — one decides which baselines the run can reach at all, the other
32
- decides which subjects are worth reaching for. The default entrypoint is all
33
- seven and is what most callers want.
147
+ Every name lives in exactly one group, and the bare `@variance-authority/core`
148
+ specifier exports only the capture artifact.
34
149
 
35
- | entrypoint | holds |
150
+ | entrypoint | exports |
36
151
  |---|---|
37
152
  | `core/format` | what a subject *is*: capture, snapshot, document, identity, hashing |
38
153
  | `core/rules` | the versioned opinions: allowlist, applicability, cascade, canonicalization |
@@ -41,129 +156,183 @@ seven and is what most callers want.
41
156
  | `core/judge` | policy: verdicts, intent claims, ignores, the docket a reader is handed |
42
157
  | `core/plan` | the whole configuration of a run — profile, ruleset version, viewport, policy, interventions — as one value, plus the identity digest derived from it |
43
158
  | `core/relate` | what rests on what: a file graph in adjacency form, the components a change reaches, and a closure digest over each one |
159
+ | `core/segment` | columnar bytes: named columns, interned strings, and the validation a decode performs before it believes a file |
160
+ | `core/share` | leaving those bytes where another machine finds them, under a key that is a commit |
161
+
162
+ Five of those are the order an answer travels through: `format`, `rules`,
163
+ `compare`, `attribute`, `judge`. `plan` and `relate` are asked before anything
164
+ is captured — one decides which baselines a run can use, the other which
165
+ subjects are worth reading. `segment` and `share` are how a derived answer
166
+ is written down and handed to another machine.
167
+
168
+ ## The four words the output uses
169
+
170
+ A **profile** records what a collector was capable of observing. `jsdom` sees
171
+ structure and declared style; `chromium` adds layout and pixels. A profile that
172
+ cannot measure a box cannot decide pixels either.
173
+
174
+ An **identity** is the content hash that addresses a result, derived from the
175
+ whole plan — ruleset version, viewport, policy, interventions. Two runs are
176
+ comparable only when their identities match, so changing any part of the plan
177
+ changes which baselines the run can see.
178
+
179
+ A **band** is the kind of change a delta is, and which band a delta lands in
180
+ decides how loudly it is reported. Change frequency and change importance run
181
+ opposite each other: an accessible name almost never changes and is a defect
182
+ when it does, while anti-aliasing changes constantly and never matters. There
183
+ are five, loudest first:
184
+
185
+ | band | what changed | example delta kinds |
186
+ |---|---|---|
187
+ | `a11y` | a role, accessible name, description or ARIA state | `role-changed`, `name-changed`, `state-changed` |
188
+ | `geometry` | boxes appeared, vanished, moved or resized | `node-added`, `node-moved`, `rect-changed`, `attribute-changed` |
189
+ | `token` | style values changed while structure stayed the same | `style-changed`, `token-changed` |
190
+ | `content` | text changed and nothing else did | `text-changed` |
191
+ | `texture` | sub-semantic rendering variance | `raster-residue` |
192
+
193
+ `bandOf(kind)` performs that assignment and is the default rather than the
194
+ answer — policy may promote or demote per project, subject, region or
195
+ component. `loudestBand(bands)` collapses a set to the loudest one present, and
196
+ returns `null` for an empty set so that *nothing changed* and *something changed
197
+ at the quietest band* stay apart.
198
+
199
+ A **verdict** is the one word a result reports, and the whole pipeline exists to
200
+ produce one with a reason attached. There are six, by severity:
201
+
202
+ | verdict | means |
203
+ |---|---|
204
+ | `unchanged` | hash hit, or never reachable from the change |
205
+ | `inherited` | fully explained by an adjudication that already happened upstream |
206
+ | `authorized` | matches declared intent, or policy auto-approves this band |
207
+ | `needs-review` | band and policy require a human to sign off |
208
+ | `violation` | policy forbids it |
209
+ | `unexplained` | raster residue with no semantic cause — the highest severity here, because a change the pipeline cannot explain means attribution failed |
44
210
 
45
- Three terms recur across those groups. A **profile** records what a collector
46
- was capable of observing — jsdom sees structure and declared style, chromium
47
- adds layout and pixels. An **identity** is the content hash that addresses a
48
- result; two runs are comparable only when their identities match. A
49
- **verdict** is one of six words this package will commit to: `unchanged`,
50
- `inherited`, `authorized`, `needs-review`, `violation`, `unexplained`.
51
-
52
- The groups exist for callers who genuinely want one. Somebody implementing the
53
- capture format for a renderer this project has never met needs `core/format` and
54
- would be misled by everything else. Somebody deciding where a baseline is stored,
55
- or whether two runs may be compared at all, needs `core/plan` and nothing else:
56
- the digest it derives is the address, so changing any part of the plan changes
57
- which baselines the run can see.
58
-
59
- ## Comparison and policy
60
-
61
- `compare` says **what moved**. `judge` says **whether anyone should mind**.
62
-
63
- Between them sits `partingOf`, which says **which input moved**. Given two
64
- snapshots carrying holdings it walks the component boundaries for the shallowest
65
- one whose inputs agreed and whose output did not, and reports that as the origin
66
- — a moved prop, a context, an external store, or a hook cell by call position.
67
- It leads with a slice (`variation`, `flake`, `refactor`, `absorbed`, `settled`,
68
- `unread`) so a reader knows whether to open the rest, and `explainParting` turns
69
- the whole thing into lines a person reads.
211
+ Beside those sits `UNOBSERVED`, which is what a band reports when the acting
212
+ profile could not see it. It is not a verdict and the types will not let you
213
+ spell it as one.
70
214
 
71
- ## Smallest working path: compare a value
215
+ ## Compare a rendered subject
72
216
 
73
- Value comparison needs no host setup and returns paths and fingerprints, not a
74
- pass/fail verdict:
217
+ Excerpt. `capture` and `recapture` are what `collect(root, options)` from
218
+ `@variance-authority/dom` returns for the same subject at two revisions; `mask`
219
+ is the `mask` field of `comparePngs(before, after)` from
220
+ `@variance-authority/png`. Core never opens an image.
75
221
 
76
222
  ```ts
77
- import { compareValues, shapeValue } from '@variance-authority/core';
223
+ import { normalize } from '@variance-authority/core/rules';
224
+ import { diffSnapshots } from '@variance-authority/core/compare';
225
+ import { attributeRegions, isolateRegions } from '@variance-authority/core/attribute';
78
226
 
79
- const before = shapeValue({ rows: [{ id: 'a', total: 10 }] }, {
80
- arrayKey: { '/rows': 'id' },
81
- });
82
- const after = shapeValue({ rows: [{ id: 'a', total: 12 }] }, {
83
- arrayKey: { '/rows': 'id' },
84
- });
85
-
86
- const deltas = compareValues(before, after);
87
- console.log(deltas[0]?.pointer); // /rows/a/total
88
- ```
89
-
90
- For a rendered subject, the same package receives captures from a collector:
91
-
92
- ```ts
93
- import { attributeRegions, diffSnapshots, isolateRegions, normalize } from '@variance-authority/core';
94
-
95
- const before = normalize(capture); // supplied by a collector
227
+ const before = normalize(capture);
96
228
  const after = normalize(recapture);
97
- const diff = diffSnapshots(before, after); // deltas, roots, no verdict
229
+ const diff = diffSnapshots(before, after); // deltas and roots, no verdict
98
230
 
99
- // `mask` is a ChangeMask from the raster tier. Core never opens an image.
100
231
  const places = isolateRegions(mask, { cell: 8 }); // pixels → regions
101
232
  const named = attributeRegions(places.regions, after, { scale: 2 });
102
233
  ```
103
234
 
104
- The value example prints a JSON Pointer for the changed field. The rendered path
105
- returns semantic roots and, when a raster mask is supplied, regions attributed to
106
- the candidate snapshot. A caller still chooses policy and a verdict in
107
- `core/judge` or in its own runner.
235
+ `diffSnapshots` returns `identical`, the flat `deltas`, the `roots` a docket
236
+ renders and approves, and the `components` implicated, separated into causes and
237
+ collateral — so "`Button` changed, and ten components render it" replaces
238
+ "eleven components changed". `attributeRegions` turns regions into components
239
+ and files. Choosing a verdict is a separate step, in `core/judge` or in your own
240
+ runner.
241
+
242
+ `scale` is device pixels per CSS pixel and it is required. Attribute a 2x
243
+ screenshot at 1x and every region lands in the top-left quadrant with the wrong
244
+ component named — a full, plausible, entirely wrong report.
108
245
 
109
- `scale` is device pixels per CSS pixel, it is **required**, and it has no default
110
- on purpose: a 2x screenshot attributed at 1x lands every region in the top-left
111
- quadrant and names the wrong component for each — a full, plausible, entirely
112
- wrong report. That last step is where regions become components and files.
246
+ Rendered comparisons throw when the two subjects differ, or when their
247
+ observation profiles differ.
113
248
 
114
- The options that change scope are explicit at the call site:
249
+ ### Options that change scope
115
250
 
116
251
  | call | useful controls |
117
252
  |---|---|
118
253
  | `normalize` | `collapseWrappers` removes layout-only wrapper boundaries, `digestText` includes text content, and `sourceRoot` relativizes source locations |
119
254
  | `isolateRegions` / `attributeRegions` | `cell` and `limit` bound mask work; `origin` and `containment` describe the coordinate origin and how much a node must contain a region |
120
- | `fingerprintOfMask` | `grid` controls the shape sample and `coverage` controls the minimum occupied share |
255
+ | `fingerprintOfMask` | `grid` controls the shape sample and `coverage` the minimum occupied share |
121
256
  | `compareLocales` / ignore validation | `slack` permits a declared locale distance; `sites` supplies resolved ignore locations and `now` evaluates expiry |
122
- | `buildDocket` / dependency reach | `sampleSize` limits review examples and `through` selects graph edge kinds |
257
+ | `buildDocket` / dependency reach | `sampleSize` limits review examples; `through` selects graph edge kinds, `avoid` names nodes a walk never enters, and `shadows` lists per file the modules its run never reaches, so `movedBy` 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 |
258
+ | `beforeReach` | `sensed` names the directories the scan already answers for, and the descent from a declared entry point stops at the first file under one of them rather than dragging the repository's own source in behind the harness |
259
+ | `sharedClosures` | `floor` drops a shared subtree below a node count |
260
+ | `lexiconOf` | `examples`, `declaredIn` and `regions` supply what the instances cannot: which components a subject is the example of, the files declaring each component, and the regions its journey entered |
123
261
  | report summaries | `source` maps component names to files when `summarizeAdjudication` or `summarizeFindings` needs an actionable path |
124
262
  | screenshot stabilization | `animations` and `caret` are explicit intervention settings; omitted means the caller did not assert either intervention |
125
263
 
126
- ### A subject that was never rendered
264
+ ## Which input changed
265
+
266
+ `compare` says what changed. `judge` says whether anyone should mind. Between
267
+ them, `partingOf` says which input changed.
268
+
269
+ Given two snapshots with holdings — the inputs a collector recorded at each
270
+ component boundary — it walks the boundaries for the shallowest one whose inputs
271
+ agreed and whose output did not, and reports that as the origin: a changed prop,
272
+ a context, an external store, or a hook cell by call position. `explainParting`
273
+ turns the result into lines a person reads.
274
+
275
+ It leads with a slice, so you know whether to open the rest:
276
+
277
+ | slice | means |
278
+ |---|---|
279
+ | `settled` | nothing changed: not the tree, not an input, not the output |
280
+ | `variation` | an input changed and the output followed — the ordinary case, and the only one where the detail below is worth reading |
281
+ | `absorbed` | an input changed and the output did not; the component ignored it |
282
+ | `refactor` | the component tree changed and the output did not |
283
+ | `reshaped` | the tree is a different tree, no input changed, and the output followed |
284
+ | `flake` | every input agreed, the tree matched, and the output changed anyway |
285
+ | `placed` | the same, but between two readings taken in different places |
286
+ | `unread` | the output changed and what would explain it was not read |
287
+
288
+ `flake` is an accusation against a page, so it is never returned on silence. A
289
+ run that recorded no component provenance gets `unread` instead.
290
+
291
+ ## Hand an answer to another machine
127
292
 
128
- `shapeValue(value, options)` is the same treatment for a value — an API
129
- response, a generated schema, a route table. It canonicalizes (keys sorted,
130
- numbers written portably, `undefined` omitted), addresses the text by content,
131
- and returns a `CapturedValue`; `compareValues` turns two of them into deltas
132
- carrying a JSON Pointer and a fingerprint, and — like everything else here — no
133
- verdict.
293
+ What a suite is made of is a fact about a commit rather than about a run.
294
+ `core/segment` writes such a thing as columns — equal facts encode to equal
295
+ bytes, which is what lets a transport skip an upload — and `core/share` moves
296
+ the bytes.
297
+
298
+ Excerpt: `token` is your store's credential and `bytes` is what `core/segment`
299
+ encoded.
134
300
 
135
301
  ```ts
136
- import { shapeValue, compareValues } from '@variance-authority/core';
302
+ import { httpShare, shareKey } from '@variance-authority/core/share';
137
303
 
138
- const baseline = shapeValue(before, { arrayKey: { '/rows': 'id' } });
139
- const deltas = compareValues(baseline, shapeValue(after, { arrayKey: { '/rows': 'id' } }));
140
- ```
304
+ const share = httpShare({
305
+ endpoint: 'https://objects.example.com/variance',
306
+ headers: { authorization: `Bearer ${token}` },
307
+ method: 'PUT',
308
+ });
141
309
 
142
- `drop` records a pointer's value as present without comparing it, `replace` puts
143
- a token you choose in its place, and `arrayKey` says which member identifies a
144
- row — the difference between *one row was added* and *two thousand rows moved*.
145
- Two more options describe the value rather than shape it: `dialect` is how a
146
- reader will interpret the text later (`'json'` by default, `'openapi'` and
147
- `'graphql'` being the ones with detectors), and `generator` names what emitted
148
- it. Neither reaches the digest, so declaring a dialect does not orphan a
149
- baseline.
310
+ await share.put(shareKey({ project: 'web', artifact: 'suite-index-v1', commit }), bytes);
311
+ ```
150
312
 
151
- Non-data throws, naming the pointer. A function, a `Date`, a `bigint` or a
152
- non-finite number cannot be canonical text, and dropping one silently puts a key
153
- in the record that the next run reads as removed.
313
+ `endpoint` is a base URL a key is appended to. `headers` is sent on every
314
+ request, which is where a bucket's `Authorization` or a deployment's token goes.
315
+ `method` is the verb a write uses — `PUT` for a bucket, `POST` for a deployment
316
+ that routes on it. A presigned base needs only the endpoint.
154
317
 
155
- Rendered comparisons also throw when the subjects or observation profiles differ.
318
+ **A share never throws.** A miss, an outage, a permission error and a body
319
+ nobody can parse are one outcome: `get` answers `null` and `put` resolves.
320
+ Everything a share stores can be derived again, so a broken share costs you the
321
+ derivation — and is indistinguishable from a cold one except by the wall clock.
322
+ [Sharing an evaluation](https://variance-authority.dev/docs/sharing) is the
323
+ operator's side of it.
156
324
 
157
325
  ## What it refuses
158
326
 
159
- **Absent is not empty.** Not measured, measured as zero, and unobservable are
160
- three states, and collapsing any two produces a pass nobody earned. A **band**
161
- (the frequency category a change falls into — `a11y`, `geometry`, `token`,
162
- `content`, `texture`, rarest to noisiest) that a profile cannot see reports
163
- `UNOBSERVED`, which the type system will not let you spell the same way as a
164
- pass.
327
+ **Absent is not empty.** Not measured, measured as zero, and unobservable stay
328
+ three distinct states through every type in this package. A band the profile
329
+ could not see reports `UNOBSERVED`, and a geometry digest is absent rather than
330
+ empty under a profile without layout.
331
+
332
+ **Two results whose identities differ never compare.** A ruleset bump, a
333
+ different viewport, a different intervention — each changes the identity, and
334
+ the comparison is refused rather than reported as a difference in the product.
165
335
 
166
- **Two results whose identities differ are `incomparable`, never `different`.**
167
- A difference in conditions reported as a difference in the product is a
168
- confident wrong answer, and the confidence is what makes it expensive.
336
+ ---
169
337
 
338
+ **[@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
@@ -1,4 +1,4 @@
1
- import type { Provenance, SourceLocation, StackFrame } from '../format/provenance.js';
1
+ import { type Provenance, type SourceLocation, type StackFrame } from '../format/provenance.js';
2
2
  import type { NodePath, SemanticSnapshot } from '../format/snapshot.js';
3
3
  /**
4
4
  * Spending the frames a page read, to get the file a reviewer opens.
@@ -33,6 +33,24 @@ import type { NodePath, SemanticSnapshot } from '../format/snapshot.js';
33
33
  */
34
34
  /** Fetch a module's text, or answer that it cannot be had. Never throws. */
35
35
  export type FetchModule = (url: string) => Promise<string | null>;
36
+ export interface CallSiteOptions {
37
+ /**
38
+ * The repository root, for a location that arrives holding an absolute path.
39
+ *
40
+ * Two kinds of frame reach {@link CallSiteResolver.locate} and only one of
41
+ * them is relative already. A frame that needed a map is answered in the map's
42
+ * own terms, which are the build's and so the repository's. A frame that
43
+ * needed none — a Node stack, or the synthetic frame React manufactures for a
44
+ * server component — is answered in the terms of the disk it was captured on,
45
+ * which means somebody's home directory in the middle of a path that is
46
+ * otherwise a fact about the project.
47
+ *
48
+ * Given, the two shapes come out as one. Absent, a location is returned as it
49
+ * arrived, because guessing at a root is how a path stops resolving for the
50
+ * reader who opens it.
51
+ */
52
+ readonly sourceRoot?: string;
53
+ }
36
54
  export interface CallSiteResolver {
37
55
  /** The location that wrote an element, from the frames its fiber carried. */
38
56
  locate(frames: readonly StackFrame[]): Promise<SourceLocation | null>;
@@ -47,7 +65,7 @@ export interface CallSiteStats {
47
65
  /** Call sites that resolved to a file. */
48
66
  readonly located: number;
49
67
  }
50
- export declare function createCallSiteResolver(fetchModule: FetchModule): CallSiteResolver;
68
+ export declare function createCallSiteResolver(fetchModule: FetchModule, options?: CallSiteOptions): CallSiteResolver;
51
69
  /**
52
70
  * Provenance with its frames spent: `source` filled in, `stack` gone.
53
71
  *
@@ -1,6 +1,7 @@
1
+ import { relativizeSource, } from '../format/provenance.js';
1
2
  import { inlineSourceMapOf, originalPositionFor, parseSourceMap, sourceMappingUrlOf, } from './source-map.js';
2
- import { isVendorPath, writerLocationOf } from './stack.js';
3
- export function createCallSiteResolver(fetchModule) {
3
+ import { isVendorPath, servedPath, writerLocationOf } from './stack.js';
4
+ export function createCallSiteResolver(fetchModule, options = {}) {
4
5
  /** Module URL → its map, `null` once we know it has none. Also the in-flight guard. */
5
6
  const maps = new Map();
6
7
  /** `url:line:column` → the answer, so a page's 4211 nodes cost 14 resolutions. */
@@ -34,10 +35,11 @@ export function createCallSiteResolver(fetchModule) {
34
35
  }
35
36
  async function locateFrame(frame) {
36
37
  const parsed = await mapsFor(frame.url);
37
- // No map is not failure. A dev server serving a plain `.js` as written, and a
38
- // Node test runner — which applies source maps to `Error.stack` itself, so
39
- // the frame arrives already original — both land here with the answer in
40
- // hand. `writerLocationOf` keeps the frame's own coordinates in that case.
38
+ // No map is not failure, and it is not an answer either. A Node test runner
39
+ // applies source maps to `Error.stack` itself, so its frame arrives already
40
+ // original and there is nothing left to add. A served module carries no such
41
+ // guarantee: a position in the text a server sent is a position in the
42
+ // repository only if a map says so, so `writerLocationOf` refuses it.
41
43
  if (parsed === null || parsed.length === 0)
42
44
  return null;
43
45
  const original = originalPositionFor(parsed, frame.line, frame.column);
@@ -61,8 +63,8 @@ export function createCallSiteResolver(fetchModule) {
61
63
  sites.set(key, answer);
62
64
  }
63
65
  const located = await answer;
64
- // The frame's own coordinates, for a module served as written. Delegated so
65
- // there is one statement of the choosing rule rather than two.
66
+ // The frame's own coordinates, for a frame that names a file on disk.
67
+ // Delegated so there is one statement of the choosing rule rather than two.
66
68
  const chosen = located === null
67
69
  ? writerLocationOf([frame], () => null)
68
70
  : isVendorPath(located.file)
@@ -70,7 +72,9 @@ export function createCallSiteResolver(fetchModule) {
70
72
  : located;
71
73
  if (chosen !== null) {
72
74
  stats.located += 1;
73
- return chosen;
75
+ return options.sourceRoot === undefined
76
+ ? chosen
77
+ : relativizeSource(chosen, options.sourceRoot);
74
78
  }
75
79
  }
76
80
  return null;
@@ -152,19 +156,12 @@ function index(root) {
152
156
  * A map's `sources` entry as a path, resolved against the module it describes.
153
157
  *
154
158
  * Maps state sources relatively — Vite writes `probe.jsx` for `/src/probe.jsx` —
155
- * so the entry alone is ambiguous between two directories. Resolved and then
156
- * stripped to a path, because the origin is a fact about the machine that ran
157
- * the capture: a baseline holding `http://localhost:5199/src/probe.jsx` would
158
- * disagree with the next run on a different port. What is left is what the
159
- * source index and `relativizeSource` already speak.
159
+ * so the entry alone is ambiguous between two directories. Resolved against the
160
+ * module first, then stripped of the origin it resolved against, which is a fact
161
+ * about the machine that served it rather than about the code.
160
162
  */
161
163
  function sourcePath(source, moduleUrl) {
162
- const resolved = absolute(source, moduleUrl) ?? source;
163
- const parsed = parseUrl(resolved);
164
- // Not a URL: a bundler that wrote an absolute filesystem path, or a
165
- // `webpack://` specifier. Left as it is — `relativizeSource` handles the
166
- // first, and inventing a shape for the second would be a guess.
167
- return parsed === null ? resolved : parsed.pathname.replace(/^\/+/, '');
164
+ return servedPath(absolute(source, moduleUrl) ?? source);
168
165
  }
169
166
  function absolute(url, base) {
170
167
  return parseUrl(url, base)?.href ?? null;